diff --git a/docs/qa/platform-checklist/RUNNER.md b/docs/qa/platform-checklist/RUNNER.md index 6ffbc6dc018..aa51614c902 100644 --- a/docs/qa/platform-checklist/RUNNER.md +++ b/docs/qa/platform-checklist/RUNNER.md @@ -236,6 +236,22 @@ contradicts it, and correct it here when it does. record a connector clause as `blocked(environment)` on the strength of `fetch failed` alone. +- **A per-boot `OS_HOME` and `OS_STORAGE_LOCAL_ROOT` do NOT isolate a package install — + boot every item that installs a package from a scratch copy of the app, never from the + shared tree.** install-local caches each installed manifest under the server's + **working directory**, `.objectstack/installed-packages/` + (`packages/cloud-connection/src/local-manifest-source.ts`, `DEFAULT_INSTALLED_PACKAGES_DIR` + resolved against `process.cwd()`; the CLI mounts the plugin with no `storageDir`), so + a boot started in `examples/app-showcase` writes into the checked-out tree, and every + later boot from that tree rehydrates the package. **Check:** `ls + examples/app-showcase/.objectstack/installed-packages/` after the install leg — any + file there means the install escaped. ⚠️ `git status` cannot see it: `.objectstack` is + in the root `.gitignore`, so a clean status is no evidence either way. Items this applies to: + `platform-core.marketplace-install-local-lifecycle` and + `platform-core.marketplace-console-honesty`. A QA run that installed from the tree + wrote a CRM manifest there for 25 s before removing it through + `DELETE /api/v1/marketplace/install-local/` (#23163). + - **`ss` is not installed in these containers — read liveness with `curl`, never a socket table.** `ss` and `netstat` are both absent (`command not found`); `lsof` and `fuser` are present. The trap is that the usual spelling hides the cause: `ss -ltn | grep` diff --git a/docs/qa/platform-checklist/areas/ai.json b/docs/qa/platform-checklist/areas/ai.json index c088ae1c9ba..987fec62d5a 100644 --- a/docs/qa/platform-checklist/areas/ai.json +++ b/docs/qa/platform-checklist/areas/ai.json @@ -593,10 +593,10 @@ }, { "id": "ai.console-ai-surface-gating", - "title": "Console AI affordances gate on the access-filtered agent catalog: an agent-less boot hides FAB / chat dock / ⌘⇧I / top-bar link and redirects a stale /ai bookmark without flash — with the ungated bare system/ai-approvals route pinned as an expected-fail probe", + "title": "Console AI affordances gate on the access-filtered agent catalog: an agent-less boot hides FAB / chat dock / ⌘⇧I / top-bar link and redirects a stale /ai bookmark without flash — and the ungated bare system/ai-approvals route degrades honestly: one 501 read stops the poll, and the error replaces the empty queue (objectui#11736)", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "browser", "personas": ["admin (form sign-in — the pending-actions poll sends the COOKIE half only, credentials:'include' with no bearer header, so an injected-token session reads as anonymous there)"], @@ -620,8 +620,8 @@ "press ⌘⇧I (the chat-dock toggle) and confirm nothing mounts — ConsoleLayout only arms the listener when dockEnabled", "navigate directly to /ai (the stale-bookmark path): capture that a loading fallback holds while the catalog resolves, then the redirect to home lands with the splash preserved — record whether any frame of chat UI flashed", "as admin, sweep the surfaces that could advertise AI Approvals — the app sidebars, the settings hub (/apps/setup/system, which forwards to …/system/settings) and Home — and record whether any offers an 'AI Approvals' entry on this agent-less boot (expected: none; the SystemHubPage card wall that carried one was retired by objectui#3743, carried out in objectui#10507)", - "navigate directly to the bare /apps/setup/system/ai-approvals route (still registered with no AI-surface gate in the console route table — AppContent systemRoutes); capture the network for ~15s: GET /api/v1/ai/pending-actions?status=pending firing every ~5s and answering 501 each time (poll never stops on error — usePendingActions clears nothing and re-arms)", - "capture what the page renders: the destructive alert's text (must carry the Cloud/EE remedy sentence from the 501 body), AND whether the 'No actions waiting / When the AI proposes a sensitive action it will appear here for review' empty state renders beneath it as if a live queue exists", + "navigate directly to the bare /apps/setup/system/ai-approvals route (still registered with no AI-surface gate in the console route table — AppContent systemRoutes); capture the network for ~15s: exactly ONE GET /api/v1/ai/pending-actions?status=pending, answering 501, and no re-arm after it — since objectui#11736 usePendingActions stops on a refused read (501, 403, 404, any 4xx but 408 / 429) and backs off only on a transient one", + "capture what the page renders: the destructive alert's text (must carry the Cloud/EE remedy sentence from the 501 body), and confirm the 'No actions waiting / When the AI proposes a sensitive action it will appear here for review' empty state does NOT render beneath it — the inbox draws the empty queue only when the read answered", "capture the browser console for the whole session" ], "acceptance": [ @@ -656,16 +656,16 @@ "evidence": "the alert screenshot + one captured 501 response" }, { - "clause": "EXPECTED FAIL at head (defect K2, same row): honest degradation means no fake empty queue and no unbounded dead poll. At head, beneath the error alert the inbox ALSO renders the 'No actions waiting' empty state (rows.length === 0 branch is not error-aware, AiPendingActionsInbox.tsx) — reading as a live, empty approval queue on a deployment that has none — and the 5s poll re-arms forever against the dead endpoint (usePendingActions keeps polling on error). Score against the honest contract; record the observed fake-empty-queue + endless poll as the FAIL evidence, do not tick green", + "clause": "honest degradation on the bare route: no fake empty queue and no unbounded dead poll. The inbox renders the destructive alert INSTEAD of the 'No actions waiting' empty state (the empty state is drawn only when the read answered, AiPendingActionsInbox.tsx), and the poll stops after the one 501 (a refused read ends it, usePendingActions.ts). This was expected-fail K2 until objectui#11736 fixed both halves", "oracle": "network", - "verify": "the ~15s network capture shows the repeated 501s with no backoff/stop; the page screenshot shows the empty-queue panel rendered alongside the error alert", + "verify": "the ~15s network capture shows a single 501 read of /api/v1/ai/pending-actions and no further request; the page screenshot shows the error alert with no empty-queue panel beneath it. objectui pins both halves — packages/plugin-chatbot/src/__tests__/usePendingActions.pollFaultPolicy-11736.test.tsx ('a refused read stops the poll') and AiPendingActionsInbox.errorReplacesEmpty-11736.test.tsx ('an errored read shows the error, not an empty queue'); run them from an objectui checkout at the .objectui-sha revision to cite them beside the live read", "evidence": "the poll trace + the page screenshot" } ], "negative": [ "ticking any AI capability as PRESENT from the 200 empty-agents courtesy is a recording error — the empty catalog is the hide signal (same negative as ai.open-edition-honest-degradation)", "scoring an affordance ABSENT from a DOM read taken before a settled screenshot is the hydration-race trap — the gated controls are hidden during load BY DESIGN, so a too-early read proves nothing", - "the expected-fail clause (clause 6) must not flip to PASS silently: if a run observes the empty-queue panel suppressed under the error and the poll bounded, the defect was fixed — revise this item (drop the expected-fail wording, bump revision) rather than quietly ticking", + "a second pending-actions read inside the capture window, or the 'No actions waiting' panel drawn beneath the error, is a REAL fail since objectui#11736 — a regression, not the old expected-fail K2; reproduce it on a fresh load before recording it", "an anonymous probe of /ai/pending-actions answering 501 instead of 401 would be a REGRESSION of the #7653 anonymous-deny ordering (auth gate precedes every capability answer) — file it, it is not this item's expected 501" ], "traps": ["hydration-race", "stale-console-bundle"], @@ -674,15 +674,16 @@ "objectui packages/app-shell/src/layout/ConsoleLayout.tsx (FAB + chat dock + ⌘⇧I all gated on showChatbot/dockEnabled)", "objectui packages/app-shell/src/layout/AppHeader.tsx (top-bar AI entry gated on the same hook) + console/home/HomeLayout.tsx (Home CTAs)", "objectui packages/app-shell/src/console/ConsoleShell.tsx (RequireAiSurface — waits for resolve, splash-preserving redirect, objectui#6507)", - "objectui apps/console/src/AppContent.tsx (systemRoutes: system/ai-approvals still registered with no AI-surface gate — the K2 route half; the bare …/system landing forwards to the settings hub since SystemHubPage and its 'AI Approvals' card were retired, objectui#3743 / objectui#10507) + pages/system/AiPendingActionsPage.tsx (thin wrapper, 'Polled every 5 seconds')", - "objectui packages/plugin-chatbot/src/usePendingActions.ts,217-300 (cookie-only call(), error → error.message, pollInterval 5000 re-arming regardless of errors) + AiPendingActionsInbox.tsx (destructive alert + the error-blind 'No actions waiting' empty state)", + "objectui apps/console/src/AppContent.tsx (systemRoutes: system/ai-approvals still registered with no AI-surface gate, which is why steps 6-7 reach it by URL; the bare …/system landing forwards to the settings hub since SystemHubPage and its 'AI Approvals' card were retired, objectui#3743 / objectui#10507) + pages/system/AiPendingActionsPage.tsx (thin wrapper)", + "objectui packages/plugin-chatbot/src/usePendingActions.ts (cookie-only call(), error → error.message; since objectui#11736 the poll follows the answer it last got — a refused read stops it, a transient failure backs it off within a ceiling) + AiPendingActionsInbox.tsx (destructive alert; the empty state renders only when the read answered) + their pins __tests__/usePendingActions.pollFaultPolicy-11736.test.tsx and __tests__/AiPendingActionsInbox.errorReplacesEmpty-11736.test.tsx", "packages/runtime/src/domains/ai.ts#capabilityUnavailable (#7653 anonymous-deny first; the /ai/agents empty-catalog courtesy #4058/#4053; every other /ai/* → capabilityUnavailable 501) + domains/unavailable.ts (single-sourced remedy sentence)", "ai.open-edition-honest-degradation (the API half this item mirrors in the browser — 501 body/discovery parity is proven THERE, not re-proven here)" ], "history": [ { "revision": 1, "date": "2026-08-30", "change": "new item (sweep 2026-08-30, cross-angle hit 1+4): the console-side AI gating had no coverage — the catalog-signal design (useAiSurfaceEnabled) hides FAB/dock/⌘⇧I/header/Home affordances and no-flash-redirects /ai on an agent-less boot, while SystemHubPage's 'AI Approvals' card is built ungated and its page polls the dead /ai/pending-actions endpoint every 5s (501) rendering a fake empty queue beside the error alert. Authored as a NEW browser sibling of ai.open-edition-honest-degradation (which keeps the API half) rather than extending it, so neither surface double-covers the other; the two K2 defect clauses are expected-fail probes, the defect row itself is the sweep's FOLLOW-UPS entry", "ref": "#sweep-2026-08-30" }, { "revision": 2, "date": "2026-10-05", "change": "clause 4, steps 6-7, negative 3, the title and the source re-pointed (stale, #21782 VF4). objectui 9eea12a2b (objectui#10507, fixing objectui#3743) removed SystemHubPage and its 'AI Approvals' card before 17.6.0, so K2's card half is moot. The clause now asserts that no console surface advertises the entry (verifier's wording). The route half of K2 remains: system/ai-approvals is still registered ungated (AppContent systemRoutes), so steps 6-7 reach it by URL and clause 6 keeps its expected-fail scoring. Negative 3 names clause 6 only. Re-read at objectui 9dfaca6543: unchanged", "ref": "#21797" }, - { "revision": 3, "date": "2026-10-06", "change": "partial-coverage gap of QA run #21782 parked on #22017 (row 9). A2's entitled side was blocked(fixture) as the clause prescribed. Disposition: recorded knownGap. The side cannot be reached in the open framework by design (service-ai is Cloud/Enterprise). The knownGap names the environment it needs, says CI lacks it, and reclassifies the verdict blocked(environment); A2's clause follows. It also names the objectui hook pins as mechanism-only", "ref": "#22017" } + { "revision": 3, "date": "2026-10-06", "change": "partial-coverage gap of QA run #21782 parked on #22017 (row 9). A2's entitled side was blocked(fixture) as the clause prescribed. Disposition: recorded knownGap. The side cannot be reached in the open framework by design (service-ai is Cloud/Enterprise). The knownGap names the environment it needs, says CI lacks it, and reclassifies the verdict blocked(environment); A2's clause follows. It also names the objectui hook pins as mechanism-only", "ref": "#22017" }, + { "revision": 4, "date": "2026-10-13", "change": "acceptance[5] (clause and verify) flipped from expected-fail K2 to the positive contract, with steps[6] and [7], negative[2], the title and source[4] and [5] (stale spec, #23163 C5; parked on #23121, carried by #23169). objectui#11736 made the poll follow its last answer (a refused read stops it, a transient one backs off within a ceiling) and made the inbox draw its empty state only when the read answered. QA run #23163 read one 501, no re-arm, and no empty state under the alert, at objectstack 36b2889c with objectui 1fb1ced36243. Re-read from objectui source at that pin, still this repo's .objectui-sha, including the two pins now named in the clause", "ref": "#23169" } ] }, { diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index ff6b5af48e1..edf1eed85c2 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -1057,10 +1057,10 @@ }, { "id": "api-backend.api-console-discovery-execute", - "title": "Studio Developer API Console: the endpoint tree mirrors /discovery, a seeded-object GET executes with live JSON + status + timing, and a malformed body surfaces the server error envelope verbatim", + "title": "Developer API Console (a deep link on a stock boot): the endpoint tree mirrors /discovery, a seeded-object GET executes with live JSON + status + timing, and a malformed body surfaces the server error envelope verbatim", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "browser", "personas": [ @@ -1069,12 +1069,12 @@ "fixtures": { "app": "showcase", "requires": [ - "the console Developer hub reachable (objectui apps/console/src/pages/developer/ApiConsolePage.tsx, developer:api-console route)", + "the console Developer hub reachable (objectui apps/console/src/pages/developer/ApiConsolePage.tsx, developer:api-console route) — on a stock boot BY DEEP LINK: /_console/apps/setup/developer/api-console. Its only nav entry, nav_api_console (componentRef developer:api-console), is declared in packages/platform-objects/src/apps/studio.app.ts#nav_api_console, the Studio app, which a stock boot does not serve (GET /api/v1/meta/app lists showcase_app, setup and account — RUNNER.md environment facts), so 'Studio → Developer → API Console' is not on screen", "stock showcase seed with showcase_task rows (the seeded object the GET executes against)" ] }, "steps": [ - "boot showcase isolated; sign in as admin; open Studio → Developer → API Console", + "boot showcase isolated; sign in as admin; open /_console/apps/setup/developer/api-console directly (the Studio → Developer → API Console nav lives only in the Studio app, which a stock boot does not serve — fixtures.requires[0])", "GET /api/v1/discovery out of band and record its services + routes maps — the ground truth the tree is gated on (useApiDiscovery gates each service group on isServiceUsable(discovery.services[name]), ADR-0076 D12)", "screenshot the endpoint tree AFTER the 'Discovering APIs' spinner settles; only then read the DOM", "spot-check the tree against REST_ROUTE_LEDGER families: a seeded object (showcase_task) shows its CRUD family (GET/POST/PATCH/DELETE /api/v1/data/showcase_task) under a Data group, the Metadata group lists /api/v1/meta/object/showcase_task, System lists discovery/packages/health", @@ -1135,6 +1135,12 @@ "date": "2026-08-08", "change": "new — Studio Developer API Console: discovery-mirrored tree, live GET execution (JSON+status+timing), malformed-body envelope passthrough; per PENDING-GAPS §C", "ref": "claude/platform-test-checklist-ocwugl" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "steps[0], fixtures.requires[0] and the title reach the page by deep link (stale step, #23163 C0; parked on #23121, carried by #23169). The 'Studio → Developer → API Console' nav entry, nav_api_console, is declared only in packages/platform-objects/src/apps/studio.app.ts, and a stock boot does not serve the Studio app. QA run #23163 reached /_console/apps/setup/developer/api-console by URL and passed every clause there", + "ref": "#23169" } ] }, diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 4982ddfb3d4..ad216cc0dd9 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -330,17 +330,17 @@ "title": "An approval notification deep-links straight into the request drawer", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P2", "surface": "browser", "personas": ["any pending approver"], "fixtures": { "app": "showcase", - "requires": ["at least one pending request on which the signed-in approver can be notified through the REMIND or REASSIGN path — POST /api/v1/approvals/requests/:id/remind (or /reassign {to}) mints a deep-linked notification on demand (since PR #22625 the OPENING also emits approval.requested to each concrete approver, so a freshly opened request is a second source); the three seeded demo requests suffice as the request source"] + "requires": ["at least one pending request on which the signed-in approver can be notified — through the OPENING (since PR #22625 opening a request emits approval.requested to each concrete approver on the slate it opens on) or through REASSIGN (POST /api/v1/approvals/requests/:id/reassign {to}). ⚠ REMIND is SUBMITTER-only: POST …/:id/remind answers 403 'FORBIDDEN: only the submitter may send reminders' to anyone else (packages/plugins/plugin-approvals/src/approval-service.ts remind), and the dev admin is an APPROVER on the seeded demo requests, not their submitter — so as the admin, mint the notification by opening a fresh request (or by reassign), never by remind; the three seeded demo requests still suffice as a reassign source"] }, "steps": [ "boot showcase isolated (dogfood §0); sign in as the dev admin (a pending approver on the seeded requests)", - "mint the notification: POST /api/v1/approvals/requests/:id/remind on a pending request (or /:id/reassign {to: }) — the remind/reassign path notifies on demand; opening a request also emits approval.requested to each concrete approver on the slate it opens on (PR #22625), so a freshly opened request's row is an equally valid source", + "mint the notification: open a fresh request the admin approves (the opening emits approval.requested to each concrete approver on its slate, PR #22625) — or POST /api/v1/approvals/requests/:id/reassign {to: } on a pending request. Not /remind: it is submitter-only and answers the admin 403 'FORBIDDEN: only the submitter may send reminders' on the seeded requests, which a run must not score as a deep-link FAIL", "read the resulting notification row and record its actionUrl — it must carry /system/approvals?request= (#2678 P1.5; ApprovalService.notify rewrites the bare /system/approvals into the deep link centrally, so every emitting topic inherits it)", "cold-load that exact URL in a fresh page (no prior navigation); wait for settle; capture the drawer that opens", "GET /api/v1/approvals/requests/:id and cross-check the drawer's request identity and status against the read", @@ -395,7 +395,8 @@ { "revision": 2, "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 3, "date": "2026-08-10", "change": "added the route-parity clause: after #7213 the same ?request= link must open the drawer on BOTH /system/approvals (what notifications carry) and the component route the account app mounts", "ref": "#7331" }, { "revision": 4, "date": "2026-08-11", "change": "fixtures re-anchored on the remind/reassign path — the old requires rested on a request's OPEN notifying somebody, and no approval.opened topic exists; and the cold-load clause's evidence restated observably (drawer field-matched against the API instead of 'screenshots with the URL visible', which the console's consumption of the query param makes unmeetable)", "ref": "#7530" }, - { "revision": 5, "date": "2026-10-10", "change": "the open-time gap is closed: since PR #22625 (#22607) openNodeRequest publishes approval.requested to each concrete approver on the slate the request opens on, through the same notify() deep-link rewrite — the knownGaps entry that said opening notifies nobody is removed, the source note names the topic, step 2 and the fixture note say the opening is a second source; the fixtures stay on remind/reassign (deterministic on demand) and the revision-4 row stands as history", "ref": "#22631" } + { "revision": 5, "date": "2026-10-10", "change": "the open-time gap is closed: since PR #22625 (#22607) openNodeRequest publishes approval.requested to each concrete approver on the slate the request opens on, through the same notify() deep-link rewrite — the knownGaps entry that said opening notifies nobody is removed, the source note names the topic, step 2 and the fixture note say the opening is a second source; the fixtures stay on remind/reassign (deterministic on demand) and the revision-4 row stands as history", "ref": "#22631" }, + { "revision": 6, "date": "2026-10-13", "change": "fixtures.requires[0] and steps[1] stop sending the admin through /remind (assertion defect, #23163 A0; parked on #23121, carried by #23169). ApprovalService.remind refuses every caller but the request's submitter, 'FORBIDDEN: only the submitter may send reminders', and the dev admin is an approver on the seeded requests. QA run #23163 met the 403 and minted the notification through the opening path instead, then passed every clause. The opening path (PR #22625) and /reassign are the two notification sources an approver can drive", "ref": "#23169" } ] }, { @@ -664,14 +665,14 @@ "title": "An active out-of-office delegation reroutes an individually-routed approver to the delegate; expiring the window hands the slot back", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", "personas": ["dev admin — the DELEGATOR A: the delegation row is written BY them FOR them (sys_approval_delegation is self-service-only, so the delegator must be whoever is authenticated)", "a SECOND REAL runtime account — the DELEGATE B (Ada Auditor usr_showcase_auditor_demo, provisioned as a real login per knownGaps), who holds their own bearer token and decides the rerouted request as themselves"], "fixtures": { "app": "showcase", "requires": [ - "a flow with an INDIVIDUALLY-routed approver — OOO delegation only applies to type user / field / manager (ApprovalService.expandApprovers), NOT position. No ACTIVE seeded showcase flow routes individually (they all route by position), so author a scratch autolaunched flow in a WRITABLE package with a single approval node config.approvers=[{type:'user', value:''}], shaped after examples/app-showcase/src/automation/flows/approver-bindings.flow.ts (the record-backed approver specimen)", + "a flow with an INDIVIDUALLY-routed approver — OOO delegation only applies to type user / field / manager (ApprovalService.expandApprovers), NOT position. No ACTIVE seeded showcase flow routes individually (they all route by position), so author a scratch flow in a WRITABLE package with a RECORD-TRIGGERED start — start config {objectName: , triggerType: 'record-after-create'} — and a single approval node config.approvers=[{type:'user', value:''}], shaped after examples/app-showcase/src/automation/flows/approver-bindings.flow.ts (the record-backed approver specimen, whose start is record-triggered the same way). ⚠ Not a trigger-only start: an approval node needs a record, and POST /api/v1/automation//trigger cannot supply one — buildAutomationContext (packages/runtime/src/domains/automation.ts) maps the body's recordId into params only, while the approval node reads its record id from $record, which the engine seeds from context.record, so the run answers 400 \"Approval node '…': no record id in $record\" (packages/plugins/plugin-approvals/src/approval-node.ts)", "delegator A = the SIGNED-IN dev admin (so the delegation is a self-service write); delegate B = a second real runtime account with its own credentials and bearer token — Ada Auditor (usr_showcase_auditor_demo / auditor.demo@example.com), provisioned as a real login per the knownGap below" ], "knownGaps": [ @@ -683,13 +684,13 @@ "steps": [ "boot showcase isolated (dogfood §0); provision B as a real login — a stock boot already does it, see knownGaps — and hold B's bearer token separately from the admin's", "sign in as the dev admin (the delegator A)", - "in a writable/scratch package author + register (POST /api/v1/automation) an autolaunched flow with a single approval node whose approvers=[{type:'user', value:''}], behavior first_response", - "baseline (no delegation): trigger the flow (POST /api/v1/automation//trigger); GET /api/v1/approvals/requests?status=pending, find the new request, GET /:id — pending_approvers must be [A]; GET /:id/actions shows NO ooo_substitute row", + "in a writable/scratch package author + register (POST /api/v1/automation) a flow whose start is record-triggered on a scratch object (start config objectName + triggerType 'record-after-create') and whose single approval node has approvers=[{type:'user', value:''}], behavior first_response", + "baseline (no delegation): start a run by creating one record of the trigger object (POST /api/v1/data/); GET /api/v1/approvals/requests?status=pending, find the new request, GET /:id — pending_approvers must be [A]; GET /:id/actions shows NO ooo_substitute row", "create an ACTIVE delegation AS A (self-service): POST /api/v1/data/sys_approval_delegation {delegate_id:'', valid_from:, valid_until:, reason:'Annual leave'} — omit delegator_id and the guard stamps the caller; passing a delegator_id that is not the caller is refused (see the negative below)", - "trigger the flow AGAIN; GET the new request /:id — pending_approvers must now be [B], not [A]; GET /:id/actions — one row action='ooo_substitute' whose comment names 'A → B'", + "start a run AGAIN (create another record of the trigger object); GET the new request /:id — pending_approvers must now be [B], not [A]; GET /:id/actions — one row action='ooo_substitute' whose comment names 'A → B'", "read B's inbox/notifications — a topic approval.ooo_substituted notification addressed to B, actionUrl /system/approvals?request= (#1322 M4)", "decide as B, WITH B'S OWN BEARER TOKEN (not the admin's, and not an override): POST /api/v1/approvals/requests/:id/approve; GET /:id/actions — the approve row's actor is B (the delegate acts under their OWN identity — nothing impersonated as A)", - "expire the window: as A, PATCH the delegation valid_until to a past instant (or DELETE it); trigger the flow ONCE more; GET the newest request /:id — pending_approvers is [A] again and its /actions carries NO ooo_substitute row" + "expire the window: as A, PATCH the delegation valid_until to a past instant (or DELETE it); start a run ONCE more (create a record); GET the newest request /:id — pending_approvers is [A] again and its /actions carries NO ooo_substitute row" ], "acceptance": [ { @@ -742,7 +743,8 @@ { "revision": 1, "date": "2026-08-08", "change": "initial — pins the #1322 OOO delegation reroute: active A→B window reroutes an individually-routed slot to B (audited + notified, decided under B's own identity); expiry hands it back to A at resolution time", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-11", "change": "persona shape rewritten around what is constructible: the old fixture had the admin write a delegation on behalf of a non-admin delegator, which sys_approval_delegation's self-service-only guard refuses from everybody (#4839) and which the named delegator could not do either (no auth account). Now A = the signed-in admin delegating their OWN slot and B = a second REAL runtime account deciding with its own bearer token — the mirror image #7517 actually drove, and the stronger test. Added the foreign-delegator refusal as a negative and the better-auth issuer provisioning detail to knownGaps", "ref": "#7530" }, { "revision": 3, "date": "2026-08-18", "change": "delegate B needs no hand-provisioning any more: the showcase gives Ada Auditor a real better-auth credential account on a stock boot, so the issuer knownGap is rewritten as closed-by-fixture. The OTHER two gaps are untouched and still bind — the self-service-only delegation guard (#4839) and the writable/scratch package the individually-routed flow needs", "ref": "#9308" }, - { "revision": 4, "date": "2026-09-19", "change": "same re-anchoring as the sibling items: the retired plugin-auth backfill citation is replaced by the retirement's migration entry in packages/spec and the reason is kept, the parenthetical 'issuer derived from the dev admin's own row' is withdrawn as false, and the step that told a runner to check issuer=local:credential when provisioning delegate B goes with it — that field no longer exists", "ref": "#17453" } + { "revision": 4, "date": "2026-09-19", "change": "same re-anchoring as the sibling items: the retired plugin-auth backfill citation is replaced by the retirement's migration entry in packages/spec and the reason is kept, the parenthetical 'issuer derived from the dev admin's own row' is withdrawn as false, and the step that told a runner to check issuer=local:credential when provisioning delegate B goes with it — that field no longer exists", "ref": "#17453" }, + { "revision": 5, "date": "2026-10-13", "change": "fixtures.requires[0] and steps[2], [3], [5] and [8] start the scratch flow from a record instead of POST …/trigger (assertion defect, #23166 A0; parked on #23121, carried by #23169). The trigger route cannot open an approval: buildAutomationContext maps the body's record into params only, the approval node reads its record id from $record, which the engine seeds from context.record, and the run answers 400 'no record id in $record'. QA run #23166 drove the item through a record-triggered start (the approver-bindings.flow.ts shape) and passed every clause. Read from source at 6d7f4df3c: runtime domains/automation.ts buildAutomationContext, plugin-approvals approval-node.ts", "ref": "#23169" } ] }, { diff --git a/docs/qa/platform-checklist/areas/attachments-storage.json b/docs/qa/platform-checklist/areas/attachments-storage.json index f8bcb0dacd6..7de6282a077 100644 --- a/docs/qa/platform-checklist/areas/attachments-storage.json +++ b/docs/qa/platform-checklist/areas/attachments-storage.json @@ -8,7 +8,7 @@ "qa-scratch-authz": { "title": "Scratch authz parents (qa_vault / qa_shared / qa_nofiles) + two real member personas", "why": "Stock showcase has NO private files-enabled parent: its only `enable.files` object is showcase_project, which is public_read_write AND readable through the showcase `everyone` baseline. Without this recipe the entire authz core of this area — the 403 deny side of download-authz-both-sides, the invisible-parent case of read-inherits-parent-rls, and the read-but-not-edit split of attach-requires-parent-edit — is blocked(fixture) on every run. Run #7635 closed the gap at runtime and scored those items instead of blocking them; this block is that provisioning made permanent and replayable.", - "provenance": "run #7635 (framework 92f26f75) → #7670", + "provenance": "run #7635 (framework 92f26f75) → #7670 Step 6 gained persona B's sys_attachment read after QA run #23166 met the panel's up-front refusal (#23169).", "app": "showcase", "requires": [ "an admin session that holds the `manage_metadata` capability — PUT /api/v1/meta/:type/:name is capability-gated per ADR-0066 D1 (#6603), so a bare authenticated session is NOT enough (packages/rest/src/rest-route-ledger.ts, the `PUT /api/v1/meta/:type/:name` row)", @@ -53,7 +53,7 @@ { "step": 6, "call": "bind each persona by inserting a sys_user_permission_set row in system context", - "expect": "persona A = read-but-NOT-edit on the scratch objects (the attach-requires-parent-edit split); persona B = no grant beyond the everyone baseline (the parent-invisible persona). Prove the split INDEPENDENTLY before scoring any storage clause: read the parent as each persona and confirm the intended visibility, so a later 403 is attributable to the storage gate rather than to a mis-bound persona.", + "expect": "persona A = read-but-NOT-edit on the scratch objects (the attach-requires-parent-edit split); persona B = no grant beyond the everyone baseline (the parent-invisible persona). Prove the split INDEPENDENTLY before scoring any storage clause: read the parent as each persona and confirm the intended visibility, so a later 403 is attributable to the storage gate rather than to a mis-bound persona. ⚠ Persona B needs READ on sys_attachment itself (and nothing on the scratch parents), unless its grants and the everyone baseline already carry it: without it the console's attachments panel refuses up front — 'You don't have access to these attachments.' — before any download is attempted, which is the listing gate answering, not the download gate the deny clauses score (QA run #23166). Check B's sys_attachment read as part of the independent split proof.", "source": "sys_user_permission_set insert in system context, mirroring showcase-permission-zoo.dogfood.test.ts — the binding shape access-security.json's permission-set items already replay. NOTE (ADR-0090 D5): every authenticated member ALSO holds the everyone baseline additively, so persona B's effective access is the UNION of its grants and that baseline — compute the union before calling a read 'invisible'." } ], @@ -173,7 +173,7 @@ "title": "Gated downloads: 401 anonymous, 403 parent-invisible, signed URL for the entitled — per gating class (attachments-scope / field-owned / public_read opt-out / ungated)", "since": "v15.1", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": ["admin (uploader/owner)", "signed-up member who cannot read the parent record", "anonymous"], @@ -199,7 +199,7 @@ "GET the same routes as the uploader/admin and follow the signed URL", "compare TTLs: the gated grant uses the short downloadTtl (default 300s), not the 3600s presignedTtl — read expiry material from the minted URL/descriptor where the adapter exposes it", "flip one file to acl='public_read' (system write) and GET /files/ anonymously — the opt-out must restore the stable anonymous capability URL", - "in the browser, open the parent record's RecordAttachmentsPanel as the denied member and capture the surfaced copy" + "in the browser, open the parent record's RecordAttachmentsPanel as the denied member — persona B, holding READ on sys_attachment (qa-scratch-authz step 6) — attempt the download, and capture the surfaced copy. Without that read the panel refuses up front ('You don't have access to these attachments.') before any download is attempted: that is the listing gate, not the download denial acceptance[4] scores" ], "acceptance": [ { @@ -253,6 +253,12 @@ "date": "2026-08-18", "change": "clause 3 (acl='public_read' reopens the anonymous capability URL) pinned — QA run #9401 scored this item partial with the OPEN side of the anonymous gate unpinned, which is the half a deny-only suite cannot see: a download-authz suite made only of refusals stays green on a surface that has started refusing everything. automated.ref now names the both-sides pin, records that the TTL branch is read out of the minted URL so the public_read early return is distinguishable from an authorized grant, and states that clause 4 (the panel's denial copy) stays manual with no pin. knownGaps marks the public_read seed gap CLOSED for the API half only, and carries the run's cross-tenant marker so it is not re-derived every wave.", "ref": "#9483" + }, + { + "revision": 4, + "date": "2026-10-13", + "change": "steps[6] names persona B's sys_attachment read, and the area recipe qa-scratch-authz step 6 now provisions it (persona recipe gap, #23166 A4; parked on #23121, carried by #23169). Without read on sys_attachment the attachments panel refuses before any download, so acceptance[4]'s friendly-denial screenshot would capture the listing refusal instead of the download one. QA run #23166 granted B that read and passed the clause. No clause changed", + "ref": "#23169" } ] }, diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index 87c56e01cc4..040c7312b7a 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -380,7 +380,7 @@ "title": "Every demonstrable flow node type authors in the designer, executes in a run, and surfaces its step in run logs", "since": "v15", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": [ @@ -422,7 +422,7 @@ "steps": [ "boot showcase isolated (dogfood §0); sign in as the dev admin", "GET /api/v1/automation/actions (the designer palette feed) and record the descriptor list; open the Studio flow-designer palette and screenshot it", - "record_change chain: POST a showcase_task then PATCH status→done — fires showcase_task_completed (script+notify), showcase_fan_out_notify (parallel+http), showcase_resilient_sync (try_catch), showcase_task_done_notify_owner (subflow); the create also fires showcase_task_follow_up (wait) and showcase_declarative_connector_ping / showcase_mcp_connector_echo (connector_action)", + "record_change chain: POST a showcase_task WITH `assignee` set to a real user's email, under a project that has an `owner`, then PATCH status→done — fires showcase_task_completed (script+notify), showcase_fan_out_notify (parallel+http), showcase_resilient_sync (try_catch), showcase_task_done_notify_owner (subflow); the create also fires showcase_task_follow_up (wait) and showcase_declarative_connector_ping / showcase_mcp_connector_echo (connector_action). ⚠ The notify steps address the task's assignee and its project's owner: a task created without an assignee, or under a project without an owner, makes them fail LOUDLY for want of a recipient — a correct refusal, not a notify FAIL (QA run #23166)", "on-demand chain: POST /api/v1/automation//trigger for showcase_batch_reminders {params:{tasks:[…]}} (loop), showcase_inquiry_purge (get_record+decision+delete_record — seed at least one closed showcase_inquiry first), showcase_release_signoff {params:{items:[…]}} (map)", "browser chain: run the Tasks row action showcase_bulk_reassign → showcase_reassign_wizard (screen + update_record)", "approval chain: PATCH a showcase_expense_report to submitted (approval via showcase_expense_signoff); PATCH a showcase_project budget above 100000 to open showcase_budget_approval, then POST /api/v1/approvals/requests/:id/revise so the run parks at the approval_revise node", @@ -517,6 +517,12 @@ "date": "2026-10-05", "change": "clauses 1 and 2 re-pointed (assertion defects, #21784 VF4), plus a knownGap from FE13. spec automation/flow.zod.ts FLOW_STRUCTURAL_NODE_TYPES = ['start','end']: those have no executor and no descriptor, so they are not in GET /automation/actions, and the palette clause now covers the 19 non-structural variants. The service-automation engine's executeNode returns on end before any step push, while start logs a success step, so end is proven by the run reaching completed. The verifier's wording is used for both. FE13: showcase_fan_out_notify needs Slack egress, so parallel / http are proven on another run where egress is denied", "ref": "#21797" + }, + { + "revision": 5, + "date": "2026-10-13", + "change": "steps[2] sets an assignee on the run-created task (recipe note, #23166 A1; parked on #23121, carried by #23169). The chain's notify steps address the task's assignee and its project's owner (showcase_task_done_notify_owner hops record.project.owner), so a task created without them makes notify refuse loudly for want of a recipient, which is correct. QA run #23166 met that refusal on tasks and projects created without an assignee or owner; its notify cell still passed (only A1-http was blocked, for egress). No clause changed", + "ref": "#23169" } ], "enumSource": { @@ -753,7 +759,7 @@ "title": "Screen flow round-trip: action trigger → paused run → rendered dialog → resume with inputs → persisted write", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "browser", "personas": [ @@ -772,7 +778,7 @@ "boot showcase isolated (dogfood §0); sign in as the dev admin; open /apps/showcase_app/showcase_task", "start capturing POSTs to /api/v1/automation/**", "open the first row's action menu ([data-testid=row-action-trigger]) and click row-action-showcase_bulk_reassign", - "record the trigger POST; GET /api/v1/automation/showcase_reassign_wizard/runs to find the new run — status must be paused", + "record the launch POST — POST /api/v1/actions/showcase_task/showcase_bulk_reassign (the action door; see clause 1) — and GET /api/v1/automation/showcase_reassign_wizard/runs to find the new run — status must be paused", "GET /api/v1/automation/showcase_reassign_wizard/runs/:runId/screen and record the served screen contract", "the console renders the screen as a dialog: screenshot it (heading 'New Assignee')", "fill #ff-new_assignee with a unique value and Submit; record the resume POST body", @@ -785,7 +791,7 @@ { "clause": "the flow action triggers a run that pauses at the screen node", "oracle": "network", - "verify": "a POST matching /automation/[^/]+/trigger was issued and the run reads status=paused at node 'collect'", + "verify": "a POST to /api/v1/actions/showcase_task/showcase_bulk_reassign (or its /:recordId form) was issued and answers the pause, and the run reads status=paused at node 'collect'. A click on a DECLARED type:'flow' action launches through the action door, POST /api/v1/actions/:object/:action, so every server-side gate the action declares applies (objectui useConsoleActionRuntime flow handler, objectui#12037); only an undeclared flow start (an inline page button, a dashboard header's synthesized action) stays on POST /api/v1/automation/:flow/trigger, so a trigger-route POST here would mean the console bypassed the declared action", "evidence": "network trace + run read" }, { @@ -862,6 +868,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": 4, + "date": "2026-10-13", + "change": "acceptance[0].verify and steps[3] re-pointed from the trigger route to the action door (stale spec, #23163 A0; parked on #23121, carried by #23169). Since objectui#12037 a click on a declared type:'flow' action — showcase_bulk_reassign is one — launches through POST /api/v1/actions/:object/:action, which runs the flow and answers the pause; the trigger route is left to undeclared flow starts. QA run #23163 saw POST /actions, the run paused at 'collect', and every other clause pass. Re-read from objectui source at 1fb1ced36243 (useConsoleActionRuntime.tsx), still this repo's .objectui-sha; the framework door is runtime route-ledger.ts /api/v1/actions/:object/:action", + "ref": "#23169" } ] }, @@ -1140,7 +1152,7 @@ "title": "The developer Flow Runs page triggers a run with typed inputs and drives a screen flow's pause to completion (no orphaned paused rows)", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "mixed", "personas": [ @@ -1159,7 +1171,7 @@ "typed-input run: select 'Batch Task Reminders (Loop)' (showcase_batch_reminders); the Test Run panel shows the `tasks` input as a JSON `list` textarea; paste three task-shaped objects [{id,title,owner}×3] and click 'Run Flow'", "record the trigger POST (client.automation.execute → POST /api/v1/automation/showcase_batch_reminders/trigger) and screenshot the Result envelope; confirm a fresh row appeared in the Recent Runs panel (client.automation.listRuns → GET /automation/showcase_batch_reminders/runs)", "GET /api/v1/automation/showcase_batch_reminders/runs and match the newest run id/status against the panel row", - "screen-flow run: select 'showcase_reassign_wizard'; fill recordId (a real showcase_task id) + new_assignee, click 'Run Flow' — the Result reads 'Waiting for input' (status=paused) and the FlowRunner dialog opens on THIS page", + "screen-flow run: select 'showcase_reassign_wizard'; fill recordId (a real showcase_task id) and LEAVE new_assignee EMPTY, click 'Run Flow' — the Result reads 'Waiting for input' (status=paused) and the FlowRunner dialog opens on THIS page. ⚠ A non-empty new_assignee satisfies the screen's one required field up front (the caller supplied a declared field — packages/services/service-automation/src/screen-input-contract.ts), so the run completes WITHOUT pausing and never reaches the FlowRunner hand-off this item exists for", "screenshot the FlowRunner dialog; fill #ff-new_assignee and Submit; record the resume POST (POST /runs/:runId/resume)", "GET /api/v1/automation/showcase_reassign_wizard/runs — the driven run reads status=completed; count the paused rows for this flow before vs after and assert the count did not grow (the framework#3528 orphaned-paused regression)", "re-read the target task over /api/v1/data/showcase_task/:id — its assignee equals the submitted value", @@ -1219,6 +1231,12 @@ "date": "2026-08-08", "change": "initial — pins the developer Flow Runs page's typed-input trigger + the framework#3528 screen-flow hand-off (paused run driven to completion, no orphaned row); distinct from flow-run-step-nesting which reads the designer's Runs preview", "ref": "claude/platform-test-checklist-ocwugl" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "steps[5] leaves new_assignee empty (assertion defect, minor, #23166 A2; parked on #23121, carried by #23169). Filling it at Run supplies the screen's one required field, so the run completes without pausing and the hand-off is never exercised (service-automation screen-input-contract: caller-supplied declared fields satisfy the screen). QA run #23166 left it empty, met the pause, the FlowRunner dialog and the resume, and passed every clause", + "ref": "#23169" } ] }, @@ -1729,7 +1747,7 @@ "title": "The Setup packaged-automation page: reachable from Setup nav, lists packaged flows + packaged actions, toggle persists, clone on flow rows only, no lineage surface, server refusals verbatim in role=alert", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "browser", "personas": [ @@ -1745,7 +1763,8 @@ ], "knownGaps": [ "REPO OWNERSHIP (ADR-0054): the PAGE — markup, testids, dialogs, refusal rendering — is objectui's (packages/app-shell/src/views/setup/PackagedAutomationPage.tsx + PackagedActionsSection.tsx, registered as ref 'automation:packaged' in services/builtinComponents.tsx); the Setup NAV METADATA that reaches it is objectstack's (packages/platform-objects/src/apps/setup-nav.contributions.ts — nav_packaged_automation, landed by #12457). A locator or copy fix goes to objectui; a nav-entry regression goes HERE. Conflating the two repos is how the original nav gap survived both repos' tests — each side now pins its own half (framework: setup-packaged-automation-nav.test.ts; objectui: PackagedAutomationPage.navContribution.test.tsx), and only the live sidebar click observes the seam between them", - "AUTOMATION IS PINNED IN THE objectui REPO — `automated.ref` names objectui component tests exclusively, so from this checkout the item is neither runnable nor pin-evidenced; and those tests are the MOCKED half only (stubbed fetch): they prove the page against faked responses and structurally cannot see the missing framework nav entry, a live server's refusal bodies, or persistence. Full protocol: RUNNER.md, the objectui-pinned-automation standing fact" + "AUTOMATION IS PINNED IN THE objectui REPO — `automated.ref` names objectui component tests exclusively, so from this checkout the item is neither runnable nor pin-evidenced; and those tests are the MOCKED half only (stubbed fetch): they prove the page against faked responses and structurally cannot see the missing framework nav entry, a live server's refusal bodies, or persistence. Full protocol: RUNNER.md, the objectui-pinned-automation standing fact", + "MEMBER POSTURE MEASURED, NOT ADJUDICATED (QA run #23163, objectstack 36b2889c, objectui 1fb1ced36243): a plain member could NOT open the Setup app — so neither the page nor its switches — and its read of sys_metadata_activation answered 403. That is the opposite of the posture acceptance[7] was authored against (the member reaches the page; the ledger and meta reads are open), and of the 'Reads stay open' note in packages/platform-objects/src/system/sys-metadata-activation.object.ts. The maintainer question the clause flags is still open; this note only records what the run met. While the member cannot reach the page, acceptance[5]'s member-toggle cell cannot be driven through it either: score that cell on the reachability a run actually meets, and say which" ] }, "steps": [ @@ -1803,7 +1822,7 @@ "evidence": "dialog screenshots: disabled submit, the 400 alert text, the post-clone notice" }, { - "clause": "posture record (not a pass/fail gate): a plain member REACHES the page and sees the switches — the component route has no capability gate and the ledger/meta reads are open — with the refusal arriving only after a click; record the posture as observed", + "clause": "posture record (not a pass/fail gate): record whether a plain member REACHES the page and sees the switches, which lists load, what the activation read answers, and where the first refusal arrives. Authored (#12438) against a posture where the member reached the page with open reads and met the refusal only after a click; at 36b2889c the member could not open Setup and the activation read answered 403 (knownGaps[2]). Record the posture as observed", "oracle": "dom", "verify": "ComponentNavView resolves the ref with no authz check (objectui ComponentNavView.tsx) and sys_metadata_activation reads are open by ruling (sys-metadata-activation.object.ts), so the member sees real install-wide state with dead controls. Whether that read-exposure is intended is a maintainer question tracked in the #12438 sweep's follow-ups — this clause RECORDS what a member sees (which lists load, what the first refusal moment is); it does not adjudicate", "evidence": "the member's pre-click page screenshot + the list of requests the page made and their statuses" @@ -1848,6 +1867,12 @@ "date": "2026-08-30", "change": "inverted the nav clause from expected-fail to a positive assertion: #12457 landed the missing framework nav entry (setup-nav.contributions.ts — nav_packaged_automation → 'automation:packaged', verified at head) with its own pin (setup-packaged-automation-nav.test.ts), so the clause now asserts the entry in the served setup app body + a real sidebar landing, including the entry's two deliberate absences (no requiresService, no requiredPermissions — #12419 / server-side gating) so neither is 'fixed' into a regression. The 'missing nav entry goes HERE' knownGap is rewritten as the two-pins-one-seam ownership note; the typed-URL fallback in steps is re-scoped to regression handling. D16 is fixed — a red here is a NEW regression, not the old finding re-found", "ref": "#12457" + }, + { + "revision": 3, + "date": "2026-10-13", + "change": "knownGaps[2] (new) and acceptance[7].clause record the member posture QA run #23163 measured, without adjudicating it (maintainer question, #23163 A7; parked on #23121, carried by #23169). At objectstack 36b2889c with objectui 1fb1ced36243 a plain member could not open the Setup app, and the sys_metadata_activation read answered 403, where the clause expected a reachable page and open reads. The clause asks the run to record, not judge, so the edit records the measured posture beside the authored one, and notes that acceptance[5]'s member cell is unreachable through the page while that posture holds. The maintainer question stays open", + "ref": "#23169" } ] } diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index f0f0ef658aa..6c4602661ee 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -391,7 +391,7 @@ "title": "The published first-run closes: create-objectstack scaffold → install → validate → build → boot → health, with the skills boundary holding", "since": "v15", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": ["new user (published-registry consumer)"], @@ -409,7 +409,7 @@ "in a scratch dir: `npx -y create-objectstack@latest qa-first-run -t blank --skip-skills; echo $?` (or the repo's bin against the local registry, for the pre-release variant in fixtures.requires) — the scaffolder runs the install ITSELF (pnpm when its probe finds pnpm, npm otherwise), so its exit code and its install output are the install step's evidence. ⛔ Do not follow it with a second `npm install`: on a pnpm-installed scaffold that fails (17.6.0 and 17.7 alike) and the failure is the extra step's, not the product's", "cd qa-first-run && npm run validate; echo $? — then npm run build; echo $?", "boot from the artifact exactly as the workflow does: `npx os start --artifact ./dist/objectstack.json --port 8080 > server.log 2>&1 &`, poll /api/v1/health up to 60s, then `curl -fsS http://localhost:8080/api/v1/ready`", - "repeat scaffold + validate + build across the template matrix (variants) — remote templates always ship build; run validate where the script exists", + "repeat scaffold + validate + build across the template matrix (variants — `blank` only since revision 2: create-objectstack's catalog, packages/create-objectstack/src/template-registry.ts, holds the one bundled template); run validate where the script exists", "skills boundary probe: in a fresh scaffold run `npx -y skills add /skills --all --copy` and compare the installed set to the curated skills/ catalog; then list repo-root discovery and assert no internal skill (dogfood-verification) surfaces", "capture server.log for any boot error" ], @@ -423,7 +423,7 @@ { "clause": "every published template validates and builds — per-variant, no template inferred from a sibling", "oracle": "build", - "verify": "validate (where present) and build exit 0 for each of the six templates", + "verify": "validate (where present) and build exit 0 for each template in variants — one, `blank`: the five remote content templates were retired from the catalog (revision 2), and the published scaffolder offers only blank", "evidence": "per-template exit codes" }, { @@ -461,7 +461,8 @@ "history": [ { "revision": 1, "date": "2026-08-07", "change": "new item: the published first-run experience mirrored step-for-step from scaffold-e2e.yml, template matrix as variants, RC-window protocol refusal recorded as gate-working instead of failure", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-14", "change": "variants narrowed to [blank]: the five remote content templates were delisted from the marketplace and retired from the create-objectstack catalog, and the scaffold-e2e registry-canary matrix this item mirrors was trimmed with them", "ref": "claude/issue-8677-retire-remote-template-catalog" }, - { "revision": 3, "date": "2026-10-05", "change": "three corrections from run #21845. (1) The separate `npm install` step is dropped: the scaffolder already installs (pnpm when found, npm otherwise), and a trailing npm install fails on the pnpm layout on 17.6.0 and 17.7 alike, so the step measured itself rather than the product. The scaffolder's own exit code is now the install evidence, which is also what the published canary job in scaffold-e2e.yml does. Of the card's three options this one was picked as the least ambiguous: it does not depend on which package manager the host has, as `pnpm install` would, and it does not change the published scaffold command, as `--skip-install` would. (2) fixtures.requires gains a pre-release variant: pack the workspace to a localhost registry with no upstream for @objectstack/* and create-objectstack, run the repo's bin against it, and prove the candidate via lockfile integrity. The published path measures only what is released. (3) The skills-boundary clause compares unique skill names, not a raw count (card #21851 entries 8 and 9)", "ref": "#21845" } + { "revision": 3, "date": "2026-10-05", "change": "three corrections from run #21845. (1) The separate `npm install` step is dropped: the scaffolder already installs (pnpm when found, npm otherwise), and a trailing npm install fails on the pnpm layout on 17.6.0 and 17.7 alike, so the step measured itself rather than the product. The scaffolder's own exit code is now the install evidence, which is also what the published canary job in scaffold-e2e.yml does. Of the card's three options this one was picked as the least ambiguous: it does not depend on which package manager the host has, as `pnpm install` would, and it does not change the published scaffold command, as `--skip-install` would. (2) fixtures.requires gains a pre-release variant: pack the workspace to a localhost registry with no upstream for @objectstack/* and create-objectstack, run the repo's bin against it, and prove the candidate via lockfile integrity. The published path measures only what is released. (3) The skills-boundary clause compares unique skill names, not a raw count (card #21851 entries 8 and 9)", "ref": "#21845" }, + { "revision": 4, "date": "2026-10-13", "change": "acceptance[1].verify and steps[3] stop naming six templates (stale wording, #23166 C2; parked on #23121, carried by #23169). The variants have been [blank] since revision 2, when the five remote content templates were retired, and create-objectstack's template registry holds blank alone. QA run #23166 scored the published 17.7.0 path's one template. No clause's assertion changed", "ref": "#23169" } ] }, { diff --git a/docs/qa/platform-checklist/areas/dashboards.json b/docs/qa/platform-checklist/areas/dashboards.json index 56ac2eda878..9bd22087025 100644 --- a/docs/qa/platform-checklist/areas/dashboards.json +++ b/docs/qa/platform-checklist/areas/dashboards.json @@ -652,7 +652,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": 3, + "revision": 4, "priority": "P1", "surface": "browser", "personas": [ @@ -662,7 +662,8 @@ "app": "showcase", "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)" + "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" ] }, "steps": [ @@ -676,8 +677,8 @@ "repeat the global-filter check on Delivery Operations: the Task Status select re-scopes the TASK-bound KPI tile (kpi_awaiting_review), composing with its own per-widget filter; the project-bound tiles (kpi_active_projects / kpi_at_risk / kpi_total_budget) opt out of task_status (#7568) and keep their values", "screenshot before/after each filter change", "D1 (dirty→Reset): on Revenue Pulse, confirm the filter bar shows NO Reset affordance in its pristine state; change a filter away from its defaultValue and confirm a Reset button (RotateCcw) appears; click it and confirm every filter returns to its default and the widgets re-query back", - "D3 ({value,label} options): open the region select and confirm its options render as {value,label} pairs (the trigger shows the LABEL, the committed value is the option value); capture the option-source request — a select whose options come from a dataset issues a server GROUP BY (queryDataset) for the distinct value list, not a truncated client dedupe", - "D2 (Studio authoring round-trip): in the Studio metadata-admin dashboard designer, select a widget and open the Filter Bindings section of the widget inspector; author a binding — re-target the dateRange (or region) filter to a specific field via the field-override combo, and toggle Apply OFF on another widget to opt it out; publish the draft", + "D3 ({value,label} options): open the region select on Revenue Pulse and confirm its options render as {value,label} pairs (the trigger shows the LABEL, the committed value is the option value); then, on the scratch dashboard's optionsFrom filter (fixtures.requires[2]), capture the option-source request — a select whose options come from a dataset issues a server GROUP BY (queryDataset) for the distinct value list, not a truncated client dedupe. Revenue Pulse's static options issue no option request, so they cannot score that half", + "D2 (Studio authoring round-trip): in the Studio metadata-admin dashboard designer, open the scratch dashboard in its writable package (fixtures.requires[2]), select a widget and open the Filter Bindings section of the widget inspector; author a binding — re-target the dateRange (or region) filter to a specific field via the field-override combo, and toggle Apply OFF on another widget to opt it out; publish the draft", "re-read the persisted metadata (GET /api/v1/meta/dashboard/) and confirm the authored widget.filterBindings shape landed; reload the dashboard and confirm the authored binding DRIVES the widget at runtime (re-targeted field in the re-query; opted-out widget no longer re-queries)" ], "acceptance": [ @@ -773,6 +774,12 @@ "date": "2026-10-05", "change": "clause 6 and step 8 re-pointed (stale, #21782 VF4). The clause composed the global status filter with an at-risk tile, but kpi_at_risk (project dataset, health=red) declares filterBindings { task_status: false } (ops-dashboard.dashboard.ts, #7568: project widgets opt out of the task-status filter; pinned by dashboard-filter-vocabulary.test.ts). That predates 17.6.0. The verifier's wording is used: the composition is read on kpi_awaiting_review (task-bound, status=in_review), and kpi_at_risk keeps its value", "ref": "#21797" + }, + { + "revision": 4, + "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" } ] }, @@ -875,7 +882,7 @@ "title": "The built-in System Overview dashboard renders as admin with LIVE sys_* counts, two widget values reconcile against direct API counts, single-environment-only widgets hide gracefully, and no widget renders blank", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "mixed", "personas": [ @@ -889,16 +896,16 @@ "at least one signed-in session (the admin's own) so sys_session count is non-zero, and the seeded sys_user population" ], "knownGaps": [ - "in a SINGLE-TENANT / single-environment runtime (the stock standalone serve) two widgets are runtime-gated OFF by design: widget_organizations carries `requiresService: 'org-scoping'` and widget_packages_installed carries `requiresObject: 'sys_package_installation'` (a cloud-only object). Their ABSENCE is the graceful-hide behavior this item checks — NOT a blank tile and NOT a FAIL; a run must not demand those two render" + "in a SINGLE-TENANT / single-environment runtime (the stock standalone serve) two widgets are runtime-gated OFF by design, by two different layers: widget_organizations carries `requiresService: 'org-scoping'`, and the SERVER strips it from the served dashboard (packages/rest/src/meta-item-read-gate.ts#filterDashboardForUser, ADR-0057 D10), so GET /api/v1/meta/dashboard/system_overview serves 8 of the 9 declared widgets; widget_packages_installed carries `requiresObject: 'sys_package_installation'` (a cloud-only object), is served, and the console hides it. Their ABSENCE from the board is the graceful-hide behavior this item checks — NOT a blank tile and NOT a FAIL; a run must not demand those two render" ] }, "steps": [ "sign in as admin, open the Setup app, and navigate to System Overview (nav_system_overview → dashboard system_overview); let the board settle and screenshot the whole dashboard BEFORE reading any DOM", "capture the dataset queries the widgets issue on load (the network trace) — each metric tile queries its sys_*_metrics dataset (count aggregate)", - "establish ground truth via the data API: GET/POST a count aggregate on sys_user, and a count aggregate on sys_session, recording both numbers", + "establish ground truth via the data API: a count aggregate on sys_user, and a count aggregate on sys_session restricted to LIVE sessions (revoked_at is null AND expires_at after now — the Active Sessions widget's own filter), recording both numbers", "read the rendered value of widget_total_users (Total Users) and widget_active_sessions (Active Sessions) from the DOM (only after the screenshot confirmed render) and compare each to its API count", - "confirm widget_organizations and widget_packages_installed are ABSENT from the rendered board (runtime-gated in single-tenant), not showing a broken/empty tile", - "walk every remaining widget: the three security KPI tiles (login/permission/config event counts over sys_audit_log_metrics), the Audit Events by Action pie, the Events by User bar, and the Audit Events by Action table — confirm each draws a real mark or a named error state, none blank", + "confirm widget_organizations and widget_packages_installed are ABSENT from the rendered board (runtime-gated in single-tenant), not showing a broken/empty tile; read the served dashboard too — widget_organizations is already missing from GET /api/v1/meta/dashboard/system_overview (the server's requiresService strip), widget_packages_installed is present there and hidden by the console", + "walk every remaining widget: the two security KPI tiles (Login Events and Config Changes, event counts over sys_audit_log_metrics — the board carries no permission-events tile; system_overview.dashboard.ts records why the 'Permission Changes' tile was removed), the Audit Events by Action pie, the Events by User bar, and the Event Volume by Action table — confirm each draws a real mark or a named error state, none blank", "capture the browser console for the whole load" ], "acceptance": [ @@ -909,10 +916,10 @@ "evidence": "the tile value next to the API count" }, { - "clause": "widget_active_sessions shows the live session count — its value equals a direct count aggregate on sys_session (the sys_session_metrics dataset carries NO active-only filter, so the honest oracle is the full sys_session count; record the label-says-'Active' vs dataset-counts-all caveat rather than glossing it)", + "clause": "widget_active_sessions shows the live session count — its value equals a direct count aggregate on sys_session under the widget's OWN predicate: the tile declares filter {revoked_at: null, expires_at: {$gt: '{now}'}} (#7531; '{now}' resolved per request by resolveFilterTokens) and opts out of the date bar, so 'Active' is honest and the oracle is the count of live sessions, not the full table", "oracle": "api", - "verify": "the rendered Active Sessions tile == COUNT(sys_session) from a direct /api/v1/data aggregate", - "evidence": "the tile value + the API count + the caveat note" + "verify": "the rendered Active Sessions tile == COUNT(sys_session WHERE revoked_at is null AND expires_at > now) from a direct /api/v1/data aggregate taken in the same run; a value equal to the unfiltered table count while revoked or expired rows exist is a FAIL", + "evidence": "the tile value + the predicate-restricted API count (and the unfiltered count beside it, when they differ)" }, { "clause": "the reconciliation is authoritative, not cosmetic: the widget's OWN dataset query (on the wire) returns the same count the direct /data aggregate returns — the tile reflects a real query, not a placeholder", @@ -923,7 +930,7 @@ { "clause": "single-environment-only widgets hide gracefully: in a single-tenant runtime widget_organizations (requiresService: org-scoping) and widget_packages_installed (requiresObject: sys_package_installation) are ABSENT, not rendered as blank/error tiles", "oracle": "screenshot", - "verify": "the board screenshot shows neither gated widget occupying a slot; the runtime-gate source (requiresService/requiresObject on the two widgets) explains the absence", + "verify": "the board screenshot shows neither gated widget occupying a slot; the served GET /api/v1/meta/dashboard/system_overview body lacks widget_organizations (server strip on requiresService) and carries widget_packages_installed (hidden client-side on requiresObject) — the two gates explain the two absences", "evidence": "the board screenshot annotated with the two gated widget ids" }, { @@ -952,7 +959,7 @@ "wrong-persona" ], "source": [ - "packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts#requiresService (widget ids, requiresService/requiresObject gates, globalFilters date range)", + "packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts#requiresService (9 widget ids; requiresService / requiresObject gates; widget_active_sessions' live-session filter, #7531; the Row 2 note on the removed Permission Changes tile; globalFilters date range) + packages/rest/src/meta-item-read-gate.ts#filterDashboardForUser (the server strip of an unmet requiresService widget, ADR-0057 D10)", "packages/platform-objects/src/apps/dashboards/system.datasets.ts (the five sys_*_metrics datasets and their count measures)", "packages/platform-objects/src/apps/setup-nav.contributions.ts#nav_system_overview (nav_system_overview → dashboard system_overview in the Setup app)" ], @@ -962,6 +969,12 @@ "date": "2026-08-08", "change": "new item: built-in System Overview live-count reconciliation + graceful single-environment widget hiding, grounded in the platform dashboard + sys_*_metrics datasets", "ref": "claude/platform-test-checklist-ocwugl" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "acceptance[1] (clause, verify, evidence), acceptance[3].verify, knownGaps[0], steps[2], [4] and [5] and source[0] (stale spec, #23166 A1 / A3; parked on #23121, carried by #23169). The Active Sessions widget filters revoked_at null and expires_at > now (#7531), so the caveat 'no active-only filter' is gone and the oracle is the live-session count. The board has two security KPI tiles, Login Events and Config Changes: the Permission Changes tile was removed. system_overview declares 9 widgets, and the server strips widget_organizations from the served body on an unmet requiresService (filterDashboardForUser, ADR-0057 D10), so a single-tenant boot serves 8. QA run #23166 read 8 served widgets and no permission KPI at objectstack 36b2889c; the source reads the same at 6d7f4df3c", + "ref": "#23169" } ] }, diff --git a/docs/qa/platform-checklist/areas/i18n.json b/docs/qa/platform-checklist/areas/i18n.json index 5f9f264f1d8..22966666ea6 100644 --- a/docs/qa/platform-checklist/areas/i18n.json +++ b/docs/qa/platform-checklist/areas/i18n.json @@ -95,7 +95,7 @@ "title": "Studio follows the in-app locale switch — no mixed-language session", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "browser", "personas": ["admin"], @@ -112,7 +112,7 @@ "sign in as admin on the showcase, switch the in-app locale to zh-CN, and confirm the app shell followed (nav in zh-CN). To switch, use the avatar-menu language switcher (it writes the user's locale); seeding browser storage alone is not a language switch — a run that only seeds the UI language in browser storage leaves the user's stored locale empty, so dates follow the browser and a FAIL read off it is the harness's, not the product's", "navigate into Studio / metadata-admin: open the object editor for showcase_project and the dashboard designer for showcase_chart_gallery", "screenshot the object editor's form — its labels/sections come from the metadataForms translation group (packages/spec/src/system/translation.zod.ts: metadataForms..label / sections / fields)", - "screenshot a metadata list surface showing relative dates ('x 天前'-style) in the switched locale", + "screenshot a surface showing RELATIVE dates in the switched locale: the Studio Data pillar's records grid (/_console/studio//data, an object's records), whose timestamps render as '6天前'-style relative forms. The metadata list and history surfaces show none — history renders absolute timestamps — so they cannot score this step", "reload the browser fully and re-screenshot one Studio surface — the locale choice must survive the reload", "switch back to en through the same avatar-menu language switcher and re-screenshot the same two Studio surfaces" ], @@ -132,7 +132,7 @@ { "clause": "relative dates follow the locale", "oracle": "screenshot", - "verify": "list/timeline timestamps render zh-CN relative forms after the switch", + "verify": "the Studio Data pillar's records-grid timestamps render zh-CN relative forms (e.g. 6天前) after the switch, and English relative forms after the switch back; the metadata list / history surfaces carry absolute timestamps and are not this clause's subject", "evidence": "screenshot" }, { @@ -164,7 +164,8 @@ { "revision": 1, "date": "2026-08-07", "change": "initial import from #3358", "ref": "#3358" }, { "revision": 2, "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 3, "date": "2026-08-11", "change": "the mixed-language clause is reconciled with the coverage ratchet (#7640): as written it read 'no untranslated declared string next to translated ones', which the showcase's FROZEN untranslated debt violates by design — the sibling item i18n.surface-matrix already declares that debt legitimate, so this item contradicted it and cost a clean PASS. The clause now judges against the bundle (translated key renders translated; untranslated key renders its EN SOURCE label; a raw dotted key is the unconditional FAIL), the same knownGap is declared on this item's fixtures, and the negative carries the matching qualifier", "ref": "#7686" }, - { "revision": 4, "date": "2026-10-05", "change": "the switch steps now name HOW to switch: through the avatar-menu language switcher, which writes the user's locale. Seeding browser storage alone is not a language switch. The 17.7 console run held a relative-date FAIL on this item, and its re-run on #21782 settled it as a harness gap: the first lane had set the UI language only in browser storage, so the user's stored locale stayed empty and dates followed the browser. Through the switcher the clause passed, so the clauses themselves are unchanged (card #21851 entry 11, filed with the follow-up run #21845)", "ref": "#21782" } + { "revision": 4, "date": "2026-10-05", "change": "the switch steps now name HOW to switch: through the avatar-menu language switcher, which writes the user's locale. Seeding browser storage alone is not a language switch. The 17.7 console run held a relative-date FAIL on this item, and its re-run on #21782 settled it as a harness gap: the first lane had set the UI language only in browser storage, so the user's stored locale stayed empty and dates followed the browser. Through the switcher the clause passed, so the clauses themselves are unchanged (card #21851 entry 11, filed with the follow-up run #21845)", "ref": "#21782" }, + { "revision": 5, "date": "2026-10-13", "change": "steps[3] and acceptance[2].verify re-pointed to the surface that renders relative dates (stale step, #23163 A2; parked on #23121, carried by #23169). The metadata list and history surfaces carry no relative dates — history is absolute — so the step pointed at nothing scorable. QA run #23163 read relative dates (6天前) on the Studio Data pillar's records grid under zh-CN, through the avatar-menu switcher, and passed", "ref": "#23169" } ] }, { @@ -172,7 +173,7 @@ "title": "Every translatable surface localizes on a zh-CN session — one pass over the full translation-group vocabulary, plus the runtime i18n routes it resolves through", "since": "v15", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "browser", "personas": ["admin on a zh-CN session"], @@ -185,7 +186,7 @@ "knownGaps": [ "the showcase's declared surface carries FROZEN untranslated debt, ratcheted by scripts/check-i18n-coverage.mjs + i18n-coverage-baseline.json — expect some declared strings to legitimately fall back to en; the per-surface check is 'translated keys render translated, untranslated keys render the SOURCE label (never a raw dotted key)', not 100% coverage", "objects._views..emptyState WAS unafforded and no longer is: the #7714 specimen has landed. examples/app-showcase/src/ui/views/task.view.ts authors an emptyState on the `urgent` view (its comment block cites #7714) and src/system/translations/index.ts carries the zh-CN half under objects.showcase_task._views.urgent.emptyState.{title,message}. The variant is runnable; the only setup it needs is an empty `urgent` view (demote the two urgent seed rows)", - "objects._sections is addressable, but ONLY on sections that declare a `name`: examples/app-showcase/src/ui/pages/task-detail.page.ts's record:details block authors label-only sections, and packages/spec/src/ui/component.zod.ts documents that a nameless section renders its authored label in EVERY locale (packages/lint's translation-section-name-missing rule exists to nudge authors toward the name). #7640 read that record page, saw English headings next to a bundle that does carry 概览/排期/详细信息, and nearly filed a resolver defect — drive the named-section form views instead (task.view.ts tabbed/wizard/split, contact.view.ts default form)", + "objects._sections is addressable ONLY on sections that declare a `name` (packages/spec/src/ui/component.zod.ts: a nameless section renders its authored label in every locale; packages/lint's translation-section-name-missing rule nudges authors toward the name). On the stock showcase every surface this item reads declares one: showcase_task's DEFAULT form (the New/Edit form, section `task`), the task record page — examples/app-showcase/src/ui/pages/task-detail.page.ts's record:details block names its sections overview / schedule / details since #22036, reusing objects.showcase_task._sections — and showcase_contact's default form. QA run #23163 read all three in zh under zh-CN, so the old reading (the record page renders EN in every locale) is stale. ⚠ The named formViews tabbed / wizard / split are reachable from NO console door: `…/showcase_task/view/tabbed` is a LIST-view route and renders the grid (records-forms.form-view-gallery says why), so do not read _sections there", "acceptance[5]'s CONFIRM half has no stock specimen at all, so on stock it is PIN-ONLY (QA run #21782 recorded it blocked(fixture) as 'no zh confirmText'; parked on #22017 row 8). No showcase action declares a `confirmText` (src/ui/actions, src/data/objects; only a view's bulkActionDefs carry one, and those are not `_actions` entries), so a zh-CN bundle entry has nothing to translate. Score it from the objectui pins in `automated`. The toast half stays live (e.g. showcase_recalc_estimate's zh successMessage). A live confirm specimen would need a scratch-package action declaring confirmText plus a translation overlay (translation is runtime-writable); that is not run-proven" ] }, @@ -197,7 +198,7 @@ "form labels + placeholders on a task edit form (fields group), and select options in the form and list filters (fields..options)", "an action dialog — trigger an object action with confirmText/successMessage and capture the confirm prompt and the success toast (_actions group: label/confirmText/successMessage)", "a view empty state (_views..emptyState) — RUNNABLE on the stock fixture since the #7714 specimen landed: examples/app-showcase/src/ui/views/task.view.ts authors an emptyState on the filtered `urgent` view, and the zh-CN bundle carries objects.showcase_task._views.urgent.emptyState.{title,message}. Demote the two urgent seed rows (or filter them away) so the view renders empty, then read the copy — it resolves to 暂无紧急任务 / 当前没有需要立即处理的事项。任务优先级调整为「紧急」后会出现在这里。 ⛔ Still do not manufacture coverage from the console's built-in zero-row placeholder, which is console chrome and not an objects._views..emptyState resolution", - "section labels (_sections group) — read them off a NAMED-section surface, not the record page: showcase_task's tabbed / wizard / split form views (examples/app-showcase/src/ui/views/task.view.ts) and showcase_contact's default edit form (ui/views/contact.view.ts) declare a stable `name` per section, which is the only thing that makes objects.._sections..label resolvable — the showcase bundle carries 概览/排期/详细信息 and 联系方式/工作信息/状态/备注 for exactly those names. The task DETAIL page's record:details block authors label-only sections and renders its EN headings in every locale by design — see the knownGap below", + "section labels (_sections group) — read them off a NAMED-section surface the console actually renders: showcase_task's New form (its default form's `task` section), a task record page (task-detail.page.ts names overview / schedule / details since #22036) and showcase_contact's default edit form (ui/views/contact.view.ts). Each declares a stable `name` per section, which is the only thing that makes objects.._sections..label resolvable — the showcase bundle carries 概览/排期/详细信息 and 联系方式/工作信息/状态/备注 for exactly those names. ⛔ Not `…/view/tabbed`: the named formViews are reachable from no console door, and that URL renders the grid (see the knownGap below)", "a dashboard — Chart Gallery's title and widget titles (dashboards..label / widgets..title)", "a page — a seeded page's header title/subtitle (pages. group)", "a Settings namespace — title, group titles, key labels, and the source badges (settings. + settingsCommon.sourceLabels)", @@ -291,7 +292,7 @@ "form labels/placeholders (objects.fields)", "action dialogs (objects._actions: label/confirmText/successMessage)", "view empty states (objects._views.emptyState) — blocked(fixture): no showcase view authors one", - "section labels (objects._sections) — via a NAMED-section form view; the record:details block on the task detail page is label-only and unaddressable", + "section labels (objects._sections) — on a named-section surface the console renders: the task New form, the task record page or the contact form; the named formViews (tabbed / wizard / split) are reachable from no console door", "dashboards (dashboards.label / widgets.title)", "pages (pages.label/title/subtitle)", "settings (settings. + settingsCommon.sourceLabels)", @@ -314,7 +315,7 @@ "content/docs/ui/translations.mdx", "scripts/check-i18n-coverage.mjs + scripts/i18n-coverage-baseline.json (frozen-debt ratchet)", "packages/spec/src/ui/component.zod.ts (record:details sections — `name` is the i18n anchor; a nameless section renders its authored label in every locale)", - "examples/app-showcase/src/ui/views/task.view.ts + ui/views/contact.view.ts (the named-section form views this item's _sections variant is read off) vs ui/pages/task-detail.page.ts (label-only record:details)", + "examples/app-showcase/src/ui/views/task.view.ts (the default form's named `task` section) + ui/views/contact.view.ts (the named contact form sections) + ui/pages/task-detail.page.ts (record:details sections named overview / schedule / details since #22036, reusing objects.showcase_task._sections) — the surfaces this item's _sections variant is read off", "#7640 (i18n area run), #7686 (the maintenance card this revision answers)", "#21782 (re-run comment: the held relative-dates FAIL was a harness gap — browser storage seeded, switcher never used; PASS through the avatar-menu switcher)" ], @@ -324,7 +325,8 @@ { "revision": 3, "date": "2026-08-11", "change": "two variants reconciled with what the stock fixture actually affords (#7640). _sections: the step pointed at the task DETAIL page, whose record:details block authors label-only sections — unaddressable by design per component.zod.ts. The variant is NOT unreachable, as first read: task.view.ts's tabbed/wizard/split form views and contact.view.ts's default form declare stable section names the bundle already translates, so the step now names those surfaces and the nameless record page is recorded as a knownGap plus a qualifier on the resolver negative. emptyState: no showcase view authors one, so the variant is waived as blocked(fixture) with the debt written down, never faked from console chrome", "ref": "#7686" }, { "revision": 4, "date": "2026-08-18", "change": "two corrections. (1) The #7714 emptyState waiver is stale: the specimen has landed — task.view.ts authors an emptyState on the urgent view and the zh-CN bundle carries objects.showcase_task._views.urgent.emptyState.{title,message} — so step 7 and the knownGap now describe a runnable variant instead of instructing blocked(fixture) (#9467 CF-1). (2) Clause 11 asserted the feed/audit verb localizes on the READER's session; audit-writers.ts renders the summary once at write time via resolveWriteLocale(tenantId, userId) and stores it, so only the tenant setting at write time moves it. The clause and its step now flip the tenant locale before writing — this premise nearly produced a false resolver FAIL (#9467 CF-2)", "ref": "#9386" }, { "revision": 5, "date": "2026-10-05", "change": "the switch step now names HOW to switch: through the avatar-menu language switcher, which writes the user's locale. Seeding browser storage alone is not a language switch. The 17.7 console run held the relative-dates variant of acceptance[0] as a FAIL, and its re-run on #21782 settled it as a harness gap: the first lane had set the UI language only in browser storage, so the user's stored locale stayed empty and dates followed the browser. Through the switcher the variant passed, so the clauses themselves are unchanged (card #21851 entry 11, filed with the follow-up run #21845)", "ref": "#21782" }, - { "revision": 6, "date": "2026-10-06", "change": "partial-coverage gap of QA run #21782 parked on #22017 (row 8). A6's confirm half was blocked(fixture): no zh confirmText. Measured: no showcase action declares a confirmText at all, so there is no live specimen. Disposition: pin. The objectui tests page-header-action-i18n and record-quick-actions.actionText-i18n prove that the confirm dialog body resolves from the `_actions` zh-CN entry through the real ActionRunner. `automated` names them for that half only, and a knownGap records why the half is pin-only on stock", "ref": "#22017" } + { "revision": 6, "date": "2026-10-06", "change": "partial-coverage gap of QA run #21782 parked on #22017 (row 8). A6's confirm half was blocked(fixture): no zh confirmText. Measured: no showcase action declares a confirmText at all, so there is no live specimen. Disposition: pin. The objectui tests page-header-action-i18n and record-quick-actions.actionText-i18n prove that the confirm dialog body resolves from the `_actions` zh-CN entry through the real ActionRunner. `automated` names them for that half only, and a knownGap records why the half is pin-only on stock", "ref": "#22017" }, + { "revision": 7, "date": "2026-10-13", "change": "knownGaps[2], steps[7], variants[6] and the showcase source note re-pointed (knownGap stale, #23163; parked on #23121, carried by #23169). task-detail.page.ts's record:details block has named its sections overview / schedule / details since #22036, so the task record page renders them translated; QA run #23163 read zh headings on the New form, the task record page and the contact form under zh-CN. The step used to name the tabbed / wizard / split formViews, but those are reachable from no console door, and `…/view/tabbed` renders the grid. The negative that keeps a nameless section out of resolver FAILs stands", "ref": "#23169" } ] }, { diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index e26992e184d..0f245846dc2 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -1827,7 +1827,7 @@ "title": "Email verification loop: a challenged sign-up lands a captured verification mail, unverified sign-in is refused EMAIL_NOT_VERIFIED server-side, the link admits, resend works — and the default boot never challenges", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": ["a fresh registrant (unverified, then verified)", "admin (reads state)", "env owner (configures the verification boot)"], @@ -1847,7 +1847,7 @@ "on the verification boot, sign up a fresh admissible address: the console routes to /verify-email-prompt after sign-up (RegisterPage header contract) and the verification mail lands at the log transport via template auth.verify_email — capture the link/token (sendVerificationEmail, auth-manager.ts: template failures THROW into the log rather than silently dropping)", "attempt POST /api/v1/auth/sign-in/email for the unverified user: capture the refusal carrying EMAIL_NOT_VERIFIED, and in the browser confirm the login page redirects that error into /verify-email-prompt (LoginPage.tsx,346,360)", "resend from the prompt page (useAuth().sendVerificationEmail → POST /api/v1/auth/send-verification-email, ledgered auth-route-ledger.ts) and capture the second mail at the transport", - "follow the captured link: the console's /verify-email consumes ?token= via POST /api/v1/auth/verify-email — the page deliberately uses the POST variant for JSON control; the GET variant 302-redirects (VerifyEmailPage.tsx; ledger row GET /api/v1/auth/verify-email) — and renders the success state", + "follow the captured link: the console's /verify-email consumes ?token= via GET /api/v1/auth/verify-email?token=… — better-auth serves the route as GET ONLY (a POST answers 404, objectui#11633), and sent WITHOUT callbackURL it answers JSON ({status: true, user} once the token verifies, 401 {code, message} for a garbage or expired one) instead of a 302, which is how the SPA keeps the post-verify UX (VerifyEmailPage.tsx; ledger row GET /api/v1/auth/verify-email) — and renders the success state", "sign in again with the same credentials: admitted; get-session identifies the user", "feed a garbage/expired token to /verify-email: the page renders its designed error state (named message, retry path), and the user's verification state is unchanged (sign-in still refused)", "default-boot contrast (stock showcase, no verification config): an admitted creation (seeded persona or invited registrant) signs in immediately with NO challenge — and /auth/config advertises emailPassword.requireEmailVerification false", @@ -1867,7 +1867,7 @@ "evidence": "the captured transport output" }, { - "clause": "the captured token verifies and flips admission: POST /api/v1/auth/verify-email with the token succeeds, and the previously-refused sign-in now returns a session", + "clause": "the captured token verifies and flips admission: GET /api/v1/auth/verify-email?token=… (no callbackURL) answers {status: true}, and the previously-refused sign-in now returns a session. There is no POST variant: it answers 404", "oracle": "api", "verify": "verify-email 2xx → sign-in 2xx → get-session identifies the user", "evidence": "the verify + sign-in + session trace" @@ -1904,14 +1904,15 @@ "packages/plugins/plugin-auth/src/audience-posture.ts#entry (entry validation, assertAudienceConfig: an explicit requireEmailVerification false is refused under email_domain from any source, and under open when only the settings console stored it)", "packages/plugins/plugin-auth/src/auth-route-ledger.ts#AUTH_ROUTE_LEDGER (POST /api/v1/auth/send-verification-email = auth.sendVerificationEmail) + (GET /api/v1/auth/verify-email = auth.verifyEmail) — re-pointed #18104: sendVerificationEmail is a CLIENT METHOD name, carried in this file only inside a dotted string value", "objectui apps/console/src/App.tsx (/verify-email + /verify-email-prompt routes)", - "objectui apps/console/src/pages/auth/VerifyEmailPage.tsx (consumes ?token= via the POST variant — GET 302s, POST returns JSON so the SPA controls the post-verify UX)", + "objectui apps/console/src/pages/auth/VerifyEmailPage.tsx (consumes ?token= via GET with no callbackURL, which answers JSON so the SPA controls the post-verify UX; better-auth serves no POST variant — it answers 404, objectui#11633)", "objectui apps/console/src/pages/auth/VerifyEmailPromptPage.tsx (shown after sign-up or an EMAIL_NOT_VERIFIED-blocked sign-in; resend via useAuth().sendVerificationEmail)", "objectui apps/console/src/pages/auth/LoginPage.tsx,346,360 (EMAIL_NOT_VERIFIED → /verify-email-prompt redirect + the named error copy)", "packages/plugins/plugin-email/src/transports/index.ts (the dev `log` capture transport — same harness as identity-auth.self-service-password-reset)" ], "history": [ { "revision": 1, "date": "2026-08-30", "change": "authored in the 2026-08-30 coverage sweep (angle 4): the verification loop (challenge, capture, verify, resend, default-never-challenges) had no item. Grounded the #11739 posture-forces-verification invariant and its advertised mirror, the console's three surfaces (/verify-email POST-variant consumption, /verify-email-prompt resend, LoginPage's EMAIL_NOT_VERIFIED redirect), and the admissibility precondition the audience wall adds to the fixture", "ref": "#sweep-2026-08-30" }, - { "revision": 2, "date": "2026-09-28", "change": "restated the forced-verification wording to the shipped rule: email_domain always forces verification on; open forces it on unless the deployment declares it off (OS_AUTH_REQUIRE_EMAIL_VERIFICATION=false, or emailAndPassword.requireEmailVerification: false in the stack config); a false stored only through the settings console is refused under open. The (b) fixture, clause 6, the mirror negative and two source notes said every permitting posture forces it; the negative now names the open opt-out boot, which advertises false correctly, as not a FAIL. Steps, oracles and the other clauses are unchanged", "ref": "#20413" } + { "revision": 2, "date": "2026-09-28", "change": "restated the forced-verification wording to the shipped rule: email_domain always forces verification on; open forces it on unless the deployment declares it off (OS_AUTH_REQUIRE_EMAIL_VERIFICATION=false, or emailAndPassword.requireEmailVerification: false in the stack config); a false stored only through the settings console is refused under open. The (b) fixture, clause 6, the mirror negative and two source notes said every permitting posture forces it; the negative now names the open opt-out boot, which advertises false correctly, as not a FAIL. Steps, oracles and the other clauses are unchanged", "ref": "#20413" }, + { "revision": 3, "date": "2026-10-13", "change": "steps[3], acceptance[2].clause and source[4] name the GET verify-email the console uses (stale step, #23166 C2; parked on #23121, carried by #23169). POST /api/v1/auth/verify-email answers 404 — better-auth serves the route as GET only — and the console's VerifyEmailPage calls GET ?token= without a callbackURL, which answers JSON (objectui#11633, at 1fb1ced36243, still this repo's .objectui-sha). The route ledger already books only the GET row. QA run #23166 verified through GET and passed", "ref": "#23169" } ] }, { @@ -1919,15 +1920,15 @@ "title": "Workspace switcher: org chrome follows posture × membership count (nothing under single; read-only indicator at one membership; switcher at two), and a switch is a full-document reload after which every tenant-scoped read answers from the new org", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": ["a member with one org membership", "the same member after gaining a second membership", "admin (provisions the second org)"], "fixtures": { "app": "showcase", "requires": [ - "a WALL-ENFORCING tenancy posture boot: OS_TENANCY_POSTURE=group (or isolated) — the stock default resolves to `single` (resolveTenancyPosture, packages/types/src/env.ts), where multiOrgEnabled is false and NO org chrome may render at all (postureEnforcesWall drives both the flag and the UI predicate — auth-manager.ts)", - "a second organization + membership for the test user — runtime-creatable on the posture boot via the better-auth org endpoints (POST /api/v1/auth/organization/create, then the platform-admin-gated POST /api/v1/auth/organization/add-member — auth-route-ledger.ts organization family); the org-create gate follows the SAME posture derivation as the advertised flag, so it admits on this boot" + "a WALL-ENFORCING tenancy posture boot: OS_TENANCY_POSTURE=group (or isolated) — the stock default resolves to `single` (resolveTenancyPosture, packages/types/src/env.ts), where multiOrgEnabled is false and NO org chrome may render at all (postureEnforcesWall drives both the flag and the UI predicate — auth-manager.ts). A walled posture REFUSES TO BOOT the stock showcase without three more things, measured by QA run #23166 from a scratch copy: (1) `@objectstack/organizations` declared in the host app's package.json — the multi-org runtime a walled posture needs (ADR-0093 D5); (2) OS_PLATFORM_OWNER_EMAIL, the declared platform owner (set it to the seeded admin's address, whose seed stamps it verified so it can be elevated); (3) a declared membership policy, OS_AUTH_MEMBERSHIP_POLICY (packages/plugins/organizations/src/membership-policy-gate.ts#MEMBERSHIP_POLICY_ERROR_CODE — WALLED_MEMBERSHIP_POLICY_UNDECLARED). Boot it from a scratch copy, never the tree", + "a second organization + membership for the test user — runtime-creatable on the posture boot via the better-auth org endpoints (POST /api/v1/auth/organization/create, then the platform-admin-gated POST /api/v1/auth/organization/add-member — auth-route-ledger.ts organization family); the org-create gate follows the SAME posture derivation as the advertised flag, so it admits on this boot. Under OS_AUTH_MEMBERSHIP_POLICY=invite-only the invited test user must also ACCEPT the invitation (POST /api/v1/auth/organization/accept-invitation) before the membership exists" ], "knownGaps": [ "not runnable on the stock boot: `single` posture renders no switcher and no indicator BY DESIGN (that absence is itself this item's clause 1, runnable on stock) — every other clause needs the OS_TENANCY_POSTURE boot and is blocked(fixture) without it" @@ -1940,7 +1941,7 @@ "provision the second org + membership; reload: the switcher renders (data-testid workspace-switcher) and the indicator yields (it renders ONLY at exactly one membership, CurrentOrganizationIndicator.tsx)", "under `group` posture, open the dropdown and capture the labeling: 'Working organization' + the writes-here/reads-span-all hint (data-testid workspace-switcher-group-hint, WorkspaceSwitcher.tsx); under `isolated` it reads 'Switch organization'", "switch to the second org: the client calls switchOrganization (POST /api/v1/auth/organization/set-active, auth-route-ledger.ts) and then performs a FULL-DOCUMENT navigation to the console root (window.location.href = resolveRootUrl(), WorkspaceSwitcher.tsx) — capture that it is a document reload, not a SPA transition", - "after the reload: GET /api/v1/auth/organization/get-active-member confirms the new active org; re-drive one tenant-scoped data read and one /meta read and confirm they answer from the NEW org — pick one row/app id that exists only in the OLD org and prove its absence (the org change drops the whole metadata cache, objectui#4486, MetadataProvider.tsx)", + "after the reload: GET /api/v1/auth/organization/get-active-member confirms the new active org; re-drive one tenant-scoped data read and confirm it answers from the NEW org — pick one ROW id that exists only in the OLD org and prove its absence. Re-drive one /meta read too: it must answer the SAME as before the switch. Since ADR-0131 D6/D7 metadata is deployment-level — no metadata write is organization-scoped (packages/metadata-protocol/src/protocol.ts) — so no org-only metadata id exists to use as a sentinel; what the switch must still do on the meta side is drop the console's metadata cache (objectui#4486, MetadataProvider.tsx) so nothing stale is served", "failure leg: as the member, forge POST /api/v1/auth/organization/set-active with an org id they hold NO membership in; capture the server refusal (403 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION) and read get-active-member: the foreign org is never active. better-auth clears the session's active org on this refusal, so 'none' is an expected reading, not a fault. Confirm the UI never renders the foreign org as active" ], "acceptance": [ @@ -1963,9 +1964,9 @@ "evidence": "the navigation trace" }, { - "clause": "tenant-scoped reads follow the switch: after the reload, data and /meta reads answer from the new org — an id that exists only in the previous org is ABSENT (one organization's metadata never survives into another's reads, objectui#4486)", + "clause": "tenant-scoped reads follow the switch: after the reload, data reads answer from the new org — a row id that exists only in the previous org is ABSENT — and /meta reads answer exactly as before the switch, because metadata is deployment-level since ADR-0131 D6/D7 (no organization-scoped metadata write exists, so there is no org-only metadata id to be absent); the console drops its metadata cache on the change (objectui#4486)", "oracle": "api", - "verify": "the post-switch data read + meta read; the old-org sentinel id is not in either response", + "verify": "the post-switch data read: the old-org sentinel row id is not in it; the post-switch /meta read equals the pre-switch one. A metadata id that differs between the two orgs would itself be a finding against ADR-0131 D6, not this clause's pass", "evidence": "the before/after reads + the sentinel id" }, { @@ -2000,7 +2001,8 @@ ], "history": [ { "revision": 1, "date": "2026-08-30", "change": "authored in the 2026-08-30 coverage sweep (angle 1): the workspace-switch surface had no item. CORRECTED against source from the register's 'single membership → indicator' hypothesis: the indicator is posture-gated (postureHasOrgWall — group/isolated only), so the stock `single` boot renders NO org chrome at all and the whole matrix needs an OS_TENANCY_POSTURE boot; encoded the three-cell affordance matrix, the deliberate full-document reload, the #4486 cache-drop read-follow proof, group-posture labeling, and the forged-switch refusal", "ref": "#sweep-2026-08-30" }, - { "revision": 2, "date": "2026-10-05", "change": "clause 6 and step 8 re-pointed (assertion defect, #21784 VF4). The switch fails closed, and the defect is not of the authorization class. better-auth 1.7.3's setActiveOrganization (plugins/organization/routes/crud-org) calls adapter.setActiveOrganization(token, null) before it throws 403 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, and plugin-auth mounts the route unwrapped (auth-route-ledger 'sdk'). So the active org becomes none, never the foreign org, and 'unchanged' was the wrong assertion. The verifier's wording is used. The verifier's optional follow-up, wrapping the vendor call to keep the previous org, would be a product enhancement, not this item's concern", "ref": "#21797" } + { "revision": 2, "date": "2026-10-05", "change": "clause 6 and step 8 re-pointed (assertion defect, #21784 VF4). The switch fails closed, and the defect is not of the authorization class. better-auth 1.7.3's setActiveOrganization (plugins/organization/routes/crud-org) calls adapter.setActiveOrganization(token, null) before it throws 403 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, and plugin-auth mounts the route unwrapped (auth-route-ledger 'sdk'). So the active org becomes none, never the foreign org, and 'unchanged' was the wrong assertion. The verifier's wording is used. The verifier's optional follow-up, wrapping the vendor call to keep the previous org, would be a product enhancement, not this item's concern", "ref": "#21797" }, + { "revision": 3, "date": "2026-10-13", "change": "fixtures.requires[0] and [1] name the walled-posture boot prerequisites (fixture gap, #23166), and steps[6] and acceptance[3] re-ground the meta half (stale spec, #23166 C3b); both parked on #23121, carried by #23169. QA run #23166 found a walled posture refuses to boot the stock showcase without @objectstack/organizations in the host package.json (ADR-0093 D5), OS_PLATFORM_OWNER_EMAIL and OS_AUTH_MEMBERSHIP_POLICY, and that under invite-only the invitee must accept the invitation. It skipped C3b because its premise is retired: metadata writes are deployment-level (ADR-0131 D6/D7), so no metadata id exists in one org only. The clause keeps the data sentinel and now expects the /meta answer to be identical across the switch", "ref": "#23169" } ] }, { @@ -2008,7 +2010,7 @@ "title": "Implicit account linking on external sign-in: no implicit link to an unverified local user; after an unlink, implicit sign-in does not re-link; an explicit, signed-in link still works and lifts the refusal; the platform identity provider's exception holds only on its OAuth path", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "mixed", "personas": [ @@ -2038,7 +2040,7 @@ "verify U's email through the product (fixtures.requires), then repeat the implicit sign-in: it lands signed in as U, and a sys_account row for the provider exists", "as U, signed in: unlink the provider (POST /api/v1/auth/unlink-account { accountId: the sys_account row id }) and confirm the row is gone. Sign out, then repeat the implicit sign-in from a fresh context", "as U, signed in: link the provider EXPLICITLY (POST /api/v1/auth/link-social { provider, callbackURL } with U's session, then navigate to the returned URL). Confirm the sys_account row is back. Sign out and repeat the implicit sign-in once more", - "explicit link for an unverified user: sign up a second unverified local user W (same provider identity re-pointed to W's email), sign in as W and link the provider explicitly through link-social; then re-read W's emailVerified", + "explicit link for an unverified user: sign up a second unverified local user W, give the fixture provider an identity for W's email, sign in as W and link the provider explicitly through link-social; then re-read W's emailVerified. The local fixture IdP derives its `sub` from the email, so W's identity is a DISTINCT provider account: W's explicit link writes W's own sys_account row and does not re-point or touch U's (QA run #23166)", "run the pin: pnpm --filter @objectstack/plugin-auth exec vitest run src/implicit-account-linking.test.ts" ], "acceptance": [ @@ -2106,6 +2108,12 @@ "date": "2026-10-06", "change": "new: the checklist had only the explicit link round-trip (identity-auth.linked-accounts-social), which predates the rule. PR #21872 tightened implicit linking on external sign-in. This item asserts the four rules the published docs state: no implicit link to an unverified local user, an unlink honoured, the explicit signed-in link kept, and the platform IdP exception bound to its OAuth path. It reuses linked-accounts-social's local OIDC provider recipe rather than forking one. The platform-IdP clause and the operator override are pin-scored, and knownGaps says why", "ref": "#21932" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "steps[6] says W's provider identity is a distinct sub (fixture note, #23166 C3; parked on #23121, carried by #23169). The step read 'the same provider identity re-pointed to W's email', but the local OIDC fixture derives `sub` from the email, so W's identity is a different provider account, and W's explicit link writes W's own sys_account row. QA run #23166 drove it that way and passed acceptance[3]. No clause changed", + "ref": "#23169" } ] } diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index ad35a681b76..d1883b15a81 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -428,7 +428,7 @@ "title": "The flow designer's connector picker mirrors GET /automation/connectors: same instances, declarative ones annotated, actions and their input schemas offered per pick", "since": "v15.1", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "browser", "personas": [ @@ -441,12 +441,12 @@ "a flow that is EDITABLE in Studio — use a shipped one (showcase_declarative_connector_ping) or create it from the console's own create page; see the knownGap before authoring one over the metadata API" ], "knownGaps": [ - "A FLOW AUTHORED OVER THE METADATA API IS NOT EDITABLE IN STUDIO (run #7690, cross-observed by run #7695 / #7753 item 3) — with or without `?package=`, it opens behind 'This flow is provided by an installed package, so it is read-only at runtime', so a run that provisions its fixture flow via PUT /api/v1/meta/flows/... cannot then drive the picker on it. objectui's ResourceEditPage treats an item as artifact-backed when `layered.code != null && _packageId !== 'sys_metadata'`, and `flow` declares allowEnvironmentOverlay:false — so for THIS type the banner is arguably telling the truth (the server would refuse the overlay write anyway). The editable path is the console's own create page. Recorded, not filed: the polarity-reversed case, where the same heuristic locks a WRITABLE-package object it should not, is the real defect and lives at objectui#4308" + "A FLOW AUTHORED OVER THE METADATA API IS NOT EDITABLE IN STUDIO (run #7690, cross-observed by run #7695 / #7753 item 3) — with or without `?package=`, it opens behind 'This flow is provided by an installed package, so it is read-only at runtime', so a run that provisions its fixture flow via PUT /api/v1/meta/flows/... cannot then drive the picker on it. objectui's ResourceEditPage treats an item as artifact-backed when `layered.code != null && _packageId !== 'sys_metadata'`, and `flow` declares allowEnvironmentOverlay:false — so for THIS type the banner is arguably telling the truth (the server would refuse the overlay write anyway). The editable path is the console's own create page. MEASURED AGAIN (QA run #23163, objectstack 36b2889c, objectui 1fb1ced36243): an ENV-WIDE flow authored through POST /api/v1/automation opened EDITABLE in Studio with no lock banner, and the picker was driven on it — so that door is a working way to provision the fixture flow. Whether a flow written through PUT /api/v1/meta/flow/… still opens behind the banner was not re-measured Recorded, not filed: the polarity-reversed case, where the same heuristic locks a WRITABLE-package object it should not, is the real defect and lives at objectui#4308" ] }, "steps": [ "before the browser: GET /api/v1/automation/connectors and record { connectors, total } — this is the ground truth the picker must mirror", - "in Studio, open a flow (e.g. showcase_declarative_connector_ping) and add/select a connector_action node — if the editor opens behind the 'provided by an installed package' banner, check how the flow was authored before filing anything (see the knownGap: metadata-API-authored flows are read-only in Studio)", + "in Studio, open a flow (e.g. showcase_declarative_connector_ping, or a scratch flow authored env-wide through POST /api/v1/automation, which opens editable — knownGaps) and add/select a connector_action node — if the editor opens behind the 'provided by an installed package' banner, check how the flow was authored before filing anything (see the knownGap)", "open the connector picker; screenshot; only AFTER the screenshot, read the DOM list", "pick showcase_mcp_tools; record which actions are offered and screenshot the action + input form for echo_upper", "pick a plugin-registered connector and compare its presentation with the declarative one's annotation" @@ -502,6 +502,12 @@ "date": "2026-08-11", "change": "recorded the run #7690 route note that a flow authored over the metadata API opens read-only in Studio ('provided by an installed package'): ResourceEditPage's artifact-backed heuristic (layered.code != null && _packageId !== 'sys_metadata') plus flow's allowOrgOverride:false. The fixture now names an editable flow and the step says check the authoring route before filing. Kept as a note, not a defect — the polarity-reversed case is objectui#4308", "ref": "#7745" + }, + { + "revision": 4, + "date": "2026-10-13", + "change": "knownGaps[0] and steps[1] record that the installed-package lock did not reproduce for an env-wide flow (knownGap stale, #23163 A0; parked on #23121, carried by #23169). QA run #23163 authored the fixture flow through POST /api/v1/automation, and it opened editable with no lock banner; the picker listed exactly the 5 served connectors. The older reading, about flows written over the metadata API, is kept and marked not re-measured", + "ref": "#23169" } ] }, @@ -510,18 +516,19 @@ "title": "Outbound webhooks materialize (spec object→object_name, isActive→active), fire per trigger variant through the sys_http_delivery outbox with HMAC + timeout honored, redeliver terminal rows tenant-scoped via POST /api/v1/webhooks/redeliver, reject retired trigger kinds, and never clobber admin edits", "since": "v15", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "mixed", "fixtures": { "app": "showcase", "requires": [ - "a reachable webhook receiver (local echo server on the run's own port range) — point the shipped showcase_task_changed row at it, or author a scratch webhook", + "a reachable webhook receiver (local echo server on the run's own port range) and a SCRATCH webhook authored INSIDE the showcase organization that points at it — Setup → Integrations → Webhooks → New as the signed-in admin, or POST /api/v1/data/sys_webhook under that session, so the row carries the organization. ⚠ The shipped showcase_task_changed cannot carry the per-record or bulk delivery legs: it materializes ORG-LESS (a declared webhook belongs to no organization), and the auto-enqueuer hands an organization-walled event only to a subscription of that organization (packages/plugins/plugin-webhooks/src/auto-enqueuer.ts, AutoEnqueuer.admitsOrganization). On the org-walled showcase it therefore never fans out, while Setup reads it active:true (QA run #23166)", "the shipped fixture: showcase_task_changed (object showcase_task, triggers create/update/delete, isActive:false ON PURPOSE — flipping it active in Setup is part of the test, examples/app-showcase/src/automation/webhooks/index.ts)", "a predicate multi-write path for the bulk variants (update/delete with multi:true on showcase_task) — NOT reachable over REST, see the knownGap below: author a flow with an `update_record` / `delete_record` node carrying multi:true and fire it through the api trigger" ], "knownGaps": [ - "PREDICATE (multi:true) WRITES ARE UNREACHABLE OVER REST BY DESIGN (run #7690) — do not re-derive this, and do not file the 400 as a defect. #3897 made the batch routes parse their body against the spec contract, and Zod object schemas STRIP unknown keys: `options.multi` and `options.where` can no longer ride into the engine's delete/update options, and `POST /data/:object/deleteMany` now deletes per id (packages/rest/src/rest-server.ts). That is a security boundary, not a gap. The bulk_update/bulk_delete clause therefore needs a FLOW `update_record` / `delete_record` node authored with multi:true, fired through the api trigger — that is the supported predicate-write door" + "PREDICATE (multi:true) WRITES ARE UNREACHABLE OVER REST BY DESIGN (run #7690) — do not re-derive this, and do not file the 400 as a defect. #3897 made the batch routes parse their body against the spec contract, and Zod object schemas STRIP unknown keys: `options.multi` and `options.where` can no longer ride into the engine's delete/update options, and `POST /data/:object/deleteMany` now deletes per id (packages/rest/src/rest-server.ts). That is a security boundary, not a gap. The bulk_update/bulk_delete clause therefore needs a FLOW `update_record` / `delete_record` node authored with multi:true, fired through the api trigger — that is the supported predicate-write door", + "acceptance[2], the bulk pair, is BLOCKED(dependency) on the org-walled showcase (QA run #23166): a flow node's predicate multi-write publishes its bulk event with NO organizationId, the org-owned subscription refuses it (auto-enqueuer.ts organization match), and an org-less subscription cannot be authored to receive it. Whether flow-node multi-writes should carry the organization is a maintainer question raised on #23166, not ruled; score acceptance[2] blocked(dependency) until it is. The same run did not attempt negative[1] or negative[2] (time budget): a re-run owes them, and their text is unchanged" ] }, "variants": [ @@ -535,8 +542,8 @@ ], "steps": [ "boot the showcase; verify the materializer bridge: read sys_webhook over /api/v1/data and locate showcase_task_changed with object_name:'showcase_task', active:false, managed_by:'package', and the envelope in definition_json", - "in Setup → Integrations → Webhooks flip the row active and point url at the local receiver (an admin edit — it must stamp customized)", - "create, update, then delete a showcase_task; capture the three deliveries at the receiver (headers incl. the HMAC signature when a secret is set, body incl. recordId)", + "in Setup → Integrations → Webhooks flip the shipped showcase_task_changed row active and point its url at the local receiver (an admin edit — it must stamp customized). This edit is the subject of the seed-not-clobber clause (acceptance[6]) ONLY: the row stays org-less, so it delivers nothing on the org-walled showcase (fixtures.requires[0]). Author the in-organization scratch webhook beside it for the delivery legs", + "with the in-organization scratch webhook (fixtures.requires[0]) active and pointed at the receiver, create, update, then delete a showcase_task; capture the three deliveries at the receiver (headers incl. the HMAC signature when a secret is set, body incl. recordId)", "run a predicate multi-update and multi-delete (multi:true) matching several rows — through a FLOW node, not REST: author update_record / delete_record nodes with multi:true and fire them via the api trigger (the REST batch routes strip options.multi, #3897 — see the knownGap); capture the bulk deliveries and their { object, matched } shape", "read sys_http_delivery over /api/v1/data: one row per delivery with status/attempts/lastStatusCode", "kill the receiver and mutate again; re-read the delivery row through its retry/failure states", @@ -559,7 +566,7 @@ { "clause": "the bulk pair delivers the AGGREGATE shape — { object, matched } with no recordId and no record body — and only to webhooks that opted into bulk_update/bulk_delete; per-record subscribers do NOT receive a fabricated per-record event for a predicate write (#4639/#4626)", "oracle": "network", - "verify": "bulk delivery bodies + absence of per-record deliveries for the same predicate write on a create/update/delete-only subscription", + "verify": "bulk delivery bodies + absence of per-record deliveries for the same predicate write on a create/update/delete-only subscription. On the org-walled showcase this clause scores blocked(dependency): knownGaps[1]", "evidence": "receiver logs for both subscriptions" }, { @@ -640,6 +647,12 @@ "date": "2026-08-30", "change": "coverage sweep: added the redeliver clause — POST /api/v1/webhooks/redeliver (a raw hono mount, webhook-outbox-plugin.ts) had no coverage. Asserts the terminal-row replay (stored HMAC signature re-sent byte-for-byte), the row update and { id, status } answer, and the full refusal matrix: 401 anonymous, 400 INVALID_REQUEST / MISSING_REQUIRED_FIELD, 404 for unknown AND cross-org ids (#10740 tenant threading), 409 DELIVERY_NOT_ELIGIBLE / DELIVERY_NEVER_SENT (the #8069 parked-row and gone-signing-config refusals — never a 500). Pins the deliberate NO-capability posture above the auth floor ('every authenticated user counts' — the plugin's own words) so a run neither files it as a missing gate nor widens it. Step + sources added", "ref": "#sweep-2026-08-30" + }, + { + "revision": 6, + "date": "2026-10-13", + "change": "fixtures.requires[0], steps[1] and [2] move the delivery legs onto an in-organization scratch webhook (stale spec, #23166 A1), and knownGaps[1] (new) and acceptance[2].verify record the bulk pair as blocked(dependency) with the maintainer question open (#23166 A2); both parked on #23121, carried by #23169. The shipped showcase_task_changed materializes org-less, and the auto-enqueuer gives an organization-walled event only to its own organization's subscriptions, so on the showcase it never fans out while Setup shows it active. QA run #23166 drove the per-record legs on a webhook authored inside the organization and passed them; in-org bulk flow events carry no organizationId, so no tenant subscription can receive them. N1 / N2 were not attempted and are owed by a re-run; the item's text for them is unchanged", + "ref": "#23169" } ], "enumSource": { @@ -757,18 +770,19 @@ }, { "id": "integration-system.email-template-render", - "title": "Email templates materialize to sys_email_template, resolve (name, locale) with en-US fallback, render {{path}} holes, gate on required variables and active:false, survive admin edits across redeploys — and the raw POST /api/v1/email/send door authenticates, refuses anonymous, and 400s malformed input", + "title": "Email templates resolve from the registry (no sys_email_template row since ADR-0131 D6/D10), resolve (name, locale) with en-US fallback, render {{path}} holes, gate on required variables and active:false, survive admin edits across redeploys — and the raw POST /api/v1/email/send door authenticates, refuses anonymous, and 400s malformed input", "since": "v15", "status": "active", - "revision": 5, + "revision": 6, "priority": "P2", "surface": "mixed", "fixtures": { "app": "showcase", "requires": [ - "the shipped fixture: showcase_task_done_email (category workflow, subject '✅ Task done: {{title}}', variables title required / project optional — examples/app-showcase/src/system/emails/index.ts)", + "the shipped fixture: showcase_task_done_email (subject '✅ Task done: {{title}}', variables title required / project optional — examples/app-showcase/src/system/emails/index.ts). It declares no `category`: that key and `isSystem` are RETIRED (#22876, ADR-0049 — retiredKey() tombstones on the template schema), so a template authored with either is refused at parse with the prescription", "a capturable email channel (dev transport / log capture — no real SMTP needed)", - "a second-locale scratch copy (same name, locale zh-CN) for the i18n-bundle clause" + "a second-locale scratch copy (same name, locale zh-CN) for the i18n-bundle clause", + "for acceptance[5]'s declared-source half a WRITABLE copy of the showcase (a scratch copy under /tmp, never the tree): the stock app is a read-only code package, so a run cannot edit its declared template source. QA run #23166 scored that half partial for want of one; the admin-edit half runs on stock" ], "knownGaps": [ "no stock example exercises an email template end-to-end; needs a small fixture flow with a notify/email step", @@ -784,21 +798,21 @@ "rejection: name not dotted snake_case" ], "steps": [ - "boot the showcase; read sys_email_template over /api/v1/data and locate showcase_task_done_email: bodyHtml→body_html remap, variables in variables_json, managed_by:'package'", + "boot the showcase; GET /api/v1/meta/email_template/showcase_task_done_email and read the served item — subject, bodyHtml, variables, active. The REGISTRY serves it: since ADR-0131 D6/D10 (#22875) a declared template is read by (name, locale) from the registry, and no sys_email_template row backs it — the table holds 0 rows on a stock boot (QA run #23166) and its organization door is closed (packages/plugins/plugin-email/src/email-template-door.ts)", "drive a send with full data ({ title, project }) through IEmailService.sendTemplate (or the fixture flow once it exists); capture the rendered subject/body at the dev transport", "drive a send MISSING the required `title`; capture the fast failure", - "set the row inactive (or author active:false) and send; capture the TEMPLATE_INACTIVE error", + "make the template inactive (a Studio save of the item with active:false, or a scratch template authored active:false) and send; capture the TEMPLATE_INACTIVE error", "author the zh-CN twin; send to a zh-CN-locale recipient and to an unmatched-locale recipient; capture which row rendered each", "author a template with fromOverride + replyTo and verify both on the outbound message", - "edit the template wording as an admin in Studio (stamps customized), redeploy/reboot, and re-read the row", - "edit the DECLARED source template and metadata-reload WITHOUT a reboot — the single item re-materializes (email_template is allowRuntimeCreate:true; the plugin subscribes to metadata changes)", + "edit the template wording as an admin in Studio (an environment overlay over the declared item), redeploy/reboot, and re-read the served item over GET /api/v1/meta/email_template/showcase_task_done_email — the admin's wording must still be served", + "on the WRITABLE showcase copy (fixtures.requires[3]), edit the DECLARED source template and metadata-reload WITHOUT a reboot, then GET /api/v1/meta/email_template/ — the registry serves the edited item (email_template is allowRuntimeCreate:true; the plugin subscribes to metadata changes). On the stock read-only showcase this half is not drivable: record it partial, as QA run #23166 did", "build the two rejection probes (stray key `body`; name 'BadName') and capture the errors", "drive the raw send door POST /api/v1/email/send three ways: (a) authed with a well-formed message { to, subject, html } (or `text`; SendEmailInput's body keys are `html`/`text` — `bodyHtml`/`bodyText` are the TEMPLATE authoring fields, not wire keys) → capture status + the dev-transport landing; (b) anonymous (no session) → capture status; (c) a non-object / malformed body → capture the envelope", "while you are at the raw door, send the wrong-vocabulary body { to, subject, bodyHtml } deliberately and capture the refusal — 400 VALIDATION_FAILED 'at least one of text or html is required'. This is the shape the checklist itself used to name (run #7690); keeping the probe pins the two vocabularies apart" ], "acceptance": [ { - "clause": "the authored template is what actually renders: subject/body substitute every {{path}} hole with the run's values, and the AUTHORED wording (not a built-in or stale copy) reaches the transport — the exact three-break disconnect #4509 closed (engine registration + managed_by stamping + bridge write)", + "clause": "the authored template is what actually renders: subject/body substitute every {{path}} hole with the run's values, and the AUTHORED wording (not a built-in or stale copy) reaches the transport — the false-compliance disconnect #4509 named, now closed by the registry read: sendTemplate resolves the registry item by (name, locale), with no table row in between (ADR-0131 D6/D10)", "oracle": "log", "verify": "captured render contains the authored strings with substituted values and zero unresolved {{placeholder}} residue", "evidence": "the captured render" @@ -810,7 +824,7 @@ "evidence": "the two outcomes" }, { - "clause": "active:false is a real gate: sendTemplate returns TEMPLATE_INACTIVE (also the withdrawal mechanism — deleting a declared template deactivates rows rather than destroying them)", + "clause": "active:false is a real gate: sendTemplate returns TEMPLATE_INACTIVE", "oracle": "log", "verify": "the inactive send's error code", "evidence": "the error" @@ -828,10 +842,10 @@ "evidence": "the captured message" }, { - "clause": "both provenance protections hold: an admin-edited (customized) row survives redeploys, AND a declared-source edit re-materializes the single item on metadata reload without a restart", + "clause": "both provenance protections hold: an admin's Studio edit (an environment overlay over the declared item) survives redeploys, AND a declared-source edit is served on metadata reload without a restart", "oracle": "api", - "verify": "post-reboot row keeps the admin wording; the reload path updates the untouched declared row", - "evidence": "before/after row reads for both paths" + "verify": "post-reboot GET /api/v1/meta/email_template/ serves the admin wording; on the writable showcase copy the reload path serves the edited declared item. Read the REGISTRY, never sys_email_template, which holds no row for a declared template (ADR-0131 D6/D10). On the stock read-only showcase the declared-source half is partial (fixtures.requires[3])", + "evidence": "before/after item reads for both paths, and which showcase copy the declared-source half ran on" }, { "clause": "authoring rejections are loud and prescriptive: a stray key errors naming it with the alias prescription (content→bodyHtml per #5013 — landing on the REQUIRED body so the rename renders); a non-dotted-snake-case name errors at parse", @@ -861,7 +875,7 @@ "source": [ "packages/spec/src/system/email-template.zod.ts#fromOverride (requireds, dotted-name regex, aliases #5013, variables/required, fromOverride/replyTo/active)", "packages/spec/liveness/email_template.json#requireVars (the whole surface live via #4509; per-prop evidence incl. requireVars, TEMPLATE_INACTIVE, seed-not-clobber, runtime re-materialization)", - "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts + email-service.ts (row mapping; (name,locale)+fallback; the gates)", + "packages/plugins/plugin-email/src/email-service.ts ((name, locale) + fallback through the registry TemplateLoader; the requireVars and TEMPLATE_INACTIVE gates) + email-template-door.ts (the closed sys_email_template organization door, ADR-0131 D6) — the row projector bootstrap-declared-email-templates.ts was retired with the table projection (#22875)", "examples/app-showcase/src/system/emails/index.ts#showcase_task_done_email (showcase_task_done_email fixture)", "packages/rest/src/rest-server.ts#registerEmailEndpoints (registerEmailEndpoints — POST /api/v1/email/send: enforceAuth 401 UNAUTHENTICATED #3963, non-object→400 INVALID_REQUEST, VALIDATION_FAILED passthrough, 501 no-provider, 500 EMAIL_SEND_FAILED)", "packages/rest/src/rest-route-ledger.ts (email family — POST /api/v1/email/send → client email.send)", @@ -898,6 +912,12 @@ "date": "2026-09-21", "change": "CORRECTION: the locale clause said resolution \"picks the best locale row\", a sentence packages/plugins/plugin-email/src/template-loader.ts enumerates as one of three false declarations. The resolver matches (name, locale) exactly, folds no language subtag, retries the single literal rung en-US for a call that named a locale and then raises TEMPLATE_NOT_FOUND. Corrected the clause and its verify line so the run cannot tick a best-match behaviour the code has never had", "ref": "#18499" + }, + { + "revision": 6, + "date": "2026-10-13", + "change": "the title, fixtures.requires[0] and [3] (new), steps[0], [3], [6] and [7], acceptance[0], [2] and [5] and source[2] re-pointed from the sys_email_template row to the registry (stale spec, #23166 A0 / A5 / A6; parked on #23121, carried by #23169). Since #22875 (ADR-0131 D6/D10) declared templates resolve from the registry by (name, locale); the row projector is gone, the table holds 0 rows on a stock boot, and its organization door is closed. `category` and `isSystem` are retired (#22876), so the fixture line no longer names a category. The declared-source half of acceptance[5] needs a writable showcase copy: QA run #23166 scored it partial on the read-only stock app and passed the admin half. The pin email-template-materialization.dogfood.test.ts already asserts the registry graph, no row included", + "ref": "#23169" } ] }, @@ -906,7 +926,7 @@ "title": "The flow notify node delivers to the recipient's inbox (sys_inbox_message + receipt), readable and markable over /notifications, recipient-scoped — with unimplemented channels dead-lettering, never faking delivery", "since": "v15", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -923,7 +943,8 @@ ], "knownGaps": [ "channels push/slack/teams/webhook have NO delivery implementation (#3197 — notification.zod.ts says the dispatcher dead-letters them, and the enum's 'in-app' spelling vs the implemented 'inbox' channel is a known naming drift); this item tests inbox only and records the dead-letter behavior as a negative, not as deliverable channels", - "THE OLD PERSONA-LOGIN RECIPE IS CLOSED (run #7690): `POST /api/v1/data/sys_account` answers 405, so the trick of inserting a credential row to turn a seeded demo persona into a real login no longer works. It is by design, not drift — sys_account is managedBy:'better-auth' and declares apiMethods ['get','list'] only, so HTTP answers 405 before the identity write guard's 403 (packages/platform-objects/src/identity/sys-account.object.ts, #1591 / ADR-0092 D2). Mint personas through better-auth instead: sign-up, or POST /api/v1/auth/admin/create-user as the platform admin (explicit password wins — #3031/#3033). Recorded here because every multi-persona item in this area pays for re-deriving it" + "THE OLD PERSONA-LOGIN RECIPE IS CLOSED (run #7690): `POST /api/v1/data/sys_account` answers 405, so the trick of inserting a credential row to turn a seeded demo persona into a real login no longer works. It is by design, not drift — sys_account is managedBy:'better-auth' and declares apiMethods ['get','list'] only, so HTTP answers 405 before the identity write guard's 403 (packages/platform-objects/src/identity/sys-account.object.ts, #1591 / ADR-0092 D2). Mint personas through better-auth instead: sign-up, or POST /api/v1/auth/admin/create-user as the platform admin (explicit password wins — #3031/#3033). Recorded here because every multi-persona item in this area pays for re-deriving it", + "acceptance[3]'s 501 half needs a boot WITHOUT MessagingServicePlugin, and no showcase boot is one: the showcase config lists 'messaging' in `requires` (examples/app-showcase/objectstack.config.ts), so the service is always composed. On a showcase run score the 501 half blocked(fixture) (QA run #23166 A3-501), or cite it from a minimal non-showcase boot without the plugin, naming that boot; the 401 half runs on stock" ] }, "variants": [ @@ -1000,6 +1021,12 @@ "date": "2026-08-11", "change": "recorded the run #7690 fixture note that POST /api/v1/data/sys_account now answers 405, closing the old recipe for turning a seeded demo persona into a real login (sys_account is managedBy better-auth with apiMethods ['get','list'] — 405 before the identity write guard's 403, #1591 / ADR-0092 D2). The three-persona requirement now names the supported way to mint them (sign-up / admin create-user). Notes only — no clause changed", "ref": "#7745" + }, + { + "revision": 3, + "date": "2026-10-13", + "change": "knownGaps[2] (new) names why acceptance[3]'s 501 half cannot run on a showcase boot (fixture gap, #23166 A3-501; parked on #23121, carried by #23169). The clause already allowed citing it from a minimal boot without the messaging plugin, but nothing said the showcase can never be that boot: its config requires 'messaging'. QA run #23166 scored the half blocked(fixture). No clause changed", + "ref": "#23169" } ] }, @@ -1435,7 +1462,7 @@ "title": "A recipient preference muting the inbox channel for a topic suppresses delivery (no inbox row, no dead-letter fake); flipping it back resumes; a sys_notification_template renders through a delivery", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "mixed", "personas": [ @@ -1450,7 +1477,8 @@ "two personas with sessions: admin and the assignee" ], "knownGaps": [ - "no shipped sys_notification_preference rows nor a sys_notification_template fixture — both authored in-run (both are writable system-data objects); the notify flow fixture itself is shipped. The spec's channel enum spells this 'in-app'; the implemented channel id is 'inbox' (the #3197 naming drift) — the preference row must name the IMPLEMENTED id 'inbox'" + "no shipped sys_notification_preference rows nor a sys_notification_template fixture — both authored in-run (both are writable system-data objects); the notify flow fixture itself is shipped. The spec's channel enum spells this 'in-app'; the implemented channel id is 'inbox' (the #3197 naming drift) — the preference row must name the IMPLEMENTED id 'inbox'", + "acceptance[3]'s mandatory-topic half is BLOCKED(fixture) on stock (QA run #23166 A3-mandatory): mandatoryTopics is a MessagingServicePlugin CONSTRUCTOR option (packages/services/service-messaging/src/messaging-service-plugin.ts, default []), not metadata and not runtime-writable, and the showcase composes messaging through `requires` with the default — so no topic is mandatory and none can be made so in a run. The bypass itself is unit-pinned in packages/services/service-messaging/src/preference-resolver.test.ts ('bypasses preferences for a mandatory topic (exact match) even when muted', and the prefix case); cite that pin for the mechanism and record the live half blocked(fixture), or drive it on a host that constructs the plugin with mandatoryTopics, naming that host" ] }, "steps": [ @@ -1517,6 +1545,12 @@ "date": "2026-08-08", "change": "new — preference-driven inbox suppression (no row, no dead-letter fake), reversal, ADR-0030 Layer-3 precedence, and a sys_notification_template render; distinct from notify-inbox-delivery (happy path). Per PENDING-GAPS §C", "ref": "claude/platform-test-checklist-ocwugl" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "knownGaps[1] (new) records the mandatory-topic half of acceptance[3] as blocked(fixture) on stock and names its unit pin (fixture gap, #23166 A3-mandatory; parked on #23121, carried by #23169). mandatoryTopics is a MessagingServicePlugin constructor option, default empty, so the stock showcase declares none and a run cannot add one. QA run #23166 passed the rest of the item. No clause changed", + "ref": "#23169" } ] }, @@ -1759,7 +1793,7 @@ "title": "Setup → Datasources federation UI (ADR-0015 §6.4): SchemaBrowser mirrors the remote-table listing, ImportObjectDialog lands a REAL object through the metadata channel, Refresh catalog and ValidationPanel drive the three previously-uncovered federation routes (import / refresh-catalog / validate) behind their capability floors", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -1769,13 +1803,13 @@ "fixtures": { "app": "showcase", "requires": [ - "the shipped read-only SQLite external datasource showcase_external + its boot-provisioned fixture DB (examples/app-showcase/src/system/datasources/ — customers/orders, 5 columns each; same fixture as external-schema-introspection)", + "the shipped read-only SQLite external datasource showcase_external + its boot-provisioned fixture DB (examples/app-showcase/src/system/datasources/ — customers/orders; same fixture as external-schema-introspection). The fixture DDL declares 5 fields per table (external-fixture.ts), but the provisioned tables carry 7 COLUMNS each, and the remote listing and the panel both report 7 (QA run #23166): the columns the managed driver adds sit beside the declared ones. Read the count off the API listing, not off the DDL", "the built console mounted (the panel lives in the app-shell metadata-admin engine)" ], "knownGaps": [ "the federation-unavailable 503 arm is NOT reachable on a stock boot (serve.ts wires ExternalDatasourceServicePlugin unconditionally — same bound as datasource-admin-lifecycle's 503 clause); it needs a deliberately unwired boot. No CLI flag or preset unwires it: serve.ts wires the plugin whenever @objectstack/service-datasource resolves (QA run #21784, parked on #22017 row 21)", "the UI's 503 detector is FIXED at the current pin. objectui 0abd4f9f8769 (objectui#11628, objectui PR #11640) maps the server's 503 envelope { success:false, error:{ code:'SERVICE_UNAVAILABLE', message } } to ExternalServiceUnavailableError (packages/app-shell/src/views/metadata-admin/external/api.ts). Its api.test.ts pins that for all four reads ('%s maps 503 SERVICE_UNAVAILABLE to the typed error') and pins that a 503 naming another code is not read as 'federation not wired'. QA runs #21782 / #21784 ran objectui 2e818d0b51ec, whose detector still matched the retired pre-#3843 string body, so acceptance[6]'s expected-fail was true for them and is stale now (re-pointed on #22017 row 21)", - "the 'unreachable' diff kind (spec 2026-08-23, emitted by external-datasource-service.ts) is NEWER than the DIFF_LABEL map objectui compiles against: ValidationPanel.tsx labels 9 kinds and lacks 'unreachable', so a validate run reporting an unreachable remote renders an unlabelled kind cell at head — the exact objectstack#4115 class the map's totality gate exists to catch; it will fail objectui's compile at the next spec pin bump (the panel's own comment documents the mechanism working that way for default_mismatch). Expected-fail if observed live; do not file as new without checking", + "FIXED at objectui 1fb1ced36243: ValidationPanel.tsx's label map (DIFF_LABEL_KEY) now labels 'unreachable' (engine.externalDatasource.diff.unreachable) beside the other nine, so every one of the 10 SchemaDiffEntryKind kinds renders labelled, and the panel's comment records the ruling that 'unreachable' reads as 'cannot check', never as 'schema changed'. An unlabelled kind cell is now a regression of the #4115 class, not an expected-fail", "diff-kind labels beyond the in-sync fixture need INDUCED drift — reuse external-schema-drift-gate's induced-drift recipe notes (scratch copy, measured column-type change); the stock in-sync fixture proves only the all-green render" ] }, @@ -1798,7 +1832,7 @@ "evidence": "the two screenshots (+ the unsaved-draft state if a wizard draft is on hand)" }, { - "clause": "SchemaBrowser mirrors the API exactly: the rendered table list equals GET .../external/tables name-for-name with per-table columnCount (customers/orders, 5 each on the stock fixture) — the network response is the authority, not the panel; data loads on mount + explicit Refresh only, never a timer", + "clause": "SchemaBrowser mirrors the API exactly: the rendered table list equals GET .../external/tables name-for-name with per-table columnCount (customers/orders, 7 each on the provisioned stock fixture — fixtures.requires[0]) — the network response is the authority, not the panel; data loads on mount + explicit Refresh only, never a timer", "oracle": "dom", "verify": "screenshot first, then DOM list diffed against the pre-captured listing (SchemaBrowser.tsx — listRemoteTables on mount/refresh; the fixture DDL in external-fixture.ts)", "evidence": "screenshot + API read, diffed" @@ -1816,9 +1850,9 @@ "evidence": "the network capture + the header screenshot" }, { - "clause": "ValidationPanel reports what the server measured: the run fires POST .../external/validate (URL-scoped to this datasource, #10537 — a read, manage_platform_settings floor) and renders one verdict row per federated object matching { ok, results }; every diff kind rendered carries its DIFF_LABEL text — with the head-version caveat that 'unreachable' is not yet in objectui's label map (knownGaps: expected-fail, the #4115 class; the map's totality over the imported spec union is the standing guard)", + "clause": "ValidationPanel reports what the server measured: the run fires POST .../external/validate (URL-scoped to this datasource, #10537 — a read, manage_platform_settings floor) and renders one verdict row per federated object matching { ok, results }; every diff kind rendered carries its label text — 'unreachable' included, which objectui's map labels at 1fb1ced36243 (the map's totality over the imported spec union is the standing guard)", "oracle": "dom", - "verify": "the rendered rows against the captured response (ValidationPanel.tsx DIFF_LABEL; route at external-datasource-routes.ts; kinds at external-errors.ts — 10 at head, 9 labelled in objectui)", + "verify": "the rendered rows against the captured response (ValidationPanel.tsx DIFF_LABEL_KEY; route at external-datasource-routes.ts; kinds at external-errors.ts — 10 at head, all 10 labelled in objectui at 1fb1ced36243)", "evidence": "the response + the rendered rows (and the unlabelled-kind capture if drift/outage was induced)" }, { @@ -1853,7 +1887,7 @@ }, "source": [ "objectui packages/app-shell/src/views/metadata-admin/external/api.ts (the five-route client; the 503 detector, which reads the envelope since objectui#11628; listRemoteTables/generateObjectDraft/refreshCatalog/validateDatasource/importObjectDraft — the import door is PUT /api/v1/meta/object/:name)", - "objectui packages/app-shell/src/views/metadata-admin/external/{SchemaBrowser,ImportObjectDialog,ValidationPanel,ExternalDatasourcePanel}.tsx (the P5 surfaces; ValidationPanel.tsx DIFF_LABEL total over the imported spec union — the #4115 recurrence guard) + previews/DatasourcePreview.tsx (panel keyed off schemaMode !== 'managed')", + "objectui packages/app-shell/src/views/metadata-admin/external/{SchemaBrowser,ImportObjectDialog,ValidationPanel,ExternalDatasourcePanel}.tsx (the P5 surfaces; ValidationPanel.tsx DIFF_LABEL_KEY total over the imported spec union — the #4115 recurrence guard, 'unreachable' included) + previews/DatasourcePreview.tsx (panel keyed off schemaMode !== 'managed')", "objectui packages/app-shell/src/views/metadata-admin/datasource/register.ts (Setup → Datasources = the engine route; left-nav names it directly, objectui#3660)", "packages/rest/src/external-datasource-routes.ts#refuseFederationRequest (the five-route family), (import → 201 { object }, EXTERNAL_IMPORT_ERROR), (refresh-catalog), (validate, #10537 URL-scoped), (refuseFederationRequest — read=manage_platform_settings, write=manage_metadata, #9686 anonymous floor), (the 503 envelope)", "packages/spec/src/shared/external-errors.ts#SchemaDiffEntryKind (SchemaDiffEntryKind — 10 kinds at head incl. index_mismatch/unmapped_index/default_mismatch/unreachable)", @@ -1872,6 +1906,12 @@ "date": "2026-10-06", "change": "partial-coverage gap of QA run #21784 parked on #22017 (row 21), re-pointed clause. A7 was blocked: it needs an unwired boot, and its expected-fail rested on objectui's 503 detector matching the retired string body. Measured at the current .objectui-sha 0abd4f9f8769: objectui#11628 (PR #11640) maps the server's SERVICE_UNAVAILABLE envelope to ExternalServiceUnavailableError, pinned by api.test.ts. The run's console (2e818d0b51ec) predates it. A7 flips from expected-fail to the positive assertion, scored from that pin (oracle test); knownGaps[1], step 9, `automated` and source follow. The live render still needs an unwired boot, which no flag or preset gives (knownGaps[0])", "ref": "#22017" + }, + { + "revision": 3, + "date": "2026-10-13", + "change": "fixtures.requires[0], acceptance[1].clause, knownGaps[2], acceptance[4] (clause and verify) and source[1] (stale spec, #23166 A1 / A4; parked on #23121, carried by #23169). The provisioned fixture tables carry 7 columns each, and the API and the panel agree, while the DDL declares 5 fields; the clause now reads the count off the API. At objectui 1fb1ced36243, still this repo's .objectui-sha, ValidationPanel's label map (now DIFF_LABEL_KEY) labels 'unreachable', so all 10 diff kinds render labelled and the expected-fail caveat is gone. QA run #23166 passed both clauses", + "ref": "#23169" } ] } diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index a7a381bf3d9..09535044b5e 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -1747,7 +1747,7 @@ "title": "Marketplace install-local: the wiring matrix mounts the right arms per OS_CLOUD_URL, the air-gapped inline install works with NO control plane, reseed/purge touch exactly the sample rows, every mutating door is manage_metadata-gated both sides, and the listing projects installedBy/storageDir per principal", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -1760,7 +1760,7 @@ "app": "showcase", "requires": [ "TWO isolated boots of the same app: boot A = plain `objectstack dev` with NO OS_CLOUD_URL (resolveCloudUrl() falls back to DEFAULT_CLOUD_URL — truthy — so the CLOUD arm mounts: proxy + install-local + cloud-connection + runtime-config); boot B = the same line with OS_CLOUD_URL=off exported (the offline arm: install-local pinned to the 'off' control plane + runtime-config ONLY — nothing that dials out mounts). The matrix is decided by Serve.planMarketplaceWiring, pure and testable (packages/cli/src/commands/serve.ts)", - "a compiled package artifact for the inline install: pnpm -C examples/app-crm build → dist/objectstack.json (the air-gapped path the #8343 fix exists for — `handleInstall`'s inline branch never reads the cloud URL)", + "a compiled package artifact for the inline install: pnpm -C examples/app-crm build → dist/objectstack.json (the air-gapped path the #8343 fix exists for — `handleInstall`'s inline branch never reads the cloud URL). Both boots that install run from a SCRATCH COPY of the app, never the shared tree: install-local caches the manifest under the working directory's .objectstack/installed-packages (packages/cloud-connection/src/local-manifest-source.ts#DEFAULT_INSTALLED_PACKAGES_DIR, resolved against process.cwd()), which a per-boot OS_HOME / OS_STORAGE_LOCAL_ROOT does not redirect (RUNNER.md environment facts)", "admin credentials passed to the CLI door (--email/--password or OS_RUNTIME_EMAIL/OS_RUNTIME_PASSWORD) — the runtime refuses an unauthenticated install with a 401 that names exactly that remedy (packages/cli/src/commands/package/install.ts)" ], "knownGaps": [ @@ -1852,6 +1852,12 @@ "date": "2026-10-04", "change": "A7 and its step re-pointed: the install-local DELETE now withdraws the package from the running kernel through SchemaRegistry.uninstallPackage — no restart is needed; the object 404s and the app leaves /meta/app at once, and the response note says so (marketplace-install-local-plugin.ts handleUninstall and withdrawFromRunningKernel). The restart-required wording was right when written. Stale clause. Source 901e7cf13 (#21581)", "ref": "#21735" + }, + { + "revision": 3, + "date": "2026-10-13", + "change": "fixtures.requires[1] gained the scratch-copy rule (setup recipe gap, #23104; recorded on platform-core.marketplace-console-honesty, which names this item, and carried by #23169). install-local caches the installed manifest relative to the server's working directory (local-manifest-source.ts DEFAULT_INSTALLED_PACKAGES_DIR under process.cwd()), and a per-boot OS_HOME / OS_STORAGE_LOCAL_ROOT does not redirect it, so an install from a boot started in the shared tree lands in examples/app-showcase/.objectstack/installed-packages (QA run #23163, 25 s, removed through the API). No clause changed", + "ref": "#23169" } ] }, @@ -1860,7 +1866,7 @@ "title": "Marketplace console honesty: nav entries live and die with their capability, the disabled state renders the configuration conclusion (never a red 'failed to load') for every viewer ahead of the admin guard, Installed Apps drives the real same-origin lifecycle, and an enabled runtime turns a non-admin away with access-denied", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "browser", "personas": [ @@ -1871,7 +1877,8 @@ "app": "showcase", "requires": [ "the same two boots as platform-core.marketplace-install-local-lifecycle (boot A default cloud arm, boot B OS_CLOUD_URL=off offline arm) — reuse them and its installed CRM artifact for the Installed Apps content", - "the marketplace routes are registered by DefaultAppContent in @object-ui/app-shell (system/marketplace, system/marketplace/:packageId), so they exist for every host console" + "the marketplace routes are registered by DefaultAppContent in @object-ui/app-shell (system/marketplace, system/marketplace/:packageId), so they exist for every host console", + "boot B's Installed Apps content comes from an install, and install-local caches the manifest under the WORKING DIRECTORY's .objectstack/installed-packages (packages/cloud-connection/src/local-manifest-source.ts#DEFAULT_INSTALLED_PACKAGES_DIR, resolved against process.cwd(); the CLI constructs the plugin with no storageDir). A per-boot OS_HOME and OS_STORAGE_LOCAL_ROOT do NOT redirect it, so a boot started from the shared tree writes examples/app-showcase/.objectstack/installed-packages/ into it. Boot every leg that installs a package from a scratch copy of the app, never from the tree (RUNNER.md environment facts)" ], "knownGaps": [ "rendering the browse CATALOG (boot A) needs the public control plane reachable — on an egress-blocked runner the catalog leg is blocked(environment). The environment it needs is a boot whose process reaches the control plane: the default packages/cloud-connection/src/cloud-url.ts#DEFAULT_CLOUD_URL (https://cloud.objectos.ai), or an OS_CLOUD_URL that answers. No CI job drives this item, and the QA sandbox's egress proxy denies the host (QA run #21782, parked on #22017 row 10); the DISABLED state (boot B) is the leg this item can always prove, and it is the one #5504 was about" @@ -1949,6 +1956,12 @@ "date": "2026-10-06", "change": "partial-coverage gap of QA run #21782 parked on #22017 (row 10). Boot A's catalog content was blocked(environment): egress to the catalog was denied. Disposition: recorded knownGap. The knownGap now names the environment the leg needs (a boot that reaches DEFAULT_CLOUD_URL or a reachable OS_CLOUD_URL) and that no CI job drives it. No clause changed", "ref": "#22017" + }, + { + "revision": 4, + "date": "2026-10-13", + "change": "fixtures.requires[2] (new) carries the scratch-copy rule for any boot that installs a package (setup recipe gap, #23104; parked on #23121, carried by #23169). install-local caches its manifest under the working directory's .objectstack/installed-packages (local-manifest-source.ts DEFAULT_INSTALLED_PACKAGES_DIR, resolved against process.cwd()), and per-boot OS_HOME / OS_STORAGE_LOCAL_ROOT do not redirect it. In QA run #23163 a boot started from the shared tree wrote a CRM manifest into examples/app-showcase/.objectstack/installed-packages for 25 s before it was removed through the API. RUNNER.md's environment facts carry the same rule. No clause changed", + "ref": "#23169" } ] }, @@ -2216,31 +2229,31 @@ }, { "id": "platform-core.keyboard-shortcut-surface", - "title": "Keyboard shortcut surface: `?` and ?shortcuts=1 open the dialog and Esc closes it, typing in an input never triggers it, the WIRED accelerators fire (⌘K, ⌘/Ctrl+B, ⌘⇧I where AI is present) — and the five advertised-but-DEAD accelerators (⌘/, N, R, ⌘E, ⌘D) plus the B-vs-⌘B display mismatch are EXPECTED-FAIL probes, never ticked green", + "title": "Keyboard shortcut surface: `?` and ?shortcuts=1 open the dialog and Esc closes it, typing in an input never triggers it, the dialog lists exactly the shortcuts whose handler is mounted and every listed one fires (⌘K, ⌘/Ctrl+B shown as ⌘B, Esc, ?), the five formerly advertised dead accelerators (⌘/, N, R, ⌘E, ⌘D) are neither listed nor bound, and the AI-chat pair ⌘⇧O / ⌘⇧S is listed only on the page that handles it (objectui#11674)", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "browser", "personas": ["seeded admin (admin@objectos.ai / admin123)"], "fixtures": { "app": "showcase", "requires": [ - "any stock boot with the console; an object list open for the dead-accelerator probes (N/R/⌘E claim record actions, so probe them where a record surface could plausibly respond)" + "any stock boot with the console; an object list open for the unlisted-key probes (N/R/⌘E were once advertised as record actions, so probe them where a record surface could plausibly respond)" ], "knownGaps": [ "⌘⇧I (chat dock) is wired but the dock only exists when the AI surface is present — on a stock open boot the agent catalog is empty, the listener is not armed and ⌘⇧I is deliberately inert; that WHOLE cell (inert-when-gated AND armed-when-entitled) is scored by ai.console-ai-surface-gating — here record only that the accelerator's handler exists and is gated, never a second verdict", - "⌘⇧O / ⌘⇧S are advertised in the dialog's 'AI assistant' group but their handlers are PAGE-SCOPED to the AI chat page (matchAiChatShortcut in AiChatPage) — on a stock open boot that page is unreachable, so these two cells cannot be reached on stock. That is an ENVIRONMENT gap (QA run #21782, parked on #22017 row 9): the AI chat page is reachable only where the access-filtered agent catalog serves at least one agent, i.e. a deployment running the Cloud/Enterprise @objectstack/service-ai, or a console rebuilt with VITE_AI_BASE_URL pointing at such a server (ai.console-ai-surface-gating knownGaps). The open framework ships no agent server by design and CI has no such deployment, so score them blocked(environment). objectui unit-pins only the key-to-intent mapping (packages/app-shell/src/console/ai/__tests__/matchAiChatShortcut.test.ts), which does not score the page-scope clause; on an AI-enabled deployment probe them ON the chat page and record the page-scope honestly" + "⌘⇧O / ⌘⇧S are advertised by the AI chat page itself, beside its handler (AiChatPage's useAdvertiseShortcut(NEW_CHAT_SHORTCUT / TOGGLE_CHATS_LIST_SHORTCUT) next to matchAiChatShortcut, objectui#11674), so the dialog lists them ONLY while that page is mounted. The off-page half is scorable on every stock boot: the dialog shows no AI-chat group and neither chord does anything. The on-page half needs the AI chat page, which is reachable only where the access-filtered agent catalog serves at least one agent, i.e. a deployment running the Cloud/Enterprise @objectstack/service-ai, or a console rebuilt with VITE_AI_BASE_URL pointing at such a server (ai.console-ai-surface-gating knownGaps). The open framework ships no agent server by design and CI has no such deployment, so that half scores blocked(environment) (QA run #21782, parked on #22017 row 9). objectui pins that the page's advertisements carry chords its matcher answers (KeyboardShortcutsDialog.wiredOnly-11674.test.tsx, the AI chat page describe) and the key-to-intent mapping (packages/app-shell/src/console/ai/__tests__/matchAiChatShortcut.test.ts); neither is a live on-page run" ] }, "steps": [ "press `?` on a settled console page (focus NOT in an input) — the shortcuts dialog opens (overlay data-testid overlay:keyboard-shortcuts) and the URL carries ?shortcuts=1; press Esc — it closes and the param clears", "deep-link: open ?shortcuts=1 cold — the dialog is open on load (URL-addressable, ADR-0054)", "input guard: focus a text input (e.g. a list filter), type `?` — the character lands in the input and NO dialog opens (the handler returns early for INPUT/TEXTAREA/contentEditable)", - "inventory the dialog: capture every advertised accelerator. At head the groups list: ⌘K palette / ? / Esc; B sidebar + ⌘/ focus-search; N create + R refresh + ⌘E edit; ⌘⇧O new chat + ⌘⇧S conversations; ⌘D dark mode", + "inventory the dialog: capture every listed accelerator. The rows are derived from the handlers mounted at that moment (advertisedShortcuts.ts, objectui#11674), so on a stock page at objectui 1fb1ced36243 the dialog lists exactly: General — Close dialog/panel `Esc`, Open command palette `⌘ K`, Show keyboard shortcuts `?`; Navigation — Toggle sidebar `⌘ B`. No AI-chat group appears off the chat page, and nothing else is listed", "fire the WIRED set: ⌘K (or Ctrl+K) opens the command palette (depth of the palette itself belongs to search.console-global-search — here only that the accelerator fires); ⌘/Ctrl+B toggles the sidebar; ⌘⇧I is NOT probed here — its gated/entitled matrix belongs to ai.console-ai-surface-gating", - "display-mismatch probe: the dialog advertises bare `B` for the sidebar — press bare B (no modifier) outside any input: NOTHING happens (the handler requires metaKey||ctrlKey); press ⌘/Ctrl+B: the sidebar toggles. Record the mismatch as expected-fail", - "EXPECTED-FAIL probes, one by one on the object list, focus on the page body: ⌘/ (advertised focus-search), N (create record), R (refresh data), ⌘E (edit record), ⌘D (toggle dark mode) — for each, capture that NO corresponding effect occurs (no focused search box, no create overlay, no refetch on the wire, no edit mode, no theme class change). A repo grep across app-shell/console/components finds no handler for any of the five; these clauses record the actual behavior and MUST NOT tick green while the defect stands (defect K1 — filed by the sweep, not per run)", + "display-equals-binding probe for the sidebar: the dialog shows `⌘ B` (a mod chord), never bare B — press bare B (no modifier) outside any input: NOTHING happens (the handler requires metaKey||ctrlKey); press ⌘B and Ctrl+B: the sidebar toggles each time. Display and binding agree", + "unlisted-key probes, one by one on the object list, focus on the page body: ⌘/ (once advertised as focus-search), N (create record), R (refresh data), ⌘E (edit record), ⌘D (toggle dark mode). First confirm none of the five is listed in the dialog; then, for each, capture that NO corresponding effect occurs (no focused search box, no create overlay, no refetch on the wire, no edit mode, no theme class change). None of the five has a handler, and since objectui#11674 none is advertised: the advertised set equals the wired set. A key here that now ACTS while unlisted, or one that is listed again with no effect, breaks that equality", "Esc closes the dialog from every open path; re-run `?` after the probes to confirm the dialog itself still works" ], "acceptance": [ @@ -2257,38 +2270,38 @@ "evidence": "the per-accelerator screenshot pairs" }, { - "clause": "EXPECTED FAIL (defect K1a): the dialog advertises bare `B` for the sidebar while the only handler requires ⌘/Ctrl — bare B does nothing, ⌘/Ctrl+B works. The run records the mismatch; ticking this clause green requires the DISPLAY to be fixed to match the binding (or a bare-B handler to exist), at which point this item needs a revision", + "clause": "the sidebar's displayed chord equals its binding: the dialog lists Toggle sidebar as `⌘ B` and lists no bare B; bare B does nothing and ⌘B / Ctrl+B toggle the sidebar. This was expected-fail K1a until objectui#11674 derived the dialog's rows from the mounted handlers", "oracle": "dom", - "verify": "bare-B keypress leaves the sidebar state unchanged (read the sidebar's data-state), ⌘/Ctrl+B flips it; the dialog's Navigation group still lists bare B", + "verify": "the dialog's Navigation group reads `⌘ B` for Toggle sidebar and carries no bare-B row; a bare-B keypress leaves the sidebar state unchanged (read the sidebar's data-state), and ⌘B and Ctrl+B each flip it. objectui pins the same pair in packages/app-shell/src/chrome/KeyboardShortcutsDialog.wiredOnly-11674.test.tsx ('a listed row whose chord does nothing is caught: a bare B leaves the sidebar alone'); run it from an objectui checkout at the .objectui-sha revision to cite it beside the live read", "evidence": "the sidebar state trace + the dialog screenshot" }, { - "clause": "EXPECTED FAIL (defect K1b): each of the five advertised accelerators ⌘/ (focus search), N (create), R (refresh), ⌘E (edit), ⌘D (dark mode) produces NO effect — no handler exists in the tree. Each probe is its own record: effect-absence proven against the specific advertised effect (focus, overlay, network refetch, edit surface, root theme class). A run must NOT tick these green while the shortcuts stay dead; when handlers land, flip the clauses to positive assertions in a revision", + "clause": "the advertised set equals the wired set: every row the dialog lists fires its own handler, and none of the five formerly advertised accelerators ⌘/ (focus search), N (create), R (refresh), ⌘E (edit), ⌘D (dark mode) is listed; each of the five still produces NO effect. Each probe is its own record: effect-absence proven against the specific once-advertised effect (focus, overlay, network refetch, edit surface, root theme class). This was expected-fail K1b until objectui#11674 made the dialog list only shortcuts whose handler is mounted", "oracle": "network", - "verify": "per-key: the advertised effect's own oracle stays silent (no focus change via document.activeElement, no ?form=new push, no list refetch on the wire, no edit route, documentElement class list unchanged); reproduce twice on fresh loads per RUNNER rule 2", + "verify": "the step-3 inventory lists none of the five; per key, the once-advertised effect's own oracle stays silent (no focus change via document.activeElement, no ?form=new push, no list refetch on the wire, no edit route, documentElement class list unchanged); reproduce twice on fresh loads per RUNNER rule 2. objectui pins the listed half in KeyboardShortcutsDialog.wiredOnly-11674.test.tsx ('every row the console dialog lists fires its real handler, with ⌘ and with Ctrl')", "evidence": "the five per-key traces/DOM reads" }, { - "clause": "the advertised-but-page-scoped pair is recorded honestly: ⌘⇧O/⌘⇧S are listed globally but handled only on the AI chat page — on stock open boots blocked(environment); on AI-enabled deployments they fire ON that page and do nothing elsewhere", + "clause": "the page-scoped pair is listed only where it is handled: off the AI chat page the dialog lists neither ⌘⇧O nor ⌘⇧S and neither chord does anything (scorable on stock); on the AI chat page both are listed and fire. The on-page half is blocked(environment) on stock open boots (knownGaps)", "oracle": "dom", - "verify": "keypress outside the chat page: no effect; on the chat page (where reachable): new-chat/list-toggle occur (matchAiChatShortcut)", + "verify": "outside the chat page: the dialog inventory has no AI-chat group, and each keypress has no effect; on the chat page (where reachable): the dialog lists both, and new-chat/list-toggle occur (matchAiChatShortcut)", "evidence": "both contexts' traces, or the blocked(environment) record on stock" } ], "negative": [ "ticking any advertised accelerator green off its LISTING in the dialog is the exact ticking-on-a-label failure this item exists to prevent — every accelerator is proven by firing it", "a dead wired shortcut (⌘K or ⌘/Ctrl+B not responding) is a REAL fail, not an expected one — but rule out automation-input first: a synthetic event that does not reach the window/document listener fakes exactly this", - "if a future run finds the five dead shortcuts now WORKING, that is not a silent pass — the expected-fail clauses are stale and the item needs a revision flipping them to positive assertions (cite the fixing PR)", + "a listed row that does nothing is a REAL fail since objectui#11674 — the dialog is derived from mounted handlers, so a listed dead key means an advertiser outlived its handler. If one of the five formerly dead keys starts to ACT, the clause is stale (the advertised set no longer equals the wired set), and the item needs a revision naming the new handler", "the `?` handler firing while an input is focused (swallowing typed text) would be a regression of the guard — probe it, it is cheap" ], "traps": ["automation-input", "stale-console-bundle", "absence-inference"], "source": [ - "objectui packages/app-shell/src/chrome/KeyboardShortcutsDialog.tsx (the advertised groups — the full inventory this item probes), (the `?` handler with the input/textarea/contentEditable guard; `?` is the ONLY key it handles), (?shortcuts=1 via useUrlOverlay)", + "objectui packages/app-shell/src/chrome/KeyboardShortcutsDialog.tsx (rows derived from the advertised shortcuts, never a list of its own — objectui#11674), (the `?` handler with the input/textarea/contentEditable guard; `?` is the ONLY key it handles), (?shortcuts=1 via useUrlOverlay) + advertisedShortcuts.ts (useAdvertiseShortcut / useAdvertisedShortcuts: a shortcut is listed for exactly as long as its handler is mounted) + KeyboardShortcutsDialog.wiredOnly-11674.test.tsx (the pin: every listed row fires its real handler; a listed bare B is caught)", "objectui packages/app-shell/src/context/CommandPaletteProvider.tsx (⌘/Ctrl+K — wired)", - "objectui packages/components/src/ui/sidebar.tsx (SIDEBAR_KEYBOARD_SHORTCUT = 'b'), (the handler requires metaKey||ctrlKey — the source of the B-vs-⌘B display mismatch)", + "objectui packages/components/src/ui/sidebar.tsx (SIDEBAR_KEYBOARD_SHORTCUT = 'b'), (the handler requires metaKey||ctrlKey) + packages/app-shell/src/layout/ConsoleLayout.tsx (advertises TOGGLE_SIDEBAR_SHORTCUT as a mod chord beside that listener, so the dialog shows ⌘B)", "objectui packages/app-shell/src/layout/chatDockState.ts (matchChatDockShortcut — ⌘/Ctrl+Shift+I, composer-safe)", - "objectui packages/app-shell/src/console/ai/AiChatPage.tsx (matchAiChatShortcut — ⌘⇧O/⌘⇧S, PAGE-scoped)", - "repo-grep evidence for the five dead accelerators: no handler for ⌘/ / N / R / ⌘E / ⌘D anywhere in app-shell, console or components (the only near-misses are page-scoped: ApprovalsInboxPage's plain `r` quick-decide and useDebugMode's Ctrl+Shift+D — neither matches an advertised accelerator)" + "objectui packages/app-shell/src/console/ai/AiChatPage.tsx (matchAiChatShortcut — ⌘⇧O/⌘⇧S, PAGE-scoped; NEW_CHAT_SHORTCUT / TOGGLE_CHATS_LIST_SHORTCUT advertised by the page, so listed only while it is mounted)", + "repo-grep evidence for the five formerly advertised accelerators: no handler for ⌘/ / N / R / ⌘E / ⌘D anywhere in app-shell, console or components, and since objectui#11674 no advertiser for them either (the only near-misses are page-scoped: ApprovalsInboxPage's plain `r` quick-decide and useDebugMode's Ctrl+Shift+D — neither is advertised)" ], "history": [ { @@ -2302,6 +2315,12 @@ "date": "2026-10-06", "change": "partial-coverage gap of QA run #21782 parked on #22017 (row 9). A5 was blocked(fixture) as the clause prescribed. Disposition: recorded knownGap. The leg cannot be reached in the open framework by design: ⌘⇧O / ⌘⇧S are handled only on the AI chat page, which exists only where an agent catalog serves an agent (Cloud/Enterprise service-ai). The knownGap names that environment, says CI lacks it, and reclassifies the verdict blocked(environment); A5's clause and evidence follow", "ref": "#22017" + }, + { + "revision": 3, + "date": "2026-10-13", + "change": "acceptance[2], [3] and [4] (#23104 C2, C3, C4) flipped from expected-fail probes to positive assertions, with the title, fixtures.requires[0], knownGaps[1], steps[3], [5] and [6], negative[2] and four source notes (stale spec; rows from #23104 parked on #23121, carried by #23169). objectui#11674 derived the shortcuts dialog's rows from the handlers mounted at that moment (advertisedShortcuts.ts), so the five dead accelerators (⌘/, N, R, ⌘E, ⌘D) are no longer listed, Toggle sidebar is listed as ⌘B rather than bare B, and the AI-chat pair ⌘⇧O / ⌘⇧S is advertised by AiChatPage and listed only while that page is mounted. QA run #23163 (objectstack 36b2889c, objectui 1fb1ced36243) read the stock inventory as General: Esc, ⌘K, ?; Navigation: ⌘B, with bare B inert, Ctrl+B toggling and each of the five keys without effect. Re-read from objectui source at the same pin, which is still this repo's .objectui-sha: KeyboardShortcutsDialog.tsx, advertisedShortcuts.ts, ConsoleLayout.tsx, AiChatPage.tsx and the pin KeyboardShortcutsDialog.wiredOnly-11674.test.tsx. The on-page half of acceptance[4] stays blocked(environment) on stock", + "ref": "#23169" } ] }, @@ -2399,7 +2418,7 @@ "title": "ADR-0057 lifecycle sweep: backdated telemetry rows past retention are reaped BOUNDED under a system context on the hourly sweep, onlyWhen scoping spares live rows, an archive-declaring object is NEVER hot-deleted unarchived (retained + reported archive-pending when no archive datasource exists), the transient class stays on the primary, and OS_LIFECYCLE_DISABLED=1 disarms the sweep", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "mixed", "personas": ["operator (local shell + direct sqlite access)", "seeded admin (API reads)"], @@ -2413,16 +2432,17 @@ "knownGaps": [ "the ARCHIVE-SUCCESS leg (rows copied to the archive datasource, then hot-deleted) needs a registered 'archive' datasource, which no stock boot has — that leg rides the unit pin (lifecycle-service.test.ts) and stays blocked(fixture) live; what IS live on stock is the SAFETY side: sys_audit_log rows older than 90d are RETAINED and the sweep reports the object skipped archive-pending", "class-routing to a dedicated 'telemetry' datasource (ADR-0057 §3.6) engages only when a telemetry datasource is registered — absent on stock, so the routing leg is asserted at the unit/source level (engine.ts getDriver priority + SYSTEM_LEDGER_LIFECYCLE_CLASSES), not live", - "the ≤ REAP_BATCH_SIZE × REAP_MAX_BATCHES_PER_SWEEP bound (500×20 per object per sweep) is proven by the unit suite; staging >10k backdated rows live is not required — the live clause asserts the reap happened via the bounded per-id path (the aggregate log line), not the bound's edge" + "the ≤ REAP_BATCH_SIZE × REAP_MAX_BATCHES_PER_SWEEP bound (500×20 per object per sweep) is proven by the unit suite; staging >10k backdated rows live is not required — the live clause asserts the reap happened via the bounded per-id path (the aggregate log line), not the bound's edge", + "RULING OWED on the idle-proof step, steps[4] (QA run #23166 C1, parked on #23121, carried by #23169): an idle sweep on a clean DB still logs '[lifecycle] sweep: 18 policy(ies) applied, ~0 rows reaped, …' at objectstack 36b2889c, and the source at 6d7f4df3c reads the same. The print gate tests report.swept.length, and the reaper pushes a report.swept entry for every policy it applies, deleted 0 included (lifecycle-service.ts; the LifecycleSweepReport.swept docblock reads 'Policies applied, one entry per (object, policy)'). So steps[4]'s 'an idle sweep logs nothing' is either a stale step (one line per sweep is the design) or a product defect (the line should print only when rows moved). Until the maintainer rules, a run records the line verbatim and scores steps[4] neither pass nor fail. No acceptance clause rests on it" ] }, "steps": [ "boot once on the file DB so schema exists; stop the server", "hand-seed via sqlite: (a) N sys_automation_run rows with created_at 40 days ago, status 'completed' (past the 30d window, terminal); (b) 2 rows created_at 40 days ago with status 'paused'/'running' (past the window but NOT terminal); (c) 1 sys_audit_log row created_at 100 days ago (past archive.after 90d); (d) 1 sys_inbox_message row (transient class). Record exact counts and ids", - "boot again (control boot, no env flag); wait ≥90s; grep the log for the aggregate sweep line '[lifecycle] sweep: … policy(ies) applied, ~… rows reaped …' (the ONE trace a sweep leaves — cleanup must not re-feed the tables it drains)", + "boot again (control boot, no lifecycle flag) with OS_LOG_LEVEL=info exported (or `--log-level info`): the aggregate sweep line is logged at INFO, and `os dev` defaults the kernel logger to warn, so a default-level boot never prints it; wait ≥90s; grep the log for '[lifecycle] sweep: … policy(ies) applied, ~… rows reaped …' (the ONE trace a sweep leaves — cleanup must not re-feed the tables it drains)", "re-read via API/sqlite: the terminal 40d-old automation rows are GONE; the paused/running 40d-old rows SURVIVE (onlyWhen scoping); the 100d-old audit row SURVIVES and the sweep report lists sys_audit_log skipped archive-pending (no 'archive' datasource registered → retained, never dropped unarchived); the inbox row's fate follows its own declared policy only — the transient class got no cross-class treatment", "idle proof: a second sweep interval later (or a fresh boot on the now-clean DB + 90s), the log carries NO sweep line — an idle sweep logs nothing (the line only prints when something was swept/errored/alerted)", - "kill switch: re-seed one terminal 40d-old automation row; boot with OS_LIFECYCLE_DISABLED=1; wait ≥90s; the row SURVIVES and no sweep line appears — and this absence is meaningful only because step 2 proved the sweep active on the same composition (absence-inference guarded)", + "kill switch: re-seed one terminal 40d-old automation row; boot with OS_LIFECYCLE_DISABLED=1 AND OS_LOG_LEVEL=info (the same level as the control boot, or the missing line proves nothing); wait ≥90s; the row SURVIVES and no sweep line appears — and this absence is meaningful only because step 2 proved the sweep active on the same composition and log level (absence-inference guarded)", "run the unit pin for the legs live cannot stage: pnpm --filter @objectstack/objectql exec vitest run src/lifecycle/lifecycle-service.test.ts — the archive-declared skip ('skips hot deletion entirely while an archive is declared (retain → archive → delete)', report.skipped == [{object, reason: 'archive-pending'}]), the bounded-reap cases, and the rotation fallback" ], "acceptance": [ @@ -2501,6 +2521,12 @@ "date": "2026-08-30", "change": "new — authored in the 2026-08-30 sweep (spec-enums angle): the ADR-0057 lifecycle vocabulary had items nowhere though stock objects declare all the interesting shapes. Grounding sharpened the register on three points: (1) sys_audit_log stock-declares archive 90d, so the never-hot-delete SAFETY side is live-stageable (the success side stays unit-pinned); (2) the sweep has NO on-demand door — first fire is 60s after engine start, so every probe brackets that delay and the kill-switch clause requires an armed control boot; (3) the enum pin is LifecycleClassSchema expect 5, verified by member count at object.zod.ts", "ref": "#sweep-2026-08-30" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "steps[2] (the control boot) and steps[5] (the kill-switch boot) now boot with OS_LOG_LEVEL=info, and a knownGap records the open ruling on steps[4], the idle proof (stale step + ruling owed, #23166 C1; parked on #23121, carried by #23169). The '[lifecycle] sweep:' line is logged at INFO, and `os dev` defaults the kernel logger to warn (packages/cli/src/commands/dev.ts --log-level, serve.ts), so the grep in steps[2] could never match on a default boot, and the kill-switch boot must match its level for its missing line to mean anything. steps[4] is not rewritten: an idle sweep still logs 'N policy(ies) applied, ~0 rows reaped', because report.swept carries one entry per applied policy and the print gate tests its length. Whether that is the design or a defect is the maintainer's call; the knownGap says how a run scores it meanwhile. No acceptance clause changed", + "ref": "#23169" } ] }, @@ -2611,10 +2637,10 @@ }, { "id": "platform-core.theme-mode-persistence", - "title": "Theme mode: dark/light apply the root class immediately and survive reload via localStorage (vite-ui-theme); system/auto resolves the OS preference at evaluation time (#2942 — never a dead 'auto' class); no half-themed surface after a toggle", + "title": "Theme mode: dark/light apply the root class immediately and survive reload via localStorage (object-ui-theme, the console shell's storage key); system/auto resolves the OS preference at evaluation time (#2942 — never a dead 'auto' class); no half-themed surface after a toggle", "since": "v15", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "browser", "personas": ["seeded admin (admin@objectos.ai / admin123)"], @@ -2623,16 +2649,16 @@ "requires": ["any stock console boot; a driver that can emulate the OS color-scheme preference (CDP prefers-color-scheme override) for the system-mode cells"] }, "steps": [ - "open the console; locate the ModeToggle in the AppHeader; read the initial documentElement class and localStorage['vite-ui-theme']", + "open the console; open the avatar menu in the AppHeader and find Theme under its Preferences group (the ModeToggle lives in that dropdown, not as a standalone top-bar button); read the initial documentElement class and localStorage['object-ui-theme']", "pick Dark: documentElement gains class 'dark' IMMEDIATELY (no reload); the toggle shows the checkmark on Dark; screenshot a data-dense surface (list + header + sidebar) and confirm no half-themed region (a light-styled panel inside the dark shell)", "reload: the dark class is present on load (localStorage read at provider init); localStorage carries 'dark'", "pick Light: class flips to 'light' immediately; reload-persists the same way", "pick System with the browser's emulated preference set to dark: the resolved class is 'dark' and localStorage stores 'system'; flip the emulated preference and RELOAD: the class follows the new preference (resolution happens at provider evaluation — the provider attaches NO matchMedia listener, so a live OS flip without reload updating the class is NOT asserted; assert resolution at load, and record live-tracking as not-implemented if probed)", - "regression probe (#2942): with a stored value of 'auto' (write it into localStorage by hand — the spec's ThemeModeSchema spelling), reload: the class resolves to the OS preference, never a literal 'auto' class locking light styling" + "regression probe (#2942): with a stored value of 'auto' (write it into localStorage['object-ui-theme'] by hand — the spec's ThemeModeSchema spelling), reload: the class resolves to the OS preference, never a literal 'auto' class locking light styling" ], "acceptance": [ { - "clause": "explicit dark/light apply the root class synchronously on selection and persist across reload via localStorage vite-ui-theme", + "clause": "explicit dark/light apply the root class synchronously on selection and persist across reload via localStorage object-ui-theme", "oracle": "dom", "verify": "documentElement.classList before/after each pick and after reload; localStorage value at each step", "evidence": "the class/storage trace" @@ -2657,8 +2683,8 @@ ], "traps": ["shared-browser-tab", "hydration-race"], "source": [ - "objectui packages/app-shell/src/chrome/ThemeProvider.tsx (storageKey 'vite-ui-theme', initial read), (the class effect — 'auto'/'system' resolve matchMedia ONCE per evaluation; the #2942 note: branching on 'system' alone sent 'auto' into classList.add('auto'), a class no Tailwind variant matches), (setTheme persists then applies)", - "objectui packages/app-shell/src/layout/ModeToggle.tsx (the three-way menu with checkmarks) + AppHeader.tsx (mounted in the header)", + "objectui packages/app-shell/src/chrome/ThemeProvider.tsx (the storageKey PROP, whose default 'vite-ui-theme' the console overrides; initial read), (the class effect — 'auto'/'system' resolve matchMedia ONCE per evaluation; the #2942 note: branching on 'system' alone sent 'auto' into classList.add('auto'), a class no Tailwind variant matches), (setTheme persists then applies) + packages/app-shell/src/console/ConsoleShell.tsx (mounts ThemeProvider with storageKey=\"object-ui-theme\": the key this item reads)", + "objectui packages/app-shell/src/layout/ModeToggle.tsx (the three-way menu with checkmarks) + AppHeader.tsx (mounted inside the avatar dropdown's Preferences group, beside the language switcher)", "objectui#2942 (the auto-class regression this item's last step re-probes)" ], "history": [ @@ -2667,6 +2693,12 @@ "date": "2026-08-30", "change": "new — authored in the 2026-08-30 sweep (console-UI angle, optional-P2 register row). Grounding tightened one claim: the provider attaches NO matchMedia change listener, so 'system tracks the OS preference' holds at evaluation time only — the item asserts resolution on selection/reload and explicitly forbids failing the product for live-flip tracking it never implemented", "ref": "#sweep-2026-08-30" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "the storage key and the toggle's location corrected in the title, steps[0] and [5], acceptance[0] and source[0] and [1] (stale step text, #23104 C0; parked on #23121, carried by #23169; also reported on #21782's closeout). The console's ThemeProvider is mounted by ConsoleShell.tsx with storageKey=\"object-ui-theme\"; 'vite-ui-theme' is only the provider's default prop, which the console overrides. ModeToggle lives under the AppHeader's avatar menu, in its Preferences group. QA run #23163 read the key object-ui-theme live (objectstack 36b2889c, objectui 1fb1ced36243). Re-read from objectui source at that pin, still this repo's .objectui-sha: ConsoleShell.tsx, ThemeProvider.tsx, AppHeader.tsx. No clause's verdict logic changed", + "ref": "#23169" } ] }, diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index 82f4377e984..a4a507a28eb 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -395,7 +395,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": 1, + "revision": 2, "priority": "P1", "surface": "browser", "personas": [ @@ -406,7 +406,8 @@ "requires": [ "showcase_task with 10 seeded tasks (2 in_progress) and the saved views in examples/app-showcase/src/ui/views/task.view.ts (in_progress/urgent/done, tabular with string sort, grid with inlineEdit, bulk_actions, legacy_row_actions)", "showcase_field_zoo views for conditional formatting + bulk-action gating (examples/app-showcase/src/ui/views/field-zoo.view.ts)", - "CJK seeds for search recall (account 华宁科技, contact 张伟)" + "CJK seeds for search recall (account 华宁科技, contact 张伟)", + "an EXPORT GRANT for the exporting principal, which no stock set carries: allowExport is opt-in (packages/plugins/plugin-security/src/permission-evaluator.ts#resolveUserExportAllowed, export = read ∧ a set saying allowExport: true), and no stock permission set grants it, the admin wildcard included (#3544). Without it the seeded admin sees no Export control and GET /api/v1/data/showcase_task/export answers 403 EXPORT_NOT_PERMITTED. Provision one before step 6: author a runtime set through the metadata door, PUT /api/v1/meta/permission/qa_task_export with {name:'qa_task_export', label, objects:{showcase_task:{allowRead:true, allowExport:true}}} (published; a ?mode=draft save also needs its POST …/publish), then bind it with POST /api/v1/data/sys_user_permission_set {user_id: , permission_set: 'qa_task_export'} — the grant names its set by NAME (ADR-0131 D4) — and reload so /me/permissions carries the merged map" ] }, "variants": [ @@ -429,7 +430,7 @@ "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; 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, export each declared format (csv, xlsx, json); capture the requests and files", + "on in_progress, with the export grant from fixtures.requires[3] bound, export each declared format (csv, xlsx, json); capture the requests and files. Without the grant the Export control is 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", "on bulk_actions, select rows and run showcase_mark_done (per-record) and showcase_recalc_selection (aggregate — ONE dispatch carrying params._selectedIds); on legacy_row_actions confirm both named row actions resolve to live menu entries", "on Field Zoo, confirm the conditionalFormatting row highlight applies to Specimen — Full (f_lookup set) and not Specimen — Minimal" @@ -474,7 +475,7 @@ { "clause": "export offers exactly the declared formats (csv, xlsx, json on in_progress) and the exported rows equal the FILTERED set, not the whole table", "oracle": "network", - "verify": "the export menu lists the three exportOptions; each downloaded file contains the 2 in_progress rows", + "verify": "with the fixtures.requires[3] export grant bound (export is opt-in): the export menu lists the three exportOptions; each downloaded file contains the 2 in_progress rows. Record which grant the run provisioned", "evidence": "menu screenshot + the three files" }, { @@ -523,6 +524,12 @@ "date": "2026-08-07", "change": "initial — deep-test capability matrix for list surfaces, pinned to the objectui live specs where they exist", "ref": "claude/platform-test-checklist-ocwugl" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "fixtures.requires[3] (new) provisions an export grant, and steps[6] and acceptance[6].verify name it (fixture gap, #23163 A6; parked on #23121, carried by #23169). Export is opt-in: resolveUserExportAllowed allows it only where a set says allowExport: true, and no stock permission set does, the admin wildcard included (#3544). So as the seeded admin Export is hidden and the export route answers 403 EXPORT_NOT_PERMITTED. QA run #23163 scored A6 through a runtime set granting showcase_task allowRead + allowExport, bound through sys_user_permission_set by the set's NAME. The recipe uses the spec's PermissionSetSchema key `objects` at the metadata door (the stored sys_permission_set column is object_permissions). No clause's verdict logic changed", + "ref": "#23169" } ] }, @@ -640,7 +647,7 @@ "title": "Form-view gallery: every FormViewSchema layout type renders and submits", "since": "v15", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "browser", "personas": [ @@ -649,7 +656,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", + "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'", "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": [ @@ -667,7 +674,7 @@ ], "steps": [ "open a task record's full-page edit form (…/showcase_task/record//edit — RecordFormPage) on the stock default form (type simple); screenshot; verify the declared section ('Task', 2 columns) and its field order", - "overlay the default form with the tabbed variant (type tabbed + the sections of formViews.tabbed); reopen the page form; verify the three tabs (Overview / Schedule / Details) and that switching tabs preserves entered values", + "overlay the default form with the tabbed variant (type tabbed + the sections of formViews.tabbed, through the fixtures.requires[0] recipe with item-level isDefault:true); reopen the page form; verify the three tabs (Overview / Schedule / Details) and that switching tabs preserves entered values", "overlay the default form with the wizard variant; open the PAGE form (…/showcase_task/new, or the record's /edit route — RecordFormPage passes the type in both modes); walk Basics → Assignment → Schedule with next/back; verify back preserves values and submit happens ONCE at the end. The New/Edit MODAL renders a wizard as a stacked section list by design (resolveFormViewLayout), so the steps are walked on the page route", "overlay the default form with the split variant; open the page form; verify primary/secondary panes render their assigned sections side-by-side", "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", @@ -734,6 +741,12 @@ "date": "2026-10-05", "change": "clause 1 (drawer variant), steps 1-5, the fixtures, a new revert step, negative 2 and the sources re-pointed (assertion defect, #21782 VF3; VF3 gave no verbatim wording, so the clause is derived). RecordFormPage renders drawer/modal as simple by design ('presentation/open-modes, not record-page layouts', objectstack#1890), and resolveFormViewLayout maps only tabbed. The modelling decision is ADR-0050 (Proposed): drawer/modal leave FormView.type, and presentation belongs to NavigationMode / addRecord.mode / action type 'modal'. Named formViews are reachable from no console door, because every consumer reads objectDef.form ?? formViews.default, so the steps now overlay the default form. Measured differently from the card in two places. (1) The card has the wizard rendering as steps only on /new; RecordFormPage passes the type on the /edit page route as well, and only the New/Edit modal degrades it. (2) task-desk.page.ts hosts BOTH a formType=\"drawer\" and a formType=\"modal\" ObjectForm, so the modal presentation is scorable there, and blocked(fixture) stays only as the fallback. Re-read at objectui 9dfaca6543: unchanged", "ref": "#21797" + }, + { + "revision": 3, + "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" } ] }, @@ -1450,7 +1463,7 @@ "title": "CSV import: GBK decode, required-field hint, legacy-fallback notice", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P2", "surface": "browser", "personas": [ @@ -1467,7 +1480,7 @@ ] }, "steps": [ - "run the pinned unit suites (they feed real GBK bytes and assert the exact decode, the disabled-Next hint, and the visible degrade notice): pnpm --filter @object-ui/plugin-grid exec vitest run src/importParsers.test.ts src/__tests__/importMissingRequiredHint.test.tsx src/__tests__/importLegacyReferenceGuard.test.tsx", + "run the pinned unit suites (they feed real GBK bytes and assert the exact decode, the disabled-Next hint, and the visible degrade notice): pnpm exec vitest run packages/plugin-grid/src/importParsers.test.ts packages/plugin-grid/src/__tests__/importMissingRequiredHint.test.tsx packages/plugin-grid/src/__tests__/importLegacyReferenceGuard.test.tsx — from the REPO ROOT of an objectui checkout at the .objectui-sha revision. ⛔ Not `pnpm --filter @object-ui/plugin-grid exec vitest run src/…`: objectui's vitest invocation guard (scripts/vitest-invocation-guard.mjs) refuses a package-cwd run, which would otherwise silently execute the wrong suite (objectui#3378)", "capture the suite output as the primary evidence (the native file-picker hand-off is deliberately not browser-automated)", "optionally spot-check visually: open the import wizard on showcase_contact, load a GBK CSV past the native picker, and screenshot the decoded preview", "in the spot-check, map columns while leaving a required field (name) unmapped; observe the Next control and its hint", @@ -1477,19 +1490,19 @@ { "clause": "GBK bytes decode to the exact expected strings (no mojibake) — pinned", "oracle": "test", - "verify": "pnpm --filter @object-ui/plugin-grid exec vitest run src/importParsers.test.ts", + "verify": "pnpm exec vitest run packages/plugin-grid/src/importParsers.test.ts (from the objectui root)", "evidence": "test run output" }, { "clause": "an unmapped required field disables Next AND shows the named required-field hint — pinned", "oracle": "test", - "verify": "pnpm --filter @object-ui/plugin-grid exec vitest run src/__tests__/importMissingRequiredHint.test.tsx", + "verify": "pnpm exec vitest run packages/plugin-grid/src/__tests__/importMissingRequiredHint.test.tsx (from the objectui root)", "evidence": "test run output" }, { "clause": "the legacy-reference fallback path shows a visible degrade notice instead of silently degrading — pinned, and PIN-ONLY: no live ObjectStack server reaches the legacy path (fixtures.knownGaps)", "oracle": "test", - "verify": "pnpm --filter @object-ui/plugin-grid exec vitest run src/__tests__/importLegacyReferenceGuard.test.tsx", + "verify": "pnpm exec vitest run packages/plugin-grid/src/__tests__/importLegacyReferenceGuard.test.tsx (from the objectui root)", "evidence": "test run output" }, { @@ -1539,6 +1552,12 @@ "date": "2026-10-06", "change": "partial-coverage gap of QA run #21782 parked on #22017 (row 3). acceptance[2] was blocked(dependency): the legacy per-row path exists only without importRecords, the live adapter always has it, and the pin is objectui-only. Disposition: pin. The objectui pin is confirmed at the current .objectui-sha (0abd4f9f8769), and the clause and a knownGap now say the leg is pin-only by design and name the environment that runs the pin", "ref": "#22017" + }, + { + "revision": 6, + "date": "2026-10-13", + "change": "steps[0] and acceptance[0], [1] and [2].verify run the three pins from the objectui root (assertion defect, #23163 A0; parked on #23121, carried by #23169). `pnpm --filter @object-ui/plugin-grid exec vitest run src/…` is refused by objectui's vitest invocation guard (scripts/vitest-invocation-guard.mjs, objectui#3378): from inside a package the root projects glob resolves against the cwd and the run would execute the wrong suite. The same three files run as `pnpm exec vitest run packages/plugin-grid/src/…` from the root; all three exist at 1fb1ced36243, still this repo's .objectui-sha. No clause's assertion changed", + "ref": "#23169" } ] }, @@ -1547,7 +1566,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": 9, + "revision": 10, "priority": "P1", "surface": "browser", "personas": [ @@ -1561,7 +1580,8 @@ ], "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.", - "[CLOSED by #22030 (PR #22036, 60ccda5a24), lifted by #22039 — the gap text is rewritten to say where each location now lives] a stock showcase boot hosts all six locations on showcase_task, including the three this gap used to block. record_header and record_more: Task Detail (showcase_task_detail, the record page the console opens for any task), whose first node is a page:header. That header renders the action ids it lists, each placed by its own locations, followed by the host's Edit / Share / Delete. Inline (record_header) on a desktop viewport, where the header's default inline budget is 3 (1 on mobile; a record_header action past the budget folds into ⋯): showcase_mark_done (script, shown only while the task is not done), showcase_log_time (form) and showcase_archive_task (script, disabled until the task is done). Under the ⋯ overflow (record_more without record_header): showcase_open_docs (url), showcase_recalc_estimate (api) and showcase_recalc_selection (api). record_related: Project Detail (showcase_project_detail, the record page for any project) → the Related tab (?tab=related), which holds the Tasks related list, a record:related_list for showcase_task through its project FK; open a project that has tasks. It is an authored node, not the synthesizer's strip: the page's tabs override still replaces that strip, and this tab puts the list back with no actions key, so its rows carry showcase_log_time beside the task's list_item actions. The Tasks tab beside it is the record:line_items grid, which hosts no record_related action. An expected action missing from one of these hosts is a missing placement, never blocked(fixture). The hosts were read from objectui source at pin a58626c88dc8 and from the served metadata, not from a browser render, so the first run against them is their first browser evidence" + "[CLOSED by #22030 (PR #22036, 60ccda5a24), lifted by #22039 — the gap text is rewritten to say where each location now lives] a stock showcase boot hosts all six locations on showcase_task, including the three this gap used to block. record_header and record_more: Task Detail (showcase_task_detail, the record page the console opens for any task), whose first node is a page:header. That header renders the action ids it lists, each placed by its own locations, followed by the host's Edit / Share / Delete. Inline (record_header) on a desktop viewport, where the header's default inline budget is 3 (1 on mobile; a record_header action past the budget folds into ⋯): showcase_mark_done (script, shown only while the task is not done), showcase_log_time (form) and showcase_archive_task (script, disabled until the task is done). Under the ⋯ overflow (record_more without record_header): showcase_open_docs (url), showcase_recalc_estimate (api) and showcase_recalc_selection (api). record_related: Project Detail (showcase_project_detail, the record page for any project) → the Related tab (?tab=related), which holds the Tasks related list, a record:related_list for showcase_task through its project FK; open a project that has tasks. It is an authored node, not the synthesizer's strip: the page's tabs override still replaces that strip, and this tab puts the list back with no actions key, so its rows carry showcase_log_time beside the task's list_item actions. The Tasks tab beside it is the record:line_items grid, which hosts no record_related action. An expected action missing from one of these hosts is a missing placement, never blocked(fixture). The hosts were read from objectui source at pin a58626c88dc8 and from the served metadata, not from a browser render, so the first run against them is their first browser evidence", + "acceptance[4] half (a), the locations-LESS probe, is BLOCKED(fixture) on stock (QA run #23163 A4, objectstack 36b2889c). A runtime-authored action (a scratch or writable package) is not joined into the served object's actions[] — the run's positive control, the same probe WITH a declared location, was invisible too — so a runtime probe sees nothing on any surface and proves nothing. The half needs a PACKAGED specimen with no `locations` key, as showcase_new_task is the packaged `locations: []` one. The stock showcase ships none: that shape is exactly what packages/lint's action-no-placement rule warns about. Score half (a) blocked(fixture) and half (b) on showcase_new_task as before" ] }, "variants": [ @@ -1580,7 +1600,7 @@ "dispatch one action per location with a ref-targeted click: bulk_reassign (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", "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) in a scratch/writable package author a copy of an api action with NO locations key; reload and 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 — then delete the probe; (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" + "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" ], "acceptance": [ { @@ -1610,7 +1630,7 @@ { "clause": "empty/missing locations means NO location — the probe action with no `locations` key renders on none of the six surfaces, and the headless `locations: []` declaration (showcase_new_task) renders on none either. Placement is a declaration, not a default: objectui#3142 collapsed four disagreeing renderers onto the single `actionRendersAt` predicate, a plain membership test, so an undeclared or empty list matches nothing. recalc_selection names record_more for that reason — an explicit single-record placement that also keeps it off the toolbar, where a dispatch would carry no selection and the endpoint would reject it — NOT to opt out of an everywhere-default", "oracle": "dom", - "verify": "both six-surface DOM sweeps (the locations-less probe, and showcase_new_task) come back with an empty placement list; recalc_selection is present under ⋯ and absent from the toolbar", + "verify": "both six-surface DOM sweeps (the packaged locations-less specimen, and showcase_new_task) come back with an empty placement list; recalc_selection is present under ⋯ and absent from the toolbar. On stock no packaged locations-less specimen exists, so half (a) scores blocked(fixture) (knownGaps) and the clause is judged on half (b) plus the recalc_selection read; a runtime-authored probe's empty sweep is never evidence for half (a)", "evidence": "the two empty placement lists + the toolbar DOM read" } ], @@ -1689,6 +1709,12 @@ "date": "2026-10-06", "change": "lifted the row-7 fixture knownGap (#22039): #22030 (PR #22036, 60ccda5a24) hosts record_header, record_more and record_related for showcase_task on stock showcase pages. knownGaps[1] is rewritten from 'no surface' to where each location lives: Task Detail's page:header (record_header inline; record_more's url / api / api under the ⋯ overflow) and Project Detail's Related tab (the Tasks related list, an authored record:related_list rather than the synthesizer's strip). The lines revision 7 added for the gap are re-pointed. Step 2 names each host and drops its blocked(fixture) sentence. Clause 1 drops 'synthesized', and its verify scores an absent action as a missing placement. Clause 3 no longer parks the url / api legs. knownGaps[0] is unchanged", "ref": "#22039" + }, + { + "revision": 10, + "date": "2026-10-13", + "change": "knownGaps[2] (new), steps[6] and acceptance[4].verify (fixture gap, #23163 A4; parked on #23121, carried by #23169). steps[6](a) authored a locations-less probe action at runtime. QA run #23163 found runtime-authored actions are not joined into the served object's actions[]: its positive control, the same probe with a location, rendered nowhere either, so the empty sweep proved nothing. The half needs a packaged specimen with no `locations` key, and the stock showcase ships none (the shape action-no-placement warns about). Half (a) now scores blocked(fixture) on stock; half (b) on showcase_new_task is unchanged. Shipping the specimen is a showcase fixture change, not made here", + "ref": "#23169" } ], "enumSource": { @@ -1801,7 +1827,7 @@ "title": "Gantt is interactive, not a picture: drag persists, locked tasks survive auto-schedule, host veto restores", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "browser", "personas": [ @@ -1813,7 +1839,7 @@ "the task gantt view (task.view.ts 'Schedule (Gantt)': startDateField/endDateField/titleField/progressField)" ], "knownGaps": [ - "the stock task gantt view (examples/app-showcase/src/ui/views/task.view.ts, listViews.gantt) declares NO dependenciesField / lockField / parentField, so the auto-schedule, locked-task and subtree-drag surfaces are unreachable LIVE on stock fixtures. ⚠ All three ARE GanttConfig keys (packages/spec/src/ui/view.zod.ts#GanttConfigSchema). QA run #21782 read `lockField` as no spec key; that is wrong, so the variants need no re-point. A2 and A3 are scored from the objectui pins `automated` names per clause. The one sub-leg no pin reaches is a DIRECT drag on a locked LEAF bar: GanttView.tsx returns from the drag start for a locked task, but only the summary-drag and link-drop locked cases are unit-pinned. It needs the view extended live, with lockField pointing at a boolean on showcase_task such as `done` (a view overlay or a scratch copy). That recipe is not run-proven, so score the sub-leg blocked(fixture) until a run proves it", + "the stock task gantt view (examples/app-showcase/src/ui/views/task.view.ts, listViews.gantt) declares NO dependenciesField / lockField / parentField, so the auto-schedule, locked-task and subtree-drag surfaces are unreachable LIVE on stock fixtures. ⚠ All three ARE GanttConfig keys (packages/spec/src/ui/view.zod.ts#GanttConfigSchema). QA run #21782 read `lockField` as no spec key; that is wrong, so the variants need no re-point. A2 and A3 are scored from the objectui pins `automated` names per clause. The one sub-leg no pin reaches is a DIRECT drag on a locked LEAF bar: GanttView.tsx returns from the drag start for a locked task, but only the summary-drag and link-drop locked cases are unit-pinned. It needs the view extended live, with lockField pointing at a boolean on showcase_task such as `done` (a view overlay or a scratch copy). That recipe is not run-proven, so score the sub-leg blocked(fixture) until a run proves it. Re-confirmed by QA run #23163 (objectstack 36b2889c, objectui 1fb1ced36243): the stock view still declares no lockField, and no pin reaches the direct leaf drag. The nearest pin, GanttView.tree.test.tsx ('omits drag/resize/progress handles and the dependency dot on a locked bar'), proves a locked leaf loses its resize grips and link dot, NOT that dragging its body writes nothing, so it does not score the sub-leg either", "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." ] }, @@ -1891,6 +1917,12 @@ "date": "2026-10-06", "change": "partial-coverage gap of QA run #21782 parked on #22017 (row 4). A2 / A3 were blocked(fixture): the stock view has no dependenciesField, and the record said lockField is not a GanttConfig key. That second premise is false: lockField, dependenciesField and parentField are all GanttConfigSchema keys (packages/spec/src/ui/view.zod.ts, also at the run's subject 316be321), so no clause is re-pointed. Disposition: pin. `automated` now names the objectui tests that prove A2 and A3 per clause, and the knownGap records the one unpinned sub-leg (a direct drag on a locked leaf) as blocked(fixture)", "ref": "#22017" + }, + { + "revision": 4, + "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" } ] }, @@ -1959,10 +1991,10 @@ }, { "id": "records-forms.form-dirty-guard", - "title": "Dirty forms guard navigation: discard prompts, save proceeds, nothing is lost silently", + "title": "Dirty forms guard ACCIDENTAL closes: the X / Escape / backdrop prompt on a dirty form, the footer Cancel discards without a prompt by design, save proceeds, nothing is lost silently", "since": "v15", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "browser", "personas": [ @@ -1976,7 +2008,7 @@ }, "steps": [ "open a record edit form (modal or drawer); change one field WITHOUT saving", - "attempt to close the dialog / navigate away; capture the guard prompt", + "attempt an ACCIDENTAL close — the X, Escape, or a backdrop click (the Radix close paths the guard intercepts) — and capture the guard prompt; a tab reload hits the browser-native beforeunload prompt instead. ⚠ The footer Cancel button is NOT a guarded path: it is an intentional discard and closes at once, dirty or not (ModalForm / DrawerForm handleCancel; pinned in discardGuard.test.tsx 'Cancel closes immediately even when dirty (intentional discard, no confirm)'). Press it once on a dirty form and record that it closes with no prompt and writes nothing", "choose stay/cancel — confirm the edit is still in the form", "choose discard — confirm the record is unchanged server-side and the form state is dropped", "repeat the edit and save normally — confirm persistence", @@ -1984,9 +2016,9 @@ ], "acceptance": [ { - "clause": "a dirty form intercepts close/navigation with the discard guard; a clean form closes without friction — both sides", + "clause": "a dirty form intercepts every ACCIDENTAL close (the X, Escape, a backdrop click) with the discard guard, and the footer Cancel discards a dirty form WITHOUT a prompt by design (an intentional exit); a clean form closes without friction on every path — both sides", "oracle": "screenshot", - "verify": "prompt appears exactly when dirty", + "verify": "the prompt appears exactly when the form is dirty AND the close is accidental (X / Escape / backdrop); the footer Cancel never prompts, and a re-read after it shows the row unchanged", "evidence": "both screenshots" }, { @@ -1998,12 +2030,12 @@ { "clause": "the guard's behavior is pinned by the existing unit suite", "oracle": "test", - "verify": "objectui: pnpm --filter @object-ui/plugin-form exec vitest run src/discardGuard.test.tsx", + "verify": "objectui, from the REPO ROOT of a checkout at the .objectui-sha revision: pnpm exec vitest run packages/plugin-form/src/discardGuard.test.tsx (QA run #23163 read 14/14 at 1fb1ced36243). ⛔ Not `pnpm --filter @object-ui/plugin-form exec vitest run src/discardGuard.test.tsx`: objectui's vitest invocation guard (scripts/vitest-invocation-guard.mjs) refuses a run whose cwd is inside a package, because the root config's project globs resolve against the cwd and such a run would silently execute the wrong suite (objectui#3378)", "evidence": "test output" } ], "negative": [ - "losing a dirty edit on close with NO prompt is the FAIL; equally, prompting on a pristine form is a paper-cut FAIL of the clean side" + "losing a dirty edit on an ACCIDENTAL close (X / Escape / backdrop) with NO prompt is the FAIL — the footer Cancel discarding without a prompt is the design, not this fail; equally, prompting on a pristine form is a paper-cut FAIL of the clean side" ], "traps": [ "automation-input" @@ -2013,7 +2045,7 @@ "ref": "objectui: packages/plugin-form/src/discardGuard.test.tsx" }, "source": [ - "objectui: packages/plugin-form/src/{ModalForm,DrawerForm}.tsx (beforeunload/guard wiring)", + "objectui: packages/plugin-form/src/{ModalForm,DrawerForm}.tsx (beforeunload/guard wiring; handleCancel — the footer Cancel is an intentional discard and closes with no prompt, while the guard rides the Radix onOpenChange close paths)", "dogfood-verification skill §4 (the beforeunload escape hatch exists precisely because this guard is real)" ], "history": [ @@ -2028,6 +2060,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-13", + "change": "acceptance[2].verify runs the pin from the objectui root (assertion defect, #23163 A2), and the title, steps[1], acceptance[0], negative[0] and source[0] separate the deliberate footer Cancel from the accidental closes (wording, #23163 A0); both parked on #23121, carried by #23169. The verify string `pnpm --filter @object-ui/plugin-form exec vitest run src/…` is refused by objectui's vitest invocation guard (objectui#3378); `pnpm exec vitest run packages/plugin-form/src/discardGuard.test.tsx` from the objectui root ran 14/14 at 1fb1ced36243 in QA run #23163. The footer Cancel closes a dirty form with no prompt by design (ModalForm handleCancel, pinned in discardGuard.test.tsx), and the guard covers the X, Escape and backdrop paths. Re-read from objectui source at that pin, still this repo's .objectui-sha", + "ref": "#23169" } ] }, @@ -2662,10 +2700,10 @@ }, { "id": "records-forms.saved-view-management", - "title": "Admin saved-view lifecycle: create kanban via dialog, rename/set-default/pin/delete each hit the meta overlay and survive reload; non-admin affordances absent and server-refused", + "title": "Admin saved-view lifecycle: create kanban via dialog (staged as a per-item draft, live only after Publish), rename/set-default/pin/delete each hit the meta overlay and survive reload; non-admin affordances absent and server-refused", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "browser", "personas": [ @@ -2690,8 +2728,8 @@ "delete" ], "steps": [ - "as admin, open showcase_task; open the Create View dialog; choose kanban; set groupByField = status; create — capture PUT /api/v1/meta/view/ (meta.saveItem) carrying {type:'kanban', kanban:{groupByField:'status'}}", - "reload; confirm the new kanban view is live and groups by status", + "as admin, open showcase_task; open the Create View dialog; choose kanban; set groupByField = status; create — capture PUT /api/v1/meta/view/?mode=draft (the console's create handler → createRuntimeMetadata → metadataClient.save(…, {mode:'draft'}): ADR-0034 stages every runtime-created view as an invisible per-item draft under the qualified name showcase_task.) carrying {type:'kanban', kanban:{groupByField:'status'}}; the console lands on view/?preview=draft", + "click Publish on the draft-preview bar — capture POST /api/v1/meta/view//publish (meta.publishItem); then reload WITHOUT ?preview=draft and confirm the new kanban view is live and groups by status. Before the publish, a plain (published-only) read does not serve it — the draft isolation itself is studio-authoring.view-authoring-live's to score", "rename the view via Manage Views; capture the PUT; reload confirms the new label", "set the view as default (unsets the prior default); capture the PUT(s); reload confirms the default moved", "pin the view; capture the PUT (isPinned); reload confirms it pinned", @@ -2701,15 +2739,15 @@ ], "acceptance": [ { - "clause": "creating a kanban view via CreateViewDialog persists a PUT /api/v1/meta/view/ carrying the type and its config (kanban.groupByField), and the view is LIVE after reload — a local repaint that never reached the overlay is not enough", + "clause": "creating a kanban view via CreateViewDialog stages a per-item DRAFT — PUT /api/v1/meta/view/?mode=draft carrying the type and its config (kanban.groupByField), landing on ?preview=draft — and the explicit Publish (POST /api/v1/meta/view//publish) makes it LIVE after a plain reload. A local repaint that never reached the overlay is not enough, and neither is a draft that was never published", "oracle": "network", - "verify": "captured PUT /api/v1/meta/view/ (data-objectstack createView → client.meta.saveItem('view', name, spec); framework route PUT /api/v1/meta/:type/:name, client meta.saveItem); reload re-reads it via listViews", - "evidence": "the PUT payload + post-reload view list" + "verify": "captured PUT /api/v1/meta/view/?mode=draft (objectui ObjectView create handler → runtime-metadata-persistence.ts createRuntimeMetadata, mode 'draft'; framework route PUT /api/v1/meta/:type/:name) + captured POST /api/v1/meta/view//publish (DraftPreviewBar → meta.publishItem; framework route POST /api/v1/meta/:type/:name/publish); a reload without ?preview=draft re-reads it via listViews", + "evidence": "the draft PUT payload + the publish POST + the post-reload view list" }, { "clause": "rename / set-default / pin / delete each issue their meta write and are live after reload; set-default UNSETS the prior default (updateView isDefault flips all others off)", "oracle": "network", - "verify": "one PUT per rename/pin/isDefault (updateView read-merge-write) and a DELETE /api/v1/meta/view/ for delete (deleteView → meta.deleteItem); reload reflects each", + "verify": "one PUT per rename/pin/isDefault (updateView read-merge-write) and a DELETE /api/v1/meta/view/ for delete (deleteView → meta.deleteItem); reload reflects each. On the PUBLISHED view (no draft pending) each write goes straight to the live overlay — a PUT with no ?mode=draft, and no publish step; while a draft is still pending, updateView writes that draft instead (data-objectstack updateView probes ?state=draft first)", "evidence": "the four traces + post-reload states" }, { @@ -2736,8 +2774,8 @@ ], "source": [ "objectui: packages/app-shell/src/views/CreateViewDialog.tsx (kanban groupByField), packages/plugin-view/src/ManageViewsDialog.tsx (rename/default/pin/delete affordances, read-only suppression)", - "objectui: packages/app-shell/src/views/ObjectView.tsx (updateView/deleteView wiring), packages/data-objectstack/src/index.ts (createView/updateView/deleteView → meta.saveItem/deleteItem)", - "framework: packages/rest/src/rest-route-ledger.ts (PUT/DELETE /api/v1/meta/:type/:name)", + "objectui: packages/app-shell/src/views/ObjectView.tsx (the create handler: a draft via createRuntimeMetadata, then the ?preview=draft landing; updateView/deleteView wiring), packages/app-shell/src/views/runtime-metadata-persistence.ts (createRuntimeMetadata: metadataClient.save with mode 'draft', ADR-0034), packages/data-objectstack/src/index.ts (updateView/deleteView → meta.saveItem/deleteItem, draft-first addressing)", + "framework: packages/rest/src/rest-route-ledger.ts (PUT/DELETE /api/v1/meta/:type/:name; POST /api/v1/meta/:type/:name/publish)", "PENDING-GAPS §C saved-view-management; cross-ref studio-authoring.view-authoring-live (distinct — this is the runtime end-user/admin lifecycle)" ], "history": [ @@ -2746,6 +2784,12 @@ "date": "2026-08-08", "change": "initial — deep-test the saved-view lifecycle (create-kanban/rename/default/pin/delete) against the meta overlay, with the non-admin both-sides gate", "ref": "claude/platform-test-checklist-ocwugl" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "steps[0] and [1], acceptance[0] (clause, verify, evidence), acceptance[1].verify, the title and source[1] and [2] re-pointed to the draft-first create (stale spec, #23163 A0; parked on #23121, carried by #23169). The Create View dialog stages the new view as a per-item draft (PUT …?mode=draft, ADR-0034) and lands on ?preview=draft; it goes live only after Publish (POST …/publish). Rename, default, pin and delete on the published view write the live overlay directly. QA run #23163 (objectstack 36b2889c, objectui 1fb1ced36243) drove create as draft-then-publish and passed every other leg. Re-read from objectui source at that pin, still this repo's .objectui-sha: ObjectView.tsx, runtime-metadata-persistence.ts createRuntimeMetadata, data-objectstack updateView", + "ref": "#23169" } ] }, @@ -2754,7 +2798,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": 2, + "revision": 3, "priority": "P2", "surface": "browser", "personas": [ @@ -2763,20 +2807,20 @@ "fixtures": { "app": "showcase", "requires": [ - "a list whose filtered result exceeds one page: showcase_contact filtered to Northwind's account has 26 rows (2 named + 24 'Prospect NN'), authored to exceed a page (examples/app-showcase/src/data/seed/index.ts, objectui#2711)", - "a bulk action registered on the object so the BulkActionBar and its aggregate/per-record dispatch are exercisable (objectui plugin-grid BulkActionBar)" + "a list whose match set exceeds one page AND whose view declares bulk actions: the showcase_task 'Bulk Actions' view (examples/app-showcase/src/ui/views/task.view.ts#bulk_actions — bulkActions showcase_mark_done + showcase_recalc_estimate, aggregate def showcase_recalc_selection). The stock seed has 10 tasks, under one page, so provision scratch tasks first: POST at least 55 rows to /api/v1/data/showcase_task, each with a title sharing one prefix (e.g. 'QA bulk 01'…) and a project (required), so a quick search isolates them; QA run #23163 used 55 against a first page of 50 rows. Delete them afterwards. ⚠ showcase_contact no longer serves this item: its views declare no bulkActions, and its 34 contacts — Northwind's 26 included — fit the list's first page; the Northwind 26 were authored to page the Account → Contacts RELATED list (objectui#2711), not a list view", + "the bulk actions above, so the BulkActionBar and both its aggregate and per-record dispatch are exercisable (objectui plugin-grid BulkActionBar)" ], "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 the showcase_contact list filtered so the match set exceeds one page (e.g. account = Northwind → 26 rows); screenshot; note page shows N rows, count bar shows server total M (M > N)", + "open the showcase_task 'Bulk Actions' view (…/showcase_task/view/bulk_actions) with the scratch tasks in place and quick-search their shared prefix so the match set exceeds one page (e.g. 'QA bulk' → 55 rows); screenshot; note page shows N rows, count bar shows server total M (M > N)", "select the whole visible page; the cross-page banner appears offering 'Select all M matching'", "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 filtered API count: GET /api/v1/data/showcase_contact?$filter=...&$top=0 (or the response total)", - "confirm an AGGREGATE action carries every matching id in params._selectedIds; a per-record action fans out one dispatch per matching id", + "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", "click clear; confirm both selection sources reset (the toolbar empties AND the row checkboxes clear)" ], "acceptance": [ @@ -2789,7 +2833,7 @@ { "clause": "select-all-matching resolves EVERY matching id across pages: the dispatched set equals a direct filtered API count, not just the visible page (paged fan-out with $top/$skip dropped, capped at 5000)", "oracle": "api", - "verify": "count the ids handed to the executor (resolveBulkRows collected set) vs GET /api/v1/data/showcase_contact filtered total; they match (up to the HARD_CAP)", + "verify": "count the ids handed to the executor (resolveBulkRows collected set) vs the GET /api/v1/data/showcase_task total for the same search; they match (up to the HARD_CAP)", "evidence": "the collected-id count + the filtered API count" }, { @@ -2826,7 +2870,7 @@ }, "source": [ "objectui: packages/plugin-grid/src/ObjectGrid.tsx (selectAllMatching, resolveBulkRows paged fan-out + HARD_CAP, resetSelection #3056, totalMatching), packages/plugin-grid/src/components/BulkActionBar.tsx (cross-page banner)", - "examples/app-showcase/src/data/seed/index.ts#Northwind (Northwind 26-contact fixture, objectui#2711)", + "examples/app-showcase/src/ui/views/task.view.ts#bulk_actions (the 'Bulk Actions' view: bulkActions + the aggregate bulkActionDefs entry — this item's fixture since #23169; the stock 10 tasks need the scratch rows to exceed a page) — not examples/app-showcase/src/data/seed/index.ts's Northwind 26, which page the Account → Contacts related list (objectui#2711)", "PENDING-GAPS §C bulk-select-all-matching; cross-ref records-forms.list-view-capabilities (bulk-actions, page-local)" ], "history": [ @@ -2841,6 +2885,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-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" } ] }, @@ -2938,7 +2988,7 @@ "title": "Record edit undo: toast Undo / Ctrl+Z restores the prior value through the API and logs a revert; undo after a concurrent change must not silently clobber", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "browser", "personas": [ @@ -2948,14 +2998,15 @@ "fixtures": { "app": "showcase", "requires": [ - "a writable record and an undoable edit path (an inline edit or an action flagged undoable) — objectui react/src/hooks/useGlobalUndo.ts + RecordDetailView toast Undo action; core globalUndoManager" + "a writable record and an action that declares `undoable: true` with operation 'update' (a patch of one field) — at objectui 1fb1ced36243 the ONLY path that pushes an Undo op: useConsoleActionRuntime and RecordDetailView attach the toast Undo for `undoable` actions, while inline grid and form edits push none. objectui react/src/hooks/useGlobalUndo.ts + core globalUndoManager hold the stack. ⚠ The stock showcase declares NO undoable action (examples/app-showcase has zero `undoable` keys), and a runtime-authored action is not joined into the served object's actions[] (records-forms.action-location-matrix knownGaps), so the action must ship PACKAGED in the showcase — see `blocked`" ], "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." + "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.", + "BLOCKED(fixture) on stock, all four clauses (QA run #23163, objectstack 36b2889c, objectui 1fb1ced36243). Inline edits push no Undo op at this pin — only actions declaring `undoable` do — and the showcase declares none, while a runtime-authored undoable action is not served. The item becomes runnable when examples/app-showcase ships a packaged undoable update action on a writable object (operation 'update' with a patch, so the Undo captures the prior value of every written field — ActionSchema.undoable in packages/spec/src/ui/action.zod.ts). Lift `blocked` in the revision that names that fixture. ⛔ Do not score an inline edit's missing Undo as this item's FAIL: the console never offers one by design" ] }, "steps": [ - "open a record; make an undoable edit to one field; confirm the success toast shows an Undo button", + "open a record; run the packaged undoable action (fixtures.requires[0]) on it; confirm the success toast shows an Undo button. An inline edit offers none by design", "click Undo (or press Ctrl+Z); capture the resulting update; re-read the record over the API and confirm the prior value is restored", "open the History/audit and confirm a revert entry was added (a forward write, not a silent rollback)", "press Ctrl+Shift+Z (redo); confirm the edited value is restored", @@ -2997,6 +3048,7 @@ "hydration-race", "shared-browser-tab" ], + "blocked": { "by": "fixture", "ref": "fixture gap: no packaged `undoable` update action in examples/app-showcase (QA run #23163 A0-A3; recorded on #23169)" }, "automated": { "kind": "unit", "ref": "objectui: packages/core/src/actions/__tests__/UndoManager.test.ts (undo/redo stack)" @@ -3017,6 +3069,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-13", + "change": "fixtures.requires[0], knownGaps[1] (new), steps[0] and a `blocked` annotation (fixture gap, #23163 A0-A3; parked on #23121, carried by #23169). At objectui 1fb1ced36243 only actions declaring `undoable` push an Undo op (useConsoleActionRuntime, RecordDetailView); inline edits push none. The showcase declares no undoable action, and a runtime-authored one is not served, so QA run #23163 scored all four clauses blocked(fixture). The item stays blocked until the showcase ships a packaged undoable update action (operation update + patch). That fixture is product work outside this checklist edit; `blocked.ref` names the gap so the selector excludes the item from default runs until then. No clause changed", + "ref": "#23169" } ] }, @@ -3121,7 +3179,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": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": [ @@ -3171,7 +3229,7 @@ "over-budget warn — FIRES: PATCH /api/v1/data/showcase_project/ {spent:650000} (budget 600000 → over budget, but ≤ 720000 so the spent_within_budget rule still permits the write); settle; read the log for 'project over budget: Data Platform (650000 / 600000)'", "over-budget warn — condition gate (under budget): PATCH 'Website Relaunch' {spent:70000} (still under its 150000 budget); settle; confirm NO warn line (record.spent > record.budget is false) — the same partial write proves `record` merges the stored budget it never sent", "abort rollback (scratch): register a scratch beforeInsert hook whose body throws, onError:'abort', on a scratch object (or showcase_task); POST a row; capture the refusal and confirm a filtered GET count stays 0 (the write rolled back, no orphan) — cross-check the dogfood hef_ref_guard beforeDelete pin", - "onError:'log' tolerate (scratch): register a scratch async afterUpdate hook whose body throws, onError:'log'; trigger it with a real update; confirm the triggering write STILL landed (re-read shows the change) and the server logged '[hook] async handler error (fire-and-forget)' — a suppressed failure, not a rollback", + "onError:'log' tolerate (scratch): register a scratch async afterUpdate hook whose body throws, onError:'log'; trigger it with a real update; confirm the triggering write STILL landed (re-read shows the change) and the server logged ERROR '[hook] handler failed (onError=log; suppressing)' — a suppressed failure, not a rollback. With onError:'log' the error policy itself logs and returns (packages/objectql/src/hook-wrappers.ts runWithErrorPolicy), so the fire-and-forget catch's '[hook] async handler error (fire-and-forget)' line never fires; that line belongs to an async hook WITHOUT onError:'log'", "priority ordering (scratch): register two scratch hooks on ONE object+event with priorities 10 and 90, each logging its own name; issue one write; confirm the log order is the priority-10 hook THEN the priority-90 hook (lower runs first)" ], "acceptance": [ @@ -3220,7 +3278,7 @@ { "clause": "onError:'log' on a THROWING async afterUpdate hook does NOT roll the triggering write back: the PATCH persists (re-read shows the change) and the failure is only logged — a fire-and-forget failure can never un-commit a write the engine already resolved", "oracle": "api", - "verify": "scratch throwing async hook; the PATCH's re-read shows the new value AND the server log carries '[hook] async handler error (fire-and-forget)'", + "verify": "scratch throwing async hook; the PATCH's re-read shows the new value AND the server log carries ERROR '[hook] handler failed (onError=log; suppressing)' with the hook's name (hook-wrappers.ts runWithErrorPolicy — not the fire-and-forget catch's '[hook] async handler error (fire-and-forget)', which an onError:'log' hook never reaches)", "evidence": "the persisted re-read + the suppressed-error log line" }, { @@ -3282,6 +3340,12 @@ "date": "2026-10-03", "change": "fixture roster only, no clause moved — the showcase gained a fifth hook, showcase_guard_task_reopen (beforeUpdate on showcase_task, throws a user-facing refusal), the fixture records-forms.script-action-hook-refusal-toast drives. Two knownGaps stated facts it made false ('no two showcase hooks share an object+event', 'the fixtures never throw'); both now name it and say why it does not close this item's ordering or beforeInsert abort-rollback clause.", "ref": "#21596" + }, + { + "revision": 4, + "date": "2026-10-13", + "change": "the onError:'log' step and acceptance[7].verify quote the log line the product writes (wording drift, #23166 A7; parked on #23121, carried by #23169). For an async afterUpdate hook declared onError:'log', hook-wrappers.ts runWithErrorPolicy catches the throw, logs ERROR '[hook] handler failed (onError=log; suppressing)' and returns. The '[hook] async handler error (fire-and-forget)' line the item quoted is the fire-and-forget catch, which only an async hook without onError:'log' reaches. QA run #23166 read the first line. Read from source at 6d7f4df3c. The clause's verdict logic is unchanged", + "ref": "#23169" } ] }, @@ -3642,10 +3706,10 @@ }, { "id": "records-forms.field-group-visible-when", - "title": "fieldGroups[].visibleWhen section predicate: both spellings accepted, the parse normalizes to the envelope, retired visibleOn refused with the located prescription, derivation carries it verbatim — the console form gates the section; the detail page still drops the key at the pinned console (objectui#11630)", + "title": "fieldGroups[].visibleWhen section predicate: both spellings accepted, the parse normalizes to the envelope, retired visibleOn refused with the located prescription, derivation carries it verbatim — the console gates the section on the form AND the detail page (objectui#11630 fixed)", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -3659,7 +3723,7 @@ ], "knownGaps": [ "no stock showcase object carries fieldGroups[].visibleWhen (grepped: contact and semantic-zoo declare groups, no predicates) — the steps author a scratch qa_group_gate object inline, reusing the qa-feeds-disabled recipe's package-install + PUT pattern. Single consumer today, so deliberately NOT a named area recipe; promote it to one if a second item ever needs the carrier", - "the console leg at objectui 9dfaca6543: the FORM adapter copies the key (plugin-form fieldGroups.ts passes visibleWhen onto the derived section), so the form gates live; the DETAIL adapter still drops it (plugin-detail buildDefaultPageSchema.ts deriveFieldGroupDetailSections maps key / label / icon / description / collapse / columns / fields only), so the detail page renders the section ungated — a product defect filed as objectui#11630, not a checklist gap. Confirm what the RUNNING console build does before scoring; if its bundle predates even the FormSection section-gating machinery (#6111/#6236/#6237), record blocked(fixture) rather than a defect" + "the console leg at objectui 1fb1ced36243: BOTH adapters carry the predicate. The FORM adapter copies the key (plugin-form fieldGroups.ts passes visibleWhen onto the derived section). The DETAIL page writes each declared group as the spec's `{ group }` reference (plugin-detail buildDefaultPageSchema.ts resolveDetailSections), and record:details resolves it and evaluates the group's visibleWhen per record (renderers/record-details.tsx; pin record-details.groupVisibleWhen-11630.test.tsx) — objectui#11630 is fixed. Confirm what the RUNNING console build does before scoring; if its bundle predates even the FormSection section-gating machinery (#6111/#6236/#6237), record blocked(fixture) rather than a defect" ] }, "steps": [ @@ -3691,9 +3755,9 @@ "evidence": "test run output" }, { - "clause": "the console renders the derived section GATED — the whole 'Pro details' section, header included, absent while kind=basic and appearing LIVE when kind flips to pro, on form and detail alike. At objectui 9dfaca6543 the FORM half holds (plugin-form fieldGroups.ts copies visibleWhen onto the derived section); the DETAIL half does not — plugin-detail buildDefaultPageSchema.ts deriveFieldGroupDetailSections drops the key, so the detail page shows the section unconditionally: score that half FAIL and cite objectui#11630 (do not re-file). Read the adapters at the running build's revision, or observe the gating itself, before scoring either half", + "clause": "the console renders the derived section GATED — the whole 'Pro details' section, header included, absent while kind=basic and appearing LIVE when kind flips to pro, on form and detail alike. At objectui 1fb1ced36243 both halves hold: plugin-form fieldGroups.ts copies visibleWhen onto the derived form section, and the detail page references the group (`{ group }`) so record:details evaluates its visibleWhen per record (objectui#11630, fixed). A section rendered ungated on EITHER page is now a FAIL — a regression, not the known defect. Read the adapters at the running build's revision, or observe the gating itself, before scoring either half", "oracle": "dom", - "verify": "screenshots of both kind states on form and detail, after settle; plus the console revision consulted (stale-console-bundle discipline) — on the form the section gates live; on detail an unconditionally-rendered section is the objectui#11630 defect at this pin, recorded FAIL with that reference", + "verify": "screenshots of both kind states on form and detail, after settle; plus the console revision consulted (stale-console-bundle discipline) — on both pages the section gates live. objectui pins the detail half in packages/plugin-detail/src/renderers/__tests__/record-details.groupVisibleWhen-11630.test.tsx ('record:details evaluates a group's visibleWhen exactly as the form does'); run it from an objectui checkout at the .objectui-sha revision to cite it beside the live read", "evidence": "the four screenshots + the console revision/adapter reading the verdict rests on" }, { @@ -3717,7 +3781,7 @@ "source": [ "packages/spec/src/data/object.zod.ts#visibleOn (visibleOn tombstone prescription), (visibleWhen, CEL via ExpressionInputSchema — #13030, commit 53dc739, Option-A maintainer ruling 2026-08-28)", "packages/spec/src/data/field-group-layout.ts (verbatim passthrough; evaluation ruled the renderer's), field-group-layout.test.ts+ and object-strictness-batch20.test.ts (the pins)", - "objectui: packages/plugin-form/src/fieldGroups.ts (copies visibleWhen onto the derived form section at 9dfaca6543) and packages/plugin-detail/src/synth/buildDefaultPageSchema.ts (deriveFieldGroupDetailSections still drops it — objectui#11630, clause 4's detail half); packages/components/src/renderers/form/form.tsx (#6236 section grouping contract) and packages/plugin-form/src/sectionPredicateDiagnostic.ts (wizard-arm design boundary) — the gating machinery derived sections plug into", + "objectui: packages/plugin-form/src/fieldGroups.ts (copies visibleWhen onto the derived form section) and packages/plugin-detail/src/synth/buildDefaultPageSchema.ts (resolveDetailSections writes `{ group }` references, so record-details.tsx evaluates the group's visibleWhen per record — objectui#11630, fixed at 1fb1ced36243; pin renderers/__tests__/record-details.groupVisibleWhen-11630.test.tsx); packages/components/src/renderers/form/form.tsx (#6236 section grouping contract) and packages/plugin-form/src/sectionPredicateDiagnostic.ts (wizard-arm design boundary) — the gating machinery derived sections plug into", "examples/app-showcase/src/data/objects/contact.object.ts#fieldGroups, semantic-zoo.object.ts (stock fieldGroups declarations, none with a predicate)", "cross-ref: field-level FormField.visibleWhen is form-view-gallery's; view-section FormSection.visibleWhen is NOT this slot — this item is the object-level fieldGroups predicate #13030 re-introduced" ], @@ -3733,6 +3797,12 @@ "date": "2026-10-05", "change": "clause 1, step 3, the console knownGap, clause 4, the title and the adapter source re-pointed (assertion defect, #21784 VF4 + FE12). The spec normalizes a bare string to {dialect:'cel',source} (shared/expression.zod.ts ExpressionInputSchema, pinned by object-strictness-batch20.test.ts), but runtime-authored metadata is persisted verbatim and served unparsed (metadata-protocol, ADR-0005 appendix (c)). So the GET reads back what was PUT, and the verifier's wording is used for clause 1. The console text is out of date. It measures differently from FE12, which says both adapters copy the key: at objectui 9dfaca6543, and at the run's 2e818d0b51ec, the FORM adapter (fieldGroups.ts) copies visibleWhen, while the DETAIL adapter (deriveFieldGroupDetailSections) still drops it. That matches the detail page still failing, a product defect at objectui#11630. Clause 4 now scores the form half live and the detail half against that issue", "ref": "#21797" + }, + { + "revision": 3, + "date": "2026-10-13", + "change": "acceptance[3] (clause and verify), knownGaps[1], the title and source[2] re-pointed: the detail half is no longer expected to fail (stale spec, #23166 A3; parked on #23121, carried by #23169). At objectui 1fb1ced36243 the detail page writes each field group as a `{ group }` reference (buildDefaultPageSchema.ts resolveDetailSections), and record:details evaluates the group's visibleWhen per record, pinned by record-details.groupVisibleWhen-11630.test.tsx. QA run #23166 (objectstack 36b2889c) saw the group gate live on the form and on the detail page. Re-read from objectui source at that pin, still this repo's .objectui-sha", + "ref": "#23169" } ] }, diff --git a/docs/qa/platform-checklist/areas/search.json b/docs/qa/platform-checklist/areas/search.json index f3c3120cc97..67ca2138212 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).", + "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).", "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", @@ -44,8 +44,8 @@ }, { "step": 5, - "call": "give the member a row of their OWN: as the member, POST /api/v1/data/showcase_invoice (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. 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).", + "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).", "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" } ], @@ -53,7 +53,8 @@ "knownGaps": [ "⚠️ This recipe has consumers in TWO areas, and the validator resolves both (#10593, maintainer ruling 2026-08-22 — option A, area-qualified references). `provisioning.use` accepts either spelling: a bare key naming a recipe of the item's own area (search.rls-both-personas uses `qa-contributor-bound-member`), or `:` naming one another area owns — records-forms.crud-roundtrip clause 7 needs this same persona and opts in from records-forms.json as `search:qa-contributor-bound-member`. Both FAIL check:platform-checklist loudly if this recipe is renamed, moved, or removed, naming the item and what resolved to nothing; the cross-area pointer used to be knownGaps prose the tooling could not see, which is the drift the qualified spelling closes. ⛔ Replay it from here; do not fork a second copy into another area — this block is the one home, and that is what the qualifier buys.", "The persona is a subset reader of showcase_invoice ONLY. showcase_account is public_read_write and showcase_contributor grants plain allowRead on it with no RLS, so a contributor still reads every account row — an item needing an invisible SOURCE row must drive showcase_invoice, not showcase_account.", - "showcase_private_note is an EMPTY table for everyone and showcase_account reads identically (15 rows) for admin, Mei and Ada (#10236) — neither is usable as a both-sides RLS control. Recorded so the next runner does not reach for them." + "showcase_private_note is an EMPTY table for everyone and showcase_account reads identically (15 rows) for admin, Mei and Ada (#10236) — neither is usable as a both-sides RLS control. Recorded so the next runner does not reach for them.", + "The own-row rule of step 5 is not invoice-specific: the same position's task_own_rows narrows showcase_task SELECT to assignee == current_user.email, so a contributor's OWN task must be created with `assignee` set to the member's email, or it too is invisible to its creator (QA run #23166). An item that gives this persona a task row applies the same discipline" ] } }, @@ -464,7 +465,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": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": [ @@ -476,7 +477,7 @@ "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 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 exactly as in search.rls-both-personas" + "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)" ], "knownGaps": [ "the /_console bundle is vendored and may be stale — verify the palette/page behavior against current objectui app-shell or a fresh build (stale-console-bundle, RUNNER §2)", @@ -490,7 +491,7 @@ "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", "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", - "from the palette's 'Open full search' item (navigates to /search), or directly at /apps//search?q=, repeat the query on the SearchResultsPage and confirm record hits grouped by object plus nav matches; also run a no-hit query and confirm the designed empty state", + "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)", "RLS parity: as the restricted member, run GET /api/v1/search?q=INV-1003 (the row their RLS hides) and the same in the palette; then as admin run the identical query — capture both persona responses", "capture the browser console for the whole session" ], @@ -567,7 +568,8 @@ "history": [ { "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": 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" } ] }, { @@ -575,7 +577,7 @@ "title": "The ⌘K palette is a NAVIGATOR beyond record search: the active app's nav corpus renders grouped (Objects/Dashboards/Pages/Reports), Enter navigates each type, app-switch and theme commands dispatch for real, and visibility-gated / inactive entries stay absent", "since": "v15", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "browser", "personas": ["seeded admin (admin@objectos.ai)"], @@ -587,7 +589,7 @@ "the objectui console app-shell (CommandPalette + CommandPaletteProvider)" ], "knownGaps": [ - "NO showcase nav item declares a visibility predicate (apps/index.ts), so the gate-hidden clause's HIDDEN side has no stock fixture. App metadata is code-authored and outside the runtime-write overlay set (view/dashboard/report/translation/email_template, RUNNER.md). FIXTURE RECIPE, the smallest one (authored from source for QA run #21782's gap, parked on #22017 row 5; not yet run-proven): in a scratch copy of examples/app-showcase under /tmp, never in the checkout, append two leaves to the showcase app's grp_data children in src/ui/apps/index.ts: { id: 'qa_nav_gate_false', type: 'object', objectName: 'showcase_task', label: 'QA Gate False', visible: 'false' } and the same with id 'qa_nav_gate_true', label 'QA Gate True' and visible: 'true'. Then boot that copy (os dev --fresh -p PORT). The key is `visible` ONLY: a nav item's `visible` takes CEL (EvaluatedExpressionInputSchema; a bare string is a CEL source), while `visibleOn` / `visibleWhen` are refused at authoring as near-misses of `visible` (packages/spec/src/ui/app.zod.ts#NAV_ITEM_ALIASES). Without the copy, score the hidden side blocked(fixture) and record it", + "NO showcase nav item declares a visibility predicate (apps/index.ts), so the gate-hidden clause's HIDDEN side has no stock fixture. App metadata is code-authored and outside the runtime-write overlay set (view/dashboard/report/translation/email_template, RUNNER.md). FIXTURE RECIPE, the smallest one (authored from source for QA run #21782's gap, parked on #22017 row 5; RUN-PROVEN by QA run #23163 at objectstack 36b2889c + objectui 1fb1ced36243, where the 'QA Gate True' leaf appeared and 'QA Gate False' appeared in no palette group): in a scratch copy of examples/app-showcase under /tmp, never in the checkout, append two leaves to the showcase app's grp_data children in src/ui/apps/index.ts: { id: 'qa_nav_gate_false', type: 'object', objectName: 'showcase_task', label: 'QA Gate False', visible: 'false' } and the same with id 'qa_nav_gate_true', label 'QA Gate True' and visible: 'true'. Then boot that copy (os dev --fresh -p PORT). The key is `visible` ONLY: a nav item's `visible` takes CEL (EvaluatedExpressionInputSchema; a bare string is a CEL source), while `visibleOn` / `visibleWhen` are refused at authoring as near-misses of `visible` (packages/spec/src/ui/app.zod.ts#NAV_ITEM_ALIASES). Without the copy, score the hidden side blocked(fixture) and record it", "the /_console bundle is vendored and may be stale — verify palette behavior against current objectui app-shell or a fresh build (stale-console-bundle, RUNNER §2)" ] }, @@ -665,7 +667,8 @@ ], "history": [ { "revision": 1, "date": "2026-08-30", "change": "new item (sweep 2026-08-30, angle 1), authored as a SIBLING of search.console-global-search rather than an extension — writer's call per the register: that item's identity is the GET /api/v1/search record path (hits, grouping, RLS, recents, /search page) and already carries seven clauses, while the palette's other half — the client-side nav corpus (objects/dashboards/pages/reports groups from the active app's navigation), per-type Enter navigation, app switching, theme dispatch, and the evaluateVisibility gate — was wholly uncovered and is a different mechanism (app metadata + client filter, no server search). Grounded in CommandPalette.tsx; the gate-hidden side is blocked(fixture) on stock seeds because no showcase nav item declares a visibility expression and app metadata is not runtime-writable", "ref": "#sweep-2026-08-30" }, - { "revision": 2, "date": "2026-10-06", "change": "partial-coverage gap of QA run #21782 parked on #22017 (row 5). A5 was blocked(fixture): no stock nav item carries a visibility predicate. Disposition: fixture recipe. The knownGap now spells the smallest fixture: two nav leaves with visible 'false' and 'true' added in a scratch copy of the showcase, authored from source and not yet run-proven. A5 named `visibleOn`, which a nav item refuses as a near-miss of `visible` (NAV_ITEM_ALIASES in packages/spec/src/ui/app.zod.ts), so the clause names `visible` only; step 8 points at the recipe", "ref": "#22017" } + { "revision": 2, "date": "2026-10-06", "change": "partial-coverage gap of QA run #21782 parked on #22017 (row 5). A5 was blocked(fixture): no stock nav item carries a visibility predicate. Disposition: fixture recipe. The knownGap now spells the smallest fixture: two nav leaves with visible 'false' and 'true' added in a scratch copy of the showcase, authored from source and not yet run-proven. A5 named `visibleOn`, which a nav item refuses as a near-miss of `visible` (NAV_ITEM_ALIASES in packages/spec/src/ui/app.zod.ts), so the clause names `visible` only; step 8 points at the recipe", "ref": "#22017" }, + { "revision": 3, "date": "2026-10-13", "change": "knownGaps[0] records that the visibility-predicate fixture recipe is run-proven (knownGaps update, #23163 A4; parked on #23121, carried by #23169). QA run #23163 drove the scratch-copy recipe — two grp_data leaves with visible 'false' and 'true' — at objectstack 36b2889c with objectui 1fb1ced36243, and the palette honoured the predicate on both sides. The recipe text is unchanged; without the scratch copy the hidden side still scores blocked(fixture)", "ref": "#23169" } ] }, { diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index eb2182d5eac..0b6bfdea53e 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -95,7 +95,7 @@ "title": "Object designer round-trip: field add/edit/reorder persists to metadata, identifiers derive and survive typing", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": ["admin"], @@ -112,7 +112,7 @@ "open the object designer on the writable package: /_console/apps/com.objectstack.studio/metadata/object/new?package=", "TYPE the object name repair_asset character by character (never .fill — the F2 per-keystroke-sanitisation bug only reproduces when typed) and confirm the underscore survives", "add a Text field, set its label to 'Asset Code', blur — the API name input must derive asset_code (F3)", - "add a Picklist field plus one EMPTY option row; wait out the debounced live validation — no validation banner may appear for the empty row (F4); then author 3 real options", + "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/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", "GET /api/v1/meta/object/repair_asset again — the rename AND the new field order must be in the persisted metadata, not just the canvas", @@ -138,9 +138,9 @@ "evidence": "screenshot + input value read" }, { - "clause": "an empty picklist option row does not trip spec validation (no developer-facing 'System identifier must be at least 2 characters' banner), while the PUBLISHED picklist carries exactly the 3 authored options", + "clause": "an empty picklist option row does not trip spec validation (no developer-facing 'System identifier must be at least 2 characters' banner) — judged with the 3 real options already present, because a picklist holding only empty rows legitimately raises the select-needs-options banner — while the PUBLISHED picklist carries exactly the 3 authored options", "oracle": "api", - "verify": "no metadata-validation-banner after the debounce (F4's negative assertion), and the meta read shows 3 options — the empty row was dropped, not persisted", + "verify": "with the 3 real options present, no metadata-validation-banner after the debounce once the empty row is added (F4's negative assertion, read in that order), and the meta read shows 3 options — the empty row was dropped, not persisted. A select-needs-options banner shown BEFORE any real option exists is correct and is not this clause's FAIL", "evidence": "banner-absence screenshot + the meta read" } ], @@ -161,6 +161,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-13", + "change": "steps[3] and acceptance[3] (clause and verify) add the empty option row after the real options (assertion defect, #23166 A3; parked on #23121, carried by #23169). The empty rows are dropped, so a picklist with no real option yet has none, and the select-needs-options banner fires correctly; the literal 'no banner for the empty row' collided with it, as objectui F4's toHaveCount(0) does. QA run #23166 asserted the empty-row half with real options present and passed", + "ref": "#23169" } ] }, @@ -240,7 +246,7 @@ "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": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": ["admin (authors)", "end user (opens the record)"], @@ -252,7 +258,7 @@ ] }, "steps": [ - "open /_console/apps/showcase_app/metadata/page/new and fill the create form: Label 'Invoice Page ' (slugifies into Name), 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_invoice; 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", @@ -316,6 +322,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": 4, + "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" } ] }, @@ -324,7 +336,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": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": ["admin (authors drafts)", "end user (must not see drafts)"], @@ -344,8 +356,8 @@ "concurrency guard: re-PUT with the STALE version as If-Match and capture the 409", "package-wide door: stage two drafts in the writable package and POST /api/v1/packages//publish-drafts — both flip in one atomic release (the audit's 'Published all drafts in this package (one atomic release)' toast); capture the STATUS and the BODY separately, because the abort path answers 200 with data.success:false (see the clause below)", "author an INVALID draft (a widget carrying a stray legacy key) and attempt its publish — the author-time gate must reject the draft→active transition (#4463)", - "meta forensics (after the two publishes of qa_lifecycle_probe — revision 1, then the second revision): GET /api/v1/meta/dashboard/qa_lifecycle_probe/history — the durable sys_metadata_history events must list BOTH published revisions (a dashboard is an overlay type, so history is real; a non-overlay type answers { events: [] } by design)", - "GET /api/v1/meta/dashboard/qa_lifecycle_probe/diff?from=1&to=2 (or omit the params for previous-vs-current) — the structural diff must name the widget key that changed between the revisions, not dump the whole body", + "meta forensics (after the two publishes of qa_lifecycle_probe — revision 1, then the second revision): GET /api/v1/meta/dashboard/qa_lifecycle_probe/history — the durable sys_metadata_history events must list BOTH published revisions (a dashboard is an overlay type, so history is real; a non-overlay type answers { events: [] } by design). History counts draft saves AND publishes, so two save-draft-then-publish rounds read v1 (draft), v2 (its publish), v3 (draft), v4 (publish): read the two PUBLISH versions off this body for the diff step, steps[9] (QA run #23166 read v2 and v4)", + "GET /api/v1/meta/dashboard/qa_lifecycle_probe/diff?from=&to= — the two publish versions read off /history in steps[8] (v2 and v4 on a clean two-round run); the structural diff must name the widget key that changed between the revisions, not dump the whole body. ⛔ Not ?from=1&to=2: v1 → v2 is a draft save and its own publish, which differ in nothing (changed []), and a default previous-vs-current read can land on the same draft/publish pair", "POST /api/v1/meta/dashboard/qa_lifecycle_probe/rollback with { toVersion: 1 }, driven under an authenticated session so the actor is attributable — the authenticated identity is the ONLY source ([#7941]; resolveMetaWriteActor no longer consults `X-Actor` at all); the runtime GET must then serve revision 1's body again and the rendered dashboard must show revision 1's widget", "repeat the rollback (or another /meta write on this item, e.g. a follow-up draft save) while ALSO sending header `X-Actor: someone_else_entirely` on the authenticated request — the header must NOT change the recorded actor: the audit/history rows for that action still name the authenticated runner's own identity, never `someone_else_entirely` ([#7941] — pinned at the unit layer in packages/rest/src/meta-write-actor-identity.test.ts)", "attempt a rollback with a missing/invalid toVersion and capture the 400 INVALID_REQUEST guard", @@ -397,13 +409,13 @@ { "clause": "the durable history lists BOTH published revisions after two publishes — GET /meta/dashboard/qa_lifecycle_probe/history returns the sys_metadata_history events for revision 1 and revision 2 (real events because a dashboard is an overlay type; a non-overlay type returning { events: [] } is by design, not a miss)", "oracle": "api", - "verify": "the /history body carries two version entries with ascending seq/version — consulted on the REST route-manager server, not the dispatcher", + "verify": "the /history body carries the two PUBLISH events with ascending seq/version, among the draft-save events history also records (v1 draft, v2 publish, v3 draft, v4 publish on a clean two-round run) — consulted on the REST route-manager server, not the dispatcher", "evidence": "the history response" }, { - "clause": "diff names the changed key, not a whole-body dump — GET .../diff (from=1&to=2, or previous-vs-current) isolates the widget/dimension key that differs between the two revisions", + "clause": "diff names the changed key, not a whole-body dump — GET .../diff between the two PUBLISH versions read off /history (v2 → v4 on a clean run) isolates the widget/dimension key that differs between the two revisions", "oracle": "api", - "verify": "the diff body reports the changed path (the edited widget key), not the entire document", + "verify": "the diff body reports the changed path (the edited widget key), not the entire document; a diff of a draft save against its own publish (from=1&to=2) reports changed [] and scores nothing either way", "evidence": "the diff response" }, { @@ -447,7 +459,8 @@ { "revision": 1, "date": "2026-08-07", "change": "new item: both sides of the draft→publish gate (staged-not-served / flipped-on-publish), plus OCC 409, atomic package-wide publish, and the #4463 invalid-draft publish refusal — grounded in the spec receipt schema, the runtime publish-drafts handler, and the pinned dogfood roundtrip", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-08", "change": "clause-extension (routes hunter #12): meta forensics — GET /meta/:type/:name/{history,audit,diff} + POST .../rollback (two publishes → history lists both, diff names the changed key, rollback restores rev-1 and the live app serves it, audit rows carry the actor); traps gain dispatcher-vs-hono-route (the dispatcher /meta branch 404s /history)", "ref": "claude/platform-test-checklist-ocwugl" }, { "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": 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" } ] }, { @@ -1022,7 +1035,7 @@ "title": "Source-authored page tiers render by the RIGHT path: kind:'html' source is PARSED-never-executed (static, in-browser sdui-parser compile), kind:'react' source is EXECUTED behind its capability gate (live data + interactivity) — command-center-jsx / capability-map (html) vs crm-workbench (react)", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "mixed", "personas": ["admin (seeded admin@objectos.ai) driving the published showcase app as an end user"], @@ -1033,7 +1046,7 @@ "the react-pages capability ON — CAP_REACT_PAGES defaults ON; a boot with OS_PAGE_REACT=off flips it OFF (the both-sides gate check)" ], "knownGaps": [ - "NO manual build step is needed to VIEW either tier in the running showcase (contra the task's guess): a kind:'html' page compiles IN-BROWSER via @object-ui/sdui-parser compile(source, manifest) at render, against a whitelist manifest built from the LIVE ComponentRegistry (packages/components/src/renderers/layout/page.tsx); a kind:'react' page is transpiled IN-BROWSER by a lazily-loaded Sucrase @object-ui/react-runtime chunk. The only build-shaped risk is the vendored /_console bundle being stale (stale-console-bundle) — a stale sdui-parser/react-runtime mis-compiles a page that is fine on a fresh build.", + "NO manual build step is needed to VIEW either tier in the running showcase (contra the task's guess): a kind:'html' page compiles IN-BROWSER via @object-ui/sdui-parser compile(source, manifest) at render, against a whitelist manifest built from the LIVE ComponentRegistry (packages/components/src/renderers/layout/page.tsx); a kind:'react' page is transpiled IN-BROWSER by a lazily-loaded Sucrase @object-ui/react-runtime chunk. The only build-shaped risk is the vendored /_console bundle being stale (stale-console-bundle) — a stale sdui-parser/react-runtime mis-compiles a page that is fine on a fresh build. ⚠ A FAULTING html source never reaches the RENDERED page on a stock boot: the metadata save door compiles an html page's source against the deployment's manifest with the same compiler `os validate` uses (packages/metadata-protocol/src/runtime-authoring-gate.ts#HTML_PAGE_SOURCE_COMPILE), so an active save or a publish of a faulting html page answers 422 INVALID_METADATA carrying the compile diagnostics, and nothing is served. The 'HTML page failed to compile (N)' panel is therefore reachable only in the page designer's DRAFT preview, where a ?mode=draft save of the faulting source is admitted and rendered (QA run #23166)", "none of showcase_command_center_jsx / showcase_capability_map / showcase_crm_workbench is in objectui e2e/live/showcase-smoke.spec.ts's SURFACES sweep — there is no automated render-clean pin for the html/react tiers, so this item is browser-driven (not marked `automated`)." ] }, @@ -1045,7 +1058,7 @@ "prove the react tier is EXECUTED against LIVE data: the 'Total projects' KPI equals GET /api/v1/data/showcase_project's count (the page computes it via useAdapter().find('showcase_project'), reading the QueryResult .data envelope), and a showcase_project find query fires on the network — the number is computed at render, not authored", "prove react interactivity (executed handlers, not a parsed tree): click a ListView row → the ObjectForm binds THAT record (React useState); click '+ New project' → the form switches to create mode", "both-sides gate: on a boot with OS_PAGE_REACT=off, re-open crm-workbench — it must render the NAMED 'React pages are disabled on this deployment' notice (CapabilityDisabledNotice), never a blank and never an executed page; the html pages still render (html is not gated). If the env toggle cannot be flipped on the stock fixture, record this clause blocked(environment)", - "degradation: confirm an html compile error would surface the 'HTML page failed to compile (N)' panel with the diagnostic messages, and a react runtime error the 'React page error' fallback — neither a blank page (drive via the scratch draft door if a shipped page cannot be made to fault)" + "degradation: (a) html — save a faulting html source at the ACTIVE door (or publish one) and capture the 422 INVALID_METADATA with its compile diagnostics; then save the same source as a ?mode=draft draft and open the page designer's draft preview, where the 'HTML page failed to compile (N)' panel renders with the diagnostic messages — the save gate keeps a faulting html page off the rendered route, so the panel cannot be met there (knownGaps[0]); (b) react — a react runtime error shows the 'React page error' fallback on the rendered page. Neither may be a blank page" ], "acceptance": [ { @@ -1079,9 +1092,9 @@ "evidence": "the on-state + off-state screenshots (or the blocked-environment note)" }, { - "clause": "compile/runtime failures degrade NAMED, never blank: an html compile error is the 'HTML page failed to compile (N)' panel listing the diagnostics; a react error is the 'React page error' fallback carrying the message", + "clause": "compile/runtime failures degrade NAMED, never blank: a faulting html source is refused 422 INVALID_METADATA at the active save / publish door, naming the compile diagnostics, so it never renders live, and in the page designer's draft preview it shows the 'HTML page failed to compile (N)' panel listing them; a react error is the 'React page error' fallback carrying the message", "oracle": "screenshot", - "verify": "a faulted html source shows the compile-error panel; a faulted react source shows the error fallback — a blank page for either is the failure this clause exists for", + "verify": "the 422 body for the faulting html source's active save or publish, plus a draft-preview screenshot of the compile-error panel; a faulted react source shows the error fallback on the rendered page — a blank page, or a 2xx that serves the faulting html page, is the failure this clause exists for", "evidence": "the error-panel screenshot(s)" } ], @@ -1105,7 +1118,8 @@ "objectui: content/docs/guide/react-pages.md (react tier: transpiled + evaluated in-app, react-pages capability, OS_PAGE_REACT=off disable)" ], "history": [ - { "revision": 1, "date": "2026-08-08", "change": "new item: the html (parse-never-execute, in-browser sdui-parser compile, static) vs react (executed at render, capability-gated, live data + interactivity) source-page tiers — the objectui renderer-path split grounded in PageRenderer's kind dispatch + react-page.tsx's gate, distinguishing command-center-jsx/capability-map (html) from crm-workbench (react); records the honest build-step finding (none needed to view — in-browser compile/transpile) as a knownGap", "ref": "claude/platform-test-checklist-ocwugl" } + { "revision": 1, "date": "2026-08-08", "change": "new item: the html (parse-never-execute, in-browser sdui-parser compile, static) vs react (executed at render, capability-gated, live data + interactivity) source-page tiers — the objectui renderer-path split grounded in PageRenderer's kind dispatch + react-page.tsx's gate, distinguishing command-center-jsx/capability-map (html) from crm-workbench (react); records the honest build-step finding (none needed to view — in-browser compile/transpile) as a knownGap", "ref": "claude/platform-test-checklist-ocwugl" }, + { "revision": 2, "date": "2026-10-13", "change": "acceptance[5] (clause and verify), steps[7] and knownGaps[0] name where an html compile error is observable (assertion defect, minor, #23166 A5; parked on #23121, carried by #23169). The save door compiles an html page's source against the deployment's manifest (runtime-authoring-gate.ts HTML_PAGE_SOURCE_COMPILE), so a faulting html page is refused 422 INVALID_METADATA at save / publish and never publishes, and the compile-error panel is reachable only in the designer's draft preview. QA run #23166 met the 422 and the draft-preview panel. The react half is unchanged", "ref": "#23169" } ] }, { @@ -1113,7 +1127,7 @@ "title": "Studio Automations pillar on a read-only package: a packaged flow is NOT editable in place — New-flow and autosave honor the courtesy gate, the enable/disable switch produces no metadata write, and inspector edits are inert or announced, never silently discarded", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "browser", "personas": ["admin (opens Studio on the read-only package)"], @@ -1124,8 +1138,8 @@ "a vendored console matching .objectui-sha — the pillar markup under test is objectui's, so a stale bundle tests yesterday's gate" ], "knownGaps": [ - "CURRENT SHAPE, RECORDED SO RUNS DON'T RE-DERIVE IT (objectui f7c52e2): the AutomationsPillar's readOnly prop is documented as 'hide/disable flow-authoring affordances' (StudioDesignSurface.tsx) and the New-flow button and autosave consult it — but the enable/disable switch does NOT (disabled={!isEditable || !!saving}) and the flow node inspector mounts with readOnly hardcoded false, where the Data pillar threads the real flag into its editors. So today the switch is clickable-but-server-refused and inspector edits take locally and silently vanish on reload. The clauses below keep the PROMISE as the assertion and name this shape as the expected finding", - "no automated pin exists in either repo for the Automations pillar's read-only posture (checked at objectui f7c52e2: no StudioDesignSurface read-only test covers the switch or the flow inspector) — this item is browser-driven end to end" + "FIXED at objectui 1fb1ced36243 (objectui#11124), recorded so runs don't re-derive it: on a read-only package the Automations pillar's enable/disable switch is disabled (disabled={!isEditable || !!saving || readOnly}, its title the read-only hint) and toggleEnabled is guarded as well, so a forced click saves nothing; the flow node inspector receives the pillar's REAL readOnly flag, threaded as the Data pillar threads it, so its inputs are disabled; New-flow and autosave still honour the gate (StudioDesignSurface.tsx). QA run #23163 measured exactly that: the switch disabled with a reason title, a forced click sending nothing, the inspector inputs disabled, the flow byte-identical. The pre-fix shape — a clickable switch only the server's 422 refused, and inspector edits that took locally and vanished on reload — is gone; meeting it again is a regression FAIL, not a standing finding", + "objectui now pins the read-only posture: packages/app-shell/src/views/studio-design/StudioDesignSurface.automationsReadOnly-11124.test.tsx ('disables the header \"Enabled\" switch, and a click on it saves nothing'; 'opens the flow inspector read-only: the node inputs are disabled'; no canvas edit handles). It lives in the objectui repo only, so from this checkout the item stays browser-driven end to end; run the pin from an objectui checkout at the .objectui-sha revision to cite it beside the live read" ] }, "steps": [ @@ -1133,7 +1147,7 @@ "pin the precondition: GET /api/v1/packages must report com.example.showcase writable:false — the pillar's readOnly gate is pkgWritable===false (objectui StudioDesignSurface.tsx) and an UNKNOWN writability (fetch failed / still loading) deliberately leaves the pillar ungated, so a race here fakes a gate failure (hydration-race)", "baseline the server truth: GET /api/v1/meta/flow/showcase_urgent_task_alert (full body) — every later clause diffs against this read", "affordance sweep with the network panel recording: confirm the New-flow button is ABSENT from the rail heading; select the packaged flow and wait past the autosave debounce after touching the canvas — no flow save may leave the browser", - "click the enable/disable switch in the pillar header and capture everything: whether it is disabled/absent, and if clickable, the exact request it fires (today: a flow DRAFT save flipping status active↔obsolete — StudioDesignSurface.tsx), the server's response, and what the UI surfaces (error banner)", + "click the enable/disable switch in the pillar header and capture everything: whether it is disabled/absent (at objectui 1fb1ced36243 it is disabled, titled with the read-only hint), whether a forced click fires any request (it must not — were it to, the request would be a flow DRAFT save flipping status active↔obsolete, StudioDesignSurface.tsx toggleEnabled), and what the UI surfaces", "select a flow node and edit a property in the inspector; record whether the edit visibly takes on the canvas/draft, and whether any affordance announces a read-only or will-not-persist state", "reload the pillar (re-open the package) and re-inspect: record whether the inspector edit survived or vanished, and whether its discard was ever announced", "close out: GET /api/v1/meta/flow/showcase_urgent_task_alert again and diff against the baseline — byte-identical" @@ -1154,13 +1168,13 @@ { "clause": "the enable/disable switch honors the read-only package lock exactly as the New-flow button does: clicking it produces NO metadata write", "oracle": "network", - "verify": "click the switch: the PASS shape is disabled/absent-or-inert (no request fires). CURRENT RECORDED FINDING (objectui f7c52e2): the switch is clickable — its disabled expression omits readOnly (StudioDesignSurface.tsx) — and toggleEnabled fires a flow draft save flipping deployment status that the server refuses 422 writable_package_required, surfaced in the pillar error banner. That clickable-but-refused shape is this clause's expected FAIL with the server gate holding: record it as the standing finding, not a pass and not a new defect; only a 2xx on that write escalates to clause 2's hard FAIL", - "evidence": "the click's network capture (no request — or the refused request + the error-banner screenshot)" + "verify": "click the switch: the PASS shape is disabled/absent-or-inert (no request fires). At objectui 1fb1ced36243 the switch is disabled with the read-only hint as its title and toggleEnabled is guarded (objectui#11124), so even a forced click fires nothing — the expected-fail this clause carried (a clickable switch the server refused 422 writable_package_required) is fixed. A switch that fires a request is now a FAIL of this clause; a 2xx on that write escalates to clause 2's hard FAIL", + "evidence": "the click's network capture (no request) + the disabled switch's DOM state and title" }, { "clause": "inspector edits are inert OR their discard is announced: the flow node inspector on a read-only package either disables its inputs or tells the author the edit will not persist — an edit that visibly takes and silently vanishes on reload is the failure", "oracle": "dom", - "verify": "edit a node property, confirm what rendered, reload the pillar and re-read the inspector + canvas: the edit must not silently vanish. CURRENT SOURCE-PREDICTED SHAPE (objectui f7c52e2): the inspector mounts with readOnly hardcoded false (StudioDesignSurface.tsx — contrast the Data pillar threading the real flag) while autosave stays blocked, so the edit takes locally and vanishes on reload unannounced — the predicted verdict is this clause's FAIL; capture it as the finding", + "verify": "edit attempt on a node property, then reload the pillar and re-read the inspector + canvas: the edit must not silently vanish. At objectui 1fb1ced36243 the inspector receives the pillar's real readOnly flag (objectui#11124), so its inputs are DISABLED — the inert half of the clause, which is the PASS shape; the expected-fail this clause carried (inputs editable, the edit taking locally and vanishing on reload) is fixed. An input that accepts the edit is now a FAIL", "evidence": "before-edit / after-edit / after-reload screenshots + the inspector input DOM state (disabled or not)" } ], @@ -1171,14 +1185,15 @@ ], "traps": ["stale-console-bundle", "hydration-race", "automation-input"], "source": [ - "objectui packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (readOnly = pkgWritable===false; unknown writability deliberately leaves the pillar ungated), (the prop's own contract: 'hide/disable flow-authoring affordances'), (autosave blocked on readOnly), (the enable/disable switch — disabled={!isEditable || !!saving}, readOnly absent), (toggleEnabled = client.save flow draft flipping status active↔obsolete), (New-flow gated on !readOnly), (inspector readOnly hardcoded false; contrast Data pillar threading), (the rail lists package-scoped flows) — objectui owns this pillar's markup, so locator and gate fixes land there (ADR-0054)", + "objectui packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (readOnly = pkgWritable===false; unknown writability deliberately leaves the pillar ungated), (the prop's own contract: 'hide/disable flow-authoring affordances'), (autosave blocked on readOnly), (the enable/disable switch — disabled={!isEditable || !!saving || readOnly} and a guarded toggleEnabled since objectui#11124), (toggleEnabled = client.save flow draft flipping status active↔obsolete), (New-flow gated on !readOnly), (the inspector receives the real readOnly flag since objectui#11124, as the Data pillar's editors do), (the rail lists package-scoped flows) + StudioDesignSurface.automationsReadOnly-11124.test.tsx (the pin) — objectui owns this pillar's markup, so locator and gate fixes land there (ADR-0054)", "packages/metadata-protocol/src/sys-metadata-repository.ts + packages/metadata-protocol/src/sys-metadata-repository.package-writability.test.ts (the 422 [writable_package_required] refusal a read-only code package answers to metadata writes — the server half of the lock; com.example.showcase is source-loaded read-only)", "docs/adr/0126-packaged-metadata-customization-model.md §2/§3 (flow = Regime C: locked base, in-place edit refused loudly at the write door — card F2)", "#12438 (the ADR-0126 packaged flow/action sweep this item lands from)", "access-security.readonly-package-locks-studio (object designer + permission matrix — cross-referenced, not widened), access-security.packaged-flow-write-door-parity (the API-side door parity for the same packaged flow), automation.setup-packaged-automation-board (the ledgered enable/disable surface — a different mechanism from this pillar's status flip)" ], "history": [ - { "revision": 1, "date": "2026-08-26", "change": "new item (#12438 card F2, UI half): the Studio Automations pillar's read-only package lock — the promise (no in-place edit, switch produces no metadata write, inspector edits inert or announced) asserted with the current source shape recorded as the expected finding: the switch is clickable with only the server-side 422 behind it (disabled expression omits readOnly, StudioDesignSurface.tsx) and the inspector hardcodes readOnly:false so edits take locally and silently vanish on reload; New-flow and autosave honor the gate. Grounded in objectui f7c52e2 + the framework's writable_package_required door; owns the Automations pillar only, cross-referencing readonly-package-locks-studio for the surfaces it already owns", "ref": "#12438" } + { "revision": 1, "date": "2026-08-26", "change": "new item (#12438 card F2, UI half): the Studio Automations pillar's read-only package lock — the promise (no in-place edit, switch produces no metadata write, inspector edits inert or announced) asserted with the current source shape recorded as the expected finding: the switch is clickable with only the server-side 422 behind it (disabled expression omits readOnly, StudioDesignSurface.tsx) and the inspector hardcodes readOnly:false so edits take locally and silently vanish on reload; New-flow and autosave honor the gate. Grounded in objectui f7c52e2 + the framework's writable_package_required door; owns the Automations pillar only, cross-referencing readonly-package-locks-studio for the surfaces it already owns", "ref": "#12438" }, + { "revision": 2, "date": "2026-10-13", "change": "acceptance[2] and [3] (verify; evidence for [2]) drop the expected-fail wording, with knownGaps[0] and [1], steps[4] and source[0] (stale spec, #23163 A2 / A3; parked on #23121, carried by #23169). objectui#11124 disabled the pillar's switch on a read-only package (with the read-only hint as its title, and a guarded toggleEnabled) and threaded the real readOnly flag into the flow node inspector, pinned by StudioDesignSurface.automationsReadOnly-11124.test.tsx. QA run #23163 measured the switch disabled, a forced click sending nothing, the inspector inputs disabled and the flow byte-identical. Re-read from objectui source at 1fb1ced36243, still this repo's .objectui-sha. The clauses' promises are unchanged; only the recorded finding flips to the fixed shape", "ref": "#23169" } ] }, { @@ -1271,7 +1286,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": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": ["admin (the governance surface is Studio's)"], @@ -1308,7 +1323,7 @@ { "clause": "stats tell the directory's truth: per sampled type, stats[type].count equals the type's own list length and stats[type].packages equals the sorted distinct _packageId set — the one-round-trip aggregate the Studio directory tiles render from", "oracle": "api", - "verify": "side-by-side of stats[] against the GET /meta/ list for each sampled type (count, packages; locked counts rows whose _lock != 'none')", + "verify": "side-by-side of stats[] against the GET /meta/ list for each sampled type (count, packages). `locked` counts the items whose served READ ENVELOPE reports a lock other than 'none' — the derivation GET /api/v1/meta// publishes (protocol.ts servedLockState, #21694) — NOT the rows whose declared `_lock` != 'none': a packaged base the write doors refuse reads locked on its envelope while declaring no `_lock`, so compare `locked` against each sampled item's envelope lock", "evidence": "the stats excerpt + the list reads" }, { @@ -1371,7 +1386,8 @@ ], "history": [ { "revision": 1, "date": "2026-08-30", "change": "new item (2026-08-30 sweep, angle 4): the governance sweep GET /meta/diagnostics + its two Studio surfaces had no coverage — nothing asserted the stats/tile parity seam, the #8924 spelling refusal (whose regression the page paints as 'All clear'), the #8855 outage honesty, or the clean-store empty state. Grounded against source with two corrections to the register hypothesis: the scoping query param is ?package= (not ?packageId= — that is the protocol-layer argument name), and severity=warning cannot 'add warning-only entries' on stock because no producer emits warnings at head (reserved for a future lint layer) — authored as a mechanism clause with the lint-layer tripwire recorded. The invalid-stored-row positive is blocked(fixture) by construction (save doors validate; sys_metadata is not a data-plane object), with the stale-row snapshot named as the honest fixture candidate", "ref": "#sweep-2026-08-30" }, - { "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": 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" } ] }, { @@ -1379,7 +1395,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": 1, + "revision": 2, "priority": "P1", "surface": "mixed", "personas": [ @@ -1398,7 +1414,7 @@ "knownGaps": [ "THE STOCK SHOWCASE CORPUS CARRIES NO { permissionSet } BOOK AND NO doc NAV ENTRY — verify first (GET /api/v1/meta/book, read each audience; GET /api/v1/meta/app?id=showcase_app, look for type:'doc'). The run authors the books, docs, permission set and nav entries at runtime and tears them down, as platform-core.docs-audience-gate allows; the audience is read off the stored row, so a runtime-authored book exercises the same resolver", "FORK WINDOW: while .objectui-sha predates objectui PR 11241 / PR 11258, the vendored console carries the PRE-feature UI — at db11afd496 the console source holds no 'You do not have access to this documentation' heading and no renderer doc-target check (source read) — so a run against it fails for a reason that is not this item's. Drive a console built from an objectui revision carrying both (RUNNER.md's two honest options) and NAME that revision in the run record, beside the pin", - "FIXTURE RECIPE, measured on the first run: the showcase is invite_only, so members come from POST /api/v1/auth/admin/create-user (mustChangePassword false), not sign-up; the Studio doc editor refuses the read-only code package com.example.showcase with a loud 422 ('Pick or create a writable base (package) first'), so install a writable base first (POST /api/v1/packages with id, name, version, type 'app') and author every book, doc and the app into it with ?package=; the seeded admin holds no app-declared set, so once the gated book claims the doc the admin's own editor and /meta/app answer the audience refusal too — author the doc before the gated book, or read it back as the reader", + "FIXTURE RECIPE, measured on the first run: the showcase is invite_only, so members come from POST /api/v1/auth/admin/create-user (mustChangePassword false), not sign-up; the Studio doc editor no longer refuses the read-only code package with a 422 — at objectui 1fb1ced36243 it saves an ENV-WIDE draft and offers no package picker (QA run #23166), so a doc authored through the editor lands outside the package that holds the books. Install a writable base first (POST /api/v1/packages with id, name, version, type 'app') and drive every book, doc and the app into it over the metadata API with ?package=, so books and docs share one package; the seeded admin holds no app-declared set, so once the gated book claims the doc the admin's own editor and /meta/app answer the audience refusal too — author the doc before the gated book, or read it back as the reader", "REPO OWNERSHIP (ADR-0054): the server half — /meta/app pruning, /meta/doc and /meta/book refusals — is objectstack's; the menu markup, the direct-route refusal page and the claiming-book permalink are objectui's. A locator or copy fix goes to objectui" ] }, @@ -1457,7 +1473,8 @@ "platform-core.docs-audience-gate, platform-core.docs-portal-render (the neighbours — cross-referenced, not merged)" ], "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": 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" } ] }, { diff --git a/docs/qa/platform-checklist/areas/ux-conventions.json b/docs/qa/platform-checklist/areas/ux-conventions.json index 4c342dbd588..e9b0ecd5ce3 100644 --- a/docs/qa/platform-checklist/areas/ux-conventions.json +++ b/docs/qa/platform-checklist/areas/ux-conventions.json @@ -9,7 +9,7 @@ "title": "Every user-initiated write answers visibly — success names what changed, a refusal says why, and a no-op claims nothing", "since": "v17.7", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "browser", "personas": [ @@ -24,10 +24,10 @@ "an independent read of server truth for every row — a second browser context or the REST API — so a write is never judged by what the UI says about it" ], "knownGaps": [ - "Expected FAIL on stock: Setup → Users → Invite User declares successMessage 'Invitation sent' (packages/platform-objects/src/identity/sys-user.object.ts#invite_user), yet after Confirm the console showed no feedback while the sys_invitation and sys_email rows were written (objectstack-ai/objectui#11817). Score it FAIL and cite the card; do not re-file.", - "Expected FAIL on stock: Studio Publish gives no success confirmation — only the header flips to 'No drafts pending publish' (objectstack-ai/objectui#11807).", - "Expected FAIL on stock: inline edit shows '1 row modified · Save All (1)' after a click with no value change (objectstack-ai/objectui#11816) — negative[1].", - "Undo is scored only where a surface offers it today (record edit — records-forms.record-edit-undo). Its absence elsewhere is recorded in the evidence and is never a FAIL of this item: which writes should offer Undo is a product decision, not this item's oracle." + "FIXED, regression row, expected PASS (objectui#11817): Setup → Users → Invite User toasts 'Invitation sent to EMAIL' — the declared successMessage 'Invitation sent to ${result.email}' (packages/platform-objects/src/identity/sys-user.object.ts#invite_user) interpolating the invitee from the action result — while the sys_invitation and sys_email rows are written (QA run #23163 at objectui 1fb1ced36243; pin ObjectView.inviteFeedback-11817.test.tsx)", + "FIXED, regression row, expected PASS (objectui#11807): Studio Publish now gives a success confirmation (QA run #23163 at objectui 1fb1ced36243). Expected FAIL on stock, filed by that run and not to be re-filed: inline-edit 'Save All' on the task grid and Studio's 'Discard draft' land their write with no feedback at all (objectstack-ai/objectui#12303), and the approvals drawer's Approve toasts a bare 'Approved.' — the framework-declared successMessage on sys_approval_request (#23157)", + "FIXED, regression row, expected PASS (objectui#11816): leaving an inline editor with no value change no longer reads '1 row modified · Save All (1)' (QA run #23163 at objectui 1fb1ced36243; pin data-table-noop-edit-11816.test.tsx) — negative[1]", + "Undo is scored only where a surface offers it, and on stock NONE does: at objectui 1fb1ced36243 only an action declaring `undoable` pushes an Undo op (inline and form edits push none), and the showcase declares no undoable action (records-forms.record-edit-undo, blocked(fixture)). So acceptance[5] scores blocked(fixture) on stock (QA run #23163 A5). Its absence elsewhere is recorded in the evidence and is never a FAIL of this item: which writes should offer Undo is a product decision, not this item's oracle" ] }, "variants": [ @@ -36,12 +36,12 @@ "bulk delete — select two rows → Delete in the selection bar → confirm", "kanban move — drag a card Backlog → To Do (direct manipulation: the card in its new column IS the success feedback; a refused move must revert with an error)", "declared record action — Mark Done on a task record (declared successMessage 'Task marked done.', examples/app-showcase/src/ui/actions/index.ts#MarkDoneAction)", - "param action dialog — Setup → Users → Invite User → Confirm (declared successMessage 'Invitation sent')", + "param action dialog — Setup → Users → Invite User → Confirm (declared successMessage 'Invitation sent to ${result.email}')", "approval decision — Approvals inbox → Approve with a comment", "import — Tasks → Import a two-row CSV → the result screen", "Studio autosave — rename a field label in the scratch package", "Studio publish — Publish the scratch package", - "Studio discard — Discard changes on a pending draft", + "Studio discard — on the metadata designer (/_console/apps/setup/metadata/object/OBJ for a scratch-package object with a pending draft), click 'Discard draft' and accept the native confirm; the Studio pillars offer no discard. A scratch object's name carries its package's namespace prefix, so read it off the package before building the URL", "settings save — Settings → Localization → save a value" ], "steps": [ @@ -50,7 +50,7 @@ "for each variant read server-side whether the write landed (GET the record, the metadata item or the sys_ row) and record the request's status from the network log", "repeat the record rows signed in as the member persona, on member-owned records", "negative pass: (a) submit the create form with a required field empty, then again with a value that breaks a validation rule; (b) open an inline editor and leave it without changing the value", - "where a variant offers Undo (record edit), click it and read the record back" + "where a variant offers Undo, click it and read the record back — on stock none does (knownGaps[3]), so record acceptance[5] blocked(fixture)" ], "acceptance": [ { @@ -86,7 +86,7 @@ { "clause": "where Undo is offered it restores the prior value server-side", "oracle": "api", - "verify": "after Undo, GET the record and compare the field with its pre-edit value", + "verify": "after Undo, GET the record and compare the field with its pre-edit value; on stock no surface offers Undo (knownGaps[3]), so the clause scores blocked(fixture) until a packaged undoable action ships", "evidence": "before / after / after-undo values" } ], @@ -111,6 +111,12 @@ "revision": 1, "date": "2026-10-08", "change": "authored on the maintainer's instruction as a cross-cutting convention; grounded in the 2026-10-07 browser QA pass, expected-fail rows cite their cards" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "knownGaps[0]-[3], variants[5] and [10], steps[5] and acceptance[5].verify (knownGaps stale and a fixture gap, #23163 write-feedback / A5; parked on #23121, carried by #23169). objectui#11817, objectui#11807 and objectui#11816 are all fixed at objectui 1fb1ced36243, still this repo's .objectui-sha: their knownGaps become regression rows, and the invite text is the declared 'Invitation sent to ${result.email}'. The same run's new feedback defects (objectui#12303 inline Save All and Studio Discard draft; #23157 the bare 'Approved.') are named so the next run cites rather than re-files them. Studio discard is driven on the metadata designer. Undo: no stock surface offers it, so acceptance[5] scores blocked(fixture), the same root as records-forms.record-edit-undo", + "ref": "#23169" } ] }, @@ -119,7 +125,7 @@ "title": "Every disabled control explains itself — the reason shows on hover and on keyboard focus, and is exposed as an accessible description", "since": "v17.7", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "browser", "personas": [ @@ -133,8 +139,8 @@ "the read-only showcase package in Studio (stock)" ], "knownGaps": [ - "Expected FAIL on stock: the task record's Archive button (examples/app-showcase/src/ui/actions/index.ts#ArchiveTaskAction, disabled while has(record.done) && record.done != true) renders disabled in both its record_header and record_section placements with no title, tooltip or aria-describedby (objectstack-ai/objectui#11811). The action spec has no author-facing reason next to `disabled`, so a generic platform fallback passes clause 3 and is recorded as generic in the evidence — note which one was shown.", - "Known PASS reference: Studio's Publish button on the read-only showcase package carries a tooltip — check it first to calibrate the oracle before scoring the other rows." + "FIXED, now the calibration reference (objectui#11811): the task record's Archive button (examples/app-showcase/src/ui/actions/index.ts#ArchiveTaskAction, disabled while has(record.done) && record.done != true) carries its reason as a tooltip AND an aria-describedby on a focusable wrapper, in both its record_header and record_section placements (objectui DeclaredActionsBar.tsx; pin DeclaredActionsBar.disabledReason-11811.test.tsx). The action spec has no author-facing reason next to `disabled`, so the reason is the generic platform fallback — record it as generic under clause 3. Check this control first to calibrate both the hover and the keyboard oracle", + "Studio's Publish button on the read-only showcase package calibrates HOVER ONLY: its reason is a `title` on a disabled button with no focusable wrapper, so Tab skips it and clause 2 fails for it. Expected FAIL on stock, filed by QA run #23163 and not to be re-filed (objectstack-ai/objectui#12303): Studio's read-only Field properties controls (Label input, Remove) are disabled with no reason at all, and Studio Publish and the import wizard's disabled Next carry reasons a keyboard cannot reach" ] }, "variants": [ @@ -189,6 +195,12 @@ "revision": 1, "date": "2026-10-08", "change": "authored on the maintainer's instruction as a cross-cutting convention; grounded in the 2026-10-07 browser QA pass, the expected-fail row cites its card" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "knownGaps[0] and [1] (knownGap stale, #23163; parked on #23121, carried by #23169). objectui#11811 is fixed at objectui 1fb1ced36243, still this repo's .objectui-sha: Archive carries a tooltip and an aria-describedby on a focusable wrapper, so it becomes the PASS reference for both oracles. Studio Publish, the old calibration, passes hover only: its title sits on a disabled button with no focusable wrapper. The run's new defects (objectui#12303) are named so the next run cites rather than re-files them. No clause changed", + "ref": "#23169" } ] }, @@ -197,7 +209,7 @@ "title": "Where a label exists users see the label — machine names, raw ids and spec paths appear only as secondary text", "since": "v17.7", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "browser", "personas": [ @@ -211,9 +223,9 @@ "read access to the served metadata of every sampled surface — labels are read from metadata, never assumed" ], "knownGaps": [ - "Expected FAIL on stock: Explain access renders the principal as the raw user id; the Access list shows a new permission set by its name; the nav editor shows nav_item_N and truncated object names; the Changes panel lists machine names (objectstack-ai/objectui#11862).", - "Expected FAIL on stock: Profile shows the auth library's raw user.role ('user') for the platform admin (objectstack-ai/objectui#11866).", - "Expected FAIL on stock: the flow inspector's Node Type select lists raw snake_case types such as http_request, connector_action and try_catch (objectstack-ai/objectui#11778).", + "objectui#11862, PARTLY fixed at objectui 1fb1ced36243 (QA run #23163). Expected PASS now: Explain access's principal line names the person, not the raw user id (pin AccessExplainPanel.principalPerson-11862.test.tsx), and the nav editor's items. Expected FAIL on stock, still objectui#11862 (do not re-file): the Access permission-set list shows a new set by its name ('qa_readers', while the detail header reads its label), the Changes panel lists machine names ('PERMISSION · 1 | qa_readers'), and the Lookup inspector's Display-field picker puts the api_name first with the label grey and truncated. ⚠ Explain's Evaluation pipeline › Principal resolution prose still names the user by raw id: that sentence is built server-side (packages/plugins/plugin-security/src/explain-engine.ts) and is #23157, not objectui#11862", + "FIXED, regression row, expected PASS (objectui#11866): Profile states the access the console resolves rather than the auth library's raw user.role ('user') (objectui apps/console/src/pages/system/ProfilePage.tsx; pin ProfilePage.access.test.tsx)", + "FIXED, regression row, expected PASS (objectui#11778): the flow inspector's Node Type select offers the add-node palette's display names instead of raw snake_case types such as http_request, connector_action and try_catch (pin FlowNodeInspector.nodeTypeOptions-11778.test.tsx); QA run #23163 read the Node Type value as its label", "Regression rows, expected PASS: the audit-log actor and composite field values render as people and labels since objectstack-ai/objectui#11697." ] }, @@ -280,6 +292,12 @@ "revision": 1, "date": "2026-10-08", "change": "authored on the maintainer's instruction as a cross-cutting convention; grounded in the 2026-10-07 browser QA pass, expected-fail rows cite their cards" + }, + { + "revision": 2, + "date": "2026-10-13", + "change": "knownGaps[0]-[2] (knownGaps partly stale, #23163; parked on #23121, carried by #23169). At objectui 1fb1ced36243, still this repo's .objectui-sha: objectui#11866 is fixed, objectui#11778 is fixed for the Node Type select, and objectui#11862 is fixed for the Explain principal line and the nav editor but still reproduces for the Access permission-set list, the Changes panel and the Display-field picker (QA run #23163 commented onto objectui#11862). The run's server-built Explain prose finding is #23157, named so it is not scored against objectui#11862. No clause changed", + "ref": "#23169" } ] }