finding-shape.md
c147 Phase 0: the finding's shape, the common tongue
- 0. The rule
- 1. The finding (immutable)
- 2. The event (append-only)
- 3. Event kinds, by writer
- 4. Resolutions
- 5. Identity: the fingerprint
- 6. Up and down
- 6a. What the plane says to the person besides events (Phase 4)
- 7. Two readers, one record
- 8. c137's message types, mapped
- 9. c133, re-homed
- 10. Calls for review stop 1
Written: 2026-11-13. Reader: Mark (review stop 1); the qnative executor in Phase 5 (types::findings and its drift test read this file); c133 (§9). Status: a proposal in the wire shape, written from the lab's model (app/data/governance-fixture.ts: Finding, FindingEvent, EventKind, Resolution, FindingState) with what this cycle adds marked new. Nothing is built in qnative and nothing changes on the plane but R4 (Q4, Q6).
0. The rule
One object, two directions. A finding is minted where the activity happened, on the endpoint, and never changes. Everything after is an event appended by one of three writers: the endpoint (qcontrol on this machine), the person at the keyboard, or the plane (an operator, a policy, a re-evaluation). No side edits another's event. Status is derived by folding the events, as the lab already does.

1. The finding (immutable)
Finding {
id: FindingId // minted on the endpoint from the fingerprint (§5)
policy_id: PolicyId
config_version: u32 // the Policy Config version live at detection (c121)
outcome: "enforced" | "observed" // c121 D16: stamped at detection, never changes
session_id: SessionId
subject: { user, team, device, agent, account? }
resource: { kind: "mcpServer" | "account" | "provider" | "path" | "dataClass", value }
observed_at: Timestamp
severity_at_generation: "critical" | "high" | "medium" | "low" | "informational"
evidence: string // one line, the sensor's words ("Read of ~/.ssh/id_rsa denied at 10:12; the agent stopped")
}
The lab's deprecated policyVersion is left out. Nothing here is mutable; a later severity override or exception lives in the plane's Changes and reaches the finding as a reevaluate event.
2. The event (append-only)
FindingEvent {
id: EventId // minted by the writer; replays are idempotent by id
finding_id: FindingId
at: Timestamp
from: "endpoint" | "person" | "plane" // new: which side wrote it
actor: string // "qcontrol on nina-mbp", the person's name, "Dana (security admin)", "qdash"
kind: EventKind // §3
origin?: "manual" | "re-evaluation" | "system" // the lab's: who moved it (c121)
to_status: "open" | "resolved" // the status after this event; an event that moves nothing repeats the current one
resolution?: Resolution // §4, when to_status is resolved
reason_code?: "false-positive" | "by-design" | "security-testing" | "other" // a manual resolve (B2)
reason?: string
cause_id?: ChangeId // the plane's Change behind a re-evaluation
payload?: object // per kind (§3): the training, the follow-up's words, the note to IT
}
from is new and says where the event was written; the lab's origin keeps its meaning (a person, a re-evaluation or the system). The two are independent: a prevented event is from: endpoint, origin: system; a resolve is from: plane, origin: manual.
3. Event kinds, by writer
| Kind | From | Moves status | Resolution | In the lab | Payload, what it is |
|---|---|---|---|---|---|
detect | endpoint | → open | ✓ | the finding is born | |
prevented | endpoint | → resolved | prevented | new (R4, Q4) | written in the same moment as detect when the outcome is enforced: the activity was blocked, so the finding is born resolved |
recurred | endpoint | no | new (§5) | { count, last_at }: the same policy, resource and person again; one finding, not a thousand | |
asked-it | person | no | new | { note, contact }: the person's question with the finding attached; in local mode the contact is qcontrol's it_contact (c4) | |
asked-access | person | no | new | { note }: a request for an exception; the plane answers with reevaluate → allowed if granted | |
acknowledged | person | no | new | answers a follow-up that asks for it | |
reported-mistake | person | no | new (review stop 2, call 7) | { note }: the person says the finding is wrong; the plane resolves it with the reason False positive, or doesn't | |
training-completed | person | no (c133 D9) | ✓ | c133's training.completed record (§9) | |
training-assigned | plane | no | new | `{ training_id, training_version, title, minutes, assigned_by: "policy" | |
follow-up | plane | no | new | { words, sender_label, author, requires_ack }: HQ's words, shown as sent (c137 D6); replaces the censure and the enforcement notice from the plane | |
resolve | plane | → resolved | manual · training-completed | ✓ | with a reason (B2); or, by the policy itself (origin system) when the person completes the training it assigned (review stop 3, 6a), "Resolved: training completed"; HQ can reopen |
reevaluate | plane | → resolved | now-enforced · policy-changed · allowed | ✓ | a Change made the finding stop applying (c121 D2, D16); allowed is how an exception granted on request lands |
reopen | plane | → open | ✓ | ||
ignore, revoke-ignore, ignore-expired, enforce-confirmed | c114 world only | not in the shape: the Oct 19 model removed Ignore on a finding (B3) and c121 D16 replaced "resolve as Enforced" |
The status rule: only the endpoint (prevented) and the plane move a finding's status. The person's events are proof and conversation, never a verdict on their own finding, which is c133 D9 generalised. The Gene story still closes: the person finishes the training, the record arrives, and the plane resolves the finding (an operator, or a policy rule that resolves on completion; review stop 3).
4. Resolutions
Phase 4 (2026-11-13): prevented is in the prototype's model. foldFinding() in governance-fixture.ts writes the prevented event right after detect for every Enforced finding, so S1, S2, S3, Reports and the lab's qdash copy all read it (review stop 2, call 2; c121 D3 superseded).
prevented (new) · training-completed (new, review stop 3, 6a) · now-enforced · policy-changed · allowed · manual. The lab's ignore and enforced stay in the c114 world only. "Resolved: prevented" is the operator's label; the person's words are Phase 3's.
5. Identity: the fingerprint
- The id is minted on the endpoint:
F-+ a hash ofpolicy_id · resource.kind · resource.value · subject.user · subject.device, plus the window below. The same activity seen twice by the same person on the same machine is one finding. qdash'sfinding_key(control · scope kind · scope id) has no person in it because qdash projects findings across the fleet; this one belongs to a person on a machine. - The window. Observed: one finding while it is open; a repeat appends
recurred; a repeat after it resolved mints a new finding. Prevented: born resolved, so the window is the day; repeats that day appendrecurred. An agent blocked a thousand times in an afternoon is one prevented finding withcount: 1000. - Event ids are minted by their writer (
E-+ ULID), so a queue replayed after a dropped connection writes nothing twice.
6. Up and down
| Goes | When | Rule | |
|---|---|---|---|
| Up (R5) | every finding with its endpoint and person events | when connected; queued in order while offline | the endpoint never writes from: plane |
| Down (R3) | the plane's events on this endpoint's findings | on sync | the plane never writes from: endpoint or from: person |
| Local mode | nothing leaves; asked-it opens the IT contact | always | the app raises findings itself from the sensor's events, with a port of the lab's detector against the local configuration's policies (review stop 3, 6c); qcontrol stays as it is |
Because nothing is edited, the two sides never disagree about what happened, only about what has arrived. In the native app the nozzle's three sources answer the same shape: synthetic from the worlds (Phase 1, derived by the lab's detector), plane from the lab's world (the prototype is the plane, Q6), local from the app's own detector over the sensor's events (review stop 3, 6c; Phase 5).
6a. What the plane says to the person besides events (Phase 4)
/api/plane-settings:{ noise_default, set_for }, the noise position the company chose for the person's team (review stop 2, call 6); the person's own choice wins over it, Quietly when there is no plane.why_plainon each policy (in/api/policies): why the policy covers this, in the person's words; HQ's ownwhyPlainwins, then Qpoint's default (WHY_PLAINingovernance-fixture.ts), then the practitioner'swhy(call 5).
In the lab these come from app/data/plane-c147.ts (the plane's moves, matched to a world's derived findings by policy and resource, and the per-team noise defaults), merged by scripts/derive-world-findings.mts.
7. Two readers, one record
| Field or value | The operator reads (the lab, today) | The person reads |
|---|---|---|
outcome: enforced | Enforced | Phase 3 (c137's draft: Stopped in time) |
outcome: observed | Observed | Phase 3 (c137's draft: Noticed) |
resolution: prevented | Resolved: prevented | Phase 3: nothing to fix |
follow-up | the message sent, its acknowledgement | from the sender label, as HQ wrote it |
| S1's "Why it violates" | as is | Phase 3: why your company's policy covers this |
The record is the same; only the words differ by reader.
8. c137's message types, mapped
| c137 type | Becomes |
|---|---|
enforcement-notice (plane and local) | the finding itself (detect, plus prevented when blocked); the OS notification announces it at the person's noise level |
censure | a follow-up event when it is about a finding; otherwise shelved (HQ's email and Slack) |
training-assignment | training-assigned |
training-completed | training-completed (unchanged) |
acknowledgement | acknowledged |
contact-it | asked-it |
identity-confirmation | Status: standalone or connected (f8d58bc9) |
sensor-state | Status and the tray icon (c131) |
hq-notice, responsibility, diagnostics-request, diagnostics-sent, reply, connect-request, grant-notice | shelved with c137; HQ's own channels, the plane's Endpoints and audit log, c136's surface |
Nine of c137's fifteen types fold into the finding or into Status; the rest were never about a finding.
9. c133, re-homed
c133's event-shape.md assumed c137's messages. Read against this shape:
- §1 step 1: the assignment is a
training-assignedevent on the finding (from: plane), not atraining-assignmentmessage; its audit row replacesmessage.sent. - §1 step 3: completion is the
training-completedevent, as before. The two steps that ended the assignment message and answered its thread in the operator's inbox fall away: the operator reads the record on the finding (S1's history, the Corrections block) and in Reports. - §2:
message_id: MessageIdbecomesassignment_event_id: EventId, thetraining-assignedevent it answers. D8's "in the assignment's thread" reads as "on the finding". - Nothing else in c133 changes. Its file is not rewritten; c133's record points here.
10. Calls for review stop 1
Ruled 2026-11-13 (Mark, "Take the defaults", review-1.md): calls 1–4 as their defaults below; call 5 stays open until Peterson.
fromas the writer's field, beside the lab'sorigin. Default: yes.- The person never moves status (c133 D9 generalised). Default: yes; the plane resolves.
- The window: open-while-open for Observed, the day for prevented, repeats as
recurred. Default: as written. Phase 1's data point: in the pilot world the day window gives Priya 42 prevented findings in 90 days (Cursor on 19 days, Codex CLI on 23). The alternative, one prevented finding per policy and resource while its mode holds, would give two, each with a count; the person's view groups them either way (S2's grouping). preventedas both an event kind and a resolution. Default: yes; the operator's list filters on the resolution, the history shows the event.- A policy rule that resolves on training completion (the Gene story closing without an operator). Default: open until review stop 3, with Peterson.