The README.md documents the commands at a surface level but does not describe the two user-facing capabilities added in v0.2.0 (see CHANGELOG.md lines 15-19):
The interactive init wizard — a charmbracelet/huh form with a filterable SPDX license picker (common licenses first), holder/year/style prompts with in-form validation, and an overwrite guard. The README only says init is "interactive on a TTY" and "prompts for missing required fields", which understates and partly mis-describes the actual behavior.
The audit findings-summary block — the at-a-glance findings: block printed at the top of the text report (source files headered/missing, license types, unknown/unrecognized, copyleft, dependencies resolved/unresolved, policy PASS/FAIL). The README's ### audit section describes the breakdown categories prose-style but never shows or names the findings: block that is now the first thing a user sees.
There is also a factual inaccuracy: the README (lines 96 and 125) states the non-TTY / CI path "hard-errors rather than hanging" when required fields are missing. The current implementation does not hard-error on missing required fields specifically in a special CI branch — the non-interactive path simply skips the wizard and relies entirely on flags, then validates through the same answersToConfig gate (which errors on an unrecognized/empty license or empty holder, on a TTY or not). The "hard-error in CI" framing should be corrected to "non-TTY uses the flag values and validates them; missing/invalid license or holder is a usage error".
The command list, install/Homebrew section, shared flags, and exit codes are otherwise current and should be preserved.
Type / Severity
Type: documentation
Severity: low (no code defect; the shipped behavior is correct and tested). The README simply lags the v0.2.0 surface, so users do not learn that init is a guided wizard or that audit leads with a findings summary, and one sentence about CI behavior is misleading.
Reproduction
The gap is observable by comparing the README to actual command output.
1. audit findings block is undocumented. Create a sample repo and run audit:
(The dependencies: line appears only when dependency resolution ran.) Nothing in README.md documents the findings: label or its six/seven lines.
2. init wizard is under-documented. On a TTY:
cd /tmp/lt-demo
license-tool init .
This launches an interactive huh form: a filterable "Target SPDX license" select (common ids labeled <id> (common) first, then the full SPDX list), a "Copyright holder" input that refuses to advance while blank, a "Year policy" input validated against current|YYYY|YYYY-YYYY|git, a "Header style" select (reuse|notice|reuse+notice), and a "Manage top-level LICENSE file?" confirm. The README describes none of this — it only says "prompts for missing required fields."
3. Overwrite guard is undocumented. Re-running init when .license-tool.yaml already exists fails unless --force is passed:
The README's ### init section does not mention that init reuses --force as the overwrite guard.
Root cause
This is purely a documentation lag; the README was last meaningfully updated for the v0.1.0 surface. The behaviors are implemented and tested but undocumented or mis-documented:
init wizard is implemented in cmd/license-tool/wizard.go:
runInitWizard (around line 57) builds the huh.NewForm with the license select, holder/year/style prompts, and the confirm.
licenseSelectOptions (around line 36) builds the common-first, filterable picker using spdx.CommonIDs() then the remaining spdx.IDs(), labeling common ids "<id> (common)". The select enables .Filtering(true).
The holder validator (errEmptyHolder, line 15) and the year validator (reusing config.ParseYearSpec) enforce inputs in-form.
Overwrite guard / non-TTY flow is in cmd/license-tool/commands.go:
newInitCmd (around line 295) seeds initAnswers from the --license/--holder/--year/--style flags, calls interactiveCollect(&a, isTTY()) (the wizard only runs on a TTY; non-TTY is a no-op), then funnels everything through answersToConfig.
answersToConfig (around line 266) is the single validation gate (rejects an unrecognized license via spdx.Validate, and an empty holder), used identically on the TTY and flag-only paths — this is why the README's "hard-errors in CI" claim is imprecise.
The overwrite guard lives in config.WriteFile (internal/config/config.go around line 231-234): if !force && fileExists(target) { return ... "already exists (use --force)" }. init reuses the --force flag for this (commands.go comment around line 327).
isTTY (commands.go around line 434) uses golang.org/x/term's IsTerminal, which is the v0.2.0 fix that makes /dev/null correctly non-interactive.
audit findings block is implemented in internal/report/findings.go:
buildFindings (around line 60) folds a model.Report into the Findings struct (source totals, per-id LicenseCounts, UnknownCount, Copyleft, dependency resolution, policy verdict).
renderFindings (around line 163) emits the exact findings: block layout — source files:, license types:, unknown/unrecognized:, copyleft:, optional dependencies:, policy:.
Proposed fix
Update README.md only (no code changes). Keep the existing Install / Homebrew, Pre-built Binaries, Shared flags, Exit codes, Configuration, Development, and Release Automation sections as-is.
Expand ### init to describe the wizard concretely:
On a TTY, init launches an interactive form (built on charmbracelet/huh) with: a filterable SPDX license picker (common licenses listed first, labeled <id> (common), then the full SPDX list); a copyright-holder prompt that requires a non-empty value; a year-policy prompt validated against current | YYYY | YYYY-YYYY | git; a header-style select (reuse | notice | reuse+notice); and a "manage top-level LICENSE file?" confirm.
Document the overwrite guard: init refuses to clobber an existing .license-tool.yaml and reuses --force to overwrite it.
Document the non-TTY path accurately: in CI or any non-terminal stdin, the wizard is skipped and the values come from the --license/--holder/--year/--style flags (plus shared --exclude); the answers are validated the same way regardless of path, so an unrecognized license or empty holder is a usage error rather than a hang. Show a flag-only example, e.g. license-tool init --license AGPL-3.0-or-later --holder "Kingsrook, LLC".
Cross-link to the existing Configuration section, since init scaffolds the .license-tool.yaml documented there.
Correct the "hard-error in CI" wording in the ### init section (currently line 96) and in the Configuration paragraph (currently line 125, "Missing required fields ... hard-error in CI") to match the validate-the-flags behavior described above.
Document the audit findings-summary block in ### audit: show a representative findings: block and name its lines (source files headered/missing, license types, unknown/unrecognized, copyleft, dependencies resolved/unresolved when deps ran, policy PASS/FAIL). Note the dependencies: line is omitted when dependency resolution did not run, and that policy: renders FAIL (<n>: <tokens>) on failure.
Verify the command/flag list is current (it is, but confirm during the edit): the command table (lines 36-42) lists audit/check/apply/license/init/version; the shared flags and exit codes match the implementation.
This mirrors the precedent set by PR #5 / v0.2.1 (the universal-shebang fix), which paired a behavior with a clear, single source-of-truth explanation: just as the shebang is now preserved universally in both render.preserveBoundary (internal/render/render.go ~line 320) and detect.PreserveBoundary (internal/detect/detect.go ~line 171) rather than per-type, the README should describe the init and audit behaviors once, accurately, rather than leaving them implied or per-command-fragmented. No relationship to the boundary code requires a doc change here; those functions and the internal/filetype/filetype.gobuiltin table are linked under References only so a reader auditing "preserve-first / must-be-first" behavior can find the canonical implementations.
Coverage gate: This is a docs-only change, so the 100% total coverage gate in .testcoverage.yml is unaffected. Note for the implementer: cmd/license-tool/wizard.go is intentionally excluded from coverage (.testcoverage.yml line 47, documented exclusion #3) because it is a thin huh-backed TUI shell with no business logic — all validation lives in answersToConfig/config/spdx, which ARE covered. Do not add tests that try to drive the real huh form (it needs a PTY); if any doc-adjacent code change is made, route logic through answersToConfig so the gate stays green.
Tests & acceptance criteria
Documentation-only; no Go tests required. Acceptance criteria:
README.md### init describes the interactive wizard: filterable common-first SPDX picker, holder/year/style prompts with validation, manage-LICENSE confirm, and the --force overwrite guard.
README.md documents the non-TTY init path accurately (flags + same validation; usage error, not hang) and includes a flag-only example.
The misleading "hard-error in CI" wording is corrected in both the ### init section and the Configuration paragraph.
README.md### audit shows and names the findings: summary block, including the conditional dependencies: line and the policy: PASS/FAIL (...) rendering.
The command table still lists audit / check / apply / license / init / version; shared flags and exit codes remain accurate; the Install/Homebrew section is unchanged.
gofmt -l ., go vet ./..., golangci-lint run, go test ./... -race -cover, and go build ./... are unaffected; the .testcoverage.yml 100% gate stays green (no instrumented code changed).
(Recommended) The README examples are spot-checked against actual license-tool init and license-tool audit output so the documented findings: lines and prompt titles match the binary verbatim.
References
README.md — current command table (lines 36-42), ### audit (lines 46-59), ### init (lines 94-100), Configuration paragraph (lines 123-144). Lines 96 and 125 carry the "hard-error in CI" wording to correct.
CHANGELOG.md lines 15-19 — the v0.2.0 entries that the README has not absorbed (init wizard, audit findings-summary).
.testcoverage.yml — 100% total gate (line 16) and the wizard.go exclusion (line 47, rationale lines 36-42).
Preserve-first / universal "must-be-first" precedent (PR #5, v0.2.1) — internal/render/render.gopreserveBoundary (~line 320, universal #! handling ~lines 328-349) and internal/detect/detect.goPreserveBoundary (~line 171, universal #! ~lines 181-202); the builtin file-type table in internal/filetype/filetype.go (builtin, ~line 50; PHP entry ~line 176 noting the shebang rule is redundant given universal preservation). Linked for context only; not modified by this issue.
## Summary
The `README.md` documents the commands at a surface level but does not describe the two user-facing capabilities added in **v0.2.0** (see `CHANGELOG.md` lines 15-19):
1. The **interactive `init` wizard** — a `charmbracelet/huh` form with a filterable SPDX license picker (common licenses first), holder/year/style prompts with in-form validation, and an overwrite guard. The README only says `init` is "interactive on a TTY" and "prompts for missing required fields", which understates and partly mis-describes the actual behavior.
2. The **`audit` findings-summary block** — the at-a-glance `findings:` block printed at the top of the text report (source files headered/missing, license types, unknown/unrecognized, copyleft, dependencies resolved/unresolved, policy PASS/FAIL). The README's `### audit` section describes the breakdown categories prose-style but never shows or names the `findings:` block that is now the first thing a user sees.
There is also a **factual inaccuracy**: the README (lines 96 and 125) states the non-TTY / CI path "hard-errors rather than hanging" when required fields are missing. The current implementation does not hard-error on *missing required fields specifically* in a special CI branch — the non-interactive path simply skips the wizard and relies entirely on flags, then validates through the same `answersToConfig` gate (which errors on an unrecognized/empty license or empty holder, on a TTY or not). The "hard-error in CI" framing should be corrected to "non-TTY uses the flag values and validates them; missing/invalid license or holder is a usage error".
The command list, install/Homebrew section, shared flags, and exit codes are otherwise current and should be preserved.
## Type / Severity
- **Type:** documentation
- **Severity:** low (no code defect; the shipped behavior is correct and tested). The README simply lags the v0.2.0 surface, so users do not learn that `init` is a guided wizard or that `audit` leads with a findings summary, and one sentence about CI behavior is misleading.
## Reproduction
The gap is observable by comparing the README to actual command output.
**1. `audit` findings block is undocumented.** Create a sample repo and run audit:
```bash
mkdir -p /tmp/lt-demo && cd /tmp/lt-demo
git init -b main >/dev/null
printf 'package main\n\nfunc main() {}\n' > main.go # source file, no header
license-tool audit .
```
The text report now begins with a block the README never mentions or shows:
```
findings:
source files: 1 (headered 0, missing 1)
license types: none 1
unknown/unrecognized: 0
copyleft: none
policy: ...
```
(The `dependencies:` line appears only when dependency resolution ran.) Nothing in `README.md` documents the `findings:` label or its six/seven lines.
**2. `init` wizard is under-documented.** On a TTY:
```bash
cd /tmp/lt-demo
license-tool init .
```
This launches an interactive `huh` form: a **filterable** "Target SPDX license" select (common ids labeled `<id> (common)` first, then the full SPDX list), a "Copyright holder" input that refuses to advance while blank, a "Year policy" input validated against `current|YYYY|YYYY-YYYY|git`, a "Header style" select (`reuse|notice|reuse+notice`), and a "Manage top-level LICENSE file?" confirm. The README describes none of this — it only says "prompts for missing required fields."
**3. Overwrite guard is undocumented.** Re-running `init` when `.license-tool.yaml` already exists fails unless `--force` is passed:
```bash
license-tool init . # second run
# error: config: /tmp/lt-demo/.license-tool.yaml already exists (use --force)
license-tool init . --force # overwrites
```
The README's `### init` section does not mention that `init` reuses `--force` as the overwrite guard.
## Root cause
This is purely a documentation lag; the README was last meaningfully updated for the v0.1.0 surface. The behaviors are implemented and tested but undocumented or mis-documented:
- **`init` wizard** is implemented in `cmd/license-tool/wizard.go`:
- `runInitWizard` (around line 57) builds the `huh.NewForm` with the license select, holder/year/style prompts, and the confirm.
- `licenseSelectOptions` (around line 36) builds the **common-first, filterable** picker using `spdx.CommonIDs()` then the remaining `spdx.IDs()`, labeling common ids `"<id> (common)"`. The select enables `.Filtering(true)`.
- The holder validator (`errEmptyHolder`, line 15) and the year validator (reusing `config.ParseYearSpec`) enforce inputs in-form.
- **Overwrite guard / non-TTY flow** is in `cmd/license-tool/commands.go`:
- `newInitCmd` (around line 295) seeds `initAnswers` from the `--license/--holder/--year/--style` flags, calls `interactiveCollect(&a, isTTY())` (the wizard only runs on a TTY; non-TTY is a no-op), then funnels everything through `answersToConfig`.
- `answersToConfig` (around line 266) is the single validation gate (rejects an unrecognized license via `spdx.Validate`, and an empty holder), used identically on the TTY and flag-only paths — this is why the README's "hard-errors in CI" claim is imprecise.
- The overwrite guard lives in `config.WriteFile` (`internal/config/config.go` around line 231-234): `if !force && fileExists(target) { return ... "already exists (use --force)" }`. `init` reuses the `--force` flag for this (commands.go comment around line 327).
- `isTTY` (commands.go around line 434) uses `golang.org/x/term`'s `IsTerminal`, which is the v0.2.0 fix that makes `/dev/null` correctly non-interactive.
- **`audit` findings block** is implemented in `internal/report/findings.go`:
- `buildFindings` (around line 60) folds a `model.Report` into the `Findings` struct (source totals, per-id `LicenseCounts`, `UnknownCount`, `Copyleft`, dependency resolution, policy verdict).
- `renderFindings` (around line 163) emits the exact `findings:` block layout — `source files:`, `license types:`, `unknown/unrecognized:`, `copyleft:`, optional `dependencies:`, `policy:`.
## Proposed fix
Update `README.md` only (no code changes). Keep the existing **Install / Homebrew**, **Pre-built Binaries**, **Shared flags**, **Exit codes**, **Configuration**, **Development**, and **Release Automation** sections as-is.
1. **Expand `### init`** to describe the wizard concretely:
- On a TTY, `init` launches an interactive form (built on `charmbracelet/huh`) with: a **filterable** SPDX license picker (common licenses listed first, labeled `<id> (common)`, then the full SPDX list); a copyright-holder prompt that requires a non-empty value; a year-policy prompt validated against `current | YYYY | YYYY-YYYY | git`; a header-style select (`reuse | notice | reuse+notice`); and a "manage top-level LICENSE file?" confirm.
- Document the **overwrite guard**: `init` refuses to clobber an existing `.license-tool.yaml` and reuses `--force` to overwrite it.
- Document the **non-TTY path** accurately: in CI or any non-terminal stdin, the wizard is skipped and the values come from the `--license/--holder/--year/--style` flags (plus shared `--exclude`); the answers are validated the same way regardless of path, so an unrecognized license or empty holder is a usage error rather than a hang. Show a flag-only example, e.g. `license-tool init --license AGPL-3.0-or-later --holder "Kingsrook, LLC"`.
- Cross-link to the existing **Configuration** section, since `init` scaffolds the `.license-tool.yaml` documented there.
2. **Correct the "hard-error in CI" wording** in the `### init` section (currently line 96) and in the **Configuration** paragraph (currently line 125, "Missing required fields ... hard-error in CI") to match the validate-the-flags behavior described above.
3. **Document the `audit` findings-summary block** in `### audit`: show a representative `findings:` block and name its lines (source files headered/missing, license types, unknown/unrecognized, copyleft, dependencies resolved/unresolved when deps ran, policy PASS/FAIL). Note the `dependencies:` line is omitted when dependency resolution did not run, and that `policy:` renders `FAIL (<n>: <tokens>)` on failure.
4. **Verify the command/flag list is current** (it is, but confirm during the edit): the command table (lines 36-42) lists `audit/check/apply/license/init/version`; the shared flags and exit codes match the implementation.
This mirrors the precedent set by **PR #5 / v0.2.1** (the universal-shebang fix), which paired a behavior with a clear, single source-of-truth explanation: just as the shebang is now preserved *universally* in both `render.preserveBoundary` (`internal/render/render.go` ~line 320) and `detect.PreserveBoundary` (`internal/detect/detect.go` ~line 171) rather than per-type, the README should describe the `init` and `audit` behaviors *once, accurately*, rather than leaving them implied or per-command-fragmented. No relationship to the boundary code requires a doc change here; those functions and the `internal/filetype/filetype.go` `builtin` table are linked under **References** only so a reader auditing "preserve-first / must-be-first" behavior can find the canonical implementations.
**Coverage gate:** This is a docs-only change, so the 100% total coverage gate in `.testcoverage.yml` is unaffected. Note for the implementer: `cmd/license-tool/wizard.go` is intentionally excluded from coverage (`.testcoverage.yml` line 47, documented exclusion #3) because it is a thin huh-backed TUI shell with no business logic — all validation lives in `answersToConfig`/`config`/`spdx`, which ARE covered. Do **not** add tests that try to drive the real `huh` form (it needs a PTY); if any doc-adjacent code change is made, route logic through `answersToConfig` so the gate stays green.
## Tests & acceptance criteria
Documentation-only; no Go tests required. Acceptance criteria:
- [ ] `README.md` `### init` describes the interactive wizard: filterable common-first SPDX picker, holder/year/style prompts with validation, manage-LICENSE confirm, and the `--force` overwrite guard.
- [ ] `README.md` documents the non-TTY `init` path accurately (flags + same validation; usage error, not hang) and includes a flag-only example.
- [ ] The misleading "hard-error in CI" wording is corrected in both the `### init` section and the **Configuration** paragraph.
- [ ] `README.md` `### audit` shows and names the `findings:` summary block, including the conditional `dependencies:` line and the `policy:` PASS/`FAIL (...)` rendering.
- [ ] The command table still lists `audit / check / apply / license / init / version`; shared flags and exit codes remain accurate; the Install/Homebrew section is unchanged.
- [ ] `gofmt -l .`, `go vet ./...`, `golangci-lint run`, `go test ./... -race -cover`, and `go build ./...` are unaffected; the `.testcoverage.yml` 100% gate stays green (no instrumented code changed).
- [ ] (Recommended) The README examples are spot-checked against actual `license-tool init` and `license-tool audit` output so the documented `findings:` lines and prompt titles match the binary verbatim.
## References
- `README.md` — current command table (lines 36-42), `### audit` (lines 46-59), `### init` (lines 94-100), Configuration paragraph (lines 123-144). Lines 96 and 125 carry the "hard-error in CI" wording to correct.
- `CHANGELOG.md` lines 15-19 — the v0.2.0 entries that the README has not absorbed (`init` wizard, `audit` findings-summary).
- `cmd/license-tool/wizard.go` — `runInitWizard` (~line 57), `licenseSelectOptions` (~line 36), `errEmptyHolder` (line 15), `collectInteractive` (~line 109).
- `cmd/license-tool/commands.go` — `newInitCmd` (~line 295), `answersToConfig` (~line 266), `interactiveCollect` seam (~line 258), `isTTY` (~line 434, `golang.org/x/term`).
- `internal/config/config.go` — `WriteFile` overwrite guard (~lines 231-234), `Defaults` (~line 89), `ParseYearSpec` (~line 488), `ParseStyle` (~line 539).
- `internal/spdx/spdx.go` — `CommonIDs` (~line 230), `IDs` (~line 203), `Validate` (~line 146) used by the picker/validator.
- `internal/report/findings.go` — `buildFindings` (~line 60), `renderFindings` (~line 163), the `Findings` struct (lines 22-55).
- `.testcoverage.yml` — 100% total gate (line 16) and the `wizard.go` exclusion (line 47, rationale lines 36-42).
- **Preserve-first / universal "must-be-first" precedent (PR #5, v0.2.1)** — `internal/render/render.go` `preserveBoundary` (~line 320, universal `#!` handling ~lines 328-349) and `internal/detect/detect.go` `PreserveBoundary` (~line 171, universal `#!` ~lines 181-202); the builtin file-type table in `internal/filetype/filetype.go` (`builtin`, ~line 50; PHP entry ~line 176 noting the shebang rule is redundant given universal preservation). Linked for context only; not modified by this issue.
Status: confirmed — the README lags the v0.2.0 surface and carries one factually wrong sentence about non-TTY init. Verified against the checked-out code at the cited lines.
Severity: low (matches the documentation label). No code defect; shipped behavior is correct and tested. Impact is purely discoverability + one misleading sentence.
Verified root cause:
Misleading "hard-error in CI" claim: README.md line 96 (### init) and line 125 (Configuration paragraph: "Missing required fields ... prompt on a TTY and hard-error in CI"). The actual flow is newInitCmd → interactiveCollect(&a, isTTY()) (cmd/license-tool/commands.go:312) → answersToConfig(a) (commands.go:315). The seam interactiveCollect = collectInteractive (commands.go:258) is a no-op when not interactive (collectInteractive at cmd/license-tool/wizard.go:109 returns nil for non-TTY); validation is the single shared gate answersToConfig (commands.go:266), so a missing/invalid license or empty holder is a usage error, not a special CI hard-error branch. isTTY (commands.go:434-435) uses term.IsTerminal (import commands.go:8).
Undocumented init wizard: implemented in wizard.go — runInitWizard (wizard.go:57), licenseSelectOptions (wizard.go:36, common-first via spdx.CommonIDs(), label "<id> (common)" at line 42, .Filtering(true) at line 68), holder validator errEmptyHolder (wizard.go:15). README only says "prompts for missing required fields" (line 96).
Undocumented overwrite guard: config.WriteFile (internal/config/config.go:233-234, already exists (use --force)); init reuses --force (commands.go:327). Not mentioned in ### init.
Suggested fix: README-only edit, both occurrences of the "hard-error in CI" wording (lines 96 and 125) corrected to "non-TTY skips the wizard, uses the --license/--holder/--year/--style flags, and validates them through the same gate; an unrecognized license or empty holder is a usage error." Expand ### init to describe the filterable common-first SPDX picker, holder/year/style prompts with in-form validation, the manage-LICENSE confirm, and the --force overwrite guard; add a flag-only example. Document the findings: block in ### audit with a representative sample, noting dependencies: is omitted when deps did not resolve and policy: renders FAIL (...) on failure. This mirrors the v0.2.1 universal-shebang precedent (PR #5) of documenting a behavior once, accurately rather than per-fragment. No filetype-table or dual-boundary change is needed here — the boundary code (render.preserveBoundary, detect.PreserveBoundary) is referenced for context only, not touched. The 100% gate (.testcoverage.yml:16) is unaffected: docs-only, and wizard.go is already an intentional exclusion (.testcoverage.yml:47). Do not add tests driving the real huh form (needs a PTY); keep logic in answersToConfig.
Corrections to the issue: Essentially none — the body is accurate, including the line refs (96/125), the function names, and the wizard.go coverage-exclusion note. One nit: the body's References list collectInteractive (~line 109) while the prose section #1 says interactiveCollect(&a, isTTY()) (the exported seam) — both are correct and refer to different identifiers (interactiveCollect is the package var at commands.go:258 aliasing collectInteractive at wizard.go:109), so no contradiction, just worth noting they are two names for the call site.
Open questions: (1) Should the documented findings: sample be pinned verbatim to current binary output (risking future doc drift) or kept illustrative? (2) Do you want the flag-only init example to include --style/--year or stay minimal? Maintainer call on scope.
Overlaps: none. References PR #5 / v0.2.1 (universal shebang) only as a documentation-style precedent, not a dependency.
Automated maintainer review.
**Status:** confirmed — the README lags the v0.2.0 surface and carries one factually wrong sentence about non-TTY `init`. Verified against the checked-out code at the cited lines.
**Severity:** low (matches the `documentation` label). No code defect; shipped behavior is correct and tested. Impact is purely discoverability + one misleading sentence.
**Verified root cause:**
- Misleading "hard-error in CI" claim: `README.md` line 96 (`### init`) and line 125 (Configuration paragraph: "Missing required fields ... prompt on a TTY and hard-error in CI"). The actual flow is `newInitCmd` → `interactiveCollect(&a, isTTY())` (`cmd/license-tool/commands.go:312`) → `answersToConfig(a)` (`commands.go:315`). The seam `interactiveCollect = collectInteractive` (`commands.go:258`) is a **no-op when not interactive** (`collectInteractive` at `cmd/license-tool/wizard.go:109` returns nil for non-TTY); validation is the single shared gate `answersToConfig` (`commands.go:266`), so a missing/invalid license or empty holder is a *usage error*, not a special CI hard-error branch. `isTTY` (`commands.go:434-435`) uses `term.IsTerminal` (import `commands.go:8`).
- Undocumented `init` wizard: implemented in `wizard.go` — `runInitWizard` (`wizard.go:57`), `licenseSelectOptions` (`wizard.go:36`, common-first via `spdx.CommonIDs()`, label `"<id> (common)"` at line 42, `.Filtering(true)` at line 68), holder validator `errEmptyHolder` (`wizard.go:15`). README only says "prompts for missing required fields" (line 96).
- Undocumented overwrite guard: `config.WriteFile` (`internal/config/config.go:233-234`, `already exists (use --force)`); `init` reuses `--force` (`commands.go:327`). Not mentioned in `### init`.
- Undocumented `audit` findings block: `renderFindings` (`internal/report/findings.go:163-172`) emits `findings:` / `source files:` / `license types:` / `unknown/unrecognized:` / `copyleft:` / optional `dependencies:` (line 170, conditional) / `policy:`. `buildFindings` at `findings.go:60`. README `### audit` (lines 46-59) never names this block.
**Suggested fix:** README-only edit, both occurrences of the "hard-error in CI" wording (lines 96 and 125) corrected to "non-TTY skips the wizard, uses the `--license/--holder/--year/--style` flags, and validates them through the same gate; an unrecognized license or empty holder is a usage error." Expand `### init` to describe the filterable common-first SPDX picker, holder/year/style prompts with in-form validation, the manage-LICENSE confirm, and the `--force` overwrite guard; add a flag-only example. Document the `findings:` block in `### audit` with a representative sample, noting `dependencies:` is omitted when deps did not resolve and `policy:` renders `FAIL (...)` on failure. This mirrors the v0.2.1 universal-shebang precedent (PR #5) of documenting a behavior *once, accurately* rather than per-fragment. No filetype-table or dual-boundary change is needed here — the boundary code (`render.preserveBoundary`, `detect.PreserveBoundary`) is referenced for context only, not touched. The 100% gate (`.testcoverage.yml:16`) is unaffected: docs-only, and `wizard.go` is already an intentional exclusion (`.testcoverage.yml:47`). Do not add tests driving the real `huh` form (needs a PTY); keep logic in `answersToConfig`.
**Corrections to the issue:** Essentially none — the body is accurate, including the line refs (96/125), the function names, and the `wizard.go` coverage-exclusion note. One nit: the body's References list `collectInteractive (~line 109)` while the prose section #1 says `interactiveCollect(&a, isTTY())` (the exported seam) — both are correct and refer to different identifiers (`interactiveCollect` is the package var at `commands.go:258` aliasing `collectInteractive` at `wizard.go:109`), so no contradiction, just worth noting they are two names for the call site.
**Open questions:** (1) Should the documented `findings:` sample be pinned verbatim to current binary output (risking future doc drift) or kept illustrative? (2) Do you want the flag-only `init` example to include `--style`/`--year` or stay minimal? Maintainer call on scope.
**Overlaps:** none. References PR #5 / v0.2.1 (universal shebang) only as a documentation-style precedent, not a dependency.
_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.
Summary
The
README.mddocuments the commands at a surface level but does not describe the two user-facing capabilities added in v0.2.0 (seeCHANGELOG.mdlines 15-19):initwizard — acharmbracelet/huhform with a filterable SPDX license picker (common licenses first), holder/year/style prompts with in-form validation, and an overwrite guard. The README only saysinitis "interactive on a TTY" and "prompts for missing required fields", which understates and partly mis-describes the actual behavior.auditfindings-summary block — the at-a-glancefindings:block printed at the top of the text report (source files headered/missing, license types, unknown/unrecognized, copyleft, dependencies resolved/unresolved, policy PASS/FAIL). The README's### auditsection describes the breakdown categories prose-style but never shows or names thefindings:block that is now the first thing a user sees.There is also a factual inaccuracy: the README (lines 96 and 125) states the non-TTY / CI path "hard-errors rather than hanging" when required fields are missing. The current implementation does not hard-error on missing required fields specifically in a special CI branch — the non-interactive path simply skips the wizard and relies entirely on flags, then validates through the same
answersToConfiggate (which errors on an unrecognized/empty license or empty holder, on a TTY or not). The "hard-error in CI" framing should be corrected to "non-TTY uses the flag values and validates them; missing/invalid license or holder is a usage error".The command list, install/Homebrew section, shared flags, and exit codes are otherwise current and should be preserved.
Type / Severity
initis a guided wizard or thatauditleads with a findings summary, and one sentence about CI behavior is misleading.Reproduction
The gap is observable by comparing the README to actual command output.
1.
auditfindings block is undocumented. Create a sample repo and run audit:The text report now begins with a block the README never mentions or shows:
(The
dependencies:line appears only when dependency resolution ran.) Nothing inREADME.mddocuments thefindings:label or its six/seven lines.2.
initwizard is under-documented. On a TTY:This launches an interactive
huhform: a filterable "Target SPDX license" select (common ids labeled<id> (common)first, then the full SPDX list), a "Copyright holder" input that refuses to advance while blank, a "Year policy" input validated againstcurrent|YYYY|YYYY-YYYY|git, a "Header style" select (reuse|notice|reuse+notice), and a "Manage top-level LICENSE file?" confirm. The README describes none of this — it only says "prompts for missing required fields."3. Overwrite guard is undocumented. Re-running
initwhen.license-tool.yamlalready exists fails unless--forceis passed:The README's
### initsection does not mention thatinitreuses--forceas the overwrite guard.Root cause
This is purely a documentation lag; the README was last meaningfully updated for the v0.1.0 surface. The behaviors are implemented and tested but undocumented or mis-documented:
initwizard is implemented incmd/license-tool/wizard.go:runInitWizard(around line 57) builds thehuh.NewFormwith the license select, holder/year/style prompts, and the confirm.licenseSelectOptions(around line 36) builds the common-first, filterable picker usingspdx.CommonIDs()then the remainingspdx.IDs(), labeling common ids"<id> (common)". The select enables.Filtering(true).errEmptyHolder, line 15) and the year validator (reusingconfig.ParseYearSpec) enforce inputs in-form.cmd/license-tool/commands.go:newInitCmd(around line 295) seedsinitAnswersfrom the--license/--holder/--year/--styleflags, callsinteractiveCollect(&a, isTTY())(the wizard only runs on a TTY; non-TTY is a no-op), then funnels everything throughanswersToConfig.answersToConfig(around line 266) is the single validation gate (rejects an unrecognized license viaspdx.Validate, and an empty holder), used identically on the TTY and flag-only paths — this is why the README's "hard-errors in CI" claim is imprecise.config.WriteFile(internal/config/config.goaround line 231-234):if !force && fileExists(target) { return ... "already exists (use --force)" }.initreuses the--forceflag for this (commands.go comment around line 327).isTTY(commands.go around line 434) usesgolang.org/x/term'sIsTerminal, which is the v0.2.0 fix that makes/dev/nullcorrectly non-interactive.auditfindings block is implemented ininternal/report/findings.go:buildFindings(around line 60) folds amodel.Reportinto theFindingsstruct (source totals, per-idLicenseCounts,UnknownCount,Copyleft, dependency resolution, policy verdict).renderFindings(around line 163) emits the exactfindings:block layout —source files:,license types:,unknown/unrecognized:,copyleft:, optionaldependencies:,policy:.Proposed fix
Update
README.mdonly (no code changes). Keep the existing Install / Homebrew, Pre-built Binaries, Shared flags, Exit codes, Configuration, Development, and Release Automation sections as-is.Expand
### initto describe the wizard concretely:initlaunches an interactive form (built oncharmbracelet/huh) with: a filterable SPDX license picker (common licenses listed first, labeled<id> (common), then the full SPDX list); a copyright-holder prompt that requires a non-empty value; a year-policy prompt validated againstcurrent | YYYY | YYYY-YYYY | git; a header-style select (reuse | notice | reuse+notice); and a "manage top-level LICENSE file?" confirm.initrefuses to clobber an existing.license-tool.yamland reuses--forceto overwrite it.--license/--holder/--year/--styleflags (plus shared--exclude); the answers are validated the same way regardless of path, so an unrecognized license or empty holder is a usage error rather than a hang. Show a flag-only example, e.g.license-tool init --license AGPL-3.0-or-later --holder "Kingsrook, LLC".initscaffolds the.license-tool.yamldocumented there.Correct the "hard-error in CI" wording in the
### initsection (currently line 96) and in the Configuration paragraph (currently line 125, "Missing required fields ... hard-error in CI") to match the validate-the-flags behavior described above.Document the
auditfindings-summary block in### audit: show a representativefindings:block and name its lines (source files headered/missing, license types, unknown/unrecognized, copyleft, dependencies resolved/unresolved when deps ran, policy PASS/FAIL). Note thedependencies:line is omitted when dependency resolution did not run, and thatpolicy:rendersFAIL (<n>: <tokens>)on failure.Verify the command/flag list is current (it is, but confirm during the edit): the command table (lines 36-42) lists
audit/check/apply/license/init/version; the shared flags and exit codes match the implementation.This mirrors the precedent set by PR #5 / v0.2.1 (the universal-shebang fix), which paired a behavior with a clear, single source-of-truth explanation: just as the shebang is now preserved universally in both
render.preserveBoundary(internal/render/render.go~line 320) anddetect.PreserveBoundary(internal/detect/detect.go~line 171) rather than per-type, the README should describe theinitandauditbehaviors once, accurately, rather than leaving them implied or per-command-fragmented. No relationship to the boundary code requires a doc change here; those functions and theinternal/filetype/filetype.gobuiltintable are linked under References only so a reader auditing "preserve-first / must-be-first" behavior can find the canonical implementations.Coverage gate: This is a docs-only change, so the 100% total coverage gate in
.testcoverage.ymlis unaffected. Note for the implementer:cmd/license-tool/wizard.gois intentionally excluded from coverage (.testcoverage.ymlline 47, documented exclusion #3) because it is a thin huh-backed TUI shell with no business logic — all validation lives inanswersToConfig/config/spdx, which ARE covered. Do not add tests that try to drive the realhuhform (it needs a PTY); if any doc-adjacent code change is made, route logic throughanswersToConfigso the gate stays green.Tests & acceptance criteria
Documentation-only; no Go tests required. Acceptance criteria:
README.md### initdescribes the interactive wizard: filterable common-first SPDX picker, holder/year/style prompts with validation, manage-LICENSE confirm, and the--forceoverwrite guard.README.mddocuments the non-TTYinitpath accurately (flags + same validation; usage error, not hang) and includes a flag-only example.### initsection and the Configuration paragraph.README.md### auditshows and names thefindings:summary block, including the conditionaldependencies:line and thepolicy:PASS/FAIL (...)rendering.audit / check / apply / license / init / version; shared flags and exit codes remain accurate; the Install/Homebrew section is unchanged.gofmt -l .,go vet ./...,golangci-lint run,go test ./... -race -cover, andgo build ./...are unaffected; the.testcoverage.yml100% gate stays green (no instrumented code changed).license-tool initandlicense-tool auditoutput so the documentedfindings:lines and prompt titles match the binary verbatim.References
README.md— current command table (lines 36-42),### audit(lines 46-59),### init(lines 94-100), Configuration paragraph (lines 123-144). Lines 96 and 125 carry the "hard-error in CI" wording to correct.CHANGELOG.mdlines 15-19 — the v0.2.0 entries that the README has not absorbed (initwizard,auditfindings-summary).cmd/license-tool/wizard.go—runInitWizard(~line 57),licenseSelectOptions(~line 36),errEmptyHolder(line 15),collectInteractive(~line 109).cmd/license-tool/commands.go—newInitCmd(~line 295),answersToConfig(~line 266),interactiveCollectseam (~line 258),isTTY(~line 434,golang.org/x/term).internal/config/config.go—WriteFileoverwrite guard (~lines 231-234),Defaults(~line 89),ParseYearSpec(~line 488),ParseStyle(~line 539).internal/spdx/spdx.go—CommonIDs(~line 230),IDs(~line 203),Validate(~line 146) used by the picker/validator.internal/report/findings.go—buildFindings(~line 60),renderFindings(~line 163), theFindingsstruct (lines 22-55)..testcoverage.yml— 100% total gate (line 16) and thewizard.goexclusion (line 47, rationale lines 36-42).internal/render/render.gopreserveBoundary(~line 320, universal#!handling ~lines 328-349) andinternal/detect/detect.goPreserveBoundary(~line 171, universal#!~lines 181-202); the builtin file-type table ininternal/filetype/filetype.go(builtin, ~line 50; PHP entry ~line 176 noting the shebang rule is redundant given universal preservation). Linked for context only; not modified by this issue.Status: confirmed — the README lags the v0.2.0 surface and carries one factually wrong sentence about non-TTY
init. Verified against the checked-out code at the cited lines.Severity: low (matches the
documentationlabel). No code defect; shipped behavior is correct and tested. Impact is purely discoverability + one misleading sentence.Verified root cause:
README.mdline 96 (### init) and line 125 (Configuration paragraph: "Missing required fields ... prompt on a TTY and hard-error in CI"). The actual flow isnewInitCmd→interactiveCollect(&a, isTTY())(cmd/license-tool/commands.go:312) →answersToConfig(a)(commands.go:315). The seaminteractiveCollect = collectInteractive(commands.go:258) is a no-op when not interactive (collectInteractiveatcmd/license-tool/wizard.go:109returns nil for non-TTY); validation is the single shared gateanswersToConfig(commands.go:266), so a missing/invalid license or empty holder is a usage error, not a special CI hard-error branch.isTTY(commands.go:434-435) usesterm.IsTerminal(importcommands.go:8).initwizard: implemented inwizard.go—runInitWizard(wizard.go:57),licenseSelectOptions(wizard.go:36, common-first viaspdx.CommonIDs(), label"<id> (common)"at line 42,.Filtering(true)at line 68), holder validatorerrEmptyHolder(wizard.go:15). README only says "prompts for missing required fields" (line 96).config.WriteFile(internal/config/config.go:233-234,already exists (use --force));initreuses--force(commands.go:327). Not mentioned in### init.auditfindings block:renderFindings(internal/report/findings.go:163-172) emitsfindings:/source files:/license types:/unknown/unrecognized:/copyleft:/ optionaldependencies:(line 170, conditional) /policy:.buildFindingsatfindings.go:60. README### audit(lines 46-59) never names this block.Suggested fix: README-only edit, both occurrences of the "hard-error in CI" wording (lines 96 and 125) corrected to "non-TTY skips the wizard, uses the
--license/--holder/--year/--styleflags, and validates them through the same gate; an unrecognized license or empty holder is a usage error." Expand### initto describe the filterable common-first SPDX picker, holder/year/style prompts with in-form validation, the manage-LICENSE confirm, and the--forceoverwrite guard; add a flag-only example. Document thefindings:block in### auditwith a representative sample, notingdependencies:is omitted when deps did not resolve andpolicy:rendersFAIL (...)on failure. This mirrors the v0.2.1 universal-shebang precedent (PR #5) of documenting a behavior once, accurately rather than per-fragment. No filetype-table or dual-boundary change is needed here — the boundary code (render.preserveBoundary,detect.PreserveBoundary) is referenced for context only, not touched. The 100% gate (.testcoverage.yml:16) is unaffected: docs-only, andwizard.gois already an intentional exclusion (.testcoverage.yml:47). Do not add tests driving the realhuhform (needs a PTY); keep logic inanswersToConfig.Corrections to the issue: Essentially none — the body is accurate, including the line refs (96/125), the function names, and the
wizard.gocoverage-exclusion note. One nit: the body's References listcollectInteractive (~line 109)while the prose section #1 saysinteractiveCollect(&a, isTTY())(the exported seam) — both are correct and refer to different identifiers (interactiveCollectis the package var atcommands.go:258aliasingcollectInteractiveatwizard.go:109), so no contradiction, just worth noting they are two names for the call site.Open questions: (1) Should the documented
findings:sample be pinned verbatim to current binary output (risking future doc drift) or kept illustrative? (2) Do you want the flag-onlyinitexample to include--style/--yearor stay minimal? Maintainer call on scope.Overlaps: none. References PR #5 / v0.2.1 (universal shebang) only as a documentation-style precedent, not a dependency.
Automated maintainer review.
Resolved on
developin commit742c124e6ec2afa9312075db3112bc2def3c91ff.Validation:
Closing as fixed.