refactor(cli): move the pages.* i18n parity verdict off the nav roster onto the page test, and translate the three platform page labels - #16505
Conversation
`User` / `Organization` / `Position` are page-level `label` members on `sys_user_detail`, `sys_organization_detail` and `sys_position_detail`. They rendered English in every locale: the pages author `regions: []`, so the extractor offers exactly one key each and nothing had a bundle entry. Adds `pages.*` entries in all four shipped locales. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
`check:app-nav-i18n` carried a second, unrelated assertion: that the default locale's `pages.*` section still says what the composed page metadata says. Its `CONTRIBUTORS` roster is a NAV roster — every entry must land a nav id, and each is booted to get them — so it was one roster serving two populations, and the two are not the same set. Measured: the booted composition holds four pages while the platform ships six. The verdict moves to `platform-page-i18n-parity.test.ts`, which reads its population from the plugins' UI bundles and the platform's `pages` barrel and needs no boot. Its three finding kinds and every red-shape control the gate ran under `--self-test` move with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…ges-parity-verdict-move
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0539a05b6c069a286bf7837578356282d2c4ae0f && git checkout 0539a05b6c069a286bf7837578356282d2c4ae0f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9bcd9bef8a8efc1d788c8607bdd3a6c94adf6d9d ac80f66caa802b2db7b6692be0460da31aa7e684 && git checkout -B drift-repro 9bcd9bef8a8efc1d788c8607bdd3a6c94adf6d9d && git merge --no-ff ac80f66caa802b2db7b6692be0460da31aa7e684
node scripts/docs-audit/affected-docs.mjs --json 9bcd9bef8a8efc1d788c8607bdd3a6c94adf6d9d |
Fixes #15743
Moves the
pages.*i18n parity verdict off the nav roster and onto the page test, then translates the three English platform page labels. Option B, ruled at PR #15739 (5549604828); option A stays refused.The measurement that justified the move, taken here rather than inherited
With the three bundle entries added and the gate untouched,
check:app-nav-i18nreturned exactly what the card predicted:pages.sys_position_detailverified clean, becauseplugin-securityis in the roster.Then the population itself, probed off the gate's own boot:
Four pages in the composition the gate boots; six shipped by the platform. That is the defect this card names — one roster silently serving two populations — measured rather than argued.
@objectstack/plugin-authcontributes the missing two and cannot join a NAV roster at all:new AuthPlugin({})refuses to boot without a secret, and its only nav contribution (nav_sso_providers) is conditional, so it can never satisfy the at-least-one-nav-id invariant the roster is built on.What moved
Out of
packages/cli/scripts/check-app-nav-i18n.mjs: thepages.*header section,DEFAULT_LOCALE,PAGE_HEADER_COMPONENT,pageSourceCopy,defaultLocalePageDrift,renderPageFinding, thepageAuthoredBymap, verdict bucket 3, its footer, its--self-testcases, and thepages.*clause of the OK line. The nav gate keeps nav. A pointer block records where the verdict went and why, and refuses option A by name.Into
packages/cli/test/platform-page-i18n-parity.test.ts: the same three finding kinds (drift/orphan/no-source), verbatim in behaviour, plus every red-shape control the gate ran under--self-test, now ordinary test cases. A verdict moved without them would be a verdict nobody has seen fail.The move is a coverage increase, not a trade: the test reads its population from the plugins' UI bundles and the
@objectstack/platform-objects/pagesbarrel — six pages, a strict superset of the four the roster could reach — and needs no boot, no credential and no nav id. It also gains the reverse direction the gate never had: a page in the population with nopages.*entry now reds, which is the exact shape of the debt below.The nine units
User/Organization/Positionare page-levellabels onsys_user_detail,sys_organization_detailandsys_position_detail. All three pages authorregions: [], so the shared walk offers exactly one key each; nothing else on them is bundle-addressable. Entries land in all four locale files (enmirrors the source literal, as that section already does for the plugin pages), and the recorded source hashes go in alongside — computed with the package's owncollectSourceHashes(en), never by hand, so a later edit to an English literal marks the translations stale instead of serving a translation of a string that no longer exists.The
es-ESterm isPosición, the spelling the bundle already uses (nav_positions: 'Posiciones', andPosiciónin the generated metadata-forms bundle) rather thanPuestos, which appears once in a page file.⛔
packages/spec/src/system/i18n-resolver.tsis untouched. The 45 authored labels underslots.*are fenced by the 2026-08-06 maintainer ruling recorded intranslation.zod.ts, and widening the walk here would reverse it in passing. Scope was 3 keys × 3 locales and stayed there.Two ownership pointers this change falsified, corrected in the same diff
platform-objects/src/apps/translations/source-hash.tsnamedcheck:app-nav-i18nas what holdspages.*in parity with its sources. It no longer does.platform-objects/scripts/i18n-extract.config.tslumpedpagesin withapps/dashboardsunder that same gate.Verification
en'ssys_organization_detaillabel and revertingzh-CN'ssys_user_detailto English turned three assertions red: the moved parity verdict, the non-English-label claim, and — unpredicted, and correct — the end-to-endtranslatePagecase.ablation-dist-preflightconfirmed the marker live in 6 built files before the run was read, and absent from all 66 after the restore rebuild. The restore leg is proven bygit diff HEADempty, not by an exit code.dispatch-gates.mjs --commandsrun and green, asserted against the script's own Reconciliation line total of 47 (34 by path + 8 by kind + 7 whole-tree, 2 reached both ways). The Artifact rosters block, which sits outside that total, run separately: 36 of 39 green; 3 NOT MEASURED and none of them a finding —check-partof-closing-keywordandcheck-single-claim-pathsprintNOT WIREDwithout PR context, andcheck:react-declaration-parityrefuses without an objectui SDUI manifest.check:i18n(the 10-package extract closure — now9 package(s) in sync),check:dual-build-cjs-loads(a full build), andcheck:type-check-debt, which OOM'd under--max-old-space-size=4096and passes at 6144, the CI-shaped ceiling the gate itself pins.@objectstack/platform-objects: 539 tests, 36 files, plustypecheck.@objectstack/cliunit tier: 2478 passed / 6 expected-fail across 182 files, plustypecheck.pnpm lintatac80f66caexits 0 over 6253 files, 0 errors and 0 warnings, measured from eslint's own--format jsonoutput.Scope notes
Clause ② grades no, from the delivered diff rather than from expectation. Nothing lands in
packages/spec/src/**and no symbol is newly exported. The three bundle entries are new keys on a published payload, but they widen no accept set:TranslationDataSchema.pagesis alreadyz.record(z.string(), strictObject(...)), and the only member key used,label, is already declared — so neither the accepted key set nor the accepted value set moved. Stated explicitly so the grading can be overturned if a reviewer reads it the other way.No out-of-scope findings were filed.
packages/cli/scripts/**andpackages/cli/test/**are not published surfaces (files: ['dist', 'README.md', 'CHANGELOG.md']), so the release is@objectstack/platform-objectsalone, atminor.🤖 Generated with Claude Code
https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
Generated by Claude Code