Finding Lifecycle
Mock the finding-lifecycle state machine and the mechanics/UI of turning findings into SLA data — the design answer to c78's open resolution-workflow gap.
Why
The c78 SLA Insights report (/pages/report-sla-v1) carries a recorded dependency: the finding resolution workflow — acknowledge / ignore / exception / escalate / enforce with a persisted audit record — isn't in the product; the report assumes it in fixture form. Mark M (Mark Peterson, external CISO advisor) named those verbs and the org-defined SLA rubric on the 2026-08-20 product review call; his own program's rubric is critical 14d / high 30d / medium 90d / low soft. SLA data is a pure function — f(finding events, rubric, now) — and until the lifecycle that emits those events is designed, the SLA report is a surface with no floor under it.
Deliberate non-goals, from the kick-off prompt: do not build what customers already have — ticketing, on-call routing, remediation project tracking. Those are integration seams (Mark M's own stack: #wiz_alerts-style Slack channels, Jira), rendered as stubs.
Objective
Understand — by building — the simple mechanics/UI of turning findings into SLA data: the state machine ruled and drawn, a disposition panel exercising all five verbs, the rubric as org config, and an interactive centerpiece where acting on a finding appends to its audit trail and recomputes the SLA rollups live.
Scope
- New fixture module
app/data/lifecycle-fixtures.ts— event-sourced findings (append-onlyFindingEventlog), Mark M's real rubric, purederiveFinding/deriveWorld. Imports fromreport-fixtures.ts; never modifies it (c78 is unclosed pending Mark's picks). - Local organs
app/components/lifecycle/—DispositionPanel,AuditTrail. - Four pages in
app/pages/pages/, registered in the existingreportsgroup:findings-lifecycle-v1(static anatomy: states, verb map, clock rules, worked example),finding-disposition-v1(the panel across scenarios),sla-rubric-config-v1(editable targets + live consequence panel),findings-to-sla-v1(interactive centerpiece with simulated clock). - Out of scope: ticketing/on-call/remediation tooling beyond one-line seam stubs; reconciling
report-sla-v1onto the derived world; cross-report entity resolution.
Key Changes
- Stage 1 — fixtures (2026-08-25):
app/data/lifecycle-fixtures.ts— the rulings as code.FindingEvent(append-only, 9 verbs),RubricTargetwithdays: number|null(Mark M's 14/30/90/soft), 19 findings as event logs (7 live c78 identities + 12 historical so quarters derive), purederiveFinding(the state-machine fold: pause credit, mechanical expiry re-open, one due formula) andderiveWorld(open/breaches/excepted/ignored, withinSlaPct with documented denominator, event-derived found/resolved/excepted quarter rows, audit feed). Sanity-checked via tsx: 90% within-SLA (10 judged), F-0951 pause exemplar (79d paused → 16d clock, within 90), F-0918 late, +30d → F-1041 breaches and verdict drops to 82%, +60d → F-1022's lapsed exception mechanically re-opens.report-fixtures.tsuntouched. - Stage 2 — anatomy (2026-08-25):
/pages/findings-lifecycle-v1+app/components/lifecycle/AuditTrail.vue(the persisted audit record rendered — verb-toned rows, reused by every later page), registered in thereportsgroup. The contract page: state diagram (open + three terminals, return paths incl. mechanical expiry), 9-verb table with clock effects and seam markers, the one clock formula with six numbered rules,FindingEventin a code block, and two worked examples computed live fromderiveFinding()(F-1027 register case; F-0951 pause exemplar) — the on-page proof rulings and code agree. Smoke-checked 200, derived values (paused 79d / clock 16d / within SLA) confirmed in SSR output, clean log. - Stage 3 — disposition organ (2026-08-25):
app/components/lifecycle/DispositionPanel.vue+/pages/finding-disposition-v1, registered. The panel owns no state machine — renders aDerivedFinding, emitsact; the page appends and re-derives. Six scenarios on the house tab-picker (fresh / acknowledged / past-SLA / near-expiry / ignored / escalated), each with its own live event log and reset. Verb availability by status; ignore/reopen gate on reason, exception on reason + expiry (+30d default); escalate/enforce are one line + one stamped ref ("would create tracker:SEC-… — finding stays open here · stub"); resolve is the visually-apart green "simulate: fix detected" (evidence, not judgment). Smoke-checked 200 (needed the nuxt.config touch — new page files added under a running server, per the known gotcha). - Stage 4 — rubric config (2026-08-25):
/pages/sla-rubric-config-v1, registered. Editable per-severity targets seeded with Mark M's real program; "hard target" checkbox makes advisory (days: null) a first-class value; three program presets (Mark M 14/30/90/adv, superseded c78 placeholder, the 48h-criticals program from the call) demonstrate that the same findings get three different judgments. Live consequence panel re-judges the open world as you type (deriveWorldtakes the rubric as an argument — the page is the function). Clock policy rendered as fixed rulings with a link to the anatomy page — deliberately not knobs. New open thread recorded: rubric changes after a quarterly export rewrite submitted history → rubric versioning is a product need. Smoke-checked 200, default verdict (90% · 10 judged) in SSR output. - Stage 5 — centerpiece (2026-08-25):
/pages/findings-to-sla-v1, registered — the c78 open-thread answer. Two columns: worklist (open nearest-due-first, accepted-risk, recently closed) with rows expanding into the shared DispositionPanel; SLA rail (within-SLA verdict numeral, open/breach/excepted counts, event-derived found/resolved/excepted quarter table, live audit feed). Reset + advance-clock chips (+1d/+7d/+30d); rubric fixed to Mark M's program (the knob lives on the config page). Verified headlessly with Playwright: escalate stampstracker:SEC-1041into the feed, exception pauses F-1041's clock, +60d drops the verdict 90%→82% and mechanically re-opens F-1022's lapsed exception with the queue line "re-entered queue mechanically", reset restores 90%, zero console/page errors. All four c79 pages smoke-checked 200 together. - Thread — findings-regroup (2026-08-25): revision of qdash
/findingsat condition grain, from Mark's 200-disallowed-tool-calls question (prompt/plan infindings-regroup-*.md). Additiveoccurverb inlifecycle-fixtures.ts(count/lastSeen fold; occur-after-terminal = mechanical reopen withrecurredflag; c79 base world regression-checked unchanged at 90% · 10 judged). Newfindings-page-fixtures.ts: qdash control catalog subset with per-control occurrence nouns (the fix for qdash c5's hardcoded-units thread), 9 conditions as event logs including the ×200 case (199 generated tool-call occurs + reclassify medium→high on magnitude) and a resolved egress condition primed for recurrence. New/pages/findings-regroup-v1(registry groupsecurity): control sections → condition rows (severity · scope · ×N unit + lastSeen · agents · SLA clock) → expanded DispositionPanel + evidence list + occur-free audit record + "simulate: new occurrence". Playwright-verified: exception moves stats 5→4 open / 1→2 excepted; simulated occurrence on the resolved condition re-opens it at 35d/30d — instantly past SLA (open 4→5, past target 0→1, verdict 100%→0%); reset restores. All six related pages (c79 four + regroup + report-sla) smoke-checked 200. - Thread — findings-regroup v2, qdash-chassis rebuild (2026-08-25):
/pages/findings-regroup-v2, registered — the condition-grain revision rebuilt 1:1 on the real/findingscomponents, all already in the layer:UxTableList+Sorter/ColumnHeader(working Policy-column search + group sorts),UxTableListExpandRowpurple-ruled expansion,SecurityPolicyRibbon(hot/warm/tepid),ComplianceTag,AgentAvatar,_shadow-boxstat cards, filter-pill bar,_darken-meopen-row brightening. Grain changes quoted against the original: members are conditions (scope + ×N unit + SLA clock + status) not signal rows; "Manage" → "Disposition" (the c79 verb panel + evidence + audit inset); group sub-line "N conditions · N open · N past SLA" replaces "N violations"; qdash's empty info stat card becomes past SLA; expansion meta gains Nearest SLA. Same interactive world as v1 (shared fixtures, simulated clock). Playwright-verified through the real ExpandRow: expand → disposition → exception drops the high card 2→1, +60d puts 2 conditions past SLA, sorter works, reset restores, zero console errors. (Playwright gotcha logged:getByRolename-matches substrings by default — "Disposition" matched the "Dispositioned" filter pill; useexact: true.) - Thread — findings-regroup: Conditions column + two-agent case (2026-08-25): from Mark's "two unapproved tool uses by different agents" question. New fixture C-2010 (Codex · ravi, ×14 tool calls, own detect/clock) — same rule as C-2001, different agent = a second condition under one control; both avatars on the group row, separate clocks and dispositions in the expansion. v2 gains a sortable Conditions column (big-numeral, "N open · N past SLA" breakdown beneath, paired beside Total Occurrences — sorting by conditions ranks workload, by occurrences ranks noise); the old "N violations" sub-line removed so the count lives in one place (policy cell now shows the control category). Ruled in notes: scope choice stays per-rule — fleet-level remediation (qdash's egress) would legitimately collapse agents into one condition. Playwright-verified: approval row reads 2 / 214 tool calls with both avatars, Conditions sort puts it first, both members (×200 high, ×14 medium) expand with independent clocks; zero console errors.
- Thread — findings-regroup: occurrences leave the summary; findings become a table (2026-08-25): Mark's hierarchy call — occurrences are a sub-item of a finding, so the Total Occurrences column left the summary table (a 200+14 sum across agents is semantically weak; the control-level noise sort is an accepted loss). Rollup moved to the expansion's Findings header ("· ×214 tool calls"); the ×N stays on each finding row. Then the findings section converted from flex rows to
UxTable variant="band"— columns Severity · Finding (scope + reason/title sub-line) · Occurrences · Last Seen · Agent · SLA · Adjudicate, with the adjudication panel opening as a full-width colspan row. Verified in browser: band header renders, ×200 row reads columnar, colspan inset opens, acknowledge stamps the audit; zero console errors. - Thread — findings-regroup: two-section expansion + terminology (2026-08-25): v2's group expansion split into two stacked
UxTableListExpandSectioncards via ExpandRowbare(the inventory-agent-expand-v2 pattern, well tint between): section 1 = the policy (ribbon, name, remediation, meta incl. Nearest SLA; close-X wired), section 2 = the findings (its own "Findings : N · open · past SLA" header + the worklist) — context and work stop sharing one card. Terminology ruling applied (see Decisions): "condition" → finding across both regroup pages' UI copy, column header now "Findings", ladder stated as Control → Finding → Occurrence. Both pages 200, sections + disposition verified in browser, zero console errors. - Thread — SecuritySeverityTag (2026-08-25): new layer component
security/SeverityTag.vue— severity pill on the ComplianceTag chassis, no icon. Rules the layer's severity-ladder split (SecuritySeverity had high on warning; qdash findings had high on error): critical = the only solid error fill (rank by intensity, not a fifth hue) · high = error outline · medium = warning outline · low = muted grey (ComplianceTag'sgapregister).inkprop = the open-row_darken-meregister (content-token border/text + 10% content fill — theme-safe, flips in dark mode), driven from ExpandRow's@detailsOpen/@detailsClose. Documented per house rule: registry entry +_demos/SecuritySeverityTag.vue(full adminProps, ladder both sizes, slot override, overflow, ink-on-tint, in-context finding row). Swapped into findings-regroup-v2 (group rows with ink-on-open, inner table, expansion meta) and the shared DispositionPanel. Dev server now runsNUXT_LOCAL_LAYER=1(moved to port 3000); layer release needed before CI/prod consumes the component. Ink flip verified in browser: error/60 border → content ink on open → reverts on close. - Thread — finding-detail (2026-08-25):
/pages/finding-detail-v1(+ registry,?id=picks the finding, default the ×200 case) — the "view all" drill and demoted-flat-view home. Identity header (SeverityTag + displayed-status chip + remediation clock + qdash-italic meta), adjudication panel, the full occurrence stream as a house table (Time · citable ID · Agent · Action chip · Detail(stretch) · Session) with text filter and simulate button, occur-free audit record. Fixture support (additive):actionon occur events (observed/held/blocked),displayStatusladder inderiveFinding— c79 base world regression-clean (90% · 10). Worklist evidence headers now read "last 5 of 200 · view all →" linking through. Verified in browser: chip flips acknowledged → risk accepted on adjudication, deep link lands with the right id, 200 rows render, zero console errors. - Thread — adjudication card, Mark's design (2026-08-25): built from Mark's manual mock. Layer:
UxTableListExpandSectiongainstheme="grape"— the CHILD register (grape-50 fill, grape-150 border, no shadow; dark grape-850/30) for a card that belongs to a row of the section above; registry entry updated. v2: each finding row in the inner findings table is now itself aUxTableListExpandRow— whole-row click AND the Adjudicate button (handler-less, bubbles to the row) open the grape adjudication card as the row's own expansion. Card per the design: severity pill + 22px title · meta band (Agent avatar / Machine·User from new seed fields / Remediation Clock as a progress bar — content fill on grape-150 track, error fill on breach) · Ignore + Accept Risk as the only verb buttons (axis 1, capture-gated — the accept-risk label applied) · Evidence and Audit Record as two bold-header columns with 3px rules · handling as the Status dropdown (UxSimpleSelect: Acknowledged/Escalated/Enforced — axis 2 as a status control, exactly the axis alignment ruling; selecting appends the clock-neutral event with its ref). DispositionPanel no longer used on v2 (remains the organ on the other three pages — convergence is an open thread). Verified: row click toggles, button opens, select/capture don't close the row, audit updates live, zero console errors. - Thread — quiet expand register (2026-08-25): Mark's ruling: the row and its parent row stay white; only the grape card is tinted. Evaluated (CSS override spike vs prop vs style-props vs new component) and ruled:
quietboolean prop onUxTableListExpandRow— open row and details well stay surface, no purple rules/tint/bordered well/flash; everything else unchanged; default false so existing consumers untouched.quiet+ExpandSection theme="grape"together form the child-register vocabulary: quiet row + grape card = "this content belongs to the row above." Applied and then refined per Mark: the OUTER control rows keep the loud register (qdash's own open treatment — purple rules + grape tint); only the nested finding rows are quiet, with the grape card as their tinted surface. CSS spike reverted; registry updated. Measured: outer open row grape/10 tint with its 2 purple rules, inner open row rgb(255,255,255), card exactly grape-50 (#F9F7FF). - Thread — status-column bake-off (2026-08-26):
/pages/findings-status-column-v1, registered — from Mark's ask for a clearly visible (possibly dropdown) status in the findings worklist. Four treatments of the same world, each with its own event-log clone: A static chip · B dropdown-everywhere (the honest failure on display — half the menu permanently grey, since adjudication values need reasons and resolved is the system's) · C hybrid (handling dropdown while open, chip when terminal — recommended) · C2 hybrid quiet-until-hover. All four share the slimmed pure-clock SLA column (11d/30d · due 09-04/paused · exp/in 2d/—) so the status word lives in one column and the treatment is the only variable. Unacknowledged wears warning tone throughout. The axis rule decides selectability: clock-neutral handling verbs are one-click safe; adjudication stays behind the card. Verified: 4 live tables, isolated worlds, C's select acts, C2's quiet border transparent, zero console errors. Verdict (Mark, 2026-08-26): C — status as a dropdown. Graduated into findings-regroup-v2: 8-column inner table (Status between Agent and SLA — handling dropdown while open with click.stop so it doesn't toggle the expand row, terminal chip otherwise), SLA slimmed to pure clock (11d/30d · due 09-04/paused · exp/in 2d/—). Verified in browser: row-level escalate via the dropdown without expanding the row. - Thread — StretchBox measurement bug (2026-08-26): Mark spotted the top expand row animating open ~an inch short, then snapping after a beat. Root cause found by instrumented measurement:
UxStretchBoxmeasures acloneNodeappended todocument.body(absolute, off-screen), where the clone lays out at body width (1440px) instead of the well's real width (1310px) — text wraps differently, so the clone measured 405px vs the real 464px; the box animates to the short height (300ms) and sits clipped until theheight:initialoverride lands (~1200ms) and snaps. Only the first row showed it because only the approval card's content is width-sensitive (wrapping remediation line + full meta cluster). Layer fix: whenstretchWidthis false (width externally determined — the ExpandRow case), the clone is pinned to the live content width before measuring; width-stretching consumers unchanged. Verified: mid-animation height now exactly matches content (464/464), no snap. - Thread — topline bake-off (2026-08-26):
/pages/findings-topline-v1, registered — from Mark's read that the severity 4-up misses the opportunity (his seeds: handling-state counts, clocks running low). Analysis ranked the topline's job as triage: unseen (unack + oldest age — the adjudication-lag signal's only home) · about-to-breach (early warning beats breach count, per c78's card audit) · already late · severity mix (context) · throughput + within-SLA % (report tie) · expiring exceptions · worst control. Eight live-derived treatments: extended cards · handling funnel · remediation runway (findings as ticks on a 45d axis — time as position) · two-axis grid · verdict + worst lines (c78 idiom) · Your move (three action cards each naming their top item; severity demoted to a footer line) · scoreboard (throughput debt + worst control) · compact strip. Provisional verdict: 6 (Your move) — every element an action with a name; 8 as the minimal alternative; 3's runway the idea worth stealing into any winner. Verified: 8 sections, 6 runway ticks, your-move names the oldest unack (2d · Codex · ravi), zero console errors. Mark's pick pending. - Thread — topline bake-off, iteration 2 (2026-08-26):
/pages/findings-topline-v2— Mark's critique of v1: anemic, wants meaning at a glance, punchy and clear. Iteration rule applied: every element is a claim — labels are sentences, top items are named, consequences stated ("returns to the queue Sep 15"), and zeros speak ("Nothing is past SLA" beats a mute 0). Six treatments (briefing lines · your-move-spoken cards · annotated runway with named tick list · claim cards · the paragraph · worded strip) + a today/+30d stress toggle — a topline must communicate on both kinds of day (stressed world crosses sofia's deadline and surfaces marcus's lapsed exception automatically). Within-SLA % dropped from the topline: at 1-judged it's a dishonest hero number; it belongs on the report. v1's explainer variants (funnel, grid) dropped — they taught the model without triaging the day. Provisional read: 2 (your-move-spoken); 1 if vertical space is cheap; 3's named runway list under either. Verified in both worlds, zero console errors. - Thread — AgentPill + finding-first column (2026-08-26): from Mark's design pair (inline and stacked pill mocks). New layer component
agent/Pill.vue(AgentPill): pixel-pawn face in its own bordered cell + name / user in grape;stackedfor the two-line layout; art via<img>per the pawn rule. Documented (registry + full demo). In findings-regroup-v2: the Finding column drops the redundantagent: Claude Code · sofiascope line — the finding title leads in the primary style, reason as the sub-line — and the Agent column's text chip becomes the AgentPill. Identity now lives in exactly one column, as a face. Verified: pill renders "Claude Code / sofia" with the pawn loaded, no scope prefixes remain, zero console errors. - Thread — policy header rework (2026-08-26, Mark's design): section 1 of the group expansion rebuilt: identity left (ribbon · 24px name · remediation as plain grey sub-text), Findings severity box (big toned numerals in a bordered cell strip, only non-zero severities render), and Upcoming Remediation Deadlines (soonest three open findings: bold day + month · "N days from now" toned by nearness — error ≤7d or overdue, warning ≤30d — · "view" linking to the finding's detail page). The old meta cluster (Policy: label, first/last seen, nearest-SLA, framework tag stack) removed — frameworks live on the collapsed row, recency on the finding rows. Verified calm (1 High 1 Med · 18/87 days out) and stressed (+30d flips to "days overdue" in error ink), zero console errors.
- Thread — worklist polish sweep (2026-08-26): batch of Mark-directed refinements on findings-regroup-v2. AgentPill hardening: tighter padding + dot separator; then two real clipping bugs fixed in the layer —
shrink-0(overflow-hidden zeroes a flex item's automatic min-width) andw-max(inline-flex width:auto is shrink-to-fit, so starved table cells compressed the pill) — a chip never compresses. Column mechanics ruled: in table auto-layout, no-slack + inline pills + wrap-on-squeeze can't all hold; chose no-slack + inline viaw-max flex-nowrapcontainers with the Policy column as thewidth:100%stretcher and 3-visible + "+N" caps on agents and frameworks (approval seeded with 5 frameworks to prove the cap). AgentPill adopted in the outer Agents column too (person when one finding, ×N when several). Findings column now the severity-box pattern at row scale (toned per-severity numerals, divider cells, past-SLA fragment only when present), placed right of Policy + Conditions after trying left; clean groups render a green 0 · Open — zeros follow the pattern. Deadline "view" opens the finding's row below via the ExpandRow open() ref pattern instead of navigating away. Adjudicate ↔ Close label flips CSS-driven off_open-row, like the chevron. Fonts: font-black was silently clamping to 700 (Google Fonts URL carried only 400–700); fixed, then Inter moved to its official home (rsms.me/inter/inter.css) — all weights + variable font; severity-box numerals font-black at both scales, Inter not mono. Every change browser-verified; gotchas saved to memory. - Thread — the qdash port (2026-08-27): findings-regroup-v2 shipped into the live product — qcontrol's qdash
/findingsrewritten in place onfeat/SLA-findings(qdash cycle c14.finding-lifecycle-port). Layer released as v0.9.11 to carry AgentPill, SecuritySeverityTag, and the grape/quiet ExpandSection registers; all five consumer lockfiles refreshed. The two-axis model derives from the backend's single workflow row (lib/findings-status.ts, id-and-label-tolerant); SLA is the Mark M rubric client-side (lib/findings-sla.ts) until the backend writessla_due; the adjudication card became the first frontend consumer of/api/finding-decision//api/finding-state(Ignore rides finding-state as aresolved+ignoresentinel — no registry id yet). Honest degradations, each wearing a ProjMark: Evidence = first/last seen only, Audit = latest state only (no occurrence event log server-side). Divergence accepted for now: the backend's block decisions Resolve, so enforce renders terminal — c79's clock-neutral enforcement is a recorded backend follow-up. Simulated clock, composition notes, and fixtures stayed here; port verified on real seed data (screenshots inpublic/cycles/c79/c79-qdash-port-*.png), 37 lib tests, published-layer build clean. Full plan:qcontrol-port-plan.md.
Outcome
(pending)
Decisions
- States are derived, never stored — status is a pure fold over the ordered event log:
open/resolved/accepted-risk/ignored, plus anacknowledgedflag on open. - Acknowledge does nothing to SLA math — purely evidentiary (who saw it, when).
- Ignore ≠ exception — ignore = "not a real finding" (no expiry, exits SLA denominator, audit persists); exception = "real risk, formally accepted" (mandatory expiry, clock paused, stays in the "not resolved — here's why" bucket).
- Escalate and enforce are events, not states — the finding stays open and the clock keeps running; routing work elsewhere never stops SLA exposure. Seams stay one inline line + one
ref-stamped event. - Resolve is a system verb, not one of Mark M's five — dispositions are human judgment, resolution is evidence (a re-scan clears the condition). (product-semantics claim — surface for Mark's confirmation)
- One clock formula —
dueTs = detectTs + targetDays + pausedDays; covers exception pause, expiry re-open, and reclassify re-baseline. - Quarter rollups show found / resolved / excepted — accepted-risk never counts as resolved.
- Simulated clock, never
Date.now()— deterministic ages, SSR-safe; advance-clock chips give back what a live clock would show. - Rubric: low severity has no hard target (
days: null, renders "advisory") — per Mark M's actual program; c78's 7/30/90/180 placeholder noted as superseded, but its module left untouched. - Three grains (findings-regroup thread): control = section/context · condition = the remediable thing (qdash's
finding_key = v2:{control}:{scope_kind}:{scope_id}is already the fingerprint) — owns status, SLA clock, reasons · occurrence = evidence (count, lastSeen, detail), never its own clock. - Occurrences never reset the clock — first occurrence = detect (the exposure anchor); repeats increment count/lastSeen.
- Occurrence after resolution = mechanical reopen (recurrence) — and it re-opens against the original anchor, so a fix that didn't hold can land instantly past SLA. That's the honest reading.
- Magnitude informs severity, not multiplicity — ×200 is a reason to reclassify, never 200 rows.
- qdash decision vocabulary maps onto the verbs — investigate/assign_owner → acknowledge · accept_risk → exception · open_ticket → escalate · block_* → enforce · approve_destination → control edit (not a finding verb).
snoozeddeliberately has no successor: snooze is an exception without a reason or expiry. - Door verb ruled: Adjudicate (2026-08-25, Mark's pick over Decide/Judge/Mark/Manage). Two registers: adjudicate is the act — the clearance-world and vuln-tooling term of art for a human rendering a recorded judgment; disposition stays the stored outcome (the auditor's word); resolve stays reserved for evidence — no human closes a finding by fiat. Filter tabs: Open / Closed / All — accepted-risk stays under Open (still on the report's "tell me why" list); only resolved/ignored are honestly Closed. Rejected: Manage (the incumbent mystery-meat door), Decide (fine, but adjudicate is unambiguous and domain-precedented), Judge/Rule/Mark/Classify/Triage (collisions or scope errors, see thread).
- Two-axis model; one displayed status (2026-08-25). Axis 1 = risk state (open / accepted-risk / resolved / ignored) — the only thing SLA math, tabs, clocks, and reports read. Axis 2 = handling (unacknowledged → acknowledged → escalated/enforced) — a derived read-out of attention progress, zero SLA effect; nothing is tracked, correlated, or automated on it (the underlying events and refs ARE the tracked record). Display: one status word by precedence — risk states decisive; handling shows only while open, latest routing wins. Derived (
displayStatusin the fold), never stored. The fence exists because qdash's singleworkflow.statusenum (open|triaged|accepted|resolved|snoozed) mixes the axes — the pathology this split fixes. - Verb clusters align to the axes (2026-08-25). Handling: acknowledge · escalate ↗ · enforce ↗ (clock-neutral, at most a stamped ref). Adjudication: ignore · accept risk · reopen (reason mandatory, changes risk state). Evidence: resolve (system only). The mandatory-reason rule and the axis boundary are the same line. Panel's "Adjudication :" label is currently over-broad — the two-cluster relabel is a pending revision.
- Verb rename ruled: "accept risk" replaces "exception" as the act — aligns verb→state (accepted-risk), qdash's
accept_riskdecision id, and auditor vocabulary; "create exception" rejected (artifact-centric, and misreadable as a policy carve-out, which is a control edit — a different door). "Exception" survives as the artifact noun (the exceptions register, "expires 10-01"). Full rename pending. - Resolution paths ruled (2026-08-25). Humans never set resolved. Three evidence shapes: re-scannable state (config/install re-check), policy-settled (rule stops matching after enforce/approve), and the gap — attestation for non-re-scannable conditions.
attest resolvedparked as an open thread: human claim + MANDATORY evidence artifact, rendered distinct from system-observed resolution in the audit record. - Clock naming ruled: it's the remediation clock (detection → resolution; Mark M's "remediated 14 days"). "Adjudication lag" (detect → first human verb, the MTTA analog) is a separate derived metric, free from the event log — open thread, no second hard target in the mock.
- Evidence vs audit record ruled (2026-08-25). Same event log, two lenses: Evidence = the occurrence stream (detect/occur — what the world did; truncation-honest: "last 5 of 200 · view all →"); Audit record = everything except occur (what humans did — complete on its face, occur-excluded so 200 system rows can't drown five human judgments).
- Occurrences get no standalone pages. Three layers instead: peek (envelope in place), anchor (deep link to the session moment / native surface), citable id (stable handle for the audit packet — qdash's
/api/security-packet). The full stream lives on the finding's detail page (/findings/:idin product terms) — the demoted flat view's true home. Future unification: raw occurrences promotable into curated EvidenceItems (the compliance report's EV-#### shape). - Occurrence envelope (partially mocked): per-occurrence attribution (agent instance), runtime action (observed/held/blocked), session anchor, signal specifics, capture provenance. The worklist inset shows ts+detail; the detail page shows the columns.
- Terminology ruled: Control → Finding → Occurrence (2026-08-25).
- "Occurrences" stays as the column header (2026-08-26) — it's the ruled third grain's name and the header is where the UI teaches it; "Total" rejected (ambiguous beside the Findings numerals, and it resurrects qdash's instances-as-the-countable framing). Recorded alternative if shorter is ever wanted: the per-control unit noun as a dynamic header (Tool Calls / Connections / Opens). "Condition" was thread scaffolding; the clocked, dispositioned unit is what the SLA report, Mark M's requirement, and the F-#### ids already call a finding. The term moves up a grain — qdash's current per-signal rows get the honest name, occurrences. "Issue" is the Wiz-style fallback if the qdash port needs to disambiguate mid-migration. UI copy renamed on both regroup pages; fixture identifiers (
ConditionSeed,conditionSeeds,conditionEvents) keep the scaffolding name until the port settles the vocabulary.
Standing Considerations
- Alert ink uses
rose-*, neverred-*(layer's flatredclobbers the scale — c78 finding). - Derived quarters are a smaller honest world than c78's authored counts — anyone comparing the two SLA surfaces will see different numbers until reconciliation (open thread).
Open Threads
- Pending panel revision (approved direction, not yet built): two-cluster verb row (Handling : / Adjudication :), full accept-risk verb rename (
FindingVerb, fixtures, anatomy table, audit badges), spent-verbs-as-receipts (acknowledged · kai · Aug 6 instead of a greyed button), Status column on the worklist's inner findings table. attest resolved— human attestation with mandatory evidence artifact for non-re-scannable conditions; renders distinct from system-observed resolution. The third cluster's human entry point.- Adjudication lag as a derived metric (detect → first human verb); possible triage-SLA config later (Vanta-style), deliberately not now.
- Occurrence → EvidenceItem promotion for the security packet; session deep-links should anchor to the specific turn (c77 dependency).
- Per-rule scope spec — each control declares its fingerprint scope (incl. compound scopes; the sens file-vs-(file,agent) wrinkle) next to its rule definition.
- Reconcile
report-sla-v1onto the derived world + Mark M rubric once c78 closes. - Enforce semantics: does policy enforcement auto-exception the instance? (Current ruling: no — policy prevents recurrence, the instance still resolves on evidence.)
- Ignore-vs-exception bucket line needs Mark M's confirmation — auditors care where "tell me why" items land.
- Layer extraction of
lifecycle/organs alongside thereport/organs (c78 thread). - Rubric versioning — changing targets after a quarterly export silently rewrites submitted history; targets become a contract at first submission.
Port the condition-grain findings table back to qdash— done 2026-08-27 (see the qdash-port thread above; qdash c14). Still open from the recorded requirements: deep-link auto-expand (blocked on the layer's ExpandRowisOpenshadowing) and the flat per-occurrence view as the finding's drill (needs the occurrence event log server-side).
Artifacts
- initial-prompt.md
- thread: findings-regroup — prompt + plan
- thread: qcontrol-port — prompt + plan
- initial-prompt.md
- plan.md