Skip to content

Commit 7a48bf7

Browse files
docs(qa): wave-3 checklist accuracy slice S10 — the 18 W3-RT findings at 6e11a92 (#27377) (#28748)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSi9paTx1Lu8ENYwwssthfK3h)_ Fixes #28701 Part of #27377 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:按 W3-RT(#28656,`6e11a929`)留下的 18 条清单准确性发现,逐条对照当前 main 源码复核后改了平台测试清单:5 个区文件、10 个条目,每个条目 revision +1 并追加一行 history。18 条全部落地修改,没有一条在上游已修好。 - 为什么改:这些条目里有已修好却仍写着「今天失败」的 knownGaps(表达式编辑器的 RLS 警告级、仪表盘全局筛选、汇总字段),有被 ui vitest 守卫拒绝的测试命令,有会被 400/403 拒绝的邮件流程配方,以及过时的记录页标签栏说明。不改,下一轮跑测试的人会多走手工核对或报出假问题,#27377 发版巡检锚点也无法收口。 - 风险与回滚:只改 QA 清单 JSON,不动产品代码、不进任何发布包;没有任何判据被放宽,只补得更准或更严(例如仪表盘 acceptance[4] 现在要求重载时必须弹出离开提示)。回滚就是 revert 本 PR。 - 先看:`docs/qa/platform-checklist/areas/studio-authoring.json` 的 `expression-editors`(删掉一条 knownGap、改写 #27482 那条的事实部分,acceptance[4] 的「no PUT issued」原样保留);同文件 `dashboard-widget-and-filter-types` 的 acceptance[4].verify(三种情形分开判);`integration-system.json` 的 `email-template-render` knownGaps[1](邮件流程配方)。 - 两分钟验证:仓库根运行 `pnpm check:platform-checklist`,预期最后一行以 `check-platform-checklist: OK — 16 areas, 352 items` 开头。 - 需要你做的:无,席位落地。 ## What changes QA run #28656 (W3-RT, at `6e11a929`) parked 18 checklist-accuracy findings on anchor comment 6557904764. Each row was re-verified against source at `1c5b1773ec` before it was edited. All 18 rows are edited, across 10 items in the five area files the card names. None had already landed upstream. Every edited item gets `revision` +1 and one `history` row citing #28656 and #28701. Main's history stays an exact prefix (checked item by item against a parsed copy). No file outside the five is touched. No clause is weakened. | # | item · clause | outcome | source confirmation | |---|---|---|---| | 1 | `studio-authoring.expression-editors` knownGaps[3] | edited: the warning-tier gap is dropped (the old [4] becomes [3]) | `celAuthoring.ts` `lintCelPredicate` / `rlsPredicateVerdict` keep the rule's error severity; `celAuthoring.test.ts` pins it for USING and CHECK. The 0 PUTs is #28656's | | 2 | same item, knownGaps[4] (now [3]) | edited, factual half only: the editor half is #27482 (open); #27612/#27643/#27644 landed; the residual is the first autosave of each newly refused text. acceptance[4]'s "no PUT issued" is left as it was | `PermissionMatrixEditor.serverVerdictHold-27482.test.tsx` case (b); issue and PR states read from GitHub | | 3 | same item, knownGaps[1], [2], acceptance[7].verify | edited: `cd ui && pnpm exec vitest run --maxWorkers=1 packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts` | `ui/scripts/vitest-invocation-guard.mjs`: the `--filter … exec` form runs in the package directory and gets the `package-cwd` verdict | | 4 | same item, steps[3] | edited: the listbox caps at 8; type a member prefix | `MAX_CEL_SUGGESTIONS = 8`. That system fields come first is #28656's reading | | 5 | `studio-authoring.access-owd-and-suggestions` knownGaps[1] | edited: D10 dropped; D13 kept with its API half, 409 `SUGGESTION_STATE` | `PackageOwdOverviewPanel.refusalClearsOnRevert-26846.test.tsx`; `suggested-audience-bindings.ts` | | 6 | `studio-authoring.held-edit-leave-guard` steps[7] | edited: a 404 `NO_DRAFT` before and after Leave is the "unchanged" reading | `noPendingDraftError` in `metadata-protocol/src/protocol.ts`; `meta-item-absent-404.test.ts` | | 7 | `studio-authoring.dashboard-widget-and-filter-types` knownGaps[0] | edited: dropped (the key goes with it) | PR #27784 merged; `InterfacesPillar.heldListItem-27459.test.tsx`. The live pass is #28656's | | 8 | same item, acceptance[4].verify | edited: three cases judged separately. A reload while held must raise the leave-site prompt (stronger than before) | the same pin's "filling the Field in sends one save carrying the item and the Label edit" | | 9 | same item, steps[4] / steps[2] | edited: a text filter commits on Enter or blur; "unbound" means `filterBindings` false for every filter | `DashboardFilterBar.tsx` `TextFilter`; `dashboard-filters.ts` `resolveBoundField` (source only, as W3-RT also says) | | 10 | `studio-authoring.summary-field-authoring` knownGaps[0] / negative[0] | edited: the knownGap is dropped; negative[0] is now a plain FAIL condition | PR #27720 merged; `DataPillar.summaryHeld-27461.test.tsx`, `ObjectFieldInspector.summaryAggregand-27461.test.tsx`. The live pass is #28656's | | 11 | `studio-authoring.app-and-nav-properties` steps[0] | edited: use a same-origin logo (`/_console/vite.svg`), as a runner-environment note | `ui/apps/console/public/vite.svg`. The proxy block is #28656's reading | | 12 | `records-forms.field-history-tracking` steps[3] | edited: open the stream from the Activity tab, the 动态 panel header, or `?related=timeline` | `RecordPageColumns.tsx` tab row (Overview · Activity · panels). The 1440/1000/700 px reading is #28656's | | 13 | same item, automated.ref | edited: the live spec needs the vite console (LIVE_APP_URL, default port 5180) | `ui/playwright.live.config.ts`; the spec's root-relative `page.goto` | | 14 | `integration-system.email-template-render` knownGaps[1] | edited: a `type:'api'` flow with `{triggerType:'api', hookId, secret}`, a notify node with no inline title/message, fired as the admin | `io-node-config.zod.ts` (template and inline title/message are mutually exclusive); `validateApiTriggerSecret`. The 403 rule comes from `flow-start-admission.ts`; see Acceptance notes | | 15 | same item, steps[7] | edited: `objectstack compile --output` or `--no-restart`, with an unchanged listener PID | `packages/cli/src/commands/dev.ts` (`restart` defaults on), `compile.ts` `--output` | | 16 | `approvals.dynamic-approver-routing` steps[4] | edited: the wire key is `outputs` on the approvals route; `outputs.next_reviewers` params on the action door | `approvals-routes.ts` reads `body.outputs`; `action-execution.ts` folds the `outputs.` family | | 17 | `api-backend.formula-stdlib-matrix` acceptance[5] / steps[6] | edited: both promotion doors named. The clause now asks for both refusals | `protocol.runtime-authoring-gate.test.ts` (`publishMetaItem` refused with 422 INVALID_METADATA); `rest-route-ledger.ts` `meta.publishItem` | | 18 | `approvals.dynamic-approver-routing`, carried line 6539452860 | edited: knownGaps[0]'s "not yet driven live" sentence now records #28656's live read | live-only, as #28656 read it. The carried line itself is a comment on #27377, not text in this file | ## Tests Docs only: no package source changes, so no build closure and no package test suite is owed (os-dev local-verification scope ① and ②). `skip-changeset`: the diff touches only `docs/qa/platform-checklist/areas/*.json`, which no package ships. ## Acceptance notes - Row 2 follows the card's rule. Only the factual half of the knownGap changes. acceptance[4]'s "no PUT issued" is left as it is until #27482 is ruled, and the knownGap says the probe fails as written until then. - Row 5 drops D10 because a pin proves the behaviour (the revert clears the banner, with a still-dirty CONTROL). The pin is a jsdom test with a stub client, and #28656 read the same thing live. - Row 14: `flow-start-admission.ts` refuses a self-triggered type only when it declares `runAs: 'system'`, and the spec default is `user`. The knownGap therefore says #28656's autolaunched flow was refused with 403, names the rule, and does not claim that every autolaunched flow is refused. The 400 and 403 statuses are #28656's readings. - Row 18: the carried line lives in comment 6539452860 on #27377, and anchor comment 6557904764 closes it. In this file, the matching text was knownGaps[0]'s "not yet driven live" sentence. That sentence is what this PR retires. - Dropping expression-editors knownGaps[3] renumbers the old [4] to [3]. `FOLLOW-UPS.md` (append-only, outside this card's files) still cites it as knownGaps[4]. - The five files hold more `pnpm --filter @object-ui/… exec vitest run` invocations that the ui vitest guard refuses, in items this card does not name. They are left for the seat: studio-authoring `object-designer-roundtrip`, `record-page-roundtrip`, `permission-matrix-editor-ux` and `packaged-automation-studio-lock`, and records-forms `import-wizard-encoding-and-hints` and `form-dirty-guard`. ## Verification <details><summary>Gates (head 4ca7c24, after one merge of origin/main)</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 16 families at `4ca7c24988`: 5 paths, +82 / -39, under the 3000 human-merge line. Each ran with stdin from /dev/null. Exit codes were captured before any output was read: - `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, after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` (lock VERDICT command-exit 0). The first run was a prerequisite refusal (exit 3) that measured nothing - `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 (`OK — 16 areas, 352 items`) - `pnpm check:refd-timer-probe` :: exit 0 - `pnpm check:watch-hint-literal` :: exit 0 A first sweep before `pnpm install` met prerequisite refusals (exit 3, nothing measured) on six gates. Every family above is from the run after the install. `--ran` reconciliation: "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)" (exit 0). </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 86fb645 commit 7a48bf7

5 files changed

Lines changed: 82 additions & 39 deletions

File tree

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2165,7 +2165,7 @@
21652165
"title": "CEL formula stdlib: all 30 bare-callable stdlib functions give known answers against the BUILT package — calendar-day timezone contract, addMonths month-end clamp, floor(-1.2)==-2, null-dropping joins, mixed int/double arithmetic, isoDate/isoDatetime text — and a formula calling an unregistered function is REFUSED at the authoring door (422), never stored to read null",
21662166
"since": "v16",
21672167
"status": "active",
2168-
"revision": 6,
2168+
"revision": 7,
21692169
"priority": "P1",
21702170
"surface": "mixed",
21712171
"personas": [
@@ -2189,7 +2189,7 @@
21892189
"calendar-day timezone contract (ADR-0053 Phase 2 D1): pin now to an instant whose calendar day DIFFERS between UTC and the reference zone (e.g. 2026-08-30T22:00:00Z with timezone Asia/Shanghai — locally already 08-31) and assert today() == 2026-08-31T00:00:00Z (the reference-tz calendar day expressed as UTC MIDNIGHT — never an instant carrying wall-clock time), daysFromNow/daysAgo offset from that same day; an unknown zone falls back to the UTC calendar day, never throws",
21902190
"mixed-arithmetic overloads (registerNumericCoercions, stdlib.ts): a double-typed record field divided/multiplied by an int literal (record.amount / 100) evaluates to the promoted double instead of faulting 'no such overload' to silent null (#1928), while pure int 7/2 == 3 keeps integer-division semantics untouched",
21912191
"live-door parity: GET a showcase_project row and confirm budget_remaining materializes (read-path evaluation); author a scratch object through the metadata channel, declaring its sharingModel (fixtures.requires[1]), whose formula field composes two stdlib calls (e.g. joinNonEmpty + upper) and read back the known answer — the stdlib reaching the LIVE read path, not just the harness",
2192-
"the unregistered-function probe: on a scratch object in the scratch package (fixtures.requires[1]), PUT /api/v1/meta/object/qaformula_NAME?package=com.objectstack.qa.formula at the ACTIVE door (no ?mode=draft) with a formula field calling an UNregistered function (e.g. bad_f = sqrt(4)), its body declaring a sharingModel (fixtures.requires[1]) — without one the 422 is security-owd-unset's refusal, not the expression's. Capture the 422 INVALID_METADATA and its expression-invalid issue, then GET the object and confirm the field was not stored. A ?mode=draft save of the same body is admitted (draft saves defer the gate to promotion, #4463 D1), and its draft→active promotion is refused with the same expression-invalid finding. Promoted through POST /api/v1/packages/PACKAGE/publish-drafts, that refusal is an entry with outcome 'refused' inside the batch envelope, not the request's own 422 (packages/metadata-protocol/src/protocol.ts, the publish-drafts outcome union). What the door cannot see is unchanged underneath: a formula that compiles but faults at EVALUATE time, or a row stored before the gate, still reads null (applyFormulaPlan's `r.ok ? … : null`, packages/objectql/src/engine.ts), now with one warn line per (object, field) (#22031)"
2192+
"the unregistered-function probe: on a scratch object in the scratch package (fixtures.requires[1]), PUT /api/v1/meta/object/qaformula_NAME?package=com.objectstack.qa.formula at the ACTIVE door (no ?mode=draft) with a formula field calling an UNregistered function (e.g. bad_f = sqrt(4)), its body declaring a sharingModel (fixtures.requires[1]) — without one the 422 is security-owd-unset's refusal, not the expression's. Capture the 422 INVALID_METADATA and its expression-invalid issue, then GET the object and confirm the field was not stored. A ?mode=draft save of the same body is admitted (draft saves defer the gate to promotion, #4463 D1), and its draft→active promotion is refused with the same expression-invalid finding at BOTH promotion doors: the single-item POST /api/v1/meta/object/qaformula_NAME/publish answers 422 INVALID_METADATA itself (publishMetaItem), while through POST /api/v1/packages/PACKAGE/publish-drafts that refusal is an entry with outcome 'refused' inside the batch envelope, not the request's own 422 (packages/metadata-protocol/src/protocol.ts, the publish-drafts outcome union). What the door cannot see is unchanged underneath: a formula that compiles but faults at EVALUATE time, or a row stored before the gate, still reads null (applyFormulaPlan's `r.ok ? … : null`, packages/objectql/src/engine.ts), now with one warn line per (object, field) (#22031)"
21932193
],
21942194
"acceptance": [
21952195
{
@@ -2223,7 +2223,7 @@
22232223
"evidence": "the responses vs the expectation"
22242224
},
22252225
{
2226-
"clause": "a formula calling an UNREGISTERED function is refused at the authoring door, before it is stored: an active-state object save (PUT /api/v1/meta/object/NAME, no ?mode=draft) whose formula field calls sqrt(...) answers 422 INVALID_METADATA carrying the build's located finding (rule expression-invalid, at that field's expression, naming `sqrt` as a NAME fault), and nothing lands; a ?mode=draft save is admitted (#4463 D1) and its promotion is refused with the same finding (through publish-drafts, as an outcome 'refused' entry of the batch envelope, steps[6]). This is the loud refusal the earlier EXPECTED-FAIL wording waited for (#22031, fixing #22019)",
2226+
"clause": "a formula calling an UNREGISTERED function is refused at the authoring door, before it is stored: an active-state object save (PUT /api/v1/meta/object/NAME, no ?mode=draft) whose formula field calls sqrt(...) answers 422 INVALID_METADATA carrying the build's located finding (rule expression-invalid, at that field's expression, naming `sqrt` as a NAME fault), and nothing lands; a ?mode=draft save is admitted (#4463 D1) and its promotion is refused with the same finding at BOTH promotion doors (the single-item POST /api/v1/meta/object/NAME/publish answers 422 INVALID_METADATA itself, and publish-drafts reports an outcome 'refused' entry of the batch envelope, steps[6]). This is the loud refusal the earlier EXPECTED-FAIL wording waited for (#22031, fixing #22019)",
22272227
"oracle": "api",
22282228
"verify": "the PUT answers 422 with code INVALID_METADATA and an issue whose rule is expression-invalid, located at the field's expression and naming the function; a follow-up GET /api/v1/meta/object/NAME shows the field was not stored. Pinned through the real saveMetaItem / publishMetaItem by packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (the 'runtime authoring gate on OBJECT writes — the formula field verdict (#22019)' block: sqrt(record.amount) refused 422 on an active save and on a draft's promotion, nothing lands, floor(record.amount) still saves), and at the rule layer by packages/lint/src/runtime-gate.object-formula-writes.test.ts. A row stored BEFORE the gate still reads null on the read path, now with one warn line per (object, field) (packages/objectql/src/engine-formula-fault-log.test.ts). That is the residual, not this clause",
22292229
"evidence": "the 422 response (code + the expression-invalid issue) + the post-refusal metadata read"
@@ -2281,7 +2281,8 @@
22812281
"change": "steps[6] and acceptance[5] say how the draft's promotion is refused. Checklist-accuracy finding 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 run promoted through publish-drafts and met the refusal as an outcome:'refused' entry inside the batch envelope, while 'refused the same way' read as a second HTTP 422. Re-read at base e4ba846f (metadata-protocol protocol.ts: outcome 'published' | 'refused' | 'nothing_to_publish'). The clause's assertion (refused, nothing lands) is unchanged",
22822282
"ref": "#25807"
22832283
},
2284-
{ "revision": 6, "date": "2026-10-31", "change": "The function count is 30, not 29, and the scratch-object legs name their writable package. Re-point of the red row QA run #27441 (18.3.0 sweep, wave 3, manual set, at c4c515ad) parked on sweep anchor #27377 §1, plus its fixture finding. (1) The registry gained label(record.f), a macro bindOptionLabel registers beside the permission predicates, and the docs table lists it, so the title, steps[0], steps[2], knownGaps[0], acceptance[0] (clause and evidence) and source[0]/[1] count 30, steps[2] adds the label() known-answer rows, and the receiver-only names outside the count are `can` and `hasCapability` (registerPermissionPredicate is now bindPermissionPredicate). (2) fixtures.requires[1] and steps[6]: a runtime object save needs a writable package and its namespace prefix as well as a sharingModel. Re-read at base 488b3ed0: packages/formula/src/stdlib.ts registerBoundStdLib and bindOptionLabel, packages/formula/src/validate.ts CEL_STDLIB_FUNCTIONS docblock ('30 added by registerStdLib'), content/docs/data-modeling/formulas.mdx, packages/metadata-protocol/src/protocol-authoring-gate.ts (NAMESPACE_PREFIX). The clause still fails on any count drift in either direction", "ref": "#27441" }
2284+
{ "revision": 6, "date": "2026-10-31", "change": "The function count is 30, not 29, and the scratch-object legs name their writable package. Re-point of the red row QA run #27441 (18.3.0 sweep, wave 3, manual set, at c4c515ad) parked on sweep anchor #27377 §1, plus its fixture finding. (1) The registry gained label(record.f), a macro bindOptionLabel registers beside the permission predicates, and the docs table lists it, so the title, steps[0], steps[2], knownGaps[0], acceptance[0] (clause and evidence) and source[0]/[1] count 30, steps[2] adds the label() known-answer rows, and the receiver-only names outside the count are `can` and `hasCapability` (registerPermissionPredicate is now bindPermissionPredicate). (2) fixtures.requires[1] and steps[6]: a runtime object save needs a writable package and its namespace prefix as well as a sharingModel. Re-read at base 488b3ed0: packages/formula/src/stdlib.ts registerBoundStdLib and bindOptionLabel, packages/formula/src/validate.ts CEL_STDLIB_FUNCTIONS docblock ('30 added by registerStdLib'), content/docs/data-modeling/formulas.mdx, packages/metadata-protocol/src/protocol-authoring-gate.ts (NAMESPACE_PREFIX). The clause still fails on any count drift in either direction", "ref": "#27441" },
2285+
{ "revision": 7, "date": "2026-11-01", "change": "QA run #28656 (W3-RT) finding: acceptance[5] and steps[6] named only publish-drafts as the draft's promotion door. The single-item POST /api/v1/meta/object/NAME/publish refuses too, 422 INVALID_METADATA with the same expression-invalid finding: #28656 read it live at 6e11a929, publishMetaItem's refusal is pinned in packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (the formula field verdict block, on a draft's promotion), and the route is meta.publishItem in packages/rest/src/rest-route-ledger.ts. Both now name the two promotion doors. The clause asks for both refusals, so it is stronger, never weaker. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); finding parked in anchor comment 6557904764", "ref": "#28656" }
22852286
]
22862287
},
22872288
{

0 commit comments

Comments
 (0)