From 0289f0219728885fc436b41bff9b8d3309c28f57 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 14:52:10 +0000 Subject: [PATCH] docs(qa): re-point the wave-5 ledger lines and add the package export round-trip item The 18.3.0 wave-5 re-check (QA runs #29890, #29893, #29877 and #29870, at 589049b0) found checklist steps, verifies and fixtures that no longer match the product. Each line was re-read at base f68b47ca before it was applied. - Part A (W5-FAIL): 19 items, among them the action door for the aggregate bulk action, the ADR-0146 D11 pair recipe for staging a shipped view or email template, the 'Switched on / off' wording, the Invite outcome copy and the per-group approval read. - Part B (W5-STUDIO): 24 studio-authoring items, among them the Studio routes for the picklist fixture and the default-currency prompt. - Part C (W5-P0): first-run-loop knownGaps[3] is a regression row since #29790, and RUNNER's console-auth fact names the Packaged Actions page. - Part D (W5-EXPORT): new item platform-core.package-export-round-trip. - RUNNER: the overlay fact now gives the D11 pair recipe, and a new fact covers the first-page currency / timezone prompt. - platform-core.manifest-install-contract source[1] is re-pointed, because its anchor no longer resolved and the checker refused the area file. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8 --- docs/qa/platform-checklist/RUNNER.md | 73 ++-- .../platform-checklist/areas/api-backend.json | 10 +- .../platform-checklist/areas/automation.json | 30 +- docs/qa/platform-checklist/areas/i18n.json | 23 +- .../areas/integration-system.json | 32 +- .../areas/platform-core.json | 136 ++++++- .../areas/records-forms.json | 48 ++- .../areas/studio-authoring.json | 363 +++++++++++++----- .../areas/ux-conventions.json | 38 +- 9 files changed, 561 insertions(+), 192 deletions(-) diff --git a/docs/qa/platform-checklist/RUNNER.md b/docs/qa/platform-checklist/RUNNER.md index 67b3d4e57a7..c38c324e7f2 100644 --- a/docs/qa/platform-checklist/RUNNER.md +++ b/docs/qa/platform-checklist/RUNNER.md @@ -181,19 +181,40 @@ Standing facts about the stock showcase environment that have each cost a run a detour. They are briefing material, not verdicts — re-confirm one only when a run contradicts it, and correct it here when it does. -- **`view` is in the overlay-allowed set, so authoring a view on the stock read-only - showcase package is NOT blocked and needs no escape hatch.** The org-overridable - types are derived from the metadata-type registry, not a hand-written list, and are - exactly **`view`, `dashboard`, `report`, `translation`, `email_template`** - (`packages/spec/src/kernel/metadata-plugin.zod.ts` — the `allowEnvironmentOverlay: true` - entries; pinned by `protocol.org-scoped-write-refused.test.ts` G5). So - `PUT /api/v1/meta/view/` answers 2xx on stock showcase with no - `OS_METADATA_WRITABLE` and no `?package=` trick, while the same shape on `object`, - `field`, `hook`, `seed`, `mapping` or `flow` is refused. ⛔ Do not record a view - step as `blocked(environment)` on a "the showcase package is read-only" assumption: - read-only-ness is per metadata TYPE here, not per package, and the read-only-package - lock the console renders is a *different* gate (see - `access-security.readonly-package-locks-studio`). +- **Customizing a view, dashboard, report, translation or email template the stock + showcase ships goes into its pair, `com.example.showcase.custom`; a package-less + write onto a shipped name is refused.** The five overlay types are derived from the + metadata-type registry, not a hand-written list: **`view`, `dashboard`, `report`, + `translation`, `email_template`** (`packages/spec/src/kernel/metadata-plugin.zod.ts`, + the `allowEnvironmentOverlay: true` entries; pinned by + `protocol.org-scoped-write-refused.test.ts` G5). Since ADR-0146 D11 a write naming no + package onto an item of one of them that the read-only showcase ships, such as + `PUT /api/v1/meta/view/showcase_task.done` with no `?package=`, answers **403 + `NOT_OVERRIDABLE`**, whose sentence names the pair to save into + (`packagelessOverlayWriteRefusal`, + `packages/metadata-protocol/src/protocol-packaged-base-refusal.ts`; QA run #29890 + met it on a view and on an email template). The working recipe: + `POST /api/v1/packages/com.example.showcase/ensure-customization` (201 the first + time, 200 after), then `PUT /api/v1/meta//?package=com.example.showcase.custom` + with the item's body plus `overrides: { package: 'com.example.showcase', basedOn: '0.1.0' }`; + revert with `DELETE /api/v1/meta//?package=com.example.showcase.custom`. + A fresh name no package ships still writes package-less (200). The same shape on + `object`, `field`, `hook`, `seed`, `mapping` or `flow` stays refused as before. + ⛔ Do not record such a step as `blocked(environment)` on a "the showcase package is + read-only" assumption: the pair is the door, and the read-only-package lock the + console renders is a *different* gate (see `access-security.readonly-package-locks-studio`). + +- **A fresh organization's first page opens under a one-time prompt: "Set the default + currency", or the system timezone, or both in one dialog.** An administrator is asked + once while `localization.timezone` is still the manifest default or the organization + has no default currency (`ui/apps/console/src/pages/settings/WorkspaceTimezonePrompt.tsx`, + `workspaceTimezonePromptLogic.ts`), so on a fresh DB the dialog + (`data-testid="workspace-timezone-prompt"`) sits over the first page the admin opens, + Studio and app pages included (QA runs #29890 and #29893). Dismiss it with **Not now** + (Keep the default when only the timezone is asked), `data-testid="workspace-timezone-prompt-decline"`, + before any screenshot or DOM read. The answer is remembered per organization and user in + that browser's `localStorage` (`os:workspace-timezone-prompt::`), so a new + browser context asks again. ⛔ Do not score a clause FAIL for chrome hidden under it. - **`objectstack verify --rls` is a separate invocation — bare `verify` prints no RLS section at all.** `runRlsProofs` runs only behind the flag @@ -460,16 +481,22 @@ contradicts it, and correct it here when it does. bearer seam. So a session driven with the localStorage token alone gets **401 `AUTH_REQUIRED`** from `POST /api/v1/storage/upload/presigned` while `GET /auth/get-session` on the same page returns 200 — a convincing fake "avatar - upload is broken". The Setup app's **Packaged Automation** page (component ref - `automation:packaged`) needs it too: every request it makes, its - `GET /api/v1/automation/_status` and `GET /api/v1/meta/flow` loads and its toggle - and clone writes, is a bare `fetch` with `credentials: 'include'` and no - `Authorization` header (`ui/packages/app-shell/src/views/setup/PackagedAutomationPage.tsx`), - so a token-only session renders a load failure ending `HTTP 401` (`automation - status HTTP 401` or `flow metadata HTTP 401`, whichever load is refused first) in - the page's `role="alert"` box instead of the flow list (QA run #27409). ⛔ Do not - drive any storage/upload surface or the Packaged Automation page from an injected - token: **sign in through the form** so both halves exist. + upload is broken". The Setup app's **Packaged Actions** page (component ref + `automation:packaged-actions`) is built the same way: its `GET /api/v1/meta/object` + and `GET /api/v1/data/sys_metadata_activation` loads and its + `POST /api/v1/actions/_activation//` toggle are bare `fetch`es with + `credentials: 'include'` and no `Authorization` header + (`ui/packages/app-shell/src/views/setup/PackagedActionsSection.tsx`), so a token-only + session should expect a load failure ending `HTTP 401` (`object metadata HTTP 401` or + `activation ledger HTTP 401`) in the page's `role="alert"` box instead of the action + list. That is read from source; QA run #27409 measured the shape on the Packaged + Automation page (`automation:packaged`), which retired with ADR-0146 D11: a bookmark of + `/apps//component/automation/packaged` now opens Studio's Automations pillar + (`packagedAutomationStudioPath`, + `ui/packages/app-shell/src/views/studio-design/setupMetadataRedirect.ts`), where a + packaged flow's switch and its New similar are. ⛔ Do not drive any storage/upload + surface or the Packaged Actions page from an injected token: **sign in through the + form** so both halves exist. - **A stock showcase boot refuses self-registration — no item can mint a member by "fresh sign-up".** The showcase declares no `auth.audience`, so its posture is the diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index 6b8380c2ac8..915cb1927c7 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -3295,7 +3295,7 @@ "title": "Error answers made honest on v18: a hook refusal on a hand-built write route answers 400 with the hook's own sentence, a crash answers a generic 500 that withholds the caught message, and a unique-constraint refusal is 409 UNIQUE_VIOLATION with details at the action door, MCP and sandboxed bodies too", "since": "v18", "status": "active", - "revision": 6, + "revision": 7, "priority": "P2", "surface": "api", "personas": [ @@ -3310,7 +3310,7 @@ ] }, "steps": [ - "POST a /data write route on the object whose hook refuses (POST /api/v1/data/OBJECT, or its /batch or /createMany door); read the body", + "POST a /data write route on the object whose hook refuses (POST /api/v1/data/OBJECT, or its /createMany door; not /batch: POST /api/v1/data/OBJECT/batch answers 200 with a per-row report, each refusal in results[i].errors[0] with its code and httpStatus, so it never answers the 400 this item reads, packages/rest/src/batch-routes.ts); read the body", "invoke the script action that inserts a duplicate value: POST /api/v1/actions/OBJECT/ACTION", "force a server fault on a storage route (e.g. a malformed adapter config on the scratch boot — QA run #24558 set storage.local_root under /dev/null through PUT /api/settings/storage) and read error.message" ], @@ -3387,6 +3387,12 @@ "date": "2026-11-02", "change": "acceptance[1] (clause, verify) and the automated.ref prose re-pointed to HOOK_ABORTED — stale spec from QA run #28929 (release-18.3.0-w4-c2a, at 7fa9873f), api-backend.error-sentences-and-unique-violation-wire-code acceptance[1], re-point card #28936. r5 asserted the pre-ruling shape: no `code` on the /data doors and 400 VALIDATION_ERROR (REFUSED_400) on the nested-envelope rows. #27597 gave an undeclared hook abort the ledgered ADR-0112 code HOOK_ABORTED (UNDECLARED_HOOK_REFUSAL_CODE in packages/types/src/thrown-http-error.ts, stamped by mapDataError) and #27672 carried it to the doors that resolve through resolveThrownHttpError (package publish). Re-read at base 135054ebaa: error-sentences-live.dogfood.test.ts asserts code HOOK_ABORTED on both /data doors, rest-hook-script-fault-envelope.test.ts asserts HOOK_ABORTED and no declaredCode through mapDataError, and rest-write-route-hook-refusal-sentence.ledger.test.ts answers HOOK_ABORTED_400 on every sharing-rule, suggested-binding, approval and package-publish row (no REFUSED_400 remains). Status and sentence unchanged; the clause now names the code instead of its absence, so it is narrower, not weaker. No other clause changed", "ref": "#28936" + }, + { + "revision": 7, + "date": "2026-11-04", + "change": "steps[0] corrected from QA run #29890 (W5-FAIL, at 589049b0): the /batch door is dropped from the doors that answer the clause's 400. It answers 200 with a per-row report (results[i].errors[0]). Read at base f68b47ca", + "ref": "#29906" } ] } diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index 19f20d9479d..7f0bdec4b6e 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -2148,7 +2148,7 @@ "title": "POST /automation/:name/clone (Studio's New similar): whole-definition copy mutating exactly name/label/status, written switched off (status obsolete) into a package — the base's .custom pair when `package` is omitted —, protection envelope dropped, no ancestry, notice verbatim, 400/404/409/422 refusal arms — and the honest clause: the copy survives a restart and reaches Studio", "since": "v17", "status": "active", - "revision": 11, + "revision": 12, "priority": "P1", "surface": "mixed", "personas": [ @@ -2171,7 +2171,7 @@ "no double-fire: POST /api/v1/data/showcase_task with priority='urgent' (with its required project), three times; GET both flows' runs lists", "references: POST /api/v1/automation/showcase_task_done_notify_owner/clone {\"name\":\"qa_task_done_clone\",\"label\":\"QA task-done clone\"}; read the clone's subflow node config", "ordinariness: PUT /api/v1/automation/qa_urgent_alert_clone with the clone's own definition (label tweaked) — it is the pair's own flow, the edit lands", - "restart probe: stop the server; cold-boot over the SAME database file; GET /api/v1/automation/qa_urgent_alert_clone and GET /api/v1/meta/flow (which package holds the copy?); create one more urgent task and read both runs lists; then, in the browser as the admin (this leg needs a served console: on a tree built with `pnpm build` alone /_console/ answers 503 until `pnpm console:build` has run), open /_console/studio/com.example.showcase/automations and confirm the copy is listed on the rail marked 'In com.example.showcase.custom' and opens in the designer; open /_console/studio/~org/automations and record what it lists", + "restart probe: stop the server; cold-boot over the SAME database file; GET /api/v1/automation/qa_urgent_alert_clone and GET /api/v1/meta/flow (which package holds the copy?); create one more urgent task and read both runs lists; then, in the browser as the admin (this leg needs a served console: on a tree built with `pnpm build` alone /_console/ answers 503 until `pnpm console:build` has run), open /_console/studio/com.example.showcase/automations and confirm the copy is listed on the rail with the 'Added' mark (engine.customizationMark.added, titled 'Added to … by its customization package com.example.showcase.custom') and opens in the designer, whose header chip reads 'In com.example.showcase.custom' (engine.studio.customize.addedIn); open /_console/studio/~org/automations and record what it lists", "teardown: DELETE /api/v1/automation/qa_urgent_alert_clone and …/qa_task_done_clone (or discard the run's own DB), and the scratch package" ], "acceptance": [ @@ -2182,9 +2182,9 @@ "evidence": "the four 400 bodies with their details arrays" }, { - "clause": "the copy is whole-definition with exactly name/label/status mutated, the protection envelope and read decorations dropped, and NO ancestry: every other key of the copy deep-equals the source; none of the 10 FLOW_CLONE_DROPPED_KEYS (_diagnostics, _draft, _lock, _lockReason, _lockSource, _provenance, _packageId, _packageVersion, _lockDocsUrl, protection) is present on the copy; no clonedFrom-shaped key exists in the response or the stored definition. The response names where the copy LIVES (packageId com.example.showcase.custom when `package` was omitted), which is not ancestry", + "clause": "the copy is whole-definition with exactly name/label/status mutated, the protection envelope and read decorations dropped, and NO ancestry: every other key of the copy deep-equals the source; none of the 11 FLOW_CLONE_DROPPED_KEYS (_diagnostics, _draft, _lock, _lockReason, _lockSource, _provenance, _packageId, _packageVersion, _lockDocsUrl, protection, overrides) is present on the copy; no clonedFrom-shaped key exists in the response or the stored definition. The response names where the copy LIVES (packageId com.example.showcase.custom when `package` was omitted), which is not ancestry", "oracle": "api", - "verify": "the step-4 diff: differences are exactly {name, label, status:'obsolete'} (FLOW_CLONE_MUTATED_FIELDS and FLOW_CLONE_STATUS, flow-clone.ts — 'obsolete' since #28149, ADR-0146 D4) plus the ABSENCE of the dropped keys (flow-clone.ts — METADATA_READ_DECORATIONS + MetadataProtectionFields, derived, plus the package author's `protection` block, named (#23120): 2+7+1=10 at base 9a7838edf5; count them from the source at run time rather than trusting this line). The step-3 response carries data.packageId 'com.example.showcase.custom' (domains/automation.ts, the clone arm's deps.success). A missing facet (nodes, edges, variables, runAs, retry…) is the #11703 param-list failure shape; a surviving _packageId or _lock defeats the feature (the copy would claim the base's package or be locked); any ancestry key violates amendment ruling 2 / §9", + "verify": "the step-4 diff: differences are exactly {name, label, status:'obsolete'} (FLOW_CLONE_MUTATED_FIELDS and FLOW_CLONE_STATUS, flow-clone.ts — 'obsolete' since #28149, ADR-0146 D4) plus the ABSENCE of the dropped keys (flow-clone.ts — METADATA_READ_DECORATIONS + MetadataProtectionFields, derived, plus the package author's `protection` block, named (#23120), plus OverrideDeclarationFields (`overrides`, ADR-0146 D2): 2+7+1+1=11 at base f68b47ca; count them from the source at run time rather than trusting this line). The step-3 response carries data.packageId 'com.example.showcase.custom' (domains/automation.ts, the clone arm's deps.success). A missing facet (nodes, edges, variables, runAs, retry…) is the #11703 param-list failure shape; a surviving _packageId or _lock defeats the feature (the copy would claim the base's package or be locked); any ancestry key violates amendment ruling 2 / §9", "evidence": "the key-by-key diff + the dropped-keys absence check + the response's packageId" }, { @@ -2212,9 +2212,9 @@ "evidence": "the PUT response + the re-read" }, { - "clause": "the honest clause — the copy OUTLIVES the process and REACHES Studio: after a cold restart over the same database file the copy still exists in com.example.showcase.custom, still switched off (it does not run on a new urgent create), and it is listed on the base's Studio Automations rail marked 'In com.example.showcase.custom' and opens in the designer (ADR-0146 D4: 'new similar' lands in the pair; ADR-0126 §1.3's 'customize in Studio' promise)", + "clause": "the honest clause — the copy OUTLIVES the process and REACHES Studio: after a cold restart over the same database file the copy still exists in com.example.showcase.custom, still switched off (it does not run on a new urgent create), and it is listed on the base's Studio Automations rail with the 'Added' mark and opens in the designer under the header chip 'In com.example.showcase.custom' (ADR-0146 D4: 'new similar' lands in the pair; ADR-0126 §1.3's 'customize in Studio' promise)", "oracle": "api", - "verify": "step 8: GET /api/v1/automation/qa_urgent_alert_clone on the new process answers the copy; GET /api/v1/meta/flow shows it held by com.example.showcase.custom; the post-restart urgent create adds a run to the base only. Reachability: /_console/studio/com.example.showcase/automations lists the copy with the pair's mark (engine.studio.customize.addedIn 'In {pair}'; a read-only base's rail lists the flows its pair adds, StudioDesignSurface.tsx railPairId) and its deep link opens it. The package-less scope /_console/studio/~org/automations no longer holds the copy: QA run #29002 read 'No flows outside a package…' there, which is correct now that no clone is written without a package (ADR-0070 D5/D6). A copy absent from the base's rail, or a rail that opens another flow for the copy's deep link, fails the clause. Record which half failed (durability, reachability, or both) — do NOT soften the clause to a same-process read", + "verify": "step 8: GET /api/v1/automation/qa_urgent_alert_clone on the new process answers the copy; GET /api/v1/meta/flow shows it held by com.example.showcase.custom; the post-restart urgent create adds a run to the base only. Reachability: /_console/studio/com.example.showcase/automations lists the copy with the pair's mark 'Added' (engine.customizationMark.added; the 'In {pair}' text, engine.studio.customize.addedIn, is the opened copy's header chip, #29693; a read-only base's rail lists the flows its pair adds, StudioDesignSurface.tsx railPairId) and its deep link opens it. The package-less scope /_console/studio/~org/automations no longer holds the copy: QA run #29002 read 'No flows outside a package…' there, which is correct now that no clone is written without a package (ADR-0070 D5/D6). A copy absent from the base's rail, or a rail that opens another flow for the copy's deep link, fails the clause. Record which half failed (durability, reachability, or both) — do NOT soften the clause to a same-process read", "evidence": "the post-restart GET (present or 404) + the meta/flow listing showing which package holds the copy + the rail screenshot" }, { @@ -2277,6 +2277,12 @@ "date": "2026-11-02", "change": "Re-pointed to ADR-0146 D4's 'new similar' (#28149: the copy lands in a package, the base's .custom pair when `package` is omitted, written status 'obsolete'). Measured input: QA run #29002 (W4-C3b1, 7fa9873f; card note 6570476620), which failed acceptance[1], [2] and [6] as stale spec. Re-read at base dfbe34d887 (flow-clone.ts FLOW_CLONE_STATUS 'obsolete' and FLOW_CLONE_NOTICE; domains/automation.ts resolveCloneTarget; new-similar-target.ts): acceptance[1]'s mutated status is 'obsolete' and the response's packageId is named as where the copy lives; acceptance[2] is inverted from 'double-fire, draft is not an off-switch' to 'switched off, never double-runs' — the property the run measured over 3 creates; acceptance[6] reads the copy on the base's Studio rail marked 'In com.example.showcase.custom', and ~org's empty list as correct; acceptance[5]'s 'tenant flow' becomes the pair's flow. New acceptance[7] asks the `package` arms the note named as unasked (blank 400, not installed 422, read-only 422, outside the closure 409). steps, fixtures, negative and source follow; no clause weakened", "ref": "#28579" + }, + { + "revision": 12, + "date": "2026-11-04", + "change": "acceptance[1] (clause and verify), acceptance[6] (clause and verify) and steps[7] corrected from QA run #29890 (W5-FAIL, at 589049b0). FLOW_CLONE_DROPPED_KEYS now also drops OverrideDeclarationFields (`overrides`), 11 keys (flow-clone.ts). On the base's rail the copy carries the row mark 'Added'; 'In com.example.showcase.custom' is the designer header's chip (#29693). Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2285,7 +2291,7 @@ "title": "The packaged-automation doors after ADR-0146 D4/D11: Setup nav serves Packaged actions (the retired flows page's bookmark lands in Studio), Studio's Automations pillar lists the packaged flows, its ledger switch persists, New similar is offered on flows only, no lineage surface, server refusals verbatim in role=alert", "since": "v17", "status": "active", - "revision": 11, + "revision": 12, "priority": "P1", "surface": "browser", "personas": [ @@ -2310,7 +2316,7 @@ "boot the showcase on its own port and DB (dogfood §0, OS_PORT exported); build/serve the pinned console; sign in as the dev admin", "nav probe: GET /api/v1/meta/app?id=setup and search the served navigation for the type:'component' item with componentRef 'automation:packaged-actions' (and confirm no item names 'automation:packaged'); then click the entry in the rendered Setup sidebar. Then type the retired page's bookmark /_console/apps/com.objectstack.setup/component/automation/packaged and record where it lands (no ?package= → /_console/studio/~all/metadata/flow), and once more with ?package=com.example.showcase (→ /_console/studio/com.example.showcase/automations, the query kept)", "open Studio on the read-only package: /_console/studio/com.example.showcase/automations; screenshot the rail settled", - "inventory: compare the rail's flows against GET /api/v1/automation/_status (the runtime list whose rows carry the ledger flag) and GET /api/v1/meta/flow (the items whose _packageId is com.example.showcase). Open a flow (the URL gains ?surface=flow:NAME, studioPillarItemPath in metadataArea.ts) and read the header switch data-testid auto-ledger-switch as two facts: aria-checked is the ledger flag of that flow's _status row, and data-reading says whether the flow RUNS ('on' — 'On for this deployment'; 'not-running' — 'On for this deployment, not running here', with the platform's reason in auto-ledger-not-running-reason; 'off' — 'Off for this deployment'). The readings come from objectui packagedFlows.ts activationReading, the same rule the retired page used. Expect showcase_approver_bindings (authored status: 'obsolete') off, and on a default boot (OS_AUTOMATION_SCHEDULED_WORK_ENABLED unset) showcase_scheduled_digest and showcase_task_due_reminder not-running with unboundCause 'deployment_policy'. Reconcile against _status, not against counts: QA run #29124 read 32 flows on both the rail and _status", + "inventory: compare the rail's flows against GET /api/v1/automation/_status (the runtime list whose rows carry the ledger flag) and GET /api/v1/meta/flow (the items whose _packageId is com.example.showcase). Open a flow (the URL gains ?surface=flow:NAME, studioPillarItemPath in metadataArea.ts) and read the header switch data-testid auto-ledger-switch as two facts: aria-checked is the ledger flag of that flow's _status row, and data-reading says whether the flow RUNS ('on' — 'Switched on for this deployment'; 'not-running' — 'Switched on for this deployment, not running here', with the platform's reason in auto-ledger-not-running-reason; 'off' — 'Switched off for this deployment'; engine.studio.auto.ledger.*). The readings come from objectui packagedFlows.ts activationReading, the same rule the retired page used. Expect showcase_approver_bindings (authored status: 'obsolete') off, and on a default boot (OS_AUTOMATION_SCHEDULED_WORK_ENABLED unset) showcase_scheduled_digest and showcase_task_due_reminder not-running with unboundCause 'deployment_policy'. Reconcile against _status, not against counts: QA run #29124 read 32 flows on both the rail and _status", "toggle: on showcase_urgent_task_alert flip auto-ledger-switch OFF (POST /api/v1/automation/showcase_urgent_task_alert/toggle); reload the page fully and re-open the flow; record the switch state and the sys_metadata_activation row (metadata_type 'flow', name showcase_urgent_task_alert, active false); flip it back ON", "New similar: on an open packaged flow open the ⋯ menu (auto-item-more) → New similar (auto-item-new-similar). The dialog (auto-new-similar-dialog) opens PREFILLED with NAME_copy and 'LABEL (copy)', both editable and submit enabled (FlowItemMoreMenu.tsx openNewSimilar), so clear each field first — then try submitting with name empty, label empty, then name 'Bad-Name!' + a label; record what renders; then make a legal copy and record the landing strip (auto-new-similar-strip, with the door's notice in auto-new-similar-notice) and where the copy opened; DELETE the copy via the API afterwards", "Packaged actions: open Setup › Packaged actions (the sidebar entry of steps[1]) and screenshot the section (data-testid packaged-actions-section) and its rows", @@ -2358,7 +2364,7 @@ { "clause": "the New similar dialog requires BOTH a new machine name and a new label before submit is possible, a server-refused name surfaces verbatim in role=alert inside the dialog, and a legal copy lands switched off in the pair with the door's notice shown as sent", "oracle": "dom", - "verify": "auto-new-similar-submit is disabled while either field is blank (copyValid, FlowItemMoreMenu.tsx — so the API's 'required' 400s are unreachable from this dialog); submitting 'Bad-Name!' round-trips the clone door's 400 and auto-new-similar-refusal (role=alert) shows the sentence actionErrorDetail extracted, with the dialog still open on the names typed — details[] never reach the DOM, so if the sentence does not state the ^[a-z_][a-z0-9_]*$ constraint legibly, record what DID render as the finding rather than inventing a pass. A legal copy on the read-only base sends no package (the door picks the pair), opens in com.example.showcase.custom, and auto-new-similar-strip reads 'Saved the copy NAME in package com.example.showcase.custom.' with the door's notice verbatim in auto-new-similar-notice. QA run #29124 measured the validation and the 400 in role=alert", + "verify": "auto-new-similar-submit is disabled while either field is blank (copyValid, FlowItemMoreMenu.tsx — so the API's 'required' 400s are unreachable from this dialog); submitting 'Bad-Name!' round-trips the clone door's 400 and auto-new-similar-refusal (role=alert) shows the sentence actionErrorDetail extracted, with the dialog still open on the names typed — details[] never reach the DOM, so if the sentence does not state the ^[a-z_][a-z0-9_]*$ constraint legibly, record what DID render as the finding rather than inventing a pass. A legal copy on the read-only base sends no package (the door picks the pair) and lands in com.example.showcase.custom, yet opens on the base's own pillar URL: the base's rail lists its pair's flows, so the URL stays /_console/studio/com.example.showcase/automations and the copy is selected there with the 'Added' mark (onFlowCopied, StudioDesignSurface.tsx); auto-new-similar-strip reads 'Saved the copy NAME in package com.example.showcase.custom.' with the door's notice verbatim in auto-new-similar-notice. QA run #29124 measured the validation and the 400 in role=alert", "evidence": "dialog screenshots: disabled submit, the 400 alert text, the landing strip with its notice" }, { @@ -2462,6 +2468,12 @@ "date": "2026-11-02", "change": "Re-pointed to the doors that replaced the retired Setup › Packaged automation page (ADR-0146 D4/D11; #28785 retired its flows half, #28818 sent its bookmark to Studio). Measured inputs: QA run #29124 (W4-L1, 7fa9873f; card note 6572222369), which failed A0, A1, A2, A3, A5 and A6 as stale spec and passed A4 and A7. Clause by clause, re-read at base dfbe34d887: acceptance[0] asserts nav_packaged_actions ('automation:packaged-actions'), the absence of 'automation:packaged', and the retired bookmark's landing in Studio (packagedAutomationStudioPath); acceptance[1] reads Studio's Automations rail against _status (32 flows in the run) and the header auto-ledger-switch's aria-checked/data-reading; acceptance[2] the same flip persisted across a reload; acceptance[3] New similar on flows, no clone on Setup › Packaged actions; acceptance[4] no lineage, now naming that the pair's 'In PAIR' mark and ADR-0146 D2's customization controls are not lineage; acceptance[5] re-points to Studio's refusal strip and, per the seat's ruling, to #29139's outcome instead of marking it transitional: #29224 put role=alert on studio-refusal-message, so the clause keeps 'announced' — the member's 403 cell is scored over the API while the member reaches neither door (#29124 measured exactly that); acceptance[6] the New similar dialog (copyValid, auto-new-similar-refusal role=alert, the landing strip in the pair); acceptance[7] records the posture #29124 met. steps, fixtures, negative, automated.ref and source[0]–[2] follow. No clause dropped; acceptance[4] no longer forbids a 'customized' badge, because ADR-0146 D2 charters that mark on served facts. After merging main at a20dcae3f9 the strip lives in its own StudioRefusalStrip.tsx (#29380) and the switch stays focused while a toggle is in flight (#29375, aria-disabled while busy); the citations follow, and no clause moves", "ref": "#28579" + }, + { + "revision": 12, + "date": "2026-11-04", + "change": "steps[3] and acceptance[6].verify corrected from QA run #29890 (W5-FAIL, at 589049b0): the ledger switch readings are 'Switched on / off for this deployment' (engine.studio.auto.ledger.*, respelled by #29784 / #29841), and a New similar copy on the read-only base opens on the base's pillar URL, selected in its rail, not on a pair URL. Read at base f68b47ca", + "ref": "#29906" } ] }, diff --git a/docs/qa/platform-checklist/areas/i18n.json b/docs/qa/platform-checklist/areas/i18n.json index c32565499e1..109cc19fbeb 100644 --- a/docs/qa/platform-checklist/areas/i18n.json +++ b/docs/qa/platform-checklist/areas/i18n.json @@ -9,7 +9,7 @@ "title": "Notification mark-as-read is server-authoritative, the deep-link target is localized — and the title is the flow-authored literal string, because notification copy has no translation group", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "browser", "personas": ["zh-CN session user receiving an app-flow notification", "en session recipient (the control that proves the title is recipient-INDEPENDENT)"], @@ -29,7 +29,7 @@ "put the RECIPIENT in Chinese the way a user does: signed in as the recipient, pick 中文 in the avatar-menu language switcher, which writes the recipient's own sys_user.locale (zh-CN) and the console's UI locale together (objectui packages/app-shell/src/layout/LocaleSwitcher.tsx). The server-side equivalent is the recipient's own PATCH /api/v1/data/sys_user/RECIPIENT_ID {\"locale\":\"zh-CN\"} followed by a reload. Seeding browser storage alone is not a language switch — it leaves sys_user.locale empty (i18n.studio-follows-app-locale step 0 records the harness gap that cost a run)", "read the showcase flow's notify-node config first (examples/app-showcase/src/automation/flows/index.ts — the record-after-update flow on `assignee != previous.assignee`, notify node `notify_assignee`, topic `task.assigned`, channel inbox) and write down cfg.title verbatim (stock: 'New task assigned: {{ record.title }}'); confirm the node authors an inline `title` and no `template`. The title expectation is that literal template with its {{ record.* }} placeholders rendered, derived from the flow, not from a locale", "trigger it by changing a seeded showcase_task's assignee to the recipient. ⛔ Not `collab.assignment` — the kernel stopped emitting assignment notifications in #3403; that topic survives only in regression pins", - "sign in as the recipient (own browser context — never a shared tab), open the top-bar bell's inbox POPOVER (InboxPopover, ui/packages/app-shell/src/layout/InboxPopover.tsx) on its Notifications tab, beside Approvals and Activity, and screenshot the notification entry there. The Home page's notifications card lists the same row and is a SECOND door: opening the row there runs the bell's own open action (openInboxNotification), which marks the notification read before following its link (ui/packages/app-shell/src/console/home/HomePage.tsx). Drive steps 4–6 from the popover, and do not open the row from Home first, or the read is spent before step 6 can capture it", + "sign in as the recipient (own browser context — never a shared tab), open the top-bar bell's inbox POPOVER (InboxPopover, ui/packages/app-shell/src/layout/InboxPopover.tsx) on its Notifications tab, beside Approvals (the Activity tab is drawn only for a caller who may read the activity feed, activity.readable, objectui#12081, so a member recipient's popover has none), and screenshot the notification entry there. The Home page's notifications card lists the same row and is a SECOND door: opening the row there runs the bell's own open action (openInboxNotification), which marks the notification read before following its link (ui/packages/app-shell/src/console/home/HomePage.tsx). Drive steps 4–6 from the popover, and do not open the row from Home first, or the read is spent before step 6 can capture it", "click the popover entry to follow the notification's deep link, and screenshot the target detail page. That click marks the notification read first (openInboxNotification), so it is the mark-as-read request step 6 captures", "capture the mark-as-read request (POST /api/v1/notifications/read) and the server's unread count before and after: `unreadCount` on GET /api/v1/notifications. ⛔ Never the header badge: it counts unread notifications PLUS pending approvals (the inbox popover's approvals segment), so a pending approval holds it above zero after a clear that worked", "reload the page fully and re-read the unread count from the server — and when unreadCount stays above zero, check the surviving rows' createdAt before calling it a failed clear (#7640's residual badge was NEW mail from the showcase digest flow, which fires every minute)", @@ -91,7 +91,8 @@ { "revision": 3, "date": "2026-08-11", "change": "c0/c4 REWRITTEN against the shipped contract (#7684). Both clauses assumed a per-recipient localized notification title — a capability the spec retired — so on a stock app they were unprovable and landed blocked(fixture) rather than testing anything. c0 now asserts the flow-authored literal title with its placeholders interpolated; c4 now asserts the title is recipient-INDEPENDENT, with a per-recipient difference as the finding. The mechanical read/clear clauses (live-server POST /notifications/read + the survives-reload re-read) are untouched and remain the item's substance. The retirement is cited on the item (translation.zod.ts, #3403, notify-node.ts) so the next author cannot re-assert the retired premise; the steps drop the unperformable 'set the recipient user's locale' (sys_user has no locale field) and the stale collab.assignment trigger. Verified before editing: the retirement guidance is still on origin/main and no notification-copy translation group has appeared — plugin-audit's per-recipient collab.mention title resolves the pre-existing `messages` group, a different producer, and is recorded in knownGaps so the correction does not overshoot", "ref": "#7684" }, { "revision": 4, "date": "2026-10-16", "change": "knownGaps[0], steps[1] and [2], acceptance[0] (clause, verify) and acceptance[4] (clause, verify) and source[7] re-pointed (checklist-accuracy note 1 of QA run #23951, wave #23878). Three premises had moved: sys_user.locale exists and the avatar-menu switcher writes it (zh-CN accepted), so steps[1]'s storage-only, language-code-only recipe is replaced by the switcher (or the recipient's own PATCH of sys_user.locale); the notify node gained a per-recipient `template` path (#13881), so 'per-recipient localization is spec-RETIRED' is wrong — only the INLINE title is unlocalized, which is what the showcase flow uses; and placeholders are {{ record.title }}, so the FAIL shape is an unrendered double-brace token. The clauses are not loosened: clause 0 still demands the literal rendered title, and clause 4 still files any per-recipient difference on an inline title; a flow that moved to a `template` is a premise change that revises the item, never a tick", "ref": "#24036" }, { "revision": 5, "date": "2026-10-26", "change": "steps[6], steps[7] and acceptance[2].verify read the server's unreadCount (GET /api/v1/notifications), never the header badge (checklist-accuracy finding of QA run #25817, release-18.2.0-manual-set at 6679644a; wave anchor #25807): the badge counts notifications plus pending approvals, so a pending approval reads as a failed clear. The clauses are unchanged", "ref": "#25842" }, - { "revision": 6, "date": "2026-10-31", "change": "steps[4] and steps[5] said 'open the bell'. QA run #27441 asked that the surface be named. The steps now name the top-bar bell's inbox popover, Notifications tab (InboxPopover.tsx). They also name the second door: the Home page's notifications card opens a row through the same openInboxNotification, which marks it read before following the link (HomePage.tsx). Since #27447 and #27653, opening a row from Home spends the read before step 6 captures it. No clause changed. Read from source at base 22ce3d13. Wave anchor #27377, wave-3 accuracy slice S5 (#28339); finding parked in anchor comment 6490438726", "ref": "#27441" } + { "revision": 6, "date": "2026-10-31", "change": "steps[4] and steps[5] said 'open the bell'. QA run #27441 asked that the surface be named. The steps now name the top-bar bell's inbox popover, Notifications tab (InboxPopover.tsx). They also name the second door: the Home page's notifications card opens a row through the same openInboxNotification, which marks it read before following the link (HomePage.tsx). Since #27447 and #27653, opening a row from Home spends the read before step 6 captures it. No clause changed. Read from source at base 22ce3d13. Wave anchor #27377, wave-3 accuracy slice S5 (#28339); finding parked in anchor comment 6490438726", "ref": "#27441" }, + { "revision": 7, "date": "2026-11-04", "change": "steps[4] corrected from QA run #29890 (W5-FAIL, at 589049b0): a member recipient's inbox popover has no Activity tab; InboxPopover.tsx draws it only while activity.readable. Read at base f68b47ca", "ref": "#29906" } ] }, { @@ -181,7 +182,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": 13, + "revision": 14, "priority": "P1", "surface": "browser", "personas": ["admin on a zh-CN session"], @@ -195,7 +196,7 @@ "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 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 unit pins under ui/ that `automated` names. 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" + "acceptance[5]'s CONFIRM half HAS a stock specimen, so it is no longer pin-only (the old reading, QA run #21782's blocked(fixture) 'no zh confirmText', parked on #22017 row 8, is stale): sys_session's revoke_my_other_sessions, a list_toolbar action on the sys_session list (packages/platform-objects/src/identity/sys-session.object.ts; confirmText 'Sign out of every other device where you're currently logged in? Your current session will remain active.'), whose zh-CN entry in platform-objects' bundle (packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts) reads 要退出你当前已登录的其他所有设备吗?当前会话将保持活跃。 with successMessage 其他所有会话已撤销. Read from source at base f68b47ca and not yet driven live: drive it as the zh-CN admin with a second session open, capture the confirm and the toast, and record it. No showcase action declares a `confirmText` still, so the showcase's live row is the toast half (e.g. showcase_recalc_estimate's zh successMessage); the unit pins `automated` names stay the copy's pin" ] }, "steps": [ @@ -212,6 +213,7 @@ "a Settings namespace — title, group titles, key labels, and the source badges (settings. + settingsCommon.sourceLabels)", "Studio metadata forms (metadataForms group — overlaps i18n.studio-follows-app-locale; a pinned pass there may be cited)", "a sys_ object surface — e.g. the All Users list showing sys_user's translated label (platform-objects-owned bundle)", + "field help — open a contact's form as the zh-CN admin and read lead_score's help: the showcase bundle carries objects.showcase_contact.fields.lead_score.help 由评分规则计算,不可手动编辑。 (examples/app-showcase/src/system/translations/index.ts) over the source inlineHelpText 'Computed by scoring rules — not user-editable.' (examples/app-showcase/src/data/objects/contact.object.ts). Before this step no variant read a help string, so the negatives reached it only incidentally", "runtime i18n routes (server truth, consulted independently of the browser — these are what the client resolves labels through): GET /api/v1/i18n/locales and record data.locales", "GET /api/v1/i18n/translations/zh-CN — record whether it is non-empty and whether every key path sits under a declared translation group", "GET /api/v1/i18n/labels/showcase_task/zh-CN — record its { object, locale, labels } and hold it next to the zh-CN list-header the browser rendered for showcase_task", @@ -307,12 +309,13 @@ "Studio metadata forms (metadataForms)", "sys_ object labels (platform-objects bundles)", "relative dates (locale formatting, not a translation group)", - "activity-feed / audit verb templates (E11 — a translatable surface not previously in the variant list)" + "activity-feed / audit verb templates (E11 — a translatable surface not previously in the variant list)", + "field help (objects..fields..help) — showcase_contact's lead_score: source inlineHelpText 'Computed by scoring rules — not user-editable.', zh-CN 由评分规则计算,不可手动编辑。" ], "traps": ["stale-console-bundle", "hydration-race", "wrong-panel", "dispatcher-vs-hono-route"], "automated": { "kind": "unit", - "ref": "objectui code under ui/, for acceptance[5]'s dialog COPY only: ui/packages/components/src/__tests__/page-header-action-i18n.test.tsx ('renders the translated button AND opens the confirm dialog with the translated body', 'passes the translated successMessage to the toast') + ui/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.actionText-i18n.test.tsx ('resolves the button AND the confirm dialog body from the same bundle entry', 'resolves successMessage from the same entry'). Each runs the real ActionRunner under a real I18nProvider holding a zh-CN `_actions.NAME` entry, but the action handler, the confirm handler and the toast handler are vi.fn stubs: no request leaves the test, so acceptance[5]'s network clause (the toast attached to a real 2xx round-trip) stays the run's. Run them from this checkout's ui/ directory, where the vitest config lives: neither package carries its own, and each package's test script is `vitest run --root ../..`. The command is `cd ui && pnpm exec vitest run --maxWorkers=2 packages/components/src/__tests__/page-header-action-i18n.test.tsx packages/plugin-detail/src/renderers/__tests__/record-quick-actions.actionText-i18n.test.tsx` and cite the output naming the commit (QA run #23951 ran them in objectui at daeb77fc84dd, 2 files, 7/7). acceptance[6], [7] and [9] have no pin: packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts reaches the i18n routes over HTTP (I18N_FACES, the signed-in member's 200 control), but asserts only that en and zh-CN are among the locales and one translated label, never the exact locale set, the group vocabulary or the zz-ZZ empty-map shape. Every other clause still needs the run" + "ref": "objectui code under ui/, for acceptance[5]'s dialog COPY only: ui/packages/components/src/__tests__/page-header-action-i18n.test.tsx ('renders the translated button AND opens the confirm dialog with the translated body', 'passes the translated successMessage to the toast') + ui/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.actionText-i18n.test.tsx ('resolves the button AND the confirm dialog body from the same bundle entry', 'resolves successMessage from the same entry'). Each runs the real ActionRunner under a real I18nProvider holding a zh-CN `_actions.NAME` entry, but the action handler, the confirm handler and the toast handler are vi.fn stubs: no request leaves the test, so acceptance[5]'s network clause (the toast attached to a real 2xx round-trip) stays the run's. Run them from this checkout's ui/ directory, where the vitest config lives: neither package carries its own, and each package's test script is `vitest run --root ../..`. The command is `cd ui && pnpm exec vitest run --maxWorkers=1 packages/components/src/__tests__/page-header-action-i18n.test.tsx packages/plugin-detail/src/renderers/__tests__/record-quick-actions.actionText-i18n.test.tsx` and cite the output naming the commit (QA run #23951 ran them in objectui at daeb77fc84dd, 2 files, 7/7). acceptance[6], [7] and [9] have no pin: packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts reaches the i18n routes over HTTP (I18N_FACES, the signed-in member's 200 control), but asserts only that en and zh-CN are among the locales and one translated label, never the exact locale set, the group vocabulary or the zz-ZZ empty-map shape. Every other clause still needs the run" }, "source": [ "packages/spec/src/system/translation.zod.ts#appTranslationDataShape (appTranslationDataShape — the fifteen-group vocabulary an APPLICATION may author: objects (with _views/_actions/_sections), picklists, apps (navigation), messages, globalActions, dashboards, datasets, reports, pages, flows, positions, permissions, books, metadataForms, settingsCommon. `settings` is platform-only since #15178 and lives in platformSettingsShape, spread into PlatformTranslationDataSchema only — TranslationItemSchema refuses it too since #19620)", @@ -350,6 +353,12 @@ "date": "2026-10-31", "change": "Three findings, from QA runs #27441 and #27438. steps[7]: the contact's New dialog renders the create formView, whose one section who_is_this shows as 这是谁. The four contact sections are read on the default form, which renders on Edit, and the step now says so. automated.ref: the two ui/ pins run from ui/, because @object-ui/components and @object-ui/plugin-detail carry no vitest config of their own (their test scripts are `vitest run --root ../..`). The command is now spelled out. acceptance[7]: the group counts were stale since hooks joined the per-app vocabulary (#26831). Per translation.zod.ts at this base, TranslationDataSchema has 16 groups, PlatformTranslationDataSchema 17 (+settings) and ServedTranslationDataSchema 16 (the 17 less metadataForms). The 16 served groups are now listed by name, which is more precise than a count. No clause's assertion changed. Read from source at base 22ce3d13. Wave anchor #27377, wave-3 accuracy slice S5 (#28339); findings parked in anchor comments 6490438726 and 6494110310", "ref": "#27441" + }, + { + "revision": 14, + "date": "2026-11-04", + "change": "automated.ref, variants, steps and fixtures.knownGaps[3] corrected from QA run #29890 (W5-FAIL, at 589049b0): the pins run at --maxWorkers=1 (RUNNER's container cap); a field-help variant and its step are added (showcase_contact lead_score, translated help 由评分规则计算,不可手动编辑。), placed after the sys_ object step so steps[6] keeps its number; knownGaps[3] was stale, because platform-objects' zh-CN bundle translates sys_session revoke_my_other_sessions' confirmText, a stock confirm specimen (read from source, not yet driven live). Read at base f68b47ca", + "ref": "#29906" } ] }, diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index c0377989ef3..ca642d82630 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -897,7 +897,7 @@ "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, requires manage_platform_settings, bounds sentBy, from, headers, organizationId and identityMail, refuses anonymous, and 400s malformed input", "since": "v15", "status": "active", - "revision": 13, + "revision": 14, "priority": "P2", "surface": "mixed", "fixtures": { @@ -925,13 +925,13 @@ "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 the scratch notify flow of knownGaps[1] (POST /api/v1/automation; channels ['email'], template, templateData; fired through its api trigger); capture the rendered subject/body from the sys_email row it lands (the dev log transport logs no body)", "drive a send MISSING the required `title`; capture the fast failure", - "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, to a zh-locale recipient (a tag the bundle folds) and to a recipient whose language the bundle lacks (e.g. ja-JP); capture which row rendered each. The showcase declares i18n fallbackLocale 'en' (examples/app-showcase/objectstack.config.ts), which folds to the en-US row", + "make the template inactive (a Studio save of the item with active:false, which on the read-only showcase goes into the pair com.example.showcase.custom as steps[6] says, 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, to a zh-locale recipient (a tag the bundle folds) and to a recipient whose language the bundle lacks (e.g. ja-JP); capture which row rendered each. The showcase declares i18n fallbackLocale 'en' (examples/app-showcase/objectstack.config.ts), which folds to the en-US row. To read one member back by name, select it with ?member= (GET /api/v1/meta/email_template/NAME?member=zh-CN): on that read ?locale= is the response's translation locale and selects no member (META_ITEM_MEMBER_PARAM, packages/rest/src/meta-item-read-gate.ts)", "author a template with fromOverride + replyTo and verify both on the outbound message. fromOverride is an object { name?, address } with address an email (EmailAddressInlineSchema, packages/spec/src/system/email-template.zod.ts) and replyTo a plain address string; a string fromOverride is refused at save, 422 INVALID_METADATA 'expected object'", - "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", + "edit the template wording as an admin in Studio, 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 read-only showcase Studio's Customize saves the edit into the pair com.example.showcase.custom; over HTTP the same write is POST /api/v1/packages/com.example.showcase/ensure-customization, then PUT /api/v1/meta/email_template/showcase_task_done_email?package=com.example.showcase.custom with overrides: {package:'com.example.showcase', basedOn:'0.1.0'}. A package-less PUT of the shipped name answers 403 NOT_OVERRIDABLE since ADR-0146 D11 (packagelessOverlayWriteRefusal, packages/metadata-protocol/src/protocol-packaged-base-refusal.ts)", "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). Keep the reboot out of it: under os dev's default watcher a declared-source save recompiles AND restarts the server (packages/cli/src/commands/dev.ts, --restart defaults on), which proves nothing about a reload. Compile into the running copy's artifact with objectstack compile --output COPY/dist/objectstack.json, or boot the copy with os dev --no-restart (the watcher then only rebuilds the artifact), and read the listener's PID before and after (lsof -ti tcp:PORT): an unchanged PID is part of the evidence, and a changed one means this half was not driven. 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 four ways: (a) as an admin (holding manage_platform_settings) 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; (d) as a member WITHOUT manage_platform_settings (mint one through admin create-user), and as the admin with a `from` other than the configured sender, with a `sentBy` naming another user, with a `headers` value that is not a plain object of single-line strings (a list of entries, say), with an `organizationId` other than the admin's active organization, and with `identityMail: true` → capture each refusal and confirm no new sys_email or sys_identity_email row; then send once more as the admin with no `organizationId` and confirm the new sys_email row's organization_id is the admin's active organization", + "drive the raw send door POST /api/v1/email/send four ways: (a) as an admin (holding manage_platform_settings) 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, and a recipient whose display name is not a string (e.g. to: {name: 42, address: 'a@example.com'}) → capture the 400 VALIDATION_FAILED naming the display name (since #29891); after each 400 VALIDATION_FAILED read sys_email for the 'failed' row it leaves; (d) as a member WITHOUT manage_platform_settings (mint one through admin create-user), and as the admin with a `from` other than the configured sender, with a `sentBy` naming another user, with a `headers` value that is not a plain object of single-line strings (a list of entries, say), with an `organizationId` other than the admin's active organization, and with `identityMail: true` → capture each refusal and confirm no new sys_email or sys_identity_email row; then send once more as the admin with no `organizationId` and confirm the new sys_email row's organization_id is the admin's active organization", "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": [ @@ -956,7 +956,7 @@ { "clause": "(name, locale) resolution walks the ladder of record, SendTemplateInput.locale in packages/spec/src/contracts/email-service.ts: two rows with one name are an i18n bundle, the name is matched exactly and only the locale member is resolved, and every rung loads one row at one tag exactly, so en and en-US stay two distinct rows (folding chooses which row is read, it never merges two). A call that NAMES a locale, over the built-in registry template loader, walks: (1) this locale, folded against the bundle's locale tags by resolveBundleLocale — exact, then case-insensitive (zh-cn → zh-CN), then base language (en-GB → en), then variant expansion (zh and zh-TW → zh-CN), the first match in ascending tag order; (2) the stack's declared fallback locale (II18nService.getFallbackLocale(): i18n.fallbackLocale, else defaultLocale), folded the same way, skipped when none is declared; (3) en-US, exactly; (4) TEMPLATE_NOT_FOUND, which is permanent. A rung that resolves to a tag already tried is skipped; a replacement TemplateLoader keeps the exact ladder. A call that names no locale starts at en-US exactly, with no fold and no declared fallback, then takes the loader's own no-locale answer (the built-in loader: the bundle's lowest locale tag)", "oracle": "log", - "verify": "the zh-CN recipient gets the zh-CN render; the zh recipient gets the zh-CN render too (variant expansion); a recipient whose language the bundle lacks (e.g. ja-JP) gets the en-US row, through the showcase's declared fallback en folding to en-US. A zh recipient getting the en-US row is the FAIL", + "verify": "the zh-CN recipient gets the zh-CN render; the zh recipient gets the zh-CN render too (variant expansion); a recipient whose language the bundle lacks (e.g. ja-JP) gets the en-US row, through the showcase's declared fallback en folding to en-US. A zh recipient getting the en-US row is the FAIL. The clause's no-locale branch cannot be driven from the HTTP door: the email channel always names a locale, the recipient's sys_user.locale or else the deployment default (resolveRecipientLocale, packages/services/service-messaging/src/recipient-locale.ts), so score that branch from source, never as a live FAIL", "evidence": "the two renders" }, { @@ -966,7 +966,7 @@ "evidence": "the captured message" }, { - "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", + "clause": "both provenance protections hold: an admin's Studio edit (an override of the declared item, saved in the pair com.example.showcase.custom, ADR-0146 D11) survives redeploys, AND a declared-source edit is served on metadata reload without a restart", "oracle": "api", "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" @@ -980,7 +980,7 @@ { "clause": "the raw transactional send door (POST /api/v1/email/send → IEmailService.send, complementary to the sendTemplate path above) authenticates, authorizes and validates: an AUTHED well-formed message from a caller holding manage_platform_settings — { to, subject } plus AT LEAST ONE of `html` / `text` — lands at the dev transport (200 whose body IS the send result, top level — { id, status: 'sent', messageId } — with no `result` wrapper and no success envelope; packages/rest/src/email-routes.ts answers res.json(result); its own refusals are flat { code, error }, e.g. { code: 'INVALID_REQUEST', error: 'JSON body required' }), sent from the configured sender, attributed (sent_by) to the caller and recorded (organization_id) under the caller's active organization; an authenticated caller WITHOUT that capability is refused 403 PERMISSION_DENIED and nothing is sent; a `sentBy` naming another user, a `from` naming an address other than the configured sender, a `headers` value that is not a plain object mapping header names to single-line strings, a custom From / Sender / Return-Path header, an `organizationId` other than the caller's active organization, and an `identityMail` other than false are each refused 403 PERMISSION_DENIED before any send; an ANONYMOUS send is refused 401 UNAUTHENTICATED (the #3963 unconditional gate — the api.requireAuth opt-out is retired); a MALFORMED body is refused 400 with a ledgered envelope code (INVALID_REQUEST for a non-object body, VALIDATION_FAILED for a bad message shape) — never a 500 for caller-fixable input, and a runtime with no email provider answers 501 NOT_IMPLEMENTED rather than a fake success", "oracle": "api", - "verify": "the POST /api/v1/email/send responses: admin 200 + dev-transport capture, anonymous 401 UNAUTHENTICATED, malformed 400 with the named code, member 403 PERMISSION_DENIED, foreign from / sentBy / organizationId, off-shape headers and identityMail 403 PERMISSION_DENIED with no new mail-log row, and the admin's row stamped with the admin's active organization (email-routes.ts registerEmailEndpoints: enforceAuth, the capability / sentBy / sender / tenant bounds → 403 PERMISSION_DENIED, non-object→400 INVALID_REQUEST, VALIDATION_FAILED passthrough, 501 no-provider). ⚠ WIRE KEYS (corrected, run #7690): the send input is SendEmailInput { to, subject, text?, html?, from?, cc?, bcc?, replyTo?, … } (packages/spec/src/contracts/email-service.ts) — NOT `bodyHtml`. `bodyHtml`/`bodyText` are the email-TEMPLATE authoring fields (this item's own variants list at the `body:` and alias lines says so); posting them to this door is refused 400 'VALIDATION_FAILED: at least one of text or html is required' (packages/plugins/plugin-email/src/email-service.ts). A 400 here is the run using the wrong vocabulary, not a product defect — re-send with `html` before filing anything", + "verify": "the POST /api/v1/email/send responses: admin 200 + dev-transport capture, anonymous 401 UNAUTHENTICATED, malformed 400 with the named code (a non-string display name too: 400 VALIDATION_FAILED '… the display name of … must be a string', formatCallerAddress in packages/plugins/plugin-email/src/email-service.ts, since #29891; a message the service refuses 400 VALIDATION_FAILED still records a sys_email row with status 'failed' and error 'rejected before delivery: …', recordRejectedMessage, while the route's own refusals, 401, 403 and 400 INVALID_REQUEST, record none), member 403 PERMISSION_DENIED, foreign from / sentBy / organizationId, off-shape headers and identityMail 403 PERMISSION_DENIED with no new mail-log row, and the admin's row stamped with the admin's active organization (email-routes.ts registerEmailEndpoints: enforceAuth, the capability / sentBy / sender / tenant bounds → 403 PERMISSION_DENIED, non-object→400 INVALID_REQUEST, VALIDATION_FAILED passthrough, 501 no-provider). ⚠ WIRE KEYS (corrected, run #7690): the send input is SendEmailInput { to, subject, text?, html?, from?, cc?, bcc?, replyTo?, … } (packages/spec/src/contracts/email-service.ts) — NOT `bodyHtml`. `bodyHtml`/`bodyText` are the email-TEMPLATE authoring fields (this item's own variants list at the `body:` and alias lines says so); posting them to this door is refused 400 'VALIDATION_FAILED: at least one of text or html is required' (packages/plugins/plugin-email/src/email-service.ts). A 400 here is the run using the wrong vocabulary, not a product defect — re-send with `html` before filing anything", "evidence": "the responses + the dev-transport landing (plus the wrong-vocabulary refusal, if the probe was driven)" } ], @@ -1084,6 +1084,12 @@ "date": "2026-11-02", "change": "CORRECTION: acceptance[3] said (name, locale) resolution is an EXACT match with no language-subtag folding, and its verify line scored a zh recipient getting the en-US row as the pass. Since #28880 a named locale, over the built-in registry template loader, is folded against the bundle's tags by resolveBundleLocale, then the stack's declared fallback locale is tried folded the same way, then en-US, then TEMPLATE_NOT_FOUND, so a run against main scored the clause wrong. The clause now copies the ladder of record, SendTemplateInput.locale in packages/spec/src/contracts/email-service.ts, and keeps en and en-US as two rows of one bundle; the verify line and steps[4] add a zh recipient (folds to zh-CN) and a recipient whose language the bundle lacks (declared fallback en folds to en-US); variants[0] names the fold and fallback rungs; automated.ref no longer calls the uncovered half 'the no-folding case' and names the plugin-email fold pins below the oracle's layer. Read from source at base 8ca9dd7b", "ref": "#29032" + }, + { + "revision": 14, + "date": "2026-11-04", + "change": "steps[3], [4], [6] and [9], acceptance[3].verify, acceptance[5].clause and acceptance[7].verify corrected from QA run #29890 (W5-FAIL, at 589049b0). (1) ADR-0146 D11: a package-less PUT of the shipped template answers 403 NOT_OVERRIDABLE, so the admin edit is an override saved in com.example.showcase.custom through the ensure door. (2) A non-string display name is 400 VALIDATION_FAILED since #29891, and every message the service refuses 400 VALIDATION_FAILED leaves a sys_email row 'failed' ('rejected before delivery: …'). (3) The clause's no-locale branch is unreachable over HTTP: the email channel always names the recipient's locale or the deployment default. (4) A member is read with ?member=, which ?locale= does not select. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2018,7 +2024,7 @@ "title": "Declarative connector static auth kinds (none/api-key/basic/bearer) resolve credentialRef at materialization and land on the wire in exactly their header/query shape; an unresolvable credentialRef degrades its instance, naming the reference, instead of failing the boot; every authored auth kind refuses an inline secret, and oauth2 is open tier with strict keys and a required grant (ADR-0143 D1)", "since": "v15.1", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "api", "fixtures": { @@ -2044,7 +2050,7 @@ ], "steps": [ "start the echo upstream; export the probe env vars (e.g. QA_BEARER_TOK, QA_API_KEY, QA_BASIC_PW); author five scratch rest-provider instances against it in a writable package: auth none / {type:'bearer',credentialRef:'QA_BEARER_TOK'} / {type:'basic',username:'qa',credentialRef:'QA_BASIC_PW'} / {type:'api-key',credentialRef:'QA_API_KEY'} / {type:'api-key',credentialRef:'QA_API_KEY',paramName:'api_key'}; boot", - "dispatch each instance's `request` action (through a runtime flow: POST /api/v1/automation with a start → connector_action node, then POST /api/v1/automation/NAME/trigger, the vehicle QA run #24615 used on a stock-shaped boot; no HTTP route dispatches a connector action directly) at an echo path; capture what the upstream received per instance", + "dispatch each instance's `request` action (through a runtime flow: POST /api/v1/automation with a start → connector_action node, then POST /api/v1/automation/NAME/trigger, the vehicle QA run #24615 used on a stock-shaped boot; no HTTP route dispatches a connector action directly) at an echo path; capture what the upstream received per instance. The node's connectorConfig takes only connectorId, actionId and input (a closed block, packages/spec/src/automation/flow.zod.ts), so an outputVariable there is refused; a later node reads the action's output under the node's id (NODE_ID.KEY, packages/services/service-automation/src/dag-traversal.ts). Take the evidence from the echo upstream's own capture: a reflecting echo returns the credentials it received, and that output lands in the flow run's variables, so never grep run records or paste them into evidence", "read the authored rows back over GET /api/v1/meta and GET /api/v1/automation/connectors; grep both full bodies for every resolved secret value", "unset QA_BEARER_TOK and boot again; capture the boot log's degrade line verbatim, the bearer instance's entry on GET /api/v1/automation/connectors, and what a dispatch to it (the step-2 vehicle) answers", "in a scratch copy, author and build (objectstack compile) one entry per case, each alone: the well-formed oauth2 control {type:'oauth2', grant:'client_credentials', tokenUrl:'https://example.invalid/token', clientId:'qa', credentialRef:'QA_OAUTH_SECRET'} (expected to build clean); that control without `grant`; that control plus clientSecret:'x'; that control plus accessToken:'x'; a bearer auth plus token:'x'; an auth {type:'oauth1', credentialRef:'X'}; and a catalog/instance entry with inline authentication {type:'api-key', key:'x'}; capture the control's clean build and each rejection's text and path", @@ -2138,6 +2144,12 @@ "date": "2026-11-02", "change": "acceptance[3] and negative[1] re-pointed to ADR-0143 D1 (Accepted 2026-10-28), which made outbound connector OAuth open tier and reversed the ADR-0097 §3 enterprise placement this item pinned. QA run #29151 (18.3.0 W4-L2, 7fa9873f) FAILed both: an oauth2 instance builds clean, as the ADR says it must. ConnectorInstanceAuthSchema now has seven strict arms (none, bearer, api-key, basic, oauth2, app-credential, signed). The title, variants[5], steps[4], acceptance[3], negative[1], automated.ref and source[0] now assert the contract that holds: an inline secret is refused by name on every kind; oauth2 keys are strict (clientSecret → the credentialRef guidance, accessToken / refreshToken → the runtime-token guidance); an oauth2 auth with no `grant` fails at auth.grant; an unknown kind fails at auth.type; the #7990 inline-`authentication` refusal stands; and a well-formed oauth2 control building clean is the expected outcome, refusing it the FAIL. acceptance[3].verify drops the AuthConfigSchema oauth2 note (that runtime variant was retired by D7) and cites the three per-kind pins. No item yet covers the per-dispatch grant kinds (oauth2 / app-credential / signed) at runtime. Read on main 0d15f895", "ref": "#29154" + }, + { + "revision": 7, + "date": "2026-11-04", + "change": "steps[1] corrected from QA run #29890 (W5-FAIL, at 589049b0): connectorConfig is closed (connectorId, actionId, input), so an outputVariable is refused and the output is read under the node's id; a reflecting echo writes the received credentials into the run's variables, so the evidence is the echo's capture, never the run records. Read at base f68b47ca", + "ref": "#29906" } ] }, diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 4f69ee9a25d..01e6f9624c2 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -1573,7 +1573,7 @@ "title": "The package manifest is enforced at the install boundary: an incompatible `engines` range is refused before the registry write, the namespace gate holds ownership, and a namespace-less runtime package has one derived", "since": "v15", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "api", "personas": [ @@ -1657,7 +1657,7 @@ ], "source": [ "packages/metadata-core/src/protocol-handshake.ts#resolveDeclaredRange — resolveDeclaredRange (protocol → platform → legacy engine.objectstack); rangeAdmitsMajor (null = unrecognized, admit-with-warning); checkProtocolCompat (the OS_PROTOCOL_INCOMPATIBLE diagnostic and its migrateCommand); assertProtocolCompat (the four arms: ok silent, no-range warn, unparsed-range warn, incompatible throw)", - "packages/metadata-protocol/src/protocol.ts#assertProtocolCompat — namespace derivation for namespace-less runtime packages, then assertProtocolCompat(manifest) BEFORE registry.installPackage", + "packages/metadata-protocol/src/protocol-package-install.ts#installPackage — namespace derivation for namespace-less runtime packages, then assertProtocolCompat(manifest) (declared in packages/metadata-core/src/protocol-handshake.ts) BEFORE registry.installPackage", "packages/objectql/src/registry.ts#NamespaceConflictError — the ADR-0048 install-time namespace gate (shareable exemption, same-package exclusion, OS_METADATA_COLLISION=warn downgrade); NamespaceConflictError", "ADR-0087 (metadata protocol upgrade contract) D1 · ADR-0048 (cross-package metadata collision) · ADR-0025 §3.10 #3 (protocol-first precedence)", "content/docs/releases/v15.mdx — 'ADR-0087 completed (#2972)': the handshake on boot-time durable rehydration and on code-defined AppPlugin loads, plus the objectstack lint nudge", @@ -1711,6 +1711,12 @@ "date": "2026-10-31", "change": "steps[2] gains the AHEAD probe and acceptance[1] scopes migrateCommand to its direction. Checklist-accuracy finding of QA run #27392 (18.3.0 wave 3, parked on anchor #27377; slice S4, #28338): acceptance[1] said the refusal always carries a `migrate meta --from` command, but since #26480 checkProtocolCompat (packages/metadata-core/src/protocol-handshake.ts) leaves migrateCommand out when the declared target major is above the runtime's and names the upgrade remedy instead. The run drove that leg off-clause (^19, >18, engines.platform ^19 and legacy ^19.0.0 on an 18 runtime: 422 OS_PROTOCOL_INCOMPATIBLE, no migrateCommand). The older-direction clause is unchanged. Re-read at 3fd80337", "ref": "#27392" + }, + { + "revision": 9, + "date": "2026-11-04", + "change": "source[1] re-pointed: its anchor, assertProtocolCompat on packages/metadata-protocol/src/protocol.ts, no longer resolves (the install path moved to protocol-package-install.ts#installPackage, which calls assertProtocolCompat from metadata-core), and scripts/check-platform-checklist.mjs refused the area file at base f68b47ca. Carried by #29906 because that PR edits this file. No clause changed", + "ref": "#29906" } ] }, @@ -2779,7 +2785,7 @@ "title": "Shell-hard-coded Administration targets + the retired System hub: every Administration target URL resolves to a real page (no RouteNotFound, no RecordDetailView for an object named 'system'), /apps/:app/system lands on the settings hub in one hop and every hub entry resolves, the five legacy system/* URLs redirect (users and organizations to their object routes, roles, positions and permissions to Studio's All metadata catalog in its environment scope), and a member's shell offers no Administration entry", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "browser", "personas": [ @@ -2799,7 +2805,7 @@ "steps": [ "as admin, enumerate the Administration target list from source — UnifiedSidebar's adminItems: sys-settings → /apps/setup/system, sys-apps → …/system/apps, sys-marketplace → …/system/marketplace, sys-datasources → /apps/setup/metadata/datasource, sys-users → …/system/users, sys-orgs → …/system/organizations, sys-roles → …/system/roles, sys-config → …/system/settings (8 entries: sys-objects left with #28818, ADR-0146 D1 — Setup carries no metadata editor or catalog; sys-datasources keeps the canonical /metadata/datasource route, #3660, and stays in Setup, the one metadata type D1 keeps there). No sidebar renders this list at the pin (see knownGaps), so it is walked as URLs", "navigate to EVERY target URL directly: each lands on a real page — screenshot first, then assert no RouteNotFound, no 'No Apps Configured' empty state, and above all no RecordDetailView rendering for an object literally named 'system' (the historical failure: short words fell through to the record-id tail — objectui#3655's two measured landings)", - "open /apps/setup/system: it redirects in ONE hop to /apps/setup/system/settings (SystemLandingRedirect — the SystemHubPage card wall, its cards and count badges, was retired by objectui#3743, carried out in objectui#10507), where SettingsHub lists the settings manifests the server returns for this user; click every hub entry the same way. The ungated bare system/ai-approvals route is ai.console-ai-surface-gating's subject, not this walk's", + "open /apps/setup/system: besides the console's app-id canonicalisation hop, which rewrites the alias /apps/setup/… to the package-id address /apps/com.objectstack.setup/… (canonicalAppPathname, ui/packages/app-shell/src/utils/appRoute.ts, #26183), it redirects in ONE hop to …/system/settings (SystemLandingRedirect — the SystemHubPage card wall, its cards and count badges, was retired by objectui#3743, carried out in objectui#10507), where SettingsHub lists the settings manifests the server returns for this user; click every hub entry the same way. The ungated bare system/ai-approvals route is ai.console-ai-surface-gating's subject, not this walk's", "legacy redirects: navigate directly to /apps/setup/system/users, …/organizations, …/roles, …/positions, …/permissions. users and organizations redirect to their object routes per the #3655 map (users→sys_user, organizations→sys_organization list). roles, positions and permissions redirect in ONE hop to Studio's All metadata catalog in its environment scope (SystemCatalogRedirect → setupMetadataStudioPath, ADR-0146 D1, #28818; positions and permission sets are registry items, ADR-0131 D3): roles→/studio/~all/metadata/position?scope=environment [ADR-0090 D3 rename — 'Roles' and 'Positions' are one surface], positions→the same, permissions→/studio/~all/metadata/permission?scope=environment [#3655 decision A: the permission-set surface]. Each landing page renders", "as the plain member, open /home and one app: the shell offers no Administration entry anywhere — /home has no sidebar (HomeLayout omits it) and the top-bar Help menu offers documentation only; the server-side half (Setup app refusal for the member) is platform-core.builtin-apps-nav-render clause 2/3 — cross-reference, do not re-score here" ], @@ -2813,19 +2819,19 @@ { "clause": "every Settings-hub entry reached from /apps/:app/system (which redirects in one hop to /system/settings) resolves to a live page.", "oracle": "screenshot", - "verify": "the navigation trace for /apps/setup/system shows the single redirect to …/system/settings; then a per-entry walk of SettingsHub's entries, screenshot before DOM", + "verify": "the navigation trace for /apps/setup/system shows the single redirect to …/system/settings, beside the app-id canonicalisation hop to /apps/com.objectstack.setup/… (canonicalAppPathname: a change of address, not a stop on another page); then a per-entry walk of SettingsHub's entries, screenshot before DOM", "evidence": "the redirect trace + the per-entry verdict table" }, { "clause": "the five legacy system/* URLs redirect and land on rendering pages — bookmarks and the URLs the retired hub once emitted keep working. users and organizations land on their object routes (sys_user, the sys_organization list — the #3655 map). roles and positions land in ONE hop on Studio's All metadata catalog at /studio/~all/metadata/position?scope=environment (the ADR-0090 rename, one surface under two vocabularies), and permissions at /studio/~all/metadata/permission?scope=environment (decision A: rules-and-assignments is layer 2) — Studio's catalog since #28818 (ADR-0146 D1: Setup carries no metadata catalog), never the sys_position / sys_permission_set object routes (ADR-0131 D3: positions and permission sets are registry items)", "oracle": "network", - "verify": "the navigation trace per legacy URL shows the redirect target: the object route for users and organizations, /studio/~all/metadata/TYPE?scope=environment for roles, positions and permissions, reached without a stop on any /apps/setup/metadata/… URL; the landing renders. QA run #28981 measured exactly these landings at 7fa9873f. A roles, positions or permissions URL landing on the Setup catalog (/apps/setup/metadata/TYPE) or on the sys_position / sys_permission_set object route is a pre-#28818 shape, so suspect the stale-console-bundle trap before scoring it", + "verify": "the navigation trace per legacy URL shows the redirect target: the object route for users and organizations, /studio/~all/metadata/TYPE?scope=environment for roles, positions and permissions, reached without a stop on any /apps/setup/metadata/… URL (the app-id canonicalisation hop to /apps/com.objectstack.setup/… may come first, canonicalAppPathname); the landing renders. QA run #28981 measured exactly these landings at 7fa9873f. A roles, positions or permissions URL landing on the Setup catalog (/apps/setup/metadata/TYPE) or on the sys_position / sys_permission_set object route is a pre-#28818 shape, so suspect the stale-console-bundle trap before scoring it", "evidence": "the five redirect traces + landings" }, { - "clause": "a member's shell offers no Administration entry anywhere (no /home sidebar — HomeLayout omits it; the Help menu offers documentation entries, Keyboard shortcuts and About — nothing that administers); the server refusal is proven by the builtin-apps item.", + "clause": "a member's shell offers no Administration entry anywhere (no /home sidebar — HomeLayout omits it; the Help menu offers documentation entries and About, plus Keyboard shortcuts inside an app — nothing that administers); the server refusal is proven by the builtin-apps item.", "oracle": "dom", - "verify": "the member's /home and one app shell, in the member's own session (wrong-persona): screenshot, then DOM — no Administration entry in any chrome region, and the Help menu lists documentation entries, Keyboard shortcuts and About and no administration entry (objectui packages/app-shell/src/layout/AppHeader.tsx at the console pin)", + "verify": "the member's /home and one app shell, in the member's own session (wrong-persona): screenshot, then DOM — no Administration entry in any chrome region, and the Help menu lists documentation entries and About — on /home exactly All documentation, Online documentation, About, and inside an app also the app's own docs entry and Keyboard shortcuts, which AppHeader.tsx draws only in an app — and no administration entry (objectui packages/app-shell/src/layout/AppHeader.tsx at the console pin)", "evidence": "the member screenshots + DOM excerpt" } ], @@ -2878,6 +2884,12 @@ "date": "2026-11-02", "change": "Re-pointed to main after N-28 (#28818, ADR-0146 D1). Measured input: QA run #28981 (18.3.0 wave 4, 7fa9873f; card note 6571282495), which failed acceptance[2] as stale spec: /apps/setup/system/roles, /positions and /permissions land in one hop on /_console/studio/~all/metadata/position?scope=environment and …/permission?scope=environment, and the routing pins (AppContent.systemHubRoutes, legacyRedirects) assert the Studio landing. Re-read at base dfbe34d887: acceptance[2] and steps[3] name the Studio landing; the Administration count is read from source, per the seat's ruling — UnifiedSidebar adminItems has 8 entries (sys-objects removed by #28818), so steps[0], acceptance[0] and fixtures.knownGaps[1] say 8, and the title names Studio's catalog; source[0]–[2] re-point to ui/ paths and SystemCatalogRedirect's Studio target. No clause weakened", "ref": "#28579" + }, + { + "revision": 7, + "date": "2026-11-04", + "change": "steps[2], acceptance[1].verify, acceptance[2].verify and acceptance[3] corrected from QA run #29890 (W5-FAIL, at 589049b0): the console rewrites the alias /apps/setup to /apps/com.objectstack.setup (canonicalAppPathname, #26183), a hop the traces show beside the one redirect, and the Help menu offers Keyboard shortcuts only inside an app (on /home: All documentation, Online documentation, About). Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -4037,6 +4049,114 @@ "ref": "#28579" } ] + }, + { + "id": "platform-core.package-export-round-trip", + "title": "A deployment's whole ontology exports in one read and installs again on a fresh boot: GET /api/v1/packages/export answers every package the deployment's code does not register, once each and dependencies first, each entry equal to that package's own export, and os package export writes them as installable directories", + "since": "v18.3", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "mixed", + "personas": [ + "admin (the seeded admin, holding studio.access): the reader of the export and the installer on the target boot", + "a member holding neither studio.access nor setup.access (the refusal leg, in its own context)", + "anonymous (no session): the 401 leg" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "TWO boots, each from its own scratch copy of the showcase on its own port and DB (dogfood §0): install-local writes .objectstack/installed-packages/ under the server's working directory, so two boots on one tree would share the installed set (RUNNER environment facts on install isolation). The source boot holds the fixture below; the target boot starts stock and receives the export", + "the source's ontology, the table of QA run #29870: (a) an authored package with an object, a flow and a connector whose auth carries a header or env map, written with POST /api/v1/packages, ?package= drafts and POST …/publish-drafts; (b) the showcase's pair, from POST /api/v1/packages/com.example.showcase/ensure-customization, holding an object_extension that adds a __c field; (c) one package installed through POST /api/v1/marketplace/install-local; (d) a second authored package that depends on (a), published and then disabled", + "the CLI on PATH (os) for the export and install legs, pointed at each boot with --url" + ] + }, + "steps": [ + "on the source boot, as the admin: GET /api/v1/packages and note each package's registry source and dependencies; then GET /api/v1/packages/export and record the order of data.packages and each entry's manifest id", + "for each entry, GET /api/v1/packages/ID/export and compare it with the entry byte for byte; then call client.packages.exportAll() (packages/client) and compare its bundle with the read", + "read (a)'s entry: find the connector's header / env map absent and named in withheldMaps, and search the whole bundle text for the header's name and value", + "read (d)'s entry (the disabled package): its objects and fields are present", + "os package export --out DIR against the source; list DIR and compare each NN-ID/objectstack.json with its entry; then run it again into the same DIR and record the exit code and that nothing changed", + "on the target boot, install each directory in the numbered order with os package install DIR/NN-ID/objectstack.json (or POST /api/v1/marketplace/install-local with the file): record each answer, the server log lines it writes, and then read every object, field, flow, connector and pair extension the source served", + "GET /api/v1/packages/export on the target and compare it with the source's read", + "negative pass: GET /api/v1/packages/export?x=1 as the admin; the read as the member; the read with no session; and on a third fresh boot install (d)'s directory before (a)'s" + ], + "acceptance": [ + { + "clause": "as a holder of studio.access or setup.access, GET /api/v1/packages/export answers every package whose registry row is not source: 'code' exactly once, a disabled one included, dependencies first: a pair after its base when both are listed, and the pair of a code base (com.example.showcase.custom) exported alone", + "oracle": "api", + "verify": "data.packages holds one entry per non-code package of GET /api/v1/packages, no code package and no duplicate; each package comes after every package it declares as a dependency (assemblePackageExportBundle orders them with orderHydrationBatch)", + "evidence": "the package list beside the bundle order" + }, + { + "clause": "each entry equals GET /api/v1/packages/:id/export for that package, byte for byte, and client.packages.exportAll() returns the same bundle", + "oracle": "api", + "verify": "per entry, the serialized entry equals the per-package read; the client call's bundle equals the HTTP read", + "evidence": "one line per package: equal or the first difference" + }, + { + "clause": "a connector's header or env map is absent from its entry and named in that entry's withheldMaps, by position only: no header name or value appears anywhere in the bundle text", + "oracle": "api", + "verify": "(a)'s entry carries withheldMaps naming the map's position and the connector keeps no copy of the map; a text search of the whole bundle for the header's name and its value finds neither (withheldConnectorMaps, packages/runtime/src/domains/packages.ts)", + "evidence": "the withheldMaps list and the two search results" + }, + { + "clause": "a disabled package's entry carries its own items. Fixed: #29871 (closed), where a disabled package exported as an empty shell", + "oracle": "api", + "verify": "(d)'s entry lists the objects and fields the package held before it was disabled (the registry read keeps a disabled package's items, includeDisabledPackages, packages/objectql/src/registry.ts); an entry with no items is a regression of #29871", + "evidence": "(d)'s entry beside its pre-disable read" + }, + { + "clause": "os package export --out DIR writes one NN-ID/objectstack.json per entry, numbered in the read's order, each equal to its entry", + "oracle": "build", + "verify": "DIR holds exactly one directory per entry, named NN-ID with NN its zero-padded place in data.packages, and each objectstack.json equals that entry (packages/cli/src/commands/package/export.ts)", + "evidence": "the DIR listing and one equality line per file" + }, + { + "clause": "on a fresh boot, installing each directory in order (os package install or POST /api/v1/marketplace/install-local) answers 200, serves every object, field, flow, connector and pair extension the source served, names each withheld map in its answer, and logs one WARN per package that has them", + "oracle": "api", + "verify": "each install answers 200; the target serves every item the source served; each answer for a package with withheld maps names them (judgeWithheldMaps, packages/cloud-connection/src/marketplace-install-local-plugin.ts) and the log holds one WARN line for that package; the CLI install prints them too (#29873)", + "evidence": "the install answers, the WARN lines and the target reads" + }, + { + "clause": "the target's GET /api/v1/packages/export equals the source's apart from withheldMaps. Fixed: #29872 (closed), where an exported pair lost the extension fields of a package installed through install-local on the second hop", + "oracle": "api", + "verify": "the two bundles are equal once withheldMaps is set aside, the pair's object_extension and its __c field included (assemblePackageManifest reads a package's extensions from the registry, registeredExtensionsOf, packages/runtime/src/domains/packages.ts); a pair entry without its extension is a regression of #29872", + "evidence": "the diff of the two bundles" + } + ], + "negative": [ + "any query parameter on GET /api/v1/packages/export answers 400 VALIDATION_ERROR naming it", + "a member holding neither studio.access nor setup.access is refused 403 PERMISSION_DENIED", + "an anonymous read answers 401", + "os package export --out DIR into a directory that already holds files exits 1 and writes nothing", + "installing an entry before the package it depends on answers 422 DEPENDENCY_MISSING" + ], + "traps": [ + "dispatcher-vs-hono-route", + "wrong-persona", + "destructive-in-place" + ], + "automated": { + "kind": "unit", + "ref": "Partial pins, none of which drives two booted deployments: packages/runtime/src/domains/packages-export-bundle.test.ts (the bundle through the dispatcher over a stub protocol: acceptance[0] and [1] in-process), packages/runtime/src/domains/packages-export-disabled-package.integration.test.ts (acceptance[3], #29871), packages/objectql/src/registry-extension-field-patches.test.ts and packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts (acceptance[6], #29872), packages/cli/test/package-export.test.ts (acceptance[4] and the --out refusal), and the dogfood set packages/qa/dogfood/test/package-export-bundle-round-trip.dogfood.test.ts, export-withheld-connector-maps.dogfood.test.ts and package-export-reinstall-custom-pair.dogfood.test.ts (acceptance[2], [5] and [6] on one deployment). The two-boot round trip is the run's leg" + }, + "source": [ + "packages/runtime/src/domains/packages.ts#assemblePackageExportBundle (GET /packages/export: every non-code package, dependencies first), packages/runtime/src/domains/packages.ts#assemblePackageManifest (each entry, as GET /packages/:id/export answers it), packages/runtime/src/domains/packages.ts#withheldConnectorMaps", + "packages/spec/src/api/package-lifecycle.zod.ts#PackageExportBundleSchema, packages/spec/src/api/package-lifecycle.zod.ts#PackageExportManifestSchema", + "packages/cli/src/commands/package/export.ts (os package export --out DIR: NN-ID/objectstack.json, refuses a non-empty DIR)", + "packages/cloud-connection/src/marketplace-install-local-plugin.ts#judgeWithheldMaps (the install answer and its WARN line)", + "packages/client/src/index.ts (packages.exportAll)", + "#29755 (GET /packages/export), #29746 (withheldMaps), #29871 and #29872 (fixed), #29873 (the CLI install names withheld maps); QA run #29870 (W5-EXPORT, at 589049b0)" + ], + "history": [ + { + "revision": 1, + "date": "2026-11-04", + "change": "new item from QA run #29870 (W5-EXPORT, at 589049b0), proposed by #29906 part D: no item covered GET /api/v1/packages/export (#29755), withheldMaps (#29746) or os package export. Acceptance[3] and [6] were proposed as failing (#29871, #29872); both cards are closed fixed at base f68b47ca, so the clauses state the fixed behaviour and name the card each regression would reopen. Read from source at base f68b47ca", + "ref": "#29906" + } + ] } ] } diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index 1fa6b169b38..fee8a9bec68 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -597,7 +597,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": 8, + "revision": 9, "priority": "P1", "surface": "browser", "personas": [ @@ -634,7 +634,7 @@ "in the grid saved view (inlineEdit: true), edit one cell in place; save; capture the PATCH; re-read the row via API", "on in_progress, with the export grant from fixtures.requires[3] bound, open the toolbar's Display popover — Export sits in its footer ('Export as CSV / XLSX / JSON'), not as a separate toolbar button — and export each declared format (csv, xlsx, json); capture the requests and files. Without the grant the Export entries are absent and the route answers 403 EXPORT_NOT_PERMITTED — the opt-in working as designed, not a fail of the export variant", "open the visualization switcher on All Tasks; enumerate the offered types; switch through each", - "on bulk_actions, select rows that are NOT done and run showcase_mark_done (per-record) — its visible gate, has(record.done) && record.done != true, judges each selected row, so a done row in the selection drops Mark Done from the bulk bar, or, over a mixed selection, the confirm reports the done rows not eligible and skipped (QA run #27438); either way the dispatch count stops matching the selection — and showcase_recalc_selection (aggregate — ONE dispatch: an api-type action, it POSTs to its own endpoint /api/v1/apps/showcase/recalc with `_selectedIds` at the body's top level); on legacy_row_actions confirm both named row actions resolve to live menu entries", + "on bulk_actions, select rows that are NOT done and run showcase_mark_done (per-record) — its visible gate, has(record.done) && record.done != true, judges each selected row, so a done row in the selection drops Mark Done from the bulk bar, or, over a mixed selection, the confirm reports the done rows not eligible and skipped (QA run #27438); either way the dispatch count stops matching the selection — and showcase_recalc_selection (aggregate — ONE dispatch: an api-type action, which the console sends through the action door, POST /api/v1/actions/showcase_task/showcase_recalc_selection with body {params:{_selectedIds:[…]}}; the server forwards those params flat to the action's own target /api/v1/apps/showcase/recalc in-process, buildApiActionRequest in packages/runtime/src/action-execution.ts, so the browser's network log shows the door request and no /recalc request); 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" ], "acceptance": [ @@ -687,9 +687,9 @@ "evidence": "dropdown screenshot + one screenshot per visualization" }, { - "clause": "bulk actions dispatch correctly by mode: per-record actions issue one dispatch per selected record; the aggregate def (showcase_recalc_selection, execution: 'aggregate', an api-type action) issues ONE request — POST /api/v1/apps/showcase/recalc — carrying every selected id in `_selectedIds` at the body's top level", + "clause": "bulk actions dispatch correctly by mode: per-record actions issue one dispatch per selected record; the aggregate def (showcase_recalc_selection, execution: 'aggregate', an api-type action) issues ONE request — POST /api/v1/actions/showcase_task/showcase_recalc_selection, the action door — carrying every selected id in `params._selectedIds`, which the server forwards flat to the action's target /api/v1/apps/showcase/recalc", "oracle": "network", - "verify": "count the captured POSTs against the selection size for each mode (the per-record selection holds not-done rows only, steps[8])", + "verify": "count the captured POSTs against the selection size for each mode (the per-record selection holds not-done rows only, steps[8]); for the aggregate mode the one POST is POST /api/v1/actions/showcase_task/showcase_recalc_selection with {params:{_selectedIds}} (QA run #29890 measured exactly one, answered 200)", "evidence": "network trace of both runs" }, { @@ -700,8 +700,8 @@ } ], "negative": [ - "a saved view whose filter is refused (400 INVALID_FILTER) while the grid silently shows ALL rows unfiltered is a FAIL — the objectui#3431 regression shape; zero rows with a captured refusal is the honest symptom, unfiltered rows is the lie. No stock view carries a refused filter, so stage one (QA run #24616): overlay PUT /api/v1/meta/view/showcase_task.done with filter [{field:'done', operator:'equals', value:'maybe'}] — authoring accepts it, every query refuses it (a non-boolean string against a boolean answers 400 INVALID_FILTER naming the field, packages/objectql/src/boolean-comparand-declared-type-door.ts) — and revert with DELETE /api/v1/meta/view/showcase_task.done. A filter on a missing field is refused at authoring (list-view-field-unknown), so it cannot stage this", - "tabs-element userFilters (or quickFilters) rendering on an OBJECT list view is a FAIL — ADR-0053 suppresses them there by design, as amended by ADR-0047: on an object list view only `element: 'tabs'` userFilters (and quickFilters) are ignored — objectui app-shell warnSuppressedListNav.ts, which logs the suppression — while `element: 'dropdown'` value chips ARE allowed there (examples/app-showcase/src/ui/views/project.view.ts ships them on All Projects, QA run #24616), so dropdown chips on an object list are not this FAIL", + "a saved view whose filter is refused (400 INVALID_FILTER) while the grid silently shows ALL rows unfiltered is a FAIL — the objectui#3431 regression shape; zero rows with a captured refusal is the honest symptom, unfiltered rows is the lie. No stock view carries a refused filter, so stage one in the showcase's customization pair. QA run #24616 staged it package-less; since ADR-0146 D11 a package-less PUT onto a name the showcase ships answers 403 NOT_OVERRIDABLE (packagelessOverlayWriteRefusal, packages/metadata-protocol/src/protocol-packaged-base-refusal.ts). So: POST /api/v1/packages/com.example.showcase/ensure-customization, then PUT /api/v1/meta/view/showcase_task.done?package=com.example.showcase.custom with the view's body, overrides: {package:'com.example.showcase', basedOn:'0.1.0'} and filter [{field:'done', operator:'equals', value:'maybe'}] — authoring accepts it, every query refuses it (a non-boolean string against a boolean answers 400 INVALID_FILTER naming the field, packages/objectql/src/boolean-comparand-declared-type-door.ts) — and revert with DELETE /api/v1/meta/view/showcase_task.done?package=com.example.showcase.custom. A filter on a missing field is refused at authoring (list-view-field-unknown), so it cannot stage this", + "tabs-element userFilters (or quickFilters) rendering on an OBJECT list view is a FAIL — ADR-0053 suppresses them there by design, as amended by ADR-0047: on an object list view only `element: 'tabs'` userFilters (and quickFilters) are ignored — objectui app-shell warnSuppressedListNav.ts, which logs the suppression — while `element: 'dropdown'` value chips ARE allowed there (examples/app-showcase/src/ui/views/project.view.ts ships them on All Projects, QA run #24616), so dropdown chips on an object list are not this FAIL. No stock object list view declares tabs userFilters; to stage one, write a view under a fresh name (a package-less write of a name no package ships still lands, ADR-0146 D11) or over a shipped view through the pair, as negative[0] does", "an export that returns the unfiltered table for a filtered view is a FAIL even though a file downloaded" ], "traps": [ @@ -768,6 +768,12 @@ "date": "2026-10-31", "change": "steps[3], acceptance[4] (clause and verify), steps[8], acceptance[8].verify, fixtures.requires[0] and source[0] corrected. From QA run #27438 (W3-C2b, at c4c515ad; wave anchor #27377, wave-3 accuracy slice S1). The tabular view's sort has been the array [{field:'estimate_hours', order:'desc'}] since #17053 retired the bare string clause platform-wide, so the objectui#2601 string-crash fixture no longer exists on stock. The clause now scores the array sort reaching the request as sort=-estimate_hours (the adapter's `-field` spelling, ui/packages/data-objectstack/src/index.ts). steps[8] says to select rows that are not done: Mark Done's per-row visible gate either drops the button or skips done rows as not eligible, and either breaks the one-dispatch-per-selected-row count. Read at base a0f865491d: task.view.ts (tabular, bulk_actions), ui/packages/plugin-grid/src/components/BulkActionDialog.tsx. The sort clause's contract (renders, server-ordered) is unchanged; a prior verdict scored on the string form does not carry", "ref": "#27438" + }, + { + "revision": 9, + "date": "2026-11-04", + "change": "steps[8], acceptance[8] (clause and verify), negative[0] and negative[1] corrected from QA run #29890 (W5-FAIL, at 589049b0; the re-point wave 4 parked until #29173 landed). The aggregate showcase_recalc_selection is sent through the action door, POST /api/v1/actions/showcase_task/showcase_recalc_selection with {params:{_selectedIds}}, since #27611, and the server forwards the params flat to /api/v1/apps/showcase/recalc in-process (buildApiActionRequest). negative[0]'s package-less overlay of showcase_task.done is refused 403 NOT_OVERRIDABLE since ADR-0146 D11, so the stage goes into com.example.showcase.custom through the ensure door; negative[1] says how to stage a tabs userFilter view. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2768,7 +2774,7 @@ "title": "User-built FilterBuilder/SortBuilder toolbar merges with the view filter, restores across nav, and reproduces rows from the URL", "since": "v16", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "browser", "personas": [ @@ -2787,11 +2793,11 @@ }, "steps": [ "sign in as admin; open showcase_task and switch to the in_progress saved view (stored filter status=in_progress); screenshot", - "open the FilterBuilder toolbar; add 2 conditions that leave at least TWO rows, so the sort clause has an order to read — on the stock seed priority = high AND estimate_hours > 4 leaves ONE row on in_progress, a single-datapoint for acceptance[1] (QA run #24616): check the count first and pick a pair that keeps ≥ 2; open the SortBuilder and add a sort (due_date desc); capture the data request", + "open the FilterBuilder toolbar; add 2 conditions that leave at least TWO rows, so the sort clause has an order to read — on the stock seed priority = high AND estimate_hours > 4 leaves ONE row on in_progress, a single-datapoint for acceptance[1] (QA run #24616): check the count first and pick a pair that keeps ≥ 2; open the SortBuilder and add a sort (due_date desc); type a term into the list's quick search that keeps rows (acceptance[2] reads it back; QA run #29890 used 'pipeline'); capture the data request", "assert the request's `filter` param is the MERGE of the view's stored rule AND the two toolbar conditions (buildEffectiveFilter/mergeFilterNodes), and `sort` carries the toolbar sort (e.g. sort=-due_date) — the console spells its list params without a `$`; the returned rows satisfy view-filter AND toolbar conditions", "navigate away via an in-app nav link to another object (the link carries no query string, so the URL state is dropped), then navigate back to showcase_task in_progress; screenshot; confirm the toolbar filter + sort restored", "read where the toolbar filter and sort are kept: the signed-in user's own sys_user_preference row keyed ui.list.showcase_task (GET /api/v1/data/sys_user_preference filtered to that key; its value's perView. carries the filters and the sort — objectui ListPreferencesProvider.tsx, objectui#12418), and its localStorage write-through cache list-state::showcase_task. Confirm both are the signed-in user's: the row's user_id, and the cache key's user segment. (Only the search term stays under the older list-filters::showcase_task: key)", - "sign out; sign in as the SECOND account; open the same object+view; confirm the first account's cached toolbar filter does NOT appear", + "sign out through the account menu (the avatar menu's Log out, user.logout in ui/packages/app-shell/src/layout/AppHeader.tsx; never by clearing storage, which would also wipe the cache this step tests); sign in as the SECOND account; open the same object+view; confirm the first account's cached toolbar filter does NOT appear", "userFilters/URL: where UserFilters are hosted (an interface page or the userFilters surface — NOT a bare object list, ADR-0053), apply a quick-filter; copy the URL; open it in a fresh tab; confirm the uf_ params (comma-joined, URI-encoded) reproduce the same rows and uf__tab carries the active preset (ADR-0047)", "negative sweep: confirm tabs-element userFilters (preset chips, uf__tab) do NOT leak onto the bare object list view — dropdown chips the object view itself declares are allowed there (ADR-0047 amendment) and their uf_ params are not a leak" ], @@ -2811,13 +2817,13 @@ { "clause": "the toolbar filter + search survive a FULL in-app navigation away and back — restored from the user's own list preference (sys_user_preference ui.list., cached in localStorage list-state::; URL params alone are lost on an in-app nav link that carries no query string, which is what the preference bridges)", "oracle": "dom", - "verify": "after a screenshot confirms the list rendered, the FilterBuilder shows the two conditions and the SortBuilder the sort; the request re-issued on return carries the same merged `filter` param (the console spells it without a `$`; objectui app-shell/src/context/ListPreferencesProvider.tsx)", + "verify": "after a screenshot confirms the list rendered, the FilterBuilder shows the two conditions, the SortBuilder the sort and the quick search box the term (kept under list-filters::showcase_task:, steps[4]); the request re-issued on return carries the same merged `filter` param (the console spells it without a `$`; objectui app-shell/src/context/ListPreferencesProvider.tsx)", "evidence": "before/after screenshots + the re-issued request" }, { "clause": "the list state is USER-SCOPED — it is the user's own sys_user_preference row (user_id = the signed-in user), and its localStorage cache key embeds the user id (list-state::), so a second account on the same browser never reads the first account's filters (a filter value can be sensitive)", "oracle": "dom", - "verify": "read account A's ui.list.showcase_task preference row and its list-state cache key, then sign in as account B on the same browser and confirm B's view opens with no A-authored toolbar filter (B reads its own row, or none)", + "verify": "read account A's ui.list.showcase_task preference row and its list-state cache key, then sign A out through the account menu's Log out and sign in as account B on the same browser and confirm B's view opens with no A-authored toolbar filter (B reads its own row, or none)", "evidence": "the two accounts' storage keys + B's clean toolbar" }, { @@ -2830,7 +2836,7 @@ "negative": [ "a toolbar filter that REPLACES the view's stored filter (rows appear that the view filter should exclude) is a FAIL — buildEffectiveFilter merges, it does not overwrite the base", "tabs-element userFilters (preset chips, the uf__tab preset) leaking onto a bare OBJECT list view is a FAIL — ADR-0053 suppresses them there by design, as amended by ADR-0047: on an object list view only `element: 'tabs'` userFilters (and quickFilters) are ignored — objectui app-shell warnSuppressedListNav.ts, which logs the suppression — while `element: 'dropdown'` value chips ARE allowed there (examples/app-showcase/src/ui/views/project.view.ts ships them on All Projects, QA run #24616) (the list-view-capabilities item flags the same regression). The reserved list-state params an object list writes for ITSELF — uf__filter, uf__search, uf__sort, uf__group (objectui#11860, userFilterUrlState.ts), e.g. uf__sort after a toolbar sort — are the list's own state mirrored into the URL, not userFilters, and do not count", - "a cached toolbar filter from account A visible to account B on the same browser is a FAIL — the key embeds the user id precisely to prevent that leak", + "a cached toolbar filter from account A visible to account B on the same browser (A signed out through the account menu's Log out, never by clearing storage) is a FAIL — the key embeds the user id precisely to prevent that leak", "an incomplete FilterBuilder row emitted as [field, op, ''] (which matches only empty and silently excludes everything) instead of being dropped is a FAIL — convertFilterGroupToAST skips valueless rows (#1964)" ], "traps": [ @@ -2897,6 +2903,12 @@ "date": "2026-10-27", "change": "automated.ref says why the unpinned clauses stay live legs: every one is browser-only, and the card adds no Playwright lane (#26573 Done-when). No clause changed", "ref": "#26573" + }, + { + "revision": 9, + "date": "2026-11-04", + "change": "steps[1], steps[5], acceptance[2].verify, acceptance[3].verify and negative[2] corrected from QA run #29890 (W5-FAIL, at 589049b0): the clause restores filter AND search, so the steps add a quick search and the verify reads it back; the sign-out between accounts is the account menu's Log out, which keeps the cache under test. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -3578,7 +3590,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": 6, + "revision": 7, "priority": "P2", "surface": "browser", "personas": [ @@ -3601,7 +3613,7 @@ "click select-all-matching; the banner reads 'All M matching records are selected'", "dispatch a bulk action; capture the requests — resolveBulkRows fans out a paged find (dropping $top/$skip, paging 500) collecting every matching id ACROSS pages", "cross-check the collected id count against a direct API count for the same search: GET /api/v1/data/showcase_task?$search=…&$top=0 (or the response total)", - "confirm the AGGREGATE action (showcase_recalc_selection) carries every matching id in `_selectedIds` — it is an api-type action, so the ONE request is POST /api/v1/apps/showcase/recalc with `_selectedIds` at the body's top level (the grid hands them to the action runner as params._selectedIds, objectui plugin-grid ObjectGrid.tsx); the run dialog then reads 'N / 55 processed' for ~15 s (QA run #24616); a per-record action (showcase_mark_done) fans out one dispatch per matching id", + "confirm the AGGREGATE action (showcase_recalc_selection) carries every matching id in `_selectedIds` — it is an api-type action, so the ONE request is the action door, POST /api/v1/actions/showcase_task/showcase_recalc_selection with body {params:{_selectedIds:[…]}} (the grid hands them to the action runner as params._selectedIds, ui/packages/plugin-grid/src/ObjectGrid.tsx; since #27611 the console sends a declared same-origin api action through the door, and the server forwards the params flat to /api/v1/apps/showcase/recalc in-process, buildApiActionRequest in packages/runtime/src/action-execution.ts); the run dialog then reads 'N / 55 processed' for ~15 s (QA run #24616); a per-record action (showcase_mark_done) fans out one dispatch per matching id", "click clear; confirm both selection sources reset (the toolbar empties AND the row checkboxes clear)", "cap leg (acceptance[5]): provision more than 5000 matching scratch tasks — POST /api/v1/data/showcase_task/createMany with a bare JSON array of at most 200 records per call (the batch doors' default maxBatchSize; a 201-record call answers 400 BATCH_TOO_LARGE), e.g. 26 calls of 200 sharing one prefix — then repeat steps[1] and steps[2] and screenshot the banner and the selected count before and after Select all; delete the rows afterwards (deleteMany, at most 200 ids per call)" ], @@ -3619,7 +3631,7 @@ "evidence": "the collected-id count + the filtered API count" }, { - "clause": "an AGGREGATE bulk action issues ONE dispatch carrying the full matching id set in `_selectedIds` (top level of the api action's request body); a per-record action fans out one dispatch per matching id", + "clause": "an AGGREGATE bulk action issues ONE dispatch carrying the full matching id set in `params._selectedIds` of the action door's body (POST /api/v1/actions/showcase_task/showcase_recalc_selection; the server forwards it flat to the api action's target); a per-record action fans out one dispatch per matching id", "oracle": "network", "verify": "count captured POSTs against the match-set size per mode (ObjectGrid dispatchBulkAction → resolveBulkRows → params._selectedIds)", "evidence": "the dispatch trace(s)" @@ -3697,6 +3709,12 @@ "date": "2026-11-01", "change": "acceptance[5] added with steps[7] and fixtures.knownGaps[1]: past HARD_CAP 5000 the cross-page banner must name the cap (e.g. 「将处理前 5000 条」), never present the uncapped server total as the set the action processes. Today the banner counts totalMatching uncapped (BulkActionBar.tsx) while resolveBulkRows stops at 5000 (ObjectGrid.tsx), so the clause is an expected FAIL until the product defect #28552 is fixed. Read at base 37184244. No existing clause changed. Ruling 6545938831 item ④ on #28345. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 7, + "date": "2026-11-04", + "change": "steps[5] and acceptance[2].clause corrected from QA run #29890 (W5-FAIL, at 589049b0): the aggregate action's one request is the action door, POST /api/v1/actions/showcase_task/showcase_recalc_selection with {params:{_selectedIds}} (since #27611), not a top-level _selectedIds POST to /api/v1/apps/showcase/recalc, which the server reaches in-process (buildApiActionRequest). Read at base f68b47ca", + "ref": "#29906" } ] }, diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index 4978cd89957..4f9d1196c2d 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -9,7 +9,7 @@ "title": "The first-run authoring loop closes: package → object → record → app → publish → end-user, zero code, zero restarts", "since": "v16", "status": "active", - "revision": 12, + "revision": 13, "priority": "P0", "surface": "mixed", "personas": ["admin (seeded admin@objectos.ai)", "end user (same account driving the published app)"], @@ -22,7 +22,7 @@ "SUPERSEDED (QA run #24571): publish is no longer one click with no confirmation. Publish opens the 'Pending changes' review sheet — every pending draft listed as New or Update, each expanding into the keys it changes, and a separate 'Publish all' confirm (objectui app-shell preview/DraftChangesPanel.tsx, mounted by StudioDesignSurface.tsx; the audit's finding 3 / ADR-0016 §3.6 step 4). A review step now exists; do not score its presence or absence here — this item scores the loop", "SUPERSEDED (QA run #21056): a new app no longer scaffolds zero nav items — the Create-app dialog seeds the app's navigation from the package's objects behind a checkbox that defaults ON (objectui StudioDesignSurface appAddObjects, objectui#2262). With it on, step 5's nav item already exists and the Interfaces step confirms it rather than builds it; only with it unticked does the manual wiring remain the contract. Do not expect zero items", "mixed-language Studio chrome (audit finding 4) is asserted by i18n.studio-follows-app-locale — locale consistency is that item's business, not this one's", - "ZH-CN LEG, acceptance[2] FAILS TODAY (open #29673; QA run #29672 at 3482f3d8d2): an object first published from Studio in zh-CN never reaches the data plane. The draft read serves the injected queue column with its translated label (队列), the autosave echoes it, the registry judges the echo byte for byte against the platform's definition (isInjectedColumnDefinition, packages/spec/src/data/injected-system-column-provenance.ts) and refuses it as an author's field named queue, and the failed registration is only a console.warn (the registerObject catch in packages/metadata-protocol/src/protocol.ts). Publish answers published while GET /api/v1/data/repairs_repair_ticket answers 404 OBJECT_NOT_FOUND. Re-read unchanged at base 339f63da30. Score acceptance[2] FAIL on the zh-CN leg citing #29673, and acceptance[3] blocked by it there; the en leg passes" + "FIXED, regression row, expected PASS on the zh-CN leg (#29673, closed by PR #29790, merged as 589049b0; pins packages/metadata-protocol/src/protocol-translated-echo.test.ts and packages/qa/dogfood/test/studio-zh-cn-echo-publish.dogfood.test.ts): an object saved back from a translated /meta read now stores its source labels, so an object first published from Studio in zh-CN reaches the data plane. QA run #29877 (W5-P0, at 589049b0) read the zh-CN leg passing acceptance[2] and [3]: publish answered published, the list served, the overlay carried no queue / owner_id, and an en read showed Owner / Queue. Score both legs normally; a zh-CN publish that answers published while the data read answers 404 OBJECT_NOT_FOUND is a regression of #29673. The failure as it was: The draft read serves the injected queue column with its translated label (队列), the autosave echoes it, the registry judges the echo byte for byte against the platform's definition (isInjectedColumnDefinition, packages/spec/src/data/injected-system-column-provenance.ts) and refuses it as an author's field named queue, and the failed registration is only a console.warn (the registerObject catch in packages/metadata-protocol/src/protocol.ts). Publish answers published while GET /api/v1/data/repairs_repair_ticket answers 404 OBJECT_NOT_FOUND. Re-read unchanged at base 339f63da30, before the fix" ] }, "steps": [ @@ -98,7 +98,8 @@ { "revision": 9, "date": "2026-11-01", "change": "steps[3] quotes the publish toast as the console draws it. It still read 'Published 1 item in this package (one atomic release)', a wording retired when the walkthrough of #27521 (row 28) dropped 'atomic' from the publish title, toast and confirm note: the toast is now 'Published N items in this package' ('Published 1 item in this package' for one; engine.studio.publishedAllCount / publishedAllCountOne in ui/packages/i18n/src/namespaces/studio/en.ts), and a publish that puts an app or object live adds the permission-set hint and an 'Open Access' action (ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx). Wording only; no clause changed. Read at base efb1620c", "ref": "#27521" }, { "revision": 10, "date": "2026-11-01", "change": "QA run #28545 (W3-C1R) finding on steps[2]: the object surface opens on Records after a navigation, Add field is on the Form tab, and an option's stored value derives from its label ('In Repair' gives in_repair), so a run that wants wip must overwrite it. steps[2] now says so, read from source (StudioDesignSurface.tsx viewMode default 'grid'; OptionsEditor derives a new row's value with toFieldNameOrEmpty). The same card's steps[3] finding, the stale publish toast, was fixed upstream by revision 9 (#28526, after eeec88d1). No clause changed. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", "ref": "#28545" }, { "revision": 11, "date": "2026-11-03", "change": "fixtures.requires[0], steps[0] and a new negative[2] name two locale legs, en and zh-CN, and a clause holds only on both; a new fixtures.knownGaps[3] records the zh-CN leg's open failure; steps[1]'s identifier note follows pinyin derivation. QA run #29672 found the item named no locale while steps[1] uses a Chinese display name: its en leg passed and its zh-CN leg failed acceptance[2] (#29673, extracted by the run), so a run that drove one locale could score the loop either way. Read at base 339f63da30: the registry judge and the swallowed registration are unchanged and #29673 is open; and since #29734 (11c4eb59dd) a Chinese display name derives a pinyin package id (deriveCreatePackageId), so steps[1]'s 'derives no package id' no longer held, while its instruction to type com.example.repairs stands. No clause text changed; each now holds per leg. Card #29679, from QA run #29672", "ref": "#29679" }, - { "revision": 12, "date": "2026-11-04", "change": "acceptance[0].verify re-pointed: the Studio package switcher, which declares the studio-pkg-chip testid, moved unchanged from ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx to PackageSwitcher.tsx beside it (#29988 slice 2, a move-only split). Re-read at base f7aab8958. No clause changed", "ref": "#29988" } + { "revision": 12, "date": "2026-11-04", "change": "acceptance[0].verify re-pointed: the Studio package switcher, which declares the studio-pkg-chip testid, moved unchanged from ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx to PackageSwitcher.tsx beside it (#29988 slice 2, a move-only split). Re-read at base f7aab8958. No clause changed", "ref": "#29988" }, + { "revision": 13, "date": "2026-11-04", "change": "fixtures.knownGaps[3] corrected from QA run #29877 (W5-P0, at 589049b0; the list of record is comment 6600018008 on #27377): the zh-CN leg's acceptance[2] failure is fixed by PR #29790 (#29673 closed), and the run read that leg passing acceptance[2] and [3], so the instruction to score it FAIL citing #29673 is withdrawn and the row is a regression row. Read at base f68b47ca", "ref": "#29906" } ] }, { @@ -106,7 +107,7 @@ "title": "Object designer round-trip: field add/edit/reorder persists to metadata, identifiers derive and survive typing", "since": "v16", "status": "active", - "revision": 12, + "revision": 13, "priority": "P1", "surface": "mixed", "personas": ["admin"], @@ -124,7 +125,7 @@ "in the New object dialog type the Label 'Repair Asset', THEN type the identifier repair_asset character by character (never .fill — the F2 per-keystroke-sanitisation bug only reproduces when typed) and confirm its underscore survives. The dialog applies the package namespace itself: it previews and stores NS_repair_asset (StudioDesignSurface prefixObjectName, objectui CreateItemDialog.storedName-11792 — no double prefix when NS_ is typed as well), which the publish gate requires in a namespaced package (NAMESPACE_PREFIX). Type the identifier after the Label, and re-check it before 'Save as draft' (objectui#12680: a Label typed after a hand-typed identifier can re-derive it). Keep the pre-selected OWD choice and click 'Save as draft' (the dialog's confirm button, engine.studio.createDraft; no Studio create dialog has a button named Create)", "add a Text field, set its label to 'Asset Code', blur — the API name input must derive asset_code (F3)", "add a Picklist field and author its 3 real options, THEN add one EMPTY option row; wait out the debounced live validation — no validation banner may appear for the empty row (F4). ⚠ Order matters: with NO real option yet the select-needs-options banner fires, legitimately — the empty rows ARE dropped, so the field has no options — which collides with a literal 'no banner' (objectui F4, as repaired at 0b36ffa, gives its picklist one real option first for this reason). Assert the empty-row half with real options present", - "let the autosave land (Studio has no Save button: the status line reads 'Saved HH:MM'), then Publish → the review sheet → 'Publish all'; GET /api/v1/meta/object/NS_repair_asset and record the full field list and its order", + "let the autosave land (Studio has no Save button; on the Data pillar the autosave shows as the PUT …?mode=draft answering 200 and the header chip 'Draft · N unpublished', engine.studio.itemDraftPending; QA run #29893 found no 'Saved HH:MM' status there), then Publish → the review sheet → 'Publish all'; GET /api/v1/meta/object/NS_repair_asset and record the full field list and its order", "reopen the object in the Data pillar: rename one picklist value, then reorder the fields on the Form tab — the form designer is dnd-kit (ObjectFormDesigner.tsx: a PointerSensor with a 4 px activation distance and a KeyboardSensor), so drag with intermediate pointer moves past 4 px, or focus a field's drag handle and press Space, an arrow key, Space; a single synthetic jump does not start a drag (automation-input). Let the autosave land and Publish again", "GET /api/v1/meta/object/NS_repair_asset again — the rename AND the new field order must be in the persisted metadata, not just the canvas", "create a record via the Records tab and GET /api/v1/data/NS_repair_asset/ — the stored column key must be the derived API name (asset_code)" @@ -225,6 +226,12 @@ "date": "2026-11-02", "change": "automated.ref, source[0], steps[0]'s second-door note and acceptance[1]/[2] verify re-pointed: the live spec's F2–F4 no longer drive the Setup designer route; since #28818 (ADR-0146 D1) they open Studio's All metadata area designer on a package the test authors, /studio/PKG/metadata/object/new?package=PKG, and F4 now saves a draft and reads back the one stored option. Re-read at base dfbe34d887 (ui/e2e/live/studio-object-designer.spec.ts newObjectIn, F4's draft read). The pins stay corroboration at a sibling door (the area's metadata-admin editor, not the Data pillar's New object dialog the steps drive), and F4 still seeds one option where acceptance[3] judges three and reads a draft where the clause reads the published picklist, so no clause's scoring basis changed", "ref": "#28579" + }, + { + "revision": 13, + "date": "2026-11-04", + "change": "steps[4] corrected from QA run #29893 (W5-STUDIO, at 589049b0): the Data pillar shows no 'Saved HH:MM' line; the autosave is read as the draft PUT's 200 and the 'Draft · N unpublished' chip. Wording only. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -308,10 +315,10 @@ "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": 14, + "revision": 15, "priority": "P1", "surface": "mixed", - "personas": ["admin (authors)", "end user (opens the record). The viewer is the admin unless a member is used: every signed-in member reads showcase_account through the showcase's everyone baseline, showcase_member_default (allowRead; examples/app-showcase/src/security/permission-sets.ts MemberDefaultPermissionSet, which the dev CLI adds to every member per request). No run has yet driven the member leg on this object, so record which"], + "personas": ["admin (authors)", "end user (opens the record). The viewer is the admin unless a member is used: every signed-in member reads showcase_account through the showcase's everyone baseline, showcase_member_default (allowRead; examples/app-showcase/src/security/permission-sets.ts MemberDefaultPermissionSet, which the dev CLI adds to every member per request). QA run #29893 (at 589049b0) drove the member leg as phone.demo@example.com and the authored page rendered; record which viewer each run used"], "fixtures": { "app": "showcase", "requires": ["seeded showcase_account records to render the published page against (the stock showcase seed, examples/app-showcase/src/data/seed/index.ts). The object is chosen for two facts. It DECLARES highlightFields (status, industry, annual_revenue; examples/app-showcase/src/data/objects/account.object.ts), and since #28907 the synthesized default draws record:highlights only for declared highlightFields (deriveHighlightFields in ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts: an object that declares none gets no strip), so only such an object can show acceptance[0]'s record:highlights. And it ships NO record page (no page under examples/app-showcase/src/ui/pages names it), so the authored page is the one usePageAssignment serves. ⛔ Not showcase_contact: it declares no highlightFields, so its full create seeds no record:highlights by design (QA run #29672). ⛔ Not showcase_invoice: it ships the package default record page showcase_invoice_document (isDefault: true, examples/app-showcase/src/ui/pages/invoice-document.page.ts), which wins by design (objectui react usePageAssignment: the default record page, else the first), and authoring a second isDefault page answers 422 DEFAULT_RECORD_PAGE_CONFLICT. showcase_project, showcase_task and showcase_preference ship defaults too (QA runs #24571 and #24604)"], @@ -324,7 +331,7 @@ "capture the PUT /api/v1/meta/page/ request the save issues", "assert the draft body: type:'record', object:'showcase_account', kind:'full', and regions seeded NON-empty from the object's synthesized default detail page (the page resource's createSeed hook) including a record:highlights block carrying the object's declared highlightFields (fixtures.requires[0]) — never a blank canvas (ADR-0034 / objectui#1541)", "open the page editor (/_console/studio/~all/metadata/page/, the All metadata item page — the generic editor, since the cross-package leg opens no pillar): the Basics, Data Context and Advanced panels render (the layout is edited on the canvas, not in a Layout section — see acceptance[1].verify), and 'Add block' opens the block-type picker", - "add a block (e.g. Card), Save draft, Publish through the ResourceEditPage draft/publish chrome", + "add a block (e.g. Card); the draft autosaves (the block pick and each property edit PUT the draft, and the toolbar Save stays disabled), then Publish through the ResourceEditPage draft/publish chrome", "GET /api/v1/meta/page/: the published regions must include the added block", "open a showcase_account record as the end user (personas[1]): the page assignment (usePageAssignment) renders the authored page over the synthesized default", "screenshot the editor canvas and the rendered record page" @@ -421,7 +428,8 @@ "change": "steps[0] and steps[3] re-pointed to Studio's All metadata area (#28579 slice 2, FOLLOW-UPS §13c): the Setup page URLs they named redirect there since #28818, which mounts the same create form and editor, so the steps now name the landing URL. A source row cites the redirect and the area. No clause changed. Read at base eb123b7ccc", "ref": "#28579" }, - { "revision": 14, "date": "2026-11-03", "change": "the item's record page moves from showcase_contact to showcase_account: fixtures.requires[0], personas[1], steps[0], steps[2], steps[6], acceptance[0] (clause and verify), acceptance[3].verify and automated.ref. Since #28907 (8bb2417a74) the synthesized default record page draws record:highlights only for an object's declared highlightFields (deriveHighlightFields, ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts), and showcase_contact declares none. So QA run #29672 read a full create on it with no record:highlights and scored acceptance[0] FAIL as stale spec; its control, a full create on showcase_account, carried the block. Read from source at base 339f63da30: showcase_account declares highlightFields status, industry and annual_revenue (examples/app-showcase/src/data/objects/account.object.ts), the page createSeed copies the synthesized regions on a full create (ui/packages/app-shell/src/views/metadata-admin/anchors.ts), no showcase page names the object, and the everyone baseline showcase_member_default reads it. The clause keeps its strength: a full create on the named object without record:highlights is still the FAIL. acceptance[1] to [3] passed on showcase_contact in the run and are not yet driven on showcase_account. Card #29679, from QA run #29672", "ref": "#29679" } + { "revision": 14, "date": "2026-11-03", "change": "the item's record page moves from showcase_contact to showcase_account: fixtures.requires[0], personas[1], steps[0], steps[2], steps[6], acceptance[0] (clause and verify), acceptance[3].verify and automated.ref. Since #28907 (8bb2417a74) the synthesized default record page draws record:highlights only for an object's declared highlightFields (deriveHighlightFields, ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts), and showcase_contact declares none. So QA run #29672 read a full create on it with no record:highlights and scored acceptance[0] FAIL as stale spec; its control, a full create on showcase_account, carried the block. Read from source at base 339f63da30: showcase_account declares highlightFields status, industry and annual_revenue (examples/app-showcase/src/data/objects/account.object.ts), the page createSeed copies the synthesized regions on a full create (ui/packages/app-shell/src/views/metadata-admin/anchors.ts), no showcase page names the object, and the everyone baseline showcase_member_default reads it. The clause keeps its strength: a full create on the named object without record:highlights is still the FAIL. acceptance[1] to [3] passed on showcase_contact in the run and are not yet driven on showcase_account. Card #29679, from QA run #29672", "ref": "#29679" }, + { "revision": 15, "date": "2026-11-04", "change": "steps[4] and personas[1] corrected from QA run #29893 (W5-STUDIO, at 589049b0): the record page draft autosaves (Save stays disabled), and the member leg has now been driven. Read at base f68b47ca", "ref": "#29906" } ] }, { @@ -429,7 +437,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": 11, + "revision": 12, "priority": "P1", "surface": "mixed", "personas": ["admin (authors drafts)", "end user (must not see drafts)"], @@ -481,7 +489,7 @@ { "clause": "optimistic concurrency holds: a write carrying a stale If-Match version answers 409 METADATA_CONFLICT — a concurrent edit is reported, never silently overwritten", "oracle": "api", - "verify": "re-PUT with the superseded version token while a draft row of this name exists → 409 with the wire code METADATA_CONFLICT (the ADR-0008 chain the receipt's version field exists for; acceptance[10]'s audit row stores it lower-case, metadata_conflict). Send it while a draft is staged: with no draft row the ?mode=draft door judges the token against no head, so any If-Match, the published version included, answers 409 '… (current is null)' (packages/metadata-protocol/src/protocol-lock-version-gates.ts#storedParentForToken) and does not exercise the stale-token guard", + "verify": "re-PUT with the superseded version token while a draft row of this name exists → 409 with the wire code METADATA_CONFLICT (the ADR-0008 chain the receipt's version field exists for; acceptance[10]'s audit row stores it lower-case, metadata_conflict). Send it while a draft is staged: with no draft row the ?mode=draft door judges the token against no head, so any If-Match, the published version included, answers 409 '… (current is null)' (packages/metadata-protocol/src/protocol-lock-version-gates.ts#storedParentForToken) and does not exercise the stale-token guard. Read what survived from what is served next, not from a draft read with ?mode=: GET …?mode=draft answers 400, because mode is a write parameter and the draft read is ?state=draft (META_ITEM_READ_PARAMS, packages/rest/src/meta-item-read-gate.ts). The no-overwrite proof is that the next publish serves the newer draft (the run's REV2), not the stale write", "evidence": "the 409 response" }, { @@ -561,7 +569,8 @@ { "revision": 8, "date": "2026-10-20", "change": "automated dropped and fixtures.knownGaps[0] added — §2 theme \"pins that cover no full clause\" of wave anchor #24579 (18.0 sweep), one of the leftovers round 6 (#24697) named. Tier 1 run #24598 scored every clause not-run on dashboard-designer-roundtrip.dogfood.test.ts, green 3/3 at ba4afcc2: it saves, publishes and reads back a layout-less dashboard, which is acceptance[1]'s post-publish half and no full clause. Round 1's QA run #24571 had scored acceptance[0]–[5] and [7]–[11] pass on that pin alone; QA run #24604 drove acceptance[6] and negative[0]–[4] by hand. The item had no knownGaps, so the line is new: it says what the pin asserts and when `automated` may return. The item is now Tier 2 (P1, no pin). No clause changed", "ref": "#24723" }, { "revision": 9, "date": "2026-10-27", "change": "acceptance[8] (clause, verify) and steps[9] follow the diff's shipped granularity, and acceptance[5].verify names the earlier refusal (checklist-accuracy findings c8 and c5 of the coordinator leg of QA run #26715, at 3fcafcd3; re-read at 43731031). (1) The clause expected the edited widget's own key (widgets[0].title); the door answered changed [{ path: 'widgets', from, to }]. diffMetaItem's diff is shallow by contract (protocol.ts diffShallow: top-level keys only, deep structural diffs out of scope), so the clause now asks for the changed top-level key and no unchanged one — still never a whole-document dump. (2) A stray widget key is refused at the draft save (422 INVALID_METADATA, unrecognized_keys), so there is no invalid draft to publish; that refusal now scores the invalid-draft clause beside the unchanged live read", "ref": "#26715" }, { "revision": 10, "date": "2026-10-31", "change": "acceptance[10] names which denials the audit trail records, in place of 'attempts (allowed and denied)'. The protocol writes a denied row only for item_locked, metadata_conflict (the save door's 409 on a stale If-Match included, packages/metadata-protocol/src/protocol-save-conflict.ts) and the batch publish's batch_aborted, namespace_prefix and stored_type_not_canonical; a 422 INVALID_METADATA spec refusal and a 404 NO_DRAFT write none. The verify gains steps[5]'s 409 as a denied metadata_conflict row with its actor, which this item already drives, so the clause is stronger. The other denial codes the sys_metadata_audit object documents (not_overridable, not_creatable, invalid_metadata, destructive_change) have no writer in packages/metadata-protocol at this base. Accuracy finding of QA run #27441. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" }, - { "revision": 11, "date": "2026-11-02", "change": "acceptance[10] (clause, verify) re-pointed to #28355: revision 10 said a 422 INVALID_METADATA spec refusal writes no row, and #28355 now stamps one denied row per catalogued refusal at the four single-item doors (protocol-refusal-audit.ts, REFUSAL_AUDIT_CODES = NOT_OVERRIDABLE, NOT_CREATABLE, INVALID_METADATA, DESTRUCTIVE_CHANGE, stored lower-case). The clause names those four beside item_locked, metadata_conflict and the batch codes, and keeps NO_DRAFT, INVALID_REQUEST and 5xx as writing none; the verify asks for steps[7]'s 422 as exactly one denied invalid_metadata save row with the actor, and for no row from steps[12]'s 400 or a NO_DRAFT publish. acceptance[3] spells the wire code METADATA_CONFLICT and says the stale If-Match must be sent while a draft row exists. Measured at base 0a41075a through the real protocol doors with a throwaway probe (deleted): the stray-key dashboard draft save answered 422 INVALID_METADATA with one denied invalid_metadata row carrying the actor; a publish with no draft answered 404 NO_DRAFT and a rollback with no toVersion 400 INVALID_REQUEST, neither writing a row; a draft save carrying the published version as If-Match with no draft row answered 409 METADATA_CONFLICT (current is null). Stronger, not weaker: the 422 that scored a row as a FAIL now owes one. QA run #29002 clause FAIL", "ref": "#29013" } + { "revision": 11, "date": "2026-11-02", "change": "acceptance[10] (clause, verify) re-pointed to #28355: revision 10 said a 422 INVALID_METADATA spec refusal writes no row, and #28355 now stamps one denied row per catalogued refusal at the four single-item doors (protocol-refusal-audit.ts, REFUSAL_AUDIT_CODES = NOT_OVERRIDABLE, NOT_CREATABLE, INVALID_METADATA, DESTRUCTIVE_CHANGE, stored lower-case). The clause names those four beside item_locked, metadata_conflict and the batch codes, and keeps NO_DRAFT, INVALID_REQUEST and 5xx as writing none; the verify asks for steps[7]'s 422 as exactly one denied invalid_metadata save row with the actor, and for no row from steps[12]'s 400 or a NO_DRAFT publish. acceptance[3] spells the wire code METADATA_CONFLICT and says the stale If-Match must be sent while a draft row exists. Measured at base 0a41075a through the real protocol doors with a throwaway probe (deleted): the stray-key dashboard draft save answered 422 INVALID_METADATA with one denied invalid_metadata row carrying the actor; a publish with no draft answered 404 NO_DRAFT and a rollback with no toVersion 400 INVALID_REQUEST, neither writing a row; a draft save carrying the published version as If-Match with no draft row answered 409 METADATA_CONFLICT (current is null). Stronger, not weaker: the 422 that scored a row as a FAIL now owes one. QA run #29002 clause FAIL", "ref": "#29013" }, + { "revision": 12, "date": "2026-11-04", "change": "acceptance[3].verify corrected from QA run #29890 (W5-FAIL, at 589049b0): a GET with ?mode=draft answers 400 (the read's parameters are closed; the draft read is ?state=draft), so the no-overwrite proof is the next publish serving the newer draft. Read at base f68b47ca", "ref": "#29906" } ] }, { @@ -569,7 +578,7 @@ "title": "An invalid authored shape is rejected at save with a LOCATED error and is not persisted", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "mixed", "personas": ["admin"], @@ -646,6 +655,12 @@ "date": "2026-11-02", "change": "steps[3] re-pointed to the Studio door: the metadata editor whose Source tab autosave reaches the server gate is now served by Studio's All metadata area (/_console/studio/~all/metadata/object/NAME, the same metadata-admin ResourceEditPage), and the Setup URL the step named redirects there (setupMetadataStudioPath, #28818, ADR-0146 D1). On the ~all leg the item page opens the generic editor, not the Data pillar (areaItemPillarPath answers null without a package), so the Source tab is still there. 'The Studio designer' that holds an incomplete shape client-side is named as the Data pillar's, since both doors are now Studio's. Read at base dfbe34d887; not yet driven live at this URL — the next run records it. No clause changed", "ref": "#28579" + }, + { + "revision": 7, + "date": "2026-11-04", + "change": "History only, from QA run #29893 (W5-STUDIO, at 589049b0): r6's 'not yet driven live at this URL' is closed. The run drove it live: /_console/apps/setup/metadata/object/NAME redirected to /_console/studio/~all/metadata/object/NAME, the Source tab's autosave PUT reached the server and the 422 banner rendered. No field changed. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -654,7 +669,7 @@ "title": "The metadata type registry gates runtime writes: allowEnvironmentOverlay=false kinds refuse overlay (403 NOT_OVERRIDABLE), allowRuntimeCreate=false kinds refuse creation (403 NOT_CREATABLE) — both sides", "since": "v16", "status": "active", - "revision": 9, + "revision": 10, "priority": "P1", "surface": "api", "personas": ["admin"], @@ -682,7 +697,7 @@ { "clause": "a kind with allowEnvironmentOverlay=false refuses the environment overlay of an artifact-backed item with 403 NOT_OVERRIDABLE — the lock is server-side", "oracle": "api", - "verify": "PUT /api/v1/meta/object/showcase_task answers 403 with code NOT_OVERRIDABLE (the runtime behavior the registry's own doc comment declares, which spells it lowercase; served codes are SCREAMING_SNAKE per ADR-0112) — score the code, not the one-sentence message", + "verify": "PUT /api/v1/meta/object/showcase_task answers 403 with code NOT_OVERRIDABLE (the runtime behavior the registry's own doc comment declares, which spells it lowercase; served codes are SCREAMING_SNAKE per ADR-0112) — score the code, not the message: the 403 on object/showcase_task now carries the managed-item sealed sentence (managedItemSealedSentence, packages/metadata-protocol/src/packaged-base-regime.ts), not the registry-flag text source[0] describes", "evidence": "the 403 response body" }, { @@ -723,7 +738,7 @@ "traps": ["dispatcher-vs-hono-route"], "automated": { "kind": "unit", "ref": "packages/objectql/src/overlay-precedence.test.ts — a unit test of the overlay whitelist (describe 'overlay whitelist enforcement (shared-DB invariant)') that stops above the HTTP door. It corroborates acceptance[0], [2] and [3] and negative[0] and scores none of them, since every clause here is api-oracle; no pin touches acceptance[4]'s DELETE reset. The whole item is the run's live leg" }, "source": [ - "packages/spec/src/kernel/metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRY (DEFAULT_METADATA_TYPE_REGISTRY per-kind flags; allowEnvironmentOverlay doc: 'runtime returns HTTP 403 not_overridable'; the object/field lock rationale; job's #4509 create lock)", + "packages/spec/src/kernel/metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRY (DEFAULT_METADATA_TYPE_REGISTRY per-kind flags; allowEnvironmentOverlay doc: 'runtime returns HTTP 403 not_overridable', whose served message is now the sealed-sentence regime's, acceptance[0]; the object/field lock rationale; job's #4509 create lock)", "packages/objectql/src/overlay-precedence.test.ts#not_overridable ('denied — must throw 403 (not_overridable or not_creatable)')", "packages/metadata-protocol/src/protocol.ts#isRuntimeCreateAllowed (isRuntimeCreateAllowed — the write-gate authority)", "ADR-0005 (metadata customization opt-in), ADR-0049 (enforce-or-remove — the job rationale)", @@ -750,6 +765,12 @@ "date": "2026-11-02", "change": "The positive overlay half is re-pointed from TRANSITIONAL to the declared override (#28579 slice 2): steps[1] ensures com.example.showcase.custom and saves the view there declaring overrides { package, basedOn }; steps[5] deletes the pair's row; acceptance[1] and [4], variants[3] and [4] and knownGaps[0] say so, and a source row cites refusePackagedBaseOverride and ADR-0146's supersede table. The package-less PUT's 403 NOT_OVERRIDABLE is recorded in steps[1] as the expected answer and scored by the new cli.packageless-overlay-ceremony-and-close. acceptance[0], [2] and [3] are unchanged. Read at base eb123b7ccc", "ref": "#28579" + }, + { + "revision": 10, + "date": "2026-11-04", + "change": "acceptance[0].verify and source[0] annotated from QA run #29893 (W5-STUDIO, at 589049b0): the 403 NOT_OVERRIDABLE on object/showcase_task carries the managed-item sealed sentence, not the registry-flag text. The clause scores the code, so the verdict stands. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -758,14 +779,14 @@ "title": "The Studio CEL editors reach the SAME verdict as the engine: formula result-type inference, previous. completion in conditional rules, and RLS lint + test-run — the editor UI, not a second grammar", "since": "v16", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "mixed", "personas": ["admin"], "fixtures": { "app": "showcase", "requires": [ - "a writable package with an object carrying a formula field (role=value) and a field conditional rule (visibleWhen/readonlyWhen/requiredWhen) — the stock showcase formula shapes are the models to author against (showcase_invoice line_total = record.quantity * record.unit_price, showcase_project remaining-budget, field-zoo); the source-loaded com.example.showcase is read-only, so the SAVE round-trip needs a writable package (the editors' lint/autocomplete/inferred-type/test-run themselves run client-side and can be OBSERVED read-only on the showcase fields)", + "a writable package with an object carrying a formula field (role=value) and a field conditional rule (visibleWhen/readonlyWhen/requiredWhen) — the stock showcase formula shapes are the models to author against (showcase_invoice line_total = record.quantity * record.unit_price, showcase_project remaining-budget, field-zoo); the source-loaded com.example.showcase is read-only, so the SAVE round-trip needs a writable package, and its object needs a sharingModel: a new object without one is refused at publish by the security-owd-unset lint (SECURITY_OWD_UNSET, packages/lint/src/validate-security-posture.ts) (the editors' lint/autocomplete/inferred-type/test-run themselves run client-side and can be OBSERVED read-only on the showcase fields)", "a writable permission set to host the RLS USING/CHECK CEL editor + test-run: a set of the writable package, opened in Studio's Access pillar (/_console/studio/PKG/access → the set → the 'Row-level security' tab, which PermissionAdvancedFacets renders) — where QA run #26715 found the RLS editor; the package's objects must be published for the set's object pickers to list them", "the @objectstack/formula engine present in the console bundle — celAuthoring lazy-loads it" ], @@ -903,6 +924,12 @@ "date": "2026-11-01", "change": "Four QA run #28656 (W3-RT) findings. (1) fixtures.knownGaps[3] is dropped. It said the editor gives a non-lowerable RLS read predicate the warning tier and autosaves it (objectui#13061). The editor now asks the published rule for both RLS clauses and keeps its error severity (celAuthoring.ts lintCelPredicate and rlsPredicateVerdict; celAuthoring.test.ts pins the error for USING and CHECK), and #28656 read it live at 6e11a929: error tier, perm-autosave-held, 0 PUTs. The former knownGaps[4] is now knownGaps[3]. (2) In that knownGap only the factual half changes: the editor half is #27482 (open), not 'no open card'; #27612, #27643 and #27644 landed; the residual is the first autosave of each newly refused text (PermissionMatrixEditor.serverVerdictHold-27482.test.tsx case (b)). acceptance[4]'s 'no PUT issued' is left as it was until #27482 is ruled. (3) knownGaps[1], knownGaps[2] and acceptance[7].verify name the invocation the ui vitest guard accepts, cd ui && pnpm exec vitest run --maxWorkers=1 packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts. The pnpm --filter @object-ui/app-shell exec form runs in the package directory, which the guard refuses (ui/scripts/vitest-invocation-guard.mjs, verdict package-cwd). (4) steps[3] says the completion listbox caps at 8 (MAX_CEL_SUGGESTIONS) and to type a member prefix. That a bare record. leads with the system fields is #28656's live reading. No clause is weakened. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); findings parked in anchor comment 6557904764", "ref": "#28656" + }, + { + "revision": 9, + "date": "2026-11-04", + "change": "fixtures.requires[0] corrected from QA run #29890 (W5-FAIL, at 589049b0): the authored object needs a sharingModel, or publish refuses it with security-owd-unset. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -911,7 +938,7 @@ "title": "Permission-matrix editor UX: field-filter + bulk apply to EXACTLY the visible fields, and the Bulk column never clips at narrow widths", "since": "v16", "status": "active", - "revision": 10, + "revision": 11, "priority": "P2", "surface": "mixed", "personas": ["admin"], @@ -931,7 +958,7 @@ "open the Permission Matrix editor on a writable permission set (Studio Access pillar, /_console/studio/PKG/access, or Studio's All metadata item page /_console/studio/~all/metadata/permission/, where the retired Setup door /_console/apps/com.objectstack.setup/metadata/permission/ lands in one hop since #28818 (setupMetadataStudioPath); ⛔ not /_console/apps/com.objectstack.studio/…, the studio app is not loaded on a stock boot — RUNNER.md environment facts); expand an object with > 6 fields so the field sub-table's filter renders (threshold is > 6)", "B4 field filter: type a filter that narrows the field set to a couple of fields; confirm the sub-table's 'n / N' counter and the DOM show only the matching fields (the hidden fields' read/edit checkboxes are absent)", "B4 bulk-over-visible: click 'Write all' (perm.field.bulk.writable) → it must grant read+write to EXACTLY the visible fields; clear the filter and confirm the previously-hidden fields are UNTOUCHED (still at their prior grant), never bulk-applied", - "on a different filtered set click 'Read-only all' (perm.field.bulk.readable) → it revokes write on the visible fields only; click 'Clear' → it drops the explicit overrides so the visible fields fall back to the read-only default (readable, not editable)", + "on a different filtered set click 'Read-only all' (perm.field.bulk.readable) → it revokes write on the visible fields only; click 'Clear' → it drops the explicit overrides so the visible fields fall back to the object's grant (drawn as data-field-rule=\"inherited\": readable, and editable too where the object grants edit; FieldRuleBox in PermissionMatrixEditor.tsx)", "Save; GET /api/v1/meta/permission/ (or the package draft read) and assert the fields map (keyed `${object}.${field}`) carries overrides for ONLY the fields that were visible when each bulk fired — never a filter-hidden field", "B3 clip: shrink the viewport to a narrow width; confirm the object matrix's enclosing overflow-auto container SCROLLS horizontally (the table carries min-w-[1304px]; Studio's is data-testid perm-studio-matrix) and the Bulk column (Read / CRUD / All / None per row) stays reachable, not clipped off the right edge", "read-only gate: open a read-only package/type set; confirm Save is hidden, every checkbox is disabled, the field sub-table's bulk buttons (Read-only all / Write all / Clear) are ABSENT — FieldsSubTable renders them only while the set is writable — and the object row's Read / CRUD / All / None, which Studio shows only on the row under the pointer or with keyboard focus, are disabled when they show; the read-only badge names package-vs-type as the reason", @@ -958,7 +985,7 @@ "evidence": "the meta read" }, { - "clause": "'Clear' drops the explicit overrides for the visible fields so they fall back to the default (readable, not editable) — the saved fields map no longer carries those keys, rather than writing an explicit default row", + "clause": "'Clear' drops the explicit overrides for the visible fields so they fall back to the object's grant (data-field-rule=\"inherited\"; editable too where the object grants edit) — the saved fields map no longer carries those keys, rather than writing an explicit default row", "oracle": "api", "verify": "after Clear + Save, the cleared fields' keys are absent from the fields map", "evidence": "the meta read" @@ -1046,6 +1073,12 @@ "date": "2026-11-03", "change": "new clause acceptance[6], step B5 (steps[8]) and negative[2] (objectstack#29677, from QA run #29672, whose acceptance[4] covers only the Bulk column): an expanded object's field Read and Edit boxes stay in view and clickable at every horizontal scroll offset and pane width. On main at 3482f3d8d2 the field sub-table's name column, its flexible column and about 697 px wide, was pinned left and covered both boxes from a scroll of about 80 px on, so below the 1304 px floor they could not be reached. The field panel is now pinned at the cell's inset instead (FIELD_PANEL in ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx); automated.ref names the browser geometry pin ui/e2e/permission-field-panel-scroll.spec.ts. Appended, so no existing index moves; no other clause changed", "ref": "#29677" + }, + { + "revision": 11, + "date": "2026-11-04", + "change": "steps[3] and acceptance[3].clause corrected from QA run #29893 (W5-STUDIO, at 589049b0): after Clear a field falls back to the object's grant (data-field-rule=\"inherited\"), not to a read-only default; the verify (keys absent from the saved map) is unchanged. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -1054,7 +1087,7 @@ "title": "Declarative custom pages render their real block COMPOSITION (not just 'no error'): each declared block resolves its renderer and binds its data source, data-bound blocks show seeded rows (API cross-check), and a block that cannot bind degrades with a NAMED error, never a blank region", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": ["admin (seeded admin@objectos.ai) driving the published showcase app as an end user"], @@ -1062,7 +1095,8 @@ "app": "showcase", "requires": [ "a fresh boot of the showcase with seeded data present — GET /api/v1/data/showcase_project · /showcase_task · /showcase_account must each return rows, and the chart datasets showcase_task_metrics / showcase_project_metrics (examples/app-showcase/src/ui/datasets/chart-gallery.dataset.ts) must resolve (seed-data-thin guard: an empty seed makes every data-bound block look 'broken')", - "a scratch page name (e.g. qa_page_probe) authorable via the draft door PUT /api/v1/meta/page/qa_page_probe?mode=draft — used ONLY for the degradation probe, so no shipped showcase page is mutated" + "a scratch page name (e.g. qa_page_probe) authorable via the draft door PUT /api/v1/meta/page/qa_page_probe?mode=draft — used ONLY for the degradation probe, so no shipped showcase page is mutated", + "a showcase_task owned by another user, for acceptance[3]: the stock seed leaves every task owned by the admin, so plant one with POST /api/v1/data/showcase_task carrying owner_id usr_showcase_auditor_demo (examples/app-showcase/src/security/demo-personas.ts), and delete it at teardown" ], "knownGaps": [ "examples/app-showcase/e2e/showcase-smoke.spec.ts (the showcase's browser smoke, run by Nightly Tiers) sweeps these pages, Command Center 大屏 / showcase_command_center included, and only asserts render-clean (no pageerror / no placeholder leak / non-empty
past the boot splash / a laid-out chart). This item DEEPENS that shallow pin: it asserts the block composition and the data binding, which the smoke does not — so it is NOT marked `automated`.", @@ -1074,11 +1108,11 @@ "Command Center (大屏): open /_console/apps/showcase_app/page/showcase_command_center (the objectui smoke drives the package-id form /apps/com.example.showcase/page/showcase_command_center); screenshot the full-bleed board FIRST (confirm render), THEN read the region DOM", "object-metric KPIs (kind:'full', variant:'bare'): read the 6 hero tiles and cross-check them against the data plane — '活跃项目 Active' == count of showcase_project where status=active, '待办任务 Open' == count of showcase_task where status != done, '风险项目 At-Risk' == count of showcase_project where health=red (aggregate authored in command-center.page.ts)", "object-chart (bar / donut / area) bound to datasets: confirm each chart drew a sized SVG (no recharts width(-1)/height(-1) collapse) with more than one bucket, and cross-check the '任务状态分布' bar buckets against the showcase_task_metrics dataset aggregation (dimensions ['status'], values ['task_count'])", - "object-grid (待审核列表 work queue) bound to showcase_task: the grid's rows and its columns (title/project/status/priority/due_date) match GET /api/v1/data/showcase_task", + "object-grid (待审核列表 work queue) bound to showcase_task: the grid's rows and its columns (title/project/status/priority/due_date) match GET /api/v1/data/showcase_task filtered to status = in_review, the queue's own filter (IN_REVIEW, examples/app-showcase/src/ui/pages/command-center.page.ts)", "My Work: open /_console/apps/showcase_app/page/showcase_my_work — the personal object-grid is filtered `owner_id = {current_user_id}`; cross-check its rows against GET /api/v1/data/showcase_task filtered to the signed-in user's id, and confirm the page:card 'Leadership View' (visible: user.email == 'admin@objectos.ai') renders for admin", "Project Workspace: open /_console/apps/showcase_app/page/showcase_project_workspace — the object-master-detail-form renders the showcase_project parent form AND the inline Tasks child (the block composes parent + children, not a bare error)", - "Active Projects (type:'list', interfaceConfig): open /_console/apps/showcase_app/page/showcase_active_projects — the always-on base filterBy (status != completed) hides completed rows, the default sort (budget desc) orders them, and the grid/kanban visualization switch + health/status userFilters render", - "degradation probe (scratch page, no shipped file touched): PUT /api/v1/meta/page/qa_page_probe?mode=draft a type:'app' page with (a) an object-grid whose objectName is a NONEXISTENT object and (b) a component whose `type` is a bogus string; publish; open /_console/apps/showcase_app/page/qa_page_probe and screenshot — each bad block must surface a NAMED error (SchemaRenderer 'Unknown component type: ' red panel / the SchemaErrorBoundary 'Component … failed to render' panel / the block's own empty-or-error state), never a silently blank region", + "Active Projects (type:'list', interfaceConfig): open /_console/apps/showcase_app/page/showcase_active_projects — the always-on base filterBy (status != completed) hides completed rows, the default sort (budget desc) orders them, and the grid/kanban visualization switch (since objectui#12423 the Layout row, Grid / Kanban, inside the list toolbar's Display popover, not a toolbar control) + health/status userFilters render", + "degradation probe (scratch page, no shipped file touched): PUT /api/v1/meta/page/qa_page_probe?mode=draft a type:'app' page with (a) an object-grid whose objectName is a NONEXISTENT object and (b) a component whose `type` is a bogus string; publish it (POST /api/v1/meta/page/qa_page_probe/publish, 200); open /_console/apps/showcase_app/page/qa_page_probe and screenshot — each bad block must surface a NAMED error (SchemaRenderer 'Unknown component type: ' red panel / the SchemaErrorBoundary 'Component … failed to render' panel / the block's own empty-or-error state), never a silently blank region", "capture the server log across the whole sweep (the render-clean and no-restart baseline)" ], "acceptance": [ @@ -1089,7 +1123,7 @@ "evidence": "per-page screenshot + region DOM read" }, { - "clause": "a data-bound block shows SEEDED rows, cross-checked against the server not pixels — command-center's object-grid rows equal GET /api/v1/data/showcase_task, and the '活跃项目 Active' object-metric equals the active-project count from the data plane", + "clause": "a data-bound block shows SEEDED rows, cross-checked against the server not pixels — command-center's object-grid rows equal GET /api/v1/data/showcase_task filtered to status = in_review (the work queue's own filter), and the '活跃项目 Active' object-metric equals the active-project count from the data plane", "oracle": "api", "verify": "the grid's visible rows and the KPI numbers each match the same query/filter run against /api/v1/data/* — a tile or grid that renders but disagrees with the data plane is caught here", "evidence": "the data reads paired with the tile/grid screenshot" @@ -1107,7 +1141,7 @@ "evidence": "the filtered data read + the grid screenshot" }, { - "clause": "the interface (list) page honours its interfaceConfig: active-projects hides completed rows (base filterBy status != completed), orders by budget desc, and exposes the grid/kanban switch plus the health/status user-filters — the page IS the view definition (ADR-0047), not a bare object dump", + "clause": "the interface (list) page honours its interfaceConfig: active-projects hides completed rows (base filterBy status != completed), orders by budget desc, and exposes the grid/kanban switch (Display › Layout) plus the health/status user-filters — the page IS the view definition (ADR-0047), not a bare object dump", "oracle": "screenshot", "verify": "the rendered list shows no completed rows, biggest-budget first, with the visualization switch and userFilters controls present; cross-check the no-completed-rows claim against the data plane", "evidence": "the list screenshot + a data read confirming completed rows exist but are filtered out" @@ -1147,7 +1181,8 @@ ], "history": [ { "revision": 1, "date": "2026-08-08", "change": "new item: declarative custom pages render their real block composition and bind data (API cross-check, not pixels), with a scratch-page probe proving a bad binding degrades to a NAMED error not a blank — grounded in the four showcase declarative page sources, the PageComponentType enum, objectui PageRenderer + SchemaRenderer, and deepening the shallow showcase-smoke render-clean pin", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 2, "date": "2026-10-28", "change": "knownGaps/source re-pointed from ui/e2e/live/showcase-smoke.spec.ts, deleted by the triage of the live specs that ran in no CI lane (ruling Q3 B) as a duplicate: every surface it swept and each of its four assertions are in examples/app-showcase/e2e/showcase-smoke.spec.ts, which Nightly Tiers runs, with a wider surface list and stricter waits. That sweep includes showcase_command_center, which the old note said it did not. Still not marked automated: the sweep is render-clean only. No clause changed", "ref": "#25914" } + { "revision": 2, "date": "2026-10-28", "change": "knownGaps/source re-pointed from ui/e2e/live/showcase-smoke.spec.ts, deleted by the triage of the live specs that ran in no CI lane (ruling Q3 B) as a duplicate: every surface it swept and each of its four assertions are in examples/app-showcase/e2e/showcase-smoke.spec.ts, which Nightly Tiers runs, with a wider surface list and stricter waits. That sweep includes showcase_command_center, which the old note said it did not. Still not marked automated: the sweep is render-clean only. No clause changed", "ref": "#25914" }, + { "revision": 3, "date": "2026-11-04", "change": "fixtures.requires (a planted foreign-owned task), steps[4], [7] and [8], acceptance[1].clause and acceptance[4].clause corrected from QA run #29893 (W5-STUDIO, at 589049b0): the work queue is filtered status = in_review; acceptance[3] needs a task the admin does not own, which the stock seed lacks; the grid/kanban switch is Display › Layout since objectui#12423; the probe page publishes through POST /api/v1/meta/page/NAME/publish. Read at base f68b47ca", "ref": "#29906" } ] }, { @@ -1342,7 +1377,7 @@ "title": "Studio Automations pillar on a read-only package: a packaged flow is NOT editable in place — New lands in the base's .custom pair, autosave never writes the base, the header switch is the activation-ledger switch (a toggle, no metadata write), and inspector edits are inert or announced, never silently discarded", "since": "v17", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "browser", "personas": ["admin (opens Studio on the read-only package)"], @@ -1363,7 +1398,7 @@ "boot the showcase (objectstack dev --ui --seed-admin -p -d file:/tmp//data.db), sign in admin@objectos.ai/admin123; open Studio, switch to the read-only com.example.showcase package, open the Automations pillar", "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: record the rail's New (+) — offered on the read-only base since ADR-0146 D3 — and open its dialog, reading where it says the flow lands (the pair, com.example.showcase.custom), then cancel; select the packaged flow (do NOT press Customize) and wait past the autosave debounce after touching the canvas — no flow save naming the base may leave the browser", + "affordance sweep with the network panel recording: record the rail's New (+) — offered on the read-only base since ADR-0146 D3 — and open its dialog. It asks only for a name and an identifier and names no target package, so prove where a New lands by creating one: confirm a scratch flow (e.g. qa_lock_probe), read the save request's package and GET /api/v1/meta/flow/qa_lock_probe?state=draft&package=com.example.showcase.custom, and delete the scratch flow at teardown; select the packaged flow (do NOT press Customize) and wait past the autosave debounce after touching the canvas — no flow save naming the base may leave the browser", "click the header switch (data-testid auto-ledger-switch, reading 'On for this deployment') and capture everything: the request it fires (POST /api/v1/automation/showcase_urgent_task_alert/toggle {\"enabled\": false}, expected 200), that no metadata write (PUT /meta/flow…, a draft save) accompanies it, and the switch's new reading; then flip it back on", "select a flow node and edit a property in the inspector (Customize NOT pressed); 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", @@ -1373,8 +1408,8 @@ { "clause": "the courtesy gate holds on the base: on a read-only package the rail's New creates into the base's customization pair, never into the base, and the flow autosave never fires against the base — no draft write naming com.example.showcase leaves the browser from ordinary canvas edits", "oracle": "network", - "verify": "the rail's New is present and its dialog targets com.example.showcase.custom (openCreateFlow is offered when !packageReadOnly || newInPair, StudioDesignSurface.tsx; QA run #29002 measured it present and targeting the pair); the network trace across the edit session and past the autosave debounce carries zero flow save/PUT naming the base (the autosave is blocked while editLocked = readOnly && !flowUnlocked). A New that writes into com.example.showcase, or a draft save naming it, is the FAIL", - "evidence": "the New dialog's target + the base-write-free network trace over the debounce window" + "verify": "the rail's New is present (openCreateFlow is offered when !packageReadOnly || newInPair, StudioDesignSurface.tsx) and a flow created from it is saved into com.example.showcase.custom: the dialog names no target, so the created flow's save request and the pair's draft read are the evidence (QA run #29890 found the dialog silent on the target); the network trace across the edit session and past the autosave debounce carries zero flow save/PUT naming the base (the autosave is blocked while editLocked = readOnly && !flowUnlocked). A New that writes into com.example.showcase, or a draft save naming it, is the FAIL", + "evidence": "the created flow's save request and the pair's draft read + the base-write-free network trace over the debounce window" }, { "clause": "a packaged flow definition is not editable in place in Studio (ADR-0126 §2 Regime C, card F2): after driving every affordance the pillar offers, the flow's server-side metadata is byte-identical — and any write that slips past client courtesy is refused server-side with a 403 — code ITEM_LOCKED when the write names the read-only package (?package=com.example.showcase: the named read-only base), NOT_OVERRIDABLE when it does not (flow declares allowEnvironmentOverlay:false) — each naming the clone or toggle remedy (ADR-0124 D1: the server is the authority, the client gate is courtesy)", @@ -1421,6 +1456,12 @@ "date": "2026-11-02", "change": "Re-pointed to ADR-0146 D3/D4 on the read-only base. Measured input: QA run #29002 (W4-C3b1, 7fa9873f; card note 6570476620), which failed acceptance[0]'s New half and acceptance[2] as stale spec and passed acceptance[1], [3], negative[0] and [1]. Re-read at base dfbe34d887: acceptance[0] now asks that New (offered since #28572) lands in com.example.showcase.custom and that nothing writes the base; acceptance[2] asks that the header switch, now the activation-ledger switch (FlowLedgerSwitch, auto-ledger-switch, #28148), fires the toggle and no metadata write, the definition byte-identical — the property the clause always guarded; acceptance[3] says Customize is not pressed. fixtures.knownGaps record the changed shape, that the 11124 pin answers _status 404 (so AutomationsPillar.ledgerSwitch-28148.test.tsx pins the live switch), and that Customize (ADR-0146 D9) is out of this item's scope; steps[3]–[5] and source follow. No clause weakened: the lock on the base is asserted as before", "ref": "#28579" + }, + { + "revision": 8, + "date": "2026-11-04", + "change": "steps[3] and acceptance[0] (verify and evidence) corrected from QA run #29890 (W5-FAIL, at 589049b0): the New flow dialog names no target package, so where a New lands is proven by creating a scratch flow and reading its save and the pair's draft. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -1743,7 +1784,7 @@ "title": "A picklist authored in a package is used by two objects' select fields from the designer: both forms offer its options, the bound field refuses inline options, and a GET → edit → PUT of an object with a bound field saves", "since": "v17.7", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", "personas": [ @@ -1752,24 +1793,24 @@ "fixtures": { "app": "showcase", "requires": [ - "a package that declares a picklist and one picklistExtensions entry adding one option to it; the stock showcase declares none, and the kind is package-owned (allowRuntimeCreate false), so add both in the run's own checkout, uncommitted: inside the defineStack call of examples/app-showcase/objectstack.config.ts add `picklists: [definePicklist({ name: NAME, label: LABEL, options: [two or more { label, value }] })]` (definePicklist is exported from '@objectstack/spec') and `picklistExtensions: [{ extend: NAME, options: [{ label, value }] }]`, whose one value the list does not already carry (a repeated value is refused at load with INVALID_METADATA). The extension may sit in the same defineStack as the list; it merges the same way as one from another package. Boot with `--compile` so the artifact carries the edit (os dev otherwise serves an existing dist/objectstack.json), and revert the file after the run", + "a package that declares a picklist and one picklistExtensions entry adding one option to it; the stock showcase declares none, and the kind is package-owned (allowRuntimeCreate false), so add both in a scratch copy of the showcase, never the shared tree: inside the defineStack call of examples/app-showcase/objectstack.config.ts add `picklists: [definePicklist({ name: NAME, label: LABEL, options: [two or more { label, value }] })]` (definePicklist is exported from '@objectstack/spec') and `picklistExtensions: [{ extend: NAME, options: [{ label, value }] }]`, whose one value the list does not already carry (a repeated value is refused at load with INVALID_METADATA). The extension may sit in the same defineStack as the list; it merges the same way as one from another package. Boot the copy with a plain os dev: since #26130 a default os dev compiles objectstack.config.ts into the copy's own dist/objectstack.json at boot (devCompilesAtBoot, packages/cli/src/utils/dev-compile-at-boot.ts), so no --compile flag is needed, and the shared checkout's artifact is never rewritten (RUNNER: on a shared checkout boot with --no-compile)", "a writable project package with two runtime objects, each with a sharingModel and one select field carrying inline options; one of them published with a field already bound (picklist: NAME, no options)", "the console under test carries objectui#10202's build (objectui PR 11669, merged as objectui c3623eb1). The objectui code under ui/ carries it (ui/packages/app-shell/src/services/builtinComponents.tsx cites objectui#10202); a console built before that merge does not (the pin 0abd4f9f at objectstack 80f9f7e6 did not), so build the console from this tree with `pnpm console:build` and name the commit it was built from in the run record" ], "knownGaps": [ - "THE PICKLIST FIXTURE IS NOT REACHABLE ON A STOCK SHOWCASE BOOT, AND NO RUNTIME DOOR MINTS ONE (QA run #26715, Data card, recorded the item blocked(fixture)): GET /api/v1/meta/picklist answers items: [] on stock (the showcase declares no picklist), PUT /api/v1/meta/picklist/NAME is refused 403 — the kind is code-only (allowRuntimeCreate false, packages/spec/src/kernel/metadata-plugin.zod.ts) — and with no served list the designer's 'Options from' offers only the field's own options. fixtures.requires[0]'s uncommitted checkout edit, booted with --compile, is the ONLY way in: a run that does not make it scores every clause blocked(fixture) citing this line, never fail. Read GET /api/v1/meta/picklist/NAME (step 1) before anything else to prove the edit reached the served artifact" + "THE PICKLIST FIXTURE IS NOT REACHABLE ON A STOCK SHOWCASE BOOT, AND NO RUNTIME DOOR MINTS ONE (QA run #26715, Data card, recorded the item blocked(fixture)): GET /api/v1/meta/picklist answers items: [] on stock (the showcase declares no picklist), PUT /api/v1/meta/picklist/NAME is refused 403 — the kind is code-only (allowRuntimeCreate false, packages/spec/src/kernel/metadata-plugin.zod.ts) — and with no served list the designer's 'Options from' offers only the field's own options. fixtures.requires[0]'s edit in a scratch copy is the ONLY way in: a run that does not make it scores every clause blocked(fixture) citing this line, never fail. Read GET /api/v1/meta/picklist/NAME (step 1) before anything else to prove the edit reached the served artifact" ] }, "steps": [ "boot the showcase on its own port and DB with the console under test; sign in as the seeded admin", "GET /api/v1/meta/picklist and GET /api/v1/meta/picklist/NAME — the list is served with name, label, its own options and _packageId", "GET /api/v1/meta/object/OBJ_A/layers for the object whose field is already bound — record the bound field as served (picklist + resolved options, the extension's option included)", - "metadata admin (Setup → Metadata → Object → OBJ_A): select an UNRELATED field, rename its label, let the autosave run; record the PUT body and status", + "the metadata admin, which is Studio's All metadata area since ADR-0146 D1 (#28818): open /_console/studio/~all/metadata/object/OBJ_A (Setup's /_console/apps/setup/metadata/object/OBJ_A redirects there, setupMetadataStudioPath; the package leg /_console/studio/PKG/metadata/object/OBJ_A opens the Data pillar instead, areaItemPillarPath) and select an UNRELATED field, rename its label, let the autosave run; record the PUT body and status", "select the bound field: record what the select-field editor shows (Options from = the picklist; the values read-only; no inline options editor)", "open OBJ_B, select its inline select field, open 'Options from', choose the picklist; record the PUT body and status", "Studio → the package → Data → OBJ_A: add a field and let the autosave run; record the PUT body and status", - "publish both objects; open the new-record form of each and open the bound select", - "Setup → Metadata → Picklist → NAME: record what the page shows and offers" + "publish both objects; open the new-record form of each (a fresh package's runtime objects have no app-nav route, so use the Studio Records tab's 'Add record' dialog) and open the bound select", + "the picklist in Studio: /_console/studio/~all/metadata/picklist lists it (Setup's /_console/apps/setup/metadata/picklist redirects there) and its page is /_console/studio/com.example.showcase/metadata/picklist/NAME; record what the page shows and offers" ], "acceptance": [ { @@ -1799,7 +1840,7 @@ { "clause": "a bound field refuses inline options: no inline options editor (no 'Add value'), the list's values shown read-only with the reason", "oracle": "dom", - "verify": "the field inspector", + "verify": "the field inspector. It lists the list's OWN values only, under the note 'Values other packages add to it are offered too', so the extension's option is not listed there; the merged set is acceptance[6]'s, on the forms", "evidence": "screenshot" }, { @@ -1811,7 +1852,7 @@ { "clause": "after publish, both objects' new-record forms offer the picklist's options, the extension's option included", "oracle": "dom", - "verify": "open each bound select", + "verify": "open each bound select in the Records tab's 'Add record' dialog (steps[7])", "evidence": "the two option lists" }, { @@ -1838,7 +1879,8 @@ { "revision": 1, "date": "2026-10-06", "change": "new — drafted with objectui#10202 against the measured run of its PR; filed by the seat. Authored from that draft with these changes: since v17.7, because 17.6.0 shipped before objectui PR 11669 merged; the picker clause split in two, one oracle each; the picklist fixture spelled with its export, its stack keys and the reload it needs; the console fixture naming the merge it must carry; the spec citations written as anchors the gate resolves", "ref": "#21957" }, { "revision": 2, "date": "2026-10-20", "change": "steps[0] respelled: 'boot the showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" }, { "revision": 3, "date": "2026-10-26", "change": "fixtures.requires[2] drops the retired pin route (.objectui-sha, pnpm objectui:refresh from a ../objectui), which #26035 left to this file's card: the console builds from this tree with pnpm console:build, and the objectui code under ui/ carries objectui#10202. Wording only; the clauses are unchanged", "ref": "#25848" }, - { "revision": 4, "date": "2026-10-27", "change": "fixtures.knownGaps[0] added — the fixture gap the Data card of QA run #26715 met (at 3fcafcd3): a stock showcase serves no picklist, the runtime door refuses to create one (403, code-only kind), and the designer then offers only inline options, so the item is runnable only on the fixtures.requires[0] checkout edit and is blocked(fixture) without it. Recorded so the next run does not rediscover it. No clause changed", "ref": "#26715" } + { "revision": 4, "date": "2026-10-27", "change": "fixtures.knownGaps[0] added — the fixture gap the Data card of QA run #26715 met (at 3fcafcd3): a stock showcase serves no picklist, the runtime door refuses to create one (403, code-only kind), and the designer then offers only inline options, so the item is runnable only on the fixtures.requires[0] checkout edit and is blocked(fixture) without it. Recorded so the next run does not rediscover it. No clause changed", "ref": "#26715" }, + { "revision": 5, "date": "2026-11-04", "change": "fixtures.requires[0], fixtures.knownGaps[0], steps[3], [7] and [8] and acceptance[4] and [6].verify corrected from QA run #29893 (W5-STUDIO, at 589049b0). Setup's metadata routes redirect into Studio's All metadata area (ADR-0146 D1, #28818), so steps[3] and [8] name the Studio routes, as object-designer-roundtrip r12 does; a fresh package's objects have no app-nav new-record route, so the form is the Records tab's 'Add record' dialog; 'Boot with --compile' is stale since #26130 (a default os dev compiles), and the edit belongs in a scratch copy; the bound field's inspector lists the list's own values only. steps[4] names no route and is unchanged. This fixture line overlaps open #29819, which skips it. Read at base f68b47ca", "ref": "#29906" } ] }, { @@ -1926,7 +1968,7 @@ "title": "One item name, one stored row PER PACKAGE (ADR-0048) on v18: a package-less save resolves its owning package and an ambiguous one is 409, a dropStorage delete while another package's row remains is 409 and keeps the table, a save into the wrong package is 422 OBJECT_OVERLAY_PACKAGE_MISMATCH, a sealed packaged name answers one ruled 403 NOT_OVERRIDABLE text, and revert discards a Studio package's drafts", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": [ @@ -1966,9 +2008,9 @@ "evidence": "refusal + read-back" }, { - "clause": "a sealed packaged name answers 403 NOT_OVERRIDABLE with the ONE ruled text every write over a sealed name reads, naming TYPE/NAME as managed and sealed, and the list door", + "clause": "a sealed packaged name of a kind with no regime row (an object, as the step drives; a hook) answers 403 NOT_OVERRIDABLE with the one ruled text those kinds' writes read, naming TYPE/NAME as managed and sealed, and the list door", "oracle": "api", - "verify": "the refusal's message is exactly \"'TYPE/NAME' is provided by a managed package and is sealed: it cannot be written here. GET /api/v1/meta/TYPE lists the names in use.\": two sentences, the seal and then the list door (packages/metadata-protocol/src/packaged-base-regime.ts managedItemSealedSentence, its save arm; pinned by packages/rest/src/rest-meta-sealed-first-draft-remedy.test.ts), with no create or source-artifact advice", + "verify": "the refusal's message is exactly \"'TYPE/NAME' is provided by a managed package and is sealed: it cannot be written here. GET /api/v1/meta/TYPE lists the names in use.\": two sentences, the seal and then the list door (packages/metadata-protocol/src/packaged-base-regime.ts managedItemSealedSentence, its save arm; pinned by packages/rest/src/rest-meta-sealed-first-draft-remedy.test.ts), with no create or source-artifact advice. Other kinds read other sentences and are not this clause's: a package-less write over a packaged view or dashboard reads ADR-0146 D11's pair remedy ('… save it into ….custom'), and a flow reads its regime's sentence ('sealed against in-place edits. Declare an override … or switch it off', pinned by packages/metadata-protocol/src/protocol.sealed-remedy-kind.test.ts)", "evidence": "the body" }, { @@ -2015,6 +2057,12 @@ "date": "2026-10-31", "change": "acceptance[3] and the title name the ruled refusal text, and steps[2] says the A-owned object needs A's namespace prefix. The write-over-a-sealed-name refusal is one fixed text in two sentences, \"'TYPE/NAME' is provided by a managed package and is sealed: it cannot be written here. GET /api/v1/meta/TYPE lists the names in use.\" (packages/metadata-protocol/src/packaged-base-regime.ts, pinned by packages/rest/src/rest-meta-sealed-first-draft-remedy.test.ts), so the old verify 'error.message is a single sentence' failed the very text the pins assert. The verify now asks for that exact text, which is stronger. QA run #27438 measured it on showcase_project and authored pkga_qa_obj for steps[2]: an unprefixed object name is refused 422 NAMESPACE_PREFIX by a publish-mode save before the mismatch gate is reached. Accuracy findings of QA run #27438. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27438" + }, + { + "revision": 5, + "date": "2026-11-04", + "change": "acceptance[3] (clause and verify) scoped from QA run #29893 (W5-STUDIO, at 589049b0): the managed-item sealed sentence is read by kinds with no regime row (object, hook); a packaged view or dashboard reads the D11 pair remedy and a flow its regime's sentence. The step drives an object, so the verdict stands. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2118,7 +2166,7 @@ "title": "Studio asks before an unsent edit is left behind: a held (incomplete) or dirty edit in any pillar survives a pillar switch, a rail switch, the package switcher, Home and a reload — or is discarded only after the author confirms", "since": "v18", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "browser", "personas": [ @@ -2133,7 +2181,7 @@ }, "steps": [ "open Studio on the writable package (/_console/studio/PKG/data) with the network panel recording; register a Playwright dialog handler BEFORE each departure (page.on('dialog')) so a native confirm or a beforeunload prompt is recorded and answered deliberately, never auto-dismissed", - "Data, held choice field: open the object → Form tab → Add field, type Picklist, label Priority, no option. The strip reads 'Not saved yet: the field “Priority” needs at least one option. Your changes are kept here and saved once it is filled in.' and no PUT for it leaves the browser", + "Data, held choice field: open the object → Form tab → Add field, type Picklist, a fresh label no earlier run published on this object (e.g. 'QA Priority 2': a label already taken leaves Create disabled with no error, and 'Priority' collides with any earlier run that published it), no option. The strip reads 'Not saved yet: the field “LABEL” needs at least one option. Your changes are kept here and saved once it is filled in.' and no PUT for it leaves the browser", "from that held state drive each departure on a fresh held edit: (a) the Automations pillar link; (b) another object in the Data rail; (c) the package switcher → another package; (d) the header Home; (e) a tab reload. For each, record whether the confirm 'You have unsaved changes. Leave this page anyway?' (engine.edit.unsavedLeaveConfirm) or the browser's leave-site prompt appeared. Answer Stay / Cancel first and check the field is still on screen; then repeat and answer Leave", "Data, held relationship: Add field, type Lookup, with no target — the strip names 'the field “…” needs the object it links to'; repeat departures (a) and (e)", "Automations, held step: open the flow, add a step whose required input is empty — the strip names '{input} on the step “{step}” needs a value'; repeat departures (a), (b) with another flow in the rail, and (e)", @@ -2151,7 +2199,7 @@ { "clause": "a reload or tab close while an edit is held or dirty raises the browser's leave-site prompt (beforeunload)", "oracle": "dom", - "verify": "page.reload() with the handler registered records a dialog of type 'beforeunload' for each held or dirty pillar, and none for a clean one", + "verify": "page.reload() with the handler registered records a dialog of type 'beforeunload' for each held or dirty pillar, and none for a clean one. That negative half needs user activation: Chrome withholds beforeunload on a page the user has not interacted with, so click inside the clean pillar before the reload (navigator.userActivation.hasBeenActive true); a reload without activation proves nothing", "evidence": "the dialog events per pillar" }, { @@ -2219,6 +2267,12 @@ "date": "2026-11-04", "change": "source[0] re-pointed: confirmDropUnsavedWork and useHeldEditReport moved unchanged from ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx to StudioPillarKit.tsx beside it (#29988 slice 4, a move-only split); the rest of the citation stays on StudioDesignSurface.tsx. Re-read at base 9d1a465f0. No clause changed", "ref": "#29988" + }, + { + "revision": 7, + "date": "2026-11-04", + "change": "steps[1] and acceptance[1].verify corrected from QA run #29893 (W5-STUDIO, at 589049b0): the label 'Priority' collides with an earlier run's published field (the card's steps[2] is steps[1] at this base), and the clean-pillar 'no prompt' half needs user activation first. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2227,7 +2281,7 @@ "title": "Publish refuses while an edit is held: the refusal names what the held edit needs, nothing is published and no draft changes — and once the edit is finished, Publish goes", "since": "v18", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": [ @@ -2245,7 +2299,7 @@ "Data: add a Picklist field with no option (held: 'the field “…” needs at least one option'); click Publish, then 'Publish all' in the review sheet; capture the toast and the network trace", "finish the field (an option with a value), let the autosave land, Publish again", "Automations: add a step and leave its required input empty (held); Publish; capture; finish the input; Publish again", - "Interfaces: add a dashboard widget with no Dataset (held); Publish; capture; bind it; Publish again", + "Interfaces: add a dashboard widget with no Dataset (held); Publish; capture; bind it, a Dataset and then a measure (with the Dataset alone the widget is held again on 'Values (measures) … needs a value', engine.studio.held.widgetNeedsInput); Publish again", "after each refused Publish re-read GET /api/v1/meta/_drafts?packageId=PKG and the live reads; after each accepted Publish read the toast's count and the live read of the edited item" ], "acceptance": [ @@ -2303,6 +2357,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 4, + "date": "2026-11-04", + "change": "steps[4] corrected from QA run #29893 (W5-STUDIO, at 589049b0): binding a KPI widget takes a dataset and a measure; with the dataset alone the widget is held again. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2311,7 +2371,7 @@ "title": "The Automations rail groups flows by the object they watch, searches by label and name, says when nothing matches, and explains its glyphs and dots in a legend; New automation's presets refuse until the watched object is chosen, and a new flow starts Off", "since": "v18", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -2327,10 +2387,10 @@ "steps": [ "open /_console/studio/PKG/automations; screenshot the rail settled", "grouping: the record-triggered flows sit under a heading per watched object (the object's label), the rest in a trailing group; each row carries its trigger-kind glyph (record / scheduled / screen / autolaunched) and a status dot", - "legend: read the rail's footer note (data-testid auto-rail-legend): the four trigger glyphs with their words and the On and Not-running dots; Off and Refused are named only while some flow in the rail reads so — disable one flow and re-read", + "legend: read the rail's footer note (data-testid auto-rail-legend): the four trigger glyphs with their words and the 'Switched on' and 'Not running here' dots; 'Switched off' and 'Refused by the engine' are named only while some flow in the rail reads so — disable one flow and re-read", "search: type part of a flow's LABEL in mixed case, then part of a flow's machine NAME that its label does not hold, then a string nothing matches; clear the box", "New automation: open it and read the presets (record created / updated / deleted, manual) and the Advanced fold; pick 'record created' and try to create without choosing the object — capture the refusal ('Choose the object it watches.'); choose the object, give a name, create", - "read the new flow: its Enabled switch reads off under 'Off by default', and GET /api/v1/meta/flow/NAME?state=draft&package=PKG carries status 'obsolete' and a Start node whose trigger names the chosen object and event" + "read the new flow: its Enabled switch reads 'Switched off' under 'Off by default · review before enabling', and GET /api/v1/meta/flow/NAME?state=draft&package=PKG carries status 'obsolete' and a Start node whose trigger names the chosen object and event" ], "acceptance": [ { @@ -2340,9 +2400,9 @@ "evidence": "the rail screenshot, described" }, { - "clause": "the legend names what the rail shows: four trigger glyphs and the On / Not-running dots always, Off and Refused only while a flow in the rail is in that state", + "clause": "the legend names what the rail shows: four trigger glyphs and the 'Switched on' / 'Not running here' dots always, 'Switched off' and 'Refused by the engine' only while a flow in the rail is in that state", "oracle": "dom", - "verify": "after a screenshot confirms the rail rendered, the auto-rail-legend note lists the glyph words and On / Not running; Off appears only after a flow is disabled", + "verify": "after a screenshot confirms the rail rendered, the auto-rail-legend note lists the glyph words and 'Switched on' / 'Not running here' (engine.studio.auto.on, .notRunning; respelled from On / Off by #29784 / #29841); 'Switched off' appears only after a flow is disabled", "evidence": "the legend DOM before and after the disable" }, { @@ -2392,6 +2452,12 @@ "date": "2026-11-04", "change": "source[0] re-pointed: FLOW_GLYPH_ICON / FLOW_GLYPH_LABEL_KEY, flowDotClass and FlowStatusDot moved unchanged from ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx to AutomationsFlowRail.tsx beside it (#29988 slice 4, a move-only split); the rail legend and the New automation presets stay. Re-read at base 9d1a465f0. No clause changed", "ref": "#29988" + }, + { + "revision": 4, + "date": "2026-11-04", + "change": "steps[2] and [5] and acceptance[1] respelled from QA run #29893 (W5-STUDIO, at 589049b0): the dots and the header switch read 'Switched on' / 'Switched off' since #29784 / #29841. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2400,7 +2466,7 @@ "title": "Flow edges are authored in Studio as the spec types them — Normal, Fault, Conditional and Back-edge write default / fault / conditional / back with their label, condition and default-branch flag — and an invalid edge condition is flagged before publish, not only by it", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", "personas": [ @@ -2420,7 +2486,7 @@ "open the flow in /_console/studio/PKG/automations; select the edge Decision → End A; in the Edge inspector set Type to Conditional, untick Default branch, give a Branch label and the Condition `record.status == 'open'`; let the autosave land", "select Decision → End B and mark it Default branch; select the faulting node's outgoing error edge and set Type to Fault; on a loop (if the flow has one) set Type to Back-edge; let each autosave land", "GET /api/v1/meta/flow/NAME?state=draft&package=PKG and read each edge's type, label, condition and isDefault", - "invalid condition: set one conditional edge's Condition to `record.name ==`; read the inspector, the Problems tab and the draft read's _diagnostics", + "invalid condition: set one conditional edge's Condition to `record.name ==`; untick 'Default branch (else)' first, since the Condition is disabled while it is ticked, and reach the Condition input by its placeholder 'CEL expression — taken when it evaluates true', since it has no accessible label (VariableTextInput in FlowEdgeInspector.tsx); read the inspector, the Problems tab and the draft read's _diagnostics", "publish the package (Publish → Publish all); capture the refusal; fix the condition and publish again" ], "acceptance": [ @@ -2492,6 +2558,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 5, + "date": "2026-11-04", + "change": "steps[3] corrected from QA run #29893 (W5-STUDIO, at 589049b0): the Condition input is disabled while 'Default branch (else)' is ticked and has no accessible label, only its placeholder. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2576,10 +2648,10 @@ }, { "id": "studio-authoring.flow-status-matches-runtime", - "title": "The flow state Studio shows is the state the deployment runs: the rail dot and the header read what GET /api/v1/automation/_status reports — On, Off, Not running here with the platform's reason — and an enabled record-triggered flow actually runs", + "title": "The flow state Studio shows is the state the deployment runs: the rail dot and the header read what GET /api/v1/automation/_status reports — Switched on, Switched off, Not running here with the platform's reason — and an enabled record-triggered flow actually runs", "since": "v18", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": [ @@ -2595,7 +2667,7 @@ }, "steps": [ "on the default boot open /_console/studio/PKG/automations; for every flow read the rail dot's words (the visually hidden text and the row title) and the header's status, then GET /api/v1/automation/_status and pair each flow's row (enabled, bound, reason)", - "enable the record-triggered flow with the header switch (it was Off by default); read the toast; publish; re-read the rail, the header and _status", + "enable the record-triggered flow with the header switch (it was Off by default); read the toast ('Switched on — publish to apply', engine.studio.auto.enabledToast); publish; re-read the rail, the header and _status", "create a record of the watched object (POST /api/v1/data/OBJ) and read the flow's runs for it", "read the schedule flow on the default boot: its dot and header read 'Not running here', titled with the platform's reason, and Problems carries a note (not counted as a problem); _status answers bound: false with that same reason", "reboot with OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1 exported; re-read the schedule flow in Studio and _status, and wait for its first run", @@ -2603,7 +2675,7 @@ ], "acceptance": [ { - "clause": "per flow, the rail dot and the header say what _status reports — On for an enabled bound or manual flow, Off for a disabled one, Not running here for an enabled flow the deployment does not bind", + "clause": "per flow, the rail dot and the header say what _status reports — 'Switched on' for an enabled bound or manual flow, 'Switched off' for a disabled one, Not running here for an enabled flow the deployment does not bind", "oracle": "api", "verify": "the pairing table: Studio words vs _status { enabled, bound } for every flow, on both boots; no row disagrees", "evidence": "the pairing table" @@ -2621,7 +2693,7 @@ "evidence": "the run reads" }, { - "clause": "on the boot that enables scheduled work, the same schedule flow reads On, _status reports bound: true, and it runs", + "clause": "on the boot that enables scheduled work, the same schedule flow reads 'Switched on', _status reports bound: true, and it runs", "oracle": "api", "verify": "Studio's words and _status agree on the second boot; a run with trigger type schedule completes within the schedule window", "evidence": "the second-boot pairing + the run" @@ -2663,6 +2735,12 @@ "date": "2026-11-04", "change": "source[0] re-pointed: FlowStatusDot and flowRailStatus moved unchanged from ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx to AutomationsFlowRail.tsx beside it (#29988 slice 4, a move-only split). Re-read at base 9d1a465f0. No clause changed", "ref": "#29988" + }, + { + "revision": 4, + "date": "2026-11-04", + "change": "title, steps[1], acceptance[0].clause and acceptance[3].clause respelled from QA run #29893 (W5-STUDIO, at 589049b0): rail sr-text and header Status read 'Switched on' / 'Switched off', and the toasts 'Switched on — publish to apply' / 'Switched off — publish to apply' (#29784 / #29841). Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2671,7 +2749,7 @@ "title": "Validation rules authored in Studio's Validations tab — the presets and all six rule types — save spec-valid, hold until their condition is given, and enforce on the write path once published, severity and the active switch included", "since": "v18", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": [ @@ -2690,7 +2768,7 @@ "steps": [ "open /_console/studio/PKG/data → the object → Validations → New: read the menu — a few presets in plain words first, the six rule types folded under Advanced", "presets: pick 'End date on or after start date' and 'Number can't be negative' (each written at once on the author's own fields, as editable rows); pick 'Reject the save when…' — it is held, not written, and opens with the condition focused; type a condition and watch it save", - "Advanced: author one rule of each remaining type with real settings — state_machine on the status field (one allowed transition; the tab has no initial-state input, fixtures.knownGaps[0]), format on the text field, json_schema on the long-text field, cross_field on the two dates, conditional wrapping a script rule — each with a message; set one rule's severity to warning and switch another rule off (active false)", + "Advanced: author one rule of each remaining type with real settings — state_machine on the status field (one allowed transition; the tab has no initial-state input, fixtures.knownGaps[0]; the rule is written as soon as it has a field and a message, with transitions {}, and the transition row comes after it, through 'Add transition'), format on the text field, json_schema on the long-text field, cross_field on the two dates, conditional wrapping a script rule — each with a message; set one rule's severity to warning and switch another rule off (active false)", "CEL: type an invalid condition into one rule and read the live verdict, then fix it", "let the autosave land, publish, and GET /api/v1/meta/object/OBJ: validations[] holds every rule as authored", "write path: for each rule POST or PATCH /api/v1/data/OBJ with the violating payload, then its happy-path twin; for the warning rule send a matching payload; for the inactive rule send the payload it would reject" @@ -2703,9 +2781,9 @@ "evidence": "the menu DOM + the network trace around the held preset" }, { - "clause": "the CEL editor gives a live verdict: an invalid condition shows an error at the input and holds the save until fixed", + "clause": "the CEL editor gives a live verdict: an invalid condition, whether a parse error (the client lint) or an expression the engine refuses (the server's verdict, e.g. a comparison on a nullable field), shows an error at the input and holds the save until fixed", "oracle": "network", - "verify": "no object PUT leaves while the error stands; the fix sends one, answered 200", + "verify": "no object PUT leaves while the error stands; the fix, to a valid condition that differs from the last saved one, sends one, answered 200 (a fix back to the saved text has nothing to save and sends none); drive both shapes of the clause", "evidence": "the inline error + the trace" }, { @@ -2762,6 +2840,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). Also acceptance[3] no longer names invalid_initial_state, and steps[2] no longer asks for an initial state: Studio's Validations tab has no initialStates input, so the state is API-only (new fixtures.knownGaps[0]; measured at base 37184244, ObjectValidationsPanel.tsx carries no initialStates). The insert-time code stays covered by records-forms.validation-rule-type-matrix. invalid_transition and every other per-type code are unchanged. Ruling 6545938831 item ③ on #28345 (no product card for a Studio input). Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 4, + "date": "2026-11-04", + "change": "steps[2] and acceptance[1] corrected from QA run #29893 (W5-STUDIO, at 589049b0): a state_machine rule is written with transitions {} before its transition row is added; the clause names both invalid shapes (client parse error, server-refused expression), and the fix must differ from the saved condition to send a PUT. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2770,7 +2854,7 @@ "title": "Hooks authored in Studio's Hooks tab save pinned and keep their name — a rename is held with the way back named — and their options take effect at runtime: the Run-only-when condition, the retry policy and On Error", "since": "v18", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -2781,7 +2865,7 @@ "requires": [ "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", "a writable package with one published object with a text field the hook body can stamp", - "the server log readable (the retry leg reads it) — boot with OS_LOG_LEVEL=info, as records-forms.object-hook-lifecycle does" + "the server log readable (the retry leg reads it). No log-level flag is needed: the retry lines ('[hook] retrying after failure' with attempt and maxRetries, packages/objectql/src/hook-wrappers.ts) are WARN and print at the default warn level" ], "knownGaps": [ "NOT JUDGED HERE: body.capabilities and runAs. Studio's Hooks tab now grants a sandboxed-JS body its capabilities (the Capabilities checkboxes, HookDefaultInspector), and a hook refused for a missing capability answers 403 INSUFFICIENT_PRIVILEGES naming the hook and the capability (packages/runtime/src/sandbox/quickjs-runner.ts#throwCapabilityDenial), not 500 INTERNAL_ERROR. No clause here scores either yet, so the bodies below use no ctx capability and runAs stays unjudged" @@ -2856,6 +2940,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 4, + "date": "2026-11-04", + "change": "fixtures.requires[2] corrected from QA run #29893 (W5-STUDIO, at 589049b0): OS_LOG_LEVEL=info is not needed, because the retry lines are WARN and the default level is warn. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2864,7 +2954,7 @@ "title": "Actions authored in Studio's Actions tab work on the record: the four presets are written at once or held until their target is given, a held one is dropped unsent by Delete, and each published preset does its job in the app — while the advanced editor writes actions the console can run", "since": "v18", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": [ @@ -2885,7 +2975,7 @@ "Run a flow / Open a web page / Open a page in a dialog: each is listed as not saved, its held line naming the input it waits for ('the action “…” needs “…”'), and no request leaves; give each its target (the flow, a URL with ${ctx.recordId} in it, the page) and watch it save", "start one more preset and Delete it while it is held: no request leaves, and it is gone", "publish; as the member open a record of the object in the app and click each action: change the status through the prompt; run the flow; open the web page; open the page dialog", - "Advanced: create an action with 'Run a script', give it a body, publish, and click it as the member; then tick 'Expose to AI agents' on an existing action and watch the next autosave" + "Advanced: create an action with 'Run a script' ('New action' is written at once as { operation: 'update', patch: {} } on the default script type, and choosing 'Run a script' holds it, '… on the action “…” needs a value', until a body is given); give it a body that writes, ticking api.write under Capabilities (ScriptCapabilitiesField, the grant a sandboxed-JS body needs, #29181); publish, and click it as the member; then tick 'Expose to AI agents' on an existing action and watch the next autosave" ], "acceptance": [ { @@ -2915,7 +3005,7 @@ { "clause": "an action the advanced editor writes as a script is runnable from the record: it is saved with type script and the member's click runs it", "oracle": "api", - "verify": "the draft read carries type: 'script' with the body; the click sends the action request and its effect lands", + "verify": "the draft read carries type: 'script' with the body and its capabilities grant (api.write for a body that writes); the click sends the action request and its write lands. A body that only returns proves no effect", "evidence": "the draft read + the action request/response" }, { @@ -2961,6 +3051,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 4, + "date": "2026-11-04", + "change": "steps[5] and acceptance[4].verify corrected from QA run #29893 (W5-STUDIO, at 589049b0): the new action is held until a body is given, and the editor now offers Capabilities for a sandboxed-JS body (ScriptCapabilitiesField, #29181), so the body writes (api.write) and the clause reads its effect. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -2969,7 +3065,7 @@ "title": "The Data pillar's API tab is a truthful REST reference: five endpoints under /api/v1/data/OBJECT, an example body built from the object's draft fields that the write path accepts, and copied cURL commands that succeed once a token is put in", "since": "v18", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": [ @@ -2985,7 +3081,7 @@ }, "steps": [ "open /_console/studio/PKG/data → the object → API: list the endpoints the tab shows (method + path) and read the create example body", - "add a field in the Form tab WITHOUT publishing; return to API and re-read the example body", + "add a field in the Form tab WITHOUT publishing; return to API and re-read the example body (with the inspector panel open, the flat tabs, API among them, fold into 'Advanced' below about 1000 px of container width: reach API through Advanced or close the panel)", "publish; copy each endpoint's cURL (Copy cURL), replace with the session token and each `` placeholder (a REQUIRED reference) with the id of an existing TARGET record, and run them in order: list, create (with the tab's example body), get, update, delete on the created id" ], "acceptance": [ @@ -3036,6 +3132,12 @@ "date": "2026-10-31", "change": "negative[0] covers the refusal the run actually met, and steps[2] says what to substitute. QA run #27438 found the tab's body refused 400 reference_not_found with right-typed values, the placeholder id 'related_record_id' in every reference field and in the auto-included owner_id, which the old parenthetical '(a value of the wrong type for its field)' did not name. The product half, #27490, is fixed by #27613: the example body now leaves out system fields and optional references and shows a required reference as a `` placeholder (ui/packages/app-shell/src/views/studio-design/ObjectApiPanel.tsx). The negative is wider, never narrower: a dangling reference is a FAIL as well as a wrong type. Accuracy finding of QA run #27438 (also recorded as its N0 finding). Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27438" + }, + { + "revision": 3, + "date": "2026-11-04", + "change": "steps[1] annotated from QA run #29893 (W5-STUDIO, at 589049b0): with the inspector open the tabs fold into 'Advanced' in a narrow container. Wording only. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -3141,7 +3243,7 @@ "title": "A Studio save never drops what another door wrote: keys a draft carries that Studio has no control for — written through the metadata API — are still in the draft, byte for byte, after an unrelated Studio edit autosaves the same item", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": [ @@ -3186,10 +3288,10 @@ "a key silently dropped or nulled by a Studio save is the FAIL: the author of the other door sees a 200 and loses the value" ], "variants": [ - "object — activityMilestones (one { field, value, summary } entry on a select field the object declares, with one of its option values), indexes, userActions, editMode, searchableFields, publicSharing, imageField; Studio edit: Settings › Basic info label (measured preserved, QA run #26715)", + "object — activityMilestones (one { field, value, summary } entry on a select field the object declares, with one of its option values), indexes (a unique index names its scope, unique: 'organization'; a bare unique: true is refused 422, ADR-0120 D1), userActions, editMode, searchableFields, publicSharing, imageField; Studio edit: Settings › Basic info label (measured preserved, QA run #26715)", "field — searchable, rows, step, currencyConfig, sortable, widget, dueLike, useGrouping, maskingRule; Studio edit: the field inspector's Help text (measured preserved)", "flow — description, successMessage, errorMessage, variables, errorHandling, requiredPermissions; Studio edit: a node label", - "app — description, branding, areas, contextSelectors; Studio edit: rename a navigation entry", + "app — description, branding, areas, contextSelectors (each { id, label, optionsSource: { endpoint, … } }, a closed shape, AppContextSelectorSchema in packages/spec/src/ui/app.zod.ts); Studio edit: rename a navigation entry", "dashboard — requiredPermissions; Studio edit: a widget title", "permission — description, isDefault, packageId; Studio edit: one object checkbox. Not allowExport: it is a per-object grant (objects.OBJ.allowExport, packages/spec/src/security/permission.zod.ts) that the matrix edits itself (its E column), not a set-level key, and a set-level allowExport is refused 422 unrecognized_keys" ], @@ -3220,7 +3322,8 @@ "change": "fixtures.knownGaps[0] is closed, variants[5] drops allowExport, and source[1] is re-pointed. Card #26837 is fixed by #27080, and QA run #27441 measured all six variants preserved through a Studio save and live after publish at c4c515ad. allowExport is not a permission-set key: it lives on each object grant (ObjectPermissionBaseSchema in packages/spec/src/security/permission.zod.ts), where the matrix's E column edits it, so the set-level key the variant asked the API to write is refused 422 unrecognized_keys by the strict set schema and was never a key Studio could drop. Accuracy finding of QA run #27441. The clauses are unchanged; acceptance[0]–[2] have no known failure. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27441" }, - { "revision": 4, "date": "2026-11-03", "change": "variants[0] replaces titleFormat with activityMilestones. Since 3557366c02 (#29499) the object save, publish and restore gate refuses a declared titleFormat (title-format-retired, validateTitleFormatRetired in packages/lint/src/validate-record-title.ts). QA run #29672 read publish-drafts answer success:false with INVALID_METADATA and BATCH_ABORTED for the other four items, and scored acceptance[2] FAIL as stale spec; with titleFormat removed from the draft, every other key of the six variants read back live as written. The replacement keeps the object variant at seven keys Studio has no control for, the item's subject. activityMilestones is graded live (packages/spec/liveness/object.json), no Studio surface writes it (no reader under ui/packages/app-shell/src/views/studio-design or metadata-admin/inspectors), and the run's no-entry matrix read its runtime row hold. titleFormat's prescription, nameField, is not used: Studio edits it (the Settings title-field picker, ObjectSettingsPanel.tsx), so it is not a key Studio has no control for. Read at base 339f63da30. Card #29679, from QA run #29672", "ref": "#29679" } + { "revision": 4, "date": "2026-11-03", "change": "variants[0] replaces titleFormat with activityMilestones. Since 3557366c02 (#29499) the object save, publish and restore gate refuses a declared titleFormat (title-format-retired, validateTitleFormatRetired in packages/lint/src/validate-record-title.ts). QA run #29672 read publish-drafts answer success:false with INVALID_METADATA and BATCH_ABORTED for the other four items, and scored acceptance[2] FAIL as stale spec; with titleFormat removed from the draft, every other key of the six variants read back live as written. The replacement keeps the object variant at seven keys Studio has no control for, the item's subject. activityMilestones is graded live (packages/spec/liveness/object.json), no Studio surface writes it (no reader under ui/packages/app-shell/src/views/studio-design or metadata-admin/inspectors), and the run's no-entry matrix read its runtime row hold. titleFormat's prescription, nameField, is not used: Studio edits it (the Settings title-field picker, ObjectSettingsPanel.tsx), so it is not a key Studio has no control for. Read at base 339f63da30. Card #29679, from QA run #29672", "ref": "#29679" }, + { "revision": 5, "date": "2026-11-04", "change": "variants[0] and [3] corrected from QA run #29893 (W5-STUDIO, at 589049b0): an index's unique needs its scope ('organization'; a bare unique: true is refused 422, ADR-0120 D1), and a contextSelector needs { id, label, optionsSource: { endpoint } }, so a valid value is spelled out. Read at base f68b47ca", "ref": "#29906" } ] }, { @@ -3228,7 +3331,7 @@ "title": "The Interfaces create dialogs work end to end: Create app derives its identifier and scaffolds one menu entry per package object (none when the box is cleared), New dashboard / report / page write spec-valid drafts linked from the menu, a report without a dataset or a measure and an identifier already taken are refused in the dialog, and everything opens in the published app", "since": "v18", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -3240,7 +3343,7 @@ "requires": [ "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", "two writable packages with NO app: P1 with two published objects, P2 with one", - "a dataset over a package object, with at least one dimension and one measure, written with PUT /api/v1/meta/dataset/NAME?mode=draft&package=PKG and published — Studio has no dataset editor (expected: the Interfaces card read it no-entry), so reports and widgets bind to an API-written dataset", + "a dataset over a package object, with at least one dimension and one measure, written with PUT /api/v1/meta/dataset/NAME?mode=draft&package=PKG and published — the fixture stays API-written for a known shape, though Studio can now create a dataset (the New report dialog carries a 'New dataset' button, ui/packages/app-shell/src/views/studio-design/datasetCreate.tsx, engine.studio.interfaces.newDataset.*), so reports and widgets bind to an API-written dataset", "a dashboard identifier and a page identifier already held elsewhere — a published one in another package and a draft — for the identifier-taken legs" ] }, @@ -3315,6 +3418,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[2]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 3, + "date": "2026-11-04", + "change": "fixtures.requires[2] corrected from QA run #29893 (W5-STUDIO, at 589049b0): 'Studio has no dataset editor' is stale; the New report dialog offers 'New dataset' (datasetCreate.tsx). The fixture stays API-written. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -3323,7 +3432,7 @@ "title": "A package dashboard renders every widget type the spec declares — those Studio offers and those written through the API — and every global-filter type renders its control and re-scopes the widgets bound to it; Studio's two widget-type entries offer the same set", "since": "v18", "status": "active", - "revision": 5, + "revision": 6, "priority": "P2", "surface": "mixed", "personas": [ @@ -3334,7 +3443,7 @@ "requires": [ "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", "a writable package with an app whose menu names one dashboard", - "a dataset over a package object, with at least one dimension and one measure, written with PUT /api/v1/meta/dataset/NAME?mode=draft&package=PKG and published — Studio has no dataset editor (expected: the Interfaces card read it no-entry), so reports and widgets bind to an API-written dataset. Give it several dimensions (a status-like category and a date) and a few measures over seeded multi-bucket rows — examples/app-showcase/src/ui/dashboards/chart-gallery.dashboard.ts is the model for each type's bindings. Sankey takes no source/target pair: it flows from one root node (the total) to each category of its first dimension, and a second dimension (two or more pivoted series) adds a second column of nodes (ui/packages/plugin-charts/src/AdvancedChartImpl.tsx, the sankey arm and buildSankeyCellFlows; the gallery's sankey_flow binds dimensions ['status'])" + "a dataset over a package object, with at least one dimension and one measure, written with PUT /api/v1/meta/dataset/NAME?mode=draft&package=PKG and published — Studio has no dataset editor (expected: the Interfaces card read it no-entry), so reports and widgets bind to an API-written dataset. Give it several dimensions (a status-like category and a date) and a few measures over seeded multi-bucket rows — examples/app-showcase/src/ui/dashboards/chart-gallery.dashboard.ts is the model for each type's bindings. Sankey takes no source/target pair: it flows from one root node (the total) to each category of its first dimension; with a second dimension (two or more pivoted series) there is no root: each flow runs from a first-dimension category to a second-dimension value, two columns of nodes (ui/packages/plugin-charts/src/AdvancedChartImpl.tsx, the sankey arm and buildSankeyCellFlows, #27460; the gallery's sankey_flow binds dimensions ['status'])" ] }, "steps": [ @@ -3343,7 +3452,7 @@ "global filters: add one filter of each type — text, select (with options or optionsFrom), date, number, lookup (with optionsFrom {object, valueField, labelField}: a lookup filter draws the same select control and, with no options or optionsFrom, offers no choices — ui/packages/plugin-dashboard/src/DashboardFilterBar.tsx) — and bind each to at least one widget through that widget's filter mapping; leave one widget unbound, which means filterBindings { NAME: false } for EVERY filter on it. A widget with no filterBindings entry is not unbound: it inherits each filter on that filter's own field (ui/packages/core/src/utils/dashboard-filters.ts resolveBoundField: an explicit binding, then targetWidgets, then the filter's field; read from source, not driven live)", "publish; open the board in the app; screenshot it settled", "change each filter on the rendered board in turn (a text or number filter commits only on Enter or when the input loses focus, ui/packages/plugin-dashboard/src/DashboardFilterBar.tsx TextFilter, so press Enter before reading the network); record which widgets re-query (network) and how their values change; reset", - "in Studio add a global filter with Add item and leave it incomplete; watch the autosave and reload" + "in Studio add a global filter with Add item and leave it incomplete: once with its Field empty, and once with the Field filled and Options From begun (an object chosen, its value and label fields not yet; the wave-4 shape, #29006, held since #29300); watch the autosave and reload" ], "acceptance": [ { @@ -3373,7 +3482,7 @@ { "clause": "Studio's global-filter editor never autosaves an incomplete filter: the item is held until complete, and no other edit is lost", "oracle": "network", - "verify": "no dashboard PUT carries the incomplete filter. Three cases, each judged: (a) the edits made BEFORE Add item survive a reload; (b) an edit made while the item is held is carried by the PUT that completes the item, answered 200 (ui/packages/app-shell/src/views/studio-design/InterfacesPillar.heldListItem-27459.test.tsx); (c) a reload while the item is held raises the browser's leave-site prompt, and Leave then discards the held item and the edits kept with it, by design (studio-authoring.held-edit-leave-guard) — a reload with no prompt that loses an edit is a FAIL", + "verify": "no dashboard PUT carries the incomplete filter. Four cases, each judged: (a) the edits made BEFORE Add item survive a reload; (b) an edit made while the item is held is carried by the PUT that completes the item, answered 200 (ui/packages/app-shell/src/views/studio-design/InterfacesPillar.heldListItem-27459.test.tsx); (c) a reload while the item is held raises the browser's leave-site prompt, and Leave then discards the held item and the edits kept with it, by design (studio-authoring.held-edit-leave-guard) — a reload with no prompt that loses an edit is a FAIL; (d) the wave-4 shape, a Field filled with Options From begun, is held like any incomplete item: no PUT carries it and a later edit still saves (#29006, fixed by #29300)", "evidence": "the trace + the post-reload read" } ], @@ -3431,6 +3540,12 @@ "date": "2026-11-01", "change": "Three QA run #28656 (W3-RT) findings. fixtures.knownGaps (its one entry, 'acceptance[4] FAILS TODAY') is dropped. #27459 is fixed by PR #27784 (InterfacesPillar.heldListItem-27459.test.tsx: Add item then a Label edit sends nothing, and filling the Field in sends one save carrying the item and the Label edit), and #28656 read acceptance[4] pass live at 6e11a929. The report-panel half is #27785. acceptance[4].verify said only 'after a reload, the other edits are present'. It now judges three cases: edits before Add item survive a reload; an edit made while held lands in the PUT that completes the item; a reload while held must raise the leave-site prompt, and Leave then discards by design. steps[4] says a text or number global filter commits only on Enter or blur (DashboardFilterBar.tsx TextFilter). steps[2] says 'unbound' means filterBindings false for every filter, since a widget with no filterBindings inherits each filter on the filter's own field (dashboard-filters.ts resolveBoundField; read from source, not driven live). No clause is weakened. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); findings parked in anchor comment 6557904764", "ref": "#28656" + }, + { + "revision": 6, + "date": "2026-11-04", + "change": "steps[5], acceptance[4].verify and fixtures.requires[2] corrected from QA run #29890 (W5-FAIL, at 589049b0): the wave-4 shape (Field filled, Options From begun; #29006, fixed by #29300) is its own case (d), and a two-dimension sankey draws no root node: its flows run from the first dimension's categories to the second's values (buildSankeyCellFlows). Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -3439,7 +3554,7 @@ "title": "Reports authored in Studio render every report type from the same dataset — tabular, summary, matrix and joined — and their runtimeFilter, order and chart take effect in the published app, while a joined report refuses a chart and a block with no dataset", "since": "v18", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -3450,13 +3565,13 @@ "requires": [ "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", "a writable package with an app", - "a dataset over a package object, with at least one dimension and one measure, written with PUT /api/v1/meta/dataset/NAME?mode=draft&package=PKG and published — Studio has no dataset editor (expected: the Interfaces card read it no-entry), so reports and widgets bind to an API-written dataset — two dimensions (for matrix rows and columns) and two measures" + "a dataset over a package object, with at least one dimension and one measure, written with PUT /api/v1/meta/dataset/NAME?mode=draft&package=PKG and published — the fixture stays API-written for a known shape, though Studio can now create a dataset (the New report dialog carries a 'New dataset' button, ui/packages/app-shell/src/views/studio-design/datasetCreate.tsx, engine.studio.interfaces.newDataset.*), so reports and widgets bind to an API-written dataset — two dimensions (for matrix rows and columns) and two measures" ] }, "steps": [ "in /_console/studio/PKG/interfaces create a report (New report: the dataset and a measure); on its canvas set the type to tabular, then summary with a row dimension, then matrix with a row and a column dimension; keep a copy of each as its own report", "on the summary report set runtimeFilter to narrow the rows (e.g. one category value), order by the measure descending, and an embedded chart", - "author a joined report with two blocks, each bound to the dataset with its own rows and values", + "author a joined report with two blocks, each bound to the dataset with its own rows and values (switching the Report type to Joined is held, and nothing saves, until the first block is added, #29022: a runner who switches the type and reloads before adding a block sees the previous type again)", "refusals: PUT a joined report carrying a chart, and one whose block has no dataset, with ?mode=draft&package=PKG; capture both", "publish; open each report in the app; compare the summary's rows against the dataset aggregate with the same filter (POST /api/v1/analytics/dataset/query with {datasetName: DATASET, selection: {dimensions: [the summary's row dimension], measures: [MEASURE], runtimeFilter: the summary's filter, order: {MEASURE: 'desc'}}}; packages/spec/src/api/analytics.zod.ts, DatasetSelectionSchema)" ], @@ -3522,6 +3637,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[2]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 4, + "date": "2026-11-04", + "change": "fixtures.requires[2] and steps[2] corrected from QA run #29893 (W5-STUDIO, at 589049b0): the 'no dataset editor' parenthetical is stale (New dataset, datasetCreate.tsx), and a switch to Joined is held until a block is added (#29022). steps[0] names no Joined switch and is unchanged. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -3530,7 +3651,7 @@ "title": "App and menu-entry keys reach the running app: branding, areas and context selectors render; an entry's viewName, recordId / recordMode, filters, badge, requiresObject and visible each change what the entry does or who sees it; hidden takes an app off the switcher and nothing else — and Studio's menu-entry inspector edits them", "since": "v18", "status": "active", - "revision": 6, + "revision": 7, "priority": "P2", "surface": "mixed", "personas": [ @@ -3550,7 +3671,7 @@ }, "steps": [ "write the app through PUT /api/v1/meta/app/APP?mode=draft&package=PKG: branding { primaryColor, accentColor, logo } (a runner-environment note: give logo a same-origin URL such as /_console/vite.svg, which the console build ships from ui/apps/console/public; an external logo URL is blocked by the runner container's egress proxy and never renders), two areas each holding some entries, a contextSelector, and menu entries: an object entry with viewName (the second list view), one with recordId + recordMode 'edit', one with filters, one with badge + badgeVariant, one with requiresObject naming an object that does not exist, and one with a visible predicate false for the member (e.g. 'org_owner' in current_user.positions: the seeded admin holds org_owner, not org_admin, because an organization's owner membership maps to org_owner alone, packages/spec/src/identity/eval-user.zod.ts, so an 'org_admin' predicate is false for both personas and proves nothing). Make this app the package's FIRST app (steps[4]); publish", - "as admin and as the member (each in its own context, identity proved with GET /api/v1/auth/get-session), open the app: read the chrome colours and logo, the areas switcher, the context selector, and click each entry", + "as admin and as the member (each in its own context, identity proved with GET /api/v1/auth/get-session), open the app (a fresh organization first shows the 'Set the default currency' prompt over the page; dismiss it with 'Not now', RUNNER environment facts) and read the chrome colours and logo, the areas switcher, the context selector, and click each entry", "GET /api/v1/meta/app?id=APP as each persona and list the entries each receives", "set hidden: true on a second app of the package (through the API) and publish; as the member read the App Switcher, the Home launcher, the avatar menu and GET /api/v1/meta/app?id=SECOND; open its URL directly", "open the app in /_console/studio/PKG/interfaces, select each entry and read its inspector. Studio's Interfaces pillar opens only the package's FIRST app (the first app the package lists, else its first app draft) and draws only that app's top-level `navigation`: an entry that lives only inside `areas` has no rail row (ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx, the Interfaces pillar's app load). So the app steps[0] writes must be the package's first, and its top-level navigation must hold at least one entry carrying each key acceptance[5] names; the area entries' runtime effect is read in acceptance[0]–[3], not here" @@ -3589,7 +3710,7 @@ { "clause": "Studio's menu-entry inspector edits these keys — visible, viewName, recordId / recordMode, filters, badge / badgeVariant, requiresObject, order — as the metadata admin's app editor does", "oracle": "dom", - "verify": "each key has an editable control in the Studio inspector, and an edit there autosaves it into the app draft", + "verify": "each key has an editable control in the Studio inspector, and an edit there autosaves it into the app draft. The View picker offers the package's view items of the bound object and writes the qualified 'OBJECT.KEY' name (StudioNavItemInspector.tsx, e.g. gsixn_project.closed_projects), while steps[0]'s API write may name the bare list-view key the runtime also accepts: record which spelling each entry carries, and whether an entry carrying the qualified name opens that view (QA run #29893 did not drive it)", "evidence": "the inspector DOM per key + one autosave PUT" } ], @@ -3644,6 +3765,12 @@ "date": "2026-11-04", "change": "source[2] re-pointed: the Studio menu-entry inspector moved unchanged from ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx to StudioNavItemInspector.tsx beside it (#29988 slice 1, PR #30002, a move-only split). Re-read at base f7aab8958. No clause changed", "ref": "#29988" + }, + { + "revision": 7, + "date": "2026-11-04", + "change": "steps[1] and acceptance[5].verify corrected from QA run #29893 (W5-STUDIO, at 589049b0): a fresh organization's first visit shows the default-currency prompt, and the Studio View picker writes the qualified OBJECT.KEY view name, not the bare key steps[0] writes. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -3738,7 +3865,7 @@ "title": "Permission sets are created, found and locked honestly in Studio's Access pillar: the New dialog derives the identifier and refuses one already taken — published or draft — the rail searches and shows its empty states, and on a read-only package New lands in its .custom pair while the packaged set offers no edit and the server refuses the forged write, and New similar on a packaged set copies it under a new name into that pair", "since": "v18", "status": "active", - "revision": 8, + "revision": 9, "priority": "P2", "surface": "mixed", "personas": [ @@ -3800,7 +3927,8 @@ } ], "negative": [ - "a create that silently overwrites a published set with an empty one is the FAIL acceptance[1] exists for — every member of that set would lose its grants on the next publish" + "a create that silently overwrites a published set with an empty one is the FAIL acceptance[1] exists for — every member of that set would lose its grants on the next publish", + "a caller without manage_metadata is refused 403 at both doors this item drives, in the same sentence (metaWriteCapabilityVerdict, '… requires the `manage_metadata` capability.') but under two codes: the clone door (POST /api/v1/security/permission-sets/:name/clone, packages/runtime/src/domains/permission-set-clone.ts) answers PERMISSION_DENIED and the metadata PUT answers FORBIDDEN (packages/rest/src/meta-item-write-routes.ts). Score the 403 and the sentence, and record both codes; their difference is not this item's FAIL" ], "traps": [ "automation-input", @@ -3865,6 +3993,12 @@ "date": "2026-11-04", "change": "source[0] re-pointed: findTakenIdentifier and CreateTakenNotice moved unchanged from ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx to StudioPillarKit.tsx beside it (#29988 slice 4, a move-only split); the Access pillar's use of them stays. Re-read at base 9d1a465f0. No clause changed", "ref": "#29988" + }, + { + "revision": 9, + "date": "2026-11-04", + "change": "negative[1] added from QA run #29890 (W5-FAIL, at 589049b0): the same manage_metadata refusal answers PERMISSION_DENIED on the clone door and FORBIDDEN on the metadata PUT, so a runner records both codes instead of reading one as a defect. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -3873,7 +4007,7 @@ "title": "The Access pillar's facet tabs author what the runtime enforces: System capabilities chips grant a capability a gated action checks, a Row-level security policy (with its Test dry-run) scopes a member's reads and writes, and Tab visibility hides an app — from the member's app list and from the launcher alike", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": [ @@ -3895,7 +4029,7 @@ "steps": [ "baseline as M: open a record of OBJ — the CAP-gated action is absent, and POST /api/v1/actions/OBJ/ACTION answers 403; GET /api/v1/data/OBJ returns every row M may read", "as admin: /_console/studio/PKG/access → S → System capabilities: add the chip CAP; let the autosave land and read the draft (systemPermissions)", - "Row-level security tab: add a policy on OBJ — name, operation read, USING a plain field comparison that keeps some rows (e.g. record.status == 'open'), enabled; open 'Test this policy against a sample record' and run it on a matching and a non-matching sample; add a second policy for update with a CHECK that refuses some value", + "Row-level security tab: add a policy on OBJ — name, operation select (the picker's operations are all / select / insert / update / delete, RLSOperation in packages/spec/src/security/rls.zod.ts; a read is 'select'), USING a plain field comparison that keeps some rows (e.g. record.status == 'open'), enabled; open 'Test this policy against a sample record' and run it on a matching and a non-matching sample; add a second policy for update with a CHECK that refuses some value", "Tab visibility tab: Add tab, TYPE the full app name and set Hidden; read the draft (tabPermissions)", "publish; as M: re-read the record (the CAP-gated action is shown) and run it; GET /api/v1/data/OBJ (only rows passing USING); PATCH a row to a value the CHECK refuses and to one it allows; GET /api/v1/me/apps; open the Home launcher", "switch the RLS read policy off (enabled false), publish, re-read as M; then remove CAP, publish, and re-run the action as M" @@ -3971,6 +4105,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 5, + "date": "2026-11-04", + "change": "steps corrected from QA run #29893 (W5-STUDIO, at 589049b0): the policy picker has no 'read' operation; a read is 'select'. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -4086,7 +4226,7 @@ "title": "Authorable keys Studio has no control for still work when another door writes them: per metadata type, each key written into a package draft through the metadata API is stored, served after publish and takes its runtime effect", "since": "v18", "status": "active", - "revision": 5, + "revision": 6, "priority": "P2", "surface": "mixed", "personas": [ @@ -4102,7 +4242,7 @@ "the showcase's external datasource showcase_external (examples/app-showcase/src/system/datasources/showcase-external.datasource.ts) for the datasource / external rows" ], "knownGaps": [ - "ROWS THAT FAIL TODAY (QA run #26715, at 3fcafcd3) — score each fail citing its record: the form-view key defaults applies only on some form surfaces (gap-view card); a list view's sharing.lockedBy is never shown (gap-view card). Out of the matrix since QA run #29672 (at 3482f3d8d2, re-read at 339f63da30): drawerSide and drawerWidth are refused at parse, so they are no longer a stored-and-inert row (their retiredKey tombstones in packages/spec/src/ui/view.zod.ts, and form-view-drawer-keys-removed in packages/spec/src/conversions/registry.ts); layout, defaultTab and tabPosition have a Studio control, since the form-view inspector renders the form variant's spec schema through SchemaForm (ui/packages/app-shell/src/views/metadata-admin/inspectors/ViewVariantInspector.tsx), so step 4 moves them out. layout:'horizontal' not drawn on the Studio canvas was #29676, fixed by #29759 (7ba8b11f52). Fixed since the run, so no longer fails: field deleteBehavior 'restrict' under a master_detail parent (#26718), an object bound to an unknown datasource (#26912), a chart list view on another object's dataset (#26719). Measured rendering by QA run #27441 at c4c515ad, so no longer a failing row: the form-view `buttons` key (the authored submit label shown, cancel hidden; ObjectForm folds buttons.submit and buttons.cancel onto its flat renderer props, ui/packages/plugin-form/src/ObjectForm.tsx)", + "ROWS THAT FAIL TODAY (QA run #26715, at 3fcafcd3) — score each fail citing its record: the form-view key defaults applies only on some form surfaces (gap-view card). Out of the matrix since QA run #29672 (at 3482f3d8d2, re-read at 339f63da30): drawerSide and drawerWidth are refused at parse, so they are no longer a stored-and-inert row (their retiredKey tombstones in packages/spec/src/ui/view.zod.ts, and form-view-drawer-keys-removed in packages/spec/src/conversions/registry.ts); layout, defaultTab and tabPosition have a Studio control, since the form-view inspector renders the form variant's spec schema through SchemaForm (ui/packages/app-shell/src/views/metadata-admin/inspectors/ViewVariantInspector.tsx), so step 4 moves them out. layout:'horizontal' not drawn on the Studio canvas was #29676, fixed by #29759 (7ba8b11f52). Fixed since the run, so no longer fails: a list view locked by sharing.lockedBy alone, which now shows its lock indicator (#29312, merged 2026-11-02); field deleteBehavior 'restrict' under a master_detail parent (#26718), an object bound to an unknown datasource (#26912), a chart list view on another object's dataset (#26719). Measured rendering by QA run #27441 at c4c515ad, so no longer a failing row: the form-view `buttons` key (the authored submit label shown, cancel hidden; ObjectForm folds buttons.submit and buttons.cancel onto its flat renderer props, ui/packages/plugin-form/src/ObjectForm.tsx)", "NOT JUDGEABLE HERE: mailAssociation (needs a mailbox with inbound ingest), ackPlaintextMasking (its only reader is a build-time warning), lifecycle.ttl (the sweep is hourly — drive it only with time to wait for the next sweep), dimensions (inconclusive at 3fcafcd3). Record them not-run with this line cited", "app / menu-entry keys are studio-authoring.app-and-nav-properties, page slots and variables studio-authoring.authored-page-slots-and-variables, the widget types studio-authoring.dashboard-widget-and-filter-types — not repeated here" ] @@ -4117,7 +4257,7 @@ { "clause": "every key is stored as written in the draft and served after publish", "oracle": "api", - "verify": "per row: the draft read and the live read carry the key with the written value", + "verify": "per row: the draft read and the live read carry the key with the written value. A conditionalFormatting condition written as a CEL string is stored as the expression envelope {dialect:'cel', source} (its condition is EvaluatedExpressionInputSchema, packages/spec/src/shared/conditional-formatting.zod.ts): compare its source, which counts as written", "evidence": "one line per row: draft value, live value" }, { @@ -4162,15 +4302,15 @@ "field.internal → the key is omitted from reads, even when selected explicitly", "field.requiredPermissions → a member without the capability gets the value omitted on read and a write refused 403; a holder reads and writes it", "field.maskingRule 'phone' → the value is masked in the API and UI, and saving the form does not write the mask back", - "field.searchable / rows / step / currencyConfig / sortable / relatedList / relatedListTitle / lookupColumns / language → each changes its control or list as its key describes (QA run #26715 Data card)", + "field.searchable / rows / step / currencyConfig / sortable / relatedList / relatedListTitle / lookupColumns / language → each changes its control or list as its key describes (QA run #26715 Data card). sortable: false is observed on the list: the column header offers no sort, the toolbar's Sort picker does not list the field, and the server refuses ?sort= on it 400 INVALID_SORT (assertSortFieldsAreOffered, packages/metadata-protocol/src/protocol-query-validation.ts; #29896 and #30024, both fixed)", "field.deleteBehavior 'restrict' → deleting a referenced parent refused 409 DELETE_RESTRICTED, also when the parent is the same child's master_detail target (#26718, fixed by #26864)", "flow.description / successMessage / errorMessage → served; a manual run answers with successMessage; a failing run carries errorMessage", "flow.variables (defaultValue) → a step reading the variable writes its default", "flow.requiredPermissions → a manual start without the capability refused 403", "flow.errorHandling { strategy, maxRetries } → maxRetries 2 yields three failed attempts", "flow.nodes[].inputSchema → stored and served", - "view.list: searchableFields, userFilters, resizable, compactToolbar, selection, pagination, rowHeight, grouping, rowColor, hiddenFields, fieldOrder, rowActions, bulkActions, conditionalFormatting, inlineEdit, exportOptions (needs an export grant), appearance, addRecord, showRecordCount, allowPrinting, emptyState → each renders as its key describes (gap-view card, 3fcafcd3)", - "view.form: groups, subforms, submitBehavior, modalSize, allowSkip, showStepIndicator, buttons → render as described (buttons measured rendering, knownGaps[0]); splitDirection / splitSize / splitResizable → refused at parse (retired in @objectstack/spec 18.3.0 with the split form, ADR-0050); drawerSide / drawerWidth → refused at parse (retired in @objectstack/spec 18.3.0, ADR-0049: no form view opens in a drawer); defaults: partial (knownGaps[0]). layout, defaultTab and tabPosition are not rows: Studio offers a control for each (step 4, knownGaps[0])", + "view.list: searchableFields, userFilters, resizable, compactToolbar, selection, pagination, rowHeight, grouping, rowColor, hiddenFields, fieldOrder, rowActions, bulkActions, conditionalFormatting, inlineEdit, exportOptions (needs an export grant), appearance, addRecord, showRecordCount, allowPrinting, emptyState → each renders as its key describes; probe compactToolbar, showRecordCount and resizable at the value opposite to what the list does with the key unset, since at that default a stored key renders exactly like no key and proves nothing (gap-view card, 3fcafcd3)", + "view.form: groups, subforms, submitBehavior, modalSize, allowSkip, showStepIndicator, buttons → render as described (buttons measured rendering, knownGaps[0]); splitDirection / splitSize / splitResizable → refused at parse (retired in @objectstack/spec 18.3.0 with the split form, ADR-0050); drawerSide / drawerWidth → refused at parse (retired in @objectstack/spec 18.3.0, ADR-0049: no form view opens in a drawer); defaults: partial (knownGaps[0]). layout, defaultTab and tabPosition are not rows: Studio offers a control for each (step 4, knownGaps[0]). modalSize acts only on the form view the record modal selects, the object's default form view, or for a create its `create` form view when one is declared (selectRecordFormView, ui/packages/app-shell/src/utils/recordFormNavigation.ts), so author it there", "dashboard.requiredPermissions → a viewer without the capability is refused the dashboard", "dataset (Studio has no dataset editor): include, dimensions with nullLabel, measures count / sum / avg / filtered / ratio, currency format → a report bound to it shows the null bucket labelled and each measure's value", "permission.description / isDefault / allowExport / rowLevelSecurity[].positions → the Setup list shows the description; isDefault raises a suggestion; allowExport → export 200 with it, 403 EXPORT_NOT_PERMITTED without; positions limit the policy to holders of the position" @@ -4213,7 +4353,8 @@ "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" }, - { "revision": 5, "date": "2026-11-03", "change": "the view.form row drops drawerSide, drawerWidth, layout, defaultTab and tabPosition as step 4 says, fixtures.knownGaps[0] records why, and the object titleFormat cell moves to a refusal. QA run #29672 read drawerSide and drawerWidth answer 422 (retired in spec 18.3.0, ADR-0049; their retiredKey tombstones in packages/spec/src/ui/view.zod.ts), so they are refused, not stored and inert; and Studio's form-view inspector now offers Layout, Columns, Default Tab and Tab Position (ViewVariantInspector.tsx hands the form variant's spec schema to SchemaForm), which step 4 moves out of the matrix. The run's layout:'horizontal' fail on the Studio canvas was #29676, fixed by #29759 (7ba8b11f52). The buttons cell follows revision 3's knownGaps[0] reading. titleFormat has been refused since 3557366c02 (title-format-retired); the run read the publish refuse it on studio-authoring.api-written-keys-survive-studio-save. The other view.form keys' Studio census was not re-read; the next run's step 4 decides it. The clauses are unchanged. Read at base 339f63da30. Card #29679, from QA run #29672", "ref": "#29679" } + { "revision": 5, "date": "2026-11-03", "change": "the view.form row drops drawerSide, drawerWidth, layout, defaultTab and tabPosition as step 4 says, fixtures.knownGaps[0] records why, and the object titleFormat cell moves to a refusal. QA run #29672 read drawerSide and drawerWidth answer 422 (retired in spec 18.3.0, ADR-0049; their retiredKey tombstones in packages/spec/src/ui/view.zod.ts), so they are refused, not stored and inert; and Studio's form-view inspector now offers Layout, Columns, Default Tab and Tab Position (ViewVariantInspector.tsx hands the form variant's spec schema to SchemaForm), which step 4 moves out of the matrix. The run's layout:'horizontal' fail on the Studio canvas was #29676, fixed by #29759 (7ba8b11f52). The buttons cell follows revision 3's knownGaps[0] reading. titleFormat has been refused since 3557366c02 (title-format-retired); the run read the publish refuse it on studio-authoring.api-written-keys-survive-studio-save. The other view.form keys' Studio census was not re-read; the next run's step 4 decides it. The clauses are unchanged. Read at base 339f63da30. Card #29679, from QA run #29672", "ref": "#29679" }, + { "revision": 6, "date": "2026-11-04", "change": "fixtures.knownGaps[0], variants[25], [32] and [33] and acceptance[0].verify corrected from QA run #29890 (W5-FAIL, at 589049b0): a lockedBy-only list view shows its lock indicator since #29312, so it leaves the failing rows; sortable names its observables (header, Sort picker, 400 INVALID_SORT; #29896, #30024); compactToolbar, showRecordCount and resizable are probed at a non-default value; modalSize acts on the form view the record modal selects; a conditionalFormatting condition is stored as a CEL envelope. Read at base f68b47ca", "ref": "#29906" } ] }, { @@ -4431,7 +4572,7 @@ "title": "Customize a read-only base's view, dashboard or report in Studio: the edit lands in .custom as a declared override, every read serves it, the pair's export carries the declaration, and 'Restore package original' brings the base back", "since": "v18", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -4464,7 +4605,7 @@ { "clause": "Customize unlocks the item and writes nothing; the first saved edit asks the ensure door once and writes the item's draft into com.example.showcase.custom, declaring overrides: { package: 'com.example.showcase', basedOn: '0.1.0' }, while the base's own item is untouched", "oracle": "network", - "verify": "step 2 sends no request; step 3 sends exactly one ensure POST (201 on the run's first customization, 200 after) and then the draft PUT naming package=com.example.showcase.custom whose body carries the overrides block and none of the read's `_` keys (overrideDraftBody, StudioCustomizeControls.tsx). No write names com.example.showcase. A draft written into com.example.showcase, or a package-less write, is the FAIL", + "verify": "step 2 sends no request; step 3 sends exactly one ensure POST (201 on the run's first customization, 200 after), once per editing session: the surface keeps the door's answer only while it is mounted (ensurePairPackage, StudioDesignSurface.tsx), so a fresh page load of the same item asks the ensure door again (200) on its first edit and then the draft PUT naming package=com.example.showcase.custom whose body carries the overrides block and none of the read's `_` keys (overrideDraftBody, StudioCustomizeControls.tsx). No write names com.example.showcase. A draft written into com.example.showcase, or a package-less write, is the FAIL", "evidence": "the network trace of steps 2–3 + the pair's draft read" }, { @@ -4544,6 +4685,12 @@ "date": "2026-11-02", "change": "automated now names the live spec ui/e2e/live/studio-customize-journey.spec.ts (#28579 slice 3) and says which clauses of the view variant it drives at which layer; kind e2e. The sentence that the e2e was still owed by slice 3 is gone. No clause, step or fixture changed", "ref": "#28579" + }, + { + "revision": 3, + "date": "2026-11-04", + "change": "acceptance[0].verify annotated from QA run #29893 (W5-STUDIO, at 589049b0): 'one ensure POST' is per editing session; a fresh page load asks again (200). Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -4653,7 +4800,7 @@ "title": "The Studio landing says 'n customized, m switched off' for each read-only base, and the All metadata lists mark the pair's items 'Customized' or 'Added' with an 'only customizations' filter — every number read from GET /packages/:id/customizations", "since": "v18", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": [ @@ -4664,7 +4811,7 @@ "requires": [ "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin (admin@objectos.ai / admin123), and a console built from this tree with `pnpm console:build`", "the stock read-only base com.example.showcase (examples/app-showcase/objectstack.config.ts: id com.example.showcase, namespace showcase, version 0.1.0 — a booted code package, so read-only to isWritablePackage) and its customization pair com.example.showcase.custom (namespace showcase_custom, dependency ^0 on the base), created by the ensure door on the first customization", - "at least one published override (studio-authoring.package-customize-restore-display's dashboard leg, before its restore), one published extension (studio-authoring.package-extension-field-c), one item the pair adds (Studio's New on the read-only base, e.g. a permission set from studio-authoring.access-permission-set-lifecycle step 6) and one packaged flow switched off (Studio's auto-ledger-switch), so every number is non-zero" + "at least one published override (studio-authoring.package-customize-restore-display's dashboard leg, before its restore), one published extension (studio-authoring.package-extension-field-c), one item the pair adds (Studio's New on the read-only base, e.g. a permission set from studio-authoring.access-permission-set-lifecycle step 6, or the same item written by API into the pair: the 'Added' mark lands on it either way) and one packaged flow switched off (Studio's auto-ledger-switch), so every number is non-zero" ], "knownGaps": [ "AUTOMATION LIVES UNDER ui/ — `cd ui && pnpm exec vitest run --maxWorkers=1 packages/app-shell/src/views/studio-design/FILE`; the jsdom pins mock the customizations read" @@ -4681,7 +4828,7 @@ { "clause": "the landing line reads 'n customized, m switched off' where n = counts.overrides + counts.extensions and m = counts.switchedOff of the served read — no number of the console's own", "oracle": "dom", - "verify": "after a screenshot confirms the card rendered, the line's numbers equal step 1's served counts (fetchPackageCustomizations computes none); the pair link opens com.example.showcase.custom and the switched-off link the Automations pillar", + "verify": "after a screenshot confirms the card rendered, the line's numbers equal step 1's served counts (fetchPackageCustomizations computes none); the pair link's href names com.example.showcase.custom, and following it lands on the base's Studio (/_console/studio/com.example.showcase/data: Studio redirects a pair URL to its base, customizationPairBaseOf, ADR-0146 D3), whatever its title 'Open the customization package … in Studio' says; the switched-off link opens the Automations pillar", "evidence": "the served read + the landing DOM read" }, { @@ -4727,6 +4874,12 @@ "date": "2026-11-02", "change": "acceptance[2] and steps[4] corrected, an assertion defect measured in the slice 3 run (base 128681f4): the clause said a writable package is never asked, but the All metadata area asks every package except a pair (usePackageCustomizations; metadataAreaHost.customizations-28568's 'a base without a pair is unchanged' pins the read), and only the landing limits itself to read-only bases. The clause now scores what the console promises: the landing asks no writable card, a package with zero counts draws nothing, a pair id is refused. automated says the slice 3 live spec does not cover this item instead of saying the e2e is owed. The run scored acceptance[2] FAIL against revision 1", "ref": "#28579" + }, + { + "revision": 3, + "date": "2026-11-04", + "change": "acceptance[0].verify and fixtures.requires[2] corrected from QA run #29893 (W5-STUDIO, at 589049b0): the pair link's href names the pair, but Studio redirects a pair URL to its base (customizationPairBaseOf), and the pair-added fixture item may be written by API. Read at base f68b47ca", + "ref": "#29906" } ] } diff --git a/docs/qa/platform-checklist/areas/ux-conventions.json b/docs/qa/platform-checklist/areas/ux-conventions.json index 22d07ab3219..80e8c740105 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": 10, + "revision": 11, "priority": "P1", "surface": "browser", "personas": [ @@ -24,7 +24,7 @@ "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": [ - "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#11817): Setup → Users → Invite User toasts the copy of the delivery outcome the server answers (#27843): on a deployment that does not deliver mail the declared outcomeMessages.link_only, 'Invitation created for EMAIL. Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it.' (QA run #29890 read it verbatim), and 'Invitation sent to EMAIL' (outcomeMessages.sent) only where mail is delivered (packages/platform-objects/src/identity/invite-outcome-messages.ts#INVITE_USER_OUTCOME_MESSAGES on packages/platform-objects/src/identity/sys-user.object.ts#invite_user, whose successMessage `Invitation sent to {{ result.email }}` is the fallback for an answer without an outcome), 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 rows, expected PASS (QA run #23953 at objectui daeb77fc84dd): Studio Publish gives a success confirmation (objectui#11807); the task grid's 'Save All' toasts '1 row saved' (objectui#12303 fixed for this row); the approval decision's Approve toasted the framework-declared successMessage, read in the approvals drawer that route no longer has (variants[6]) `Approved \"{{ result.request.record_title }}\"` (packages/plugins/plugin-approvals/src/sys-approval-request.object.ts), e.g. Approved \"EXP-2001\" (#23157, closed). Studio's 'Discard draft' toast and its confirm now name the item by its LABEL — QA run #24570 at objectui 4016370a read 'Draft discarded: QA Widget' — a regression row, expected PASS (objectstack-ai/objectui#12690, closed by objectui PR #12830, 0f42aaf8, in the console pin 4016370a and on objectui main; ResourceEditPage.tsx falls back to type/name only for an item with no label)", "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 ONE surface does: the import. An import run as a job (the wizard's 'runs as an undoable job' option) is listed in the import history with 'Undo import', which deletes the records it created and restores the records it updated to their previous values; QA run #25817 at 6679644a read it working in both create and update modes. Elsewhere no stock surface offers Undo: 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] is scored on the import variant. Undo's 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" @@ -36,8 +36,8 @@ "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 move the transition rule blocks never leaves the client — its lane is dimmed, data-kanban-drop-blocked, and the drop sends nothing, #25933)", "declared record action — Mark Done on a task record (declared successMessage `{{ record.title | default:'Task' }} marked done.`, which names the task, examples/app-showcase/src/ui/actions/index.ts#MarkDoneAction)", - "param action dialog — Setup → Users → Invite User → Confirm (declared successMessage `Invitation sent to {{ result.email }}`)", - "approval decision — open the request from the approvals page, /_console/apps/com.example.showcase/system/approvals (the 'Approvals inbox' link opens the review-queue page, which is not where the decision is taken). The route has no drawer: opening a request navigates to its record page (sys_approval_request_detail), where the decision is taken → Approve with a comment", + "param action dialog — Setup → Users → Invite User → Confirm (declared outcomeMessages: link_only on a deployment that sends no mail, sent where it does; successMessage `Invitation sent to {{ result.email }}` is the fallback, knownGaps[0])", + "approval decision — open the request from the approvals page, /_console/apps/com.example.showcase/system/approvals (the 'Approvals inbox' link opens the review-queue page, which is not where the decision is taken). The route has no drawer: opening a request navigates to its record page (sys_approval_request_detail), where the decision is taken → Approve with a comment. On a per_group request (e.g. showcase_expense_signoff's 会签: a manager AND a finance approver must each approve, examples/app-showcase/src/automation/flows/index.ts#ExpenseSignoffFlow) one approval leaves the request pending, so read the decision from sys_approval_action, not from the request's status", "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", @@ -66,9 +66,9 @@ "evidence": "the verbatim feedback strings" }, { - "clause": "a declared successMessage is the text the user sees", + "clause": "a declared successMessage, or the declared outcomeMessages entry the server's answer names, is the text the user sees", "oracle": "dom", - "verify": "for Mark Done and Invite User compare the toast text with the action's declared successMessage (examples/app-showcase/src/ui/actions/index.ts#MarkDoneAction, packages/platform-objects/src/identity/sys-user.object.ts#invite_user)", + "verify": "for Mark Done compare the toast text with the action's declared successMessage (examples/app-showcase/src/ui/actions/index.ts#MarkDoneAction); for Invite User compare it with the declared outcomeMessages entry the server's answer names, link_only on a deployment that sends no mail and sent where mail is delivered, and with successMessage only for an answer without an outcome (packages/platform-objects/src/identity/sys-user.object.ts#invite_user, invite-outcome-messages.ts)", "evidence": "declared string vs shown string, per action" }, { @@ -165,6 +165,12 @@ "date": "2026-11-02", "change": "variants[10] re-pointed to the Studio door: the Setup designer URL it named, /_console/apps/setup/metadata/object/OBJ, now redirects to /_console/studio/~all/metadata/object/OBJ (ADR-0146 D1, #28818), the All metadata area's item page, where 'Discard draft' lives (ResourceEditPage.tsx), so the row is driven there directly; the per-package /_console/studio/PKG/metadata/object/OBJ opens the Data pillar, which has no discard (areaItemPillarPath, metadataArea.ts). Measured input: QA run #28981 (18.3.0 wave 4, 7fa9873f; card note 6571282495). Re-read at base dfbe34d887. The 'Studio pillars offer no discard' half is narrowed to the Data pillar, since the Access pillar offers one (access-discard-draft). No clause changed", "ref": "#28579" + }, + { + "revision": 11, + "date": "2026-11-04", + "change": "fixtures.knownGaps[0], variants[5] and [6] and acceptance[2] corrected from QA run #29890 (W5-FAIL, at 589049b0): Invite User shows the declared outcomeMessages copy the answer names (link_only on a deployment without mail, which the run read verbatim; successMessage is the fallback since #27843), and a per_group approval stays pending after one approval, so the decision is read from sys_approval_action. Read at base f68b47ca", + "ref": "#29906" } ] }, @@ -173,7 +179,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": 4, + "revision": 5, "priority": "P1", "surface": "browser", "personas": [ @@ -187,17 +193,17 @@ "the read-only showcase package in Studio (stock)" ], "knownGaps": [ - "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 and the import wizard's disabled Next are FIXED for the keyboard (QA run #23953 at objectui daeb77fc84dd): each carries its reason on a focusable wrapper with an aria-describedby, so both now calibrate hover AND keyboard, expected PASS; Studio's automation Enabled toggle and the field inspector's Remove pass the same way. Regression rows, expected PASS since QA run #24570 (objectui 4016370a, two fresh loads each): the field inspector's read-only Label / API name / Type / Required / Move up-down sit in a focusable panel wrapper described by the read-only reason, with the tooltip on focus (objectstack-ai/objectui#12303, closed by objectui PR #12324, b64fda3f — the rail body described by fieldRailReasonId, StudioDesignSurface.tsx); and the Studio validation editor's body on a read-only package is a role=group tab stop described by the reason, with the tooltip on hover and focus (objectstack-ai/objectui#12691, closed by objectui PR #12833, d0a00d79, ObjectValidationsPanel.tsx). Both fixes are in the console pin 4016370a and on objectui main" + "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 its one placement, record_header (it declares locations: ['record_header'] only; objectui DeclaredActionsBar.tsx; pin DeclaredActionsBar.disabledReason-11811.test.tsx). It declares its own reason, disabledReason: 'Mark the task done first.' (the action spec's disabledReason, #26619), so the text is the author's, not the generic platform fallback — record it as specific 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 and the import wizard's disabled Next are FIXED for the keyboard (QA run #23953 at objectui daeb77fc84dd): each carries its reason on a focusable wrapper with an aria-describedby, so both now calibrate hover AND keyboard, expected PASS; the field inspector's Remove passes the same way (the automation Enabled switch is not a row: it stays enabled on a read-only package, variants[2]). Regression rows, expected PASS since QA run #24570 (objectui 4016370a, two fresh loads each): the field inspector's read-only Label / API name / Type / Required / Move up-down sit in a focusable panel wrapper described by the read-only reason, with the tooltip on focus (objectstack-ai/objectui#12303, closed by objectui PR #12324, b64fda3f — the rail body described by fieldRailReasonId, StudioDesignSurface.tsx); and the Studio validation editor's body on a read-only package is a role=group tab stop described by the reason, with the tooltip on hover and focus (objectstack-ai/objectui#12691, closed by objectui PR #12833, d0a00d79, ObjectValidationsPanel.tsx). Both fixes are in the console pin 4016370a and on objectui main" ] }, "variants": [ - "declared action disabled by its `disabled` predicate — Archive on a not-done task, record header AND record section placement", + "declared action disabled by its `disabled` predicate — Archive on a not-done task, in its record header placement (its only declared location)", "Studio Publish on a read-only package", - "Studio controls on a read-only package — the automation enable toggle, field inspector inputs, validation editor selects", + "Studio controls on a read-only package — field inspector inputs, validation editor selects, and the default inspector of the All metadata item page (/_console/studio/~all/metadata/TYPE/NAME, #29897). Not the automation Enabled switch: it is the deployment's ledger switch and stays enabled on a read-only package (FlowLedgerSwitch.tsx draws it disabled only on a walled boot)", "import wizard — the step's continue/import button while a required field is unmapped", - "a form submit button, where the surface disables it instead of validating on submit", - "toolbar or bulk buttons rendered disabled for the member persona" + "a form submit button, where the surface disables it instead of validating on submit (QA run #29890 found no stock instance: record it not offered unless the run finds one)", + "toolbar or bulk buttons rendered disabled for the member persona (QA run #29890 found no stock instance: record it not offered unless the run finds one)" ], "steps": [ "sign in as admin; on each variant locate every control rendered disabled (the disabled attribute, aria-disabled=true, or data-disabled)", @@ -261,6 +267,12 @@ "date": "2026-10-19", "change": "knownGaps[1]'s two expected FAILs re-pointed to regression rows — checklist-accuracy finding of the 18.0 QA sweep (QA run #24570, ux-conventions.disabled-control-reason knownGaps[1]; wave anchor #24579 §3): objectstack-ai/objectui#12303 and #12691 no longer reproduce at 4016370a. Read on objectui: both issues are closed completed, by PR #12324 (merge b64fda3f) and PR #12833 (merge d0a00d79); each merge is an ancestor of the console pin 4016370a and of objectui main (merge-base --is-ancestor exit 0), and the wrapper code is present on main. No clause changed", "ref": "#24649" + }, + { + "revision": 5, + "date": "2026-11-04", + "change": "fixtures.knownGaps[0] and [1] and variants[0], [2], [4] and [5] corrected from QA run #29890 (W5-FAIL, at 589049b0): Archive declares its own disabledReason and only the record_header placement; the automation Enabled switch is the ledger switch and stays enabled on a read-only package; variants[2] names the All metadata item page's default inspector (#29897, fixed); variants[4] and [5] had no stock instance. Read at base f68b47ca", + "ref": "#29906" } ] },