Skip to content

Commit c3a4c74

Browse files
os-warrenclaude
andauthored
fix(lint): the translation rule's remaining collection rungs read what the artifact's packages[] provide (#19625)
Fixes #19349 `translation-target-unknown` built the universe for six collection rungs from the top-level collection alone, while the same file already read `packages[].manifest.…` for three others. On `os build`'s per-package leg a package translating what a SIBLING package of the same artifact declares was reported as an orphan at `error` — a loud, wrong refusal whose remedy deletes a translation the runtime honours. Clause-②: no ## Line numbers, re-derived by SHAPE on my own tree — and the card's divergence resolved The card records two seats' numbers differing by about 90 lines and deliberately does not guess the cause. Re-derived at merge base `0b4022b41c`, by grepping for `recordsOf(stack.CONST)` and its `recordsOf(stack.packages)` siblings rather than by coordinate: ``` reads packages[] ALREADY only the TOP-LEVEL collection :733 navigationContributions :964 objects (closed by #19064 / PR #19347) :828 objectExtensions :1034 views :886 objects (#19064) :1040 pages :1060 actions :1091 apps :1146 dashboards :1177 flows ``` ⭐ The divergence is arithmetic, not a mystery. Every one of the six shifted by **exactly +96**: ``` views 938->1034 · pages 944->1040 · actions 964->1060 apps 995->1091 · dashboards 1050->1146 · flows 1081->1177 all delta = +96 ``` PR #19347's net change to this one file is `+104 / -8` = **+96**, and it inserted `artifactProvidedObjects` and its docblock ABOVE every collection rung. So the triage seat measured on `2277d1fcd1` (before #19347 landed) and the construction seat's numbers were taken with #19347 applied. Neither set was wrong; they named different trees. My tree is post-merge and matches the construction seat exactly. ## The reproduction — both layers probed first-hand, ⛔ nothing inherited A throwaway probe (deleted in the same act; `git status --porcelain` verified empty, 0 lines) ran every level on the per-package leg `packageBodyAsStack(body, entries)` builds. **Layer A re-reproduced** rather than relayed, **layer B probed** where the card explicitly had not: | layer | level | probe result | |:--|:--|:--| | A | `dashboards` | 1 `error` · `translations[0]["zh-CN"].dashboards.crm_overview` | | A | `flows` | 1 `error` · `….flows.lead_conversion` | | A | `globalActions` | 1 `error` · `….globalActions.export_all` | | ⭐ B | `views` | 1 `error` · `….objects.crm_order._views.board` | | ⭐ B | `pages` | 1 `error` · `….objects.crm_order._tabs.mine` | | ⭐ B | `apps` | 1 `error` · `….apps.crm_app` | ⭐ **`apps` is partly covered, and the card was right to demand that be judged first — but BOTH halves were broken.** #18442 closed the *contributed-app* level, which reads `manifest.navigationContributions`: contributions, never declarations. A sibling's `apps[]` declaration was invisible either way, so a second probe was run: - **contributing nothing** into the sibling's app, the app NAME itself is the orphan (the reading above); - **contributing one item**, the name resolves through #18442, but the owner's own navigation ids are orphans — 1 `error` at `….apps.crm_app.navigation.leads`, diagnosed "this stack contributes no such item", advising a move to a package sitting in the same artifact. ⇒ #18442 covers neither half of the DECLARATION question. Both are closed here. ⚠️ `views` and `pages` are deliberately bound to `crm_order`, the object the TRANSLATING package declares, so these readings do not ride on #19064's object fold. If that rung regressed, the cases would fail for the wrong reason and the `_views` / `_tabs` readings would be worthless. ## The carrier is proven, not assumed Every one of the six keys carries disposition `concat` in `COMPOSE_KEY_DISPOSITIONS`, which is exactly what puts it inside `ASSEMBLED_PACKAGE_BODY_DISPOSITIONS` and so inside an ADR-0130 D4 entry's assembled body — the same proof `objectExtensionsByTarget` (#18441) and #19064's fold rest on. `objects` carries disposition `objects`, also in that set. ⛔ And there is no `stack.manifest.KEY` form to read beside it: all six are STACK collections, not manifest keys. `artifactProvidedObjects` is generalised to `artifactProvidedRecords(stack, collection)` — one reader of that carrier for all seven call sites, rather than six copies free to disagree about the entry shape. ## Records or names — decided PER LEVEL, with three different reasons ⛔ Not one assumption applied six times. All six fold **records**, but not for the same reason: | levels | why RECORDS, and why names would be wrong | |:--|:--| | `dashboards`, `flows`, `apps` | keyed by their own name, with a SUB-RUNG derived from the record (widget ids and header `actionUrl`s; screen node ids and each screen's `config.fields[].name`; navigation ids). Names alone would resolve the top key and then judge that sub-rung against an EMPTY set — #19064's trap, moved one rung down. For `flows` the record also carries `otherNodes`, which is a DIAGNOSIS ("declares it as a TYPE node, not a `screen`") a name-only fold would lose. | | `actions` | keyed by name, but the stored RECORD is read downstream — `checkActionParams` judges `params.NAME` off it. ⚠️ And the OWNER is read from the record, which is what keeps the widening honest: a sibling action bound to an object joins that object's `_actions`, never `globalActions`. Registering it globally would make legal a key the resolver never reads. | | `views`, `pages` | there is no name to fold. These have **no bundle rung of their own at all** — they contribute `_views`, `_sections` and `_tabs` FACTS under the object they bind to, so the record is the only thing carrying both the fact and its binding. | **Precedence** follows #19064: where the stack in hand and a sibling declare the same name, the declaration this leg is JUDGING keeps the slot (`ownDeclaration`), for `actions`, `dashboards` and `flows` — the three whose fold carries a value. `views` and `pages` contribute only Set adds, so two adds are one add and no precedence rule is needed. `apps` accumulates nav ids into one Set per name, which is the merge the declaration loop already performs for two same-named entries and what `concat` composition delivers at runtime. ⚠️ **Order is load-bearing for `apps`:** the artifact fold runs BEFORE the contributed-only pass, so a sibling-declared app is a DECLARED app and never acquires the `contributedOnly` diagnosis. An app owned OUTSIDE the artifact keeps #18442's wording, pinned below. ## Both control directions, per level A false-positive fix without a false-NEGATIVE control is the shape this repo keeps carding, so every acceptance sits beside its control in the same block: | direction | pin | |:--|:--| | the false orphan is gone | each level's sibling-declared key, and its sub-rung keys, resolve | | ⭐ CONTROL — the context does the work | for every level, the same bundle judged ALONE still errors | | ⭐ NON-DEGENERACY | a dashboard / flow / action / app / view / tab NO package of the artifact declares still errors, and each remedy enumerates what the artifact provides | | ⭐ NON-DEGENERACY, one rung down | a widget id, header action, screen id, screen field and nav id the sibling does not declare still error under the now-resolved parent | | ⭐ routing | an object-BOUND sibling action keyed under `globalActions` still errors, with its "is bound to object" redirect | | ⭐ precedence | a sibling's same-named dashboard does NOT make its widget ids addressable under this package's dashboard | | ⭐ #18442 kept intact | an app contributed into but owned OUTSIDE the artifact keeps the contributed-only wording | | the widening is not a path | an entry with no readable body (a segment reference) makes nothing addressable, on every rung | | no collateral widening | the single-`defineStack` shape is untouched — all six are stack collections with no `stack.manifest` form | One fixture correction is worth naming: the `record:details` case first used a `regions` spelling the shared page walk does not recognise, so it failed after the fix. That was a wrong FIXTURE, not a wrong fix — corrected to the `regions[].components[]` shape the file's own section-anchor block uses, and the case then passed in both directions. ## Reverse verification, from the COMMITTED fix `scripts/ablation-replace.mjs`, one anchor, guarding the fold so that ONLY the six new rungs are neutralised and #19064's object fold survives — which makes the reds provably this PR's: ``` anchor " provided.push(...recordsOf(body[collection]));" x1 -> x0 blob d6b2706 -> 42ee18972671 (the mutation LANDED) Tests 18 failed | 114 passed (132) (expected direction: RED) restore blob == HEAD (d6b2706) and `git diff HEAD` is empty ``` - The 18 reds are **exactly** the 18 that were red before the fix, and **every one is in this PR's block** — no test of the #19064 object rung went red, which is the targeting proof. - Every level's NON-DEGENERACY control is among the reds, so those hint assertions are not vacuous. - ⭐ No build ran between the mutation and the child, and the child went RED — which proves the suite resolves this rule through `src/`, not a stale `dist/`. That is the false-green hazard closed by observation rather than by assertion. - Restore verified by **observed state**, never an exit code: disk blob equals the HEAD blob, `git diff HEAD` is 0 lines and `git status --porcelain` is 0 lines. ## Checks — real exit codes, captured before any pipe | command | exit | |:--|:--| | `pnpm --filter '@objectstack/lint^...' build` (dependency closure) | **0** | | `pnpm --filter @objectstack/lint test` — 108 files, 4106 passed / 5 skipped | **0** | | `pnpm --filter @objectstack/lint typecheck` (incl. `check:test-typecheck`) | **0** | | `pnpm lint` — repo-wide `eslint . --no-inline-config`, ⛔ not narrowed | **0** | | 56 of the 59 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` | **0** | `dispatch-gates --ran` reconciles: **59 derived, 56 run, 3 NOT-MEASURED, 0 UNRUN**, with every family carrying a recorded exit code so the NOT-MEASURED count is derived rather than claimed. The derivation was re-taken after `git fetch origin main` and was byte-identical. ⊘ **NOT MEASURED locally, declared to CI** — each exited **3**, which these gates define as "nothing was measured", neither a pass nor a finding. All three refuse without a whole-repo `pnpm build`: `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt`. ## Acceptance notes - `content/docs/protocol/kernel/i18n-standard.mdx`'s key table describes `objects.{object}` as "an object this stack defines, or a platform object". #19347 noted the same row as already understating the universe and named "the next rung card on this rule" as its natural carrier — that is this card, and this change widens the same table's understatement further, now across six more rungs. ⛔ It is still not filed and not fixed here: the declared file surface for this card is this rule, its tests and `.changeset/`, and `content/docs/` is outside it. Noted for whoever owns that page. - The file header documented the `packages[]` reach for the object ladder only, so the implementation would have reached wider than the declaration — the same mismatch, reversed, that got the object rung carded. One paragraph in the header now names the single carrier reader, the disposition table that decides which collections may be asked of it, and why `apps` reads both that carrier and `navigationContributions`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- _Generated by [Claude Code](https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 49d5069 commit c3a4c74

3 files changed

Lines changed: 628 additions & 22 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
`translation-target-unknown` no longer reports the locale keys a package ships for a view, page, action, app, dashboard or flow a SIBLING package of the same artifact declares. Six collection rungs built their universe from the top-level collection alone, while the same file already read `packages[].manifest.…` for `navigationContributions` (#18442), `objectExtensions` (#18441) and `objects` (#19064) — so the capability was present and these rungs did not use it (#19349).
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 each `stack.<collection>` holds ONE package's declarations. Measured on a throwaway probe before anything was touched, each level produced exactly one `error`, at `translations[0]["zh-CN"].dashboards.crm_overview`, `….flows.lead_conversion`, `….globalActions.export_all`, `….objects.crm_order._views.board`, `….objects.crm_order._tabs.mine` and `….apps.crm_app` — each carrying a remedy (`or drop it`) that deletes a translation the runtime honours, at a severity that FAILS the run.
8+
9+
**The carrier is proven rather than assumed.** Every one of these keys carries disposition `concat` in `COMPOSE_KEY_DISPOSITIONS`, which is precisely what puts it inside `ASSEMBLED_PACKAGE_BODY_DISPOSITIONS` and so inside an ADR-0130 D4 entry's assembled body — the same proof the `objectExtensions` and `objects` folds rest on. 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.
10+
11+
**Records, not names — and for three different reasons, not one assumption applied six times.** `dashboards`, `flows` and `apps` are keyed by their own name and carry a sub-rung derived from the record (widget ids and header `actionUrl`s, screen node ids and their field names, navigation ids), so a name-only fold would resolve the top key and then judge that sub-rung against an empty set. The `actions` record is itself read downstream by `checkActionParams`, and it carries the owner that keeps an object-bound action under `objects.<obj>._actions` instead of making it globally addressable. `views` and `pages` have no bundle rung of their own at all — they contribute `_views`, `_sections` and `_tabs` facts under the object they bind to — so the record is the only thing carrying both the fact and its binding.
12+
13+
**`apps` is the half #18442 did not cover.** That change reads `navigationContributions`, so an app became addressable only where THIS package contributes into it; a sibling's `apps[]` declaration was invisible either way. With no contribution the app NAME was the orphan; with one, the name resolved through #18442 while the owner's own navigation ids were orphans — and were diagnosed "this stack contributes no such item", advising a move to a package sitting in the same artifact. Folding the records before the contributed-only pass closes both halves and restores the declared-app diagnosis, while an app owned OUTSIDE the artifact keeps #18442's wording.
14+
15+
**The controls, which are what make this a narrowing and not a hole.** Every rung pins both directions side by side: the same bundle judged ALONE still errors (so "no findings" cannot be confused with the rung going quiet); a name no entry of the artifact declares still errors with its rule id and a remedy that now enumerates what the artifact provides; every sub-rung stays judged against the sibling's declaration, so a typo under a resolved dashboard, flow, screen, app or object is still an `error`; an object-bound sibling action keyed under `globalActions` still errors with its routing message; where both packages declare the same name the declaration being judged keeps the slot, so a sibling's widget ids do not become addressable under this package's dashboard; an entry with no readable body (a segment reference) makes nothing addressable on any rung; and the single-`defineStack` shape is untouched, because all six are stack collections with no `stack.manifest` form to read.
16+
17+
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)