plan/README.md
c121 · plan: v1 scope in the phase-4 lab
The decisions from the week of Oct 19 go into the lab: /pages/phase4/mock/*, which is c114's composed lab with c118's L1–L9 on top. Every surface reads one shared world: useGovernanceMock() (app/composables/useGovernanceMock.ts) → act() → applyChange() (app/data/governance-fixture.ts).
- Sources:
../sources/and../intentsatce9ce2b. Prompt:../initial-prompt.md. - Lab map, 2026-10-23: the current state of every item, with line numbers, is summarised in each micro-plan's "Today" block.
1. The changes, sorted
Source key: L Logseq 10-21 · R Rob's summary · S Mark Peterson's Slack · I intents PR #4. A ✓ means the source asks for it; ≠ marks a disagreement, settled in §2.
| # | Change | L | R | S | I | Lab today | Plan |
|---|---|---|---|---|---|---|---|
| 1 | Exception changes mode only (Off/Observe/Enforce); advanced knobs later | ✓ | ✓ | ✓ | the model has advanced allows for later | 00 | |
| 2 | A reason is required when an edit loosens | ✓ | ≠ nudge | ≠ nudge | optional, stored as — | 00, 02 | |
| 3 | Expiry is optional; encourage it when loosening; indefinite is fine | ✓ | ✓ | ✓ | required when loosening (W.6) | 02 | |
| 4 | Revoke records a reason | ✓ require | ✓ note | ✓ note | nothing captured | 00, 02 | |
| 5 | Blast radius, one visual component: who it affects, which teams, who drops out of visibility, tightened (blue) vs loosened (red), findings that resolve or reopen; used on exceptions and drafts | ✓ | ✓ | ✓ | counts only, in three places; none on Changes | 01, 02, 03 | |
| 6 | Snooze goes; policy mode gets an optional Until, the same control the exception editor uses | ✓ | ✓ | ✓ | Snooze in PolicyMode; three separate "until" UIs | 00, 01, 02 | |
| 7 | Remove "Who does this apply to?" (the per-person cascade simulator was cut) | ✓ | ✓ | ✓ | PolicyExceptions l.58–77 | 02 | |
| 8 | Policy header: frameworks · org severity + Qpoint recommendation · open findings | ✓ | ✓ | there; the label says "Recommended" | 02 | ||
| 9 | One shared draft; publish is all of it ("publish up to here"); no selective publish, no hotfix path; no approval in v1 | ✓ | ✓ | ✓ | named drafts D-6/D-7, approve/promote, approval setting | 00, 03 | |
| 10 | Name the whole footprint Policy Config; versions v13 (an integer) | ≠ v13.6 | ✓ | ✓ | "v13" with no name | 03 | |
| 11 | View / download the config as syntax-coloured JSON; import into the draft | ✓ | ✓ | ✓ +import | none | 00, 03 | |
| 12 | Propagation footprint: target version vs each endpoint's active version; stale/offline/failed never shown as updated | ✓ | ✓ | ✓ | fleet is counts only; no Endpoints page | 00, 06 | |
| 13 | Findings: Snoozed line out; Not enforced → Observed; severity + framework filters; fewer filters; Observed hidden by default with a zero state | ✓ | ✓ | ✓ | tagged (W.1); no severity filter | 04 | |
| 14 | Resolve records a decision: reasons + text; no inline exception; an optional follow-on link to the policy/exception | ✓ | ✓ | ✓ | the capture can add an Off exception inline | 04 | |
| 15 | Manual resolution kept distinct from re-evaluation transitions; config version at detection | ✓ | told apart only by actor; "policy v{n}" | 00, 04 | |||
| 16 | Teams listing: proportional Enforced / Observed / Off bar, exceptions, open findings: "how unique is this team", options drawn from the catalog | ✓ | ✓ | ✓ | text "N on · M enforcing" | 05 | |
| 17 | Team detail: policies, effective mode, team exceptions, members who differ, findings per policy; pending vs live | ✓ | ✓ | mostly there (TeamPolicies) | 05 | ||
| 18 | Adoption lifecycle in demos and docs: Observe org-wide with Enforce pilots → Enforce org-wide with Observe/Off exceptions | ✓ demo | ✓ | no stage knob | 07 | ||
| 19 | Equal-specificity tie (two teams) | open Q | D9 stricter wins | 08 (feedback) | |||
| — | Pricing, custom policies, Monitor propagation view, suppressing repeated blocks, frameworks next | ✓ | ✓ | future | — | out of scope |
2. Decisions and defaults
Ruled by Mark, 2026-10-23 (these become CYCLE.md D1–D4):
- D1 · Reasons are required when loosening and when revoking.
- Save is disabled until there's a reason. This applies to every loosening edit, whether to a policy's mode or to an exception.
- Tightening keeps the reason optional.
- This goes further than the intent ("strongly prompt … without blocking") and Rob's "nudge hard (red)". → feedback list.
- D2 · Versions are integers.
- Published: "Policy Config v13".
- The shared draft: "v13 · 6 pending changes". Publishing makes it v14.
- The "v13.6" in the note means the 6 pending changes on top of v13; it is not a version scheme.
- D3 · Observed findings are hidden by default.
- An "Observed (N)" filter is one click away.
- The zero state names what's hidden.
- Once included, rows keep W.1's tag. This supersedes W.1's default.
- D4 · Local staging stays, in front of one shared draft.
- Edits stage locally, as the tray does today.
- "Save to draft" writes them into the one shared Policy Config draft: no title, author per change, itemized log.
- Publish takes a note and makes the whole draft live.
- There are no named drafts, no approve step and no promote-a-draft.
- The intent says edits save straight into the shared draft. → feedback list.
Defaults (stated here so the executors don't guess; correct any of them at review):
- D5 · Multi-draft is parked in git, not behind a knob.
- The lab shows only the single shared draft.
- Commits
0beb2f879(L8) and02bbfe261(L8b) are the record of the multi-draft exploration. Rob calls it "parked, not dead". - The "Require approval to promote" setting goes too. Rob: "may become a per-org setting", later.
- D6 · A policy's Until reverts to the mode it replaced. It reads "Observe until Nov 15, then Enforce". The same holds for exceptions: when one lapses, the next applicable exception or the default applies (the intent).
- D7 · Expiry stays empty by default. When an edit loosens, a red-family hint under Until reads "No end date: this stays open until someone revokes it". It never blocks the save.
- D8 · Blast-radius colours: tightened = blue, loosened = red (rose-* scale; per the gotcha, not the flat
red). Neutral counts stay ink. - D9 · "Out of visibility" means people for whom the change sets the policy to Off. Off generates no findings, so those people stop producing evidence or notifications. Observe keeps them visible.
- D10 · Resolve's follow-on is a link: "Change this policy or add an exception →". It opens S3
#exceptionswith the editor prefilled through?add=user:<id>|team:<id>. Resolving never stages an edit. - D11 · Per-policy rollout (c114 D3, configured → confirmed) is derived from version propagation.
- An endpoint confirms a policy's mode when it runs a Policy Config version that contains that mode.
- There is one fleet model: per endpoint, the active version, last seen, and status (current · behind · offline · failed).
- D12 · Propagation footprint, where. It goes in:
- Endpoints listing: a column, plus the green bar from the note.
- Endpoint detail: active vs target, last sync, status, and an "Check MDM" pointer when stale.
- Team detail: the Endpoints ledger column.
- Changes: the version row (exists).
- Tray footer: exists.
- Policy detail: the mode note, derived per D11.
The Monitor-section view stays future (I, R). Users detail stays blank. - D13 · Import validates a pasted or uploaded JSON into the staged edits, with the errors listed. Export is the same file as Download. A support export strips
tokens(the mock has none; say so). - D14 · Adoption stage is a lab knob,
?stage=adoption|established(defaultestablished, today's world). The "user docs" land as a section on/pages/phase4/governance-model. The qdash demo world (c113, demo.qpoint.io) is a follow-up outside the lab. - D15 · Equal-specificity tie: the lab keeps D9 (stricter wins). It goes on the feedback list as our answer to the intent's open question.
3. Linear or parallel?
Both. The lab is one world, so its two global files can't be shared by concurrent writers:
| Contended file | Readers |
|---|---|
app/data/governance-fixture.ts | every page |
app/composables/useGovernanceMock.ts | every page |
policy-detail.vue, PolicyExceptions.vue | items 1–8, 12 |
Everything else splits cleanly by surface. So:
Wave 1 (2 in parallel) 00 model ─────────────┐
01 shared components ─┤ ← contract: §4 below
▼
review gate (me): types compile, /parts renders, nothing on the surfaces broke
▼
Wave 2 (5 in parallel) 02 policy detail · 03 publishing · 04 findings · 05 teams · 06 endpoints
▼
review gate per package: preview → screenshots → revisions → commit per package
▼
Wave 3 (serial) 07 adoption stage (fixture again) → 08 records, captures, deploy
- Why Wave 1 can be two:
- 00 owns the fixture and the composable.
- 01 owns two new components and the type file they share (
app/data/governance-impact.ts), which 00 imports. - The contract in §4 is all they share.
- Why Wave 2 can be five:
- Each package owns a disjoint set of files (table in §5).
- Cross-package links are URLs, fixed in §4.
- None of them writes the fixture or the composable. If one needs a model change, it stops and reports it, and I make the change between waves.
- Why Wave 3 is serial: 07 rewrites fixture seeds, which every page reads, and 08 records what landed.
Execution rules for every executor:
- Edit only your owned files.
- Don't commit. I review and commit per package with
git commit -- <paths>. The tree carries other sessions' uncommitted work: c105 cost centre, c117, c100. - Don't start a dev server.
- Use the live
:3000and verify with headless Playwright (reponode_modules, channel chrome). The browser pane refuses:3000when another session owns it. - For a stale transform, make a real one-line touch of
nuxt.config.ts, and only if the page won't pick up your edit.
- Use the live
- Report back: files touched, the URLs to look at, screenshots in
cycles/c121.governance-v1-scope/shots/, and what you couldn't do.
4. Contracts between packages
Impact (01 defines it in app/data/governance-impact.ts; 00's previewChange returns it):
type Direction = 'tightens' | 'loosens' | 'mixed' | 'neutral'
interface Impact {
direction: Direction
people: { id: string; name: string; from: Mode; to: Mode; via: string }[] // who it affects
teams: { id: string; name: string; affected: number; of: number }[]
outOfVisibility: { id: string; name: string }[] // D9
tightened: { policyId: string; label: string; scope: string }[] // blue
loosened: { policyId: string; label: string; scope: string }[] // red
findings: { resolve: string[]; reopen: string[]; enforce: string[] } // finding ids
openInScope: number
}
// previewChange(world, change) → Impact ; previewDraft(edits[]) → Impact (union)
Edit reason (00): every staged edit carries { reason?: string; loosens: boolean }, and stageEdit refuses a loosening edit with no reason (D1). An exception revoke carries { revokedBy, revokedAt, revokeNote }.
Mode + Until (00 model, 01 control):
Policy.until?: stringandPolicy.revertTo?: Mode(D6);policy-snooze/snooze-clearare removed.- Change kind
mode-setgainsuntil?.
Publishing API (00, in useGovernanceMock):
stage(edit) · unstage(i) · saveToDraft() // local → shared draft (D4)
draft: { base: number; changes: DraftChange[] } // DraftChange = edit + author + at
publish(note) → v(n+1) · rollback(v, note) · discardDraft()
configJson(v | 'draft') → string · importConfig(json) → { ok, errors[], staged: number }
fleet: Endpoint[] // { id, name, team, os, activeVersion, lastSeen, status }
targetVersion = pointer
URLs:
- S3
?add=user:<id>|team:<id>|orgopens the exception editor prefilled (02 reads it; 04 links to it). - S2
?outcome=enforced|observed|all(defaultenforced) and?sev=critical,high. 04 owns these; 05 links withoutcome=all. - Endpoints:
/pages/phase4/mock/organization/endpointsand…/endpoints/detail?endpoint=<id>, owned by 06. Linked from the team ledger and the tray footer. - Changes
#json,#import(03).
5. Packages and file ownership
| Plan | Wave | Owns |
|---|---|---|
| 00-model | 1 | app/data/governance-fixture.ts, app/composables/useGovernanceMock.ts, app/components/governance/mock/Bar.vue, app/data/governance-finding-rows.ts |
| 01-shared-components | 1 | new app/data/governance-impact.ts, new app/components/governance/mock/ModeUntil.vue, new app/components/governance/mock/BlastRadius.vue, app/pages/pages/phase4/mock/parts.vue (demo rows), app/data/governance-parts.ts (parts registry only) |
| 02-policy-detail | 2 | mock/policy-detail.vue, part/PolicyExceptions.vue, part/PolicyMode.vue, part/PolicyOverview.vue, mock/ModeControl.vue, mock/SeverityPicker.vue, live/ControlsPolicies.vue, useControlCenterMock.ts |
| 03-publishing | 2 | mock/changes.vue, mock/PublishTray.vue, app/layouts/governance-mock.vue, new mock/ConfigJson.vue |
| 04-findings | 2 | mock/findings.vue, part/FindingsTable.vue, mock/finding-detail.vue, mock/ResolveCapture.vue, part/FindingHistory.vue, part/FindingDetails.vue |
| 05-teams | 2 | mock/organization/teams/index.vue, …/teams/detail.vue, part/TeamPolicies.vue, app/composables/useTeamMock.ts, new mock/PostureBar*.vue |
| 06-endpoints | 2 | new mock/organization/endpoints/{index,detail}.vue, new live/Endpoint*.vue, live/TeamDevicesLedger.vue, app/data/governance-parts.ts (rail only, after 01 is done with it) |
| 07-adoption-stage | 3 | fixture seeds, Bar.vue, app/pages/pages/phase4/governance-model.vue |
| 08-records | 3 | CYCLE.md, app/data/c114-review.ts, feedback-intents.md, captures, deploy |
Paths shown as mock/…, part/… and live/… are under app/components/governance/, except mock/*.vue pages, which are under app/pages/pages/phase4/.
6. Feedback list for Mark Peterson (built in 08)
- D1: reasons are required, not nudged, on loosen and revoke.
- D4: a local staging layer in front of the shared draft.
- D15: equal-specificity tie, stricter wins.
- D6: Until reverts to the replaced mode.
- The intents repo still carries superseded intents. PR #4 didn't remove these, and they now contradict it:
enforce_manage-policy-groups.mdenforce_manually-resolve-with-ignore.mdenforce_configure-policy-scope.mdenforce_enable-disable-policy-enforcement.md
- The publish intent's open question, "What happens to pending edits when rolling back?". The lab's answer: the draft stays, rebased on the new live, and the changes now redundant with it are shown as such.
7. Not in this cycle
- market-analyst extraction of the Oct 19/21 calls (Mark, separately).
- Pricing (marketing).
- Frameworks as entry point (W.11, next per Mark Peterson).
- The qdash port.
- The demo.qpoint.io world.
- Layer extractions: BlastRadius and ModeUntil are candidates after review.