You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 2b3eb17
Browse filesBrowse the repository at this point in the historyBrowse files
fix(lint): the translation rule's OBJECT rung reads what the artifact's packages[] provide (#19347)
Fixes#19064
`translation-target-unknown`'s OBJECT rung built its universe from
`stack.objects` alone while this rule's own docblock already declared
the wider reach `validateObjectReferences`'
`artifactProvidedObjectNames` uses. The declaration and the
implementation disagreed inside one file, and on `os build`'s
per-package leg the author paid for it.
Clause-②: no
## The measurement, re-taken on `origin/main` before anything was
touched
PR #19060 rewrote this very file on 2026-09-20, so the card's premise
was re-measured first-hand at `74fb2f7a80` rather than inherited. Every
reading below is a PRINT of every hit, never a count:
```
$ git grep -n -E 'stack\.objects|manifest\.objects' -- packages/lint/src/validate-translation-references.ts
:752 * registry merges the layers. So a universe built from `stack.objects` alone
:851 for (const obj of recordsOf(stack.objects)) { ← the OBJECT rung's only universe
⇒ `manifest.objects`: ZERO hits. The query asks for BOTH tokens, so the absence is a reading.
LIT CONTROL — same query form, same corpus, the rule that already closed this shape:
$ git grep -n 'artifactProvidedObjectNames' -- packages/lint/src/
validate-object-references.ts:163 function artifactProvidedObjectNames(stack: AnyRec)
validate-object-references.ts:210 for (const n of artifactProvidedObjectNames(stack)) resolvable.add(n);
validate-translation-references.ts:692 the docblock that CLAIMS the reach — and nothing that implements it
```
⇒ the instrument finds the token where it exists, so its absence in the
translation rule is a reading and not a dark grep.
**The diagnostic, re-run rather than relayed** (the probe is
`examples/app-multi-package`'s shape: `core` owns `crm_account`,
`orders` translates it, judged on the per-package leg
`packageBodyAsStack(body, entries)` builds):
```
translations[0]["zh-CN"].objects.crm_account error · translation-target-unknown
Translations are keyed to "crm_account", which no object in this stack defines. The
resolver looks up keys derived from the metadata, so this whole subtree is dead weight …
Rename the key to the object it was written for, drop it, or ignore this if the object is
contributed by another installed package. Defined objects: crm_order.
```
That is the remedy that deletes a translation the runtime resolves, at
`error`, so the run FAILS on it — and it is byte-identical, but for the
name, to what a genuine typo yields on the same stack.
⚠️ The card body still carries `Blocked-by: #18442`. Re-taken here:
#18441 and #18442 read `closed` / `completed` and #19060 landed as
`6f8d751bc1`. The line is stale; it is not a reason to stop.
## The precedent — followed, and the judgement the card asked for
`validateObjectReferences` closed this exact shape on this exact carrier
for object NAMES (#16611). **It cannot be reused as-is, and the
difference is the RETURN, not the reach**:
| | `artifactProvidedObjectNames` (#16611) | `artifactProvidedObjects`
(here) |
|:--|:--|:--|
| carrier | `packages[].manifest.objects` | the same, unchanged |
| entry shape | ADR-0130 D4 `{ manifest: body }`, no readable body
contributes nothing | the same |
| returns | `string[]` — that rule asks only whether a reference
RESOLVES | the RECORDS — this universe is keyed by FACTS (the fields,
options, views, sections and rules a key may address beneath
`objects.NAME`) |
A name-only fold here would resolve the object key and then judge the
owner's own field keys against an EMPTY fact set — reporting every one
of them as an orphan, which is the same false positive one level up and
the mistake `objectExtensionsByTarget` records for a target it cannot
see. A wholesale subtree SKIP (rung 2b's answer) is wrong for the
opposite reason: rung 2b skips because the owner's field set is
genuinely invisible from a package that only EXTENDS the target, whereas
a sibling's declaration is fully readable here — skipping would leave
the per-package leg unable to see a typo the union leg reports. So the
records are folded through the SAME collector the declaration loop uses,
and the subtree stays judged.
Where both the stack in hand and a sibling declare the same field or
action name, the record this leg is JUDGING keeps the slot: which
layer's `options` a merged field carries is the registry's precedence
question, and `checkOptionKeys` is the only consumer of the stored
definition — the same decision the `objectExtensions` fold took one
collection over.
## Both control directions, pinned side by side
A false-positive fix without a false-NEGATIVE control is the shape this
repo keeps carding, so every pin below is in the same describe block as
the acceptance it neighbours:
| direction | pin |
|:--|:--|
| the false orphan is gone | the sibling-declared object key, and its
field / option / `_validations` keys, resolve |
| ⭐ CONTROL — the context is what does the work | the same bundle, the
same package judged ALONE, still errors |
| ⭐ NON-DEGENERACY | a name NO entry of the artifact declares is still
`error`, and the remedy now enumerates what the artifact provides
(`Defined objects: crm_account, crm_order.`) |
| ⭐ NON-DEGENERACY, one rung down | a field the sibling does not declare
is still `error` under the now-resolved object (`Declared fields:
industry, name.`) |
| both on ONE bundle | a sibling key beside a typo reports exactly the
typo |
| the widening is not a path | an entry with no readable body (a segment
reference) makes nothing addressable |
| no collateral widening | the single-`defineStack` shape is untouched —
`objects` is a STACK collection, so there is no `stack.manifest.objects`
form to read |
## Reverse verification, from the COMMITTED fix
`scripts/ablation-replace.mjs` deleted the fold's two call lines and ran
the file's suite, with the mutation proved on disk rather than by an
exit code:
```
anchor x1 -> x0 · blob 21d35a8 -> 7995be70fa39 (the mutation LANDED)
Tests 5 failed | 100 passed (105) (the expected direction: RED)
restore: blob after restore == blob at HEAD (21d35a8) and `git diff HEAD` is empty
```
All five reds are the new pins — the acceptances AND both non-degeneracy
controls, whose hint assertions are themselves non-vacuous.
## Checks — real exit codes
| command | exit |
|:--|:--|
| `pnpm --filter '@objectstack/lint^...' build` (dependency closure) |
**0** |
| `pnpm --filter @objectstack/lint test` — 106 files, 4000 passed / 5
skipped | **0** |
| `pnpm --filter @objectstack/lint typecheck` (incl.
`check:test-typecheck`) | **0** |
| `pnpm lint` (repo-wide `eslint . --no-inline-config`) | **0** |
| 53 of the 57 families `scripts/pm/dispatch-gates.mjs --commands`
derives for this diff | **0** each |
| `pnpm check:docs-transcript-drift` — re-run after `pnpm --filter
'@objectstack/lint...' build`, since it derives from the BUILT registry
| **0** |
⊘ NOT MEASURED locally, declared to CI — each exited **3**, which these
gates define as "nothing was measured", neither a pass nor a finding:
`check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:type-check-debt` all refuse without a whole-repo `pnpm build`.
The artifact-roster and whole-tree families the derivation prints as
unplaceable are CI's runs, not this PR's.
## Acceptance notes
- `content/docs/protocol/kernel/i18n-standard.mdx`'s key table still
describes `objects.{object}` as "an object this stack defines, or a
platform object". That row already understated the universe after #18441
added the `objectExtensions[]` target rung and nothing updated it; it is
a doc completeness nit on a page this PR does not touch, noted rather
than filed, and the next rung card on this rule is the natural carrier.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_
Co-authored-by: Claude <noreply@anthropic.com>
`translation-target-unknown` no longer reports the locale keys a package ships for an object a SIBLING package of the same artifact declares — the OBJECT rung's universe read `stack.objects` alone, while this rule's own docblock already declared the wider `artifactProvidedObjectNames` reach (#19064).
6
+
7
+
`os build` runs the rule table per PACKAGE as well as over the union (`compile.ts` step 3b-ii): each package body is judged as its own stack with the artifact's `packages[]` beside it as resolution context (`packageBodyAsStack`, #16611). On that leg `stack.objects` holds ONE package's objects, so a bundle key naming a sibling's object resolved against nothing. Measured on the probe stack (`examples/app-multi-package`'s shape — `core` owns `crm_account`, `orders` reads it and here also translates it), the key produced one `error` at `translations[0]["zh-CN"].objects.crm_account`:
8
+
9
+
> Translations are keyed to "crm_account", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight — every label it carries renders untranslated.
10
+
>
11
+
> *Rename the key to the object it was written for, drop it, or ignore this if the object is contributed by another installed package. Defined objects: crm_order.*
12
+
13
+
That is the remedy that deletes a translation the runtime resolves, at `error`, so the run FAILED on it — and it is byte-identical, but for the name, to the finding a genuine typo produces. ADR-0130 makes the release artifact the co-ownership boundary, so the miss is the RUN's blind spot and not the author's mistake.
14
+
15
+
**The precedent is followed, not re-decided.**`validateObjectReferences` closed this exact shape on this exact carrier for object NAMES (#16611 — `artifactProvidedObjectNames` folded into its `resolvable` set). What differs here is the RETURN, and two pins hold it: this rule's universe is keyed by FACTS, not names, so the sibling's fields, options, views, sections and rules are folded WITH the name through the same collector the declaration loop uses. A name-only fold would resolve the object key and then judge the owner's own field keys against an empty fact set — the same false positive one level up — and a wholesale subtree skip (rung 2b's answer, for a target whose declaration is genuinely invisible) would leave the per-package leg unable to see a typo the union leg reports.
16
+
17
+
**The control, which is what makes this a narrowing and not a hole.** Widening a universe trades a false positive for a blind spot unless every genuine orphan still reports, so both directions are pinned side by side: the same package judged ALONE still errors (the context is what does the work); a name no entry of the artifact declares is still an `error` with its rule id, and the remedy now enumerates what the artifact provides; a field the sibling does not declare is still an `error` under the now-resolved object; one bundle carrying both a sibling key and a typo reports exactly the typo; an entry with no readable body (a segment reference) makes nothing addressable; and the single-`defineStack` shape is untouched, because `objects` is a stack collection with no `stack.manifest` form to read.
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