feat(0.2): explain finding + suppress writer + --new-findings-only (Tracks 4.6/4.7/4.8)#140
Open
pmclSF wants to merge 1 commit intofeat/0.2-suppressionsfrom
Open
feat(0.2): explain finding + suppress writer + --new-findings-only (Tracks 4.6/4.7/4.8)#140pmclSF wants to merge 1 commit intofeat/0.2-suppressionsfrom
pmclSF wants to merge 1 commit intofeat/0.2-suppressionsfrom
Conversation
…/4.7/4.8)
Bundles the three remaining Track 4 deliverables into one PR. With
4.4 (finding IDs) and 4.5 (suppression model) already in flight,
this PR makes the suppression workflow actually usable end-to-end:
Track 4.6 — terrain explain <finding-id>
Extends `terrain explain` to recognize stable finding IDs (e.g.
"weakAssertion@internal/auth/login.go:TestLogin#a1b2c3d4"). On a
hit, prints a finding-detail block: detector + severity + location +
evidence + explanation + suggested action + the canonical
`terrain suppress <id> --reason "..."` invocation.
A finding ID that parses but isn't in the snapshot returns a
distinct exit-5 (not-found) message that distinguishes "stale ID
after refactor" from "garbage input" — common adoption flow when
a user keeps a CI link to a finding that has since moved.
Implementation: lookupSignalByFindingID + renderFindingExplanation
in cmd/terrain/cmd_explain.go.
Track 4.7 — terrain suppress <finding-id> --reason "..." [--expires] [--owner]
New top-level Gate-pillar primitive. Validates the ID format,
refuses duplicates (existing entry → usage error pointing at the
existing reason), appends a YAML entry to .terrain/suppressions.yaml.
Writes text rather than re-marshaling the file so any comments /
ordering the user added by hand are preserved. Schema header is
auto-emitted on first call.
--reason required (every suppression justifies itself, per Track
4.5 schema). --expires optional but recommended; ISO YYYY-MM-DD
shape validated up front. --owner optional free-text pointer.
Implementation: cmd/terrain/cmd_suppress.go + 7 unit tests.
Track 4.8 — terrain analyze --new-findings-only --baseline <path>
Filters the snapshot to keep only signals whose FindingID is NOT
present in the baseline. The "established repos with debt"
adoption flow: `--fail-on critical` would brick CI on day one
against existing high findings; combining with
`--new-findings-only --baseline old.json` makes the gate fire
only on findings introduced AFTER the baseline.
Implementation: PipelineOptions.NewFindingsOnly +
internal/engine/new_findings_only.go (applyNewFindingsOnly).
Runs after suppression apply so the baseline comparison sees
the user's intended-active signal set.
No-baseline case: --new-findings-only is inert; logs a warning so
the user notices the flag had no effect (better than silent
success that masks the misconfiguration).
Signals without FindingID (older / specialized emissions) are
KEPT — over-report rather than under-report.
Implementation: 6 unit tests including the "no-baseline" warning
path, empty baseline, per-file signals, and signals without IDs.
Refactor: runAnalyze gets a `analyzeRunOpts` struct so the call site
in main.go isn't a 17-positional-argument list. The struct collapses
the existing args + adds SuppressionsPath + NewFindingsOnly. Future
flag additions stop expanding the call signature.
Validation in main.go: --new-findings-only requires --baseline; the
combination is rejected at usage-error level (exit 2) so the user
gets a clear message rather than a silent no-op.
Verification:
go test ./cmd/terrain/ -run "TestRunSuppress|TestLooksLikeISODate" — 7 tests green
go test ./internal/engine/ -run "TestApplyNewFindingsOnly" — 6 tests green
go test ./... — full suite green
go test ./internal/testdata/ — golden + CLI suite green
Plan link: /Users/pzachary/.claude/plans/kind-mapping-turing.md
(Tracks 4.6, 4.7, 4.8).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bundles Tracks 4.6, 4.7, and 4.8 from the 0.2.0 parity-gated release plan. With finding IDs (#138) and the suppression schema (#139) in flight, this PR makes the suppression workflow usable end-to-end.
Base branch: `feat/0.2-suppressions` (#139, which itself stacks on #138). Will rebase onto main after the chain merges.
Track 4.6 — `terrain explain `
`terrain explain` recognizes stable finding IDs and prints a finding-detail block: detector + severity + location + evidence + explanation + suggested action + the canonical `terrain suppress` invocation. A finding ID that parses but isn't in the snapshot returns a distinct exit-5 (not-found) message that distinguishes "stale ID after refactor" from "garbage input."
Track 4.7 — `terrain suppress --reason "..." [--expires] [--owner]`
New top-level Gate-pillar primitive. Validates the ID format, refuses duplicates (existing entry → usage error pointing at the existing reason), appends a YAML entry to `.terrain/suppressions.yaml`. Writes text rather than re-marshaling so user comments / ordering are preserved. Schema header auto-emitted on first call.
Track 4.8 — `--new-findings-only --baseline `
The "established repos with debt" adoption flow. `--fail-on critical` alone would brick CI on day one against existing findings; combining with `--new-findings-only --baseline old.json` makes the gate fire only on findings introduced AFTER the baseline. `--new-findings-only` without `--baseline` is rejected at usage-error level so the user gets a clear message rather than a silent no-op.
Refactor (small, helpful)
`runAnalyze` gets an `analyzeRunOpts` struct so the call site in `main.go` isn't a 17-positional-argument list. Future flag additions don't expand the signature.
Test plan
Plan link
`/Users/pzachary/.claude/plans/kind-mapping-turing.md` (Tracks 4.6 / 4.7 / 4.8).
🤖 Generated with Claude Code