From 59aa497088d5faa14523362c9e30a160228b5030 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 22:03:13 +0000 Subject: [PATCH 1/5] =?UTF-8?q?docs(qa):=2018.0=20sweep=20anchor=20round?= =?UTF-8?q?=204=20=E2=80=94=20search's=20palette=20minLength=20and=20accel?= =?UTF-8?q?erator=20toggle,=20the=20pinyin=20roster=20oracle,=20global=20s?= =?UTF-8?q?earch's=20perObject=20cap=20and=20stock=20hidden=20value,=20and?= =?UTF-8?q?=20the=20contributor=20recipe's=20admin=20doors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checklist ledger only. Three search items take one revision each and the qa-contributor-bound-member recipe names the REST doors QA run #24574 measured; every finding re-read at objectstack f89d29e382 and, for the console, at the shipped pin 4016370af3c9. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/search.json | 52 +++++++++++--------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/docs/qa/platform-checklist/areas/search.json b/docs/qa/platform-checklist/areas/search.json index b04e5ad6610..3e6eb2d16a0 100644 --- a/docs/qa/platform-checklist/areas/search.json +++ b/docs/qa/platform-checklist/areas/search.json @@ -8,11 +8,11 @@ "qa-contributor-bound-member": { "title": "A CONTRIBUTOR-BOUND member — an identity that reads a strict SUBSET of showcase_invoice rows (invited sign-up + one sys_user_position row)", "why": "Both-sides RLS needs a persona for whom a specific row is invisible while the object itself is readable. ⚠️ Neither seeded demo persona is that persona, and reaching for them is the trap this recipe closes — measured on 17.1.0 (#10257): Mei (phone.demo@example.com) holds NO position, so showcase_contributor never merges and showcase_invoice answers PERMISSION_DENIED — the OBJECT gate, no rows at all, not a subset; Ada (auditor.demo@example.com) holds `auditor` → showcase_auditor, which carries `showcase_invoice: { allowRead: true, viewAllRecords: true }` — VAMA bypasses record scope entirely, so she sees all 12 — also not a subset. The persona that IS a subset is a member bound to the `contributor` position: showcase_contributor grants read/create/edit on showcase_invoice while the `invoice_own_rows` RLS narrows selects to `owner == current_user.email`, and none of the seeded invoices are owned by a fresh sign-up. Nothing here needs a seed change or a repo file — the whole gap was that the recipe existed only as prose on one item, which is why a runner reached for the demo personas instead and recorded blocked(fixture).", - "provenance": "run #7629 (framework, 2026-08-11) drove search.rls-both-personas to 4/4 PASS on exactly this persona — 3 invoices visible to the member vs 15 to admin. Promoted from prose on that one item to a replayable area recipe by the 17.1.0 sweep (#10236 B1); the Mei/Ada measurements above are from #10257. Step 1 re-pointed invite-first by #21060 after QA run #21056 met the invite_only default (403 SELF_REGISTRATION_CLOSED on an uninvited sign-up). Step 5's body gained the `owner` it always needed after QA run #23166 met an own-row invisible to its creator (#23169).", + "provenance": "run #7629 (framework, 2026-08-11) drove search.rls-both-personas to 4/4 PASS on exactly this persona — 3 invoices visible to the member vs 15 to admin. Promoted from prose on that one item to a replayable area recipe by the 17.1.0 sweep (#10236 B1); the Mei/Ada measurements above are from #10257. Step 1 re-pointed invite-first by #21060 after QA run #21056 met the invite_only default (403 SELF_REGISTRATION_CLOSED on an uninvited sign-up). Step 5's body gained the `owner` it always needed after QA run #23166 met an own-row invisible to its creator (#23169). Steps 2 and 3 name the admin REST doors QA run #24574 measured (an admin GET of sys_user by email and an admin POST of sys_user_position, the position as plain text), and step 5 names the invoice number field `name`, after that run met 400 INVALID_FIELD on `number` and #24593 met 400 on an OData `$filter` (wave anchor #24579, PR #PENDING-24579R4).", "app": "showcase", "requires": [ "a stock dev boot — the `contributor` position declares `showcase_contributor` in its `permissionSets` (examples/app-showcase/src/security/positions.ts), which the resolver reads by name (ADR-0131 D3/D4). ⛔ Do NOT hand-create a sys_position_permission_set row: no resolver reads it, so it proves nothing about the merge", - "system-context write access for the position binding (the sys_user_position insert) — the same pattern access-security.json's permission-set items already replay", + "an admin session for the position binding: the admin's POST /api/v1/data/sys_user_position is the insert (sys_user_position is admin-writable system data, packages/plugins/plugin-security/src/objects/sys-user-position.object.ts managedBy 'system-data'; QA run #24574 bound the member this way) — the same pattern access-security.json's permission-set items already replay", "an isolated boot (own port + file DB, dogfood §0); export OS_PORT= as well as passing -p (RUNNER.md's connector self-URL fact) so unrelated flow noise does not pollute the run", "an admin session to INVITE the step-1 address before step 1: POST /api/v1/auth/organization/invite-member {\"email\": \"\", \"role\": \"member\"}. The stock showcase declares no auth.audience, so its posture is the invite_only default, which answers an uninvited sign-up 403 SELF_REGISTRATION_CLOSED; a pending, unexpired sys_invitation row for the address is what admits it (an admin POST /api/v1/auth/admin/create-user is the other door, and answers mustChangePassword:true)" ], @@ -26,13 +26,13 @@ }, { "step": 2, - "call": "resolve the new principal's sys_user id with a system-context read of sys_user by email", + "call": "as admin, resolve the new principal's sys_user id: GET /api/v1/data/sys_user?email= (a bare query parameter is a field filter on the data list door). ⛔ Not an OData `$filter=email eq '…'`: the list door takes JSON filters only and answers that text 400 (metadata-protocol protocol.ts, 'must be valid JSON')", "expect": "one row. Needed because step 3 binds by user id, not by email.", "source": "the house pattern access-security.json's items already use ('fresh sign-ups whose sys_user ids are resolved system-side')" }, { "step": 3, - "call": "insert a sys_user_position row in system context binding that user id to the `contributor` position", + "call": "as admin, POST /api/v1/data/sys_user_position { user_id: , position: 'contributor', organization_id: } — `position` is the position's machine name as plain text; there is no sys_position object to point at (positions are metadata, plugin-security sys-user-position.object.ts)", "expect": "one row. This is THE step that makes the persona a subset reader: `contributor` resolves to showcase_contributor (auto-bound at boot, see requires), which grants showcase_invoice allowRead/allowCreate/allowEdit, and the RLS rule `invoice_own_rows` (using: `owner == current_user.email`, positions: ['contributor']) narrows every select to rows the caller owns.", "source": "examples/app-showcase/src/security/permission-sets.ts#ContributorPermissionSet — ContributorPermissionSet.objects.showcase_invoice and rowLevelSecurity['invoice_own_rows']; examples/app-showcase/src/security/positions.ts#contributor (the `contributor` position); bind-position-sets.ts for the position→set edge" }, @@ -45,7 +45,7 @@ { "step": 5, "call": "give the member a row of their OWN: as the member, POST /api/v1/data/showcase_invoice with `owner` set to the member's OWN email (the step-1 address) — contributors hold allowCreate", - "expect": "2xx, and that row IS visible to them on a re-read — turning the subset from empty into a proper non-empty one. This is what makes the restriction provably SUBTRACTIVE rather than a blackout; without it, zero-hits and no-access are indistinguishable. ⚠ The body MUST carry `owner: ''`: invoice_own_rows narrows SELECT to owner == current_user.email, and `owner` is a plain text field with no default (examples/app-showcase/src/data/objects/invoice.object.ts), so an invoice the member creates WITHOUT it lands 2xx and is then invisible to its own creator — reading exactly like a blackout (QA run #23166). Point the invoice's required `account` at a SEEDED account, never at one a consuming item later deletes: a required lookup turns set_null into restrict, so that delete answers 409 DELETE_RESTRICTED (records-forms.crud-roundtrip met it in QA run #21056).", + "expect": "2xx, and that row IS visible to them on a re-read — turning the subset from empty into a proper non-empty one. This is what makes the restriction provably SUBTRACTIVE rather than a blackout; without it, zero-hits and no-access are indistinguishable. ⚠ The body MUST carry `owner: ''`: invoice_own_rows narrows SELECT to owner == current_user.email, and `owner` is a plain text field with no default (examples/app-showcase/src/data/objects/invoice.object.ts), so an invoice the member creates WITHOUT it lands 2xx and is then invisible to its own creator — reading exactly like a blackout (QA run #23166). The invoice number is the required `name` field (there is no `number` field: a body naming `number` answers 400 INVALID_FIELD, QA run #24574); `status` may be omitted (its 'draft' option is default:true). Point the invoice's required `account` at a SEEDED account id (read one from GET /api/v1/data/showcase_account), never at one a consuming item later deletes: a required lookup turns set_null into restrict, so that delete answers 409 DELETE_RESTRICTED (records-forms.crud-roundtrip met it in QA run #21056).", "source": "ContributorPermissionSet.objects.showcase_invoice.allowCreate === true (permission-sets.ts); the seeded owner emails hold no credential, so borrowing a seeded owner is not an option — this is why the own-row is CREATED" } ], @@ -309,7 +309,7 @@ "title": "Pinyin recall is gated end-to-end by OS_SEARCH_PINYIN_ENABLED: on, latin pinyin (full + initials) hits CJK names; off, it does not — while CJK terms keep matching", "since": "v15.1", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": ["seeded admin (admin@objectos.ai)"], @@ -319,10 +319,10 @@ "seeded CJK rows: contact 张伟 (zhangwei@huaning.example) and account 华宁科技 — placed in the seed precisely so pinyin recall is demonstrable out of the box", "the showcase i18n config (supportedLocales includes zh-CN), which makes the flag default ON: unset OS_SEARCH_PINYIN_ENABLED + any zh-* locale → enabled, stamped into the env at serve boot", "an environment lever for the OFF side: restart the server with OS_SEARCH_PINYIN_ENABLED=false (an explicit value always beats the locale-derived default — truthy is exactly 1/true/on/yes; anything else explicit disables)", - "a readable boot log for BOTH boots (debug level) — the OFF-side oracle is the loaded-plugin roster, so the run needs the plugin count / `requires` list from each boot, not just the ON-side output" + "a readable boot log for BOTH boots (debug level) — the OFF-side oracle is the loaded-plugin roster, so the run needs the loaded-plugin roster from each boot — the banner's `Plugins: N loaded` line and the plugins it names. No boot prints the `requires` list itself (packages/cli/src/commands/serve.ts builds it from served capabilities and never logs it; the banner, packages/cli/src/utils/format.ts, prints the plugin count), not just the ON-side output" ], "knownGaps": [ - "the plugin's own inert line ('OS_SEARCH_PINYIN_ENABLED is off — inert') is UNOBTAINABLE in a CLI-served deployment and must not be used as the OFF-side oracle: the CLI gates one layer upstream and omits `pinyin-search` from `requires` when the flag is off, so the plugin is never loaded and never reaches its own inert branch. Run #7629 measured 47 loaded plugins with the flag off vs 48 with it on (51 vs 52 on main c91af8ed6 under #24036), and found ZERO occurrences of 'pinyin' in a 1.2 MB debug boot log. The clause below therefore reads the roster difference; the inert line is kept only as the alternative oracle for a host that loads the plugin unconditionally" + "the plugin's own inert line ('OS_SEARCH_PINYIN_ENABLED is off — inert') is UNOBTAINABLE in a CLI-served deployment and must not be used as the OFF-side oracle: the CLI gates one layer upstream and omits `pinyin-search` from `requires` when the flag is off, so the plugin is never loaded and never reaches its own inert branch. Run #7629 measured 47 loaded plugins with the flag off vs 48 with it on (51 vs 52 on main c91af8ed6 under #24036; 52 vs 53 at 5b14b7f3 under QA run #24574), and found ZERO occurrences of 'pinyin' in a 1.2 MB debug boot log. The clause below therefore reads the roster difference; the inert line is kept only as the alternative oracle for a host that loads the plugin unconditionally" ] }, "steps": [ @@ -332,7 +332,7 @@ "POST /api/v1/data/showcase_account/query { search: 'huaning' } and { search: 'hnkj' } — both must return 华宁科技", "confirm the seeded rows were companion-backfilled on this very boot (rows written by seeds BEFORE hook binding are reconciled by the kernel:bootstrapped backfill — no restart needed) by checking the boot log for the backfill, then verify in the browser that the list quick-search / ⌘K / lookup picker send the same $search and hit", "restart the server with OS_SEARCH_PINYIN_ENABLED=false and re-run the same queries", - "capture the OFF-side inertness from the plugin ROSTER, not from a plugin log line: record the loaded-plugin count and the `requires` list on both boots (the CLI omits `pinyin-search` when the flag is off) and grep the whole debug boot log for 'pinyin' — zero occurrences is the proof. Only if the host loaded the plugin anyway does the plugin's own inert line become the thing to capture" + "capture the OFF-side inertness from the plugin ROSTER, not from a plugin log line: record the loaded-plugin roster on both boots — the banner's `Plugins: N loaded` and the plugins it names; the CLI omits `pinyin-search` from `requires` when the flag is off, but prints no `requires` list, so the roster is the readable form and grep the whole debug boot log for 'pinyin' — zero occurrences is the proof. Only if the host loaded the plugin anyway does the plugin's own inert line become the thing to capture" ], "acceptance": [ { @@ -356,8 +356,8 @@ { "clause": "OFF is inert end-to-end, not half-disabled — and under CLI serving the gate sits one layer UPSTREAM of the plugin: with the flag off the CLI does not add `pinyin-search` to `requires`, so the plugin is never loaded (no hooks, no backfill, pinyin-pro never imported, and its own 'is off — inert' line never printed). A host that loads the plugin unconditionally takes the in-plugin inert branch instead. Either route satisfies the ADR-0049 no-half-state design: column provisioning and population share the SINGLE decision point resolveSearchPinyinEnabled", "oracle": "log", - "verify": "diff the two boots' loaded-plugin roster — the OFF boot is exactly one plugin short and `pinyin-search` is absent from `requires` (#7629 measured 47 vs 48; main c91af8ed6 measured 51 vs 52 — the absolute count moves with the plugin set, the one-plugin difference with PinyinSearchPlugin as the missing entry is the oracle) — and confirm a full-text grep of the OFF debug boot log yields ZERO 'pinyin' occurrences. Do NOT wait for the plugin's inert line: it cannot appear when the plugin was never loaded, and treating its absence as a failure is the trap this clause was rewritten to remove. If the roster shows the plugin WAS loaded, then the inert line is required instead. No backfill activity on either route", - "evidence": "both boots' plugin count / `requires` lines + the zero-hit grep over the OFF boot log (or, plugin-loaded hosts only, the inert line)" + "verify": "diff the two boots' loaded-plugin roster — the OFF boot is exactly one plugin short and PinyinSearchPlugin is the missing entry (the CLI left `pinyin-search` out of `requires`, which no boot prints — the roster is how it is read; #7629 measured 47 vs 48; main c91af8ed6 measured 51 vs 52; 5b14b7f3 measured 52 vs 53, QA run #24574 — the absolute count moves with the plugin set, the one-plugin difference with PinyinSearchPlugin as the missing entry is the oracle) — and confirm a full-text grep of the OFF debug boot log yields ZERO 'pinyin' occurrences. Do NOT wait for the plugin's inert line: it cannot appear when the plugin was never loaded, and treating its absence as a failure is the trap this clause was rewritten to remove. If the roster shows the plugin WAS loaded, then the inert line is required instead. No backfill activity on either route", + "evidence": "both boots' plugin roster lines (`Plugins: N loaded` and the named plugins) + the zero-hit grep over the OFF boot log (or, plugin-loaded hosts only, the inert line)" }, { "clause": "seeded (pre-hook) rows are recallable on the FIRST boot: the kernel:bootstrapped backfill reconciles rows written before/around hook binding, and existing deployments' stale rows after a restart (#3027 / 15.1 plan G9)", @@ -391,7 +391,8 @@ { "revision": 1, "date": "2026-08-07", "change": "new item: both sides of the pinyin flag with toggle semantics read from resolveSearchPinyinEnabled's source (explicit-wins + zh-locale default), recall variants pinned to the seeded CJK rows, backfill contract from the plugin source", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-11", "change": "the OFF-inertness clause now names an oracle that exists. It demanded a specific plugin log line ('OS_SEARCH_PINYIN_ENABLED is off — inert') which run #7629 proved unobtainable under CLI serving: the flag gates one layer upstream, the CLI omits pinyin-search from `requires`, the plugin is never loaded, and a 1.2 MB debug boot log contained zero occurrences of 'pinyin' (47 loaded plugins off vs 48 on). That is a STRONGER proof of inertness than the log line would have been, so the clause now reads the plugin roster / `requires` difference plus the zero-hit grep, and keeps the inert line only as the alternative oracle for a host that loads the plugin unconditionally — the run is told which route it is on by the roster itself. Step 7, the fixtures requirement and the plugin source citation follow; knownGaps records the unobtainability so the next author does not restore the log-line oracle. This was the item's ONLY partial in #7629 — the substance (both flag sides, per-variant recall, backfill) is unchanged", "ref": "#7647" }, { "revision": 3, "date": "2026-08-18", "change": "re-targeted clause 3's OFF-side expectation at the initials forms. It demanded that 'zhangwei' stop returning 张伟 with the flag off, but the seeded contact carries email zhangwei@huaning.example and email is searchable:true, so the term still matches through the source column — correct behaviour that the clause scores as the flag failing to disable. The initials (zw / hnkj / wf) have no source-column spelling and are the honest discriminator (#9467 CF-4)", "ref": "#9386" }, - { "revision": 4, "date": "2026-10-17", "change": "clause 2 named 'wf' as an account initials form, but 'wf' is the initials of the seeded CONTACT 王芳 (company 华宁科技); the clause now places 'wf' on showcase_contact and 'hnkj' on showcase_account, and its ⛔ note adds 'wangfang', which still matches 王芳 through her email with the flag off. The roster counts in clause 3 and the knownGap gain the current reading (51 vs 52) and clause 3 states that the one-plugin difference, not the absolute count, is the oracle. Re-run on main c91af8ed6: flag ON (stock, zh-CN default) contact zhangwei/zw/张 → 张伟, contact wf → 王芳, account huaning/hnkj/华 → 华宁科技; flag OFF (OS_SEARCH_PINYIN_ENABLED=false, fresh DB, debug log) contact zw → 0, wf → 0, 张 → 张伟, zhangwei → 张伟 and wangfang → 王芳 (email column), account hnkj → 0, huaning and 华 → 华宁科技; banner 'Plugins: 51 loaded' OFF vs 52 ON, the difference PinyinSearchPlugin, and zero 'pinyin' occurrences in the 589 KB OFF debug boot log", "ref": "#24036" } + { "revision": 4, "date": "2026-10-17", "change": "clause 2 named 'wf' as an account initials form, but 'wf' is the initials of the seeded CONTACT 王芳 (company 华宁科技); the clause now places 'wf' on showcase_contact and 'hnkj' on showcase_account, and its ⛔ note adds 'wangfang', which still matches 王芳 through her email with the flag off. The roster counts in clause 3 and the knownGap gain the current reading (51 vs 52) and clause 3 states that the one-plugin difference, not the absolute count, is the oracle. Re-run on main c91af8ed6: flag ON (stock, zh-CN default) contact zhangwei/zw/张 → 张伟, contact wf → 王芳, account huaning/hnkj/华 → 华宁科技; flag OFF (OS_SEARCH_PINYIN_ENABLED=false, fresh DB, debug log) contact zw → 0, wf → 0, 张 → 张伟, zhangwei → 张伟 and wangfang → 王芳 (email column), account hnkj → 0, huaning and 华 → 华宁科技; banner 'Plugins: 51 loaded' OFF vs 52 ON, the difference PinyinSearchPlugin, and zero 'pinyin' occurrences in the 589 KB OFF debug boot log", "ref": "#24036" }, + { "revision": 5, "date": "2026-10-19", "change": "fixtures.requires[3], fixtures.knownGaps[0], steps[6] and acceptance[3] (verify, evidence) corrected from QA run #24574 (18.0 sweep, 5b14b7f3; carried by #24593), re-read at objectstack f89d29e382. The item asked for each boot's `requires` list, which no boot prints: packages/cli/src/commands/serve.ts builds `requires` from the served capabilities and never logs it, and the banner (packages/cli/src/utils/format.ts) prints `Plugins: N loaded`. The roster is the readable form, and the one-plugin difference with PinyinSearchPlugin as the missing entry stays the oracle. The 5b14b7f3 count (52 vs 53) is recorded beside the older readings; the absolute count still moves with the plugin set. No clause substance changed. Wave anchor #24579 §3", "ref": "#PENDING-24579R4" } ] }, { @@ -472,7 +473,7 @@ "title": "The console global search — ⌘K palette, header Search button, and /search page — drives ONE path (GET /api/v1/search): hits group under object headings, RLS hides invisible rows, Enter opens the record, empty input shows recents", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": [ @@ -482,7 +483,7 @@ "fixtures": { "app": "showcase", "requires": [ - "the objectui console (app-shell) with CommandPaletteProvider — the ⌘K/Ctrl+K accelerator, the header Search button, and the ?palette=1/?cmdk=1 deep-link all drive the SAME idempotent open (ADR-0054 C1/C3)", + "the objectui console (app-shell) with CommandPaletteProvider — the ⌘K/Ctrl+K accelerator, the header Search button, and the ?palette=1/?cmdk=1 deep-link all open the SAME palette overlay (ADR-0054 C1/C3). The button, links, deep-links and programmatic callers share the idempotent openCommandPalette(); the keyboard accelerator TOGGLES by source design (objectui app-shell CommandPaletteProvider.tsx: \"the keydown handler toggles (close-on-repeat is a keyboard nicety)\"), so a repeated ⌘K closes the palette and that is not a FAIL", "the seeded CJK/pinyin rows 张伟 (zhangwei@huaning.example) and 华宁科技 (examples/app-showcase/src/data/seed/index.ts) so a CJK/pinyin query has a real hit; the search service (pinyin full-text plugin) installed so GET /api/v1/search resolves", "seeded invoices INV-1001..INV-1012 (12 rows, owner spread ada/linus/grace) and a signed-up member bound to the contributor position (sys_user_position) so the RLS-parity side has an invisible row (INV-1003, owned by linus) to prove absence — provisioned by this area's qa-contributor-bound-member recipe, exactly as in search.rls-both-personas. ⚠ Its step-5 own-row invoice must carry `owner` set to the member's email: invoice_own_rows reads owner == current_user.email and `owner` has no default, so a member-created invoice without it is invisible to its creator (QA run #23166 A5)" ], @@ -495,7 +496,7 @@ "steps": [ "sign in as admin in the console; open the palette with ⌘K (Cmd/Ctrl+K), screenshot it (data-testid overlay:command-palette), then close and re-open it via the header Search button (data-testid action:command-palette:open) — confirm the SAME overlay opens (idempotent, ADR-0054 C1)", "with the input EMPTY (after visiting a couple of records first to populate recents), confirm the palette shows the 'Recently viewed' group and fires NO search request", - "type a seeded CJK/pinyin query (e.g. 'zhangwei', '张', or 'huaning') and capture the network request — confirm it is GET /api/v1/search?q= (searchAll), and capture the returned hits", + "type a seeded CJK/pinyin query (e.g. 'zhangwei', '张伟', or 'huaning' — ⛔ not the single character '张': the palette sends no request below its minLength of 2, so a one-character query never reaches the wire and the palette reads 'No results found.' — objectui react useRecordSearch minLength = 2, which CommandPalette.tsx leaves at its default) and capture the network request — confirm it is GET /api/v1/search?q= (searchAll), and capture the returned hits", "screenshot the settled palette and confirm record hits are grouped under per-object headings (issue #3371); read the DOM only after the screenshot", "press Enter on the top hit (ref-targeted select, not a coordinate click) and confirm navigation to /apps///record/ and the record page renders", "reach the SearchResultsPage at /apps//search?q= by URL and repeat the query there; confirm record hits grouped by object plus nav matches; also run a no-hit query and confirm the designed empty state. The palette's 'Open full search' item is no shortcut for this: the palette offers it only for an EMPTY query or one matching the words of its value 'search all results full page' (e.g. type 'full'), never beside a record query, and it navigates to /search without carrying the typed term (objectui CommandPalette.tsx FULL_SEARCH_VALUE, objectui#11812)", @@ -504,7 +505,7 @@ ], "acceptance": [ { - "clause": "one open path: ⌘K, Ctrl+K, the header Search button (action:command-palette:open), and the ?palette=1/?cmdk=1 deep-link all open the SAME idempotent palette overlay (ADR-0054 C1/C3) — calling open when already open is a no-op, not a toggle-closed", + "clause": "one open path: ⌘K, Ctrl+K, the header Search button (action:command-palette:open), and the ?palette=1/?cmdk=1 deep-link all open the SAME idempotent palette overlay (ADR-0054 C1/C3) — calling open when already open through a NON-keyboard affordance (the button, a deep-link, a programmatic caller) is a no-op, not a toggle-closed; the ⌘K/Ctrl+K accelerator toggles by design, so a repeated keystroke closing the palette is not this clause's FAIL", "oracle": "dom", "verify": "each affordance yields the overlay:command-palette overlay and the URL carries the palette param; DOM checked only after a screenshot confirms the overlay rendered", "evidence": "screenshots of the palette opened by each affordance + the URL param" @@ -516,7 +517,7 @@ "evidence": "the search request trace" }, { - "clause": "CJK/pinyin recall reaches grouped hits: 'zhangwei' / '张' / 'huaning' returns the seeded CJK record(s), listed under the object's heading — record hits grouped per object (#3371), not a flat undifferentiated list", + "clause": "CJK/pinyin recall reaches grouped hits: 'zhangwei' / '张伟' / 'huaning' (two characters at least: the palette fires no request for a one-character query, minLength 2) returns the seeded CJK record(s), listed under the object's heading — record hits grouped per object (#3371), not a flat undifferentiated list", "oracle": "network", "verify": "the /api/v1/search response carries the CJK hit; the palette renders it under its object heading (screenshot confirms the grouping before any DOM read)", "evidence": "the search response + the grouped-palette screenshot" @@ -576,7 +577,8 @@ { "revision": 1, "date": "2026-08-08", "change": "new item: console global-search UI (⌘K palette + header button ADR-0054 C1 + /search page) over GET /api/v1/search — object-grouped hits, RLS parity, Enter-navigates, empty-shows-recents; grounded in objectui app-shell + the framework search route ledger", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-11", "change": "two text corrections from run #7629, no clause substance changed. (1) The RLS-parity fixture carried the same stale invoice count as search.rls-both-personas — the seed ships 12 rows INV-1001..INV-1012, not 8; INV-1003's owner (linus) is now named so the invisible-row premise is checkable in place. (2) knownGaps records that this path inherits the declared-case-insensitive contract and the #7641 executor gap, since searchAll also emits `$contains` — without it a run scores a case-driven miss as a palette defect. Direction is per the #4706 Q2 = A ruling: case-insensitive is the declared truth, so the checklist reconciles toward the declaration and #7641 owns the product side", "ref": "#7647" }, { "revision": 3, "date": "2026-08-12", "change": "the executor gap this item's third knownGap recorded is CLOSED, so the gap flips to past tense. #7641 found searchAll to be a SECOND producer of search clauses with the same declared≠enforced defect — metadata-protocol/src/protocol.ts built its AND-of-OR from `$contains` under a comment asserting `$contains` was the case-insensitive operator — and moved it onto `$icontains` in the same PR as search-filter.ts. No clause substance changed; a case-varying miss on a current build is now a real palette/UI defect rather than an expected executor FAIL, which is the scoring instruction this revision corrects", "ref": "#7641" }, - { "revision": 4, "date": "2026-10-13", "change": "fixtures.requires[2] names the recipe's owner rule (persona recipe gap, #23166 A5), and steps[5] reaches /search by URL (stale step, #23166 A6); both parked on #23121, carried by #23169. A member-created showcase_invoice is invisible to its creator unless `owner` is set to the member's email, so the area recipe qa-contributor-bound-member's step 5 now carries it, with a recipe knownGap for the same rule on showcase_task (assignee). The palette's Open Full Search Page is offered only for an empty query or one matching 'search all results full page' (CommandPalette.tsx at objectui 1fb1ced36243, still this repo's .objectui-sha), and it drops the query, so the step reaches the page by URL. QA run #23166 passed both clauses that way", "ref": "#23169" } + { "revision": 4, "date": "2026-10-13", "change": "fixtures.requires[2] names the recipe's owner rule (persona recipe gap, #23166 A5), and steps[5] reaches /search by URL (stale step, #23166 A6); both parked on #23121, carried by #23169. A member-created showcase_invoice is invisible to its creator unless `owner` is set to the member's email, so the area recipe qa-contributor-bound-member's step 5 now carries it, with a recipe knownGap for the same rule on showcase_task (assignee). The palette's Open Full Search Page is offered only for an empty query or one matching 'search all results full page' (CommandPalette.tsx at objectui 1fb1ced36243, still this repo's .objectui-sha), and it drops the query, so the step reaches the page by URL. QA run #23166 passed both clauses that way", "ref": "#23169" }, + { "revision": 5, "date": "2026-10-19", "change": "steps[2], fixtures.requires[0], acceptance[0].clause and acceptance[2].clause corrected from QA run #24574 (18.0 sweep, 5b14b7f3; carried by #24593 at ba4afcc2), checklist-accuracy findings re-read at the console pin .objectui-sha 4016370af3c9 and unchanged at objectui main 282b138c07. (1) The palette's minLength is 2 (objectui react useRecordSearch, left at its default by CommandPalette.tsx), so the suggested query '张' never reaches the wire and reads 'No results found.' — the CJK example is now '张伟'. (2) The accelerator toggles by source design (CommandPaletteProvider.tsx: only the button, links, deep-links and programmatic callers share the idempotent openCommandPalette()), so 'open when already open is a no-op' now covers the non-keyboard affordances and a repeated ⌘K closing the palette is not a FAIL. Wave anchor #24579 §3", "ref": "#PENDING-24579R4" } ] }, { @@ -683,7 +685,7 @@ "title": "Global search (GET /api/v1/search) skips what the caller cannot read instead of failing: an unreadable object is never queried, named or counted; a readable object is searched only on fields the caller may query; row scope still narrows every searched object", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -695,7 +697,7 @@ "requires": [ "an object W the member is refused at its own door (GET /api/v1/data/W answers 403 for the member) that holds a row matching the run's term T. Pick W and T on the live boot as admin; ⛔ never assume them. A platform sys_* object is the likely W: an unscoped search sweeps every registered object, the platform's own included", "seeded invoices INV-1001..INV-1012 with INV-1003 invisible to the member, plus one invoice the member creates (as in search.rls-both-personas)", - "a searchable field the member may NOT query that holds a value found in no field the member may query. On stock this is one of sys_user's admin-only search columns (role, ban_reason, last_login_ip, measured by #21879 on a fresh boot). A last_login_ip value is a candidate. Confirm the premise as admin before relying on it" + "a searchable field the member may NOT query that holds a value found in no field the member may query. On stock this is one of sys_user's admin-only search columns (role, ban_reason, last_login_ip, measured by #21879 on a fresh boot). On a fresh boot the usable stock V is the `role` value `user`: it is held by sign-up and seed-admin users only and no member-readable column contains it, while `last_login_ip` is null on every row (a direct sign-in sends no forwarded header, and the column is stamped from the trusted proxy's forwarded header — sys-user.object.ts) and `ban_reason` is null too (QA run #24593). Confirm the premise as admin before relying on it" ], "provisioning": { "use": "qa-contributor-bound-member", @@ -712,8 +714,8 @@ "positive control as admin: GET /api/v1/search?q=T&objects=W,showcase_invoice hits in W. A W with no matching row proves nothing about skipping", "as the member, an UNSCOPED search: GET /api/v1/search?q=T. Capture the status and the full body text", "as the member, a mixed scope: GET /api/v1/search?q=T&objects=showcase_invoice,W. Then W alone: objects=W; then a name that matches no object: objects=qa_no_such_object. Capture all three bodies", - "row scope: as the member, GET /api/v1/search?q=INV&objects=showcase_invoice; as admin, the identical query. Compare the hit sets against each persona's readable rows", - "hidden field: as admin, read a sys_user row and pick a value V from an admin-only search column that appears in no column the member may query (premise guard). As admin, GET /api/v1/search?q=V&objects=sys_user must hit (positive control). Then as the member, the identical query", + "row scope: as the member, GET /api/v1/search?q=INV&objects=showcase_invoice&perObject=25; as admin, the identical query. ⚠ perObject is required here: searchAll caps each object at 5 hits by default (max 25 — metadata-protocol protocol.ts searchAll), newest updated first, and `truncated` describes only the overall limit, so without it the admin's 5 hits omit INV-1003 and read like acceptance[3]'s admin half failing (QA run #24593). q=INV-1003 is the narrower alternative. Compare the hit sets against each persona's readable rows", + "hidden field: as admin, read a sys_user row and pick a value V from an admin-only search column (on a fresh boot: `role` = `user`, fixtures.requires[2]) that appears in no column the member may query (premise guard). As admin, GET /api/v1/search?q=V&objects=sys_user must hit (positive control). Then as the member, the identical query", "run the pins: the dogfood file and the metadata-protocol unit file in automated.ref" ], "acceptance": [ @@ -784,6 +786,12 @@ "date": "2026-10-16", "change": "knownGaps[0] called pinyin OFF 'the stock default' (#23878 checklist-accuracy finding). On the showcase, OS_SEARCH_PINYIN_ENABLED unset derives ON from the zh-CN locale (resolveSearchPinyinEnabled; re-measured on main c91af8ed6: the stock boot loads com.objectstack.plugin.pinyin-search and logs the companion backfill), so the run must set the flag false explicitly for acceptance[4]. Wording only, no re-run owed", "ref": "#24036" + }, + { + "revision": 3, + "date": "2026-10-19", + "change": "fixtures.requires[2], steps[4] and steps[5] corrected from QA run #24593 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382. (1) searchAll caps each object at perObject = 5 by default (max 25, protocol.ts searchAll) and `truncated` describes only the overall limit, so step 5's literal query returns 5 admin hits without INV-1003 and reads as acceptance[3]'s admin half failing; the step now sends perObject=25. (2) The suggested hidden-field value is unusable on a fresh boot: last_login_ip is stamped from the trusted proxy's forwarded header (sys-user.object.ts) and is null after a direct sign-in, and ban_reason is null; the measured stock V is `role` = `user`. No clause changed. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] } From a6a3579e5c03ce7e68afc5621aaf540b3771f669 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 22:04:39 +0000 Subject: [PATCH 2/5] =?UTF-8?q?docs(qa):=2018.0=20sweep=20anchor=20round?= =?UTF-8?q?=204=20=E2=80=94=20dashboards:=20the=20inspector=20has=20no=20f?= =?UTF-8?q?ree-key=20door,=20combo=20marks=20are=20the=20renderer's,=20the?= =?UTF-8?q?=20Interfaces=20pillar=20needs=20an=20app,=20the=20dangling-bin?= =?UTF-8?q?ding=20guard=20sits=20at=20publish,=20and=20the=20unexposed=20c?= =?UTF-8?q?ube=20answers=20CUBE=5FNOT=5FFOUND?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checklist ledger only. Five dashboards items take one revision each, every finding re-read at objectstack f89d29e382 and the console pin 4016370af3c9. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../platform-checklist/areas/dashboards.json | 62 ++++++++++++++----- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/docs/qa/platform-checklist/areas/dashboards.json b/docs/qa/platform-checklist/areas/dashboards.json index 62f507d6fcf..33909a9ccad 100644 --- a/docs/qa/platform-checklist/areas/dashboards.json +++ b/docs/qa/platform-checklist/areas/dashboards.json @@ -9,7 +9,7 @@ "title": "A widget authored with a stray/legacy key fails loud, never renders blank", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -26,7 +26,7 @@ "for each variant in `variants`, author a dashboard draft via PUT /api/v1/meta/dashboard/qa_stray_key_probe?mode=draft whose single widget carries a valid dataset binding (dataset: showcase_task_metrics, dimensions: ['status'], values: ['task_count']) PLUS the stray key (e.g. \"categoryField\": \"status\")", "capture the full response status and error body for each variant", "additionally author one widget with a wrong-layer key INSIDE chartConfig (e.g. chartConfig.dataset or chartConfig.aggregate) — ChartConfigSchema carries per-key guidance for these — and capture that error text too", - "attempt the same stray-key widget through the Studio dashboard designer inspector (browser door) and capture the surfaced error", + "at the Studio dashboard designer's widget inspector (browser door), confirm it offers NO free-key path: it is a structured form whose legacy inline-analytics fields were removed (objectui app-shell metadata-admin/inspectors/DashboardWidgetInspector.tsx: 'its fields are gone so no Studio surface can emit the dead shape'), so the stray key cannot be typed there and there is no error to capture. A raw-JSON door that does take one — the metadata editor's Source tab — saves through the same PUT; if you use it, capture the 422 it surfaces", "after each rejection, GET /api/v1/meta/dashboard/qa_stray_key_probe and record whether anything was persisted", "finally author the SAME widget with the stray key removed (pure dataset + dimensions + values) and load it in the browser" ], @@ -106,6 +106,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[4] corrected from QA run #24573 (18.0 sweep, 5b14b7f3; carried by #24612 at ba4afcc2), re-read at the console pin 4016370af3c9: the Studio widget inspector is a structured form with no free-key or JSON entry (DashboardWidgetInspector.tsx removed the legacy fields), so 'attempt the same stray-key widget through the inspector and capture the surfaced error' could not be executed. The step now asks the run to confirm the inspector offers no free-key path. The same record's second note, that a pending draft is unreadable until publish, does not hold and is not taken: GET /meta/dashboard/NAME?state=draft reads the draft (META_ITEM_READ_PARAMS includes `state`); only `?mode=draft`, a save-door parameter, is refused on the read. No clause changed. Wave anchor #24579 §3", + "ref": "#PENDING-24579R4" } ] }, @@ -280,7 +286,7 @@ "title": "Every ChartTypeSchema member renders a real chart with the correct marks and series count", "since": "v15", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "browser", "personas": [ @@ -294,7 +300,7 @@ "a scratch draft dashboard for the period-over-period compareTo probe (via PUT /api/v1/meta/dashboard/qa_compareto_probe?mode=draft) — NO showcase widget authors compareTo, so this is authored fresh exactly like the strict-widget probes. Two things the probe needs, both measured by QA run #23927: (a) a DATED WINDOW — compareTo shifts the selection's date range, so the dashboard declares one, dateRange: { field: 'issued_on', defaultRange: 'last_90_days' }; without it the dataset query answers a loud, correct 400 'compareTo needs a dated window to shift…' (packages/services/service-analytics/src/dataset-executor.ts) instead of a render; (b) data in BOTH windows — bind a line widget on the issued_on time dimension and a metric widget, each to dataset showcase_invoice_metrics with values ['invoice_count'] and compareTo: { kind: 'previousPeriod' }. The invoice seed spreads issued_on across ~6 months (examples/app-showcase/src/data/seed/index.ts: 7 dated invoices in the last 90 days, 3 in the 90 days before), so the comparison is non-zero and differs from the base. ⛔ Not showcase_task_metrics on created_at: every seeded task is created inside the current window, so the previous period is empty and a flat-zero comparison barely discriminates" ], "knownGaps": [ - "kpi / gauge / solid-gauge / bullet have NO distinct renderer today — all four route to the SAME single-value renderer as `metric` (chart.zod.ts NOTE: '`metric`/`kpi` are kept as honest single-value synonyms; `gauge`/`solid-gauge`/`bullet` render a value today and gain a dial when a gauge renderer lands'; examples/app-showcase/test/coverage.test.ts SAME_AS_METRIC = {kpi, gauge, solid-gauge, bullet}). Two consequences a run must hold BOTH of: (a) the demonstrable claim is 'renders the value, not blank', NOT 'renders a dial' — do not tick a dial that does not exist; and (b) ⚠️ the four scratch tiles are expected to be VISUALLY IDENTICAL to each other and to the metric tile. Four indistinguishable tiles is the CORRECT observation, not evidence that the renderer ignored the declared type and not a per-variant FAIL — re-confirmed on 17.1.0 (#10560). The type IS being honoured; there is simply one renderer behind five names, which is what makes them synonyms rather than families" + "kpi / gauge / solid-gauge / bullet have NO distinct renderer today — all four route to the SAME single-value renderer as `metric` (chart.zod.ts NOTE: '`metric`/`kpi` are kept as honest single-value synonyms; `gauge`/`solid-gauge`/`bullet` render a value today and gain a dial when a gauge renderer lands'; examples/app-showcase/test/coverage.test.ts SAME_AS_METRIC = {kpi, gauge, solid-gauge, bullet}). Two consequences a run must hold BOTH of: (a) the demonstrable claim is 'renders the value, not blank', NOT 'renders a dial' — do not tick a dial that does not exist; and (b) ⚠️ the four scratch tiles are expected to be VISUALLY IDENTICAL to each other and to show the same value as the metric tile (the gallery's metric card differs only in its own title and placement — compare values, not chrome). Four indistinguishable tiles is the CORRECT observation, not evidence that the renderer ignored the declared type and not a per-variant FAIL — re-confirmed on 17.1.0 (#10560). The type IS being honoured; there is simply one renderer behind five names, which is what makes them synonyms rather than families" ] }, "steps": [ @@ -303,7 +309,7 @@ "open the Chart Gallery dashboard (Showcase app → Analytics → Chart Gallery), let it settle, and screenshot the full board", "per gallery widget (16 families: metric ×3 tiles, bar, column, horizontal-bar, line, area, combo, pie, donut, funnel, scatter, radar, treemap, sankey, table, pivot), screenshot the widget, then verify in the DOM that a real SVG (or table element for table/pivot) rendered with the mark type the family names", "count marks against ground truth: bar/column/horizontal-bar bars == distinct statuses (or priorities), pie/donut/funnel segments == distinct statuses/priorities, line/area points == month buckets", - "for combo, verify BOTH mark types on shared axes: task_count as bars bound to the left axis and avg_progress as a line on the right axis (the widget's series[] declares exactly that)", + "for combo, verify BOTH mark types on shared axes: task_count as bars bound to the left axis and avg_progress as a line on the right axis. The widget authors no series[] — the per-series mark type is gone with the structure keys (the D3 entry dashboard-widget-chart-config-structure-refused; examples/app-showcase/src/ui/dashboards/chart-gallery.dashboard.ts), so which mark each measure gets is the renderer's: in a combo the first measure draws as bars and the others as lines, and a combo always draws both value axes (objectui plugin-charts AdvancedChartImpl.tsx)", "for table and pivot, verify real grouped tables: Projects by Account rows == distinct accounts with 3 measure columns; Tasks by Status × Priority cross-tab dimensions match distinct status × priority", "author the scratch draft dashboard with four widgets of types kpi, gauge, solid-gauge, bullet, each bound to dataset showcase_task_metrics, values ['task_count'], publish it, and load it", "verify each of the four renders the same numeric value the metric tile shows — a number, never a blank tile", @@ -324,7 +330,7 @@ "evidence": "API bucket counts vs DOM mark counts, per widget" }, { - "clause": "combo renders MIXED marks on dual axes — bars (left) and a line (right) in one plot, per its series[].type / series[].yAxis config", + "clause": "combo renders MIXED marks on dual axes — bars (left) and a line (right) in one plot, by the renderer's combo default (first measure bars, the others lines, both value axes drawn) — the widget authors no series[] since the D3 entry dashboard-widget-chart-config-structure-refused", "oracle": "screenshot", "verify": "the combo widget shows both rect-bars and a path-line, with two y-axes", "evidence": "combo screenshot + DOM excerpt" @@ -338,7 +344,7 @@ { "clause": "the four single-value synonyms (kpi, gauge, solid-gauge, bullet) render the SAME value as metric — a number, never a blank or an error", "oracle": "screenshot", - "verify": "scratch-dashboard tiles each show the task_count value; evidence must note these are value-only renders (no dial) per the spec's own NOTE, and that the four tiles are EXPECTED to look identical to one another and to the metric tile — one renderer, five names. ⛔ Do not score 'the four tiles are indistinguishable' against the negative below: that negative is about a variant rendering as a DIFFERENT FAMILY (sankey drawn as a bar), which is the opposite failure", + "verify": "scratch-dashboard tiles each show the task_count value; evidence must note these are value-only renders (no dial) per the spec's own NOTE, and that the four tiles are EXPECTED to look identical to one another and to show the same VALUE as the metric tile — one renderer, five names. The gallery's metric card is not pixel-identical (its title and placement differ, QA run #24573): compare values, not chrome. ⛔ Do not score 'the four tiles are indistinguishable' against the negative below: that negative is about a variant rendering as a DIFFERENT FAMILY (sankey drawn as a bar), which is the opposite failure", "evidence": "scratch-dashboard screenshot annotated with the honest-synonym caveat" }, { @@ -436,6 +442,12 @@ "date": "2026-10-16", "change": "fixtures.requires[2], steps[9] and acceptance[7].verify re-pointed so the compareTo probe can render and discriminate (fixture gaps F1 and F2 of QA run #23927, wave #23878). F1: as written the probe declared no dateRange, and the dataset query refused it with a loud, correct 400 'compareTo needs a dated window to shift' — #23927 had to add one to get a render. F2: bound to showcase_task_metrics on created_at, the previous 90-day window held no tasks on the stock seed, so the comparison series was flat at 0. The probe now declares dateRange { field: 'issued_on', defaultRange: 'last_90_days' } and binds to showcase_invoice_metrics, whose seeded issued_on dates fall in both windows (7 and 3), and the verify text reconciles the comparison against the previous window's literal-window count. Not loosened: a comparison silently absent, or one that disagrees with the previous window's count, still FAILS", "ref": "#24036" + }, + { + "revision": 6, + "date": "2026-10-19", + "change": "steps[5], acceptance[2].clause, acceptance[4].verify and fixtures.knownGaps[0] corrected from QA run #24573 (18.0 sweep, 5b14b7f3; carried by #24612), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) The combo widget no longer authors series[]: chart-gallery.dashboard.ts drops it under the D3 entry dashboard-widget-chart-config-structure-refused, and the renderer picks each measure's mark (objectui AdvancedChartImpl.tsx: first measure bars, the rest lines, both value axes drawn). The run's combo read (2 bars + 1 line on dual axes) is exactly that default. (2) The single-value tiles are compared to the metric tile by value, not by look: the gallery's metric card carries its own title and placement. No clause substance changed. Wave anchor #24579 §3", + "ref": "#PENDING-24579R4" } ], "enumSource": { @@ -449,7 +461,7 @@ "title": "Dataset-only authoring works end to end: layout-less designer drafts save/publish, and every ReportType renders from the same semantic datasets", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -528,7 +540,7 @@ ], "negative": [ "a joined block nested inside blocks[] must be rejected at parse (block type enum is tabular|summary|matrix — no recursion); a silent save is a FAIL", - "a report or widget silently rendering while its dataset binding names nothing (blank-without-error) is a FAIL" + "a report or widget silently rendering while its dataset binding names nothing (blank-without-error) is a FAIL. The guard sits at PUBLISH (and at an active save, the same verb `os build` gates), not at the draft save: a draft naming a dangling dataset, measure or dimension is admitted by design (packages/metadata-protocol/src/runtime-authoring-gate.ts D1 — 'a draft save is always let through'), and its publish answers 422 widget-dataset-unknown / widget-measure-unknown / widget-dimension-unknown / chart-dataset-unknown. Drive the negative through the publish door; a 200 on the draft save is not this FAIL (QA run #24612)" ], "variants": [ "tabular (via ListView lens)", @@ -564,6 +576,12 @@ "date": "2026-10-16", "change": "acceptance[5] (clause, verify, evidence) and steps[6] made computable (checklist-accuracy finding C2 of QA run #23927, wave #23878). The clause asked for 'open_block totals + done_block totals == unfiltered totals', but the two blocks carry different measures — open_block est_hours, done_block task_count (examples/app-showcase/src/ui/reports/index.ts) — so the sum mixed hours with a count and could not be computed; #23927 reconciled it per measure through the API. The clause now checks each block's rendered total against its own measure under its own runtimeFilter, and the per-measure partition (done:false + done:true == unfiltered) for both measures. Not loosened: a block whose runtimeFilter is ignored still renders its measure's unfiltered total and FAILS", "ref": "#24036" + }, + { + "revision": 3, + "date": "2026-10-19", + "change": "negative[1] corrected from QA run #24612 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382: the dangling-binding guard is an author-time rule run at publish and on an active save, never on a draft (runtime-authoring-gate.ts D1), so a draft with a dangling dataset, measure or dimension answers 200 and its publish answers 422 with the named rule. The negative now names the publish door, so a draft 200 is not read as the blank-without-error FAIL. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -664,7 +682,7 @@ "title": "Dashboard-level dateRange + global filters re-scope every bound widget through its OWN field mapping; opted-out widgets stay fixed; and the Studio widget inspector AUTHORS those filterBindings", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "browser", "personas": [ @@ -675,7 +693,7 @@ "requires": [ "the Revenue Pulse dashboard (showcase_revenue_pulse) — the framework#2501 acceptance fixture: one dateRange (default field issued_on) + one region filter (default field region) driving invoice AND account widgets, account widgets re-mapping via filterBindings (dateRange→signed_on, region→sales_region), one KPI opted out of both (filterBindings false) as the all-time reference", "seeded accounts spread across sales_region amer/emea/apac with signed_on dates (src/data/seed/index.ts)", - "for D2 and D3 a SCRATCH dashboard, because no stock dashboard serves either leg (QA run #23163 scored both on runtime scratch dashboards): D2's Studio authoring round-trip needs a dashboard in a WRITABLE package to publish into — create a scratch package (POST /api/v1/packages) and author the dashboard there, since the showcase package is read-only in Studio; D3's dataset-sourced option half needs a globalFilter whose options come from `optionsFrom` ({object, valueField, labelField} — GlobalFilterOptionsFromSchema in packages/spec/src/ui/dashboard.zod.ts), which NO stock dashboard declares (Revenue Pulse's region filter lists static options). D3's {value,label} render half still reads on Revenue Pulse. Delete the scratch package at teardown" + "for D2 and D3 a SCRATCH dashboard, because no stock dashboard serves either leg (QA run #23163 scored both on runtime scratch dashboards): D2's Studio authoring round-trip needs a dashboard in a WRITABLE package to publish into — create a scratch package (POST /api/v1/packages) and author the dashboard there, since the showcase package is read-only in Studio — AND author an app in that package whose navigation carries the dashboard (PUT /api/v1/meta/app/NAME?package=PKG): the Studio Interfaces pillar IS the app navigation tree (objectui app-shell StudioDesignSurface.tsx: \"Interfaces: the real App navigation tree\"), so a package with no app shows no dashboard to select (QA run #24573); D3's dataset-sourced option half needs a globalFilter whose options come from `optionsFrom` ({object, valueField, labelField} — GlobalFilterOptionsFromSchema in packages/spec/src/ui/dashboard.zod.ts), which NO stock dashboard declares (Revenue Pulse's region filter lists static options). D3's {value,label} render half still reads on Revenue Pulse. Delete the scratch package at teardown" ] }, "steps": [ @@ -792,6 +810,12 @@ "date": "2026-10-13", "change": "fixtures.requires[2] (new) names the scratch dashboard D2 and D3 need, and steps[10] and [11] point at it (fixture gap, #23163 D2 / D3; parked on #23121, carried by #23169). No stock dashboard declares a globalFilter with optionsFrom (examples/app-showcase has none; Revenue Pulse's region filter lists static options), so D3's server GROUP BY half has no stock subject, and D2's authoring round-trip needs a writable package to publish into. QA run #23163 passed both on runtime scratch dashboards. No clause changed", "ref": "#23169" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "fixtures.requires[2] corrected from QA run #24573 (18.0 sweep, 5b14b7f3; carried by #24612), re-read at the console pin 4016370af3c9: the scratch package needs an app whose navigation points at the dashboard, because Studio's Interfaces pillar is the app navigation tree (StudioDesignSurface.tsx), and its create paths return early with no app. The recipe named only the package and the dashboard. No clause changed. Wave anchor #24579 §3", + "ref": "#PENDING-24579R4" } ] }, @@ -1225,7 +1249,7 @@ "title": "Analytics on v18 asks each object's `enable` block and internal flags: a cube or dataset over an apiEnabled:false object is 404, an internal field as dimension / measure / filter is 400 even for admin, an unexposed configured cube is warned at boot and absent from /analytics/meta, and a dataset answer's measure column states its aggregate", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -1240,10 +1264,10 @@ ] }, "steps": [ - "POST /api/v1/analytics/query on the cube over the unexposed object", + "POST /api/v1/analytics/query on the cube over the unexposed object, then the same query reaching the OBJECT itself: its inferred cube (cube: '') or an inline dataset over it", "POST /api/v1/analytics/query using the internal field as a dimension, then as a filter", "GET /api/v1/analytics/meta and the boot log", - "POST /api/v1/analytics/dataset/query {datasetName: \"sys_user_metrics\", selection: {measures: [\"user_count\"]}} (a saved dataset is named by `datasetName`; `dataset` is an INLINE definition object); read fields[]" + "POST /api/v1/analytics/dataset/query {datasetName: \"sys_user_metrics\", selection: {measures: [\"user_count\"]}} (a saved dataset is named by `datasetName`; `dataset` is an INLINE definition object); read fields[]. sys_user_metrics declares NO dimensions (packages/platform-objects/src/apps/dashboards/system.datasets.ts), so read the dimension half on a dimensioned dataset too: {datasetName: \"showcase_task_metrics\", selection: {dimensions: [\"status\"], measures: [\"task_count\"]}}" ], "acceptance": [ { @@ -1253,7 +1277,7 @@ "evidence": "the vitest summary line per pin file, with the package name and the subject sha" }, { - "clause": "the unexposed cube query → 404 OBJECT_API_DISABLED; the internal field → 400 INVALID_FIELD (admin too)", + "clause": "a query over the unexposed object answers 404 — CUBE_NOT_FOUND for the configured cube, whose registration acceptance[2] refused (so there is no such cube to query), and OBJECT_API_DISABLED for a query that reaches the object itself (its inferred cube or an inline dataset; service-analytics api-exposure-door.ts); the internal field → 400 INVALID_FIELD (admin too)", "oracle": "api", "verify": "status + code", "evidence": "answers" @@ -1267,7 +1291,7 @@ { "clause": "the count measure column carries aggregate:\"count\"; dimension columns carry none", "oracle": "api", - "verify": "fields[] entries", + "verify": "fields[] entries: sys_user_metrics for the measure column, and a dimensioned dataset (showcase_task_metrics by status) for the dimension column, since sys_user_metrics has none", "evidence": "the excerpt" } ], @@ -1286,6 +1310,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], steps[3], acceptance[1].clause and acceptance[3].verify corrected from QA run #24558 (18.0 since:v18 sweep, 5b14b7f3), re-read at objectstack f89d29e382. (1) A configured cube over an apiEnabled:false object is refused at registration (api-exposure-door.ts, the registration face), so querying it answers 404 CUBE_NOT_FOUND, consistent with acceptance[2]; OBJECT_API_DISABLED is reached by a query that names the object itself, through its inferred cube (assertInferableCube admits a registered object) or an inline dataset. The run read CUBE_NOT_FOUND where the clause expected OBJECT_API_DISABLED. (2) sys_user_metrics declares no dimensions (system.datasets.ts), so 'dimension columns carry none' needs a dimensioned dataset; step 4 adds showcase_task_metrics by status. Wave anchor #24579 (round 3 named this item)", + "ref": "#PENDING-24579R4" } ] } From f448d2e8d17f3366040cc8aa514056e257c4c795 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 22:07:27 +0000 Subject: [PATCH 3/5] =?UTF-8?q?docs(qa):=2018.0=20sweep=20anchor=20round?= =?UTF-8?q?=204=20=E2=80=94=20studio-authoring:=20eleven=20items=20re-poin?= =?UTF-8?q?ted=20to=20the=20shipped=20console=20and=20server=20(review=20s?= =?UTF-8?q?heet,=20autosave,=20record=20page=20on=20showcase=5Fcontact,=20?= =?UTF-8?q?draft=20line,=20Source-tab=20door,=20package-scoped=20matrix,?= =?UTF-8?q?=20plantable=20diagnostics=20row,=20owner-mismatch=20and=20publ?= =?UTF-8?q?ish-drafts=20doors,=20rule=20ids)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checklist ledger only. Eleven studio-authoring items take one revision each, every finding re-read at objectstack f89d29e382 and the console pin 4016370af3c9; findings that did not hold are left alone. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/studio-authoring.json | 148 +++++++++++------- 1 file changed, 92 insertions(+), 56 deletions(-) diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index 79bf0427978..f94398c1eb5 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -9,7 +9,7 @@ "title": "The first-run authoring loop closes: package → object → record → app → publish → end-user, zero code, zero restarts", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P0", "surface": "mixed", "personas": ["admin (seeded admin@objectos.ai)", "end user (same account driving the published app)"], @@ -19,19 +19,19 @@ "a fresh boot of the showcase example — objectstack dev --ui --seed-admin -p -d file:/tmp//data.db (the audit's environment: vendored console matching .objectui-sha, seeded admin admin@objectos.ai/admin123)" ], "knownGaps": [ - "publish fires in ONE click with no confirmation and the Changes panel lists items without field-level diff (audit finding 3, ADR-0016 §3.6 step 4 open) — do NOT assert a review/confirm step exists; its absence is a logged UX gap, not a run failure", + "SUPERSEDED (QA run #24571): publish is no longer one click with no confirmation. Publish opens the 'Pending changes' review sheet — every pending draft listed as New or Update, each expanding into the keys it changes, and a separate 'Publish all' confirm (objectui app-shell preview/DraftChangesPanel.tsx, mounted by StudioDesignSurface.tsx; the audit's finding 3 / ADR-0016 §3.6 step 4). A review step now exists; do not score its presence or absence here — this item scores the loop", "SUPERSEDED (QA run #21056): a new app no longer scaffolds zero nav items — the Create-app dialog seeds the app's navigation from the package's objects behind a checkbox that defaults ON (objectui StudioDesignSurface appAddObjects, objectui#2262). With it on, step 5's nav item already exists and the Interfaces step confirms it rather than builds it; only with it unticked does the manual wiring remain the contract. Do not expect zero items", "mixed-language Studio chrome (audit finding 4) is asserted by i18n.studio-follows-app-locale — locale consistency is that item's business, not this one's" ] }, "steps": [ "boot the showcase example (objectstack dev --ui --seed-admin -p -d file:/tmp//data.db), sign in as admin@objectos.ai/admin123, and note the boot timestamp in the server log (the zero-restart clause reads it later)", - "Home → 'Build an app' → Studio landing → create a NEW writable package (维修中心 / com.example.repairs, namespace repairs) via the new-package wizard", - "Data pillar → new object 'Repair Ticket' (the identifier auto-suggests repair_ticket from the display name, and the dialog PREFIXES it with the package namespace, so the object is named repairs_repair_ticket — the name every later step and clause uses (framework#2694, objectui StudioDesignSurface prefixObjectName). The dialog also carries an OWD (sharing model) choice, pre-selected to the platform's recommended baseline: confirm or change it before Create) → add a picklist field Status with 3 values → Save draft", - "open the Changes panel and Publish ('Published all drafts in this package (one atomic release)')", + "Home → 'Build an app' → Studio landing → create a NEW writable package (维修中心 / com.example.repairs, namespace repairs) via the new-package wizard. Type the identifier yourself: a CJK-only display name derives no package id (objectui PackageFormDialog.tsx toIdSegment: 'Empty when nothing usable remains (a CJK-only name, say)'), and the namespace derives from the id you type", + "Data pillar → new object 'Repair Ticket' (the identifier auto-suggests repair_ticket from the display name, and the dialog PREFIXES it with the package namespace, so the object is named repairs_repair_ticket — the name every later step and clause uses (framework#2694, objectui StudioDesignSurface prefixObjectName). The dialog also carries an OWD (sharing model) choice, pre-selected to the platform's recommended baseline: confirm or change it before Create) → add a picklist field Status with 3 values. There is no Save draft button: the designer autosaves, and a picklist field is held unsaved until it has at least one option (the status line reads 'Not saved yet: the field “Status” needs at least one option'), so add the field and fill its options, and the draft save fires", + "open the Changes panel and Publish, then confirm with 'Publish all' in the review sheet; the toast names the count — 'Published 1 item in this package (one atomic release)' (objectui#11807; 'Published all drafts in this package' is the fallback when the count is unknown)", "create a record in the runtime-faithful Records grid with a Status value chosen", "Create app 'Repair Center' (identifier auto-suggested) → Interfaces pillar → add a nav item bound to repairs_repair_ticket → Publish", - "return Home: the launcher must show Repair Center; open it and drive the end-user list", + "return to the console Home (/_console/home — the Studio header's house icon goes to the Studio landing, 'Studio home', and the console Home is one click on from the landing's top bar): the launcher must show Repair Center; open it and drive the end-user list", "verify server truth after each publish: GET /api/v1/meta/object/repairs_repair_ticket, GET /api/v1/data/repairs_repair_ticket, GET /api/v1/meta/app?id= — ?id= matches the App document name, NOT the package id com.example.repairs; fetch /api/v1/meta/app with no query first to read the name the wizard actually minted", "negative probe: on the READ-ONLY com.example.showcase package there is no gesture to drive — the console HIDES New object and Add field there (objectui StudioDesignSurface gates them on !readOnly) — so the probe is a FORGED request: send the authoring write the gesture would have made straight to the metadata write door against that package, and capture the server response (QA run #21056 read 422 WRITABLE_PACKAGE_REQUIRED and 403 ITEM_LOCKED)" ], @@ -87,7 +87,8 @@ "history": [ { "revision": 1, "date": "2026-08-07", "change": "new area item: the full first-run authoring loop, grounded step-by-step in the 2026-07 Studio dogfood audit ('The loop closes'), with the audit's open UX findings carried as knownGaps instead of asserted", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-18", "change": "corrected the ?id= spelling in step 8 and clause 4, which said in an item whose step 2 creates the package com.example.repairs — inviting the package-id spelling that answers with an empty items array (#9334's class). Found by grounding rather than from a report: this item was not on the #9334 list. ⚠️ The card's other instruction for this item — drop any inherited 'Studio is not installed' assumption (#9453 CF-4) — needed NO edit: the item never carried one. It has no blocked marker, no such knownGap, and its steps drive the App Builder directly; the assumption lived in the wave briefing and the run records, not here", "ref": "#9386" }, - { "revision": 3, "date": "2026-10-01", "change": "checklist-accuracy findings of QA run #21056, each grounded in objectui at the pin: the object is named repairs_repair_ticket (the dialog prefixes the package namespace), and every step and clause now uses that name; the New object dialog carries a pre-selected OWD choice; the zero-nav-items knownGap is superseded (the Create-app scaffold checkbox defaults on); steps[8]'s probe is a forged request because the console hides authoring on a read-only package; acceptance[0]'s cited objectui pin targets testids absent at the pin", "ref": "#21060" } + { "revision": 3, "date": "2026-10-01", "change": "checklist-accuracy findings of QA run #21056, each grounded in objectui at the pin: the object is named repairs_repair_ticket (the dialog prefixes the package namespace), and every step and clause now uses that name; the New object dialog carries a pre-selected OWD choice; the zero-nav-items knownGap is superseded (the Create-app scaffold checkbox defaults on); steps[8]'s probe is a forged request because the console hides authoring on a read-only package; acceptance[0]'s cited objectui pin targets testids absent at the pin", "ref": "#21060" }, + { "revision": 4, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[1], steps[2], steps[3] and steps[6] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604 at ba4afcc2) and the P0 run #24556's continuation, re-read at the console pin 4016370af3c9. (1) knownGaps[0] said publish has no review step; Publish now opens the Pending changes sheet with per-item diff and a separate 'Publish all' (DraftChangesPanel.tsx). (2) A CJK-only display name derives no package id (PackageFormDialog.tsx toIdSegment). (3) There is no Save draft button for a field: the designer autosaves, and a picklist is held until it has an option ('Not saved yet: … needs at least one option'). (4) The publish toast names the count (objectui#11807). (5) The Studio house icon goes to the Studio landing, not the console Home. No clause changed. Wave anchor #24579 (round 3 named this item)", "ref": "#PENDING-24579R4" } ] }, { @@ -95,7 +96,7 @@ "title": "Object designer round-trip: field add/edit/reorder persists to metadata, identifiers derive and survive typing", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": ["admin"], @@ -113,8 +114,8 @@ "TYPE the object name NS_repair_asset character by character (never .fill — the F2 per-keystroke-sanitisation bug only reproduces when typed) and confirm both underscores survive. The NS_ prefix is required: in a namespaced writable package the publish gate refuses an unprefixed object name (NAMESPACE_PREFIX). Type the name after the label, or re-check it before saving — objectui#12680: typing the Label after a hand-typed name overwrites it and drops the prefix", "add a Text field, set its label to 'Asset Code', blur — the API name input must derive asset_code (F3)", "add a Picklist field and author its 3 real options, THEN add one EMPTY option row; wait out the debounced live validation — no validation banner may appear for the empty row (F4). ⚠ Order matters: with NO real option yet the select-needs-options banner fires, legitimately — the empty rows ARE dropped, so the field has no options — which collides with a literal 'no banner' (and with objectui F4's toHaveCount(0) the same way). Assert the empty-row half with real options present", - "Save draft, Publish; GET /api/v1/meta/object/NS_repair_asset and record the full field list and its order", - "reopen the designer: rename one picklist value, drag-reorder the fields, Save draft + Publish again", + "Save (the first save; the editor then autosaves every later edit — its Auto-save toggle defaults on, objectui ResourceEditPage.tsx metadata-admin:autosave — so later 'Save draft' steps are implicit), Publish; GET /api/v1/meta/object/NS_repair_asset and record the full field list and its order", + "reopen the designer: rename one picklist value, drag-reorder the fields (native HTML5 drag-and-drop: Playwright's dragTo moves a row, raw mouse moves do not — automation-input), let the autosave land, and Publish again", "GET /api/v1/meta/object/NS_repair_asset again — the rename AND the new field order must be in the persisted metadata, not just the canvas", "create a record via the Records tab and GET /api/v1/data/NS_repair_asset/ — the stored column key must be the derived API name (asset_code)" ], @@ -173,6 +174,12 @@ "date": "2026-10-16", "change": "steps[0], [1], [4], [6] and [7] and acceptance[0].verify and [2] (clause, verify) re-pointed (checklist-accuracy finding 3 of QA run #23942, wave #23878). The object name repair_asset cannot publish in a namespaced writable package — the publish gate answers NAMESPACE_PREFIX — so #23942 minted repairs_repair_asset; the steps and reads now use NS_repair_asset, which still carries the underscores the F2 sanitisation clause tests. The route /_console/apps/com.objectstack.studio/… renders inside the Showcase chrome because the studio app is not loaded on a stock boot, so step 0 opens the designer under the Setup app. Not loosened: the typed name must still read back character for character", "ref": "#24036" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "steps[4] and steps[5] corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: after the first save the editor autosaves (ResourceEditPage.tsx, the metadata-admin:autosave toggle defaults on), so the steps' 'Save draft' is implicit, and the field reorder is native HTML5 drag-and-drop, which raw mouse moves do not drive. No clause changed. Wave anchor #24579 §3", + "ref": "#PENDING-24579R4" } ] }, @@ -181,21 +188,21 @@ "title": "List + form view authoring goes live in the running app on publish — no server restart", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", - "personas": ["admin (authors)", "end user (consumes the published views)"], + "personas": ["admin (authors)", "end user (consumes the published views). A runtime-authored object grants no non-admin persona any access, so a non-admin list render needs a permission-set grant on it first; without one, score the end-user surfaces as the admin and say so (QA run #24571)"], "fixtures": { "app": "showcase", "requires": [ - "a runtime-authored object in a writable package to bind views to (e.g. repair_asset from studio-authoring.object-designer-roundtrip), so no shipped showcase view is mutated" + "a runtime-authored object in a writable package to bind views to (e.g. repair_asset from studio-authoring.object-designer-roundtrip), so no shipped showcase view is mutated. The object must declare a `sharingModel`: its draft PUT answers 200 without one, but publish refuses it (security-owd-unset, error tier — packages/lint/src/validate-security-posture.ts), so an object authored without it never goes live (QA run #24571)" ] }, "steps": [ "author a view CONTAINER draft via the metadata draft door: PUT /api/v1/meta/view/qa_repair_asset_views?mode=draft with { object: 'repair_asset', list: { type: 'grid', columns: [...] }, form: { ... } } — the container's own keys are list/form/listViews/formViews (ViewSchema); capture the receipt", "as an end user, open the object's list BEFORE publishing: it must still render the previously published views (or the synthesized default) — the draft is staged only", "publish: POST /api/v1/meta/view/qa_repair_asset_views/publish", - "reload the app page (client reload allowed, NO server restart): the list must render the authored column set; open a record: the form view layout must apply", + "reload the app page (client reload allowed, NO server restart): the list must render the authored column set; open New or Edit on a record: the authored form view layout applies in those dialogs (opening a record shows its record page, not the form view)", "read back through the consumer door: GET /api/v1/meta/view?object=repair_asset (the getViewsByObject read) returns the container", "edit the list view — reorder the columns and add one — re-publish, reload, and re-screenshot", "capture the server log across the whole sequence (the no-restart clause reads it)" @@ -244,7 +251,8 @@ ], "history": [ { "revision": 1, "date": "2026-08-07", "change": "new item: list/form view authoring through the ?mode=draft door with live-in-app verification, grounded in ViewSchema's container contract and the audit's zero-restart loop", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 2, "date": "2026-10-02", "change": "re-pointed clause 4 at the object door's designed shape. It expected the container name qa_repair_asset_views in GET /meta/view?object=repair_asset; the door serves the container's expanded . items instead, by design since #7163 (runtime door #7736 / #13407). The 17.6.0 release-verification run measured the authored bodies round-tripping as repair_asset.default / repair_asset.form with the container name appearing 0 times", "ref": "#21330" } + { "revision": 2, "date": "2026-10-02", "change": "re-pointed clause 4 at the object door's designed shape. It expected the container name qa_repair_asset_views in GET /meta/view?object=repair_asset; the door serves the container's expanded . items instead, by design since #7163 (runtime door #7736 / #13407). The 17.6.0 release-verification run measured the authored bodies round-tripping as repair_asset.default / repair_asset.form with the container name appearing 0 times", "ref": "#21330" }, + { "revision": 3, "date": "2026-10-19", "change": "personas[1], fixtures.requires[0] and steps[3] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604), re-read at objectstack f89d29e382: a runtime object needs `sharingModel` to publish (security-owd-unset is error tier in validate-security-posture.ts, though the draft PUT answers 200), the authored form view shows in the New / Edit dialogs rather than on the opened record's page, and a non-admin list render of a runtime object needs a grant. No clause changed. Wave anchor #24579 §3", "ref": "#PENDING-24579R4" } ] }, { @@ -252,32 +260,32 @@ "title": "Record-page authoring round-trip: created bound to its object, seeded from the default layout, block-edited, published, rendered", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", - "personas": ["admin (authors)", "end user (opens the record)"], + "personas": ["admin (authors)", "end user (opens the record). On stock no non-admin persona reads showcase_contact (auditor.demo is refused 'You don't have access to Contact records', QA run #24604), so the viewer is the admin unless a grant is provisioned — e.g. a member bound to the `manager` position, whose showcase_manager set reads showcase_contact (examples/app-showcase/src/security/permission-sets.ts); record which"], "fixtures": { "app": "showcase", - "requires": ["seeded showcase_invoice records to render the published page against (stock showcase seed)"], + "requires": ["seeded showcase_contact records to render the published page against (stock showcase seed). ⛔ Not showcase_invoice: it ships the package default record page showcase_invoice_document (isDefault: true, examples/app-showcase/src/ui/pages/invoice-document.page.ts), which wins by design (objectui react usePageAssignment: the default record page, else the first), and authoring a second isDefault page answers 422 DEFAULT_RECORD_PAGE_CONFLICT. showcase_project, showcase_task and showcase_preference ship defaults too; showcase_contact ships no record page (QA runs #24571 and #24604)"], "knownGaps": [ "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." ] }, "steps": [ - "open /_console/apps/showcase_app/metadata/page/new and fill the create form: Label 'Invoice Page ' (slugifies into Name), set Page type = record (the create form defaults Page type to list — objectui metadata-admin anchors.ts, the page createSchema), Object showcase_invoice; Save", + "open /_console/apps/showcase_app/metadata/page/new and fill the create form: Label 'Invoice Page ' (slugifies into Name), set Page type = record (the create form defaults Page type to list — objectui metadata-admin anchors.ts, the page createSchema), Object showcase_contact (fixtures.requires[0]); Save", "capture the PUT /api/v1/meta/page/ request the save issues", - "assert the draft body: type:'record', object:'showcase_invoice', and regions seeded NON-empty from the object's synthesized default detail page (the page resource's createSeed hook) including a record:highlights block — never a blank canvas (ADR-0034 / objectui#1541)", - "open the page editor (/apps/showcase_app/metadata/page/): Basics and Layout sections render, and 'Add block' opens the block-type picker", + "assert the draft body: type:'record', object:'showcase_contact', and regions seeded NON-empty from the object's synthesized default detail page (the page resource's createSeed hook) including a record:highlights block — never a blank canvas (ADR-0034 / objectui#1541)", + "open the page editor (/apps/showcase_app/metadata/page/): the Basics, Data Context and Advanced panels render (the layout is edited on the canvas, not in a Layout section — see acceptance[1]'s stale-pin note), and 'Add block' opens the block-type picker", "add a block (e.g. Card), Save draft, Publish through the ResourceEditPage draft/publish chrome", "GET /api/v1/meta/page/: the published regions must include the added block", - "open a showcase_invoice record as an end user: the page assignment (usePageAssignment) renders the authored page over the synthesized default", + "open a showcase_contact record as the end user (personas[1]): the page assignment (usePageAssignment) renders the authored page over the synthesized default", "screenshot the editor canvas and the rendered record page" ], "acceptance": [ { "clause": "the created page persists bound to its object with PRE-SEEDED regions — record:highlights present, regions non-empty", "oracle": "network", - "verify": "the captured PUT body has type:'record', object:'showcase_invoice', Array.isArray(regions) with blocks including record:highlights. ⚠ Drive this by hand: the pin (objectui e2e/live/studio-record-page.spec.ts) is STALE against the current surface — it fills the Object control as an input when it is now a role=combobox button — see automated.stale", + "verify": "the captured PUT body has type:'record', object:'showcase_contact', Array.isArray(regions) with blocks including record:highlights. ⚠ Drive this by hand: the pin (objectui e2e/live/studio-record-page.spec.ts) is STALE against the current surface — it fills the Object control as an input when it is now a role=combobox button — see automated.stale", "evidence": "the captured PUT payload" }, { @@ -295,7 +303,7 @@ { "clause": "the published record page renders on a real record for the end user", "oracle": "screenshot", - "verify": "opening a showcase_invoice record shows the authored page including the added block", + "verify": "opening a showcase_contact record shows the authored page including the added block (a showcase_invoice record keeps its shipped default page, by design — fixtures.requires[0])", "evidence": "record-page screenshot" } ], @@ -334,6 +342,12 @@ "date": "2026-10-13", "change": "steps[0] sets Page type = record (stale step, #23166 A0; parked on #23121, carried by #23169). The page create form now defaults Page type to list, the primary interface-page type (objectui metadata-admin anchors.ts page createSchema at 1fb1ced36243, still this repo's .objectui-sha), so Label + Object + Save made a list page and steps[2]'s type:'record' draft body could not hold. QA run #23166 set the type and passed", "ref": "#23169" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "the page's object moved from showcase_invoice to showcase_contact (fixtures.requires[0], steps[0], steps[2], steps[6], acceptance[0].verify, acceptance[3].verify), with personas[1] and steps[3] beside it — §3 of wave anchor #24579 from QA run #24571 (5b14b7f3), confirmed by #24604 (ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. showcase_invoice ships showcase_invoice_document isDefault:true, which usePageAssignment serves ahead of any authored page and which makes a second default answer 422 DEFAULT_RECORD_PAGE_CONFLICT, so acceptance[3] could not pass on it; showcase_contact ships no record page and #24604 scored acceptance[3] pass there. No stock non-admin reads showcase_contact, so personas[1] names the grant or the admin. The page editor shows Basics / Data Context / Advanced, not a Layout section. Clause substance unchanged", + "ref": "#PENDING-24579R4" } ] }, @@ -342,7 +356,7 @@ "title": "Metadata draft→publish lifecycle: drafts staged not served, publish flips visibility atomically, conflicts and invalid drafts refused, and the history/audit/diff/rollback forensics tell the truth", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "mixed", "personas": ["admin (authors drafts)", "end user (must not see drafts)"], @@ -356,7 +370,7 @@ "steps": [ "author a dashboard draft: PUT /api/v1/meta/dashboard/qa_lifecycle_probe?mode=draft with one dataset-bound widget (dataset: showcase_task_metrics, dimensions: ['status'], values: ['task_count']) — the same door areas/dashboards.json exercises", "record the receipt: { success, version, seq, state: 'draft' }", - "BOTH sides pre-publish: GET /api/v1/meta/dashboard/qa_lifecycle_probe as the runtime read → 404 (never published) — the draft body must not be served; in Studio the item shows the 'Unpublished draft' chip and a numbered pill on the Changes button (objectui#12459). For the Studio half stage the draft in a WRITABLE package (?package=PKG on the draft PUT): the pill counts the pending drafts of the package in scope, and an env-wide, package-less draft is counted in no package scope, so it shows no pill. Open the item in Studio through that package: stage, in the same package, an app draft whose navigation names the dashboard (e.g. an entry of type dashboard, dashboardName qa_lifecycle_probe) and select that entry in the Interfaces pillar — a package with no app shows 'This package has no app yet' and no dashboard to select, so no chip can render; the pill then counts both drafts", + "BOTH sides pre-publish: GET /api/v1/meta/dashboard/qa_lifecycle_probe as the runtime read → 404 (never published) — the draft body must not be served; in Studio the item header shows the draft line 'Draft · N unpublished' (objectui#12725, studioItemDraftStatus.ts; N is the package's pending count) and a numbered pill on the Changes button (objectui#12459). For the Studio half stage the draft in a WRITABLE package (?package=PKG on the draft PUT): the pill counts the pending drafts of the package in scope, and an env-wide, package-less draft is counted in no package scope, so it shows no pill. Open the item in Studio through that package: stage, in the same package, an app draft whose navigation names the dashboard (e.g. an entry of type dashboard, dashboardName qa_lifecycle_probe) and select that entry in the Interfaces pillar — a package with no app shows 'This package has no app yet' and no dashboard to select, so no chip can render; the pill then counts both drafts", "publish per-ref: POST /api/v1/meta/dashboard/qa_lifecycle_probe/publish → 200; GET now serves the authored body", "author a SECOND draft revision on the same name and confirm the live read keeps serving revision 1 until that draft is published", "concurrency guard: re-PUT with the STALE version as If-Match and capture the 409", @@ -407,9 +421,9 @@ "evidence": "the refused publish + follow-up GET" }, { - "clause": "Studio chrome tells the truth: the 'Unpublished draft' chip and the numbered pill on the Changes button while pending, cleared after publish", + "clause": "Studio chrome tells the truth: the item header's 'Draft · N unpublished' line and the numbered pill on the Changes button while pending, cleared after publish", "oracle": "screenshot", - "verify": "for a draft staged in a writable package (an env-wide draft is counted in no package scope, so it shows no pill): chip and pill visible pre-publish, both gone post-publish and after a reload (the audit found this model 'reads consistently everywhere')", + "verify": "for a draft staged in a writable package (an env-wide draft is counted in no package scope, so it shows no pill): the draft line and the pill visible pre-publish, both gone post-publish and after a reload. A publish made outside Studio (the REST publish of steps[3]) shows in Studio only after a reload; Studio's own 'Publish all' clears them at once (QA run #24604) (the audit found this model 'reads consistently everywhere')", "evidence": "before/after Studio screenshots" }, { @@ -467,7 +481,8 @@ { "revision": 3, "date": "2026-08-11", "change": "recorded the run #7695 ledger note that publish-drafts' namespace-gate abort answers HTTP 200 with data.success:false, not a 4xx: the step and the clause now say capture status and body separately and score the abort off data.success (the atomicity itself was verified — the abort leaves both names 404). Logged in negatives as an OPEN ledger question awaiting one deliberate ruling (#7753 item 2) — deliberately RECORDED, not adjudicated: a runner asserts the current behaviour and does not file the status code as a defect", "ref": "#7753" }, { "revision": 4, "date": "2026-08-17", "change": "re-spell after [#7941]: the rollback step and the audit clause named X-Actor as an alternate actor source alongside the session identity; #7941 removed the header limb from resolveMetaWriteActor entirely (landed as commit 4dc8a618e, PR #9119) so it is now the SOLE source and X-Actor is ignored outright, not consulted even as a fallback. Re-spelled the rollback step, the audit acceptance clause, and the rest-server.ts source pointer to name the single source; added a new step + acceptance clause pairing the inverted check the finding asked for — send X-Actor and confirm it does NOT change the recorded actor — since that is now a meaningful QA-layer assertion nothing else covers (the unit pins live in packages/rest/src/meta-write-actor-identity.test.ts, added as a source pointer). The capability itself (audit rows name the acting user, never anonymous) is unchanged and was never wrong.", "ref": "#9118" }, { "revision": 5, "date": "2026-10-13", "change": "steps[9] and acceptance[8] (clause and verify) diff the two publish versions read off /history, and steps[8] and acceptance[7].verify say how history counts (assertion defect, #23166 A8; parked on #23121, carried by #23169). sys_metadata_history records draft saves and publishes alike, so ?from=1&to=2 compared the first draft save with its own publish (changed []); on QA run #23166's clean two-round run the published revisions were v2 and v4, and their diff named the edited widget key. The rollback steps are unchanged", "ref": "#23169" }, - { "revision": 6, "date": "2026-10-16", "change": "steps[2] and acceptance[6] (clause, verify) re-pointed (checklist-accuracy finding 4 of QA run #23942, wave #23878). The Studio counter is no longer 'Changes · n' text but a numbered pill on the Changes button (objectui#12459), and the pill counts the drafts of the package in scope — an env-wide (package-less) dashboard draft is counted in none, so #23942 staged the browser leg's draft in a writable package. The step and clause name the pill and the writable-package staging, and the step says how to reach the draft in Studio — through an app of that package whose navigation names the dashboard, since a package with no app offers no dashboard to select (the re-run on #24036 found no chip until it staged one). Not loosened: a chip or pill that survives the publish, or is missing while the draft is pending, still FAILS", "ref": "#24036" } + { "revision": 6, "date": "2026-10-16", "change": "steps[2] and acceptance[6] (clause, verify) re-pointed (checklist-accuracy finding 4 of QA run #23942, wave #23878). The Studio counter is no longer 'Changes · n' text but a numbered pill on the Changes button (objectui#12459), and the pill counts the drafts of the package in scope — an env-wide (package-less) dashboard draft is counted in none, so #23942 staged the browser leg's draft in a writable package. The step and clause name the pill and the writable-package staging, and the step says how to reach the draft in Studio — through an app of that package whose navigation names the dashboard, since a package with no app offers no dashboard to select (the re-run on #24036 found no chip until it staged one). Not loosened: a chip or pill that survives the publish, or is missing while the draft is pending, still FAILS", "ref": "#24036" }, + { "revision": 7, "date": "2026-10-19", "change": "steps[2], acceptance[6].clause and acceptance[6].verify corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: the Studio item header reads 'Draft · N unpublished' (engine.studio.itemDraftPending, objectui#12725) — 'Unpublished draft' is the chip of other surfaces (ObjectListViewInspector, the OWD overview), not this item header — and a REST publish shows in Studio only after a reload. The round's other draft notes are not taken: no step of this item discards a draft (so the ?state=draft discard spelling has no clause to correct), and the NS_ prefix a scratch dashboard was given is not required (the namespace-prefix rule judges objects only — packages/spec/src/kernel/namespace-prefix.ts). Wave anchor #24579 round 2 §3 (6272314386)", "ref": "#PENDING-24579R4" } ] }, { @@ -475,7 +490,7 @@ "title": "An invalid authored shape is rejected at save with a LOCATED error and is not persisted", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": ["admin"], @@ -488,9 +503,9 @@ }, "steps": [ "attempt PUT /api/v1/meta/object/qa_invalid_probe?mode=draft with a field MISSING its type; capture the status and full error body", - "attempt PUT /api/v1/meta/view/qa_invalid_views?mode=draft with `type`/`columns` at the CONTAINER level (the flat-list-view mistake); capture the guidance error", + "attempt PUT /api/v1/meta/view/qa_invalid_views?mode=draft with `type`/`columns` at the CONTAINER level (the flat-list-view mistake) BESIDE a container key (e.g. `list: {…}`), so the body is container-shaped and the container's guidance answers; capture the guidance error. A body of `type`/`columns` alone is judged as an inline view config instead and refused as naming no `object`/`viewKind` — the same 'Wrap it: defineView({ list: … })' remedy, different first sentence (packages/spec/src/ui/view.zod.ts INLINE_VIEW_OBJECT_REQUIRED / INLINE_VIEW_KIND_REQUIRED)", "after each rejection, GET the name back and record what the store holds", - "drive one invalid shape through the Studio designer (browser door) and capture the surfaced error — it must locate the offending field, not say a generic 'save failed'", + "drive one invalid shape through the browser door that reaches the server gate — the Setup metadata editor's Source tab (/_console/apps/setup/metadata/object/NAME), whose autosave PUTs it — and capture the surfaced error. The Studio designer holds an incomplete shape client-side and never sends it ('Not saved yet: …'), so it does not exercise the 422 — it must locate the offending field, not say a generic 'save failed'", "author the CORRECTED shapes (field with a type; views wrapped under list:) and confirm both save 200 — the gate rejects the shape, not the name", "dashboard-kind stray keys are deep-covered by dashboards.strict-widget-rejects-stray-keys — cite a pass there rather than re-enumerating the 11 legacy keys here" ], @@ -502,9 +517,9 @@ "evidence": "the error body" }, { - "clause": "a wrong-layer view container key is rejected with guidance naming where the key belongs ('`type` belongs to a single VIEW, not to the container. Wrap it: defineView({ list: { … } }) …')", + "clause": "a wrong-layer view container key is rejected with guidance naming where the key belongs — on a container-shaped body (steps[1]); a flat body gets the inline-config refusal carrying the same Wrap remedy ('`type` belongs to a single VIEW, not to the container. Wrap it: defineView({ list: { … } }) …')", "oracle": "api", - "verify": "the error text for container-level type/columns carries the ViewSchema guidance-map prescription, giving the author the fix", + "verify": "the error text for container-level type/columns carries the ViewSchema guidance-map prescription, giving the author the fix; if the run sent a flat body, the 'names no `viewKind`' / 'names no `object`' refusal with the same 'Wrap it: defineView({ list: … })' remedy is the equivalent answer, not a FAIL", "evidence": "the error text" }, { @@ -516,7 +531,7 @@ { "clause": "the browser door surfaces the same rejection visibly — a located error banner/toast, never a silent dead Save", "oracle": "screenshot", - "verify": "the Studio save attempt shows the validation error naming the offending field", + "verify": "the Source-tab save attempt (steps[3]) shows the validation error naming the offending field — a located banner (e.g. 'Fields → amount → type')", "evidence": "the error-state screenshot" }, { @@ -541,7 +556,8 @@ "history": [ { "revision": 1, "date": "2026-08-07", "change": "new item: authoring validation with located errors and verified non-persistence, sampling object + view kinds and cross-referencing the deepened dashboard stray-key item instead of duplicating it", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-11", "change": "recorded run #7695's sighting of the ALREADY-KNOWN spurious '_diagnostics' banner on a freshly saved VALID draft (the designer re-validates the server's own annotation on read-back) as a knownGap + a NOT-this-item's-FAIL negative, so the sighting is not filed as a new defect and the false positive is told apart from a real located-error miss. Note only; no clause changed", "ref": "#7753" }, - { "revision": 3, "date": "2026-09-23", "change": "re-spelled the located-error shape, which had stopped being true: the clause quoted '[invalid_metadata] … fields..type: Required', but saveMetaItem's spec-validation refusal no longer opens with a bracketed restatement of its code (retired in 7a25a3ee9c; the token rides code INVALID_METADATA), and on the REST and dispatcher doors its message is a headline of 'path [zod code]' locators with the per-issue prose on issues[]. Measured on c11852406 for a field missing its type: 'object/qa_invalid_probe failed spec validation: 1 issue — fields.amount.type [invalid_value]', 422, INVALID_METADATA. The dispatcher source pointer presented that test's hand-built mock message as the producer's text; it now says what the test pins, and the producer's per-face renderer is cited beside it. The capability itself (4xx, located path, nothing persisted) is unchanged", "ref": "#19709" } + { "revision": 3, "date": "2026-09-23", "change": "re-spelled the located-error shape, which had stopped being true: the clause quoted '[invalid_metadata] … fields..type: Required', but saveMetaItem's spec-validation refusal no longer opens with a bracketed restatement of its code (retired in 7a25a3ee9c; the token rides code INVALID_METADATA), and on the REST and dispatcher doors its message is a headline of 'path [zod code]' locators with the per-issue prose on issues[]. Measured on c11852406 for a field missing its type: 'object/qa_invalid_probe failed spec validation: 1 issue — fields.amount.type [invalid_value]', 422, INVALID_METADATA. The dispatcher source pointer presented that test's hand-built mock message as the producer's text; it now says what the test pins, and the producer's per-face renderer is cited beside it. The capability itself (4xx, located path, nothing persisted) is unchanged", "ref": "#19709" }, + { "revision": 4, "date": "2026-10-19", "change": "steps[1], steps[3], acceptance[1] (clause, verify) and acceptance[3].verify corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) The quoted container guidance answers only a container-shaped body; a flat `type`/`columns` body is judged as an inline view config and refused as naming no `object`/`viewKind`, with the same Wrap remedy (view.zod.ts). (2) The Studio designer holds an incomplete shape client-side and never PUTs it, so the browser door that reaches the server gate is the Setup metadata editor's Source tab, which the run used. Wave anchor #24579 §3", "ref": "#PENDING-24579R4" } ] }, { @@ -763,7 +779,7 @@ "title": "Permission-matrix editor UX: field-filter + bulk apply to EXACTLY the visible fields, and the Bulk column never clips at narrow widths", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": ["admin"], @@ -771,7 +787,7 @@ "app": "showcase", "requires": [ "a writable permission set to edit in the Permission Matrix editor (env-scope metadata-admin, or a package permission set in the Studio Access pillar) — a read-only source-loaded set can be opened to verify the read-only gate but not to exercise bulk writes", - "an object with MORE THAN 6 fields to expand in the matrix, so the field sub-table's filter renders (FieldsSubTable shows the filter only when fields.length > 6) — a stock showcase object with a wide field set (showcase_account / showcase_invoice)", + "an object with MORE THAN 6 fields to expand in the matrix, so the field sub-table's filter renders (FieldsSubTable shows the filter only when fields.length > 6) — a stock showcase object with a wide field set (showcase_account / showcase_invoice) on the environment door, or an object OF THE PACKAGE in the Studio Access pillar: in package scope the matrix lists only the package's own objects (objectui PermissionMatrixEditor.tsx, 'In package scope, list only the objects this package declares'), so a scratch package's set reads '0 objects granted' even when it grants showcase_account (QA run #24604)", "a narrow viewport for the Bulk-column clip check (B3)" ], "knownGaps": [ @@ -816,8 +832,8 @@ { "clause": "at a narrow viewport the object matrix container scrolls horizontally and the Bulk column (Read / CRUD / All / None) stays reachable — the min-w-[960px] table forces a scrollbar instead of clipping the last column off the right edge (objectui#2600 B3)", "oracle": "screenshot", - "verify": "a narrow-width screenshot shows a horizontal scrollbar and, after scrolling right, the full Bulk column", - "evidence": "the narrow-viewport screenshot(s)" + "verify": "at a narrow width the matrix container's scrollWidth exceeds its clientWidth and, after a horizontal scroll (scrollLeft > 0), a screenshot shows the full Bulk column. A painted scrollbar is not required: headless Chromium uses overlay scrollbars that paint none (QA run #24604) — the scroll is the evidence", + "evidence": "the narrow-viewport screenshot(s) before and after the scroll + the scrollWidth / clientWidth reading" }, { "clause": "the read-only gate holds: a read-only set hides Save and disables every bulk button + checkbox; the read-only badge names WHICH gate holds", @@ -857,6 +873,12 @@ "date": "2026-10-16", "change": "acceptance[5] (clause, verify) re-pointed (checklist-accuracy finding 7 of QA run #23942, wave #23878). The clause distinguished only a package gate and a type gate, but the console names more read-only reasons — #23942 saw three (package, artifact, type) and the pin also carries the wall (objectui#12183) — and the type gate is unreachable on stock. The verify text now names each gate's key, the two reachable on stock (package in Studio Access, artifact in the metadata admin, both on showcase_contributor), and records the other two as not reachable on stock. Not loosened: Save must still be absent, every bulk control disabled, and the badge must name the right gate", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "fixtures.requires[1] and acceptance[4] (verify, evidence) corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: in package scope the Studio Access matrix lists only the package's own objects (PermissionMatrixEditor.tsx), so the wide stock objects the fixture named are reachable only on the environment door; and headless Chromium paints no scrollbar, so the scroll is evidenced by scrollWidth against clientWidth and the scrolled screenshot. Clause substance unchanged. Wave anchor #24579 §3", + "ref": "#PENDING-24579R4" } ] }, @@ -1311,7 +1333,7 @@ "title": "The metadata governance sweep: GET /meta/diagnostics reports every STORED item failing spec validation with located errors, refuses outages and bad spellings loudly instead of answering '0 problems', and the Studio diagnostics page + directory tile badges paint the same numbers", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", "personas": ["admin (the governance surface is Studio's)"], @@ -1322,7 +1344,7 @@ "the stock metadata corpus (any booted showcase — the sweep runs over whatever the store holds)" ], "knownGaps": [ - "THE POSITIVE ENTRY LEG IS FIXTURE-BLOCKED ON STOCK, BY CONSTRUCTION: every authoring door validates at save (the 422 gate studio-authoring.authoring-validation-not-persisted proves), so a stock boot's store holds NO invalid item and the sweep legitimately answers clean — entries:[], total:0. An invalid STORED row exists only via spec evolution (a row saved under an older spec the current schema rejects — the population POST /meta/_migrate-stored exists for), and sys_metadata is a storage table, not a data-plane object, so a runner cannot plant one through /api/v1/data either. The invalid-entry clause therefore scores blocked(fixture) until a stale-row fixture exists (a pre-seeded DB snapshot from an older spec revision is the honest recipe candidate); the CLEAN-store empty state is the live positive in the meantime, and the outage/spelling refusals are pinned at the unit layer. The environment the invalid-entry leg needs is a store that already holds such a row. A stock environment never does, by design (QA run #21784, parked on #22017 row 17), and no CI job builds one. The located-error half is unit-pinned at the producer: packages/metadata-protocol/src/metadata-diagnostics.union-issues.test.ts (computeMetadataDiagnostics names the offending key per union branch, and decorateMetadataItem carries it). That pin scores neither the sweep entry nor the page's deep link", + "THE POSITIVE ENTRY LEG NEEDS A PLANTED ROW, AND ONE CAN BE PLANTED ON AN ISOLATED SCRATCH FILE DB (QA run #24604): every authoring door validates at save (the 422 gate studio-authoring.authoring-validation-not-persisted proves), so a stock store holds NO invalid item and the sweep legitimately answers clean — entries:[], total:0 — and sys_metadata is a storage table, not a data-plane object, so /api/v1/data cannot plant one. The recipe: save a VALID view through the door WITH ?package= (DirectoryPage is always package-scoped, so a package-less row never reaches a tile badge), then rewrite that row's sys_metadata.metadata in the scratch SQLite file (e.g. config.columns[0].field = 123); the next sweep reports it with no restart, and deleting the row restores a clean sweep. ⛔ Only on the run's own file DB, never a shared one. The located-error half is also unit-pinned at the producer: packages/metadata-protocol/src/metadata-diagnostics.union-issues.test.ts (computeMetadataDiagnostics names the offending key per union branch, and decorateMetadataItem carries it). That pin scores neither the sweep entry nor the page's deep link", "severity=warning ADDS NO ROWS ON TODAY'S PRODUCERS: the sweep's contract includes warning-only entries (protocol.ts getMetaDiagnostics: valid items with diagnostics.warnings), but computeMetadataDiagnostics emits errors only and the docstring reserves warnings 'for a future lint layer' — no read-decoration producer emits a warning at head. The severity clause asserts the MECHANISM (param accepted, error rows unchanged), not warning rows; a warning row appearing is a finding that the lint layer landed (revise this item), never noise", "REPO OWNERSHIP (ADR-0054): the API half — route, sweep, stats, refusals — is objectstack's; the page markup, grouping, deep links, badges are objectui's (metadata-admin/DiagnosticsPage.tsx, DirectoryPage.tsx, useMetadata.ts). A locator or copy fix goes to objectui" ] @@ -1334,9 +1356,9 @@ "package scoping: GET /api/v1/meta/diagnostics?package=com.example.showcase and record how stats narrows; then open Studio's metadata directory (/apps//metadata?package=com.example.showcase) and compare the tile counts against the scoped stats (DirectoryPage feeds its tiles from the package-scoped sweep)", "severity mechanism: GET /api/v1/meta/diagnostics?severity=warning — capture that it answers 200 and that the error entries are unchanged (no warning producer exists at head; see knownGaps)", "refusal probes: GET /api/v1/meta/diagnostics?type=viewes (an unrecognised spelling of a DECLARED type) and capture the 400; GET /api/v1/meta/diagnostics?severity=error&severity=warning (a repeated query param) and capture that refusal too", - "browser, clean store: open /apps//metadata/_diagnostics — screenshot it settled; on a clean corpus the DESIGNED empty state must render (title + the scanned-items/types counts), never a blank region; back on the directory page confirm the red diagnostics-link button is ABSENT (it renders only when total > 0)", + "browser, clean store: open /apps//metadata/_diagnostics — screenshot it settled; on a clean corpus the DESIGNED empty state must render (title + the scanned-items/types counts), never a blank region; back on the directory page confirm the red diagnostics-link button is ABSENT (it renders only when total > 0). The directory always repairs ?package= to a project package, so the button is judged per package scope", "outage clause: run the unit pin (packages/metadata-protocol/src/protocol.diagnostics-store-outage.test.ts) and cite its output — driving a live store outage is not attempted", - "blocked leg (only when an invalid-stored-row fixture exists): re-GET the sweep — the planted row must appear as { type, name, diagnostics: { errors: [{ path, message }] } } with the LOCATED path, the page must group it under its type with a deep link to ..// (the item's editor), and the directory badge count must include it" + "invalid-row leg (plant the row first, fixtures.knownGaps[0]): re-GET the sweep — the planted row must appear as { type, name, diagnostics: { errors: [{ path, message }] } } with the LOCATED path, the page must group it under its type with a deep link to ..// (the item's editor), and the directory badge count must include it" ], "acceptance": [ { @@ -1376,9 +1398,9 @@ "evidence": "the two screenshots + the API body's counts" }, { - "clause": "an invalid STORED item surfaces with a located error and a working deep link: the sweep lists it as type+name+errors[{path,message}] (union failures named per-branch via zodIssuesToMetadataIssues — never one rootless 'Invalid input', #5598), the page groups it under its type, and the row's link lands on that item's editor — blocked(fixture) on stock (see knownGaps: the save gates make an invalid stored row unplantable in-run)", + "clause": "an invalid STORED item surfaces with a located error and a working deep link: the sweep lists it as type+name+errors[{path,message}] (union failures named per-branch via zodIssuesToMetadataIssues — never one rootless 'Invalid input', #5598), the page groups it under its type, and the row's link lands on that item's editor — on a row planted on the run's own scratch file DB (knownGaps[0]); the save gates keep stock clean", "oracle": "api", - "verify": "with the stale-row fixture: the entry's diagnostics.errors carry the failing path; clicking the row navigates to ..// (the ResourceEditPage for the item); the DirectoryPage invalid badge for that type increments to match", + "verify": "with the planted row: the entry's diagnostics.errors carry the failing path (rootless union-parent entries — path '' or config.columns, 'Invalid input' — may ride beside the located leaf; the clause needs the located leaf present, not their absence); clicking the row navigates to ..// (the ResourceEditPage for the item); the DirectoryPage invalid badge for that type increments to match", "evidence": "the sweep entry + the deep-link navigation screenshot" }, { @@ -1413,7 +1435,8 @@ { "revision": 1, "date": "2026-08-30", "change": "new item (2026-08-30 sweep, angle 4): the governance sweep GET /meta/diagnostics + its two Studio surfaces had no coverage — nothing asserted the stats/tile parity seam, the #8924 spelling refusal (whose regression the page paints as 'All clear'), the #8855 outage honesty, or the clean-store empty state. Grounded against source with two corrections to the register hypothesis: the scoping query param is ?package= (not ?packageId= — that is the protocol-layer argument name), and severity=warning cannot 'add warning-only entries' on stock because no producer emits warnings at head (reserved for a future lint layer) — authored as a mechanism clause with the lint-layer tripwire recorded. The invalid-stored-row positive is blocked(fixture) by construction (save doors validate; sys_metadata is not a data-plane object), with the stale-row snapshot named as the honest fixture candidate", "ref": "#sweep-2026-08-30" }, { "revision": 2, "date": "2026-10-06", "change": "partial-coverage gap of QA run #21784 parked on #22017 (row 17). A7 was blocked(fixture): an invalid stored row cannot be planted, because every authoring door validates at save. Disposition: recorded knownGap. The leg cannot be reached on a stock environment by design. The knownGap now names the environment it needs (a store already holding a stale row), says no CI job builds one, and points at the producer-level pin as mechanism-only. No clause changed", "ref": "#22017" }, { "revision": 3, "date": "2026-10-13", "change": "acceptance[1].verify compares stats.locked against the served envelope lock (stale spec, #23166 A1; parked on #23121, carried by #23169). getMetaDiagnostics counts an item as locked when its read envelope reports a lock other than 'none' — the same derivation getMetaItem publishes (#21694) — not when its declared _lock is set. QA run #23166 compared against the envelope lock and passed. Read from source at 6d7f4df3c (protocol.ts getMetaDiagnostics docblock and loop). The A6 row of the same run, the unplantable invalid stored row, is already recorded in full in knownGaps[0] and needs no edit", "ref": "#23169" }, - { "revision": 4, "date": "2026-10-16", "change": "acceptance[1].verify names the read that carries the envelope lock (checklist-accuracy finding 2 of QA run #23942, wave #23878). It told the runner to compare stats.locked against the lock GET /api/v1/meta/TYPE/NAME publishes, but that default read is the cached arm and carries no `lock` by design; the envelope lock is on GET /api/v1/meta/TYPE/NAME/layers, which is where #23942 read it (object 107, view 0, flow 32 locked, matching stats). The comparison itself is unchanged", "ref": "#24036" } + { "revision": 4, "date": "2026-10-16", "change": "acceptance[1].verify names the read that carries the envelope lock (checklist-accuracy finding 2 of QA run #23942, wave #23878). It told the runner to compare stats.locked against the lock GET /api/v1/meta/TYPE/NAME publishes, but that default read is the cached arm and carries no `lock` by design; the envelope lock is on GET /api/v1/meta/TYPE/NAME/layers, which is where #23942 read it (object 107, view 0, flow 32 locked, matching stats). The comparison itself is unchanged", "ref": "#24036" }, + { "revision": 5, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[6], steps[8] and acceptance[6] (clause, verify) corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), the round-2 §3 note that knownGaps[0] is wrong. The invalid stored row is plantable in-run on an isolated scratch file DB: save a valid view through the door with ?package=, rewrite its sys_metadata.metadata, and the next sweep lists it — the run scored acceptance[6] pass that way, deep link and badge included. The directory always repairs ?package= to a project package, so the red button is judged per package scope; rootless union-parent entries may ride beside the located leaf. Wave anchor #24579 round 2 §3 (6272314386)", "ref": "#PENDING-24579R4" } ] }, { @@ -1421,7 +1444,7 @@ "title": "A doc on the app menu follows the reader's audience: an admin authors a doc and puts it (or its book) on an app menu; a member who may read it sees it and opens it in its book; a member outside its audience does not see the entry and gets a refusal — never \"not found\", never a blank page — on the direct route", "since": "v17.5", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": [ @@ -1445,7 +1468,7 @@ ] }, "steps": [ - "boot the showcase isolated (dogfood §0) with the console named above; as admin, verify the stock corpus first (knownGaps), then create the doc in Studio (metadata admin, doc editor) and publish it; author the remaining fixture books/docs and the permission set, assign SET to the reader", + "boot the showcase isolated (dogfood §0) with the console named above; as admin, verify the stock corpus first (knownGaps), then author the doc in the writable base package over the metadata API (PUT /api/v1/meta/doc/DOC?package=PKG, then publish) — knownGaps[2]'s recipe, so the doc shares the books' package, rather than through the Studio doc editor; author the remaining fixture books/docs and the permission set the same way, and assign SET to the reader", "as admin, add the three doc entries to the app navigation (Studio app designer, or PUT /api/v1/meta/app/APP) over the gated and the open corpus", "GET /api/v1/meta/app?id=APP as each member in that member's own session (prove the identity with GET /api/v1/auth/get-session first); record which doc entries each receives", "as each member, open the app and capture the rendered menu after a screenshot confirms it settled; diff the rendered doc entries against that member's /meta/app answer (UI mirror)", @@ -1501,7 +1524,8 @@ "history": [ { "revision": 1, "date": "2026-09-30", "change": "new — the 清单项待写 row of docs/NORTH-STAR.md beside platform-core.docs-audience-gate and platform-core.docs-portal-render; drafted with objectui#10188 and corrected by the at-tier review of objectui PR 11258 (the UI-mirror clause scoped to entries naming an installed doc or book; no signed-out persona — /docs sits behind ProtectedRoute)", "ref": "#20939" }, { "revision": 2, "date": "2026-10-13", "change": "knownGaps[2]'s fixture recipe re-pointed (knownGap stale, #23166 A0; parked on #23121, carried by #23169). The Studio doc editor no longer answers the read-only showcase package with a 422: it saves an env-wide draft and offers no package picker, so the recipe's reason for a writable base changed, not its need. The books and docs must share one package, so the recipe drives the writable base over the metadata API with ?package=. QA run #23166 did exactly that and passed", "ref": "#23169" }, - { "revision": 3, "date": "2026-10-16", "change": "knownGaps[1] closed (checklist-accuracy finding 9 of QA run #23942, wave #23878): the .objectui-sha pin has been past the fork window since daeb77fc84dd — the console carries the refusal page and the menu pruning — so the knownGap no longer asks for a console built elsewhere. Wording only: no step, request or oracle changed", "ref": "#24036" } + { "revision": 3, "date": "2026-10-16", "change": "knownGaps[1] closed (checklist-accuracy finding 9 of QA run #23942, wave #23878): the .objectui-sha pin has been past the fork window since daeb77fc84dd — the console carries the refusal page and the menu pruning — so the knownGap no longer asks for a console built elsewhere. Wording only: no step, request or oracle changed", "ref": "#24036" }, + { "revision": 4, "date": "2026-10-19", "change": "steps[0] aligned with fixtures.knownGaps[2] from QA run #24571 (18.0 sweep, 5b14b7f3): the step said to create the doc in the Studio doc editor while knownGaps[2] prescribes authoring every book, doc and the app over the metadata API with ?package= so they share one package; the run drove the API recipe. No clause changed. Wave anchor #24579 §3", "ref": "#PENDING-24579R4" } ] }, { @@ -1680,7 +1704,7 @@ "title": "One item name, one stored row PER PACKAGE (ADR-0048) on v18: a package-less save resolves its owning package and an ambiguous one is 409, a dropStorage delete while another package's row remains is 409 and keeps the table, a save into the wrong package is 422 OBJECT_OVERLAY_PACKAGE_MISMATCH, a sealed packaged name answers one 403 NOT_OVERRIDABLE sentence, and revert discards a Studio package's drafts", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -1696,9 +1720,9 @@ "steps": [ "PUT /api/v1/meta/view/showcase_task.qa_shared_view with no ?package= — expect 409 (two holders)", "DELETE /api/v1/meta/view/showcase_task.qa_shared_view?package=A; then re-save without ?package= — expect it to update B's row", - "PUT /api/v1/meta/object/showcase_task?package=A (a package that does not own it)", + "author an object package A owns at runtime (a Studio-authored object in A), then PUT /api/v1/meta/object/?package=B (a package that does not own it) — expect 422 OBJECT_OVERLAY_PACKAGE_MISMATCH. ⛔ Not a showcase object such as showcase_task: it is code-packaged and sealed, and the package door answers 403 NOT_OVERRIDABLE ahead of the D9.9 check (packages/objectql/src/protocol-runtime-owner-overlay-refusal.test.ts, its code-owner control; QA run #24558)", "PUT a sealed packaged name (any showcase object) as a create; read the 403 sentence", - "publish package A, edit to a draft, POST /api/v1/packages/A/revert; then revert a never-published package" + "publish package A through POST /api/v1/packages/A/publish-drafts, edit an item to a draft, POST /api/v1/packages/A/revert; then revert a never-published package. ⛔ Not POST /api/v1/packages/A/publish: it collects the in-memory registry's members, which a Studio package's stored rows and drafts are not, and answers 200 {success:false, itemsPublished:0, 'No metadata items found for package …'} (packages/metadata/src/metadata-manager.ts publishPackage; QA run #24558)" ], "acceptance": [ { @@ -1751,6 +1775,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; the sealed-name sentence was rewritten twice on the line (22591 → 23406 → 23577): assert the 23577 one-sentence form", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "steps[2] and steps[4] corrected from QA run #24558 (18.0 since:v18 sweep, 5b14b7f3), re-read at objectstack f89d29e382. (1) A save into a non-owning package of showcase_task cannot reach OBJECT_OVERLAY_PACKAGE_MISMATCH: the sealed code object answers 403 NOT_OVERRIDABLE first, which the item's own pin protocol-runtime-owner-overlay-refusal.test.ts holds as its code-owner control; the 422 needs a runtime object another package owns. (2) POST /packages/:id/publish collects only in-memory registry members (metadata-manager.ts), so for a Studio package it answers success:false 'No metadata items found'; the published version revert needs comes from publish-drafts, the door the revert pin's own measured recipe uses. Clause substance unchanged", + "ref": "#PENDING-24579R4" } ] }, @@ -1759,7 +1789,7 @@ "title": "The metadata save door runs the build's authoring rules on v18 — a page with an unknown component prop, a page with both details and tabs slots, a second default record page, a chart list view with no dataset, an object predicate that does not compile, a `protection` block or a lock change, and a non-http(s) URL target are each refused 4xx at PUT (drafts still admitted where the rule says so)", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -1789,9 +1819,9 @@ "evidence": "the vitest summary line per pin file, with the package name and the subject sha" }, { - "clause": "each refused body answers 422 INVALID_METADATA (400 VALIDATION_ERROR for protection / _lock*, 422 DEFAULT_RECORD_PAGE_CONFLICT naming both pages) with the rule id and located path, and a read-back shows nothing stored", + "clause": "each refused body answers 422 INVALID_METADATA (400 VALIDATION_ERROR for protection / _lock*, 422 DEFAULT_RECORD_PAGE_CONFLICT naming both pages) with the located path — and the rule id where the refusal comes from an authoring lint rule (the component-props and object-predicate refusals) — and a read-back shows nothing stored", "oracle": "api", - "verify": "status + code + rule id + path per PUT; GET afterwards", + "verify": "status + code + path per PUT, and the rule id on the lint-rule refusals; GET afterwards. The spec-schema refusals (the slots pair, the chart binding, the action URL target) carry the located path and zod code 'custom' with no `rule` key (e.g. packages/spec/src/ui/page.zod.ts checkPageSlotPair), and protection / _lock* and DEFAULT_RECORD_PAGE_CONFLICT carry their error code: a missing rule id on those is the shipped shape, not a FAIL (QA run #24558; the pins assert path and code only)", "evidence": "one line per PUT" }, { @@ -1828,6 +1858,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[1] (clause, verify) corrected from QA run #24558 (18.0 since:v18 sweep, 5b14b7f3) — §3's since:v18 bullet 'save-door spec refusals carry no rule id' — re-read at objectstack f89d29e382: the spec-schema refusals are zod custom issues with a located path and no rule key (page.zod.ts checkPageSlotPair is one), protection / _lock answer 400 VALIDATION_ERROR and the default-page conflict its own code, and only the authoring-lint refusals name a rule id. The clause demanded a rule id on every refusal. Wave anchor #24579 §3 (round 3 named this item)", + "ref": "#PENDING-24579R4" } ] } From 2c7430d086f5f1d933abfd32b2853b1c44a55e1e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 22:12:23 +0000 Subject: [PATCH 4/5] =?UTF-8?q?docs(qa):=2018.0=20sweep=20anchor=20round?= =?UTF-8?q?=204=20=E2=80=94=20records-forms:=20nineteen=20items=20re-point?= =?UTF-8?q?ed=20to=20the=20shipped=20console=20and=20server=20(wire=20para?= =?UTF-8?q?ms=20without=20a=20$,=20three=20densities,=20the=20server-backe?= =?UTF-8?q?d=20mention=20bell,=20hook=20route=20and=20refs,=20the=20shippe?= =?UTF-8?q?d=20500=20for=20an=20unevaluable=20hook=20condition,=20autonumb?= =?UTF-8?q?er=20off=20the=20form,=20the=20crypto=20provider=20on=20every?= =?UTF-8?q?=20CLI=20boot,=20and=20run-proven=20fixture=20recipes)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checklist ledger only. Nineteen records-forms items take one revision each, inside the dispatch's 20-item cap; every finding re-read at objectstack f89d29e382 and the console pin 4016370af3c9. The object-hook ref now names four test files measured at base (83 passed). Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/records-forms.json | 291 ++++++++++++------ 1 file changed, 204 insertions(+), 87 deletions(-) diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index 9f67c3a733b..b075030d1d2 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -66,7 +66,7 @@ "title": "Create → read → update → delete a record through the console UI", "since": "v15", "status": "active", - "revision": 10, + "revision": 11, "priority": "P0", "surface": "browser", "personas": [ @@ -79,7 +79,7 @@ ], "provisioning": { "use": "search:qa-contributor-bound-member", - "why": "supplies the persona clause 7 (clone is RLS-gated) rests on and that no other clause here needs — a reader for whom a SOURCE row is invisible. showcase_account, the object the rest of this item drives, is public_read_write, so no persona can fail to read a row on it and the clause is unscoreable there; clause 7 is driven on showcase_invoice against INV-1003 (linus@example.com's) instead. Without the recipe the clause scores blocked(fixture) — which is what the 17.1.0 sweep recorded after reaching for the seeded demo personas and cloning showcase_account as Mei, who reads it fine (#10236)." + "why": "supplies the persona clause 7 (clone is RLS-gated) rests on, and the non-owner cloner clause 6's ownership half needs (cloning a seeded showcase_field_zoo row, acceptance[6].verify) — a reader for whom a SOURCE row is invisible. showcase_account, the object the rest of this item drives, is public_read_write, so no persona can fail to read a row on it and the clause is unscoreable there; clause 7 is driven on showcase_invoice against INV-1003 (linus@example.com's) instead. Without the recipe the clause scores blocked(fixture) — which is what the 17.1.0 sweep recorded after reaching for the seeded demo personas and cloning showcase_account as Mei, who reads it fine (#10236)." }, "knownGaps": [ "Clause 7 (clone is RLS-gated) cannot be scored on showcase_account, the object every other clause here drives: it is public_read_write, so there is no persona for whom a source row is invisible. It needs showcase_invoice plus a contributor-bound member — the recipe qa-contributor-bound-member, which areas/search.json owns. That reference now lives in fixtures.provisioning as the AREA-QUALIFIED `search:qa-contributor-bound-member`, and check:platform-checklist RESOLVES it (#10593 gap 2, maintainer ruling 2026-08-22 — option A): both spellings are valid, an own-area key and `:` for one another area owns. So this is a real opt-in rather than a prose pointer — rename or move the recipe and this item fails the gate by name, instead of leaving a sentence aimed at nothing. ⛔ Replay it from areas/search.json; do not fork a second copy into this area." @@ -88,7 +88,7 @@ "steps": [ "sign in as seeded admin; open Accounts via its nav entry (/_console/apps/com.example.showcase/showcase_account)", "click New; fill name with a distinctive value (os-qa-), industry, status=active; leave website empty; Save", - "capture the create request/response (expect POST /api/v1/data/showcase_account)", + "capture the create request/response (expect POST /api/v1/data/showcase_account). The payload also carries the select defaults of fields the step leaves untouched — sales_region:'amer' (its AMER option is default:true, examples/app-showcase/src/data/objects/account.object.ts) — so expect that key in the body you diff (QA run #24556)", "re-read server-side: GET /api/v1/data/showcase_account?$filter=[[\"name\",\"=\",\"os-qa-\"]] — field-by-field vs the submitted payload", "open the record detail; edit exactly one field (annual_revenue); Save; capture the PATCH /api/v1/data/showcase_account/; re-read the full row via GET /api/v1/data/showcase_account/", "open the record's History tab (screenshot first, then read entries)", @@ -101,7 +101,7 @@ { "clause": "create returns success with an id and the API re-read shows every submitted value verbatim (no silent coercion/loss on any field)", "oracle": "api", - "verify": "field-by-field diff of the POST payload vs the filtered GET re-read; the empty optional (website) stays empty, not defaulted", + "verify": "field-by-field diff of the POST payload (the form's defaulted keys such as sales_region:'amer' included — steps[2]) vs the filtered GET re-read; the empty optional (website) stays empty, not defaulted", "evidence": "create payload + re-read JSON" }, { @@ -137,7 +137,7 @@ { "clause": "clone (POST /data/:object/:id/clone, gated by enable.clone default-on) returns 201 with a NEW id and the source's field VALUES copied, but engine-owned columns (id, audit, autonumber, formula, summary) and readonly columns RE-DERIVED not carried, and the clone is owned by the CLONER — not the source's owner. ⚠️ The readonly half is UNOBSERVABLE on the stock seed: showcase_account declares no readonly column, no showcase object has an approval_status, and the showcase's one readonly field (showcase_contact.lead_score) is null on every seeded row, so carried and re-derived read identically. Score that half from the pins in `automated`, never as blocked(fixture) and never off two reads that agree only because the column is null: no live run can stage it, because the server drops a readonly column set over HTTP (`x-objectstack-dropped-fields: lead_score;reason=readonly`, QA run #21782)", "oracle": "api", - "verify": "the 201 result carries {id (new ≠ sourceId), sourceId, record}; field-by-field diff shows business values copied and system/readonly columns re-derived (#3043 CLONE_STRIP_FIELDS + the engine's create-side static-readonly strip, #14147); owner_id resolves to the signed-in cloner (the clone is a create in the caller's context — packages/metadata-protocol/src/protocol.ts cloneData)", + "verify": "the 201 result carries {id (new ≠ sourceId), sourceId, record}; field-by-field diff shows business values copied and system/readonly columns re-derived (#3043 CLONE_STRIP_FIELDS + the engine's create-side static-readonly strip, #14147); owner_id resolves to the signed-in cloner (the clone is a create in the caller's context — packages/metadata-protocol/src/protocol.ts cloneData). ⚠ The owner half needs a cloner who is NOT the source's owner: the admin cloning a row the admin created reads the same owner_id either way (QA run #24572). Drive it as the provisioned contributor-bound member cloning a seeded showcase_field_zoo row (QA run #24572's suggestion, source-derived and not yet run — record the substitution): showcase_contributor grants allowCreate there (not on showcase_account, which it may only read; examples/app-showcase/src/security/permission-sets.ts), the zoo is public_read_write, and the same clone shows f_autonumber and f_formula re-derived", "evidence": "clone response + source-vs-clone field diff + owner_id read" }, { @@ -150,7 +150,7 @@ "negative": [ "save with the required Account Name empty → the form blocks with a named field error AND no row is created (filtered API count stays 0) — a silent success is a FAIL. ⛔ Not the Lifecycle (status) select: it offers no empty option and defaults to Prospect, so a status-empty save cannot be staged in the form", "a clone that carries over the source's id / audit columns / an autonumber / a readonly approval_status instead of re-deriving them is a FAIL (#3043 — a clone must not mint an already-approved record); a clone of an RLS-invisible source that SUCCEEDS (200/201 with a new row) is a FAIL — the findOne runs in the caller's context precisely to refuse it", - "a direct API POST missing required status on an object whose required field has NO default → 400 VALIDATION_FAILED with fields[] carrying code 'required' (server enforces, not just the form — packages/objectql/src/validation/record-validator.ts). ⛔ Do NOT drive this on showcase_account: its status is required:true but the 'prospect' option carries default:true (data/objects/account.object.ts), so the default satisfies the requirement and the row is created with status:'prospect' — a 201 here is CORRECT behaviour, not a missing server guard. Pick a required field with no defaulted option, or record the substitution", + "a direct API POST missing a required field that has NO default — on showcase_account that is `name` (QA run #24572) — → 400 VALIDATION_FAILED with fields[] carrying code 'required' (server enforces, not just the form — packages/objectql/src/validation/record-validator.ts). ⛔ Do NOT drive this on showcase_account: its status is required:true but the 'prospect' option carries default:true (data/objects/account.object.ts), so the default satisfies the requirement and the row is created with status:'prospect' — a 201 here is CORRECT behaviour, not a missing server guard. Pick a required field with no defaulted option, or record the substitution", "a create with tax_id violating the tax_id_format rule → named validation error, never a silently-stored bad value" ], "traps": [ @@ -225,6 +225,12 @@ "date": "2026-10-16", "change": "negative[0] re-pointed (checklist-accuracy finding of QA run #23949, wave #23878): the Lifecycle select has no empty option and defaults to Prospect, so 'save with required status empty' could not be staged; the negative now empties the required Account Name, which the form can leave empty. Not loosened: the form must still block with a named error and create no row", "ref": "#24036" + }, + { + "revision": 11, + "date": "2026-10-19", + "change": "steps[2], acceptance[0].verify, acceptance[6].verify, negative[2] and fixtures.provisioning.why corrected from QA run #24572 (18.0 sweep, 5b14b7f3) and the P0 run #24556, re-read at objectstack f89d29e382. (1) The New form submits select defaults the step never touches (sales_region:'amer', default:true in account.object.ts), so the payload diff expects that key. (2) acceptance[6]'s ownership half is unobservable when the admin clones its own row; the provisioned contributor member cloning a seeded showcase_field_zoo row (allowCreate there, permission-sets.ts) is a non-owner cloner, and the same clone shows the autonumber and formula re-derived. (3) negative[2] named `status`, which defaults on showcase_account; the required field with no default there is `name`. The round-2 Tier 1 note that this item's automated.ref over-claims does not hold and is not taken: the ref already scopes itself to 'acceptance[6]'s READONLY half ONLY … Every other clause still needs the run'. Wave anchor #24579 (round 3 named this item)", + "ref": "#PENDING-24579R4" } ] }, @@ -233,7 +239,7 @@ "title": "Field-type matrix: every FieldTypeSchema member renders its widget, accepts a valid value, round-trips over HTTP, and rejects invalid input", "since": "v15", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": [ @@ -247,7 +253,7 @@ "knownGaps": [ "summary is NOT on the zoo (a roll-up needs a child object and the zoo is a leaf) — verify it on its real carriers showcase_invoice.total and showcase_expense_report.total_amount/approved_amount instead", "f_user / f_users / f_owner cannot be seeded or written on a fresh boot with no signed-up users (sys_user rows come from sign-up, not seeds) — assign after creating a user, or record blocked(fixture)", - "f_secret writes fail closed unless an ICryptoProvider is registered (ADR-0100) — the seed deliberately omits it; on a stock boot verify the masked-read contract via the dogfood pin, not a raw UI write" + "STALE on a CLI boot (QA run #24616): f_secret writes fail closed only when no ICryptoProvider is registered (ADR-0100), and `os dev` / `serve` wire a LocalCryptoProvider on every boot (packages/cli/src/commands/serve.ts, the secret-field crypto wiring: 'Dev/self-host gets a LocalCryptoProvider'), so a raw PATCH of f_secret answers 200 and reads back masked. Only a host composed WITHOUT that provider refuses the write — there, verify the masked-read contract via the dogfood pin" ] }, "variants": [ @@ -304,7 +310,7 @@ "steps": [ "run the pinned HTTP round-trip matrix: pnpm --filter @objectstack/dogfood exec vitest run test/field-zoo-roundtrip.dogfood.test.ts (write vectors + expected read shapes live in test/field-zoo.matrix.ts)", "boot showcase isolated; open Field Zoo (/_console/apps/com.example.showcase/showcase_field_zoo) and open 'Specimen — Full' in edit mode; screenshot the form before reading DOM", - "enumerate the rendered control for every f_* field and build a variant→widget table (date→date input, color→color input, richtext / markdown / html / code→their dedicated field widgets, select/multiselect→pickers, image/file→upload, location/address→structured inputs, rating/slider/progress→their own controls, autonumber/formula→read-only). At the console pin the richtext, markdown, html and code widgets are textarea-based BY DESIGN (objectui packages/fields/src/widgets/RichTextField.tsx / CodeField.tsx: a Textarea inside the dedicated widget, with its own label and controls): record that as the dedicated widget, not as a generic text input", + "enumerate the rendered control for every f_* field and build a variant→widget table (date→date input, color→color input, richtext / markdown / html / code→their dedicated field widgets, select/multiselect→pickers, image/file→upload, location/address→structured inputs, rating/slider/progress→their own controls, formula→read-only). f_autonumber is NOT on the form at all: the field is readonly by construction (Field.autonumber sets readonly: true, packages/spec/src/data/field.zod.ts) and objectui's auto-form drops schema-readonly fields in create AND edit mode (plugin-form autoLayout.ts filterSystemFields) — read it on the record detail page instead. At the console pin the richtext, markdown, html and code widgets are textarea-based BY DESIGN (objectui packages/fields/src/widgets/RichTextField.tsx / CodeField.tsx: a Textarea inside the dedicated widget, with its own label and controls): record that as the dedicated widget, not as a generic text input", "through the form, change one representative value per family (a select, an array type, a temporal, a structured JSON), Save, and capture the PATCH", "re-read the record via GET /api/v1/data/showcase_field_zoo/ and diff against the submitted values (arrays compared as sets)", "POST an out-of-set select value (f_select: 'not-a-value') directly to /api/v1/data/showcase_field_zoo and capture the refusal", @@ -319,10 +325,10 @@ "evidence": "test run output" }, { - "clause": "PER-VARIANT: each of the 49 FieldTypeSchema members renders its real widget on the Specimen — Full form (its dedicated field widget, not the generic text input), with per-variant evidence recorded in the variant→widget table. For richtext, markdown, html and code the dedicated widget is textarea-based by design (RichTextField / CodeField) — that counts; the generic text input rendered for one of them does not", + "clause": "PER-VARIANT: each FieldTypeSchema member renders its real widget where the console draws it — 47 on the Specimen — Full edit form, autonumber read-only on that record's detail page (the auto-form drops readonly fields in create and edit, plugin-form autoLayout.ts), and summary on its real carriers (no zoo field — knownGaps[0]) (its dedicated field widget, not the generic text input), with per-variant evidence recorded in the variant→widget table. For richtext, markdown, html and code the dedicated widget is textarea-based by design (RichTextField / CodeField) — that counts; the generic text input rendered for one of them does not", "oracle": "dom", "verify": "after the screenshot confirms render, match each f_* control against its declared type; every variant row in the table carries its own observed control + screenshot crop", - "evidence": "form screenshot + the 49-row variant→widget table" + "evidence": "form screenshot + detail-page screenshot + the 49-row variant→widget table (each row naming the surface it was read on)" }, { "clause": "constrained types reject invalid input server-side with a named error: out-of-set select → VALIDATION_FAILED with fields[].code 'invalid_option'; missing required name → code 'required'", @@ -394,6 +400,12 @@ "date": "2026-10-16", "change": "steps[2] and acceptance[1].clause say what counts for the textarea-based widgets (checklist-accuracy finding of QA run #23949, wave #23878). 'not a generic text input' with 'richtext→editor' read as a FAIL for richtext, markdown, html and code, whose dedicated widgets are textarea-based by design at the console pin (RichTextField / CodeField). The clause now scores the dedicated widget, and the generic text input still FAILS. Wording: no request or oracle changed", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "steps[2], acceptance[1] (clause, evidence) and fixtures.knownGaps[2] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) acceptance[1] asked for all 49 members on the Full edit form, which cannot hold: summary has no zoo carrier (knownGaps[0]) and autonumber is readonly by construction (field.zod.ts) and dropped from create and edit forms by plugin-form autoLayout.ts filterSystemFields; it is read on the detail page. (2) knownGaps[2] said f_secret writes fail closed on stock; the CLI wires a LocalCryptoProvider on every boot (serve.ts), and the run's PATCH answered 200 and read back masked. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ], "enumSource": { @@ -407,7 +419,7 @@ "title": "List-view capability matrix: filter, search, sort, pagination, saved views, inline edit, export, visualization switcher, bulk/row actions, conditional formatting", "since": "v15", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "browser", "personas": [ @@ -440,7 +452,7 @@ "switch to the in_progress saved view; capture the data request — expect `filter` carrying the stored rule as an ObjectQL AST ([[\"status\",\"equals\",\"in_progress\"]]) and a 200; the count bar must read 2. The console spells its list params WITHOUT a `$` — filter, top, skip, search, sort — the same AST under the bare name, so a literal grep for $filter finds nothing", "quick-search 'huaning' on Accounts (or '张' on Contacts); capture the `search` request and the hits (pinyin + CJK recall, ADR-0061)", "open the tabular saved view (string-form sort 'estimate_hours desc' — the objectui#2601 fixture); confirm it renders and the request carries the sort", - "page a list past page 1. No stock list holds more rows than the 50-row page (the largest seeded task and contact lists are under it), so stage one first: POST /api/v1/data/showcase_inquiry/createMany with a bare JSON array of 55 rows (name + email each), then page the Inquiries list; capture `top`/`skip` on every data request; confirm no unpaged full fetch", + "page a list past page 1. No stock list holds more rows than the 50-row page (the largest seeded task and contact lists are under it), so stage one first: POST /api/v1/data/showcase_inquiry/createMany with a bare JSON array of 55 rows (name + email + message each — showcase_inquiry requires `message` too, and a name + email body answers 400 \"Message is required\", QA run #24616), then page the Inquiries list; capture `top`/`skip` on every data request; confirm no unpaged full fetch", "in the grid saved view (inlineEdit: true), edit one cell in place; save; capture the PATCH; re-read the row via API", "on in_progress, with the export grant from fixtures.requires[3] bound, open the toolbar's Display popover — Export sits in its footer ('Export as CSV / XLSX / JSON'), not as a separate toolbar button — and export each declared format (csv, xlsx, json); capture the requests and files. Without the grant the Export entries are absent and the route answers 403 EXPORT_NOT_PERMITTED — the opt-in working as designed, not a fail of the export variant", "open the visualization switcher on All Tasks; enumerate the offered types; switch through each", @@ -510,8 +522,8 @@ } ], "negative": [ - "a saved view whose filter is refused (400 INVALID_FILTER) while the grid silently shows ALL rows unfiltered is a FAIL — the objectui#3431 regression shape; zero rows with a captured refusal is the honest symptom, unfiltered rows is the lie", - "userFilters leaking onto an OBJECT list view is a FAIL — ADR-0053 suppresses them there by design (filter elements belong to interface pages; objectui warns since #2220)", + "a saved view whose filter is refused (400 INVALID_FILTER) while the grid silently shows ALL rows unfiltered is a FAIL — the objectui#3431 regression shape; zero rows with a captured refusal is the honest symptom, unfiltered rows is the lie. No stock view carries a refused filter, so stage one (QA run #24616): overlay PUT /api/v1/meta/view/showcase_task.done with filter [{field:'done', operator:'equals', value:'maybe'}] — authoring accepts it, every query refuses it (a non-boolean string against a boolean answers 400 INVALID_FILTER naming the field, packages/objectql/src/boolean-comparand-declared-type-door.ts) — and revert with DELETE /api/v1/meta/view/showcase_task.done. A filter on a missing field is refused at authoring (list-view-field-unknown), so it cannot stage this", + "tabs-element userFilters (or quickFilters) rendering on an OBJECT list view is a FAIL — ADR-0053 suppresses them there by design, as amended by ADR-0047: on an object list view only `element: 'tabs'` userFilters (and quickFilters) are ignored — objectui app-shell warnSuppressedListNav.ts, which logs the suppression — while `element: 'dropdown'` value chips ARE allowed there (examples/app-showcase/src/ui/views/project.view.ts ships them on All Projects, QA run #24616), so dropdown chips on an object list are not this FAIL", "an export that returns the unfiltered table for a filtered view is a FAIL even though a file downloaded" ], "traps": [ @@ -521,7 +533,7 @@ ], "automated": { "kind": "e2e", - "ref": "objectui: e2e/live/saved-view-filter.spec.ts, e2e/live/user-filters.spec.ts, e2e/live/inline-edit-polish-2572.spec.ts; packages/qa/dogfood/test/showcase-search.dogfood.test.ts" + "ref": "objectui: e2e/live/saved-view-filter.spec.ts, e2e/live/user-filters.spec.ts, e2e/live/inline-edit-polish-2572.spec.ts; packages/qa/dogfood/test/showcase-search.dogfood.test.ts (scores NO clause of this item: it pins search.cross-field-object-search's matching — Northwind / retail — not acceptance[2]'s CJK 'huaning' recall, QA runs #24572 and #24598)" }, "source": [ "examples/app-showcase/src/ui/views/task.view.ts#inlineEdit (saved views, sort string form, inlineEdit, exportOptions, bulk/row actions, allowedVisualizations)", @@ -548,6 +560,12 @@ "date": "2026-10-16", "change": "steps[1], [2], [4], [6] and [8] and acceptance[1], [2], [3], [6] and [8] re-pointed (checklist-accuracy findings of QA run #23949, wave #23878). The console sends its list params without a `$` (filter / top / skip / search / sort — the same AST), so every $-spelled expectation named a param no request carries; the aggregate bulk action is api-typed and POSTs `_selectedIds` at the body top level to /api/v1/apps/showcase/recalc, not params._selectedIds to /actions; no stock list exceeds the 50-row page, so the paging step now stages 55 inquiries through createMany (as #23949 did); and Export sits in the Display popover's footer. Not loosened: every clause scores the same server requests and counts", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "steps[4], negative[0], negative[1] and automated.ref corrected from QA runs #24572 (5b14b7f3) and #24616 (ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) showcase_inquiry requires `message` (inquiry.object.ts), so the 55-row createMany needs it. (2) negative[1] was too broad: since the ADR-0047 amendment dropdown userFilters chips are allowed on an object list view and only `element: 'tabs'` (and quickFilters) are suppressed — objectui warnSuppressedListNav.ts; project.view.ts ships dropdown chips. (3) negative[0] had no stageable fixture on stock; the run's overlay with a non-boolean comparand stages one (the boolean comparand door refuses it 400 INVALID_FILTER — the run's note that the code is not INVALID_FILTER does not hold at source, boolean-comparand-declared-type-door.ts). (4) The framework dogfood ref is search-scoped and scores no list-view clause; the ref says so. The objectui live specs in the ref are §2's theme and untouched. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -665,7 +683,7 @@ "title": "Form-view gallery: every FormViewSchema layout type renders and submits", "since": "v15", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "browser", "personas": [ @@ -674,7 +692,7 @@ "fixtures": { "app": "showcase", "requires": [ - "showcase_task's default form (type simple — the `form` key) and its formViews tabbed / wizard / split / quick (drawer) — examples/app-showcase/src/ui/views/task.view.ts. The console reads ONLY the default (objectDef.form ?? formViews.default — RecordFormPage, resolveFormViewLayout), so a named formViews entry is reachable from no console door: each page-level variant is reached by overlaying the object's default form with that variant's type + sections (view is org-overridable, ADR-0005) and reverting afterwards. The overlay recipe: PUT /api/v1/meta/view/showcase_task.form?mode=draft with the ViewItem {name:'showcase_task.form', object:'showcase_task', viewKind:'form', isDefault:true, config:{type:, sections:}}, then POST /api/v1/meta/view/showcase_task.form/publish. ⚠ Item-level isDefault:true is REQUIRED: the console takes an object's default form — what /new and /edit render — ONLY from a form ViewItem that carries it (objectui packages/app-shell/src/providers/MetadataProvider.tsx applyViewItem, objectui#11539). Without it the publish succeeds, the flag is gone from the served row, and the page falls back to the all-fields layout, which reads like 'the variant does not render'", + "showcase_task's default form (type simple — the `form` key) and its formViews tabbed / wizard / split / quick (drawer) — examples/app-showcase/src/ui/views/task.view.ts. The console reads ONLY the default (objectDef.form ?? formViews.default — RecordFormPage, resolveFormViewLayout), so a named formViews entry is reachable from no console door: each page-level variant is reached by overlaying the object's default form with that variant's type + sections (view is org-overridable, ADR-0005) and reverting afterwards. The overlay recipe: PUT /api/v1/meta/view/showcase_task.form?mode=draft with the ViewItem {name:'showcase_task.form', object:'showcase_task', viewKind:'form', isDefault:true, config:{type:, sections:}}, then POST /api/v1/meta/view/showcase_task.form/publish. Each variant's sections can be read from its own served view item — the container serves formViews as showcase_task.tabbed / .wizard / .split / .quick (GET /api/v1/meta/view/showcase_task., QA run #24572). ⚠ Item-level isDefault:true is REQUIRED: the console takes an object's default form — what /new and /edit render — ONLY from a form ViewItem that carries it (objectui packages/app-shell/src/providers/MetadataProvider.tsx applyViewItem, objectui#11539). Without it the publish succeeds, the flag is gone from the served row, and the page falls back to the all-fields layout, which reads like 'the variant does not render'", "examples/app-showcase/src/ui/pages/task-desk.page.ts — the presentation surface: a JSX page whose ObjectForm opens with formType=\"drawer\" (edit, a side panel) and formType=\"modal\" (create, a dialog) over the task list" ], "knownGaps": [ @@ -698,7 +716,7 @@ "drawer and modal: open task-desk.page.ts; click a task row — ObjectForm formType=\"drawer\" opens as a side panel over the list with the task's fields; click New task — ObjectForm formType=\"modal\" opens as a dialog. An overlay of the default form with type drawer/modal renders the page form as simple, which is the designed fallback, not a fail", "on the simple form, set priority to 'urgent' and verify the notes field appears (FormField.visibleWhen CEL); set it back and verify notes hides", "save an edit through each rendered variant; capture each save request; re-read via GET /api/v1/data/showcase_task/", - "revert the default-form overlay so the stock simple form is back, and confirm it with a re-read of the view" + "revert the default-form overlay so the stock simple form is back — DELETE /api/v1/meta/view/showcase_task.form, the reset door, which restores the artifact default byte-identically (QA run #24572) — and confirm it with a re-read of the view" ], "acceptance": [ { @@ -765,6 +783,12 @@ "date": "2026-10-13", "change": "fixtures.requires[0] gained the overlay recipe with its item-level isDefault:true, and steps[1] points at it (fixture gap, #23163; parked on #23121, carried by #23169). The console sets an object's default form only from a form ViewItem carrying isDefault (MetadataProvider applyViewItem, objectui#11539). An overlay PUT to showcase_task.form?mode=draft without it publishes cleanly, and the page falls back to the all-fields layout. QA run #23163 scored the simple, tabbed and wizard overlays with the flag set. Re-read from objectui source at 1fb1ced36243, still this repo's .objectui-sha. No clause changed", "ref": "#23169" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "fixtures.requires[0] and steps[7] corrected from QA run #24572 (18.0 sweep, 5b14b7f3), re-read at objectstack f89d29e382: the revert step named no door, and the run measured DELETE /api/v1/meta/view/NAME (the reset, which restores the artifact default byte-identically); and each named variant is served as its own view item, so its sections can be read before overlaying them. No clause changed. Wave anchor #24579 §3", + "ref": "#PENDING-24579R4" } ] }, @@ -876,7 +900,7 @@ "title": "Grid-level conditional rules: row-scoped requiredWhen and parent-scoped readonlyWhen in the inline line-item grid", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "browser", "personas": [ @@ -886,7 +910,7 @@ "app": "showcase", "requires": [ "showcase_invoice_line rules (examples/app-showcase/src/data/objects/invoice.object.ts): description requiredWhen record.quantity >= 100 (ROW-scoped); product/quantity/unit_price readonlyWhen parent.status == 'paid' (PARENT-scoped); receipt Field.file upload cell (objectui#2360)", - "a draft invoice with editable lines (INV-1002) and a paid one (INV-1003); for clause 5, INV-1002 must carry a SECOND line with a DISTINCT stored position. The stock INV-1002 ships one line, and a line added through the data API with no position defaults to 0, which collides with the first; seed it with position 1, or any value the first line does not hold. On an invoice whose lines share a stored position the grid legitimately re-emits the sibling's position on save: it stamps row[sortField] = index on every change (GridField), with the sort field deriveDetail picks from the child (position)" + "a draft invoice with editable lines (INV-1002) and a paid one (INV-1003); for clause 5, INV-1002 must carry a SECOND line with a DISTINCT stored position. The stock INV-1002 ships one line, and a line added through the data API with no position defaults to 0, which collides with the first; seed it with position 1, or any value the first line does not hold — AND with its `amount` (quantity × unit_price): amount is computed in the client and stored as sent (examples/app-showcase/src/data/objects/invoice.object.ts, the amount field's comment), so a line staged over the data API without it carries amount null and the invoice total ignores it (QA run #24616). Or add the line through the grid, which computes it. On an invoice whose lines share a stored position the grid legitimately re-emits the sibling's position on save: it stamps row[sortField] = index on every change (GridField), with the sort field deriveDetail picks from the child (position)" ], "knownGaps": [ "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." @@ -895,7 +919,7 @@ "steps": [ "open INV-1002 (draft) — the inline Line Items grid renders via inlineEdit: 'grid'", "on one line, raise quantity to 120 with description empty; observe the description cell flag required ON THAT ROW; screenshot", - "confirm a second line with quantity 1 stays unflagged and editable (per-row evaluation). On INV-1002 this is the seeded second line with a distinct stored position (fixtures.requires); if it is not there, add it over the data API with position 1 before opening the invoice", + "confirm a second line with quantity 1 stays unflagged and editable (per-row evaluation). On INV-1002 this is the seeded second line with a distinct stored position (fixtures.requires); if it is not there, add it over the data API with position 1 and its amount (fixtures.requires[1]) before opening the invoice", "attempt to save the batch with the flagged description still empty; capture the block; then fill it and save; capture the batch request", "server-side negative: submit the same shape directly to the API (line with quantity 120, no description) and capture the refusal", "flip the header status to paid and save; observe product/quantity/unit_price cells lock; flip back to sent/draft and observe them unlock (live parent-scope re-evaluation)", @@ -974,6 +998,12 @@ "date": "2026-10-16", "change": "acceptance[0].verify records what 'blocked' means on this surface (the #23949 note for the next author): a client-blocked grid save still sends one batch, which the server refuses 400, by design per recheck 6218243967 item 13, and the objectui pin asserts only the inline flag. The verify text now scores the refusal and the unchanged rows. Wording: the clause and its oracle are unchanged", "ref": "#24036" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "fixtures.requires[1] and steps[2] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382: a second INV-1002 line staged over the data API needs its `amount` too — amount is client-computed and stored as sent (invoice.object.ts), so without it the line carries amount null and the total ignores it. The negative[1] FAIL of that run stays a product defect (objectstack-ai/objectui#13062, fixed at objectui main after the pin; #24619 row (a), open) and no clause changes for it. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -1191,7 +1221,7 @@ "title": "Related lists derive from the relationship and page on the server — never fetch every child row — and are read-gated on both ends", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "browser", "personas": [ @@ -1207,26 +1237,26 @@ "the permission-zoo sets examples/app-showcase/src/security/permission-sets.ts: showcase_manager grants showcase_contact read (line 124), showcase_contributor omits it (lines 36-45) — the both-sides read-gate probe binds a user to each" ], "knownGaps": [ - "the child-read-gate clause needs two signed-up users bound to showcase_manager and showcase_contributor — sys_user rows come from sign-up, not seeds — so on a single-user stock boot record that clause blocked(fixture); the paging clauses run on the seeded admin alone", + "STALE (QA run #24616): the child-read-gate clause needs two members bound to the manager and contributor positions, and a stock boot provides them — the search:qa-contributor-bound-member recipe (invite, sign-up, then an admin POST of sys_user_position; run it twice, once with position 'manager', once with 'contributor'). It is not blocked(fixture) on a single-user boot; the paging clauses still run on the seeded admin alone", "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." ] }, "steps": [ "open the Northwind account detail page (/_console/apps/com.example.showcase/showcase_account → Northwind)", - "screenshot; enumerate the related-list tabs (expect relationship-derived lists incl. Contacts and the declared Invoices tab)", - "open the Contacts related list; capture the page-1 data request", + "screenshot; enumerate the related lists: the declared Invoices list (relatedList: 'primary') has its own tab, and the non-primary children — Contacts among them — stack under ONE shared 'Related' tab (?tab=related; packages/spec/src/data/field.zod.ts relatedList: 'true/absent = shown (stacked under the shared \"Related\" tab)')", + "open the Related tab's Contacts section; capture the page-1 data request (page size 5 — the pager reads 'Page 1 of 6' for Northwind's 26 contacts, QA run #24616)", "click next-page; capture the page-2 request; note the pager text", "diff page-1 and page-2 row ids for overlap", "open the Invoices related tab; verify its title and columns match the declaration on the lookup field (name, status, total, issued_on)", - "capture every data request issued by the detail page and check each child query for a $top bound", + "capture every data request issued by the detail page and check each child query for a `top` bound (the console spells its list params without a `$` — top, skip, filter, sort — objectui data-objectstack's adapter; the server's own spelling $top is the same bound)", "read-gate both-sides: sign in as showcase_contributor (reads showcase_account, NOT showcase_contact); open the Northwind account detail; screenshot and confirm the Contacts related section/tab is ABSENT; then forge GET /api/v1/data/showcase_contact?$filter=[[\"account\",\"=\",\"\"]] as that persona and capture the 403", "repeat as showcase_manager (has showcase_contact read): confirm the Contacts section renders AND the same child query answers 200 with rows" ], "acceptance": [ { - "clause": "paging issues server-side $top/$skip requests scoped by the parent filter; the full child set is never fetched", + "clause": "paging issues server-side top/skip requests (the console's `$`-less spelling of $top/$skip) scoped by the parent filter; the full child set is never fetched", "oracle": "network", - "verify": "page 1 = $top=N + parent filter; page 2 = $top=N&$skip=N; NO child-list request without $top anywhere on the page", + "verify": "page 1 = top=N + the parent `filter`; page 2 = top=N&skip=N; NO child-list request without a `top` bound anywhere on the page", "evidence": "the captured request URLs" }, { @@ -1261,7 +1291,7 @@ } ], "negative": [ - "any child-list request WITHOUT a $top bound is a FAIL even when the rendered page looks right — the unpaged fetch is the defect", + "any child-list request WITHOUT a top bound (`top`, the console's spelling of $top) is a FAIL even when the rendered page looks right — the unpaged fetch is the defect", "a pager total that disagrees with a direct filtered API count (GET /api/v1/data/showcase_contact?$filter=[[\"account\",\"=\",\"\"]]) is a FAIL", "a Contacts section rendering for showcase_contributor (an empty grid + a New button that 403s on save) is the objectui#2359 regression shape — FAIL; equally, the child query returning rows to a persona the object read gate denies is a server-side FAIL (UI absence alone never proves the server refuses)" ], @@ -1303,6 +1333,12 @@ "date": "2026-08-21", "change": "recorded on the item that its automation is pinned exclusively in the objectui repo and is therefore neither runnable nor pin-evidenced from this checkout — one of 23 items in that position, each of which every sweep had been re-deriving from scratch. The knownGap names the two honest ways to score it (hand-drive it as a manual run, or run the pin in an objectui checkout and cite the revision) and forbids the third: treating the `automated` field itself as coverage. The protocol and the exclusive-vs-mixed distinction live once in RUNNER.md; this line is the pointer (#10236 A5)", "ref": "#10236" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "fixtures.knownGaps[0], steps[1], steps[2], steps[6], acceptance[0] (clause, verify) and negative[0] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2 — the run that hand-drove this item to PASS), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) The console sends top / skip / filter with no `$` (data-objectstack adapter), so the clauses' literal $top reads nothing on the wire. (2) Contacts is a section of the shared 'Related' tab — only relatedList: 'primary' children get their own tab (field.zod.ts). (3) knownGaps[0] said the read-gate personas are fixture-blocked on stock; the invite + sign-up + sys_user_position recipe mints both. The pin and its pass are untouched (the clause-free-pin theme is not this round's). Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -1590,7 +1626,7 @@ "title": "Action buttons surface at exactly their declared locations — list toolbar, list row, record header/more/related/section — and each dispatches for real", "since": "v15", "status": "active", - "revision": 11, + "revision": 12, "priority": "P1", "surface": "browser", "personas": [ @@ -1621,7 +1657,7 @@ "boot the showcase isolated; sign in as admin; open the showcase_task list view", "for each location variant, navigate to its surface (list header / row menu / a not-done task's Task Detail title bar / that title bar's ⋯ menu / the rows of the Tasks related list on a project's Project Detail Related tab, not the Tasks tab's line-items grid / the Task Detail quick-actions bar; fixtures.knownGaps names each record host), screenshot AFTER render settles, then read the rendered action buttons from the DOM", "record for every fixture action WHERE it rendered — building the full placement matrix (rendered locations vs declared locations)", - "dispatch one action per location with a ref-targeted click: bulk_reassign from the list toolbar — select a row FIRST: with none selected it toasts 'This action runs on a single record — select a row first' and dispatches nothing — (screen-flow wizard opens), quick_view (modal opens), mark_done (script executes), log_time (form dialog opens on showcase_task.form), open_docs (url navigation), recalc_selection from the ⋯ menu (api POST); capture each network trace", + "dispatch one action per location with a ref-targeted click: bulk_reassign from the list toolbar — select a row FIRST: with none selected it toasts 'This action runs on a single record — select a row first' and dispatches nothing — (screen-flow wizard opens), quick_view (modal opens), mark_done (script executes), log_time (a `form` action opens its FormView as a ROUTED PAGE, not a dialog: /_console/forms/showcase_task.form?recordId=…&recordObject=showcase_task — objectui core ActionRunner.ts, \"open a FormView as a routed page (/forms/:name)\"; QA run #24616), open_docs (url navigation), recalc_selection from the ⋯ menu (api POST); capture each network trace", "verify the state-changing dispatches server-side: mark_done flips the task's done flag (API re-read), recalc_selection's per-record branch recomputes the estimate", "CEL visibility both sides: locate a done task and a not-done task; read the row menu and record header of each for showcase_mark_done", "empty-locations probe, both halves: (a) only where a PACKAGED action with NO locations key exists (none ships on stock — knownGaps), sweep all six surfaces (list toolbar, row menu, record header, ⋯ overflow, related-list section, quick-actions bar) recording every surface it appears on — the expected list is EMPTY; otherwise record half (a) blocked(fixture). ⛔ Do not author the probe at runtime: a runtime-authored action is not served at all, so its empty sweep is no evidence; (b) sweep the same six surfaces for showcase_new_task, the fixture fleet's headless `locations: []` declaration, and record that its list is empty too" @@ -1745,6 +1781,12 @@ "date": "2026-10-16", "change": "steps[3] names the selection the toolbar dispatch needs (checklist-accuracy finding of QA run #23949, wave #23878): showcase_bulk_reassign on the list toolbar runs on a selected row, and with none selected it only toasts 'select a row first'. The clauses are unchanged", "ref": "#24036" + }, + { + "revision": 12, + "date": "2026-10-19", + "change": "steps[3] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: a type 'form' action navigates to the routed form page /forms/:name (ActionRunner.ts), it does not open a dialog. The run's acceptance[2] / negative[2] FAIL stays the product defect objectstack-ai/objectui#13063 and no clause changes for it; the round's suggestion to name examples/app-showcase/test/record-action-location-hosts.test.ts in automated.ref is not taken here (the ref's objectui half is §2's clause-free-pin theme). Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ], "enumSource": { @@ -1758,7 +1800,7 @@ "title": "All six validation-rule types enforce on the write path with their exact per-type error codes — and advisory severities flag without blocking", "since": "v15", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": [ @@ -1767,7 +1809,7 @@ "fixtures": { "app": "showcase", "requires": [ - "the seeded per-type rules: showcase_account account_lifecycle (state_machine), tax_id_format + billing_email_format (format), support_config_shape (json_schema), churn_reason_consistency (conditional); showcase_project end_after_start (cross_field), spent_within_budget (script), project_status_flow (state_machine)", + "the seeded per-type rules: showcase_account account_lifecycle (state_machine, events [update] only — transitions, no create-time check), tax_id_format + billing_email_format (format), support_config_shape (json_schema), churn_reason_consistency (conditional); showcase_project end_after_start (cross_field), spent_within_budget (script), project_status_flow (state_machine)", "the seeded ADVISORY rule: showcase_project project_health_progression — state_machine on `health` with severity:'warning' (green↔yellow↔red one step at a time; a jump is flagged, not blocked — examples/app-showcase/src/data/objects/project.object.ts)" ], "knownGaps": [ @@ -1775,7 +1817,7 @@ ] }, "variants": [ - "state_machine — create with non-initial status → invalid_initial_state; illegal transition → invalid_transition", + "state_machine — create a showcase_project with a non-initial status → invalid_initial_state (project_status_flow binds insert AND update with initialStates ['planned']; showcase_account's account_lifecycle binds update only, so it raises invalid_transition alone); illegal transition → invalid_transition", "format — bad tax_id / billing_email → invalid_format", "json_schema — off-schema support_config → json_schema_violation; non-JSON string → invalid_json", "cross_field — project end_date < start_date → rule_violation on start_date (the rule declares fields [start_date, end_date], and the server targets fields[0] — acceptance[1])", @@ -1855,6 +1897,12 @@ "date": "2026-10-16", "change": "variants[3] and acceptance[4] (clause, verify) corrected (checklist-accuracy finding of QA run #23949, wave #23878, and #23879). variants[3] said cross_field answers rule_violation naming end_date, but the server targets fields[0] = start_date, which is what acceptance[1] already says. And a write that matches an advisory rule now reports the hit in its answer's `warnings` (#22726), so the verify text's 'do not require the message in the response' is stale — the clause now asks for the warnings entry by rule, severity, field and code — invalid_transition, the state_machine's own code, which this card's re-run read — beside the log line. Not loosened: the advisory must still not block, and the log line is still required", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "variants[0] and fixtures.requires[0] corrected from QA run #24572 (18.0 sweep, 5b14b7f3), re-read at objectstack f89d29e382: account_lifecycle binds `events: ['update']` only (examples/app-showcase/src/data/objects/account.object.ts), so the create-with-non-initial-status leg raises invalid_initial_state from project_status_flow alone (events insert + update, initialStates ['planned'], project.object.ts). The since:v18 run #24558's note that this item describes the advisory hit as server-log only is already on main (revision 3 reads the write answer's warnings) and is not repeated. No clause changed. Wave anchor #24579 §3", + "ref": "#PENDING-24579R4" } ] }, @@ -1863,7 +1911,7 @@ "title": "Gantt is interactive, not a picture: drag persists, locked tasks survive auto-schedule, host veto restores", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "browser", "personas": [ @@ -1890,7 +1938,7 @@ "drag one task bar to new dates; capture the PATCH and re-read the record over the API", "reload and confirm the bar re-renders from the persisted dates", "on an extended fixture (per knownGaps): configure dependencies + a locked row; run toolbar auto-schedule; capture the confirm dialog, cancel once (verify zero writes), run again and apply", - "trigger a veto path (write rejected server-side) and confirm the bar snaps back" + "trigger a veto path (write rejected server-side) and confirm the bar snaps back. Runnable on stock with no lockField (QA run #24616): bind a member to the `contributor` position (search:qa-contributor-bound-member's steps; task_own_rows shows it only its assigned tasks), open the gantt as that member, have the admin reassign one of its tasks away over the API, then drag that bar — the PATCH answers 404 and the bar snaps back with 'Save failed — the change was rolled back'" ], "acceptance": [ { @@ -1959,6 +2007,12 @@ "date": "2026-10-13", "change": "knownGaps[0] records QA run #23163's re-confirmation of the A2-leaf fixture gap, and names the near-miss pin (fixture gap, #23163 A2-leaf; parked on #23121, carried by #23169). At objectui 1fb1ced36243 the stock gantt view has no lockField, no pin drives a direct drag on a locked leaf, and the view-overlay recipe is still not run-proven. GanttView.tree.test.tsx pins the locked leaf's missing resize grips and link dot, which is not the drag clause. The sub-leg stays blocked(fixture); no clause changed", "ref": "#23169" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "steps[4] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2): the veto step named no way to make the server reject a drag on stock; the run's recipe (a contributor member's task reassigned away server-side, then dragged → 404 → snap-back with the 'Save failed' toast) needs no lockField and scored acceptance[3] pass. No clause changed. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -2172,7 +2226,7 @@ "title": "User-built FilterBuilder/SortBuilder toolbar merges with the view filter, restores across nav, and reproduces rows from the URL", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "browser", "personas": [ @@ -2191,25 +2245,25 @@ }, "steps": [ "sign in as admin; open showcase_task and switch to the in_progress saved view (stored filter status=in_progress); screenshot", - "open the FilterBuilder toolbar; add 2 conditions (e.g. priority = high, estimate_hours > 4); open the SortBuilder and add a sort (due_date desc); capture the data request", - "assert the request $filter is the MERGE of the view's stored rule AND the two toolbar conditions (buildEffectiveFilter/mergeFilterNodes), and $orderby carries the toolbar sort; the returned rows satisfy view-filter AND toolbar conditions", + "open the FilterBuilder toolbar; add 2 conditions that leave at least TWO rows, so the sort clause has an order to read — on the stock seed priority = high AND estimate_hours > 4 leaves ONE row on in_progress, a single-datapoint for acceptance[1] (QA run #24616): check the count first and pick a pair that keeps ≥ 2; open the SortBuilder and add a sort (due_date desc); capture the data request", + "assert the request's `filter` param is the MERGE of the view's stored rule AND the two toolbar conditions (buildEffectiveFilter/mergeFilterNodes), and `sort` carries the toolbar sort (e.g. sort=-due_date) — the console spells its list params without a `$`; the returned rows satisfy view-filter AND toolbar conditions", "navigate away via an in-app nav link to another object (the link carries no query string, so the URL state is dropped), then navigate back to showcase_task in_progress; screenshot; confirm the toolbar filter + sort restored", "read where the toolbar filter and sort are kept: the signed-in user's own sys_user_preference row keyed ui.list.showcase_task (GET /api/v1/data/sys_user_preference filtered to that key; its value's perView. carries the filters and the sort — objectui ListPreferencesProvider.tsx, objectui#12418), and its localStorage write-through cache list-state::showcase_task. Confirm both are the signed-in user's: the row's user_id, and the cache key's user segment. (Only the search term stays under the older list-filters::showcase_task: key)", "sign out; sign in as the SECOND account; open the same object+view; confirm the first account's cached toolbar filter does NOT appear", "userFilters/URL: where UserFilters are hosted (an interface page or the userFilters surface — NOT a bare object list, ADR-0053), apply a quick-filter; copy the URL; open it in a fresh tab; confirm the uf_ params (comma-joined, URI-encoded) reproduce the same rows and uf__tab carries the active preset (ADR-0047)", - "negative sweep: confirm userFilters (quick-filter chips / uf_* params) do NOT leak onto the bare object list view" + "negative sweep: confirm tabs-element userFilters (preset chips, uf__tab) do NOT leak onto the bare object list view — dropdown chips the object view itself declares are allowed there (ADR-0047 amendment) and their uf_ params are not a leak" ], "acceptance": [ { - "clause": "the FilterBuilder toolbar conditions MERGE with the view's stored filter — the data request $filter carries BOTH the view rule AND the user conditions (buildEffectiveFilter over baseFilter + userFilter + normalized per-field conditions), never a replacement that drops the view filter", + "clause": "the FilterBuilder toolbar conditions MERGE with the view's stored filter — the data request's `filter` param (the console's `$`-less spelling) carries BOTH the view rule AND the user conditions (buildEffectiveFilter over baseFilter + userFilter + normalized per-field conditions), never a replacement that drops the view filter", "oracle": "network", - "verify": "captured GET /api/v1/data/showcase_task with $filter containing the view's status=in_progress node AND the two toolbar conditions merged (objectui plugin-list/src/ListView.tsx buildEffectiveFilter → mergeFilterNodes); rows returned satisfy every clause", + "verify": "captured GET /api/v1/data/showcase_task with `filter` containing the view's status=in_progress node AND the two toolbar conditions merged (objectui plugin-list/src/ListView.tsx buildEffectiveFilter → mergeFilterNodes); rows returned satisfy every clause", "evidence": "the request URL + first-page rows" }, { - "clause": "the SortBuilder sort reaches the request as $orderby and the rows come back server-ordered (client re-sorting is not the oracle)", + "clause": "the SortBuilder sort reaches the request as `sort` (e.g. sort=-due_date; the console's spelling, not $orderby) and the rows come back server-ordered (client re-sorting is not the oracle)", "oracle": "network", - "verify": "the request carries $orderby for the toolbar sort field/direction; response row order matches", + "verify": "the request carries `sort` for the toolbar sort field/direction; response row order matches over at least two rows (steps[1])", "evidence": "the request URL + row order" }, { @@ -2233,7 +2287,7 @@ ], "negative": [ "a toolbar filter that REPLACES the view's stored filter (rows appear that the view filter should exclude) is a FAIL — buildEffectiveFilter merges, it does not overwrite the base", - "userFilters (quick-filter chips, uf_ params, the uf__tab preset) leaking onto a bare OBJECT list view is a FAIL — ADR-0053 suppresses them there by design (filter elements belong to interface pages; the list-view-capabilities item flags the same regression). The reserved list-state params an object list writes for ITSELF — uf__filter, uf__search, uf__sort, uf__group (objectui#11860, userFilterUrlState.ts), e.g. uf__sort after a toolbar sort — are the list's own state mirrored into the URL, not userFilters, and do not count", + "tabs-element userFilters (preset chips, the uf__tab preset) leaking onto a bare OBJECT list view is a FAIL — ADR-0053 suppresses them there by design, as amended by ADR-0047: on an object list view only `element: 'tabs'` userFilters (and quickFilters) are ignored — objectui app-shell warnSuppressedListNav.ts, which logs the suppression — while `element: 'dropdown'` value chips ARE allowed there (examples/app-showcase/src/ui/views/project.view.ts ships them on All Projects, QA run #24616) (the list-view-capabilities item flags the same regression). The reserved list-state params an object list writes for ITSELF — uf__filter, uf__search, uf__sort, uf__group (objectui#11860, userFilterUrlState.ts), e.g. uf__sort after a toolbar sort — are the list's own state mirrored into the URL, not userFilters, and do not count", "a cached toolbar filter from account A visible to account B on the same browser is a FAIL — the key embeds the user id precisely to prevent that leak", "an incomplete FilterBuilder row emitted as [field, op, ''] (which matches only empty and silently excludes everything) instead of being dropped is a FAIL — convertFilterGroupToAST skips valueless rows (#1964)" ], @@ -2271,6 +2325,12 @@ "date": "2026-10-16", "change": "steps[4], acceptance[2] (clause, verify), acceptance[3] (clause, verify), negative[1] and source[1] re-pointed (checklist-accuracy findings of QA run #23949, wave #23878). Since objectui#12418 the toolbar filter and sort live in the user's sys_user_preference row ui.list. (cached in localStorage list-state::), not in localStorage list-filters::: — #23949 read them there, user-scoped. The negative now says that the reserved list-state params (uf__sort and siblings, objectui#11860) are the list's own state and do not count as a userFilters leak. Not loosened: a second account must still see none of the first account's filters", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "steps[1], steps[2], steps[7], acceptance[0] (clause, verify), acceptance[1] (clause, verify) and negative[1] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2 — the run that hand-drove this item to PASS), re-read at the console pin 4016370af3c9. (1) The console sends `filter=` and `sort=` with no `$` (data-objectstack adapter); the clauses named $filter / $orderby, which a wire read never finds — acceptance[2] already said so. (2) The suggested condition pair leaves one row on the stock seed, a single-datapoint for the row-order clause; the step now asks for ≥ 2 rows. (3) The userFilters negative carried the same over-broad scope as list-view-capabilities negative[1]: only tabs-element userFilters are suppressed on an object list (warnSuppressedListNav.ts), dropdown chips are allowed — corrected beside it so the two items agree. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -2279,7 +2339,7 @@ "title": "Calendar is interactive: drag reschedules via PATCH, a failing PATCH reverts with an error, mode switches re-render, empty-day click quick-creates", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "browser", "personas": [ @@ -2305,9 +2365,9 @@ "open showcase_task; switch to the Calendar view; wait for cards to render (screenshot FIRST)", "drag one task card to another day; capture the PATCH /api/v1/data/showcase_task/ carrying the new due_date; re-read the record over the API", "reload the view; confirm the card renders on the new day from server state", - "force a FAILING reschedule (drive as a persona lacking edit, or drag a record the server rejects); observe the optimistic move, then the rollback to the original day AND an error toast", + "force a FAILING reschedule: as a contributor-bound member (search:qa-contributor-bound-member; task_own_rows shows only its assigned tasks) load the calendar with a task assigned to it, have the admin reassign that task away server-side, then drag it — the PATCH answers 404 RECORD_NOT_FOUND; observe the optimistic move, then the rollback to the original day AND an error toast. ⛔ Not a persona lacking edit: it gets draggable=false, so nothing moves and there is no optimistic step to roll back (QA run #24616)", "switch month → week → day modes; confirm the same records re-render placed by their date fields", - "click an empty day cell; a quick-create dialog opens pre-filled with that date; submit; capture the create and re-read the new row's due_date = the clicked day", + "click an empty day cell; on showcase_task this hands off to the FULL create form with that date pre-filled, not a quick-create dialog — the object has a required field quick-create cannot fill (project), objectui plugin-calendar ObjectCalendar.tsx, objectui#12291; fill it and submit; capture the create and re-read the new row's due_date = the clicked day", "capture one screenshot per interaction variant" ], "acceptance": [ @@ -2330,9 +2390,9 @@ "evidence": "per-mode screenshots + 2 API reads" }, { - "clause": "empty-day quick-create inserts a REAL row: the dialog pre-fills the clicked date, submit issues dataSource.create, and the API re-read shows the new row with its date field = the clicked day", + "clause": "empty-day create inserts a REAL row: the create surface pre-fills the clicked date (the full create form on showcase_task, whose required project quick-create cannot fill — objectui#12291; a quick-create dialog on an object with nothing it cannot fill), submit issues the create, and the API re-read shows the new row with its date field = the clicked day", "oracle": "api", - "verify": "capture the create request and GET the new row; the start/due date equals the clicked cell's date (ObjectCalendar quick-create → dataSource.create)", + "verify": "capture the create request and GET the new row; the start/due date equals the clicked cell's date (ObjectCalendar quick-create, or its hand-off to the host's full create form → the create)", "evidence": "create trace + the row read" } ], @@ -2366,6 +2426,12 @@ "date": "2026-08-21", "change": "recorded on the item that its automation is pinned exclusively in the objectui repo and is therefore neither runnable nor pin-evidenced from this checkout — one of 23 items in that position, each of which every sweep had been re-deriving from scratch. The knownGap names the two honest ways to score it (hand-drive it as a manual run, or run the pin in an objectui checkout and cite the revision) and forbids the third: treating the `automated` field itself as coverage. The protocol and the exclusive-vs-mixed distinction live once in RUNNER.md; this line is the pointer (#10236 A5)", "ref": "#10236" + }, + { + "revision": 3, + "date": "2026-10-19", + "change": "steps[3], steps[5] and acceptance[3] (clause, verify) corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2 — the run that hand-drove this item to PASS), re-read at the console pin 4016370af3c9. (1) A persona lacking edit gets draggable=false, so the failing-reschedule step produced no optimistic move; the run's recipe — a contributor member's task reassigned away server-side, then dragged → 404 → rollback + toast — is now the step. (2) On showcase_task the empty-day click hands off to the full create form with the date pre-filled (ObjectCalendar.tsx, objectui#12291: a required field quick-create cannot fill), not a quick-create dialog. The pin and its pass are untouched. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -2485,7 +2551,7 @@ "title": "Record discussion: an @mention comment reconciles optimistically, persists to sys_comment, interleaves with activity, pings the mentioned user's bell — and the feed filter shows exactly each mode's subset without dropping author-extended activity", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "browser", "personas": [ @@ -2511,10 +2577,10 @@ "as admin, open a record detail with the discussion panel (feeds enabled); screenshot", "type a comment containing an @mention (the mention autocomplete offers mentionSuggestions); post it", "observe the OPTIMISTIC comment row appear immediately; capture the sys_comment create", - "after the feed refetch, confirm the optimistic row reconciles with the server row (same id → union-by-id merge, no duplicate, no phantom)", + "reload the record page (no sys_comment refetch follows a post within ~6 s, QA run #24616), then confirm the optimistic row reconciles with the server row (same id → union-by-id merge, no duplicate, no phantom)", "read sys_comment over the API: GET /api/v1/data/sys_comment?$filter for the record's thread_id, ordered by created_at asc — the posted comment is present", "perform an attributable change on the record (e.g. an edit) and confirm the resulting sys_activity row interleaves into the SAME feed, oldest-first", - "as the mentioned second user, open the notification bell — it gains the mention notification with a deep link back to the record; unreadCount increments; mark-as-read drops it", + "as the mentioned second user, open the notification bell — it gains the mention notification with a deep link back to the record; unreadCount increments; mark-as-read drops it. The bell is SERVER-backed: plugin-audit writes one sys_notification (topic collab.mention) per mentioned user (packages/plugins/plugin-audit/src/audit-writers.ts), and the console's bell reads the inbox feed over GET /api/v1/notifications (objectui app-shell useInboxBell.ts) — read unreadCount there", "confirm a NON-mentioned user's bell does NOT gain the notification (recipient scoping)", "CREATE-SIDE FEEDS_DISABLED probe — as admin, POST one throwaway qa_nofeeds record ({ name: 'qa-nofeeds-probe' }) for a real parent_id (qa_nofeeds is a scratch OBJECT from the qa-feeds-disabled recipe with no standing rows), then POST /api/v1/data/sys_comment with { thread_id: 'qa_nofeeds:', body: 'should be refused by the feeds gate' } and capture the refusal. Admin can read AND edit that record (public_read_write), which is what isolates the refusal from the sibling RECORD_NOT_ACCESSIBLE code", "RE-THREAD (UPDATE-SIDE) FEEDS_DISABLED probe — take the sys_comment row posted earlier against the feeds-ENABLED record and PATCH /api/v1/data/sys_comment/ with { thread_id: 'qa_nofeeds:' } as admin, capturing the refusal. This is the UPDATE half of the #10170 gate (enforceFeedsCapability registered on beforeUpdate as well as beforeInsert, packages/plugins/plugin-audit/src/audit-writers.ts): re-read the comment afterward and confirm its thread_id is UNCHANGED", @@ -2544,13 +2610,13 @@ { "clause": "the mentioned user's bell gains the notification with a WORKING deep link to the record, and unreadCount increments; mark-as-read / mark-all-read drop the count", "oracle": "dom", - "verify": "as the mentioned user, the bell shows the new mention notification, its link navigates to the commented record, and reading it decrements unreadCount (collaboration/useMentionNotifications)", + "verify": "as the mentioned user, the bell shows the new mention notification, its link navigates to the commented record, and reading it decrements unreadCount — GET /api/v1/notifications as that user, the server inbox the bell reads (useInboxBell.ts; plugin-audit writes the collab.mention row)", "evidence": "bell screenshots before/after + the deep-link navigation" }, { - "clause": "notifications are RECIPIENT-scoped both sides: only the @mentioned user's bell gains it (addNotification is gated recipientId === currentUserId); a non-mentioned user's bell does not", + "clause": "notifications are RECIPIENT-scoped both sides: only the @mentioned user's bell gains it (plugin-audit writes the sys_notification for the mentioned user ids only, never the author, and each user's inbox reads only its own rows); a non-mentioned user's bell does not", "oracle": "dom", - "verify": "the mentioned user sees it; a second, non-mentioned session does not (useMentionNotifications recipient gate)", + "verify": "the mentioned user sees it; a second, non-mentioned session does not (each user's GET /api/v1/notifications, beside the bell)", "evidence": "both users' bells" }, { @@ -2606,7 +2672,7 @@ "source": [ "objectui: packages/plugin-detail/src/{RecordChatterPanel,CommentInput,MentionAutocomplete,extractMentions}.tsx (compose + @mention)", "objectui: packages/app-shell/src/views/RecordDetailView.tsx (mergeFeedRows union-by-id, sys_comment + sys_activity fetch/merge, mentionSuggestions), packages/plugin-detail/src/renderers/recordActivityFeed.ts (activity→feed map)", - "objectui: packages/collaboration/src/useMentionNotifications.ts (recipient-scoped bell, unreadCount)", + "framework: packages/plugins/plugin-audit/src/audit-writers.ts (the @mention writer: one sys_notification, topic collab.mention, per mentioned user, the author excluded) + objectui: packages/app-shell/src/hooks/useInboxBell.ts (the console bell over the shared inbox feed). objectui packages/collaboration/src/useMentionNotifications.ts is exported but the console does not consume it", "framework: packages/spec/src/data/object.zod.ts#enable.feeds (enable.feeds default true → FEEDS_DISABLED — re-pointed #18104: FEEDS_DISABLED is named in this file only inside a describe() string and a comment; the key this item means is enable.feeds, 'a new comment and an update that re-threads an existing one alike'); PENDING-GAPS §B record-discussion-mentions", "framework: packages/plugins/plugin-audit/src/audit-writers.ts#enforceFeedsCapability (enforceFeedsCapability, registered on beforeInsert AND beforeUpdate per #10170), packages/plugins/plugin-audit/src/comment-access-hooks.ts#RECORD_NOT_ACCESSIBLE (RECORD_NOT_ACCESSIBLE — the sibling refusal the probes isolate against), packages/plugins/plugin-audit/src/objects/sys-comment.object.ts#thread_id (thread_id is conventionally `{object}:{record_id}`)", "framework: packages/spec/src/data/feed.zod.ts#FeedFilterMode (FeedFilterMode, 4 members — pinned above),+ (SYS_ACTIVITY_BUILTIN_TYPES + the #11507 open-vocabulary ruling: built-ins are the floor, never the ceiling); packages/plugins/plugin-audit/src/objects/sys-activity.object.ts#apiMethods (apiMethods ['get','list'] — why the probe cannot direct-POST)", @@ -2634,6 +2700,12 @@ "date": "2026-08-30", "change": "added the feed-filter axis: the four FeedFilterMode values (spec feed.zod.ts) and the #11507 open-vocabulary floor (an author-extended sys_activity.type renders via the generic fallback, never dropped) had no coverage — the item drove the feed's content but never its filter control. Two clauses, a variants list, and the FeedFilterMode enumSource pin (expect 4) so a fifth mode trips VARIANTS STALE. Register corrected against source on two counts: the mode type is NOT hand-local in objectui (RecordActivityTimeline.tsx imports it from @objectstack/spec/data, and the icon/color maps are Total over FeedItemType with the objectui#5969 two-directional pin — no drift-risk caveat warranted), and the unknown-type probe cannot direct-POST sys_activity (apiMethods ['get','list']) — the sanctioned channel is activityMilestones[].type, with the objectui pin as the recorded fallback", "ref": "#sweep-2026-08-30" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "steps[3], steps[6], acceptance[3].verify, acceptance[4] (clause, verify) and source[2] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. The bell is server-backed: plugin-audit's audit-writers.ts writes a collab.mention sys_notification per mentioned user and the console bell reads it through useInboxBell.ts (GET /api/v1/notifications); objectui's useMentionNotifications, which the item cited as the bell, has no console consumer. And no feed refetch follows a post within ~6 s, so the reconcile step reloads. Clause substance unchanged. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -2642,7 +2714,7 @@ "title": "Grid personalization persists across reload: column resize/reorder/pin, row-height, group-by (with API-true totals), row-color", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "browser", "personas": [ @@ -2667,7 +2739,7 @@ ], "steps": [ "open showcase_task grid; screenshot", - "resize a column (reorder two columns only on a grid that sets reorderableColumns; there is no runtime pin control — acceptance[0]), and set a row-height mode (compact/short/medium/tall/extra_tall)", + "resize a column (reorder two columns only on a grid that sets reorderableColumns; there is no runtime pin control — acceptance[0]), and set a density from the toolbar — three modes, Compact / Comfortable / Spacious, stored as rowHeight compact / medium / tall (the spec's five row heights compact/short/medium/tall/extra_tall narrow onto these three: objectui core normalize-list-view.ts DENSITY_MODE_TO_ROW_HEIGHT / ROW_HEIGHT_TO_DENSITY_MODE)", "reload; confirm the columnState (widths, and the order where reorder is enabled) and row-height restored — they now persist in the signed-in user's own list preference, sys_user_preference ui.list.showcase_task (perView..columnState / rowHeight, objectui#12418), with localStorage grid-columns-showcase_task[-] as the component's cache; the old per-view override through dataSource.updateViewConfig is retired (objectui#12504). Also reload in a FRESH browser context as the same user: the layout follows the user", "group-by status; confirm per-group headers appear and each group shows its record count — summarizeField (estimate_hours) is declared on the KANBAN config of task.view.ts, not on the grid, so grid groups carry counts; clear grouping and confirm the headers are removed cleanly", "cross-check each group's count against a direct per-group server count (GET /api/v1/data/showcase_task filtered to that status, read its total)", @@ -2682,9 +2754,9 @@ "evidence": "before/after-reload screenshots + the storage value" }, { - "clause": "row-height mode persists across reload (one of compact/short/medium/tall/extra_tall)", + "clause": "the toolbar density persists across reload (Compact / Comfortable / Spacious — the runtime control offers three modes over the spec's five rowHeight values)", "oracle": "dom", - "verify": "the reloaded grid renders at the chosen density (ObjectGrid rowHeightMode)", + "verify": "the reloaded grid renders at the chosen density (ObjectGrid rowHeightMode), and the preference row's rowHeight reads compact / medium / tall for the three modes", "evidence": "before/after screenshots" }, { @@ -2749,6 +2821,12 @@ "date": "2026-10-16", "change": "steps[2], [3], [4] and [5] and acceptance[2] (clause, verify) and [4] (clause, verify, evidence) re-pointed (checklist-accuracy findings of QA run #23949, wave #23878). Column state and row height now persist in the user's own sys_user_preference row (ui.list., objectui#12418) and restore in a fresh browser — #23949 read them there — so acceptance[4]'s 'deliberately NOT user-scoped' describes a store the console replaced; the clause now asserts the per-user row (a stale spec re-pointed to objectui#12418). summarizeField is a kanban config, so grid groups show counts and the reconciliation is per-group count; the row-color step now names the rule the stock Grid view declares, since no runtime authoring control exists; the updateViewConfig override is retired (objectui#12504). Not loosened: counts must still match the server, and another account must not see the layout", "ref": "#24036" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "steps[1] and acceptance[1] (clause, verify) corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2 — the run that hand-drove this item to PASS), re-read at the console pin 4016370af3c9: the toolbar offers three densities, Compact / Comfortable / Spacious, stored as compact / medium / tall (normalize-list-view.ts), not the five modes the item listed — those are the spec's row heights, which the renderer narrows onto three. The pin and its pass are untouched. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -2852,7 +2930,7 @@ "title": "Select-all-matching covers every matching id across pages (server total, not the rendered page); clear resets both selection sources", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "browser", "personas": [ @@ -2874,7 +2952,7 @@ "click select-all-matching; the banner reads 'All M matching records are selected'", "dispatch a bulk action; capture the requests — resolveBulkRows fans out a paged find (dropping $top/$skip, paging 500) collecting every matching id ACROSS pages", "cross-check the collected id count against a direct API count for the same search: GET /api/v1/data/showcase_task?$search=…&$top=0 (or the response total)", - "confirm the AGGREGATE action (showcase_recalc_selection) carries every matching id in params._selectedIds; a per-record action (showcase_mark_done) fans out one dispatch per matching id", + "confirm the AGGREGATE action (showcase_recalc_selection) carries every matching id in `_selectedIds` — it is an api-type action, so the ONE request is POST /api/v1/apps/showcase/recalc with `_selectedIds` at the body's top level (the grid hands them to the action runner as params._selectedIds, objectui plugin-grid ObjectGrid.tsx); the run dialog then reads 'N / 55 processed' for ~15 s (QA run #24616); a per-record action (showcase_mark_done) fans out one dispatch per matching id", "click clear; confirm both selection sources reset (the toolbar empties AND the row checkboxes clear)" ], "acceptance": [ @@ -2891,7 +2969,7 @@ "evidence": "the collected-id count + the filtered API count" }, { - "clause": "an AGGREGATE bulk action issues ONE dispatch carrying the full matching id set in params._selectedIds; a per-record action fans out one dispatch per matching id", + "clause": "an AGGREGATE bulk action issues ONE dispatch carrying the full matching id set in `_selectedIds` (top level of the api action's request body); a per-record action fans out one dispatch per matching id", "oracle": "network", "verify": "count captured POSTs against the match-set size per mode (ObjectGrid dispatchBulkAction → resolveBulkRows → params._selectedIds)", "evidence": "the dispatch trace(s)" @@ -2945,6 +3023,12 @@ "date": "2026-10-13", "change": "fixtures.requires[0] and [1], steps[0], [4] and [5], acceptance[1].verify and source[1] re-pointed from showcase_contact to the showcase_task 'Bulk Actions' view (stale fixture, #23163; parked on #23121, carried by #23169). showcase_contact's views declare no bulkActions, and its 34 contacts fit the list's first page of 50, so the item could not run as written. QA run #23163 re-pointed to the bulk_actions view with 55 scratch tasks and passed every clause there (one aggregate POST with 55 ids, Clear resets both sources, HARD_CAP 5000 read from source and pin). The Northwind 26 contacts page the related list, objectui#2711's subject, not a list view", "ref": "#23169" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "steps[5] and acceptance[2].clause corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: showcase_recalc_selection is an api-type action whose one request (POST /api/v1/apps/showcase/recalc) carries `_selectedIds` at the body's top level; params._selectedIds is the grid's hand-off to the action runner (ObjectGrid.tsx), not the wire spelling — list-view-capabilities already states the top-level form for the same action. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -3143,7 +3227,7 @@ "title": "Async import job: terminal state + per-row results, undo removes exactly the imported rows, cancel mid-job leaves a coherent partial", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", "personas": [ @@ -3158,12 +3242,13 @@ ], "knownGaps": [ "CLOSED for runs by the live-gated console spec in requires (#21845): e2e/import-console/import-console-undo.spec.ts skips unless IMPORT_CONSOLE_LIVE=1, and its goto path needs the /_console mount — with both supplied it runs against the stock console. A skip without the gate is the control, never a result. The harness twin e2e/import-harness/import-undo.spec.ts still skips unless the harness origin serves /live.html; it is optional evidence, and the API half below needs no gate at all", - "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." + "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.", + "HAND-DRIVEN CONSOLE ROUTE, measured by QA run #24616 without the gated spec: on showcase_task's list, Import → Preview → tick 'Import in the background (runs as an undoable job)' → import → History → 'Undo import'. The Undo asks a NATIVE window.confirm (objectui plugin-grid ImportWizard.tsx), which Playwright dismisses by default, so an unhandled dialog makes Undo a silent no-op (automation-input): accept it (page.on('dialog', d => d.accept())). showcase_contact's list shows no Import button (it sits in the header's overflow menu there); showcase_task's does" ] }, "steps": [ "run the console spec with its gate on, from the scratch copy whose goto path is /_console/apps/{app}/{object} (fixtures.requires): IMPORT_CONSOLE_LIVE=1 pnpm exec playwright test (the harness twin e2e/import-harness/import-undo.spec.ts only where /live.html is served); capture the suite output, naming the objectui revision, as the primary evidence", - "API half (no gate needed): create with POST /api/v1/data/:object/import/jobs with a marker-tagged CSV — every other job route lives under /api/v1/data/import/jobs: poll progress at GET /api/v1/data/import/jobs/:jobId until terminal; GET /api/v1/data/import/jobs/:jobId/results for per-row results; poll GET /api/v1/data/import/jobs/:jobId until terminal; GET /api/v1/data/import/jobs/:jobId/results for per-row results", + "API half (no gate needed): create with POST /api/v1/data/:object/import/jobs with a marker-tagged CSV — every other job route lives under /api/v1/data/import/jobs: poll progress at GET /api/v1/data/import/jobs/:jobId until terminal; GET /api/v1/data/import/jobs/:jobId/results for per-row results", "count marker rows via a filtered GET before and after the import; then POST /api/v1/data/import/jobs/:jobId/undo and re-count", "GET /api/v1/data/import/jobs and confirm the job list distinguishes undoable/non-undoable and reverted state ({ jobId, undoable, revertedAt, createdAt } — revertedAt is ABSENT from an entry until that job is reverted, not null) so the fresh job is findable", "start a fresh import and POST /api/v1/data/import/jobs/:jobId/cancel mid-job; inspect the row set for coherence" @@ -3201,7 +3286,8 @@ ], "traps": [ "seed-data-thin", - "dispatcher-vs-hono-route" + "dispatcher-vs-hono-route", + "automation-input" ], "automated": { "kind": "e2e", @@ -3237,6 +3323,12 @@ "date": "2026-10-16", "change": "fixtures.requires[2] names the scratch object the console spec needs (checklist-accuracy finding of QA run #23949, wave #23878): the spec imports a name,email,status CSV onto LIVE_IMPORT_OBJECT, and no showcase object has those three fields, so #23949 authored qa_lead and ran with LIVE_IMPORT_APP=showcase_app LIVE_IMPORT_OBJECT=qa_lead. The clauses are unchanged", "ref": "#24036" + }, + { + "revision": 5, + "date": "2026-10-19", + "change": "steps[1], a new fixtures.knownGaps entry and the traps list corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2 — the run that hand-drove this item to PASS), re-read at the console pin 4016370af3c9: steps[1] repeated its poll/results clause twice; the run found a hand-driven console route that needs no gated spec, whose Undo is a native window.confirm (ImportWizard.tsx) that Playwright dismisses by default, so the automation-input trap is now listed. The pin and its pass are untouched. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -3245,7 +3337,7 @@ "title": "Object lifecycle hooks fire on the write path with the right timing, gate, async and error semantics — driven over /api/v1/data/*, oracled by the record effect and the log line", "since": "v15", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "mixed", "personas": [ @@ -3257,7 +3349,7 @@ "the five seeded showcase hooks (examples/app-showcase/src/data/hooks/index.ts, bound via defineStack({ hooks: allHooks })) — the four this item drives, plus showcase_guard_task_reopen (showcase_task, beforeUpdate, condition previous.done == true && record.done != true, body throws a user-facing sentence, onError:'abort', priority 60), the refusal fixture records-forms.script-action-hook-refusal-toast drives; showcase_normalize_task_title (showcase_task, events ['beforeInsert','beforeUpdate'], trims title, priority 50, onError:'abort'); showcase_stamp_inquiry_defaults (showcase_inquiry, beforeInsert, stamps status='new'/source='web', onError:'abort'); showcase_audit_task_completion (showcase_task, afterUpdate, condition previous.done!=true && record.done==true, async, retryPolicy {maxRetries:3,backoffMs:1000}, capabilities:['log'], onError:'log', priority 90); showcase_warn_over_budget (showcase_project, afterUpdate, condition record.spent!=null && record.budget!=null && record.spent>record.budget, async, capabilities:['log'], onError:'log')", "showcase_task (title required text + done boolean, defaultValue false), showcase_inquiry (status select whose 'new' option is default:true + source plain text with NO default), showcase_project (budget/spent currency + the spent_within_budget script rule that REJECTS spent > budget*1.2)", "seeded rows: not-done task 'Build homepage' (done:false) for the transition PATCH; already-done tasks 'Audit current IA' and 'App wireframes' (done:true) for the non-transition proof; projects 'Data Platform' (budget 600000 / spent 420000) and 'Website Relaunch' (budget 150000 / spent 60000)", - "server log capture — the async audit/warn bodies call ctx.log.info/warn, routed to the engine logger (packages/runtime/src/sandbox/body-runner.ts log: engineCtx.logger); AND the ability to register a scratch hook (throwing / ordered) for the variants the four fixtures cannot observe", + "server log capture — the async audit/warn bodies call ctx.log.info/warn, routed to the engine logger (packages/runtime/src/sandbox/body-runner.ts log: engineCtx.logger); AND the ability to register a scratch hook (throwing / ordered) for the variants the four fixtures cannot observe. Run-proven recipe (QA run #24616): on a scratch app copy, put every scratch hook on showcase_inquiry behind a name-prefix condition (record.name.startsWith('qa-…')) so it touches only the probe rows; for the fail-closed probe, `record.name.startsWith('qa-fc') && int(record.name) > 0` compiles and faults only for a 'qa-fc…' row", "a real running server: hooks fire in the ObjectQL engine on the live /api/v1/data/* route, never on a simulated dispatch" ], "knownGaps": [ @@ -3287,7 +3379,7 @@ "condition fails CLOSED (#4775) — SCRATCH: an unevaluable/uncompilable condition ABORTS the operation and is NOT softened by onError:'log' nor fire-and-forgotten (the gate runs OUTSIDE both); add a scratch hook with a broken condition (unit-pinned in hook-wrappers.ts)" ], "steps": [ - "boot showcase isolated WITH OS_LOG_LEVEL=info exported (or --log-level info): the audit oracle is a ctx.log.info line, and `os dev` defaults the kernel logger to warn (packages/cli/src/utils/log-level.ts DEFAULT_LOG_LEVEL), so on a default boot a correct product prints no audit line and every 'fires' clause reads as a no-fire; sign in as seeded admin; confirm the four hooks this item drives actually registered before asserting any no-fire — GET /api/v1/meta/types/hook (or the boot log) lists showcase_normalize_task_title / showcase_stamp_inquiry_defaults / showcase_audit_task_completion / showcase_warn_over_budget (a hook that never bound fakes every no-fire — seed-data-thin)", + "boot showcase isolated WITH OS_LOG_LEVEL=info exported (or --log-level info): the audit oracle is a ctx.log.info line, and `os dev` defaults the kernel logger to warn (packages/cli/src/utils/log-level.ts DEFAULT_LOG_LEVEL), so on a default boot a correct product prints no audit line and every 'fires' clause reads as a no-fire; sign in as seeded admin; confirm the four hooks this item drives actually registered before asserting any no-fire — GET /api/v1/meta/hook (or the boot log; ⛔ not /meta/types/hook — /meta/types is the type listing and /meta/types/hook answers 404) lists showcase_normalize_task_title / showcase_stamp_inquiry_defaults / showcase_audit_task_completion / showcase_warn_over_budget (a hook that never bound fakes every no-fire — seed-data-thin)", "stamp defaults (beforeInsert): POST /api/v1/data/showcase_inquiry {name:'os-qa-', email, message} — email and message are required, so a name-only body answers 400 VALIDATION_FAILED before any hook runs — OMITTING status AND source; re-read via GET and confirm source == 'web' — the hook is the SOLE producer of that value (the source field carries no default), so a correct read proves the hook fired; status == 'new' corroborates but is NOT hook-attributable alone (the 'new' status option is default:true)", "title trim (beforeInsert + beforeUpdate): POST /api/v1/data/showcase_task with title ' os-qa- ' (leading/trailing spaces) and valid required fields; re-read → title == 'os-qa-' (insert trim); then PATCH the same row's title to another padded value and re-read → trimmed again (update trim); ONE multi-event hook covered both", "completion audit — FIRES (transition): PATCH /api/v1/data/showcase_task/ {done:true} (previous done:false); after the HTTP response resolves, settle briefly (the audit is async fire-and-forget, it runs after the write returns), then read the server log for 'task completed: Build homepage'", @@ -3350,8 +3442,8 @@ { "clause": "a condition that cannot be evaluated FAILS CLOSED (#4775): the write is ABORTED, not silently skipped — and it is NOT softened by onError:'log' nor fire-and-forgotten, because the condition gate runs OUTSIDE the async/retry/onError wrappers", "oracle": "api", - "verify": "scratch hook whose condition COMPILES but cannot be evaluated at runtime (e.g. int(record.name) > 0, written to a row whose name is not numeric); a write to that object returns a HookConditionError-class refusal and the row does not land (hook-wrappers.ts: conditionFn throws outside runWithErrorPolicy). An UNCOMPILABLE condition never reaches this gate: it is refused at authoring (PUT /api/v1/meta/hook → 422 INVALID_METADATA, expression-invalid), so it cannot stand in for the runtime fail-closed probe", - "evidence": "the refusal + a post-attempt count showing no row" + "verify": "scratch hook whose condition COMPILES but cannot be evaluated at runtime (e.g. int(record.name) > 0, written to a row whose name is not numeric); a write to that object is refused and the row does not land (hook-wrappers.ts: conditionFn throws outside runWithErrorPolicy). The 18.0 wire envelope is a generic 500 {code: 'INTERNAL_ERROR'}, with the class only in the server log ('[REST] Unhandled error: HookConditionError …') — #24619 row (b), open, tracks the coded refusal it should be. Until that lands score the clause on the refusal, the absent row and the log line, and record the 500 against #24619 rather than as a new finding. An UNCOMPILABLE condition never reaches this gate: it is refused at authoring (PUT /api/v1/meta/hook → 422 INVALID_METADATA, expression-invalid), so it cannot stand in for the runtime fail-closed probe", + "evidence": "the refusal (status + body) + the HookConditionError server-log line + a post-attempt count showing no row" }, { "clause": "two hooks on the SAME object+event run lowest-priority-first: scratch hooks at priority 10 and 90 log in that order on one write (hook-binder passes priority to engine.registerHook; the engine orders by it, lower first)", @@ -3363,7 +3455,7 @@ "negative": [ "a beforeInsert (or beforeDelete) hook that THROWS under onError:'abort' but leaves the row written (filtered count > 0) is a FAIL — abort must roll the WHOLE write back (the hef_ref_guard dogfood pin is the reference shape)", "an async onError:'log' hook failure that ROLLS BACK the triggering write is a FAIL — fire-and-forget runs after the write resolved and can only be logged, never un-commit it", - "a SILENT no-fire on a real done:false→true transition is a FAIL — the audit MUST emit; if it truly did not, first rule out that the hooks registered (meta/types/hook) and that the async line was given time to settle before filing (seed-data-thin / async-settle)", + "a SILENT no-fire on a real done:false→true transition is a FAIL — the audit MUST emit; if it truly did not, first rule out that the hooks registered (GET /api/v1/meta/hook) and that the async line was given time to settle before filing (seed-data-thin / async-settle)", "the audit firing on a NON-transition edit of an already-done task is a FAIL — it would prove the condition collapsed from the two-root transition to a bare `record.done == true` state test (the exact #4784 regression)", "an unevaluable condition that SILENTLY SKIPS (letting a before* guard through, or dropping an after* audit) instead of aborting the operation is a FAIL — #4775 fails closed, loudly, and onError never sees the condition error" ], @@ -3375,7 +3467,7 @@ ], "automated": { "kind": "dogfood", - "ref": "packages/qa/dogfood/test/showcase-public-form.dogfood.test.ts (pins the beforeInsert stamp: status='new'/source='web' on an anonymous inquiry submit); packages/qa/dogfood/test/hook-error-format.dogfood.test.ts (pins the beforeDelete onError:'abort' throw → REST error body). Declarative-wrapper semantics (two-root condition, async, retry, onError, fail-closed) are unit-pinned in packages/objectql/src/hook-wrappers.ts + hook-binder.ts tests. The transition audit / over-budget warn / priority-ordering LOG oracles and the abort/log/priority SCRATCH variants are NOT yet dogfood-pinned — drive them by hand." + "ref": "packages/qa/dogfood/test/showcase-public-form.dogfood.test.ts (pins the beforeInsert stamp: status='new'/source='web' on an anonymous inquiry submit); packages/qa/dogfood/test/hook-error-format.dogfood.test.ts (pins the beforeDelete onError:'abort' throw → REST error body). Declarative-wrapper MECHANISMS are unit-pinned in packages/objectql (each file run at objectstack f89d29e382, 83 tests, 0 failed): hook-condition-previous-scope.test.ts (acceptance[3]'s two-root transition — 'fires on the update that FLIPS the field, not on later updates of a done record'), hook-condition-merged-record.test.ts (acceptance[5]'s stored ⊕ payload record — 'fires on a declared field the update payload does NOT carry'), hook-binder.test.ts (acceptance[7] — 'swallows errors when onError=log', 'runs async after-events fire-and-forget'; plus retry, timeout and async-ignored-on-before*) and hook-condition-fail-loud.test.ts (acceptance[8] — a HookConditionError that onError:'log' does not swallow, retryPolicy does not retry and an async after* hook still raises). They score the mechanism only: acceptance[2]–[5]'s LOG oracles, the scratch variants' API reads and acceptance[8]'s wire envelope stay the run's. The transition audit / over-budget warn / priority-ordering LOG oracles and the abort/log/priority SCRATCH variants are NOT yet dogfood-pinned — drive them by hand." }, "source": [ "examples/app-showcase/src/data/hooks/index.ts#allHooks (the four fixture hooks this item drives, the showcase_guard_task_reopen refusal fixture beside them, + allHooks export; header comments spell out the two-root #4784 transition and the != null / not has() #4770 rationale verbatim)", @@ -3418,6 +3510,12 @@ "date": "2026-10-16", "change": "steps[0], steps[1] and acceptance[8].verify corrected (checklist-accuracy findings of QA run #23949, wave #23878, and of this card's re-run). The audit oracle is a ctx.log.info line, and the CLI's default log level is warn, so the boot step now exports OS_LOG_LEVEL=info (#23949 ran on an info boot). The fail-closed scratch must be runtime-unevaluable, not uncompilable: an uncompilable condition is refused at authoring with 422 INVALID_METADATA expression-invalid and never reaches the runtime gate. And steps[1]'s inquiry body named only `name`, which answers 400 VALIDATION_FAILED (email and message are required) before any hook runs; the step now carries them. Not loosened: a write that lands past an unevaluable condition still FAILS", "ref": "#24036" + }, + { + "revision": 6, + "date": "2026-10-19", + "change": "steps[0], negative[2], fixtures.requires[3], acceptance[8] (verify, evidence) and automated.ref corrected from wave anchor #24579 round 2 part 1 (6272032736) and QA runs #24598 / #24616 (ba4afcc2), re-read at objectstack f89d29e382. (1) The hook list route is GET /api/v1/meta/hook; /meta/types/hook answers 404. (2) acceptance[8]'s refusal answers 500 INTERNAL_ERROR with the class in the server log only; #24619 row (b) (open) tracks the coded refusal, so the clause states the shipped envelope and scores against #24619. (3) The ref named source files (hook-wrappers.ts, hook-binder.ts) no resolver can run; it now names four test files, each measured at base under os-verify-lock (83 passed), mapped to the clause whose mechanism it pins. (4) The run-proven scratch-hook recipe is recorded in requires[3]. Part 1's note on a `$select` in the steps does not hold: no step of this item spells `$select` (0 hits in records-forms.json)", + "ref": "#PENDING-24579R4" } ] }, @@ -4205,7 +4303,7 @@ "title": "Per-field trackHistory: a tracked edit lands the ADR-0052 §5b old→new timeline summary with display values, an untracked edit does not, and object-level enable.trackHistory gates the History tab and derived history op both ways", "since": "v15", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "mixed", "personas": [ @@ -4244,7 +4342,7 @@ { "clause": "the History TAB gates on OBJECT-level enable.trackHistory, both ways: the account detail renders the tab (flag true) and the task detail does NOT (no enable block — per-field flags alone do not summon it)", "oracle": "dom", - "verify": "screenshot both detail pages after settle; gating ruled at objectui RecordDetailView.tsx (trackHistory must be explicitly true)", + "verify": "screenshot both detail pages after settle (the task detail page carries no tablist at all, so corroborate its 'no History tab' with the absent sys_audit_log fetch, QA run #24616); gating ruled at objectui RecordDetailView.tsx (trackHistory must be explicitly true)", "evidence": "both screenshots" }, { @@ -4261,7 +4359,7 @@ } ], "negative": [ - "an untracked edit producing an old→new diff row is a FAIL — trackHistory is opt-in per field (spec default false, object.zod.ts family note)", + "an untracked edit producing an old→new diff in the sys_activity summary / Discussion timeline is a FAIL — trackHistory is opt-in per field (spec default false, object.zod.ts family note), and the per-field opt-in governs exactly that surface. ⛔ Not the History tab: it reads sys_audit_log (the compliance audit, objectui RecordDetailView.tsx), which records every field and is not per-field-gated, so 'Website: old → new' there for the untracked field is correct; sys_activity.metadata also carries raw old/new for untracked fields by design — only the rendered summary is gated (QA run #24616)", "a task detail rendering a History tab is a FAIL — per-field flags without the object-level opt-in must not summon it", "a tracked select rendering raw option VALUES ('in_progress') instead of labels is a FAIL (#7289 closed exactly that)", "cross-ref guard: do NOT re-score crud-roundtrip clause 5 (History-tab display quality on the create/update pair) here — this item owns the tracked/untracked boundary and the two gating directions, that one owns the tab's rendering contract" @@ -4286,6 +4384,12 @@ "date": "2026-08-30", "change": "initial — sweep gap (docs-claims angle): 'full change history when tracking is on' (capabilities/data.mdx) had only the History-tab display clause in crud-roundtrip; nothing drove the per-field tracked/untracked boundary, the §5b declarative summary, or the enable.trackHistory gating of tab and derived history op. The account/task pair makes both gating directions stock-drivable", "ref": "#sweep-2026-08-30" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "negative[0] and acceptance[2].verify corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: the History tab reads sys_audit_log (RecordDetailView.tsx), a compliance audit that is not per-field-gated and correctly renders an untracked field's old → new; the per-field opt-in governs the sys_activity summary / Discussion timeline only, so negative[0] now names that surface. The task page has no tablist, so the absent tab is corroborated by the absent sys_audit_log fetch. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] }, @@ -4835,7 +4939,7 @@ "title": "Discussion on v18: reactions are their own records (sys_comment_reaction — 201 with user_id forced, duplicate 409, unreadable comment 403, no update, no deleting another member's), the retired sys_comment.reactions column is refused, a comment edit is platform-stamped, and milestone activity is withheld whole from a reader missing a watched field", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "api", "personas": [ @@ -4846,7 +4950,8 @@ "app": "showcase", "requires": [ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", - "a showcase record both personas can read, with one comment by the admin" + "a showcase record both personas can read, with one comment by the admin", + "object grants for the member on sys_comment and sys_comment_reaction: no stock set gives a plain org member either (/api/v1/auth/me/permissions lists neither; the member's POST /api/v1/data/sys_comment_reaction and GET /api/v1/data/sys_comment answer 403 PERMISSION_DENIED — QA run #24558). Author a runtime permission set granting what the steps drive (read the comment; create, edit and delete reactions) through the metadata door and bind it with POST /api/v1/data/sys_user_permission_set {user_id, permission_set} — the run used one named qa_comment_set" ] }, "steps": [ @@ -4900,6 +5005,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 gains the member's grant on sys_comment and sys_comment_reaction, from QA run #24558 (18.0 since:v18 sweep, 5b14b7f3): a plain org member holds no object permission on either (403 PERMISSION_DENIED on both doors), so every member clause needs a runtime set the item never named; the run bound one (qa_comment_set). No clause changed. Wave anchor #24579 §3 (the record's continuation comment)", + "ref": "#PENDING-24579R4" } ] }, @@ -4908,12 +5019,12 @@ "title": "Defaults that changed meaning on v18 (ADR-0087 D3 18.* entries): an omitted list-view page size is 50 (was 25), an omitted editInline means editable in place, an autonumber with no `unique` is unique per organization, and a field-heavy form opens as a bounded modal on desktop", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "mixed", "personas": [ "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", - "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password). Neither stock demo persona is an updater on showcase_task: the updater is a member bound to the `contributor` position (allowEdit on showcase_task), whose task RLS (task_own_rows) shows only tasks assigned to it — so assign it some tasks first (admin PATCH assignee), QA runs #24558 / #24616" ], "fixtures": { "app": "showcase", @@ -4924,12 +5035,12 @@ "the console bundle for the browser legs — see knownGaps" ], "knownGaps": [ - "the browser clauses need the vendored console; at framework 94114c1f the pinned objectui (.objectui-sha daeb77fc) does not build against current spec (MISSING_EXPORT checkPageComponentViewData) — score those clauses blocked(environment) until the pin is bumped" + "STALE (QA runs #24558, #24616): the pinned console at .objectui-sha 4016370af3c9 builds against current spec and its stamp matches the pin, so the browser clauses run — they are no longer blocked(environment)" ] }, "steps": [ - "GET /api/v1/meta/view/VIEW and read the resolved pageSize / editInline; open the list in the console and count the first page", - "as a member who may update, open the list and confirm the inline-edit affordance; as a read-only member confirm it is absent", + "GET /api/v1/meta/view/VIEW to confirm the view declares no pagination and no userActions block — the meta read serves the authored view, so nothing resolved can be read there; the defaults show only in the renderer (the list request's top=50, the Edit inline switch). Open the list in the console and count the first page", + "as a member who may update, open the list and confirm the inline-edit affordance — the 'Edit inline' switch in the list's Display / view-settings popover at desktop width; as a read-only member confirm it is absent. The edit mode itself is session state that starts OFF (objectui app-shell ObjectView.tsx: 'The edit mode is now session state'), so cells are not editable on load until the switch is turned on — a click on a cell opens the record before then", "create two rows of an autonumber object in the same org and read their numbers", "open New on a field-heavy object on a desktop viewport, then on a mobile viewport" ], @@ -4947,7 +5058,7 @@ "evidence": "row count + one-line screenshot description" }, { - "clause": "inline edit is offered to an updater and absent for a read-only member with editInline omitted", + "clause": "inline edit is offered to an updater and absent for a read-only member with editInline omitted — 'offered' means the Edit inline switch is present (the edit mode starts off for the session, steps[1])", "oracle": "screenshot", "verify": "per-persona affordance", "evidence": "two one-line descriptions" @@ -4979,6 +5090,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; D3 entries: view-pagination-page-size-default-50, list-view-edit-inline-default-on, autonumber-default-unique-organization", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "personas[1], fixtures.knownGaps[0], steps[0], steps[1] and acceptance[2].clause corrected from QA runs #24558 (5b14b7f3) and #24616 (ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) knownGaps[0] was stale: the pinned console builds and was served. (2) The meta read serves the authored view, so step 1 has no resolved pageSize / editInline to read (`pagination` is an optional block; its pageSize default materialises only inside one); the defaults show in the renderer. (3) The inline affordance is the Edit inline switch, and the edit mode is session state that starts off (ObjectView.tsx), so 'offered' does not mean editable cells on load. (4) The updater persona needs a contributor binding and assigned tasks. Wave anchor #24579 round 2 §3 (6272314386)", + "ref": "#PENDING-24579R4" } ] } From 7d32eecf3bf409c93de7a04b6d09aab7ca92c28d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 22:22:36 +0000 Subject: [PATCH 5/5] docs(qa): fill round 4's history refs with the PR number Swaps the 39 '#PENDING-24579R4' placeholders (38 history rows and the qa-contributor-bound-member recipe's provenance line) for #24671. Nothing else changes. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../platform-checklist/areas/dashboards.json | 10 ++--- .../areas/records-forms.json | 38 +++++++++---------- docs/qa/platform-checklist/areas/search.json | 8 ++-- .../areas/studio-authoring.json | 22 +++++------ 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/docs/qa/platform-checklist/areas/dashboards.json b/docs/qa/platform-checklist/areas/dashboards.json index 33909a9ccad..bbf491ea24c 100644 --- a/docs/qa/platform-checklist/areas/dashboards.json +++ b/docs/qa/platform-checklist/areas/dashboards.json @@ -111,7 +111,7 @@ "revision": 3, "date": "2026-10-19", "change": "steps[4] corrected from QA run #24573 (18.0 sweep, 5b14b7f3; carried by #24612 at ba4afcc2), re-read at the console pin 4016370af3c9: the Studio widget inspector is a structured form with no free-key or JSON entry (DashboardWidgetInspector.tsx removed the legacy fields), so 'attempt the same stray-key widget through the inspector and capture the surfaced error' could not be executed. The step now asks the run to confirm the inspector offers no free-key path. The same record's second note, that a pending draft is unreadable until publish, does not hold and is not taken: GET /meta/dashboard/NAME?state=draft reads the draft (META_ITEM_READ_PARAMS includes `state`); only `?mode=draft`, a save-door parameter, is refused on the read. No clause changed. Wave anchor #24579 §3", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -447,7 +447,7 @@ "revision": 6, "date": "2026-10-19", "change": "steps[5], acceptance[2].clause, acceptance[4].verify and fixtures.knownGaps[0] corrected from QA run #24573 (18.0 sweep, 5b14b7f3; carried by #24612), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) The combo widget no longer authors series[]: chart-gallery.dashboard.ts drops it under the D3 entry dashboard-widget-chart-config-structure-refused, and the renderer picks each measure's mark (objectui AdvancedChartImpl.tsx: first measure bars, the rest lines, both value axes drawn). The run's combo read (2 bars + 1 line on dual axes) is exactly that default. (2) The single-value tiles are compared to the metric tile by value, not by look: the gallery's metric card carries its own title and placement. No clause substance changed. Wave anchor #24579 §3", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ], "enumSource": { @@ -581,7 +581,7 @@ "revision": 3, "date": "2026-10-19", "change": "negative[1] corrected from QA run #24612 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382: the dangling-binding guard is an author-time rule run at publish and on an active save, never on a draft (runtime-authoring-gate.ts D1), so a draft with a dangling dataset, measure or dimension answers 200 and its publish answers 422 with the named rule. The negative now names the publish door, so a draft 200 is not read as the blank-without-error FAIL. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -815,7 +815,7 @@ "revision": 5, "date": "2026-10-19", "change": "fixtures.requires[2] corrected from QA run #24573 (18.0 sweep, 5b14b7f3; carried by #24612), re-read at the console pin 4016370af3c9: the scratch package needs an app whose navigation points at the dashboard, because Studio's Interfaces pillar is the app navigation tree (StudioDesignSurface.tsx), and its create paths return early with no app. The recipe named only the package and the dashboard. No clause changed. Wave anchor #24579 §3", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -1315,7 +1315,7 @@ "revision": 2, "date": "2026-10-19", "change": "steps[0], steps[3], acceptance[1].clause and acceptance[3].verify corrected from QA run #24558 (18.0 since:v18 sweep, 5b14b7f3), re-read at objectstack f89d29e382. (1) A configured cube over an apiEnabled:false object is refused at registration (api-exposure-door.ts, the registration face), so querying it answers 404 CUBE_NOT_FOUND, consistent with acceptance[2]; OBJECT_API_DISABLED is reached by a query that names the object itself, through its inferred cube (assertInferableCube admits a registered object) or an inline dataset. The run read CUBE_NOT_FOUND where the clause expected OBJECT_API_DISABLED. (2) sys_user_metrics declares no dimensions (system.datasets.ts), so 'dimension columns carry none' needs a dimensioned dataset; step 4 adds showcase_task_metrics by status. Wave anchor #24579 (round 3 named this item)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] } diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index b075030d1d2..d80083ef225 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -230,7 +230,7 @@ "revision": 11, "date": "2026-10-19", "change": "steps[2], acceptance[0].verify, acceptance[6].verify, negative[2] and fixtures.provisioning.why corrected from QA run #24572 (18.0 sweep, 5b14b7f3) and the P0 run #24556, re-read at objectstack f89d29e382. (1) The New form submits select defaults the step never touches (sales_region:'amer', default:true in account.object.ts), so the payload diff expects that key. (2) acceptance[6]'s ownership half is unobservable when the admin clones its own row; the provisioned contributor member cloning a seeded showcase_field_zoo row (allowCreate there, permission-sets.ts) is a non-owner cloner, and the same clone shows the autonumber and formula re-derived. (3) negative[2] named `status`, which defaults on showcase_account; the required field with no default there is `name`. The round-2 Tier 1 note that this item's automated.ref over-claims does not hold and is not taken: the ref already scopes itself to 'acceptance[6]'s READONLY half ONLY … Every other clause still needs the run'. Wave anchor #24579 (round 3 named this item)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -405,7 +405,7 @@ "revision": 4, "date": "2026-10-19", "change": "steps[2], acceptance[1] (clause, evidence) and fixtures.knownGaps[2] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) acceptance[1] asked for all 49 members on the Full edit form, which cannot hold: summary has no zoo carrier (knownGaps[0]) and autonumber is readonly by construction (field.zod.ts) and dropped from create and edit forms by plugin-form autoLayout.ts filterSystemFields; it is read on the detail page. (2) knownGaps[2] said f_secret writes fail closed on stock; the CLI wires a LocalCryptoProvider on every boot (serve.ts), and the run's PATCH answered 200 and read back masked. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ], "enumSource": { @@ -565,7 +565,7 @@ "revision": 4, "date": "2026-10-19", "change": "steps[4], negative[0], negative[1] and automated.ref corrected from QA runs #24572 (5b14b7f3) and #24616 (ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) showcase_inquiry requires `message` (inquiry.object.ts), so the 55-row createMany needs it. (2) negative[1] was too broad: since the ADR-0047 amendment dropdown userFilters chips are allowed on an object list view and only `element: 'tabs'` (and quickFilters) are suppressed — objectui warnSuppressedListNav.ts; project.view.ts ships dropdown chips. (3) negative[0] had no stageable fixture on stock; the run's overlay with a non-boolean comparand stages one (the boolean comparand door refuses it 400 INVALID_FILTER — the run's note that the code is not INVALID_FILTER does not hold at source, boolean-comparand-declared-type-door.ts). (4) The framework dogfood ref is search-scoped and scores no list-view clause; the ref says so. The objectui live specs in the ref are §2's theme and untouched. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -788,7 +788,7 @@ "revision": 4, "date": "2026-10-19", "change": "fixtures.requires[0] and steps[7] corrected from QA run #24572 (18.0 sweep, 5b14b7f3), re-read at objectstack f89d29e382: the revert step named no door, and the run measured DELETE /api/v1/meta/view/NAME (the reset, which restores the artifact default byte-identically); and each named variant is served as its own view item, so its sections can be read before overlaying them. No clause changed. Wave anchor #24579 §3", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -1003,7 +1003,7 @@ "revision": 5, "date": "2026-10-19", "change": "fixtures.requires[1] and steps[2] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382: a second INV-1002 line staged over the data API needs its `amount` too — amount is client-computed and stored as sent (invoice.object.ts), so without it the line carries amount null and the total ignores it. The negative[1] FAIL of that run stays a product defect (objectstack-ai/objectui#13062, fixed at objectui main after the pin; #24619 row (a), open) and no clause changes for it. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -1338,7 +1338,7 @@ "revision": 5, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[1], steps[2], steps[6], acceptance[0] (clause, verify) and negative[0] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2 — the run that hand-drove this item to PASS), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) The console sends top / skip / filter with no `$` (data-objectstack adapter), so the clauses' literal $top reads nothing on the wire. (2) Contacts is a section of the shared 'Related' tab — only relatedList: 'primary' children get their own tab (field.zod.ts). (3) knownGaps[0] said the read-gate personas are fixture-blocked on stock; the invite + sign-up + sys_user_position recipe mints both. The pin and its pass are untouched (the clause-free-pin theme is not this round's). Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -1786,7 +1786,7 @@ "revision": 12, "date": "2026-10-19", "change": "steps[3] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: a type 'form' action navigates to the routed form page /forms/:name (ActionRunner.ts), it does not open a dialog. The run's acceptance[2] / negative[2] FAIL stays the product defect objectstack-ai/objectui#13063 and no clause changes for it; the round's suggestion to name examples/app-showcase/test/record-action-location-hosts.test.ts in automated.ref is not taken here (the ref's objectui half is §2's clause-free-pin theme). Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ], "enumSource": { @@ -1902,7 +1902,7 @@ "revision": 4, "date": "2026-10-19", "change": "variants[0] and fixtures.requires[0] corrected from QA run #24572 (18.0 sweep, 5b14b7f3), re-read at objectstack f89d29e382: account_lifecycle binds `events: ['update']` only (examples/app-showcase/src/data/objects/account.object.ts), so the create-with-non-initial-status leg raises invalid_initial_state from project_status_flow alone (events insert + update, initialStates ['planned'], project.object.ts). The since:v18 run #24558's note that this item describes the advisory hit as server-log only is already on main (revision 3 reads the write answer's warnings) and is not repeated. No clause changed. Wave anchor #24579 §3", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -2012,7 +2012,7 @@ "revision": 5, "date": "2026-10-19", "change": "steps[4] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2): the veto step named no way to make the server reject a drag on stock; the run's recipe (a contributor member's task reassigned away server-side, then dragged → 404 → snap-back with the 'Save failed' toast) needs no lockField and scored acceptance[3] pass. No clause changed. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -2330,7 +2330,7 @@ "revision": 4, "date": "2026-10-19", "change": "steps[1], steps[2], steps[7], acceptance[0] (clause, verify), acceptance[1] (clause, verify) and negative[1] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2 — the run that hand-drove this item to PASS), re-read at the console pin 4016370af3c9. (1) The console sends `filter=` and `sort=` with no `$` (data-objectstack adapter); the clauses named $filter / $orderby, which a wire read never finds — acceptance[2] already said so. (2) The suggested condition pair leaves one row on the stock seed, a single-datapoint for the row-order clause; the step now asks for ≥ 2 rows. (3) The userFilters negative carried the same over-broad scope as list-view-capabilities negative[1]: only tabs-element userFilters are suppressed on an object list (warnSuppressedListNav.ts), dropdown chips are allowed — corrected beside it so the two items agree. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -2431,7 +2431,7 @@ "revision": 3, "date": "2026-10-19", "change": "steps[3], steps[5] and acceptance[3] (clause, verify) corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2 — the run that hand-drove this item to PASS), re-read at the console pin 4016370af3c9. (1) A persona lacking edit gets draggable=false, so the failing-reschedule step produced no optimistic move; the run's recipe — a contributor member's task reassigned away server-side, then dragged → 404 → rollback + toast — is now the step. (2) On showcase_task the empty-day click hands off to the full create form with the date pre-filled (ObjectCalendar.tsx, objectui#12291: a required field quick-create cannot fill), not a quick-create dialog. The pin and its pass are untouched. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -2705,7 +2705,7 @@ "revision": 4, "date": "2026-10-19", "change": "steps[3], steps[6], acceptance[3].verify, acceptance[4] (clause, verify) and source[2] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. The bell is server-backed: plugin-audit's audit-writers.ts writes a collab.mention sys_notification per mentioned user and the console bell reads it through useInboxBell.ts (GET /api/v1/notifications); objectui's useMentionNotifications, which the item cited as the bell, has no console consumer. And no feed refetch follows a post within ~6 s, so the reconcile step reloads. Clause substance unchanged. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -2826,7 +2826,7 @@ "revision": 5, "date": "2026-10-19", "change": "steps[1] and acceptance[1] (clause, verify) corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2 — the run that hand-drove this item to PASS), re-read at the console pin 4016370af3c9: the toolbar offers three densities, Compact / Comfortable / Spacious, stored as compact / medium / tall (normalize-list-view.ts), not the five modes the item listed — those are the spec's row heights, which the renderer narrows onto three. The pin and its pass are untouched. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -3028,7 +3028,7 @@ "revision": 4, "date": "2026-10-19", "change": "steps[5] and acceptance[2].clause corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: showcase_recalc_selection is an api-type action whose one request (POST /api/v1/apps/showcase/recalc) carries `_selectedIds` at the body's top level; params._selectedIds is the grid's hand-off to the action runner (ObjectGrid.tsx), not the wire spelling — list-view-capabilities already states the top-level form for the same action. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -3328,7 +3328,7 @@ "revision": 5, "date": "2026-10-19", "change": "steps[1], a new fixtures.knownGaps entry and the traps list corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2 — the run that hand-drove this item to PASS), re-read at the console pin 4016370af3c9: steps[1] repeated its poll/results clause twice; the run found a hand-driven console route that needs no gated spec, whose Undo is a native window.confirm (ImportWizard.tsx) that Playwright dismisses by default, so the automation-input trap is now listed. The pin and its pass are untouched. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -3515,7 +3515,7 @@ "revision": 6, "date": "2026-10-19", "change": "steps[0], negative[2], fixtures.requires[3], acceptance[8] (verify, evidence) and automated.ref corrected from wave anchor #24579 round 2 part 1 (6272032736) and QA runs #24598 / #24616 (ba4afcc2), re-read at objectstack f89d29e382. (1) The hook list route is GET /api/v1/meta/hook; /meta/types/hook answers 404. (2) acceptance[8]'s refusal answers 500 INTERNAL_ERROR with the class in the server log only; #24619 row (b) (open) tracks the coded refusal, so the clause states the shipped envelope and scores against #24619. (3) The ref named source files (hook-wrappers.ts, hook-binder.ts) no resolver can run; it now names four test files, each measured at base under os-verify-lock (83 passed), mapped to the clause whose mechanism it pins. (4) The run-proven scratch-hook recipe is recorded in requires[3]. Part 1's note on a `$select` in the steps does not hold: no step of this item spells `$select` (0 hits in records-forms.json)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -4389,7 +4389,7 @@ "revision": 2, "date": "2026-10-19", "change": "negative[0] and acceptance[2].verify corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: the History tab reads sys_audit_log (RecordDetailView.tsx), a compliance audit that is not per-field-gated and correctly renders an untracked field's old → new; the per-field opt-in governs the sys_activity summary / Discussion timeline only, so negative[0] now names that surface. The task page has no tablist, so the absent tab is corroborated by the absent sys_audit_log fetch. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -5010,7 +5010,7 @@ "revision": 2, "date": "2026-10-19", "change": "fixtures.requires gains the member's grant on sys_comment and sys_comment_reaction, from QA run #24558 (18.0 since:v18 sweep, 5b14b7f3): a plain org member holds no object permission on either (403 PERMISSION_DENIED on both doors), so every member clause needs a runtime set the item never named; the run bound one (qa_comment_set). No clause changed. Wave anchor #24579 §3 (the record's continuation comment)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -5095,7 +5095,7 @@ "revision": 2, "date": "2026-10-19", "change": "personas[1], fixtures.knownGaps[0], steps[0], steps[1] and acceptance[2].clause corrected from QA runs #24558 (5b14b7f3) and #24616 (ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) knownGaps[0] was stale: the pinned console builds and was served. (2) The meta read serves the authored view, so step 1 has no resolved pageSize / editInline to read (`pagination` is an optional block; its pageSize default materialises only inside one); the defaults show in the renderer. (3) The inline affordance is the Edit inline switch, and the edit mode is session state that starts off (ObjectView.tsx), so 'offered' does not mean editable cells on load. (4) The updater persona needs a contributor binding and assigned tasks. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] } diff --git a/docs/qa/platform-checklist/areas/search.json b/docs/qa/platform-checklist/areas/search.json index 3e6eb2d16a0..b95a04b9d71 100644 --- a/docs/qa/platform-checklist/areas/search.json +++ b/docs/qa/platform-checklist/areas/search.json @@ -8,7 +8,7 @@ "qa-contributor-bound-member": { "title": "A CONTRIBUTOR-BOUND member — an identity that reads a strict SUBSET of showcase_invoice rows (invited sign-up + one sys_user_position row)", "why": "Both-sides RLS needs a persona for whom a specific row is invisible while the object itself is readable. ⚠️ Neither seeded demo persona is that persona, and reaching for them is the trap this recipe closes — measured on 17.1.0 (#10257): Mei (phone.demo@example.com) holds NO position, so showcase_contributor never merges and showcase_invoice answers PERMISSION_DENIED — the OBJECT gate, no rows at all, not a subset; Ada (auditor.demo@example.com) holds `auditor` → showcase_auditor, which carries `showcase_invoice: { allowRead: true, viewAllRecords: true }` — VAMA bypasses record scope entirely, so she sees all 12 — also not a subset. The persona that IS a subset is a member bound to the `contributor` position: showcase_contributor grants read/create/edit on showcase_invoice while the `invoice_own_rows` RLS narrows selects to `owner == current_user.email`, and none of the seeded invoices are owned by a fresh sign-up. Nothing here needs a seed change or a repo file — the whole gap was that the recipe existed only as prose on one item, which is why a runner reached for the demo personas instead and recorded blocked(fixture).", - "provenance": "run #7629 (framework, 2026-08-11) drove search.rls-both-personas to 4/4 PASS on exactly this persona — 3 invoices visible to the member vs 15 to admin. Promoted from prose on that one item to a replayable area recipe by the 17.1.0 sweep (#10236 B1); the Mei/Ada measurements above are from #10257. Step 1 re-pointed invite-first by #21060 after QA run #21056 met the invite_only default (403 SELF_REGISTRATION_CLOSED on an uninvited sign-up). Step 5's body gained the `owner` it always needed after QA run #23166 met an own-row invisible to its creator (#23169). Steps 2 and 3 name the admin REST doors QA run #24574 measured (an admin GET of sys_user by email and an admin POST of sys_user_position, the position as plain text), and step 5 names the invoice number field `name`, after that run met 400 INVALID_FIELD on `number` and #24593 met 400 on an OData `$filter` (wave anchor #24579, PR #PENDING-24579R4).", + "provenance": "run #7629 (framework, 2026-08-11) drove search.rls-both-personas to 4/4 PASS on exactly this persona — 3 invoices visible to the member vs 15 to admin. Promoted from prose on that one item to a replayable area recipe by the 17.1.0 sweep (#10236 B1); the Mei/Ada measurements above are from #10257. Step 1 re-pointed invite-first by #21060 after QA run #21056 met the invite_only default (403 SELF_REGISTRATION_CLOSED on an uninvited sign-up). Step 5's body gained the `owner` it always needed after QA run #23166 met an own-row invisible to its creator (#23169). Steps 2 and 3 name the admin REST doors QA run #24574 measured (an admin GET of sys_user by email and an admin POST of sys_user_position, the position as plain text), and step 5 names the invoice number field `name`, after that run met 400 INVALID_FIELD on `number` and #24593 met 400 on an OData `$filter` (wave anchor #24579, PR #24671).", "app": "showcase", "requires": [ "a stock dev boot — the `contributor` position declares `showcase_contributor` in its `permissionSets` (examples/app-showcase/src/security/positions.ts), which the resolver reads by name (ADR-0131 D3/D4). ⛔ Do NOT hand-create a sys_position_permission_set row: no resolver reads it, so it proves nothing about the merge", @@ -392,7 +392,7 @@ { "revision": 2, "date": "2026-08-11", "change": "the OFF-inertness clause now names an oracle that exists. It demanded a specific plugin log line ('OS_SEARCH_PINYIN_ENABLED is off — inert') which run #7629 proved unobtainable under CLI serving: the flag gates one layer upstream, the CLI omits pinyin-search from `requires`, the plugin is never loaded, and a 1.2 MB debug boot log contained zero occurrences of 'pinyin' (47 loaded plugins off vs 48 on). That is a STRONGER proof of inertness than the log line would have been, so the clause now reads the plugin roster / `requires` difference plus the zero-hit grep, and keeps the inert line only as the alternative oracle for a host that loads the plugin unconditionally — the run is told which route it is on by the roster itself. Step 7, the fixtures requirement and the plugin source citation follow; knownGaps records the unobtainability so the next author does not restore the log-line oracle. This was the item's ONLY partial in #7629 — the substance (both flag sides, per-variant recall, backfill) is unchanged", "ref": "#7647" }, { "revision": 3, "date": "2026-08-18", "change": "re-targeted clause 3's OFF-side expectation at the initials forms. It demanded that 'zhangwei' stop returning 张伟 with the flag off, but the seeded contact carries email zhangwei@huaning.example and email is searchable:true, so the term still matches through the source column — correct behaviour that the clause scores as the flag failing to disable. The initials (zw / hnkj / wf) have no source-column spelling and are the honest discriminator (#9467 CF-4)", "ref": "#9386" }, { "revision": 4, "date": "2026-10-17", "change": "clause 2 named 'wf' as an account initials form, but 'wf' is the initials of the seeded CONTACT 王芳 (company 华宁科技); the clause now places 'wf' on showcase_contact and 'hnkj' on showcase_account, and its ⛔ note adds 'wangfang', which still matches 王芳 through her email with the flag off. The roster counts in clause 3 and the knownGap gain the current reading (51 vs 52) and clause 3 states that the one-plugin difference, not the absolute count, is the oracle. Re-run on main c91af8ed6: flag ON (stock, zh-CN default) contact zhangwei/zw/张 → 张伟, contact wf → 王芳, account huaning/hnkj/华 → 华宁科技; flag OFF (OS_SEARCH_PINYIN_ENABLED=false, fresh DB, debug log) contact zw → 0, wf → 0, 张 → 张伟, zhangwei → 张伟 and wangfang → 王芳 (email column), account hnkj → 0, huaning and 华 → 华宁科技; banner 'Plugins: 51 loaded' OFF vs 52 ON, the difference PinyinSearchPlugin, and zero 'pinyin' occurrences in the 589 KB OFF debug boot log", "ref": "#24036" }, - { "revision": 5, "date": "2026-10-19", "change": "fixtures.requires[3], fixtures.knownGaps[0], steps[6] and acceptance[3] (verify, evidence) corrected from QA run #24574 (18.0 sweep, 5b14b7f3; carried by #24593), re-read at objectstack f89d29e382. The item asked for each boot's `requires` list, which no boot prints: packages/cli/src/commands/serve.ts builds `requires` from the served capabilities and never logs it, and the banner (packages/cli/src/utils/format.ts) prints `Plugins: N loaded`. The roster is the readable form, and the one-plugin difference with PinyinSearchPlugin as the missing entry stays the oracle. The 5b14b7f3 count (52 vs 53) is recorded beside the older readings; the absolute count still moves with the plugin set. No clause substance changed. Wave anchor #24579 §3", "ref": "#PENDING-24579R4" } + { "revision": 5, "date": "2026-10-19", "change": "fixtures.requires[3], fixtures.knownGaps[0], steps[6] and acceptance[3] (verify, evidence) corrected from QA run #24574 (18.0 sweep, 5b14b7f3; carried by #24593), re-read at objectstack f89d29e382. The item asked for each boot's `requires` list, which no boot prints: packages/cli/src/commands/serve.ts builds `requires` from the served capabilities and never logs it, and the banner (packages/cli/src/utils/format.ts) prints `Plugins: N loaded`. The roster is the readable form, and the one-plugin difference with PinyinSearchPlugin as the missing entry stays the oracle. The 5b14b7f3 count (52 vs 53) is recorded beside the older readings; the absolute count still moves with the plugin set. No clause substance changed. Wave anchor #24579 §3", "ref": "#24671" } ] }, { @@ -578,7 +578,7 @@ { "revision": 2, "date": "2026-08-11", "change": "two text corrections from run #7629, no clause substance changed. (1) The RLS-parity fixture carried the same stale invoice count as search.rls-both-personas — the seed ships 12 rows INV-1001..INV-1012, not 8; INV-1003's owner (linus) is now named so the invisible-row premise is checkable in place. (2) knownGaps records that this path inherits the declared-case-insensitive contract and the #7641 executor gap, since searchAll also emits `$contains` — without it a run scores a case-driven miss as a palette defect. Direction is per the #4706 Q2 = A ruling: case-insensitive is the declared truth, so the checklist reconciles toward the declaration and #7641 owns the product side", "ref": "#7647" }, { "revision": 3, "date": "2026-08-12", "change": "the executor gap this item's third knownGap recorded is CLOSED, so the gap flips to past tense. #7641 found searchAll to be a SECOND producer of search clauses with the same declared≠enforced defect — metadata-protocol/src/protocol.ts built its AND-of-OR from `$contains` under a comment asserting `$contains` was the case-insensitive operator — and moved it onto `$icontains` in the same PR as search-filter.ts. No clause substance changed; a case-varying miss on a current build is now a real palette/UI defect rather than an expected executor FAIL, which is the scoring instruction this revision corrects", "ref": "#7641" }, { "revision": 4, "date": "2026-10-13", "change": "fixtures.requires[2] names the recipe's owner rule (persona recipe gap, #23166 A5), and steps[5] reaches /search by URL (stale step, #23166 A6); both parked on #23121, carried by #23169. A member-created showcase_invoice is invisible to its creator unless `owner` is set to the member's email, so the area recipe qa-contributor-bound-member's step 5 now carries it, with a recipe knownGap for the same rule on showcase_task (assignee). The palette's Open Full Search Page is offered only for an empty query or one matching 'search all results full page' (CommandPalette.tsx at objectui 1fb1ced36243, still this repo's .objectui-sha), and it drops the query, so the step reaches the page by URL. QA run #23166 passed both clauses that way", "ref": "#23169" }, - { "revision": 5, "date": "2026-10-19", "change": "steps[2], fixtures.requires[0], acceptance[0].clause and acceptance[2].clause corrected from QA run #24574 (18.0 sweep, 5b14b7f3; carried by #24593 at ba4afcc2), checklist-accuracy findings re-read at the console pin .objectui-sha 4016370af3c9 and unchanged at objectui main 282b138c07. (1) The palette's minLength is 2 (objectui react useRecordSearch, left at its default by CommandPalette.tsx), so the suggested query '张' never reaches the wire and reads 'No results found.' — the CJK example is now '张伟'. (2) The accelerator toggles by source design (CommandPaletteProvider.tsx: only the button, links, deep-links and programmatic callers share the idempotent openCommandPalette()), so 'open when already open is a no-op' now covers the non-keyboard affordances and a repeated ⌘K closing the palette is not a FAIL. Wave anchor #24579 §3", "ref": "#PENDING-24579R4" } + { "revision": 5, "date": "2026-10-19", "change": "steps[2], fixtures.requires[0], acceptance[0].clause and acceptance[2].clause corrected from QA run #24574 (18.0 sweep, 5b14b7f3; carried by #24593 at ba4afcc2), checklist-accuracy findings re-read at the console pin .objectui-sha 4016370af3c9 and unchanged at objectui main 282b138c07. (1) The palette's minLength is 2 (objectui react useRecordSearch, left at its default by CommandPalette.tsx), so the suggested query '张' never reaches the wire and reads 'No results found.' — the CJK example is now '张伟'. (2) The accelerator toggles by source design (CommandPaletteProvider.tsx: only the button, links, deep-links and programmatic callers share the idempotent openCommandPalette()), so 'open when already open is a no-op' now covers the non-keyboard affordances and a repeated ⌘K closing the palette is not a FAIL. Wave anchor #24579 §3", "ref": "#24671" } ] }, { @@ -791,7 +791,7 @@ "revision": 3, "date": "2026-10-19", "change": "fixtures.requires[2], steps[4] and steps[5] corrected from QA run #24593 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382. (1) searchAll caps each object at perObject = 5 by default (max 25, protocol.ts searchAll) and `truncated` describes only the overall limit, so step 5's literal query returns 5 admin hits without INV-1003 and reads as acceptance[3]'s admin half failing; the step now sends perObject=25. (2) The suggested hidden-field value is unusable on a fresh boot: last_login_ip is stamped from the trusted proxy's forwarded header (sys-user.object.ts) and is null after a direct sign-in, and ban_reason is null; the measured stock V is `role` = `user`. No clause changed. Wave anchor #24579 round 2 §3 (6272314386)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] } diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index f94398c1eb5..ca1e469f6ea 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -88,7 +88,7 @@ { "revision": 1, "date": "2026-08-07", "change": "new area item: the full first-run authoring loop, grounded step-by-step in the 2026-07 Studio dogfood audit ('The loop closes'), with the audit's open UX findings carried as knownGaps instead of asserted", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-18", "change": "corrected the ?id= spelling in step 8 and clause 4, which said in an item whose step 2 creates the package com.example.repairs — inviting the package-id spelling that answers with an empty items array (#9334's class). Found by grounding rather than from a report: this item was not on the #9334 list. ⚠️ The card's other instruction for this item — drop any inherited 'Studio is not installed' assumption (#9453 CF-4) — needed NO edit: the item never carried one. It has no blocked marker, no such knownGap, and its steps drive the App Builder directly; the assumption lived in the wave briefing and the run records, not here", "ref": "#9386" }, { "revision": 3, "date": "2026-10-01", "change": "checklist-accuracy findings of QA run #21056, each grounded in objectui at the pin: the object is named repairs_repair_ticket (the dialog prefixes the package namespace), and every step and clause now uses that name; the New object dialog carries a pre-selected OWD choice; the zero-nav-items knownGap is superseded (the Create-app scaffold checkbox defaults on); steps[8]'s probe is a forged request because the console hides authoring on a read-only package; acceptance[0]'s cited objectui pin targets testids absent at the pin", "ref": "#21060" }, - { "revision": 4, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[1], steps[2], steps[3] and steps[6] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604 at ba4afcc2) and the P0 run #24556's continuation, re-read at the console pin 4016370af3c9. (1) knownGaps[0] said publish has no review step; Publish now opens the Pending changes sheet with per-item diff and a separate 'Publish all' (DraftChangesPanel.tsx). (2) A CJK-only display name derives no package id (PackageFormDialog.tsx toIdSegment). (3) There is no Save draft button for a field: the designer autosaves, and a picklist is held until it has an option ('Not saved yet: … needs at least one option'). (4) The publish toast names the count (objectui#11807). (5) The Studio house icon goes to the Studio landing, not the console Home. No clause changed. Wave anchor #24579 (round 3 named this item)", "ref": "#PENDING-24579R4" } + { "revision": 4, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[1], steps[2], steps[3] and steps[6] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604 at ba4afcc2) and the P0 run #24556's continuation, re-read at the console pin 4016370af3c9. (1) knownGaps[0] said publish has no review step; Publish now opens the Pending changes sheet with per-item diff and a separate 'Publish all' (DraftChangesPanel.tsx). (2) A CJK-only display name derives no package id (PackageFormDialog.tsx toIdSegment). (3) There is no Save draft button for a field: the designer autosaves, and a picklist is held until it has an option ('Not saved yet: … needs at least one option'). (4) The publish toast names the count (objectui#11807). (5) The Studio house icon goes to the Studio landing, not the console Home. No clause changed. Wave anchor #24579 (round 3 named this item)", "ref": "#24671" } ] }, { @@ -179,7 +179,7 @@ "revision": 5, "date": "2026-10-19", "change": "steps[4] and steps[5] corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: after the first save the editor autosaves (ResourceEditPage.tsx, the metadata-admin:autosave toggle defaults on), so the steps' 'Save draft' is implicit, and the field reorder is native HTML5 drag-and-drop, which raw mouse moves do not drive. No clause changed. Wave anchor #24579 §3", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -252,7 +252,7 @@ "history": [ { "revision": 1, "date": "2026-08-07", "change": "new item: list/form view authoring through the ?mode=draft door with live-in-app verification, grounded in ViewSchema's container contract and the audit's zero-restart loop", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-10-02", "change": "re-pointed clause 4 at the object door's designed shape. It expected the container name qa_repair_asset_views in GET /meta/view?object=repair_asset; the door serves the container's expanded . items instead, by design since #7163 (runtime door #7736 / #13407). The 17.6.0 release-verification run measured the authored bodies round-tripping as repair_asset.default / repair_asset.form with the container name appearing 0 times", "ref": "#21330" }, - { "revision": 3, "date": "2026-10-19", "change": "personas[1], fixtures.requires[0] and steps[3] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604), re-read at objectstack f89d29e382: a runtime object needs `sharingModel` to publish (security-owd-unset is error tier in validate-security-posture.ts, though the draft PUT answers 200), the authored form view shows in the New / Edit dialogs rather than on the opened record's page, and a non-admin list render of a runtime object needs a grant. No clause changed. Wave anchor #24579 §3", "ref": "#PENDING-24579R4" } + { "revision": 3, "date": "2026-10-19", "change": "personas[1], fixtures.requires[0] and steps[3] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604), re-read at objectstack f89d29e382: a runtime object needs `sharingModel` to publish (security-owd-unset is error tier in validate-security-posture.ts, though the draft PUT answers 200), the authored form view shows in the New / Edit dialogs rather than on the opened record's page, and a non-admin list render of a runtime object needs a grant. No clause changed. Wave anchor #24579 §3", "ref": "#24671" } ] }, { @@ -347,7 +347,7 @@ "revision": 5, "date": "2026-10-19", "change": "the page's object moved from showcase_invoice to showcase_contact (fixtures.requires[0], steps[0], steps[2], steps[6], acceptance[0].verify, acceptance[3].verify), with personas[1] and steps[3] beside it — §3 of wave anchor #24579 from QA run #24571 (5b14b7f3), confirmed by #24604 (ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. showcase_invoice ships showcase_invoice_document isDefault:true, which usePageAssignment serves ahead of any authored page and which makes a second default answer 422 DEFAULT_RECORD_PAGE_CONFLICT, so acceptance[3] could not pass on it; showcase_contact ships no record page and #24604 scored acceptance[3] pass there. No stock non-admin reads showcase_contact, so personas[1] names the grant or the admin. The page editor shows Basics / Data Context / Advanced, not a Layout section. Clause substance unchanged", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -482,7 +482,7 @@ { "revision": 4, "date": "2026-08-17", "change": "re-spell after [#7941]: the rollback step and the audit clause named X-Actor as an alternate actor source alongside the session identity; #7941 removed the header limb from resolveMetaWriteActor entirely (landed as commit 4dc8a618e, PR #9119) so it is now the SOLE source and X-Actor is ignored outright, not consulted even as a fallback. Re-spelled the rollback step, the audit acceptance clause, and the rest-server.ts source pointer to name the single source; added a new step + acceptance clause pairing the inverted check the finding asked for — send X-Actor and confirm it does NOT change the recorded actor — since that is now a meaningful QA-layer assertion nothing else covers (the unit pins live in packages/rest/src/meta-write-actor-identity.test.ts, added as a source pointer). The capability itself (audit rows name the acting user, never anonymous) is unchanged and was never wrong.", "ref": "#9118" }, { "revision": 5, "date": "2026-10-13", "change": "steps[9] and acceptance[8] (clause and verify) diff the two publish versions read off /history, and steps[8] and acceptance[7].verify say how history counts (assertion defect, #23166 A8; parked on #23121, carried by #23169). sys_metadata_history records draft saves and publishes alike, so ?from=1&to=2 compared the first draft save with its own publish (changed []); on QA run #23166's clean two-round run the published revisions were v2 and v4, and their diff named the edited widget key. The rollback steps are unchanged", "ref": "#23169" }, { "revision": 6, "date": "2026-10-16", "change": "steps[2] and acceptance[6] (clause, verify) re-pointed (checklist-accuracy finding 4 of QA run #23942, wave #23878). The Studio counter is no longer 'Changes · n' text but a numbered pill on the Changes button (objectui#12459), and the pill counts the drafts of the package in scope — an env-wide (package-less) dashboard draft is counted in none, so #23942 staged the browser leg's draft in a writable package. The step and clause name the pill and the writable-package staging, and the step says how to reach the draft in Studio — through an app of that package whose navigation names the dashboard, since a package with no app offers no dashboard to select (the re-run on #24036 found no chip until it staged one). Not loosened: a chip or pill that survives the publish, or is missing while the draft is pending, still FAILS", "ref": "#24036" }, - { "revision": 7, "date": "2026-10-19", "change": "steps[2], acceptance[6].clause and acceptance[6].verify corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: the Studio item header reads 'Draft · N unpublished' (engine.studio.itemDraftPending, objectui#12725) — 'Unpublished draft' is the chip of other surfaces (ObjectListViewInspector, the OWD overview), not this item header — and a REST publish shows in Studio only after a reload. The round's other draft notes are not taken: no step of this item discards a draft (so the ?state=draft discard spelling has no clause to correct), and the NS_ prefix a scratch dashboard was given is not required (the namespace-prefix rule judges objects only — packages/spec/src/kernel/namespace-prefix.ts). Wave anchor #24579 round 2 §3 (6272314386)", "ref": "#PENDING-24579R4" } + { "revision": 7, "date": "2026-10-19", "change": "steps[2], acceptance[6].clause and acceptance[6].verify corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: the Studio item header reads 'Draft · N unpublished' (engine.studio.itemDraftPending, objectui#12725) — 'Unpublished draft' is the chip of other surfaces (ObjectListViewInspector, the OWD overview), not this item header — and a REST publish shows in Studio only after a reload. The round's other draft notes are not taken: no step of this item discards a draft (so the ?state=draft discard spelling has no clause to correct), and the NS_ prefix a scratch dashboard was given is not required (the namespace-prefix rule judges objects only — packages/spec/src/kernel/namespace-prefix.ts). Wave anchor #24579 round 2 §3 (6272314386)", "ref": "#24671" } ] }, { @@ -557,7 +557,7 @@ { "revision": 1, "date": "2026-08-07", "change": "new item: authoring validation with located errors and verified non-persistence, sampling object + view kinds and cross-referencing the deepened dashboard stray-key item instead of duplicating it", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-11", "change": "recorded run #7695's sighting of the ALREADY-KNOWN spurious '_diagnostics' banner on a freshly saved VALID draft (the designer re-validates the server's own annotation on read-back) as a knownGap + a NOT-this-item's-FAIL negative, so the sighting is not filed as a new defect and the false positive is told apart from a real located-error miss. Note only; no clause changed", "ref": "#7753" }, { "revision": 3, "date": "2026-09-23", "change": "re-spelled the located-error shape, which had stopped being true: the clause quoted '[invalid_metadata] … fields..type: Required', but saveMetaItem's spec-validation refusal no longer opens with a bracketed restatement of its code (retired in 7a25a3ee9c; the token rides code INVALID_METADATA), and on the REST and dispatcher doors its message is a headline of 'path [zod code]' locators with the per-issue prose on issues[]. Measured on c11852406 for a field missing its type: 'object/qa_invalid_probe failed spec validation: 1 issue — fields.amount.type [invalid_value]', 422, INVALID_METADATA. The dispatcher source pointer presented that test's hand-built mock message as the producer's text; it now says what the test pins, and the producer's per-face renderer is cited beside it. The capability itself (4xx, located path, nothing persisted) is unchanged", "ref": "#19709" }, - { "revision": 4, "date": "2026-10-19", "change": "steps[1], steps[3], acceptance[1] (clause, verify) and acceptance[3].verify corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) The quoted container guidance answers only a container-shaped body; a flat `type`/`columns` body is judged as an inline view config and refused as naming no `object`/`viewKind`, with the same Wrap remedy (view.zod.ts). (2) The Studio designer holds an incomplete shape client-side and never PUTs it, so the browser door that reaches the server gate is the Setup metadata editor's Source tab, which the run used. Wave anchor #24579 §3", "ref": "#PENDING-24579R4" } + { "revision": 4, "date": "2026-10-19", "change": "steps[1], steps[3], acceptance[1] (clause, verify) and acceptance[3].verify corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) The quoted container guidance answers only a container-shaped body; a flat `type`/`columns` body is judged as an inline view config and refused as naming no `object`/`viewKind`, with the same Wrap remedy (view.zod.ts). (2) The Studio designer holds an incomplete shape client-side and never PUTs it, so the browser door that reaches the server gate is the Setup metadata editor's Source tab, which the run used. Wave anchor #24579 §3", "ref": "#24671" } ] }, { @@ -878,7 +878,7 @@ "revision": 4, "date": "2026-10-19", "change": "fixtures.requires[1] and acceptance[4] (verify, evidence) corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), re-read at the console pin 4016370af3c9: in package scope the Studio Access matrix lists only the package's own objects (PermissionMatrixEditor.tsx), so the wide stock objects the fixture named are reachable only on the environment door; and headless Chromium paints no scrollbar, so the scroll is evidenced by scrollWidth against clientWidth and the scrolled screenshot. Clause substance unchanged. Wave anchor #24579 §3", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -1436,7 +1436,7 @@ { "revision": 2, "date": "2026-10-06", "change": "partial-coverage gap of QA run #21784 parked on #22017 (row 17). A7 was blocked(fixture): an invalid stored row cannot be planted, because every authoring door validates at save. Disposition: recorded knownGap. The leg cannot be reached on a stock environment by design. The knownGap now names the environment it needs (a store already holding a stale row), says no CI job builds one, and points at the producer-level pin as mechanism-only. No clause changed", "ref": "#22017" }, { "revision": 3, "date": "2026-10-13", "change": "acceptance[1].verify compares stats.locked against the served envelope lock (stale spec, #23166 A1; parked on #23121, carried by #23169). getMetaDiagnostics counts an item as locked when its read envelope reports a lock other than 'none' — the same derivation getMetaItem publishes (#21694) — not when its declared _lock is set. QA run #23166 compared against the envelope lock and passed. Read from source at 6d7f4df3c (protocol.ts getMetaDiagnostics docblock and loop). The A6 row of the same run, the unplantable invalid stored row, is already recorded in full in knownGaps[0] and needs no edit", "ref": "#23169" }, { "revision": 4, "date": "2026-10-16", "change": "acceptance[1].verify names the read that carries the envelope lock (checklist-accuracy finding 2 of QA run #23942, wave #23878). It told the runner to compare stats.locked against the lock GET /api/v1/meta/TYPE/NAME publishes, but that default read is the cached arm and carries no `lock` by design; the envelope lock is on GET /api/v1/meta/TYPE/NAME/layers, which is where #23942 read it (object 107, view 0, flow 32 locked, matching stats). The comparison itself is unchanged", "ref": "#24036" }, - { "revision": 5, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[6], steps[8] and acceptance[6] (clause, verify) corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), the round-2 §3 note that knownGaps[0] is wrong. The invalid stored row is plantable in-run on an isolated scratch file DB: save a valid view through the door with ?package=, rewrite its sys_metadata.metadata, and the next sweep lists it — the run scored acceptance[6] pass that way, deep link and badge included. The directory always repairs ?package= to a project package, so the red button is judged per package scope; rootless union-parent entries may ride beside the located leaf. Wave anchor #24579 round 2 §3 (6272314386)", "ref": "#PENDING-24579R4" } + { "revision": 5, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[6], steps[8] and acceptance[6] (clause, verify) corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), the round-2 §3 note that knownGaps[0] is wrong. The invalid stored row is plantable in-run on an isolated scratch file DB: save a valid view through the door with ?package=, rewrite its sys_metadata.metadata, and the next sweep lists it — the run scored acceptance[6] pass that way, deep link and badge included. The directory always repairs ?package= to a project package, so the red button is judged per package scope; rootless union-parent entries may ride beside the located leaf. Wave anchor #24579 round 2 §3 (6272314386)", "ref": "#24671" } ] }, { @@ -1525,7 +1525,7 @@ { "revision": 1, "date": "2026-09-30", "change": "new — the 清单项待写 row of docs/NORTH-STAR.md beside platform-core.docs-audience-gate and platform-core.docs-portal-render; drafted with objectui#10188 and corrected by the at-tier review of objectui PR 11258 (the UI-mirror clause scoped to entries naming an installed doc or book; no signed-out persona — /docs sits behind ProtectedRoute)", "ref": "#20939" }, { "revision": 2, "date": "2026-10-13", "change": "knownGaps[2]'s fixture recipe re-pointed (knownGap stale, #23166 A0; parked on #23121, carried by #23169). The Studio doc editor no longer answers the read-only showcase package with a 422: it saves an env-wide draft and offers no package picker, so the recipe's reason for a writable base changed, not its need. The books and docs must share one package, so the recipe drives the writable base over the metadata API with ?package=. QA run #23166 did exactly that and passed", "ref": "#23169" }, { "revision": 3, "date": "2026-10-16", "change": "knownGaps[1] closed (checklist-accuracy finding 9 of QA run #23942, wave #23878): the .objectui-sha pin has been past the fork window since daeb77fc84dd — the console carries the refusal page and the menu pruning — so the knownGap no longer asks for a console built elsewhere. Wording only: no step, request or oracle changed", "ref": "#24036" }, - { "revision": 4, "date": "2026-10-19", "change": "steps[0] aligned with fixtures.knownGaps[2] from QA run #24571 (18.0 sweep, 5b14b7f3): the step said to create the doc in the Studio doc editor while knownGaps[2] prescribes authoring every book, doc and the app over the metadata API with ?package= so they share one package; the run drove the API recipe. No clause changed. Wave anchor #24579 §3", "ref": "#PENDING-24579R4" } + { "revision": 4, "date": "2026-10-19", "change": "steps[0] aligned with fixtures.knownGaps[2] from QA run #24571 (18.0 sweep, 5b14b7f3): the step said to create the doc in the Studio doc editor while knownGaps[2] prescribes authoring every book, doc and the app over the metadata API with ?package= so they share one package; the run drove the API recipe. No clause changed. Wave anchor #24579 §3", "ref": "#24671" } ] }, { @@ -1780,7 +1780,7 @@ "revision": 2, "date": "2026-10-19", "change": "steps[2] and steps[4] corrected from QA run #24558 (18.0 since:v18 sweep, 5b14b7f3), re-read at objectstack f89d29e382. (1) A save into a non-owning package of showcase_task cannot reach OBJECT_OVERLAY_PACKAGE_MISMATCH: the sealed code object answers 403 NOT_OVERRIDABLE first, which the item's own pin protocol-runtime-owner-overlay-refusal.test.ts holds as its code-owner control; the 422 needs a runtime object another package owns. (2) POST /packages/:id/publish collects only in-memory registry members (metadata-manager.ts), so for a Studio package it answers success:false 'No metadata items found'; the published version revert needs comes from publish-drafts, the door the revert pin's own measured recipe uses. Clause substance unchanged", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }, @@ -1863,7 +1863,7 @@ "revision": 2, "date": "2026-10-19", "change": "acceptance[1] (clause, verify) corrected from QA run #24558 (18.0 since:v18 sweep, 5b14b7f3) — §3's since:v18 bullet 'save-door spec refusals carry no rule id' — re-read at objectstack f89d29e382: the spec-schema refusals are zod custom issues with a located path and no rule key (page.zod.ts checkPageSlotPair is one), protection / _lock answer 400 VALIDATION_ERROR and the default-page conflict its own code, and only the authoring-lint refusals name a rule id. The clause demanded a rule id on every refusal. Wave anchor #24579 §3 (round 3 named this item)", - "ref": "#PENDING-24579R4" + "ref": "#24671" } ] }