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) hasrevokedAtonly;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.tsl.84–129:edits,ops,versions,pointer,drafts(D-6, D-7),log,fleet: Record<version,count>(FLEET=100),requireApproval; verbssubmit/approve/discardDraft/promote/rollback/setRequireApproval/propagate(l.257–293).stageEditl.160–170 keeps one line per setting. - Per-finding
policyVersion("policy v{n}").
Steps
- Reasons (D1).
MODE_RANKalready exists. Addloosens(change, world)(mode goes down for anyone in scope, incl. exception add with a lower mode, exception revoke that lowers someone, policy mode down).stageEditstores{ 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 inapplyChange. - Mode + Until (D6).
Policy.until?,Policy.revertTo?;mode-settakesuntil?and recordsrevertTo= the mode it replaced. Clock passinguntilreverts and logs a system history line ("Until passed · back to Enforce"). Removepolicy-snooze/snooze-clearand the snooze branch in applicability; migrate the P-NO-ROOT seed snooze tomode: observe, until: 2026-11-15, revertTo: enforce. Grep the components forsnoozeand list every hit in your report (02/04 remove them; don't edit their files). - Impact (README §4). Rewrite
previewChangeto returnImpact(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). AddpreviewDraft(edits)= cumulative impact of a list of changes applied in order. Keep acounts(impact)helper so today's callers (policy-detail.vuel.103–109,ModeControl.vuel.78–99,ResolveCapture.vuel.29–31) can be shimmed without editing them — export the old shape aspreviewChangeCountsand point the old name at it only if needed to keep them compiling; note it for removal in Wave 2. - Publishing (D4, D5, D2). In the composable: keep
edits(local staging) andstage/unstage; replacedrafts[]with onedraft: { 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 asredundant: true). Dropsubmit/approve/promote/setRequireApproval/requireApproval/draftStandingand 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: exportconfigLabel(v) = 'Policy Config v' + vandpendingLabel = 'v13 · N pending changes'. - Fleet (D11, D12). Replace the count map with
fleet: Endpoint[]— ~100 endpoints generated deterministically from the team subjects (names likemac-eng-07, reuse the ids already inRollout.reach.pendingl.207–210 and inuseTeamMock), 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 itsactiveVersioncontains that mode. ExportfleetSummary(v)→{ on, of, behind, offline, failed }. - 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 }. - Findings. Each finding carries
configVersionat detection (map from today'spolicyVersionvia the version chain) and history events carrykind: 'manual' | 're-evaluation' | 'system'.governance-finding-rows.ts: outcome label Observed (not "Not enforced"), keep Enforced. - 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 toshots/00-*.png.- Report: the list of
snoozehits,previewChangecallers 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).