Skip to content

Commit f86106b

Browse files
docs(qa): wave-2 checklist-accuracy close-out, slice B — six areas (#27009)
Part of #25807 (wave-2 accuracy, slice B) Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:按 18.2.0 第二波五份测试记录,修正 ai、approvals、attachments-storage、i18n、platform-core、records-forms 六个领域共 35 条测试清单条目里的过时描述。只改文档,不动产品代码。 - 为什么改:这些条目的步骤、字段、错误码、界面文案或引用的测试文件已与当前代码不符(例如审批抽屉已移除、货币字段不再带 scale、动态摘要按读者语言渲染),下一轮运行会被误导或误判。 - 风险与回滚:只影响 QA 清单文本。三条断言随产品已变的行为改写(i18n.surface-matrix 的 a7/a10、records-forms.field-type-constraints 的 a2),旧版本的判定不再沿用。每条 revision +1、追加一行 history,回滚即 revert 本 PR。 - 先看:`areas/records-forms.json` 的 field-type-constraints(scale 从“已知缺口”改为“拒收、不舍入”);`areas/i18n.json` 的 surface-matrix(a10 改为读者语言,#26167);`areas/approvals.json` 的 quorum-m-of-n(抽屉改为请求记录页)。 - 两分钟验证:`node scripts/check-platform-checklist.mjs`,预期打印 `check-platform-checklist: OK`、退出码 0。 - 需要你做的:无,席位落地。 ## What changes The slice-B 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 read against the code at base `fd246c22` before its edit; each item's new history row names the files read. - **35 items** in `ai` (5), `approvals` (7), `attachments-storage` (8), `i18n` (2), `platform-core` (9) and `records-forms` (4): `revision` +1 from main's value and one appended `history` row each, citing the run record that found it (`ref` is the anchor #25807, as slice A's rows are). No existing history row is changed; the previous last row only gains the comma the appended row needs. - **Three clauses change meaning with the product**, and say so in their history rows (a verdict for an older revision does not carry): `i18n.surface-matrix` acceptance[7] (the served document is fifteen groups, not twelve) and acceptance[10] (activity summaries render in the reader's locale since #26167, not the write locale); `records-forms.field-type-constraints` acceptance[2] (scale is enforced by rejection, `max_scale`, and currency carries no scale; it was recorded as an unenforced gap). - `attachments-storage.upload-limits-scope-and-ownership` acceptance[3] now scores pass from a pin the ref did not name (`storage-upload-door-ownership.wall.test.ts`, HTTP over a booted wall); `attachments-storage.chunked-upload-integrity` knownGaps[0] is closed for its pin half by `storage-upload-organization-change.wall.test.ts`. - No pin upgrades of the `Parked-on: #25807` partials. No `studio-authoring`, `access-security`, slice-A area file or `RUNNER.md` is touched. | item | before | after | source record | what | |---|---|---|---|---| | ai.mcp-http-surface | 6 | 7 | #26468 | automated.ref and variants[2] corrected | | ai.mcp-stdio-fail-closed | 7 | 8 | #26468, #26481 | steps[4] waits for the HTTP listener, and automated.ref names two corroborating files | | ai.mcp-run-action-exposure-gate | 4 | 5 | #26476 | acceptance[1].verify says which half the stock fixture discriminates | | ai.mcp-validate-expression | 4 | 5 | #26468 | variants[2] and automated.ref corrected | | ai.mcp-oauth-private-host-transport | 4 | 5 | #26468 | fixtures.requires[0] and steps[6] name the base-URL door and the no-private-address case | | approvals.quorum-m-of-n | 7 | 8 | #26456 | steps[8] and acceptance[4] read the request page, not the retired drawer, and fixtures.requires[1] names the admin's double role | | approvals.inbox-metadata-actions | 6 | 7 | #26456 | acceptance[1].verify names the override label | | approvals.decision-only-via-service | 3 | 4 | #26476 | steps[5] names the doors that pause and resume the contrast run | | approvals.account-app-entry | 5 | 6 | #26481 | fixtures.requires[2] says the invoice's account must be active, and acceptance[3] names the door for the appended row | | approvals.approver-resolution-matrix | 7 | 8 | #26476 | the auto_approve marker names a real field | | approvals.status-mirror-field | 4 | 5 | #26468 | automated.ref names three corroborating pins | | approvals.act-door-and-admin-rescue-slate | 4 | 5 | #26468 | automated.ref says which 404 arm is pinned | | attachments-storage.presigned-upload-roundtrip | 2 | 3 | #26468 | automated.ref names a corroborating pin | | attachments-storage.download-authz-both-sides | 8 | 9 | #26468 | automated.ref names the component pin of clause 4 | | attachments-storage.sys-file-status-pipeline | 4 | 5 | #26468 | automated.ref gains attachment-lifecycle.test.ts | | attachments-storage.upload-session-abort | 5 | 6 | #26468 | automated.ref names two more storage-routes cases and the chunked dogfood file | | attachments-storage.inline-grid-receipt-cells | 4 | 5 | #26468 | steps[0] names the base URL | | attachments-storage.field-accept-maxsize-server-enforced | 6 | 7 | #26476 | steps[1](B), steps[5] and negative[3] corrected | | attachments-storage.upload-limits-scope-and-ownership | 2 | 3 | #26468 | automated.ref names two HTTP pins and rescores acceptance[3] | | attachments-storage.chunked-upload-integrity | 7 | 8 | #26468 | knownGaps[0], acceptance[0] and automated.ref follow the chunked-door wall pin | | i18n.surface-matrix | 11 | 12 | #26450 | acceptance[7], acceptance[10], steps[17] and the appTranslationDataShape source line corrected | | i18n.strict-translation-key-rejection | 3 | 4 | #26456 | steps[3] names the out-of-tree route | | platform-core.boot-health | 11 | 12 | #26481 | steps[3] scopes the ⚠ read past the compile | | platform-core.seed-integrity | 5 | 6 | #26481 | acceptance[1].verify names the masked field | | platform-core.builtin-apps-nav-render | 8 | 9 | #26481 | fixtures.knownGaps[2] and steps[9] corrected | | platform-core.settings-hub-roundtrip | 5 | 6 | #26456 | steps[9], steps[10], steps[13], acceptance[9].verify and negative[1] corrected | | platform-core.manifest-install-contract | 6 | 7 | #26476 | acceptance[1] names the protocolVersion field, and the derivation clause names the 20-character cut | | platform-core.marketplace-install-local-lifecycle | 8 | 9 | #26476 | acceptance[5] names the refusal code, and the member persona says how to prove it | | platform-core.runtime-config-boot-read | 4 | 5 | #26476 | steps[1] says why cloudUrl is empty on the showcase | | platform-core.seed-mode-matrix | 5 | 6 | #26476 | fixtures.requires[0] names the earlier refusal | | platform-core.v18-boot-refusals | 7 | 8 | #26481 | acceptance[0].verify, steps[1] and fixtures.requires[1] corrected | | records-forms.crud-roundtrip | 14 | 15 | #26481 | steps[0], steps[1], steps[4], acceptance[4].verify and one source line corrected | | records-forms.form-view-gallery | 4 | 5 | #26456 | steps[6] says how to reach the page form | | records-forms.field-type-constraints | 4 | 5 | #26450 | acceptance[2], negative[3], steps[3], fixtures.requires[0], knownGaps[0] and one source line follow the enforced scale contract | | records-forms.field-group-visible-when | 9 | 10 | #26456 | acceptance[3] says when the detail page re-gates | ### No change, because … - `ai.skill-instructions-mcp-prompts`, `approvals.email-action-token-door`, `attachments-storage.read-inherits-parent-rls`, `attachments-storage.attach-requires-parent-edit`, `attachments-storage.orphan-tombstone-reap`: the run's line is "ref accurate" (a confirmation); re-read, still true. - `ai.mcp-coded-refusals-and-runtime-exposure`: a confirmation that revision 3 already names both files; both exist. - `approvals.request-page-reply-and-reader-visibility`: ref accurate; the extra service-unit comment cases corroborate acceptance[1] below its layer only. - `approvals.decision-action-matrix` A1: an observation (a skipped exec_review step is logged); the run says the discriminator holds. - `approvals.account-app-entry` acceptance[0] "the runner builds the console itself": that is the clause's design. - `approvals.approver-resolution-matrix` "administrators are identified by sys_member role": the item identifies them by their grants, as `approval-service.ts` organizationAdministrators does; the membership role is only the grant's provisioning source. - `attachments-storage.field-accept-maxsize-server-enforced` recipe line (`media_qa_media`): already closed by revision 6 (#26876, `qamedia_media`). The item's other three lines are edited above. - `records-forms.action-location-matrix` ("authoring refuses a throwing visible"): already in knownGaps[2] since revision 16 (#26625), which cites #26456. - `records-forms.record-discussion-mentions` (`qa_nofeeds` → 422 NAMESPACE_PREFIX): already renamed `qafeeds_nofeeds` (revision 11). - `records-forms.field-group-visible-when` namespace line: steps[1] already declares namespace `qagroup` and names `qagroup_group_gate`. Its other line is edited above. - `platform-core.boot-health` "no default-level line names the Console mount path": no clause reads a mount line; acceptance[5] reads `GET /_console/`. - `ai.mcp-oauth-private-host-transport` "missing clause: AS metadata issuer equals authorization_servers[0]": that is the open product defect #26472; its fix owns the clause. - The other items the sources name in these areas (`approvals.per-group-signoff`, `i18n.notification-localized-and-clears`, `i18n.flow-copy-translation-and-host-override`, `platform-core` console-login, nav-surfaces-render, metadata-registry-serving, metadata-authoring-roundtrip, interrupted-migration-boot-report, activation-ledger-registration-home, activation-ledger-row-contract, url-overlay-contract, lifecycle-retention-sweep, settings-audit-secret-fingerprint, protocol-18-handshake-and-stored-replay, settings-organization-isolation-and-platform-rung, serve-composition-v18, and the remaining `records-forms` items of #26450): named only with clause verdicts or parked partial coverage, no accuracy line. #26481 re-checked every wave-1 line on its P0 items and found them fixed. - **Gaps, not edited (need a pin or a run, not text):** `approvals.decision-only-via-service` has no pin and #26468 carried it NOT-RUN; `records-forms.record-edit-undo` is carried BLOCKED (no packaged undoable update action on the showcase). ## Tests No product code changed, so no package test is owed. `node scripts/check-platform-checklist.mjs` exits 0 at the head (`check-platform-checklist: OK — 16 areas, 329 items (324 active, 1 planned)`). The edits were applied by a script that rebuilds each file from base text and asserts, per area, that only the 35 items differ, each at base revision + 1 with exactly one appended history row and every earlier row equal. ## Acceptance notes - `approvals`: four items not named by any wave-2 source still describe the retired request drawer (viewer-gating-submitter-side, notification-deep-link, dynamic-approver-routing, inbox-keyboard-flow). Rewriting them is not mechanical (inbox-keyboard-flow's drawer walk may describe removed behaviour), so they are reported for the anchor, not edited here. - `approvals.approver-resolution-matrix` steps[6] / acceptance[5] still describe `queue` as a dead slot (#3508), while `approval-service.ts` now expands a queue to its members (ADR-0142 D6) and only warns when it expands to nobody. Not named by a wave-2 source; reported for the anchor. ## Verification <details><summary>Derived gates at afa60d7 (dispatch-gates --commands, then --ran)</summary> 16 derived commands, each exit captured before any pipe: check-adr-links 0, --self-test 0, check-closing-keyword-parity 0, --self-test 0, check-comment-mask-corpus 0, plan-test-core-shards --self-test 0, check:doc-formula-expressions 0 (first run exit 3 PREREQUISITE NOT MET; re-run after a locked turbo build of @objectstack/formula and @objectstack/lint, VERDICT command-exit 0), check:ci-filter-parity 0, check:cross-package-test-inputs 0, check:doc-authoring 0, check:driver-memory-census 0, check:gitlink-declared 0, check:nul-bytes 0, check:platform-checklist 0, check:refd-timer-probe 0, check:watch-hint-literal 0. `--ran`: 16 derived families accounted for, 16 run, 0 NOT-MEASURED. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QSeKYkzo6HfVywHEmtRgH2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c6bc269 commit f86106b

6 files changed

Lines changed: 329 additions & 119 deletions

File tree

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

Lines changed: 49 additions & 19 deletions
Large diffs are not rendered by default.

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

Lines changed: 64 additions & 22 deletions
Large diffs are not rendered by default.

‎docs/qa/platform-checklist/areas/attachments-storage.json‎

Lines changed: 69 additions & 21 deletions
Large diffs are not rendered by default.

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

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@
179179
"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",
180180
"since": "v15",
181181
"status": "active",
182-
"revision": 11,
182+
"revision": 12,
183183
"priority": "P1",
184184
"surface": "browser",
185185
"personas": ["admin on a zh-CN session"],
@@ -214,7 +214,7 @@
214214
"GET /api/v1/i18n/translations/zh-CN — record whether it is non-empty and whether every key path sits under a declared translation group",
215215
"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",
216216
"unknown-locale probe: GET /api/v1/i18n/translations/zz-ZZ and GET /api/v1/i18n/labels/showcase_task/zz-ZZ — capture WHICH degradation the running server does (the FileI18nAdapter returns an empty {} map for an unloaded locale; getTranslations does NOT synthesize the default bundle — record the observed shape, do not assume a fallback)",
217-
"feed/audit verb variant (E11): set the TENANT locale to zh-CN first (PUT /api/settings/localization {values:{locale:'zh-CN'}}), THEN trigger an activity-feed / audit entry (an assignment or a record edit) and screenshot its rendered verb/template — the verb is localized and frozen at write time, so an entry created before the flip stays in its original locale no matter which session reads it",
217+
"feed verb variant (E11): with the tenant locale left at its default, trigger an activity-feed entry (an assignment or a record edit), then read the record's feed as a zh-CN reader (the user's language set to zh-CN, which the console sends as Accept-Language) and screenshot its rendered verb/template. Since #26167 a sys_activity row stores its summary as a code beside the English sentence, and a read with a locale re-composes it in the READER's language, so an entry written before any switch renders zh-CN too (packages/plugins/plugin-audit/src/activity-summary-message.ts)",
218218
"record a per-variant verdict table with the screenshot evidence"
219219
],
220220
"acceptance": [
@@ -261,7 +261,7 @@
261261
"evidence": "the /i18n/locales response"
262262
},
263263
{
264-
"clause": "GET /i18n/translations/:locale returns a non-empty bundle whose key paths all sit within the declared translation-group vocabulary (PlatformTranslationDataSchema — the SERVED document is the merged tree, so it is the twelve-group platform face — the eleven per-app groups plus the platform-only `settings` — and not the eleven-group per-app one) — a key outside the declared groups, or a raw dotted key handed to a consumer, is the silent-strip class",
264+
"clause": "GET /i18n/translations/:locale returns a non-empty bundle whose key paths all sit within the declared translation-group vocabulary (ServedTranslationDataSchema — the SERVED document is the merged tree's platform face, PlatformTranslationDataSchema's sixteen groups (the fifteen per-app groups plus the platform-only `settings`), less metadataForms, the one server-resolved group (SERVER_RESOLVED_TRANSLATION_GROUPS), so fifteen groups) — a key outside the declared groups, or a raw dotted key handed to a consumer, is the silent-strip class",
265265
"oracle": "api",
266266
"verify": "translations for zh-CN is non-empty; spot-check that sampled keys resolve under objects/_views/_actions/_sections/apps.navigation/dashboards/pages/settings groups, not a fourth dialect",
267267
"evidence": "the /translations/zh-CN response + the key check"
@@ -279,9 +279,9 @@
279279
"evidence": "the two unknown-locale responses"
280280
},
281281
{
282-
"clause": "the activity-feed / audit verb template localizes on the WRITE locale (E11) — the stored verb renders its zh-CN copy, not an English verb and not a raw key. ⚠️ It does NOT key on the reader's session: plugin-audit/src/audit-writers.ts renders the summary ONCE at write time via resolveWriteLocale(tenantId, userId) and STORES the string, with an EN literal as the fallback. Switching only the browser session locale therefore changes nothing, and reading that as a broken resolver is a false FAIL",
282+
"clause": "the activity-feed verb template localizes in the READER's locale (E11, since #26167) — a zh-CN reader sees the zh-CN copy, not an English verb and not a raw key, whatever locale the row was written under. plugin-audit stores each sys_activity summary as a code (ACTIVITY_SUMMARY_MESSAGE_KEY in the row's metadata) beside the English sentence, and the read middleware (installActivitySummaryLocale) re-composes it in the request's locale: Accept-Language, else the localization settings",
283283
"oracle": "screenshot",
284-
"verify": "flip the TENANT locale (PUT /api/settings/localization {values:{locale:'zh-CN'}}), then perform the write; the newly stored entry's verb renders zh-CN. An entry written BEFORE the flip keeps its original string by design — re-reading it in a zh-CN session proves nothing. A raw key or an English verb on a post-flip write is the E11 gap (record which group backs it — the messages/globalActions area is the likely home)",
284+
"verify": "write the entry with the tenant locale at its default, then read the feed as a zh-CN reader: the verb renders zh-CN; read the same row as an en reader: it renders English. A raw key, or an English verb for the zh-CN reader, is the E11 gap (record which group backs it). A row written before #26167 carries no code and keeps its stored sentence, so score only rows written on the run's boot",
285285
"evidence": "the feed/audit screenshot"
286286
}
287287
],
@@ -313,7 +313,7 @@
313313
"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 (`pnpm --filter @object-ui/components exec vitest run --maxWorkers=2 FILE`, and the same under @object-ui/plugin-detail) 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"
314314
},
315315
"source": [
316-
"packages/spec/src/system/translation.zod.ts#appTranslationDataShape (appTranslationDataShape — the eleven-group vocabulary an APPLICATION may author: objects (with _views/_actions/_sections), picklists, apps (navigation), messages, globalActions, dashboards, datasets, pages, flows, metadataForms, settingsCommon. `settings` is platform-only since #15178 and lives in platformSettingsShape, spread into PlatformTranslationDataSchema only — TranslationItemSchema refuses it too since #19620)",
316+
"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)",
317317
"examples/app-showcase/src/system/translations/index.ts (full-column coverage rationale)",
318318
"packages/services/service-i18n/src/i18n-service-plugin.ts#i18n (GET /i18n/locales | /translations/:locale | /labels/:object/:locale; { success, data } envelope #3636/#3675; resolveObjectFieldLabels nested shape #3778/#3833; the plugin mount and the dispatcher /i18n domain serve the same routes interchangeably)",
319319
"packages/services/service-i18n/src/file-i18n-adapter.ts#getLocales (getLocales / getTranslations — unloaded locale → {}; fallbackLocale applies per-KEY in t(), not to the bulk route)",
@@ -336,15 +336,21 @@
336336
{ "revision": 8, "date": "2026-10-16", "change": "variants[5], acceptance[7].clause, steps[8] and [9], source[0] and automated.ref corrected (checklist-accuracy note 4 and the fixture gaps of QA run #23951, wave #23878). variants[5] still marked the view-empty-state variant blocked(fixture), contradicting knownGaps[1] and steps[6] — it ran and passed. acceptance[7] counted the served tree as an eleven-group platform face over a ten-group per-app one; the spec declares eleven per-app groups (appTranslationDataShape, picklists included — source[0]'s list had dropped it) and twelve platform groups (plus settings). The dashboard and page steps now name a translated specimen each, because no showcase page carries a translated title (the run scored pages on platform page connect_agent). automated.ref's revision note moves to the current pin. Wording and specimen names only: no request or oracle changed", "ref": "#24036" },
337337
{ "revision": 9, "date": "2026-10-19", "change": "acceptance[2].verify says how to find an untranslated key — checklist-accuracy finding of the 18.0 QA sweep (QA run #24553, i18n.surface-matrix; wave anchor #24579 §3): the coverage baseline debt is a count, not a key list. Re-derived at base 9a7838edf5 (scripts/i18n-coverage-baseline.json maps each config path to a number). No clause changed", "ref": "#24649" },
338338
{ "revision": 10, "date": "2026-10-26", "change": "automated.ref corrected (checklist-accuracy findings a5 and a6/a7/a9 of QA run #25815, release-18.2.0-tier1 at 6679644a; wave anchor #25807), with the retired objectui-pin wording replaced (#26035 left this file to its card). a5: the two objectui pins cover the confirm copy and the successMessage handed to the toast, under stubbed action, confirm and toast handlers with no server round-trip, so the network-oracle clause stays the run's. a6/a7/a9: the i18n routes have HTTP coverage only as the anonymous-deny suite's member 200 control, which asserts none of the locale set, the group vocabulary or the zz-ZZ shape these clauses name. The pins now resolve under ui/ in this checkout, not in an objectui checkout at a .objectui-sha revision. No clause is loosened", "ref": "#25842" },
339-
{ "revision": 11, "date": "2026-10-27", "change": "fixtures.knownGaps[3]: the CONFIRM half is scored from the unit pins under ui/ that automated names, not 'the objectui pins' (#26626, maintainer-directed). No clause changed, no status changed", "ref": "#26626" }
339+
{ "revision": 11, "date": "2026-10-27", "change": "fixtures.knownGaps[3]: the CONFIRM half is scored from the unit pins under ui/ that automated names, not 'the objectui pins' (#26626, maintainer-directed). No clause changed, no status changed", "ref": "#26626" },
340+
{
341+
"revision": 12,
342+
"date": "2026-10-28",
343+
"change": "acceptance[7], acceptance[10], steps[17] and the appTranslationDataShape source line corrected. Checklist-accuracy findings of the 18.2.0 wave 2 (QA run #26450, Tier 1 half B, at 756220c4; wave anchor #25807, slice B). (1) acceptance[7] called the served document the twelve-group face; the served document is ServedTranslationDataSchema, the platform face's sixteen groups less the server-resolved metadataForms, so fifteen, and the per-app vocabulary is fifteen groups (reports, positions, permissions and books were missing from the source line). (2) acceptance[10] and steps[17] pinned the pre-#26167 write-locale premise; since #26167 an activity summary is stored as a code and re-composed in the reader's locale, so the clause now scores the reader's language and drops the tenant-locale flip. Re-read at base fd246c22: packages/spec/src/system/translation.zod.ts (appTranslationDataShape, 15 keys; PlatformTranslationDataSchema; SERVER_RESOLVED_TRANSLATION_GROUPS; ServedTranslationDataSchema), packages/plugins/plugin-audit/src/activity-summary-message.ts and activity-summary-reader-locale.integration.test.ts. acceptance[10]'s clause changed meaning with the product, so a verdict for revision 11 or older does not carry to it. The run's n1 FAIL was a product defect, extracted as #26452 (closed); no edit here",
344+
"ref": "#25807"
345+
}
340346
]
341347
},
342348
{
343349
"id": "i18n.strict-translation-key-rejection",
344350
"title": "Unknown, legacy-dialect, and retired translation keys are rejected loudly at BOTH authoring doors — never silently stripped",
345351
"since": "v17",
346352
"status": "active",
347-
"revision": 3,
353+
"revision": 4,
348354
"priority": "P1",
349355
"surface": "mixed",
350356
"personas": ["admin"],
@@ -358,7 +364,7 @@
358364
"item door — via the metadata API, attempt to save a zh-CN `translation` item carrying each variant in turn (one save per variant), always alongside one VALID key (e.g. objects.showcase_task.label) so the rejection provably targets the bad key, not the document. Name the item in snake_case (e.g. qa_strict_zh_cn) and set `locale: zh-CN`: the conventional name `zh-CN` is refused first by the item-name rule (400 INVALID_REQUEST, 'is not a legal metadata item name'), before any key is judged, so a run that keeps it never reaches the gate this item tests. The `name` describe in translation.zod.ts still calls the locale code the convention; do not follow it at this door",
359365
"capture the exact error text for each attempted save",
360366
"after each rejection, read the item back and record that nothing (including the valid key) was persisted by the failed save",
361-
"bundle door — in a scratch script inside the worktree, call defineTranslation and defineTranslationBundle with the same variants and capture each parse error (this is the door #3778's item-only guard missed; #4001's strict shapes must cover both). ⚠️ They are NOT on the same entry point: defineTranslationBundle is re-exported from the package ROOT (@objectstack/spec), while defineTranslation resolves only through the @objectstack/spec/system subpath. Importing both from the root yields 'defineTranslation is not a function', which reads exactly like a missing guard",
367+
"bundle door — in a scratch script inside the worktree (a runner barred from writing in the tree can run the script from a scratch dir and require the built packages/spec/dist entry points by absolute path, after a spec build), call defineTranslation and defineTranslationBundle with the same variants and capture each parse error (this is the door #3778's item-only guard missed; #4001's strict shapes must cover both). ⚠️ They are NOT on the same entry point: defineTranslationBundle is re-exported from the package ROOT (@objectstack/spec), while defineTranslation resolves only through the @objectstack/spec/system subpath. Importing both from the root yields 'defineTranslation is not a function', which reads exactly like a missing guard",
362368
"verify the alias suggestion channel: a near-miss key (helpText on a FIELD translation, label on a WIDGET translation) must be rejected WITH the did-you-mean pointing at the declared spelling (help; title)",
363369
"verify the guidance channel: legacy object-first keys (o, app, nav, dashboard, errors, _meta) and retired validationMessages must carry their prescription (where the content belongs now, or that it has no home), not just 'unrecognized key'",
364370
"save a correct version of the same content and confirm it persists and resolves (the gate rejects keys, not translations)"
@@ -425,7 +431,13 @@
425431
"history": [
426432
{ "revision": 1, "date": "2026-08-07", "change": "new item: strict-key rejection at both translation doors, variants from the spec's own legacy/guidance/alias tables", "ref": "claude/platform-test-checklist-ocwugl" },
427433
{ "revision": 2, "date": "2026-08-18", "change": "named the two bundle-door entry points separately in step 4. packages/spec/src/index.ts re-exports only defineTranslationBundle; defineTranslation is reachable solely through the @objectstack/spec/system subpath (system/index.ts re-exports translation.zod). A scratch script importing both from the root fails with 'defineTranslation is not a function' and reads as an absent guard rather than a wrong import path (#9467 CF-3)", "ref": "#9386" },
428-
{ "revision": 3, "date": "2026-10-26", "change": "steps[0], acceptance[0].verify, acceptance[3].verify and acceptance[5].verify corrected (checklist-accuracy findings of QA run #25817, release-18.2.0-manual-set at 6679644a; wave anchor #25807). The conventional item name `zh-CN` is refused by the item-name rule (400 INVALID_REQUEST) before the key gate runs, so the steps name a snake_case item and the clause scores the 422 INVALID_METADATA. The live validationMessages guidance cites ADR-0049 and spec 17.0.0, not #4667. The singular label renders on the record-page badge and the New dialog, while the list header and the nav read pluralLabel. The stale `name` describe in translation.zod.ts that still calls `zh-CN` the convention is a spec text, not ledger text: it goes to the seat as a finding, and steps[0] only warns the runner", "ref": "#25842" }
434+
{ "revision": 3, "date": "2026-10-26", "change": "steps[0], acceptance[0].verify, acceptance[3].verify and acceptance[5].verify corrected (checklist-accuracy findings of QA run #25817, release-18.2.0-manual-set at 6679644a; wave anchor #25807). The conventional item name `zh-CN` is refused by the item-name rule (400 INVALID_REQUEST) before the key gate runs, so the steps name a snake_case item and the clause scores the 422 INVALID_METADATA. The live validationMessages guidance cites ADR-0049 and spec 17.0.0, not #4667. The singular label renders on the record-page badge and the New dialog, while the list header and the nav read pluralLabel. The stale `name` describe in translation.zod.ts that still calls `zh-CN` the convention is a spec text, not ledger text: it goes to the seat as a finding, and steps[0] only warns the runner", "ref": "#25842" },
435+
{
436+
"revision": 4,
437+
"date": "2026-10-28",
438+
"change": "steps[3] names the out-of-tree route. Checklist-accuracy finding of the 18.2.0 wave 2 (QA run #26456, manual browser set, at 756220c4; wave anchor #25807, slice B): a runner barred from writing in the tree drove the bundle door by requiring packages/spec/dist by absolute path from a scratch dir. The step said only 'inside the worktree'. The entry-point warning (defineTranslation through the system subpath) is unchanged. No clause changed",
439+
"ref": "#25807"
440+
}
429441
]
430442
},
431443
{

0 commit comments

Comments
 (0)