diff --git a/PLAN.md b/PLAN.md index 7728c7a..e7f938a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -236,10 +236,24 @@ artifact stream in devMode with the 24-file digest set; Access shows last-match- `edge-console` leaf rule beating the line/site catch-alls). Console clean; all four core gates green (workspace coverage 90.70%). -**Remaining slice-5 cuts — the write path (a decision for the user, not yet built).** Authoring and +**Remaining slice-5 cuts — the write path. UNBLOCKED 2026-07-24 (register #16).** Authoring and branch/draft orchestration both require the server to *gain a write path* into Git — the read-only cuts -deliberately do not. Branch/draft orchestration additionally depends on **W8 (concurrent drafts), still -open on the register**; it should not be invented unilaterally. These are the next decision before slice 6. +deliberately do not. The concurrency story that gated branch/draft is now ruled: **optimistic +concurrency, no locks; semantic conflict detection at the effective-config level; continuous invisible +rebase; detect-and-surface, never auto-resolve; branches are not user-visible — a draft is a named change +and the vocabulary is propose → review → apply; apply is the host's PR merge gated by CODEOWNERS.** + +Two consequences to carry into the build: + +- **Credentials.** The Studio holds none today by design. The write path needs Git write access: + ship **(i)** a bot/App identity with the human in commit trailers, behind the same port as + **(ii)** per-user OAuth (acting-as-user), which is the enterprise-scale target. Design for (ii), + ship (i). +- **Authorization must not drift.** Authentication, when it lands, buys attribution, acting-as-user + and "you can/cannot approve this" — **not** a role system. Authorization stays CODEOWNERS + branch + protection enforced by the Git host (register #10). + +Next: the authoring cut (designable now), then branch/draft orchestration, then slice 6. **Design-repo drift from the profiles epic — closed (2026-07-23).** This repo's `schema/` and `fixtures/dallas/` were still the pre-profiles **flat form** (the profiles epic updated the kernel copy @@ -281,7 +295,9 @@ Mock verified: all nine pages render with zero console errors; wizard walks 4 st - `design/REVIEW.md` §6: #5 (device layers / blocked overrides — recommendation authoring-side only), #6 (render into existing config sources first), #8 (commit render snapshots at release boundaries). #4 (dataflows) resolves with step 2.4. -- **W8** — concurrent drafts have no story; bites in the first multi-user week. +- ~~**W8** — concurrent drafts have no story; bites in the first multi-user week.~~ **RESOLVED + 2026-07-24 (user), register #16** — optimistic concurrency, semantic output-level conflict detection, + no user-visible branches. Propagated to the deck's draft cards, the mock's draft chip, and DESIGN-cli §8.4. - Dependency: the deck's Greengrass scenario needs the deferred IPC-primary `uns-bridge` variant (ROADMAP A52) — a real blocker recorded in REVIEW W7.1. diff --git a/design/REVIEW-UI.md b/design/REVIEW-UI.md index 3c11a2b..87e6f39 100644 --- a/design/REVIEW-UI.md +++ b/design/REVIEW-UI.md @@ -194,9 +194,11 @@ instance-leaf scope, blocked-override language) and one real missing feature (bl (`fleet.html:193`). Under the decided model every node carries a *pair* (config release, artifact release), each with its own drift state. The fleet roll-up needs both, or it will actively mislead the first time a config release ships without an artifact release. -- **F2 — The draft list displays the exact hazard the design has no story for (W8).** Two drafts touch - line 7 (`fleet.html:231-232`) with no overlap/conflict indicator. Until W8 is decided, the minimum is a - conflict badge when two drafts modify the same layer file or definition object. +- **F2 — The draft list displays the exact hazard the design had no story for (W8).** Two drafts touched + one line with no overlap/conflict indicator. **Answered by register #16:** presence is *advisory, not a + claim* — the surface shows "N other open drafts touch this scope" (ruling 6), while a real conflict badge + is raised by **semantic** detection at submit and apply (ruling 3 — compare rendered outputs, not layer + text), never by textual overlap alone. - **F3 — Components as tree leaves** (`fleet.html:54`) makes "Component" an eighth pseudo-level. Fine — but state the rule: tree tiers = `hierarchy.levels` + two fixed leaf kinds (node, component). - **F4 — "Open draft" in the header routes to Dataflows** (`fleet.html:40`) — an arbitrary landing. Draft @@ -296,7 +298,9 @@ instance-leaf scope, blocked-override language) and one real missing feature (bl - **M7 — Secret provider management.** The component editor picks refs (`component-editor.html:81-90`); nothing defines providers. - **M8 — Draft management (W8).** No draft detail view: dirty objects, base release, rebase state, - conflicts with sibling drafts. + conflicts with sibling drafts. **Scoped by register #16:** the view is keyed on the change's *name* + (never a ref), shows the consequence diff when the base has moved under it, and lists semantic + conflicts for a human to resolve — it never offers auto-resolution or a lock. - **M9 — Day-two component addition.** Adding a component to an existing node lives only inside the wizard (U1); the component editor has no add flow, and "Add component instance" on Dataflows (`definition-map.html:40`) routes to an editor with no such capability. diff --git a/design/REVIEW.md b/design/REVIEW.md index b665128..90eed24 100644 --- a/design/REVIEW.md +++ b/design/REVIEW.md @@ -48,7 +48,8 @@ which is the failure mode this review exists to catch. They are now consistent: bundle's `config:`. All corrected. **W4.1 and W4.2 are deliberately untouched** — device-scope layers and blocked overrides are decision #5, -which is still the user's to make. **W8** (concurrent drafts) remains open and unaddressed. +which is still the user's to make. **W8** (concurrent drafts) is **resolved as of 2026-07-24 — register #16** +(optimistic concurrency, semantic output-level conflict detection, no user-visible branches). **A separate adversarial review of the mock UI now exists: `REVIEW-UI.md` (2026-07-22)** — feature/function review of all eight screens against the deck's ch. 10 contracts, the settled decisions, and the shipped @@ -393,12 +394,18 @@ Separately, chapter 4's "moving toward full hierarchical config" (`index.html:31 the shipped state (§2). The design is stronger than its own current-state chapter says, and should claim that credit accurately. -### W8 — Concurrent drafts have no story +### W8 — Concurrent drafts have no story — **RESOLVED 2026-07-24, register #16** -The fleet mock shows two open drafts touching the same line (`fleet.html:227-234`); the draft model is "a Git -branch that freezes the base release" (`app.js:735-738`). Two branches editing the same layer file merge -textually, not semantically; nothing addresses definition-level conflict detection, rebase semantics, or lock -scope. For a multi-owner site — which the ownership matrix implies — this will surface in week one of real use. +The fleet mock showed two open drafts touching the same line; the draft model was "a Git branch that freezes +the base release". Two branches editing the same layer file merge textually, not semantically; nothing +addressed definition-level conflict detection, rebase semantics, or lock scope. For a multi-owner site — which +the ownership matrix implies — this would surface in week one of real use. + +**Answered by register #16:** optimistic concurrency, **no locks**; conflict detection is **semantic, at the +effective-config level** (render `base`, `draft`, `main-now`, `merge(draft, main-now)` and compare outputs, so +the no-textual-conflict-but-changed-output case is caught); rebase is continuous and surfaced as a consequence +diff rather than a Git conflict; conflicts are detected and surfaced, never auto-resolved; and **branches are +not user-visible** — a draft is a named change, the vocabulary is propose → review → apply. --- @@ -751,6 +758,53 @@ immediately. access-policy narrowing) acquires a driver — the cheap path then is publishing each component's existing recipe in its release descriptor and overlaying version/artifact/config. +16. **Concurrent drafts (W8).** — **RESOLVED 2026-07-24 (user).** + *Decision:* **optimistic concurrency with semantic, output-level conflict detection, and no + user-visible branches.** Seven rulings: + + 1. **A draft is a named change; the branch is an implementation detail.** The author names the + change ("Add file-replicator to the filling line"); the Studio derives the ref + (`draft/-`). The vocabulary is **propose → review → apply** — never create-branch or + merge-branch. A draft opens implicitly on the first edit to committed state. Git remains the + only durable state; the author simply never types a ref. + 2. **Optimistic concurrency. No locks, at any granularity.** A lock is a second control plane over + Git: it goes stale, it blocks across teams at exactly the scale this product targets, and + enforcing it properly needs identity that is deliberately deferred. Rejected as the mechanism. + 3. **Conflict detection is semantic, at the effective-config level — never textual.** Because + render is deterministic and pure, the Studio renders four points at submit and again + immediately before apply — `base`, `draft`, `main-now`, and `merge(draft, main-now)` — and + compares **outputs**. A merged render that differs from what the draft's own diff predicted is + a conflict, whether or not Git found one. Textual auto-merge is never trusted alone; it is a + proposal that must survive an output-level check. This catches the case Git cannot see: two + drafts touching *different* files (one re-parents a node, another edits the layer at its old + scope) with no textual conflict and a changed effective config. + 4. **Rebase is continuous and invisible.** The Studio keeps a draft rebased on main. When main + moves underneath, the author sees "your change now produces a different result than when you + started" plus the consequence diff — not a Git conflict. + 5. **Detect and surface; never auto-resolve.** A semantic conflict requires a human decision. + Scope deletion or rename across drafts is an unconditional conflict. + 6. **Presence is advisory, not a claim.** "N other open drafts touch this scope" removes surprise + without blocking anyone — the fleet surface already depicts concurrent drafts on one line. + 7. **Apply is the Git host's PR merge**, gated by CODEOWNERS (already built and rendering on the + gate). No parallel approval system — consistent with #10. + + *Consequence — the write path forces a credential decision now.* The Studio deliberately holds no + credentials today (the Runner port holds target credentials; the Studio holds none). Writing to Git + requires write access, in one of two shapes: **(i)** the Studio acts as itself (bot/App identity, + human attributed in commit trailers) — simple, unblocks authoring, per-user permissions unenforced + but CODEOWNERS still gates the merge; or **(ii)** the Studio acts as the user (per-user OAuth, + GitHub App user-to-server) — the host enforces everything per-user, correct at enterprise scale, + needs the auth work. **Design for (ii), ship (i) behind the same port** so the swap is plumbing. + + *Consequence — the authorization posture is already decided and must not drift.* When + authentication arrives it buys attribution, acting-as-user, and "you can/cannot approve this" — + **not** a role system. Authorization stays CODEOWNERS + branch protection, enforced by the host + (#10). Authenticate users; do not build a policy engine. + + *Propagated in the same change:* this register, `PLAN.md`, the deck's draft-journey and anatomy + cards (which said the UI "creates a Git branch and locks the editing context" — both now wrong), + the mock's draft chip (branch name → change title), and `DESIGN-cli` §8.4. + --- ## Open doc-sync item — CLOSED 2026-07-22 diff --git a/design/index.html b/design/index.html index a61db2c..ca239c9 100644 --- a/design/index.html +++ b/design/index.html @@ -795,7 +795,7 @@

10. Product surface: detailed UI flows, screens, and interaction model

2 Open a draft -

Create a Git branch and lock the editing context.

+

Name the change. Nothing is locked, and no ref is ever typed.

3 @@ -864,8 +864,11 @@

Primary user journeys

Create a deployment draft

- Entry is the current release lock for a site, line, or set of nodes. The UI creates a Git branch, - freezes the base release, and shows dirty fields separately from rendered artifact changes. + Entry is the current release lock for a site, line, or set of nodes. A draft is a named + change — the author names it, the Studio derives the ref, and nothing is locked + (register #16). Dirty fields show separately from rendered artifact changes, and the draft + stays rebased on main: when the base moves, the author sees a consequence diff rather than a + Git conflict.

@@ -932,13 +935,13 @@

Open the mock as an app, not as a book widget.

diff --git a/design/mock-app/app.js b/design/mock-app/app.js index 16e61d5..2d7f0d8 100644 --- a/design/mock-app/app.js +++ b/design/mock-app/app.js @@ -94,10 +94,12 @@ function renderContextBar() { .map((k) => ``) .join(''); + // Register #16 ruling 1: a draft is a *named change*; the ref is derived, never shown as the + // primary identity and never typed by the author. const d = state.data.draft; el('draft-chip').innerHTML = d - ? `draft ${esc(d.branch)} · ${d.changed} files` - : `no draft · reading main`; + ? `draft: ${esc(d.title)} · ${d.changed} files` + : `no draft · showing committed state`; // Evidence provenance is global and always visible; a degraded state is designed, not promised. const e = state.data.evidence; @@ -443,11 +445,38 @@ function render() { renderPanel(); } +/* Deep links, so the deck can point at a screen: `#area=Releases` selects a global area, + * `#tab=Config` selects a workspace tab over the root scope. */ +function applyHash() { + if (typeof location === 'undefined' || !location.hash) return; + const h = new Map( + location.hash.replace(/^#/, '').split('&').filter(Boolean).map((kv) => { + const i = kv.indexOf('='); + return i < 0 ? [kv, ''] : [kv.slice(0, i), decodeURIComponent(kv.slice(i + 1))]; + }), + ); + const area = h.get('area'); + if (area && GLOBAL_AREAS.some((g) => g.id === area)) { + state.sel = { kind: 'global', id: area }; + return; + } + const tab = h.get('tab'); + if (tab) { + // Pick a selection that actually offers the requested tab (Components/Topology need nodes). + const node = state.data.nodes[0]; + if (node && (tab === 'Components' || tab === 'Topology')) { + state.sel = { kind: 'node', id: node.key }; + } + if (tabsFor(state.sel).includes(tab)) state.tab = tab; + } +} + function load(key) { state.key = key; state.data = window.MOCK_DATA[key]; state.sel = { kind: 'scope', id: state.data.scopes.find((s) => !s.parent).id }; state.tab = 'Overview'; + applyHash(); render(); } diff --git a/design/mock-app/data/dallas.js b/design/mock-app/data/dallas.js index 6347aab..f329144 100644 --- a/design/mock-app/data/dallas.js +++ b/design/mock-app/data/dallas.js @@ -5,7 +5,7 @@ window.MOCK_DATA.dallas = "repo": "bottling-company-test/sites/dallas-site", "description": "Bottles-R-Us Dallas plant — the north-star site.", "hierarchy": { "levels": ["enterprise", "site", "line", "device"] }, - "draft": { "branch": "deploy/add-file-replicator", "author": "@m.breissinger", "changed": 3 }, + "draft": { "title": "Add file-replicator to the filling line", "branch": "draft/add-file-replicator-7f3a", "author": "@m.breissinger", "changed": 3 }, "evidence": { "mode": "snapshot", "age": "02:15", "source": "dallas", "degraded": false }, "scopes": [ { "id": "enterprise/bottles-r-us", "parent": null, "layer": "layers/scopes/enterprise-bottles-r-us.json", "keys": 6 },