Repository navigation
fix(plugin-approvals): resolve a reference field's target through referenceTargetOf - #19263
Conversation
…ferenceTargetOf`
`ApprovalService.resolveLookupFields` read the MATERIALIZED `reference`
carrier, so a `{ type: 'user' }` field authored without one was dropped from
inbox display enrichment — with no refusal and no diagnostic.
The spec declares that metadata complete: `IMPLICIT_REFERENCE_TARGETS`
(`@objectstack/spec/data`) says a `user` field's target is "a CONSTANT OF THE
TYPE, so `reference` on a `user` field materializes that constant; it does not
supply it. Metadata authored without it … is fully specified, not
under-specified." Reading the carrier therefore refused the one spelling the
contract calls complete.
The reader now asks `referenceTargetOf` — the spec's single arbiter of what a
reference field points at, and the same function the expand gate and the
expansion engine ask (Framework#4443 / cloud#983). The unreadable-carrier
behaviour is unchanged: `referenceTargetOf` reads through `referenceCarrierOf`,
the throw is still caught per field, and the warning now names this reader
because the arbiter's message names itself.
Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF
Co-authored-by: Claude <noreply@anthropic.com>
…t reference target repair Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 6 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 705be093d0a7f5e7a8b0c0c9370ee30482174028 && git checkout 705be093d0a7f5e7a8b0c0c9370ee30482174028
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7d0f911da9ab9af94b72c0dfcfed16f676879bd5 aef74f0fa5df95511af4c166b8e49d80391bf947 && git checkout -B drift-repro 7d0f911da9ab9af94b72c0dfcfed16f676879bd5 && git merge --no-ff aef74f0fa5df95511af4c166b8e49d80391bf947
node scripts/docs-audit/affected-docs.mjs --json 7d0f911da9ab9af94b72c0dfcfed16f676879bd5
|
… `referenceTargetOf` (#19264) `audit-writers.ts` admitted `user` as a reference type and then required the materialized `reference` carrier, so a `trackHistory`'d `{ type: 'user' }` field authored without one was planned for nobody and the timeline rendered the raw id — silently, with no refusal and no diagnostic. `IMPLICIT_REFERENCE_TARGETS` declares the opposite for exactly that type: the target is a CONSTANT OF THE TYPE, and metadata authored without it is fully specified. `referenceTargetOf` exists to supply it, and is the same arbiter `plugin-approvals` moved to in #19263. The carrier was read at FOUR sites, not two: the two read planners key the plan by target object and the two renderers look the resolved titles back up under the same key, so a plan-only repair would issue the read and still print the id. One helper answers for all four. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF
… `referenceTargetOf` (objectstack-ai#19264) (objectstack-ai#19285) Fixes objectstack-ai#19264 Clause-②: no ## The defect `packages/plugins/plugin-audit/src/audit-writers.ts` admits `user` as a reference type (`REFERENCE_FIELD_TYPES`) and then required the **materialized `reference` carrier**, so a `trackHistory`'d `{ type: 'user' }` field authored without an explicit `reference` was planned for nobody and the timeline rendered the raw id. The contract says the opposite for exactly that type. `IMPLICIT_REFERENCE_TARGETS` (`packages/spec/src/data/field-value.zod.ts`), re-read verbatim on `origin/main` at `215840f43` before this branch was cut: > `user` is the only member: `field.zod` defines it as "a lookup specialized to the `sys_user` system object … target fixed to the `sys_user` system object", and the `Field.user()` builder — unlike `Field.lookup(reference, …)` / `Field.masterDetail(reference, …)` — takes NO target argument and writes `reference: 'sys_user'` itself. The target is a CONSTANT OF THE TYPE, so `reference` on a `user` field materializes that constant; it does not supply it. Metadata authored without it (hand-written JSON, an AI author, a Studio form) is fully specified, not under-specified. So the one spelling the contract calls **complete** was the one the reader refused, and it refused it **silently**: no refusal, no diagnostic, the field simply absent from the read plan. `referenceTargetOf` lives in that same module to supply the constant, and is the arbiter `plugin-approvals` moved to in PR objectstack-ai#19263 / `841a71e51`. ## FOUR sites, not two — the card's site count is falsified The card and the dispatch order both name **two** carrier-gated sites, the two read planners. Re-measured on this branch's own base rather than trusted, the carrier was read at **four**: | line on `215840f43` | function | role | |---|---|---| | `:472` | `planTrackedLookupReads` | builds the read plan, **keyed by target object** | | `:527` | `renderTrackedChangeSummary` | looks the resolved titles back up **under the same key** | | `:629` | `planMilestoneTokenReads` | builds the milestone read plan, keyed by target object | | `:693` | `renderMilestoneSummary` | looks the resolved titles back up under the same key | The target is the KEY of the `objectName -> (id -> title)` map, so it has two ends. Repairing only the two planners issues the read and then looks the result up under a key the plan never used — the timeline still shows the id, and now pays for a read it cannot use. That is measured below, not argued: the renderer-only ablation leaves every read-count assertion green and every rendered-string assertion red. Everything else the card states holds as given: the line ranges `:471-473` and `:628-630`, and the reading that `:434` in `displayFieldValue` does not gate on the carrier and is the consumer where the symptom surfaces rather than a defect site. ## The repair One module-private helper, `referenceTargetForSummary`, asks `referenceTargetOf`, and all four sites use it. - **Nothing widens.** `REFERENCE_FIELD_TYPES` is untouched, so the admitted types stay `lookup` / `master_detail` / `user`. `tree` is deliberately still out: it carries an author-chosen target, so admitting it would widen what the timeline tracks rather than repair what it silently dropped. A `lookup` / `master_detail` whose author-chosen target is absent still names nothing and still issues no read. - **A padded carrier can no longer split the key.** The planners used to `trim()` and the renderers did not, so `reference: ' crm_account '` produced two keys and no title. One helper trims once, for both ends. - **The unreadable-carrier behaviour is unchanged, and the throw is caught deliberately.** `referenceTargetOf` reads the carrier through `referenceCarrierOf`, which throws for an object- or array-valued `reference`. All four call sites run inside `writeAudit`'s summary composition, which is **not** inside the `try` that guards the audit row write — measured: between `writeAudit`'s first line and its end there is exactly one `try`, and it wraps `persistAuditTrailRow` alone. An escaping `TypeError` would therefore leave the hook, turning a display-enrichment miss into a failure on the audited write's own path. Such a carrier is left out of the plan exactly as `typeof field.reference === 'string'` already left it out: byte-identical behaviour for that class, not a new silence. - **No authoring change.** Metadata that already spells `reference: 'sys_user'` resolves to the same target it always did. ## Tests — silence is the defect, so the read and the rendering are pinned separately New `packages/plugins/plugin-audit/src/implicit-reference-target-summary.test.ts`, 10 cases against the counting driver the sibling summary suites already use (reads counted, rows copied). Every implicit-target case asserts **both** halves: the `sys_user` read delta (the half a happy-value assertion cannot see — before this change that read was never issued at all) and the rendered summary string (the half a read count cannot see — a plan-only repair pays for the read and still prints the id). The target is asserted through `referenceTargetOf({ type: 'user' })`, never a hand-copied `'sys_user'`, so a second de-facto constant in this package would not satisfy it. Both planner sites are covered: the tracked-change branch through a tracked `user` field with no `reference`, and the milestone branch through a milestone token on an **untracked** `user` field with no `reference` (that branch is deliberately not gated on `trackHistory`). Four narrowness controls, so "the implicit target resolves" cannot be read as "everything resolves now": a tracked targetless `lookup` (as a diff row and as a milestone token) stays unresolved and costs nothing; a tracked `tree` with a perfectly readable target stays out of the type gate and renders its raw id; and a write that moves no reference field, on a row that HOLDS one, still pays zero reads. The second commit is that file's `find` double, not the subject: see Gates below. ### Reverse verification — two ablations, both through `scripts/ablation-replace.mjs` Re-run from the final committed state at `7ecf511d2`, after the test file's last edit. The subject is resolved from SOURCE, not from `dist`: the test imports `./audit-writers.js`, a relative specifier inside the package, so vitest transforms the mutated file directly and no build sits between the mutation and the run. **Leg 1 — revert BOTH planner sites** (anchor hit 2 times as declared, blob `9123759bc8e2` to `f6b9e89ac1a7`): ``` Test Files 1 failed (1) Tests 3 failed | 7 passed (10) ``` The three implicit-target cases go red on the READ: `expected +0 to be 1`, three times. The plan is empty again, exactly as before the repair. **Leg 2 — revert BOTH renderer sites, leaving both planners repaired** (anchor hit 2 times as declared, blob `9123759bc8e2` to `90516141da24`). This is the leg that measures the card's two-site reading: ``` Test Files 1 failed (1) Tests 4 failed | 6 passed (10) ``` Every read-count assertion stays **green** and every rendered string goes red — `expected 'Owner: ∅ → usr_1' to be 'Owner: ∅ → Grace Hopper'` and `expected 'Deal won by usr_1' to be 'Deal won by Grace Hopper'`. The reads are issued and thrown away. That is the reported symptom, unrepaired, at a higher read cost. Both legs restored and proven restored by the tool: blob back to `9123759bc8e2`, identical to HEAD, `git diff HEAD` empty. ## The census triage asked for > whoever takes either should sweep for a third: a contract that two consumers read the same wrong way is a contract whose *consumers* need a census, not two point fixes. Corpus: every `referenceCarrierOf` and `referenceTargetOf` call site in the repo's package sources, plus every raw `field.reference` read, at `7ecf511d2`. **Positive control on the same corpus**: `referenceTargetOf` is a real, importable symbol with call sites in `metadata-protocol/src/protocol.ts`, `objectql/src/engine.ts`, `objectql/src/integrity/dangling-reference-audit.ts`, `objectql/src/record-title.ts`, `plugin-approvals/src/approval-service.ts` and `service-analytics/src/dimension-labels.ts` — so a zero below is a reading. A methodology note that is itself a finding. The first pass of this sweep used the pathspec `packages/**/src/**/*.ts`, which under git's default (non-`:(glob)`) matching **does not reach** `packages/plugins/plugin-audit/src/audit-writers.ts` — the very file this card is about. It returned a clean five hits. What caught it was a control: grepping for a string the defect file is known to contain and finding the file absent from the result. Every count below was re-taken without that pathspec. **One third site, same silent shape — reported, deliberately not fixed** (outside this card's file surface): - `packages/metadata-protocol/src/seed-loader.ts:712`, `SeedLoader.buildDependencyGraph`. The type gate is `lookup || master_detail || user` — `user` admitted — and the next line reads the carrier and `continue`s when it is absent. A seed row whose `{ type: 'user' }` field carries no `reference` therefore contributes **no `dependsOn` edge** and is **never pushed onto `references`**, so its natural key is written verbatim instead of being resolved to a record id. That consequence is named in this same function's own docblock, for the case it does handle: "every lookup / master_detail seed value was written verbatim … Dangling references break parent joins." Silent, user-visible, same misread contract. Dedupe words: `seed-loader user implicit reference target` · `buildDependencyGraph referenceCarrierOf user` · `seed natural key written verbatim user field` · `metadata-protocol implicit target census`. **One near-site of a different shape — LOUD, reported for judgment:** - `packages/rest/src/rest-server.ts:10914`, the public-form lookup picker. It has **no type gate at all** (any field the form opts in with `publicPicker`), reads the carrier, and answers `500 LOOKUP_TARGET_MISSING` when it is absent — so a `publicPicker` on a `{ type: 'user' }` field authored without `reference` is refused rather than silently dropped. Same misreading, opposite failure mode. `PUBLIC_FORM_SERVER_MANAGED_FIELDS` already refuses server-managed anchors on this surface, so the reachable case is a hand-authored user field. Dedupe words: `public form lookup picker user implicit target` · `LOOKUP_TARGET_MISSING user field` · `rest picker referenceCarrierOf implicit`. **The card's two unverified analytics readings — both CONFIRMED:** - `service-analytics/src/dimension-labels.ts:142` already delegates to `referenceTargetOf`. Correct today; no action. - `service-analytics/src/plugin.ts:748` (`relationshipResolver`) gates on `lookup || master_detail`, so `user` never enters the gate at all — materialized carrier or not — and falls through to `return engine ? undefined : relationshipName`. With an engine present that `undefined` makes the dataset compiler reject the relationship. **LOUD**, and a different shape: a type-gate exclusion, not an implicit-target misread. No action. **Cleared, each with its reason** — every other carrier consumer in the corpus: - Type gate excludes `user`, so the implicit target cannot arise: `cli/src/commands/doctor.ts:726` and `:930` (`lookup` only), `objectql/src/engine.ts:13113` and `:13568` (cascade; `master_detail` / `lookup`), `lint/src/validate-expressions.ts:395` and `lint/src/validate-sharing-rule-enforceability.ts:267` (`master_detail` only), `verify/src/derive.ts:148` (its `RELATIONAL` set is the `lookup` / `master_detail` spellings plus `tree`), and `spec/src/kernel/functional-completeness.ts:177` — whose `RELATIONSHIP_TYPES` is `lookup` / `master_detail`, so `field/relationship-without-reference` does **not** fire on a spec-complete `user` field. - Reads the carrier as the AUTHOR'S DECLARED KEY, which is their actual subject rather than "what does this field point at": the five generic lint readers (`data-model-rules.ts:250`, `object-graph.ts:242`, `validate-field-consumers.ts:560`, `validate-object-references.ts:306`, `validate-security-posture.ts:292`). ## Gates Every exit code below was captured **before** any pipe. The derived set was re-derived at the final head `7ecf511d2` and is byte-identical to the one derived at `dce04112d`. - `pnpm --filter @objectstack/plugin-audit test` — exit 0, **25 files / 363 tests passed**, at the final head. - `pnpm --filter @objectstack/plugin-audit typecheck` — exit 0. The leg count was read from the package, not assumed: its `typecheck` script is `tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json && pnpm check:test-typecheck`, **three legs**, all run. The test-layer ledger is unmoved — `0 file(s) / 0 error(s) / 0 pinned signature(s)` — and this package carries no `test-typecheck-debt.json` of its own, so nothing was touched there. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived **61 families** from a change set of **3 paths** (three-dot semantics, merge base `215840f43`), **542 changed lines**, under the 5,000 human-merge threshold. All 61 were run. - **One family went genuinely RED and was repaired, not baselined.** `pnpm check:objectql-double-limit` exit 1: the new test file's counting `find` double was UNJUDGED, because the gate lifts the double and drives it with a row stub in place of every non-function declaration, and `(reads.findOn[object] ?? 0) + 1` throws on that stub before the body can be judged. The double now reads its counter by TYPE and applies the caller's `limit` by PRESENCE, after the filter and before the rows are copied. Re-run: exit 0, and the census moved the right way — **393 graded (was 392), 195 applying the bound (was 194), 39 proving the bound precedes a row-touching stage (was 38), 55 unjudged (was 56)**, with `baseline key set verified against 215840f: no files added`. - **Three families first answered exit 3 = PREREQUISITE NOT MET = NOT MEASURED**: `check:dual-build-cjs-loads`, `check:i18n` and `check:type-check-debt`. Each named the same prerequisite ("this gate reads built output, and some package has no dist/ … Run `pnpm build` first. ⛔ This is NOT a pass: nothing was measured"). The prerequisite was cleared with a whole-tree `pnpm build` (exit 0, 73/73 tasks) and each re-ran to **exit 0**. None is reported as a zero. - The remaining **57** families ran to exit 0 on the first pass. After the second commit, every family whose subject is the changed test file was re-run at the final head — `check:objectql-double-limit`, `check:type-check-debt`, `check:type-check-coverage`, `check:test-source-alias`, `check:cross-package-test-inputs`, `check:nul-bytes`, `check:tier-file-adoption`, `check:engine-double-contract`, `check:driver-memory-census`, `check:where-matcher`, `check:published-files`, `check-empty-changeset`, `check-changeset-no-major`, `check-adr-0087-registration`, `check-closing-keyword-parity` — all **exit 0**. - `pnpm lint` (`eslint . --no-inline-config`, the repo's only style authority) — **exit 0 over the whole tree**, so no narrowing is being claimed and no narrowing evidence is owed. - `grep os-regen .gitattributes`, read on the spot: every merge-driver-managed path is under `packages/spec/`, `docs/`, `content/docs/`, `skills/` or `scripts/platform-object-tenancy-census.json`. **No path in this diff is driver-managed.** - A control-byte self-scan beyond `check:nul-bytes`, over all three changed files: `grep -naP` for the C0 range plus DEL — no hits. - Changeset: decided by reading, not habit. `packages/plugins/plugin-audit/package.json` carries no `private` key and ships `files: ["dist", "README.md", "CHANGELOG.md"]`, so a fix here publishes. A `patch` changeset is included. ### Verdict lines transcribed verbatim, because those logs die with the worktree From `pnpm check:engine-double-contract` (exit 0): ``` UNRECOGNISED [engine-double-contract]: 9 construct(s) in packages, examples declare a scanned verb (delete, update, findOne) alongside engine siblings, and this gate could not read the implementation -- so they are in NEITHER the pinned population nor the ledger. 213 further construct(s) are SCOPED OUT by a stated criterion and are not counted here. This is a verdict, not a finding: it never fails a run (objectstack-ai#9747, ruling of 2026-08-18). ``` From `node scripts/check-changeset-no-major.mjs --base origin/main` (exit 0): ``` ℹ️ LEVEL AXIS: NOT APPLICABLE — this run has no `pull_request` to read a declaration from, so the clause-② axis has no input by construction rather than by omission. It is a PR-scoped reading: the RC cut (`cut-rc.yml`) and a local run reach here, and neither is a PR that could have declared. · no `pull_request` payload was available to read a declaration from ``` That second one is a local-run artefact by construction: on this PR the axis has its input, and the declaration it will read is the `Clause-②: no` line at the top of this body. ## Not this PR `packages/spec` is read-only on this lane: `referenceTargetOf` is imported and nothing in that package is edited. No release-notes page is touched. `plugin-approvals` was repaired in PR objectstack-ai#19263 and is out of scope here. The two census findings above are reported for the seat to file and deliberately **not** fixed here — that is the difference between two bugs and one systemic gap. ## Acceptance notes - `noted, not filed` — the sibling summary suites (`audit-lookup-summary.test.ts`, `audit-milestone-summary.test.ts`, `audit-option-label-summary.test.ts`, `audit-bound-previous.test.ts`) each hold the same counting driver in its pre-repair, UNJUDGED shape and are grandfathered in `scripts/objectql-double-limit.baseline.json`. The one-line change made here would shrink that ledger by four entries. Not done in this PR: it is unrelated to the card and would put four files this card has no business in on the diff. Carrier: any later PR editing those suites. - `noted, not filed` — `renderTrackedChangeSummary` and `renderMilestoneSummary` both build an `optionLabelFor` closure per field per write. Ordinary allocation on a hot path; no defect, no repro, no contract text. Carrier: none — no open PR or queued card touches those two functions. - `noted, not filed` — `referenceTargetOf` answers a whitespace-only carrier (`reference: ' '`) with that whitespace rather than falling through to the type's constant, because `referenceCarrierOf` treats only the empty string as absence. The helper added here trims afterwards, so the audit path's behaviour is unchanged in both directions; making the arbiter itself trim is a `packages/spec` question and this lane is read-only there. Carrier: none observed. --- _Generated by [Claude Code](https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19198
Clause-②: no
The defect
ApprovalService.resolveLookupFieldsadmittedtype: 'user'fields but resolved their target from the materializedreferencecarrier. The spec declares the opposite for exactly that type —IMPLICIT_REFERENCE_TARGETSinpackages/spec/src/data/field-value.zod.ts, re-read verbatim onorigin/mainat7d0f911dabefore this branch was cut:So the one spelling the contract calls complete was the one the reader refused — and it refused it silently: no refusal, no diagnostic, the field simply absent from
payload_display, and a business reviewer reading a raw user id where every other reference field showed a name.referenceTargetOflives in that same module precisely to supply the constant (Framework#4443 / cloud#983 fixed the identical blindness at the$expandgate and in the expansion engine).The repair
resolveLookupFieldsnow asksreferenceTargetOf— the spec's single arbiter of what a reference field points at — instead of reading the carrier.lookup,master_detail,user). Alookup/master_detailwhose author-chosen target is absent still names nothing, is still left out, and still issues no read;treeis deliberately not added (see acceptance notes).referenceTargetOfreads the carrier throughreferenceCarrierOf, so theTypeErroris still raised for an object- or array-valuedreferenceand is still caught per field, and the existing pins insrc/lookup-field-reference-carrier.test.tspass untouched. The warning line now names this reader in its own prefix, because the message the arbiter throws names itself.reference: 'sys_user'resolves to the same target it always did. Making authors restate the constant was rejected on the card as the 消费端宽容 direction the charter rejects.Tests — silence is the defect, so both halves are pinned
New
src/implicit-reference-target-lookup.test.ts(5 cases):{ type: 'user' }field with noreferenceresolves tosys_user, and that value is asserted to equalreferenceTargetOf({ type: 'user' })— so a hand-copiedsys_userliteral in this package would not satisfy it;sys_userread for the payload's id (toHaveLength(1), ids['u7']) and producespayload_display = { assignee: 'Grace Hopper' }. The read is the half a happy-value assertion cannot see — before the repair that read was never issued at all;lookupproduces no read against any referenced object and leavespayload_displayunset, so "the implicit target is enriched" cannot be read as "everything is enriched now".Reverse verification (ablation), run from the committed state through
scripts/ablation-replace.mjs— anchor hit 1 time, blob703a308132→b1fa2e3f0300on disk, mutation replacing the arbiter call with the pre-repair carrier read:Cases 1, 2 and 4 go red (
expected undefined to deeply equal { key: 'owner', reference: 'sys_user' };expected [] to have a length of 1 but got +0); cases 3 and 5 stay green because they are the controls, and the whole sibling carrier suite stays green — the mutation is narrow. Restored and proven restored: blob back to703a308132, identical to HEAD,git diff HEADempty.Gates
Everything below was captured with the exit code read before any pipe, at
aef74f0fa.pnpm --filter @objectstack/plugin-approvals test— exit 0, 49 files / 780 tests passed.pnpm --filter @objectstack/plugin-approvals typecheck— exit 0, all three legs this package's script names (tsc --noEmit,tsc --noEmit -p tsconfig.scripts.json,check:test-typecheck). The test-layer ledger is unmoved:8 file(s) / 324 error(s) / 27 pinned signature(s)— the new test file adds none and takes no entry.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 61 families; all 61 were run and reconciled with--ran: "61 derived famil(ies) accounted for — 61 run, 0 NOT-MEASURED (a DERIVED zero — all 61 recorded an exit code and none of them is 3)."check:dts-closure,check:dual-build-cjs-loads,check:i18n,check:lean-entry-closure,check:sourcemap-no-sources-content,check:type-check-debt). Each prerequisite was cleared — the dependency closure, thenturbo run buildover all packages — and each re-run to exit 0. None is reported as a zero.pnpm lint(eslint . --no-inline-config, the repo's only style authority) — exit 0 over the whole tree, so no narrowing is being claimed.grep os-regen .gitattributesread on the spot: no path in this diff is merge-driver managed.Acceptance notes
Noted, not filed — each states who would meet it:
treeis a reference type this enrichment still does not admit.REFERENCE_VALUE_TYPEShas four members;resolveLookupFieldscarries three, andplugin-auditdeliberately carries the same three (audit-writers.ts: "treeis NOT on the list: it carries noreference"). Admitting it would widen what the inbox resolves rather than repair what it silently dropped, which is not this card. Successor: whoever next touches the shared three-type vocabulary in either package.referenceTargetOftakes no reader label. The existing assertion on it still passes unchanged.Reported to the dispatching seat rather than filed here (dev agents report findings with dedupe words; they do not file): one class-(b) sibling of this defect in
plugin-audit, and a reading that partly falsifies this card's own unverified note about the analytics resolver.Not this PR
packages/specis read-only on this lane —referenceTargetOfandIMPLICIT_REFERENCE_TARGETSare imported and nothing in them is edited. No release-notes page is touched.Generated by Claude Code