Uh oh!
There was an error while loading. Please reload this page.
fix(lint): walk stack.translations as the locale-keyed bundle it is - #11383
Conversation
`lintLivenessProperties` registered `{ type: 'translation', key:
'translations' }` in TYPE_COLLECTIONS and then walked those items flat, so
`checkItem` read `bundle['flows']` for the ledger's one `authorWarn` row. An
item of `stack.translations` is a `TranslationBundle` — `z.record(LocaleSchema,
TranslationDataSchema)` — whose top-level keys are locale codes, so every warned
lookup missed and the whole translation ledger was silent for file-authored
bundles, the only way apps author translations today.
`translation` now walks bespoke, the way `object`/`field` do: each bundle, each
locale entry's `TranslationData`, with the subject naming both. The registry row
is replaced by a tombstone comment saying why it must not come back —
registering a collection is only half the contract, the walk has to match the
collection's shape.
The regression test is pinned on the bundle shape with a `TranslationItem`-shaped
anti-fixture beside it: that shape warns on the BROKEN walk, so a fixture written
that way would have been green from the day the bug shipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 4 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 0fe506c6a576a0e0a02e770d4af29ba95315c284 && git checkout 0fe506c6a576a0e0a02e770d4af29ba95315c284
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a872ce93ab967daeb017429220bcd5723c7682fa dd0e782b275ffbdd7ba02cc98e4e705e3d892827 && git checkout -B drift-repro a872ce93ab967daeb017429220bcd5723c7682fa && git merge --no-ff dd0e782b275ffbdd7ba02cc98e4e705e3d892827
node scripts/docs-audit/affected-docs.mjs --json a872ce93ab967daeb017429220bcd5723c7682fa |
Uh oh!
There was an error while loading. Please reload this page.
…stead of throwing The docblock's own contract — "Advisory only — returns findings, never throws" — was not held by three walks: the flat TYPE_COLLECTIONS loop, the object walk, and the field walk nested under it. Each read `item.name` (or `item.object`) off every collection element with no record guard, so a null element threw TypeError instead of being skipped. The translation bundle walk already guarded its two levels this way (#11383); this adds the same `isRecord()` guard to the three that did not. Measured before the fix: all three walks throw on a null element, not just the flat loop the card's suggested shape named. Three new regression tests (one per guarded walk) reverse-verified red against the unguarded source.
Fixes#11288
What was wrong
lintLivenessPropertiesregistered{ type: 'translation', key: 'translations' }inTYPE_COLLECTIONSand then walked those items the way every other row in that list is walked — flat, withcheckItemreadingitem[path]for a dotless warned path. But an item ofstack.translationsis aTranslationBundle:z.record(LocaleSchema, TranslationDataSchema)(packages/spec/src/system/translation.zod.ts:904, wired atpackages/spec/src/stack.zod.ts:275), whose top-level keys are locale codes. So the lookup wasbundle['flows'], a bundle has noflowskey at any depth reachable that way, and every warned property missed.Verified against the shipped ledgers at
b863f01: thetranslationwarn map has exactly one entry,flows(status: planned,authorWarn: true), and the ledger has 8 warned rows in total across all shipped types. So the wholetranslationchannel amounted to one row that reached nobody — for file-authored bundles, which is the only way apps author translations today.The fix
translationjoinsobject/fieldas a bespoke walk, and the registry row becomes a tombstone comment saying why it must not come back: registering the collection is only half the contract, the walk has to match the collection's shape. For each bundle, each locale entry'sTranslationDatais checked, with a subject naming both:Non-record bundles and non-record locale values are skipped, which keeps the function's own "advisory only — returns findings, never throws" contract across the two new levels.
One deviation from the shape the card and triage endorsed, declared: the subject carries the bundle index as well as the locale (the card suggested
translation bundle · locale 'zh-CN'). Bundles are an unnamed array and one locale can appear in more than one of them, so without the index the finding does not say which bundle to open. It is the same addressing the sibling rulevalidate-translation-referencesalready reports (translations[0]["zh-CN"]), rendered in this file's prose-subject convention.Non-vacuity: the new test fails on the pre-fix code
Ruling 3 of the dispatch. The test block was written and run before the source change, on an otherwise untouched tree (
pnpm --filter @objectstack/lint test):The four failures are the four load-bearing assertions, and two of them fail in opposite directions — which is what makes this a discriminator rather than a fixture that happens to be red:
warns on a warned group authored under a locale entry—AssertionError: expected false to be true. The locale-keyed bundle produced no finding on the broken walk.does not treat a runtime TranslationItem shape as a bundle—AssertionError: expected [ { …(4) } ] to deeply equal [], the received finding beingsets \flows` but this translation property has no runtime effect (liveness: dead). TheTranslationItem`-shaped fixture did warn on the broken walk. That is exactly why ruling 2 requires the pin to be bundle-shaped: an item-shaped fixture would have been green from the day the bug shipped and would have pinned nothing. It is kept in the file as an anti-fixture so nobody "repairs" the bundle fixtures into the shape that cannot fail.reaches every locale of every bundle, not just the first of each— authored on the second locale of the second bundle, so neither level of the walk can stop at index 0.never throws on a malformed bundle—TypeError: Cannot read properties of null (reading 'name')on the pre-fix walk.After the fix, on the same tree:
Test Files 80 passed (80) · Tests 2257 passed (2257), with all six new cases named individually by--reporter=verbose.Fixture triage: the pre-existing
#4667silence pin authoredtranslations: [{ name: 'zh_cn', locale: 'zh-CN', messages: … }]— item-shaped. It stayed green in both directions (it asserts silence), so it pinned nothing about this collection either way; it is now bundle-shaped, which is whatstack.translationsactually holds.The side question the card left to the dev: is the runtime door reached at all?
No — and it cannot be, for two independent reasons. Measured, not reasoned from the card:
StackDefinitionSchemahas exactly one translation-shaped key,translations: z.array(TranslationBundleSchema).TranslationItemSchemaappears in the kernel's metadata-type registry (packages/spec/src/kernel/metadata-type-schemas.ts:158) and nowhere in the stack.authoring-rules.tsit is declaredsurfaces: CLI_ONLY, so it is not among the rules the runtime publish gate selects (runtime-gate.tsfilters onsurfaces.includes('runtime-publish')).There is also a third, softer reason the item shape cannot arrive through this door by accident:
stack.translationsparses asz.record(LocaleSchema, TranslationDataSchema), so an item-shaped entry would have to mean a locale namedflowswhose value isTranslationData— a parse error two tiers before the advisory ever runs.So there is one door, not two, and the ledger's own subject is the other one: its
_notedescribesTranslationItemSchema. The two doors share the group vocabulary and not the container; only the file-authored one is lintable from here, and now it is linted. Both facts are recorded in the source comment so the next reader does not have to re-derive them.Can this redden a currently-green tree?
Checked before pushing, since a newly-firing advisory on unrelated PRs would change the landing plan.
os lintexits 1 only onerrors.length > 0(packages/cli/src/commands/lint.ts:634); warnings never move the exit code. The finding istier: 'advisory', emitted asseverity: 'warning'.os lint --eval, which exits 1 on!report.okagainst a scored corpus. It is invoked by no workflow in.github/workflows/.translation.flows. No file in the tree authors aflowsgroup inside a translation bundle (scannedexamples/*/src/**translations plus a repo-wide grep). So the new walk produces zero new findings in-tree today, and PM's assumption holds.Gates
Derived from the actual changed paths with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(change set read by the script from the merge base, not from a hand-built diff), then re-run in full against the final commit dd0e782 — 20 families, all green, plus the package suite, the package typecheck, and repo-widepnpm lint(eslint . --no-inline-config, run whole rather than narrowed: 85s, clean).Out of scope, filed rather than fixed: two findings on this same file are recorded on the issue thread — the
plannedstatus being reported with theliveness-dead-propertyvocabulary, and the flat-collection walk throwing on a null item.Generated by Claude Code