Uh oh!
There was an error while loading. Please reload this page.
feat(cli): i18n coverage + extract gain the flow/screen bucket - #11615
Conversation
`COVERAGE_SOURCE` had no flow bucket, so `os lint`'s `i18n/missing-*` family could not report a screen-flow copy gap at all and `os i18n extract` never scaffolded the `flows..screens..` skeleton. The key face is imported from `@objectstack/spec/system` (FLOW_SCREEN_COPY_KEYS / FLOW_SCREEN_FIELD_COPY_KEYS) so the extractor and the resolver cannot drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
…freeze the newly-visible flow debt Under moduleResolution NodeNext the extensionless relative imports did not resolve, and every symbol they named became `any` — 2x TS2835 plus a 10-error TS7006 cascade, +12 on the packages/cli TEST_DEBT ledger. With the extensions the layer re-measures 146, its recorded number, 0 of them in this file. The flow bucket makes 55 pre-existing untranslated screen-flow strings visible to `check:i18n-coverage` for the first time (app-crm 89->93, app-showcase 393->424, app-todo 120->140). Frozen in the baseline the way #3370's surface widening was: the debt is not new, only newly visible, and translating three example apps' wizard copy is not this card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
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 98f7cd2f60ed16a1fbf383213d89403c232cd375 && git checkout 98f7cd2f60ed16a1fbf383213d89403c232cd375
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 502ff8b506d3015b3f22816387886bffa8c7740d 48e17f02c1234438463be263ffeafe47e48d001f && git checkout -B drift-repro 502ff8b506d3015b3f22816387886bffa8c7740d && git merge --no-ff 48e17f02c1234438463be263ffeafe47e48d001f
node scripts/docs-audit/affected-docs.mjs --json 502ff8b506d3015b3f22816387886bffa8c7740d
|
os-zhuang
commented
Aug 24, 2026
ACCEPT — PM seat review ( |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11485
@objectstack/cli's coverage taxonomy had no flow bucket. #11287 gave the bundle aflowsgroup and a resolver that applies it; nothing on the CLI side walked it, soos lint'si18n/missing-*family could not report a screen-flow copy gap at all, andos i18n extractnever scaffolded the keys — leaving authors with no way to discover the vocabulary.Measured on #11287: HotCRM reported 0
i18n/missing-*issues on a tree whose six screen dialogs rendered English in all four locales. An app whose i18n gate is green is green because the surface is invisible to it.What landed
The shared walker
collectExpectedEntries(one definition of what is translatable, feeding both the gate and the extractor) gains awalkScreenFlowspass, andCOVERAGE_SOURCEgains theflowbucket, so the same keys are gated asi18n/missing-flowand scaffolded byos i18n extract:Screens keyed by
FlowNode.id, fields byScreenFieldConfig.name— the identifiers the runner already holds at render time, pertranslation.zod.ts's own addressing table.The key face is imported, never restated
FLOW_SCREEN_COPY_KEYS/FLOW_SCREEN_FIELD_COPY_KEYScome from@objectstack/spec/system. Re-verified on the ref this branch was cut from (6d355096) rather than carried from the card, as triage asked — resolved frompackages/cliafter building the closure:translation.test.tspins schema-to-list spec-side; the newemits exactly the spec-exported key facetest pins list-to-walker here, so a hand-copied list fails rather than silently offering a key nothing reads.Two seeding rules, measured against what the reader sees
titlefalls back to the nodelabel. The executor builds the wire title asconfig.title ?? node.label(ScreenSpec.title), andtranslateFlowoverlays the bundle ontoconfig.titlefor that reason — one key covers whichever the runner draws. A screen with only a canvas label still shows English text somebody owes a translation for.labelfalls back to itsname, as a derived seed.ScreenFieldConfig.labelis optional and forwarded as-is, so the runner renders the name. The skeleton stays usable while the gate demands no translation of a string nobody authored.One inclusion beyond the card's literal wording, named here
The card enumerates the screen level.
flows.<flow>.labelis harvested too:lookupFlowLabelreads it,translateFlowapplies it to every flow, and the strict schema declares it. Leaving it out would scaffold aflows.<name>object with the one key an author most expects missing — the same "no path to discover the vocabulary" defect one level up. It accounts for 3 of app-todo's 10 new keys. Strike it if you disagree; the walker's flow-label line is onepushOptionalcall.Anti-vacuity, both directions
The pin that matters is a tree whose object surface is fully translated and whose wizard is not: it must report the wizard. Its twin is a fully translated tree reporting nothing, or the bucket is a noise generator authors learn to ignore. Both are in
packages/cli/test/i18n-flow-screen-coverage.test.ts, plus the monolingual-project silence that keeps the gate opt-in.Shown failing without the change. Prediction was recorded before running, and both legs matched it exactly:
walkScreenFlows(config, out);call removedflow: 'flow'removed fromCOVERAGE_SOURCEThe 4 that survive leg A are the anti-vacuity twins — they are supposed to hold in both worlds; that is what makes them the noise guard rather than the defect pin. Leg B isolates the taxonomy half: only
attributes them to the flow bucketgoes red.Mutations proven on disk both ways rather than trusted to an editor's exit code (a zero-hit
sedexits 0). Leg A: injected marker1, live call0; restored: live call1, marker0. Leg B: live mapping1 → 0with marker0 → 1, and back. Both legs carriedtrap … EXIT INT TERMrestoring fromHEAD, and the fix was committed first so the restore had a real anchor. No rebuild between legs: both mutated files are imported relatively by the test, so vitest transforms the checkout source; the one dist-resolved import in play (@objectstack/spec/system) is not mutated and was preflighted above.Clause-②: no — measured, not inheritedpackages/spec/src/**, no*.zod.ts, no error-code ledger.flowsgroup's accept/reject was settled spec-side by i18n: nothing readsTranslationData.flows— a screen flow still renders its authored English in every locale (the runner half #7646 deferred) #11287, and this change only produces inputs that face already accepts — pinned byscaffolds a bundle the strict schema accepts.'flow'live insrc/utils/*, whichpackages/cli/src/index.tsdoes not re-export and which the package'sexports(undefined;main: dist/index.js) does not address.warningseverity — soos lint's exit code is unchanged except under--i18n-strict, where the new warnings become errors. Reporting more problems is a tightening, and scaffolding is output rather than contract.The ratchet moved, and that number is the defect
check:i18n-coveragewent red on all three examples the first time it ran with this bucket:examples/app-crmexamples/app-showcaseexamples/app-todo55 untranslated screen-flow strings that no gate in this repo could see. They are real:
os lint examples/app-todo/objectstack.config.tsnow listsquick_add_task's screen title, its four field labels and its success-screen title, in bothzh-CNandja-JP.Frozen in
scripts/i18n-coverage-baseline.jsonrather than translated, which is the route that script's own header records for exactly this move (#3370 widened the surface and froze the debt it exposed): the debt is not new, only newly visible, and translating three example apps' wizard copy into two locales is a content change, not this card. The ratchet now fails the moment it grows.Item 3 — reference-validation for
flows: its own card, filed as #11608packages/lint/src/validate-translation-references.tsskips unrecognised top-level namespaces, so authoredflowskeys are unvalidated against the flow they name. Probed against the rule's own entry point with a bundle naming a nonexistent flow, screen node and field:The sibling namespaces each report their orphan; all three flow levels produce nothing. Decided as a separate card, not folded:
packages/lintis outside this card's declared surface, the work is a new universe collector for three nesting levels plus an orphan leg mirroringdashboards→widgets(~50 lines there) plus tests, and the failure class differs (orphan keys, allwarningper ADR-0072 D1, vs. missing translations). No ordering constraint either way —os i18n extractderives every key it writes from live metadata, so a scaffolded key resolves by construction; that rule is about hand-authored keys.Verification
Full local gate union re-run at
48e17f02, the final commit, each quoted from the gate's own verdict line:check-i18n-coverage: OK (12 config(s), 657 baselined untranslated string(s), none new).check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 255.5s, 1897 raw tsc error(s) total, none above its recorded number.check-nul-bytes: OK (scanned 6491 text file(s) …; no raw ASCII control bytes).check:published-files,check:slot-lookup,check:test-source-alias,check:type-check-coverage,check:type-source-resolution,check:query-options-erasure,check:engine-double-contract,check:cross-package-test-inputs,check:where-matcher,check:entry-guard,check:parse-guard,check:pnpm-filter-targets,check:changeset-gate-self-tests,check:objectui-changeset,check-ci-filter-parity,check-plugin-teardown-shape,check-affected-docs,check-empty-changeset,check-adr-0087-registration,check-changeset-no-major,release-rehearsal-clone --self-test— all green.pnpm lint(eslint . --no-inline-config, whole repo): exit 0 in 89s. No narrowing claimed.packages/clisuite, whole, in three shards under the shared verify lock: 168 files / 1881 tests, all passing (759 + 555 + 567).The gate set was derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derived after the final commit —scripts/i18n-coverage-baseline.jsonpulled in three families (check:entry-guard,check:parse-guard,check:pnpm-filter-targets) that the first derivation could not name, and those were run too. No gate refused.Generated by Claude Code