Skip to content

Commit c6bc269

Browse files
docs(qa): wave-2 checklist-accuracy close-out, slice A — seven areas and three RUNNER facts (#26969)
Part of #25807 (wave-2 accuracy, slice A) Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:按 18.2.0 第二波五份测试记录,修正 7 个领域共 37 条测试清单条目和 1 个共享配方里的过时描述,并给 RUNNER.md 补 3 条环境事实。只改文档,不动产品代码。 - 为什么改:第二波运行发现这些条目的步骤、字段名、错误码或引用的测试文件已与当前代码不符,下一轮运行会被误导、误判或白跑。 - 风险与回滚:只影响 QA 清单文本,不改任何断言的判定标准(新增的只有 trigger-status-contract 的第五行和 query-contract-matrix 的六个运算符变体)。每条 revision +1、追加一行 history,回滚就是 revert 本 PR。 - 先看:`areas/integration-system.json` 的 webhook-lifecycle(#23166 是已关闭的运行记录,不是阻塞卡);`areas/automation.json` 的 trigger-status-contract(新增 422 FLOW_INPUT_SCHEMA_INVALID 行);`RUNNER.md` 的三条新事实。 - 两分钟验证:`node scripts/check-platform-checklist.mjs`,预期打印 `check-platform-checklist: OK`、退出码 0。 - 需要你做的:无,席位落地。 ## What changes The slice-A close-out of the wave-2 checklist-accuracy findings that RUNNER.md routes to the wave anchor ("close out through the wave's anchor card … not extracted"). Sources: anchor comments 6433587719, 6433708086, 6433784748, 6433878766, 6433966437 and the run records #26450, #26456, #26468, #26476, #26481. Every finding was checked against the code at base `e4ba846f` before its edit; the history row of each item names the file read. - **37 items** in `automation`, `cli`, `api-backend`, `identity-auth`, `integration-system`, `search` and `ux-conventions`: `revision` +1 from main's value and one appended `history` row each, citing the run record that found it. No existing history row is touched. - **1 shared recipe**: `search.json` `qa-contributor-bound-member` step 5 asks for a non-churned account (recipes carry no revision; its `provenance` line records the change). - **RUNNER.md**: (1) a per-boot `-d` DB does not isolate the showcase's upload root or its external datasource file; (2) the process-kill hook refuses one command that runs `ps`, `grep` and a kill, so the group stop runs on its own; (3) the `OS_PORT`-connector entry now says `os dev` compiles by default since #26130. - `automation.packaged-flow-clone-contract` changes `surface` from `api` to `mixed`: its acceptance[6] is a Studio browser leg. | item | before | after | source record | what | |---|---|---|---|---| | integration-system.webhook-lifecycle | 10 | 11 | #26456 | `lastStatusCode` → `response_code`; activation row's `active`; `$filter` JSON; 4xx receiver for a terminal row; no public read of the switch; requires pair; #23166 recorded as a closed run record with no open carrier | | automation.trigger-status-contract | 7 | 8 | #26476 | fifth trigger row 422 FLOW_INPUT_SCHEMA_INVALID in A0, variants, knownGaps | | cli.hook-body-extraction-gates | 6 | 7 | #26476 | FORBIDDEN_PATTERNS is 13 (ctx.provenance lowers since #25905); body.source, not isExpression | | api-backend.batch-transactional-discovery | 7 | 8 | #26476 | #24781 is done; knownGaps[0] kept as a CLOSED record | | api-backend.error-envelope-ledger | 6 | 7 | #26476 | OBJECT_NOT_FOUND / RECORD_NOT_FOUND are StandardErrorCode members | | api-backend.filter-comparand-conformance | 3 | 4 | #26476 | Date/bigint scored in process; the null head class reaches the driver | | api-backend.aggregate-contract-matrix | 5 | 6 | #26476 | count_distinct over `status` | | api-backend.api-methods-verb-gate | 2 | 3 | #26476 | `searchable` flag on derived search; createMany body is a bare array | | api-backend.enforce-or-remove-authoring-gates | 6 | 7 | #26476 | the regex advisory has no relevantWhen, so it prints for any stack | | automation.packaged-flow-clone-contract | 8 | 9 | #26476 | surface mixed; its browser leg needs `pnpm console:build` | | cli.build-own-contract | 8 | 9 | #26476 | namespace prefix on plain-literal objects; live-conversion probe for the parity | | integration-system.connector-degraded-recovery | 5 | 6 | #26476 | `--log-level info` for the interval line; the unknown-connector comparator's sentence | | identity-auth.two-factor-verify-to-activate | 2 | 3 | #26476 | DB-file read as the live-boot equivalent | | identity-auth.two-factor-disable-lifecycle | 1 | 2 | #26476 | DB-file read; settings door `/api/settings/auth`; `user.authGate` | | automation.flow-node-type-matrix | 9 | 10 | #26456 | clone needs a label; record-shaped items; timestamped signature only | | automation.rollup-summary-filter | 6 | 7 | #26456 | child key `expense_report`; editor path and publish step | | api-backend.formula-stdlib-matrix | 4 | 5 | #26456 | promotion refused as an outcome 'refused' batch entry, not a 422 | | identity-auth.api-key-ui-lifecycle | 4 | 5 | #26456 | the two API Keys routes; mint is two clicks | | identity-auth.self-signup-gate | 6 | 7 | #26456 | the invitation bounce shows a card, no auto-forward | | identity-auth.first-run-owner-bootstrap | 3 | 4 | #26456 | no demo personas; two document loads on exit | | cli.scaffold-console-first-paint | 6 | 7 | #26456 | quick-start.mdx has no scaffold command; `pnpm run dev` | | ux-conventions.write-feedback | 6 | 7 | #26456 | kanban leg not drivable (#25933 drop gate); create/edit toast rows now expected PASS (#26172, #26461); import Undo uses window.confirm | | cli.dev-boot-contract | 8 | 9 | #26481 | `--no-compile` for the staleness leg (#26130) | | automation.packaged-flow-disable-durable | 6 | 7 | #26481 | task POST needs `title` | | integration-system.datasource-credential-refusal-matrix | 6 | 7 | #26481 | where the Postgres dirs can live; the cause is in `statusReason` | | automation.trigger-type-matrix | 8 | 9 | #26468 | two corroborating pins named | | automation.screen-flow-roundtrip | 6 | 7 | #26468 | engine pin under A5/N0 named | | automation.setup-packaged-automation-board | 9 | 10 | #26468 | invite-first member; prefilled clone dialog; the activation-read verify no longer asserts 'open by ruling' | | cli.scaffold-first-run | 6 | 7 | #26468 | exit 0 is no install evidence; SKILL.md dirs, not `ls`; pre-release variant required on an unreleased subject | | cli.dev-automigrate-policy | 7 | 8 | #26468 | three under-claimed pins named | | api-backend.query-contract-matrix | 6 | 7 | #26468 | expand HTTP pin named; six AST operators added to variants; comparand-less null spellings | | api-backend.bulk-write-contract | 5 | 6 | #26468 | uniform member deleteMany refusal; a step for the per-object batch variant | | integration-system.connector-declarative-boot | 5 | 6 | #26450 | two pins below A3–A5/N1 named | | integration-system.datasource-admin-lifecycle | 7 | 8 | #26450 | four corroborating pins named, the capability-floor pin among them | | integration-system.email-template-render | 8 | 9 | #26450 | send-template.test.ts under A1/A2/A4 | | integration-system.external-schema-browser-ui | 6 | 7 | #26450 | auth-guard unit pin under A5/N2 | | integration-system.external-schema-introspection | 6 | 7 | #26450 | service unit pin under A0 | | search.json recipe qa-contributor-bound-member | — | — | #26456 | step 5: a non-churned account | | RUNNER.md | — | — | #26476, #26481, #26456 | the three environment facts above | ### No change, and why - `cli.migrate-plan-apply-json`: already corrected by #26795 (dispatch note). - The api-backend items re-pinned the same day by #26572 (`declarative-endpoint-execution`, `packaged-action-disabled-dispatch`, `action-activation-door-contract`, `rest-crud-config-contract`, `rest-batch-config-contract`, `rest-metadata-config-contract`, `rest-route-generation-tombstones`, `second-object-exposure-and-internal-fields`, `data-request-shape-and-query-arity`, `meta-read-doors-v18`, `error-sentences-and-unique-violation-wire-code`): each wave-2 line on them was read against the current text, which already names the pins and the layer each reaches (for example, `rest-crud-config-contract` says its discovery pin runs at `/objects`, and `rest-route-generation-tombstones` says §E's plugin-path case drives `batch.defaultAtomic`). - Lines that read "ref accurate" (confirmations, nothing stale): `automation.durable-suspend-restart` (its ref already calls the fixture the pin's own), `paused-run-trigger-record-masked`, `flow-registration-verdict-on-read`, `flow-authoring-refusals-v18`, `flow-run-semantics-v18`, `inbound-hook-timestamped-signature`, `scheduled-work-opt-in`; `cli.organization-ownership-ceremony`, `meta-package-flags-and-explain`, `validate-v18-authoring-verdicts`, `verify-derived-proof-families`; `cli.migrate-meta-write-stored-and-all` (a scoring divergence; its ref claims no command layer). - `automation.packaged-flow-subflow-disable-refusal`: the run confirmed knownGaps[0] live, and its N1 fixture gap already has a recipe (#26622). - `integration-system.connector-auth-kind-application`: its ref already names both pins the mapping cited. - `identity-auth.two-factor-enrollment-reveal` and `two-factor-backup-codes`: the shared two-factor lines are applied where the items read rows or settings; backup-codes already names the DB-file read. - The identity-auth (9) and search (3) items of #26450 carry clause verdicts only, no accuracy line. - `cli.scaffold-first-run`'s revision-6 history row says 'seven clauses' for five: history rows are append-only, so it stays. - Outside slice A, not touched: the v17 `--legacy-peer-deps` recipe line lives in `platform-core.v18-boot-refusals` fixtures, and `studio-authoring.draft-publish-lifecycle`'s METADATA_CONFLICT / NO_DRAFT lines belong to slice B. ## Tests No product code changed. Gates at head `0a7d5b4f` (after merging `origin/main`): <details><summary>dispatch-gates: 16 derived, 16 run, 0 NOT-MEASURED</summary> ```text node scripts/check-adr-links.mjs exit 0 node scripts/check-adr-links.mjs --self-test exit 0 node scripts/check-closing-keyword-parity.mjs exit 0 node scripts/check-closing-keyword-parity.mjs --self-test exit 0 node scripts/check-comment-mask-corpus.mjs exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions exit 0 (exit 3 before building @objectstack/formula and @objectstack/lint) pnpm check:ci-filter-parity exit 0 pnpm check:cross-package-test-inputs exit 0 pnpm check:doc-authoring exit 0 pnpm check:driver-memory-census exit 0 pnpm check:gitlink-declared exit 0 pnpm check:nul-bytes exit 0 pnpm check:platform-checklist exit 0 pnpm check:refd-timer-probe exit 0 pnpm check:watch-hint-literal exit 0 ✓ dispatch-gates --ran: 16 derived famil(ies) accounted for — 16 run, 0 NOT-MEASURED (a DERIVED zero — all 16 recorded an exit code and none of them is 3). ``` </details> <details><summary>check-platform-checklist</summary> ```text check-platform-checklist: OK — 16 areas, 329 items (324 active, 1 planned); coverage: 39 kinds mapped, 4 waived; traps: 19 documented, 19 in use; provisioning: 10 area recipes, 15 item references resolved (2 area-qualified), 10/10 recipes referenced ``` </details> A script compared every item in the seven files against `origin/main`: 37 changed, each at main's revision + 1 with exactly one appended history row and the earlier rows byte-equal. ## Acceptance notes Observations from these records that are not ledger edits (reported to the seat, not filed here): - `webhook-lifecycle` A2: whether a flow node's predicate multi-write should carry the organization is a maintainer question raised in #23166, and no open issue carries it (its successor anchors closed with it noted). The wave-2 run measured the block again. - A retired-filter-operator refusal is cut at the 500-character client-message bound before the `$contains` alternative its prescription names (#26476; `truncateClientMessage`). - The kanban drop gate's dimmed lane carries no reason text (#26456), against the disabled-control convention. - After a failed datasource connect, `POST /api/v1/datasources/NAME/test` answers 'no registered driver to introspect', which names no cause (#26481). ## Verification `node scripts/check-platform-checklist.mjs` exits 0 on the head. Run `git diff origin/main...HEAD -- docs/qa/platform-checklist/areas` to see that each hunk is the item's text plus one new history row. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QSeKYkXktsD6m4eAJ18rUE --- _Generated by [Claude Code](https://claude.ai/code/session_01QSeKYkXktsD6m4eAJ18rUE)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 045cfa4 commit c6bc269

8 files changed

Lines changed: 389 additions & 131 deletions

File tree

‎docs/qa/platform-checklist/RUNNER.md‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,14 @@ contradicts it, and correct it here when it does.
283283
before scoring any connector clause, prove where the live dispatch lands: create a
284284
`showcase_task`, read the `showcase_declarative_connector_ping` run it fires, and confirm
285285
it completed with a `ping.body` whose uptime matches THIS boot, not another server's.
286-
⛔ Do not read the instance metadata for it: `os dev` boots the prebuilt
287-
`dist/objectstack.json` unless `--compile` is passed (`packages/cli/src/commands/dev.ts`),
288-
so `GET /api/v1/meta/connector/showcase_status_api`'s `item.providerConfig.baseUrl` can
289-
carry the build-time value. QA run #25817 at `6679644a` measured it: with `OS_PORT=41302`
286+
⛔ Do not read the instance metadata for it: an `os dev --no-compile` boot serves the
287+
existing `dist/objectstack.json` as built, so `GET /api/v1/meta/connector/showcase_status_api`'s
288+
`item.providerConfig.baseUrl` can carry the build-time value. Since #26130 a default
289+
`os dev` compiles the config under the boot's own environment first
290+
(`packages/cli/src/commands/dev.ts`, the `compile` flag, default on), so on a default
291+
boot that read can follow an exported `OS_PORT` — the dispatch check above stays the
292+
proof on either boot. Before #26130 `os dev` booted the prebuilt artifact unless
293+
`--compile` was passed, and QA run #25817 at `6679644a` measured that old default: with `OS_PORT=41302`
290294
exported, that read still answered `http://127.0.0.1:3000`, while the live
291295
`connector_action` reached 41302 (`ping.body` uptime matched the boot, and nothing
292296
listened on 3000) — so the metadata read misreports the fixture in both directions.
@@ -347,6 +351,23 @@ contradicts it, and correct it here when it does.
347351
wrote a CRM manifest there for 25 s before removing it through
348352
`DELETE /api/v1/marketplace/install-local/<id>` (#23163).
349353

354+
- **A per-boot `-d file:…` database does NOT isolate the showcase's upload root or its
355+
external datasource either — the same class as the install-local entry above.** Two
356+
more paths resolve against the server's working directory: the local storage root,
357+
whose settings-manifest default is `./.objectstack/data/uploads` unless
358+
`OS_STORAGE_LOCAL_ROOT` is exported (only `--fresh` sets it, to its own temp home:
359+
`packages/cli/src/commands/dev.ts`), and the showcase's external datasource file
360+
`.objectstack/data/showcase_external.db`
361+
(`examples/app-showcase/src/system/datasources/showcase-external.datasource.ts`). So a
362+
tree boot with only `-d` writes uploads into `examples/app-showcase/.objectstack/data/uploads/`
363+
and creates `showcase_external.db` beside it, and even a boot that writes neither leaves
364+
`.objectstack/data/` behind, empty. All of it is git-ignored, so a clean `git status` is
365+
no evidence (QA runs #26456, #26476 and #26481 met all three). **Fix:** export
366+
`OS_STORAGE_LOCAL_ROOT=/tmp/RUN/uploads` beside `OS_PORT` for any item that uploads, and
367+
boot from a scratch copy of the app for any item that writes the external datasource.
368+
An empty `.objectstack/data/` left by a tree boot is harmless and is not the
369+
installed-packages trap.
370+
350371
- **`ss` is not installed in these containers — read liveness with `curl`, never a socket
351372
table.** `ss` and `netstat` are both absent (`command not found`); `lsof` and `fuser`
352373
are present. The trap is that the usual spelling hides the cause: `ss -ltn | grep`
@@ -376,6 +397,11 @@ contradicts it, and correct it here when it does.
376397
read the group from the listener, `PGID=$(ps -o pgid= -p "$(lsof -ti tcp:PORT)" | tr -d ' ')`;
377398
stop it with `kill -TERM -- -$PGID`, and confirm `lsof -ti tcp:PORT` then prints nothing.
378399
⛔ Never stop a server by name (`pkill`), and never a group you did not start.
400+
⚠️ Run that stop as a command of its own: the repo's PreToolUse hook
401+
`.claude/hooks/guard-process-kill.sh` refuses any one Bash command that runs `ps` and
402+
`grep` and also kills, judged over the whole command rather than per pipeline (its
403+
header says why), so a stop chained after a `ps … | grep` look at the process table is
404+
refused although its `kill` is group-scoped (QA run #26476 met it).
379405

380406
- **Console session auth is a bearer token in `localStorage`, and a form sign-in ALSO
381407
sets a cookie — you need both halves.** The console's auth client stores the session

‎docs/qa/platform-checklist/areas/api-backend.json‎

Lines changed: 84 additions & 23 deletions
Large diffs are not rendered by default.

‎docs/qa/platform-checklist/areas/automation.json‎

Lines changed: 77 additions & 27 deletions
Large diffs are not rendered by default.

‎docs/qa/platform-checklist/areas/cli.json‎

Lines changed: 65 additions & 28 deletions
Large diffs are not rendered by default.

‎docs/qa/platform-checklist/areas/identity-auth.json‎

Lines changed: 48 additions & 18 deletions
Large diffs are not rendered by default.

‎docs/qa/platform-checklist/areas/integration-system.json‎

Lines changed: 72 additions & 24 deletions
Large diffs are not rendered by default.

‎docs/qa/platform-checklist/areas/search.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"qa-contributor-bound-member": {
99
"title": "A CONTRIBUTOR-BOUND member — an identity that reads a strict SUBSET of showcase_invoice rows (invited sign-up + one sys_user_position row)",
1010
"why": "Both-sides RLS needs a persona for whom a specific row is invisible while the object itself is readable. ⚠️ Neither seeded demo persona is that persona, and reaching for them is the trap this recipe closes — measured on 17.1.0 (#10257): Mei (phone.demo@example.com) holds NO position, so showcase_contributor never merges and showcase_invoice answers PERMISSION_DENIED — the OBJECT gate, no rows at all, not a subset; Ada (auditor.demo@example.com) holds `auditor` → showcase_auditor, which carries `showcase_invoice: { allowRead: true, viewAllRecords: true }` — VAMA bypasses record scope entirely, so she sees all 12 — also not a subset. The persona that IS a subset is a member bound to the `contributor` position: showcase_contributor grants read/create/edit on showcase_invoice while the `invoice_own_rows` RLS narrows selects to `owner == current_user.email`, and none of the seeded invoices are owned by a fresh sign-up. Nothing here needs a seed change or a repo file — the whole gap was that the recipe existed only as prose on one item, which is why a runner reached for the demo personas instead and recorded blocked(fixture).",
11-
"provenance": "run #7629 (framework, 2026-08-11) drove search.rls-both-personas to 4/4 PASS on exactly this persona — 3 invoices visible to the member vs 15 to admin. Promoted from prose on that one item to a replayable area recipe by the 17.1.0 sweep (#10236 B1); the Mei/Ada measurements above are from #10257. Step 1 re-pointed invite-first by #21060 after QA run #21056 met the invite_only default (403 SELF_REGISTRATION_CLOSED on an uninvited sign-up). Step 5's body gained the `owner` it always needed after QA run #23166 met an own-row invisible to its creator (#23169). Steps 2 and 3 name the admin REST doors QA run #24574 measured (an admin GET of sys_user by email and an admin POST of sys_user_position, the position as plain text), and step 5 names the invoice number field `name`, after that run met 400 INVALID_FIELD on `number` and #24593 met 400 on an OData `$filter` (wave anchor #24579, PR #24671).",
11+
"provenance": "run #7629 (framework, 2026-08-11) drove search.rls-both-personas to 4/4 PASS on exactly this persona — 3 invoices visible to the member vs 15 to admin. Promoted from prose on that one item to a replayable area recipe by the 17.1.0 sweep (#10236 B1); the Mei/Ada measurements above are from #10257. Step 1 re-pointed invite-first by #21060 after QA run #21056 met the invite_only default (403 SELF_REGISTRATION_CLOSED on an uninvited sign-up). Step 5's body gained the `owner` it always needed after QA run #23166 met an own-row invisible to its creator (#23169). Steps 2 and 3 name the admin REST doors QA run #24574 measured (an admin GET of sys_user by email and an admin POST of sys_user_position, the position as plain text), and step 5 names the invoice number field `name`, after that run met 400 INVALID_FIELD on `number` and #24593 met 400 on an OData `$filter` (wave anchor #24579, PR #24671). Step 5 asks for a non-churned account after QA run #26456 (wave anchor #25807) met the first listed account churned and the invoice POST refused 400.",
1212
"app": "showcase",
1313
"requires": [
1414
"a stock dev boot — the `contributor` position declares `showcase_contributor` in its `permissionSets` (examples/app-showcase/src/security/positions.ts), which the resolver reads by name (ADR-0131 D3/D4). ⛔ Do NOT hand-create a sys_position_permission_set row: no resolver reads it, so it proves nothing about the merge",
@@ -45,7 +45,7 @@
4545
{
4646
"step": 5,
4747
"call": "give the member a row of their OWN: as the member, POST /api/v1/data/showcase_invoice with `owner` set to the member's OWN email (the step-1 address) — contributors hold allowCreate",
48-
"expect": "2xx, and that row IS visible to them on a re-read — turning the subset from empty into a proper non-empty one. This is what makes the restriction provably SUBTRACTIVE rather than a blackout; without it, zero-hits and no-access are indistinguishable. ⚠ The body MUST carry `owner: '<the member's email>'`: invoice_own_rows narrows SELECT to owner == current_user.email, and `owner` is a plain text field with no default (examples/app-showcase/src/data/objects/invoice.object.ts), so an invoice the member creates WITHOUT it lands 2xx and is then invisible to its own creator — reading exactly like a blackout (QA run #23166). The invoice number is the required `name` field (there is no `number` field: a body naming `number` answers 400 INVALID_FIELD, QA run #24574); `status` may be omitted (its 'draft' option is default:true). Point the invoice's required `account` at a SEEDED account id (read one from GET /api/v1/data/showcase_account), never at one a consuming item later deletes: a required lookup turns set_null into restrict, so that delete answers 409 DELETE_RESTRICTED (records-forms.crud-roundtrip met it in QA run #21056).",
48+
"expect": "2xx, and that row IS visible to them on a re-read — turning the subset from empty into a proper non-empty one. This is what makes the restriction provably SUBTRACTIVE rather than a blackout; without it, zero-hits and no-access are indistinguishable. ⚠ The body MUST carry `owner: '<the member's email>'`: invoice_own_rows narrows SELECT to owner == current_user.email, and `owner` is a plain text field with no default (examples/app-showcase/src/data/objects/invoice.object.ts), so an invoice the member creates WITHOUT it lands 2xx and is then invisible to its own creator — reading exactly like a blackout (QA run #23166). The invoice number is the required `name` field (there is no `number` field: a body naming `number` answers 400 INVALID_FIELD, QA run #24574); `status` may be omitted (its 'draft' option is default:true). Point the invoice's required `account` at a SEEDED account id whose `status` is not churned (read one from GET /api/v1/data/showcase_account and check it: the first row the list returns can be a churned one, and an invoice against it answers 400 'This account has churned — reactivate it before invoicing.', the invoice object's own validation rule, QA run #26456), never at one a consuming item later deletes: a required lookup turns set_null into restrict, so that delete answers 409 DELETE_RESTRICTED (records-forms.crud-roundtrip met it in QA run #21056).",
4949
"source": "ContributorPermissionSet.objects.showcase_invoice.allowCreate === true (permission-sets.ts); the seeded owner emails hold no credential, so borrowing a seeded owner is not an option — this is why the own-row is CREATED"
5050
}
5151
],

‎docs/qa/platform-checklist/areas/ux-conventions.json‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"title": "Every user-initiated write answers visibly — success names what changed, a refusal says why, and a no-op claims nothing",
1010
"since": "v17.7",
1111
"status": "active",
12-
"revision": 6,
12+
"revision": 7,
1313
"priority": "P1",
1414
"surface": "browser",
1515
"personas": [
@@ -34,7 +34,7 @@
3434
"record create — New form save on showcase_task",
3535
"record inline edit — double-click a Priority cell on the task grid (the grid has no 'Edit inline' button), change it, then Save row or Save All",
3636
"bulk delete — select two rows → Delete in the selection bar → confirm",
37-
"kanban move — drag a card Backlog → To Do (direct manipulation: the card in its new column IS the success feedback; a refused move must revert with an error)",
37+
"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)",
3838
"declared record action — Mark Done on a task record (declared successMessage 'Task marked done.', examples/app-showcase/src/ui/actions/index.ts#MarkDoneAction)",
3939
"param action dialog — Setup → Users → Invite User → Confirm (declared successMessage 'Invitation sent to ${result.email}')",
4040
"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",
@@ -49,8 +49,8 @@
4949
"for each variant perform the write once, then within ~2 s capture a screenshot and the DOM of every feedback region — toasts ([data-sonner-toast]), role=status / role=alert regions, and the surface's own save indicator — recording the verbatim text, or its absence",
5050
"for each variant read server-side whether the write landed (GET the record, the metadata item or the sys_ row) and record the request's status from the network log",
5151
"repeat the record rows signed in as the member persona, on member-owned records — the stock member's set (showcase_member_default) grants showcase_task read + create only, and the console applies it up front: it offers the member no inline edit, no row selection and no kanban drag, so those three rows cannot be driven as the member (record them as not offered, never as refused). The member's create row runs; for the refusal row use the member's Mark Done on a task record, which answers 403 PERMISSION_DENIED (its error feedback is clause 3's to score). To drive the edit rows as a member, bind the member to the contributor position (showcase_contributor grants task edit, not delete)",
52-
"negative pass: (a) submit the create form with a required field empty; then break a validation rule — showcase_task's only rule is the UPDATE-time state machine task_status_flow (no create-time rule exists), so drive it as a refused transition, e.g. a kanban move Backlog → Done; (b) open an inline editor and leave it without changing the value",
53-
"where a variant offers Undo, click it and read the record back — on stock the import does (knownGaps[3]): run the two-row CSV import as a job, once creating and once updating, then 'Undo import' from the import history and read the rows back"
52+
"negative pass: (a) submit the create form with a required field empty; then break a validation rule — showcase_task's only rule is the UPDATE-time state machine task_status_flow (no create-time rule exists), so drive it as a refused transition, Backlog → Done, on the record form or an inline status edit. ⛔ Not the kanban: the drop gate (#25933, ui/packages/plugin-kanban/src/KanbanImpl.tsx) dims a lane the transition rule blocks (data-kanban-drop-blocked) and a drop there sends no request, so no refusal reaches the server; record the dimmed lane as that surface's answer; (b) open an inline editor and leave it without changing the value",
53+
"where a variant offers Undo, click it and read the record back — on stock the import does (knownGaps[3]): run the two-row CSV import as a job, once creating and once updating, then 'Undo import' from the import history — it asks through the browser's native window.confirm (ui/packages/plugin-grid/src/ImportWizard.tsx), unlike Studio discard's in-app dialog, so a driver accepts it as a dialog event — and read the rows back"
5454
],
5555
"acceptance": [
5656
{
@@ -62,7 +62,7 @@
6262
{
6363
"clause": "success feedback names its subject: a record-level write names the record or item (its title, label or display name) or the count; a singleton page (settings) and a named automation (e.g. Flow \"X\" completed) are their own subject and pass as they are; a bare 'Success' / 'Done' or a machine name never passes",
6464
"oracle": "screenshot",
65-
"verify": "two steps per row. First classify the write: record-level (a record create, edit or delete, the import, the approval decision, a record or param action, a Studio item) or singleton / named automation (the settings save, a named Flow run). Then score it by that class's test. Record-level: PASS when the captured text names the record or item (record title or display name, invitee email, package or item label, e.g. 'Approved \"EXP-2001\"') or the count ('N created · M skipped', '1 row saved'); text that names only the object ('Task created successfully') scores FAIL — the console's record create and edit toasts read that way until ui card #26172 lands, so those rows are expected FAILs that cite it. Singleton / named automation: PASS when the text names that page or automation ('Settings saved', 'Flow \"Reassign Task\" completed'). In either class a generic 'Done' / 'Success' or a raw snake_case name scores FAIL for that row",
65+
"verify": "two steps per row. First classify the write: record-level (a record create, edit or delete, the import, the approval decision, a record or param action, a Studio item) or singleton / named automation (the settings save, a named Flow run). Then score it by that class's test. Record-level: PASS when the captured text names the record or item (record title or display name, invitee email, package or item label, e.g. 'Approved \"EXP-2001\"') or the count ('N created · M skipped', '1 row saved'); text that names only the object ('Task created successfully') scores FAIL — the console's record create and edit toasts read that way until #26172 (the dialog paths) and #26461 (the full-page /new and /edit form route) — both closed fixed, so those rows are now expected PASS, and a toast naming only the object is a regression. Singleton / named automation: PASS when the text names that page or automation ('Settings saved', 'Flow \"Reassign Task\" completed'). In either class a generic 'Done' / 'Success' or a raw snake_case name scores FAIL for that row",
6666
"evidence": "the verbatim feedback strings"
6767
},
6868
{
@@ -141,6 +141,12 @@
141141
"date": "2026-10-26",
142142
"change": "acceptance[1] clause and verify reworded to the maintainer's ruling on the object-only success strings QA run #25817 (release-18.2.0-manual-set at 6679644a, objectui pin b41ed7f68956) asked about: ruling comment 6417978207 on #25849, letter C — a record-level write names the record (its title) or the count; a singleton page (settings) and a named automation (Flow \"X\" completed) are their own subject and pass as they are, and the clause states that boundary. verify now classifies each row (record-level, or singleton / named automation) before scoring it by that class's test. Not loosened: a generic 'Success' / 'Done' or a machine name still FAILS, and a record-level toast that names only the object ('Task created successfully') is an expected FAIL until the ui card #26172 lands. The 2026-10-07 instruction the clause rests on (every write has feedback; no machine names) is unchanged in meaning",
143143
"ref": "#25849"
144+
},
145+
{
146+
"revision": 7,
147+
"date": "2026-10-27",
148+
"change": "steps[4], variants[3], acceptance[1].verify and steps[5] follow the console at the wave-2 head. Checklist-accuracy findings of the 18.2.0 wave 2 (QA run #26456, release-18.2.0-w2-manual-browser, at 756220c4; wave anchor #25807, slice A): the kanban refused-transition leg is no longer drivable (the #25933 drop gate dims a blocked lane and a drop sends nothing), the record create/edit rows are no longer expected FAILs (#26172 and #26461 both closed fixed), and the import Undo confirms through window.confirm. Re-read at base e4ba846f (KanbanImpl.tsx data-kanban-drop-blocked; ImportWizard.tsx undo confirm). The dimmed lane carries no reason text; that is reported on the wave anchor as an observation, not edited here. No clause's assertion changed",
149+
"ref": "#25807"
144150
}
145151
]
146152
},

0 commit comments

Comments
 (0)