Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
429 changes: 429 additions & 0 deletions internal/handoff/CRITIQUE-02.md

Large diffs are not rendered by default.

921 changes: 921 additions & 0 deletions internal/handoff/claim.go

Large diffs are not rendered by default.

687 changes: 687 additions & 0 deletions internal/handoff/critique02_regression_test.go

Large diffs are not rendered by default.

1,992 changes: 1,992 additions & 0 deletions internal/handoff/handoff_test.go

Large diffs are not rendered by default.

441 changes: 441 additions & 0 deletions internal/handoff/reaper.go

Large diffs are not rendered by default.

711 changes: 711 additions & 0 deletions internal/handoff/state_machine.go

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions internal/record/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ missing key and an unsealed half must not be the same observation.
### 1.3 `anvil/dastStatus` — audit-level DAST outcome (rulings G3 + G6, found twice)

`not_run | skipped_no_manifest | running | completed_clean | completed_findings | completed_partial |
completed_failed |
target_boot_failed | target_unreachable | timed_out`

**Producer:** the scan controller, **derived from** the DAST half's `anvil/status` and from
Expand Down Expand Up @@ -473,3 +474,37 @@ S6 additions it predates — `anvil/state`, `anvil/version`, `anvil/deadline`, `
is the schema working**, not a defect; the packet's original "validates the annotated example with zero
errors" criterion was written before §6's rulings and cannot be satisfied simultaneously with S6's
"all of these are required."


---

## Amendment 2026-08-07 — `anvil/dastStatus` gains `completed_failed`

The frozen enum had **no image for "the DAST half itself broke"**. A half with `anvil/status = failed`
against a target whose provenance is `booted_clean` had nowhere legal to land, and `R.6` was folding it
onto `completed_partial` — flagging the compromise rather than absorbing it silently.

That fold is wrong for the same reason S6 requires a failed target to be distinguishable from one
scanned clean: a half that **crashed** differs from one that **covered part of the surface**. Collapsing
them makes `dast_coverage` uninterpretable, because a 40% figure could mean "we probed 40% and stopped"
or "we probed 40% and the engine died". `DeriveDastStatus` is now total — every
(provenance, half-status) pair has exactly one image.

**This vocabulary lives in five places and all five must move together:**

| # | Location | What it is |
|---|---|---|
| 1 | `plan/IMPLEMENTATION-PLAN.md` §6 | the ruling |
| 2 | `internal/record/contract.go` | the Go constants and `DastStatusValues()` |
| 3 | `internal/store/schema.sql` | `ck_audit_record_dast_status` |
| 4 | `schemas/anvil-record-v1.schema.json` | the published wire schema |
| 5 | this file | the contract other areas are pointed at |

**The amendment initially landed in only 1 and 2, and the tree went red.** `R.4`'s
`TestEnumCheckConstraintsMatchContractLiteralForLiteral` caught it immediately by comparing the SQL
CHECK against the Go enum literal-for-literal — the guard working exactly as intended. The operational
consequence had it shipped was worse than the fold it replaced: an audit whose DAST half crashed could
not be persisted **at all**, because the derivation produced a literal the store rejected.

Recorded because the lesson generalises: **one vocabulary with five definitions is the same defect §6
was written to close**, and an amendment is exactly when it recurs.
233 changes: 233 additions & 0 deletions internal/record/SECRETS.md

Large diffs are not rendered by default.

38 changes: 33 additions & 5 deletions internal/record/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,14 +319,24 @@ func ValidateHalfStatus(v string) error {
// by two critics. Area 40 declared seven values and area D declared five with
// ZERO literal overlap — D could not have written a single row into 40's NOT
// NULL column. The frozen set is the union of both plus D's `partial`
// (renamed `completed_partial`), which is nine values. D.26 emits these.
// (renamed `completed_partial`), which was nine values. D.26 emits these.
//
// AMENDED — a TENTH value, `completed_failed`, was added to
// plan/IMPLEMENTATION-PLAN.md §6 after the R.10 critic (CRITIQUE-02 F8/rule 8)
// showed the nine-value set had no image for "the DAST half itself broke".
// DeriveDastStatus was mapping a HalfStatusFailed half against a target that
// booted cleanly onto `completed_partial`, which is the same category error
// plan/00-SPINE.md S6 forbids one level down: a half that CRASHED is not a
// half that COVERED PART of the surface, and collapsing them makes
// `dast_coverage` uninterpretable — the reader cannot tell a 31-of-50 scan
// from a scan that died at endpoint 1. See DastStatusCompletedFailed.
//
// This value is DERIVED from the DAST half's HalfStatus and from
// TargetProvenance (the boot/reachability outcome), never from
// TargetProvisioning (which provisioning path was used).
type DastStatus string

// The nine legal anvil/dastStatus literals.
// The ten legal anvil/dastStatus literals.
//
// WHY DastStatusSkippedNoManifest IS DISTINCT FROM DastStatusNotRun — do not
// merge them:
Expand Down Expand Up @@ -363,6 +373,23 @@ const (
// coverage detail lives in DastCoverage, which is what makes this value
// interpretable rather than merely worrying.
DastStatusCompletedPartial DastStatus = "completed_partial"
// DastStatusCompletedFailed: the target booted cleanly and the DAST half
// then FAILED mid-scan — the scanner crashed, lost its connection, or was
// killed. Derived from HalfStatusFailed against
// TargetProvenanceBootedClean, and from nothing else: a half that never
// had a target reports target_boot_failed, target_unreachable or
// skipped_no_manifest, all of which outrank this value.
//
// It is DISTINCT from completed_partial on purpose. Both mean "less than
// the whole surface was probed", but only completed_partial means the
// coverage numbers in DastCoverage describe a scan that ran to its own
// conclusion. Reporting a crash as completed_partial invites a consumer to
// read "31 of 50 endpoints" as a deliberate scope, when in fact the run
// died and the denominator is meaningless.
//
// Like every value except completed_clean, MeansDynamicallyScannedClean is
// false for it.
DastStatusCompletedFailed DastStatus = "completed_failed"
// DastStatusTargetBootFailed: the target never booted, so nothing was
// scanned. Derived from TargetProvenanceBootFailed or
// TargetProvenanceBuildFailed.
Expand All @@ -379,12 +406,13 @@ func DastStatusValues() []DastStatus {
return []DastStatus{
DastStatusNotRun, DastStatusSkippedNoManifest, DastStatusRunning,
DastStatusCompletedClean, DastStatusCompletedFindings,
DastStatusCompletedPartial, DastStatusTargetBootFailed,
DastStatusTargetUnreachable, DastStatusTimedOut,
DastStatusCompletedPartial, DastStatusCompletedFailed,
DastStatusTargetBootFailed, DastStatusTargetUnreachable,
DastStatusTimedOut,
}
}

// Valid reports whether s is one of the nine legal anvil/dastStatus literals.
// Valid reports whether s is one of the ten legal anvil/dastStatus literals.
func (s DastStatus) Valid() bool { return inEnum(s, DastStatusValues()) }

// ValidateDastStatus reports whether v is a legal anvil/dastStatus literal.
Expand Down
9 changes: 8 additions & 1 deletion internal/record/contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,16 @@ var frozenEnums = map[string][]string{
"anvil/status": {
"running", "sealed", "failed", "timed_out", "skipped",
},
// TEN values since the section 6 amendment: `completed_failed` was added
// between completed_partial and target_boot_failed because the nine-value
// set had no image for "the DAST half itself broke", and DeriveDastStatus
// was folding that case into completed_partial -- which makes dast_coverage
// uninterpretable for the same reason S6 requires a failed target to be
// distinguishable from one scanned clean.
"anvil/dastStatus": {
"not_run", "skipped_no_manifest", "running", "completed_clean", "completed_findings",
"completed_partial", "target_boot_failed", "target_unreachable", "timed_out",
"completed_partial", "completed_failed", "target_boot_failed", "target_unreachable",
"timed_out",
},
"anvil/target.provenance": {
"booted_clean", "boot_failed", "build_failed", "no_target_declared",
Expand Down
Loading
Loading