Dry-run apply computes per-file diffs, but the command output does not show the unified diff that the README and security policy describe as the primary review step before writes.
Evidence
README.md:72 to README.md:80 describes dry-run apply as printing a unified diff.
SECURITY.md:39 to SECURITY.md:42 repeats that dry-run prints a unified diff and write mode is explicit.
internal/applier/applier.go:120 to internal/applier/applier.go:127 stores the computed diff on FileResult.
internal/report/report.go:412 onward renders the text report summary, but does not emit the stored diffs.
cmd/license-tool/commands.go:232 to cmd/license-tool/commands.go:234 makes license print only path and action.
Reproduction
Create a clean git repo with a headerless supported source file.
Run license-tool apply <repo> without write mode.
Observe that stdout contains an audit-style summary and no unified diff hunk.
Why It Matters
Dry-run output is the operator's chance to review exactly what will be rewritten. Without diffs, users must either trust a summary or run write mode blind.
Suggested Fix
Render FileResult.Diff for dry-run apply and license operations. Add command tests that assert unified diff headers and header content appear in dry-run output when a change is pending.
## Finding
Dry-run apply computes per-file diffs, but the command output does not show the unified diff that the README and security policy describe as the primary review step before writes.
## Evidence
- `README.md:72` to `README.md:80` describes dry-run apply as printing a unified diff.
- `SECURITY.md:39` to `SECURITY.md:42` repeats that dry-run prints a unified diff and write mode is explicit.
- `internal/applier/applier.go:120` to `internal/applier/applier.go:127` stores the computed diff on `FileResult`.
- `internal/report/report.go:412` onward renders the text report summary, but does not emit the stored diffs.
- `cmd/license-tool/commands.go:232` to `cmd/license-tool/commands.go:234` makes `license` print only path and action.
## Reproduction
1. Create a clean git repo with a headerless supported source file.
2. Run `license-tool apply <repo>` without write mode.
3. Observe that stdout contains an audit-style summary and no unified diff hunk.
## Why It Matters
Dry-run output is the operator's chance to review exactly what will be rewritten. Without diffs, users must either trust a summary or run write mode blind.
## Suggested Fix
Render `FileResult.Diff` for dry-run apply and license operations. Add command tests that assert unified diff headers and header content appear in dry-run output when a change is pending.
Status: confirmed — dry-run apply/license compute and store a unified diff on FileResult.Diff, but no output format ever renders it; stdout is the audit summary only.
Severity: medium (matches the bug label; not high because it is a documentation/observability gap, not a write-safety or correctness defect — --write still behaves correctly, and git diff after a write is the real undo/review path). The promise in SECURITY.md under "Safe By Default" does inflate the impact slightly, since the diff is the advertised pre-write review surface.
Verified root cause (two render sites, both omit the stored diff):
internal/report/report.go:renderText:457-460 — the source files: block prints only fileLine(fr) (path + bracketed detected-license note). fileLine (report.go:480-491) never references fr.Diff. A repo-wide grep finds zero.Diff references anywhere in internal/report/ or cmd/.
cmd/license-tool/commands.go:newLicenseCmd:232-234 — the license command prints only "%s: %s\n" (path + action), discarding fr.Diff.
The data is present: applier.go:ApplyFile:174 computes diff = unifiedDiff(...), Apply:127 sets fr.Diff = diff, and report.Build:106 preserves it via append([]model.FileResult(nil), files...). So the diff reaches the model.Report and is simply never emitted by any renderer.
The JSON renderer drops it too: jsonFile (report.go struct) has no Diff/diff field, so even --format json cannot surface it.
Reproduction (built from HEAD, go build ./cmd/license-tool): a clean git repo with a headerless main.go + .license-tool.yaml (MIT). license-tool apply (no --write) prints the audit report and main.go [no managed header] — no ---/+++/@@ hunk and no SPDX-License-Identifier header text. Confirmed.
Suggested fix:
Render fr.Diff for the dry-run path in renderText (gate on !opts.Write && fr.Diff != ""), and in newLicenseCmd's result loop. The report must know it is a dry run — model.Report does not currently carry a Write/DryRun flag, so either thread one through report.Build or have the command decide whether to print diffs (the command already knows f.write). Threading a flag onto the report is cleaner and keeps license and apply consistent.
Add a diff field to jsonFile (omitempty) so JSON consumers get parity.
This is a render-layer fix; it does not touch the preserve-first boundaries. The v0.2.1 universal-shebang precedent ("touch both render.preserveBoundary and detect.PreserveBoundary + the filetype table") does not apply here — that pattern is for boundary/splice behavior, and this defect is purely about emitting already-correct data. Do not let the issue's framing pull a fixer into the boundary code.
Keep the 100% gate green: add command/report tests asserting unified-diff headers (---/+++/@@) and at least one line of header content appear in dry-run output, and assert they are absent under --write (so the new branch's both sides are covered).
Corrections to the issue:
Line refs are accurate: README.md:72 and README.md:75 (not 80) carry the "prints a unified diff" promise; SECURITY.md:39 carries it. applier.go:120-127 (the fr.Diff = diff assignment is at line 127) and commands.go:232-234 are correct.
Minor: commands.go:233 prints "%s: %s" (path-colon-action), not "path and action" as separate columns — cosmetic, not load-bearing.
The impact is slightly broader than the body implies in one respect: --format json also omits the diff (no Diff field on jsonFile), so the gap is not text-renderer-only.
Open questions:
Scope: should the diff print in all formats (text + markdown + json) or text-only? JSON parity argues for adding the field everywhere.
Should diffs print on every dry-run by default (potentially large output across a whole repo), or behind a --diff/--no-diff flag with a sensible default? This is a UX/behavior decision the README's blanket "prints a unified diff" doesn't settle for large trees.
Should license and apply share the exact same diff-rendering path, or does license (whole-file LICENSE/LICENSES rewrites) warrant different treatment?
Overlaps: none found in the cited code; this is an output-rendering gap independent of the v0.2.1 shebang fix (#5).
Automated maintainer review.
**Status**: confirmed — dry-run `apply`/`license` compute and store a unified diff on `FileResult.Diff`, but no output format ever renders it; stdout is the audit summary only.
**Severity**: medium (matches the `bug` label; not high because it is a documentation/observability gap, not a write-safety or correctness defect — `--write` still behaves correctly, and `git diff` after a write is the real undo/review path). The promise in `SECURITY.md` under "Safe By Default" does inflate the impact slightly, since the diff is the advertised pre-write review surface.
**Verified root cause** (two render sites, both omit the stored diff):
- `internal/report/report.go:renderText:457-460` — the `source files:` block prints only `fileLine(fr)` (path + bracketed detected-license note). `fileLine` (`report.go:480-491`) never references `fr.Diff`. A repo-wide grep finds **zero** `.Diff` references anywhere in `internal/report/` or `cmd/`.
- `cmd/license-tool/commands.go:newLicenseCmd:232-234` — the `license` command prints only `"%s: %s\n"` (path + action), discarding `fr.Diff`.
- The data is present: `applier.go:ApplyFile:174` computes `diff = unifiedDiff(...)`, `Apply:127` sets `fr.Diff = diff`, and `report.Build:106` preserves it via `append([]model.FileResult(nil), files...)`. So the diff reaches the `model.Report` and is simply never emitted by any renderer.
- The JSON renderer drops it too: `jsonFile` (`report.go` struct) has no `Diff`/`diff` field, so even `--format json` cannot surface it.
**Reproduction** (built from HEAD, `go build ./cmd/license-tool`): a clean git repo with a headerless `main.go` + `.license-tool.yaml` (MIT). `license-tool apply` (no `--write`) prints the audit report and `main.go [no managed header]` — no `---`/`+++`/`@@` hunk and no `SPDX-License-Identifier` header text. Confirmed.
**Suggested fix**:
- Render `fr.Diff` for the dry-run path in `renderText` (gate on `!opts.Write && fr.Diff != ""`), and in `newLicenseCmd`'s result loop. The report must know it is a dry run — `model.Report` does not currently carry a `Write`/`DryRun` flag, so either thread one through `report.Build` or have the command decide whether to print diffs (the command already knows `f.write`). Threading a flag onto the report is cleaner and keeps `license` and `apply` consistent.
- Add a `diff` field to `jsonFile` (omitempty) so JSON consumers get parity.
- This is a render-layer fix; it does **not** touch the preserve-first boundaries. The v0.2.1 universal-shebang precedent ("touch both `render.preserveBoundary` and `detect.PreserveBoundary` + the filetype table") does **not** apply here — that pattern is for boundary/splice behavior, and this defect is purely about emitting already-correct data. Do not let the issue's framing pull a fixer into the boundary code.
- Keep the 100% gate green: add command/report tests asserting unified-diff headers (`---`/`+++`/`@@`) and at least one line of header content appear in dry-run output, and assert they are **absent** under `--write` (so the new branch's both sides are covered).
**Corrections to the issue**:
- Line refs are accurate: `README.md:72` and `README.md:75` (not 80) carry the "prints a unified diff" promise; `SECURITY.md:39` carries it. `applier.go:120-127` (the `fr.Diff = diff` assignment is at line 127) and `commands.go:232-234` are correct.
- Minor: `commands.go:233` prints `"%s: %s"` (path-colon-action), not "path and action" as separate columns — cosmetic, not load-bearing.
- The impact is slightly **broader** than the body implies in one respect: `--format json` also omits the diff (no `Diff` field on `jsonFile`), so the gap is not text-renderer-only.
**Open questions**:
- Scope: should the diff print in **all** formats (text + markdown + json) or text-only? JSON parity argues for adding the field everywhere.
- Should diffs print on every dry-run by default (potentially large output across a whole repo), or behind a `--diff`/`--no-diff` flag with a sensible default? This is a UX/behavior decision the README's blanket "prints a unified diff" doesn't settle for large trees.
- Should `license` and `apply` share the exact same diff-rendering path, or does `license` (whole-file LICENSE/LICENSES rewrites) warrant different treatment?
**Overlaps**: none found in the cited code; this is an output-rendering gap independent of the v0.2.1 shebang fix (#5).
_Automated maintainer review._
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Finding
Dry-run apply computes per-file diffs, but the command output does not show the unified diff that the README and security policy describe as the primary review step before writes.
Evidence
README.md:72toREADME.md:80describes dry-run apply as printing a unified diff.SECURITY.md:39toSECURITY.md:42repeats that dry-run prints a unified diff and write mode is explicit.internal/applier/applier.go:120tointernal/applier/applier.go:127stores the computed diff onFileResult.internal/report/report.go:412onward renders the text report summary, but does not emit the stored diffs.cmd/license-tool/commands.go:232tocmd/license-tool/commands.go:234makeslicenseprint only path and action.Reproduction
license-tool apply <repo>without write mode.Why It Matters
Dry-run output is the operator's chance to review exactly what will be rewritten. Without diffs, users must either trust a summary or run write mode blind.
Suggested Fix
Render
FileResult.Difffor dry-run apply and license operations. Add command tests that assert unified diff headers and header content appear in dry-run output when a change is pending.Status: confirmed — dry-run
apply/licensecompute and store a unified diff onFileResult.Diff, but no output format ever renders it; stdout is the audit summary only.Severity: medium (matches the
buglabel; not high because it is a documentation/observability gap, not a write-safety or correctness defect —--writestill behaves correctly, andgit diffafter a write is the real undo/review path). The promise inSECURITY.mdunder "Safe By Default" does inflate the impact slightly, since the diff is the advertised pre-write review surface.Verified root cause (two render sites, both omit the stored diff):
internal/report/report.go:renderText:457-460— thesource files:block prints onlyfileLine(fr)(path + bracketed detected-license note).fileLine(report.go:480-491) never referencesfr.Diff. A repo-wide grep finds zero.Diffreferences anywhere ininternal/report/orcmd/.cmd/license-tool/commands.go:newLicenseCmd:232-234— thelicensecommand prints only"%s: %s\n"(path + action), discardingfr.Diff.applier.go:ApplyFile:174computesdiff = unifiedDiff(...),Apply:127setsfr.Diff = diff, andreport.Build:106preserves it viaappend([]model.FileResult(nil), files...). So the diff reaches themodel.Reportand is simply never emitted by any renderer.jsonFile(report.gostruct) has noDiff/difffield, so even--format jsoncannot surface it.Reproduction (built from HEAD,
go build ./cmd/license-tool): a clean git repo with a headerlessmain.go+.license-tool.yaml(MIT).license-tool apply(no--write) prints the audit report andmain.go [no managed header]— no---/+++/@@hunk and noSPDX-License-Identifierheader text. Confirmed.Suggested fix:
fr.Difffor the dry-run path inrenderText(gate on!opts.Write && fr.Diff != ""), and innewLicenseCmd's result loop. The report must know it is a dry run —model.Reportdoes not currently carry aWrite/DryRunflag, so either thread one throughreport.Buildor have the command decide whether to print diffs (the command already knowsf.write). Threading a flag onto the report is cleaner and keepslicenseandapplyconsistent.difffield tojsonFile(omitempty) so JSON consumers get parity.render.preserveBoundaryanddetect.PreserveBoundary+ the filetype table") does not apply here — that pattern is for boundary/splice behavior, and this defect is purely about emitting already-correct data. Do not let the issue's framing pull a fixer into the boundary code.---/+++/@@) and at least one line of header content appear in dry-run output, and assert they are absent under--write(so the new branch's both sides are covered).Corrections to the issue:
README.md:72andREADME.md:75(not 80) carry the "prints a unified diff" promise;SECURITY.md:39carries it.applier.go:120-127(thefr.Diff = diffassignment is at line 127) andcommands.go:232-234are correct.commands.go:233prints"%s: %s"(path-colon-action), not "path and action" as separate columns — cosmetic, not load-bearing.--format jsonalso omits the diff (noDifffield onjsonFile), so the gap is not text-renderer-only.Open questions:
--diff/--no-diffflag with a sensible default? This is a UX/behavior decision the README's blanket "prints a unified diff" doesn't settle for large trees.licenseandapplyshare the exact same diff-rendering path, or doeslicense(whole-file LICENSE/LICENSES rewrites) warrant different treatment?Overlaps: none found in the cited code; this is an output-rendering gap independent of the v0.2.1 shebang fix (#5).
Automated maintainer review.
Resolved on
developin commit742c124e6ec2afa9312075db3112bc2def3c91ff.Validation:
Closing as fixed.