Uh oh!
There was an error while loading. Please reload this page.
fix(cli): walk bulkActions, validation messages and datasets in i18n extract, so the coverage ratchet can see them - #14651
Conversation
… extract #14253 gave three authored display surfaces their first bundle keys and a resolver for each; the extractor walked none of them, so `os i18n extract` scaffolded nothing and `check:i18n-coverage` — which measures against that walk — was blind to all three. - `objects.<o>._views.<v>.bulkActions.<def>.*` from a list view's authored `bulkActionDefs[]`, keyed under the same `_views` root the view's own label uses (`translateView` overlays it with `viewTranslationKey`). - `objects.<o>._validations.<rule>.message`, with a `conditional` wrapper contributing no key of its own — its branch supplies the violation. - `datasets.<n>.{label,description,dimensions.<d>.label,measures.<m>.label}`, top level because a dataset is bound by reference from N presentations. `dataset` gets its own coverage bucket (`i18n/missing-dataset`); bulk-action copy reports under `view` and a rule message under `object`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…8n-extract-three-families
The three families the extract walk now visits are authored by the example apps and untranslated for the locales those apps claim to support, so the ratchet moves — and only there. Every unit is accounted for: examples/app-crm 89 -> 102 (+13: 5 _validations, 8 datasets) examples/app-showcase 393 -> 454 (+61: 18 bulkActions, 11 _validations, 32 datasets) examples/app-todo 120 -> 146 (+26: 4 _validations, 22 datasets) Every other line is byte-identical, including app-multi-package and all nine packages that own an `i18n-extract.config.ts` — no package authors a bulkActionDef, a validations[].message or a dataset, so platform bundles move zero and `check:i18n` stays in sync across all nine. The pre-existing per-config counts are unchanged (89 / 393 / 120 / 0 measured again after the change), so nothing already gated was dropped: the movement is new coverage, not a loosened gate. Also corrects the bulk-action pin's expected key list: `pushOptional` records an optional key with no seed, exactly as `_views.<v>.description` does, so a def that authors no confirm copy still contributes those two keys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…8n-extract-three-families
`check:type-check-debt` measured @objectstack/cli's TEST_DEBT at 171 against a recorded 144 (+27) — the pins' own errors. Cause is the one the ledger entry already names twice: under `moduleResolution: NodeNext` an extension-less relative import does not resolve, so every symbol it names becomes `any` and each file grows a TS2835 plus a TS7006 per untyped binding. One extension per file, and the layer is back to its frozen number: `--re-measure` now reports 22 entries re-measured, none above its recorded number. The same one-import repair collapsed 35 -> 0 and 7 -> 0 in this very file family before; `platform-page-i18n-parity.test.ts` already carries it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
📓 Docs Drift CheckThis PR changes 1 package(s): 8 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 — 22 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 db79e1b5b16f61ae1fe1a93a440e4e73b14a48fb && git checkout db79e1b5b16f61ae1fe1a93a440e4e73b14a48fb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4d0d9445a8ed0240e7ca6a393bbe7f4c637e6bd6 fee694e4d6244c7841ccd57f15e5b44211002e5c && git checkout -B drift-repro 4d0d9445a8ed0240e7ca6a393bbe7f4c637e6bd6 && git merge --no-ff fee694e4d6244c7841ccd57f15e5b44211002e5c
node scripts/docs-audit/affected-docs.mjs --json 4d0d9445a8ed0240e7ca6a393bbe7f4c637e6bd6
|
Uh oh!
There was an error while loading. Please reload this page.
…8n-extract-three-families
…#14517 PR #14517 (the New Project wizard) landed on `main` after this branch measured, and it touched two of the showcase files these numbers are computed from. The collision is exactly on the family this branch introduces, so the number could not stand: `examples/app-showcase` re-derives 454 -> 450. The move is DOWNWARD and fully attributed. #14517 added a `_validations` block to the showcase bundle carrying zh-CN for four rule messages — `project_status_flow`, `project_health_progression`, `end_after_start`, `spent_within_budget` — and those are exactly the four findings that disappear: showcase `_validations` findings 11 -> 7 (-4, no key added) showcase bulkActions / datasets 18 / 32 (unchanged) pre-existing count 393 (unchanged) No new key appeared. `project_status_flow` already existed as a rule, so #14517's rewritten message text moved its content, not its address. Those four are the first real consumers of the key shape #14253 declared and this branch scaffolds: an author-written refusal that now reaches a zh-CN caller in Chinese, which the coverage gate can only see because the walk visits the family. Re-derived with `node scripts/check-i18n-coverage.mjs --update`, never by hand. Every other line is byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33678366780 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
os-trump
commented
Sep 2, 2026
Read from the job log directly rather than inferred from the shard name: One test out of 2710, in a file this PR does not touch, asserting a process signal on a 40-second wall-clock cap. This PR's diff is ⇒ The patch round is not implicated. The re-derived baseline (showcase 454 → 450) is green: What happens next, and why not a re-runThis is the sixth independent PR that ⛔ Re-running this shard now would burn a full CI lap on a failure whose cause is known and whose repair is one merge away. So this PR stays flipped but unarmed until #14715 lands, then goes back to the queue — the same treatment PR #14631 is getting, and for the same reason. Nothing here is a re-run to make a red go away, and nothing about the flake's assertion is being waved off: it is a genuine defect in the instrument, which is why it has its own card and its own fix rather than a re-run. Generated by Claude Code |
…8n-extract-three-families
…#14709#14709 put the remaining eight authored validation messages on the showcase's translation channel, moving the `objects.OBJECT._validations.RULE.message` population this branch is the first to walk. Re-derived with the repo's own tooling (`node scripts/check-i18n-coverage.mjs --update`), never by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14376
#14253 gave three authored display surfaces their first bundle keys and a
resolver for each. The extractor walked none of them, and that costs twice:
os i18n extractscaffolded nothing, andcheck:i18n-coverage— which measuresagainst exactly what
collectExpectedEntriesproduces — could not see thefamilies at all. A gate that cannot fail on a population is counted as if it
could.
What changed, per family
Key shapes are taken from the resolver and the schema on disk, not from the
report.
1. Bulk actions —
objects.OBJECT._views.VIEW.bulkActions.DEF.*pushBulkActionDefs, called frompushViewEntries, so a def's keys are emittedunder the same
_viewsroot the view's ownlabel/descriptionuse.That is not tidiness:
translateViewoverlays the defs withviewTranslationKey(view, objectName)— the bare_viewskey — so deriving thekey a second time is the #5164 defect one surface over. It reaches both authoring
carriers (
obj.listViews.VIEWand adefineView()container'slist/listViews), and for a container's defaultlistthe key is whateverdefaultListViewKeysays the runtime registry calls it (the showcase projectcontainer resolves to
default, which is what the emitted keys show).Emitted:
label,confirmText,confirmLabel,params.PARAM.{label,help,placeholder}.Three exclusions, each measured against
BulkActionDefSchema/BulkActionParamTranslationSchemarather than assumed:successMessagedescriptionconfirmTextparams.PARAM.optionsguidanceagainst it instead of a key, so scaffolding it would write keys.strict()rejectshelp, nothelpText. A bulk param spells its hinthelp(
BulkActionParamSchema); an ACTION param spells the same ideahelpText. Thepin asserts both directions — the key is emitted, and no emitted key ends in
.helpText.Read from the AUTHORED address.
translateViewreadsconfig.bulkActionDefsbecause a servedViewItemnests the wholeListViewSchemaunderconfig; this walker is handed the authored stack config,where the defs sit on the list view itself — the same authored addresses the rest
of the file reads (
view.list.data.object,obj.listViews). Accepting theserved spelling too would be a tolerant alias for a shape this walk is never
given.
2. Validation messages —
objects.OBJECT._validations.RULE.messagepushValidationMessages, from the object walk. The address is the oneobjectValidationMessageKeyspells and the ObjectQL rule evaluator resolvesthrough the engine's existing
i18nServicechannel (authoredRuleMessage).messageonly. A rule'slabelis its entry in the admin listing anddescriptionis the maintainer's note; neither reaches a rejected caller, andthe schema carries
guidanceagainst both rather than a slot.conditionalrule contributes no key of its own, and recursion goes toits
then/otherwise.checkConditionalevaluateswhenand returnsevaluateRule(branch, …)— the branch supplies the violation the caller sees,so the wrapper's
messagecan never be shown. Nested conditionals are followed(depth-capped at 10; the schema is recursive with no declared bound and this
walker is handed hand-authored TypeScript).
active: falsestill emits. It is a toggle on a surface that exists, not theabsence of one, and no other family in this walker consults a runtime toggle.
3. Datasets —
datasets.NAME.*walkDatasets, a top-level pass beside dashboards. Emitted:label,description,dimensions.DIM.label,measures.MEASURE.label— matchinglookupDatasetAttrandlookupDatasetMemberLabel, withtranslateDatasetregistered in
METADATA_DOCUMENT_TRANSLATORS.Top level rather than under
dashboardsbecause a dataset is the ONE definitionevery presentation binds to by reference (ADR-0021 D1): the same measure is drawn
by N widgets across M dashboards, and a dataset no dashboard references would
otherwise be unaddressable.
Every key here uses
pushOptional, notpushDerived: these four areI18nLabelSchemaat the authoring site, so a value may already be an inline{ en, 'zh-CN' }map (#5728) — not source text to scaffold from — and norenderer fallback is measured for a member that declares no
labelat all, sothere is no reader-visible string to seed one from. That is the same posture
translateDatasettakes: it writes only where the bundle answered.datasetsgets its own coverage bucket, so a gap reports asi18n/missing-dataset; bulk-action copy reports underviewand a rule messageunder
object, the buckets whose namespace each key lives in. The bucket additionforced the three-line taxonomy edit in
i18n-coverage.ts(CoverageIssue['source'],COVERAGE_SOURCE,SOURCE_NOUN) —COVERAGE_SOURCEis an exhaustiveRecordkeyed byExpectedEntry['source'], so the compiler, not judgement, fixed that shape.Red-first evidence, per family
One emitter removed at a time from
packages/cli/src/utils/i18n-extract.ts, eachmutation proven on disk before measuring (anchor count 1 → 0, marker count 1) and
each restored from
HEADwith a byte-identity check(
git hash-object==13dc74a6d41d71dc49d58ca75a2767de9bd9c259,git diff HEADempty). No rebuild leg is needed and none is claimed: the pins import the subject
by relative source path (
../src/utils/i18n-extract), not through thepackage's
exports→dist, so the mutation takes effect directly.pushBulkActionDefs(out, root, view, objectName);i18n-bulk-action-coverage.test.tsexpected [] to deeply equal [ …(9) ];expected undefined to be 'Set Labels';expected undefined to be 'Archive Selected'pushValidationMessages(out, objectName, obj.validations, 0);i18n-validation-message-coverage.test.tsexpected [] to deeply equal [ …(2) ];expected undefined to be 'Discount cannot exceed 40% without an…';expected 0 to be greater than 0walkDatasets(config, out);i18n-dataset-coverage.test.tsexpected [] to deeply equal [ …(6) ];expected undefined to be 'Tasks';expected [] to include 'datasets.task_metrics.description'All three green with the emitters in place:
Test Files 3 passed (3) · Tests 45 passed (45).Baseline movement — every unit accounted for
scripts/i18n-coverage-baseline.jsonmoved on exactly three lines, all exampleapps. Platform bundles moved zero, as the card measured:
check:i18nreportsall nine packages that own an
i18n-extract.config.tsstillin sync, becausenone of them authors a
bulkActionDefs, avalidations[].messageor a dataset._validationsexamples/app-crm/objectstack.config.tsexamples/app-showcase/objectstack.config.tsexamples/app-todo/objectstack.config.tsexamples/app-multi-package/objectstack.config.tsi18n-extract.config.tspackagesThe check that makes this new coverage rather than a loosened gate, re-made
on the merged tree that now carries both #14517 and #14709, and this time
anchored to a number nobody on this branch chose. The
beforecolumn is not arecollection: it is what
main's own committed baseline says today, readfrom
git show origin/main:scripts/i18n-coverage-baseline.json— 89 / 393 /120 / 0. Three facts, each measured:
equal to
main's enforced numbers, config for config.findings whose key shape lies outside the three new families is byte-for-byte
the same set as before the merge (89, 393 and 120 paths,
diffempty).Δequals the sum of the three new families exactly, for every config(
89+0+5+8 = 102,393+18+0+32 = 443,120+0+4+22 = 146,0 = 0).So nothing already gated was dropped, and every unit of the difference between
main's baseline and this branch's is a member of one of the three familiesthis PR teaches the walk to visit — an authored English string with no
translation for a locale that example claims to support. The three new families
also contribute no duplicate paths at all; every repeated path in these
reports (29 in showcase, 40 in todo, 1 in crm) is in the pre-existing
_actionshalf and ismain's number, not this PR's.Where the movement comes from, by file:
zh-CN) —src/objects/lead.object.ts(lead_status_transitions,lead_score_range) andsrc/objects/opportunity.object.ts(discount_cap,opp_close_date_not_past,opp_stage_transitions);src/datasets/opportunity.dataset.ts(
opportunity_metricslabel + description, 3 dimensions, 3 measures).zh-CN) —src/ui/views/project.view.ts, whose containerdefault list carries six defs (
set_labels,assign_team,reassign_account,reschedule,relabel_ops,purge_restricted: labels, confirm text and fiveparam labels = 17) plus
src/ui/views/field-zoo.view.ts(
inline_bulk_defs.showcase_zoo_visible_string.label= 1);src/ui/datasets/{chart-gallery,revenue-pulse}.dataset.ts(4 datasets, 32keys). Rule messages contribute nothing here any more: all eleven
addressable
_validationskeys — four onshowcase_project, six ofshowcase_account's seven declared names, one onshowcase_task— are nowtranslated, four by fix(objectql,showcase): keep the state-machine refusal's facts with an authored message, and stop the New Project wizard offering statuses it refuses #14517 and seven by fix(showcase): put the remaining eight authored validation messages on the translation channel #14709, which is the whole of the
movement recorded below.
zh-CNandja-JP, hence ×2 per key) —src/objects/task.object.ts(completed_date_required,recurrence_fields_required= 2 keys × 2 locales) andsrc/datasets/task.dataset.ts(task_metrics: 11 keys × 2 locales).Translating those strings is the examples' own work, not this PR's: the ratchet's
contract is that the debt is frozen and cannot grow, and freezing a newly VISIBLE
family is what ratcheting it in means here.
Re-derived twice while this PR waited — 454 → 450 → 443
This branch's committed ratchet holds counts only this branch can compute.
On
mainthe walk does not exist yet, so a PR that moves one of these keys movesthe number without any gate on
mainbeing able to see that it did. It has nowhappened twice, and both times the gate named the movement and its direction
itself rather than a reviewer noticing:
mainbroughtshowcase_projectrule messagesuntranslated declared strings improved 454 → 450 — ratchet DOWNuntranslated declared strings improved 450 → 443 — ratchet DOWNBoth re-derived with the repo's own tooling
(
node scripts/check-i18n-coverage.mjs --update), never by hand. Every otherline of the baseline is byte-identical, and that is measured rather than assumed:
no commit in the merge range touches
examples/app-crm,examples/app-todo,examples/app-multi-packageor any of the nine packages that own ani18n-extract.config.ts. #14707'stranslateActionoverlay landed in the samerange and cannot move a coverage count either —
packages/cli/src/utils/i18n-coverage.tsimportscollectExpectedEntriesandtwo types, and no runtime resolver at all.
The -7, attributed by set difference
Measured, not inferred from the total. Showcase was linted on this tree with
#14709's three source files reverted to this branch's pre-merge tip
(
git checkout ce637132fd -- …), then linted again with the tree restored. Eachleg is proved on disk by blob hash rather than by an exit code: after the
mutation each file's
git hash-objectequals the tip's blob (13347c4d01,e9af404cbf,86af31d1eb), after the restore each equalsHEAD's(
93268bbf25,e8f8b78802,8e9a838829), andgit diff HEADis empty. Thecomparison is a multiset difference over the
i18n/population the gatecounts, so a path that appears twice cannot hide a move: 7 dropped, 0 added,
net -7.
translations.zh-CN.objects.…)_validations.account_lifecycle.messageshowcase_account_validations.tax_id_format.messageshowcase_account_validations.billing_email_format.messageshowcase_account_validations.support_config_shape.messageshowcase_account_validations.churn_reason_present.messageshowcase_account_validations.churn_reason_absent.messageshowcase_account_validations.task_status_flow.messageshowcase_taskSeven, not the eight #14709 translated — and the shortfall is this walker's
documented exclusion, not a miss.
showcase_accountdeclares seven rule namesand #14709 gave all seven a zh-CN entry, but
churn_reason_consistencyis aconditionalwrapper, and the walk emits no key for one on purpose:checkConditionalrenders the matching BRANCH's message, so the wrapper's ownsentence is structurally unreachable. Six of account's seven are addressable,
plus
showcase_task.task_status_flow, which is 7. The measured drop and theexclusion rule agree — which is the property worth checking here, and it is the
same rule stated under family 2 above.
Two independent constructions of the "before" state agree byte for byte: the
counterfactual described above, and the previous round's real measurement taken
on
ce637132fd— 450 findings each, identical path sets.The family this PR makes visible is now fully translated on showcase: 7 → 0.
That is the ratchet working rather than a hole opening. Showcase's
_validationscontribution is frozen at zero, so the next authored rule message that ships
there without a translation reds the gate on the PR that adds it.
app-crm(5)and
app-todo(4) still carry theirs, frozen at those numbers.#14517 and #14709 are the first author-side consumers of the key shape this PR
scaffolds.
objects.OBJECT._validations.RULE.messagewas declared by #14253with a reader in the same change; these two PRs are the first uses of it from the
authoring side, and a zh-CN caller's rejected write now gets the author's own
sentence in Chinese instead of English beside a Chinese built-in message. The
coverage gate can see that transition — untranslated to translated — only
because this branch teaches the walk to visit the family. Before it, those eleven
keys contributed nothing to the ratchet in either state.
The derivability reading (recorded, not built)
The triage asked whoever did the fourth manual walk to record what they learned
about derivability while inside the file. Measured, not speculated:
The extractor is NOT derivable from the resolvers, and the reason is
structural rather than effort. The two enumerations answer different questions
over different inputs:
document and reads the served shape; the walker is handed a whole stack
config and reads the authored shape.
translateViewreadsconfig.bulkActionDefsbecause a servedViewItemnests the ListView underconfig; the authored config has it on the list view itself. A mechanicalderivation from the resolver's read path would emit against an address the
walk never sees.
viewTranslationKey(view, …)needsview.name— already resolved. The walker has to derive it first, throughexpandViewContainer/defaultListViewKey, including the case where acontainer's default
listcollapses into alistViewsentry and must emit nokey of its own. Nothing in any resolver knows that.
sourceValue/inlinesplit does not exist resolver-side at all.inlinedecides whether coverage may DEMAND a translation, and it turns onwhether the author wrote the literal or the renderer derives a fallback —
def.label ?? formatActionLabel(def.name),param.label ?? param.name, bothof which live in objectui, another repo. A resolver never sees an
untranslated render.
optionsmust not be scaffolded comes from
guidanceintranslation.zod.ts; that aconditionalwrapper'smessagemust not be comes fromcheckConditionalinobjectql. Neither is visible from a resolver.
What IS derivable, today, with no restructuring: the key FACE — which leaf
keys exist under each group — is already machine-readable in
packages/spec/src/system/translation.zod.ts(strictObject, withguidancenaming every deliberately-excluded neighbour). That is the half that drifted
here: three groups landed in the schema with a reader each, and the walk produced
zero keys for them, with every gate green.
⇒ The fileable construct is not "derive the extractor from the resolvers" but a
parity gate: walk
TranslationDataSchema's declared leaf keys and asserteach is produced by
collectExpectedEntriesfor some fixture, or is listed in anexplicit, shrink-only exemption ledger. It needs only two things that already
exist and are already pure — the schema and the walker — so it lands in one new
file,
scripts/check-i18n-walk-parity.mjs, wired aspnpm check:i18n-walk-paritybeside the two gates this PR reports. It would have failed the day #14253 landed,
which is the property the three previous instances of this shape all lacked. Full
derivation would additionally need a per-group adapter declaring the three things
the schema cannot carry (authoring locator, identity function, seed/inline rule)
— strictly more work, for the half that has NOT been the one drifting.
Verification
Re-run at head
fee694e4d6, on the tree withorigin/mainmerged at4d0d9445a8— the merge brought #14709, #14707, #14711 and #14714. The gateunion and the repo lint were run after the final commit, on that head.
pnpm --workspace-concurrency=2 buildvitest run --maxWorkers=2 test/i18n-{bulk-action,validation-message,dataset}-coverage.test.tsTest Files 3 passed (3),Tests 45 passed (45)pnpm check:i18n-coverageexamples/app-showcase/objectstack.config.ts: untranslated declared strings improved 450 → 443 — ratchet DOWN: run node scripts/check-i18n-coverage.mjs --update and commit the baseline.node scripts/check-i18n-coverage.mjs --updatei18n coverage baseline updated: 13 config(s).— one line moved, showcase 450 → 443pnpm check:i18n-coveragecheck-i18n-coverage: OK (13 config(s), 691 baselined untranslated string(s), none new).pnpm check:i18ncheck-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).dispatch-gates --repo objectstack-ai/objectstack --commands, then--ran✓ dispatch-gates --ran: 44 derived famil(ies) accounted for — 44 run, 0 NOT-MEASURED.pnpm linteslint . --no-inline-config, no outputCarried from the previous head
ce637132fdand not re-run on this one, saidplainly rather than implied:
pnpm --filter @objectstack/cli typecheck(exit 0),the 16-file i18n suite (191 tests) and the package's unit project (161 files /
2119 tests). Nothing in
packages/changed between the two heads on thisbranch's side — the only commit is one number in
scripts/i18n-coverage-baseline.json— and the three pins, which are the teststhis card adds, were re-run here.
Two of the union's 44 gates are NOT MEASURED, in their own words, and neither
is a red.
node scripts/check-test-completeness.mjsexits 3:"PREREQUISITE NOT MET — this gate grades a saved
turbo run testlog, and no logwas named"; its own text adds "Nothing was measured … ⛔ It is NOT a finding".
node scripts/pm/check-half-states.mjsexits 3: "PREREQUISITE NOT MET — thetransport authenticates but repo-scoped reads are refused", with
GET /rate_limitanswering 200 (15000 left) whileGET /repos/objectstack-ai/objectstackanswers 403 with nox-ratelimit-*headers at all. The other 42 ran and all 42 exited 0.
Every exit code was captured before any pipe (
cmd > log 2>&1; EXIT=$?), andeach verdict quoted above is the gate's own printed line, not a bare
$?. Heavyruns went through
scripts/pm/os-verify-lock.sh; its closing line for this roundis
VERDICT command-exit 0 · held the lock 1048s (17m28s) · waited 2s.NOT MEASURED, stated rather than implied:
packages/cli/tsconfig.jsonhasinclude: ["src"], sopnpm --filter @objectstack/cli typecheckdoes nottype-check
test/— its exit 0 says nothing about the three new pin files, andpackages/cliis not wired intocheck:test-typecheck. The pins are covered byrunning them, not by tsc.
One red this found, and what it was
pnpm check:type-check-debtwent red on the first union run:@objectstack/cliTEST_DEBT recorded 144 raw tsc errors,
--re-measurereported 171 (+27) —the three new pins' own errors. Cause is the class that ledger entry already
names twice in its own note: under
moduleResolution: NodeNextanextension-less relative import does not resolve, so every symbol it names
becomes
anyand the file grows one TS2835 plus a TS7006 per untyped binding.The pins imported
'../src/utils/i18n-extract'and'../src/utils/i18n-coverage'without
.js.One extension per file, and the layer is back to its frozen number:
check-type-check-coverage --re-measure: OK — 22 ledger entr(ies) re-measured in 483.6s, 755 raw tsc error(s) total, none above its recorded number.The verysame one-import repair collapsed 35 → 0 and 7 → 0 in this file family before,
and
platform-page-i18n-parity.test.tsalready carries it — worth noting thatthis cascade recurs in the same package every time it gains test files, which
is its own small instance of the pattern this card is about.
Changeset level
minor, matching the direct precedent: the flow/screen bucket(
.changeset/cli-i18n-flow-screen-bucket.md) is the same shape — a family theresolver already read, given its first walk plus a new coverage bucket — and
shipped
minor. Thebuglabel describes why this exists, not what ships: whatships is
os i18n extractwriting keys it never wrote andos lintgaining thei18n/missing-datasetrule id, both of which a consumer can observe.Deviations
packages/cli/src/utils/i18n-extract.ts; the change also touchespackages/cli/src/utils/i18n-coverage.tsby three lines.COVERAGE_SOURCEisan exhaustive
Recordkeyed byExpectedEntry['source'], soadding the
datasetkind does not compile without it — same package, samedefect class, shape fixed by the existing type.
packages/spec, nothing inscripts/check-i18n-coverage.mjs, noplatform bundle, no
content/docs/releases/**.🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza