From a1d68d7d265f0802ac8c8f797ace7a3e75d45f4e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 17:46:35 +0000 Subject: [PATCH 1/7] docs(qa): correct twelve platform-core checklist items the 18.0 sweep measured as inaccurate Checklist-accuracy findings of QA run #24557 (platform-core, 5b14b7f3), each re-derived from source at 8fbbd5082f: builtin-apps-nav-render, home-admin-cluster-links, notification-center, v18-boot-refusals, package-lifecycle-enable-disable, docs-audience-gate, runtime-config-boot-read, manifest-install-contract, marketplace-install-local-lifecycle, serve-composition-v18, url-overlay-contract, activation-ledger-row-contract. One revision and one history row per item; history refs are a placeholder until the PR exists. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/platform-core.json | 177 +++++++++++++----- 1 file changed, 125 insertions(+), 52 deletions(-) diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 962d40dde4..9efb434015 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -510,7 +510,7 @@ "title": "The built-in apps a stock boot REGISTERS (Setup / Account) render every merged-nav destination; app-level and entry-level gates are enforced, not errored — and Studio's deliberate absence is asserted, never hunted", "since": "v15", "status": "active", - "revision": 6, + "revision": 7, "priority": "P0", "surface": "browser", "personas": [ @@ -532,10 +532,10 @@ "steps": [ "as admin, GET /api/v1/meta/app?id=setup and extract every merged nav destination — Setup is a SHELL of empty group anchors (packages/platform-objects/src/apps/setup.app.ts: group_overview/apps/people_org/access_control/approvals/configuration/diagnostics/integrations/advanced) filled by SETUP_NAV_CONTRIBUTIONS (setup-nav.contributions.ts) plus capability-plugin contributions (plugin-security Positions/Permission Sets, plugin-sharing Sharing Rules/Record Shares, plugin-approvals, plugin-webhooks)", "GET /api/v1/meta/app with NO query first and record the names actually served — a stock boot answers exactly showcase_app, setup, account. Then GET /api/v1/meta/app?id=account (account.app.ts — Profile + Inbox/Security/Developer groups). ⚠️ GET /api/v1/meta/app?id=studio answers 200 {\"items\":[]} on every stock boot and that is the expected result (knownGap): record the absence as an assertion and move on — do NOT install @objectstack/studio to make the step runnable", - "as admin, hand-walk each destination in each app: navigate, wait for render, SCREENSHOT FIRST, then read the DOM — assert no pageerror, no 'failed to load', no blank
, no 'no actions configured' placeholder", + "as admin, hand-walk each destination in each app. EXPAND every nav group first: some render collapsed by default — Setup's Advanced group and Account's Developer group declare `expanded: false` (packages/platform-objects/src/apps/setup.app.ts, account.app.ts), and QA run #24557 found Setup's Configuration group collapsed as well — so a walk that never expands them never reaches their entries. Then navigate, wait for render, SCREENSHOT FIRST, then read the DOM — assert no pageerror, no 'failed to load', no blank
, no 'no actions configured' placeholder", "Setup detail: confirm each settings URL entry (nav_settings_* → /apps/setup/system/settings/) opens the settings namespace form (localization/company/branding/auth/mail/storage/ai/knowledge/feature_flags), and Users / Organization / Business Units / Teams / Sessions / OAuth Applications render. User preferences and identity links are not Setup destinations: open a user from Users and confirm the user page's Preferences and Linked Accounts tabs render that user's rows", "Studio detail is NOT walked from here — the app is not registered on a stock boot (knownGap), so there is no merged nav to render. Its metadata:resource lists and component surfaces (App Builder, All Metadata Types, Packages, API Console, Flow Runs, Public Forms) are reached through the console surface at /_console/studio// and are covered by area studio-authoring. What THIS item asserts about Studio is the absence itself, per clause 6", - "Account detail: confirm Profile (account:profile_card), Notifications (sys_inbox_message/mine), Approvals (sys_approval_request/my_pending), Linked Accounts (sys_account), Active Sessions (sys_session/mine), API Keys (sys_api_key/mine), OAuth Applications (sys_oauth_application/mine) and Connect an Agent (nav_connect_agent) each render — 8 Account destinations and 45 Setup destinations for the admin, re-measured 2026-10-01 (GET /api/v1/meta/app leaves; #9453 recorded 43 and 7). Recount from the payload each run", + "Account detail: confirm Profile (account:profile_card), Notifications (sys_inbox_message/mine), Approvals (sys_approval_request/my_pending), Linked Accounts (sys_account), Active Sessions (sys_session/mine), API Keys (sys_api_key/mine), OAuth Applications (sys_oauth_application/mine) and Connect an Agent (nav_connect_agent) each render — 8 Account destinations and 42 Setup destinations for the admin on a stock single-org showcase boot, re-measured by QA run #24557 at 5b14b7f3 (GET /api/v1/meta/app leaves, which match the nav source minus its gates; 45 on 2026-10-01, and #9453 recorded 43 and 7). Recount from the payload each run", "confirm the gated entries resolve to ABSENT-not-erroring for the admin: nav_organizations (requiresService org-scoping) is absent in single-org mode; nav_jwks Signing Keys is ABSENT for EVERY persona, admin included — the entry was removed in #7544 because sys_jwks declares apiEnabled:false, so its list answers OBJECT_API_DISABLED (404) for all callers and no permission gate could prune it; SSO Providers is absent unless OS_SSO_ENABLED (knownGap)", "sign in as the plain member: attempt to open Setup (app name setup) — capture the app-level refusal (App.requiredPermissions setup.access); open Account (declares no requiredPermissions) — capture it opening. Studio is not part of this comparison: it is absent for EVERY persona including the admin, so a member failing to reach it proves nothing about the gate (the same reasoning #7544 applied to nav_jwks)", "as the member, GET /api/v1/meta/app for a reachable app and confirm every manage_platform_settings-gated entry (API Keys, the manage_platform_settings settings URLs) is ABSENT from the member's payload — the server prunes, the client does not merely hide. JWKS is deliberately NOT in this list any more: it is absent for admins too, so finding it absent for a member proves nothing about the gate (#7544)", @@ -625,6 +625,12 @@ "date": "2026-10-16", "change": "the Setup detail step listed Identity Links and User Preferences among Setup's rendered destinations; #23803 removed both entries from Setup (nav_accounts, nav_user_preferences), and an administrator reaches those rows from the User record page's Linked Accounts and Preferences tabs. The step now drives those two tabs from a user's page instead. The destination count in the Account-detail step is left as it is: it is #23803 item 4's, re-measured by whichever of that card's changes lands last. No acceptance clause changed", "ref": "#23803" + }, + { + "revision": 7, + "date": "2026-10-19", + "change": "steps[2] and steps[5] corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.builtin-apps-nav-render; wave anchor #24579 §3). steps[5] said 45 Setup destinations; the run's GET /api/v1/meta/app payload at 5b14b7f3 served 42, which matches the nav source minus its gates, and no nav source changed between 5b14b7f3 and this edit's base (the count was #23803 item 4's to re-measure, and #23803 is closed). steps[2] now has the walk expand the groups that render collapsed (Advanced and Developer declare expanded: false in setup.app.ts / account.app.ts; the run also found Configuration collapsed) before it navigates. No acceptance clause changed", + "ref": "#PENDING-24579R2" } ] }, @@ -792,7 +798,7 @@ "title": "Package lifecycle: disable stops serving, enable restores, uninstall de-registers, commits grow per publish and revert restores the prior published shape", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "mixed", "personas": ["seeded admin (admin@objectos.ai / admin123)"], @@ -806,7 +812,7 @@ ] }, "steps": [ - "create or reuse the writable probe package (POST /api/v1/packages); author a probe object + view + nav entry into it and publish (POST /api/v1/packages/:id/publish or /publish-drafts). Record its id", + "create or reuse the writable probe package (POST /api/v1/packages); author a probe object + view + nav entry into it and publish (POST /api/v1/packages/:id/publish or /publish-drafts). The draft PUT accepts what publish then refuses, so author both up front: the object must declare `sharingModel` (else publish answers 422 `security-owd-unset`, an error-tier rule — packages/lint/src/validate-security-posture.ts) and the view its `object` and `viewKind` (the authored view overlay requires both — packages/spec/src/ui/view.zod.ts). Record its id", "confirm it serves: GET /api/v1/meta lists the probe object; GET /api/v1/data/ answers 200; the probe nav appears in the relevant /api/v1/meta/app", "disable: PATCH /api/v1/packages/:id/disable → capture; then GET /api/v1/data/ and the probe routes → now 404/403 (stopped serving); /meta/app no longer lists its nav", "enable: PATCH /api/v1/packages/:id/enable → capture; the object/nav/routes serve again (restore)", @@ -878,7 +884,13 @@ "ADR-0070 (writable vs read-only packages), ADR-0033 (drafts/publish/commits)" ], "history": [ - { "revision": 1, "date": "2026-08-08", "change": "initial — package enable/disable/uninstall + commits/revert lifecycle, driven against the writable probe package the authoring round-trip already creates", "ref": "claude/platform-test-checklist-ocwugl" } + { "revision": 1, "date": "2026-08-08", "change": "initial — package enable/disable/uninstall + commits/revert lifecycle, driven against the writable probe package the authoring round-trip already creates", "ref": "claude/platform-test-checklist-ocwugl" }, + { + "revision": 2, + "date": "2026-10-19", + "change": "steps[0] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.package-lifecycle-enable-disable; wave anchor #24579 §3, and QA run #24571 measured the same gate on a runtime object): the probe object needs `sharingModel` and the probe view its `viewKind` before publish accepts them — the draft PUT answers 200 and publish then refuses (security-owd-unset is error-tier in packages/lint/src/validate-security-posture.ts; the view overlay requires object + viewKind in packages/spec/src/ui/view.zod.ts). No acceptance clause changed; acceptance[5]'s FAIL is the objectui defect objectstack-ai/objectui#13052 and is untouched", + "ref": "#PENDING-24579R2" + } ] }, { @@ -886,7 +898,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": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": ["seeded admin (admin@objectos.ai / admin123)"], @@ -901,21 +913,21 @@ ] }, "steps": [ - "open the console as admin; the header (AppHeader.tsx) polls GET /api/v1/notifications?view=mine — capture the initial inbox rows", + "open the console as admin; the header bell's feed polls `sys_inbox_message` joined with `sys_notification_receipt` through the data API (objectui packages/app-shell/src/hooks/sharedUserFeeds.ts at the console pin), not GET /api/v1/notifications?view=mine — capture the initial inbox rows from those two reads", "read the bell badge: it must equal distinct unread (topic,title) TOPICS + pending approvals, NOT raw row count (#2765) — the recurring digest producing many identical rows must not inflate it to 9+ off one topic", "cross-check: count distinct unread (topic,title) groups from the payload (groupNotifications in inboxGrouping.ts) + pendingApprovalsCount and confirm it equals the rendered totalBadge", "open the popover; confirm THREE tabs render: Notifications / Approvals / Activity", "confirm coalescing: repeats of the same (topic,title) collapse into ONE expandable group with a ×N count pill; click the chevron to expand and reveal the members", "per-group mark-read: click a group's 'Mark read' → capture POST /api/v1/notifications/read {ids:[...]} on the wire (ONE request for the whole group, not N); the group's unread dot clears and the badge drops by one topic", "global mark-all-read: click 'Mark all read' → capture POST /api/v1/notifications/read/all; the notifications portion of the badge drops to 0; the approvals portion is unaffected", - "Approvals tab: confirm it lists/points at the pending approval request(s) and pendingApprovalsCount equals the sys_approval_request my_pending count", + "Approvals tab: confirm it shows the pending count with a 'View approvals' link to the app's system/approvals page — the tab is a count and a link, not a list of requests (objectui packages/app-shell/src/layout/InboxPopover.tsx) — and pendingApprovalsCount equals the sys_approval_request my_pending count", "persistence: reload; read-state persists — the marked rows read read: true in the post-reload GET /notifications (sys_notification_receipt upsert, ADR-0030); a row that flips back is either synthetic (its sys_inbox_message row has no notification_id — knownGap) or the receipt bug" ], "acceptance": [ { "clause": "the badge counts distinct unread TOPICS + pending approvals, not raw rows — a recurring digest producing N identical rows contributes 1 to the badge (#2765)", "oracle": "network", - "verify": "distinct-(topic,title) unread group count from the GET /notifications payload + pendingApprovalsCount == the rendered totalBadge", + "verify": "distinct-(topic,title) unread group count from the inbox rows the bell polls (steps[0]: sys_inbox_message joined with sys_notification_receipt through the data API) + pendingApprovalsCount == the rendered totalBadge", "evidence": "the payload + the badge value" }, { @@ -943,9 +955,9 @@ "evidence": "the captured POST + badge" }, { - "clause": "the Approvals tab lists the pending request(s) and pendingApprovalsCount equals the sys_approval_request my_pending count", + "clause": "the Approvals tab shows the pending count with a 'View approvals' link (a count and a link, not a list) and pendingApprovalsCount equals the sys_approval_request my_pending count", "oracle": "dom", - "verify": "the tab content + the count vs a direct sys_approval_request my_pending query", + "verify": "the tab's count and its 'View approvals' link target + the count vs a direct sys_approval_request my_pending query", "evidence": "the tab + the count comparison" }, { @@ -972,7 +984,13 @@ ], "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" }, - { "revision": 2, "date": "2026-10-16", "change": "fixtures.requires[0], knownGaps[0], steps[8] and acceptance[6].verify re-pointed (checklist-accuracy findings and fixture gap of QA run #23939, wave #23878, and #23879). The showcase_scheduled_digest fixture never fires on a default boot — OS_AUTOMATION_SCHEDULED_WORK_ENABLED leaves packaged schedules unbound — so the requirement names the switch and the fallback #23939 used (the seeded approval.requested rows). The payload's rows carry no notification_id field — the row id IS the event id when one exists — so 'verify persistence on a keyed row' could not be followed; persistence is proven on read: true after reload, and a reverting row is classified through its backing sys_inbox_message row. Not loosened: a keyed row that reverts is still the receipt bug and FAILS", "ref": "#24036" } + { "revision": 2, "date": "2026-10-16", "change": "fixtures.requires[0], knownGaps[0], steps[8] and acceptance[6].verify re-pointed (checklist-accuracy findings and fixture gap of QA run #23939, wave #23878, and #23879). The showcase_scheduled_digest fixture never fires on a default boot — OS_AUTOMATION_SCHEDULED_WORK_ENABLED leaves packaged schedules unbound — so the requirement names the switch and the fallback #23939 used (the seeded approval.requested rows). The payload's rows carry no notification_id field — the row id IS the event id when one exists — so 'verify persistence on a keyed row' could not be followed; persistence is proven on read: true after reload, and a reverting row is classified through its backing sys_inbox_message row. Not loosened: a keyed row that reverts is still the receipt bug and FAILS", "ref": "#24036" }, + { + "revision": 3, + "date": "2026-10-19", + "change": "steps[0], steps[7], acceptance[0].verify and acceptance[5] (clause, verify) corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.notification-center; wave anchor #24579 §3). At the console pin 4016370a the bell does not poll GET /api/v1/notifications?view=mine: its feed reads sys_inbox_message joined with sys_notification_receipt through the data API (objectui sharedUserFeeds.ts), which is the payload the run counted the badge from. The Approvals tab shows a pending count and a 'View approvals' link, not a list of requests (InboxPopover.tsx); the run scored acceptance[5] on the count match. Mark-read still POSTs /api/v1/notifications/read[/all] (useInboxBell.ts), and acceptance[6]'s post-reload GET /api/v1/notifications is a direct read of the same rows, so those clauses are unchanged", + "ref": "#PENDING-24579R2" + } ] }, { @@ -1176,7 +1194,7 @@ "title": "Documentation audience gating (book + doc): the tree endpoint and the doc read layer gate on ONE semantics — public/org/permissionSet, fail-closed — and an unclaimed doc never leaks through a public book", "since": "v14.1", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -1192,7 +1210,7 @@ "a session for each of the four personas above" ], "knownGaps": [ - "Stock showcase seeds are not known to author a book with `audience: { permissionSet: '' }` NOR one with `audience: 'public'`. Verify against the installed corpus FIRST (GET /api/v1/meta/book and read each item's `audience`). If neither gated nor public books exist, the permissionSet and public clauses are blocked(fixture) and MUST be recorded as such — do NOT score them from the `org` default alone, which is the one audience every authenticated caller passes and therefore proves nothing about the gate.", + "Stock showcase authors ONE book with `audience: 'public'` — `showcase_manual` (examples/app-showcase/src/system/books/index.ts) — and none with `audience: { permissionSet: '' }`. Verify against the installed corpus FIRST (GET /api/v1/meta/book and read each item's `audience`): the public clauses run on `showcase_manual`; if no permissionSet-gated book exists, the permissionSet clauses are blocked(fixture) and MUST be recorded as such (or staged per knownGaps[1]) — do NOT score them from the `org` default alone, which is the one audience every authenticated caller passes and therefore proves nothing about the gate.", "Authoring a book/doc at runtime to create the fixture is acceptable only if the run records that it did so and tears it down; the audience is read off the stored row, so a runtime-authored book exercises the same resolver." ] }, @@ -1226,9 +1244,9 @@ "evidence": "test output naming the fail-closed case" }, { - "clause": "gating is TWO-LAYERED: passing the book gate does not reveal every entry — the tree's entries are additionally filtered by each doc's effective audience, so a lesser persona's tree is a subset of admin's and contains no entry that would refuse on fetch", + "clause": "gating is TWO-LAYERED: passing the book gate does not reveal every entry — the tree's entries are additionally filtered by each doc's effective audience, so a caller's tree holds only the entries whose audience that caller passes and no entry that would refuse on fetch. For a permissionSet-gated doc the axis is HOLDING the set, not rank: audienceAllows grants no admin bypass (packages/spec/src/system/book.zod.ts#audienceAllows), so an admin who does not hold the set sees LESS than a member who does — a non-holder's tree is a subset of a holder's, never 'a lesser persona's of admin's'", "oracle": "api", - "verify": "diff the entry sets; then fetch EVERY entry the lesser persona's tree offers as that persona — each must resolve, none may 401/403 (a nav entry that refuses on click is the exact defect this filter exists to prevent)", + "verify": "diff the entry sets of a holder and a non-holder of each gating set (and, for org/unset docs, an authenticated caller against anonymous); then fetch EVERY entry the narrower tree offers as its own persona — each must resolve, none may 401/403 (a nav entry that refuses on click is the exact defect this filter exists to prevent)", "evidence": "entry-set diff + the per-entry fetch statuses" }, { @@ -1273,6 +1291,12 @@ "date": "2026-08-17", "change": "new — authored to RETIRE the `book` and `doc` coverage waivers, which claimed these kinds are 'display-only, no independent runtime behavior to gate beyond serving'. Grounding disproved that: the read layer runs a three-member audience vocabulary with an ADR-0049 fail-closed branch, a two-layer gate (book, then per-entry doc audience union), a deliberate orphan-group non-leak rule, an implicit-package-book fallback, and locale resolution. That is an access-control surface, tested here like every other one. Capability predates the ledger baseline (landed at spec 14.1.0, #2774) — never authored until now, which is itself the finding", "ref": "#9299" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "fixtures.knownGaps[0] and acceptance[3] (clause, verify) corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.docs-audience-gate; wave anchor #24579 §3). knownGaps[0] said stock showcase authors neither a public nor a permissionSet book; it ships the public `showcase_manual` (examples/app-showcase/src/system/books/index.ts), so only the permissionSet half is a fixture gap. acceptance[3] said a lesser persona's tree is a subset of admin's; audienceAllows (packages/spec/src/system/book.zod.ts) admits a permissionSet doc for holders only and has no admin bypass, so for those books the run's admin non-holder saw less than a holder — the subset relation is holder vs non-holder. acceptance[2]'s unknown-shape half (unreachable live, no pin) is a fixture/pin gap left on #24579 §4", + "ref": "#PENDING-24579R2" } ] }, @@ -1369,7 +1393,7 @@ "title": "The package manifest is enforced at the install boundary: an incompatible `engines` range is refused before the registry write, the namespace gate holds ownership, and a namespace-less runtime package has one derived", "since": "v15", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": [ @@ -1386,7 +1410,7 @@ "the DURABLE half of the handshake is NOT scored here: ADR-0087 also refuses an incompatible `sys_packages` row during boot-time rehydration, with boot CONTINUING (v15 release notes). Staging it needs an already-stored incompatible row — DB-level access, or a runtime whose major moved under a previously-installed package. Record blocked(fixture) if attempted. The install-path refusal this item does score is the same assertProtocolCompat call one boundary earlier, so a green here says nothing about the rehydration arm", "OS_METADATA_COLLISION=warn downgrades the namespace refusal to a console warning (registry.ts installPackage) — the policy is read by the registry, not per request, so that leg needs a SECOND server booted with the env set, never a mid-run toggle. Score it only if that boot is cheap, and record which arm the run exercised", "the install's durable leg is best-effort BY DESIGN (protocol.installPackage warns and continues when the `package` service cannot persist to sys_packages), so a probe that installs and does not survive a restart is that documented non-fatal path, not a failure of any clause here", - "NamespaceConflictError is a plain Error subclass with no `code` field (registry.ts) — unlike the protocol refusal, its HTTP status/code mapping is decided by whatever handler catches it, not pinned by the class. The run RECORDS the status and code it observes rather than asserting a pre-agreed one; see `negative` for the direction that is a finding" + "CLOSED (commit df657d9df): NamespaceConflictError carries the ADR-0112 envelope — `code` NAMESPACE_CONFLICT, `status` 422 (packages/objectql/src/registry.ts#NamespaceConflictError) — so the wire answer of POST /api/v1/packages for a conflict is pinned by the class, no longer decided by whichever handler catches it (before that commit the door fell through to 500 INTERNAL_ERROR). The 18.0 sweep observed 422 NAMESPACE_CONFLICT (QA run #24557). Assert that status and code; see `negative` for the 5xx direction" ] }, "steps": [ @@ -1442,7 +1466,7 @@ "negative": [ "an incompatible package that installs — any 2xx, or a row present after the refusal — is the FAIL this item exists for: it means the mismatch will surface later as a deep schema/renderer crash instead of at the boundary that could name it", "a compatible package REFUSED is the mirror fail and the more expensive one: no-range and unparsed-range must admit, so a false rejection here blocks every package that predates the range convention or is workspace-linked", - "a namespace conflict surfacing as a 5xx is the platform reporting its own fault for a manifest the caller authored — record it as a finding rather than scoring the clause on status alone; NamespaceConflictError carries no `code` (registry.ts), so nothing pins its mapping today", + "a namespace conflict surfacing as a 5xx is the platform reporting its own fault for a manifest the caller authored — a FAIL: NamespaceConflictError carries `code` NAMESPACE_CONFLICT and `status` 422 (packages/objectql/src/registry.ts#NamespaceConflictError), so anything other than 422 NAMESPACE_CONFLICT on the wire means a handler dropped the envelope", "a refusal whose message says only that something conflicted, without the namespace / current owner / incoming id, fails the ownership clause even when the refusal itself is correct — the ADR-0048 text exists so the author knows which of the two packages to rename", "a derived namespace present in the POST echo but absent from the read-back (or vice versa) is a disagreement between the in-memory registry and sys_packages, not a pass" ], @@ -1477,6 +1501,12 @@ "date": "2026-10-16", "change": "steps[1], [4] and [7](b) re-pointed (checklist-accuracy findings of QA run #23939, wave #23878). The probe manifests used scope 'custom', which POST /packages refuses (the enum is cloud | system | project), so the baseline never installed; the legacy engine.objectstack range must be full semver ('^17.0.0') because a bare major is a 400 VALIDATION_ERROR at parse; and #23939 drove the own-namespace reinstall leg as POST ?overwrite=true. Not loosened: every refusal and admission the clauses score is unchanged", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "fixtures.knownGaps[3] and negative[2] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.manifest-install-contract; wave anchor #24579 §3): both said NamespaceConflictError has no `code`, so its wire mapping was unpinned. Since commit df657d9df the class carries code NAMESPACE_CONFLICT and status 422 (packages/objectql/src/registry.ts), and the run observed 422 NAMESPACE_CONFLICT on POST /api/v1/packages. knownGaps[3] is now marked CLOSED with that reading, and negative[2] scores a 5xx as a FAIL instead of a finding to record. No acceptance clause changed", + "ref": "#PENDING-24579R2" } ] }, @@ -1594,10 +1624,10 @@ }, { "id": "platform-core.activation-ledger-row-contract", - "title": "sys_metadata_activation row contract: reads open / generic-data-API writes 405, one row per (metadata_type, name) with NO tenant column on the table at all, re-enable updates never deletes, and rows exist ONLY for the metadata types a shipped door writes — flow, action, position, permission (the ADR-0126 scope wall, as ADR-0131 D3 widens it)", + "title": "sys_metadata_activation row contract: reads open / generic-data-API writes 405, one row per (metadata_type, name) with NO tenant column on the table at all, re-enable updates never deletes, and rows exist ONLY for the metadata types a shipped door writes — flow, action, position, permission, sharing_rule (the ADR-0126 scope wall, as ADR-0131 D3 and D6 widen it)", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "api", "personas": ["seeded admin (admin@objectos.ai / admin123)"], @@ -1605,17 +1635,17 @@ "app": "showcase", "requires": [ "an isolated boot on a file DB (-d file:/tmp//data.db) WITH direct sqlite access to that file between boots: the object declares apiMethods ['get','list'], so REST can never stage the org-row and duplicate-row probes — same scratch-DB class of fixture platform-core.interrupted-migration-boot-report uses. On a stock boot no 'cloud' datasource is registered, so the table rides the global default driver and lives in the run's own file DB", - "the shipped switches as row producers: the flow toggle (POST /api/v1/automation/showcase_task_completed/toggle), the action activation door (POST /api/v1/actions/_activation/showcase_task/showcase_mark_done), and the security catalog activation door POST /api/v1/security/_activation/:type/:name for :type position and permission (packages/runtime/src/domains/catalog-activation.ts — e.g. position legal and permission set showcase_auditor; ⛔ never admin_full_access, which the last-admin guard refuses to switch off). Their own door contracts belong to automation.packaged-flow-disable-durable, api-backend.packaged-action-disabled-dispatch and access-security.security-catalog-activation-and-clone-doors — this item owns the ROWS they leave behind" + "the shipped switches as row producers: the flow toggle (POST /api/v1/automation/showcase_task_completed/toggle), the action activation door (POST /api/v1/actions/_activation/showcase_task/showcase_mark_done), and the security catalog activation door POST /api/v1/security/_activation/:type/:name for :type position, permission and sharing_rule (packages/runtime/src/domains/catalog-activation.ts — e.g. position legal, permission set showcase_auditor and the declared sharing rule share_new_inquiries_with_field_ops; ⛔ never admin_full_access, which the last-admin guard refuses to switch off). Their own door contracts belong to automation.packaged-flow-disable-durable, api-backend.packaged-action-disabled-dispatch and access-security.security-catalog-activation-and-clone-doors — this item owns the ROWS they leave behind" ], "knownGaps": [ - "metadata_type is an untyped Field.text (maxLength 100) with three writer sites and four values in the tree — 'flow' (packages/services/service-automation/src/flow-activation-store.ts), 'action' (packages/objectql/src/action-activation.ts), and 'position' / 'permission' (packages/runtime/src/domains/catalog-activation.ts, which constructs the shared store with the path's :type and closes that :type to exactly those two). There is NO spec enum to pin: the variants below are enumerated BY HAND and deliberately carry no enumSource, so a further writer (ADR-0126 §8 pre-charts tool/skill) lands without failing any ratchet. Re-derive the writer list (grep the METADATA_TYPE literals and every `new ObjectStoreMetadataActivationStore(` call site) before scoring the scope wall, and bump this item's variants when a new writer ships — revision 3 is that bump for the catalog door", + "metadata_type is an untyped Field.text (maxLength 100) with three writer sites and five values in the tree — 'flow' (packages/services/service-automation/src/flow-activation-store.ts), 'action' (packages/objectql/src/action-activation.ts), and 'position' / 'permission' / 'sharing_rule' (packages/runtime/src/domains/catalog-activation.ts, which constructs the shared store with the path's :type and closes that :type to exactly those three — sharing_rule since ADR-0131 D6). There is NO spec enum to pin: the variants below are enumerated BY HAND and deliberately carry no enumSource, so a further writer (ADR-0126 §8 pre-charts tool/skill) lands without failing any ratchet. Re-derive the writer list (grep the METADATA_TYPE literals and every `new ObjectStoreMetadataActivationStore(` call site) before scoring the scope wall, and bump this item's variants when a new writer ships — revision 3 is that bump for the catalog door, revision 5 the one for its sharing_rule :type", "the tenant-column leg is a SCHEMA probe, not a row probe: there is no organization_id column left to hand-seed (systemFields { tenant: false } — ADR-0131 D7 withdrew ADR-0126 D3's reserved-and-never-written column before it ever shipped), so it is staged by reading the physical column list and by an INSERT that NAMES the column and is refused. Its anti-vacuity control INSERT does land a row — that row is the run's own artifact and must be deleted before the restart (or the DB discarded)" ] }, "steps": [ - "boot isolated on the file DB; as admin flip EVERY shipped switch off — the flow toggle (showcase_task_completed), the action flip (showcase_mark_done), and the catalog door for one position and one permission set (POST /api/v1/security/_activation/position/legal and /permission/showcase_auditor, body {\"enabled\": false}); capture the four 200s", + "boot isolated on the file DB; as admin flip EVERY shipped switch off — the flow toggle (showcase_task_completed), the action flip (showcase_mark_done), and the catalog door for one position, one permission set and one declared sharing rule (POST /api/v1/security/_activation/position/legal, /permission/showcase_auditor and /sharing_rule/share_new_inquiries_with_field_ops, body {\"enabled\": false}); capture the five 200s", "read the ledger through the open read path: GET /api/v1/data/sys_metadata_activation — capture every row; each carries the four declared ledger columns (metadata_type, name, package_id, active) beside the driver's id and the injected audit family, and NO tenant column: organization_id is ABSENT from the row's key set, never present-and-NULL on it. ⛔ Assert the KEY SET, never a value read — a value assertion of the shape `row.organization_id ?? null` is `null` for a column that does not exist, so it passes while measuring nothing (the dogfood pin was inverted into a key-set assertion for exactly this reason)", - "scope wall sweep: the distinct metadata_type set over ALL rows is exactly {'flow','action','position','permission'} — the four values the shipped doors write. Any other value (hook/capability/tool/skill/…) is a FAIL of the model, not a coverage discovery: hooks are code-only by ADR-0126 §3 amendment ruling 1 (no ledger row, no disable switch), and a capability is code-only on the regime with no switch (catalog-activation.ts CATALOG_ACTIVATION_SUBJECTS). Then probe the door's own wall: POST /api/v1/security/_activation/capability/showcase.export_data {\"enabled\": false} answers 400 naming the two types it switches, and POST /api/v1/security/_activation/position/everyone {\"enabled\": false} answers 400 (an audience anchor) — re-read the ledger: neither left a row. Position and permission rows are no longer foreign here: ADR-0131 D3 moved the on/off switch of a position and of a permission set off the catalog row into this ledger (regime C as ADR-0131 D6 amends ADR-0126 §3; readDisabledCatalogNames in packages/core/src/security/resolve-authz-context.ts reads these two types and never the row's active column), which supersedes the 2026-08-26 #12159 ruling that a packaged permission set rides its kind's own active field", + "scope wall sweep: the distinct metadata_type set over ALL rows is exactly {'flow','action','position','permission','sharing_rule'} — the five values the shipped doors write. Any other value (hook/capability/tool/skill/…) is a FAIL of the model, not a coverage discovery: hooks are code-only by ADR-0126 §3 amendment ruling 1 (no ledger row, no disable switch), and a capability is code-only on the regime with no switch (catalog-activation.ts CATALOG_ACTIVATION_SUBJECTS). Then probe the door's own wall: POST /api/v1/security/_activation/capability/showcase.export_data {\"enabled\": false} answers 400 naming the three types it switches, and POST /api/v1/security/_activation/position/everyone {\"enabled\": false} answers 400 (an audience anchor) — re-read the ledger: neither left a row. Position and permission rows are no longer foreign here: ADR-0131 D3 moved the on/off switch of a position and of a permission set off the catalog row into this ledger (regime C as ADR-0131 D6 amends ADR-0126 §3; readDisabledCatalogNames in packages/core/src/security/resolve-authz-context.ts reads these two types and never the row's active column), which supersedes the 2026-08-26 #12159 ruling that a packaged permission set rides its kind's own active field. sharing_rule rows are not foreign either: ADR-0131 D6 charts a package-declared sharing rule under regime C, and the same door writes its row (packages/runtime/src/domains/catalog-activation.ts, CATALOG_ACTIVATION_SUBJECTS) — an organization-authored sys_sharing_rule row keeps its own switch and writes none", "write-refusal probes, all three verbs: POST /api/v1/data/sys_metadata_activation {metadata_type:'action',name:'qa_probe',package_id:'qa',active:false}, PATCH /api/v1/data/sys_metadata_activation/ {active:true}, DELETE /api/v1/data/sys_metadata_activation/ — each answers 405 naming the refused operation and the allowed set; re-read confirms every row unchanged", "re-enable every switch flipped in step 1 — the SAME rows update to active true (same ids, row count unchanged, nothing vanished)", "uniqueness probe (direct DB): stop the server; in the sqlite file INSERT a second row for an existing (metadata_type, name) pair — the declared unique:'global' index over exactly those two columns refuses with a UNIQUE constraint error; capture it. Both key parts are required text, so no NULL arm is left to collapse: the earlier unique:'organization' spelling asked the driver to prepend the tenant column in its COALESCE'd form (ADR-0120 D3) against the #5030 NULL-distinct hole, and with the column gone normalizeDeclaredIndex resolves tenantField to null — so the materialized DDL is byte-identical either way and the current spelling is a re-SPELLING, not a change of shape (sys-metadata-activation.object.ts records it)", @@ -1648,9 +1678,9 @@ "evidence": "the before/after row listings keyed by id" }, { - "clause": "the scope wall: after exercising every shipped switch, rows exist ONLY for metadata_type ∈ {'flow','action','position','permission'} — hand-enumerated, because no pinnable enum exists (the column is untyped Field.text; the values are string literals at the flow and action stores and the catalog door's closed :type set). Position and permission rows belong here since ADR-0131 D3 moved those two kinds' on/off switch off the catalog row into this ledger (written by POST /api/v1/security/_activation/:type/:name, read by readDisabledCatalogNames), superseding the #12159 'permission rides its own active field' ruling. A hook/capability (or any other) row is a FAIL of the model (ADR-0126 §3: hook reclassified code-only by amendment ruling 1; a capability is code-only with no switch), and the catalog door refuses every other :type, and the audience anchors, with a 400 that writes nothing", + "clause": "the scope wall: after exercising every shipped switch, rows exist ONLY for metadata_type ∈ {'flow','action','position','permission','sharing_rule'} — hand-enumerated, because no pinnable enum exists (the column is untyped Field.text; the values are string literals at the flow and action stores and the catalog door's closed :type set). Position and permission rows belong here since ADR-0131 D3 moved those two kinds' on/off switch off the catalog row into this ledger (written by POST /api/v1/security/_activation/:type/:name, read by readDisabledCatalogNames), superseding the #12159 'permission rides its own active field' ruling; sharing_rule rows belong here since ADR-0131 D6 charts a package-declared sharing rule under regime C and the same door writes them. A hook/capability (or any other) row is a FAIL of the model (ADR-0126 §3: hook reclassified code-only by amendment ruling 1; a capability is code-only with no switch), and the catalog door refuses every other :type, and the audience anchors, with a 400 that writes nothing", "oracle": "api", - "verify": "distinct metadata_type over the full table equals exactly the four-value set; the capability-type and audience-anchor door probes each answer 400 and the ledger's row count is unchanged after them; re-derive the writer list (the METADATA_TYPE literals and the `new ObjectStoreMetadataActivationStore(` call sites) first, per knownGaps — a new LEGITIMATE writer means this item needs a revision, a value outside the four from stock switches without one means the wall fell", + "verify": "distinct metadata_type over the full table equals exactly the five-value set; the capability-type and audience-anchor door probes each answer 400 and the ledger's row count is unchanged after them; re-derive the writer list (the METADATA_TYPE literals and the `new ObjectStoreMetadataActivationStore(` call sites) first, per knownGaps — a new LEGITIMATE writer means this item needs a revision, a value outside the five from stock switches without one means the wall fell", "evidence": "the distinct-set query result + the two door refusals with the unchanged row count + the writer-grep excerpt" }, { @@ -1666,13 +1696,14 @@ "a row deleted by re-enable is a FAIL of §6 wall 3 even though every door answered 200 — the wire looks identical either way, only the row read tells them apart", "two contradictory active rows accepted for one artifact is a FAIL of the declared unique:'global' index — and ⛔ do not diagnose it as the #5030 NULL-distinct hole: that hole needed a nullable tenant column inside the key, and this table has none. Both key parts are required text, so a duplicate accepted here means the index is missing or was never materialized, not that a NULL slipped through", "treating this item's variants as a pinned enum is a mis-read: they are hand-enumerated and un-pinned by construction — a stale value list over a tree that grew a new writer is a checklist defect, not a platform one; revise the item", - "a catalog-door write for a :type other than position or permission (capability, hook, …), or for an audience anchor (everyone, guest), that answers 2xx or leaves a ledger row is a FAIL of the door's closed :type set — it would switch off something the regime gives no switch" + "a catalog-door write for a :type other than position, permission or sharing_rule (capability, hook, …), or for an audience anchor (everyone, guest), that answers 2xx or leaves a ledger row is a FAIL of the door's closed :type set — it would switch off something the regime gives no switch" ], "variants": [ "metadata_type 'flow' — written by ObjectStoreFlowActivationStore (packages/services/service-automation/src/flow-activation-store.ts)", "metadata_type 'action' — written by ObjectStoreActionActivationStore (packages/objectql/src/action-activation.ts)", "metadata_type 'position' — written by the security catalog activation door (packages/runtime/src/domains/catalog-activation.ts, the shared ObjectStoreMetadataActivationStore constructed with the path's :type), read by readDisabledCatalogNames", - "metadata_type 'permission' — the same door, for a permission set (ADR-0131 D3)" + "metadata_type 'permission' — the same door, for a permission set (ADR-0131 D3)", + "metadata_type 'sharing_rule' — the same door, for a package-declared sharing rule (ADR-0131 D6 regime C; catalog-activation.ts SHARING_RULE_ACTIVATION_SUBJECT, read from the sharing-rule service rather than the security catalog)" ], "traps": ["destructive-in-place", "absence-inference"], "automated": { @@ -1717,6 +1748,12 @@ "date": "2026-10-16", "change": "acceptance[0].verify, steps[6] and acceptance[2].clause corrected (residual checklist-accuracy finding of QA run #23939, wave #23878; PR #24000 re-pointed clause 4). The 405's allowed set is [get, list, aggregate, search, export] — the declared apiMethods ['get','list'] plus the verbs list derives (API_METHOD_DERIVATION) — not the bare apiMethods pair, and PRAGMA table_info also lists the __search companion column the searchable object carries. The verify text now names the allowed set exactly (a write verb in it FAILS) and the column list names __search. Not loosened: organization_id must still be absent and every generic write still 405", "ref": "#24036" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "the scope wall widened from four metadata_type values to five — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.activation-ledger-row-contract; wave anchor #24579 §3, which lists it under access-security). The security catalog activation door now also accepts :type sharing_rule (packages/runtime/src/domains/catalog-activation.ts CATALOG_ACTIVATION_SUBJECTS, ADR-0131 D6 regime C) and writes its row to this ledger through the same ObjectStoreMetadataActivationStore — a fifth writer the item's hand-enumerated variants did not carry, the case negative[4] says is a checklist defect. Title, steps[0] and [2], fixtures.requires[1], knownGaps[0], acceptance[4] (clause, verify) and negative[5] now name sharing_rule, variants gains it, and steps[0] flips the showcase's declared rule share_new_inquiries_with_field_ops. QA run #24601 drove that switch and read the row {metadata_type: sharing_rule, active: false}. No other clause changed", + "ref": "#PENDING-24579R2" } ] }, @@ -1804,19 +1841,19 @@ "title": "Marketplace install-local: the wiring matrix mounts the right arms per OS_CLOUD_URL, the air-gapped inline install works with NO control plane, reseed/purge touch exactly the sample rows, every mutating door is manage_metadata-gated both sides, and the listing projects installedBy/storageDir per principal", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": [ "seeded admin (admin@objectos.ai / admin123 — admin_full_access carries manage_metadata)", - "a plain member (fresh runtime sign-up — member_default holds NO manage_metadata)", + "a plain member, created by the admin (POST /api/v1/auth/admin/create-user) — the stock audience posture is invite_only (packages/plugins/plugin-auth/src/audience-posture.ts), so a fresh self sign-up is refused; member_default holds NO manage_metadata", "anonymous", "operator (local shell — the `os package install` door)" ], "fixtures": { "app": "showcase", "requires": [ - "TWO isolated boots of the same app: boot A = plain `objectstack dev` with NO OS_CLOUD_URL (resolveCloudUrl() falls back to DEFAULT_CLOUD_URL — truthy — so the CLOUD arm mounts: proxy + install-local + cloud-connection + runtime-config); boot B = the same line with OS_CLOUD_URL=off exported (the offline arm: install-local pinned to the 'off' control plane + runtime-config ONLY — nothing that dials out mounts). The matrix is decided by Serve.planMarketplaceWiring, pure and testable (packages/cli/src/commands/serve.ts)", + "TWO isolated boots of the same app: boot A = plain `objectstack dev` with NO OS_CLOUD_URL (resolveCloudUrl() falls back to DEFAULT_CLOUD_URL — truthy — so the CLOUD arm mounts: proxy + install-local + cloud-connection + runtime-config); boot B = the same line with OS_CLOUD_URL=off exported (the offline arm: install-local pinned to the 'off' control plane + runtime-config ONLY — nothing that dials out mounts). The matrix is decided by Serve.planMarketplaceWiring, pure and testable (packages/cli/src/commands/serve.ts). ⚠️ On showcase the boot-A cloud arm is the HOST's, not the CLI's: examples/app-showcase/objectstack.config.ts mounts MarketplaceProxyPlugin, MarketplaceInstallLocalPlugin and CloudConnectionPlugin itself whenever resolveCloudUrl() is truthy, and RuntimeConfigPlugin always, and planMarketplaceWiring checks presence — so on boot A the CLI wires none of the four, and on boot B its offline arm adds install-local alone (pinned to the 'off' control plane) beside the host's runtime-config. The observable route matrix is the same either way", "a compiled package artifact for the inline install: pnpm -C examples/app-crm build → dist/objectstack.json (the air-gapped path the #8343 fix exists for — `handleInstall`'s inline branch never reads the cloud URL). Both boots that install run from a SCRATCH COPY of the app, never the shared tree: install-local caches the manifest under the working directory's .objectstack/installed-packages (packages/cloud-connection/src/local-manifest-source.ts#DEFAULT_INSTALLED_PACKAGES_DIR, resolved against process.cwd()), which a per-boot OS_HOME / OS_STORAGE_LOCAL_ROOT does not redirect (RUNNER.md environment facts)", "admin credentials passed to the CLI door (--email/--password or OS_RUNTIME_EMAIL/OS_RUNTIME_PASSWORD) — the runtime refuses an unauthenticated install with a 401 that names exactly that remedy (packages/cli/src/commands/package/install.ts)" ], @@ -1829,7 +1866,7 @@ "boot A (no OS_CLOUD_URL), as admin: GET /api/v1/runtime/config and record features.marketplace + features.installLocal; probe the mounts — GET /api/v1/marketplace/install-local (authed) → 200, and the browse namespace answers as a mounted passthrough (a non-404 that reflects the upstream's reachability, not this kernel's routing). Capture the boot banner's Marketplace plugin line", "boot B (OS_CLOUD_URL=off), as admin: GET /api/v1/runtime/config → still served, now reporting installLocal:true AND marketplace:false (#8389 discovery + #8356 route-table derivation); GET /api/v1/marketplace/install-local → 200 []; a browse-namespace probe (e.g. GET /api/v1/marketplace/packages) → 404 (the proxy did NOT mount); GET /api/v1/cloud-connection/status → 404 (cloud-connection did NOT mount)", "build the artifact: pnpm -C examples/app-crm build; confirm dist/objectstack.json exists", - "air-gapped install against boot B: `os package install./examples/app-crm/dist/objectstack.json` with admin credentials — the CLI signs in, then POSTs the manifest inline to /api/v1/marketplace/install-local (install.ts); capture the success output and the installed entry", + "air-gapped install against boot B: `os package install ./examples/app-crm/dist/objectstack.json` with admin credentials — the CLI signs in, then POSTs the manifest inline to /api/v1/marketplace/install-local (install.ts); capture the success output and the installed entry", "confirm the install registered: GET /api/v1/meta/app lists the CRM app; a CRM object answers on /api/v1/data; GET /api/v1/marketplace/install-local as admin shows the entry WITH installedBy, and storageDir on the listing envelope (data.storageDir — one value for the whole ledger, not a per-entry field)", "per-principal projection (#9011): repeat the GET as the plain member — 200, the entry listed, but installedBy ABSENT from the entry and storageDir ABSENT from the envelope; repeat anonymous — 401", "reseed/purge on the installed entry: create ONE user-authored row in an installed CRM object and record its id; POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data as admin — capture {deleted, skipped, errors}; re-read: every manifest-declared seed id is gone, the user-authored row SURVIVES (the purge deletes only ids declared in the cached manifest's seed datasets — marketplace-install-local-plugin.ts); restart boot B against the same DB and confirm the purged rows do NOT come back (sampleDataPurged persisted on the ledger entry)", @@ -1921,6 +1958,12 @@ "date": "2026-10-16", "change": "steps[4] and [5] and acceptance[3].clause corrected (checklist-accuracy finding of QA run #23939, wave #23878). storageDir is served once, on the listing envelope (data.storageDir), not on each entry — the admin entry carries installedBy only — so 'installedBy + storageDir on each entry' named a field the entries never carry. The clause now reads each field where it lives and still requires both to be omitted for the member. Not loosened: a member who sees either field, or is refused or served an empty list, still FAILS", "ref": "#24036" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "personas[1], steps[3] and fixtures.requires[0] corrected — three checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.marketplace-install-local-lifecycle; wave anchor #24579 §3). The plain member cannot be a fresh sign-up: the stock audience posture is invite_only (plugin-auth audience-posture.ts), so the run created the member through the admin create-user door. steps[3] read `install./examples` (a missing space). On showcase the cloud arm of boot A, and runtime-config on both boots, are mounted by the host config (examples/app-showcase/objectstack.config.ts), and Serve.planMarketplaceWiring wires only what the host did not — the matrix the clauses score is unchanged. No acceptance clause changed", + "ref": "#PENDING-24579R2" } ] }, @@ -2039,7 +2082,7 @@ "title": "Runtime config boot read: GET /api/v1/runtime/config answers 200 ANONYMOUSLY with cloud URL, capability flags, branding and telemetry posture; the legacy /api/v1/studio/runtime-config alias is byte-identical (same handler); nothing credential-shaped is in the body", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": ["anonymous", "seeded admin (control only)"], @@ -2051,7 +2094,7 @@ }, "steps": [ "on a stock isolated boot, with NO session: GET /api/v1/runtime/config — capture status, headers and full body", - "confirm the payload shape: features (installLocal, marketplace, …), branding (productName, productShortName, logoUrl, faviconUrl, brandColor, pwaDescription, pwaThemeColor), telemetry (errorReporting when configured), and the cloud/control-plane URL field", + "confirm the payload shape: features (installLocal, marketplace, …), branding (productName, productShortName, pwaDescription and pwaThemeColor always; logoUrl, faviconUrl and brandColor ONLY when configured — by the plugin config or their env vars — since an unset one serialises as absent, which is not a missing key; `stage` likewise only when set — packages/cloud-connection/src/runtime-config-plugin.ts), telemetry (errorReporting when configured), and the cloud/control-plane URL field", "GET /api/v1/studio/runtime-config with the same (absent) auth — diff the two bodies byte for byte (the alias is mounted with the SAME handler instance; ledgered as its own row because it is its own wire path)", "scan the serialised body for credential-shaped content: no session token, no secret handle plaintext, no Authorization material, no sys_secret ciphertext; telemetry.errorReporting.dsn, when present, is a PUBLIC client key by design — record its presence, it is not a finding", "control: repeat the read WITH an admin session and confirm the payload is not silently enriched with anything the anonymous read withheld (the route is unauthenticated by construction — there is no per-principal projection to leak through)", @@ -2103,6 +2146,12 @@ "date": "2026-08-30", "change": "new — authored in the 2026-08-30 sweep (routes angle): the anonymous boot-read the whole console SPA depends on had no item. Kept as its own item rather than clauses on the marketplace lifecycle item, split along the register's suggested seam: this item owns the route + alias + payload hygiene; the marketplace item owns the features-vs-live-routes agreement", "ref": "#sweep-2026-08-30" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "steps[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.runtime-config-boot-read steps[1]; wave anchor #24579 §3): the branding block carries logoUrl, faviconUrl and brandColor only when they are configured — runtime-config-plugin.ts reads each from the plugin config or its env var and serialises an unset one as undefined, which JSON drops — so their absence on a stock boot is the contract, not a missing key. No acceptance clause changed", + "ref": "#PENDING-24579R2" } ] }, @@ -2209,7 +2258,7 @@ "title": "Reserved URL params (ADR-0054 C3): recordId drawer and form overlay push so Back closes them, tab/palette/shortcuts replace so Back never pages through chrome, formObject/formLink pre-link a child create, ?tab cold-loads, the from trail keeps a clickable path back, and a recordId+recordObject mismatch fails closed", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "browser", "personas": ["seeded admin (admin@objectos.ai / admin123)"], @@ -2228,7 +2277,7 @@ "child pre-link: from a parent record, create a subtable child via the overlay — capture ?form=new&formObject=&formLink=:; the FK field arrives pre-linked to the parent; RELOAD the URL cold — the pre-link survives (refresh-safe by construction: the state is the URL)", "tab cold-load: copy a record URL, append ?tab=related (or a related: value), open in a fresh tab — the detail lands on that tab; now switch tabs in-page several times and press Back ONCE — you leave the record (or close the prior overlay), you do NOT step back through tab selections (tab REPLACEs)", "palette/shortcuts chrome: open ?palette=1 and ?shortcuts=1 overlays, close them, and confirm Back never replays them (replace semantics — useUrlOverlay defaults to replace)", - "drill trail: from a record, drill into a related record — the URL carries from=…; the breadcrumb renders the ancestor trail and clicking a crumb navigates back to that level", + "drill trail: from a record, drill into a related record — the URL carries from=…; the drilled record shows an inline '← PARENT' back link above its body (the top bar draws no record crumb since objectui#12461 — objectui packages/app-shell/src/views/RecordDetailView.tsx), and clicking it navigates to the parent, whose own back link carries the rest of the trail", "cross-object refusal (#4292): take a real record id of object A; navigate to /forms/?recordId=&recordObject= — the route renders an ERROR state; capture the network: NO /data/ read was issued for the mismatched pair (the refusal precedes the read, so nothing is read and nothing can be written); control: the same URL with the AGREEING recordObject loads and prefills normally", "case probe: repeat the mismatch with only a case difference in recordObject — still refused (object names are not case-folded)" ], @@ -2258,10 +2307,10 @@ "evidence": "both screenshots + the URL" }, { - "clause": "the from drill trail renders a clickable ancestor path and each crumb navigates to its level", + "clause": "the from drill trail keeps a clickable path back: the drilled record's inline back link names its immediate parent and navigates there, and that parent's own back link continues up the trail — one level per link, built from ?from= so it survives a refresh and a shared link", "oracle": "screenshot", - "verify": "the drilled record shows the trail; a crumb click lands on the ancestor", - "evidence": "trail screenshot + the crumb navigation" + "verify": "the drilled record shows the back link naming its parent; clicking it lands on the parent, and the parent's back link leads to the next ancestor", + "evidence": "back-link screenshot per level + the navigation per click" }, { "clause": "a recordId whose recordObject names ANOTHER object fails CLOSED (#4292): error state, NO /data read issued for the mismatch, no form to submit — and the refusal is exact (case differences refuse too); the agreeing control loads normally", @@ -2279,7 +2328,7 @@ "traps": ["hydration-race", "automation-input", "shared-browser-tab"], "automated": { "kind": "unit", - "ref": "objectui: apps/console/src/components/FormPage.recordObject.test.tsx (the #4292 fail-closed matrix: mismatch → error state, no /data request, no write path, case-sensitive; agreeing control unchanged); objectui: packages/app-shell/src/hooks/__tests__/useUrlOverlay.test.tsx (replace semantics)" + "ref": "objectui: apps/console/src/components/FormPage.recordObject.test.tsx (the #4292 fail-closed matrix: mismatch → error state, no /data request, no write path, case-sensitive; agreeing control unchanged); objectui: packages/app-shell/src/hooks/__tests__/useUrlOverlay.test.tsx (open / close / toggle / alias param state only — it asserts no history push or replace, so the push-vs-replace semantics of acceptance[0] and [1] are evidenced by hand)" }, "source": [ "objectui packages/app-shell/src/urlParams.ts (the single registry of reserved params with the per-param push/replace column and the rule of thumb: user-opened overlays push so Back closes; passive selection and transient chrome replace so Back never pages through them; objectui#2269 P3, ADR-0054 C3)", @@ -2293,6 +2342,12 @@ "date": "2026-08-30", "change": "new — authored in the 2026-08-30 sweep (console-UI angle): the console's reserved-URL-param contract (ADR-0054 C3) had no item though it is the cross-route state contract every overlay and deep link rides. Grounded against urlParams.ts's own push/replace table and the FormPage #4292 fail-closed pin (objectui-repo-only, so the clause is drivable by hand per the RUNNER standing fact). since v16 by judgment from the issue numbers (#2269/#2604/#4278/#4292 pre-date the v17 window)", "ref": "#sweep-2026-08-30" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "steps[5], acceptance[4] (clause, verify, evidence) and automated.ref corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.url-overlay-contract; wave anchor #24579 §3). The from trail no longer renders as a breadcrumb: since objectui#12461 the top bar draws no record crumb and RecordDetailView.tsx shows an inline back link to the immediate parent, each parent's link carrying the trail above it, so the clause now scores that chain. automated.ref described useUrlOverlay.test.tsx as pinning replace semantics; at the console pin it asserts param state only and nothing about history, which is why the run evidenced replace by hand. The push/replace clauses themselves are unchanged", + "ref": "#PENDING-24579R2" } ] }, @@ -2404,7 +2459,7 @@ "title": "Shell-hard-coded Administration targets + the retired System hub: every Administration target URL resolves to a real page (no RouteNotFound, no RecordDetailView for an object named 'system'), /apps/:app/system lands on the settings hub in one hop and every hub entry resolves, the five legacy system/* URLs redirect (users and organizations to their object routes, roles, positions and permissions to the Setup metadata catalog), and a member's shell offers no Administration entry", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "browser", "personas": [ @@ -2448,9 +2503,9 @@ "evidence": "the five redirect traces + landings" }, { - "clause": "a member's shell offers no Administration entry anywhere (no /home sidebar — HomeLayout omits it; Help menu offers Documentation); the server refusal is proven by the builtin-apps item.", + "clause": "a member's shell offers no Administration entry anywhere (no /home sidebar — HomeLayout omits it; the Help menu offers documentation entries, Keyboard shortcuts and About — nothing that administers); the server refusal is proven by the builtin-apps item.", "oracle": "dom", - "verify": "the member's /home and one app shell, in the member's own session (wrong-persona): screenshot, then DOM — no Administration entry in any chrome region, and the Help menu lists documentation entries only", + "verify": "the member's /home and one app shell, in the member's own session (wrong-persona): screenshot, then DOM — no Administration entry in any chrome region, and the Help menu lists documentation entries, Keyboard shortcuts and About and no administration entry (objectui packages/app-shell/src/layout/AppHeader.tsx at the console pin)", "evidence": "the member screenshots + DOM excerpt" } ], @@ -2485,6 +2540,12 @@ "date": "2026-10-13", "change": "clause 3, acceptance[2] (the legacy-redirect clause) re-pointed, with step 4, the title and the AppContent source note (stale spec, #23104 A2). Since objectui#7611, roles, positions and permissions no longer land on the sys_position / sys_permission_set object routes: SystemCatalogRedirect forwards them to the Setup metadata catalog in its environment scope (roles and positions to /apps/setup/metadata/position?scope=environment, permissions to /apps/setup/metadata/permission?scope=environment), because ADR-0131 D3 makes positions and permission sets registry items. users and organizations still use SystemObjectRedirect and are unchanged. Read at objectui 1fb1ced36243 (this repo's .objectui-sha): AppContent.tsx's systemRoutes, and the 'the catalog entries reach the Setup catalog (objectui#7611)' block of AppContent.systemHubRoutes.test.tsx. Both files are byte-identical at 7578323a544e", "ref": "#23121" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "acceptance[3] (clause, verify) corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.home-admin-cluster-links acceptance[3]; wave anchor #24579 §3): the member's Help menu does not list documentation entries only — at the console pin 4016370a it also lists Keyboard shortcuts and About (objectui packages/app-shell/src/layout/AppHeader.tsx). The clause's point is unchanged: no Administration entry anywhere in the member's chrome", + "ref": "#PENDING-24579R2" } ] }, @@ -2977,7 +3038,7 @@ "title": "A v18 server refuses to boot rather than serve wrongly: against a 17.x-shaped database (a physical organization_id on a non-tenant table, no verified ADR-0131 ceremony marker) it stops before listening with ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED naming the tables and the command, and with security on but no identity objects it stops with AuthzIdentityObjectsMissingError", "since": "v18", "status": "active", - "revision": 2, + "revision": 3, "priority": "P0", "surface": "cli", "personas": [ @@ -2992,7 +3053,7 @@ }, "steps": [ "snapshot the v17 copy through SQLite — its schema (`sqlite_master`, `pragma_table_info` per table) and every table's rows, read with the sqlite3 CLI or node:sqlite so a `-wal` file is included (copy the v17 DB together with its `-wal`, or checkpoint the original before copying); run `os serve` (v18) against the copy with `OS_DATABASE_URL=file:COPY.db` (`os serve` has no `-d` flag); read the output and exit code; snapshot again and diff the two", - "run `os dev` on a scratch config with services:{auth:false} and security on" + "boot the auth-less security composition from a scratch config whose own `plugins` mounts `new SecurityPlugin()` (@objectstack/plugin-security) with the `auth` tier off (`tiers` without 'auth', or `--preset minimal`), so the platform composes no AuthPlugin and nothing registers sys_user / sys_member. ⚠️ `services: {auth: false}` is not a stack key (packages/spec/src/stack.zod.ts declares no `services`), and a stock boot composes the security plugin only beside AuthPlugin (packages/core/src/stack-auth.ts#resolvePlatformAuthComposition), so no stock `os dev` reaches this refusal. This rig is derived from the composition source and not yet replayed live: if it fails for any other reason, score acceptance[3] on its pin and record the rig as not proven" ], "acceptance": [ { @@ -3002,9 +3063,9 @@ "evidence": "the vitest summary line per pin file, with the package name and the subject sha" }, { - "clause": "the v18 serve against the v17 DB exits before kernel:listening with ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED, naming the tables and `os migrate organization-ownership`", + "clause": "the v18 serve against the v17 DB exits 1 before kernel:listening with the organization-ownership refusal, which names the tables and `os migrate organization-ownership`. ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED is that refusal's `code` (packages/runtime/src/organization-ownership-boot-guard.ts#OrganizationOwnershipBootRefusal) and the boot never prints it — `os serve` prints the refusal's message only — so the code is the pins' evidence (acceptance[0]), not this log's", "oracle": "log", - "verify": "boot output + exit code + no listening line", + "verify": "boot output + exit code + no listening line: the printed refusal names each table still carrying organization_id and the `os migrate organization-ownership` command. ⛔ Do not grep the log for the code literal — it is not printed (packages/cli/src/commands/serve.ts prints the refusal's message)", "evidence": "the refusal excerpt" }, { @@ -3016,7 +3077,7 @@ { "clause": "the auth-less security boot is refused naming createIdentityObjectsPlugin(), not served with 503s", "oracle": "log", - "verify": "boot output", + "verify": "boot output of the steps[1] boot: the refusal names createIdentityObjectsPlugin() and the boot does not reach listening. With no such boot run, this clause rests on packages/plugins/plugin-security/src/authz-identity-objects-boot-refusal.test.ts (automated.ref) — record which", "evidence": "the excerpt" } ], @@ -3049,6 +3110,12 @@ "date": "2026-10-19", "change": "acceptance[2] (clause, verify, evidence) and steps[0] re-worded — an assertion defect found by the 18.0 QA sweep (QA run #24556, platform-core.v18-boot-refusals acceptance[2]: FAIL, confirmed by an independent RUNNER rule 7 re-derivation; card #24567 row 1). The product is right and the clause was wrong: the guard judges in its own start(), after the engine (whose start() runs the additive schema sync the ceremony waits for) and ahead of every plugin that writes a row (packages/runtime/src/organization-ownership-boot-guard.ts), and ADR-0131 D10 item 5 says the refusing runtime never auto-migrates (the ceremony's fates); nothing in it forbids additive schema sync. So a refused boot legitimately adds tables, columns and indexes, and 'did not alter the database' (checksum before == after) failed on a correct product. The clause now asserts what the guard promises — no row written, no D10 step run — through a row-level SQLite diff that includes the `-wal` (the run's second boot left the main file's hash unchanged with its DDL in the `-wal`). steps[0] also stops passing `-d` to `os serve`, which declares no such flag (the same run's checklist-accuracy finding); `OS_DATABASE_URL` is what the run booted with. The other three clauses are unchanged", "ref": "#24592" + }, + { + "revision": 3, + "date": "2026-10-19", + "change": "steps[1], acceptance[1] (clause, verify) and acceptance[3].verify corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.v18-boot-refusals; wave anchor #24579 §3). steps[1] booted `os dev` with `services: {auth: false}`, which is not a stack key (packages/spec/src/stack.zod.ts), so the run recorded acceptance[3] blocked(fixture); a stock boot composes the security plugin only beside AuthPlugin (packages/core/src/stack-auth.ts resolvePlatformAuthComposition), so the step now names a scratch config that mounts SecurityPlugin itself with the auth tier off, marked as not yet replayed live, and acceptance[3] names its pin as the fallback. acceptance[1] said the boot exits with ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED on a log oracle; that literal is only the refusal error's code (OrganizationOwnershipBootRefusal) and serve.ts prints the message alone, so the log oracle now reads the named tables and command and the code is left to the pins. acceptance[2] and steps[0] (#24592) are unchanged", + "ref": "#PENDING-24579R2" } ] }, @@ -3205,7 +3272,7 @@ "title": "`os serve` composition on v18: unclaimed top-level items of a packages[] config are served under manifest.id with a warn line, an app shipping its own default sys_organization record page boots, a host datasourceMapping is installed before the first schema sync, a production host with no storage refuses to boot naming three remedies, and /_console/ answers 503 with the remedy when the console is not built", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "cli", "personas": [ @@ -3215,7 +3282,7 @@ "app": "showcase", "requires": [ "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", - "per leg a scratch config: packages[] plus one top-level view; an isDefault:true sys_organization record page; a datasourceMapping (examples/app-crm declares one); NODE_ENV=production with no storage service" + "per leg a scratch config: packages[] plus one top-level view; an isDefault:true sys_organization record page; a datasourceMapping — no example declares one any more (examples/app-crm removed its rule; its objectstack.config.ts says why), so author one on the scratch config: `datasourceMapping: [{ default: true, datasource: NAME }]` over a declared, file-backed sqlite datasource, the shape packages/cli/test/serve-datasource-mapping-before-sync.integration.test.ts boots; NODE_ENV=production with no storage service" ], "knownGaps": [ "test/serve-organization-record-page.e2e.test.ts is a `*.e2e.test.ts` file, which packages/cli collects ONLY under OS_TEST_TIERS=nightly (vitest.config.ts nightly tiers): run it as `OS_TEST_TIERS=nightly npx vitest run test/serve-organization-record-page.e2e.test.ts` in packages/cli — a plain run of the automated.ref list silently collects 25 of its 26 cli files" @@ -3281,6 +3348,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "fixtures.requires[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.serve-composition-v18; wave anchor #24579 §3): examples/app-crm no longer declares a datasourceMapping (its config records why the rule was deleted, #4462), and no other example declares one, so the leg's scratch config authors the mapping itself, in the shape the item's own pin serve-datasource-mapping-before-sync.integration.test.ts boots. No acceptance clause changed", + "ref": "#PENDING-24579R2" } ] } From 3fd4d5800fa3056a061606c497b184e6a686f4b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 17:59:17 +0000 Subject: [PATCH 2/7] docs(qa): correct thirteen access-security checklist items the 18.0 sweep measured as inaccurate Checklist-accuracy findings of QA run #24559 (access-security, 5b14b7f3) and round 2's #24601 (ba4afcc2), each re-derived from source at 8fbbd5082f: sharing-rules-widen, record-share-grant-revoke, scope-depth-asymmetry, fls-mask-and-strip, record-access-explain, anonymous-floor-v18-doors, meta-authoring-trail-and-audience-gates, grant-table-write-contract, deployment-tables-platform-only, share-link-combined-gates, audit-log-browser, permission-matrix-edit-loop, capability-declaration-lifecycle. One revision and one history row per item. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/access-security.json | 166 +++++++++++++----- 1 file changed, 122 insertions(+), 44 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index e17fa4b013..9673e350aa 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -257,7 +257,7 @@ "title": "Record-grained access explain shows per-layer attribution and a verdict", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -274,7 +274,7 @@ }, "steps": [ "boot showcase with the console; as admin create the probe: a member-owned showcase_private_note; grant a second member showcase_auditor", - "in the console's access explain panel pick user = the auditor, object = showcase_private_note, record = the probe; screenshot the layered result", + "in the console's access explain panel — Studio's Access pillar (/_console/studio/PKG/access, e.g. com.example.showcase) → the header's 'Explain access' action (objectui packages/app-shell/src/views/metadata-admin/AccessExplainPanel.tsx) — pick user = the auditor, object = showcase_private_note, record = the probe; screenshot the layered result", "call the API twin: POST /api/v1/security/explain (client security.explain; GET variant also mounted — rest-route-ledger security-explain family) with the same user/object/record; capture the JSON", "as the auditor (real session): GET /api/v1/data/showcase_private_note/ — the impersonated-read cross-check", "repeat panel + API explain for the PLAIN member (no auditor set) on the same record", @@ -340,6 +340,12 @@ "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" + }, + { + "revision": 3, + "date": "2026-10-19", + "change": "steps[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.record-access-explain; wave anchor #24579 §3): the step named 'the console's access explain panel' without saying where it opens; at the console pin 4016370a it is the 'Explain access' action in Studio's Access pillar header (/_console/studio/PKG/access), the route the run drove. No clause changed", + "ref": "#PENDING-24579R2" } ] }, @@ -709,7 +715,7 @@ "title": "Field-level security: editable:false strips/denies writes API-side and renders read-only in the UI; masked-read half needs an authored readable:false grant", "since": "v15", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": [ @@ -735,7 +741,7 @@ "re-read the project system-side; compare budget before/after", "as admin: PATCH the same budget field to a new value and re-read (the entitled side)", "in the console as the contributor: open the project's detail/edit form; screenshot; inspect the state of the budget and spent controls — the two budget figures the edit form renders (project.view.ts form section 'Budget & Schedule': budget, spent, start_date, end_date). budget_remaining is a formula field and is not on the edit form at all", - "read-mask half (see knownGaps): grant a scratch readable:false set to a fresh member, then GET the project as that member and inspect whether the masked field is absent/nulled in the API body AND blank in the UI render", + "read-mask half (see knownGaps): grant the STOCK showcase_client_liaison set (fixtures.requires[2] — the readable:false grant on budget, spent and budget_remaining; no scratch set is needed) to a fresh member who holds no showcase_contributor, then GET the project as that member and inspect whether the masked field is absent/nulled in the API body AND blank in the UI render", "as admin GET the same row — the unmasked contrast" ], "acceptance": [ @@ -811,6 +817,12 @@ "date": "2026-10-16", "change": "clause 4 and step 5 asked for the disabled state of THREE budget controls (#23878 checklist-accuracy finding). The edit form renders two: project.view.ts's 'Budget & Schedule' section lists budget, spent, start_date and end_date, and budget_remaining is a formula field that no edit form carries (re-measured on main c91af8ed6). The clause now scores budget and spent, and says why the formula cannot discriminate the lock. The API clauses are unchanged", "ref": "#24036" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "steps[5] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.fls-mask-and-strip steps[5]; wave anchor #24579 §3): the step still told the run to author a scratch readable:false set, while fixtures.requires[2] and knownGaps[0] already record that the stock showcase_client_liaison set (examples/app-showcase/src/security/permission-sets.ts) is that grant. The step now grants the stock set to a fresh member who holds no showcase_contributor. No clause changed", + "ref": "#PENDING-24579R2" } ] }, @@ -819,7 +831,7 @@ "title": "Scope depth (readScope/writeScope): org-wide read with own-only write, per persona — depth widens along geometry, never bypasses", "since": "v15", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -836,7 +848,7 @@ ] }, "steps": [ - "boot showcase isolated; create member M plus manager/executive/ops personas; as M: POST /api/v1/data/showcase_inquiry {\"subject\": \"M probe\", ...minimal valid payload}; as the manager: POST their OWN inquiry", + "boot showcase isolated; create member M plus manager/executive/ops personas; as M: POST /api/v1/data/showcase_inquiry {\"name\": \"M probe\", \"email\": \"m.probe@example.com\", \"message\": \"scope probe\"} (showcase_inquiry requires name, email and message and has no `subject` field — examples/app-showcase/src/data/objects/inquiry.object.ts); as the manager: POST their OWN inquiry", "as the manager: GET /api/v1/data/showcase_inquiry (list) — M's inquiry must appear (readScope org on a private object)", "as the manager: PATCH M's inquiry (foreign write, writeScope own) and PATCH their own inquiry", "as the executive: GET the inquiry list and M's inquiry by id; then attempt PATCH (no edit bit at all); also GET /api/v1/data/showcase_private_note list (readScope org over private notes)", @@ -892,7 +904,7 @@ ], "automated": { "kind": "dogfood", - "ref": "packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts + packages/qa/dogfood/test/showcase-scope-depth-write.dogfood.test.ts + packages/qa/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts" + "ref": "packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts + packages/qa/dogfood/test/showcase-scope-depth-write.dogfood.test.ts + packages/qa/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts — ⚠️ ADJACENT pins, not this item's: they drive the business-unit depth values (readScope unit / unit_and_below / own_and_reports, writeScope unit, the absent-writeScope default and the fallback profile), not the org-read / own-write asymmetry of the manager, executive and ops personas that acceptance[0]–[5] score, so no clause here is satisfied by running them — drive the clauses by hand" }, "traps": [ "wrong-persona" @@ -909,7 +921,13 @@ "change": "new — scope-depth read/write asymmetry matrix, per the deep-test contract", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 2, "date": "2026-08-18", "change": "spelled the two companion refs as full paths instead of bare names. Both resolve, but a bare name costs the next runner a search before the pin can be consulted (#9401)", "ref": "#9386" } + { "revision": 2, "date": "2026-08-18", "change": "spelled the two companion refs as full paths instead of bare names. Both resolve, but a bare name costs the next runner a search before the pin can be consulted (#9401)", "ref": "#9386" }, + { + "revision": 3, + "date": "2026-10-19", + "change": "steps[0] and automated.ref corrected — checklist-accuracy findings of the 18.0 QA sweep (QA run #24559, access-security.scope-depth-asymmetry; wave anchor #24579 §3; round 2's Tier 1 record #24598, which names this item among the refs that over-claim clause coverage). steps[0] POSTed a showcase_inquiry with `subject`; the object has no such field and requires name, email and message (examples/app-showcase/src/data/objects/inquiry.object.ts). The three dogfood pins exercise the business-unit depth axis (unit / unit_and_below / own_and_reports and the fallback profile), not the manager / executive / ops org-read and own-write asymmetry the clauses score, so automated.ref now says so and that no clause rests on them. No clause changed", + "ref": "#PENDING-24579R2" + } ] }, { @@ -1037,7 +1055,7 @@ "title": "Criteria sharing rules widen the OWD baseline: matching rows become visible to the rule's audience, non-matching stay hidden", "since": "v15.1", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -1068,7 +1086,7 @@ "as a member OUTSIDE the audience: same reads", "read sys_record_share for the matching record; run /security/explain for the audience member + matching record", "manual-share the non-matching inquiry to a specific user via the record-shares route (POST /api/v1/data/showcase_inquiry//shares); that user re-reads, and a third member re-reads too. The recipient needs object-level read on the object: a share on showcase_contact to a baseline member lands 201 but the member's read answers 403 at the object gate, because the member baseline grants no showcase_contact read — so a manual share is only observable on an object the recipient can already read at object level", - "§A13: as admin edit the SEEDED sharing-rule row (PATCH /api/v1/data/sys_sharing_rule/, e.g. its description), restart the server on the same database, re-read the row (seed-not-clobber). The materialized share is not editable: PATCH /api/v1/data/sys_record_share/ answers 405 OBJECT_API_METHOD_NOT_ALLOWED (allowed get, list, aggregate, search, export)" + "§A13: as admin switch the DECLARED rule off at the activation door — POST /api/v1/security/_activation/sharing_rule/share_new_inquiries_with_field_ops {\"enabled\": false}; re-read the audience member's list and by-id read of the matching inquiry, the rule-sourced sys_record_share rows and the sys_metadata_activation row; restart the server on the same database and re-read; switch it back on ({\"enabled\": true}), re-read, restart once more and re-read. There is no seeded sys_sharing_rule row to edit any more: declared rules are read from the registry (ADR-0131 D2, packages/plugins/plugin-sharing/src/declared-sharing-rules.ts) and GET /api/v1/data/sys_sharing_rule answers total 0 on stock, so the activation switch is the administrator's durable artifact. The materialized share is not editable: PATCH /api/v1/data/sys_record_share/ answers 405 OBJECT_API_METHOD_NOT_ALLOWED (allowed get, list, aggregate, search, export)" ], "acceptance": [ { @@ -1096,10 +1114,10 @@ "evidence": "the two reads" }, { - "clause": "§A13 — an admin edit to a SEEDED sharing-rule row (sys_sharing_rule) survives a restart: the declared-rules bootstrap does not clobber it (#2909). sys_record_share refuses edits (405), so the rule row is the editable seeded artifact", + "clause": "§A13 — an administrator's switch on a DECLARED sharing rule survives a restart and is never undone by the boot: switched off at the activation door, share_new_inquiries_with_field_ops withdraws its grant (the audience member is denied, its rule-sourced sys_record_share row is gone, sys_metadata_activation reads active false); a cold restart on the same database leaves it off — the boot neither re-arms it nor re-materializes the grant; switched back on it restores exactly one rule-sourced share, which a second restart keeps. The declared rule is code with no stored row (ADR-0131 D2), so the ledger row IS the administrator's edit; sys_record_share refuses edits (405)", "oracle": "api", - "verify": "post-restart re-read of the sys_sharing_rule row shows the admin's edit intact", - "evidence": "before/after-restart reads" + "verify": "per stage — baseline, off, restart, on, restart — the audience member's list + by-id read of the matching inquiry, the outsider's read, the sys_record_share rows with source 'rule' for the rule (none while off, exactly one while on, the same id after the second restart), and the sys_metadata_activation row (active false/true, updated_at unchanged across each restart). ⛔ GET /api/v1/meta/sharing_rule/NAME keeps answering active: true while the ledger holds the rule off — that read is the DECLARATION, not the effective state, so it is never this clause's oracle", + "evidence": "the five-stage read table (member, outsider, share rows, ledger row)" } ], "negative": [ @@ -1127,6 +1145,12 @@ "date": "2026-10-16", "change": "re-pointed on main c91af8ed6 (#23878 checklist-accuracy findings). (1) The position-audience variant is confounded on stock: the manager position binds showcase_manager, which reads showcase_contact at readScope 'org', so a manager reads all 32 contacts and explain answers decidedBy 'depth' on matching and non-matching rows alike. A knownGap records it, the variant is marked recorded-not-scored, and the widening clauses are scored on the unit_and_subordinates rule (measured: a member added to bu_west_coast reads exactly the one status='new' inquiry, rule-sourced share row). Steps 1-2 name the provisioning and the seeded probes instead of the retired red-project example. (2) Step 7 / clause 5 (§A13) said 'edit a materialized share', but sys_record_share answers 405 to PATCH; the editable seeded artifact is the sys_sharing_rule row. (3) Step 6 records that a manual-share recipient needs object-level read (a contact share to a baseline member reads 403)", "ref": "#24036" + }, + { + "revision": 3, + "date": "2026-10-19", + "change": "steps[6] and acceptance[4] (clause, verify, evidence) re-pointed — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.sharing-rules-widen acceptance[4]; wave anchor #24579 §3) with its oracle from round 2 (QA run #24601). §A13 asked that an admin edit to the SEEDED sys_sharing_rule row survive a restart; the seeder that copied declared rules into that table is retired (ADR-0131 D2, packages/plugins/plugin-sharing/src/declared-sharing-rules.ts) and GET /api/v1/data/sys_sharing_rule answers total 0, so there is no seeded row to edit. The administrator's durable act on a declared rule is now the activation switch (POST /api/v1/security/_activation/sharing_rule/NAME), and the clause asserts that switch across two restarts. QA run #24601 drove exactly this recipe at ba4afcc2 and passed. It measured that GET /api/v1/meta/sharing_rule/NAME keeps reading active: true while the ledger holds the rule off, so the oracle is the ledger row and the member's reads, never the metadata read. acceptance[0]–[3] are unchanged", + "ref": "#PENDING-24579R2" } ] }, @@ -1135,7 +1159,7 @@ "title": "Per-record manual shares grant, scope, and revoke access on a private-OWD record; rule evaluate reconciles the audience", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -1204,10 +1228,10 @@ "evidence": "the mis-scoped DELETE trace + survival read" }, { - "clause": "rule evaluate reconciles the audience: POST /sharing/rules/share_key_account_qualified_contacts_with_managers/evaluate returns {ruleId, matchedRecords>=1, grantsCreated/grantsUpdated} and a sys_record_share row exists for the matched contact with source 'rule' and source_id = the rule's ROW ID (sys_sharing_rule.id, e.g. srule_…), NOT the rule name", + "clause": "rule evaluate reconciles the audience: POST /sharing/rules/share_key_account_qualified_contacts_with_managers/evaluate returns {ruleId, matchedRecords>=1, grantsCreated/grantsUpdated} and a sys_record_share row exists for the matched contact with source 'rule' and source_id = the rule's id. For this DECLARED rule that id IS its name — a declared rule is projected from the registry, not stored, and its id is its name (ADR-0131 D2) — while an organization-authored rule's id is its sys_sharing_rule row id (srule_…)", "oracle": "api", - "verify": "the SharingRuleEvaluationResult body (packages/plugins/plugin-sharing/src/sharing-rule-service.ts evaluateRule) + the materialized rule-sourced share row: assert share.source_id === the sys_sharing_rule row's id, not 'share_key_account_qualified_contacts_with_managers'. The row id is the stable FK the reconcile path relies on — purgeRuleGrants(ruleId) deletes on where {source:'rule', source_id: ruleId} with ruleId = rule.id (sharing-rule-service.ts), and sys-sharing-rule.object.ts documents 'source_id={rule.id}' — so a run asserting the NAME here would be asserting a value the implementation never writes", - "evidence": "evaluate response + the sys_record_share read (both the row's source_id and the sys_sharing_rule id it must equal)" + "verify": "the SharingRuleEvaluationResult body (packages/plugins/plugin-sharing/src/sharing-rule-service.ts evaluateRule) + the materialized rule-sourced share row: assert share.source_id === the body's ruleId, which for this rule is 'share_key_account_qualified_contacts_with_managers' (packages/plugins/plugin-sharing/src/sharing-rule-service.ts#projectDeclaredRule projects a declared rule with id = name, and that id is what its grants carry as sys_record_share.source_id). The rule id is the stable key the reconcile path relies on — revokeRuleGrants(ruleId) purges where {source:'rule', source_id: ruleId}. ⛔ Do not look for a sys_sharing_rule row for this rule: that table holds only organization-authored rules and answers total 0 on stock", + "evidence": "evaluate response + the sys_record_share read (the row's source_id beside the body's ruleId)" } ], "negative": [ @@ -1255,6 +1279,12 @@ "date": "2026-10-16", "change": "source[1] re-pointed (carried reading of the #23878 wave, confirmed on main c91af8ed6): it said the record-shares family is 'registered inline, under no registrar of its own'. packages/rest/src/sharing-routes.ts exports registerSharingEndpoints, which registers GET and POST /data/:object/:id/shares and DELETE .../:shareId; the evaluate leg stays on registerSharingRuleEndpoints. Anchor and wording only: no step, fixture or oracle changed, so no re-run is owed", "ref": "#24036" + }, + { + "revision": 6, + "date": "2026-10-19", + "change": "acceptance[6] (clause, verify, evidence) corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.record-share-grant-revoke acceptance[6]; wave anchor #24579 §3). The clause required source_id to be the rule's sys_sharing_rule ROW id and NOT its name; that holds only for an organization-authored rule. The rule it evaluates, share_key_account_qualified_contacts_with_managers, is DECLARED, and a declared rule is projected with id = name (sharing-rule-service.ts projectDeclaredRule, ADR-0131 D2), so its grants carry the name. The clause now asserts source_id equals the rule's id as evaluate reports it, and the verify names revokeRuleGrants, the current name of the purge it cited as purgeRuleGrants", + "ref": "#PENDING-24579R2" } ] }, @@ -1263,7 +1293,7 @@ "title": "The audit-log browser surfaces attributable events over sys_audit_log with correct actor/object, filters, and a before/after payload drawer", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "mixed", "personas": [ @@ -1286,7 +1316,7 @@ "narrow to writes and confirm the narrowing is server-side; capture the re-issued /api/v1/data/sys_audit_log request with its $filter. /system/audit-log (AuditLogPage) has an Action select in its filter bar that writes action into the request's $filter. The page's Presets row offers sys_audit_log's list views (Writes / Auth / Config / …), and its 'Open as list' link opens the sys_audit_log object list (no longer a Setup menu entry), which offers the same views and the generic Filter Records → Where → Add filter builder. Any of these is a valid server-side narrowing", "click an UPDATE row (e.g. PATCH a showcase_contact first) → the side drawer opens; screenshot the Before (old_value) / After (new_value) JSON panels — the update event is the shape that carries both", "cross-check the API twin directly: GET /api/v1/data/sys_audit_log?$filter=... for each of the three actions and compare actor/object/action to the page", - "attempt to forge the trail: POST and PATCH /api/v1/data/sys_audit_log — both must be refused (get+list only)" + "attempt to forge the trail: POST and PATCH /api/v1/data/sys_audit_log — both must be refused 405 — the declared apiMethods are get + list, and the refusal's allowed set reads [get, list, aggregate, search, export] because aggregate, search and export derive from list (packages/spec/src/data/api-derivation.ts#API_METHOD_DERIVATION) — none of them a write" ], "acceptance": [ { @@ -1322,7 +1352,7 @@ { "clause": "the trail is append-only via the data API: POST and PATCH /api/v1/data/sys_audit_log are refused (enable.apiMethods = get + list) — an actor cannot forge or edit their own audit record", "oracle": "api", - "verify": "POST and PATCH both answer >=400 (method not permitted for the object); a follow-up list shows no forged row", + "verify": "POST and PATCH both answer >=400 (method not permitted for the object; a 405 lists the derived read verbs aggregate, search and export beside get and list, and an allowed set naming create or update is a FAIL); a follow-up list shows no forged row", "evidence": "the two write traces + the list" } ], @@ -1351,7 +1381,13 @@ { "revision": 3, "date": "2026-10-02", "change": "re-pointed clause 1 (and the clauses that counted its three rows) at 17.6.0's ledger read rule. 30c530e5 (#21194) serves a non-system reader, administrators included, only audit rows about records it can read, so the delete row is written but never served on the data API; the 17.6.0 release-verification run measured the admin's delete-filtered read at 0 rows while the stored row carried correct attribution. The clause now asserts the login and config_change rows served, the delete row stored and NOT served; step 6's drawer check moves to an update row, since no delete row reaches the page", "ref": "#21330" }, { "revision": 4, "date": "2026-10-05", "change": "clauses 1, 3 and 5 and steps 5-6 re-pointed (stale after 17.6.0, #21784 VF4 + FE7). 7ebb543164 (#21296, fixes #21260, 2026-10-02) gives platform admins view_all_audit_log by default (ADMIN_FULL_ACCESS_CAPABILITIES). That exempts them from the ledger's parent-record read gate, so the admin's read serves the delete row. Revision 3 was measured on 17.6.0 and encodes only #21194. Clauses 1 and 5 use the verifier's wording: the delete row is served to a view_all_audit_log holder and withheld from a non-holder who cannot read the deleted record. FE7: revision 2's 'there is no Action dropdown' is false at the console pin, because AuditLogPage has an Action select that narrows through $filter. Clause 3 and step 5 now name it beside the object list's prebuilt views. Re-read at objectui 9dfaca6543: unchanged", "ref": "#21797" }, { "revision": 5, "date": "2026-10-16", "change": "clause 1's non-holder arm re-pointed to the measured answer (#23878 checklist-accuracy finding, re-measured on main c91af8ed6): a fresh member is refused sys_audit_log at the object level (GET /api/v1/data/sys_audit_log and the action='delete' filter both answer 403 PERMISSION_DENIED), so the 'empty filtered list' the clause expected never appears. No stock non-admin permission set grants read on sys_audit_log, so the parent-record gate (#21194) has no live non-admin reader; the clause now names the plugin-audit visibility pins as its oracle for that gate. The clause still fails if the fresh member is served any row", "ref": "#24036" }, - { "revision": 6, "date": "2026-10-17", "change": "step 5 and the nav source re-pointed at Setup's one audit entry (#23890): nav_audit_logs now opens this page (audit:log) and is labelled Audit Log, the sys_audit_log object list has no Setup menu entry and is reached from the page's Open as list link, and the page offers the list views as presets (objectui#12667, at the console pin 5e15928b4631). No acceptance clause changes", "ref": "#23890" } + { "revision": 6, "date": "2026-10-17", "change": "step 5 and the nav source re-pointed at Setup's one audit entry (#23890): nav_audit_logs now opens this page (audit:log) and is labelled Audit Log, the sys_audit_log object list has no Setup menu entry and is reached from the page's Open as list link, and the page offers the list views as presets (objectui#12667, at the console pin 5e15928b4631). No acceptance clause changes", "ref": "#23890" }, + { + "revision": 7, + "date": "2026-10-19", + "change": "steps[7] and acceptance[5].verify clarified — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.audit-log-browser; wave anchor #24579 §3): the 405 body advertises allowed [get, list, aggregate, search, export], which a run reading 'get + list only' could take for a widened door. sys_audit_log still declares apiMethods ['get', 'list']; the other three are the read verbs list derives (packages/spec/src/data/api-derivation.ts). The append-only clause is unchanged", + "ref": "#PENDING-24579R2" + } ] }, { @@ -2091,7 +2127,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": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": [ @@ -2107,10 +2143,10 @@ ] }, "steps": [ - "boot showcase with the console; admin session; provision member M granted a writable set S that WITHHOLDS a verb on an object (e.g. delete:false on showcase_task)", - "baseline: as M, DELETE /api/v1/data/showcase_task/ — must be refused 403 (the withheld verb)", + "boot showcase with the console; admin session; provision member M granted a writable set S that WITHHOLDS a verb on an object (e.g. delete:false on showcase_task); as M create the showcase_task rows the DELETE probes below use — M's OWN rows, never seeded ids: a set silent on writeScope is owner-only (packages/plugins/plugin-security/src/permission-evaluator.ts getEffectiveScope), so a seeded task owned by someone else stays refused after the verb flips ON and confounds the verb with ownership", + "baseline: as M, DELETE /api/v1/data/showcase_task/ — must be refused 403 (the withheld verb)", "in the console Permission Matrix editor for S, flip delete ON for showcase_task; save (draft) then publish — capture PUT /api/v1/meta/permission/S and POST /api/v1/meta/permission/S/publish", - "as M: DELETE /api/v1/data/showcase_task/ — must now be 2xx (access flipped)", + "as M: DELETE /api/v1/data/showcase_task/ — must now be 2xx (access flipped)", "flip delete back OFF in the matrix; publish; as M: DELETE again — must be refused 403", "field-level half: flip an FLS bit (a showcase_project field editable:false→true) in the lower matrix; publish; as M PATCH that field — the write flips accordingly", "assign/unassign: grant M a SECOND permission set (insert sys_user_permission_set) — M gains its verbs; remove the row — the access retracts", @@ -2185,7 +2221,13 @@ "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-18", "change": "corrected clause 1, which demanded a 2xx from POST /api/v1/meta/permission/S/publish. That route is mounted type-generically, but it answers 404 [no_draft] when no pending draft exists — and the console's permission save is a plain PUT that lands state:'active' directly, so there is nothing to promote. As written the clause was unsatisfiable on the path the item's own fixture block describes (#9453 CF-5). The clause now keys on the published set carrying the verb and records which save path was taken", "ref": "#9386" }, - { "revision": 3, "date": "2026-09-23", "change": "re-keyed clause 2 and its verify on the refusal's code axis: the nothing-to-publish answer is now stated as HTTP 404 with code NO_DRAFT instead of quoting a bracketed lowercase opener inside the message. That opener was retired from every metadata-protocol refusal in 7a25a3ee9c (error is human language, code is the machine token), so a checker matching the old quoted spelling against a live answer recorded a false failure. Pass/fail semantics are unchanged; the producer is now cited in source", "ref": "#19705" } + { "revision": 3, "date": "2026-09-23", "change": "re-keyed clause 2 and its verify on the refusal's code axis: the nothing-to-publish answer is now stated as HTTP 404 with code NO_DRAFT instead of quoting a bracketed lowercase opener inside the message. That opener was retired from every metadata-protocol refusal in 7a25a3ee9c (error is human language, code is the machine token), so a checker matching the old quoted spelling against a live answer recorded a false failure. Pass/fail semantics are unchanged; the producer is now cited in source", "ref": "#19705" }, + { + "revision": 4, + "date": "2026-10-19", + "change": "steps[0], [1] and [3] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.permission-matrix-edit-loop step 4; wave anchor #24579 §3): the DELETE probes named seeded task ids, but a permission set silent on writeScope is owner-only (plugin-security permission-evaluator.ts), so a seeded row owned by someone else stays refused after the delete bit flips ON. The probes now use rows M creates. No clause changed", + "ref": "#PENDING-24579R2" + } ] }, { @@ -2193,7 +2235,7 @@ "title": "A package-declared capability is served by the registry (no sys_capability row — the object is retired, ADR-0131 D3) and resolves across the grant/require three-way (ADR-0066) on the PLATFORM action path; platform names cannot be shadowed", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": [ @@ -2227,7 +2269,7 @@ "REGRANT leg: showcase_ops is sealed (PUT /api/v1/meta/permission/showcase_ops → 403 NOT_OVERRIDABLE), so clone it (fixtures.requires[1]) and grant the CLONE to a fresh member through a sys_user_permission_set row; invoke showcase_zoo_perm_held as that member → executes; withdraw showcase.export_data from the CLONE's systemPermissions (PUT /api/v1/meta/permission/ with the read-back item minus that entry), re-invoke → refused; restore it, re-invoke → executes again", "invoke showcase_zoo_perm_missing (requires showcase.restricted_ops, granted to nobody) as EVERY persona incl. the OpsPermissionSet holder → refused for all; and showcase_zoo_perm_empty (requiredPermissions:[]) → executes for all, since 'declares nothing' is not 'denies everything'", "author a scratch package declaring a capability whose name is in PLATFORM_CAPABILITY_NAMES and BOOT it: os validate and os build both pass the declaration (the name is lexically valid), and the refusal is the registry's one-holder check at boot, which stops the process. The runtime door is closed to the same attempt: PUT /api/v1/meta/capability/ answers 403 NOT_OVERRIDABLE (capability is a code-only type)", - "the capability write doors validate before anything reaches the authorization namespace (the #5961 concern): author a scratch capability with a malformed shape (e.g. scope 'galaxy') in source and run os validate — refused at parse; the runtime metadata door refuses the type outright, PUT /api/v1/meta/capability/ → 403 NOT_CREATABLE (capability is allowRuntimeCreate:false and allowEnvironmentOverlay:false, packages/spec/src/kernel/metadata-plugin.zod.ts); and there is no sys_capability table to upsert into (step 1's 404)" + "the capability write doors validate before anything reaches the authorization namespace (the #5961 concern): author a scratch capability with a malformed shape (e.g. scope 'galaxy') in source as a RAW `capabilities: [{…}]` entry and run os validate — refused at parse with the located `capabilities.0.scope` error. ⚠️ Not through defineCapability: it parses its argument itself (packages/spec/src/security/capabilities.ts#defineCapability), so the malformed shape throws while the config loads and os validate never reaches its located message; the runtime metadata door refuses the type outright, PUT /api/v1/meta/capability/ → 403 NOT_CREATABLE (capability is allowRuntimeCreate:false and allowEnvironmentOverlay:false, packages/spec/src/kernel/metadata-plugin.zod.ts); and there is no sys_capability table to upsert into (step 1's 404)" ], "acceptance": [ { @@ -2239,7 +2281,7 @@ { "clause": "the three-way resolves by NAME on the surface that actually enforces it — the PLATFORM action path (ADR-0066 D4 actionPermissionError, reached via the dispatcher /actions domain — POST /api/v1/actions/:object/:action/:recordId on the stock dev server — or, on a host without it, the MCP run_action bridge or ql.executeAction): invoking an action that REQUIREs the capability is refused for a member whose granted sets do not carry it and executes for one that does, the refusal follows a grant WITHDRAWN from the (cloned) permission set, and execution returns when it is re-granted — grant / deny / regrant, all three legs", "oracle": "api", - "verify": "on the dispatcher /actions domain (or the recorded alternative): showcase_zoo_perm_held answers 200 for the OpsPermissionSet holder and 403 PERMISSION_DENIED for a non-granted member, the message being the actionPermissionError sentence naming the action and the missing capability; a member holding the CLONE of showcase_ops (showcase_ops itself is sealed, 403 NOT_OVERRIDABLE) gets 200, then 403 after showcase.export_data is withdrawn from the clone, then 200 again after it is restored. Cross-checks on the same surface: showcase_zoo_perm_missing (showcase.restricted_ops, granted to nobody) is refused for every persona, and showcase_zoo_perm_empty (requiredPermissions:[]) executes for every persona. This clause is NOT scored against /api/v1/data/:object/:id/actions/... — @objectstack/rest mounts no action-invoke route by design (#7680)", + "verify": "on the dispatcher /actions domain (or the recorded alternative): showcase_zoo_perm_held answers 200 for the OpsPermissionSet holder and 403 PERMISSION_DENIED for a non-granted member, the message being the actionPermissionError sentence naming the action and the missing capability; a member holding the CLONE of showcase_ops (showcase_ops itself is sealed, 403 NOT_OVERRIDABLE) gets 200, then 403 after showcase.export_data is withdrawn from the clone, then 200 again after it is restored. Cross-checks on the same surface: showcase_zoo_perm_missing (showcase.restricted_ops, granted to nobody) is refused for every persona, and showcase_zoo_perm_empty (requiredPermissions:[]) executes for every persona. The seeded admin is refused showcase_zoo_perm_held too (403 PERMISSION_DENIED, measured by QA run #24559) unless one of the admin's sets carries showcase.export_data: the gate reads the caller's held capabilities and only a system context skips it (packages/runtime/src/action-execution.ts#actionPermissionError) — an administrator is not exempt. This clause is NOT scored against /api/v1/data/:object/:id/actions/... — @objectstack/rest mounts no action-invoke route by design (#7680)", "evidence": "the invocation results for all three legs (grant → deny → regrant) plus the two cross-checks, each with the refusal text or the executed result, and a note of which platform surface was driven" }, { @@ -2291,7 +2333,13 @@ "ref": "claude/issue-7680-package-capability-doctrine" }, { "revision": 3, "date": "2026-08-18", "change": "named the specimens' object correctly: showcase_field_zoo, not `zoo`. The fixture block quoted the local TS const from predicate-matrix.action.ts (const zoo = 'showcase_field_zoo'), so an invoke keyed on `zoo` answers 'Action ... not found on object zoo' — which reads like a missing specimen rather than a wrong object name (#9453 CF-6)", "ref": "#9386" }, - { "revision": 4, "date": "2026-10-16", "change": "re-pointed against main c91af8ed6 (#23878 checklist-accuracy findings, #23879 out_of_scope_findings). (1) sys_capability is retired (GET /api/v1/data/sys_capability → 404 OBJECT_NOT_FOUND), so the title, step 1, clause 1 and the old steps 7-8 no longer describe a sys_capability row or upsert; clause 4 now states the doors that exist: source parsed by os validate, the runtime metadata door refusing the code-only type (403 NOT_CREATABLE), and no table. (2) The dispatcher /actions domain IS mounted on the stock dev server and enforces actionPermissionError (measured: held 200 for the ops holder, 403 PERMISSION_DENIED naming the missing capability for a plain member; missing and and refused, empty executes), so step 3 and the knownGap name it as the surface; /api/v1/data/:object/:id/actions/... stays the routing 404. (3) showcase_ops is sealed (PUT → 403 NOT_OVERRIDABLE, ADR-0126), so the REGRANT leg withdraws and restores the capability on a clone made through POST /api/v1/security/permission-sets/showcase_ops/clone (label mandatory); measured 200 → 403 → 200. The clause still fails if a non-holder executes, a withdrawn grant keeps executing or a holder is refused. The build-oracle clauses re-run on a minimal scratch stack: a capability with scope 'galaxy' → os validate exit 1 \"capabilities.0.scope: Invalid value 'galaxy'. Expected one of: platform, org.\"; a capability named manage_platform_settings → os validate and os build exit 0, and os dev refused to boot with '✗ Security catalog name conflict: package \"com.qa.caps\" cannot register a name another holder already holds — capability \"manage_platform_settings\" is already held by the platform …' (step 6 now says the refusal is at boot); the runtime door answered both a platform name and the malformed body 403 (sealed / code-only type)", "ref": "#24036" } + { "revision": 4, "date": "2026-10-16", "change": "re-pointed against main c91af8ed6 (#23878 checklist-accuracy findings, #23879 out_of_scope_findings). (1) sys_capability is retired (GET /api/v1/data/sys_capability → 404 OBJECT_NOT_FOUND), so the title, step 1, clause 1 and the old steps 7-8 no longer describe a sys_capability row or upsert; clause 4 now states the doors that exist: source parsed by os validate, the runtime metadata door refusing the code-only type (403 NOT_CREATABLE), and no table. (2) The dispatcher /actions domain IS mounted on the stock dev server and enforces actionPermissionError (measured: held 200 for the ops holder, 403 PERMISSION_DENIED naming the missing capability for a plain member; missing and and refused, empty executes), so step 3 and the knownGap name it as the surface; /api/v1/data/:object/:id/actions/... stays the routing 404. (3) showcase_ops is sealed (PUT → 403 NOT_OVERRIDABLE, ADR-0126), so the REGRANT leg withdraws and restores the capability on a clone made through POST /api/v1/security/permission-sets/showcase_ops/clone (label mandatory); measured 200 → 403 → 200. The clause still fails if a non-holder executes, a withdrawn grant keeps executing or a holder is refused. The build-oracle clauses re-run on a minimal scratch stack: a capability with scope 'galaxy' → os validate exit 1 \"capabilities.0.scope: Invalid value 'galaxy'. Expected one of: platform, org.\"; a capability named manage_platform_settings → os validate and os build exit 0, and os dev refused to boot with '✗ Security catalog name conflict: package \"com.qa.caps\" cannot register a name another holder already holds — capability \"manage_platform_settings\" is already held by the platform …' (step 6 now says the refusal is at boot); the runtime door answered both a platform name and the malformed body 403 (sealed / code-only type)", "ref": "#24036" }, + { + "revision": 5, + "date": "2026-10-19", + "change": "steps[7] and acceptance[1].verify corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24559, access-security.capability-declaration-lifecycle; wave anchor #24579 §3). The located `capabilities.0.scope` refusal appears only for a raw capabilities entry: defineCapability parses its argument (packages/spec/src/security/capabilities.ts), so a malformed shape written through it throws at config load. The item stated no expectation for the admin on showcase_zoo_perm_held. The run measured the admin refused, and actionPermissionError has no administrator exemption (only a system context skips it), so the verify now says so. The run also measured DELETE /api/v1/meta/capability/NAME answering 200 while removing nothing; no clause asserts that door, and it is left to the wave anchor as an observation, not encoded here", + "ref": "#PENDING-24579R2" + } ] }, { @@ -3207,7 +3255,7 @@ "title": "The anonymous floor answers 401 FIRST on the doors v18 added to it — /notifications (was 501), non-POST /keys (was 405), share-link management (was 501), /openapi.json and /docs, every /i18n face and the settings family — while the token faces and the public-form submit stay anonymous and the submit echoes only {id}", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -3222,7 +3270,7 @@ ], "knownGaps": [ "the dispatcher `/ui` domain (also floored by 23225) is NOT mounted over HonoServer on a stock `os dev` / `os serve` — GET /api/v1/ui/... answers 404 ENDPOINT_NOT_FOUND to the admin too — so it is not driven here; it is reachable only on a dispatcher-only (hosted) kernel and is pinned by the dogfood sweep", - "the dispatcher mounts only POST /keys on HonoServer (packages/runtime/src/dispatcher-plugin.ts), so a non-POST /keys never reaches the domain's floor on a stock boot: the HonoServer fallback answers it 405 METHOD_NOT_ALLOWED (packages/plugins/plugin-hono-server/src/adapter.ts unmatchedResponse) — run 2026-10-16 measured exactly that; the clause stays as the 23225 contract states it, so a 405 there is a FAIL, not a re-point" + "CLOSED (#23909): the dispatcher now registers `/keys` for EVERY verb on HonoServer, POST alone served (packages/runtime/src/dispatcher-plugin.ts), so a non-POST /keys reaches the domain's floor and an anonymous caller is answered 401 UNAUTHENTICATED — the 18.0 sweep measured anonymous GET /api/v1/keys → 401 (QA run #24559). Before #23909 only POST /keys was registered and the HonoServer fallback answered other verbs 405 METHOD_NOT_ALLOWED (run 2026-10-16). A 405 to an anonymous non-POST /keys is now a regression of #23909 and a FAIL" ] }, "steps": [ @@ -3280,6 +3328,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it; extends access-security.anonymous-deny-surfaces (v15), which enumerates data/meta/actions/automation/batch/security/analytics only", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "fixtures.knownGaps[1] marked CLOSED — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.anonymous-floor-v18-doors; wave anchor #24579 §3): the gap said a non-POST /keys never reaches the floor on a stock boot and answers 405; since #23909 the dispatcher registers /keys for every verb (packages/runtime/src/dispatcher-plugin.ts) and the run measured anonymous GET /api/v1/keys → 401. No clause changed: acceptance[1] already demanded 401", + "ref": "#PENDING-24579R2" } ] }, @@ -3360,7 +3414,7 @@ "title": "/meta read gates added on v18: the authoring-trail doors (history / audit / diff / package commits) refuse a setup.access-only caller, and a list view or dashboard carrying requiredPermissions is withheld from a caller without them on EVERY alternate door", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -3377,7 +3431,7 @@ }, "steps": [ "as the setup.access-only member: GET /api/v1/meta/object/showcase_task/history, /audit, /diff; GET /api/v1/packages/com.example.showcase/commits", - "as a member without the capability: GET /api/v1/meta/view (list) and GET /api/v1/meta/view/showcase_task.qa_gated_view, plus /published, /layers, ?state=draft, /history", + "as a member without the capability: GET /api/v1/meta/view (list) and GET /api/v1/meta/view/showcase_task.qa_gated_view, plus /published, /layers, ?state=draft, and /history as a member who also holds studio.access (the authoring-trail gate refuses a member without it first — acceptance[2])", "as admin: the same reads (control); then delete the scratch view" ], "acceptance": [ @@ -3394,9 +3448,9 @@ "evidence": "four status lines" }, { - "clause": "the gated view is absent from the member's list and every by-name door answers 403 PERMISSION_DENIED; the admin reads it on all of them", + "clause": "the gated view is absent from the member's list and every by-name read door (by name, /published, /layers, ?state=draft) answers 403 PERMISSION_DENIED; on /history the audience refusal (403 PERMISSION_DENIED) is reached only by a caller the authoring-trail gate admits (studio.access or manage_metadata); a member holding neither is refused there FIRST with 403 FORBIDDEN — acceptance[1]'s gate (packages/rest/src/rest-server.ts#mayReadAuthoringTrail). The admin reads it on all of them", "oracle": "api", - "verify": "list membership + status per door, both personas", + "verify": "list membership + status per door, both personas. Score /history's PERMISSION_DENIED with a member who holds studio.access (or manage_metadata) but not the view's requiredPermissions; a 403 FORBIDDEN there from a member holding neither is the authoring gate answering first, not this clause's verdict", "evidence": "the list ids + per-door lines" } ], @@ -3422,6 +3476,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "acceptance[2] (clause, verify) and steps[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.meta-authoring-trail-and-audience-gates; wave anchor #24579 §3). The clause said every by-name door of the gated view answers 403 PERMISSION_DENIED. /history is an authoring-trail door: it asks mayReadAuthoringTrail (studio.access or manage_metadata, packages/rest/src/rest-server.ts) before anything, so a member without either is refused 403 FORBIDDEN first — acceptance[1]'s gate — and PERMISSION_DENIED is reached only by a member the trail admits. The run saw exactly that split. The other doors and clauses are unchanged", + "ref": "#PENDING-24579R2" } ] }, @@ -3590,7 +3650,7 @@ "title": "Grant-table write contract (sys_user_permission_set / sys_user_position): the retired permission_set_id column is refused, a duplicate grant is 409, an inverted validity window is 400, granted_by is stamped with the caller, and (walled) a grant to a non-member is refused", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -3625,9 +3685,9 @@ "evidence": "two refusal bodies" }, { - "clause": "an INSERT with valid_until <= valid_from is refused as the rule's VIOLATION: 400 VALIDATION_FAILED at valid_until with constraint.rule validity_window_order, constraint.reason NOT `unevaluable`, and the rule's authored message (\"Valid Until must be later than Valid From …\") — an unevaluable-predicate refusal is a FAIL even though it also answers 400", + "clause": "an INSERT with valid_until <= valid_from is refused as the rule's VIOLATION: 400 VALIDATION_FAILED with fields[0] {field: valid_until, code: rule_violation} carrying the rule's authored message (\"Valid Until must be later than Valid From …\") and no `constraint.reason: 'unevaluable'` — a plain violation carries field, code and message only, so `constraint.rule` is not observable on this path; only an unevaluable predicate attaches a `constraint` (reason 'unevaluable'), and an unevaluable-predicate refusal is a FAIL even though it also answers 400", "oracle": "api", - "verify": "fields[0].constraint + message on the inverted-window insert; a valid window inserts 201 (control)", + "verify": "fields[0].field + fields[0].code + the message's opening on the inverted-window insert, and fields[0].constraint absent or without reason 'unevaluable' (packages/objectql/src/validation/rule-validator.ts: a true predicate returns {field, code: 'rule_violation', message}; only the unevaluable branches attach constraint {rule, reason: 'unevaluable', fault}); a valid window inserts 201 (control)", "evidence": "the refusal body + the control" }, { @@ -3658,6 +3718,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "acceptance[2] (clause, verify) corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.grant-table-write-contract acceptance[2]; wave anchor #24579 §3): the clause required constraint.rule validity_window_order on the refusal, which is unobservable — a plain rule violation carries field, code and message and no constraint object (packages/objectql/src/validation/rule-validator.ts); only an unevaluable predicate attaches one. The clause now asserts what the rule's verdict carries and what the item's own pin sys-user-permission-set-window-order.test.ts asserts (valid_until · rule_violation, the authored message, no unevaluable reason). The walled leg acceptance[4] is §2's and is unchanged", + "ref": "#PENDING-24579R2" } ] }, @@ -3743,7 +3809,7 @@ "title": "Seven deployment tables (sys_job, sys_job_run, sys_job_queue, sys_flow_dispatch, sys_migration, sys_migration_journal, sys_presence) carry no organization column on v18 and are readable only with manage_platform_settings — an org admin who is not platform admin is refused even on single", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -3760,7 +3826,7 @@ "steps": [ "as the org admin GET /api/v1/data/sys_job and /api/v1/data/sys_migration", "as the platform admin repeat", - "as the platform admin GET /api/v1/data/sys_job?filter=[[\"organization_id\",\"=\",\"x\"]]" + "as the platform admin GET /api/v1/data/sys_job?filter=[[\"organization_id\",\"=\",\"x\"]] — URL-encode the filter value (e.g. curl -G --data-urlencode 'filter=[[\"organization_id\",\"=\",\"x\"]]'); sent raw, the value is mangled before it reaches the door (curl reads [ ] as a glob range)" ], "acceptance": [ { @@ -3787,7 +3853,7 @@ ], "automated": { "kind": "unit", - "ref": "packages/platform-objects/src/deployment-level-no-tenant-column.test.ts + packages/plugins/plugin-security/src/authz-identity-objects-boot-refusal.test.ts" + "ref": "packages/platform-objects/src/deployment-level-no-tenant-column.test.ts (sys_job, sys_job_queue, sys_job_run, sys_migration, sys_migration_journal) + packages/services/service-automation/src/sys-flow-dispatch.object.test.ts (sys_flow_dispatch) + packages/services/service-realtime/src/objects/sys-presence.object.test.ts (sys_presence) + packages/plugins/plugin-security/src/authz-identity-objects-boot-refusal.test.ts" }, "source": [ "packages/platform-objects/src/audit/sys-job.object.ts", @@ -3800,6 +3866,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "automated.ref and steps[2] corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24559, access-security.deployment-tables-platform-only; wave anchor #24579 §3). acceptance[0] says the SEVEN tables are pinned, but the named pin, deployment-level-no-tenant-column.test.ts, lists five — the platform-objects ones. sys_flow_dispatch and sys_presence live in service-automation and service-realtime, and each has its own object pin asserting no tenant column and requiredPermissions manage_platform_settings (sys-flow-dispatch.object.test.ts, sys-presence.object.test.ts). automated.ref now names both, so the seven are pinned, and says which tables each file holds. steps[2]'s filter must be URL-encoded. No clause changed", + "ref": "#PENDING-24579R2" } ] }, @@ -3808,7 +3880,7 @@ "title": "Share links with two gates combined: signed_in + password answers an anonymous caller 401 SIGN_IN_REQUIRED whatever password is sent, email + password answers a uniform 404 INVALID_OR_EXPIRED, and /messages opens exactly where /resolve opens", "since": "v18", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "api", "personas": [ @@ -3858,7 +3930,7 @@ ], "automated": { "kind": "conformance", - "ref": "packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts + packages/plugins/plugin-sharing/src/share-link-envelope.conformance.test.ts" + "ref": "packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts + packages/plugins/plugin-sharing/src/share-link-envelope.conformance.test.ts + packages/runtime/src/domains/share-links-messages-audience.test.ts (acceptance[3]: `/messages` opens for a caller exactly when `/resolve` opens the same link, on both mounts)" }, "source": [ "packages/plugins/plugin-sharing/src/share-link-routes.ts#registerShareLinkRoutes", @@ -3877,6 +3949,12 @@ "date": "2026-10-19", "change": "acceptance[3] re-pointed to open/refused parity (QA run #24559 acceptance[3] fail, disposition assertion defect; parked on #24579 §1; QA run #24558 scored the same clause on that corrected intent). #24559 records the divergence as \"the pinned design (share-link-envelope.conformance.test.ts: messages for an unresolvable token → 404 NOT_FOUND; for a non-ai_conversations link → 400 UNSUPPORTED)\" and re-points the clause to \"/messages opens iff /resolve opens\". packages/plugins/plugin-sharing/src/share-link-routes.ts#registerShareLinkRoutes hands /messages the same audience inputs and answers every refusal 404 NOT_FOUND, and packages/runtime/src/domains/share-links-messages-audience.test.ts holds both mounts to one open/refused verdict per audience. The clause asked for identical status+code pairs, which the design contradicts on every refused call. The title follows; re-derived from main 5a871e4e41, where every source cited here is byte-identical to the run subject 5b14b7f3", "ref": "#24622" + }, + { + "revision": 3, + "date": "2026-10-19", + "change": "automated.ref gains packages/runtime/src/domains/share-links-messages-audience.test.ts — the out-of-scope §3 note of #24622's round (wave anchor #24579 §3; QA run #24559). That file pins the parity acceptance[3] has asserted since revision 2: /messages hands resolveToken every audience input /resolve hands it, on the plugin mount and the dispatcher twin, so /messages serves exactly the callers /resolve serves. No clause changed", + "ref": "#PENDING-24579R2" } ] } From b7f6391716129ba507562279bd5b7c78b0fabdc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:01:48 +0000 Subject: [PATCH 3/7] docs(qa): correct two attachments-storage checklist items and a recipe's provenance from the 18.0 sweep Checklist-accuracy findings of QA run #24560 (attachments-storage, 5b14b7f3), re-derived from source at 8fbbd5082f: orphan-tombstone-reap names the unit pin that covers two reap-guard variants; chunked-upload-integrity records that no pin drives a chunked door after an organization switch. The qa-media-constraints recipe's provenance records that the run replayed it live (recipes carry no revision or history of their own). Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/attachments-storage.json | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/qa/platform-checklist/areas/attachments-storage.json b/docs/qa/platform-checklist/areas/attachments-storage.json index d5c3fea69e..1ea43ce69b 100644 --- a/docs/qa/platform-checklist/areas/attachments-storage.json +++ b/docs/qa/platform-checklist/areas/attachments-storage.json @@ -135,7 +135,7 @@ "qa-media-constraints": { "title": "Scratch object qa_media carrying FIELD-level accept/maxSize (plus an extension-only accept field)", "why": "The showcase declares accept/maxSize ONLY on action params (examples/app-showcase/src/ui/actions/index.ts,337 — the dialog-widget/ADR-0059 upload-guard lane), never on an object FIELD, so the server-side record-write re-enforcement (field-accept-maxsize-server-enforced) has no stock target: without this recipe every clause of that item is blocked(fixture) on every run. Landing one constrained field in the showcase seeds proper would retire this recipe.", - "provenance": "authored from source in the 2026-08-20 scoped scan-functionality sweep (claude/new-session-0pv25p); call shapes copied from qa-scratch-authz (#7670), constraint keys from the FieldSchema declaration — not yet proven by a live run", + "provenance": "authored from source in the 2026-08-20 scoped scan-functionality sweep (claude/new-session-0pv25p); call shapes copied from qa-scratch-authz (#7670), constraint keys from the FieldSchema declaration. Proven live by QA run #24560 (framework 5b14b7f3, 2026-10-19): the sequence replayed cleanly and attachments-storage.field-accept-maxsize-server-enforced was scored on it", "app": "showcase", "requires": [ "an admin session that holds the `manage_metadata` capability — same gate as qa-scratch-authz (PUT /api/v1/meta/:type/:name is capability-gated per ADR-0066 D1, packages/rest/src/rest-route-ledger.ts)", @@ -642,7 +642,7 @@ "title": "The platform lifecycle sweep reaps expired tombstones and abandoned pending uploads WITH byte reclaim; sweep-time re-verification vetoes rather than losing data", "since": "v15.1", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "api", "personas": ["admin (setup access, drives the sweep)"], @@ -663,7 +663,7 @@ { "clause": "each reap-guard verdict variant behaves per its declared contract: pending → bytes best-effort deleted + row confirmed; deleted with zero references and no field owner → bytes reclaimed BEFORE the row delete; deleted-but-regained-reference → un-tombstoned and vetoed; non-attachments-scope tombstone with the ADR-0104 migration gate closed → kept (vetoed, still tombstoned); byte-delete failure → vetoed and retried next sweep", "oracle": "test", - "verify": "the dogfood lifecycle describe block ('sys_file orphan lifecycle (ADR-0057 reap guard)') passes; its cases map one-to-one onto these variants", + "verify": "the dogfood lifecycle describe block ('sys_file orphan lifecycle (ADR-0057 reap guard)') passes, and so does attachment-lifecycle.test.ts's createSysFileReapGuard block. The dogfood cases cover the pending, zero-reference, regained-reference and abandoned-upload variants; the byte-delete failure (vetoed, retried next sweep) and the non-attachments-scope tombstone with the migration gate closed (vetoed) are pinned only in the unit block — so neither file alone covers every variant", "evidence": "test run output" }, { @@ -697,14 +697,20 @@ "byte-delete failure (veto, retry next sweep)" ], "traps": ["stale-dist"], - "automated": { "kind": "api", "ref": "packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts" }, + "automated": { "kind": "api", "ref": "packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts + packages/services/service-storage/src/attachment-lifecycle.test.ts (its createSysFileReapGuard block: the byte-delete-failure veto and the field-file-with-the-gate-closed veto, the two variants the dogfood block does not drive)" }, "source": [ "packages/services/service-storage/src/attachment-lifecycle.ts#createSysFileReapGuard (createSysFileReapGuard — the verdict variants are its documented branch set, incl. the ADR-0104 isCollectionOpen gate re-read each sweep)", "packages/services/service-storage/src/objects/system-file.object.ts#deleted_at (lifecycle: ttl deleted_at+30d, retention 7d onlyWhen pending)", "docs/plans/release-15.1-test-plan.md §C4 (#2755/#2970)" ], "history": [ - { "revision": 1, "date": "2026-08-07", "change": "new item: reap-guard verdict matrix enumerated from createSysFileReapGuard's source, pinned to the dogfood lifecycle suite; TTL/retention figures from the sys_file lifecycle declaration", "ref": "claude/platform-test-checklist-ocwugl" } + { "revision": 1, "date": "2026-08-07", "change": "new item: reap-guard verdict matrix enumerated from createSysFileReapGuard's source, pinned to the dogfood lifecycle suite; TTL/retention figures from the sys_file lifecycle declaration", "ref": "claude/platform-test-checklist-ocwugl" }, + { + "revision": 2, + "date": "2026-10-19", + "change": "automated.ref and acceptance[0].verify corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24560, attachments-storage.orphan-tombstone-reap acceptance[0]; wave anchor #24579 §3): the verify said the dogfood block's cases map one-to-one onto the five variants, but two of them — the byte-delete-failure veto and the field file kept while the gate is closed — are pinned only by packages/services/service-storage/src/attachment-lifecycle.test.ts (createSysFileReapGuard), which automated.ref did not name. The run ran it singly and scored the clause on both files. automated.ref now names it and the verify says which file pins which variant. No clause changed", + "ref": "#PENDING-24579R2" + } ] }, { @@ -1080,7 +1086,7 @@ "title": "Chunked upload integrity on v18: complete with a missing chunk is 409 with details.missingChunks and the session stays in_progress, a re-sent chunk counts once, parallel chunks all record, and an org switch mid-upload answers 409 on chunk and complete, and on progress only once the session's expiry stamp is due", "since": "v18", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "api", "personas": [ @@ -1090,6 +1096,9 @@ "app": "showcase", "requires": [ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)" + ], + "knownGaps": [ + "acceptance[0] names the org-switch guard among what the pins prove, but none of the three automated.ref files drives a CHUNKED door after an organization switch, so the door-level 409 of acceptance[3] (chunk PUT, chunked complete, a progress read whose expiry stamp is due) is unpinned. The guard is the doors' shared requireStartingOrganization (packages/services/service-storage/src/storage-routes.ts, asking packages/services/service-storage/src/metadata-store.ts#organizationOutOfWriteReach); the one door-level pin of it is the presigned COMMIT door's 409 RESOURCE_CONFLICT in packages/services/service-storage/src/error-envelope.conformance.test.ts, a sibling door, and chunk-part-record-concurrency.test.ts pins only the store-level miss. Score acceptance[0] on chunk accounting and the missing-chunk refusal alone, and do not cite the pins for the org-switch guard. acceptance[3]'s live leg also needs a second organization, which a stock 18.0 boot refuses (403 ORGANIZATION_CREATION_DISABLED, QA runs #24560 / #24558) — the wave's no-second-organization theme, parked on #24579 §2" ] }, "steps": [ @@ -1100,7 +1109,7 @@ ], "acceptance": [ { - "clause": "chunk accounting, missing-chunk refusal and the org-switch guard — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "clause": "chunk accounting and the missing-chunk refusal — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand). The org-switch guard of acceptance[3] has no chunked-door pin (knownGaps[0])", "oracle": "test", "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", "evidence": "the vitest summary line per pin file, with the package name and the subject sha" @@ -1144,6 +1153,12 @@ "date": "2026-10-19", "change": "acceptance[3] re-pointed and the chunk numbering made the API's (QA run #24560 acceptance[3] fail, disposition stale spec; parked on #24579 §1). #24560: \"the progress door (#22218, storage-routes.ts) deliberately keeps answering the uploader from any organization when there is nothing to stamp\" — packages/services/service-storage/src/storage-routes.ts#registerStorageRoutes asks the starting-organization gate on progress only when the expiry stamp is due, so after an org switch progress answers 200 unless the session's expires_at has passed, and 409 then; chunk PUT and complete answer 409 as before. The steps numbered chunks from 1 while the API numbers them from 0 (#24560 and #24558: the server answered missingChunks [1] for the step's \"chunk 2\"), so the steps now number from 0, acceptance[1] expects [1], and the org-switch step names all three calls. The title follows; re-derived from main 5a871e4e41, where every source cited here is byte-identical to the run subject 5b14b7f3", "ref": "#24622" + }, + { + "revision": 3, + "date": "2026-10-19", + "change": "acceptance[0].clause narrowed to what its pins prove and fixtures.knownGaps added — the out-of-scope §3 note of #24622's round (wave anchor #24579 §3; QA run #24560): none of the three automated.ref pins drives a chunked door after an organization switch, so the door-level 409 acceptance[3] asserts is unpinned. The only door-level pin of the shared guard (storage-routes.ts requireStartingOrganization) is the presigned commit door's, in error-envelope.conformance.test.ts, and chunk-part-record-concurrency.test.ts pins the store-level miss only. acceptance[0] no longer claims the org-switch guard. The knownGap records the unpinned leg, and points at §2 for its live half, which needs a second organization stock 18.0 refuses. acceptance[1]–[3] (#24622) are unchanged", + "ref": "#PENDING-24579R2" } ] } From a4ca510569be663efb1da1ab507926ffca65165b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:05:05 +0000 Subject: [PATCH 4/7] docs(qa): correct eight identity-auth checklist items the 18.0 sweep measured as inaccurate Checklist-accuracy findings of QA run #24538 (identity-auth, 5b14b7f3), each re-derived from source at 8fbbd5082f: invitation-scope-gates, api-key-ui-lifecycle, org-membership-team-management, self-service-password-reset, self-signup-gate, auth-gate-held-session-raw-host-families, default-organization-boot-invariant, self-service-profile-password. One revision and one history row per item. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/identity-auth.json | 104 +++++++++++++----- 1 file changed, 76 insertions(+), 28 deletions(-) diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index 6dc2415831..ab92943cb0 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -292,15 +292,15 @@ "title": "Invitation issuance honors role-scope gates: delegated_admin can invite members but cannot mint admins; a plain member cannot invite at all", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": ["delegated_admin org member", "plain member", "tenant admin (setup only)"], "fixtures": { "app": "showcase", "requires": [ - "a multi-org boot with the organization capability on (the sys_invitation actions — invite_user/cancel_invitation/resend_invitation — are gated on features.organization)", - "a member holding the delegated_admin org role (the ADR-0105 D8 principal; the pinned dogfood test shows the setup: mint the org, set membership roles via the better-auth-managed writer)" + "a stock single-tenancy showcase boot with the organization capability on (the sys_invitation actions — invite_user/cancel_invitation/resend_invitation — are gated on features.organization): the Default Organization is the org every clause needs, and no second organization is involved — the pinned dogfood test boots exactly this (bootShowcase({}), reading the slug-default organization), and 18.0 permits no second organization in any case", + "a member holding the delegated_admin org role (the ADR-0105 D8 principal; the pinned dogfood test shows the setup: read the Default Organization, set membership roles via the better-auth-managed writer)" ] }, "steps": [ @@ -362,7 +362,7 @@ "UI-only enforcement (affordance hidden but the forged request succeeds) is a FAIL — the server is the authority (ADR-0124 D1)", "my-delegable-scope OVER-reporting is a FAIL — a position, permission set or BU subtree the caller cannot actually delegate, or (were the shape ever to grow one) a role the caller cannot mint: a client that trusts the scope would offer what the endpoint then refuses. UNDER-reporting is NOT a FAIL and must not be filed as one: the endpoint answering an empty scope while the delegate can still mint a member invitation is the safe direction and the expected state on stock fixtures (run #7663 — DelegableScope has no invitation-role field at all)" ], - "automated": { "kind": "e2e", "ref": "packages/qa/dogfood/test/delegated-admin-invite.dogfood.test.ts" }, + "automated": { "kind": "e2e", "ref": "packages/qa/dogfood/test/delegated-admin-invite.dogfood.test.ts (its first case asserts the invitation row and its role, not `status: 'pending'` — acceptance[0]'s pending half is scored live)" }, "traps": ["wrong-persona", "dispatcher-vs-hono-route"], "source": [ "packages/qa/dogfood/test/delegated-admin-invite.dogfood.test.ts (ADR-0105 D8 / #3697; the escalation chain the role cap blocks)", @@ -375,7 +375,13 @@ { "revision": 1, "date": "2026-08-07", "change": "new item: invitation scope gates and lifecycle, pinned to the delegated-admin-invite dogfood test", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-08", "change": "added GET /api/v1/security/my-delegable-scope clause (delegated_admin scope excludes admin-mintable roles; plain member empty/denied) — the read half that feeds the invite role picker (PENDING-GAPS §D)", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 3, "date": "2026-08-11", "change": "CORRECTION from run #7663: the delegable-scope clause conflated two axes. DelegableScope models ObjectStack positions / permission sets / business units (security-service.ts) and has NO field that could carry a better-auth org invitation role, so 'the returned role set contains member but not admin' was unassertable — the run watched a delegate mint a member invitation while the endpoint reported an empty assignable set. Rewrote the clause to assert the DelegableScope shape and the self-scoping, moved the invite-role gate onto clauses 1-4 (where the endpoint is the authority), and split the negative: OVER-reporting is the FAIL, UNDER-reporting is the safe direction and must not be filed", "ref": "#7740" }, - { "revision": 4, "date": "2026-10-17", "change": "steps[1] and [6] and acceptance[5].verify corrected (checklist-accuracy findings of the identity-auth slice report on #23878 (6216631966)). cancel_invitation is owner/admin only, so 'the entitled persona' in steps[6] was ambiguous and the delegated_admin the item drives is refused it; and the invite affordance lives at /_console/apps/com.objectstack.account/sys_invitation, which no nav entry links to. Not loosened: the lifecycle clause still requires each transition to show in a fresh read", "ref": "#24036" } + { "revision": 4, "date": "2026-10-17", "change": "steps[1] and [6] and acceptance[5].verify corrected (checklist-accuracy findings of the identity-auth slice report on #23878 (6216631966)). cancel_invitation is owner/admin only, so 'the entitled persona' in steps[6] was ambiguous and the delegated_admin the item drives is refused it; and the invite affordance lives at /_console/apps/com.objectstack.account/sys_invitation, which no nav entry links to. Not loosened: the lifecycle clause still requires each transition to show in a fresh read", "ref": "#24036" }, + { + "revision": 5, + "date": "2026-10-19", + "change": "fixtures.requires[0]–[1] and automated.ref corrected — checklist-accuracy findings of the 18.0 QA sweep (QA run #24538, identity-auth.invitation-scope-gates; wave anchor #24579 §3). The item required a multi-org boot; every clause runs on the stock single-tenancy showcase's Default Organization, which is what its own pin boots (delegated-admin-invite.dogfood.test.ts: bootShowcase({}), reading the slug-default organization rather than minting one). automated.ref now says the pin's first case asserts the row and its role but not status pending, which the run proved live. No clause changed", + "ref": "#PENDING-24579R2" + } ] }, { @@ -614,7 +620,7 @@ "title": "Personal API key: minted show-once on Connect-an-Agent, authenticates as its owner, revoke kills it and restore brings it back — mine-view scoped", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": ["a signed-in member (key owner)", "platform admin", "the key itself (machine caller via x-api-key)"], @@ -683,14 +689,20 @@ "source": [ "objectui apps/console/src/pages/developer/AgentConnectSection.tsx (ADR-0036 Phase 2b: POST /api/v1/keys mint, show-once dialog, x-api-key connect steps)", "packages/runtime/src/route-ledger.ts (POST /keys → client keys.create)", - "packages/platform-objects/src/identity/sys-api-key.object.ts#bodyExtra (revoke_api_key/restore_api_key actions PATCH /api/v1/data/sys_api_key/{id} bodyExtra revoked; mine view user_id={current_user_id} vs all_keys; hashed hidden `key`, visible `prefix`; apiMethods ['get','list'])", + "packages/platform-objects/src/identity/sys-api-key.object.ts#bodyExtra (revoke_api_key/restore_api_key actions PATCH /api/v1/data/sys_api_key/{id} bodyExtra revoked; mine view user_id={current_user_id} vs all_keys; hashed hidden `key`, visible `prefix`; apiMethods ['get','list','update'] — `update` is what lets the revoke/restore PATCH through; the batch primitive stays off deliberately)", "packages/platform-objects/src/apps/setup-nav.contributions.ts#nav_api_keys (nav_api_keys requiredPermissions ['manage_platform_settings'] — admin all-view)", "packages/core/src/security/api-key.ts#lifecycleRefusalOf (#23367 / #23541 — a recognised revoked key is refused 403 API_KEY_REVOKED with a located message; an unknown key keeps the generic 401) + packages/rest/src/rest-server.ts#enforceAuth (the data routes answer the remembered lifecycle refusal's status and code)" ], "history": [ { "revision": 1, "date": "2026-08-08", "change": "new item: personal API key mint (show-once, POST /api/v1/keys) → authenticate-as-owner via x-api-key → revoke 401 → restore 200, plus mine-view vs admin all-view scoping, grounded in AgentConnectSection + sys_api_key object (PENDING-GAPS §B)", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-10-16", "change": "stale oracle re-pointed (#23992): step 4 and acceptance[2] expected the post-revoke x-api-key call to answer 401, but since the API-key lifecycle (#23367 ruling A, executed by #23541) a recognised revoked key is refused 403 with error.code API_KEY_REVOKED (packages/core/src/security/api-key.ts#lifecycleRefusalOf) and only an unknown key gets the generic 401, so the item scored a correct runtime red. The step and the clause now capture the 403, the code and the absence of rows, and pin none of the refusal's wording (#23944 rewords it). Not loosened: a 200, a generic 401 and a 403 with any other code each still FAIL (the negative names both directions). Step 5 (restore → 200) is unchanged; source gains the refusal's decision and relay sites", "ref": "#23992" }, - { "revision": 3, "date": "2026-10-17", "change": "fixtures.requires[0] and [1], steps[0] and [3] and acceptance[1].verify re-pointed (checklist-accuracy residual of the identity-auth slice report on #23878 (6216631966), after PR #24012). showcase_task returns the same rows to a member and an admin, so the owner-view comparison could not tell an RLS-scoped key from a bypass; sys_session is owner-scoped, so the key's read must equal the owner's and fall short of the admin's. The Integrations page is named by its path, /_console/apps/com.objectstack.account/developer/integrations. Not loosened: a key that reads more than its owner still FAILS", "ref": "#24036" } + { "revision": 3, "date": "2026-10-17", "change": "fixtures.requires[0] and [1], steps[0] and [3] and acceptance[1].verify re-pointed (checklist-accuracy residual of the identity-auth slice report on #23878 (6216631966), after PR #24012). showcase_task returns the same rows to a member and an admin, so the owner-view comparison could not tell an RLS-scoped key from a bypass; sys_session is owner-scoped, so the key's read must equal the owner's and fall short of the admin's. The Integrations page is named by its path, /_console/apps/com.objectstack.account/developer/integrations. Not loosened: a key that reads more than its owner still FAILS", "ref": "#24036" }, + { + "revision": 4, + "date": "2026-10-19", + "change": "source[2] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.api-key-ui-lifecycle; wave anchor #24579 §3): the citation said sys_api_key declares apiMethods ['get','list']; packages/platform-objects/src/identity/sys-api-key.object.ts declares ['get','list','update'], which is why the revoke/restore PATCH answers 200. Citation only; behaviour and clauses unchanged", + "ref": "#PENDING-24579R2" + } ] }, { @@ -698,7 +710,7 @@ "title": "Self-service profile: name + avatar edits persist through real storage, and password change verifies the current password (passwordless users get set-initial)", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "mixed", "personas": ["a signed-in email+password user", "a signed-in passwordless (SSO-only) user"], @@ -707,10 +719,10 @@ "requires": [ "the Account app Profile page (objectui apps/console ProfilePage.tsx) reachable for a signed-in user", "a working file-storage backend so the avatar upload writes a real object (useUpload → presigned/committed file); if storage is unconfigured the avatar clause is blocked(fixture) not faked", - "for the set-initial-password clause: a user with NO local password (signed in via SSO/social) — hasLocalPassword() returns false; on stock showcase this needs a social/OIDC provider, so that clause is blocked(fixture) same class as sso-enforced unless a passwordless user can be provisioned" + "for the set-initial-password clause: a user with NO local password — hasLocalPassword() returns false. On stock showcase it is provisionable without an IdP: as admin POST /api/v1/auth/admin/import-users {format: 'json', rows: [{email, name}], passwordPolicy: 'none'} (packages/plugins/plugin-auth/src/admin-import-users.ts — the user is created with no credential), then POST /api/v1/auth/admin/impersonate-user {userId} for that user's session (carry its session_token cookie, or the localStorage auth-session-token, into the browser context)" ], "knownGaps": [ - "the set-initial-password branch needs a passwordless (SSO-only) user; stock showcase ships no configured IdP, so provision one or run that clause blocked(fixture)" + "CLOSED (QA run #24538): the set-initial-password branch was believed to need a passwordless SSO-only user and so an IdP; the import-users passwordPolicy 'none' + impersonate-user recipe in fixtures.requires[2] provisions a credential-less user on stock showcase, and the run scored acceptance[5] pass on it (email+password sign-in refused before, 'Set Local Password' with no current-password field, set-initial-password 200, sign-in after). A run without admin impersonation still records the clause blocked(fixture)" ] }, "steps": [ @@ -721,7 +733,7 @@ "change the password: enter the correct current password + a new one (PasswordCard → auth.changePassword(current,new) → POST /api/v1/auth/change-password); sign out and sign in with the NEW password (2xx), then confirm the OLD password is refused", "negative: attempt change-password with a WRONG current password and capture the refusal — the current-password check is server-enforced, not just a UI confirm", "confirm email is immutable on this surface (the email input is disabled, 'Email cannot be changed') — self-service email change is a distinct endpoint (identity-auth.auth-method-matrix covers /auth/change-email)", - "passwordless branch (if a set-initial-capable user exists): as the SSO-only user, the card renders 'Set Local Password' (hasLocalPassword false) — set an initial password (setInitialPassword → no current-password field) and then sign in with email+password to prove the local credential now exists" + "passwordless branch: provision a credential-less user per fixtures.requires[2] (import-users with passwordPolicy none, then impersonate-user for its session); as that user, the card renders 'Set Local Password' (hasLocalPassword false) — set an initial password (setInitialPassword → no current-password field) and then sign in with email+password to prove the local credential now exists" ], "acceptance": [ { @@ -774,7 +786,13 @@ "packages/platform-objects/src/identity/sys-account.object.ts#previous_password_hashes (previous_password_hashes ring — ADR-0069 D1 reuse-prevention backs change-password)" ], "history": [ - { "revision": 1, "date": "2026-08-08", "change": "new item: self-service name + avatar (real storage write, persists) and password change with current-password verification, plus the passwordless set-initial branch, grounded in ProfilePage.tsx + the auth route ledger (PENDING-GAPS §B)", "ref": "claude/platform-test-checklist-ocwugl" } + { "revision": 1, "date": "2026-08-08", "change": "new item: self-service name + avatar (real storage write, persists) and password change with current-password verification, plus the passwordless set-initial branch, grounded in ProfilePage.tsx + the auth route ledger (PENDING-GAPS §B)", "ref": "claude/platform-test-checklist-ocwugl" }, + { + "revision": 2, + "date": "2026-10-19", + "change": "fixtures.requires[2], knownGaps[0] and steps[7] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.self-service-profile-password; wave anchor #24579 §3): the knownGap said the set-initial-password branch needs an IdP, but a credential-less user is provisionable on stock showcase through POST /api/v1/auth/admin/import-users with passwordPolicy 'none' (admin-import-users.ts) and a session through POST /api/v1/auth/admin/impersonate-user — the recipe the run scored acceptance[5] pass on. The knownGap is marked CLOSED and the step drives the recipe. No clause changed", + "ref": "#PENDING-24579R2" + } ] }, { @@ -782,7 +800,7 @@ "title": "Setup Organization page resolves the active org and drives member/invitation/team management through the better-auth org endpoints — non-admins refused server-side", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": ["tenant admin / org owner", "a target org member", "a non-admin org member (forger)"], @@ -799,7 +817,7 @@ "change a member's role: POST /api/v1/auth/organization/update-member-role (client organizations.updateMemberRole) to one of the ADR-0108 closed 4-name vocabulary {owner, admin, delegated_admin, member}; read the membership back and confirm the new role", "prove the vocabulary is CLOSED, not merely conventional: attempt the same call with role 'guest' (and with any stack-declared position/permission-set name, e.g. showcase's 'contributor') — better-auth's role check refuses it (400 ROLE_NOT_FOUND) before any insert, and no membership/invitation row is left behind", "prove the role change bites: an operation the new role gates flips outcome for that member (e.g. promote to admin → an org-admin-only action now permitted; demote → refused)", - "rename the organization: POST /api/v1/auth/organization/update (organizations.update) with a new name; re-read GET get-full-organization and confirm the rename persisted and the nav label follows", + "rename the organization: POST /api/v1/auth/organization/update (organizations.update) with a new name; re-read GET get-full-organization and confirm the rename persisted and the organization record page's title follows. The Setup nav entry does NOT follow: nav_organization's label is the static string 'Organization' (packages/platform-objects/src/apps/setup-nav.contributions.ts#nav_organization)", "remove a member: POST /api/v1/auth/organization/remove-member (organizations.removeMember); confirm the member's org-scoped access SHRINKS through a request that is answered BECAUSE of membership of that org — e.g. GET /api/v1/auth/organization/get-full-organization?organizationId=ORG, or POST /api/v1/auth/organization/set-active for it — succeeding before and refused 403 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION after. Read the after-side on a FRESH sign-in: remove-member also revokes the member's live sessions (revoke_reason organization_membership_ended), so the old session answers 401 to every request and discriminates nothing. ⛔ Not a data read: on the stock `single` posture the `everyone` baseline grants survive remove-member, so an ex-member still reads the same showcase rows and the check could not discriminate", "create a team + add members: POST /api/v1/auth/organization/create-team (organizations.teams.create), then POST /api/v1/auth/organization/add-team-member (organizations.teams.addMember) for two users; read sys_team_member and confirm two join rows exist (unique on team_id+user_id)", "both-sides gate: as a NON-admin member, fire update-member-role / remove-member / update / create-team / add-team-member directly and capture each server refusal; screenshot that the management affordances are absent in the non-admin's UI" @@ -824,7 +842,7 @@ "evidence": "the refusal response + the follow-up read" }, { - "clause": "rename via organization/update persists: get-full-organization returns the new name and the surface follows", + "clause": "rename via organization/update persists: get-full-organization returns the new name and the organization record page's title follows (the static 'Organization' nav label is not expected to change)", "oracle": "api", "verify": "GET get-full-organization after the update shows the renamed org", "evidence": "the before/after org reads" @@ -869,7 +887,13 @@ { "revision": 1, "date": "2026-08-08", "change": "new item: Setup Organization page {current_org_id} resolution (ADR-0081) with Members/Invitations/Teams tabs, update-member-role (4-name vocab)/remove-member/rename, create-team + add-team-member → sys_team_member rows, non-admin refused server-side (PENDING-GAPS §B). Teams membership deep-tested in identity-auth.teams-bu-membership; org-member management stays here", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-11", "change": "CORRECTION from run #7663: the role vocabulary named here was wrong. The enforced builtin set is {owner, admin, delegated_admin, member} (ADR-0108 BUILTIN_MEMBERSHIP_ROLE_OPTIONS), NOT {owner, admin, member, guest} — 'guest' is rejected 400 ROLE_NOT_FOUND and delegated_admin is legitimate. Corrected the step, the acceptance clause and the negative; added a closed-vocabulary clause (guest / a stack position / a PermissionSet name each refused, no row left behind); re-pointed source at membership-role.ts + ADR-0108 + the vocabulary dogfood pin instead of the stale organization.zod.ts doc-comment the wrong text came from (that doc-comment is fixed in the same PR)", "ref": "#7740" }, { "revision": 3, "date": "2026-09-12", "change": "PROVENANCE, no clause change: #16270 measured that no object under packages/platform-objects/src/identity/ declares Field.relatedList and read the Members/Invitations/Teams claim here as possibly stale. It is not stale — the strip is declared by SysOrganizationDetailPage (packages/platform-objects/src/pages/sys-organization.page.ts), a slotted page whose slots.tabs override replaces the synthesized strip. Added that file as a source entry so the next reader does not have to re-derive it. Steps, acceptance clauses, oracles and negatives are unchanged", "ref": "#16270" }, - { "revision": 4, "date": "2026-10-17", "change": "steps[6] and acceptance[4].verify name an org-membership-scoped oracle (checklist-accuracy finding of the identity-auth slice report on #23878 (6216631966)). On the stock `single` posture the `everyone` baseline reads survive remove-member, so 'a resource they could read as a member now refuses' found no data resource to refuse; the oracle is now a request answered because of membership of that org, read on a fresh sign-in — the re-run on #24036 found remove-member revoking the member's live sessions (organization_membership_ended), while the fresh session still read 5 showcase rows and drew 403 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION from get-full-organization and set-active. Not loosened: removal must still refuse it", "ref": "#24036" } + { "revision": 4, "date": "2026-10-17", "change": "steps[6] and acceptance[4].verify name an org-membership-scoped oracle (checklist-accuracy finding of the identity-auth slice report on #23878 (6216631966)). On the stock `single` posture the `everyone` baseline reads survive remove-member, so 'a resource they could read as a member now refuses' found no data resource to refuse; the oracle is now a request answered because of membership of that org, read on a fresh sign-in — the re-run on #24036 found remove-member revoking the member's live sessions (organization_membership_ended), while the fresh session still read 5 showcase rows and drew 403 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION from get-full-organization and set-active. Not loosened: removal must still refuse it", "ref": "#24036" }, + { + "revision": 5, + "date": "2026-10-19", + "change": "steps[5] and acceptance[3].clause corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.org-membership-team-management; wave anchor #24579 §3): after a rename the step expected the nav label to follow, but nav_organization's label is the static 'Organization' (setup-nav.contributions.ts); what follows is the organization record page's title, which the run observed. Not a loosening: the persisted name and the page title must both show the rename", + "ref": "#PENDING-24579R2" + } ] }, { @@ -958,7 +982,7 @@ "title": "Forgot-password: request → token captured at the dev mail transport → reset; old password refused, expired/reused token refused loudly", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": ["anonymous visitor (holds no session)", "the account owner (before and after reset)"], @@ -985,7 +1009,7 @@ "acceptance": [ { "clause": "the request issues a real reset artifact: POST /api/v1/auth/request-password-reset produces an email at the dev transport carrying a reset token/link (GET /api/v1/auth/reset-password/:token)", - "oracle": "log", + "oracle": "api", "verify": "the sys_email row the request wrote (to = the requester) carries the reset link with a token in its body; the request response is a non-leaking 2xx/accepted", "evidence": "the captured sys_email row (to, subject, the link with the token redacted after use)" }, @@ -1028,7 +1052,13 @@ ], "history": [ { "revision": 1, "date": "2026-08-08", "change": "new item: forgot-password round trip (request-password-reset → dev-transport token capture → reset-password), old-password-refused/new-works, single-use + expiry refusals, anti-enumeration, with the token-capture dev-mail dependency recorded as a knownGap (PENDING-GAPS §B)", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 2, "date": "2026-10-17", "change": "fixtures.requires[0], knownGaps[0], steps[1] and acceptance[0] (verify, evidence) re-pointed to the capture oracle that exists (checklist-accuracy finding of the identity-auth slice report on #23878 (6216631966)). EMAIL_TRANSPORT_PROVIDERS is a const list, not an env switch, and LogTransport logs only to / subject / hasText / hasHtml, so 'capture the token at the dev transport output' named an output that never carries the link. The reset mail's sys_email row holds the rendered body, and the slice read the token there. Not loosened: the clause still requires a real mail carrying a working tokened link", "ref": "#24036" } + { "revision": 2, "date": "2026-10-17", "change": "fixtures.requires[0], knownGaps[0], steps[1] and acceptance[0] (verify, evidence) re-pointed to the capture oracle that exists (checklist-accuracy finding of the identity-auth slice report on #23878 (6216631966)). EMAIL_TRANSPORT_PROVIDERS is a const list, not an env switch, and LogTransport logs only to / subject / hasText / hasHtml, so 'capture the token at the dev transport output' named an output that never carries the link. The reset mail's sys_email row holds the rendered body, and the slice read the token there. Not loosened: the clause still requires a real mail carrying a working tokened link", "ref": "#24036" }, + { + "revision": 3, + "date": "2026-10-19", + "change": "acceptance[0].oracle 'log' → 'api' — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.self-service-password-reset acceptance[0]; wave anchor #24579 §3): the clause's verify and evidence already read the sys_email row the request wrote, an API read, not a log line; the oracle label now names what the run consults. Clause text, verify and evidence unchanged", + "ref": "#PENDING-24579R2" + } ] }, { @@ -1766,7 +1796,7 @@ "title": "Self-signup gate: the default audience posture (invite_only) refuses at the server with a LOUD code, a pending invitation carves through, and OS_DISABLE_SIGNUP bounces the UI AND the endpoint — the console offers the register form only to a visitor the server would admit (an invitation redirect, or a deployment with no owner) and explains the by-invitation rule to everyone else; the form is never the gate", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": ["anonymous uninvited registrant", "an invited email's registrant", "org admin (mints the invitation)", "env owner (sets OS_DISABLE_SIGNUP)"], @@ -1784,7 +1814,7 @@ ] }, "steps": [ - "stock boot (the seeded admin exists, so the deployment has an owner): GET /api/v1/auth/config and record features.audiencePosture ('invite_only') and emailPassword.disableSignUp (false) — then confirm the login page offers NO generic sign-up link and /register renders the by-invitation notice (data-testid register-by-invitation: 'Self-registration is not open on this environment. Ask an administrator for an invitation.') with NO form. The form is offered only where the server would admit the visitor: a visitor bounced from an invitation (/login?redirect=/accept-invitation/ID, which the login page forwards to /register) or a deployment with no owner yet (GET /api/v1/auth/bootstrap-status answers hasOwner false) — objectui decideSignUpOffer (objectui#11691, objectui#11705). The server side is unchanged and DELIBERATE: the posture does not force disableSignUp, because hiding the form outright would dead-end invited users (getPublicConfig comment, auth-manager.ts); the console reads both keys instead", + "stock boot (the seeded admin exists, so the deployment has an owner): GET /api/v1/auth/config and record features.audiencePosture ('invite_only') and emailPassword.disableSignUp (false) — then confirm the login page offers NO generic sign-up link and /register renders the by-invitation notice (data-testid register-by-invitation — judge the testid and the absent form, not the notice's sentence, whose wording moves between console builds: the step used to quote 'not open on this environment', and the console pin 4016370a reads 'not open on this system') with NO form. The form is offered only where the server would admit the visitor: a visitor bounced from an invitation (/login?redirect=/accept-invitation/ID, which the login page forwards to /register) or a deployment with no owner yet (GET /api/v1/auth/bootstrap-status answers hasOwner false) — objectui decideSignUpOffer (objectui#11691, objectui#11705). The server side is unchanged and DELIBERATE: the posture does not force disableSignUp, because hiding the form outright would dead-end invited users (getPublicConfig comment, auth-manager.ts); the console reads both keys instead", "POST /api/v1/auth/sign-up/email directly for an UNINVITED address (the stock /register offers an uninvited visitor no form — step 1): capture the 403 with code SELF_REGISTRATION_CLOSED — a loud, deterministic refusal, NOT a synthetic 200. The sign-up route raises the audience refusal BEFORE better-auth's anti-enumeration shield can swallow a creation-seam 403 into a fake success (auth-manager.ts — the measured black-hole this ordering exists to prevent)", "confirm no sys_user row was created for the refused address (admin read)", "carve-out: as admin mint an invitation for a fresh email, then sign up with EXACTLY that email on the same stock boot — admitted (a pending, unexpired sys_invitation trumps the posture; audience-posture.ts)", @@ -1852,7 +1882,13 @@ { "revision": 1, "date": "2026-08-30", "change": "authored in the 2026-08-30 coverage sweep (angle 4). CORRECTED against source from the register's hypothesis 'default boot sign-up completes': since #11739 (epic #11723, ruling 2026-08-24) the undeclared audience resolves to invite_only, so a stock-boot uninvited self-signup is REFUSED 403 SELF_REGISTRATION_CLOSED — the item encodes the posture matrix (enumSource-pinned on AUDIENCE_POSTURES), the pre-anti-enumeration-shield loud-refusal ordering, the invitation carve-out, the deliberate form-stays-rendered shape, and the OS_DISABLE_SIGNUP env gate on both the UI and the endpoint", "ref": "#sweep-2026-08-30" }, { "revision": 2, "date": "2026-09-28", "change": "restated two knownGaps, the open variant and one source note to the shipped rule. Verification: email_domain always forces it on; open forces it on unless the deployment declares it off (OS_AUTH_REQUIRE_EMAIL_VERIFICATION=false, or emailAndPassword.requireEmailVerification: false in the stack config), and a false stored only through the settings console is refused under open — the BOUNDARY gap said every permitting posture forces it. Audience declaration: it is not config-only — the auth settings namespace carries it, through the console and the OS_AUTH_AUDIENCE_* env overrides; the gap and the source note said no env knob. Steps, acceptance clauses, oracles and negatives are unchanged", "ref": "#20413" }, { "revision": 3, "date": "2026-10-13", "change": "negative 5, negative[4], re-pointed with step 1, step 2's UI half, the title and a new objectui source note (stale spec, #23104 N5). The item pinned that under stock invite_only the login page offers the register link and /register renders the form. Since objectui#11691 / objectui#11705 the console offers the form only where the server would admit the visitor: decideSignUpOffer reads features.audiencePosture beside disableSignUp, and offers the form under invite_only only to an /accept-invitation/ID redirect or on a deployment with no owner (bootstrap-status hasOwner false). Everyone else gets no sign-up link on /login and the by-invitation notice with no form on /register. The negative now fails the old shape (form offered, then refused 403 SELF_REGISTRATION_CLOSED) and still fails an invitee left without a form. Step 2 keeps its direct POST, since the stock /register gives an uninvited visitor no form to submit. The server rule is unchanged (getPublicConfig still does not force disableSignUp from the posture). Read at objectui 1fb1ced36243 (this repo's .objectui-sha): signUpOffer.ts, RegisterPage.tsx, LoginPage.tsx and signUpFollowsPosture-11691.test.tsx, byte-identical at 7578323a544e. acceptance[2]'s closing phrase, which says the form stays rendered under the closed posture, is left as written: it is true for an invitee, and that clause is outside this revision", "ref": "#23121" }, - { "revision": 4, "date": "2026-10-17", "change": "knownGaps[0], steps[5] and acceptance[4] (verify, evidence) corrected (checklist-accuracy findings of the identity-auth slice report on #23878 (6216631966)). On an OS_DISABLE_SIGNUP boot an uninvited forge never reaches the toggle: the audience gate refuses it first, 403 SELF_REGISTRATION_CLOSED, so 'returns non-2xx' passed on the wrong gate; only an invited address reaches the toggle and draws 400 EMAIL_PASSWORD_SIGN_UP_DISABLED. The step and clause now say which address proves which door. knownGaps[0] was stale: the open posture through the env door is measured live, so that variant is runnable on stock. Not loosened: the toggle must still refuse the invited address with no row written", "ref": "#24036" } + { "revision": 4, "date": "2026-10-17", "change": "knownGaps[0], steps[5] and acceptance[4] (verify, evidence) corrected (checklist-accuracy findings of the identity-auth slice report on #23878 (6216631966)). On an OS_DISABLE_SIGNUP boot an uninvited forge never reaches the toggle: the audience gate refuses it first, 403 SELF_REGISTRATION_CLOSED, so 'returns non-2xx' passed on the wrong gate; only an invited address reaches the toggle and draws 400 EMAIL_PASSWORD_SIGN_UP_DISABLED. The step and clause now say which address proves which door. knownGaps[0] was stale: the open posture through the env door is measured live, so that variant is runnable on stock. Not loosened: the toggle must still refuse the invited address with no row written", "ref": "#24036" }, + { + "revision": 5, + "date": "2026-10-19", + "change": "steps[0] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.self-signup-gate; wave anchor #24579 §3): the step quoted the by-invitation notice as 'Self-registration is not open on this environment…'; the pinned console (objectui 4016370a, RegisterPage.tsx) renders '…not open on this system…'. The step no longer pins the sentence; the data-testid and the absent form are the oracle. No clause changed", + "ref": "#PENDING-24579R2" + } ] }, { @@ -2163,7 +2199,7 @@ "title": "A session held by the ADR-0069 credential policy (password expired, forced reset, MFA not enrolled) is refused with the gate's own 403 code on EVERY raw-host route family, and an API key whose owner holds an account-scope posture is refused too", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -2174,7 +2210,7 @@ "app": "showcase", "requires": [ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", - "a member whose session the policy holds — the cheapest: create the member with POST /api/v1/auth/admin/create-user, which lands mustChangePassword:true, and sign it in BEFORE it changes its password (held with PASSWORD_RESET_REQUIRED); an expiry / MFA-required policy in auth settings holds it with the other two codes", + "a member whose session the policy holds — the cheapest: create the member with POST /api/v1/auth/admin/create-user, which lands mustChangePassword:true, and sign it in BEFORE it changes its password (held with PASSWORD_RESET_REQUIRED); an expiry / MFA-required policy in auth settings holds it with the other two codes. ⚠️ An organization's require_mfa reaches the gate only after the 'any org requires MFA' cache refreshes — a 60 s TTL, refreshed in the background by a request made after it goes stale (packages/plugins/plugin-auth/src/auth-manager.ts refreshOrgMfaCacheIfStale) — so the MFA hold can lag the switch by over a minute (QA run #24538 saw ~70 s): poll the held call until it answers MFA_REQUIRED before scoring, never probe once and score a 200", "an API key minted by the member before the hold (POST /api/v1/keys)" ] }, @@ -2235,6 +2271,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "fixtures.requires[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.auth-gate-held-session-raw-host-families; wave anchor #24579 §3): the MFA hold appeared ~70 s after require_mfa was switched on, because plugin-auth reads organization MFA through a 60 s-TTL cache refreshed in the background (auth-manager.ts refreshOrgMfaCacheIfStale). A runner probing at once sees 200 and could score a false FAIL; the fixture now says to poll. No clause changed", + "ref": "#PENDING-24579R2" } ] }, @@ -2380,7 +2422,7 @@ "title": "ADR-0131 single posture: the Default Organization exists from boot BEFORE any sign-up — the first sign-up becomes its owner, later sign-ups its members, every session carries it as active org, and seeded org-columned rows carry it instead of NULL", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P0", "surface": "api", "personas": [ @@ -2394,7 +2436,7 @@ ] }, "steps": [ - "on a fresh DB, confirm the slug `default` sys_organization row exists before the first user: read the boot log ordering (the Default Organization is ensured before --seed-admin signs the admin up) or boot WITHOUT --seed-admin and read the scratch DB", + "on a fresh DB, confirm the slug `default` sys_organization row exists before the first user: read the boot log ordering (the Default Organization is ensured before --seed-admin signs the admin up) — at --log-level info only: the [default-org] lines a healthy boot prints are info-level (packages/plugins/plugin-auth/src/ensure-default-organization.ts), and `os dev`'s default warn level prints no Default-Organization line at all — or compare the created_at of the slug-default sys_organization row with the first sys_user row's, or boot WITHOUT --seed-admin and read the scratch DB", "let --seed-admin create the first user, then create a second with POST /api/v1/auth/admin/create-user (self-registration is closed on the stock showcase); GET /api/v1/auth/get-session for each", "as admin GET /api/v1/data/sys_member?filter organization = default", "as admin GET /api/v1/data/sys_business_unit and read organization_id on the seeded rows" @@ -2409,7 +2451,7 @@ { "clause": "the Default Organization exists before the first sign-up", "oracle": "log", - "verify": "boot log line / sys_organization row read before any POST /auth/sign-up", + "verify": "boot log line (printed at --log-level info only) / sys_organization row read before any POST /auth/sign-up, or the created_at ordering of the organization row and the first user row", "evidence": "the row (id, slug) and the timestamp ordering against the first sign-up" }, { @@ -2448,6 +2490,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "steps[0] and acceptance[1].verify corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.default-organization-boot-invariant; wave anchor #24579 §3): at os dev's default log level the boot prints no Default-Organization line, so 'read the boot log ordering' had nothing to read — ensure-default-organization.ts logs its success lines at info. The step now names --log-level info and the created_at ordering the run used instead. No clause changed", + "ref": "#PENDING-24579R2" } ] }, From 82f40a0cab3551034f633e79c4acd74d1f143970 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:08:33 +0000 Subject: [PATCH 5/7] docs(qa): correct six approvals checklist items the 18.0 sweep measured as inaccurate Checklist-accuracy findings of QA run #24542 (approvals, 5b14b7f3), each re-derived from source at 8fbbd5082f: per-group-signoff, quorum-m-of-n, decision-action-matrix, account-app-entry, approver-resolution-matrix, request-page-reply-and-reader-visibility. email-action-token-door and act-door-and-admin-rescue-slate are left alone. One revision and one history row per item. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../platform-checklist/areas/approvals.json | 82 +++++++++++++------ 1 file changed, 59 insertions(+), 23 deletions(-) diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 323a42be76..20f25564d9 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -9,7 +9,7 @@ "title": "Per-group sign-off (会签) needs one approval from EACH group", "since": "v16", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "browser", "personas": ["approver holding exactly one group (e.g. manager)", "second approver holding the other group (e.g. finance/auditor)"], @@ -56,7 +56,7 @@ { "clause": "after the finance-group approval: the request finalizes approved and the flow run resumes down its approve edge to the Approved end", "oracle": "api", - "verify": "request status=approved; the showcase_expense_signoff run transitions paused→completed with the approve branch taken", + "verify": "request status=approved; the showcase_expense_signoff run transitions paused→completed. The approve EDGE itself is not observable — end nodes log no step, and this flow's approve edge ends at the Approved end — so the request's approved status plus the run's status transition is the evidence, the wording approvals.quorum-m-of-n uses for its veto edge", "evidence": "final request read + run read" }, { @@ -90,7 +90,13 @@ { "revision": 4, "date": "2026-08-11", "change": "knownGaps records the better-auth issuer=local:credential provisioning detail — the one field that decides whether Ada can be driven as a real second session or the run falls back to the admin override; it used to be rediscovered every sweep, behind a 'User not found' warn that points at the wrong cause", "ref": "#7530" }, { "revision": 5, "date": "2026-08-18", "change": "the fixture blocker is CLOSED, not merely documented: the showcase now provisions Ada's better-auth credential account at boot (issuer derived from the dev admin's own row), so she signs in on stock seeds. knownGaps rewritten as closed-by-fixture rather than deleted — the issuer detail is the reason the gap existed and the next sweep still needs it. The admin-override fallback is withdrawn as an acceptable path for this item", "ref": "#9308" }, { "revision": 6, "date": "2026-09-19", "change": "the issuer knownGap is re-anchored rather than re-pointed: better-auth 1.7.3 rolled the issuer-scoped account identity back and sys_account.issuer retired with it, so the cited plugin-auth backfill module no longer exists and has no successor. The reason the gap carries is kept verbatim in substance — a password hash alone was not enough, and the failure was a silent lockout whose 'User not found' warn pointed at the wrong row — and the durable anchor is now the retirement's own migration entry in packages/spec. The clause describing the seed as reading the issuer off the dev admin's credential row is withdrawn as false: there is no issuer to read", "ref": "#17453" }, - { "revision": 7, "date": "2026-10-16", "change": "the actorId clause described a silent override ('a hint … regardless of any actorId in the body'), but the server REFUSES a mismatched actorId (#23878 checklist-accuracy finding). Re-measured on main c91af8ed6 on EXP-2001: the admin's approve carrying Ada's id answers 403 FORBIDDEN \"cannot act as 'usr_showcase_auditor_demo' — an approval action is recorded against the authenticated caller\" and adds no action row; the same call carrying the admin's own id answers 200 and the approve row's actor is the admin. The clause now states the stricter, measured behavior; it still fails if a foreign actorId is recorded", "ref": "#24036" } + { "revision": 7, "date": "2026-10-16", "change": "the actorId clause described a silent override ('a hint … regardless of any actorId in the body'), but the server REFUSES a mismatched actorId (#23878 checklist-accuracy finding). Re-measured on main c91af8ed6 on EXP-2001: the admin's approve carrying Ada's id answers 403 FORBIDDEN \"cannot act as 'usr_showcase_auditor_demo' — an approval action is recorded against the authenticated caller\" and adds no action row; the same call carrying the admin's own id answers 200 and the approve row's actor is the admin. The clause now states the stricter, measured behavior; it still fails if a foreign actorId is recorded", "ref": "#24036" }, + { + "revision": 8, + "date": "2026-10-19", + "change": "acceptance[3].verify corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24542, approvals.per-group-signoff acceptance[3]; wave anchor #24579 §3): it asked for 'the approve branch taken' in the run read, but end nodes log no step and showcase_expense_signoff's approve edge ends at an end node, so no run read can show the branch. The verify now scores the run status transition and the request status, as approvals.quorum-m-of-n already words the same evidence. Clause text unchanged", + "ref": "#PENDING-24579R2" + } ] }, { @@ -98,7 +104,7 @@ "title": "M-of-N quorum approves at the threshold; one rejection vetoes", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "browser", "personas": ["three DISTINCT users each holding one of the approver positions"], @@ -106,7 +112,7 @@ "app": "showcase", "requires": [ "a quorum (minApprovals: 2) request whose three position approvers resolve to three distinct users", - "RUNTIME FIXTURE (admin API only, stock boot, nothing seeded or committed; proved by run #21845) — apply it before the quorum steps, in this order: (1) bind a second committee position holder: as the admin, POST /api/v1/data/sys_user_position {user_id: , position: 'finance', organization_id: } — admin + Ada is a real 2-of-2 slate; (2) for the full 2-of-3, invite + sign up a third user and bind them to position 'legal' the same way; prove each persona with GET /api/v1/auth/get-session before it acts; (3) DISABLE the competing per-group sign-off flow for the run: POST /api/v1/automation/showcase_expense_signoff/toggle {\"enabled\": false} — it has no amount ceiling and opens its request first on any >= 5000 report, so without this the quorum flow's open fails DUPLICATE_REQUEST; re-enable it with {\"enabled\": true} afterwards; (4) open a FRESH request after the bindings, because a slate is fixed when its request opens: create a draft showcase_expense_report, add one showcase_expense_line of 6000 (total_amount >= 5000, the committee threshold), then PATCH the report's status to 'submitted'. Use a second fresh request the same way for the veto case" + "RUNTIME FIXTURE (admin API only, stock boot, nothing seeded or committed; proved by run #21845) — apply it before the quorum steps, in this order: (1) bind a second committee position holder: as the admin, POST /api/v1/data/sys_user_position {user_id: , position: 'finance', organization_id: } — admin + Ada is a real 2-of-2 slate; (2) for the full 2-of-3, invite + sign up a third user and bind them to position 'legal' the same way. On a boot an earlier item already used, first list sys_user_position for positions finance and legal and unbind any other member a previous item bound there (e.g. approvals.account-app-entry's runtime persona, which holds finance) — a slate is every holder, so a leftover binding makes it four distinct people and the 2-of-3 never forms; prove each persona with GET /api/v1/auth/get-session before it acts; (3) DISABLE the competing per-group sign-off flow for the run: POST /api/v1/automation/showcase_expense_signoff/toggle {\"enabled\": false} — it has no amount ceiling and opens its request first on any >= 5000 report, so without this the quorum flow's open fails DUPLICATE_REQUEST; re-enable it with {\"enabled\": true} afterwards; (4) open a FRESH request after the bindings, because a slate is fixed when its request opens: create a draft showcase_expense_report, add one showcase_expense_line of 6000 (total_amount >= 5000, the committee threshold), then PATCH the report's status to 'submitted'. Use a second fresh request the same way for the veto case" ], "knownGaps": [ "the FLOW half of the fixture is real and seeded — showcase_committee_quorum (2-of-3 quorum over positions manager/finance/legal, examples/app-showcase/src/automation/flows/index.ts) is launched on EXP-DEMO by seed-approval-demo.ts. What still collapses the slate is POSITION ASSIGNMENT, not a missing flow or a pending design call: the admin holds all three committee positions (ADMIN_APPROVAL_POSITIONS = manager/finance/legal/exec, seed-approval-demo.ts) and Ada Auditor deliberately holds ONLY auditor, so all three slots resolve to one person and the runtime clamps 2-of-3 to 1-of-1", @@ -176,7 +182,13 @@ { "revision": 1, "date": "2026-08-07", "change": "initial import from #3358; carried the fixture blocker forward explicitly", "ref": "#3358" }, { "revision": 2, "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 3, "date": "2026-08-30", "change": "blocked ref re-worded to price the gap honestly: the old 'showcase design call pending' read as an open-ended product decision, but the quorum flow fixture ships (showcase_committee_quorum, 2-of-3 over manager/finance/legal) and Ada Auditor has been a real login since #9308 — the slate collapse survives only because seed-approval-demo.ts gives the admin all three committee positions and Ada only auditor. knownGaps now states the exact remaining cost (one seed line assigning Ada a committee position → 2-of-2 demonstrable; a third distinct holder for the full 2-of-3), verified against source including the per-group demo's non-collision (its finance group keys on position 'auditor')", "ref": "#sweep-2026-08-30" }, - { "revision": 4, "date": "2026-10-05", "change": "blocked lifted: run #21845 passed 2-of-2 and 2-of-3 on fresh requests under a runtime fixture (admin-API position bindings, a third signed-up holder, and the per-group sign-off flow disabled because it opens first on any >= 5000 report and the quorum open then fails DUPLICATE_REQUEST); the recipe is written into fixtures.requires and the seed-gap knownGap is marked CLOSED-for-runs, not deleted. Three clauses re-pointed from the same run: negative[0] now requires a NON-PRIVILEGED repeat actor, because an admin decision is the documented override (#3424, via_override:true) and finalizes by design; acceptance[2] asserts the run's paused → completed transition with the request rejected, since the reject edge is not observable (end nodes log no step); acceptance[3] names the English inbox tab 'My Pending', and acceptance[4] plus the screenshot step read the drawer's 'Approvals — x of y' line as the tally, not the static '(2 of 3)' subtitle (card #21851 entries 1, 5, 6, 7)", "ref": "#21845" } + { "revision": 4, "date": "2026-10-05", "change": "blocked lifted: run #21845 passed 2-of-2 and 2-of-3 on fresh requests under a runtime fixture (admin-API position bindings, a third signed-up holder, and the per-group sign-off flow disabled because it opens first on any >= 5000 report and the quorum open then fails DUPLICATE_REQUEST); the recipe is written into fixtures.requires and the seed-gap knownGap is marked CLOSED-for-runs, not deleted. Three clauses re-pointed from the same run: negative[0] now requires a NON-PRIVILEGED repeat actor, because an admin decision is the documented override (#3424, via_override:true) and finalizes by design; acceptance[2] asserts the run's paused → completed transition with the request rejected, since the reject edge is not observable (end nodes log no step); acceptance[3] names the English inbox tab 'My Pending', and acceptance[4] plus the screenshot step read the drawer's 'Approvals — x of y' line as the tally, not the static '(2 of 3)' subtitle (card #21851 entries 1, 5, 6, 7)", "ref": "#21845" }, + { + "revision": 5, + "date": "2026-10-19", + "change": "fixtures.requires[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24542, approvals.quorum-m-of-n; wave anchor #24579 §3): in a shared boot an earlier item (approvals.account-app-entry's runtime persona) had bound another member to 'finance', so the committee slate resolved four distinct holders; the run deleted that binding before opening the quorum requests. The fixture now says to clear such bindings first. No clause changed", + "ref": "#PENDING-24579R2" + } ] }, { @@ -405,7 +417,7 @@ "title": "Every approval action executes its REST route and produces the expected state transition plus a timeline entry", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "mixed", "personas": ["dev admin (pending approver on the seeded requests; submitter of the invoice request)"], @@ -428,7 +440,7 @@ "resubmit: POST /:id/resubmit {comment} as the submitter — then re-read BOTH sides: GET /:id (the resubmitted request, which stays returned) and GET /api/v1/approvals/requests?status=pending filtered to the same object/record, which now carries a DIFFERENT id — the round-N+1 request the approval node minted on re-entry; then drive revise→resubmit→revise again and attempt a THIRD revise (maxRevisions: 2 — it must auto-reject)", "recall: raise one more pending request; POST /:id/recall as the submitter; re-read", "reassign: on a pending request POST /:id/reassign {to: , comment}; re-read pending_approvers", - "remind: POST /:id/remind on the admin-submitted invoice request; then POST it again immediately (throttle probe)", + "remind: POST /:id/remind on an admin-submitted PENDING request — the seeded invoice request while it is still pending, else any budget-approval request the admin raised (in a shared boot approvals.decision-only-via-service consumes the invoice one); then POST it again immediately (throttle probe)", "request-info + comment: POST /:id/request-info {comment} AS A PENDING APPROVER of the request (request-info is approver-only: once the admin has reassigned their own slot away in the reassign step, the admin's request-info answers 403 FORBIDDEN \"actor '…' is not a pending approver\" — drive it as the pending approver, e.g. the reassign target with their own token), and POST /:id/comment {comment}, which a non-approver admin may still post", "after EVERY action above: GET /:id and GET /:id/actions; record the status transition and the appended timeline row", "lock contrast: while a request from exec_review (lockRecord: true) is pending, PATCH the project record — refused; while parked at manager_review (lockRecord: false) the same PATCH succeeds", @@ -444,7 +456,7 @@ { "clause": "decisions finalize per the APPROVAL_STATUSES lifecycle: approve → approved, reject → rejected, and the parked flow run resumes down the matching branch label", "oracle": "api", - "verify": "request status transitions pending→approved / pending→rejected; the owning run transitions paused→completed with the approve/reject edge taken", + "verify": "request status transitions pending→approved / pending→rejected; the owning run transitions paused→completed. The approve/reject EDGE itself is not logged — end nodes log no step — so on showcase_budget_approval the branch is evidenced by the approve edge's non-end successor: manager_review's approve edge leads to the needs_exec decision node, whose step row is present after an approve and absent after a reject (manager_review's reject edge ends at the Rejected end) — examples/app-showcase/src/automation/flows/index.ts", "evidence": "before/after request + run reads" }, { @@ -521,7 +533,13 @@ { "revision": 2, "date": "2026-08-11", "change": "corrected the resubmit transition the item asserted: it does NOT move the original returned→pending — the approval node's re-entry mints a round-N+1 request while the original stays terminally returned (source-confirmed as by design). Split the old combined revise/resubmit clause into three (revise parks · resubmit mints · original stays returned) and sharpened the maxRevisions verify to name which request the auto-reject lands on. Fixes the sole reason #7517 scored this item PARTIAL", "ref": "#7530" }, { "revision": 3, "date": "2026-08-30", "change": "added the READ_BACK_FAILED clause (#12769, landed #13181 2026-08-29): all ten fresh! non-null assertions on the post-write echo are gone — deciding an org-less request (the schedule/api-trigger shape, #10131) as an org-scoped caller now answers a loud named refusal with the write recorded, never a malformed 200 with request:null. Fixture reality recorded in knownGaps: no seeded flow opens an org-less request on demand, so the live-HTTP leg is blocked(fixture) while the unit pin covers the mechanism", "ref": "#13181" }, { "revision": 4, "date": "2026-10-06", "change": "partial-coverage gap of QA run #21784 parked on #22017 (row 19). A10 was blocked(fixture): no org-less request can be provisioned, and even an api-hook approval is org-stamped. Disposition: pin. approval-service.test.ts (the #12769 describe) proves the clause's verdict at the service. `automated` names it for that clause, and the knownGap records the refuted api-trigger candidate, the one untried schedule candidate, and the wire-envelope half that stays unobserved on stock", "ref": "#22017" }, - { "revision": 5, "date": "2026-10-16", "change": "step 9 ran request-info + comment as the admin on a pending request, but request-info is approver-only (#23878 checklist-accuracy finding). Re-measured on main c91af8ed6: request-info by the admin while a pending approver → 200; after the admin reassigns their slot to Ada → the admin's request-info answers 403 FORBIDDEN \"actor … is not a pending approver\", the admin's comment still answers 200, and Ada's request-info answers 200. The step now names the pending approver as the request-info caller; the clauses are unchanged", "ref": "#24036" } + { "revision": 5, "date": "2026-10-16", "change": "step 9 ran request-info + comment as the admin on a pending request, but request-info is approver-only (#23878 checklist-accuracy finding). Re-measured on main c91af8ed6: request-info by the admin while a pending approver → 200; after the admin reassigns their slot to Ada → the admin's request-info answers 403 FORBIDDEN \"actor … is not a pending approver\", the admin's comment still answers 200, and Ada's request-info answers 200. The step now names the pending approver as the request-info caller; the clauses are unchanged", "ref": "#24036" }, + { + "revision": 6, + "date": "2026-10-19", + "change": "steps[7] and acceptance[1].verify corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24542, approvals.decision-action-matrix; wave anchor #24579 §3). steps[7] reminded 'the admin-submitted invoice request', which a shared boot's approvals.decision-only-via-service has already decided; any admin-submitted pending request serves, and the run used a budget request. acceptance[1] asked for 'the approve/reject edge taken', but end nodes log no step, so the run evidenced the reject by the absence of the approve edge's successor, the needs_exec decision node (examples/app-showcase/src/automation/flows/index.ts). The verify now names that discriminator. Clause text unchanged", + "ref": "#PENDING-24579R2" + } ] }, { @@ -917,7 +935,7 @@ "title": "A non-admin reaches the full Approvals Inbox from the account app's Approvals entry", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P0", "surface": "browser", "personas": ["a NON-ADMIN business user who is a pending approver and has no Setup access"], @@ -926,7 +944,7 @@ "requires": [ "a bundled console: packages/console/dist is absent in a fresh checkout — build it with `pnpm objectui:build` and confirm dist/.objectui-sha equals .objectui-sha before trusting anything in the browser (`pnpm check:console-sha` exits 0 when there is NO dist at all, so a green check does not prove a console is present)", "a non-admin sign-in account holding only business positions (showcase: contributor + finance) — the seeded personas (Ada Auditor, Mei Phone) ARE loginable since #9308 (showcase123, examples/app-showcase/src/security/demo-personas.ts) but hold the wrong positions (Mei none, Ada auditor only), so provision one at runtime and invite each one first — the stock showcase declares no auth.audience, so the invite_only default answers an uninvited POST /api/v1/auth/sign-up/email 403 SELF_REGISTRATION_CLOSED; an admin POST /api/v1/auth/organization/invite-member for the address admits the sign-up (or admin POST /api/v1/auth/admin/create-user, which answers mustChangePassword:true)", - "at least one pending request routed to that user (showcase: PATCH a draft invoice to status=sent + issued_on to launch showcase_invoice_signoff, whose finance/legal positions the persona holds). ⚠️ Launch a NEW one after the persona holds its positions: the sign-off requests seed-approval-demo.ts launches at boot were routed before the runtime persona held finance, so they never list it as a pending approver. Make that invoice one the persona OWNS — acceptance[5] needs a row whose record the persona can read" + "at least one pending request routed to that user (showcase: PATCH a draft invoice to status=sent + issued_on to launch showcase_invoice_signoff, whose finance/legal positions the persona holds). ⚠️ Launch a NEW one after the persona holds its positions: the sign-off requests seed-approval-demo.ts launches at boot were routed before the runtime persona held finance, so they never list it as a pending approver. Make that invoice one the persona OWNS — acceptance[5] needs a row whose record the persona can read. ⚠️ Ownership here is the TEXT field `owner`, not owner_id: the contributor RLS invoice_own_rows reads `owner == current_user.email` (examples/app-showcase/src/security/permission-sets.ts), so an invoice the persona creates gets owner_id = the persona and `owner` null, and its own creator reads it 404 until an admin PATCHes `owner` to the persona's email" ] }, "steps": [ @@ -979,7 +997,7 @@ { "clause": "zh-CN wording is consistent: the nav entry reads 待我审批 and the inbox's tab / status vocabulary reads 待我审批 / 待审批", "oracle": "screenshot", - "verify": "set the console language to 中文(中国) — code zh-CN, the language the showcase ships; the picker offers no bare zh entry here — and screenshot the nav entry and the inbox; nav label 待我审批, tab 待我审批, status badge 待审批", + "verify": "set the console language to 中文(中国) through the avatar (user) menu's Preferences › Language row — not a top-bar globe (objectui packages/app-shell/src/layout/AppHeader.tsx at the console pin) — code zh-CN, the language the showcase ships; the picker offers no bare zh entry here — and screenshot the nav entry and the inbox; nav label 待我审批, tab 待我审批, status badge 待审批", "evidence": "zh screenshots of nav + inbox" } ], @@ -996,7 +1014,13 @@ ], "history": [ { "revision": 1, "date": "2026-08-10", "change": "initial — the account-app entry had no checklist coverage; authored from the #7331 browser run that first drove it end to end against a dist built at pin 8aad9fd50b16", "ref": "#7331" }, - { "revision": 2, "date": "2026-10-01", "change": "checklist-accuracy findings of QA run #21056: acceptance[6] names the zh-CN option (中文(中国)) — no bare zh exists; acceptance[1] gives the tab labels in both locales; acceptance[5] needs a row the persona can read, since unreadable rows render no link; fixtures.requires asks for a request launched after the persona holds finance (the boot-seeded ones were routed before). Also corrected while grounding: the seeded personas are loginable since #9308 — they hold the wrong positions, which is the real reason to provision one, invite-first under invite_only", "ref": "#21060" } + { "revision": 2, "date": "2026-10-01", "change": "checklist-accuracy findings of QA run #21056: acceptance[6] names the zh-CN option (中文(中国)) — no bare zh exists; acceptance[1] gives the tab labels in both locales; acceptance[5] needs a row the persona can read, since unreadable rows render no link; fixtures.requires asks for a request launched after the persona holds finance (the boot-seeded ones were routed before). Also corrected while grounding: the seeded personas are loginable since #9308 — they hold the wrong positions, which is the real reason to provision one, invite-first under invite_only", "ref": "#21060" }, + { + "revision": 3, + "date": "2026-10-19", + "change": "fixtures.requires[2] and acceptance[6].verify corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24542, approvals.account-app-entry; wave anchor #24579 §3). 'Make that invoice one the persona OWNS' did not say which field owns it: showcase_invoice's contributor RLS reads the text field `owner` against current_user.email (invoice_own_rows), so a persona-created invoice is unreadable to its creator until an admin sets `owner`. acceptance[6] did not say where the language switch is: at the console pin 4016370a it is the avatar menu's Preferences › Language row (AppHeader.tsx). No clause text changed", + "ref": "#PENDING-24579R2" + } ] }, { @@ -1215,7 +1239,7 @@ "title": "Every approver type resolves the slate it declares — department includes all descendants, deprecated role warns and resolves, and a slate with no concrete person takes the declared onEmptyApprovers fork, never a fake approval", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": ["dev admin (authors the scratch flows; org owner)", "Ada Auditor / Mei Phone (real logins since #9308 — directory rows the graph legs resolve to)"], @@ -1241,8 +1265,8 @@ "field leg: a multi-select user field naming two users — two slots, in order, never one 'u1,u2' literal", "manager leg: the submitter's manager_id resolves; user leg: the literal id resolves", "role leg: author {type:'role', value:'owner'} (it still parses for the deprecation window); read the boot/trigger log for the ADR-0090 D3 deprecation warn naming the canonical spelling, and assert the slate equals what {type:'org_membership_level', value:'owner'} resolves on the same org", - "queue leg: a stored flow carrying {type:'queue'} (author it directly through the metadata channel if the designer refuses — the designer withholding it IS the xEnumDeprecated contract) — trigger: the slot resolves to nobody and the log carries the #3508 'not implemented — the slot resolves to nobody' warn; the slate then names no one, so the default admin_rescue opens the request on the organization's administrators (#22824) — pending_approvers holds the administrators' ids and NOT the literal queue: slot, which is kept as the abandoned slate in node_config_json.__resolvedFrom['admin_rescue:abandoned'] — and the request stays pending", - "empty-slate forks, one scratch flow per policy over an UNSTAFFED position: onEmptyApprovers absent/admin_rescue → the request opens on the request organization's ADMINISTRATORS as ordinary approvers (#22824): first read who they are — the organization's sys_member rows whose grants hold organization_admin or organization_admin_no_bypass (on stock showcase, the dev admin as the org owner) — then read the request: pending_approvers equals exactly those ids, the abandoned position:

literal and its reason sit in node_config_json.__resolvedFrom ('admin_rescue:abandoned', 'admin_rescue:reason'), GET /:id/actions carries one system comment (no actor) 'Routed to this organization's administrators because the approver slate named no one: position '

' has no holder.', the request stays pending, and the WARN '… resolved to no concrete approver (…) — opening on this organization's administrators per onEmptyApprovers: 'admin_rescue'' lands; 'fail' → the run fails with NO_APPROVERS and no request row; 'auto_approve' → NO request row, the run continues down the approve edge with output.autoApproved=true and the auto-approve warn lands; 'fallback' (fallbackApprovers naming a STAFFED target, e.g. org_membership_level 'owner') → the request opens on the fallback slate — pending_approvers holds the fallback's resolved ids, no literal slot — and the 'opening on the declared fallbackApprovers' warn lands; a fallback that itself resolves to nobody degrades to admin_rescue with its own 'resolved to nobody either' warn, and so opens on the organization's administrators too", + "queue leg: a stored flow carrying {type:'queue'} (author it directly through the metadata channel if the designer refuses — the designer withholding it IS the xEnumDeprecated contract) — trigger: the slot resolves to nobody and the log carries the #3508 'not implemented — the slot resolves to nobody' warn; the slate then names no one, so the default admin_rescue opens the request on the organization's administrators (#22824) — pending_approvers holds the administrators' ids and NOT the literal queue: slot, which is kept as the abandoned slate in node_config_json.__resolvedFrom['admin_rescue:abandoned'] (read node_config_json through GET /api/v1/data/sys_approval_request/ID as the admin — GET /api/v1/approvals/requests/:id projects no node_config_json, packages/plugins/plugin-approvals/src/approval-service.ts rowFromRequest) — and the request stays pending", + "empty-slate forks, one scratch flow per policy over an UNSTAFFED position: onEmptyApprovers absent/admin_rescue → the request opens on the request organization's ADMINISTRATORS as ordinary approvers (#22824): first read who they are — the organization's sys_member rows whose grants hold organization_admin or organization_admin_no_bypass (on stock showcase, the dev admin as the org owner) — then read the request: pending_approvers equals exactly those ids, the abandoned position:

literal and its reason sit in node_config_json.__resolvedFrom ('admin_rescue:abandoned', 'admin_rescue:reason') — on the data door's row (GET /api/v1/data/sys_approval_request/ID as the admin), since the approvals request read omits node_config_json — GET /:id/actions carries one system comment (no actor) 'Routed to this organization's administrators because the approver slate named no one: position '

' has no holder.', the request stays pending, and the WARN '… resolved to no concrete approver (…) — opening on this organization's administrators per onEmptyApprovers: 'admin_rescue'' lands; 'fail' → the run fails with NO_APPROVERS and no request row; 'auto_approve' → NO request row, the run continues down the approve edge with output.autoApproved=true and the auto-approve warn lands; 'fallback' (fallbackApprovers naming a STAFFED target, e.g. org_membership_level 'owner') → the request opens on the fallback slate — pending_approvers holds the fallback's resolved ids, no literal slot — and the 'opening on the declared fallbackApprovers' warn lands; a fallback that itself resolves to nobody degrades to admin_rescue with its own 'resolved to nobody either' warn, and so opens on the organization's administrators too", "throughout: read the trigger-time log — every graph expansion to nobody must carry the #3807 'expanded to nobody' warn naming type and value" ], "acceptance": [ @@ -1267,19 +1291,19 @@ { "clause": "deprecated 'role' still parses for the deprecation window, warns with the canonical prescription, and resolves IDENTICALLY to org_membership_level", "oracle": "log", - "verify": "the ADR-0090 D3 warn ('approver type role is deprecated — author org_membership_level instead') appears at resolution, and the two spellings' slates are equal on the same org (canonicalApproverType maps role → org_membership_level before any lookup)", + "verify": "the ADR-0090 D3 deprecation warn appears at resolution, naming the type `role` and prescribing `org_membership_level` (match those two names, not the sentence — the live line quotes both and carries the ADR tag), and the two spellings' slates are equal on the same org (canonicalApproverType maps role → org_membership_level before any lookup)", "evidence": "the warn line + the two equal slates" }, { "clause": "the empty-slate fork honors the declared policy: admin_rescue (the default) opens the request on the request organization's ADMINISTRATORS as ordinary approvers, warns loudly and records why (#22824) — never on the slate that names no one while the organization has an administrator; 'fail' kills the node with NO_APPROVERS and opens nothing; 'auto_approve' opens nothing and resumes down the approve edge with output.autoApproved=true; 'fallback' opens the request on the declared fallbackApprovers (resolved by the same resolver as approvers) and warns that the fallback fired — a fallback that resolves to nobody too degrades to admin_rescue, and so to the administrators — 'empty' meaning no CONCRETE person, so a slate of only type:value literals takes the fork too", "oracle": "api", - "verify": "per policy: request-row presence/absence, run outcome, and the matching warn/error text ('resolved to no concrete approver…'); the admin_rescue request's pending_approvers holds exactly the organization's administrators (the TENANT_ADMIN rung of the request's organization, ADR-0095 D3 — read from its sys_member rows and their grants) and NOT the abandoned position:

literal, which is recorded under node_config_json.__resolvedFrom['admin_rescue:abandoned'] beside 'admin_rescue:reason'; its actions carry the one system comment 'Routed to this organization's administrators because the approver slate named no one: …'; its WARN reads '… opening on this organization's administrators per onEmptyApprovers: 'admin_rescue' …'; it stays pending, never finalized (approval-service.ts openNodeRequest's empty-slate fork and organizationAdministrators; pinned by admin-rescue-named-slate.integration.test.ts); the fallback request's pending_approvers holds the fallback slate, never the abandoned literal slot", + "verify": "per policy: request-row presence/absence, run outcome, and the matching warn/error text ('resolved to no concrete approver…'); the admin_rescue request's pending_approvers holds exactly the organization's administrators (the TENANT_ADMIN rung of the request's organization, ADR-0095 D3 — read from its sys_member rows and their grants) and NOT the abandoned position:

literal, which is recorded under node_config_json.__resolvedFrom['admin_rescue:abandoned'] beside 'admin_rescue:reason' (read on GET /api/v1/data/sys_approval_request/ID as the admin — the approvals request read projects no node_config_json); for auto_approve, output.autoApproved is not surfaced by GET /api/v1/automation/NAME/runs/RUNID (QA run #24542: its steps carry no output and the run's output is {}), so prove the approve edge with a marker node on it (e.g. an update_record) and read the marker; its actions carry the one system comment 'Routed to this organization's administrators because the approver slate named no one: …'; its WARN reads '… opening on this organization's administrators per onEmptyApprovers: 'admin_rescue' …'; it stays pending, never finalized (approval-service.ts openNodeRequest's empty-slate fork and organizationAdministrators; pinned by admin-rescue-named-slate.integration.test.ts); the fallback request's pending_approvers holds the fallback slate, never the abandoned literal slot", "evidence": "per-policy request/run reads (pending_approvers, node_config_json.__resolvedFrom, the request's actions) + log lines + the administrator read" }, { "clause": "queue is a dead slot that stays LOUD, never a fake approval: it resolves to nobody and warns #3508; its slate naming no one, the default admin_rescue opens the request on the organization's administrators (#22824) — the literal queue: slot is the abandoned slate, never a pending approver — and the request waits for an administrator's decision rather than passing", "oracle": "log", - "verify": "the 'approver type 'queue' is not implemented — the slot resolves to nobody' warn (#3508, approval-service.ts resolveApproverSpec) + the request read: status still pending, pending_approvers equals the organization's administrators with no queue: entry, node_config_json.__resolvedFrom['admin_rescue:abandoned'] is ['queue:'], and the routed-to-administrators comment is on its actions", + "verify": "the 'approver type 'queue' is not implemented — the slot resolves to nobody' warn (#3508, approval-service.ts resolveApproverSpec) + the request read: status still pending, pending_approvers equals the organization's administrators with no queue: entry, node_config_json.__resolvedFrom['admin_rescue:abandoned'] is ['queue:'] (on the data door's row, as acceptance[4] reads it), and the routed-to-administrators comment is on its actions", "evidence": "warn line + request read + its actions" } ], @@ -1314,6 +1338,12 @@ "date": "2026-10-16", "change": "residual of the #23878 finding, settled now that #23965 closed: approver-bindings.flow.ts ships status 'obsolete' (PR #24008), not 'draft', so knownGaps[0] and the fixture line stop calling it a draft (re-measured on main c91af8ed6: a showcase_field_zoo update opens no admin_rescue request; three pending requests before and after). The scratch-flow door is re-pointed too (the approvals.ooo-delegation-reroute finding covers this item): POST /api/v1/automation registers a runtime flow on the stock boot, record-triggered on showcase_field_zoo, so no writable package is needed. Re-run of the door with the user leg: a flow routing {type:'user', value:} opened its request with pending_approvers [admin]", "ref": "#24036" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "steps[6]–[7] and acceptance[3]–[5] verify corrected — three checklist-accuracy findings of the 18.0 QA sweep (QA run #24542, approvals.approver-resolution-matrix; wave anchor #24579 §3). node_config_json, and so __resolvedFrom, is not on GET /api/v1/approvals/requests/:id — rowFromRequest in approval-service.ts projects no node_config_json — and the run read it through GET /api/v1/data/sys_approval_request/ID as the admin, so the steps and verifies now name that door. output.autoApproved is not surfaced by the automation runs read (no step output, run output {}), so the auto_approve leg proves the approve edge with a marker node, as the run did. acceptance[3]'s quoted warn was a paraphrase of the live line; the verify now matches the two names it carries instead of a sentence. No clause text changed", + "ref": "#PENDING-24579R2" } ] }, @@ -1475,7 +1505,7 @@ "title": "Approval requests on v18: the request record page serves record:approval_decision and a Reply action that lands in its timeline, `enable.approvalsVisibleToReaders` lets a plain record reader see the record's requests read-only (and only then), the generic /data door serves request rows by approvals visibility, and titles shown to a reader use only what that reader may read", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "mixed", "personas": [ @@ -1495,7 +1525,7 @@ }, "steps": [ "GET /api/v1/meta/page/sys_approval_request_detail and find record:approval_decision", - "as the pending approver POST /api/v1/approvals/requests/ID/comment {body}; read the timeline", + "as the pending approver POST /api/v1/approvals/requests/ID/comment {comment} (the key is `comment` — a {body} is refused 400 VALIDATION_FAILED, packages/plugins/plugin-approvals/src/approval-service.ts comment); read the timeline", "as the member (non-participant) GET /api/v1/data/sys_approval_request/ID and list with a total", "flip approvalsVisibleToReaders on the scratch object; as a plain reader list that record's requests; flip it off and repeat without restart" ], @@ -1537,7 +1567,7 @@ ], "automated": { "kind": "unit", - "ref": "packages/plugins/plugin-approvals/src/sys-approval-request-page.test.ts + packages/spec/src/ui/component-record-approval-decision-22472.test.ts + packages/plugins/plugin-approvals/src/record-reader-opt-in.integration.test.ts + packages/plugins/plugin-approvals/src/request-child-read-gate.integration.test.ts + packages/plugins/plugin-approvals/src/subject-title-reader-fls.integration.test.ts" + "ref": "packages/plugins/plugin-approvals/src/sys-approval-request-page.test.ts + packages/spec/src/ui/component-record-approval-decision-22472.test.ts + packages/plugins/plugin-approvals/src/record-reader-opt-in.integration.test.ts + packages/plugins/plugin-approvals/src/request-read-gate.integration.test.ts (acceptance[2]: the REQUEST object's own generic-door gate — a hidden request answers by id exactly as a missing one, 404, and list / count exclude it) + packages/plugins/plugin-approvals/src/request-child-read-gate.integration.test.ts (the sys_approval_action / approver-index CHILD tables only) + packages/plugins/plugin-approvals/src/subject-title-reader-fls.integration.test.ts" }, "source": [ "packages/plugins/plugin-approvals/src/sys-approval-request.page.ts", @@ -1551,6 +1581,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "steps[1] and automated.ref corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24542, approvals.request-page-reply-and-reader-visibility; wave anchor #24579 §3). The comment route's body key is `comment`, not `body`: {body} answers 400 VALIDATION_FAILED 'comment is required' (approval-service.ts comment). automated.ref named request-child-read-gate.integration.test.ts, which covers only the child tables, and omitted request-read-gate.integration.test.ts, the pin of the request object's own generic-door gate that acceptance[2] asserts. That clause's live leg is unreachable on stock showcase (no member reads sys_approval_request), a fixture gap left on #24579 §4. No clause changed", + "ref": "#PENDING-24579R2" } ] }, From d5dd7edb0bf371f03e7f28542b97f59931620d6b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:10:35 +0000 Subject: [PATCH 6/7] docs(qa): re-point expression-editors' RLS clauses to the engine's error tier, and fold a consumed changeset path into prose studio-authoring.expression-editors acceptance[2] and [4] encoded the editor's warning tier for a non-lowerable RLS read predicate; the engine refuses it at error tier (rls-predicate-unenforceable, ADR-0056 D4) and the item's own negative[0] fails any divergence. Run records: QA run #24604 (ba4afcc2) and #24571 (5b14b7f3). cli.organization-ownership-ceremony: the source path entry .changeset/pre/15212-180-guards-fresh-single-only.md dangles once 18.0.0 is versioned; it joins the item's prose list of consumed v18 changesets. A citation fold, so no revision. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/cli.json | 3 +-- .../areas/studio-authoring.json | 27 ++++++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index 4ac65e73ee..e871c5652a 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -1897,11 +1897,10 @@ "source": [ "packages/cli/src/commands/migrate/organization-ownership.ts", "packages/cli/src/commands/migrate/organization-ownership.ts#ORGANIZATION_OWNERSHIP_UPGRADE_UNAVAILABLE", - ".changeset/pre/15212-180-guards-fresh-single-only.md", "packages/spec/src/system/fate-supplement.zod.ts#OrganizationOwnershipSupplementSchema", "packages/cli/src/utils/organization-ownership-drop-gate.ts", "docs/adr/0131-total-organization-ownership-no-null-organization-id.md", - "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22617-cli-organization-ownership-plan, 22937-cli-ceremony-promotions, 22937-cli-organization-ownership-apply, 22937-core-email-template-row-definition, 22937-core-row-definition-mappers, 15211-ceremony-metadata-promotion, 15211-cli-apply-reviewed-plan, 23280-host-fate-supplement, 23400-ownership-occupancy-gate, 23429-sys-metadata-family-organization-column-retired, 23558-ceremony-dialect-resolver, 23684-drop-gate-host-tables, 23281-comment-reactions-upgrade-conversion" + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22617-cli-organization-ownership-plan, 22937-cli-ceremony-promotions, 22937-cli-organization-ownership-apply, 22937-core-email-template-row-definition, 22937-core-row-definition-mappers, 15211-ceremony-metadata-promotion, 15211-cli-apply-reviewed-plan, 23280-host-fate-supplement, 23400-ownership-occupancy-gate, 23429-sys-metadata-family-organization-column-retired, 23558-ceremony-dialect-resolver, 23684-drop-gate-host-tables, 23281-comment-reactions-upgrade-conversion, 15212-180-guards-fresh-single-only" ], "history": [ { diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index 2d581013ba..c28d272495 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -635,7 +635,7 @@ "title": "The Studio CEL editors reach the SAME verdict as the engine: formula result-type inference, previous. completion in conditional rules, and RLS lint + test-run — the editor UI, not a second grammar", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": ["admin"], @@ -649,15 +649,16 @@ "knownGaps": [ "the CEL editors lazy-load @objectstack/formula and FEATURE-DETECT every entry point (celAuthoring.ts) — a missing/older engine degrades to no lint / no suggestions / test-run 'unavailable', never an exception. If the vendored bundle lacks the engine, record the affected clauses blocked(dependency) and verify the graceful-degradation clause instead", "AUTOMATION IS PINNED IN THE objectui REPO, NOT HERE — `automated.ref` names objectui specs exclusively, so from this checkout the item is neither runnable nor pin-evidenced (no console bundle is built here and none of those specs exist here). Drive it by hand in the browser and score it as a manual run, OR run the pin in an objectui checkout and cite the output naming the revision (17.1.0 sweep subject: 9a3daf8d37ad). ⛔ Never score it as covered on the strength of the `automated` field alone — an unrun pin is a claim, not evidence. Full protocol: RUNNER.md, the objectui-pinned-automation standing fact.", - "acceptance[7] (graceful degradation) is PIN-ONLY BY DESIGN: its verify forbids hand-breaking the bundle. The environment it needs is an objectui checkout at the .objectui-sha revision with its install done, to run packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts (describe 'celAuthoring · graceful degradation', present at objectui 0abd4f9f8769). QA run #21784 had no runnable checkout and recorded blocked(environment) (parked on #22017 row 16), which is the right verdict without one: never a hand-driven verdict, never inferred from the source. CI in this repository does not run objectui's suites; objectui's own CI does, on objectui PRs" + "acceptance[7] (graceful degradation) is PIN-ONLY BY DESIGN: its verify forbids hand-breaking the bundle. The environment it needs is an objectui checkout at the .objectui-sha revision with its install done, to run packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts (describe 'celAuthoring · graceful degradation', present at objectui 0abd4f9f8769). QA run #21784 had no runnable checkout and recorded blocked(environment) (parked on #22017 row 16), which is the right verdict without one: never a hand-driven verdict, never inferred from the source. CI in this repository does not run objectui's suites; objectui's own CI does, on objectui PRs", + "acceptance[2] and acceptance[4] score the editor against the ENGINE's tier for a non-lowerable RLS read predicate, which is ERROR (rls-predicate-unenforceable, fail-closed). At the console pin 4016370a the editor still gives it severity 'warning' with fail-open wording (objectui packages/app-shell/src/views/metadata-admin/celAuthoring.ts lintCelPredicate) and autosaves it — the product defect objectstack-ai/objectui#13061 (QA run #24604 negative[0], rule-7 verified). Until that lands, those two clauses FAIL as written: score them fail with that card cited, never 'pass as written'" ] }, "steps": [ - "ground the engine truth FIRST (rule 6 parity): for each probe expression, capture the canonical verdict from the engine — via MCP validate_expression (site: formula / flow_condition) or `objectstack build` — so the editor is judged against server truth. The ENGINE itself is pinned by ai.mcp-validate-expression / api-backend.formula-gates; this item only checks the EDITOR agrees", + "ground the engine truth FIRST (rule 6 parity): for each probe expression, capture the canonical verdict from the engine — via MCP validate_expression (site: formula / flow_condition) or `objectstack build` — so the editor is judged against server truth. validate_expression has NO RLS site (formula | validation | flow_condition | template), so the RLS probes' engine verdict is the author-time gate `os build` and publish run: packages/lint/src/validate-rls-predicate-enforceability.ts (rls-predicate-unenforceable / rls-predicate-unparseable, both error tier). The ENGINE itself is pinned by ai.mcp-validate-expression / api-backend.formula-gates; this item only checks the EDITOR agrees", "F1 formula result-type: open the object field designer (ObjectFieldInspector) on a formula field (type=formula, role=value, scope=record), TYPE a proven-number expression (record.est_hours * 1.1, or record.quantity * record.unit_price) and, separately, one cel-js cannot prove (record.a + record.b); after the 250ms debounce, screenshot the inferred-result-type (Σ) affordance for each", "type an unknown field (record.est_hourz) and a bare field ref in record scope (est_hours, no record. prefix); capture the inline error under the field (aria-invalid, destructive border) and its record. fix text", "F2 conditional-rule editor: open the same field's visibleWhen editor (scope=record, roots record/previous/parent); type `previous.` and then `record.` and capture the autocomplete listbox — it must offer the object's field catalog; type `current_user.` and confirm it does NOT list this object's fields; accept a suggestion and confirm the insertion", - "F4 RLS editor: open the permission set's Advanced Facets RLS policy (PermissionAdvancedFacets), enter a USING predicate that is NOT a simple field comparison (blast-radius) and one with a parse fault; confirm the non-pushdownable filter raises a WARNING (Save stays enabled) while the parse fault raises a blocking ERROR that disables Save (celErrorCount>0 → title perm.cel.saveBlocked)", + "F4 RLS editor: open the permission set's Advanced Facets RLS policy (PermissionAdvancedFacets), enter a USING predicate that is NOT a simple field comparison (blast-radius) and one with a parse fault; confirm BOTH raise a blocking ERROR: the non-pushdownable filter at the engine's tier — rls-predicate-unenforceable refuses it at build and publish (ADR-0056 D4) because the runtime drops the policy and fails CLOSED, so it grants no access — and the parse fault as a parse error; with an error present the save is held (celErrorCount>0): the environment door's Save is disabled with title perm.cel.saveBlocked, and the Studio package door, which autosaves and has no Save button, shows the perm-autosave-held status and issues no PUT", "RLS test-run: open the CelTestRunDialog ('Test this policy against a sample record'), supply a sample record + current_user, Run; capture allow (shield-check) / deny (shield-x) / a non-boolean value (non-bool smell) across three sample+predicate pairs", "parity cross-check: for the SAME expressions, confirm the editor's verdict tier (error/warning/ok, inferred type, allow/deny) equals the engine verdict captured in step 1", "persistence round-trip (writable package): Save the edited formula field → GET /api/v1/meta/object/ shows Field.returnType stamped from the inferred type; Save the RLS policy → GET /api/v1/meta/permission/ carries the USING/CHECK predicate" @@ -676,9 +677,9 @@ "evidence": "the meta read" }, { - "clause": "the editor's lint TIERS equal the canonical engine's — an unknown field and a bare field ref in record scope are ERRORS (with the record. fix, blocking Save), a non-pushdownable USING read filter is a WARNING; the editor calls the SAME @objectstack/formula validateExpression the server and validate_expression use (ADR-0032), so the verdict is identical, not a second grammar", + "clause": "the editor's lint TIERS equal the canonical engine's — an unknown field and a bare field ref in record scope are ERRORS (with the record. fix, blocking Save), and a non-pushdownable (non-lowerable) USING read filter is an ERROR too: the engine refuses it at build and publish (rls-predicate-unenforceable, ADR-0056 D4) because the runtime drops the policy and fails CLOSED. The editor calls the SAME @objectstack/formula validateExpression the server and validate_expression use (ADR-0032), so the verdict is identical, not a second grammar", "oracle": "api", - "verify": "for each probe, the editor's inline finding severity matches validate_expression / build for the same expression+site (error vs warning vs ok)", + "verify": "for each probe, the editor's inline finding severity matches the engine for the same expression+site (error vs warning vs ok): validate_expression / build for the formula and conditional-rule probes, and — validate_expression having no RLS site — the build/publish gate's rls-predicate-unenforceable (error) for the non-pushdownable USING probe (POST /api/v1/meta/permission/NAME/publish answers 422 naming the rule). An editor WARNING there is the tier divergence negative[0] scores as a FAIL", "evidence": "the paired editor screenshot + engine verdict, per probe" }, { @@ -688,10 +689,10 @@ "evidence": "the autocomplete DOM reads + the bare-ref error" }, { - "clause": "the RLS blast-radius advisory and the Save gate work together: a non-pushdownable USING read filter raises the fail-open WARNING (advisory, Save stays enabled), while a parse fault raises a blocking ERROR that disables Save (celErrorCount>0, title perm.cel.saveBlocked) — a malformed predicate cannot be persisted", + "clause": "the RLS lint and the save gate agree with the engine: a non-pushdownable USING read filter raises a blocking ERROR, not an advisory warning, because the engine refuses it (rls-predicate-unenforceable, ADR-0056 D4) and the runtime fails CLOSED — the policy is dropped and grants no access, it never widens access — and a parse fault raises a blocking ERROR too; with either error present the save is held (celErrorCount>0) — a predicate the engine refuses cannot be persisted", "oracle": "screenshot", - "verify": "the non-pushdownable filter shows the widen-access warning with Save still enabled; a parse-faulted predicate disables Save", - "evidence": "the two editor states" + "verify": "the non-pushdownable filter shows an error-tier finding and the save is held; a parse-faulted predicate is held the same way. Held means: on the environment door, Save disabled with title perm.cel.saveBlocked; on the Studio package door, which autosaves and has no Save button, the perm-autosave-held status with no PUT issued (only the package door is writable on stock showcase)", + "evidence": "the two held editor states + the network trace showing no save PUT" }, { "clause": "test-run dry-runs through the SAME engine the server uses: a predicate that should allow the sample returns allow, one that should deny returns deny, and a non-boolean returns the value/non-bool smell — matching a direct engine evaluate of the same predicate+scope", @@ -720,7 +721,7 @@ "variants": [ "formula field expression (role=value, scope=record) — inferred result type", "field conditional rule visibleWhen / readonlyWhen / requiredWhen (scope=record, roots record/previous/parent) — previous. completion", - "RLS USING read filter (pushdown / fail-open advisory)", + "RLS USING read filter (lowerable, or non-lowerable → ERROR and fail-CLOSED — never a fail-open advisory)", "RLS CHECK write filter" ], "traps": ["stale-console-bundle", "hydration-race", "automation-input"], @@ -748,6 +749,12 @@ "date": "2026-10-06", "change": "partial-coverage gap of QA run #21784 parked on #22017 (row 16). A8 was blocked(environment): its pin lives only in objectui and no checkout at the pin was runnable. Disposition: pin. The existing objectui pin (celAuthoring.test.ts, 'graceful degradation') is confirmed at the current .objectui-sha (0abd4f9f8769), and a knownGap names the environment that runs it and the verdict without it. No clause changed", "ref": "#22017" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "acceptance[2] (clause, verify) and acceptance[4] (clause, verify, evidence) re-pointed from the editor's WARNING tier to the engine's ERROR tier for a non-lowerable RLS read predicate — §1 of wave anchor #24579 (added by its round-2 comment 6272314386), from QA run #24604 at ba4afcc2 and QA run #24571 at 5b14b7f3. The two clauses required a non-pushdownable USING filter to be an advisory warning with Save enabled and fail-open framing. The engine refuses that predicate at build and publish (packages/lint/src/validate-rls-predicate-enforceability.ts, rls-predicate-unenforceable, error; ADR-0056 D4), and the runtime fails CLOSED (plugin-security's compileFilter answers the deny filter). So the item contradicted its own negative[0], which fails any editor verdict that differs from the engine, and #24604 scored acceptance[4] 'pass (as written)' beside that negative[0] FAIL. Riding the same re-point, so the item does not contradict it: steps[4] and variants[2] now expect the error tier; steps[0] names the build/publish gate as the RLS probes' engine truth (validate_expression has no RLS site); acceptance[4] names the Studio package door's autosave-held status beside the environment door's disabled Save (#24604: the package door has no Save button); and a knownGap records that the clauses fail until objectstack-ai/objectui#13061 lands", + "ref": "#PENDING-24579R2" } ] }, From d07b0fe237aaefe10bb5f1a141ed240edf8c11fc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:31:24 +0000 Subject: [PATCH 7/7] docs(qa): name the pull request in the 42 history rows this round added The history rows were committed with a placeholder ref because the pull request had no number before it was opened. This commit changes exactly those 42 strings and nothing else. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/access-security.json | 26 +++++++++---------- .../platform-checklist/areas/approvals.json | 12 ++++----- .../areas/attachments-storage.json | 4 +-- .../areas/identity-auth.json | 16 ++++++------ .../areas/platform-core.json | 24 ++++++++--------- .../areas/studio-authoring.json | 2 +- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index 9673e350aa..7fe8ece5b9 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -345,7 +345,7 @@ "revision": 3, "date": "2026-10-19", "change": "steps[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.record-access-explain; wave anchor #24579 §3): the step named 'the console's access explain panel' without saying where it opens; at the console pin 4016370a it is the 'Explain access' action in Studio's Access pillar header (/_console/studio/PKG/access), the route the run drove. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -822,7 +822,7 @@ "revision": 5, "date": "2026-10-19", "change": "steps[5] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.fls-mask-and-strip steps[5]; wave anchor #24579 §3): the step still told the run to author a scratch readable:false set, while fixtures.requires[2] and knownGaps[0] already record that the stock showcase_client_liaison set (examples/app-showcase/src/security/permission-sets.ts) is that grant. The step now grants the stock set to a fresh member who holds no showcase_contributor. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -926,7 +926,7 @@ "revision": 3, "date": "2026-10-19", "change": "steps[0] and automated.ref corrected — checklist-accuracy findings of the 18.0 QA sweep (QA run #24559, access-security.scope-depth-asymmetry; wave anchor #24579 §3; round 2's Tier 1 record #24598, which names this item among the refs that over-claim clause coverage). steps[0] POSTed a showcase_inquiry with `subject`; the object has no such field and requires name, email and message (examples/app-showcase/src/data/objects/inquiry.object.ts). The three dogfood pins exercise the business-unit depth axis (unit / unit_and_below / own_and_reports and the fallback profile), not the manager / executive / ops org-read and own-write asymmetry the clauses score, so automated.ref now says so and that no clause rests on them. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1150,7 +1150,7 @@ "revision": 3, "date": "2026-10-19", "change": "steps[6] and acceptance[4] (clause, verify, evidence) re-pointed — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.sharing-rules-widen acceptance[4]; wave anchor #24579 §3) with its oracle from round 2 (QA run #24601). §A13 asked that an admin edit to the SEEDED sys_sharing_rule row survive a restart; the seeder that copied declared rules into that table is retired (ADR-0131 D2, packages/plugins/plugin-sharing/src/declared-sharing-rules.ts) and GET /api/v1/data/sys_sharing_rule answers total 0, so there is no seeded row to edit. The administrator's durable act on a declared rule is now the activation switch (POST /api/v1/security/_activation/sharing_rule/NAME), and the clause asserts that switch across two restarts. QA run #24601 drove exactly this recipe at ba4afcc2 and passed. It measured that GET /api/v1/meta/sharing_rule/NAME keeps reading active: true while the ledger holds the rule off, so the oracle is the ledger row and the member's reads, never the metadata read. acceptance[0]–[3] are unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1284,7 +1284,7 @@ "revision": 6, "date": "2026-10-19", "change": "acceptance[6] (clause, verify, evidence) corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.record-share-grant-revoke acceptance[6]; wave anchor #24579 §3). The clause required source_id to be the rule's sys_sharing_rule ROW id and NOT its name; that holds only for an organization-authored rule. The rule it evaluates, share_key_account_qualified_contacts_with_managers, is DECLARED, and a declared rule is projected with id = name (sharing-rule-service.ts projectDeclaredRule, ADR-0131 D2), so its grants carry the name. The clause now asserts source_id equals the rule's id as evaluate reports it, and the verify names revokeRuleGrants, the current name of the purge it cited as purgeRuleGrants", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1386,7 +1386,7 @@ "revision": 7, "date": "2026-10-19", "change": "steps[7] and acceptance[5].verify clarified — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.audit-log-browser; wave anchor #24579 §3): the 405 body advertises allowed [get, list, aggregate, search, export], which a run reading 'get + list only' could take for a widened door. sys_audit_log still declares apiMethods ['get', 'list']; the other three are the read verbs list derives (packages/spec/src/data/api-derivation.ts). The append-only clause is unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -2226,7 +2226,7 @@ "revision": 4, "date": "2026-10-19", "change": "steps[0], [1] and [3] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.permission-matrix-edit-loop step 4; wave anchor #24579 §3): the DELETE probes named seeded task ids, but a permission set silent on writeScope is owner-only (plugin-security permission-evaluator.ts), so a seeded row owned by someone else stays refused after the delete bit flips ON. The probes now use rows M creates. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -2338,7 +2338,7 @@ "revision": 5, "date": "2026-10-19", "change": "steps[7] and acceptance[1].verify corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24559, access-security.capability-declaration-lifecycle; wave anchor #24579 §3). The located `capabilities.0.scope` refusal appears only for a raw capabilities entry: defineCapability parses its argument (packages/spec/src/security/capabilities.ts), so a malformed shape written through it throws at config load. The item stated no expectation for the admin on showcase_zoo_perm_held. The run measured the admin refused, and actionPermissionError has no administrator exemption (only a system context skips it), so the verify now says so. The run also measured DELETE /api/v1/meta/capability/NAME answering 200 while removing nothing; no clause asserts that door, and it is left to the wave anchor as an observation, not encoded here", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -3333,7 +3333,7 @@ "revision": 2, "date": "2026-10-19", "change": "fixtures.knownGaps[1] marked CLOSED — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.anonymous-floor-v18-doors; wave anchor #24579 §3): the gap said a non-POST /keys never reaches the floor on a stock boot and answers 405; since #23909 the dispatcher registers /keys for every verb (packages/runtime/src/dispatcher-plugin.ts) and the run measured anonymous GET /api/v1/keys → 401. No clause changed: acceptance[1] already demanded 401", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -3481,7 +3481,7 @@ "revision": 2, "date": "2026-10-19", "change": "acceptance[2] (clause, verify) and steps[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.meta-authoring-trail-and-audience-gates; wave anchor #24579 §3). The clause said every by-name door of the gated view answers 403 PERMISSION_DENIED. /history is an authoring-trail door: it asks mayReadAuthoringTrail (studio.access or manage_metadata, packages/rest/src/rest-server.ts) before anything, so a member without either is refused 403 FORBIDDEN first — acceptance[1]'s gate — and PERMISSION_DENIED is reached only by a member the trail admits. The run saw exactly that split. The other doors and clauses are unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -3723,7 +3723,7 @@ "revision": 2, "date": "2026-10-19", "change": "acceptance[2] (clause, verify) corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.grant-table-write-contract acceptance[2]; wave anchor #24579 §3): the clause required constraint.rule validity_window_order on the refusal, which is unobservable — a plain rule violation carries field, code and message and no constraint object (packages/objectql/src/validation/rule-validator.ts); only an unevaluable predicate attaches one. The clause now asserts what the rule's verdict carries and what the item's own pin sys-user-permission-set-window-order.test.ts asserts (valid_until · rule_violation, the authored message, no unevaluable reason). The walled leg acceptance[4] is §2's and is unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -3871,7 +3871,7 @@ "revision": 2, "date": "2026-10-19", "change": "automated.ref and steps[2] corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24559, access-security.deployment-tables-platform-only; wave anchor #24579 §3). acceptance[0] says the SEVEN tables are pinned, but the named pin, deployment-level-no-tenant-column.test.ts, lists five — the platform-objects ones. sys_flow_dispatch and sys_presence live in service-automation and service-realtime, and each has its own object pin asserting no tenant column and requiredPermissions manage_platform_settings (sys-flow-dispatch.object.test.ts, sys-presence.object.test.ts). automated.ref now names both, so the seven are pinned, and says which tables each file holds. steps[2]'s filter must be URL-encoded. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -3954,7 +3954,7 @@ "revision": 3, "date": "2026-10-19", "change": "automated.ref gains packages/runtime/src/domains/share-links-messages-audience.test.ts — the out-of-scope §3 note of #24622's round (wave anchor #24579 §3; QA run #24559). That file pins the parity acceptance[3] has asserted since revision 2: /messages hands resolveToken every audience input /resolve hands it, on the plugin mount and the dispatcher twin, so /messages serves exactly the callers /resolve serves. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] } diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 20f25564d9..26c2ad037a 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -95,7 +95,7 @@ "revision": 8, "date": "2026-10-19", "change": "acceptance[3].verify corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24542, approvals.per-group-signoff acceptance[3]; wave anchor #24579 §3): it asked for 'the approve branch taken' in the run read, but end nodes log no step and showcase_expense_signoff's approve edge ends at an end node, so no run read can show the branch. The verify now scores the run status transition and the request status, as approvals.quorum-m-of-n already words the same evidence. Clause text unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -187,7 +187,7 @@ "revision": 5, "date": "2026-10-19", "change": "fixtures.requires[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24542, approvals.quorum-m-of-n; wave anchor #24579 §3): in a shared boot an earlier item (approvals.account-app-entry's runtime persona) had bound another member to 'finance', so the committee slate resolved four distinct holders; the run deleted that binding before opening the quorum requests. The fixture now says to clear such bindings first. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -538,7 +538,7 @@ "revision": 6, "date": "2026-10-19", "change": "steps[7] and acceptance[1].verify corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24542, approvals.decision-action-matrix; wave anchor #24579 §3). steps[7] reminded 'the admin-submitted invoice request', which a shared boot's approvals.decision-only-via-service has already decided; any admin-submitted pending request serves, and the run used a budget request. acceptance[1] asked for 'the approve/reject edge taken', but end nodes log no step, so the run evidenced the reject by the absence of the approve edge's successor, the needs_exec decision node (examples/app-showcase/src/automation/flows/index.ts). The verify now names that discriminator. Clause text unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1019,7 +1019,7 @@ "revision": 3, "date": "2026-10-19", "change": "fixtures.requires[2] and acceptance[6].verify corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24542, approvals.account-app-entry; wave anchor #24579 §3). 'Make that invoice one the persona OWNS' did not say which field owns it: showcase_invoice's contributor RLS reads the text field `owner` against current_user.email (invoice_own_rows), so a persona-created invoice is unreadable to its creator until an admin sets `owner`. acceptance[6] did not say where the language switch is: at the console pin 4016370a it is the avatar menu's Preferences › Language row (AppHeader.tsx). No clause text changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1343,7 +1343,7 @@ "revision": 5, "date": "2026-10-19", "change": "steps[6]–[7] and acceptance[3]–[5] verify corrected — three checklist-accuracy findings of the 18.0 QA sweep (QA run #24542, approvals.approver-resolution-matrix; wave anchor #24579 §3). node_config_json, and so __resolvedFrom, is not on GET /api/v1/approvals/requests/:id — rowFromRequest in approval-service.ts projects no node_config_json — and the run read it through GET /api/v1/data/sys_approval_request/ID as the admin, so the steps and verifies now name that door. output.autoApproved is not surfaced by the automation runs read (no step output, run output {}), so the auto_approve leg proves the approve edge with a marker node, as the run did. acceptance[3]'s quoted warn was a paraphrase of the live line; the verify now matches the two names it carries instead of a sentence. No clause text changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1586,7 +1586,7 @@ "revision": 2, "date": "2026-10-19", "change": "steps[1] and automated.ref corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24542, approvals.request-page-reply-and-reader-visibility; wave anchor #24579 §3). The comment route's body key is `comment`, not `body`: {body} answers 400 VALIDATION_FAILED 'comment is required' (approval-service.ts comment). automated.ref named request-child-read-gate.integration.test.ts, which covers only the child tables, and omitted request-read-gate.integration.test.ts, the pin of the request object's own generic-door gate that acceptance[2] asserts. That clause's live leg is unreachable on stock showcase (no member reads sys_approval_request), a fixture gap left on #24579 §4. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, diff --git a/docs/qa/platform-checklist/areas/attachments-storage.json b/docs/qa/platform-checklist/areas/attachments-storage.json index 1ea43ce69b..5beb78f9cc 100644 --- a/docs/qa/platform-checklist/areas/attachments-storage.json +++ b/docs/qa/platform-checklist/areas/attachments-storage.json @@ -709,7 +709,7 @@ "revision": 2, "date": "2026-10-19", "change": "automated.ref and acceptance[0].verify corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24560, attachments-storage.orphan-tombstone-reap acceptance[0]; wave anchor #24579 §3): the verify said the dogfood block's cases map one-to-one onto the five variants, but two of them — the byte-delete-failure veto and the field file kept while the gate is closed — are pinned only by packages/services/service-storage/src/attachment-lifecycle.test.ts (createSysFileReapGuard), which automated.ref did not name. The run ran it singly and scored the clause on both files. automated.ref now names it and the verify says which file pins which variant. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1158,7 +1158,7 @@ "revision": 3, "date": "2026-10-19", "change": "acceptance[0].clause narrowed to what its pins prove and fixtures.knownGaps added — the out-of-scope §3 note of #24622's round (wave anchor #24579 §3; QA run #24560): none of the three automated.ref pins drives a chunked door after an organization switch, so the door-level 409 acceptance[3] asserts is unpinned. The only door-level pin of the shared guard (storage-routes.ts requireStartingOrganization) is the presigned commit door's, in error-envelope.conformance.test.ts, and chunk-part-record-concurrency.test.ts pins the store-level miss only. acceptance[0] no longer claims the org-switch guard. The knownGap records the unpinned leg, and points at §2 for its live half, which needs a second organization stock 18.0 refuses. acceptance[1]–[3] (#24622) are unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] } diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index ab92943cb0..d36214ba11 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -380,7 +380,7 @@ "revision": 5, "date": "2026-10-19", "change": "fixtures.requires[0]–[1] and automated.ref corrected — checklist-accuracy findings of the 18.0 QA sweep (QA run #24538, identity-auth.invitation-scope-gates; wave anchor #24579 §3). The item required a multi-org boot; every clause runs on the stock single-tenancy showcase's Default Organization, which is what its own pin boots (delegated-admin-invite.dogfood.test.ts: bootShowcase({}), reading the slug-default organization rather than minting one). automated.ref now says the pin's first case asserts the row and its role but not status pending, which the run proved live. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -701,7 +701,7 @@ "revision": 4, "date": "2026-10-19", "change": "source[2] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.api-key-ui-lifecycle; wave anchor #24579 §3): the citation said sys_api_key declares apiMethods ['get','list']; packages/platform-objects/src/identity/sys-api-key.object.ts declares ['get','list','update'], which is why the revoke/restore PATCH answers 200. Citation only; behaviour and clauses unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -791,7 +791,7 @@ "revision": 2, "date": "2026-10-19", "change": "fixtures.requires[2], knownGaps[0] and steps[7] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.self-service-profile-password; wave anchor #24579 §3): the knownGap said the set-initial-password branch needs an IdP, but a credential-less user is provisionable on stock showcase through POST /api/v1/auth/admin/import-users with passwordPolicy 'none' (admin-import-users.ts) and a session through POST /api/v1/auth/admin/impersonate-user — the recipe the run scored acceptance[5] pass on. The knownGap is marked CLOSED and the step drives the recipe. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -892,7 +892,7 @@ "revision": 5, "date": "2026-10-19", "change": "steps[5] and acceptance[3].clause corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.org-membership-team-management; wave anchor #24579 §3): after a rename the step expected the nav label to follow, but nav_organization's label is the static 'Organization' (setup-nav.contributions.ts); what follows is the organization record page's title, which the run observed. Not a loosening: the persisted name and the page title must both show the rename", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1057,7 +1057,7 @@ "revision": 3, "date": "2026-10-19", "change": "acceptance[0].oracle 'log' → 'api' — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.self-service-password-reset acceptance[0]; wave anchor #24579 §3): the clause's verify and evidence already read the sys_email row the request wrote, an API read, not a log line; the oracle label now names what the run consults. Clause text, verify and evidence unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1887,7 +1887,7 @@ "revision": 5, "date": "2026-10-19", "change": "steps[0] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.self-signup-gate; wave anchor #24579 §3): the step quoted the by-invitation notice as 'Self-registration is not open on this environment…'; the pinned console (objectui 4016370a, RegisterPage.tsx) renders '…not open on this system…'. The step no longer pins the sentence; the data-testid and the absent form are the oracle. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -2276,7 +2276,7 @@ "revision": 2, "date": "2026-10-19", "change": "fixtures.requires[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.auth-gate-held-session-raw-host-families; wave anchor #24579 §3): the MFA hold appeared ~70 s after require_mfa was switched on, because plugin-auth reads organization MFA through a 60 s-TTL cache refreshed in the background (auth-manager.ts refreshOrgMfaCacheIfStale). A runner probing at once sees 200 and could score a false FAIL; the fixture now says to poll. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -2495,7 +2495,7 @@ "revision": 2, "date": "2026-10-19", "change": "steps[0] and acceptance[1].verify corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.default-organization-boot-invariant; wave anchor #24579 §3): at os dev's default log level the boot prints no Default-Organization line, so 'read the boot log ordering' had nothing to read — ensure-default-organization.ts logs its success lines at info. The step now names --log-level info and the created_at ordering the run used instead. No clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 9efb434015..fca23f6437 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -630,7 +630,7 @@ "revision": 7, "date": "2026-10-19", "change": "steps[2] and steps[5] corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.builtin-apps-nav-render; wave anchor #24579 §3). steps[5] said 45 Setup destinations; the run's GET /api/v1/meta/app payload at 5b14b7f3 served 42, which matches the nav source minus its gates, and no nav source changed between 5b14b7f3 and this edit's base (the count was #23803 item 4's to re-measure, and #23803 is closed). steps[2] now has the walk expand the groups that render collapsed (Advanced and Developer declare expanded: false in setup.app.ts / account.app.ts; the run also found Configuration collapsed) before it navigates. No acceptance clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -889,7 +889,7 @@ "revision": 2, "date": "2026-10-19", "change": "steps[0] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.package-lifecycle-enable-disable; wave anchor #24579 §3, and QA run #24571 measured the same gate on a runtime object): the probe object needs `sharingModel` and the probe view its `viewKind` before publish accepts them — the draft PUT answers 200 and publish then refuses (security-owd-unset is error-tier in packages/lint/src/validate-security-posture.ts; the view overlay requires object + viewKind in packages/spec/src/ui/view.zod.ts). No acceptance clause changed; acceptance[5]'s FAIL is the objectui defect objectstack-ai/objectui#13052 and is untouched", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -989,7 +989,7 @@ "revision": 3, "date": "2026-10-19", "change": "steps[0], steps[7], acceptance[0].verify and acceptance[5] (clause, verify) corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.notification-center; wave anchor #24579 §3). At the console pin 4016370a the bell does not poll GET /api/v1/notifications?view=mine: its feed reads sys_inbox_message joined with sys_notification_receipt through the data API (objectui sharedUserFeeds.ts), which is the payload the run counted the badge from. The Approvals tab shows a pending count and a 'View approvals' link, not a list of requests (InboxPopover.tsx); the run scored acceptance[5] on the count match. Mark-read still POSTs /api/v1/notifications/read[/all] (useInboxBell.ts), and acceptance[6]'s post-reload GET /api/v1/notifications is a direct read of the same rows, so those clauses are unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1296,7 +1296,7 @@ "revision": 2, "date": "2026-10-19", "change": "fixtures.knownGaps[0] and acceptance[3] (clause, verify) corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.docs-audience-gate; wave anchor #24579 §3). knownGaps[0] said stock showcase authors neither a public nor a permissionSet book; it ships the public `showcase_manual` (examples/app-showcase/src/system/books/index.ts), so only the permissionSet half is a fixture gap. acceptance[3] said a lesser persona's tree is a subset of admin's; audienceAllows (packages/spec/src/system/book.zod.ts) admits a permissionSet doc for holders only and has no admin bypass, so for those books the run's admin non-holder saw less than a holder — the subset relation is holder vs non-holder. acceptance[2]'s unknown-shape half (unreachable live, no pin) is a fixture/pin gap left on #24579 §4", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1506,7 +1506,7 @@ "revision": 4, "date": "2026-10-19", "change": "fixtures.knownGaps[3] and negative[2] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.manifest-install-contract; wave anchor #24579 §3): both said NamespaceConflictError has no `code`, so its wire mapping was unpinned. Since commit df657d9df the class carries code NAMESPACE_CONFLICT and status 422 (packages/objectql/src/registry.ts), and the run observed 422 NAMESPACE_CONFLICT on POST /api/v1/packages. knownGaps[3] is now marked CLOSED with that reading, and negative[2] scores a 5xx as a FAIL instead of a finding to record. No acceptance clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1753,7 +1753,7 @@ "revision": 5, "date": "2026-10-19", "change": "the scope wall widened from four metadata_type values to five — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.activation-ledger-row-contract; wave anchor #24579 §3, which lists it under access-security). The security catalog activation door now also accepts :type sharing_rule (packages/runtime/src/domains/catalog-activation.ts CATALOG_ACTIVATION_SUBJECTS, ADR-0131 D6 regime C) and writes its row to this ledger through the same ObjectStoreMetadataActivationStore — a fifth writer the item's hand-enumerated variants did not carry, the case negative[4] says is a checklist defect. Title, steps[0] and [2], fixtures.requires[1], knownGaps[0], acceptance[4] (clause, verify) and negative[5] now name sharing_rule, variants gains it, and steps[0] flips the showcase's declared rule share_new_inquiries_with_field_ops. QA run #24601 drove that switch and read the row {metadata_type: sharing_rule, active: false}. No other clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -1963,7 +1963,7 @@ "revision": 5, "date": "2026-10-19", "change": "personas[1], steps[3] and fixtures.requires[0] corrected — three checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.marketplace-install-local-lifecycle; wave anchor #24579 §3). The plain member cannot be a fresh sign-up: the stock audience posture is invite_only (plugin-auth audience-posture.ts), so the run created the member through the admin create-user door. steps[3] read `install./examples` (a missing space). On showcase the cloud arm of boot A, and runtime-config on both boots, are mounted by the host config (examples/app-showcase/objectstack.config.ts), and Serve.planMarketplaceWiring wires only what the host did not — the matrix the clauses score is unchanged. No acceptance clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -2151,7 +2151,7 @@ "revision": 2, "date": "2026-10-19", "change": "steps[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.runtime-config-boot-read steps[1]; wave anchor #24579 §3): the branding block carries logoUrl, faviconUrl and brandColor only when they are configured — runtime-config-plugin.ts reads each from the plugin config or its env var and serialises an unset one as undefined, which JSON drops — so their absence on a stock boot is the contract, not a missing key. No acceptance clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -2347,7 +2347,7 @@ "revision": 2, "date": "2026-10-19", "change": "steps[5], acceptance[4] (clause, verify, evidence) and automated.ref corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.url-overlay-contract; wave anchor #24579 §3). The from trail no longer renders as a breadcrumb: since objectui#12461 the top bar draws no record crumb and RecordDetailView.tsx shows an inline back link to the immediate parent, each parent's link carrying the trail above it, so the clause now scores that chain. automated.ref described useUrlOverlay.test.tsx as pinning replace semantics; at the console pin it asserts param state only and nothing about history, which is why the run evidenced replace by hand. The push/replace clauses themselves are unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -2545,7 +2545,7 @@ "revision": 4, "date": "2026-10-19", "change": "acceptance[3] (clause, verify) corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.home-admin-cluster-links acceptance[3]; wave anchor #24579 §3): the member's Help menu does not list documentation entries only — at the console pin 4016370a it also lists Keyboard shortcuts and About (objectui packages/app-shell/src/layout/AppHeader.tsx). The clause's point is unchanged: no Administration entry anywhere in the member's chrome", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -3115,7 +3115,7 @@ "revision": 3, "date": "2026-10-19", "change": "steps[1], acceptance[1] (clause, verify) and acceptance[3].verify corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.v18-boot-refusals; wave anchor #24579 §3). steps[1] booted `os dev` with `services: {auth: false}`, which is not a stack key (packages/spec/src/stack.zod.ts), so the run recorded acceptance[3] blocked(fixture); a stock boot composes the security plugin only beside AuthPlugin (packages/core/src/stack-auth.ts resolvePlatformAuthComposition), so the step now names a scratch config that mounts SecurityPlugin itself with the auth tier off, marked as not yet replayed live, and acceptance[3] names its pin as the fallback. acceptance[1] said the boot exits with ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED on a log oracle; that literal is only the refusal error's code (OrganizationOwnershipBootRefusal) and serve.ts prints the message alone, so the log oracle now reads the named tables and command and the code is left to the pins. acceptance[2] and steps[0] (#24592) are unchanged", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] }, @@ -3353,7 +3353,7 @@ "revision": 2, "date": "2026-10-19", "change": "fixtures.requires[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.serve-composition-v18; wave anchor #24579 §3): examples/app-crm no longer declares a datasourceMapping (its config records why the rule was deleted, #4462), and no other example declares one, so the leg's scratch config authors the mapping itself, in the shape the item's own pin serve-datasource-mapping-before-sync.integration.test.ts boots. No acceptance clause changed", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] } diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index c28d272495..79bf042797 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -754,7 +754,7 @@ "revision": 4, "date": "2026-10-19", "change": "acceptance[2] (clause, verify) and acceptance[4] (clause, verify, evidence) re-pointed from the editor's WARNING tier to the engine's ERROR tier for a non-lowerable RLS read predicate — §1 of wave anchor #24579 (added by its round-2 comment 6272314386), from QA run #24604 at ba4afcc2 and QA run #24571 at 5b14b7f3. The two clauses required a non-pushdownable USING filter to be an advisory warning with Save enabled and fail-open framing. The engine refuses that predicate at build and publish (packages/lint/src/validate-rls-predicate-enforceability.ts, rls-predicate-unenforceable, error; ADR-0056 D4), and the runtime fails CLOSED (plugin-security's compileFilter answers the deny filter). So the item contradicted its own negative[0], which fails any editor verdict that differs from the engine, and #24604 scored acceptance[4] 'pass (as written)' beside that negative[0] FAIL. Riding the same re-point, so the item does not contradict it: steps[4] and variants[2] now expect the error tier; steps[0] names the build/publish gate as the RLS probes' engine truth (validate_expression has no RLS site); acceptance[4] names the Studio package door's autosave-held status beside the environment door's disabled Save (#24604: the package door has no Save button); and a knownGap records that the clauses fail until objectstack-ai/objectui#13061 lands", - "ref": "#PENDING-24579R2" + "ref": "#24632" } ] },