Skip to content

Commit 6f8d751

Browse files
os-elon-muskclaude
andauthored
fix(lint): translation-target-unknown resolves the two declared contribution surfaces (#19060)
Fixes #18441 Fixes #18442 Clause-②: no Judged against the actual diff, not predicted: two files under `packages/lint/src` plus one changeset. No `packages/spec` path (0 files), no Zod schema, no closed-set member, and no published export moved — the diff adds exactly one line beginning `import` (in the test file, `@objectstack/spec/data`) and no `export` line at all. `packages/spec/api-surface-declarations/**`: **0** of the 17 shards touched, so this face stays clear of the live three-way collision there. ⚠️ `scripts/pm/check-widening-tells.mjs --declaration no` over this diff exits 0 but reports **2 files NOT MEASURED — no declared surface covers `packages/lint`** — so that instrument is not evidence here in either direction, and the declaration above rests on the reading spelled out in this paragraph. ## The defect, in one sentence per card `translation-target-unknown` built its universe from a stack's AUTHORED collections alone, so a locale key for something a package **contributes** into metadata another package owns was reported as an orphan at severity `error` — the run failed — with an orphan's remedy, which deletes a translation the runtime resolves. ## Measured, with its control, before the fix Instrument: `validateTranslationReferences(stack)` called directly, unit = findings; taken in this worktree at `abb01f105` (the branch point). | probe | before | after | |---|---|---| | **#18441** `objects: [crm_lead { name }]` + `objectExtensions: [{ extend: 'crm_lead', fields: { sla_tier } }]` + a `zh-CN` key for `sla_tier` | **1** `error` at `translations[0]["zh-CN"].objects.crm_lead.fields.sla_tier`, remedy *"Point the key at a declared field, or drop it"* | **0** | | ⭐ its control — a genuinely undeclared `zzz_gone` on the SAME stack | **1** `error`, message and remedy identical but for the name ⇒ **a correct author and a real typo were indistinguishable in the output** | **1** `error`, and the remedy now enumerates `name, sla_tier` | | the sibling rung — a `zh-CN` key for a validation rule the same extension merges in | **1** `error` at `…._validations.sla_required` | **0** | | the cross-package shape — an extension target this stack does not DEFINE | **1** `error` on the object key: *"which no object in this stack defines … this whole subtree is dead weight"* | **0** | | **#18442** `packageBodyAsStack(contributorBody, artifactPackages)`, contributor carries `navigationContributions` + `translations` and no apps, app owner INSIDE the artifact | **1** `error` at `translations[0]["zh-CN"].apps.crm_enterprise`: *"which this stack does not define"*, remedy *"Match the key to an app's `name`, or drop it"* | **0** | | the same with the app owner OUTSIDE the artifact | **1** `error`, identical | **0** | | ⭐ its control — `apps.zzz_not_an_app`, neither declared nor contributed into | **1** `error` | **1** `error`, remedy now lists `crm_enterprise` | The #18441 control row is the card's grading in one line: it IS the defect. The fix is judged on that row staying lit, not on the first row going quiet. ## The fix One fold per declared surface, folded into the universe the rule already builds: - **`objectExtensions[]`** (`objectExtensionsByTarget`) — the fields and validation rules an extension merges into its target. Read from `stack.objectExtensions` **and** `packages[].manifest.objectExtensions`, for the reason #16611 gave for object names: the release artifact is the co-ownership boundary, so a per-package leg that cannot see a sibling package's extension reports the RUN's blind spot as the author's mistake. Without the second carrier the union leg and the per-package leg would disagree about one key. - **the app-name rung** — an app this stack contributes into without declaring becomes resolvable, carrying exactly the ids contributed. ⛔ This is not a new resolution-context decision: the runtime's own contribution diagnostic already took it, one field over and in the opposite direction of severity. `checkNavContributionGroups` yields NOTHING for a contribution whose target app is absent, because 「a package may legally contribute into an app shipped by a DIFFERENT artifact installed separately」 and reporting the absent-app case there 「would refuse the supported cross-artifact case at build time」. A rule that FAILS the run over the translation of such a contribution refuses that same supported case, through the one door an author cannot argue with. Both folds widen what a key may RESOLVE against and nothing else. No severity moved, no path is exempted, no rule was disabled, and nothing was baselined or quarantined. ## Two bounds, both measured rather than assumed - **The extension fold is exactly two rungs wide, because `ObjectExtensionSchema` is.** The card's reason for not fixing this in place cited a fold surface of "fields, views, actions, sections, tabs, validations". Measured against the schema (instrument: `ObjectExtensionSchema.safeParse`, unit = issues, key refusal separated from value rejection by `code === 'unrecognized_keys'` at path length 0): `fields`, `validations`, `indexes`, `label`, `pluralLabel`, `description` parse; `views`, `listViews`, `actions`, `fieldGroups`, `sections`, `tabs`, `hooks` are each refused BY NAME at the extension level. ⇒ only `fields.*` and `_validations.*` are reachable through an extension, so a `_views` / `_sections` / `_tabs` / `_actions` key on an extended object is an orphan exactly as before — pinned, with the schema surface itself pinned beside it so the sizing cannot silently stop being true. The card's DEFECT premise holds; that one sentence of its reasoning was wider than the schema. - **An extension target this stack does not DEFINE is rung 2b of the cross-package ladder**: the object key resolves (the extension is proof the stack means that name) and the subtree is skipped WHOLLY, for the reason rung 2 skips a registered platform object's. A package that only extends an object cannot see the owner's field set, so judging the subtree against the injected names alone would report the owner's own field keys as orphans — the same defect one level up. The sibling rule already takes that decision on this surface (`validate-object-references.ts`: `objectExtensions[].fields` is deliberately not walked, because judging it "would refuse the legitimate cross-ARTIFACT case by the rule that exists to catch the typo"). ## Tests — every widening carries a lit control `packages/lint/src/validate-translation-references.test.ts`, 20 new cases in two blocks. Both card shapes, and for each: the correct-author case silent, the typo case still `error` with the rule id and severity pinned, and the two of them on ONE bundle so "one finding, and it is the typo's" is the assertion. Controls that must keep firing, and do: a typo'd field on an extended object; a `_validations` name no layer declares; an object neither defined nor extended; an app neither defined nor contributed into; a contributed id nothing contributes; an id contributed into app B keyed under app A; an extension entry with no `extend` (makes nothing addressable); a declared app keeping its own diagnosis, so the new branch cannot leak into the population #18203 covers. ## Reverse verification — the ablation Fix committed first (`8be3be14a`), then the RULE file alone restored to its pre-fix bytes with the new tests left in place: - mutation landed, proven by hash, not by an exit code: on-disk `ca631c6215ef4edd86b51353ec746919f2bb777d` = the `abb01f105` blob, ≠ the HEAD blob `952315ff1495647ec1ed2cf093b5b8a9191c7649`; `grep -c` for both fold markers in the ablated file = **0**. - run: **16 failed | 80 passed (96)**. Direction: RED, as predicted. The 4 new cases that stay green are the ones that must hold in BOTH worlds — the no-`extend` guard, the "folds no other rung" case, the schema surface pin, and the declared-app diagnosis. - restored with `git checkout HEAD --` (not the bare form, which would have taken the mutation back out of the polluted index), proven by `git diff HEAD` empty AND the on-disk hash back to `952315ff…`; re-run from the restored state: **96 passed (96)**. ## Local verification | leg | command | reading | |---|---|---| | ① dependency closure | `pnpm --filter '@objectstack/lint^...' build` | exit 0 | | ② affected package | `pnpm --filter @objectstack/lint test` | **104 files, 3925 passed / 5 skipped**, exit 0 | | ② affected package | `pnpm --filter @objectstack/lint typecheck` | exit 0 (`tsc --noEmit` + `check:test-typecheck`, 2 files / 6 errors / 2 pinned signatures held in the shrink-only ledger, unmoved) | | ② build | `pnpm --filter @objectstack/lint build` | exit 0, `check-dts-emitted: 4/4` | | ③ derived gate families | `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` over the real change set, then every command it printed | **54 of 57 exit 0** | | repo-wide style | `pnpm lint` (`eslint . --no-inline-config`) | exit 0, whole repo, no narrowing claimed | ⊘ **NOT MEASURED**, 3 families, each refusing loudly with `PREREQUISITE NOT MET` rather than passing: `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt` — all three read BUILT output of the whole workspace (77+ packages with no `dist` in this worktree), which is CI's `Build Core` / `TypeScript Type Check`. ⛔ Neither a pass nor a finding. `check:docs-transcript-drift` first read the same refusal and was then MEASURED after `packages/lint` was built: exit 0. ## Changeset `.changeset/18441-translation-target-unknown-contribution-surfaces.md`, **`@objectstack/lint`: patch** — ⛔ not `skip-changeset`. `@objectstack/lint` publishes (`files: [dist, README.md, CHANGELOG.md]`, `publishConfig.access: public`) and the rule ships inside that `dist`; its verdict is what `os build` / `os validate` / `os lint` print, so an author upgrading past a run that used to fail on their correct keys greps the CHANGELOG for exactly this sentence. A behaviour fix in a released package takes `patch`, and the diff moves nothing that would take more: `Clause-②: no`, non-breaking, so no ADR-0087 disposition applies. ## Acceptance notes Noted, not filed — out of scope for this PR, no card: - **The same `#16611` gap on this rule's OBJECT rung.** Measured here on the per-package leg (`packageBodyAsStack`): a package translating an object a SIBLING package of the same artifact declares still reports **1** `error` on the object key, because `buildUniverse` reads `stack.objects` and never `packages[].manifest.objects`. It is the shape `artifactProvidedObjectNames` already closed in `validate-object-references.ts`, on a rung neither card names, so it is left alone and reported rather than carried in. Carrier when someone takes it: the same file. Dedupe words: `translation-target-unknown`, `artifactProvidedObjectNames`, `per-package leg`, `sibling package object`, `packageBodyAsStack`. - **A doc nit inside the rule's own header** — the §4 ladder still says rungs 3 and 4 "warn on the object key only", wording that predates the #16310 promotion of this rule to `error`. Not corrected here: a wording change in a header this PR is already editing reads as part of the fold, and nothing mechanical is wrong. --- _Generated by [Claude Code](https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a362e0e commit 6f8d751

3 files changed

Lines changed: 701 additions & 20 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
`translation-target-unknown` no longer reports the locale keys a package ships for what it CONTRIBUTES into metadata another package owns — `objectExtensions[]`-injected fields and validation rules, and the navigation items it contributes into an app it does not declare (#18441, #18442).
6+
7+
Both were `error`, so each one FAILED the run it appeared in, and both carried the orphan remedy — *"Point the key at a declared field, or drop it"*, *"Match the key to an app's `name`, or drop it"* — which deletes a translation the runtime resolves. Measured on the two probe stacks:
8+
9+
- `objects: [crm_lead { name }]` + `objectExtensions: [{ extend: 'crm_lead', fields: { sla_tier } }]` + a `zh-CN` key for `sla_tier` produced one `error` at `translations[0]["zh-CN"].objects.crm_lead.fields.sla_tier`. A genuinely undeclared field on the same stack produced a finding identical but for the name, so **a correct author and a real typo were indistinguishable in the output** — an author who extended an object correctly was told their correct key was wrong, in a run that failed.
10+
- in `os build`'s per-package leg, a contributor package carrying `navigationContributions` and no apps of its own was told app `crm_enterprise` is one *"which this stack does not define"* — whether or not the app's owner was an entry of the same artifact. Declaring that app is the owning package's job; the contributor cannot do it.
11+
12+
Both folds widen what a key may RESOLVE against and nothing else, so every genuine orphan still reports at `error` with the rule id intact: a typo on an extended object, a `_validations` name no layer declares, an object neither defined nor extended, an app neither defined nor contributed into, and a contributed navigation id nothing contributes are each pinned as a control beside the case they neighbour.
13+
14+
Two bounds worth reading before widening either fold further:
15+
16+
- **The extension fold is exactly two rungs wide because `ObjectExtensionSchema` is.** The declared entry keys are `extend`, `priority`, `fields`, `validations`, `indexes`, `label`, `pluralLabel` and `description`; `views`, `listViews`, `actions`, `fieldGroups`, `sections`, `tabs` and `hooks` are refused BY NAME at the extension level with authoring guidance. So `fields.*` and `_validations.*` are the only rungs of this rule an extension can reach, and a `_views` / `_sections` / `_tabs` / `_actions` key on an extended object is an orphan exactly as before. A new pin asserts that surface against the schema, so the sizing cannot silently stop being true.
17+
- **An extension target this stack does not DEFINE is rung 2b of the cross-package ladder**: the object key resolves — the extension is proof the stack means that name — and the subtree is skipped WHOLLY, for the reason rung 2 skips a registered platform object's. The owner's field set is not visible from a package that only extends it, and judging the subtree against the injected names alone would report the owner's own field keys as orphans, which is the same defect one level up.
18+
19+
No schema moved, no export moved, and no accept set moved: this is a lint rule's false-positive set narrowing. `Clause-②: no`

0 commit comments

Comments
 (0)