Uh oh!
There was an error while loading. Please reload this page.
fix(cli): match i18n-extract's per-component walk to translatePage's - #13215
fix(cli): match i18n-extract's per-component walk to translatePage's#13215os-trump wants to merge 5 commits into
Conversation
…anslatePage Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
…ractor now offers Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
…r the TEST_DEBT ledger to the measurement Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
📓 Docs Drift Check5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 23 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 f8972ced0ff0ef5b19fff20a1e46dc4b4e89e5ab && git checkout f8972ced0ff0ef5b19fff20a1e46dc4b4e89e5ab
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a51da094b04a4510d220776def16e0405d8045d4 9ac73cd02d9e3cb416e4647adb213b985d286272 && git checkout -B drift-repro a51da094b04a4510d220776def16e0405d8045d4 && git merge --no-ff 9ac73cd02d9e3cb416e4647adb213b985d286272
node scripts/docs-audit/affected-docs.mjs --json a51da094b04a4510d220776def16e0405d8045d4 |
Fixes#13109
translatePagedescends a container's declaredproperties.childrenrecursively, sopages.PAGE.components.ID.KEYresolves for a nested component id.collectExpectedEntriesin
packages/cli/src/utils/i18n-extract.tsstill iteratedregions[].components[]andstopped — the extractor OMITTED keys the resolver reads, which is the second half of the
failure pair
PAGE_COMPONENT_COPY_KEYS' own JSDoc names. The KEY list is shared and bothsides import it; the WALK was written twice.
Premise, verified against the tree before writing anything
Read from
packages/spec/src/system/i18n-resolver.tsandpackages/lint/src/page-walk.tsat
origin/main33184fd, not from the card:translatePage(the resolver)walkPageComponents(@objectstack/lint)regions[].components[]regions[].components[]andslots.SLOTproperties.childrenonlyproperties.children,properties.items[].children,properties.body,properties.footerkind: html/react/jsx)The dispatched premise HOLDS, with one correction:
walkPageComponentsis wider on fouraxes, not three (
properties.bodyandproperties.footerwere not named), and it is alsonarrower on one — it skips source-authored pages, which
translatePagedoes not.Why the walk is matched by hand rather than reused
Reuse was the cheaper edit and the file already has the precedent one pass down (the object
sections pass, which reuses
walkPageComponentsbecause a duplicated walk "produced a deadrule once already"). It is refused here on measurement, not taste:
of the same documented pair, which is exactly what this card warns against;
a regression at the level the extractor already covered.
So the new pass mirrors
translatePageexactly: rootsregions[].components[], descentproperties.childrenonly, the same depth cap, the same ancestor cycle guard, and the sameruled collision arbitration. Two consequences worth naming:
page:headerIS offered. The page-name route (pages.PAGE.title) addressesTHE page's header and stops at region level, so a nested one is reachable by the id route
only. Region-level
page:headerstays skipped, as before.page:header's id still blocks a nested namesake even though it emitsnothing, because the resolver counts it as region-level. Missing that would offer a key
the resolver ignores.
MAX_NESTED_COMPONENT_DEPTHis deliberately not exported bypackages/spec, so the CLImirrors the number. The deep-chain test below is what keeps the mirror honest; a shared walk
exported from
packages/specis the durable fix and is reported to the PM as out of scopehere (it is a
packages/specedit).The test can fail for the reason this card exists
The existing guard in
packages/cli/test/platform-page-i18n-parity.test.tscompares extractoroutput against the shipped bundle, so it only ever sees what the extractor already emits — it
is structurally blind to this class. The new block is a differential, not a restatement of
either walk: it runs the real
translatePageagainst a sentinel bundle, asks which componentsit ACTUALLY rewrote, and compares that set against the ids the real extractor ACTUALLY offered.
It fails in both directions.
Reverse verification, both legs run from the committed state, each mutation and restore proven
on disk by blob hash (
git hash-objectagainstgit rev-parse HEAD:PATH, plus an emptygit diff HEAD), never by an exit code. No rebuild is needed for either leg: the mutatedmodule is reached by a relative source import (
../src/utils/i18n-extract.js), not through apackage
exportsmap intodist/.i18n-extract.tsto the base blobb15473bb0d80: 5 of the 6 new tests go red,appliedButNotOfferedgrows from['hdr']tosix ids.
properties.items[].children(marker grep-counted on disk, 1 occurrence; blob differs from HEAD): 2 tests go red with
offeredButIgnored: ['tab_child'].Restore verified after each leg: on-disk blob back to
c920cb2a26aaandgit diff HEADempty.The coverage denominator, measured — it does not move silently
os i18n coveragesharescollectExpectedEntries, so nested copy that counted as neithertranslated nor missing now counts. In this repo that is
examples/app-showcase, and the tennewly visible keys are real copy, in the shape the card describes:
showcase_command_center.components.cc_k1..cc_k6.label— six KPI blocks inside atype: 'flex'container'sproperties.children;showcase_styling_gallery.components.cta_Free / cta_Solo / cta_Team / cta_Business.label—four pricing CTA buttons nested two levels deep.
check:i18n-coveragemoved393 -> 403on that config. The ten are translated in the samechange, so the frozen ratchet baseline is unchanged at
393and the gate readsOK (12 config(s), 602 baselined untranslated string(s), none new). The ratchet was notraised.
Deviations from the dispatched file surface, declared
The dispatch named
packages/cli/src/utils/i18n-extract.tsandpackages/cli/test/platform-page-i18n-parity.test.ts. Three more files are touched, each withits reason:
.changeset/i18n-extract-nested-component-walk.md— required; user-visible CLI behaviour.examples/app-showcase/src/system/translations/index.ts— the ten strings above. This isthe growth branch's own prescribed remedy ("translate them");
--updateis offered by thatgate only for the not-baselined and improved branches, and freezing a growth would raise a
shrink-only ratchet. The bundle already carries a
pages.PAGE.componentsblock written forexactly this dynamic when
#6080made the surface declared, so the form is pinned by thefile itself. Serial-constraint note: the PM cleared
i18n-extract.tsagainst all open PRheads; this file was not in that probe and this seat cannot run it (repo-scoped REST is 403
here).
scripts/check-type-check-coverage.mjs— one ledger number, plus a re-tally of its note.The new tests made
@objectstack/cli's TEST_DEBT drift up by 4 (146 -> 150), becausethis test file's one extension-less relative import made the module
anyand every callbackparameter implicitly
any. Fixed at the root, the way this ledger's own note records#8612doing twice: adding.jsto the import. That took the file to zero and the entry to144, so--lowerwas run and the note re-tallied from the same measurement(
TS7006 x59, TS2835 x56 across 23 files, TS2339 x24, TS2307 x3, TS18046 x2 = 144, measureddirectly over the 106-file test tree, not decremented).
check:type-check-debtnow reportssurplus: none.Gates
Union re-derived for the ACTUAL change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackafter the last commit, and run at9ac73cd0. Exit codes capturedbefore any pipe. All green except two that refuse to run and say so themselves — recorded as
NOT MEASURED, not as passes:
scripts/pm/check-half-states.mjs— exit 3, PREREQUISITE NOT MET: the container's GitHubtoken is a proxy placeholder (401 on
/rate_limit), and the anonymous path is rate-limited.scripts/check-test-completeness.mjs— exit 3, PREREQUISITE NOT MET: it grades a savedturbo run testlog, which CI tees and passes on every invocation. Unreachable locally.Green:
check:nul-bytes,check:agent-test-spelling,check:bash32-floor,check:changeset-gate-self-tests,check:cli-command-ids,check:cli-test-child-env,check:cross-package-test-inputs,check:dual-build-cjs-loads,check:engine-double-contract,check:entry-guard,check:examples-live-imports,check:i18n,check:i18n-coverage,check:logger-receiver-detach,check:objectql-double-limit,check:objectui-changeset,check:page-declaration-shape,check:parse-guard,check:pm-dispatch-gates,check:pm-half-states,check:pnpm-filter-targets,check:published-files,check:query-options-erasure,check:react-page-adapter-contract,check:slot-lookup,check:test-source-alias,check:type-check-coverage,check:type-check-debt,check:type-source-resolution,check:watch-hint-literal,check:where-matcher,check-adr-0087-registration,check-changeset-no-major,check-ci-filter-parity,check-comment-mask-adoption,check-cross-package-test-inputs,check-empty-changeset,check-keyed-text-bounds,check-plugin-teardown-shape,check-ratchet-remedy-authority,check-shard-attestation,check-undeclared-dep-imports,docs-audit/check-affected-docs,docs-audit/check-drift-comment,pm/bare-root-worklist --self-test,pm/release-rehearsal-clone --self-test.Tests, all at
9ac73cd0:packages/clii18n surface — 12 files, 144 tests, all passing. This is a declarednarrowing of the package suite (185 files, ~13 min measured in this package's own
vitest.config.tsheader, which does not fit the container's foreground ceiling undercontention). The narrowing is bounded by measurement, not guesswork:
collectExpectedEntriesis imported by exactly
src/commands/i18n/extract.ts,src/utils/format.tsandsrc/utils/i18n-coverage.ts, and by nine test files — all nine plus three siblings are inthe twelve above. CI runs the whole farm regardless.
packages/clitypecheck— clean, and stated honestly: that program isinclude: ["src"], so it does not readtest/**(ledgered TEST_DEBT). Coverage of thenew test file is the ledger re-measure above, plus a direct
tscover the file: 0 errors.@objectstack/example-showcase—typecheckclean, 26 files / 364 tests passing.Generated by Claude Code