← Design · c121

plan/00-model.md

00 · Model (Wave 1)

TL;DR: the shared world learns the v1 rules: reasons on loosening and revoking, mode + Until instead of snooze, one shared draft behind local staging, per-endpoint fleet, JSON of a version, findings that carry their config version and say whether a person or a re-evaluation moved them. No surface changes beyond keeping today's pages compiling.

Owns: app/data/governance-fixture.ts, app/composables/useGovernanceMock.ts, app/components/governance/mock/Bar.vue, app/data/governance-finding-rows.ts. Imports the Impact type from app/data/governance-impact.ts (01 writes it; if it isn't there yet, write the interface from README §4 into that file verbatim and tell 01).

Today

  • Exception (fixture l.169–185) has revokedAt only; applyChange 'exception-revoke' l.968–971.
  • Snooze: change kinds policy-snooze / snooze-clear; applicability turns Enforce→Observe while snoozed (l.786–787).
  • previewChange() l.1506–1548 returns counts (reachedBefore/After, resolves, reopens, enforces, conflicted, openInScope).
  • Publishing state in useGovernanceMock.ts l.84–129: edits, ops, versions, pointer, drafts (D-6, D-7), log, fleet: Record<version,count> (FLEET=100), requireApproval; verbs submit/approve/discardDraft/promote/rollback/setRequireApproval/propagate (l.257–293). stageEdit l.160–170 keeps one line per setting.
  • Per-finding policyVersion ("policy v{n}").

Steps

  1. Reasons (D1). MODE_RANK already exists. Add loosens(change, world) (mode goes down for anyone in scope, incl. exception add with a lower mode, exception revoke that lowers someone, policy mode down). stageEdit stores { reason, loosens }; a loosening edit without a non-empty reason is refused (return { ok:false, why:'reason' } so the UI can show it). Revoke: Exception.revokedBy, revokeNote (required, D1), set in applyChange.
  2. Mode + Until (D6). Policy.until?, Policy.revertTo?; mode-set takes until? and records revertTo = the mode it replaced. Clock passing until reverts and logs a system history line ("Until passed · back to Enforce"). Remove policy-snooze / snooze-clear and the snooze branch in applicability; migrate the P-NO-ROOT seed snooze to mode: observe, until: 2026-11-15, revertTo: enforce. Grep the components for snooze and list every hit in your report (02/04 remove them; don't edit their files).
  3. Impact (README §4). Rewrite previewChange to return Impact (people with from→to, teams with affected/of, outOfVisibility = people whose resulting mode is Off and wasn't, tightened/loosened per policy+scope, finding ids). Add previewDraft(edits) = cumulative impact of a list of changes applied in order. Keep a counts(impact) helper so today's callers (policy-detail.vue l.103–109, ModeControl.vue l.78–99, ResolveCapture.vue l.29–31) can be shimmed without editing them — export the old shape as previewChangeCounts and point the old name at it only if needed to keep them compiling; note it for removal in Wave 2.
  4. Publishing (D4, D5, D2). In the composable: keep edits (local staging) and stage/unstage; replace drafts[] with one draft: { base, changes: DraftChange[] } (DraftChange = edit + author + at), saveToDraft() moves local edits into it (same-setting line: last value wins, author updated — keep commit 06b083095's rule), discardDraft(), publish(note) = new version n+1 from pointer + all draft changes, pointer moves, log line; rollback(v, note) moves the pointer, the draft stays and is re-based (mark changes that now equal live as redundant: true). Drop submit/approve/promote/setRequireApproval/requireApproval/draftStanding and the ?draft= preview knob → replace with ?preview=draft (replay draft on every page). Seed: v12 → v13 live as today; the shared draft holds Riley's P-TRANSPORT Observe and Dana's P-ACCT Critical (the old D-6/D-7 content) so the page isn't empty. Labels: export configLabel(v) = 'Policy Config v' + v and pendingLabel = 'v13 · N pending changes'.
  5. Fleet (D11, D12). Replace the count map with fleet: Endpoint[] — ~100 endpoints generated deterministically from the team subjects (names like mac-eng-07, reuse the ids already in Rollout.reach.pending l.207–210 and in useTeamMock), each { id, name, team, owner, os, activeVersion, lastSeen, status } with status ∈ current | behind | offline | failed. Seed: 97 on v13, 3 behind on v12 (mac-eng-07, win-fin-02, mac-b1 — one offline, one failed, one behind). propagate() advances non-offline/non-failed ones. Derive per-policy reach (configured→confirmed) from it: an endpoint confirms a policy's mode when its activeVersion contains that mode. Export fleetSummary(v) → { on, of, behind, offline, failed }.
  6. JSON (D13). configJson(v | 'draft') → stable, pretty JSON: { policyConfig: { version, publishedAt, publishedBy, note }, policies: [{ id, mode, until?, severity, config }], exceptions: [{ id, policy, scope, mode, until?, reason, by, at }] }. importConfig(json) → parse, validate (unknown policy ids, bad modes, exception scope that doesn't exist, loosening without reason), stage the differences as local edits, return { ok, errors, staged }.
  7. Findings. Each finding carries configVersion at detection (map from today's policyVersion via the version chain) and history events carry kind: 'manual' | 're-evaluation' | 'system'. governance-finding-rows.ts: outcome label Observed (not "Not enforced"), keep Enforced.
  8. Bar.vue knobs. Remove the approval toggle and ?draft= links; keep ?as=you|dana; add ?preview=draft. Leave a placeholder comment for 07's ?stage=.

Done when

  • npx nuxi typecheck (or the dev server's overlay) shows no new errors in owned files; every lab page still renders (headless sweep of /pages/phase4/mock/{policy-detail,findings,finding-detail,changes,policies,compliance,organization/teams,organization/teams/detail?team=eng}), screenshot each to shots/00-*.png.
  • Report: the list of snooze hits, previewChange callers you shimmed, anything you had to decide.

Look for (review)

/pages/phase4/mock/changes shows one draft with Riley's and Dana's lines; publishing makes v14; the P-NO-ROOT row reads Observe (until 11-15).

Qpoint Brand Style Guide