Commit 2b321a4
Fixes #19289
Clause-②: no
`IMPLICIT_REFERENCE_TARGETS`
(`packages/spec/src/data/field-value.zod.ts`) declares a `user` field's
target "a **CONSTANT OF THE TYPE**" and metadata authored without
`reference` "**fully specified, not under-specified**". Two arbiters
answer two different questions — `referenceCarrierOf` what the carrier
says, `referenceTargetOf` what the field points at — and for `user` only
the second matches that text. #18550 standardised a population of
readers on the first. This is the census of that population.
**The arbiters are NOT edited.**
`packages/spec/src/data/field-value.zod.ts` is untouched; the diff is
`packages/lint`, `packages/metadata-protocol`, `packages/rest` and their
tests.
## The census was re-measured, and it had moved
Taken on `8f6d831` with **no pathspec**, excluding tests and the
arbiters' own module: **17 `referenceCarrierOf` vs 8
`referenceTargetOf`** — ⛔ not the card's 16 vs 7. Class (A) is **8**
(card: 7); class (B) is **9** (unchanged). Both deltas landed *after*
the card's census:
| delta | commit | landed |
|---|---|---|
| `objectql/src/engine.ts:9077` — a NEW class-(A) carrier site
(`buildSummaryIndex`) | `875e9ad` (#19293) | 2026-09-20T10:37Z |
| `plugin-audit/src/audit-writers.ts:429` — raw read → target, so the
target count rose | `5636641` (#19264 / PR #19285) | 2026-09-20T10:09Z |
The `objectql/engine.ts` line numbers also drifted (13113 → 13204, 13568
→ 13659). The card's instrument trap reproduces on this tree, with
`REFERENCE_FIELD_TYPES` as the known-present needle: bare
`packages/**/src/**/*.ts` → **0 files**, `:(glob)…` → **2**, no pathspec
→ **2**.
## Per-site verdict — all 17
The deciding question is each site's **own type gate**: where it
excludes `user`, the carrier *is* the target for the question that site
asks, and the site stays on the carrier.
### Class (A) — handed a real field definition (8)
| # | site | own type gate | verdict |
|---|---|---|---|
| A1 | `metadata-protocol/src/seed-loader.ts:712` | `lookup \|
master_detail \| user` — **admits** | 🔴 **DEFECT ① (silent)** — repaired
|
| A2 | `rest/src/rest-server.ts:10914` | **none** — any field the picker
names | 🔴 **DEFECT ② (loud, 500)** — repaired |
| A3 | `cli/src/commands/doctor.ts:726` | `lookup` only | ✅ not a defect
— type-gate exclusion |
| A4 | `cli/src/commands/doctor.ts:930` | `lookup` only | ✅ not a defect
— type-gate exclusion |
| A5 | `objectql/src/engine.ts:9077` *(new)* | `master_detail \| lookup`
| ✅ not a defect — type-gate exclusion |
| A6 | `objectql/src/engine.ts:13204` | `master_detail \| lookup` | ✅
not a defect — type-gate exclusion |
| A7 | `objectql/src/engine.ts:13659` | `master_detail \| lookup` | ✅
not a defect — type-gate exclusion |
| A8 | `services/service-analytics/src/plugin.ts:748` | `lookup \|
master_detail` | ✅ not a defect — pre-judged on the card |
### Class (B) — synthesize `{ reference: … }`, discarding `type` (9)
| # | site | own type gate | verdict |
|---|---|---|---|
| B1 | `lint/src/data-model-rules.ts:250` (`refOf`) | 4 callers:
`RELATIONSHIP_TYPES` = `{lookup, master_detail}`, `OPTION_FIELD_TYPES`,
`summary` | ✅ not a defect |
| B2 | `lint/src/object-graph.ts:242` (`graphFieldOf`) | **none**;
consumer `RELATIONSHIP_FIELD_TYPES` **admits `user`** | 🔴 **DEFECT ③
(silent, widest)** — repaired |
| B3 | `lint/src/validate-expressions.ts:395` | `master_detail` only | ✅
not a defect |
| B4 | `lint/src/validate-field-consumers.ts:560` | **none** | 🔴
**DEFECT ④ (silent)** — repaired |
| B5 | `lint/src/validate-object-references.ts:306` |
`RELATIONSHIP_TARGET_FIELD_TYPES` **admits `user`** | 🟡 latent misread —
aligned, no output change |
| B6 | `lint/src/validate-object-references.ts:329` (action param) |
`ActionParamSchema.type` is **optional** | ✅ not a defect —
**measured**, see below |
| B7 | `lint/src/validate-security-posture.ts:292` (`refOf`) |
`CBP_TIERS` = master_detail / lookup | ✅ not a defect |
| B8 | `lint/src/validate-sharing-rule-enforceability.ts:267` |
`master_detail` only | ✅ not a defect |
| B9 | `verify/src/derive.ts:148` | `RELATIONAL` = lookup /
master_detail / tree — **excludes `user`** | ✅ not a defect |
⛔ **Class (B) was smaller than the card's framing, not larger.** Seven
of the nine are type-gated away from `user`; only B2 and B4 needed the
pass-the-field-through repair. Three of the nine (B3, B7, B9)
additionally keep their synthesized `{ reference: x.reference }` literal
because the **#5017 receiver meta-test reads their source** to prove
they read `reference` and never an alias — folding that read into a
helper call would disarm that scan silently. Those three are untouched.
## B6 — the site the tests refused, and why it is in the table as judged
I initially swapped the action-param site too.
`reference-integrity-suite.test.ts` went red: `object-reference-unknown`
vanished from the suite's findings entirely. The cause is that a param
is **not** a field definition — `ActionParamSchema.type` is optional,
because a field-backed param inherits its type at runtime, "not visible
at parse time" per that schema's own refinement comment. So
`referenceTargetOf` answered `undefined` for **every param that declares
no type**, and the corpus param `{ name: 'owner', reference: 'user' }`
(`user` being the classic miss for `sys_user`) stopped being checked.
The swap deleted a live check.
Nothing was owed there in the other direction either: a carrier-less
`user` param already produced no finding, because `check` returns early
on absence. Reverted, verdict recorded as not-a-defect, with a
regression guard kept beside it.
## The four repairs
1. **`metadata-protocol` seed-loader — SILENT, and it stored a wrong
value.** A `{type:'user'}` field with no `reference` contributed no
`dependsOn` edge and never reached `references`, so its natural key was
written **verbatim** into a column holding a record id.
2. **`rest` public-form picker — LOUD.** A `publicPicker` on a
spec-complete `{type:'user'}` field answered `500
LOOKUP_TARGET_MISSING`. Now `200` over `sys_user`.
3. **`lint` `object-graph.graphFieldOf` — SILENT and widest.** The slice
feeds `resolveFieldPath`, whose `RELATIONSHIP_FIELD_TYPES` admits
`user`; a carrier-less one answered `hop-untargeted`, which
`isUnjudgeable` treats as "the graph could not answer" and
`describeFieldPathVerdict` renders as *nothing*. Every rule in the
package that resolves a field path therefore **stopped judging** any
path through such a field — the failure mode `isUnjudgeable`'s own
docblock says this family exists to end ("a missed report is silence").
4. **`lint` `validate-field-consumers.walkObject` — SILENT.** The
`displayField` consumer edge onto `sys_user` was never recorded, so a
field that column displays was reported consumed by nobody. ⚠️
Materiality stated honestly: recordable only where `sys_user` is
compiled into the linted stack.
**Nothing widens.** `user` is the only member of
`IMPLICIT_REFERENCE_TARGETS`; `lookup` / `master_detail` / `tree` with
an absent carrier still name nothing, pinned at every repaired site.
**The unreadable-carrier refusal is unchanged** — `referenceTargetOf`
reads the carrier through `referenceCarrierOf` *before* it judges the
type, so #13053/#18550's `TypeError` still fires everywhere it fired.
⛔ **Not a re-widening of #12920.** A control pins it: a `user` field
spelling `referenceTo: 'zzz_aliased_object'` resolves `sys_user` from
the type and is never asked for the aliased name; a `lookup` spelling
the same alias still resolves nothing and still answers `500`.
## Evidence
- `@objectstack/lint` — **106 files / 4019 tests passed**
- `@objectstack/metadata-protocol` — **184 passed, 3 skipped / 2627
passed, 19 skipped**
- `@objectstack/rest` — **194 files / 3254 passed, 1 skipped**
- `typecheck` green on all three (incl. `check:test-typecheck` for lint
and rest)
- Re-run in full **after** merging `origin/main`; the closure was
rebuilt first because `packages/spec` moved on main's side
- **Gates:** `dispatch-gates --ran` reconciles **63 derived / 60 run / 3
NOT MEASURED / 0 UNRUN**. The three are `PREREQUISITE NOT MET` (exit 3,
⛔ not a pass): `check:dual-build-cjs-loads` and `check:type-check-debt`
need a whole-repo build; `check-plugin-teardown-shape --self-test`
cannot reach a commit-pinned fixture on a shallow clone.
- **`pnpm lint`, narrowed and declared:** ① the population is read from
`eslint.config.mjs`, which states in its own comment that this repo
"never enables type-aware linting (no `parserOptions.project`, no typed
`@typescript-eslint` rules) for ANY file" ⇒ this diff cannot move the
verdict on any file it does not touch; ② `--format json` reports **37
files linted, 0 errors, 0 warnings** (a superset — the three-dot set
includes what the merge brought); ③ measured at `97b689b`.
- Control-character self-scan over the 12 changed files: 0 hits.
## Attribution
Authored by Claude Code session `session_01UDXER3sdqfeVYpEWZs5mZx`.
Recorded here in prose deliberately: this body was edited on the raw
REST edit side, which appends the BARE footer form carrying no session
id (pm-dispatch `references/platform-readings.md`:350).
## Acceptance notes
- **Noted, not filed:** `referenceTargetOf` takes no `reader` label,
while `referenceCarrierOf` does precisely so "the message says who could
not read it". Every site moved onto the target arbiter therefore loses
its own name from the refusal, and four existing pins were retargeted
from the site label to `/referenceTargetOf/` here. The error class, the
offending shape and the prescription stay asserted, and this matches the
two landed repairs (#19198, #19264). It is a diagnostic-fidelity
question about the arbiter's signature, ⛔ not a defect — fixing it would
edit `packages/spec/src/data/field-value.zod.ts` and change this card's
landing path. **Successor: the next PR that moves a consumer onto
`referenceTargetOf`.**
- **Noted, not filed:** `graphFieldOf` no longer populates `reference`
for a **non-relationship** field carrying a stray `reference` (e.g.
`{type:'text', reference:'foo'}`), because `referenceTargetOf` returns
`undefined` outside `REFERENCE_VALUE_TYPES`. ⚠️ **Corrected by the
at-tier contract review (`5754774179`), re-measured on this head by the
seat:** `resolveFieldPath`'s `RELATIONSHIP_FIELD_TYPES` gate is on the
intermediate HOPS (`object-graph.ts:405`, `hop-untargeted`), ⛔ not on
the leaf — the leaf comes back `ok` carrying its `meta` whatever its
type (`object-graph.ts:413`) — and `validate-preset-comparands.ts:450`
DOES read it (`verdict?.kind === 'ok' ? strName(verdict.meta?.reference)
: undefined`). So «no other module reads `GraphField.reference`» is
**false on the tree**, and the sentence is withdrawn. What actually
follows: a `user` picker's filter rows now bind to `sys_user`, which is
the object the route queries — correct; a `text`-plus-stray-carrier
picker's filter rows bind to nothing where they bound to the stray
object. The same shape reaches the REST picker, which has no field-type
gate before `referenceTargetOf` (`rest-server.ts:10935`) and whose
`FieldSchema.reference` carries no non-reference-type refusal, so
`{type:'text', reference:'foo'}` behind a `publicPicker` now answers 500
`LOOKUP_TARGET_MISSING` where it used to search `foo`. Both are
pull-backs to the declared contract — the `reference` describe text
scopes it to lookup/master_detail fields, and `forms.mdx:238` scopes
pickers to lookup / master-detail / `user` — on a shape outside the
documented surface, so ⛔ neither is a widening and `Clause-②: no` is
unaffected. Strictly more correct: a `text` field's stray carrier is not
a target.
## 维护者速读(草稿)
**改了什么** — `user` 字段的目标由类型常量决定(`sys_user`),不必作者手写 `reference`。本 PR 普查了全部
17 个读「目标」的调用点,逐个判定该问哪个仲裁器,修了其中 4 个真缺陷,其余 13 个判为「读载体本来就对」并留档。
**为什么改** — 契约白纸黑字写着这种元数据「已完整声明」,但四处消费者把它当作「没写目标」。后果:公开表单上点开「负责人」选择器直接
500 错误页;种子数据把人名原样写进本该存记录 id 的列;lint 静默放弃对这类字段路径的全部校验。
**风险与代价(含回滚)** — 风险低。没有放宽任何拼写:别名仍旧拒收,不可读的载体仍旧抛错,`lookup`/`master_detail`
缺目标仍旧当作没目标。回滚即 revert 本 PR,无数据迁移、无存量数据改写。
**席位意见** — *(留空,待席位定稿)*
**你要做的** — 确认一件事即可:**本 PR 没有改动 `packages/spec`
的两个仲裁器**,所以不触发合流闸的路径腿。其余按常规复核。
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent d00692f commit 2b321a4
13 files changed
Lines changed: 607 additions & 35 deletions
File tree
- .changeset
- content/docs/ui
- packages
- lint/src
- metadata-protocol/src
- rest/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
257 | 257 | | |
258 | 258 | | |
259 | 259 | | |
260 | | - | |
| 260 | + | |
261 | 261 | | |
262 | 262 | | |
263 | 263 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
291 | 291 | | |
292 | 292 | | |
293 | 293 | | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
71 | | - | |
| 71 | + | |
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| |||
239 | 239 | | |
240 | 240 | | |
241 | 241 | | |
242 | | - | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
243 | 262 | | |
244 | 263 | | |
245 | 264 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
513 | 513 | | |
514 | 514 | | |
515 | 515 | | |
516 | | - | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
517 | 519 | | |
518 | 520 | | |
519 | 521 | | |
| |||
530 | 532 | | |
531 | 533 | | |
532 | 534 | | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
121 | 121 | | |
122 | 122 | | |
123 | 123 | | |
124 | | - | |
| 124 | + | |
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
| |||
550 | 550 | | |
551 | 551 | | |
552 | 552 | | |
553 | | - | |
| 553 | + | |
554 | 554 | | |
555 | 555 | | |
556 | 556 | | |
557 | | - | |
558 | | - | |
559 | | - | |
560 | | - | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
561 | 573 | | |
562 | 574 | | |
563 | 575 | | |
| |||
0 commit comments