Source: c79 finding-lifecycle — state-machine anatomy · 2026-08-25
The contract page. c78's SLA report assumes a resolution workflow the product doesn't have; this page rules it: status is a fold over an append-only event log, and SLA data is a pure function — f(finding events, rubric, now). Mark M's five verbs plus two system verbs, one clock formula, and a worked example computed live from the same code the interactive pages run (app/data/lifecycle-fixtures.ts).
States
Four statuses, derived, never stored — plus acknowledged, a flag on open, not a state (no effect on SLA math; purely evidentiary).
Verbs
Mark M's five (2026-08-20 call) plus two system verbs. Acting on a finding = appending one event; nothing is ever edited or deleted.
Escalate and enforce are events, not states — the finding stays open and the clock keeps running. Routing work elsewhere never stops SLA exposure. The seams stay one line and one stamped ref; ticketing, on-call routing, and remediation tracking are the customer's existing tools, not ours.
The clock
dueTs = detectTs + targetDays + pausedDays
- Starts at detection — not at acknowledgment. Exposure begins when the condition exists, not when someone notices.
- Exception pauses it — pause credit accrues from the exception to whichever comes first: its expiry, a re-judgment, or resolution.
- Expiry re-opens mechanically — no stored event; once now passes expiresTs the fold re-opens the finding with its remaining time preserved.
- Reclassify re-baselines — the due date recomputes from the original detect timestamp with the new severity's target (plus any pause credit). Severity changes never erase exposure history.
- Advisory severities have no due date — days: null in the rubric (Mark M's low). They age informationally and can never breach.
- Rollups count found / resolved / excepted — accepted risk never counts as resolved. Within-SLA % judges only findings with a hard target that concluded or are overdue; excepted findings are excluded while the clock is paused.
The persisted audit record
One append-only type carries the whole workflow. Every derived field on every c79 page is a fold over rows of this shape.
interface FindingEvent {
id: string // E-####, monotonic — append-only, never edited
findingId: string
ts: string // ISO
verb: 'detect' | 'acknowledge' | 'ignore' | 'exception'
| 'escalate' | 'enforce' | 'resolve' | 'reopen' | 'reclassify'
actor: string // 'system' for detect / resolve
reason?: string // MANDATORY: ignore · exception · reopen(human) · reclassify
expiresTs?: string // exception only
severity?: Severity // reclassify only — the new value
ref?: string // seam stamp: 'tracker:SEC-214' | 'policy:P8'
}Worked examples
Rendered from a live deriveFinding() call against the fixture event log — the on-page proof that these rulings and the code cannot drift.
Composition notes
- Resolve is deliberately not one of the five verbs — dispositions are human judgment; resolution is evidence (a re-scan observes the condition cleared). The mock's other pages give it a distinct "simulate: fix detected" affordance. Product-semantics claim, flagged for Mark M's confirmation.
- Ignore ≠ exception is the crispest line in the machine: "not a real finding" (no expiry, exits the denominator) vs "real risk, formally accepted until a date" (stays on the report with its why-line). Auditors care which bucket an open item lands in.
- Enforce doesn't close the finding — policy prevents recurrence; the instance still resolves on evidence. The defensible alternative (enforce ⇒ auto-exception) is recorded as an open thread.
- Rubric is org config — Mark M's real program: critical 14d · high 30d · medium 90d · low advisory. c78's 7/30/90/180 placeholder is superseded here; its module stays untouched while c78 awaits picks.
- Rulings live in three always-agreeing places: this page, doc comments in lifecycle-fixtures.ts, and c79 CYCLE.md Decisions.