86 Commits
Author SHA1 Message Date
james.maes 43fa40c111 Merge pull request 'ci: build on Gitea with the shared Go workflow' (#53) from ci/gitea-go-workflow into develop 2026-11-08 15:01:43 +00:00
James MaesandClaude Opus 5.5 48bf47d4a8 ci: pin the shared workflow to v1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:00:29 -06:00
James MaesandClaude Opus 5.5 7da7f93fc7 ci: call the shared workflow from ci-workflows main
go / go (push) Successful in 2m0s
build / go (push) Successful in 2m0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 08:56:21 -06:00
James MaesandClaude Opus 5.5 147082206f ci: build on Gitea with the shared Go workflow
go / go (push) Successful in 3m16s
build / go (push) Successful in 3m16s
Port the GitHub CI gates (gofmt, go vet, golangci-lint, race tests with
the coverage profile, the 100% coverage gate, go build) to the shared
ci-workflows go.yaml. v* tags build the GoReleaser-equivalent archives
with scripts/build-release.sh and attach them with SHA256SUMS to the
Gitea release. The GitHub workflows ran on ubuntu-22.04/macos/windows
runners that Gitea does not serve; removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 08:27:29 -06:00
james.maes 8a8bc4b1e4 docs: session handoff after v0.4.0 release 2026-06-09 10:34:13 -05:00
james.maes c32a65621c docs: changelog for v0.4.0; add RELEASES.md release runbook 2026-06-09 10:17:32 -05:00
james.maes 9cf00681b7 style: gofmt commands_test.go after merge conflict resolution 2026-06-09 10:15:10 -05:00
james.maes a14dee227a Merge feature/GH-30-detect-overmatch (#30): confine header detection 2026-06-09 10:12:26 -05:00
james.maes e1e4563e03 Merge feature/GH-33-symlink-license (#33): refuse symlinked LICENSE 2026-06-09 10:12:19 -05:00
james.maes 5f11b5ad38 Merge feature/GH-34-quiet-verbose-flags (#34): remove no-op flags
# Conflicts:
#	CHANGELOG.md
2026-06-09 10:12:19 -05:00
james.maes d4c72311cf Merge feature/GH-31-spdxnorm-no-guess (#31): stop guessing SPDX ids 2026-06-09 10:11:35 -05:00
james.maes e116c60b88 Merge feature/GH-29-init-tui-wizard (#29): init full TUI wizard
# Conflicts:
#	CHANGELOG.md
#	cmd/license-tool/commands_test.go
#	docs/SESSION-STATE.md
2026-06-09 10:11:34 -05:00
james.maes c035b8be6b Merge feature/GH-35-audit-summary-reports (#35): audit summary + group-by reporting 2026-06-09 09:58:43 -05:00
james.maes 472d78b914 docs: track the six open PRs (#29/#30/#31/#33/#34/#35) and review notes 2026-06-09 09:49:55 -05:00
james.maes 6497f0be81 fix(#30): confine license-header detection to contiguous comment lines
Header detection over-matched on line-comment file types: lineCommentRegion
tolerated truly-blank lines between comment lines, so a layout of
"license header / blank line / unrelated file doc comment" collapsed into a
single detected span. render.replaceSpan then deleted that doc comment on
relicense or year-refresh. This is safety-critical -- a false positive in
detection means apply removes real source content.

Two fixes:

1. lineCommentRegion now stops at the first non-comment line, and a
   truly-blank line ends the contiguous run. Our own rendered headers never
   contain a truly-blank internal line: the renderer emits separator lines as
   a bare comment prefix (e.g. "#" or "//"), which is still a comment line and
   stays in-region. Confining the region to contiguous comment lines is
   therefore byte-idempotent for headers we write, while refusing to absorb a
   foreign comment that a blank line has separated from the header.

2. spdxIdentifierTag now requires the SPDX-License-Identifier tag to begin a
   comment line (leading whitespace allowed) rather than matching anywhere via
   strings.Index. A comment that merely mentions the tag mid-prose is
   documentation, not a managed header, and must not qualify a foreign comment
   for replacement.

When in doubt the detector now under-detects (leaves a foreign comment alone)
rather than over-detecting. Block-comment multi-block de-stacking (issue #30
LOW) is deferred: spanning only the first block is the safe, under-detecting
choice.

Idempotency and round-trip are preserved and verified by the existing
TestSpliceIdempotent / TestApplyFile* suites plus a new end-to-end applier
test that relicenses a line-comment file and asserts a doc comment below the
header's blank separator survives. The enshrining region test was updated to
require the corrected contiguous behavior with the safety rationale inline.
2026-06-09 08:38:50 -05:00
james.maes 05157ddf48 fix(#33): refuse to overwrite a symlinked license file
AtomicWrite used os.Stat + os.Rename with no symlink check, so writing a
managed LICENSE / LICENSES/<id>.txt that is a deliberate symlink (e.g.
LICENSE -> LICENSES/MIT.txt) would replace the link with a regular file,
silently breaking it.

AtomicWrite now lstats the target and refuses with errSymlinkTarget when it
is a symlink, leaving the link and its target untouched. The error message
contains "refusing to write" so the CLI's isWriteRefusal mapping classifies
it as a write refusal (exit code 3). writeManaged propagates this refusal as
a returned error (other write failures stay recorded per-file as before).

Closes #33
2026-06-09 08:00:58 -05:00
james.maes 20a0cd0f48 fix(#34): remove the no-op --quiet/--verbose flags
The persistent --quiet/-q and --verbose/-v flags were bound on the root
flag set but read nowhere -- pure no-op flags advertising behavior they
never delivered.

Decision: REMOVE (Option B), not WIRE. A review of every output site
showed there is no non-essential, human-facing output to gate behind
--quiet (the only stdout output is the machine report from audit/check/
apply and essential confirmations like "wrote X"), and no diagnostic or
tracing facility for --verbose to enable (the model's ResolveOptions.
Verbose field is itself dead -- no resolver reads it, nothing writes
diagnostics). Wiring would have required inventing new output purely to
justify the flags, i.e. hollow/cosmetic wiring. Removing the no-ops is
the honest, lower-risk choice for pre-1.0 (v0.3.0).

Passing --quiet/-q/--verbose/-v now yields an unknown-flag usage error
(exit 2). Updated TestNewRootCmd and added TestQuietVerboseFlagsRemoved;
updated README, the design doc, and the CHANGELOG.

The optional LOW item (typed-sentinel refactor of isWriteRefusal) is
deferred: the refusal strings span both internal/applier and
internal/config, so a sentinel refactor would touch the applier
write/refusal path that issue #33 is changing on a separate branch,
risking merge conflicts and cross-package sprawl. Kept this change
scoped to the CLI flag surface.

Closes #34
2026-06-09 08:00:09 -05:00
james.maes 194fb0e49f fix(#31): resolve ambiguous license aliases to unresolved instead of guessing
The spdxAliases table fabricated SPDX ids, violating the package's
"never guess" invariant:

- "bsd" / "bsd license" invented a clause count (could be 2/3/4-clause).
- "gnu lesser general public license" invented BOTH a version (2.1 vs 3.0)
  and the grant ("only" vs "or-later").

These wrong guesses flowed into deny/allow verdicts and the incompatibility
table, where -only vs -or-later and clause counts are decisive. Such ambiguous
spellings now leave normalizeSPDX returning ok=false, so the caller emits an
unresolved dependency with a reason rather than a fabricated id. Only
unambiguous, exact spellings remain (explicit 3-clause BSD, Apache, MIT, ISC,
MPL-2.0, Unlicense, the explicit AGPL "v3.0 or later" spelling).

Also fixes the related resolved-but-unclassifiable contradiction: alias
re-validation now uses spdx.Lookup (curated rendering set) instead of
spdx.Validate (full index). The Eclipse Public License aliases (EPL-2.0 /
EPL-1.0) passed Validate but are outside the curated set, so policy.Classify
returned Unknown while the dependency was still marked resolved. They are now
left unresolved. Any future alias whose target is valid-but-uncurated is
structurally rejected by the Lookup gate.

Closes #31
2026-06-09 07:59:28 -05:00
james.maes 7159bc1b48 docs(#35): close punch list; record review follow-ups and PR readiness 2026-06-09 07:45:58 -05:00
james.maes 3852c9122b fix(#35): harden group risk under --only; review-driven follow-ups
Address findings from a three-agent adversarial review of the GH-35
delta:

- Compute the repo-wide incompatibility set from the full report, not the
  --only-narrowed listing, so a filtered listing can no longer understate
  a group's policy-aware risk. Add groupFilesWith(r, spec, incompatIDs)
  and thread the full-report set through the text/markdown/JSON renderers.
- Use path.Base (not filepath.Base) on the slash-normalized enumerate
  path when matching the config file, for cross-platform correctness.
- Tests: directory-grouping escalation across a mixed bucket, incompat
  beating the headerless-unknown fallback, non-policy tokens not
  escalating, incompatibleIDs cardinality (0/1/3 ids), the filtered-
  listing risk contract, nested/--only config exclusion, no detected-
  license/violation leak on the skipped config, and a check exit-code
  regression guard for the config exclusion.
- Docs: correct the README grouped-output samples (risk markers,
  (no-header) label, JSON group shape) and note the check exit-code
  implication of the config exclusion in README and CHANGELOG.
2026-06-09 07:44:21 -05:00
james.maes 56c3e9410b docs(#35): document self-config exclusion, policy-aware risk, and decisions
README: policy-aware group risk and the .license-tool.yaml exclusion.
CHANGELOG: comprehensive Unreleased entry for the audit reporting overhaul.
DESIGN: post-implementation addenda recording the two new features and the
three audit-flagged items resolved by decision (dep-grouping non-goal,
JSON percentages declined, go.mod not a supported ecosystem).
2026-06-09 07:12:06 -05:00
james.maes 2822d862d8 feat(#35): policy-aware group risk
A group's [risk] marker was computed solely from its files' license
categories, so it was policy-blind: an Apache-2.0 group beside an
AGPL-3.0-or-later group read "low" even though the pair is a curated
hard incompatibility, and a deny-listed permissive license also read
"low".

Escalate a group's risk to "high" when it carries a policy concern: a
file whose license is party to a repo-level hard incompatibility
(computed once over the distinct source ids via policy.Incompatible),
or a file carrying a file-scoped policy-violation token. Category-only
risk is preserved when there is no policy concern, with no false
escalation (MIT beside AGPL, not an incompatibility, stays "low").
2026-06-09 07:10:16 -05:00
james.maes ce192cad54 feat(#35): exclude tool's own .license-tool.yaml from source coverage
The config file is YAML (commentable), so the audit counted it as a
coverable source file missing a managed header, inflating sourceTotal
and sourceMissing. It is tool metadata, not coverable source, and never
carries a header.

Mark any file whose basename is .license-tool.yaml as skipped (reason
"tool config") in the audit pipeline, so it still appears in the file
listing but is excluded from source/header tallies. Export
config.RepoConfigName as the single source of truth for the name.
2026-06-08 15:30:38 -05:00
james.maes bbba9d28bf docs(#35): document --only filter and JSON-always-full behavior 2026-06-08 15:12:54 -05:00
james.maes 366ffece45 feat(#35): --only filter to list problem files (missing|unknown|copyleft|violations) 2026-06-08 15:12:29 -05:00
james.maes b43e362a21 fix(#35): unify (no-header) token, unknown-risk for headerless groups, full JSON under --summary 2026-06-08 14:58:42 -05:00
james.maes 83df6402ab docs(#35): document audit risk/attribution/findings/sort/depth enhancements 2026-06-08 14:24:37 -05:00
james.maes 96389108a8 feat(#35): per-group license breakdown, --depth directory grouping 2026-06-08 14:21:52 -05:00
james.maes 29c7108c80 feat(#35): per-group risk markers, (no-header) rename, --sort key|count 2026-06-08 14:14:19 -05:00
james.maes daef9c396d feat(#35): findings + totals/percentages + riskLevel parity across text/markdown/json 2026-06-08 14:07:52 -05:00
james.maes 48fd159368 feat(#35): attributable policy violations (which license/rule/file) in all formats 2026-06-08 14:01:30 -05:00
james.maes b28d245c60 feat(#35): wire --summary and --group-by audit flags + document them
Closes #35
2026-06-08 13:29:35 -05:00
james.maes dd4b34ba93 feat(#35): RenderOptions for summary + grouped views across all formats 2026-06-08 13:19:45 -05:00
james.maes c3755f7baf feat(#35): add pure GroupFiles + ParseGroupBy for audit grouping 2026-06-08 13:12:04 -05:00
james.maes 6051a36198 docs(#35): spec audit summary + group-by report options 2026-06-08 13:04:49 -05:00
james.maes 32b44e5143 docs(#29): record init redesign completion in session state 2026-06-08 12:34:30 -05:00
james.maes 0031b00eb3 feat(#29): single-form init wizard with adaptive live preview
Replaces the 7-step 3-pane wizard with a single repo-seeded form and a
persistent adaptive preview. wizard.go is now a thin Bubble Tea adapter;
all logic lives in internal/initwizard at 100% coverage.

Closes #29
2026-06-08 12:33:49 -05:00
james.maes 09c8174841 feat(#29): pure adaptive view rendering for init form 2026-06-08 12:21:34 -05:00
james.maes a6cc331fea feat(#29): adaptive panel layout planner 2026-06-08 12:14:31 -05:00
james.maes 45dcfd90e1 feat(#29): pure form state machine with live validation 2026-06-08 12:13:25 -05:00
james.maes ddcc51d27f feat(#29): seed init answers from repo detection 2026-06-08 11:58:01 -05:00
james.maes bafa2810a6 refactor(#29): unify Answers->Config behind one translator 2026-06-08 11:50:28 -05:00
james.maes 4e3561ef29 fix(#32): include globs honor ** via shared gitignore matcher 2026-06-08 11:44:56 -05:00
james.maes 44d4986063 feat(init): add pane-based TUI wizard
Refs #29
2026-06-08 09:28:03 -05:00
james.maes 1bfddf89f9 merge: init tui shell slice 2026-06-04 16:37:21 -05:00
james.maes ca3479ef02 merge: init preview engine slice 2026-06-04 16:36:55 -05:00
james.maes f80ab905f2 feat(init): add source preview engine
Refs #29
2026-06-04 16:36:05 -05:00
james.maes ef70f64fff merge: init include config slice 2026-06-04 16:36:04 -05:00
james.maes 4773596e5d feat(init): wire tui collector
Refs #29.
2026-06-04 16:36:03 -05:00
james.maes fa1b86da03 feat(init): persist include patterns
Refs #29
2026-06-04 16:35:20 -05:00