diff --git a/docs/qa/platform-checklist/FOLLOW-UPS.md b/docs/qa/platform-checklist/FOLLOW-UPS.md index ba9cbc4b25c..e66a1967f26 100644 --- a/docs/qa/platform-checklist/FOLLOW-UPS.md +++ b/docs/qa/platform-checklist/FOLLOW-UPS.md @@ -719,3 +719,63 @@ on `main` and the 2026-10-07 browser QA pass on the showcase boot (objectstack `since: "v17.7"` records the release in which the ledger began checking these conventions; the surfaces they sample predate it. + +## 12. Scoped authoring 2026-10-27 — the Studio QA run #26715 (checklist-author, one hunter) + +**Not a full sweep.** One hunter only: the Studio QA run #26715 (framework `3fcafcd3`) — +four pillar cards, two gap-fill cards and the coordinator — whose checklist-accuracy +findings and uncovered capabilities were authored by one writer, sequentially, and re-read +at `43731031`. No other SWEEP.md angle was run, so this section supports **no** claim about +what else is missing. + +- **Accuracy fixes** to 12 `studio-authoring` items; each item's own `history` row says + what changed and why. +- **23 new items**, all `active`, `since: "v18"`. Where the run found the capability broken, + the clause is written against the correct behaviour and the item's `knownGaps` names the + card or the run record: #26705 (held edits lost on departure), #26707 (draft and editor + verdicts disagree with the publish verdict), #26708 (an uninstalled package id is + accepted), #26711 (the view envelope strips unknown keys), #26712 (`app.hidden` rewritten + to `_unpublished`), #26718 (`restrict` overridden by a master-detail cascade), #26719 (a + chart view accepts another object's dataset). Defects the run recorded that had no card + number when the items were written are cited as "recorded by QA run #26715". +- **`coverage.json`**: the new items are mapped to their kinds, and the `picklist` waiver was + re-audited and replaced by items — its premise, "no runtime reads a picklist", no longer + holds; the entry's `$comment` says what the mapping does and does not claim. + `realtime_subscription`, the one remaining waiver, was **not** re-audited by this pass. + +### 12a. Not checklist items — for their owners + +- **Liveness ledger drift** (the gap-data card): `field.settledWhen` is graded `planned` in + `packages/spec/liveness/field.json`, yet on a `dueLike` date it changes the served + console's overdue styling. It belongs to the liveness ledger, outside this directory. The + card's second ledger finding — "`object.tenancy` has no ledger row" — does **not** hold: + `packages/spec/liveness/object.json` carries `tenancy.children.enabled` as `live` + (re-read at `43731031`). No action. +- **The jsdom pin spelling** `pnpm --filter @object-ui/PKG exec vitest run FILE`, written in + several items' `knownGaps` and in RUNNER.md's objectui-pinned-automation fact, does not + reach the `ui/` workspace config for `@object-ui/app-shell`, which has no vitest config of + its own (its `test` script is `vitest run --root ../.. packages/app-shell/`). QA run #26715 + measured the working form for one pin only — from `ui/`, `pnpm exec vitest run --root . + FILE` — and only that item (`packaged-automation-studio-lock`) was corrected. The other + texts need a measured run before they change. +- **Explain access** was listed as uncovered by the Access card; Studio's Access pillar + already drives it in `access-security.record-access-explain`, so no item was added. + +### 12b. Re-verification on takeover, 2026-10-28 (`main` `3230bdbf`) + +§12 was written against `43731031`. Before landing, the branch was merged with `main` and +the 35 items were re-read at `3230bdbf`. Every card §12 names as broken is now **closed as +fixed**: #26705 (PR #26839, PR #26927), #26707 (PR #26856), #26708 (PR #26920), #26711 +(PR #26851), #26712 (dbf7e04a, which was already an ancestor of `43731031`), #26718 +(PR #26864, PR #26930) and #26719 (PR #26842, PR #26906). Several defects that §12 cited +only as "recorded by QA run #26715" have also been fixed since, through their own cards. +For each affected item, the `fixtures.knownGaps` entry that said "FAILS TODAY" was removed +or narrowed to what still fails. The clauses were left unchanged as the correct behaviour, +and each item has a `history` row naming the fix. Three items still carry a +half that fails today, with no open card: +`flow-edge-type-authoring` (the Studio Automations pillar shows no draft verdict), +`summary-field-authoring` (an unconfigured summary is saved, not held; sum and avg offer +non-numeric fields) and `expression-editors` knownGaps[4] (the RLS editor reads 'Valid +CEL' on a formula-field predicate). These three were read from source, not measured. +`app-and-nav-properties` scores acceptance[5]'s `requiresObject` cell as not judged: +neither Studio nor Setup edits that key, and no ruling says Studio must. diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index 22b4ac42ec5..fd040360662 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -99,7 +99,7 @@ "title": "Object designer round-trip: field add/edit/reorder persists to metadata, identifiers derive and survive typing", "since": "v16", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "mixed", "personas": ["admin"], @@ -113,12 +113,12 @@ ] }, "steps": [ - "open the object designer on the writable package: /_console/apps/setup/metadata/object/new?package=PKG (PKG = the writable package's id; note its namespace NS, from GET /api/v1/packages). ⛔ Not /_console/apps/com.objectstack.studio/…: the studio app is not loaded on a stock boot (RUNNER.md environment facts), so that route renders inside another app's chrome", - "TYPE the object name NS_repair_asset character by character (never .fill — the F2 per-keystroke-sanitisation bug only reproduces when typed) and confirm both underscores survive. The NS_ prefix is required: in a namespaced writable package the publish gate refuses an unprefixed object name (NAMESPACE_PREFIX). Type the name after the label, or re-check it before saving — objectui#12680: typing the Label after a hand-typed name overwrites it and drops the prefix", + "open Studio on the writable package — /_console/studio/PKG/data (PKG = the writable package's id; note its namespace NS, from GET /api/v1/packages) — and click New object. Studio is the authoring door this item drives (QA run #26715 drove every clause through it); the Setup metadata editor (/_console/apps/setup/metadata/object/new?package=PKG) is a second door onto the same save, and it is the one the objectui pins in automated.ref drive. ⛔ Not /_console/apps/com.objectstack.studio/…: the studio app is not loaded on a stock boot (RUNNER.md environment facts), so that route renders inside another app's chrome", + "in the New object dialog type the Label 'Repair Asset', THEN type the identifier repair_asset character by character (never .fill — the F2 per-keystroke-sanitisation bug only reproduces when typed) and confirm its underscore survives. The dialog applies the package namespace itself: it previews and stores NS_repair_asset (StudioDesignSurface prefixObjectName, objectui CreateItemDialog.storedName-11792 — no double prefix when NS_ is typed as well), which the publish gate requires in a namespaced package (NAMESPACE_PREFIX). Type the identifier after the Label, and re-check it before Create (objectui#12680: a Label typed after a hand-typed identifier can re-derive it). Keep the pre-selected OWD choice and Create", "add a Text field, set its label to 'Asset Code', blur — the API name input must derive asset_code (F3)", "add a Picklist field and author its 3 real options, THEN add one EMPTY option row; wait out the debounced live validation — no validation banner may appear for the empty row (F4). ⚠ Order matters: with NO real option yet the select-needs-options banner fires, legitimately — the empty rows ARE dropped, so the field has no options — which collides with a literal 'no banner' (objectui F4, as repaired at 0b36ffa, gives its picklist one real option first for this reason). Assert the empty-row half with real options present", - "Save (the first save; the editor then autosaves every later edit — its Auto-save toggle defaults on, objectui ResourceEditPage.tsx metadata-admin:autosave — so later 'Save draft' steps are implicit), Publish; GET /api/v1/meta/object/NS_repair_asset and record the full field list and its order", - "reopen the designer: rename one picklist value, drag-reorder the fields (native HTML5 drag-and-drop: Playwright's dragTo moves a row, raw mouse moves do not — automation-input), let the autosave land, and Publish again", + "let the autosave land (Studio has no Save button: the status line reads 'Saved HH:MM'), then Publish → the review sheet → 'Publish all'; GET /api/v1/meta/object/NS_repair_asset and record the full field list and its order", + "reopen the object in the Data pillar: rename one picklist value, then reorder the fields on the Form tab — the form designer is dnd-kit (ObjectFormDesigner.tsx: a PointerSensor with a 4 px activation distance and a KeyboardSensor), so drag with intermediate pointer moves past 4 px, or focus a field's drag handle and press Space, an arrow key, Space; a single synthetic jump does not start a drag (automation-input). Let the autosave land and Publish again", "GET /api/v1/meta/object/NS_repair_asset again — the rename AND the new field order must be in the persisted metadata, not just the canvas", "create a record via the Records tab and GET /api/v1/data/NS_repair_asset/ — the stored column key must be the derived API name (asset_code)" ], @@ -132,13 +132,13 @@ { "clause": "a new field's API name derives from its label on blur, so the data column is the derived name — never a frozen field_N (the contract behind audit finding 2, where the saved record stored 'field_2': 'in_progress' forever)", "oracle": "api", - "verify": "the field-apiname input shows asset_code after the label blur (pinned by objectui F3), and the stored record's column key is asset_code", + "verify": "the field's API-name input shows asset_code after the label blur (corroborated at the Setup designer door by objectui F3, which drives that door, not Studio), and the stored record's column key is asset_code", "evidence": "designer screenshot + the record read" }, { - "clause": "typed identifiers survive per-keystroke sanitisation — NS_repair_asset keeps its underscores when typed char-by-char", + "clause": "typed identifiers survive per-keystroke sanitisation — repair_asset keeps its underscore when typed char-by-char, and the stored name is NS_repair_asset", "oracle": "dom", - "verify": "after a screenshot confirms the designer rendered, the name input's value equals the typed NS_repair_asset, character for character (pinned by objectui F2; pre-fix repair_asset yielded 'repairasset')", + "verify": "after a screenshot confirms the dialog rendered, the identifier input's value equals the typed repair_asset, character for character, and the dialog's stored-name preview reads NS_repair_asset; the meta read's name is NS_repair_asset (pre-fix repair_asset yielded 'repairasset'; objectui F2 corroborates the typing half at the Setup designer door)", "evidence": "screenshot + input value read" }, { @@ -152,7 +152,7 @@ "an API name frozen at field_N after its label was set pre-save is audit finding 2 regressed — FAIL, but confirm against a fresh objectui build first: the vendored /_console bundle may predate the derive-on-blur fix the e2e now pins" ], "traps": ["stale-console-bundle", "automation-input"], - "automated": { "kind": "e2e", "ref": "ui/e2e/live/studio-object-designer.spec.ts — F3 pins acceptance[1]'s designer half (the API-name input derives asset_code from the label on blur) and F2 pins acceptance[2] (a name typed character by character keeps its underscore). F4 pins acceptance[3]'s banner half ONLY: it gives the picklist one real option, not three, adds an empty row and finds no metadata-validation banner after the debounce, but it never publishes or reads the stored options, so 'the PUBLISHED picklist carries exactly the 3 authored options' is driven by hand. F1 (a package created from the Studio surface's switcher appears in it without a reload) asserts no clause of this item. No pin covers acceptance[0] or acceptance[1]'s stored-column half" }, + "automated": { "kind": "e2e", "ref": "ui/e2e/live/studio-object-designer.spec.ts — ⚠ F2, F3 and F4 drive the Setup designer route (/apps/showcase_app/metadata/object/new), not the Studio Data pillar this item's steps drive since revision 9, so each is evidence at a sibling door onto the same save. F3 pins acceptance[1]'s designer half (the API-name input derives asset_code from the label on blur) and F2 pins acceptance[2] (a name typed character by character keeps its underscore). F4 pins acceptance[3]'s banner half ONLY: it gives the picklist one real option, not three, adds an empty row and finds no metadata-validation banner after the debounce, but it never publishes or reads the stored options, so 'the PUBLISHED picklist carries exactly the 3 authored options' is driven by hand. F1 (a package created from the Studio surface's switcher appears in it without a reload) asserts no clause of this item. No pin covers acceptance[0] or acceptance[1]'s stored-column half" }, "source": [ "objectui: e2e/live/studio-object-designer.spec.ts (F1–F4, the objectui#1926 regression classes: per-keystroke sanitisation, label→api-name derivation, empty picklist row, switcher refresh)", "docs/audits/2026-07-studio-package-create-ux-dogfood.md (finding 2 — field API name did not follow the label at audit time)", @@ -201,6 +201,12 @@ "date": "2026-10-26", "change": "acceptance[3].verify no longer reads F4 as this clause's pin (checklist-accuracy finding a3 of QA run #25815, release-18.2.0-tier1 at 6679644a, objectui pin b41ed7f68956; wave anchor #25807): F4 seeds one real option, not the three the clause judges with, and never publishes, as the ref already said, so its banner half is corroboration under a different fixture. automated.ref and fixtures.knownGaps[0] re-pointed to ui/. The retired objectui-pin wording is replaced on the way (#26035 left this file to its card): objectui is imported under ui/, so the pins resolve in this checkout and the console builds from this tree with pnpm console:build. No clause is loosened", "ref": "#25848" + }, + { + "revision": 9, + "date": "2026-10-27", + "change": "steps[0], [1], [4] and [5], acceptance[1].verify, acceptance[2] (clause, verify) and automated.ref re-pointed to Studio as the door (checklist-accuracy finding of the Data card of QA run #26715, at 3fcafcd3; re-read at 43731031). Studio's New object dialog applies the package namespace itself (prefixObjectName, so the author types repair_asset and the stored name is NS_repair_asset), Studio has no Save button (autosave, then Publish → Publish all), and the Form tab's reorder is dnd-kit (a 4 px pointer activation and a keyboard sensor), not native HTML5 drag. The objectui pins drive the Setup designer route, so automated.ref says they are evidence at a sibling door. The run passed every clause this way. Not loosened: the typed underscore must survive and the stored name must carry the prefix", + "ref": "#26715" } ] }, @@ -209,18 +215,19 @@ "title": "List + form view authoring goes live in the running app on publish — no server restart", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": ["admin (authors)", "end user (consumes the published views). A runtime-authored object grants no non-admin persona any access, so a non-admin list render needs a permission-set grant on it first; without one, score the end-user surfaces as the admin and say so (QA run #24571)"], "fixtures": { "app": "showcase", "requires": [ - "a runtime-authored object in a writable package to bind views to (e.g. repair_asset from studio-authoring.object-designer-roundtrip), so no shipped showcase view is mutated. The object must declare a `sharingModel`: its draft PUT answers 200 without one, but publish refuses it (security-owd-unset, error tier — packages/lint/src/validate-security-posture.ts), so an object authored without it never goes live (QA run #24571)" + "a runtime-authored object in a writable package to bind views to (e.g. repair_asset from studio-authoring.object-designer-roundtrip), so no shipped showcase view is mutated. The object must declare a `sharingModel`: its draft PUT answers 200 without one, but publish refuses it (security-owd-unset, error tier — packages/lint/src/validate-security-posture.ts), so an object authored without it never goes live (QA run #24571)", + "the object must hold NO stored view container yet — a fresh scratch object is safest. A second container whose expansion serves names an existing container already serves (OBJECT.default / OBJECT.form) is refused 400 VALIDATION_ERROR naming the other owner (packages/metadata-protocol/src/protocol.ts, viewContainerNameCollisionRefusal), and that refusal reads like a failed draft door (QA run #26715 met it). If the object already has one, edit that container (add the views as its members) rather than authoring a second" ] }, "steps": [ - "author a view CONTAINER draft via the metadata draft door: PUT /api/v1/meta/view/qa_repair_asset_views?mode=draft with { object: 'repair_asset', list: { type: 'grid', columns: [...] }, form: { ... } } — the container's own keys are list/form/listViews/formViews (ViewSchema); capture the receipt", + "author a view CONTAINER draft via the metadata draft door: PUT /api/v1/meta/view/qa_repair_asset_views?mode=draft with { object: 'repair_asset', list: { type: 'grid', columns: [...] }, form: { ... } } — the container's own keys are list/form/listViews/formViews (ViewSchema); capture the receipt. A 400 VALIDATION_ERROR naming another container as the owner of the names is the second-container collision (fixtures.requires[1]), not this item's FAIL: re-run on an object with no stored container", "as an end user, open the object's list BEFORE publishing: it must still render the previously published views (or the synthesized default) — the draft is staged only", "publish: POST /api/v1/meta/view/qa_repair_asset_views/publish", "reload the app page (client reload allowed, NO server restart): the list must render the authored column set; open New or Edit on a record: the authored form view layout applies in those dialogs (opening a record shows its record page, not the form view)", @@ -273,7 +280,8 @@ "history": [ { "revision": 1, "date": "2026-08-07", "change": "new item: list/form view authoring through the ?mode=draft door with live-in-app verification, grounded in ViewSchema's container contract and the audit's zero-restart loop", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-10-02", "change": "re-pointed clause 4 at the object door's designed shape. It expected the container name qa_repair_asset_views in GET /meta/view?object=repair_asset; the door serves the container's expanded . items instead, by design since #7163 (runtime door #7736 / #13407). The 17.6.0 release-verification run measured the authored bodies round-tripping as repair_asset.default / repair_asset.form with the container name appearing 0 times", "ref": "#21330" }, - { "revision": 3, "date": "2026-10-19", "change": "personas[1], fixtures.requires[0] and steps[3] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604), re-read at objectstack f89d29e382: a runtime object needs `sharingModel` to publish (security-owd-unset is error tier in validate-security-posture.ts, though the draft PUT answers 200), the authored form view shows in the New / Edit dialogs rather than on the opened record's page, and a non-admin list render of a runtime object needs a grant. No clause changed. Wave anchor #24579 §3", "ref": "#24671" } + { "revision": 3, "date": "2026-10-19", "change": "personas[1], fixtures.requires[0] and steps[3] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604), re-read at objectstack f89d29e382: a runtime object needs `sharingModel` to publish (security-owd-unset is error tier in validate-security-posture.ts, though the draft PUT answers 200), the authored form view shows in the New / Edit dialogs rather than on the opened record's page, and a non-admin list render of a runtime object needs a grant. No clause changed. Wave anchor #24579 §3", "ref": "#24671" }, + { "revision": 4, "date": "2026-10-27", "change": "fixtures.requires[1] added and steps[0] annotated (checklist-accuracy finding of the Data card of QA run #26715, at 3fcafcd3; re-read at 43731031): a second view container on an object that already has one is refused 400 VALIDATION_ERROR by the container name-collision check (protocol.ts viewContainerNameCollisionRefusal), which the run met before it moved to a fresh object. The fixture now asks for an object with no stored container, and the step says how to recognise the collision. No clause changed", "ref": "#26715" } ] }, { @@ -392,7 +400,7 @@ "title": "Metadata draft→publish lifecycle: drafts staged not served, publish flips visibility atomically, conflicts and invalid drafts refused, and the history/audit/diff/rollback forensics tell the truth", "since": "v16", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "mixed", "personas": ["admin (authors drafts)", "end user (must not see drafts)"], @@ -416,7 +424,7 @@ "package-wide door: stage two drafts in the writable package and POST /api/v1/packages//publish-drafts — both flip in one atomic release (the audit's 'Published all drafts in this package (one atomic release)' toast); capture the STATUS and the BODY separately, because the abort path answers 200 with data.success:false (see the clause below)", "author an INVALID draft (a widget carrying a stray legacy key) and attempt its publish — the author-time gate must reject the draft→active transition (#4463)", "meta forensics (after the two publishes of qa_lifecycle_probe — revision 1, then the second revision): GET /api/v1/meta/dashboard/qa_lifecycle_probe/history — the durable sys_metadata_history events must list BOTH published revisions (a dashboard is an overlay type, so history is real; a non-overlay type answers { events: [] } by design). History counts draft saves AND publishes, so two save-draft-then-publish rounds read v1 (draft), v2 (its publish), v3 (draft), v4 (publish): read the two PUBLISH versions off this body for the diff step, steps[9] (QA run #23166 read v2 and v4)", - "GET /api/v1/meta/dashboard/qa_lifecycle_probe/diff?from=&to= — the two publish versions read off /history in steps[8] (v2 and v4 on a clean two-round run); the structural diff must name the widget key that changed between the revisions, not dump the whole body. ⛔ Not ?from=1&to=2: v1 → v2 is a draft save and its own publish, which differ in nothing (changed []), and a default previous-vs-current read can land on the same draft/publish pair", + "GET /api/v1/meta/dashboard/qa_lifecycle_probe/diff?from=&to= — the two publish versions read off /history in steps[8] (v2 and v4 on a clean two-round run; QA run #26715 read v2 and v5, because one extra If-Match draft save sat between them); the diff is SHALLOW by design — it names the changed TOP-LEVEL key (`widgets`, with its whole before/after value) and only the keys that differ, never the whole body and never a path inside the array (packages/metadata-protocol/src/protocol.ts#diffShallow: deep structural diffs are out of scope, Studio renders the full bodies side by side). ⛔ Not ?from=1&to=2: v1 → v2 is a draft save and its own publish, which differ in nothing (changed []), and a default previous-vs-current read can land on the same draft/publish pair", "POST /api/v1/meta/dashboard/qa_lifecycle_probe/rollback with { toVersion: 1 }, driven under an authenticated session so the actor is attributable — the authenticated identity is the ONLY source ([#7941]; resolveMetaWriteActor no longer consults `X-Actor` at all); the runtime GET must then serve revision 1's body again and the rendered dashboard must show revision 1's widget", "repeat the rollback (or another /meta write on this item, e.g. a follow-up draft save) while ALSO sending header `X-Actor: someone_else_entirely` on the authenticated request — the header must NOT change the recorded actor: the audit/history rows for that action still name the authenticated runner's own identity, never `someone_else_entirely` ([#7941] — pinned at the unit layer in packages/rest/src/meta-write-actor-identity.test.ts)", "attempt a rollback with a missing/invalid toVersion and capture the 400 INVALID_REQUEST guard", @@ -456,7 +464,7 @@ { "clause": "an invalid draft cannot cross into active: the author-time rules gate the draft→active transition (#4463) — publish of the stray-key draft is refused", "oracle": "api", - "verify": "the publish attempt on the invalid draft errors; the live read still serves nothing (or the prior good revision)", + "verify": "the publish attempt on the invalid draft errors; the live read still serves nothing (or the prior good revision). A stray widget key is now refused already at the DRAFT save — 422 INVALID_METADATA at widgets.N, unrecognized_keys (QA run #26715) — so no invalid draft exists to publish: that refusal plus the unchanged live read scores this clause too, since nothing invalid crossed into active", "evidence": "the refused publish + follow-up GET" }, { @@ -472,9 +480,9 @@ "evidence": "the history response" }, { - "clause": "diff names the changed key, not a whole-body dump — GET .../diff between the two PUBLISH versions read off /history (v2 → v4 on a clean run) isolates the widget/dimension key that differs between the two revisions", + "clause": "diff names the changed top-level key, not a whole-body dump — GET .../diff between the two PUBLISH versions read off /history (v2 → v4 on a clean run) reports `widgets` under changed[] with its before and after values, and no top-level key whose value did not change", "oracle": "api", - "verify": "the diff body reports the changed path (the edited widget key), not the entire document; a diff of a draft save against its own publish (from=1&to=2) reports changed [] and scores nothing either way", + "verify": "changed[] carries an entry with path 'widgets' (the top-level key the second revision edited) whose from/to carry the two arrays, and no entry for a top-level key whose value is the same in both revisions; the diff is shallow by contract (protocol.ts#diffShallow), so a path inside the array (widgets[0].title) is NOT expected and its absence is not a FAIL, while an unchanged key listed, or the whole document under one path, is. A diff of a draft save against its own publish (from=1&to=2) reports changed [] and scores nothing either way", "evidence": "the diff response" }, { @@ -521,7 +529,8 @@ { "revision": 5, "date": "2026-10-13", "change": "steps[9] and acceptance[8] (clause and verify) diff the two publish versions read off /history, and steps[8] and acceptance[7].verify say how history counts (assertion defect, #23166 A8; parked on #23121, carried by #23169). sys_metadata_history records draft saves and publishes alike, so ?from=1&to=2 compared the first draft save with its own publish (changed []); on QA run #23166's clean two-round run the published revisions were v2 and v4, and their diff named the edited widget key. The rollback steps are unchanged", "ref": "#23169" }, { "revision": 6, "date": "2026-10-16", "change": "steps[2] and acceptance[6] (clause, verify) re-pointed (checklist-accuracy finding 4 of QA run #23942, wave #23878). The Studio counter is no longer 'Changes · n' text but a numbered pill on the Changes button (objectui#12459), and the pill counts the drafts of the package in scope — an env-wide (package-less) dashboard draft is counted in none, so #23942 staged the browser leg's draft in a writable package. The step and clause name the pill and the writable-package staging, and the step says how to reach the draft in Studio — through an app of that package whose navigation names the dashboard, since a package with no app offers no dashboard to select (the re-run on #24036 found no chip until it staged one). Not loosened: a chip or pill that survives the publish, or is missing while the draft is pending, still FAILS", "ref": "#24036" }, { "revision": 7, "date": "2026-10-19", "change": "steps[2], acceptance[6].clause and acceptance[6].verify corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: the Studio item header reads 'Draft · N unpublished' (engine.studio.itemDraftPending, objectui#12725) — 'Unpublished draft' is the chip of other surfaces (ObjectListViewInspector, the OWD overview), not this item header — and a REST publish shows in Studio only after a reload. The round's other draft notes are not taken: no step of this item discards a draft (so the ?state=draft discard spelling has no clause to correct), and the NS_ prefix a scratch dashboard was given is not required (the namespace-prefix rule judges objects only — packages/spec/src/kernel/namespace-prefix.ts). Wave anchor #24579 round 2 §3 (6272314386)", "ref": "#24671" }, - { "revision": 8, "date": "2026-10-20", "change": "automated dropped and fixtures.knownGaps[0] added — §2 theme \"pins that cover no full clause\" of wave anchor #24579 (18.0 sweep), one of the leftovers round 6 (#24697) named. Tier 1 run #24598 scored every clause not-run on dashboard-designer-roundtrip.dogfood.test.ts, green 3/3 at ba4afcc2: it saves, publishes and reads back a layout-less dashboard, which is acceptance[1]'s post-publish half and no full clause. Round 1's QA run #24571 had scored acceptance[0]–[5] and [7]–[11] pass on that pin alone; QA run #24604 drove acceptance[6] and negative[0]–[4] by hand. The item had no knownGaps, so the line is new: it says what the pin asserts and when `automated` may return. The item is now Tier 2 (P1, no pin). No clause changed", "ref": "#24723" } + { "revision": 8, "date": "2026-10-20", "change": "automated dropped and fixtures.knownGaps[0] added — §2 theme \"pins that cover no full clause\" of wave anchor #24579 (18.0 sweep), one of the leftovers round 6 (#24697) named. Tier 1 run #24598 scored every clause not-run on dashboard-designer-roundtrip.dogfood.test.ts, green 3/3 at ba4afcc2: it saves, publishes and reads back a layout-less dashboard, which is acceptance[1]'s post-publish half and no full clause. Round 1's QA run #24571 had scored acceptance[0]–[5] and [7]–[11] pass on that pin alone; QA run #24604 drove acceptance[6] and negative[0]–[4] by hand. The item had no knownGaps, so the line is new: it says what the pin asserts and when `automated` may return. The item is now Tier 2 (P1, no pin). No clause changed", "ref": "#24723" }, + { "revision": 9, "date": "2026-10-27", "change": "acceptance[8] (clause, verify) and steps[9] follow the diff's shipped granularity, and acceptance[5].verify names the earlier refusal (checklist-accuracy findings c8 and c5 of the coordinator leg of QA run #26715, at 3fcafcd3; re-read at 43731031). (1) The clause expected the edited widget's own key (widgets[0].title); the door answered changed [{ path: 'widgets', from, to }]. diffMetaItem's diff is shallow by contract (protocol.ts diffShallow: top-level keys only, deep structural diffs out of scope), so the clause now asks for the changed top-level key and no unchanged one — still never a whole-document dump. (2) A stray widget key is refused at the draft save (422 INVALID_METADATA, unrecognized_keys), so there is no invalid draft to publish; that refusal now scores the invalid-draft clause beside the unchanged live read", "ref": "#26715" } ] }, { @@ -529,20 +538,23 @@ "title": "An invalid authored shape is rejected at save with a LOCATED error and is not persisted", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": ["admin"], "fixtures": { "app": "showcase", - "requires": ["scratch metadata names only (qa_invalid_probe / qa_invalid_views) — rejected drafts must leave nothing behind, so no cleanup dependency"], + "requires": [ + "scratch metadata names only (qa_invalid_probe / qa_invalid_views) — rejected drafts must leave nothing behind, so no cleanup dependency", + "for steps[1], an object with no stored view container (a fresh scratch object) to bind the container-shaped body to — see steps[1] for the collision refusal that masks the guidance otherwise" + ], "knownGaps": [ "KNOWN SPURIOUS BANNER — do not file it as new (run #7695's sighting recorded here so it is not re-filed a third time): a freshly saved VALID draft can show \"Unrecognized key(s) on this object: `_diagnostics`\". The designer re-validates the server's OWN annotation on read-back — `_diagnostics` is added by the server to the saved body, and the strict client-side schema then rejects it. It is a false positive on a save that succeeded, and it is the exact inverse of what this item asserts (a LOCATED error on an INVALID shape), so it is worth telling apart on sight: if the banner names `_diagnostics` and the save returned 2xx, it is this known issue, not a validation failure" ] }, "steps": [ "attempt PUT /api/v1/meta/object/qa_invalid_probe?mode=draft with a field MISSING its type; capture the status and full error body", - "attempt PUT /api/v1/meta/view/qa_invalid_views?mode=draft with `type`/`columns` at the CONTAINER level (the flat-list-view mistake) BESIDE a container key (e.g. `list: {…}`), so the body is container-shaped and the container's guidance answers; capture the guidance error. A body of `type`/`columns` alone is judged as an inline view config instead and refused as naming no `object`/`viewKind` — the same 'Wrap it: defineView({ list: … })' remedy, different first sentence (packages/spec/src/ui/view.zod.ts INLINE_VIEW_OBJECT_REQUIRED / INLINE_VIEW_KIND_REQUIRED)", + "attempt PUT /api/v1/meta/view/qa_invalid_views?mode=draft with `type`/`columns` at the CONTAINER level (the flat-list-view mistake) BESIDE a container key (e.g. `list: {…}`), so the body is container-shaped and the container's guidance answers; capture the guidance error. Bind the body's `object` to an object that holds NO stored view container (a fresh scratch object): against one that already has a container, the container name-collision check answers 400 VALIDATION_ERROR naming the other owner first and masks the guidance (protocol.ts viewContainerNameCollisionRefusal; QA run #26715 met it). A body of `type`/`columns` alone is judged as an inline view config instead and refused as naming no `object`/`viewKind` — the same 'Wrap it: defineView({ list: … })' remedy, different first sentence (packages/spec/src/ui/view.zod.ts INLINE_VIEW_OBJECT_REQUIRED / INLINE_VIEW_KIND_REQUIRED)", "after each rejection, GET the name back and record what the store holds", "drive one invalid shape through the browser door that reaches the server gate — the Setup metadata editor's Source tab (/_console/apps/setup/metadata/object/NAME), whose autosave PUTs it — and capture the surfaced error. The Studio designer holds an incomplete shape client-side and never sends it ('Not saved yet: …'), so it does not exercise the 422 — it must locate the offending field, not say a generic 'save failed'", "author the CORRECTED shapes (field with a type; views wrapped under list:) and confirm both save 200 — the gate rejects the shape, not the name", @@ -596,15 +608,16 @@ { "revision": 1, "date": "2026-08-07", "change": "new item: authoring validation with located errors and verified non-persistence, sampling object + view kinds and cross-referencing the deepened dashboard stray-key item instead of duplicating it", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-11", "change": "recorded run #7695's sighting of the ALREADY-KNOWN spurious '_diagnostics' banner on a freshly saved VALID draft (the designer re-validates the server's own annotation on read-back) as a knownGap + a NOT-this-item's-FAIL negative, so the sighting is not filed as a new defect and the false positive is told apart from a real located-error miss. Note only; no clause changed", "ref": "#7753" }, { "revision": 3, "date": "2026-09-23", "change": "re-spelled the located-error shape, which had stopped being true: the clause quoted '[invalid_metadata] … fields..type: Required', but saveMetaItem's spec-validation refusal no longer opens with a bracketed restatement of its code (retired in 7a25a3ee9c; the token rides code INVALID_METADATA), and on the REST and dispatcher doors its message is a headline of 'path [zod code]' locators with the per-issue prose on issues[]. Measured on c11852406 for a field missing its type: 'object/qa_invalid_probe failed spec validation: 1 issue — fields.amount.type [invalid_value]', 422, INVALID_METADATA. The dispatcher source pointer presented that test's hand-built mock message as the producer's text; it now says what the test pins, and the producer's per-face renderer is cited beside it. The capability itself (4xx, located path, nothing persisted) is unchanged", "ref": "#19709" }, - { "revision": 4, "date": "2026-10-19", "change": "steps[1], steps[3], acceptance[1] (clause, verify) and acceptance[3].verify corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) The quoted container guidance answers only a container-shaped body; a flat `type`/`columns` body is judged as an inline view config and refused as naming no `object`/`viewKind`, with the same Wrap remedy (view.zod.ts). (2) The Studio designer holds an incomplete shape client-side and never PUTs it, so the browser door that reaches the server gate is the Setup metadata editor's Source tab, which the run used. Wave anchor #24579 §3", "ref": "#24671" } + { "revision": 4, "date": "2026-10-19", "change": "steps[1], steps[3], acceptance[1] (clause, verify) and acceptance[3].verify corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) The quoted container guidance answers only a container-shaped body; a flat `type`/`columns` body is judged as an inline view config and refused as naming no `object`/`viewKind`, with the same Wrap remedy (view.zod.ts). (2) The Studio designer holds an incomplete shape client-side and never PUTs it, so the browser door that reaches the server gate is the Setup metadata editor's Source tab, which the run used. Wave anchor #24579 §3", "ref": "#24671" }, + { "revision": 5, "date": "2026-10-27", "change": "steps[1] and fixtures.requires[1] name the fixture the wrong-layer view probe needs (checklist-accuracy finding of the Data card of QA run #26715, at 3fcafcd3; re-read at 43731031): bound to an object that already has a stored view container, the container-shaped body is refused 400 VALIDATION_ERROR by the name-collision check before the ViewSchema guidance can answer, so the probe needs an object with no container. No clause changed", "ref": "#26715" } ] }, { "id": "studio-authoring.org-override-registry-gate", - "title": "The metadata type registry gates runtime writes: allowEnvironmentOverlay=false kinds refuse overlay (403 not_overridable), allowRuntimeCreate=false kinds refuse creation — both sides", + "title": "The metadata type registry gates runtime writes: allowEnvironmentOverlay=false kinds refuse overlay (403 NOT_OVERRIDABLE), allowRuntimeCreate=false kinds refuse creation (403 NOT_CREATABLE) — both sides", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": ["admin"], @@ -629,9 +642,9 @@ ], "acceptance": [ { - "clause": "a kind with allowEnvironmentOverlay=false refuses the environment overlay of an artifact-backed item with 403 not_overridable — the lock is server-side", + "clause": "a kind with allowEnvironmentOverlay=false refuses the environment overlay of an artifact-backed item with 403 NOT_OVERRIDABLE — the lock is server-side", "oracle": "api", - "verify": "PUT /api/v1/meta/object/showcase_task answers 403 with the not_overridable code (the runtime behavior the registry's own doc comment declares)", + "verify": "PUT /api/v1/meta/object/showcase_task answers 403 with code NOT_OVERRIDABLE (the runtime behavior the registry's own doc comment declares, which spells it lowercase; served codes are SCREAMING_SNAKE per ADR-0112) — score the code, not the one-sentence message", "evidence": "the 403 response body" }, { @@ -641,9 +654,9 @@ "evidence": "the accepted write + the read showing the overlay" }, { - "clause": "a kind with allowRuntimeCreate=false refuses creation with 403 (not_creatable) — declared-but-inert metadata is refused at the door, not stored to never run", + "clause": "a kind with allowRuntimeCreate=false refuses creation with 403 NOT_CREATABLE — declared-but-inert metadata is refused at the door, not stored to never run", "oracle": "api", - "verify": "PUT /api/v1/meta/job/qa_probe_job answers 403 not_creatable (job's flags exist precisely because a runtime job's handler could never resolve — ADR-0049)", + "verify": "PUT /api/v1/meta/job/qa_probe_job answers 403 with code NOT_CREATABLE (job's flags exist precisely because a runtime job's handler could never resolve — ADR-0049)", "evidence": "the 403 response" }, { @@ -665,7 +678,7 @@ "variants": [ "object (override locked, create open)", "field (override locked, create LOCKED — the standalone runtime-create door was retired by the 2026-08-12 ruling, #7893: field declares allowRuntimeCreate:false, so PUT /api/v1/meta/field/OBJECT.NAME for a new name answers 403 NOT_CREATABLE; fields are authored inside the object)", - "job (create locked — 403 not_creatable)", + "job (create locked — 403 NOT_CREATABLE)", "view (override open)", "dashboard (override open)" ], @@ -684,7 +697,8 @@ { "revision": 2, "date": "2026-08-11", "change": "recorded two notes from run #7695 — (a) the CORRECTION to run #7637: with OS_METADATA_WRITABLE=permission the Studio read-only badge clears and the editor is fully writable (GET /meta reports overrideSource:'env'), so the earlier 'badge asserts a lock the server is not applying' observation does not reproduce and must not be acted on as written; (b) the route note that a metadata-API-authored flow opens read-only in Studio (ResourceEditPage's artifact-backed heuristic + flow's allowOrgOverride:false) — correct for this kind, with the polarity-reversed defect filed at objectui#4308. Notes only; no clause, variant or oracle changed", "ref": "#7753" }, { "revision": 3, "date": "2026-10-16", "change": "variants[1] corrected (checklist-accuracy finding 1 of QA run #23942, wave #23878). The field variant still read 'create open', but DEFAULT_METADATA_TYPE_REGISTRY has declared field allowRuntimeCreate:false since #7893 (a standalone field row reached no object's fields), so its create side is locked like job's and answers 403 NOT_CREATABLE. The variant now states the expectation the registry declares; step[0] already tells the runner to read the flags first", "ref": "#24036" }, { "revision": 4, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). source[5] cited lines 1311-1323 of objectui ResourceEditPage.tsx in the comma-range spelling, written with a space (authored in 3bb9340974 as the list 951-955 and 1311-1323). At the objectui pin that commit shipped, 6314e87f2d49, 1311-1323 was the isArtifactItem test; at the shipped pin 12dc7b47c340 it has moved and also excludes provenance 'org'. source[5] now names isArtifactItem. objectui is read at the shipped pin 12dc7b47c340 and cited in prose, because no objectui anchor resolves in this gate. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" }, - { "revision": 5, "date": "2026-10-26", "change": "automated.ref says what its only pin proves (checklist-accuracy finding of QA run #25815, release-18.2.0-tier1 at 6679644a, objectui pin b41ed7f68956; wave anchor #25807): overlay-precedence.test.ts is a unit test of the overlay whitelist that stops above the HTTP door, so it corroborates acceptance[0], [2], [3] and negative[0] and scores none, and no pin touches acceptance[4]'s DELETE reset. The clauses are unchanged", "ref": "#25848" } + { "revision": 5, "date": "2026-10-26", "change": "automated.ref says what its only pin proves (checklist-accuracy finding of QA run #25815, release-18.2.0-tier1 at 6679644a, objectui pin b41ed7f68956; wave anchor #25807): overlay-precedence.test.ts is a unit test of the overlay whitelist that stops above the HTTP door, so it corroborates acceptance[0], [2], [3] and negative[0] and scores none, and no pin touches acceptance[4]'s DELETE reset. The clauses are unchanged", "ref": "#25848" }, + { "revision": 6, "date": "2026-10-27", "change": "title, acceptance[0] (clause, verify), acceptance[2] (clause, verify) and variants[2] spell the served codes NOT_OVERRIDABLE and NOT_CREATABLE (checklist-accuracy note of the coordinator leg of QA run #26715, at 3fcafcd3, where all five clauses passed): the item spelled them lowercase, after the registry's doc comment, while the door serves SCREAMING_SNAKE codes per ADR-0112. The source rows keep the lowercase where they quote source text. No clause is loosened", "ref": "#26715" } ] }, { @@ -692,7 +706,7 @@ "title": "The Studio CEL editors reach the SAME verdict as the engine: formula result-type inference, previous. completion in conditional rules, and RLS lint + test-run — the editor UI, not a second grammar", "since": "v16", "status": "active", - "revision": 5, + "revision": 7, "priority": "P1", "surface": "mixed", "personas": ["admin"], @@ -700,14 +714,15 @@ "app": "showcase", "requires": [ "a writable package with an object carrying a formula field (role=value) and a field conditional rule (visibleWhen/readonlyWhen/requiredWhen) — the stock showcase formula shapes are the models to author against (showcase_invoice line_total = record.quantity * record.unit_price, showcase_project remaining-budget, field-zoo); the source-loaded com.example.showcase is read-only, so the SAVE round-trip needs a writable package (the editors' lint/autocomplete/inferred-type/test-run themselves run client-side and can be OBSERVED read-only on the showcase fields)", - "a writable permission set to host the RLS USING/CHECK CEL editor + test-run (PermissionAdvancedFacets)", + "a writable permission set to host the RLS USING/CHECK CEL editor + test-run: a set of the writable package, opened in Studio's Access pillar (/_console/studio/PKG/access → the set → the 'Row-level security' tab, which PermissionAdvancedFacets renders) — where QA run #26715 found the RLS editor; the package's objects must be published for the set's object pickers to list them", "the @objectstack/formula engine present in the console bundle — celAuthoring lazy-loads it" ], "knownGaps": [ "the CEL editors lazy-load @objectstack/formula and FEATURE-DETECT every entry point (celAuthoring.ts) — a missing/older engine degrades to no lint / no suggestions / test-run 'unavailable', never an exception. If the vendored bundle lacks the engine, record the affected clauses blocked(dependency) and verify the graceful-degradation clause instead", "AUTOMATION LIVES UNDER ui/ — `automated.ref` names objectui specs, and since objectui was imported into this repository they resolve at ui/PATH in this checkout. A jsdom unit pin runs here (`pnpm --filter @object-ui/PKG exec vitest run --maxWorkers=2 FILE`); a ui/e2e/live spec runs from ui/ with `pnpm test:e2e:live SPEC` against a live backend and the console dev server (the prerequisites are in ui/playwright.live.config.ts). Cite the output naming the commit, or drive the item by hand in the browser and score it as a manual run. ⛔ Never score it as covered on the strength of the `automated` field alone — an unrun pin is a claim, not evidence. Full protocol: RUNNER.md, the objectui-pinned-automation standing fact.", "acceptance[7] (graceful degradation) is PIN-ONLY BY DESIGN: its verify forbids hand-breaking the bundle. The environment it needs is this checkout with its install done, to run ui/packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts (describe 'celAuthoring · graceful degradation') with `pnpm --filter @object-ui/app-shell exec vitest run --maxWorkers=2 FILE`. QA run #21784 had no runnable checkout and recorded blocked(environment) (parked on #22017 row 16), which is the right verdict without one: never a hand-driven verdict, never inferred from the source. CI in this repository now runs the ui/ suites (the Test UI job), which is evidence about the commit CI built, not about the run subject: cite a run of the file at the subject sha", - "acceptance[2] and acceptance[4] score the editor against the ENGINE's tier for a non-lowerable RLS read predicate, which is ERROR (rls-predicate-unenforceable, fail-closed). At the console pin 4016370a the editor still gives it severity 'warning' with fail-open wording (objectui packages/app-shell/src/views/metadata-admin/celAuthoring.ts lintCelPredicate) and autosaves it — the product defect objectstack-ai/objectui#13061 (QA run #24604 negative[0], rule-7 verified). Until that lands, those two clauses FAIL as written: score them fail with that card cited, never 'pass as written'" + "acceptance[2] and acceptance[4] score the editor against the ENGINE's tier for a non-lowerable RLS read predicate, which is ERROR (rls-predicate-unenforceable, fail-closed). At the console pin 4016370a the editor still gives it severity 'warning' with fail-open wording (objectui packages/app-shell/src/views/metadata-admin/celAuthoring.ts lintCelPredicate) and autosaves it — the product defect objectstack-ai/objectui#13061 (QA run #24604 negative[0], rule-7 verified). Until that lands, those two clauses FAIL as written: score them fail with that card cited, never 'pass as written'", + "THE FORMULA-FIELD PROBE FAILS TODAY (QA run #26715, Data card, at 3fcafcd3; re-read at 3230bdbf): the Studio RLS editor reads 'Valid CEL' for a USING predicate on a formula field (record.f_formula > 5) and autosaves it, and only the publish refuses it, rls-predicate-unenforceable at INVALID_FIELD — the editor's lint judges a one-clause stack that names no object (celAuthoring.ts rlsPredicateVerdict), so the rule's virtual-field branch cannot fire there. #26707 is closed by 57820085, which fixed the draft half: a draft read's _diagnostics now carries the publish verdict. The editor half has no open card. Until it is fixed, acceptance[2] and acceptance[4] FAIL on this probe: score them fail citing the run" ] }, "steps": [ @@ -715,7 +730,7 @@ "F1 formula result-type: open the object field designer (ObjectFieldInspector) on a formula field (type=formula, role=value, scope=record), TYPE a proven-number expression (record.est_hours * 1.1, or record.quantity * record.unit_price) and, separately, one cel-js cannot prove (record.a + record.b); after the 250ms debounce, screenshot the inferred-result-type (Σ) affordance for each", "type an unknown field (record.est_hourz) and a bare field ref in record scope (est_hours, no record. prefix); capture the inline error under the field (aria-invalid, destructive border) and its record. fix text", "F2 conditional-rule editor: open the same field's visibleWhen editor (scope=record, roots record/previous/parent); type `previous.` and then `record.` and capture the autocomplete listbox — it must offer the object's field catalog; type `current_user.` and confirm it does NOT list this object's fields; accept a suggestion and confirm the insertion", - "F4 RLS editor: open the permission set's Advanced Facets RLS policy (PermissionAdvancedFacets), enter a USING predicate that is NOT a simple field comparison (blast-radius) and one with a parse fault; confirm BOTH raise a blocking ERROR: the non-pushdownable filter at the engine's tier — rls-predicate-unenforceable refuses it at build and publish (ADR-0056 D4) because the runtime drops the policy and fails CLOSED, so it grants no access — and the parse fault as a parse error; with an error present the save is held (celErrorCount>0): the environment door's Save is disabled with title perm.cel.saveBlocked, and the Studio package door, which autosaves and has no Save button, shows the perm-autosave-held status and issues no PUT", + "F4 RLS editor: in Studio open /_console/studio/PKG/access, select the permission set, open its 'Row-level security' tab (PermissionAdvancedFacets) and add a policy on the object; enter a USING predicate that is NOT a simple field comparison (blast-radius), one with a parse fault, and — the virtual-field probe — one comparing the object's FORMULA field (e.g. record.FORMULA_FIELD > 5: a formula field has no storage column, so the engine cannot push the filter down and refuses it, rls-predicate-unenforceable, packages/lint/src/validate-rls-predicate-enforceability.ts#RLS_PREDICATE_UNENFORCEABLE); confirm ALL THREE raise a blocking ERROR: the non-pushdownable filter at the engine's tier — rls-predicate-unenforceable refuses it at build and publish (ADR-0056 D4) because the runtime drops the policy and fails CLOSED, so it grants no access — and the parse fault as a parse error; with an error present the save is held (celErrorCount>0): the environment door's Save is disabled with title perm.cel.saveBlocked, and the Studio package door, which autosaves and has no Save button, shows the perm-autosave-held status and issues no PUT", "RLS test-run: open the CelTestRunDialog ('Test this policy against a sample record'), supply a sample record + current_user, Run; capture allow (shield-check) / deny (shield-x) / a non-boolean value (non-bool smell) across three sample+predicate pairs", "parity cross-check: for the SAME expressions, confirm the editor's verdict tier (error/warning/ok, inferred type, allow/deny) equals the engine verdict captured in step 1", "persistence round-trip (writable package): Save the edited formula field → GET /api/v1/meta/object/ shows Field.returnType stamped from the inferred type; Save the RLS policy → GET /api/v1/meta/permission/ carries the USING/CHECK predicate" @@ -736,7 +751,7 @@ { "clause": "the editor's lint TIERS equal the canonical engine's — an unknown field and a bare field ref in record scope are ERRORS (with the record. fix, blocking Save), and a non-pushdownable (non-lowerable) USING read filter is an ERROR too: the engine refuses it at build and publish (rls-predicate-unenforceable, ADR-0056 D4) because the runtime drops the policy and fails CLOSED. The editor calls the SAME @objectstack/formula validateExpression the server and validate_expression use (ADR-0032), so the verdict is identical, not a second grammar", "oracle": "api", - "verify": "for each probe, the editor's inline finding severity matches the engine for the same expression+site (error vs warning vs ok): validate_expression / build for the formula and conditional-rule probes, and — validate_expression having no RLS site — the build/publish gate's rls-predicate-unenforceable (error) for the non-pushdownable USING probe (POST /api/v1/meta/permission/NAME/publish answers 422 naming the rule). An editor WARNING there is the tier divergence negative[0] scores as a FAIL", + "verify": "for each probe, the editor's inline finding severity matches the engine for the same expression+site (error vs warning vs ok): validate_expression / build for the formula and conditional-rule probes, and — validate_expression having no RLS site — the build/publish gate's rls-predicate-unenforceable (error) for the non-pushdownable USING probe and for the formula-field probe (POST /api/v1/meta/permission/NAME/publish, or the package's publish-drafts, answers 422 naming the rule). An editor WARNING there is the tier divergence negative[0] scores as a FAIL", "evidence": "the paired editor screenshot + engine verdict, per probe" }, { @@ -748,7 +763,7 @@ { "clause": "the RLS lint and the save gate agree with the engine: a non-pushdownable USING read filter raises a blocking ERROR, not an advisory warning, because the engine refuses it (rls-predicate-unenforceable, ADR-0056 D4) and the runtime fails CLOSED — the policy is dropped and grants no access, it never widens access — and a parse fault raises a blocking ERROR too; with either error present the save is held (celErrorCount>0) — a predicate the engine refuses cannot be persisted", "oracle": "screenshot", - "verify": "the non-pushdownable filter shows an error-tier finding and the save is held; a parse-faulted predicate is held the same way. Held means: on the environment door, Save disabled with title perm.cel.saveBlocked; on the Studio package door, which autosaves and has no Save button, the perm-autosave-held status with no PUT issued (only the package door is writable on stock showcase)", + "verify": "the non-pushdownable filter shows an error-tier finding and the save is held; a parse-faulted predicate and the formula-field predicate are held the same way. Held means: on the environment door, Save disabled with title perm.cel.saveBlocked; on the Studio package door, which autosaves and has no Save button, the perm-autosave-held status with no PUT issued (only the package door is writable on stock showcase)", "evidence": "the two held editor states + the network trace showing no save PUT" }, { @@ -779,6 +794,7 @@ "formula field expression (role=value, scope=record) — inferred result type", "field conditional rule visibleWhen / readonlyWhen / requiredWhen (scope=record, roots record/previous/parent) — previous. completion", "RLS USING read filter (lowerable, or non-lowerable → ERROR and fail-CLOSED — never a fail-open advisory)", + "RLS USING read filter on a FORMULA (virtual) field → ERROR (rls-predicate-unenforceable: no storage column to push down)", "RLS CHECK write filter" ], "traps": ["stale-console-bundle", "hydration-race", "automation-input"], @@ -818,6 +834,18 @@ "date": "2026-10-26", "change": "automated.ref says what its pins score, fixtures.knownGaps[1] and [2] and acceptance[7].verify re-pointed to ui/ (checklist-accuracy finding a0-a6 of QA run #25815, release-18.2.0-tier1 at 6679644a, objectui pin b41ed7f68956; wave anchor #25807). Every pin in the ref is a jsdom component or unit test: evidence for the editor's lint, inference, completion and test-run logic, scoring acceptance[7] only. The retired objectui-pin wording is replaced on the way (#26035 left this file to its card): objectui is imported under ui/, so the pins resolve in this checkout and the console builds from this tree with pnpm console:build; the Test UI job now runs these suites. No clause is loosened", "ref": "#25848" + }, + { + "revision": 6, + "date": "2026-10-27", + "change": "fixtures.requires[1] and steps[4] put the RLS editor where it lives, Studio's Access pillar (the permission set's 'Row-level security' tab), and add the virtual-field probe: a USING predicate on a formula field, which the engine refuses at publish (rls-predicate-unenforceable). acceptance[2].verify and acceptance[4].verify judge it beside the non-pushdownable and parse-fault probes, variants[3] lists it, and fixtures.knownGaps[4] records that it fails today — the editor reads 'Valid CEL' and autosaves (family card #26707). Checklist-accuracy findings of the Data card of QA run #26715, at 3fcafcd3; re-read at 43731031. No clause is loosened", + "ref": "#26715" + }, + { + "revision": 7, + "date": "2026-10-28", + "change": "fixtures.knownGaps[4] re-read against main 3230bdbf: #26707 is closed by 57820085 (the draft read's _diagnostics reports the publish verdict), but the RLS editor still lints a one-clause stack with no object and reads 'Valid CEL' on the formula-field probe, so acceptance[2] and [4] still fail on it, now cited to the run. No clause changed", + "ref": "#26848" } ] }, @@ -826,7 +854,7 @@ "title": "Permission-matrix editor UX: field-filter + bulk apply to EXACTLY the visible fields, and the Bulk column never clips at narrow widths", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P2", "surface": "mixed", "personas": ["admin"], @@ -835,20 +863,21 @@ "requires": [ "a writable permission set to edit in the Permission Matrix editor (env-scope metadata-admin, or a package permission set in the Studio Access pillar) — a read-only source-loaded set can be opened to verify the read-only gate but not to exercise bulk writes", "an object with MORE THAN 6 fields to expand in the matrix, so the field sub-table's filter renders (FieldsSubTable shows the filter only when fields.length > 6) — a stock showcase object with a wide field set (showcase_account / showcase_invoice) on the environment door, or an object OF THE PACKAGE in the Studio Access pillar: in package scope the matrix lists only the package's own objects (objectui PermissionMatrixEditor.tsx, 'In package scope, list only the objects this package declares'), so a scratch package's set reads '0 objects granted' even when it grants showcase_account (QA run #24604)", - "a narrow viewport for the Bulk-column clip check (B3)" + "a narrow viewport for the Bulk-column clip check (B3)", + "in package scope, the package's objects PUBLISHED before the set is opened: while they are only drafts, the Studio Access matrix lists none of them ('0 objects') — a defect QA run #26715 recorded (the matrix lists objects without the draft preview); publish them first, or record the leg blocked(fixture) citing the run" ], "knownGaps": [ "AUTOMATION LIVES UNDER ui/ — `automated.ref` names objectui specs, and since objectui was imported into this repository they resolve at ui/PATH in this checkout. A jsdom unit pin runs here (`pnpm --filter @object-ui/PKG exec vitest run --maxWorkers=2 FILE`); a ui/e2e/live spec runs from ui/ with `pnpm test:e2e:live SPEC` against a live backend and the console dev server (the prerequisites are in ui/playwright.live.config.ts). Cite the output naming the commit, or drive the item by hand in the browser and score it as a manual run. ⛔ Never score it as covered on the strength of the `automated` field alone — an unrun pin is a claim, not evidence. Full protocol: RUNNER.md, the objectui-pinned-automation standing fact." ] }, "steps": [ - "open the Permission Matrix editor on a writable permission set (Studio Access pillar, or /_console/apps/com.objectstack.studio/metadata/permission/); expand an object with > 6 fields so the field sub-table's filter renders (threshold is > 6)", + "open the Permission Matrix editor on a writable permission set (Studio Access pillar, /_console/studio/PKG/access, or the metadata admin's door /_console/apps/com.objectstack.setup/metadata/permission/; ⛔ not /_console/apps/com.objectstack.studio/…, the studio app is not loaded on a stock boot — RUNNER.md environment facts); expand an object with > 6 fields so the field sub-table's filter renders (threshold is > 6)", "B4 field filter: type a filter that narrows the field set to a couple of fields; confirm the sub-table's 'n / N' counter and the DOM show only the matching fields (the hidden fields' read/edit checkboxes are absent)", "B4 bulk-over-visible: click 'Write all' (perm.field.bulk.writable) → it must grant read+write to EXACTLY the visible fields; clear the filter and confirm the previously-hidden fields are UNTOUCHED (still at their prior grant), never bulk-applied", "on a different filtered set click 'Read all' → it revokes write on the visible fields only; click 'Clear' → it drops the explicit overrides so the visible fields fall back to the read-only default (readable, not editable)", "Save; GET /api/v1/meta/permission/ (or the package draft read) and assert the fields map (keyed `${object}.${field}`) carries overrides for ONLY the fields that were visible when each bulk fired — never a filter-hidden field", "B3 clip: shrink the viewport to a narrow width; confirm the object matrix's enclosing overflow-auto container SCROLLS horizontally (the table carries min-w-[960px]) and the Bulk column (Read / CRUD / All / None per row) stays reachable, not clipped off the right edge", - "read-only gate: open a read-only package/type set; confirm Save is hidden and the bulk buttons + checkboxes are disabled (the writable gate), and the read-only badge names package-vs-type as the reason", + "read-only gate: open a read-only package/type set; confirm Save is hidden, every checkbox is disabled, the field sub-table's bulk buttons (Read all / Write all / Clear) are ABSENT — FieldsSubTable renders them only while the set is writable — and the object row's Read / CRUD / All / None, which Studio shows only on the row under the pointer or with keyboard focus, are disabled when they show; the read-only badge names package-vs-type as the reason", "the grant→access flip (a flipped verb actually changing a persona's API access) is access-security.permission-matrix-edit-loop — cite a pass there; this item does NOT re-prove that a saved grant changes access" ], "acceptance": [ @@ -883,9 +912,9 @@ "evidence": "the narrow-viewport screenshot(s) before and after the scroll + the scrollWidth / clientWidth reading" }, { - "clause": "the read-only gate holds: a read-only set hides Save and disables every bulk button + checkbox; the read-only badge names WHICH gate holds", + "clause": "the read-only gate holds: a read-only set hides Save, offers no field bulk at all and no operating object-row bulk, and disables every checkbox; the read-only badge names WHICH gate holds", "oracle": "screenshot", - "verify": "on the read-only set Save is absent, bulk buttons are disabled, and the badge names the gate that holds; the console distinguishes FOUR (objectui PermissionMatrixEditor.tsx at the pin): the package gate (engine.studio.pkg.readonly — Studio on a read-only package), the artifact gate (perm.readOnly.artifact, 'Read-only (this set is provided by a code package)' — a set a code package ships, in the metadata admin), the wall (perm.readOnly.walled — the security catalog under isolated/group tenancy) and the type gate (perm.readOnly — a type with no runtime write channel). On a stock boot only the package and artifact gates are reachable — the wall needs a walled tenancy posture and the type gate a permission type with no write channel, which stock does not have — so score those two on the packaged showcase_contributor set (Studio Access and the metadata admin) and record the other two as not reachable on stock. A badge that blames the wrong gate is the FAIL", + "verify": "on the read-only set Save is absent, every checkbox is disabled, the field bulk buttons (Read all / Write all / Clear) are absent — not merely disabled (PermissionMatrixEditor.tsx FieldsSubTable renders them only while writable; QA run #26715 read them absent in read-only Studio) — and the object row's bulk, which appears only on hover or keyboard focus (engine.studio.access.bulkHint), is disabled when it appears; an operating bulk control is the FAIL. The badge names the gate that holds; the console distinguishes FOUR (objectui PermissionMatrixEditor.tsx at the pin): the package gate (engine.studio.pkg.readonly — Studio on a read-only package), the artifact gate (perm.readOnly.artifact, 'Read-only (this set is provided by a code package)' — a set a code package ships, in the metadata admin), the wall (perm.readOnly.walled — the security catalog under isolated/group tenancy) and the type gate (perm.readOnly — a type with no runtime write channel). On a stock boot only the package and artifact gates are reachable — the wall needs a walled tenancy posture and the type gate a permission type with no write channel, which stock does not have — so score those two on the packaged showcase_contributor set (Studio Access and the metadata admin) and record the other two as not reachable on stock. A badge that blames the wrong gate is the FAIL", "evidence": "the read-only screenshot" } ], @@ -932,6 +961,12 @@ "date": "2026-10-26", "change": "automated.ref adds PermissionMatrixEditor.artifactTier.test.tsx and says what each pin proves (checklist-accuracy findings of QA run #25815, release-18.2.0-tier1 at 6679644a, objectui pin b41ed7f68956; wave anchor #25807): fieldBulk is jsdom evidence for acceptance[0]/[1] at the component layer, never the saved fields map acceptance[2], [3] and negative[0] judge by; and acceptance[5]'s artifact gate is pinned in artifactTier, not in the ref's readonly file. fixtures.knownGaps[0] re-pointed to ui/. The retired objectui-pin wording is replaced on the way (#26035 left this file to its card): objectui is imported under ui/, so the pins resolve in this checkout and the console builds from this tree with pnpm console:build. No clause is loosened", "ref": "#25848" + }, + { + "revision": 6, + "date": "2026-10-27", + "change": "steps[0], steps[6], acceptance[5] (clause, verify) and fixtures.requires[3] corrected from the Access card of QA run #26715 (at 3fcafcd3; re-read at 43731031). (1) The metadata-admin door is /_console/apps/com.objectstack.setup/metadata/permission/NAME; the com.objectstack.studio route renders nothing useful, the studio app being unloaded on a stock boot. (2) In read-only Studio the field bulk buttons are absent, not disabled (FieldsSubTable renders them only while writable), and the object-row bulk shows only on hover or focus and is disabled there. (3) In package scope the matrix lists no object while the package's objects are only drafts, so the fixture publishes them first. No other clause changed", + "ref": "#26715" } ] }, @@ -1225,7 +1260,7 @@ "title": "Studio Automations pillar on a read-only package: a packaged flow is NOT editable in place — New-flow and autosave honor the courtesy gate, the enable/disable switch produces no metadata write, and inspector edits are inert or announced, never silently discarded", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "browser", "personas": ["admin (opens Studio on the read-only package)"], @@ -1237,7 +1272,7 @@ ], "knownGaps": [ "FIXED at objectui 1fb1ced36243 (objectui#11124), recorded so runs don't re-derive it: on a read-only package the Automations pillar's enable/disable switch is disabled (disabled={!isEditable || !!saving || readOnly}, its read-only reason carried by aria-describedby and a tooltip since objectui#12303, not a `title`) and toggleEnabled is guarded as well, so a forced click saves nothing; the flow node inspector receives the pillar's REAL readOnly flag, threaded as the Data pillar threads it, so its inputs are disabled; New-flow and autosave still honour the gate (StudioDesignSurface.tsx). QA run #23163 measured exactly that: the switch disabled with a reason title, a forced click sending nothing, the inspector inputs disabled, the flow byte-identical. The pre-fix shape — a clickable switch only the server's 422 refused, and inspector edits that took locally and vanished on reload — is gone; meeting it again is a regression FAIL, not a standing finding", - "objectui code now pins the read-only posture: ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.automationsReadOnly-11124.test.tsx ('disables the header \"Enabled\" switch, and a click on it saves nothing'; 'opens the flow inspector read-only: the node inputs are disabled'; no canvas edit handles). It is a jsdom unit test, so the item stays browser-driven end to end; run it from this checkout (`pnpm --filter @object-ui/app-shell exec vitest run --maxWorkers=2 FILE`) to cite it beside the live read" + "objectui code now pins the read-only posture: ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.automationsReadOnly-11124.test.tsx ('disables the header \"Enabled\" switch, and a click on it saves nothing'; 'opens the flow inspector read-only: the node inputs are disabled'; no canvas edit handles). It is a jsdom unit test, so the item stays browser-driven end to end; run it from this checkout to cite it beside the live read — FROM ui/, against the ui/ workspace config: `cd ui && pnpm exec vitest run --root . --maxWorkers=2 packages/app-shell/src/views/studio-design/StudioDesignSurface.automationsReadOnly-11124.test.tsx`. app-shell has no vitest config of its own (its `test` script is `vitest run --root ../.. packages/app-shell/`), so `pnpm --filter @object-ui/app-shell exec vitest run FILE` from the package misses the ui/ setup; QA run #26715 read 10/10 only the ui/ way" ] }, "steps": [ @@ -1248,7 +1283,7 @@ "click the enable/disable switch in the pillar header and capture everything: whether it is disabled/absent (it is disabled, and its read-only reason rides aria-describedby and a tooltip — objectui#12303 — not a `title`), whether a forced click fires any request (it must not — were it to, the request would be a flow DRAFT save flipping status active↔obsolete, StudioDesignSurface.tsx toggleEnabled), and what the UI surfaces", "select a flow node and edit a property in the inspector; record whether the edit visibly takes on the canvas/draft, and whether any affordance announces a read-only or will-not-persist state", "reload the pillar (re-open the package) and re-inspect: record whether the inspector edit survived or vanished, and whether its discard was ever announced", - "forge the write the client gate withholds, both spellings: PUT /api/v1/meta/flow/showcase_urgent_task_alert?mode=draft&package=com.example.showcase and the same PUT without ?package=, each with a modified body — capture each status and error.code. Then close out: GET /api/v1/meta/flow/showcase_urgent_task_alert again and diff against the baseline — byte-identical" + "forge the write the client gate withholds, both spellings: PUT /api/v1/meta/flow/showcase_urgent_task_alert?mode=draft&package=com.example.showcase and the same PUT without ?package=, each with a modified body — capture each status and code (the top-level `code` of the flat { error, code } refusal body). Then close out: GET /api/v1/meta/flow/showcase_urgent_task_alert again and diff against the baseline — byte-identical" ], "acceptance": [ { @@ -1258,9 +1293,9 @@ "evidence": "the rail DOM read + the flow-write-free network trace over the debounce window" }, { - "clause": "a packaged flow definition is not editable in place in Studio (ADR-0126 §2 Regime C, card F2): after driving every affordance the pillar offers, the flow's server-side metadata is byte-identical — and any write that slips past client courtesy is refused server-side with a 403 — error.code ITEM_LOCKED when the write names the read-only package (?package=com.example.showcase: the named read-only base), NOT_OVERRIDABLE when it does not (flow declares allowEnvironmentOverlay:false) — each naming the clone or toggle remedy (ADR-0124 D1: the server is the authority, the client gate is courtesy)", + "clause": "a packaged flow definition is not editable in place in Studio (ADR-0126 §2 Regime C, card F2): after driving every affordance the pillar offers, the flow's server-side metadata is byte-identical — and any write that slips past client courtesy is refused server-side with a 403 — code ITEM_LOCKED when the write names the read-only package (?package=com.example.showcase: the named read-only base), NOT_OVERRIDABLE when it does not (flow declares allowEnvironmentOverlay:false) — each naming the clone or toggle remedy (ADR-0124 D1: the server is the authority, the client gate is courtesy)", "oracle": "api", - "verify": "GET /api/v1/meta/flow/showcase_urgent_task_alert before/after the whole probe: identical; every flow write captured in the trace, and the two forged in step 7, answer 403 with error.code ITEM_LOCKED (the write names com.example.showcase) or NOT_OVERRIDABLE (it names no package) — score the status and the code, never the message; a 2xx on any of them is the hard FAIL", + "verify": "GET /api/v1/meta/flow/showcase_urgent_task_alert before/after the whole probe: identical; every flow write captured in the trace, and the two forged in step 7, answer 403 with code ITEM_LOCKED (the write names com.example.showcase) or NOT_OVERRIDABLE (it names no package) — score the status and the code, never the message; a 2xx on any of them is the hard FAIL. Read the code where the body carries it: this /meta write door answers the flat { error, code } body today, the code at the top level (QA run #26715), not nested as error.code", "evidence": "the before/after meta reads, diffed + each refusal body" }, { @@ -1294,7 +1329,8 @@ { "revision": 1, "date": "2026-08-26", "change": "new item (#12438 card F2, UI half): the Studio Automations pillar's read-only package lock — the promise (no in-place edit, switch produces no metadata write, inspector edits inert or announced) asserted with the current source shape recorded as the expected finding: the switch is clickable with only the server-side 422 behind it (disabled expression omits readOnly, StudioDesignSurface.tsx) and the inspector hardcodes readOnly:false so edits take locally and silently vanish on reload; New-flow and autosave honor the gate. Grounded in objectui f7c52e2 + the framework's writable_package_required door; owns the Automations pillar only, cross-referencing readonly-package-locks-studio for the surfaces it already owns", "ref": "#12438" }, { "revision": 2, "date": "2026-10-13", "change": "acceptance[2] and [3] (verify; evidence for [2]) drop the expected-fail wording, with knownGaps[0] and [1], steps[4] and source[0] (stale spec, #23163 A2 / A3; parked on #23121, carried by #23169). objectui#11124 disabled the pillar's switch on a read-only package (with the read-only hint as its title, and a guarded toggleEnabled) and threaded the real readOnly flag into the flow node inspector, pinned by StudioDesignSurface.automationsReadOnly-11124.test.tsx. QA run #23163 measured the switch disabled, a forced click sending nothing, the inspector inputs disabled and the flow byte-identical. Re-read from objectui source at 1fb1ced36243, still this repo's .objectui-sha. The clauses' promises are unchanged; only the recorded finding flips to the fixed shape", "ref": "#23169" }, { "revision": 3, "date": "2026-10-16", "change": "acceptance[1] (clause, verify), acceptance[2] (verify, evidence), steps[4] and [7], knownGaps[0] and source re-pointed (checklist-accuracy finding 5 of QA run #23942, wave #23878). Clause 1 named the server refusal as 422 [writable_package_required]; the forged flow write is refused 403 — ITEM_LOCKED when it names com.example.showcase, NOT_OVERRIDABLE when it names no package (sys-metadata-repository.package-writability.test.ts pins the pair) — so step 7 now forges both spellings and the clause scores status and code. The switch's reason is an aria-describedby plus tooltip since objectui#12303, not a `title`. Not loosened: a 2xx on any flow write is still the hard FAIL, and a 403 with any other code fails the clause", "ref": "#24036" }, - { "revision": 4, "date": "2026-10-26", "change": "fixtures.requires[1] and fixtures.knownGaps[1] drop the retired objectui-pin wording (a vendored console matching .objectui-sha; a pin runnable only from an objectui checkout), which #26035 left to this file's card: the console builds from this tree with pnpm console:build and the pin resolves under ui/. Wording only; the clauses are unchanged", "ref": "#25848" } + { "revision": 4, "date": "2026-10-26", "change": "fixtures.requires[1] and fixtures.knownGaps[1] drop the retired objectui-pin wording (a vendored console matching .objectui-sha; a pin runnable only from an objectui checkout), which #26035 left to this file's card: the console builds from this tree with pnpm console:build and the pin resolves under ui/. Wording only; the clauses are unchanged", "ref": "#25848" }, + { "revision": 5, "date": "2026-10-27", "change": "fixtures.knownGaps[1], steps[7] and acceptance[1] (clause, verify) corrected from the Automations card of QA run #26715 (at 3fcafcd3, all four clauses pass; re-read at 43731031). (1) The pin runs only from ui/ against the ui/ workspace config (`pnpm exec vitest run --root . FILE`): app-shell has no vitest config of its own, so the filtered spelling from the package misses the setup. (2) The forged write's 403 is the flat { error, code } body, so the code is read at the top level, not as error.code. Not loosened: the status must be 403 and the code ITEM_LOCKED or NOT_OVERRIDABLE", "ref": "#26715" } ] }, { @@ -1302,7 +1338,7 @@ "title": "Packaged views and dashboards are 'yours to edit directly' (ADR-0126 Regime O): the designer live-edits the PACKAGED artifact, publish reaches the end user, reset-overlay returns the shipped base — while a Regime-C flow in the same surface stays locked", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "browser", "personas": ["admin (authors the overlay in the designer)", "end user (renders the published change)"], @@ -1323,10 +1359,10 @@ "steps": [ "boot the showcase, sign in admin@objectos.ai/admin123; open the metadata-admin designer on a PACKAGED showcase view (/_console/…/metadata/view/VIEW, VIEW picked from GET /api/v1/meta/view?object=showcase_task — e.g. showcase_task.default; the showcase ships individual views, not a container) — it must open EDITABLE, with no lock banner", "record the shipped base: GET /api/v1/meta/view?object=showcase_task and the layered read of the picked view (GET /api/v1/meta/view/VIEW/layers), before any edit — the reset clause diffs against this", - "edit DIRECTLY on the packaged artifact — change the default list's label and add/remove a column — save draft and publish through the designer chrome", + "edit DIRECTLY on the packaged artifact — change the default list's label and add/remove a column — then BLUR the edited input (Tab out, or click elsewhere) before Save: the designer form takes an input's value on blur, so Save does not see an edit whose input still holds focus (QA run #26715); save draft and publish through the designer chrome", "as an end user, open the showcase task list (client reload only, no server restart): the edited label/column set renders", "server truth: GET the view again — the overlay body is served over the shipped base (overlay wins at read; the code layer remains underneath in the layered read)", - "dashboard leg: open the designer on the built-in system_overview dashboard — it opens EDITABLE the same way (dashboard allowEnvironmentOverlay:true); edit a widget title; publish; open the rendered System Overview board and confirm the edited title renders (its live-count semantics stay dashboards.system-overview-live-counts' business)", + "dashboard leg: open the designer on the built-in system_overview dashboard — it opens EDITABLE the same way (dashboard allowEnvironmentOverlay:true); edit a widget title and blur it before Save (steps[2]); publish; open the rendered System Overview board and confirm the edited title renders (its live-count semantics stay dashboards.system-overview-live-counts' business)", "reset BOTH overlays via the designer's destructive control: it must carry the 'Reset overlay' verb (resettable:true on the layered envelope — a package baseline exists), confirm; the follow-up GET serves the SHIPPED body again and the end-user surfaces render the base", "negative, same surface: open the designer route on the PACKAGED flow — it opens read-only behind the lock banner (at the pin it reads 'This item is locked and cannot be edited or deleted.' and names the owning package com.example.showcase; artifact-backed + flow allowEnvironmentOverlay:false) with no operating save/publish path" ], @@ -1366,7 +1402,7 @@ "a packaged view or dashboard opening behind the installed-package lock is the FAIL of the shipped tier-1 claim (content/docs/capabilities/integrations.mdx, content/docs/build-without-code.mdx) — but before filing it as a registry defect, forge the raw PUT: if the server ACCEPTS while the UI locks, it is the objectui#4308 client mis-tiering class, filed against objectui, not the registry", "a reset that answers 200 while the read or render still serves the overlay is a FAIL — the restore must flip the served body", "leaving the system_overview overlay behind after the run corrupts dashboards.system-overview-live-counts' stock fixture — the reset step is part of the item, not cleanup courtesy", - "SCOPE: the raw-PUT registry gate in both directions (403 not_overridable on locked kinds, accepted overlay + DELETE reset semantics at API grain) is studio-authoring.org-override-registry-gate — cite it, never duplicate it; studio-authoring.view-authoring-live deliberately avoids packaged views, and this item is its packaged complement" + "SCOPE: the raw-PUT registry gate in both directions (403 NOT_OVERRIDABLE on locked kinds, accepted overlay + DELETE reset semantics at API grain) is studio-authoring.org-override-registry-gate — cite it, never duplicate it; studio-authoring.view-authoring-live deliberately avoids packaged views, and this item is its packaged complement" ], "traps": ["stale-console-bundle", "hydration-race", "automation-input", "cache-staleness"], "source": [ @@ -1381,7 +1417,8 @@ "history": [ { "revision": 1, "date": "2026-08-26", "change": "new item (#12438, E1 tier-1 claim / ADR-0126 §3 Regime O): 'views and dashboards are yours to edit directly' proven on PACKAGED artifacts through the designer — live edit, publish, end-user render, reset-overlay back to the shipped base — with a built-in dashboard leg (system_overview; the dashboard kind's allowOrgOverride:true overlay door verified in the registry at metadata-plugin.zod.ts before asserting, per the register's instruction) and a same-surface Regime-C flow negative. Complements org-override-registry-gate (raw-PUT half) and view-authoring-live (which deliberately avoids packaged views)", "ref": "#12438" }, { "revision": 2, "date": "2026-10-16", "change": "fixtures.requires[0] and [1], steps[0], [1] and [7] and acceptance[2] (clause) and [4] (clause, verify) corrected (checklist-accuracy finding 6 of QA run #23942, wave #23878). The showcase ships individual views, not a container, so the 'discover the container name' instruction pointed at nothing — the item now picks a packaged view from the list and reads its /layers. The flow's banner is the lock banner ('This item is locked and cannot be edited or deleted.' plus the owning package), not an 'installed-package' banner; its wording is recognised, not scored. system_overview is defined in platform-objects but registered by plugin-auth, which is why its /layers reports com.objectstack.plugin-auth. Not loosened: the flow must still open locked with no operating save path", "ref": "#24036" }, - { "revision": 3, "date": "2026-10-26", "change": "fixtures.requires[3] names a console built from this tree with pnpm console:build instead of a vendored console matching the retired .objectui-sha pin (#26035 left this file to its card). Wording only; the clauses are unchanged", "ref": "#25848" } + { "revision": 3, "date": "2026-10-26", "change": "fixtures.requires[3] names a console built from this tree with pnpm console:build instead of a vendored console matching the retired .objectui-sha pin (#26035 left this file to its card). Wording only; the clauses are unchanged", "ref": "#25848" }, + { "revision": 4, "date": "2026-10-27", "change": "steps[2] and steps[5] say to blur the edited input before Save (checklist-accuracy finding of the Interfaces card of QA run #26715, at 3fcafcd3, where every clause passed): the designer form takes a value on blur, so Save does not see an edit whose input still holds focus. negative[3] spells the cross-referenced code NOT_OVERRIDABLE, as org-override-registry-gate now does. No clause changed", "ref": "#26715" } ] }, { @@ -1504,7 +1541,7 @@ "title": "A doc on the app menu follows the reader's audience: an admin authors a doc and puts it (or its book) on an app menu; a member who may read it sees it and opens it in its book; a member outside its audience does not see the entry and gets a refusal — never \"not found\", never a blank page — on the direct route", "since": "v17.5", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "mixed", "personas": [ @@ -1523,13 +1560,13 @@ "knownGaps": [ "THE STOCK SHOWCASE CORPUS CARRIES NO { permissionSet } BOOK AND NO doc NAV ENTRY — verify first (GET /api/v1/meta/book, read each audience; GET /api/v1/meta/app?id=showcase_app, look for type:'doc'). The run authors the books, docs, permission set and nav entries at runtime and tears them down, as platform-core.docs-audience-gate allows; the audience is read off the stored row, so a runtime-authored book exercises the same resolver", "FORK WINDOW CLOSED: every objectui revision from daeb77fc84dd on carried objectui PR 11241 and PR 11258, and so does the objectui code imported under ui/ — the 'You do not have access to this documentation' refusal page and the menu pruning — so a console built from this tree with `pnpm console:build` is the right one to drive (QA run #23942 ran outside the window). Only a console older than those PRs (db11afd496 held neither, source read) fails for a reason that is not this item's; record the commit the console was built from in the run record as usual", - "FIXTURE RECIPE, measured on the first run: the showcase is invite_only, so members come from POST /api/v1/auth/admin/create-user (mustChangePassword false), not sign-up; the Studio doc editor no longer refuses the read-only code package with a 422 — at objectui 1fb1ced36243 it saves an ENV-WIDE draft and offers no package picker (QA run #23166), so a doc authored through the editor lands outside the package that holds the books. Install a writable base first (POST /api/v1/packages with id, name, version, type 'app') and drive every book, doc and the app into it over the metadata API with ?package=, so books and docs share one package; the seeded admin holds no app-declared set, so once the gated book claims the doc the admin's own editor and /meta/app answer the audience refusal too — author the doc before the gated book, or read it back as the reader", + "FIXTURE RECIPE, measured on the first run: the showcase is invite_only, so members come from POST /api/v1/auth/admin/create-user, not sign-up — and the body must SEND `mustChangePassword: false`: the door defaults it to true (packages/plugins/plugin-auth/src/admin-user-endpoints.ts#runAdminCreateUser), and a member left on true cannot drive the menu until it has changed its password (QA run #26715); the Studio doc editor no longer refuses the read-only code package with a 422 — at objectui 1fb1ced36243 it saves an ENV-WIDE draft and offers no package picker (QA run #23166), so a doc authored through the editor lands outside the package that holds the books. Install a writable base first (POST /api/v1/packages with id, name, version, type 'app') and drive every book, doc and the app into it over the metadata API with ?package=, so books and docs share one package; the seeded admin holds no app-declared set, so once the gated book claims the doc the admin's own editor and /meta/app answer the audience refusal too — author the doc before the gated book, or read it back as the reader", "REPO OWNERSHIP (ADR-0054): the server half — /meta/app pruning, /meta/doc and /meta/book refusals — is objectstack's; the menu markup, the direct-route refusal page and the claiming-book permalink are objectui's. A locator or copy fix goes to objectui" ] }, "steps": [ "boot the showcase on its own port and DB (dogfood §0) with the console named above; as admin, verify the stock corpus first (knownGaps), then author the doc in the writable base package over the metadata API (PUT /api/v1/meta/doc/DOC?package=PKG, then publish) — knownGaps[2]'s recipe, so the doc shares the books' package, rather than through the Studio doc editor; author the remaining fixture books/docs and the permission set the same way, and assign SET to the reader", - "as admin, add the three doc entries to the app navigation (Studio app designer, or PUT /api/v1/meta/app/APP) over the gated and the open corpus", + "as admin, add the three doc entries to the app navigation with PUT /api/v1/meta/app/APP?package=PKG (then publish) over the gated and the open corpus. ⛔ Not through the Studio nav editor: its doc and book pickers list only what the signed-in author may read, and the seeded admin holds no app-declared set, so the gated doc and book are not offered at all (QA run #26715) — the API is the only door that can name them", "GET /api/v1/meta/app?id=APP as each member in that member's own session (prove the identity with GET /api/v1/auth/get-session first); record which doc entries each receives", "as each member, open the app and capture the rendered menu after a screenshot confirms it settled; diff the rendered doc entries against that member's /meta/app answer (UI mirror)", "as the reader, open every doc entry: the page (lands in the book that claims it); the book (lands on its first readable page with the book sidebar showing only that member's pruned subset, compared with GET /api/v1/meta/book/BOOK/tree as that member); the page in its book", @@ -1588,7 +1625,8 @@ { "revision": 4, "date": "2026-10-19", "change": "steps[0] aligned with fixtures.knownGaps[2] from QA run #24571 (18.0 sweep, 5b14b7f3): the step said to create the doc in the Studio doc editor while knownGaps[2] prescribes authoring every book, doc and the app over the metadata API with ?package= so they share one package; the run drove the API recipe. No clause changed. Wave anchor #24579 §3", "ref": "#24671" }, { "revision": 5, "date": "2026-10-20", "change": "steps[0] respelled: 'boot the showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" }, { "revision": 6, "date": "2026-10-26", "change": "fixtures.knownGaps[1] drops the retired .objectui-sha wording (#26035 left this file to its card): the console builds from this tree with pnpm console:build, and the objectui code under ui/ carries PR 11241 and PR 11258 (the documentation refusal page is in ui/apps/console). Wording only; the clauses are unchanged", "ref": "#25848" }, - { "revision": 7, "date": "2026-10-27", "change": "fixtures.knownGaps[1]: 'objectui pin' reads 'objectui revision'; the historical objectui PR numbers and revisions are kept (#26626, maintainer-directed). No clause changed, no status changed", "ref": "#26626" } + { "revision": 7, "date": "2026-10-27", "change": "fixtures.knownGaps[1]: 'objectui pin' reads 'objectui revision'; the historical objectui PR numbers and revisions are kept (#26626, maintainer-directed). No clause changed, no status changed", "ref": "#26626" }, + { "revision": 8, "date": "2026-10-27", "change": "steps[1] and fixtures.knownGaps[2] corrected from the Interfaces card of QA run #26715 (at 3fcafcd3, where every clause passed; re-read at 43731031). (1) The Studio nav editor cannot place the gated entries: its doc and book pickers offer only what the author may read, and the seeded admin reads no gated doc, so the step writes the navigation through PUT /meta/app. (2) admin create-user defaults mustChangePassword to true (runAdminCreateUser), so the recipe sends false explicitly; the old '(mustChangePassword false)' read like the door's answer. No clause changed", "ref": "#26715" } ] }, { @@ -1596,7 +1634,7 @@ "title": "A picklist authored in a package is used by two objects' select fields from the designer: both forms offer its options, the bound field refuses inline options, and a GET → edit → PUT of an object with a bound field saves", "since": "v17.7", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -1608,6 +1646,9 @@ "a package that declares a picklist and one picklistExtensions entry adding one option to it; the stock showcase declares none, and the kind is package-owned (allowRuntimeCreate false), so add both in the run's own checkout, uncommitted: inside the defineStack call of examples/app-showcase/objectstack.config.ts add `picklists: [definePicklist({ name: NAME, label: LABEL, options: [two or more { label, value }] })]` (definePicklist is exported from '@objectstack/spec') and `picklistExtensions: [{ extend: NAME, options: [{ label, value }] }]`, whose one value the list does not already carry (a repeated value is refused at load with INVALID_METADATA). The extension may sit in the same defineStack as the list; it merges the same way as one from another package. Boot with `--compile` so the artifact carries the edit (os dev otherwise serves an existing dist/objectstack.json), and revert the file after the run", "a writable project package with two runtime objects, each with a sharingModel and one select field carrying inline options; one of them published with a field already bound (picklist: NAME, no options)", "the console under test carries objectui#10202's build (objectui PR 11669, merged as objectui c3623eb1). The objectui code under ui/ carries it (ui/packages/app-shell/src/services/builtinComponents.tsx cites objectui#10202); a console built before that merge does not (the pin 0abd4f9f at objectstack 80f9f7e6 did not), so build the console from this tree with `pnpm console:build` and name the commit it was built from in the run record" + ], + "knownGaps": [ + "THE PICKLIST FIXTURE IS NOT REACHABLE ON A STOCK SHOWCASE BOOT, AND NO RUNTIME DOOR MINTS ONE (QA run #26715, Data card, recorded the item blocked(fixture)): GET /api/v1/meta/picklist answers items: [] on stock (the showcase declares no picklist), PUT /api/v1/meta/picklist/NAME is refused 403 — the kind is code-only (allowRuntimeCreate false, packages/spec/src/kernel/metadata-plugin.zod.ts) — and with no served list the designer's 'Options from' offers only the field's own options. fixtures.requires[0]'s uncommitted checkout edit, booted with --compile, is the ONLY way in: a run that does not make it scores every clause blocked(fixture) citing this line, never fail. Read GET /api/v1/meta/picklist/NAME (step 1) before anything else to prove the edit reached the served artifact" ] }, "steps": [ @@ -1687,7 +1728,8 @@ "history": [ { "revision": 1, "date": "2026-10-06", "change": "new — drafted with objectui#10202 against the measured run of its PR; filed by the seat. Authored from that draft with these changes: since v17.7, because 17.6.0 shipped before objectui PR 11669 merged; the picker clause split in two, one oracle each; the picklist fixture spelled with its export, its stack keys and the reload it needs; the console fixture naming the merge it must carry; the spec citations written as anchors the gate resolves", "ref": "#21957" }, { "revision": 2, "date": "2026-10-20", "change": "steps[0] respelled: 'boot the showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" }, - { "revision": 3, "date": "2026-10-26", "change": "fixtures.requires[2] drops the retired pin route (.objectui-sha, pnpm objectui:refresh from a ../objectui), which #26035 left to this file's card: the console builds from this tree with pnpm console:build, and the objectui code under ui/ carries objectui#10202. Wording only; the clauses are unchanged", "ref": "#25848" } + { "revision": 3, "date": "2026-10-26", "change": "fixtures.requires[2] drops the retired pin route (.objectui-sha, pnpm objectui:refresh from a ../objectui), which #26035 left to this file's card: the console builds from this tree with pnpm console:build, and the objectui code under ui/ carries objectui#10202. Wording only; the clauses are unchanged", "ref": "#25848" }, + { "revision": 4, "date": "2026-10-27", "change": "fixtures.knownGaps[0] added — the fixture gap the Data card of QA run #26715 met (at 3fcafcd3): a stock showcase serves no picklist, the runtime door refuses to create one (403, code-only kind), and the designer then offers only inline options, so the item is runnable only on the fixtures.requires[0] checkout edit and is blocked(fixture) without it. Recorded so the next run does not rediscover it. No clause changed", "ref": "#26715" } ] }, { @@ -1775,7 +1817,7 @@ "title": "One item name, one stored row PER PACKAGE (ADR-0048) on v18: a package-less save resolves its owning package and an ambiguous one is 409, a dropStorage delete while another package's row remains is 409 and keeps the table, a save into the wrong package is 422 OBJECT_OVERLAY_PACKAGE_MISMATCH, a sealed packaged name answers one 403 NOT_OVERRIDABLE sentence, and revert discards a Studio package's drafts", "since": "v18", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -1785,12 +1827,12 @@ "app": "showcase", "requires": [ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", - "two scratch Studio-authored packages A and B (POST /api/v1/packages), each saving a view named showcase_task.qa_shared_view (?package=A / ?package=B)" + "two scratch Studio-authored packages A and B (POST /api/v1/packages), each saving a DASHBOARD named qa_shared_dash (?package=A / ?package=B). ⛔ Not a view on showcase_task: a view written into a Studio package is judged against that package's closure, and showcase_task lies outside it, so the save is refused (list-view-object-outside-closure, packages/lint/src/rule-explanations.ts) — QA run #26715 met it and used a dashboard" ] }, "steps": [ - "PUT /api/v1/meta/view/showcase_task.qa_shared_view with no ?package= — expect 409 (two holders)", - "DELETE /api/v1/meta/view/showcase_task.qa_shared_view?package=A; then re-save without ?package= — expect it to update B's row", + "PUT /api/v1/meta/dashboard/qa_shared_dash with no ?package= — expect 409 (two holders)", + "DELETE /api/v1/meta/dashboard/qa_shared_dash?package=A; then re-save without ?package= — expect it to update B's row", "author an object package A owns at runtime (a Studio-authored object in A), then PUT /api/v1/meta/object/?package=B (a package that does not own it) — expect 422 OBJECT_OVERLAY_PACKAGE_MISMATCH. ⛔ Not a showcase object such as showcase_task: it is code-packaged and sealed, and the package door answers 403 NOT_OVERRIDABLE ahead of the D9.9 check (packages/objectql/src/protocol-runtime-owner-overlay-refusal.test.ts, its code-owner control; QA run #24558)", "PUT a sealed packaged name (any showcase object) as a create; read the 403 sentence", "publish package A through POST /api/v1/packages/A/publish-drafts, edit an item to a draft, POST /api/v1/packages/A/revert; then revert a never-published package. ⛔ Not POST /api/v1/packages/A/publish: it collects the in-memory registry's members, which a Studio package's stored rows and drafts are not, and answers 200 {success:false, itemsPublished:0, 'No metadata items found for package …'} (packages/metadata/src/metadata-manager.ts publishPackage; QA run #24558)" @@ -1805,7 +1847,7 @@ { "clause": "an ambiguous package-less save or delete → 409 RESOURCE_CONFLICT and nothing changes; once one holder remains the package-less save updates THAT row (no second row)", "oracle": "api", - "verify": "status + GET /meta/view/showcase_task.qa_shared_view/layers before/after", + "verify": "status + GET /meta/dashboard/qa_shared_dash/layers before/after", "evidence": "answers + layer lists" }, { @@ -1852,6 +1894,12 @@ "date": "2026-10-19", "change": "steps[2] and steps[4] corrected from QA run #24558 (18.0 since:v18 sweep, 5b14b7f3), re-read at objectstack f89d29e382. (1) A save into a non-owning package of showcase_task cannot reach OBJECT_OVERLAY_PACKAGE_MISMATCH: the sealed code object answers 403 NOT_OVERRIDABLE first, which the item's own pin protocol-runtime-owner-overlay-refusal.test.ts holds as its code-owner control; the 422 needs a runtime object another package owns. (2) POST /packages/:id/publish collects only in-memory registry members (metadata-manager.ts), so for a Studio package it answers success:false 'No metadata items found'; the published version revert needs comes from publish-drafts, the door the revert pin's own measured recipe uses. Clause substance unchanged", "ref": "#24671" + }, + { + "revision": 3, + "date": "2026-10-27", + "change": "fixtures.requires[1], steps[0], steps[1] and acceptance[1].verify use a scratch DASHBOARD, qa_shared_dash, instead of the view showcase_task.qa_shared_view (checklist-accuracy finding of the coordinator leg of QA run #26715, at 3fcafcd3, where every clause passed on the dashboard; re-read at 43731031): a view written into a Studio package is judged against the package's closure, and showcase_task, a showcase object, is outside it, so the two package saves the item starts from are refused (list-view-object-outside-closure). The multi-package row addressing the item tests is the same for any overlay-allowed kind. No clause changed", + "ref": "#26715" } ] }, @@ -1943,6 +1991,2019 @@ "ref": "#25848" } ] + }, + { + "id": "studio-authoring.held-edit-leave-guard", + "title": "Studio asks before an unsent edit is left behind: a held (incomplete) or dirty edit in any pillar survives a pillar switch, a rail switch, the package switcher, Home and a reload — or is discarded only after the author confirms", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P1", + "surface": "browser", + "personas": [ + "admin (authors in a writable package)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package created from the Studio landing (New package) holding one published object with a few fields, one flow, an app whose navigation names one dashboard, and one permission set" + ] + }, + "steps": [ + "open Studio on the writable package (/_console/studio/PKG/data) with the network panel recording; register a Playwright dialog handler BEFORE each departure (page.on('dialog')) so a native confirm or a beforeunload prompt is recorded and answered deliberately, never auto-dismissed", + "Data, held choice field: open the object → Form tab → Add field, type Picklist, label Priority, no option. The strip reads 'Not saved yet: the field “Priority” needs at least one option. Your changes are kept here and saved once it is filled in.' and no PUT for it leaves the browser", + "from that held state drive each departure on a fresh held edit: (a) the Automations pillar link; (b) another object in the Data rail; (c) the package switcher → another package; (d) the header Home; (e) a tab reload. For each, record whether the confirm 'You have unsaved changes. Leave this page anyway?' (engine.edit.unsavedLeaveConfirm) or the browser's leave-site prompt appeared. Answer Stay / Cancel first and check the field is still on screen; then repeat and answer Leave", + "Data, held relationship: Add field, type Lookup, with no target — the strip names 'the field “…” needs the object it links to'; repeat departures (a) and (e)", + "Automations, held step: open the flow, add a step whose required input is empty — the strip names '{input} on the step “{step}” needs a value'; repeat departures (a), (b) with another flow in the rail, and (e)", + "Interfaces, held widget: open the app's dashboard, add a widget and leave its Dataset empty — the strip names 'Dataset on the widget “…” needs a value'; repeat departures (a) and (e)", + "controls: Access — tick one matrix checkbox (dirty, not held); Interfaces — rename a navigation entry with nav editing open; repeat departures (a), (d) and (e) for each", + "server truth after each answer: GET /api/v1/meta/_drafts?packageId=PKG (the package's pending drafts) and the item's draft read (GET /api/v1/meta/TYPE/NAME?state=draft&package=PKG) — after Leave they carry nothing of the discarded edit; after Stay, finishing the edit (an option with a value, a target, the input, a dataset) lets the autosave PUT carry it" + ], + "acceptance": [ + { + "clause": "every in-app departure from a pillar holding a held OR a dirty edit asks first — the same native confirm in Data, Automations, Interfaces (navigation and widget) and Access — and a departure from a clean pillar asks nothing", + "oracle": "dom", + "verify": "per departure cell (pillar link, rail switch, package switcher, Home), the dialog event fired with the engine.edit.unsavedLeaveConfirm text while an edit was held or dirty, and did not fire on the clean controls; read after a screenshot confirms the pillar rendered", + "evidence": "the departure × pillar matrix: dialog fired / not fired, its text" + }, + { + "clause": "a reload or tab close while an edit is held or dirty raises the browser's leave-site prompt (beforeunload)", + "oracle": "dom", + "verify": "page.reload() with the handler registered records a dialog of type 'beforeunload' for each held or dirty pillar, and none for a clean one", + "evidence": "the dialog events per pillar" + }, + { + "clause": "Stay keeps the edit: the held field, step or widget is still on screen after the cancelled departure, and completing it saves it", + "oracle": "network", + "verify": "after Cancel the held line is still shown with the item; completing the edit sends the autosave PUT whose body carries it, answered 200", + "evidence": "the post-cancel screenshot description + the autosave request/response" + }, + { + "clause": "Leave discards only after the author said so, and writes nothing: the pending drafts and the item's draft read carry nothing of the discarded edit", + "oracle": "api", + "verify": "GET /api/v1/meta/_drafts?packageId=PKG and the item's ?state=draft read, before and after the confirmed departure, are unchanged by the discarded edit", + "evidence": "the before/after reads" + } + ], + "negative": [ + "a departure that drops a held edit with no prompt is the FAIL: the strip had told the author 'Your changes are kept here' (the #26705 shape, fixed by PR #26839 and PR #26927)", + "a prompt on a CLEAN pillar is also a FAIL — a guard that always asks teaches authors to click through it", + "SCOPE: Publish while an edit is held is studio-authoring.held-edit-publish-refusal; this item owns departure only" + ], + "traps": [ + "automation-input", + "hydration-race", + "stale-console-bundle" + ], + "source": [ + "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (unsavedPillarWork — pillarDirty or a held edit — feeding confirmLeavePillar, the beforeunload effect and useRegisterUnsavedEdits; confirmDropUnsavedWork, which the Data, Automations and Interfaces rails also ask before opening another item; onDirtyChange from the Access pillar and the Interfaces navigation editor, onHeldEditChange / useHeldEditReport from the Data, Automations and Interfaces leaf editors)", + "ui/packages/i18n/src/namespaces/studio/en.ts (engine.edit.unsavedLeaveConfirm; engine.studio.held.needsOptions / needsTarget / needsInput / widgetNeedsInput / line)", + "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.pillarNavGuard.test.tsx (jsdom pin of the dirty half on the Access stack — corroboration only, it scores no browser clause here)", + "#26705 (family card: held edits discarded silently on departure in Data, Automations and Interfaces; fixed by PR #26839 and PR #26927 — jsdom pins StudioDesignSurface.heldDataLeave-26705 / heldFlowLeave-26705 / heldWidgetLeave-26705 .test.tsx, corroboration only)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the Studio leave guard across the four pillars, from QA run #26715 (all four pillar cards exercised it; no item covered departure). Written against the correct behaviour, every held or dirty edit asks before it is left, with the three pillars that fail today recorded in knownGaps against #26705 and the two passing surfaces kept as controls", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0] removed: family card #26705 is closed, fixed on main by PR #26839 (942ffb21: a held edit feeds the same departure guard as a dirty pillar, and the Data, Automations and Interfaces rails ask before opening another item) and PR #26927 (3fd0ba1f: creates and Pending changes). The departure cells now score against the clauses as written. source[0] names unsavedPillarWork and confirmDropUnsavedWork; source[1] follows the strings to the studio i18n namespace (ui/packages/i18n/src/namespaces/studio/en.ts). Re-read at 3230bdbf. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.held-edit-publish-refusal", + "title": "Publish refuses while an edit is held: the refusal names what the held edit needs, nothing is published and no draft changes — and once the edit is finished, Publish goes", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P1", + "surface": "mixed", + "personas": [ + "admin (authors in a writable package)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with at least one pending draft (so a Publish has something to promote) and one published object, flow and app dashboard to hold edits on" + ] + }, + "steps": [ + "record the baseline: GET /api/v1/meta/_drafts?packageId=PKG (the pending set) and the live read of each item about to be edited", + "Data: add a Picklist field with no option (held: 'the field “…” needs at least one option'); click Publish, then 'Publish all' in the review sheet; capture the toast and the network trace", + "finish the field (an option with a value), let the autosave land, Publish again", + "Automations: add a step and leave its required input empty (held); Publish; capture; finish the input; Publish again", + "Interfaces: add a dashboard widget with no Dataset (held); Publish; capture; bind it; Publish again", + "after each refused Publish re-read GET /api/v1/meta/_drafts?packageId=PKG and the live reads; after each accepted Publish read the toast's count and the live read of the edited item" + ], + "acceptance": [ + { + "clause": "a Publish while an edit is held is refused in the author's words, naming the held clause — 'Not published: the field “Priority” needs at least one option, so that change is not saved yet. Finish it, then publish.' (engine.studio.held.publish over the pillar's needs* clause)", + "oracle": "screenshot", + "verify": "the error toast after Publish all names the held field, step input or widget input, per pillar", + "evidence": "the three toast texts" + }, + { + "clause": "the refusal promotes nothing: no publish request leaves the browser and the pending drafts and live reads are unchanged", + "oracle": "network", + "verify": "no POST /api/v1/packages/PKG/publish-drafts (nor any single-item publish) in the trace after the refused click; GET /api/v1/meta/_drafts?packageId=PKG and the live reads equal the baseline", + "evidence": "the trace window + the paired reads" + }, + { + "clause": "once the held edit is finished and saved, Publish goes: the batch publishes and the toast names the count", + "oracle": "api", + "verify": "publish-drafts answers 200 with outcome published; the toast names the count published (objectui#11807); the live read carries the finished field, step or widget", + "evidence": "the publish response + the live read" + } + ], + "negative": [ + "a Publish that goes around a held edit — the batch publishes the server's drafts while the screen still shows an unsent change — is the FAIL this refusal exists for: the author believes the change is live", + "SCOPE: leaving the pillar with the edit held is studio-authoring.held-edit-leave-guard; a draft the server holds that publish refuses (an invalid one) is studio-authoring.draft-publish-lifecycle" + ], + "traps": [ + "automation-input", + "stale-console-bundle" + ], + "automated": { + "kind": "unit", + "ref": "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.heldPublish-11786.test.tsx (a held flow step) + StudioDesignSurface.heldWidgetPublish-11910.test.tsx (a held widget): jsdom pins of the refusal and of 'once filled in, Publish goes'. Component-layer evidence for acceptance[0] and [2]; acceptance[1]'s live network and api reads stay the run's" + }, + "source": [ + "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (heldEdit state; doPublish refuses with engine.studio.held.publish while one is held, objectui#11786)", + "ui/packages/i18n/src/namespaces/studio/en.ts (engine.studio.held.publish and the needs* clauses)", + "QA run #26715: the coordinator's C4 (Data) and the Automations card measured the refusal; held-widget refusal per objectui#11910" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — Publish refuses while an edit is held, from QA run #26715 (coordinator C4 and the Automations card measured it pass). No item asserted the refusal, that it promotes nothing, or that a finished edit then publishes", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "source[1] follows engine.studio.held.publish and the needs* clauses from metadata-admin/i18n.ts to the studio i18n namespace (ui/packages/i18n/src/namespaces/studio/en.ts, #25922 slices). Re-read at 3230bdbf. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.automations-rail-and-new-flow", + "title": "The Automations rail groups flows by the object they watch, searches by label and name, says when nothing matches, and explains its glyphs and dots in a legend; New automation's presets refuse until the watched object is chosen, and a new flow starts Off", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin (authors in a writable package)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with two published objects, holding a record-triggered flow on each, a manual (autolaunched) flow, and — for the legend's grey dot — a schedule flow on a DEFAULT boot (package-authored scheduled work is off unless OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1 is exported, RUNNER.md environment facts)" + ] + }, + "steps": [ + "open /_console/studio/PKG/automations; screenshot the rail settled", + "grouping: the record-triggered flows sit under a heading per watched object (the object's label), the rest in a trailing group; each row carries its trigger-kind glyph (record / scheduled / screen / autolaunched) and a status dot", + "legend: read the rail's footer note (data-testid auto-rail-legend): the four trigger glyphs with their words and the On and Not-running dots; Off and Refused are named only while some flow in the rail reads so — disable one flow and re-read", + "search: type part of a flow's LABEL in mixed case, then part of a flow's machine NAME that its label does not hold, then a string nothing matches; clear the box", + "New automation: open it and read the presets (record created / updated / deleted, manual) and the Advanced fold; pick 'record created' and try to create without choosing the object — capture the refusal ('Choose the object it watches.'); choose the object, give a name, create", + "read the new flow: its Enabled switch reads off under 'Off by default', and GET /api/v1/meta/flow/NAME?state=draft&package=PKG carries status 'obsolete' and a Start node whose trigger names the chosen object and event" + ], + "acceptance": [ + { + "clause": "the rail groups record-triggered flows by watched object and gives every row its trigger glyph and status dot", + "oracle": "screenshot", + "verify": "the settled rail shows one heading per watched object with that object's flows beneath, the others trailing; each row's glyph matches its trigger kind", + "evidence": "the rail screenshot, described" + }, + { + "clause": "the legend names what the rail shows: four trigger glyphs and the On / Not-running dots always, Off and Refused only while a flow in the rail is in that state", + "oracle": "dom", + "verify": "after a screenshot confirms the rail rendered, the auto-rail-legend note lists the glyph words and On / Not running; Off appears only after a flow is disabled", + "evidence": "the legend DOM before and after the disable" + }, + { + "clause": "search matches label (case-insensitively) and machine name, says so when nothing matches, and clearing it brings every flow back", + "oracle": "dom", + "verify": "the filtered rows per query; the no-match query shows the rail's no-match message, never a blank rail; cleared, the full list returns", + "evidence": "the three filtered lists + the no-match text" + }, + { + "clause": "a record preset refuses to create until its object is chosen, and a created flow starts Off — a spec-valid draft with status obsolete and its trigger bound to the chosen object", + "oracle": "api", + "verify": "the refusal text is shown and no save request is sent; after choosing, the draft read carries status obsolete and the Start node's object and event", + "evidence": "the refusal screenshot description + the draft read" + } + ], + "negative": [ + "a blank rail for a search that matches nothing (no message) is a FAIL — QA run #26715 recorded the same gap on the Data pillar's object rail (an empty rail with no 'no matches' line)", + "a new flow created On, or saved without the trigger's object, is a FAIL: 'Off by default' is the promise the header makes" + ], + "traps": [ + "hydration-race", + "stale-console-bundle" + ], + "automated": { + "kind": "unit", + "ref": "ui/packages/app-shell/src/views/studio-design/AutomationsPillar.railSearch-11794.test.tsx (label / name / no-match search) + StudioDesignSurface.flowPresets-11861.test.tsx + StudioDesignSurface.flowStatusTruth-11779.test.tsx ('\"New\" saves status: obsolete … Off by default'): jsdom evidence for acceptance[2] and [3]; the live rail, legend and draft read stay the run's" + }, + "source": [ + "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (the rail legend auto-rail-legend, FLOW_GLYPH_ICON / FLOW_GLYPH_LABEL_KEY, flowDotClass, FlowStatusDot; the New automation presets; objectui#12454, #11794, #11861, #11779)", + "QA run #26715 (Automations card E1 / E2 pass; the rail search, legend and preset refusal were listed as uncovered)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the Automations rail (grouping, glyphs, legend, search) and the New automation presets, from QA run #26715 (Automations card E1 and E2 passed; the card listed rail search, legend and the preset object refusal as uncovered)", + "ref": "#26715" + } + ] + }, + { + "id": "studio-authoring.flow-edge-type-authoring", + "title": "Flow edges are authored in Studio as the spec types them — Normal, Fault, Conditional and Back-edge write default / fault / conditional / back with their label, condition and default-branch flag — and an invalid edge condition is flagged before publish, not only by it", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin (authors in a writable package)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with one published object and a flow on it: Start → Decision → two End nodes, plus a node that can fault (e.g. an HTTP step) for the fault edge" + ], + "knownGaps": [ + "acceptance[3] IS HALF FIXED: family card #26707 is closed by PR #26856 (57820085) — the draft read's _diagnostics now runs the publish's authoring gate, so `record.name ==` reads valid: false with the expression-invalid rule id and the publish's message, and the metadata edit page (ResourceEditPage) shows that verdict. The Studio Automations pillar does not: StudioDesignSurface hands its FlowPreview no diagnostics, and the client check (inspectors/expression-validate.ts validateExpressionClient) parses no CEL, so the edge inspector and the Problems tab are expected to stay silent on this condition (source read at 3230bdbf, not measured). Score the _diagnostics half on the draft read; score the inspector / Problems half as measured, citing the run. A package publish still rolls back the whole batch on this draft (atomic by design); acceptance[4] passes" + ] + }, + "steps": [ + "open the flow in /_console/studio/PKG/automations; select the edge Decision → End A; in the Edge inspector set Type to Conditional, untick Default branch, give a Branch label and the Condition `record.status == 'open'`; let the autosave land", + "select Decision → End B and mark it Default branch; select the faulting node's outgoing error edge and set Type to Fault; on a loop (if the flow has one) set Type to Back-edge; let each autosave land", + "GET /api/v1/meta/flow/NAME?state=draft&package=PKG and read each edge's type, label, condition and isDefault", + "invalid condition: set one conditional edge's Condition to `record.name ==`; read the inspector, the Problems tab and the draft read's _diagnostics", + "publish the package (Publish → Publish all); capture the refusal; fix the condition and publish again" + ], + "acceptance": [ + { + "clause": "each Edge Type choice writes the spec member: Normal → default, Fault → fault, Conditional → conditional, Back-edge → back", + "oracle": "api", + "verify": "the draft read's edges[].type equals the choice made per edge (FlowEdgeSchema type enum)", + "evidence": "the draft read's edges" + }, + { + "clause": "a conditional edge carries its label and CEL condition, and the default branch carries isDefault: true", + "oracle": "api", + "verify": "edges[].label / condition / isDefault in the draft read equal what the inspector shows", + "evidence": "the draft read" + }, + { + "clause": "the authored edges survive publish unchanged", + "oracle": "api", + "verify": "after the accepted publish, GET /api/v1/meta/flow/NAME serves the same edges[] (type, label, condition, isDefault)", + "evidence": "the live read" + }, + { + "clause": "an invalid edge condition is flagged BEFORE publish — in the edge inspector or the Problems tab, and the draft read's _diagnostics is not valid: true", + "oracle": "dom", + "verify": "after a screenshot confirms the inspector rendered, an error is shown at the condition (or Problems counts it), and the draft read's _diagnostics reports the faulting condition", + "evidence": "the inspector / Problems DOM read + the _diagnostics excerpt" + }, + { + "clause": "the publish refuses the invalid condition with a located error and publishes nothing", + "oracle": "api", + "verify": "publish-drafts answers outcome refused naming expression-invalid at the edge's condition; the live flow is unchanged", + "evidence": "the refusal body + the live read" + } + ], + "negative": [ + "an edge type written outside the spec's four members, or an inspector choice that writes nothing, is a FAIL", + "SCOPE: how the RUNTIME routes a fault path is automation.flow-error-handling, and node-by-node execution is automation.flow-node-type-matrix — this item owns what Studio writes and what it says before publish" + ], + "traps": [ + "automation-input", + "stale-console-bundle" + ], + "source": [ + "packages/spec/src/automation/flow.zod.ts#FlowEdgeSchema (type: default | fault | conditional | back; label; condition; isDefault — the BPMN default flow)", + "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (the flow canvas and Edge inspector — Type Normal / Fault / Conditional / Back-edge, Branch label, Condition, Default branch)", + "#26707 (family card: the draft and editor verdicts said valid while publish refused; closed by PR #26856, which fixed the draft read's _diagnostics and the metadata edit page — the invalid edge condition is one instance)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — edge-type authoring in the Automations pillar, from QA run #26715 (the Automations card authored fault and conditional edges and persisted them; edge-type editing was listed as uncovered). The pre-publish flag is written against the correct behaviour and recorded failing against #26707", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0] re-measured by source read at 3230bdbf: family card #26707 is closed by PR #26856 (57820085), which makes the draft read's _diagnostics carry the publish verdict, so that half of acceptance[3] now passes by construction; the Studio Automations pillar passes no diagnostics to its flow preview and the client check parses no CEL, so the inspector / Problems half is still expected to fail. source[2] records the closure. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.org-scope-flows", + "title": "Flows outside any package have their own Studio scope: /studio/~org offers only the Automations pillar, lists only package-less flows, opens them editable, saves drafts bound to no package and publishes each by reference", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "one package-less flow: a clone of a packaged showcase flow, POST /api/v1/automation/showcase_urgent_task_alert/clone with { \"name\": \"qa_org_clone\", \"label\": \"QA org clone\" } (the clone door automation.packaged-flow-clone-contract drives) — the clone names no package (ADR-0126 §7.1). There is no New in this scope by design, so a flow reaches it only this way" + ] + }, + "steps": [ + "with no package-less flow yet, open the Studio landing's 'Organization flows' entry: it lands on /_console/studio/~org/automations, offers one pillar only, no Create app, and an empty state ('No flows outside a package yet…') with an Open in Setup link", + "create the clone (fixtures.requires[1]); reload: the rail lists qa_org_clone and none of the showcase's packaged flows; GET /api/v1/meta/flow?package=com.example.showcase omits it, the unscoped list includes it", + "open it: the canvas and the Enabled switch are editable; change a node label; let the autosave land; read GET /api/v1/meta/_drafts?type=flow — the draft row carries no package", + "Publish → the review sheet lists only package-less flow drafts → Publish all; capture the network trace", + "GET /api/v1/meta/flow/qa_org_clone serves the edited label; deep link /_console/studio/~org/automations?surface=flow:qa_org_clone opens it; a deep link naming a packaged flow in this scope opens no other flow and says so", + "try another pillar in this scope (/_console/studio/~org/data): it lands back on Automations" + ], + "acceptance": [ + { + "clause": "the scope offers one pillar and lists package-less flows only — no Create app, no packaged flow", + "oracle": "screenshot", + "verify": "the header shows only Automations; the rail holds the clone and none of the showcase's flows; the ~org/data URL lands on Automations", + "evidence": "the scope screenshot + the landed URL" + }, + { + "clause": "a package-less flow opens editable and its edit saves a draft bound to no package", + "oracle": "api", + "verify": "the autosave PUT names no ?package=, and GET /api/v1/meta/_drafts?type=flow lists the draft with no package", + "evidence": "the PUT + the drafts row" + }, + { + "clause": "Publish promotes each package-less draft by reference — a single-item publish per flow, never the package batch door — and the edit goes live", + "oracle": "network", + "verify": "the trace carries one publish per flow draft and no POST /api/v1/packages/…/publish-drafts; the live read serves the new label; the toast counts the flows published", + "evidence": "the trace + the live read" + } + ], + "negative": [ + "a packaged flow editable in this scope, or a package-less draft pushed through the package batch door, is a FAIL — the two scopes must not leak into each other", + "SCOPE: the clone door itself (refusals, the notice, base and clone both bound) is automation.packaged-flow-clone-contract" + ], + "traps": [ + "hydration-race", + "stale-console-bundle" + ], + "automated": { + "kind": "unit", + "ref": "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.packageLessFlows-11553.test.tsx (lists the clone only, opens it editable, one pillar, publishes by reference and nothing through the batch door) + BuilderLanding.orgScope-11553.test.tsx: jsdom evidence for all three clauses at the component layer; the live server reads stay the run's" + }, + "source": [ + "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (readOnly is false for the package-less scope, objectui#11553; doPublish promotes package-less flow drafts one by one through publishDraft)", + "ADR-0126 §7.1 (a clone of a packaged flow is an ordinary org-owned flow)", + "automation.packaged-flow-clone-contract (the clone door — cross-referenced)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the package-less (~org) Studio scope, from QA run #26715 (Automations card E6 passed: no New by design, a clone created through the API was edited and published there; 'org flows publish outside a package' was listed as uncovered)", + "ref": "#26715" + } + ] + }, + { + "id": "studio-authoring.flow-status-matches-runtime", + "title": "The flow state Studio shows is the state the deployment runs: the rail dot and the header read what GET /api/v1/automation/_status reports — On, Off, Not running here with the platform's reason — and an enabled record-triggered flow actually runs", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "mixed", + "personas": [ + "admin" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with a published object, a record-triggered flow on it (created Off), a manual flow, and a schedule flow", + "TWO boots for the schedule leg, named in the run record: a default boot (package-authored scheduled work OFF) and a boot with OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1 exported (RUNNER.md environment facts)" + ] + }, + "steps": [ + "on the default boot open /_console/studio/PKG/automations; for every flow read the rail dot's words (the visually hidden text and the row title) and the header's status, then GET /api/v1/automation/_status and pair each flow's row (enabled, bound, reason)", + "enable the record-triggered flow with the header switch (it was Off by default); read the toast; publish; re-read the rail, the header and _status", + "create a record of the watched object (POST /api/v1/data/OBJ) and read the flow's runs for it", + "read the schedule flow on the default boot: its dot and header read 'Not running here', titled with the platform's reason, and Problems carries a note (not counted as a problem); _status answers bound: false with that same reason", + "reboot with OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1 exported; re-read the schedule flow in Studio and _status, and wait for its first run", + "disable the record-triggered flow; publish; create another record and confirm no new run" + ], + "acceptance": [ + { + "clause": "per flow, the rail dot and the header say what _status reports — On for an enabled bound or manual flow, Off for a disabled one, Not running here for an enabled flow the deployment does not bind", + "oracle": "api", + "verify": "the pairing table: Studio words vs _status { enabled, bound } for every flow, on both boots; no row disagrees", + "evidence": "the pairing table" + }, + { + "clause": "Not running here carries the platform's own reason verbatim — in the rail title, the header and the Problems note — and the note is not counted as a problem", + "oracle": "dom", + "verify": "after a screenshot confirms the pillar rendered, the schedule flow's title text equals _status's reason string; Problems lists it as a note with a zero problem count", + "evidence": "the title / note text beside the _status reason" + }, + { + "clause": "the switch is the runtime switch: enabled and published, the record-triggered flow runs on a create; disabled and published, it does not", + "oracle": "api", + "verify": "one completed run for the first record and none for the record created after the disable", + "evidence": "the run reads" + }, + { + "clause": "on the boot that enables scheduled work, the same schedule flow reads On, _status reports bound: true, and it runs", + "oracle": "api", + "verify": "Studio's words and _status agree on the second boot; a run with trigger type schedule completes within the schedule window", + "evidence": "the second-boot pairing + the run" + } + ], + "negative": [ + "a green On for a flow _status reports unbound, or a schedule flow read as a defect on a default boot, is a FAIL of this item — the deployment policy is not a fault, and Studio must not paint it as either health or error", + "SCOPE: the API kill switch ({enabled} at the automation door) is automation.flow-toggle-kill-switch; scheduled-work policy itself is the RUNNER.md fact, not a defect" + ], + "traps": [ + "hydration-race", + "eventual-consistency", + "stale-console-bundle" + ], + "automated": { + "kind": "unit", + "ref": "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.flowStatusTruth-11779.test.tsx + FlowStatusDot.test.tsx + StudioDesignSurface.railUnboundReason-11281.test.tsx: jsdom pins of the reading against a _status double — evidence for acceptance[0] and [1] at the component layer; the live pairing and the runs stay the run's" + }, + "source": [ + "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (FlowStatusDot and flowRailStatus: green On, grey Not running here titled with the reason, amber Refused, hollow Off; objectui#11779, #11281, #12454)", + "packages/services/service-automation/src/engine.ts (getFlowRuntimeStates — the rows GET /automation/_status passes through)", + "RUNNER.md environment facts (package-authored scheduled work is OFF unless OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — Studio's flow status against the runtime, from QA run #26715 (Automations card E4 passed on two boots: default off, toasts, status matching _status, a record create producing a completed run, the schedule flow Not running here on the default boot and bound on the second). The card listed 'Studio status vs _status' as uncovered", + "ref": "#26715" + } + ] + }, + { + "id": "studio-authoring.validation-rules-authoring", + "title": "Validation rules authored in Studio's Validations tab — the presets and all six rule types — save spec-valid, hold until their condition is given, and enforce on the write path once published, severity and the active switch included", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P1", + "surface": "mixed", + "personas": [ + "admin (authors the rules and drives the writes)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with one published object carrying two date fields, a number field, a text field, a status picklist (with option values) and a long-text field for JSON — the fields the presets and the six types bind to" + ] + }, + "steps": [ + "open /_console/studio/PKG/data → the object → Validations → New: read the menu — a few presets in plain words first, the six rule types folded under Advanced", + "presets: pick 'End date on or after start date' and 'Number can't be negative' (each written at once on the author's own fields, as editable rows); pick 'Reject the save when…' — it is held, not written, and opens with the condition focused; type a condition and watch it save", + "Advanced: author one rule of each remaining type with real settings — state_machine on the status field (initial state plus one allowed transition), format on the text field, json_schema on the long-text field, cross_field on the two dates, conditional wrapping a script rule — each with a message; set one rule's severity to warning and switch another rule off (active false)", + "CEL: type an invalid condition into one rule and read the live verdict, then fix it", + "let the autosave land, publish, and GET /api/v1/meta/object/OBJ: validations[] holds every rule as authored", + "write path: for each rule POST or PATCH /api/v1/data/OBJ with the violating payload, then its happy-path twin; for the warning rule send a matching payload; for the inactive rule send the payload it would reject" + ], + "acceptance": [ + { + "clause": "the New menu offers presets first and the six rule types under Advanced; a preset that fills its condition is written at once, and one that does not is held with its condition focused", + "oracle": "dom", + "verify": "after a screenshot confirms the tab rendered: the menu order, the Advanced list (script, state_machine, format, cross_field, json_schema, conditional), the held preset's focused condition input and no save request until it is filled", + "evidence": "the menu DOM + the network trace around the held preset" + }, + { + "clause": "the CEL editor gives a live verdict: an invalid condition shows an error at the input and holds the save until fixed", + "oracle": "network", + "verify": "no object PUT leaves while the error stands; the fix sends one, answered 200", + "evidence": "the inline error + the trace" + }, + { + "clause": "every authored rule is spec-valid and published as authored", + "oracle": "api", + "verify": "the object PUTs answer 200 and the live validations[] carries each rule's type, fields, message, severity and active flag", + "evidence": "the live object read" + }, + { + "clause": "each published rule enforces with its per-type code: invalid_initial_state / invalid_transition (state_machine), invalid_format, json_schema_violation, rule_violation (script, cross_field) and the wrapped rule under conditional — 400 VALIDATION_FAILED, nothing stored — while every happy-path twin lands", + "oracle": "api", + "verify": "status, code and targeted field per violating write; a read-back shows nothing persisted; each twin answers 2xx", + "evidence": "one line per write" + }, + { + "clause": "severity warning flags without blocking, and a rule switched off never fires", + "oracle": "api", + "verify": "the matching payload of the warning rule answers 2xx with the hit in the answer's warnings; the inactive rule's violating payload answers 2xx", + "evidence": "the two write answers" + } + ], + "negative": [ + "a rule that saves in Studio but does not enforce once published is a FAIL — the authored rule must be the enforced rule", + "SCOPE: the per-type code matrix on stock showcase objects is records-forms.validation-rule-type-matrix; this item proves the Studio-authored rule reaches it" + ], + "traps": [ + "automation-input", + "stale-console-bundle" + ], + "automated": { + "kind": "unit", + "ref": "ui/packages/app-shell/src/views/studio-design/ObjectValidationsPanel.presets-11861.test.tsx (presets first, written-or-held, never a system field) + ObjectValidationsPanel.newRuleWaits-11820.test.tsx: jsdom evidence for acceptance[0]; every api clause stays the run's" + }, + "source": [ + "packages/spec/src/data/validation.zod.ts#ValidationRuleSchema (the six types; active, events, severity, message on every rule)", + "ui/packages/app-shell/src/views/studio-design/ObjectValidationsPanel.tsx (the New menu, presets, Advanced types, the CEL gate; objectui#11861, #11820)", + "records-forms.validation-rule-type-matrix (the runtime code matrix — cross-referenced, not duplicated)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the Studio Validations tab end to end, from QA run #26715 (Data card D7 passed: presets, six types, CEL live verdicts, severity, active false, and the runtime of each type). No item drove a Studio-authored rule to the write path", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0] removed: both Validations-tab sightings of QA run #26715 are fixed on main by c4a4aefc (#26731). A new state_machine / format / json_schema rule is held until it has a field and a message, an empty transition row is not stored, and a conditional's `when` carries its own Guard caption. Re-read at 3230bdbf. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.hooks-tab-authoring", + "title": "Hooks authored in Studio's Hooks tab save pinned and keep their name — a rename is held with the way back named — and their options take effect at runtime: the Run-only-when condition, the retry policy and On Error", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin (authors the hooks and drives the writes)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with one published object with a text field the hook body can stamp", + "the server log readable (the retry leg reads it) — boot with OS_LOG_LEVEL=info, as records-forms.object-hook-lifecycle does" + ], + "knownGaps": [ + "NOT JUDGED HERE: body.capabilities and runAs. Studio's Hooks tab now grants a sandboxed-JS body its capabilities (the Capabilities checkboxes, HookDefaultInspector), and a hook refused for a missing capability answers 403 INSUFFICIENT_PRIVILEGES naming the hook and the capability (packages/runtime/src/sandbox/quickjs-runner.ts#throwCapabilityDenial), not 500 INTERNAL_ERROR. No clause here scores either yet, so the bodies below use no ctx capability and runAs stays unjudged" + ] + }, + "steps": [ + "open /_console/studio/PKG/data → the object → Hooks → New; pick events beforeInsert and beforeUpdate, give a body that stamps the text field; let the autosave land and capture the request headers", + "rename the hook in the inspector: the autosave stops and the line reads 'Not saved yet: a hook keeps the name it was created with. Change the name back to NAME to save your changes.'; change it back and confirm it saves again", + "type a CEL syntax error in Run only when; confirm the autosave is held while it stands; then set a real condition (e.g. record.name == 'cond')", + "More fields: set Retry Policy (max retries 2, a short backoff) and On Error log on a second hook whose body throws; on a third, On Error abort", + "publish; then POST /api/v1/data/OBJ with name 'cond' and with another name; read both records", + "POST a record that fires the throwing hooks: read the log hook's outcome and the abort hook's answer" + ], + "acceptance": [ + { + "clause": "a new hook's first save is the first-write pin (If-None-Match: *) and a later save of a hook with a pending draft sends If-Match with the version its draft read served", + "oracle": "network", + "verify": "the PUT headers of the first and a later autosave", + "evidence": "the two requests" + }, + { + "clause": "a renamed hook is held, not saved under the partial names: the held line names the original name, and no draft row appears under any other name", + "oracle": "api", + "verify": "no PUT while renamed; GET /api/v1/meta/_drafts?packageId=PKG&type=hook lists only the original name", + "evidence": "the held line + the drafts read" + }, + { + "clause": "a CEL syntax error in Run only when holds the autosave; a valid condition gates the body at runtime — the body runs only for the matching record", + "oracle": "api", + "verify": "no PUT while the error stands; after publish, the record named cond carries the stamp and the other does not", + "evidence": "the two record reads" + }, + { + "clause": "retry and On Error take effect: the throwing body retries the authored number of times, On Error log lets the write land, and On Error abort refuses it with the body's message and stores nothing", + "oracle": "log", + "verify": "the server log shows the retry attempts ('attempt 1/2', '2/2'); the log-hook write answers 201; the abort-hook write answers 400 with the thrown message and a read-back finds no row", + "evidence": "the log excerpt + the two write answers" + } + ], + "negative": [ + "a rename that stages a second hook per pause while typing is the FAIL the hold exists for (two partial-name drafts beside the original, as measured before objectui#11787)", + "SCOPE: hook timing and the stock hooks on the write path are records-forms.object-hook-lifecycle" + ], + "traps": [ + "automation-input", + "stale-console-bundle" + ], + "automated": { + "kind": "unit", + "ref": "ui/packages/app-shell/src/views/studio-design/ObjectHooksPanel.firstSavePinned-11773.test.tsx (the If-None-Match / If-Match pin) + ObjectHooksPanel.autosave-11787.test.tsx + ObjectHooksPanel.celGate.test.tsx: jsdom evidence for acceptance[0] to [2] at the component layer; the runtime clauses stay the run's" + }, + "source": [ + "ui/packages/app-shell/src/views/studio-design/ObjectHooksPanel.tsx (renamedFrom holds a renamed buffer, engine.studio.hooks.renameHeld; the CEL gate; the first-save pin, objectui#11773 / #11787)", + "packages/spec/src/data/hook.zod.ts (condition, retryPolicy, onError, runAs)", + "records-forms.object-hook-lifecycle (stock hooks on the write path — cross-referenced)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the Studio Hooks tab, from QA run #26715 (Data card D9 passed: new hook, events, async, first-save pin, rename hold; the gap-data card drove condition, retry policy and On Error to their runtime effect). The hook rename hold was listed as uncovered", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0] rewritten: the two defects QA run #26715 recorded there are fixed on main — the Hooks tab grants body.capabilities (fc92c62a, #26726) and a missing capability answers a coded 403 rather than 500 (e46e25c0). The entry now only states what this item does not judge. Re-read at 3230bdbf. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.actions-tab-presets", + "title": "Actions authored in Studio's Actions tab work on the record: the four presets are written at once or held until their target is given, a held one is dropped unsent by Delete, and each published preset does its job in the app — while the advanced editor writes actions the console can run", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P1", + "surface": "mixed", + "personas": [ + "admin (authors the actions)", + "member (clicks them in the app)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with one published object carrying a status picklist (with option values), an app whose navigation lists the object, one published flow on that object, and one page to open in a dialog", + "a member persona for the runtime legs: POST /api/v1/auth/admin/create-user sending mustChangePassword: false (the door defaults it to true), granted a package permission set that reads and writes the object, signed in through the form in its own browser context (RUNNER.md: a stock showcase refuses self-registration)" + ] + }, + "steps": [ + "open /_console/studio/PKG/data → the object → Actions → New: the presets come first — Change a picklist field, Run a flow, Open a web page, Open a page in a dialog — and the blank action sits under Advanced", + "Change a picklist field: it is written at once on the author's own picklist (never a system, hidden or read-only one); on an object with no picklist of the author's it is offered disabled, saying 'Needs a picklist field on this object.'", + "Run a flow / Open a web page / Open a page in a dialog: each is listed as not saved, its held line naming the input it waits for ('the action “…” needs “…”'), and no request leaves; give each its target (the flow, a URL with ${ctx.recordId} in it, the page) and watch it save", + "start one more preset and Delete it while it is held: no request leaves, and it is gone", + "publish; as the member open a record of the object in the app and click each action: change the status through the prompt; run the flow; open the web page; open the page dialog", + "Advanced: create an action with 'Run a script', give it a body, publish, and click it as the member; then tick 'Expose to AI agents' on an existing action and watch the next autosave" + ], + "acceptance": [ + { + "clause": "the presets come first in plain words; Change a picklist field is written at once on the author's own picklist, or offered disabled with the reason when there is none", + "oracle": "dom", + "verify": "after a screenshot confirms the menu rendered, the order and the disabled reason; the draft read shows the written action bound to the author's picklist", + "evidence": "the menu DOM + the draft read" + }, + { + "clause": "a preset that needs a target is held, not written, until it has one — and Delete drops a held preset unsent", + "oracle": "network", + "verify": "no PUT carries a held action; giving the target sends one; the deleted held action never reached the server (GET /api/v1/meta/object/OBJ?state=draft&package=PKG never lists it)", + "evidence": "the trace + the draft read" + }, + { + "clause": "each published preset does its job for the member: the picklist changes and persists, the flow runs, the web page opens with the record id interpolated, and the page opens in a dialog", + "oracle": "api", + "verify": "GET /api/v1/data/OBJ/ID shows the new status; the flow's run for that record completed; the opened URL carries the real record id (never a literal ${ctx.recordId}); the dialog renders the page", + "evidence": "the record read, the run read, the opened URL, the dialog screenshot described" + }, + { + "clause": "the presets' saves are spec-valid object saves: every autosave of the object answers 200", + "oracle": "network", + "verify": "the object PUT statuses across the session", + "evidence": "the PUT status list" + }, + { + "clause": "an action the advanced editor writes as a script is runnable from the record: it is saved with type script and the member's click runs it", + "oracle": "api", + "verify": "the draft read carries type: 'script' with the body; the click sends the action request and its effect lands", + "evidence": "the draft read + the action request/response" + }, + { + "clause": "'Expose to AI agents' writes the spec's ai block (ai.exposed) and the object's autosave still answers 200", + "oracle": "api", + "verify": "the PUT body carries ai: { exposed: true … } and no flat aiExposed key; the answer is 200 and later edits keep saving", + "evidence": "the PUT body + status" + } + ], + "negative": [ + "a held preset written to the server before its target is given, or a published preset that does nothing when clicked, is a FAIL", + "SCOPE: where an action renders (toolbar, record header, row menu) is records-forms.action-location-matrix; a refused script hook's toast is records-forms.script-action-hook-refusal-toast" + ], + "traps": [ + "automation-input", + "wrong-persona", + "stale-console-bundle" + ], + "automated": { + "kind": "unit", + "ref": "ui/packages/app-shell/src/views/studio-design/ObjectActionsPanel.presets-11861.test.tsx (presets first; written-or-held; Delete drops a held action; the disabled 'Change a picklist field'; every preset parses with ActionSchema and ObjectSchema) + ObjectActionsPanel.newActionDefault-11820.test.tsx: jsdom evidence for acceptance[0], [1] and [3]; the runtime clauses stay the run's" + }, + "source": [ + "ui/packages/app-shell/src/views/studio-design/ObjectActionsPanel.tsx (the New menu and the presets, objectui#11861; engine.studio.actions.heldNeeds)", + "packages/spec/src/ui/action.zod.ts#ActionType (script | url | modal | flow | api | form) and the action's ai block (ai.exposed, ai.description)", + "records-forms.action-location-matrix (placement — cross-referenced)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the Studio Actions tab, from QA run #26715 (Data card D10: the four presets passed, held until target; the gap-data card drove 30 action properties to the record). The two clauses on the advanced editor are written against the correct behaviour and recorded failing in knownGaps", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0] removed: the four Actions-tab defects QA run #26715 recorded are fixed on main — Expose to AI agents writes ai.exposed and switching Type clears the keys the new type refuses (e9e344a9), Add field value keeps its row and a script action stores type script (2720874a), and the console runs an untyped stored action as a script (27e4d656). acceptance[4] and [5] are now scored as written. Re-read at 3230bdbf. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.object-api-tab-curl", + "title": "The Data pillar's API tab is a truthful REST reference: five endpoints under /api/v1/data/OBJECT, an example body built from the object's draft fields that the write path accepts, and copied cURL commands that succeed once a token is put in", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with one published object carrying fields of several types (text, number, date, picklist, lookup)", + "the session's bearer token, read from the console's localStorage key auth-session-token (RUNNER.md: console session auth)" + ] + }, + "steps": [ + "open /_console/studio/PKG/data → the object → API: list the endpoints the tab shows (method + path) and read the create example body", + "add a field in the Form tab WITHOUT publishing; return to API and re-read the example body", + "publish; copy each endpoint's cURL (Copy cURL), replace with the session token, and run them in order: list, create (with the tab's example body), get, update, delete on the created id" + ], + "acceptance": [ + { + "clause": "the tab lists exactly the five CRUD endpoints for this object under /api/v1/data/OBJ — list, create, get, update, delete", + "oracle": "dom", + "verify": "after a screenshot confirms the tab rendered: methods GET / POST / GET / PATCH / DELETE on the object's path", + "evidence": "the endpoint list" + }, + { + "clause": "the example body follows the DRAFT fields — a field added but not yet published appears in it — and each value is one the write path accepts for its field type", + "oracle": "api", + "verify": "the re-read example carries the new field; after publish, the create cURL with that body answers 201", + "evidence": "the two example bodies + the create answer" + }, + { + "clause": "every copied cURL carries the Authorization header and, with the token substituted, succeeds against the server", + "oracle": "api", + "verify": "list 200, create 201, get 200, update 200, delete 200 (or 204) on the created record", + "evidence": "the five commands and their statuses" + } + ], + "negative": [ + "an example body the write path refuses (a value of the wrong type for its field) is a FAIL — the tab's promise is a body that works", + "running the create before publishing a draft-only field answers 400 for that field: that is the draft not being live, not this item's FAIL" + ], + "traps": [ + "auth-state-leak", + "stale-console-bundle" + ], + "automated": { + "kind": "unit", + "ref": "ui/packages/app-shell/src/views/studio-design/ObjectApiPanel.exampleBody-11803.test.tsx (the example value per field type): jsdom evidence for acceptance[1]'s value half; the live cURL runs stay the run's" + }, + "source": [ + "ui/packages/app-shell/src/views/studio-design/ObjectApiPanel.tsx (five endpoints per object; the example body derived from the object's DRAFT fields, objectui#11803; the copied cURL carries the Authorization header)", + "RUNNER.md environment facts (the console's bearer token in localStorage)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the Data pillar's API tab, from QA run #26715 (Data card D11 passed: five samples, cURL works with a token). Listed as uncovered", + "ref": "#26715" + } + ] + }, + { + "id": "studio-authoring.summary-field-authoring", + "title": "A summary (roll-up) field authored in Studio is held until it names its child object and function, offers only numeric child fields for sum and avg, and once published computes and recomputes from the children", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with a published parent object and a published child object holding a master_detail field to the parent and a number field (and a text field, for the numeric-only check)" + ], + "knownGaps": [ + "acceptance[0] AND [1] FAIL TODAY (QA run #26715, Data card defect 6, at 3fcafcd3; re-read at 3230bdbf). (1) An unconfigured summary field is not held: the Data pillar saves it, and the publish refuses it. Since 57820085 (#26707, closed) the draft read's _diagnostics carries that publish verdict beforehand, but the field is still saved, not held. (2) For sum and avg the field picker still offers every visible child field, numeric or not (ObjectFieldInspector SummaryConfigFields). No card is open for either; score the two clauses fail citing the run. The computing clauses pass" + ] + }, + "steps": [ + "on the parent in /_console/studio/PKG/data → Form → Add field, type Summary; stop before choosing anything and watch the network", + "configure it: child object = the child, function = count; save; then add a second summary with function sum and open its field picker", + "publish; create a parent and five children (POST /api/v1/data/CHILD, the master_detail field set), with numbers in the number field", + "GET the parent: read both summaries; then PATCH one child's number and DELETE another child; GET the parent again" + ], + "acceptance": [ + { + "clause": "an unconfigured summary field is held, not saved — like a lookup before its target", + "oracle": "network", + "verify": "no object PUT carries the summary until its child object and function are chosen, and a held line names what it needs", + "evidence": "the trace + the held line" + }, + { + "clause": "for sum and avg the field picker offers only the child's numeric fields", + "oracle": "dom", + "verify": "after a screenshot confirms the picker rendered, the text field is not offered and the number field is", + "evidence": "the picker DOM" + }, + { + "clause": "the configured summary is saved spec-valid: summaryOperations { object, field, function }", + "oracle": "api", + "verify": "GET /api/v1/meta/object/PARENT carries both summaries with the authored object, field and function", + "evidence": "the meta read" + }, + { + "clause": "published, the summaries compute from the children and recompute when a child changes or goes", + "oracle": "api", + "verify": "count = 5 and sum = the numbers' total after the creates; after the PATCH and DELETE both reflect the change", + "evidence": "the two parent reads" + } + ], + "negative": [ + "a summary saved unconfigured instead of held — refused only at publish, rolling back the rest of the package release with it — is the shape this item fails today (its draft verdict now reports it, #26707; the hold is still missing)", + "SCOPE: filtered roll-ups (summaryOperations.filter) at runtime are automation.rollup-summary-filter" + ], + "traps": [ + "automation-input", + "stale-console-bundle" + ], + "source": [ + "packages/spec/src/data/field.zod.ts (summaryOperations: object, field, function count | sum | min | max | avg, relationshipField, filter)", + "ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx (the summary options editor)", + "#26707 (closed by 57820085: the draft verdict now reports the publish refusal; the unconfigured summary was one instance)", + "automation.rollup-summary-filter (filtered roll-ups — cross-referenced)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — summary-field authoring in Studio, from QA run #26715 (Data card: summaryOperations count verified at runtime; defect 6 found the unconfigured field unheld and sum/avg offering non-numeric fields). Listed as uncovered; the two failing clauses are written against the correct behaviour", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0], negative[0] and source[2] re-read against main 3230bdbf: #26707 is closed by 57820085, which makes the draft read's _diagnostics report the publish verdict, but the Data pillar still saves an unconfigured summary rather than holding it, and the sum/avg picker still offers non-numeric fields — acceptance[0] and [1] still fail, now cited to the run alone. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.api-written-keys-survive-studio-save", + "title": "A Studio save never drops what another door wrote: keys a draft carries that Studio has no control for — written through the metadata API — are still in the draft, byte for byte, after an unrelated Studio edit autosaves the same item", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P1", + "surface": "mixed", + "personas": [ + "admin" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with one published item per variant below; the keys are written with PUT /api/v1/meta/TYPE/NAME?mode=draft&package=PKG" + ], + "knownGaps": [ + "THE PERMISSION VARIANT FAILS TODAY (QA run #26715, Access card defect D4, at 3fcafcd3): a Studio Access save rebuilds the set from the PUBLISHED body and copies back only the keys the editor manages, so a draft's description and packageId written through the API read null after the next Studio click. Score that variant fail citing card #26837. The object and field variants were measured preserved (15 keys through a Studio Settings autosave; every API-set field key through a field-inspector save)" + ] + }, + "steps": [ + "for each variant: PUT the item's draft through the API adding the keys listed (values that differ from the defaults); GET ?state=draft&package=PKG and keep the body", + "open the same item in Studio and make ONE unrelated edit the variant names; let the autosave land and capture its PUT body", + "GET the draft again and diff it against the kept body; then publish and read the live item" + ], + "acceptance": [ + { + "clause": "per variant, every API-written key is still in the draft after the Studio autosave, with its value unchanged", + "oracle": "api", + "verify": "the before/after draft diff shows only the Studio edit; no API-written key is missing or nulled", + "evidence": "the per-variant diff" + }, + { + "clause": "the Studio autosave's own PUT carries the API-written keys (it saves the whole body, not a rebuilt one)", + "oracle": "network", + "verify": "the captured PUT body contains each API-written key", + "evidence": "the PUT bodies" + }, + { + "clause": "published, the live item serves the keys as written", + "oracle": "api", + "verify": "GET /api/v1/meta/TYPE/NAME carries each key", + "evidence": "the live reads" + } + ], + "negative": [ + "a key silently dropped or nulled by a Studio save is the FAIL: the author of the other door sees a 200 and loses the value" + ], + "variants": [ + "object — titleFormat, indexes, userActions, editMode, searchableFields, publicSharing, imageField; Studio edit: Settings › Basic info label (measured preserved, QA run #26715)", + "field — searchable, rows, step, currencyConfig, sortable, widget, dueLike, useGrouping, maskingRule; Studio edit: the field inspector's Help text (measured preserved)", + "flow — description, successMessage, errorMessage, variables, errorHandling, requiredPermissions; Studio edit: a node label", + "app — description, branding, areas, contextSelectors; Studio edit: rename a navigation entry", + "dashboard — requiredPermissions; Studio edit: a widget title", + "permission — description, isDefault, allowExport, packageId; Studio edit: one object checkbox (FAILS today — knownGaps[0], #26837)" + ], + "traps": [ + "silent-coercion", + "stale-console-bundle" + ], + "source": [ + "QA run #26715 (Data card: 15 API-written object/field keys survived a Studio Settings autosave; gap-data card: a field-inspector save kept every API-set key; Access card D4: description / packageId lost)", + "ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx (the Access save, the failing variant)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — Studio saves preserve what other doors wrote, from QA run #26715 ('API-written props preserved across autosave' was listed as uncovered; measured for object and field, failing for permission). The flow, app and dashboard variants are drivable but not yet measured", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0] and variants[5] cite the open card for the permission variant, #26837 (Access card D4 of QA run #26715). Re-read at 3230bdbf. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.interfaces-create-dialogs", + "title": "The Interfaces create dialogs work end to end: Create app derives its identifier and scaffolds one menu entry per package object (none when the box is cleared), New dashboard / report / page write spec-valid drafts linked from the menu, a report without a dataset or a measure and an identifier already taken are refused in the dialog, and everything opens in the published app", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "mixed", + "personas": [ + "admin (authors)", + "admin as end user (opens the published app)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "two writable packages with NO app: P1 with two published objects, P2 with one", + "a dataset over a package object, with at least one dimension and one measure, written with PUT /api/v1/meta/dataset/NAME?mode=draft&package=PKG and published — Studio has no dataset editor (expected: the Interfaces card read it no-entry), so reports and widgets bind to an API-written dataset", + "a dashboard identifier and a page identifier already held elsewhere — a published one in another package and a draft — for the identifier-taken legs" + ] + }, + "steps": [ + "open /_console/studio/P1/interfaces: the pillar says the package has no app and offers Create app, and no New dashboard / report / page entry (there is no menu to link from)", + "Create app: type App name 'Repair Center' — the Identifier derives repair_center; leave 'Add this package's objects as menu items' ON (its default); Create. Read the toast ('App “Repair Center” saved as draft — open it after publishing') and where the surface lands", + "on P2, Create app with the box CLEARED; read its navigation", + "P1 → New dashboard (a fresh identifier): it saves a draft, adds a dashboard entry to the menu and opens on the dashboard canvas in design mode ('+ add widget')", + "New report: try Create with no dataset, then with a dataset but no measure — capture each refusal and the network; then choose the dataset and a measure and Create", + "New page: once as HTML and once as React; each opens its source beside the live preview", + "identifier taken: in New dashboard type the identifier a dashboard in another package holds, then one a dashboard draft holds; repeat for New page; capture each refusal and the network", + "publish P1 (Publish → Publish all); GET /api/v1/meta/app with no query, then ?id=repair_center; open the Home launcher; open the app and each new menu entry" + ], + "acceptance": [ + { + "clause": "Create app derives the identifier from the name and, box on, scaffolds one object entry per package object; box off, the navigation is empty; the dialog lands on Interfaces with the app open", + "oracle": "api", + "verify": "GET /api/v1/meta/app/repair_center?state=draft&package=P1 holds one { type: object, objectName } entry per P1 object; P2's app has none; the URL after Create is the Interfaces pillar", + "evidence": "the two draft reads + the landed URL" + }, + { + "clause": "New dashboard, report and page each save a spec-valid draft, link it from the menu with an entry of the matching type, and open it on its own canvas", + "oracle": "network", + "verify": "each create sends the item PUT (?mode=draft&package=P1, If-None-Match: *) and the app PUT carrying the new entry, both 200; the canvas shown is the dashboard, report or page canvas", + "evidence": "the PUT pairs + the canvas per kind" + }, + { + "clause": "a report with no dataset, or no measure, is refused in the dialog and nothing is written", + "oracle": "network", + "verify": "the dialog shows the refusal and no PUT leaves for either attempt", + "evidence": "the refusals + the empty trace window" + }, + { + "clause": "an identifier another package publishes, or a draft holds, is refused in the dialog for dashboards and pages, and nothing is written", + "oracle": "network", + "verify": "the four refusals; no PUT leaves", + "evidence": "the refusal texts + the trace" + }, + { + "clause": "published, the app is live: listed by /meta/app, on the Home launcher, and every scaffolded and created entry opens and renders", + "oracle": "screenshot", + "verify": "?id=repair_center returns the app with its entries; the launcher shows Repair Center; each entry renders its object list, dashboard, report or page without an error panel", + "evidence": "the meta read + one screenshot description per entry" + } + ], + "negative": [ + "a create that writes the item but not the menu entry (or the reverse) is a FAIL — the dialog promises both", + "SCOPE: the first-run loop through Create app is studio-authoring.first-run-loop; this item owns the dialogs' own contract (scaffold off, refusals, the other kinds)" + ], + "traps": [ + "automation-input", + "hydration-race", + "stale-console-bundle" + ], + "automated": { + "kind": "unit", + "ref": "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.interfacesCreate-11823.test.tsx (dashboard and report create, the no-dataset refusal, identifier-taken refusals, no create entry without an app) + StudioDesignSurface.interfacesCreatePage-11823.test.tsx (HTML and React pages) + StudioDesignSurface.createAppOpensInterfaces-11794.test.tsx: jsdom evidence for acceptance[0] to [3] at the component layer; the live writes and the published app stay the run's" + }, + "source": [ + "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (doCreateApp, doCreateSurface; the scaffold checkbox appAddObjects; objectui#11823, #11794, #2262)", + "ui/packages/app-shell/src/views/studio-design/CreateItemDialog.tsx (the one create form; identifier derivation)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the Interfaces create dialogs, from QA run #26715 (Interfaces card F1 and F5 passed: create app, dashboard, report with its refusals, HTML and React pages, identifier-taken refusals; the coordinator's H1 read the scaffold). Create-app scaffold, identifier refusals and report refusals were listed as uncovered", + "ref": "#26715" + } + ] + }, + { + "id": "studio-authoring.dashboard-widget-and-filter-types", + "title": "A package dashboard renders every widget type the spec declares — those Studio offers and those written through the API — and every global-filter type renders its control and re-scopes the widgets bound to it; Studio's two widget-type entries offer the same set", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin (authors, then views the published board)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with an app whose menu names one dashboard", + "a dataset over a package object, with at least one dimension and one measure, written with PUT /api/v1/meta/dataset/NAME?mode=draft&package=PKG and published — Studio has no dataset editor (expected: the Interfaces card read it no-entry), so reports and widgets bind to an API-written dataset. Give it several dimensions (a status-like category, a date, a source/target pair for sankey) and a few measures over seeded multi-bucket rows — examples/app-showcase/src/ui/dashboards/chart-gallery.dashboard.ts is the model for each type's bindings" + ], + "knownGaps": [ + "acceptance[4] FAILS TODAY (QA run #26715, Interfaces card D11, at 3fcafcd3; source re-read at 3230bdbf): the global-filter editor's Add item autosaves a filter with no field, the whole dashboard draft is then refused, and the other edits in it are lost on reload. Family card #26707 closed with #26856, which makes the draft read's _diagnostics and Problems report the refusal; the Studio form still autosaves the incomplete item, and no open card carries that half" + ] + }, + "steps": [ + "open the dashboard in /_console/studio/PKG/interfaces; read the Add-widget menu's type list and, on one widget, the Type picker's list", + "add one widget of every type the Add menu offers, bound to the dataset (a dimension and a measure each); the menu offers every ChartTypeSchema member since #26967, so for any type it does not list, record the missing type and write that widget into the same dashboard draft with PUT /api/v1/meta/dashboard/NAME?mode=draft&package=PKG (keep the Studio-authored widgets in the body)", + "global filters: add one filter of each type — text, select (with options or optionsFrom), date, number, lookup — and bind each to at least one widget through that widget's filter mapping; leave one widget unbound", + "publish; open the board in the app; screenshot it settled", + "change each filter on the rendered board in turn; record which widgets re-query (network) and how their values change; reset", + "in Studio add a global filter with Add item and leave it incomplete; watch the autosave and reload" + ], + "acceptance": [ + { + "clause": "every ChartTypeSchema member renders a real chart on the published board — sized marks, more than one bucket wherever the type groups — whether Studio or the API authored it", + "oracle": "screenshot", + "verify": "per type: a sized SVG/canvas (no width(-1)/height(-1) collapse) with marks; grouping types show several buckets; single-value types show their value; no error panel", + "evidence": "one line per type" + }, + { + "clause": "Studio's Add-widget menu and the Type picker offer the same set of widget types", + "oracle": "dom", + "verify": "after a screenshot confirms both rendered, the two lists compared", + "evidence": "the two lists" + }, + { + "clause": "every global-filter type renders its control on the board", + "oracle": "screenshot", + "verify": "text input, select, date picker, number input and lookup picker are each shown", + "evidence": "the filter bar screenshot described" + }, + { + "clause": "changing a filter re-scopes exactly the widgets bound to it, through their own field mapping, and leaves the unbound widget fixed", + "oracle": "network", + "verify": "per filter change, the bound widgets' dataset queries carry the filter and their values change; the unbound widget sends no new query and keeps its value", + "evidence": "the query trace per change" + }, + { + "clause": "Studio's global-filter editor never autosaves an incomplete filter: the item is held until complete, and no other edit is lost", + "oracle": "network", + "verify": "no dashboard PUT carries the incomplete filter; after a reload, the other edits are present", + "evidence": "the trace + the post-reload read" + } + ], + "negative": [ + "a type that renders an empty or zero-height frame over multi-bucket data is a FAIL (single-datapoint)", + "SCOPE: the chart-type matrix on the stock showcase gallery is dashboards.chart-type-matrix, and the stock Revenue Pulse re-scoping is dashboards.global-filters-rescope — this item proves a package-authored board reaches the same renderer" + ], + "variants": [ + "widget types (ChartTypeSchema, 20): bar, horizontal-bar, column, line, area, pie, donut, funnel, scatter, treemap, sankey, combo, gauge, solid-gauge, metric, kpi, bullet, radar, table, pivot — the Studio-less nine at 3fcafcd3: column, treemap, sankey, combo, gauge, solid-gauge, kpi, bullet, radar", + "global-filter types (GlobalFilterSchema.type, inline enum — not pinnable): text, select, date, number, lookup" + ], + "enumSource": { + "file": "packages/spec/src/ui/chart.zod.ts", + "export": "ChartTypeSchema", + "expect": 20 + }, + "traps": [ + "single-datapoint", + "seed-data-thin", + "hydration-race", + "stale-console-bundle" + ], + "source": [ + "packages/spec/src/ui/chart.zod.ts#ChartTypeSchema (the 20 widget types)", + "packages/spec/src/ui/dashboard.zod.ts#GlobalFilterSchema (type: text | select | date | number | lookup; optionsFrom; targetWidgets)", + "dashboards.chart-type-matrix, dashboards.global-filters-rescope (the stock-fixture twins — cross-referenced)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — a package dashboard across all 20 widget types and the 5 global-filter types, from QA run #26715 (Interfaces card: all 20 rendered through the API, Studio offering 11 / 10; the 5 filter types passed L3). Listed as uncovered", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0] removed: #26967 (card #26745) makes the Add-widget menu and the widget Type select read one registry, ChartTypeSchema.options, so both offer all 20 types; steps[1] now writes a type through the API only if the menu does not list it, and records that. The global-filter knownGap re-attributed: #26707 closed with #26856 (the draft verdict now reports the publish refusal), but the Studio form still autosaves the incomplete filter, so acceptance[4] still fails. Re-read at 3230bdbf. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.report-authoring-options", + "title": "Reports authored in Studio render every report type from the same dataset — tabular, summary, matrix and joined — and their runtimeFilter, order and chart take effect in the published app, while a joined report refuses a chart and a block with no dataset", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin (authors, then views the published report)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with an app", + "a dataset over a package object, with at least one dimension and one measure, written with PUT /api/v1/meta/dataset/NAME?mode=draft&package=PKG and published — Studio has no dataset editor (expected: the Interfaces card read it no-entry), so reports and widgets bind to an API-written dataset — two dimensions (for matrix rows and columns) and two measures" + ] + }, + "steps": [ + "in /_console/studio/PKG/interfaces create a report (New report: the dataset and a measure); on its canvas set the type to tabular, then summary with a row dimension, then matrix with a row and a column dimension; keep a copy of each as its own report", + "on the summary report set runtimeFilter to narrow the rows (e.g. one category value), order by the measure descending, and an embedded chart", + "author a joined report with two blocks, each bound to the dataset with its own rows and values", + "refusals: PUT a joined report carrying a chart, and one whose block has no dataset, with ?mode=draft&package=PKG; capture both", + "publish; open each report in the app; compare the summary's rows against the dataset aggregate with the same filter (POST the equivalent query)" + ], + "acceptance": [ + { + "clause": "each report type renders from the one dataset: tabular rows, summary groups, the matrix's row × column grid, and a joined report as one table per block", + "oracle": "screenshot", + "verify": "per report, the rendered layout matches its type with no error panel", + "evidence": "one screenshot description per type" + }, + { + "clause": "runtimeFilter scopes and order sorts the rendered rows; the embedded chart draws the same values", + "oracle": "api", + "verify": "the summary's rows equal the dataset aggregate under the same filter, in measure-descending order; the chart's buckets equal the rows", + "evidence": "the aggregate read beside the rendered rows" + }, + { + "clause": "a joined report refuses a chart and a block with no dataset, located, and stores nothing", + "oracle": "api", + "verify": "both PUTs answer 4xx naming chart, and the block's dataset, respectively; GET after each shows nothing new", + "evidence": "the two refusals + reads" + } + ], + "negative": [ + "a type that renders as a different type (a matrix drawn as a flat table), or a runtimeFilter that is stored but not applied, is a FAIL", + "SCOPE: every ReportType on stock datasets is dashboards.dataset-report-authoring; drill-through clicks are dashboards.drill-through-range" + ], + "variants": [ + "tabular", + "summary", + "matrix", + "joined" + ], + "enumSource": { + "file": "packages/spec/src/ui/report.zod.ts", + "export": "ReportType", + "expect": 4 + }, + "traps": [ + "single-datapoint", + "hydration-race", + "stale-console-bundle" + ], + "source": [ + "packages/spec/src/ui/report.zod.ts#ReportType (tabular | summary | matrix | joined); ReportSchema runtimeFilter, order, drilldown, chart (refused on joined), blocks (each needs a dataset)", + "dashboards.dataset-report-authoring, dashboards.drill-through-range (stock-fixture twins — cross-referenced)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — Studio-authored reports across the four types with runtimeFilter, order, chart and joined blocks, from QA run #26715 (Interfaces card: 13 of 13 report properties covered, all four types rendered). Listed as uncovered", + "ref": "#26715" + } + ] + }, + { + "id": "studio-authoring.app-and-nav-properties", + "title": "App and menu-entry keys reach the running app: branding, areas and context selectors render; an entry's viewName, recordId / recordMode, filters, badge, requiresObject and visible each change what the entry does or who sees it; hidden takes an app off the switcher and nothing else — and Studio's menu-entry inspector edits them", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin (authors)", + "member (sees the published app)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with an app and two published objects (with a few records and a second list view on one of them)", + "a member persona: POST /api/v1/auth/admin/create-user sending mustChangePassword: false (the door defaults it to true), granted a package permission set that reads the package objects, signed in through the form in its own browser context" + ], + "knownGaps": [ + "REQUIRESOBJECT HAS NO EDITOR (source read at 3230bdbf): #26977 (card #26740) gave Studio's menu-entry inspector controls for visible, viewName, recordId / recordMode, filters, badge / badgeVariant and order, but neither it nor Setup's AppNavInspector offers requiresObject, so steps[0] writes that key through the API and acceptance[5]'s 'as the metadata admin's app editor does' does not hold for it. Whether Studio must offer requiresObject is not ruled: score acceptance[5]'s requiresObject cell not judged, with this entry cited, and the other keys as written; requiresObject's runtime effect is acceptance[2]" + ] + }, + "steps": [ + "write the app through PUT /api/v1/meta/app/APP?mode=draft&package=PKG: branding { primaryColor, accentColor, logo }, two areas each holding some entries, a contextSelector, and menu entries: an object entry with viewName (the second list view), one with recordId + recordMode 'edit', one with filters, one with badge + badgeVariant, one with requiresObject naming an object that does not exist, and one with a visible predicate false for the member (e.g. 'org_admin' in current_user.positions); publish", + "as admin and as the member (each in its own context, identity proved with GET /api/v1/auth/get-session), open the app: read the chrome colours and logo, the areas switcher, the context selector, and click each entry", + "GET /api/v1/meta/app?id=APP as each persona and list the entries each receives", + "set hidden: true on a second app of the package (through the API) and publish; as the member read the App Switcher, the Home launcher, the avatar menu and GET /api/v1/meta/app?id=SECOND; open its URL directly", + "open the app in /_console/studio/PKG/interfaces, select each entry and read its inspector" + ], + "acceptance": [ + { + "clause": "branding, areas and the context selector render as authored", + "oracle": "screenshot", + "verify": "the chrome carries the primary / accent colours and the logo; the areas switcher shows both areas and each shows its own entries; the context selector renders", + "evidence": "the chrome screenshot described" + }, + { + "clause": "each entry key changes what the entry does: viewName opens that list view, recordId opens the record (in edit when recordMode is edit), filters narrow the list, badge shows its text in its variant", + "oracle": "dom", + "verify": "after a screenshot confirms each landing rendered: the active view, the record route and mode, the list's row count against the filtered data read, the badge text and class", + "evidence": "one line per entry" + }, + { + "clause": "requiresObject naming an object that is not registered hides the entry", + "oracle": "dom", + "verify": "the entry is absent from the rendered menu for both personas", + "evidence": "the menu DOM" + }, + { + "clause": "visible gates the entry per viewer: absent for the member whose predicate is false, present for the admin", + "oracle": "dom", + "verify": "the rendered menus compared per persona, each read in its own session", + "evidence": "the two menu lists" + }, + { + "clause": "hidden takes the app off the App Switcher and the launcher and nothing else: the avatar menu still lists it, GET /meta/app?id= still answers it for the member, and its URL opens", + "oracle": "api", + "verify": "the member's /meta/app?id=SECOND returns the app; the switcher and launcher omit it; the avatar menu lists it; the direct URL renders", + "evidence": "the meta read + the three surfaces described" + }, + { + "clause": "Studio's menu-entry inspector edits these keys — visible, viewName, recordId / recordMode, filters, badge / badgeVariant, requiresObject, order — as the metadata admin's app editor does", + "oracle": "dom", + "verify": "each key has an editable control in the Studio inspector, and an edit there autosaves it into the app draft", + "evidence": "the inspector DOM per key + one autosave PUT" + } + ], + "negative": [ + "an app whose hidden flag hides it from users who may use it — or shows it on the switcher — is a FAIL (the shape QA run #26715 recorded as #26712, at 3fcafcd3, before dbf7e04a retired the conversion)", + "SCOPE: the doc-type menu entry is studio-authoring.doc-on-app-menu; nav rendering on the stock showcase is platform-core.nav-surfaces-render" + ], + "traps": [ + "auth-state-leak", + "wrong-persona", + "hydration-race", + "stale-console-bundle" + ], + "source": [ + "packages/spec/src/ui/app.zod.ts (branding: primaryColor / accentColor / logo / favicon; areas; contextSelectors; hidden — 'navigation only, never an access gate'; the nav item keys viewName, recordId, recordMode, filters, badge, badgeVariant, requiresObject, visible, order)", + "packages/metadata-core/src/artifact-forward-conversion.ts (where the retired app-hidden-to-unpublished conversion stood; dbf7e04a removed it from packages/spec/src/conversions/registry.ts, so a stored hidden: true round-trips — #26712, closed as fixed)", + "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (the Studio menu-entry inspector)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — app and menu-entry keys from the API to the running app, from QA run #26715 (Interfaces card: branding, areas and context selectors L3 pass; viewName, recordId, filters, badge, requiresObject and visible L3 pass through the API; hidden fails, #26712; the Studio inspector lacks the entry keys). Listed as uncovered; the two failing clauses are written against the correct behaviour", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0] removed: card #26712 is closed as fixed — dbf7e04a (already an ancestor of this item's read base 43731031) retired the app-hidden-to-unpublished conversion, and the card's re-measure kept hidden: true with a member answered 200; source[1] and negative[0] re-pointed. The inspector knownGap narrowed: #26977 (card #26740) gave the Studio inspector every listed key but requiresObject, which neither Studio nor Setup's AppNavInspector edits; that cell is scored not judged until it is ruled whether Studio must offer it. Re-read at 3230bdbf. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.authored-page-slots-and-variables", + "title": "Pages authored in a package take the page model's full shape: a slotted record page renders its slot overrides on the object's records, and page variables of all six types drive visibleWhen on the rendered page — at first render and live as a writer changes them", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin (authors, then views the published pages)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with an app and one published object that has records and NO default record page (a second default answers 422 DEFAULT_RECORD_PAGE_CONFLICT)" + ] + }, + "steps": [ + "slotted page: PUT /api/v1/meta/page/NAME?mode=draft&package=PKG with type 'record', object OBJ, kind 'slotted', isDefault true and slots overriding highlights, details and discussion (details OR tabs, never both — the save door refuses the pair); publish; open a record of OBJ in the app", + "variables: on an app page of the package (created in Studio's Interfaces, New page, or written through the API) declare six variables, one per type — string, number, boolean, object, array, record_id — each with a default; add one element:text per variable whose visibleWhen reads it (e.g. page.flag == true, page.count > 2, size(page.items) > 0, page.obj.k == 'v', page.name == '', page.rec == ''), plus an element:text_input writing the string variable and an element:record_picker writing the record_id one", + "publish; open the page; screenshot the first render; then type into the text input and pick a record, re-reading after each without reloading" + ], + "acceptance": [ + { + "clause": "the slotted page is the record's page, and each overridden slot renders the authored blocks in place of the default slot", + "oracle": "screenshot", + "verify": "the record shows the authored highlights, details and discussion content; slots not overridden keep their defaults", + "evidence": "the record page screenshot described" + }, + { + "clause": "each of the six variable types initialises to its default and gates its block on first render as its visibleWhen says", + "oracle": "dom", + "verify": "after a screenshot confirms the page rendered, the six blocks' presence matches each predicate evaluated on the defaults", + "evidence": "the six presence reads" + }, + { + "clause": "a writer changes its variable and the dependent visibleWhen re-evaluates live, with no reload", + "oracle": "dom", + "verify": "typing flips the string-gated block, picking a record flips the record_id-gated block, in the same page load", + "evidence": "the before / after presence reads" + } + ], + "negative": [ + "a slot override saved but not rendered, or a variable whose block ignores its predicate, is a FAIL", + "SCOPE: the shipped showcase variable pages and their submit action are studio-authoring.page-variables-and-actions; the save door's slot-pair refusal is studio-authoring.save-door-runs-authoring-rules" + ], + "variants": [ + "string", + "number", + "boolean", + "object", + "array", + "record_id" + ], + "traps": [ + "automation-input", + "hydration-race", + "stale-console-bundle" + ], + "source": [ + "packages/spec/src/ui/page.zod.ts#PageVariableSchema (type: string | number | boolean | object | array | record_id; source = the writer component)", + "packages/spec/src/ui/page.zod.ts (kind full | slotted | html | react; slots header / actions / alerts / highlights / details / tabs / discussion, honoured only when kind is slotted)", + "studio-authoring.page-variables-and-actions, studio-authoring.save-door-runs-authoring-rules (cross-referenced)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — authored slotted pages and the six page-variable types, from QA run #26715 (Interfaces card: slots L3 pass through the API, variables of all six types L3 pass). Slotted pages and page-variable visibleWhen were listed as uncovered", + "ref": "#26715" + } + ] + }, + { + "id": "studio-authoring.access-permission-set-lifecycle", + "title": "Permission sets are created, found and locked honestly in Studio's Access pillar: the New dialog derives the identifier and refuses one already taken — published or draft — the rail searches and shows its empty states, and a read-only package offers no edit while the server refuses the forged write", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with two or three PUBLISHED objects and records owned by different users — while the objects are only drafts the Access matrix lists none of them (a defect QA run #26715 recorded), so publish first", + "the stock read-only package com.example.showcase and one of its packaged permission sets (e.g. showcase_contributor) for the read-only legs" + ] + }, + "steps": [ + "open /_console/studio/PKG/access on a package with no set: read the empty state ('No permission sets in this package yet.')", + "New permission set (+): type the display name 'Project Editor' — the identifier derives project_editor; Create; read the toast ('Permission set “Project Editor” created') and GET /api/v1/meta/permission/project_editor?state=draft&package=PKG", + "New permission set again with the identifier project_editor (a draft holds it): capture the refusal and the network", + "publish; then New permission set with the identifier project_editor once more (now published): capture what happens, and GET /api/v1/meta/permission/project_editor (live) and ?state=draft", + "rail search: type part of the label, part of the identifier, then a string nothing matches", + "read-only: switch to com.example.showcase → Access; read the banner and the affordances on a packaged set; then forge PUT /api/v1/meta/permission/showcase_contributor?mode=draft&package=com.example.showcase with a modified body, and a brand-new name into the same package; GET showcase_contributor before and after" + ], + "acceptance": [ + { + "clause": "the New dialog derives the identifier from the name and creates the set as a package draft", + "oracle": "api", + "verify": "the draft read exists under the derived identifier with the typed label, bound to PKG", + "evidence": "the draft read" + }, + { + "clause": "an identifier already taken is refused in the dialog — whether a draft holds it or the set is published — and nothing is written over it", + "oracle": "network", + "verify": "both attempts show a refusal and send no save that lands; afterwards the live set and its draft read are unchanged", + "evidence": "the two refusals + the before/after reads" + }, + { + "clause": "the rail searches label and identifier and shows a message, not a blank rail, when nothing matches; a package with no set shows its empty state", + "oracle": "dom", + "verify": "after a screenshot confirms the rail rendered: the filtered rows per query; the no-match state; the empty-package state", + "evidence": "the three DOM reads" + }, + { + "clause": "on a read-only package the pillar shows the read-only banner, offers no New set and no editing control", + "oracle": "screenshot", + "verify": "the banner names the package as read-only; no + button; the set's controls are disabled or absent", + "evidence": "the read-only screenshot described" + }, + { + "clause": "the server refuses the forged write — 403 for the packaged set (ITEM_LOCKED), 422 WRITABLE_PACKAGE_REQUIRED for a new name — and the packaged set is byte-identical afterwards", + "oracle": "api", + "verify": "status and code of both forged PUTs; the before/after reads of showcase_contributor diff empty", + "evidence": "the two refusals + the diff" + } + ], + "negative": [ + "a create that silently overwrites a published set with an empty one is the FAIL acceptance[1] exists for — every member of that set would lose its grants on the next publish", + "SCOPE: the object designer and matrix locks on a read-only package are access-security.readonly-package-locks-studio; the matrix editor's own UX is studio-authoring.permission-matrix-editor-ux" + ], + "traps": [ + "automation-input", + "stale-console-bundle" + ], + "source": [ + "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (the Access pillar: the New set dialog, the taken-identifier check findTakenIdentifier and CreateTakenNotice before the create's If-None-Match: *, discardDraft, the rail search and empty states)", + "ui/packages/i18n/src/namespaces/studio/en.ts (engine.studio.access.created / createTaken / createTakenElsewhere / emptyMain / search / bannerTitleReadOnly)", + "access-security.readonly-package-locks-studio (the sibling read-only surfaces — cross-referenced)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the permission-set lifecycle in Studio's Access pillar, from QA run #26715 (Access card G1 partial: the draft-duplicate refused, the published-duplicate not; G6 pass: read-only banner and forged writes 403). The create-set dialog and the read-only 403 were listed as uncovered", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0] removed: #26916 (card #26836) refuses a new set over an identifier any package publishes or a draft holds, naming the set instead of the first-write pin, and adds Discard draft; the rail's no-match message landed with #26959 (card #26733). source[1] re-pointed: the engine.studio.access strings moved to the studio i18n namespace (#26900, #26942). Re-read at 3230bdbf. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.access-facets-runtime", + "title": "The Access pillar's facet tabs author what the runtime enforces: System capabilities chips grant a capability a gated action checks, a Row-level security policy (with its Test dry-run) scopes a member's reads and writes, and Tab visibility hides an app — from the member's app list and from the launcher alike", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P1", + "surface": "mixed", + "personas": [ + "admin (authors the set)", + "member M (holds it)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with two or three PUBLISHED objects and records owned by different users — while the objects are only drafts the Access matrix lists none of them (a defect QA run #26715 recorded), so publish first", + "a permission set S of the package granting M read and write on one object OBJ, with S assigned to M (a sys_user_permission_set row), and a package action on OBJ declaring requiredPermissions ['qa_cap']", + "a member persona M: POST /api/v1/auth/admin/create-user sending mustChangePassword: false (the door defaults it to true), signed in through the form in its own browser context, identity proved with GET /api/v1/auth/get-session before each persona leg (auth-state-leak)" + ], + "knownGaps": [ + "acceptance[4]'s launcher half FAILS TODAY (QA run #26715, Access card D5, at 3fcafcd3; source re-read at 3230bdbf): the console's Home launcher reads GET /api/v1/meta/app and ignores tabPermissions, which only GET /api/v1/me/apps applies, so a hidden app's tile still shows and opens (no data is exposed: the app's data stays gated by the set's object grants). Score that half fail with the run cited. The typing defect the run also recorded (card D2) is fixed by #27025" + ] + }, + "steps": [ + "baseline as M: open a record of OBJ — the qa_cap action is absent, and POST /api/v1/actions/OBJ/ACTION answers 403; GET /api/v1/data/OBJ returns every row M may read", + "as admin: /_console/studio/PKG/access → S → System capabilities: add the chip qa_cap; let the autosave land and read the draft (systemPermissions)", + "Row-level security tab: add a policy on OBJ — name, operation read, USING a plain field comparison that keeps some rows (e.g. record.status == 'open'), enabled; open 'Test this policy against a sample record' and run it on a matching and a non-matching sample; add a second policy for update with a CHECK that refuses some value", + "Tab visibility tab: Add tab, TYPE the full app name and set Hidden; read the draft (tabPermissions)", + "publish; as M: re-read the record (the qa_cap action is shown) and run it; GET /api/v1/data/OBJ (only rows passing USING); PATCH a row to a value the CHECK refuses and to one it allows; GET /api/v1/me/apps; open the Home launcher", + "switch the RLS read policy off (enabled false), publish, re-read as M; then remove qa_cap, publish, and re-run the action as M" + ], + "acceptance": [ + { + "clause": "a capability chip writes systemPermissions and the runtime honours it both ways: with it M sees and runs the gated action; without it the action is hidden and its door answers 403", + "oracle": "api", + "verify": "the draft carries systemPermissions ['qa_cap']; after publish M's POST to the action answers 200; after removing it and publishing, 403 again", + "evidence": "the draft + the three action answers" + }, + { + "clause": "an RLS policy authored in the tab is stored as authored and its Test dry-run answers allow for the matching sample and deny for the other", + "oracle": "api", + "verify": "the draft's rowLevelSecurity entry carries name, object, operation, using / check and enabled; the Test dialog's outcomes", + "evidence": "the draft + the two outcomes" + }, + { + "clause": "published, the policy scopes M: reads return only rows passing USING, a write the CHECK refuses is refused, one it allows lands, and a disabled policy stops filtering", + "oracle": "api", + "verify": "M's row set against the data filtered by the same predicate; the two PATCH answers; the row set after enabled false", + "evidence": "the reads and writes per state" + }, + { + "clause": "Tab visibility takes the full app name typed into its input and writes tabPermissions { APP: 'hidden' }", + "oracle": "api", + "verify": "the draft's tabPermissions key is the whole app name", + "evidence": "the draft read" + }, + { + "clause": "a hidden app is gone for M everywhere the console lists apps: GET /api/v1/me/apps omits it, and the Home launcher shows no tile for it", + "oracle": "api", + "verify": "M's /me/apps list; the launcher screenshot described", + "evidence": "the list + the launcher description" + } + ], + "negative": [ + "a facet that saves but changes nothing at runtime is a FAIL — every clause pairs the authored value with its effect on M", + "SCOPE: the CEL editor's lint and the test-run engine parity are studio-authoring.expression-editors; RLS semantics on stock objects are access-security.rls-both-sides; capability declaration and resolution are access-security.capability-declaration-lifecycle" + ], + "traps": [ + "auth-state-leak", + "wrong-persona", + "automation-input", + "stale-console-bundle" + ], + "source": [ + "packages/spec/src/security/permission.zod.ts#tabPermissions (visible | hidden | default_on | default_off per app name); systemPermissions; rowLevelSecurity", + "ui/packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.tsx (the System capabilities, Row-level security and Tab visibility tabs; CelTestRunDialog)", + "ui/packages/i18n/src/namespaces/studio/en.ts (engine.studio.access.tab.capabilities / rls / tabs)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the Access facet tabs to their runtime effect, from QA run #26715 (Access card G3: capabilities and RLS with Test passed, Tab visibility failed on typing; D5 found the launcher ignoring hidden tabs). Tab visibility, RLS editor + Test, capabilities chips and hidden tabs vs launcher were listed as uncovered", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0] narrowed to the launcher defect: the tab-name input's one-character defect is fixed by #27025 (card #26748: rows keyed by position, TabNameInput buffers the typed name), so acceptance[3] has no known failure and steps[3] drops its paste fallback. source[2] re-pointed to the studio i18n namespace (#26900, #26942). Re-read at 3230bdbf. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.access-owd-and-suggestions", + "title": "The Access pillar's org-wide defaults panel and sharing suggestions do what they say: the OWD panel lists every package object, saves only the changed one and offers controlled-by-parent only with a master; a package's suggested set is granted to Everyone, revoked or dismissed from the pillar, with immediate effect", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin", + "member M (for the suggestion's runtime effect)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", + "a writable package with two or three PUBLISHED objects and records owned by different users — while the objects are only drafts the Access matrix lists none of them (a defect QA run #26715 recorded), so publish first, one of them a master-detail child of another", + "a package permission set marked isDefault: true (written through PUT /api/v1/meta/permission/NAME?mode=draft&package=PKG — Studio has no control for it) granting read on one object, so that publishing it raises an Everyone suggestion (ADR-0090 D5: package-level default sets become suggestions an admin confirms)", + "a member persona M: POST /api/v1/auth/admin/create-user sending mustChangePassword: false (the door defaults it to true), signed in through the form in its own browser context, identity proved with GET /api/v1/auth/get-session before each persona leg (auth-state-leak), holding no set of the package" + ], + "knownGaps": [ + "THE GUEST PROMPT CANNOT BE REACHED ON STOCK (QA run #26715, Access card G5): only Everyone suggestions exist, and accepting one for Guest answers 405; the pillar's Guest wording ('Cannot be granted here: the Guest position takes no sets from accepted suggestions.') is scored only where a guest suggestion exists — record that leg blocked(fixture) otherwise", + "TWO POLISH DEFECTS RECORDED BY THE RUN (Access card D10 and D13): an OWD refusal banner stays after the change is undone, and a dismissed or revoked suggestion offers no way to grant it again from Studio. Neither blocks a clause" + ] + }, + "steps": [ + "open /_console/studio/PKG/access → the org-wide defaults panel: read each package object's OWD, the master link on the child, and the controlled-by-parent option", + "change one object's OWD (private → public_read): read the unsaved state, save, and capture the PUT; then try controlled_by_parent on an object with no master — capture the refusal — and undo", + "set controlled_by_parent on the child; save; publish; GET /api/v1/meta/object/CHILD and the other objects", + "publish the isDefault set: GET /api/v1/security/suggested-bindings?packageId=PKG lists it pending; the pillar shows 'This package suggests granting \"SET\" to all signed-in users (Everyone).' with Grant and Dismiss", + "as M, GET /api/v1/data/OBJ (the set's object): refused; as admin click Grant; as M re-read", + "as admin click Revoke; as M re-read; then raise the suggestion again on a second set and Dismiss it; read the suggestion's status" + ], + "acceptance": [ + { + "clause": "the OWD panel lists every package object with its OWD and saves only the object changed, as a package-scoped draft", + "oracle": "network", + "verify": "one object PUT (?mode=draft&package=PKG) carrying the new sharingModel, none for the others", + "evidence": "the trace" + }, + { + "clause": "controlled_by_parent is offered with its master named, is refused on an object with no master, and is saved and published on the child", + "oracle": "api", + "verify": "the refusal for the master-less object (nothing written); the child's live read carries sharingModel controlled_by_parent with its master_detail field required", + "evidence": "the refusal + the live read" + }, + { + "clause": "a package default set appears as an Everyone suggestion in the pillar, and Grant takes effect at once: M reads the object; Revoke undoes it at once", + "oracle": "api", + "verify": "suggested-bindings status pending → accepted → revoked; M's GET refused → 200 → refused", + "evidence": "the status reads + M's three answers" + }, + { + "clause": "Dismiss leaves the set ungranted and says so", + "oracle": "api", + "verify": "the suggestion reads dismissed and the pillar shows the not-granted state; M is still refused", + "evidence": "the status read + M's answer" + } + ], + "negative": [ + "a Grant that toasts success while M is still refused, or a Revoke that leaves M reading, is a FAIL — the suggestion's whole value is its runtime effect", + "SCOPE: OWD semantics at runtime are access-security.owd-sharing-matrix; the OWD save gate at the door is access-security.owd-save-gate; the suggestion door's API contract is access-security.suggested-binding-loop — this item owns the Studio panels" + ], + "traps": [ + "auth-state-leak", + "wrong-persona", + "stale-console-bundle" + ], + "source": [ + "ui/packages/app-shell/src/views/studio-design/PackageOwdOverviewPanel.tsx (every package object with its OWD, draft-only included; saves only the changed object; the master link; objectui#12326 controlled-by-parent with a required master)", + "ui/packages/i18n/src/namespaces/studio/en.ts (engine.studio.access.suggestPromptEveryone / suggestConfirm / suggestDismiss / suggestRevoke / suggestGuestUnsupported)", + "packages/spec/src/security/permission.zod.ts (isDefault — ADR-0090 D5: package-level sets become install-time suggestions)", + "access-security.owd-sharing-matrix, access-security.owd-save-gate, access-security.suggested-binding-loop (cross-referenced)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the Access pillar's OWD panel and sharing-suggestions panel, from QA run #26715 (Access card G4 passed; G5 passed for Everyone with immediate runtime effect, Guest blocked by fixture). Both panels were listed as uncovered. Explain access is already covered by access-security.record-access-explain and is not duplicated", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "source[1] re-pointed: the engine.studio.access suggestion strings moved from metadata-admin/i18n.ts to ui/packages/i18n/src/namespaces/studio/en.ts (#26900, #26942). Re-read at 3230bdbf; the panel and both knownGaps are unchanged. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.no-entry-property-matrix", + "title": "Authorable keys Studio has no control for still work when another door writes them: per metadata type, each key written into a package draft through the metadata API is stored, served after publish and takes its runtime effect", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P2", + "surface": "mixed", + "personas": [ + "admin (writes the keys)", + "member (the runtime legs that need a non-admin: field requiredPermissions, fileAccessDelegate, defaultRowScope)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, with a console built from this tree with `pnpm console:build` for the rendering rows", + "a writable package with an app; author each probe on its OWN fresh scratch object, field or view, as QA run #26715 did, so one row's setting cannot colour another's reading", + "a member persona: POST /api/v1/auth/admin/create-user sending mustChangePassword: false, granted a package set reading and writing the scratch objects, signed in through the form in its own context", + "the showcase's external datasource showcase_external (examples/app-showcase/src/system/datasources/showcase-external.datasource.ts) for the datasource / external rows" + ], + "knownGaps": [ + "ROWS THAT FAIL TODAY (QA run #26715, at 3fcafcd3) — score each fail citing its record: the form-view keys buttons, layout:'horizontal', drawerSide and drawerWidth are stored and served but never rendered, and defaults / defaultTab / tabPosition apply only on some form surfaces (gap-view card); a list view's sharing.lockedBy is never shown (gap-view card). Fixed since the run, so no longer fails: field deleteBehavior 'restrict' under a master_detail parent (#26718), an object bound to an unknown datasource (#26912), a chart list view on another object's dataset (#26719)", + "NOT JUDGEABLE HERE: mailAssociation (needs a mailbox with inbound ingest), ackPlaintextMasking (its only reader is a build-time warning), lifecycle.ttl (the sweep is hourly — drive it only with time to wait for the next sweep), dimensions (inconclusive at 3fcafcd3). Record them not-run with this line cited", + "app / menu-entry keys are studio-authoring.app-and-nav-properties, page slots and variables studio-authoring.authored-page-slots-and-variables, the widget types studio-authoring.dashboard-widget-and-filter-types — not repeated here" + ] + }, + "steps": [ + "for each variant row: PUT /api/v1/meta/TYPE/NAME?mode=draft&package=PKG with the key set to a non-default value; GET ?state=draft&package=PKG and confirm it is stored byte-equal", + "publish the package (POST /api/v1/packages/PKG/publish-drafts); GET /api/v1/meta/TYPE/NAME and confirm it is served", + "drive the row's observable as the row names it, as admin or as the member, through /api/v1/data/*, the action and flow doors, or the rendered app; record the answer", + "for each row also note whether Studio offers a control (the census the item is named for); a control appearing moves the key out of this matrix, it is not a fail" + ], + "acceptance": [ + { + "clause": "every key is stored as written in the draft and served after publish", + "oracle": "api", + "verify": "per row: the draft read and the live read carry the key with the written value", + "evidence": "one line per row: draft value, live value" + }, + { + "clause": "every key takes the runtime effect its row names", + "oracle": "api", + "verify": "per row: the observable named in the variant, read from the server (a status, a stored column, a run, a served field) — or, for a rendering row, from the rendered app after a screenshot confirms it settled", + "evidence": "one line per row: the observable and its value" + }, + { + "clause": "a key the runtime cannot honour is refused at save or publish, never accepted and then silently inert", + "oracle": "api", + "verify": "the failing rows of knownGaps[0] each show the gap; any further row that stores and serves a key with no effect is a new finding", + "evidence": "the per-row verdicts" + } + ], + "negative": [ + "a key the draft door stores, publish serves and the runtime ignores is the PD#10 class (declared, not enforced) — a FAIL of acceptance[1] or [2], filed, never ticked on the stored value", + "SCOPE: preservation of these keys across a Studio save is studio-authoring.api-written-keys-survive-studio-save" + ], + "variants": [ + "object.managedBy 'append-only' → POST to the data API refused 403 (engine-owned), DELETE refused", + "object.ownership 'org' / 'business_unit' → created rows carry no owner_id / only owning_business_unit_id", + "object.systemFields { audit: false } / { tenant: false } → no created_by / updated_by columns / no organization_id", + "object.datasource showcase_external + object.external { remoteName, writable: false } → reads return the remote rows; a POST is refused 403 EXTERNAL_WRITE_FORBIDDEN; an unknown datasource id is refused 422 INVALID_METADATA at datasource, at the draft save and at publish", + "object.attachedOnRead → an action visible predicate reading the attachment publishes; one reading an undeclared attachment key is refused at publish", + "object.fileAccessDelegate (a delegate service that is not registered) → a non-uploader's download refused 403 FILE_DOWNLOAD_DENIED; the control object without a delegate answers 200", + "object.activityMilestones → the transition writes a sys_activity row with the milestone text", + "object.displayNameField → the record label in activity rows and the detail title is that field", + "object.listViews → the end-user tab shows the named list view with its columns and filter", + "object.sharing.controlledByParent.ownerMayEdit true → the detail owner may update (200); the control child without it answers 403", + "object.defaultRowScope + object.membershipKeys → the member sees only rows in its membership set; an out-of-scope PATCH answers 404", + "object.userActions / editMode / searchableFields / titleFormat / imageField / indexes / publicSharing → each changes the record list, form or search as its key describes (QA run #26715 Data card verified userActions, editMode and searchableFields at L3)", + "field.format (autonumber) → the stored value follows the pattern (GD-{0000} → GD-0003)", + "field.storage { notNull: true } → an insert without the value refused 400", + "field.multiple (lookup) → the value stores an id array and renders as several chips", + "field.useGrouping false → 12345 rendered without separators beside a sibling showing 12,345", + "field.accept / field.maxSize (file) → the server refuses another media type (400) and an oversize file", + "field.referenceVia → the detail renders the referenced record card", + "field.inlineEdit / inlineTitle / inlineColumns / inlineAmountField (master_detail) → the parent form shows the titled line-item grid with those columns and the total", + "field.relatedListColumns / relatedListFilter → the Related tab shows those columns and excludes rows the filter rejects", + "field.dueLike → a past date renders as overdue", + "field.widget 'textarea' → the field renders as a textarea", + "field.internal → the key is omitted from reads, even when selected explicitly", + "field.requiredPermissions → a member without the capability gets the value omitted on read and a write refused 403; a holder reads and writes it", + "field.maskingRule 'phone' → the value is masked in the API and UI, and saving the form does not write the mask back", + "field.searchable / rows / step / currencyConfig / sortable / relatedList / relatedListTitle / lookupColumns / language → each changes its control or list as its key describes (QA run #26715 Data card)", + "field.deleteBehavior 'restrict' → deleting a referenced parent refused 409 DELETE_RESTRICTED, also when the parent is the same child's master_detail target (#26718, fixed by #26864)", + "flow.description / successMessage / errorMessage → served; a manual run answers with successMessage; a failing run carries errorMessage", + "flow.variables (defaultValue) → a step reading the variable writes its default", + "flow.requiredPermissions → a manual start without the capability refused 403", + "flow.errorHandling { strategy, maxRetries } → maxRetries 2 yields three failed attempts", + "flow.nodes[].inputSchema → stored and served", + "view.list: searchableFields, userFilters, resizable, compactToolbar, selection, pagination, rowHeight, grouping, rowColor, hiddenFields, fieldOrder, rowActions, bulkActions, conditionalFormatting, inlineEdit, exportOptions (needs an export grant), appearance, addRecord, showRecordCount, allowPrinting, emptyState → each renders as its key describes (gap-view card, 3fcafcd3)", + "view.form: groups, subforms, submitBehavior, modalSize, allowSkip, showStepIndicator, splitDirection / splitSize / splitResizable → render as described; buttons, layout, drawerSide, drawerWidth: FAIL today (knownGaps[0]); defaults, defaultTab, tabPosition: partial", + "dashboard.requiredPermissions → a viewer without the capability is refused the dashboard", + "dataset (Studio has no dataset editor): include, dimensions with nullLabel, measures count / sum / avg / filtered / ratio, currency format → a report bound to it shows the null bucket labelled and each measure's value", + "permission.description / isDefault / allowExport / rowLevelSecurity[].positions → the Setup list shows the description; isDefault raises a suggestion; allowExport → export 200 with it, 403 EXPORT_NOT_PERMITTED without; positions limit the policy to holders of the position" + ], + "traps": [ + "silent-coercion", + "wrong-persona", + "auth-state-leak", + "seed-data-thin", + "stale-console-bundle" + ], + "source": [ + "QA run #26715 — the property-matrix pass of the Data, Automations, Interfaces and Access cards and the two gap-fill cards (object / field / hook / action and view): every top-level authorable key of Studio's metadata types, classed L1 (Studio control) / L2 (stored) / L3 (runtime effect)", + "packages/spec/src/data/object.zod.ts, packages/spec/src/data/field.zod.ts, packages/spec/src/automation/flow.zod.ts, packages/spec/src/ui/view.zod.ts, packages/spec/src/ui/dataset.zod.ts, packages/spec/src/security/permission.zod.ts (the keys)", + "packages/spec/liveness/*.json (each key's liveness grade — a key graded live with no effect here is a finding)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the no-Studio-entry property matrix, from QA run #26715: every authorable key Studio offers no control for, written through the metadata API into a package draft, published and driven to its runtime effect, per type. Rows are the keys the run measured at L3, with the failing rows recorded against their records", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[0], variants[3] and variants[26] follow three fixes on main (re-read at 3230bdbf). The restrict-vs-master_detail delete now refuses 409 DELETE_RESTRICTED (#26718, fixed by #26864 and #26930), an object naming an unknown datasource is refused 422 INVALID_METADATA at datasource at save and publish (#26912), and a list-view chart over another object's dataset is refused at publish (#26719, fixed by #26842 and #26906). The gap-view rows still fail. No clause changed", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.removed-key-save-door-refusal", + "title": "A removed metadata key is refused at the save door at its real location, with a prescription, and nothing is stored — for the 33 keys QA run #26715 drove across nine types — whether the key still carries its tombstone or its retirement kit has aged out", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P1", + "surface": "api", + "personas": [ + "admin" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin", + "a writable package (the save door judges a ?package= draft as Studio writes it) and a clean baseline draft of each type — object, hook, action, view, app, page, dashboard, flow, permission — that saves 200 BEFORE any removed key is added, so a refusal is the key's and not the body's" + ], + "knownGaps": [ + "TWO REFUSAL SHAPES AT HEAD, BOTH CORRECT (read at 43731031). QA run #26715, at 3fcafcd3, saw all 33 refused with the per-key sentence '… was removed in @objectstack/spec N … use … / delete …'. Since #26599 the retirement kits that retired at or below 16.x have aged out: 22 of the 33 keys no longer carry a tombstone and are refused as an ordinary unknown key, whose message names the nearest declared spelling and ends with the protocol-upgrade-guide sentence (packages/spec/src/shared/protocol-upgrade-guide.ts#RETIRED_KEY_UPGRADE_GUIDE_SENTENCE). variants mark which shape each key answers at HEAD; a key moving from one shape to the other is not a fail" + ] + }, + "steps": [ + "for each type, PUT the clean baseline draft (?mode=draft&package=PKG) and confirm 200", + "for each variant row, PUT the baseline with the removed key added at the location the row names; capture status, code, the issue path and the message", + "after each refusal GET the draft (?state=draft&package=PKG) and confirm it is still the baseline", + "junk-key control: PUT the view baseline with a top-level key zzz_junk_key: true, then the object, page and dashboard baselines with the same key; read each stored draft" + ], + "acceptance": [ + { + "clause": "every removed key is refused 422 INVALID_METADATA at its real path, with a prescription: the per-key removal sentence where the key still carries its tombstone, or the unknown-key refusal naming the protocol upgrade guide where its kit aged out", + "oracle": "api", + "verify": "per row: status 422, code INVALID_METADATA, an issue whose path is the key's location, and a message carrying either 'was removed in @objectstack/spec' or the upgrade-guide sentence — never a bare unrecognized-key line with no way forward", + "evidence": "one line per key: status, code, path, the message's prescription half" + }, + { + "clause": "nothing is stored: each refused save leaves the baseline draft unchanged", + "oracle": "api", + "verify": "the draft read after each refusal equals the baseline", + "evidence": "the paired reads" + }, + { + "clause": "an unknown top-level key is refused on every type's envelope alike — the view envelope included — never answered 200 and dropped", + "oracle": "api", + "verify": "the junk-key PUT answers 422 unrecognized_keys for view as for object, page and dashboard, and no stored draft lacks a key the author sent while the save answered 200", + "evidence": "the four answers + the stored drafts" + } + ], + "negative": [ + "a 200 for a removed key — stored, or silently stripped — is the FAIL: the author (or an AI) reads success while the key is discarded", + "SCOPE: retired keys at parse and build (query and error-envelope tombstones, the retired filter operators) are api-backend.enforce-or-remove-authoring-gates; the save door's authoring LINT rules are studio-authoring.save-door-runs-authoring-rules" + ], + "variants": [ + "object.externalSharingModel — tombstone at HEAD", + "field.conditionalRequired (→ requiredWhen) — aged out: unknown-key refusal", + "hook.timeout — tombstone", + "action.execute (→ target) — aged out", + "action.shortcut — aged out", + "action.bulkEnabled — aged out", + "action.aria — tombstone", + "view.owner (view item envelope) — tombstone", + "view.hidden (view item envelope) — tombstone", + "view list config: filterableFields — tombstone", + "view list config: tabs — tombstone", + "view list config: pageName — tombstone", + "view list config: responsive — aged out", + "view list config: performance — aged out", + "view list config: striped — aged out", + "view list config: bordered — aged out", + "view list config: virtualScroll — aged out", + "form view config: defaultSort — aged out", + "app.version — aged out", + "app.homePageId — aged out", + "app.objects — aged out", + "app.apis — aged out", + "app.sharing — aged out", + "app.embed — aged out", + "app.mobileNavigation — aged out", + "app.aria — aged out", + "page.assignedProfiles — tombstone", + "dashboard.refreshInterval (→ refreshIntervalSeconds) — tombstone", + "dashboard.aria — aged out", + "dashboard.performance — aged out", + "flow.template — aged out", + "flow.active — aged out", + "permission.managedBy — tombstone" + ], + "traps": [ + "silent-coercion", + "destructive-in-place" + ], + "source": [ + "packages/spec/src/shared/retired-key.ts#retiredKey (the tombstone and its per-key prescription)", + "packages/spec/src/shared/protocol-upgrade-guide.ts#RETIRED_KEY_UPGRADE_GUIDE_SENTENCE (the sentence the strict unknown-key refusal carries once a kit ages out, #26599 / ADR-0087 amendment 2026-10-26)", + "packages/spec/src/data/object.zod.ts, packages/spec/src/data/hook.zod.ts, packages/spec/src/ui/action.zod.ts, packages/spec/src/ui/view.zod.ts, packages/spec/src/ui/page.zod.ts, packages/spec/src/ui/dashboard.zod.ts, packages/spec/src/security/permission.zod.ts (the 11 tombstones still present at 43731031)", + "#26711 (the view envelope stripped unknown top-level keys instead of refusing; fixed by #26851, which closed the view item record and the inline list and form members)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — removed-key refusal at the metadata save door across nine types, from QA run #26715 (coordinator: 33 of 33 keys refused at their real location with a prescription, at 3fcafcd3). Re-read at 43731031, after #26599 aged out 22 of those tombstones: the item accepts either prescription shape and marks each key's shape at HEAD. The view-envelope strip (#26711) is probed against the correct behaviour", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps[1] removed and source[3] put in the past tense: card #26711 is fixed on main by #26851 (re-read at 3230bdbf), so the view envelope refuses an unknown top-level key 422 unrecognized_keys like the object, page and dashboard envelopes. The 11 tombstones and the 22 aged-out keys are unchanged since 43731031. acceptance[2] is unchanged and is now expected to pass", + "ref": "#26848" + } + ] + }, + { + "id": "studio-authoring.uninstalled-package-refused", + "title": "A write that names a package nobody installed is refused, for every metadata type as it already is for flows: the save door answers WRITABLE_PACKAGE_REQUIRED and stores nothing, publish-drafts on that id promotes nothing, and Studio opened on it says the package does not exist instead of rendering a writable builder", + "since": "v18", + "status": "active", + "revision": 2, + "priority": "P1", + "surface": "mixed", + "personas": [ + "admin" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build`", + "a package id that no package carries, e.g. com.example.nosuch — confirm GET /api/v1/packages/com.example.nosuch answers 404 first" + ] + }, + "steps": [ + "control: PUT /api/v1/meta/flow/qa_ghost_flow?mode=draft&package=com.example.nosuch with a valid flow body; capture status, code and message", + "the same named-package save for an object (a valid body with sharingModel), a dashboard and a page, each once with ?mode=draft and once as an active save; capture each answer, then GET each name with ?state=draft&package=com.example.nosuch and without", + "POST /api/v1/packages/com.example.nosuch/publish-drafts; capture the answer; GET the names live", + "GET /api/v1/meta/diagnostics and read stats..packages for the phantom id", + "open /_console/studio/com.example.nosuch and /_console/studio/com.example.nosuch/data; screenshot each settled; then open /_console/studio//bogus_pillar", + "teardown: DELETE every name the run stored (DELETE /api/v1/meta/TYPE/NAME?package=com.example.nosuch, and ?state=draft for drafts) and re-read until each answers 404" + ], + "acceptance": [ + { + "clause": "control: a flow saved into an uninstalled package is refused 422 WRITABLE_PACKAGE_REQUIRED, naming the package, and nothing is written", + "oracle": "api", + "verify": "status 422, code WRITABLE_PACKAGE_REQUIRED, the message names com.example.nosuch and says nothing was written; the read-back answers 404", + "evidence": "the refusal + the read-back" + }, + { + "clause": "object, dashboard and page saves naming the uninstalled package are refused the same way — draft and active alike — and nothing is stored", + "oracle": "api", + "verify": "each save answers 4xx (WRITABLE_PACKAGE_REQUIRED, as for flow) and each read-back answers 404", + "evidence": "one line per save + its read-back" + }, + { + "clause": "publish-drafts on an uninstalled package id promotes nothing, and no phantom package appears in the diagnostics stats", + "oracle": "api", + "verify": "the publish answers 404 (or a refusal) and every name stays 404 live; stats..packages lists no com.example.nosuch", + "evidence": "the publish answer + the live reads + the stats excerpt" + }, + { + "clause": "Studio opened on a package that does not exist shows that it does not exist, with a way back — never a writable builder; an unknown pillar segment does not silently render another pillar", + "oracle": "screenshot", + "verify": "the not-found state for both URLs (no New object, no Create app); the bogus pillar URL is not shown as the Interfaces pillar with the bogus URL kept", + "evidence": "the screenshots described" + } + ], + "negative": [ + "a 200 for a write bound to a package that does not exist is the FAIL: the row is invisible to the package doors that would govern it, and it skips the namespace gate on the way in", + "SCOPE: the read-only package refusals (WRITABLE_PACKAGE_REQUIRED and ITEM_LOCKED on com.example.showcase) are studio-authoring.first-run-loop's negative probe and access-security.readonly-package-locks-studio; multi-package row addressing is studio-authoring.multi-package-item-rows" + ], + "traps": [ + "silent-coercion", + "destructive-in-place", + "hydration-race" + ], + "source": [ + "packages/metadata-protocol/src/protocol.ts (uninstalledNamedBaseRefusal: the named-base rule — a base the write names must be a package the registry holds as installed — asked by saveMetaItem for every type since #26920; tenantAuthoredWriteRefusal, the flow arm of the 2026-09-30 ruling on #20863, calls the same helper)", + "docs/adr/0070-package-first-authoring.md (the docs link the refusal carries)", + "#26708 (the save and publish doors accepted an uninstalled package id for every type but flow, and Studio rendered a builder for it; fixed by #26920, which also answers publish-drafts on an unknown id 404 RESOURCE_NOT_FOUND)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-27", + "change": "new — the uninstalled-package refusal across metadata types, from QA run #26715 (coordinator A5, sharpened by the removed-key pass: flow refused, object / dashboard / page admitted, published and live; #26708 filed). Written against the correct behaviour with the flow door as the passing control", + "ref": "#26715" + }, + { + "revision": 2, + "date": "2026-10-28", + "change": "fixtures.knownGaps removed and source[0], source[2] re-pointed: card #26708 is fixed on main by #26920 (re-read at 3230bdbf). Every type's named-package save now asks uninstalledNamedBaseRefusal and answers 422 WRITABLE_PACKAGE_REQUIRED with nothing written, publish-drafts on an unknown id answers 404 RESOURCE_NOT_FOUND, and Studio renders 'Package not found' and a not-found state for an unknown pillar. The clauses already stated this behaviour and are unchanged", + "ref": "#26848" + } + ] } ] } diff --git a/docs/qa/platform-checklist/coverage.json b/docs/qa/platform-checklist/coverage.json index 97d46c28fa1..0957050ea2e 100644 --- a/docs/qa/platform-checklist/coverage.json +++ b/docs/qa/platform-checklist/coverage.json @@ -10,7 +10,8 @@ "api-backend.action-activation-door-contract", "automation.setup-packaged-automation-board", "platform-core.activation-ledger-registration-home", - "records-forms.script-action-hook-refusal-toast" + "records-forms.script-action-hook-refusal-toast", + "studio-authoring.actions-tab-presets" ] }, "agent": { @@ -40,7 +41,9 @@ "access-security.me-permissions-aggregation-parity", "search.command-palette-navigation", "platform-core.marketplace-install-local-lifecycle", - "platform-core.marketplace-console-honesty" + "platform-core.marketplace-console-honesty", + "studio-authoring.interfaces-create-dialogs", + "studio-authoring.app-and-nav-properties" ] }, "assignment_rule": { @@ -84,13 +87,17 @@ "dashboards.chart-first-paint", "dashboards.empty-null-bucket-boundaries", "studio-authoring.packaged-display-class-direct-edit", - "search.command-palette-navigation" + "search.command-palette-navigation", + "studio-authoring.dashboard-widget-and-filter-types", + "studio-authoring.uninstalled-package-refused" ] }, "dataset": { "items": [ "dashboards.dataset-report-authoring", - "api-backend.aggregate-contract-matrix" + "api-backend.aggregate-contract-matrix", + "studio-authoring.report-authoring-options", + "studio-authoring.no-entry-property-matrix" ] }, "datasource": { @@ -130,7 +137,11 @@ "api-backend.aggregate-contract-matrix", "api-backend.formula-stdlib-matrix", "approvals.status-mirror-field", - "cli.dev-automigrate-policy" + "cli.dev-automigrate-policy", + "studio-authoring.summary-field-authoring", + "studio-authoring.no-entry-property-matrix", + "studio-authoring.api-written-keys-survive-studio-save", + "studio-authoring.removed-key-save-door-refusal" ] }, "flow": { @@ -151,7 +162,12 @@ "studio-authoring.packaged-automation-studio-lock", "approvals.email-action-token-door", "approvals.approver-resolution-matrix", - "approvals.status-mirror-field" + "approvals.status-mirror-field", + "studio-authoring.automations-rail-and-new-flow", + "studio-authoring.flow-edge-type-authoring", + "studio-authoring.org-scope-flows", + "studio-authoring.flow-status-matches-runtime", + "studio-authoring.uninstalled-package-refused" ] }, "hook": { @@ -159,7 +175,8 @@ "records-forms.object-hook-lifecycle", "cli.hook-body-extraction-gates", "access-security.record-view-read-audit", - "records-forms.script-action-hook-refusal-toast" + "records-forms.script-action-hook-refusal-toast", + "studio-authoring.hooks-tab-authoring" ] }, "job": { @@ -203,7 +220,14 @@ "api-backend.api-methods-verb-gate", "integration-system.external-schema-browser-ui", "search.command-palette-navigation", - "platform-core.lifecycle-retention-sweep" + "platform-core.lifecycle-retention-sweep", + "studio-authoring.held-edit-leave-guard", + "studio-authoring.held-edit-publish-refusal", + "studio-authoring.object-api-tab-curl", + "studio-authoring.no-entry-property-matrix", + "studio-authoring.api-written-keys-survive-studio-save", + "studio-authoring.removed-key-save-door-refusal", + "studio-authoring.uninstalled-package-refused" ] }, "page": { @@ -211,7 +235,10 @@ "studio-authoring.record-page-roundtrip", "platform-core.nav-surfaces-render", "search.command-palette-navigation", - "platform-core.marketplace-console-honesty" + "platform-core.marketplace-console-honesty", + "studio-authoring.interfaces-create-dialogs", + "studio-authoring.authored-page-slots-and-variables", + "studio-authoring.uninstalled-package-refused" ] }, "permission": { @@ -228,11 +255,18 @@ "access-security.share-link-capability-tokens", "access-security.platform-owner-email-anchor", "access-security.me-permissions-aggregation-parity", - "api-backend.api-methods-verb-gate" + "api-backend.api-methods-verb-gate", + "studio-authoring.access-permission-set-lifecycle", + "studio-authoring.access-facets-runtime", + "studio-authoring.access-owd-and-suggestions" ] }, "picklist": { - "waived": "WAIVED 2026-09-30 — there is no runtime behaviour to drive yet, measured rather than asserted. The spec layer landed (#19518, addbbf02ab): PicklistSchema, `Field.select({ picklist })`, PicklistServedFieldSchema, `picklistExtensions` and the `picklists.NAME` translation face. No runtime reads a picklist: packages/spec/liveness/picklist.json grades every key `planned`, and scripts/check-stack-collection-maps.mjs carries `picklists` and `picklistExtensions` as rows PENDING the runtime layer (#19519) in the METADATA_ARRAY_KEYS and ARTIFACT_FIELD_TO_TYPE registration maps. No code resolves a picklist onto a field or judges a write against one, so an item written to RUN today could only assert absence. The item that will carry this kind is authored as `planned`: records-forms.picklist-shared-across-objects (phase 1: two objects, the package extension, the write door, the locale relabel; the phase-2 per-organization overlay is ruled out, #22869, and no item carries it). It is not listed under `items`, because a kind whose only items are planned is UNMAPPED and an entry cannot hold a waiver and items together. WHAT RETIRES THIS WAIVER — #19519 lands resolve-at-serve, the additive merge and write validation; records-forms.picklist-shared-across-objects gains its steps, runs, passes and is promoted to `active`, and the same edit replaces this waiver with `items` naming it. ⚠️ Re-audit it every sweep, as SWEEP.md asks of every waiver." + "$comment": "Re-audited 2026-10-27 (the checklist-author pass on QA run #26715, framework 43731031): the 2026-09-30 waiver is STALE and is replaced by items, as SWEEP.md asks of every waiver. Its premise was that no runtime reads a picklist, packages/spec/liveness/picklist.json grading every key planned. At 43731031 that ledger grades 10 of its 11 keys live, verified 2026-10-01: the ObjectQL registry resolves a list onto every field that names it and merges picklistExtensions additively (packages/objectql/src/picklist-resolution.ts; the name row's evidence is packages/objectql/src/registry.ts, resolvePicklistOptions), and an ACTIVE item drives that runtime end to end: studio-authoring.picklist-bound-select (since v17.7) reads GET /meta/picklist, the served options with the extension's option, the bound field's PUT round-trip and both objects' forms. What the mapping does NOT claim: that item's fixture is an uncommitted checkout edit (the stock showcase declares no picklist and the kind is code-only, so no runtime door mints one), and QA run #26715 scored it blocked(fixture) because the run did not make the edit; the coverage is real but costs a run that edit. records-forms.picklist-shared-across-objects stays PLANNED and is listed as the map target the waiver named; it contributes zero, and its promotion still needs a passing run of its own.", + "items": [ + "studio-authoring.picklist-bound-select", + "records-forms.picklist-shared-across-objects" + ] }, "position": { "items": [ @@ -264,7 +298,9 @@ "items": [ "dashboards.dataset-report-authoring", "dashboards.drill-through-range", - "search.command-palette-navigation" + "search.command-palette-navigation", + "studio-authoring.interfaces-create-dialogs", + "studio-authoring.report-authoring-options" ] }, "rest_api": { @@ -316,7 +352,8 @@ "items": [ "access-security.write-path-guards", "records-forms.conditional-rules-header", - "records-forms.validation-rule-type-matrix" + "records-forms.validation-rule-type-matrix", + "studio-authoring.validation-rules-authoring" ] }, "view": { @@ -328,7 +365,9 @@ "records-forms.gantt-interactions", "records-forms.saved-view-management", "studio-authoring.packaged-display-class-direct-edit", - "records-forms.field-history-tracking" + "records-forms.field-history-tracking", + "studio-authoring.no-entry-property-matrix", + "studio-authoring.removed-key-save-door-refusal" ] }, "webhook": {