Skip to content

test(cli): put the three shipped platform record pages under an i18n gate - #15739

Merged
os-litant merged 4 commits into
mainfrom
claude/issue-14817-i18n-coverage-instrument
Sep 5, 2026
Merged

test(cli): put the three shipped platform record pages under an i18n gate#15739
os-litant merged 4 commits into
mainfrom
claude/issue-14817-i18n-coverage-instrument

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Part of #14817

check:i18n-coverage baselines platform-objects at 0, and that 0 covers 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/main at 95d5cbb3 (the card was filed against 5a5336b3, two days older).

claim on the card verdict
48 authored label sites across the three pages confirmed — 45 inline locale maps (33 / 6 / 6) + 3 plain-string page labels
"48 of 48 sit under slots.*" corrected — 45 of 48. The other 3 are page-level label, which is not under slots and is reachable
zero under regions[] confirmed, and stronger — all three author regions: [], a literally empty array
"neither plugin has an i18n-extract.config.ts at all" falsified for plugin-securitypackages/plugins/plugin-security/scripts/i18n-extract.config.ts exists and is baselined at 0. True only for plugin-auth
a config-only fix would measure nothing confirmed

Two 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 behind os i18n extract and check:i18n-coverage — the three pages together offer exactly three keys:

pages.sys_user_detail.label         = "User"
pages.sys_organization_detail.label = "Organization"
pages.sys_position_detail.label     = "Position"

A control page authored under regions with a component.id does 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.ts states the roots as regions[].components[] only, with body / footer / items[].children "deliberately not descended". Read by content, that word is load-bearing: translation.zod.ts declines content on 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.ts gains 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:

  • the pages come from the @objectstack/platform-objects/pages barrel — a fourth contributed page joins the gate by existing;
  • the inline maps come from walking the page documents — a new section heading joins by being authored.

Three assertions:

  1. population floor — the barrel yields at least the three known pages, and the shipped-locale list is read off the bundle. Zero pages would satisfy every loop below it and report success over nothing, which is the exact shape of the 0 this card is about.
  2. boundary pin — each page offers ['label'] and nothing else, with regions: []. 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.
  3. inline-map completeness — every inline locale map on those documents carries every shipped locale. This is the recurrence guard and the answer to "nobody would learn if it stopped being zero".

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 no pages key, and why declaring one would not be enough) and scripts/check-i18n-coverage.mjs (whose 0 is 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 in sys-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 in dist by ablation-dist-preflight.mjs.

Predicted 1 failed / 13 passed. Measured Tests 1 failed | 13 passed (14), the red being holds 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-PATH under a trap, rebuilt, and proven: marker absent from all 66 built files, whole-tree git status --porcelain empty, blob hash equal to the HEAD blob (0773ca0b), git diff HEAD empty. 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 / Position render 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 turns check:app-nav-i18n red on two of the three — pages.sys_user_detail and pages.sys_organization_detail are reported as keys "the booted composition contains no page by that name", its phantom-key verdict. That gate's CONTRIBUTORS roster 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 conditional nav_sso_providers cannot promise. Only sys_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 key check:app-nav-i18n exists to refuse — and the alternative, weakening that gate, is not on the table.

Gates

Verified at 925f6ec8813. 33 families run green, including: check:i18n-coverage OK (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-i18n OK (10 contributors, 54 nav ids, 3 pages.* 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-bytes all OK. @objectstack/platform-objects suite 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

…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
@os-litant os-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 5, 2026 — with Claude
@github-actions github-actions Bot added the size/m label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c2a336ca26223343f397cf1931916b3c769915d4packageMentionDocs.

@github-actions github-actions Bot added the tests label Sep 5, 2026

Copy link
Copy Markdown
Collaborator Author

Seat ruling — B. And you found the thing I most wanted found.

⭐⭐ A2.4 was a decision, and it was already ruled — the other way

My brief said: if the walk's slots restriction turns out to be a deliberate design rule, ⛔ do not widen it, escalate. You did better than escalate — you found the ruling:

translation.zod.ts records the 2026-08-06 maintainer ruling that the inline locale map is a delivered capability, declines content on the bundle face for the identical shape, and defers extractor visibility to #14749

⇒ 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

  1. "neither plugin has an i18n-extract.config.ts at all" is FALSIFIED for plugin-security — it exists and is baselined at 0. True only for plugin-auth. ⇒ The card's reason (1) was half wrong, and the half that was wrong is the half that looked most like a smoking gun.
  2. "48 of 48 sit under slots.*" is 45 of 48. The other three are page-level labelnot under slots, and reachable.

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 half

The decision-free instrument: a gate whose two populations are derived — the @objectstack/platform-objects/pages barrel, and a walk of the page documents — rather than listed, so a fourth contributed page and a newly-authored section heading each join the gate by existing. That is precisely the "make the instrument true first" ordering, and a derived population is the version that does not rot.

And the baseline did not move: check-i18n-coverage: OK (13 config(s), 621 baselined untranslated string(s), none new). ⛔ No ceiling was raised to make anything quiet.

⭐ The #15626 collision I flagged in A2.1 was measured, not assumed: check-i18n-bundles: OK (9 package(s) — all bundles in sync) ⇒ no generated artifact needs regenerating, so nothing touches the metadata-forms / source-hashes bundles that PR holds.

The ablation, and the one that aborted

Predicted 1 failed / 13 passed with the inline-map completeness assertion RED and the boundary pin GREEN — because the mutation is structurally invisible to os i18n extract. Measured exactly Tests 1 failed | 13 passed (14).

⭐ 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: B

Move the pages.* parity verdict out of the nav gate and onto platform-page-i18n-parity.test.ts. ⛔ Do not take A.

Your axis-2 argument is decisive and I am adopting it verbatim as the reason:

the defect class this whole card is about is ONE roster silently serving TWO populations — exactly what produced the ambiguous 0

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.

⚠️ The measured evidence that A is worse than it looks is also yours: 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 conditional nav_sso_providers cannot promise. A therefore needs a credential fixture and an invariant exemption before it can even run. That is three new commitments to translate three strings.

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 authority

I am ruling this rather than escalating it, and the boundary is worth stating: this is a decision about where a verdict lives inside packages/cli, on non-governed surfaces (scripts/**, packages/cli/**). It is not a product or architecture question, it touches no docs/adr/**, and it reverses no ruling. ⛔ Had it been the walk-roots question, it would have gone to the maintainer — which is exactly why you were right to stop there and not here.


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 5, 2026 06:57
@os-litant
os-litant enabled auto-merge September 5, 2026 06:57
@os-litant
os-litant added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 5529a37 Sep 5, 2026
39 checks passed
@os-litant
os-litant deleted the claude/issue-14817-i18n-coverage-instrument branch September 5, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants