diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index 05adf4e36fc..6d738552d7b 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -2307,7 +2307,7 @@ "title": "Editing the permission matrix and publishing flips an affected persona's live API access; revoke flips it back; assign/unassign moves access", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "mixed", "personas": [ @@ -2372,8 +2372,7 @@ ], "negative": [ "an edit that repaints the matrix but whose PUT/publish never lands (M's access unchanged) is a FAIL — the network + M's live access are the oracles, not the checkbox paint", - "enforcement flipping on the DRAFT alone (before publish) is wrong: access must key on the PUBLISHED set", - "cross-ref: enforcement correctness is access-security.crud-permission-matrix and the read-only-package lock is access-security.readonly-package-locks-studio — this item is the WRITE/edit loop only" + "enforcement flipping on the DRAFT alone (before publish) is wrong: access must key on the PUBLISHED set" ], "variants": [ "object-verb flip (CRUD)", @@ -2391,7 +2390,8 @@ "packages/rest/src/rest-route-ledger.ts#REST_ROUTE_LEDGER (PUT /api/v1/meta/:type/:name saveItem; POST .../publish publishItem — re-pointed #18104: saveItem is a CLIENT METHOD name, carried in this file only inside a string value)", "packages/metadata-protocol/src/sys-metadata-repository.ts#promoteDraft (the publish-with-nothing-pending refusal: status 404, code NO_DRAFT)", "examples/app-showcase/src/security/permission-sets.ts", - "cross-ref access-security.crud-permission-matrix + access-security.readonly-package-locks-studio, ADR-0090 D1/D5, ADR-0033" + "cross-ref access-security.crud-permission-matrix + access-security.readonly-package-locks-studio, ADR-0090 D1/D5, ADR-0033", + "cross-ref: enforcement correctness is access-security.crud-permission-matrix and the read-only-package lock is access-security.readonly-package-locks-studio — this item is the WRITE/edit loop only" ], "history": [ { @@ -2413,6 +2413,12 @@ "date": "2026-10-31", "change": "steps[2], [4] and [5] said 'save (draft) then publish'. The console Save on the Setup door lands active, as QA run #27441 measured. Source agrees: PermissionMatrixEditor.tsx's saveVersioned call adds mode 'draft' only when the editor is mounted with a packageId (Studio's Access pillar), and Setup's metadata-admin door mounts it with none, so its Save is a plain PUT that writes state 'active'. The steps now say Save, and that the publish call then answers 404 NO_DRAFT. acceptance[1] already scored both paths and is unchanged. Read from source at base efdddc9060. Wave anchor #27377, wave-3 accuracy slice S3; finding parked in anchor comment 6490438726", "ref": "#27441" + }, + { + "revision": 6, + "date": "2026-11-01", + "change": "negative[2] moved to source[6]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3087,7 +3093,7 @@ "title": "OS_PLATFORM_OWNER_EMAIL platform-admin anchor: only a VERIFIED stored-email match confers PLATFORM_ADMIN — unset, unverified match, non-match and malformed lists each confer nothing, fail closed", "since": "v17", "status": "active", - "revision": 12, + "revision": 13, "priority": "P1", "surface": "api", "personas": [ @@ -3153,8 +3159,7 @@ "negative": [ "no runtime write reaches the anchor: OS_PLATFORM_OWNER_EMAIL is declared a NON-TABLE derivation input (admin-standing-surface.ts) — revocation is a configuration change plus a process roll, by design. Any in-product surface found mutating who is a platform administrator is an authorization finding: record it under RUNNER rule 2's carve-out (no card; one run-record row `area.item · clause · security finding — detail withheld, reported to PM`), never with a reproduction", "the derivation reads the caller's OWN STORED sys_user row, never the session-seeded grants.email — the ⭐ P1 pin of the leg. That property is asserted by the unit pin (the '⭐ … reads the STORED row, never the seeded email' describe); do not attempt to re-prove it against a live server", - "run every refused probe as the seeded admin too: a 200 proves each refusal keyed on the member's standing, not on a broken route (wrong-persona, both sides)", - "out of scope here: the WALLED-posture boot refusal when the variable is unset (#11184) is plugin-auth's separate door — do not score it on this item" + "run every refused probe as the seeded admin too: a 200 proves each refusal keyed on the member's standing, not on a broken route (wrong-persona, both sides)" ], "variants": [ "unset (zero config-derived admins)", @@ -3171,7 +3176,7 @@ ], "automated": { "kind": "dogfood", - "ref": "packages/qa/dogfood/test/platform-owner-email-anchor.dogfood.test.ts (#26571) restarts one showcase deployment per variable value on one database file, in the steps' order, with the datasource admin routes mounted as serve mounts them; probes are GET /datasources and GET /auth/me/permissions as member M and as the seeded admin. acceptance[0] — unset: M 403 PERMISSION_DENIED and no manage_platform_settings, the admin 200 on the grant anchor; acceptance[1] — the variable names unverified M: the same refusal; acceptance[2] — it names an address no row holds: the same, the admin still 200; acceptance[4]'s live leg — it names M after a system-context email_verified stamp (the knownGaps doorway): M 200 and every ADMIN_FULL_ACCESS_CAPABILITIES systemPermission; acceptance[3] — 'M, not an address' with M still verified and holding no admin_full_access grant: the [authz] refusal is printed once, naming the entry verbatim and the zero-administrator consequence, M is refused and the admin served; negative[0] — ADMIN_STANDING_NON_TABLE_INPUTS (@objectstack/core) declares {kind:'env', name:'OS_PLATFORM_OWNER_EMAIL'}; negative[2] — every refused probe has the admin's 200 beside it. A fresh database booted with a well-formed variable promotes only the declared owner (bootstrap-platform-admin.ts), so the variants run on one database first booted unset, as the steps restart one deployment. Unit pins beneath: packages/core/src/security/resolve-authz-context.platform-admin-config.test.ts (the acceptance criterion, all four fail-closed arms, the ⭐ stored-row-not-seed pin, the #11663 L5 posture split of the legacy grant anchor, the '[ADR-0131 D5] under `single` the Default Organization's admin_full_access grant is the anchor' describe) + packages/core/src/security/platform-admin.test.ts (parse/refusal/memo) + packages/core/src/security/admin-standing-surface.test.ts. acceptance[5] and negative[1] are test-scored; negative[3] is out of scope" + "ref": "packages/qa/dogfood/test/platform-owner-email-anchor.dogfood.test.ts (#26571) restarts one showcase deployment per variable value on one database file, in the steps' order, with the datasource admin routes mounted as serve mounts them; probes are GET /datasources and GET /auth/me/permissions as member M and as the seeded admin. acceptance[0] — unset: M 403 PERMISSION_DENIED and no manage_platform_settings, the admin 200 on the grant anchor; acceptance[1] — the variable names unverified M: the same refusal; acceptance[2] — it names an address no row holds: the same, the admin still 200; acceptance[4]'s live leg — it names M after a system-context email_verified stamp (the knownGaps doorway): M 200 and every ADMIN_FULL_ACCESS_CAPABILITIES systemPermission; acceptance[3] — 'M, not an address' with M still verified and holding no admin_full_access grant: the [authz] refusal is printed once, naming the entry verbatim and the zero-administrator consequence, M is refused and the admin served; negative[0] — ADMIN_STANDING_NON_TABLE_INPUTS (@objectstack/core) declares {kind:'env', name:'OS_PLATFORM_OWNER_EMAIL'}; negative[2] — every refused probe has the admin's 200 beside it. A fresh database booted with a well-formed variable promotes only the declared owner (bootstrap-platform-admin.ts), so the variants run on one database first booted unset, as the steps restart one deployment. Unit pins beneath: packages/core/src/security/resolve-authz-context.platform-admin-config.test.ts (the acceptance criterion, all four fail-closed arms, the ⭐ stored-row-not-seed pin, the #11663 L5 posture split of the legacy grant anchor, the '[ADR-0131 D5] under `single` the Default Organization's admin_full_access grant is the anchor' describe) + packages/core/src/security/platform-admin.test.ts (parse/refusal/memo) + packages/core/src/security/admin-standing-surface.test.ts. acceptance[5] and negative[1] are test-scored; the out-of-scope note that was negative[3] is source[10]" }, "source": [ "packages/core/src/security/resolve-authz-context.ts#hasPlatformAdminStanding (§6b-config — the config anchor inside the ONE derivation site; additive, never subtractive) + (hasPlatformAdminStanding, the id-shaped projection)", @@ -3183,7 +3188,8 @@ "packages/types/src/env.ts#PLATFORM_OWNER_EMAIL_ENV + packages/types/src/env.ts#resolvePlatformOwnerEmail (the variable's name, and its live per-call read)", "packages/plugins/plugin-auth/src/auth-plugin.ts (#11343 — the dev seed stamps its admin's address verified)", "packages/services/service-datasource/src/admin-routes.ts#DATASOURCE_ADMIN_CAPABILITY (the manage_platform_settings-floor probe)", - "#11663 L2 (the design and ruled bundle), #13146 (landed 2026-08-29), ADR-0068 D2 (the stored-grant anchor beside it)" + "#11663 L2 (the design and ruled bundle), #13146 (landed 2026-08-29), ADR-0068 D2 (the stored-grant anchor beside it)", + "out of scope here: the WALLED-posture boot refusal when the variable is unset (#11184) is plugin-auth's separate door — do not score it on this item" ], "history": [ { @@ -3257,6 +3263,12 @@ "date": "2026-10-29", "change": "RUNNER rule 2's security-finding carve-out re-worded (#27382; maintainer rulings of 2026-10-29, recorded on #27377): a security finding gets no GitHub card at all, the run record carries the one row `area.item · clause · security finding — detail withheld, reported to PM`, and the reproduction goes privately to the dispatching PM session. negative[0] quoted the retired 'detail withheld pending maintainer' row and now names the new one. The same PR re-words every item string that quoted the retired row. No clause, oracle or status changed", "ref": "#27642" + }, + { + "revision": 13, + "date": "2026-11-01", + "change": "negative[3] moved to source[10]: it was a scope note (the walled-posture boot refusal is plugin-auth's door, not scored here), which no run can decide, so it could only score skipped. Its text is unchanged; automated.ref's closing note, which called negative[3] out of scope, now points at source[10]. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3402,7 +3414,7 @@ "title": "Opt-in record-view auditing writes `read` rows for exactly the opted-in objects' detail reads, batched off the request path — stock boot audits nothing, excluded names refuse loudly, values never copied", "since": "v17", "status": "active", - "revision": 6, + "revision": 7, "priority": "P2", "surface": "api", "personas": [ @@ -3467,8 +3479,7 @@ ], "negative": [ "an audit write must never break the read: reads answer 200 even when the ledger write fails — the failure is reported once per CAUSE with the durability text (read-audit.ts; #18247): a second, different cause reports again at error, and a repeat of a cause already reported degrades to debug. Rows are LOST, not retried. Observing that needs a broken ledger and is out of scope for a stock run; do not fabricate it", - "a read with no principal (no userId and no actor) lands no row — noise in the WHO ledger is refused by design (read-audit.ts)", - "cross-ref access-security.audit-log-browser — WRITE auditing, the browser UI, and the append-only guard belong there; this item owes only the read-writer's contract (one ledger, two items, no double-scoring). The append-only negative there covers action:'read' rows too: user-context POSTs to sys_audit_log are refused regardless of action" + "a read with no principal (no userId and no actor) lands no row — noise in the WHO ledger is refused by design (read-audit.ts)" ], "variants": [ "stock boot (nothing audited)", @@ -3485,13 +3496,14 @@ ], "automated": { "kind": "dogfood", - "ref": "packages/qa/dogfood/test/record-view-read-audit.dogfood.test.ts (#26571) boots the showcase twice with the AuditPlugin serve would end up running (bootStack constructs none), reads rows through GET /data/sys_audit_log as the admin and keeps the 2 s flush. Stock (new AuditPlugin()): acceptance[0] — the boot log carries INFO lines and AuditPlugin lines (the positive control) and no 'record-view auditing installed' line; a member's GET /data/showcase_account/:id answers 200 and past the flush window the ledger holds no action 'read' row. Configured (readAudit.objects ['showcase_account', 'sys_job'], the docs' opt-in): acceptance[1] and [4] — one install line, 'on 1 object(s) — showcase_account', without sys_job, and the exclusion warning naming sys_job; acceptance[1] and [2] — one detail read lands exactly one row with action 'read', user_id = M, object_name, record_id, old_value and new_value null, and created_at inside the read's wall-clock window (before the flush could have run); acceptance[3] — a filtered LIST read that serves the record leaves the ledger count unchanged; acceptance[1] — an un-opted showcase_project detail read (200) lands no row. Unit pins beneath: packages/plugins/plugin-audit/src/read-audit.test.ts (discriminator, batching/flush, isSystem + no-principal skips, exclusion filter) + audit-plugin.test.ts. acceptance[5] stays test-scored; negative[0] needs a broken ledger and is out of scope for a stock run; negative[1] is the unit pin's no-principal skip; negative[2] is a cross-reference" + "ref": "packages/qa/dogfood/test/record-view-read-audit.dogfood.test.ts (#26571) boots the showcase twice with the AuditPlugin serve would end up running (bootStack constructs none), reads rows through GET /data/sys_audit_log as the admin and keeps the 2 s flush. Stock (new AuditPlugin()): acceptance[0] — the boot log carries INFO lines and AuditPlugin lines (the positive control) and no 'record-view auditing installed' line; a member's GET /data/showcase_account/:id answers 200 and past the flush window the ledger holds no action 'read' row. Configured (readAudit.objects ['showcase_account', 'sys_job'], the docs' opt-in): acceptance[1] and [4] — one install line, 'on 1 object(s) — showcase_account', without sys_job, and the exclusion warning naming sys_job; acceptance[1] and [2] — one detail read lands exactly one row with action 'read', user_id = M, object_name, record_id, old_value and new_value null, and created_at inside the read's wall-clock window (before the flush could have run); acceptance[3] — a filtered LIST read that serves the record leaves the ledger count unchanged; acceptance[1] — an un-opted showcase_project detail read (200) lands no row. Unit pins beneath: packages/plugins/plugin-audit/src/read-audit.test.ts (discriminator, batching/flush, isSystem + no-principal skips, exclusion filter) + audit-plugin.test.ts. acceptance[5] stays test-scored; negative[0] needs a broken ledger and is out of scope for a stock run; negative[1] is the unit pin's no-principal skip; the cross-reference that was negative[2] is source[4]" }, "source": [ "packages/plugins/plugin-audit/src/read-audit.ts#READ_AUDIT_ACTION (READ_AUDIT_ACTION, installReadAuditWriter opt-in + exclusion filter, extractDetailReadId, buildRow nulls + view instant, isSystem/no-principal skips, batcher)", "packages/plugins/plugin-audit/src/audit-plugin.ts#writer (wiring readAudit options into the writer; the install log line)", "content/docs/permissions/record-view-auditing.mdx ('Under os serve' — the option-less auto-instance audits no views; last-one-wins supersession is the opt-in path; no metadata key / stack key / env flag by design)", - "#8992 (the card and the 2026-08-16 maintainer ruling: Option A scoped MVP — detail views only, per-object closed opt-in, async batched writes)" + "#8992 (the card and the 2026-08-16 maintainer ruling: Option A scoped MVP — detail views only, per-object closed opt-in, async batched writes)", + "cross-ref access-security.audit-log-browser — WRITE auditing, the browser UI, and the append-only guard belong there; this item owes only the read-writer's contract (one ledger, two items, no double-scoring). The append-only negative there covers action:'read' rows too: user-context POSTs to sys_audit_log are refused regardless of action" ], "history": [ { @@ -3529,6 +3541,12 @@ "date": "2026-10-27", "change": "automated.kind unit -> dogfood (#26571 slice 3; the slice-2 ACCEPT noted it): revision 5's ref already leads with a dogfood pin over the in-process HTTP stack, with the unit pins beneath. No ref, clause or status changed", "ref": "#26571" + }, + { + "revision": 7, + "date": "2026-11-01", + "change": "negative[2] moved to source[4]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged, and automated.ref's closing note, which called negative[2] a cross-reference, now points at source[4]. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 37b73a5b84c..e740579b77f 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -1293,7 +1293,7 @@ "title": "The bell badge and Home's action card agree on the pending-approval count and lead to an app the user can open", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "browser", "personas": ["a non-admin pending approver", "an admin with >9 combined unread notifications + pending approvals"], @@ -1304,7 +1304,8 @@ "enough seeded sys_inbox_message rows plus pending approvals that the combined total exceeds 9, so the CLAMP case is the one exercised (a total of 9 or less never proves the breakdown is unclamped)" ], "knownGaps": [ - "[STALE premise corrected — #7344 closed 2026-08-11] a plain member's notification count is NOT structurally 0: a member holding only the baseline sets reads its own notifications (GET /api/v1/notifications?view=mine 200), so the >9 case is reachable for a non-admin. Measured on main c91af8ed6: seed it by routing approvals to the member through a runtime user-routed flow (POST /api/v1/automation, record-triggered on showcase_field_zoo; DELETE it afterwards). Notifications of one type COALESCE in the popover ('Approval requested ×9' counts as one), so drive the total past 9 with pending approvals: at 10 pending + 2 notification groups the button read 9+ and the header 12 total" + "[STALE premise corrected — #7344 closed 2026-08-11] a plain member's notification count is NOT structurally 0: a member holding only the baseline sets reads its own notifications (GET /api/v1/notifications?view=mine 200), so the >9 case is reachable for a non-admin. Measured on main c91af8ed6: seed it by routing approvals to the member through a runtime user-routed flow (POST /api/v1/automation, record-triggered on showcase_field_zoo; DELETE it afterwards). Notifications of one type COALESCE in the popover ('Approval requested ×9' counts as one), so drive the total past 9 with pending approvals: at 10 pending + 2 notification groups the button read 9+ and the header 12 total", + "the bell's 'View all notifications' and Home's 'View all activity' links are OUT of this item's scope and are known to route into /apps/setup/... for every persona — tracked at objectui#4074 (moved from #7266), browser-confirmed by the #7331 run; do not record them here" ] }, "steps": [ @@ -1335,8 +1336,7 @@ } ], "negative": [ - "a popover header that clamps its own breakdown (e.g. '9+ notifications') is a FAIL — the clamp belongs to the button badge; the popover exists to say which N and which M produced it", - "the bell's 'View all notifications' and Home's 'View all activity' links are OUT of this item's scope and are known to route into /apps/setup/... for every persona — tracked at objectui#4074 (moved from #7266), browser-confirmed by the #7331 run; do not record them here" + "a popover header that clamps its own breakdown (e.g. '9+ notifications') is a FAIL — the clamp belongs to the button badge; the popover exists to say which N and which M produced it" ], "traps": ["hydration-race", "single-datapoint", "wrong-persona"], "source": [ @@ -1347,7 +1347,8 @@ "history": [ { "revision": 1, "date": "2026-08-10", "change": "initial — the bell breakdown (objectui#4073) and Home's card target had no checklist coverage", "ref": "#7331" }, { "revision": 2, "date": "2026-10-17", "change": "knownGaps[0] said a plain member's notification count is structurally 0, so the >9 case was admin-only (#7344); #7344 closed on 2026-08-11 and the #23878 run saw a member read 2 notifications. Re-run on main c91af8ed6 with the console at objectui f8267df93aba as a baseline-only member seeded with 10 pending approvals and 2 notification groups: the bell button read 9+, the popover header '12 total · 2 notifications + 10 pending approvals', Home's Awaiting approval card 10, the inbox's My Pending tab 10 and the approvals API 10; the card's View all opened /_console/apps/com.example.showcase/system/approvals inside the Showcase app. The knownGap, step 1 and clause 3's verify now say so", "ref": "#24036" }, - { "revision": 3, "date": "2026-10-20", "change": "steps[0] respelled: 'boot 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-20", "change": "steps[0] respelled: 'boot 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": 4, "date": "2026-11-01", "change": "negative[1] moved to fixtures.knownGaps[1]: it was a scope note (the bell's and Home's 'View all' links are out of this item's scope), which no run can decide, so it could only score skipped; it carries the tracked objectui#4074 routing defect, so it lands with the known gaps rather than as a source cross-ref. Its text is unchanged. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries). No clause changed. Wave anchor #27377", "ref": "#28345" } ] }, { diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 79d3a0ed07b..1af9d3d5ea7 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -1023,7 +1023,7 @@ "title": "Notification center (bell / InboxPopover): badge counts distinct unread topics + pending approvals, repeats coalesce, per-group and global mark-read drop the badge", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": ["seeded admin (admin@objectos.ai / admin123)"], @@ -1095,8 +1095,7 @@ "negative": [ "a badge showing raw unread ROW count (inflating to 9+ off one recurring topic) is the #2765 regression — FAIL", "mark-read firing one POST per row for a coalesced group (instead of one {ids} request) is a FAIL of the coalescing contract", - "read-state reverting to unread on the next poll for a keyed row (writing sys_notification_receipt through the generic data API, which rejects it — ADR-0103) is a FAIL", - "cross-ref: this is distinct from i18n.notification-localized-and-clears (single-entry localization + clear) and approvals.notification-deep-link (deep-link) — do not double-count their coverage here" + "read-state reverting to unread on the next poll for a keyed row (writing sys_notification_receipt through the generic data API, which rejects it — ADR-0103) is a FAIL" ], "traps": ["hydration-race", "single-datapoint"], "source": [ @@ -1105,7 +1104,8 @@ "objectui packages/app-shell/src/layout/AppHeader.tsx (postMarkRead → POST /api/v1/notifications/read[/all]; poll GET /notifications?view=mine)", "packages/runtime/src/route-ledger.ts (GET /notifications, POST /notifications/read, POST /notifications/read/all)", "packages/runtime/src/domains/notifications.ts#handleNotificationRequest (markRead upserts sys_notification_receipt; empty/mis-keyed ids → 400 — re-pointed #18104: markRead is a PROVIDER METHOD this file only calls off the inbox capability, a member access; the POST /notifications/read branch that validates the body and issues it is in handleNotificationRequest)", - "ADR-0030 (sys_inbox_message L5 materialization + sys_notification_receipt); ADR-0103 (receipt object api-method lockdown)" + "ADR-0030 (sys_inbox_message L5 materialization + sys_notification_receipt); ADR-0103 (receipt object api-method lockdown)", + "cross-ref: this is distinct from i18n.notification-localized-and-clears (single-entry localization + clear) and approvals.notification-deep-link (deep-link) — do not double-count their coverage here" ], "history": [ { "revision": 1, "date": "2026-08-08", "change": "initial — bell/InboxPopover: topic-based badge (#2765), (topic,title) coalescing, per-group + global mark-read on the wire, approvals tab, receipt persistence; cross-referenced with i18n + approvals notification items", "ref": "claude/platform-test-checklist-ocwugl" }, @@ -1121,6 +1121,12 @@ "date": "2026-10-31", "change": "steps[5] and acceptance[3].verify name a group the scheduled digest cannot race. Checklist-accuracy finding of QA run #27441 (18.3.0 wave 3, parked on anchor #27377; slice S4, #28338): the per-minute showcase_scheduled_digest (examples/app-showcase/src/automation/flows/index.ts, a 60-second interval) can land a new row in the group between the per-group mark-read and the badge read, so the badge does not drop. The clause is unchanged: it is still one POST for the group and a one-topic drop. Re-read at 3fd80337", "ref": "#27441" + }, + { + "revision": 5, + "date": "2026-11-01", + "change": "negative[3] moved to source[6]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3609,7 +3615,7 @@ "title": "Settings on v18: a global-scope key is stored in sys_platform_setting (readable through /data only with manage_platform_settings), and under a walled posture one organization's tenant save or lock does not change or lock another's, with a data-API read of sys_setting filtered to readable namespaces", "since": "v18", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -3632,7 +3638,7 @@ "steps": [ "as platform admin write a global-scope key through PUT /api/settings/NAMESPACE; GET /api/v1/data/sys_platform_setting and /sys_setting?filter scope=global", "as the member GET /api/v1/data/sys_platform_setting", - "walled: as A's admin save a tenant key (PUT /api/settings/NAMESPACE); lock A's row with the fixture write of fixtures.requires[2] (knownGaps[1]) and re-read the key as A (locked: true); as B's admin read and write the same key", + "walled: as A's admin save a tenant key (PUT /api/settings/NAMESPACE); lock A's row with the fixture write of fixtures.requires[2] (knownGaps[1]) and re-read the key as A (locked: true); then as A's admin save the locked key again (negative[0]); as B's admin read and write the same key", "walled: write a tenant key with no organization in scope" ], "acceptance": [ @@ -3661,6 +3667,9 @@ "evidence": "the answer" } ], + "negative": [ + "walled: LOCKED means platform-locked — after steps[2]'s lock, A's admin (an organization administrator, not the platform admin) saving the same key again through PUT /api/settings/NAMESPACE must be refused, the way a lock refuses a lower rung's save (409 SETTINGS_LOCKED, packages/services/service-settings/src/settings-routes.ts), with A's value unchanged and the row still locked on re-read; a 2xx, or a re-read showing the new value, is a security FAIL. Unprovisioned walled fixtures make it blocked(fixture), like acceptance[2] and acceptance[3]" + ], "traps": [ "wrong-persona", "cache-staleness" @@ -3704,6 +3713,12 @@ "date": "2026-10-20", "change": "steps[2]'s tenant lock made a named fixture prerequisite (fixtures.requires[2], new knownGaps[1]), and knownGaps[0]'s prerequisite sentence says os dev — QA run #24783 (findings F6 and F2), round 11 of wave anchor #24579. No HTTP door sets a tenant-row lock: sys_setting answers update 405 (apiMethods get/list) and the settings PUT writes values only, so the run stamped the lock with a system write on its scratch DB. A bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md), so the refusal sentence now names os dev. No clause changed", "ref": "#24801" + }, + { + "revision": 6, + "date": "2026-11-01", + "change": "negative[0] added, with steps[2] driving it: LOCKED means platform-locked, so an organization administrator's save of its own locked tenant row must be refused, and a save that lands is a security FAIL. Ruling 6545938831 item ⑤ on #28345, which settles the semantics; the clause is a walled leg like acceptance[2] and [3]. No existing clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index bbcb415cc36..945d245fa83 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -3572,7 +3572,7 @@ "title": "Select-all-matching covers every matching id across pages (server total, not the rendered page); clear resets both selection sources", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P2", "surface": "browser", "personas": [ @@ -3585,7 +3585,8 @@ "the bulk actions above, so the BulkActionBar and both its aggregate and per-record dispatch are exercisable (objectui plugin-grid BulkActionBar)" ], "knownGaps": [ - "AUTOMATION LIVES IN THE objectui TREE, WHICH IS ui/ IN THIS CHECKOUT — `automated.ref` names objectui specs exclusively, by objectui-relative paths that resolve under ui/ (a unit suite at ui/packages/…, a Playwright spec at ui/e2e/…). They run from that tree: a unit suite with cwd ui/ (pnpm exec vitest run FILE), a live spec only against a booted stack serving a console built by pnpm console:build. Drive the item by hand in the browser and score it as a manual run, OR run the pin from ui/ and cite its output naming the framework commit it ran at. ⛔ 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, whose 'none of those specs exist here' predates the ui/ import." + "AUTOMATION LIVES IN THE objectui TREE, WHICH IS ui/ IN THIS CHECKOUT — `automated.ref` names objectui specs exclusively, by objectui-relative paths that resolve under ui/ (a unit suite at ui/packages/…, a Playwright spec at ui/e2e/…). They run from that tree: a unit suite with cwd ui/ (pnpm exec vitest run FILE), a live spec only against a booted stack serving a console built by pnpm console:build. Drive the item by hand in the browser and score it as a manual run, OR run the pin from ui/ and cite its output naming the framework commit it ran at. ⛔ 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, whose 'none of those specs exist here' predates the ui/ import.", + "OPEN defect, acceptance[5] expected FAIL until it is fixed — #28552: the banner draws the uncapped server total (ui/packages/plugin-grid/src/components/BulkActionBar.tsx, the grid.bulkSelectAllMatching, grid.bulkAllMatchingSelected and grid.bulkSelectedAllMatches strings, each counting totalMatching) while resolveBulkRows stops at its local HARD_CAP of 5000 (ui/packages/plugin-grid/src/ObjectGrid.tsx), so past 5000 the user is told every match is selected and the action processes the first 5000. Read at base 37184244" ] }, "steps": [ @@ -3595,7 +3596,8 @@ "dispatch a bulk action; capture the requests — resolveBulkRows fans out a paged find (dropping $top/$skip, paging 500) collecting every matching id ACROSS pages", "cross-check the collected id count against a direct API count for the same search: GET /api/v1/data/showcase_task?$search=…&$top=0 (or the response total)", "confirm the AGGREGATE action (showcase_recalc_selection) carries every matching id in `_selectedIds` — it is an api-type action, so the ONE request is POST /api/v1/apps/showcase/recalc with `_selectedIds` at the body's top level (the grid hands them to the action runner as params._selectedIds, objectui plugin-grid ObjectGrid.tsx); the run dialog then reads 'N / 55 processed' for ~15 s (QA run #24616); a per-record action (showcase_mark_done) fans out one dispatch per matching id", - "click clear; confirm both selection sources reset (the toolbar empties AND the row checkboxes clear)" + "click clear; confirm both selection sources reset (the toolbar empties AND the row checkboxes clear)", + "cap leg (acceptance[5]): provision more than 5000 matching scratch tasks — POST /api/v1/data/showcase_task/createMany with a bare JSON array of at most 200 records per call (the batch doors' default maxBatchSize; a 201-record call answers 400 BATCH_TOO_LARGE), e.g. 26 calls of 200 sharing one prefix — then repeat steps[1] and steps[2] and screenshot the banner and the selected count before and after Select all; delete the rows afterwards (deleteMany, at most 200 ids per call)" ], "acceptance": [ { @@ -3627,6 +3629,12 @@ "oracle": "network", "verify": "the collected set never exceeds 5000; where the match set is larger, the run records the cap rather than asserting full coverage", "evidence": "the collected-set size vs the server total" + }, + { + "clause": "past the cap the banner names it: when the server total exceeds HARD_CAP 5000, the cross-page offer, the all-selected line and the selected count say the action covers the first 5000 (e.g. 「将处理前 5000 条」, 'the first 5000 will be processed'), never the uncapped total alone — a banner claiming all M matching while the dispatch stops at 5000 is a silent partial execution", + "oracle": "dom", + "verify": "with more than 5000 matching rows (steps[7]), after a screenshot confirms the bar rendered: read the bulk-cross-page-banner text and the selected count before and after Select all; PASS when each names the 5000 cap, FAIL when any presents the uncapped total M as the set the action will process. The fan-out itself is acceptance[4]'s; this clause scores only what the banner tells the user", + "evidence": "the banner screenshot and its DOM text before and after Select all, the find response total, the collected-set size" } ], "negative": [ @@ -3677,6 +3685,12 @@ "date": "2026-10-26", "change": "automated.ref says what its three files score, from Tier 1 run #25815 (6679644a, pin b41ed7f68956), re-read under ui/packages/plugin-grid on main cc378e93. (1) None exercises resolveBulkRows' paged fan-out or HARD_CAP (acceptance[1], [4], negative[0]). The run's 'unpinned even at component level' does not hold: bulkFanoutHostParams.test.tsx, not in the ref, drives the fan-out and the cap at component level, so the ref names it as evidence of that layer only. (2) The Clear case in bulkActionRefresh.test.tsx pins acceptance[3] and negative[2] in jsdom, and their dom oracle needs the browser. fixtures.knownGaps[0], the objectui-pinned-automation note, said the objectui specs live in a separate repo and none exist here; they are under ui/ in this checkout since the import, so it says where they resolve and how they run (devx seat 2's direction, carried for #26035). Re-read on main cc378e93. No clause changed", "ref": "#25846" + }, + { + "revision": 6, + "date": "2026-11-01", + "change": "acceptance[5] added with steps[7] and fixtures.knownGaps[1]: past HARD_CAP 5000 the cross-page banner must name the cap (e.g. 「将处理前 5000 条」), never present the uncapped server total as the set the action processes. Today the banner counts totalMatching uncapped (BulkActionBar.tsx) while resolveBulkRows stops at 5000 (ObjectGrid.tsx), so the clause is an expected FAIL until the product defect #28552 is fixed. Read at base 37184244. No existing clause changed. Ruling 6545938831 item ④ on #28345. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -5401,7 +5415,7 @@ "title": "Import mapping TransformType matrix: each of the 7 declared transforms does exactly its documented conversion, javascript is refused loudly at the door (no sandbox), format mismatch is a named 400", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "api", "personas": [ @@ -5462,8 +5476,7 @@ "negative": [ "a javascript-transform import that silently SKIPS the column (or worse, executes anything) is a FAIL — the refusal must be loud, total and row-free", "a map transform DROPPING an unmapped value (instead of passing it through unchanged) is a FAIL — the passthrough is the documented arm", - "an unknown transform string reaching applyMappingToRows answers 400 UNSUPPORTED_TRANSFORM 'unknown transform' (import-mapping.ts) — around-the-parse-seam guard, not HTTP-drivable end-to-end (spec parse rejects it at authoring); if ever observed answering 200, that is a FAIL", - "cross-ref guard: header mapping, idempotent re-import and the unknown-mappingName refusal are named-import-mapping's clauses — do not re-score them here" + "an unknown transform string reaching applyMappingToRows answers 400 UNSUPPORTED_TRANSFORM 'unknown transform' (import-mapping.ts) — around-the-parse-seam guard, not HTTP-drivable end-to-end (spec parse rejects it at authoring); if ever observed answering 200, that is a FAIL" ], "traps": [ "seed-data-thin", @@ -5478,7 +5491,8 @@ "packages/spec/src/data/mapping.zod.ts#TransformType (TransformType — none/constant/lookup/split/join/javascript/map, 7 members, named export pinned above)", "packages/rest/src/import-mapping.ts#resolveNamedMapping (format guard + the javascript refusal naming #2611), packages/core/src/utils/import-mapping.ts#transform (the per-transform switch + around-the-seam unknown-transform refusal)", "examples/app-showcase/src/data/mappings/index.ts#showcase_inquiry_feed (showcase_inquiry_feed — the stock none+map carrier)", - "cross-ref: records-forms.named-import-mapping (header mapping, idempotence, unknown-name refusal — kept there; this item is the transform axis)" + "cross-ref: records-forms.named-import-mapping (header mapping, idempotence, unknown-name refusal — kept there; this item is the transform axis)", + "cross-ref guard: header mapping, idempotent re-import and the unknown-mappingName refusal are named-import-mapping's clauses — do not re-score them here" ], "history": [ { @@ -5487,7 +5501,8 @@ "change": "initial — sweep gap (spec-enums angle): TransformType's 7 members had no per-variant coverage; named-import-mapping drives only the default copy + upsert path. Authored as a sibling item rather than widening that one (different axis, different fixtures), with the enumSource pin so an 8th transform trips VARIANTS STALE", "ref": "#sweep-2026-08-30" }, - { "revision": 2, "date": "2026-10-02", "change": "clause 2 no longer requires the refusal to name framework#2611: f115b1f (#21188) made REST refusals state each decision in words instead of a tracker number, and on 17.6.0 the UNSUPPORTED_TRANSFORM message names the missing sandbox without it (measured by the release-verification run, 0 rows landed). Step 3 notes the javascript transform is authored bare", "ref": "#21330" } + { "revision": 2, "date": "2026-10-02", "change": "clause 2 no longer requires the refusal to name framework#2611: f115b1f (#21188) made REST refusals state each decision in words instead of a tracker number, and on 17.6.0 the UNSUPPORTED_TRANSFORM message names the missing sandbox without it (measured by the release-verification run, 0 rows landed). Step 3 notes the javascript transform is authored bare", "ref": "#21330" }, + { "revision": 3, "date": "2026-11-01", "change": "negative[3] moved to source[4]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" } ] }, { diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index b923efc1a53..bf91d4452dd 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -1271,7 +1271,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": 5, + "revision": 6, "priority": "P1", "surface": "browser", "personas": ["admin (opens Studio on the read-only package)"], @@ -1324,8 +1324,7 @@ ], "negative": [ "a 2xx on ANY flow write into the read-only package is the hard FAIL — the server gate is the authority (ADR-0124 D1), and its fall here is the same class access-security.packaged-flow-write-door-parity hunts at the /meta and /automation doors", - "an inspector edit that visibly takes and silently vanishes on reload is a FAIL of the inert-or-announced clause even though nothing persisted — 'no server write happened' is clause 2's business, not an announcement", - "SCOPE: do not re-prove or widen access-security.readonly-package-locks-studio — its steps own the object designer + permission matrix surfaces; this item owns the Automations pillar. And do not conflate this switch with the packaged-flow activation ledger: the Studio pillar switch flips the flow's deployment `status` field via a draft save (a metadata write, refused on a read-only package), while the LEDGERED enable/disable (sys_metadata_activation) lives on the Setup packaged-automation board — automation.setup-packaged-automation-board / automation.packaged-flow-disable-durable own that surface" + "an inspector edit that visibly takes and silently vanishes on reload is a FAIL of the inert-or-announced clause even though nothing persisted — 'no server write happened' is clause 2's business, not an announcement" ], "traps": ["stale-console-bundle", "hydration-race", "automation-input"], "source": [ @@ -1334,14 +1333,16 @@ "docs/adr/0126-packaged-metadata-customization-model.md §2/§3 (flow = Regime C: locked base, in-place edit refused loudly at the write door — card F2)", "packages/metadata-protocol/src/sys-metadata-repository.ts#readOnlyBaseOverrideError (the 403 ITEM_LOCKED a write naming a read-only base answers) + packages/metadata-protocol/src/protocol.ts (the 403 NOT_OVERRIDABLE an overlay write on a non-overlayable type answers when no package is named)", "#12438 (the ADR-0126 packaged flow/action sweep this item lands from)", - "access-security.readonly-package-locks-studio (object designer + permission matrix — cross-referenced, not widened), access-security.packaged-flow-write-door-parity (the API-side door parity for the same packaged flow), automation.setup-packaged-automation-board (the ledgered enable/disable surface — a different mechanism from this pillar's status flip)" + "access-security.readonly-package-locks-studio (object designer + permission matrix — cross-referenced, not widened), access-security.packaged-flow-write-door-parity (the API-side door parity for the same packaged flow), automation.setup-packaged-automation-board (the ledgered enable/disable surface — a different mechanism from this pillar's status flip)", + "cross-ref: do not re-prove or widen access-security.readonly-package-locks-studio — its steps own the object designer + permission matrix surfaces; this item owns the Automations pillar. And do not conflate this switch with the packaged-flow activation ledger: the Studio pillar switch flips the flow's deployment `status` field via a draft save (a metadata write, refused on a read-only package), while the LEDGERED enable/disable (sys_metadata_activation) lives on the Setup packaged-automation board — automation.setup-packaged-automation-board / automation.packaged-flow-disable-durable own that surface" ], "history": [ { "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": 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" } + { "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" }, + { "revision": 6, "date": "2026-11-01", "change": "negative[2] moved to source[6]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" } ] }, { @@ -1349,7 +1350,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": 4, + "revision": 5, "priority": "P2", "surface": "browser", "personas": ["admin (authors the overlay in the designer)", "end user (renders the published change)"], @@ -1412,8 +1413,7 @@ "negative": [ "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" + "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" ], "traps": ["stale-console-bundle", "hydration-race", "automation-input", "cache-staleness"], "source": [ @@ -1423,13 +1423,15 @@ "packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts (the code-shipped built-in board the dashboard leg edits)", "objectui packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx (isArtifactItem tiering), (artifact-backed canWrite = allowEnvironmentOverlay — why the view/dashboard open editable and the flow does not), (resettable → the Reset-overlay verb), (the installed-package lock banner) — objectui owns the designer and end-user markup, so locator fixes land there (ADR-0054); the framework owns the registry flags and the overlay/reset doors", "#12438 (the ADR-0126 sweep this item lands from)", - "studio-authoring.org-override-registry-gate (the raw-PUT registry-gate half — cross-referenced, not duplicated), studio-authoring.view-authoring-live (the non-packaged authoring loop this item complements), dashboards.system-overview-live-counts (the same board's live-count semantics — and the reason the overlay must be reset)" + "studio-authoring.org-override-registry-gate (the raw-PUT registry-gate half — cross-referenced, not duplicated), studio-authoring.view-authoring-live (the non-packaged authoring loop this item complements), dashboards.system-overview-live-counts (the same board's live-count semantics — and the reason the overlay must be reset)", + "cross-ref: 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" ], "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": 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" } + { "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" }, + { "revision": 5, "date": "2026-11-01", "change": "negative[3] moved to source[7]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" } ] }, { @@ -1437,7 +1439,7 @@ "title": "The metadata governance sweep: GET /meta/diagnostics reports every STORED item failing spec validation with located errors, refuses outages and bad spellings loudly instead of answering '0 problems', and the Studio diagnostics page + directory tile badges paint the same numbers", "since": "v16", "status": "active", - "revision": 9, + "revision": 10, "priority": "P2", "surface": "mixed", "personas": ["admin (the governance surface is Studio's)"], @@ -1518,8 +1520,7 @@ "a 200 {\"scannedTypes\":1,\"stats\":{}} for a misspelled declared type is the #8924 silence regressed — the page would paint it 'All clear' — FAIL", "an outage answered as a clean sweep (total 0 from a store nobody could read) is the #8855 / ADR-0110 D3 FAIL shape — a diagnostics endpoint whose job is reporting problems must not answer '0 problems' precisely when it cannot read anything", "a blank diagnostics region on a clean store (instead of the designed empty state) is a FAIL of the page, not proof of health — and conversely, do NOT read the clean answer itself as 'the feature is missing': a store with no invalid rows is the healthy norm the save gates enforce (absence-inference)", - "consulting the dispatcher for this route is a recording error — the ledger marks GET /meta/diagnostics REST-server-only, so a simulated dispatch 404 says nothing about the live server (dispatcher-vs-hono-route)", - "SCOPE: per-item _diagnostics badges on list rows and the editor banner are the item-grain surfaces (ResourceListPage/ResourceEditPage) — this item owns only the cross-type sweep + directory/diagnostics pages; the save-time 422 gate is studio-authoring.authoring-validation-not-persisted — cite, don't duplicate" + "consulting the dispatcher for this route is a recording error — the ledger marks GET /meta/diagnostics REST-server-only, so a simulated dispatch 404 says nothing about the live server (dispatcher-vs-hono-route)" ], "traps": ["dispatcher-vs-hono-route", "hydration-race", "absence-inference", "stale-console-bundle"], "automated": { @@ -1533,7 +1534,8 @@ "packages/metadata-protocol/src/metadata-diagnostics.ts#computeMetadataDiagnostics (computeMetadataDiagnostics — errors only, undefined = no opinion for schema-less types; zodIssuesToMetadataIssues names union branches #5598 so Studio has a path to highlight)", "packages/spec/src/api/protocol.zod.ts#GetMetaDiagnosticsResponseSchema (GetMetaDiagnosticsResponseSchema — entries[].diagnostics is the canonical MetadataValidationResultSchema the save path's 422 also speaks)", "objectui packages/app-shell/src/views/metadata-admin/DiagnosticsPage.tsx (groups by type descending, per-row deep link..//, severity tabs, the summary badge, the clean Empty state, the loadFailed banner) + useMetadata.ts (useGlobalDiagnostics: strict error count vs warn-only count, counts/locked/packages from stats, older-server catch degrades to empty-not-fatal) + DirectoryPage.tsx (MetadataDirectoryPage: the package-scoped useGlobalDiagnostics sweep drives tile badges; the diagnostics link renders only when diagSummary.total > 0) + console/AppContent.tsx (the AppContent component's metadata/_diagnostics route)", - "studio-authoring.authoring-validation-not-persisted (why stock stores are clean — the save-time gate; also the known spurious _diagnostics banner on the DESIGNER, which is that item's business, not this sweep's)" + "studio-authoring.authoring-validation-not-persisted (why stock stores are clean — the save-time gate; also the known spurious _diagnostics banner on the DESIGNER, which is that item's business, not this sweep's)", + "cross-ref: per-item _diagnostics badges on list rows and the editor banner are the item-grain surfaces (ResourceListPage/ResourceEditPage) — this item owns only the cross-type sweep + directory/diagnostics pages; the save-time 422 gate is studio-authoring.authoring-validation-not-persisted — cite, don't duplicate" ], "history": [ { "revision": 1, "date": "2026-08-30", "change": "new item (2026-08-30 sweep, angle 4): the governance sweep GET /meta/diagnostics + its two Studio surfaces had no coverage — nothing asserted the stats/tile parity seam, the #8924 spelling refusal (whose regression the page paints as 'All clear'), the #8855 outage honesty, or the clean-store empty state. Grounded against source with two corrections to the register hypothesis: the scoping query param is ?package= (not ?packageId= — that is the protocol-layer argument name), and severity=warning cannot 'add warning-only entries' on stock because no producer emits warnings at head (reserved for a future lint layer) — authored as a mechanism clause with the lint-layer tripwire recorded. The invalid-stored-row positive is blocked(fixture) by construction (save doors validate; sys_metadata is not a data-plane object), with the stale-row snapshot named as the honest fixture candidate", "ref": "#sweep-2026-08-30" }, @@ -1544,7 +1546,8 @@ { "revision": 6, "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 242-254 of objectui DirectoryPage.tsx and line 982 of app-shell's console AppContent.tsx in the comma-range spelling (authored in 45397edf53 as the lists 133-158 and 242-254, and 882 and 982). At the objectui pin that commit shipped, 9602dc820450, 242-254 was the diagnostics link that renders only when diagSummary.total is above 0, 133-158 the package-scoped useGlobalDiagnostics sweep, and 982 the nested metadata route block that holds _diagnostics; at the shipped pin 12dc7b47c340 all have moved. source[5] now names the MetadataDirectoryPage component's sweep and link, and the AppContent component's metadata/_diagnostics route. 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": 7, "date": "2026-10-20", "change": "steps[0] respelled: 'boot 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": 8, "date": "2026-10-21", "change": "Respelled — fixtures.knownGaps[0]: 'ON AN ISOLATED SCRATCH FILE DB' → 'ON THE RUN'S OWN SCRATCH FILE DB'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", "ref": "#25018" }, - { "revision": 9, "date": "2026-10-26", "change": "automated.ref names the protocol-layer evidence its pin carries for acceptance[3] and negative[0] (checklist-accuracy finding a3/n0 of QA run #25815, release-18.2.0-tier1 at 6679644a, objectui pin b41ed7f68956; wave anchor #25807): the ref said the pin covered the outage clause ONLY, but its [#8924] describe asserts the ?type= misspelling refusal at the protocol layer; the api-oracle clauses still need the REST door. fixtures.requires[0] names a console built from this tree. 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-26", "change": "automated.ref names the protocol-layer evidence its pin carries for acceptance[3] and negative[0] (checklist-accuracy finding a3/n0 of QA run #25815, release-18.2.0-tier1 at 6679644a, objectui pin b41ed7f68956; wave anchor #25807): the ref said the pin covered the outage clause ONLY, but its [#8924] describe asserts the ?type= misspelling refusal at the protocol layer; the api-oracle clauses still need the REST door. fixtures.requires[0] names a console built from this tree. 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": 10, "date": "2026-11-01", "change": "negative[4] moved to source[7]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" } ] }, { @@ -1552,7 +1555,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": 8, + "revision": 9, "priority": "P1", "surface": "mixed", "personas": [ @@ -1615,8 +1618,7 @@ "a doc entry drawn for the outsider is an audience leak — cross-file to platform-core.docs-audience-gate and access-security", "\"Documentation not found\" where the server answers 401 / 403 — the UI half of platform-core.docs-audience-gate's \"a 404 where a 401/403 is due\" negative", "a blank region or a crash on any direct route — FAIL", - "a page entry that opens in a book that does not claim the page — the addendum's \"in its book's context\" fails", - "SCOPE: the book/doc audience semantics at API grain are platform-core.docs-audience-gate, and the portal's own rendering is platform-core.docs-portal-render — cite them, never duplicate them; this item owns the app-menu entry, its UI mirror and the direct-route refusal" + "a page entry that opens in a book that does not claim the page — the addendum's \"in its book's context\" fails" ], "traps": ["auth-state-leak", "hydration-race", "wrong-persona", "stale-console-bundle", "cache-staleness"], "source": [ @@ -1627,7 +1629,8 @@ "packages/rest/src/meta-item-read-gate.ts#filterAppForUser (/meta/app pruning), (NavDocAudienceGate — fails CLOSED: an absent gate prunes every doc entry)", "objectui PR 11258: packages/layout NavigationRenderer passesNavItemGuards (checkDocTarget), packages/app-shell hooks/useNavDocTargetCheck, apps/console pages DocsSlug / DocPage / BookPage (DocRefusal) / book-nav homeBook", "objectui PR 11241 (the doc editor the admin authors with)", - "platform-core.docs-audience-gate, platform-core.docs-portal-render (the neighbours — cross-referenced, not merged)" + "platform-core.docs-audience-gate, platform-core.docs-portal-render (the neighbours — cross-referenced, not merged)", + "cross-ref: the book/doc audience semantics at API grain are platform-core.docs-audience-gate, and the portal's own rendering is platform-core.docs-portal-render — cite them, never duplicate them; this item owns the app-menu entry, its UI mirror and the direct-route refusal" ], "history": [ { "revision": 1, "date": "2026-09-30", "change": "new — the 清单项待写 row of docs/NORTH-STAR.md beside platform-core.docs-audience-gate and platform-core.docs-portal-render; drafted with objectui#10188 and corrected by the at-tier review of objectui PR 11258 (the UI-mirror clause scoped to entries naming an installed doc or book; no signed-out persona — /docs sits behind ProtectedRoute)", "ref": "#20939" }, @@ -1637,7 +1640,8 @@ { "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": 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" } + { "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" }, + { "revision": 9, "date": "2026-11-01", "change": "negative[4] moved to source[8]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" } ] }, { @@ -2020,7 +2024,7 @@ "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": 3, + "revision": 4, "priority": "P1", "surface": "browser", "personas": [ @@ -2071,8 +2075,7 @@ ], "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" + "a prompt on a CLEAN pillar is also a FAIL — a guard that always asks teaches authors to click through it" ], "traps": [ "automation-input", @@ -2083,7 +2086,8 @@ "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)" + "#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)", + "cross-ref: Publish while an edit is held is studio-authoring.held-edit-publish-refusal; this item owns departure only" ], "history": [ { @@ -2103,6 +2107,12 @@ "date": "2026-10-31", "change": "steps[6] says when a dirty control must be left for the departure to test anything. The Access matrix tick and the navigation rename are dirty only until Studio's shared draft autosave lands, 1.5 s after the last edit (ui/packages/app-shell/src/views/studio-design/useDraftAutoSave.ts, the same at the run subject c4c515ad), so a departure after that leaves a clean pillar. The finding's text put the window at about 150 ms; the source says 1.5 s, and the step uses the source. Accuracy finding of QA run #27441. No clause changed. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" + }, + { + "revision": 4, + "date": "2026-11-01", + "change": "negative[2] moved to source[4]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -2111,7 +2121,7 @@ "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, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -2153,8 +2163,7 @@ } ], "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" + "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" ], "traps": [ "automation-input", @@ -2167,7 +2176,8 @@ "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" + "QA run #26715: the coordinator's C4 (Data) and the Automations card measured the refusal; held-widget refusal per objectui#11910", + "cross-ref: 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" ], "history": [ { @@ -2181,6 +2191,12 @@ "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" + }, + { + "revision": 3, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -2272,7 +2288,7 @@ "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": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -2328,8 +2344,7 @@ } ], "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" + "an edge type written outside the spec's four members, or an inspector choice that writes nothing, is a FAIL" ], "traps": [ "automation-input", @@ -2338,7 +2353,8 @@ "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)" + "#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)", + "cross-ref: 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" ], "history": [ { @@ -2358,6 +2374,12 @@ "date": "2026-10-31", "change": "fixtures.knownGaps[0] and acceptance[0] follow the edge inspector as it is. Since #26999 (card #26858) the edge inspector shows the draft's server verdict at the condition (FlowEdgeInspector.tsx, data-testid flow-edge-server-error), so the inspector half of acceptance[3] holds; QA run #27441 measured it at c4c515ad, with only the Problems tab silent. acceptance[0] now says that Normal is stored as no type key (FlowEdgeSchema's default is 'default', and the inspector deletes it) and uses the option labels the picker shows, 'Fault (error path)' and 'Back-edge (revise loop)' (ui/packages/i18n/src/namespaces/studio/en.ts). Accuracy findings of QA run #27441. The member each choice must write is unchanged. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" + }, + { + "revision": 4, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -2366,7 +2388,7 @@ "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, + "revision": 2, "priority": "P2", "surface": "mixed", "personas": [ @@ -2408,8 +2430,7 @@ } ], "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" + "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" ], "traps": [ "hydration-race", @@ -2422,7 +2443,8 @@ "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)" + "automation.packaged-flow-clone-contract (the clone door — cross-referenced)", + "cross-ref: the clone door itself (refusals, the notice, base and clone both bound) is automation.packaged-flow-clone-contract" ], "history": [ { @@ -2430,6 +2452,12 @@ "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" + }, + { + "revision": 2, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -2438,7 +2466,7 @@ "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, + "revision": 2, "priority": "P1", "surface": "mixed", "personas": [ @@ -2487,8 +2515,7 @@ } ], "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" + "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" ], "traps": [ "hydration-race", @@ -2502,7 +2529,8 @@ "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)" + "RUNNER.md environment facts (package-authored scheduled work is OFF unless OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1)", + "cross-ref: 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" ], "history": [ { @@ -2510,6 +2538,12 @@ "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" + }, + { + "revision": 2, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -2518,7 +2552,7 @@ "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, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -2529,12 +2563,15 @@ "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" + ], + "knownGaps": [ + "Studio's Validations tab has no initialStates input: a state_machine rule authored there carries its field and transitions only (ui/packages/app-shell/src/views/studio-design/ObjectValidationsPanel.tsx; the metadata previews only display initialStates, ValidationPreview.tsx). initialStates is written through the API only (packages/spec/src/data/validation.zod.ts, the state_machine rule's initialStates), so the insert-time invalid_initial_state refusal is not part of this item: acceptance[3] does not name it, and its runtime code is records-forms.validation-rule-type-matrix's (variants[0], showcase_project's project_status_flow)" ] }, "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)", + "Advanced: author one rule of each remaining type with real settings — state_machine on the status field (one allowed transition; the tab has no initial-state input, fixtures.knownGaps[0]), 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" @@ -2559,7 +2596,7 @@ "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", + "clause": "each published rule enforces with its per-type code: 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" @@ -2572,8 +2609,7 @@ } ], "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" + "a rule that saves in Studio but does not enforce once published is a FAIL — the authored rule must be the enforced rule" ], "traps": [ "automation-input", @@ -2586,7 +2622,8 @@ "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)" + "records-forms.validation-rule-type-matrix (the runtime code matrix — cross-referenced, not duplicated)", + "cross-ref: 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" ], "history": [ { @@ -2600,6 +2637,12 @@ "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" + }, + { + "revision": 3, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). Also acceptance[3] no longer names invalid_initial_state, and steps[2] no longer asks for an initial state: Studio's Validations tab has no initialStates input, so the state is API-only (new fixtures.knownGaps[0]; measured at base 37184244, ObjectValidationsPanel.tsx carries no initialStates). The insert-time code stays covered by records-forms.validation-rule-type-matrix. invalid_transition and every other per-type code are unchanged. Ruling 6545938831 item ③ on #28345 (no product card for a Studio input). Wave anchor #27377", + "ref": "#28345" } ] }, @@ -2608,7 +2651,7 @@ "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, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": [ @@ -2660,8 +2703,7 @@ } ], "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" + "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)" ], "traps": [ "automation-input", @@ -2674,7 +2716,8 @@ "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)" + "records-forms.object-hook-lifecycle (stock hooks on the write path — cross-referenced)", + "cross-ref: hook timing and the stock hooks on the write path are records-forms.object-hook-lifecycle" ], "history": [ { @@ -2688,6 +2731,12 @@ "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" + }, + { + "revision": 3, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -2696,7 +2745,7 @@ "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, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -2758,8 +2807,7 @@ } ], "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" + "a held preset written to the server before its target is given, or a published preset that does nothing when clicked, is a FAIL" ], "traps": [ "automation-input", @@ -2773,7 +2821,8 @@ "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)" + "records-forms.action-location-matrix (placement — cross-referenced)", + "cross-ref: 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" ], "history": [ { @@ -2787,6 +2836,12 @@ "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" + }, + { + "revision": 3, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -2870,7 +2925,7 @@ "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": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -2919,8 +2974,7 @@ } ], "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" + "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)" ], "traps": [ "automation-input", @@ -2930,7 +2984,8 @@ "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)" + "automation.rollup-summary-filter (filtered roll-ups — cross-referenced)", + "cross-ref: filtered roll-ups (summaryOperations.filter) at runtime are automation.rollup-summary-filter" ], "history": [ { @@ -2950,6 +3005,12 @@ "date": "2026-10-30", "change": "acceptance[2] re-pointed after #27533 (fbb8bec3e7), which lets a count roll-up save without summaryOperations.field and stops Studio offering the field control for count: the count summary now carries object and function with no field, the sum summary still carries object, field and function. The old clause and verify (both summaries carry \"the authored object, field and function\") would score a correct run as a failure", "ref": "#27608" + }, + { + "revision": 4, + "date": "2026-11-01", + "change": "negative[1] moved to source[4]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3044,7 +3105,7 @@ "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, + "revision": 2, "priority": "P1", "surface": "mixed", "personas": [ @@ -3103,8 +3164,7 @@ } ], "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)" + "a create that writes the item but not the menu entry (or the reverse) is a FAIL — the dialog promises both" ], "traps": [ "automation-input", @@ -3117,7 +3177,8 @@ }, "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)" + "ui/packages/app-shell/src/views/studio-design/CreateItemDialog.tsx (the one create form; identifier derivation)", + "cross-ref: 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)" ], "history": [ { @@ -3125,6 +3186,12 @@ "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" + }, + { + "revision": 2, + "date": "2026-11-01", + "change": "negative[1] moved to source[2]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3133,7 +3200,7 @@ "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": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -3191,8 +3258,7 @@ } ], "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" + "a type that renders an empty or zero-height frame over multi-bucket data is a FAIL (single-datapoint)" ], "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", @@ -3212,7 +3278,8 @@ "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)" + "dashboards.chart-type-matrix, dashboards.global-filters-rescope (the stock-fixture twins — cross-referenced)", + "cross-ref: 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" ], "history": [ { @@ -3232,6 +3299,12 @@ "date": "2026-10-31", "change": "fixtures.requires[2] says how sankey binds, and steps[2] says a lookup filter needs optionsFrom. The sankey renderer flows from one root to each category of its first dimension, and a pivoted second dimension adds a second column (ui/packages/plugin-charts/src/AdvancedChartImpl.tsx, buildSankeyCellFlows), so the 'source/target pair for sankey' the dataset recipe asked for is not a shape it reads. A lookup global filter renders through the same select control as select (ui/packages/plugin-dashboard/src/DashboardFilterBar.tsx), whose choices come from options or optionsFrom, so one written without optionsFrom offers nothing to pick. Accuracy finding of QA run #27441. No clause changed. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" + }, + { + "revision": 4, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3240,7 +3313,7 @@ "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": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": [ @@ -3282,8 +3355,7 @@ } ], "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" + "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" ], "variants": [ "tabular", @@ -3303,7 +3375,8 @@ ], "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)" + "dashboards.dataset-report-authoring, dashboards.drill-through-range (stock-fixture twins — cross-referenced)", + "cross-ref: every ReportType on stock datasets is dashboards.dataset-report-authoring; drill-through clicks are dashboards.drill-through-range" ], "history": [ { @@ -3317,6 +3390,12 @@ "date": "2026-10-31", "change": "steps[4] names the door and body for the comparison query: POST /api/v1/analytics/dataset/query with datasetName beside a selection carrying the row dimension, the measure, the summary's runtimeFilter and order {MEASURE: 'desc'} (DatasetSelectionSchema in packages/spec/src/api/analytics.zod.ts; the selection spells sorting `order`, and orderBy is its alias). Accuracy finding of QA run #27441. No clause changed. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" + }, + { + "revision": 3, + "date": "2026-11-01", + "change": "negative[1] moved to source[2]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3325,7 +3404,7 @@ "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": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -3389,8 +3468,7 @@ } ], "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" + "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)" ], "traps": [ "auth-state-leak", @@ -3401,7 +3479,8 @@ "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)" + "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (the Studio menu-entry inspector)", + "cross-ref: the doc-type menu entry is studio-authoring.doc-on-app-menu; nav rendering on the stock showcase is platform-core.nav-surfaces-render" ], "history": [ { @@ -3421,6 +3500,12 @@ "date": "2026-10-31", "change": "steps[0]'s example visible predicate names org_owner, and steps[4] says which app and entries Studio's Interfaces pillar can show. QA run #27441 found the seeded admin holds org_owner, not org_admin: an organization's owner membership maps to org_owner alone (packages/spec/src/identity/eval-user.zod.ts), so the old 'org_admin' predicate was false for both personas and acceptance[3] could not tell them apart. The pillar loads only the package's first app and draws only its top-level navigation (ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx), so entries written only inside areas never reach acceptance[5]'s inspector. Accuracy finding of QA run #27441. The clauses are unchanged. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" + }, + { + "revision": 4, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3429,7 +3514,7 @@ "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": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": [ @@ -3468,8 +3553,7 @@ } ], "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" + "a slot override saved but not rendered, or a variable whose block ignores its predicate, is a FAIL" ], "variants": [ "string", @@ -3487,7 +3571,8 @@ "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)" + "studio-authoring.page-variables-and-actions, studio-authoring.save-door-runs-authoring-rules (cross-referenced)", + "cross-ref: 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" ], "history": [ { @@ -3501,6 +3586,12 @@ "date": "2026-10-31", "change": "steps[0] stops offering slots.tabs as an alternative to details. #27053 retired `page.slots.tabs` (ADR-0085): the slot map now refuses it with a retiredKey tombstone, and the authorable slots are header, actions, alerts, highlights, details and discussion (packages/spec/src/ui/page.zod.ts). The old parenthetical, 'details OR tabs, never both — the save door refuses the pair', named a pairing rule that no longer exists. Accuracy finding of QA run #27441. No clause changed. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" + }, + { + "revision": 3, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3509,7 +3600,7 @@ "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": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -3564,8 +3655,7 @@ } ], "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" + "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" ], "traps": [ "automation-input", @@ -3574,7 +3664,8 @@ "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)" + "access-security.readonly-package-locks-studio (the sibling read-only surfaces — cross-referenced)", + "cross-ref: 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" ], "history": [ { @@ -3594,6 +3685,12 @@ "date": "2026-10-31", "change": "steps[1] names the New permission set dialog's confirm button: 'Save as draft', not 'Create'. Every Studio create dialog, the Access pillar's included, takes its submit label from engine.studio.createDraft (ui/packages/i18n/src/namespaces/studio/en.ts, 'Save as draft'; ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx, the engine.studio.access.new dialog). Accuracy finding of QA run #27441. No clause changed. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" + }, + { + "revision": 4, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3602,7 +3699,7 @@ "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": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": [ @@ -3662,8 +3759,7 @@ } ], "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" + "a facet that saves but changes nothing at runtime is a FAIL — every clause pairs the authored value with its effect on M" ], "traps": [ "auth-state-leak", @@ -3674,7 +3770,8 @@ "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)" + "ui/packages/i18n/src/namespaces/studio/en.ts (engine.studio.access.tab.capabilities / rls / tabs)", + "cross-ref: 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" ], "history": [ { @@ -3694,6 +3791,12 @@ "date": "2026-10-31", "change": "fixtures.requires[2], steps[0], [1], [4], [5] and acceptance[0].verify name a declared, ungranted capability CAP instead of qa_cap, requires[2] says where the gated action must live, and knownGaps[0] is closed. QA run #27441 (at c4c515ad) met three things. qa_cap cannot exist at runtime: a capability is code-only, so its runtime create answers 403 NOT_CREATABLE, and the run used showcase.restricted_ops, which the showcase declares and grants to no set (examples/app-showcase/src/security/capabilities.ts). A standalone runtime action item rendered nowhere, so the action is authored inline on OBJ's actions[] (Studio Data › Actions). The launcher half the knownGap called failing held: since #27021 Home, the switcher and the launcher blocks are narrowed by /me/apps (ui/packages/app-shell/src/hooks/useLauncherApps.tsx). acceptance[4] is now scored on both halves with no known failure; no clause is weakened. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" + }, + { + "revision": 4, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3702,7 +3805,7 @@ "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": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -3757,8 +3860,7 @@ } ], "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" + "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" ], "traps": [ "auth-state-leak", @@ -3769,7 +3871,8 @@ "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)" + "access-security.owd-sharing-matrix, access-security.owd-save-gate, access-security.suggested-binding-loop (cross-referenced)", + "cross-ref: 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" ], "history": [ { @@ -3789,6 +3892,12 @@ "date": "2026-10-31", "change": "acceptance[2].verify spells the suggestion statuses as the server writes them: pending → confirmed → dismissed, not pending → accepted → revoked. The status vocabulary is 'pending' | 'confirmed' | 'dismissed' (packages/plugins/plugin-security/src/suggested-audience-bindings.ts, packages/spec/src/contracts/security-service.ts), and the sys_audience_binding_suggestion object defines confirmed as the everyone definition carrying the set and dismissed as declined or revoked. Accuracy finding of QA run #27441. The clause is unchanged. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" + }, + { + "revision": 4, + "date": "2026-11-01", + "change": "negative[1] moved to source[4]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3797,7 +3906,7 @@ "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": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -3845,8 +3954,7 @@ } ], "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" + "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" ], "variants": [ "object.managedBy 'append-only' → POST to the data API refused 403 (engine-owned), DELETE refused", @@ -3897,7 +4005,8 @@ "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)" + "packages/spec/liveness/*.json (each key's liveness grade — a key graded live with no effect here is a finding)", + "cross-ref: preservation of these keys across a Studio save is studio-authoring.api-written-keys-survive-studio-save" ], "history": [ { @@ -3917,6 +4026,12 @@ "date": "2026-10-31", "change": "fixtures.knownGaps[0] moves the form-view `buttons` key off the failing rows. QA run #27441 measured it rendering at c4c515ad (the authored submit label shown, cancel hidden), and ObjectForm folds buttons.submit and buttons.cancel onto its flat renderer props (ui/packages/plugin-form/src/ObjectForm.tsx). The other gap-view rows are not re-measured and stay as recorded. The run reached 15 of about 45 rows; the not-run handover is a re-run, not a ledger edit. Accuracy finding of QA run #27441. No clause changed. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" + }, + { + "revision": 4, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -3925,7 +4040,7 @@ "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": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": [ @@ -3968,8 +4083,7 @@ } ], "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" + "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" ], "variants": [ "object.externalSharingModel — tombstone at HEAD", @@ -4014,7 +4128,8 @@ "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)" + "#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)", + "cross-ref: 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" ], "history": [ { @@ -4034,6 +4149,12 @@ "date": "2026-10-31", "change": "acceptance[0]'s clause and verify state both refusal shapes as the save door writes them. A rename tombstone reads 'was renamed to `refreshIntervalSeconds` in @objectstack/spec 17 … Rename the key' (dashboard.refreshInterval, packages/spec/src/ui/dashboard.zod.ts), which the old verify's 'was removed in' did not admit; an aged-out key is reported by unrecognized_keys at its PARENT's path, with the key named in the message (packages/spec/src/shared/suggestions.zod.ts), not at the key's own path. Accuracy finding of QA run #27392 (33 of 33 keys refused, 11 tombstones at the key path and 22 aged-out keys as unrecognized_keys). The clause still asks for the same refusal, stated per shape; no verdict moves. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27392" + }, + { + "revision": 4, + "date": "2026-11-01", + "change": "negative[1] moved to source[4]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] }, @@ -4042,7 +4163,7 @@ "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, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -4090,8 +4211,7 @@ } ], "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" + "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" ], "traps": [ "silent-coercion", @@ -4101,7 +4221,8 @@ "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)" + "#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)", + "cross-ref: 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" ], "history": [ { @@ -4115,6 +4236,12 @@ "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" + }, + { + "revision": 3, + "date": "2026-11-01", + "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", + "ref": "#28345" } ] } diff --git a/docs/qa/platform-checklist/areas/ux-conventions.json b/docs/qa/platform-checklist/areas/ux-conventions.json index f413dd08d7c..ad5e39955ec 100644 --- a/docs/qa/platform-checklist/areas/ux-conventions.json +++ b/docs/qa/platform-checklist/areas/ux-conventions.json @@ -9,7 +9,7 @@ "title": "Every user-initiated write answers visibly — success names what changed, a refusal says why, and a no-op claims nothing", "since": "v17.7", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "browser", "personas": [ @@ -56,13 +56,13 @@ { "clause": "every write that landed server-side produced visible feedback on the surface that started it — a toast, an inline status, or (direct manipulation only) the item visibly in its new place", "oracle": "dom", - "verify": "per variant, the captured feedback text is non-empty, or (kanban only) the card's column in the DOM matches the status the server read returns; pair every row with its step-3 server read", + "verify": "per variant, the captured feedback text is non-empty, or (kanban only) the card's column in the DOM matches the status the server read returns; pair every row with its step-3 server read. The Studio top bar's save status ('Saved' / 「已保存」, with or without a time) is not write feedback under this convention and does not count here: a Studio row whose only feedback is that status scores FAIL", "evidence": "per-variant table: variant · landed (y/n, request status) · feedback text verbatim or 'none' · screenshot" }, { "clause": "success feedback names its subject: a record-level write names the record or item (its title, label or display name) or the count; a singleton page (settings) and a named automation (e.g. Flow \"X\" completed) are their own subject and pass as they are; a bare 'Success' / 'Done' or a machine name never passes", "oracle": "screenshot", - "verify": "two steps per row. First classify the write: record-level (a record create, edit or delete, the import, the approval decision, a record or param action, a Studio item) or singleton / named automation (the settings save, a named Flow run). Then score it by that class's test. Record-level: PASS when the captured text names the record or item (record title or display name, invitee email, package or item label, e.g. 'Approved \"EXP-2001\"') or the count ('N created · M skipped', '1 row saved'); text that names only the object ('Task created successfully') scores FAIL — the console's record create and edit toasts read that way until #26172 (the dialog paths) and #26461 (the full-page /new and /edit form route) — both closed fixed, so those rows are now expected PASS, and a toast naming only the object is a regression. Singleton / named automation: PASS when the text names that page or automation ('Settings saved', 'Flow \"Reassign Task\" completed'). In either class a generic 'Done' / 'Success' or a raw snake_case name scores FAIL for that row", + "verify": "two steps per row. First classify the write: record-level (a record create, edit or delete, the import, the approval decision, a record or param action, a Studio item) or singleton / named automation (the settings save, a named Flow run). Then score it by that class's test. Record-level: PASS when the captured text names the record or item (record title or display name, invitee email, package or item label, e.g. 'Approved \"EXP-2001\"') or the count ('N created · M skipped', '1 row saved'); text that names only the object ('Task created successfully') scores FAIL — the console's record create and edit toasts read that way until #26172 (the dialog paths) and #26461 (the full-page /new and /edit form route) — both closed fixed, so those rows are now expected PASS, and a toast naming only the object is a regression. Singleton / named automation: PASS when the text names that page or automation ('Settings saved', 'Flow \"Reassign Task\" completed'). In either class a generic 'Done' / 'Success' or a raw snake_case name scores FAIL for that row. The Studio top bar's save status ('Saved' / 「已保存」, with or without a time) names no item, so it is not write feedback under this convention: a Studio row (autosave, publish, discard) passes only on a toast or inline text that names the item or package, and a row whose only feedback is that status scores FAIL", "evidence": "the verbatim feedback strings" }, { @@ -153,6 +153,12 @@ "date": "2026-10-31", "change": "Three findings of QA run #27441. First, variants[4], variants[5] and knownGaps[0] quoted stale successMessages. MarkDoneAction now declares `{{ record.title | default:'Task' }} marked done.` and invite_user `Invitation sent to {{ result.email }}`. knownGaps[1]'s approve message had the same ${...} spelling and now reads `Approved \"{{ result.request.record_title }}\"` (sys-approval-request.object.ts). Second, variants[1] now names the task Grid view, the only task list that declares inlineEdit: true (task.view.ts). Third, steps[4](a): the Backlog → Done refusal cannot be driven in the UI, because the record form and the grid editor offer only the moves the state machine admits (stateMachineOptions.ts). The step now drives it through the API (400, invalid_transition) and records the narrowed picker as the UI's answer. No clause changed. acceptance[1].verify, held on #28345, is untouched. Read from source at base 22ce3d13. Wave anchor #27377, wave-3 accuracy slice S5 (#28339); findings parked in anchor comment 6490438726", "ref": "#27441" + }, + { + "revision": 9, + "date": "2026-11-01", + "change": "acceptance[0].verify and acceptance[1].verify say the Studio top bar's save status ('Saved' / 「已保存」) is not write feedback: it names no item, so a Studio row passes only on a toast or inline text naming the item or package, and a row with only that status scores FAIL. This is ruling 6545938831 item ② on #28345, which keeps the convention's wording (success names its subject) and settles how a runner scores that indicator, in both clauses; neither clause's wording changed. Held from slice S5 (revision 8). Wave anchor #27377", + "ref": "#28345" } ] },