Found while fixing #16121 (PR: claude/issue-16121-i18n-extract-key-count). Unrelated to that card's defect — that one is about which NUMBER is printed, this one is about which RECORDS the provenance companion keeps — and deliberately not fixed there: it changes emitted companion bytes, which is a wider verification surface than the count repair.
What is wrong
packages/cli/src/commands/i18n/extract.ts builds the section list for narrowToCommittedSections from two literals:
const committed: string[] = [];
if (/* the stack module is emitted */) committed.push('objects');
if (/* the companion is emitted */) committed.push('metadataForms');
narrowToCommittedSections documents its own identity rule — "A section is named by a leaf path's FIRST dotted segment" — and says the caller "passes the sections they are committing". Under --no-objects-only the stack module holds every group the stack authors (objects, apps, dashboards, flows, …), so the caller is committing more sections than the one literal it names, and every record whose path does not start with objects. is filtered out of <locale>.source-hashes.generated.ts.
Measured
On f5aec38a6af + the #16121 branch (the narrowing predicate there moves the guard, not the section name; the behaviour below is identical on plain f5aec38a6af). Fixture: one object with one field, one app, i18n.defaultLocale: 'zh-CN'.
$ os i18n extract FIXTURE --locales=ja-JP --no-objects-only --no-metadata-forms \
--source-hashes --fill=default --out=OUT
Wrote OUT/ja-JP.objects.generated.ts (3 keys)
Wrote OUT/ja-JP.source-hashes.generated.ts (2 keys)
The emitted module holds 3 leaves — objects.kpi_metric.label, objects.kpi_metric.fields.name.label, apps.kpi.label — and the companion beside it holds 2. The apps.kpi.label record is the one dropped, and it is dropped for exactly the reason the companion exists to prevent: with no record, a leaf that is a stale byte copy of a revised source label is indistinguishable by value from a real translation, so check:i18n-stale-fill can never report it. Under --no-objects-only that silence covers every group except objects.
Why it is not urgent
Dormant in this repository today: --source-hashes is used by exactly one config (packages/platform-objects/scripts/i18n-extract.config.ts), which runs under the default --objects-only, where objects really is the only committed section and the literal happens to be right. It bites the first bundle set that commits apps or dashboards and opts into provenance.
Likely shape of the fix
Name the sections from the payload the module actually holds — the top-level keys of translationModulePayload(bundle, kind) — rather than from a mode-specific literal, which is the same "one selector decides everything a module reports" discipline #16121 applies to the counts. That makes --metadata-forms's own literal fall out of the same expression too.
Dedupe
search_issues in this repo for the companion/section-narrowing shape returns #16121 (this finding's origin), #14894 (closed — which keys the two translation modules emit), #14376 and #5405 (both closed, unrelated walker gaps). No open card covers the source-hash companion's section list. ⚠️ Not an exhaustive enumeration: repo-scoped REST is 403 from this seat, so this is one targeted semantic search, not a sweep.
Generated by Claude Code
Found while fixing #16121 (PR:
claude/issue-16121-i18n-extract-key-count). Unrelated to that card's defect — that one is about which NUMBER is printed, this one is about which RECORDS the provenance companion keeps — and deliberately not fixed there: it changes emitted companion bytes, which is a wider verification surface than the count repair.What is wrong
packages/cli/src/commands/i18n/extract.tsbuilds the section list fornarrowToCommittedSectionsfrom two literals:narrowToCommittedSectionsdocuments its own identity rule — "A section is named by a leaf path's FIRST dotted segment" — and says the caller "passes the sections they are committing". Under--no-objects-onlythe stack module holds every group the stack authors (objects,apps,dashboards,flows, …), so the caller is committing more sections than the one literal it names, and every record whose path does not start withobjects.is filtered out of<locale>.source-hashes.generated.ts.Measured
On
f5aec38a6af+ the #16121 branch (the narrowing predicate there moves the guard, not the section name; the behaviour below is identical on plainf5aec38a6af). Fixture: one object with one field, one app,i18n.defaultLocale: 'zh-CN'.The emitted module holds 3 leaves —
objects.kpi_metric.label,objects.kpi_metric.fields.name.label,apps.kpi.label— and the companion beside it holds 2. Theapps.kpi.labelrecord is the one dropped, and it is dropped for exactly the reason the companion exists to prevent: with no record, a leaf that is a stale byte copy of a revised source label is indistinguishable by value from a real translation, socheck:i18n-stale-fillcan never report it. Under--no-objects-onlythat silence covers every group exceptobjects.Why it is not urgent
Dormant in this repository today:
--source-hashesis used by exactly one config (packages/platform-objects/scripts/i18n-extract.config.ts), which runs under the default--objects-only, whereobjectsreally is the only committed section and the literal happens to be right. It bites the first bundle set that commits apps or dashboards and opts into provenance.Likely shape of the fix
Name the sections from the payload the module actually holds — the top-level keys of
translationModulePayload(bundle, kind)— rather than from a mode-specific literal, which is the same "one selector decides everything a module reports" discipline #16121 applies to the counts. That makes--metadata-forms's own literal fall out of the same expression too.Dedupe
search_issuesin this repo for the companion/section-narrowing shape returns #16121 (this finding's origin), #14894 (closed — which keys the two translation modules emit), #14376 and #5405 (both closed, unrelated walker gaps). No open card covers the source-hash companion's section list.Generated by Claude Code