Skip to content

feat(0.2.1): --fail-on and --timeout flags on terrain analyze#134

Open
pmclSF wants to merge 2 commits intomainfrom
feat/0.2.1-gate-flags
Open

feat(0.2.1): --fail-on and --timeout flags on terrain analyze#134
pmclSF wants to merge 2 commits intomainfrom
feat/0.2.1-gate-flags

Conversation

@pmclSF
Copy link
Copy Markdown
Owner

@pmclSF pmclSF commented May 2, 2026

Summary

Closes the platform/SRE adoption gap flagged in the 0.2.0 launch-readiness review (Item 40, Item 39). Two CI-grade flags on `terrain analyze`:

  • `--fail-on critical|high|medium` — exit with new code 6 when at least one finding is at or above the requested severity. Standard CI gate.
  • `--timeout ` — abort the analysis after the duration elapses (e.g. `5m`, `30s`). Wraps the existing SIGINT-aware context with a deadline.

Approach

  • New `cmd/terrain/cmd_severity_gate.go` — pure functions over `analyze.SignalBreakdown` plus an `errSeverityGateBlocked` sentinel. Tested at unit level; no engine coupling.
  • `runPipelineWithSignals` becomes a 0-timeout shim around the new `runPipelineWithSignalsAndTimeout`, so the six other callsites (`impact`, `explain`, `pr`, `ai *`) keep their contract and can adopt the flag in follow-up PRs without churning this one.
  • Gate evaluated last in `runAnalyze` so the report renders first, then the exit code is decided.
  • Exit code 6 added to the conventions block. Code 3 stays reserved for the planned policy-vs-usage split.

Manual smoke (against the Terrain repo itself)

```
$ terrain analyze --fail-on critical → exit 0 (no critical findings)
$ terrain analyze --fail-on medium → exit 6 ("0 critical + 493 high + 169 medium finding(s)")
$ terrain analyze --fail-on bogus → exit 2 (usage error)
$ terrain analyze --timeout 100ms → analysis aborts cleanly with context.DeadlineExceeded
```

Test plan

  • `TestParseSeverityGate` — canonical / case-insensitive / whitespace / invalid inputs
  • `TestSeverityGateBlocked` — the threshold cascade (critical / critical+high / critical+high+medium) plus the gate-none always-pass case
  • `go test ./...` and `go vet ./...` clean
  • Three manual exit-code smokes above

Out of scope

`--new-findings-only --baseline ` from the plan — needs the stable finding IDs work (Phase 3.1) to ship clean across renamed files. Tracked for a 0.2.x follow-up.

Plan link

`/Users/pzachary/.claude/plans/kind-mapping-turing.md` (Phase 3.6).

🤖 Generated with Claude Code

CI integration was the launch-readiness review's top adoption gap
for the platform/SRE audience. Two flags close most of it:

  --fail-on critical|high|medium
      Exit with the new severity-gate code (6) when at least one
      finding at or above the requested severity is present.
      Standard CI integration pattern. The report still renders
      first; the gate decision is the last thing that happens.

  --timeout <duration>
      Abort the analysis after the duration elapses (e.g. 5m, 30s).
      Wraps the existing SIGINT-aware context with a deadline so a
      runaway monorepo scan doesn't block CI indefinitely.

Exit-code conventions extended:

  6 — Severity gate block. Returned by analyze --fail-on. Same
      pattern as the existing AI-gate code 4. CI scripts can branch
      on "the analysis succeeded but the gate blocked us" without
      parsing stderr text. Code 3 stays reserved for the planned
      policy-vs-usage split.

Implementation:
  * cmd/terrain/cmd_severity_gate.go — parseSeverityGate +
    severityGateBlocked + errSeverityGateBlocked sentinel. Pure
    function over analyze.SignalBreakdown; no engine coupling.
  * cmd/terrain/cmd_pipeline_helpers.go — new
    runPipelineWithSignalsAndTimeout(...). The original
    runPipelineWithSignals is preserved as a 0-timeout shim so the
    other six callsites (impact / explain / pr / ai *) keep their
    contract; they can adopt the timeout flag in follow-up PRs
    without churning this one.
  * cmd/terrain/main.go — flag wiring; errors.Is(err,
    errSeverityGateBlocked) routes the gate exit code without
    confusing it for an analysis crash.
  * cmd/terrain/cmd_analyze.go — gate evaluated last so the report
    is always rendered before the exit decision.

Tests:
  * TestParseSeverityGate covers canonical / case-insensitive /
    whitespace / invalid inputs
  * TestSeverityGateBlocked covers the threshold cascade
    (critical-only, critical+high, critical+high+medium) and
    confirms gateNone never blocks

Manual smoke (against the Terrain repo itself):
  $ terrain analyze --fail-on critical   → exit 0 (no critical)
  $ terrain analyze --fail-on medium     → exit 6 (matched
    "0 critical + 493 high + 169 medium finding(s)")
  $ terrain analyze --fail-on bogus      → exit 2, usage error

The remaining gate flag from the plan
(--new-findings-only --baseline) is a 0.2.x follow-up; it needs
the suppressions/finding-IDs work to ship clean against renamed
files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 2, 2026

Terrain AI Risk Review

Metric Value
AI surfaces 13
Eval scenarios 16
Impacted scenarios 0
Uncovered surfaces 13

Decision: PASS — AI surfaces are covered.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 2, 2026

[INFO] Terrain — Informational only

Insufficient data to assess change risk confidently.

Metric Value
Changed files 5 (4 source · 1 test)
Impacted units 4
Protection gaps 4
Tests selected 1 of 773 (0% of suite)

Coverage gaps in changed code

  • cmd/terrain/cmd_analyze.go [LOW] — cmd_analyze.go has no observed test coverage.
    → Add unit tests for cmd_analyze.go.
  • cmd/terrain/cmd_pipeline_helpers.go [LOW] — cmd_pipeline_helpers.go has no observed test coverage.
    → Add unit tests for cmd_pipeline_helpers.go.
  • cmd/terrain/cmd_severity_gate.go [LOW] — cmd_severity_gate.go has no observed test coverage.
    → Add unit tests for cmd_severity_gate.go.
  • cmd/terrain/main.go [LOW] — main.go has no observed test coverage.
    → Add unit tests for main.go.
4 pre-existing issues on changed files
  • cmd/terrain/cmd_analyze.go [HIGH] — [blastRadiusHotspot] Changes to this file propagate to 167 tests (167 direct, 0 indirect). High blast radius increases regression risk.
  • cmd/terrain/cmd_pipeline_helpers.go [HIGH] — [blastRadiusHotspot] Changes to this file propagate to 167 tests (167 direct, 0 indirect). High blast radius increases regression risk.
  • cmd/terrain/cmd_severity_gate.go [HIGH] — [blastRadiusHotspot] Changes to this file propagate to 167 tests (167 direct, 0 indirect). High blast radius increases regression risk.
  • cmd/terrain/main.go [HIGH] — [blastRadiusHotspot] Changes to this file propagate to 167 tests (167 direct, 0 indirect). High blast radius increases regression risk.

Recommended tests

1 test(s) with exact coverage of 0 impacted unit(s). 4 impacted unit(s) have no covering tests in the selected set.

Test Confidence Why
cmd/terrain/cmd_severity_gate_test.go exact test file directly changed

Limitations
  • No coverage artifacts provided; protection gaps reflect missing data, not measured absence. Provide --coverage to improve accuracy.
  • Mixed test cultures reduce cross-framework optimization confidence. Consider standardizing on fewer frameworks.

Generated by Terrain · terrain pr --json for machine-readable output

Targeted Test Results

Terrain selected 1 test(s) instead of the full suite.

  • Go tests: passed

The launch-readiness review flagged six concrete gaps in PR #134
that block defensible merge. This commit closes all six and fixes
one real bug uncovered while writing the e2e tests.

Bug fix: the JSON output branch (and SARIF / annotation / HTML
branches) early-returned from runAnalyze before reaching the
--fail-on gate check. So `terrain analyze --json --fail-on=medium`
silently exited 0 even with matching findings — the gate worked in
text mode only. The TestRunAnalyze_JSONStdoutPurity test caught
this: it expected a gate-blocked error but got nil.

The fix factors the gate decision into a closure (gateErr) computed
before any rendering branch, then each branch returns gateErr()
after its renderer completes. Text, JSON, SARIF, annotation, and
HTML output all gate uniformly now.

Review gaps closed:

  Pluralization
    cmd_severity_gate.go: replaced "finding(s)" with proper plural
    via plural() helper, mirroring the 0.2 polish work in
    internal/reporting/plural.go.

  Negative-timeout validation
    cmd/terrain/main.go: --timeout < 0 now exits with usage error
    (code 2) and a clear message, rather than the silent
    immediate-DeadlineExceeded that read like an analysis crash.

  Stale "0.1.2 contract" comments
    cmd/terrain/main.go: exit-code conventions block rewritten as
    pre-0.1.2 / additive 4+ semantics; "0.2 will move policy
    violations" deferred phrasing replaced with concrete
    0.2.x → 0.3 milestone reference.

  E2E test for exit code 6 + report-renders-before-exit invariant
    TestRunAnalyze_GateBlocksOnFixture: runs runAnalyze against
    the calibration corpus with --fail-on=medium, asserts:
      1. Returns errSeverityGateBlocked (so main.go maps to exit 6)
      2. Error message contains the --fail-on label + counts
      3. stdout is non-empty (report rendered before gate fired)
      4. stdout contains the report header

  JSON stdout purity test
    TestRunAnalyze_JSONStdoutPurity: with --json + --fail-on
    matching, the entire stdout body parses as JSON. Verifies the
    gate message goes to the error channel (stderr via main.go),
    not into the JSON document. This is the test that uncovered
    the early-return bug above.

  Gate-passes inverse test
    TestRunAnalyze_GatePassesWhenSeverityAbsent: --fail-on=critical
    against a fixture whose worst severity is below critical
    returns nil. Locks in the false-positive-prevention property.

All three new e2e tests run via captureRun (existing helper at
cli_smoke_test.go:154) — function-level invocation, not exec
subprocess, so they're fast and don't shell out.

Verification:
  go build ./...               clean
  go test ./...                green
  go test ./internal/testdata/ green
  make docs-verify             green

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant