fix(spec-consumers): the implicit-target census — 17 sites judged, four repaired through referenceTargetOf (#19289) - #19472
Conversation
…target question through `referenceTargetOf` (#19289) `IMPLICIT_REFERENCE_TARGETS` declares a `user` field's target a CONSTANT OF THE TYPE and metadata authored without `reference` "fully specified, not under-specified". Sites that answered from `referenceCarrierOf` read such a field as targetless. Repaired (a site reached by `user`, i.e. no type gate or a gate admitting it): - `metadata-protocol` seed-loader `buildDependencyGraph` — SILENT: no `dependsOn` edge, never on `references`, natural key written verbatim. - `rest` public-form lookup picker — LOUD: `500 LOOKUP_TARGET_MISSING` on a spec-complete field. - `lint` `object-graph.graphFieldOf` — SILENT and widest: `hop-untargeted` is `isUnjudgeable`, so every rule resolving a path through a `user` field stopped judging it. - `lint` `validate-field-consumers.walkObject` — SILENT: the `displayField` consumer edge onto `sys_user` was never recorded. Aligned (latent misread, no output change today): - `lint` `validate-object-references` field target and action param target. Judged and left on the carrier: the eleven sites whose own type gate excludes `user`, so the carrier IS the target for the question they ask. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…fects (#19289) One pin per repaired site, each asserting the shape the defect actually had: - seed-loader — the `references` ROW, ⛔ not a throw: the defect was silent. - REST picker — `200` over `sys_user` AND `findData`'s object, so a 200 over the wrong object cannot pass. - object-graph — `isUnjudgeable` flips false, plus a MISS through the implicit hop now being reported, so judgement is proven to have resumed. - validate-field-consumers — the displayed field leaving the findings list. Every block carries the same three controls: the two legal spellings agreeing, `lookup`/`master_detail` still naming NO target (`user` is the only member of `IMPLICIT_REFERENCE_TARGETS`), and an unreadable carrier still refusing — the implicit target is not a fallback that swallows #13053/#18550's throw. The REST block adds the #12920 control: a `user` field spelling `referenceTo` resolves `sys_user` from the type and never the alias. Four reader-label pins move to `referenceTargetOf`, which is the arbiter these sites now ask; the error class, the offending shape and the prescription stay asserted. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…m declares no type Reverts one of the six census edits, on measurement rather than reasoning. `ActionParamSchema.type` is optional (a field-backed param inherits its type at runtime, not visible at parse time), so the target question is unanswerable from a param alone: `referenceTargetOf` answered `undefined` for every param that declares no type, and `object-reference-unknown` vanished from `reference-integrity-suite.test.ts` entirely. Nothing was owed at this site in the other direction either — a carrier-less `user` param already produced no finding, because `check` returns early on absence. Census verdict corrected to NOT-a-defect, with the regression guard kept beside it. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Clause-②: no — it deletes a mistaken refusal of metadata the published contract already declares complete, not a widening. No key, alias or spelling is newly accepted anywhere. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…plicit-reference-target-census
📓 Docs Drift Check4 anchor(s) derived from 3 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 28 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 b5702e3acdc82c0e194a07b1622b6c21f6bdb2d3 && git checkout b5702e3acdc82c0e194a07b1622b6c21f6bdb2d3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 841ed38947850738cf6ac12388abbd7029a2a7b9 01b387e05b08bc34099f583410c6b1a5e22df9ae && git checkout -B drift-repro 841ed38947850738cf6ac12388abbd7029a2a7b9 && git merge --no-ff 01b387e05b08bc34099f583410c6b1a5e22df9ae
node scripts/docs-audit/affected-docs.mjs --json 841ed38947850738cf6ac12388abbd7029a2a7b9 |
Contract reviewServed-tier: Isolated at-tier review for the ① Derived judgments1. The census, re-taken. 2. All 17 sites judged, each verdict checked against the site's own gate on the head tree. The failure mode I was looking for — a site waved through as type-gated while its gate admits
3. The measured reversal (B6) is real and the guard guards. Commit 4. Carrier discipline, for the seat rather than the dev: 5. Seed-loader accept/reject behaviour, stated precisely. For a 6. Lint object-graph, reach checked. Where 7. Docs — the ground for the verdict. But the dev's docs verdict — "NO page is falsified, and NO docs edit is owed", from 9 pages read — is wrong, because the 9 do not include ② Semver level
③ Boundary flags
What closes this FAIL: the one-sentence correction at Implemented-by: VERDICT: FAIL Generated by Claude Code |
…target case (#19289) The `object` row documented the resolution rule BY ITS INPUTS — "its `reference` key, and only that key" — which was true of the runtime before this branch and is false after it: for a `{ type: 'user' }` field the target comes from the type's own constant `sys_user`, never from the carrier. That is the class the Docs Drift Check declares it structurally cannot see: a page stating a rule by its inputs shares no identifier with the emitter that implements it, so no anchor links `rest-server.ts` to this page. Minimal correction, one row. What stays, because it is all still true: the `referenceTo` / `target` / `options.objectName` alias is still never resolved (this branch pins that in both directions), the carrier is still read through the one accessor, and `500 LOOKUP_TARGET_MISSING` is still the answer for a `lookup` / `master_detail` naming no target. What the row now separates is the alias being ignored from what answers next, which differs by type. The changeset gains the clause an upgrading seed author greps: a `user` field's unresolvable natural key now DROPS the whole record, counted, reported and logged at `error`, where it used to be written verbatim. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
|
Contract reviewServed-tier: 163/163
Isolated at-tier re-review after the FAIL recorded in comment ① Derived judgments1. The prior FAIL's ground is CLOSED — measured, not accepted. 2. Accept set and public surface — nothing widens; 3. The 4. Docs and code agree. 5. One consequence the PR does not name — judged correct, NOT a FAIL ground, should be named in the acceptance notes. The picker route has no field-type gate before 6. Census table — re-measured, every row true. 7. B6 reversal and its guard. 8. Reader-label pins. Four pins retargeted from site labels to ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Dequeued on
|
| reading | value |
|---|---|
the same check on this PR's head 01b387e0 |
success — 35 check names, latest run per name: 0 failures, 0 pending |
the same check on main at d00692f (the exact base the queue build used) |
success (run 106197283044) |
the same check on main at 32b5831 (the prior tip) |
success ×3 (106193513539, 106198012040, 106199867344) |
| what the base actually carries | d00692f is PR #19479 — scripts/pm/close-cards.mjs, .claude/settings.json, .github/workflows/lint.yml, package.json. PM tooling. |
⇒ green on the head, green on the base, green on the base's predecessor three times over, and the one commit the queue build added is PM tooling. ⛔ That is not a "the job is broken" reading either — a broken job would not be green four times in the same hour.
What the failure names, against what this PR touches. The service-container log carries Cannot create the table \os_lv_sql_driver_11565_row_byte_budget_…`.`os11565_narrow` because the record size will exceed the maximum allowed size for a record— a **SQL-driver row-byte-budget** conformance case. This PR's 13 files are reference-target arbiter swaps inpackages/metadata-protocol, packages/lint, packages/rest, one hand-written docs page and one changeset. ⛔ It touches no driver, no DDL path and nothing temporal. ⭐ The many PG ERROR:` lines in the same teardown block are expected negative-path noise — the conformance suite provokes constraint violations on purpose — so they are ⛔ not the failure and ⛔ not evidence of one.
⛔ "Flake" is not being offered as a root cause. The test bodies plainly ran (statements at 03:02–03:03), so this is not a died-before-the-body case. What the four readings support is the narrower claim the rule allows: an error naming a subsystem this diff does not touch, on a check that is green on both this head and this base. That buys exactly one re-run, and ⛔ nothing more.
No fix exists to port. There is no open PR, revert or fix branch addressing this failure — nothing to carry into this PR, so nothing was carried. ⛔ This PR is not widened.
What was done
Re-queued, ⛔ not re-pushed: PUT .../pulls/19472/ccr/auto_merge {"merge_method":"SQUASH"} → {"enabled":true,"merge_method":"squash"}, confirmed on the timeline — added_to_merge_queue 2026-09-21T03:16:27Z — and by the live ref gh-readonly-queue/main/pr-19472-d00692f5…. Preconditions re-verified immediately before, ⛔ not recalled from the first enqueue: head 01b387e0 green on all 35 check names with none pending, mergeable_state: clean, and node scripts/pm/check-clause2-carriers.mjs --pair 19472 = exit 0 with the at-tier PASS record (5754774179) naming this head.
⚠️ The re-run is now SPENT
If Temporal Conformance fails again on the next queue build, that second failure is real, and this seat will root-cause it rather than re-queue a third time. ⛔ No further re-queue on the same signature without a diagnosis first.
Reading taken 2026-09-21T03:17Z.
Generated by Claude Code
Fixes #19289
Clause-②: no
IMPLICIT_REFERENCE_TARGETS(packages/spec/src/data/field-value.zod.ts) declares auserfield's target "a CONSTANT OF THE TYPE" and metadata authored withoutreference"fully specified, not under-specified". Two arbiters answer two different questions —referenceCarrierOfwhat the carrier says,referenceTargetOfwhat the field points at — and foruseronly 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.tsis untouched; the diff ispackages/lint,packages/metadata-protocol,packages/restand their tests.The census was re-measured, and it had moved
Taken on
8f6d831with no pathspec, excluding tests and the arbiters' own module: 17referenceCarrierOfvs 8referenceTargetOf— ⛔ 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:objectql/src/engine.ts:9077— a NEW class-(A) carrier site (buildSummaryIndex)875e9ad(#19293)plugin-audit/src/audit-writers.ts:429— raw read → target, so the target count rose5636641(#19264 / PR #19285)The
objectql/engine.tsline numbers also drifted (13113 → 13204, 13568 → 13659). The card's instrument trap reproduces on this tree, withREFERENCE_FIELD_TYPESas the known-present needle: barepackages/**/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)
metadata-protocol/src/seed-loader.ts:712lookup | master_detail | user— admitsrest/src/rest-server.ts:10914cli/src/commands/doctor.ts:726lookuponlycli/src/commands/doctor.ts:930lookuponlyobjectql/src/engine.ts:9077(new)master_detail | lookupobjectql/src/engine.ts:13204master_detail | lookupobjectql/src/engine.ts:13659master_detail | lookupservices/service-analytics/src/plugin.ts:748lookup | master_detailClass (B) — synthesize
{ reference: … }, discardingtype(9)lint/src/data-model-rules.ts:250(refOf)RELATIONSHIP_TYPES={lookup, master_detail},OPTION_FIELD_TYPES,summarylint/src/object-graph.ts:242(graphFieldOf)RELATIONSHIP_FIELD_TYPESadmitsuserlint/src/validate-expressions.ts:395master_detailonlylint/src/validate-field-consumers.ts:560lint/src/validate-object-references.ts:306RELATIONSHIP_TARGET_FIELD_TYPESadmitsuserlint/src/validate-object-references.ts:329(action param)ActionParamSchema.typeis optionallint/src/validate-security-posture.ts:292(refOf)CBP_TIERS= master_detail / lookuplint/src/validate-sharing-rule-enforceability.ts:267master_detailonlyverify/src/derive.ts:148RELATIONAL= lookup / master_detail / tree — excludesuser⛔ 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 readreferenceand 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.tswent red:object-reference-unknownvanished from the suite's findings entirely. The cause is that a param is not a field definition —ActionParamSchema.typeis optional, because a field-backed param inherits its type at runtime, "not visible at parse time" per that schema's own refinement comment. SoreferenceTargetOfansweredundefinedfor every param that declares no type, and the corpus param{ name: 'owner', reference: 'user' }(userbeing the classic miss forsys_user) stopped being checked. The swap deleted a live check.Nothing was owed there in the other direction either: a carrier-less
userparam already produced no finding, becausecheckreturns early on absence. Reverted, verdict recorded as not-a-defect, with a regression guard kept beside it.The four repairs
metadata-protocolseed-loader — SILENT, and it stored a wrong value. A{type:'user'}field with noreferencecontributed nodependsOnedge and never reachedreferences, so its natural key was written verbatim into a column holding a record id.restpublic-form picker — LOUD. ApublicPickeron a spec-complete{type:'user'}field answered500 LOOKUP_TARGET_MISSING. Now200oversys_user.lintobject-graph.graphFieldOf— SILENT and widest. The slice feedsresolveFieldPath, whoseRELATIONSHIP_FIELD_TYPESadmitsuser; a carrier-less one answeredhop-untargeted, whichisUnjudgeabletreats as "the graph could not answer" anddescribeFieldPathVerdictrenders as nothing. Every rule in the package that resolves a field path therefore stopped judging any path through such a field — the failure modeisUnjudgeable's own docblock says this family exists to end ("a missed report is silence").lintvalidate-field-consumers.walkObject— SILENT. ThedisplayFieldconsumer edge ontosys_userwas never recorded, so a field that column displays was reported consumed by nobody.sys_useris compiled into the linted stack.Nothing widens.
useris the only member ofIMPLICIT_REFERENCE_TARGETS;lookup/master_detail/treewith an absent carrier still name nothing, pinned at every repaired site. The unreadable-carrier refusal is unchanged —referenceTargetOfreads the carrier throughreferenceCarrierOfbefore it judges the type, so #13053/#18550'sTypeErrorstill fires everywhere it fired.⛔ Not a re-widening of #12920. A control pins it: a
userfield spellingreferenceTo: 'zzz_aliased_object'resolvessys_userfrom the type and is never asked for the aliased name; alookupspelling the same alias still resolves nothing and still answers500.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 skippedtypecheckgreen on all three (incl.check:test-typecheckfor lint and rest)origin/main; the closure was rebuilt first becausepackages/specmoved on main's sidedispatch-gates --ranreconciles 63 derived / 60 run / 3 NOT MEASURED / 0 UNRUN. The three arePREREQUISITE NOT MET(exit 3, ⛔ not a pass):check:dual-build-cjs-loadsandcheck:type-check-debtneed a whole-repo build;check-plugin-teardown-shape --self-testcannot reach a commit-pinned fixture on a shallow clone.pnpm lint, narrowed and declared: ① the population is read fromeslint.config.mjs, which states in its own comment that this repo "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file" ⇒ this diff cannot move the verdict on any file it does not touch; ②--format jsonreports 37 files linted, 0 errors, 0 warnings (a superset — the three-dot set includes what the merge brought); ③ measured at97b689b.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-dispatchreferences/platform-readings.md:350).Acceptance notes
referenceTargetOftakes noreaderlabel, whilereferenceCarrierOfdoes 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 ([finding] ApprovalService.resolveLookupFields requires an EXPLICIT reference onuserfields, which spec declares fully specified without one — implicit-target metadata is silently invisible to inbox enrichment #19198, [finding] plugin-audit:planTrackedLookupReadsand the milestone planner both admituserthen require the materializedreference— a trackHistory'd implicit-target field is planned for nobody and the timeline shows the raw id (#19198's shape, one package over, TWO sites) #19264). It is a diagnostic-fidelity question about the arbiter's signature, ⛔ not a defect — fixing it would editpackages/spec/src/data/field-value.zod.tsand change this card's landing path. Successor: the next PR that moves a consumer ontoreferenceTargetOf.graphFieldOfno longer populatesreferencefor a non-relationship field carrying a strayreference(e.g.{type:'text', reference:'foo'}), becausereferenceTargetOfreturnsundefinedoutsideREFERENCE_VALUE_TYPES.5754774179), re-measured on this head by the seat:resolveFieldPath'sRELATIONSHIP_FIELD_TYPESgate is on the intermediate HOPS (object-graph.ts:405,hop-untargeted), ⛔ not on the leaf — the leaf comes backokcarrying itsmetawhatever its type (object-graph.ts:413) — andvalidate-preset-comparands.ts:450DOES read it (verdict?.kind === 'ok' ? strName(verdict.meta?.reference) : undefined). So «no other module readsGraphField.reference» is false on the tree, and the sentence is withdrawn. What actually follows: auserpicker's filter rows now bind tosys_user, which is the object the route queries — correct; atext-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 beforereferenceTargetOf(rest-server.ts:10935) and whoseFieldSchema.referencecarries no non-reference-type refusal, so{type:'text', reference:'foo'}behind apublicPickernow answers 500LOOKUP_TARGET_MISSINGwhere it used to searchfoo. Both are pull-backs to the declared contract — thereferencedescribe text scopes it to lookup/master_detail fields, andforms.mdx:238scopes pickers to lookup / master-detail /user— on a shape outside the documented surface, so ⛔ neither is a widening andClause-②: nois unaffected. Strictly more correct: atextfield'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