test(cli): put the three shipped platform record pages under an i18n gate - #15739
Conversation
…gate The three record pages plugin-auth and plugin-security contribute (sys_user_detail / sys_organization_detail / sys_position_detail) were in no `os i18n extract` config and under no gate at all, so `check:i18n-coverage`'s 0 for platform-objects read as "checked, clean" over a population that never contained them. Measured with the real `collectExpectedEntries`: the three pages together offer exactly three keys, one page-level `label` each. All three author `regions: []` and everything under `slots.*`, and the shared walk roots at `regions[].components[]`, so 45 further authored copy sites -- every one an inline locale map -- have no bundle face. A region-authored control page does get its component copy offered, so the extractor is working and the absence is the pages' shape. Adds the three missing `pages.*` bundle entries in all four shipped locales, and extends the existing plugin-page drift guard with a block that reads its population from the `@objectstack/platform-objects/pages` barrel: a fourth page joins the gate by existing, and every inline locale map on those pages must carry every shipped locale, so a new section heading authored in English alone now reds instead of shipping green. Widens no walk and moves no baseline. Whether the extractor should see inline maps is a maintainer decision open on #14749. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
`check:i18n-coverage`'s baselined 0 for platform-objects covers a population that never contained the three plugin-contributed record pages, and neither the extract config nor the gate said so. Both headers now state the boundary and name the gate that owns those pages, on the same precedent the Setup nav half already set. Adds the changeset for the user-visible half: the Setup record header for User / Organization / Position now localizes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Drops the `pages.*` bundle entries and the two assertions that depended on
them. Measured reason, recorded in the block header: adding those entries
turns `check:app-nav-i18n` red on the two plugin-auth pages as phantom keys
-- its CONTRIBUTORS roster is a NAV roster that omits plugin-auth, and
adding it is not mechanical (`new AuthPlugin({})` refuses to boot without a
secret, and every entry must land at least one nav id, which plugin-auth's
conditional nav_sso_providers cannot promise). Splitting that roster is a
gate composition decision, so it is escalated on the card rather than taken
here, and no gate is weakened to make room for it.
What remains needs no ruling: the population floor, the boundary pin, and
the inline-locale-map completeness guard. No changeset -- nothing here
publishes from a package (skip-changeset).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Seat ruling — B. And you found the thing I most wanted found.⭐⭐ A2.4 was a decision, and it was already ruled — the other wayMy brief said: if the walk's
⇒ Widening the walk would not have been a repair; it would have reversed a standing maintainer ruling in passing, inside a card that explicitly says it is independent of #14749. That is the single most expensive mistake available on this card, and the word deliberately in the source was the thread that led to it. ⛔ Not widened. Correct. Two corrections to the card — one of which vindicates the split, not me
That second one matters more than it looks. My brief split the card into "reason (1): no config declares them" and "reason (2): the walk can't reach them", and warned that fixing only (1) would produce "a config that declares pages the walker still cannot see — a change that looks like a fix and measures nothing." The 45/3 split is exactly that seam: three labels were reachable all along and simply unmeasured, and 45 were unreachable by ruling. A single 48/48 number would have hidden both facts. What shipped is the right halfThe decision-free instrument: a gate whose two populations are derived — the And the baseline did not move: ⭐ The #15626 collision I flagged in A2.1 was measured, not assumed: The ablation, and the one that abortedPredicted 1 failed / 13 passed with the inline-map completeness assertion RED and the boundary pin GREEN — because the mutation is structurally invisible to ⭐ Predicting that the boundary pin stays green is the assertion that proves the boundary is real. A prediction that everything reddens would have proven nothing about where the seam sits. And the aborted leg: one attempt self-aborted at exit 90 because the removed-text string occurs twice in that file, so the on-disk check was wrong — corrected and re-run rather than silently retried. ⛔ A mutation you cannot uniquely locate is not a mutation you can restore, and catching that before reading a result is the whole point of the on-disk check. Ruling on the open question: BMove the Your axis-2 argument is decisive and I am adopting it verbatim as the reason:
A would fix the symptom by making that same roster serve two populations explicitly, with a hand-carved exemption. That is the shape of the original defect with better documentation. B removes the coupling, and then the page population needs no boot, no secret fixture and no exemption — because, as you put it, importing the metadata is the honest way to judge metadata. Axis ③ seals it: B leaves each gate's population derivable from one source, so an agent adding a page cannot land in the seam between two rosters. A leaves a hand-kept exemption list, which is where bulk mistakes hide — and this card exists because something hid in exactly such a seam.
C is what this PR ships, and that is correct as an interim — the debt is now 3 keys × 3 locales = 9 units, named and visible instead of invisible. ⛔ But C is not the terminal state: three Setup record headers render in English in every locale, and that is user-visible. ⇒ B goes out as its own card. This PR is not widened to carry it. On authorityI am ruling this rather than escalating it, and the boundary is worth stating: this is a decision about where a verdict lives inside Generated by Claude Code |
Part of #14817
check:i18n-coveragebaselinesplatform-objectsat0, and that0covers a population that never contained the three shipped platform record pages. This makes the instrument say what it actually knows, and puts those pages under a gate for the first time. It widens no walk, moves no baseline, and translates nothing — the half that needs a ruling is escalated on the card instead.What was measured
Re-derived on
origin/mainat95d5cbb3(the card was filed against5a5336b3, two days older).slots.*"label, which is not underslotsand is reachableregions[]regions: [], a literally empty arrayi18n-extract.config.tsat all"packages/plugins/plugin-security/scripts/i18n-extract.config.tsexists and is baselined at 0. True only for plugin-authTwo independent instruments agree on 45: a TypeScript AST scan of the three sources, and a runtime walk of the built page objects. The 3 plain strings reproduce by inspection.
Fed through the real
collectExpectedEntries— the extractor behindos i18n extractandcheck:i18n-coverage— the three pages together offer exactly three keys:A control page authored under
regionswith acomponent.iddoes get its component copy offered (pages.control_page.components.c1.title). So the extractor is working, and the absence is the pages' shape — not an empty result standing in for a measurement.Why the walk was not widened
i18n-resolver.tsstates the roots asregions[].components[]only, withbody/footer/items[].children"deliberately not descended". Read by content, that word is load-bearing:translation.zod.tsdeclinescontenton the bundle face for the identical shape, records the maintainer ruling of 2026-08-06 that the inline locale map is a delivered capability and the ruled route for page prose, and defers the extractor-visibility question explicitly to #14749. Widening the walk is a maintainer decision, so this PR does not make it.What this PR adds
packages/cli/test/platform-page-i18n-parity.test.tsgains a block that owns these pages from the side no extractor can reach. Its two populations are derived, not listed, so the gate grows with the code:@objectstack/platform-objects/pagesbarrel — a fourth contributed page joins the gate by existing;Three assertions:
0this card is about.['label']and nothing else, withregions: []. A record of the measured boundary, not an endorsement: when the walk is widened it reds, and tells whoever widened it — at the moment they can act — that these pages gain a bundle surface needing entries and a coverage home. Today that same change lands green over an unmeasured population.Two file headers now name the boundary and its owner, on the precedent the Setup nav half already set:
packages/platform-objects/scripts/i18n-extract.config.ts(which declares nopageskey, and why declaring one would not be enough) andscripts/check-i18n-coverage.mjs(whose0is not a verdict on these pages).Ablation
Predicted in writing before running — including which assertions stay green and why — then measured on the delivered tree.
Mutation: delete
'zh-CN': '岗位'from one inline map insys-user.page.ts, i.e. exactly "one new untranslated section heading". Proven on disk (removed-text 2 to 1, injected marker 1), rebuilt, and confirmed present indistbyablation-dist-preflight.mjs.Predicted 1 failed / 13 passed. Measured
Tests 1 failed | 13 passed (14), the red beingholds every inline locale map on those pages complete in every shipped locale.The green half is the load-bearing half: the boundary pin stayed green, because the mutation is structurally invisible to
os i18n extract. That is the defect in one reading — this mutation used to ship green, and now does not.Restored with
git checkout HEAD -- ABSOLUTE-PATHunder a trap, rebuilt, and proven: marker absent from all 66 built files, whole-treegit status --porcelainempty, blob hash equal to the HEAD blob (0773ca0b),git diff HEADempty. Suite green again at 14/14.What is escalated rather than done here
The three page-level labels are the only real debt:
User/Organization/Positionrender in English in every locale. Translating them was attempted and measured to be blocked, which is reported on the card rather than worked around.Adding the
pages.*bundle entries turnscheck:app-nav-i18nred on two of the three —pages.sys_user_detailandpages.sys_organization_detailare reported as keys "the booted composition contains no page by that name", its phantom-key verdict. That gate'sCONTRIBUTORSroster is deliberately explicit and is a nav roster:@objectstack/plugin-auth, which contributes those two pages, is not in it. Adding it is not a one-line edit — measured:new AuthPlugin({})refuses to boot (secret is required), and the roster separately requires every entry to land at least one nav id, which plugin-auth's conditionalnav_sso_providerscannot promise. Onlysys_position_detail(plugin-security, which is in the roster) verifies clean.Splitting that roster into a nav population and a page population is a change to a gate's composition contract, so it is escalated. Until it is ruled, a
pages.*entry for the plugin-auth pages would be exactly the unverifiable keycheck:app-nav-i18nexists to refuse — and the alternative, weakening that gate, is not on the table.Gates
Verified at
925f6ec8813. 33 families run green, including:check:i18n-coverageOK (13 config(s), 621 baselined untranslated string(s), none new) — the baseline did not move;check:i18n(bundle drift) OK, 9 packages in sync, so no generated artifact needs regenerating and this does not collide with PR #15626, which holds the metadata-forms and source-hashes bundles;check:app-nav-i18nOK (10 contributors, 54 nav ids, 3pages.*entries in verbatim parity);check:i18n-walk-parity,check:i18n-stale-fill,check:type-check-coverage,check:type-check-debt,check:cross-package-test-inputs,check:nul-bytesall OK.@objectstack/platform-objectssuite 518/518; the parity suite 14/14.skip-changeset: nothing here publishes from a package — a test and two script/config file headers.Generated by Claude Code