Skip to content

fix(lint): translation-target-unknown resolves the two declared contribution surfaces - #19060

Merged
os-elon-musk merged 2 commits into
mainfrom
claude/issue-18441-translation-target-unknown-rungs
Sep 18, 2026
Merged

os-elon-musk merged 2 commits into
mainfrom
claude/issue-18441-translation-target-unknown-rungs

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

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 validate / lint / build accept a lookup or master_detail whose reference names an object that exists nowhere — the dangling target is found only at runtime #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 lint: an orphaned locale key is unfailable — translation-target-unknown is warning-only and its rule id carries no namespace, so no app can gate it #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

…ibution surfaces

`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 `error`
— the run failed, and the remedy ("or drop it") deleted a translation the
runtime honours. Two surfaces, both declared, both measured:

  - `objectExtensions[]`-injected fields and validation rules. Probe
    stack `objects: [crm_lead { name }]` + `objectExtensions: [{ extend:
    'crm_lead', fields: { sla_tier } }]` reported 1 error at
    `…objects.crm_lead.fields.sla_tier`; a genuinely undeclared field on
    the same stack reported a finding identical but for the name, so a
    correct author and a real typo were indistinguishable in the output.
  - the app-name rung. A contributor package shipping the labels for the
    nav items it contributed was told the app is one "which this stack
    does not define", in `os build`'s per-package leg, whether or not the
    app's owner was an entry of the same artifact.

Both folds widen what a key may RESOLVE against and nothing else. The
controls stay lit and pinned: 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 id nothing
contributes all still report at `error` with the rule id intact.

Two bounds worth reading before widening either fold further:

  - the extension fold is exactly two rungs wide because
    `ObjectExtensionSchema` is: `views`, `listViews`, `actions`,
    `fieldGroups`, `sections`, `tabs` and `hooks` are refused by name at
    the extension level, which a new case pins so the sizing cannot
    silently stop being true.
  - an extension target this stack does not DEFINE is rung 2b: the object
    key resolves and the subtree is skipped wholly, for the reason rung 2
    skips a platform object's — the owner's field set is not visible from
    here, and judging it against the injected names alone would report the
    owner's own field keys.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
…unknown fold

A behaviour fix in a released package, so `patch` rather than
`skip-changeset`: the rule ships inside `@objectstack/lint`'s `dist` and
its verdict is what `os build` / `os validate` / `os lint` print, so the
sentence an upgrading author greps belongs in the CHANGELOG.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 5 documentable anchor(s).

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via crm_lead (literal, a string literal in a comment on a changed line))
  • content/docs/api/client-sdk.mdx (via crm_lead (literal, a string literal in a comment on a changed line))
  • content/docs/api/error-catalog.mdx (via crm_lead (literal, a string literal in a comment on a changed line))
  • content/docs/data-modeling/index.mdx (via crm_lead (literal, a string literal in a comment on a changed line))
  • content/docs/deployment/validating-metadata.mdx (via crm_lead (literal, a string literal in a comment on a changed line))
  • content/docs/permissions/authorization.mdx (via crm_lead (literal, a string literal in a comment on a changed line))
  • content/docs/ui/index.mdx (via crm_lead (literal, a string literal in a comment on a changed line))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v13.mdx (via crm_lead (literal, a string literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7af6983ccf99f02cb3f3cdf6296f4691922f8298 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from eb12fdb74b85e3fc4828a695ec829dea8c0bd46d — the merge of head fb365260704eb4357f314975592413f5c9fb61e5 into base 7af6983ccf99f02cb3f3cdf6296f4691922f8298, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin eb12fdb74b85e3fc4828a695ec829dea8c0bd46d && git checkout eb12fdb74b85e3fc4828a695ec829dea8c0bd46d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7af6983ccf99f02cb3f3cdf6296f4691922f8298 fb365260704eb4357f314975592413f5c9fb61e5 && git checkout -B drift-repro 7af6983ccf99f02cb3f3cdf6296f4691922f8298 && git merge --no-ff fb365260704eb4357f314975592413f5c9fb61e5

node scripts/docs-audit/affected-docs.mjs --json 7af6983ccf99f02cb3f3cdf6296f4691922f8298

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 7af6983ccf99f02cb3f3cdf6296f4691922f8298 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

This was referenced Sep 18, 2026
@os-elon-musk
os-elon-musk marked this pull request as ready for review September 18, 2026 17:19
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit 6f8d751 Sep 18, 2026
36 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-18441-translation-target-unknown-rungs branch September 18, 2026 21:41
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…'s packages[] provide (objectstack-ai#19347)

Fixes objectstack-ai#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 objectstack-ai#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: objectstack-ai#18442`. Re-taken here:
objectstack-ai#18441 and objectstack-ai#18442 read `closed` / `completed` and objectstack-ai#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 (objectstack-ai#16611). **It cannot be reused as-is, and the
difference is the RETURN, not the reach**:

| | `artifactProvidedObjectNames` (objectstack-ai#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 objectstack-ai#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment