Refuse an unreadable reference carrier at the ten residual readers (ruling E item 2 residue) - #19080
Conversation
…gh the one arbiter Ruling letter E item 2 asked for the loud refusal at EVERY reader of `FieldSchema.reference`. PR #18503 delivered it at the arbiter (`referenceCarrierOf`) and the lint read sites; these ten reads still answered "no target" for a carrier no reader can read. Each site keeps absence and unreadability as DIFFERENT answers: `null`, `undefined` and `''` still answer `undefined` and are still silent (the key is `.optional()` and `StrictField` declares it nullable); only a carrier in a shape the contract does not admit refuses. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
…uted reader Every refusal case has an absence partner (`undefined` / `null` / `''`) and a positive control, so a harness that had stopped exercising the reader could not pass vacuously. The objectql suite also pins that nothing is written when the refusal fires, which is the measured defect inverted. The four lint sites take the form the already-routed lint readers use: the literal `.reference` read stays at the site so the #5017 receiver meta-test keeps its subject, and only the shape judgment moves to the arbiter. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
… presence `check:objectql-double-limit` graded the new file's `find` double limit-blind. Fixed at the double — the baseline never grows. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
… list `relationTarget`'s subject is WHICH KEY it reads, stated at length in its own docblock, so the read stays greppable in the function body and only the shape judgment moves to the arbiter — the same form the lint readers use. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 38 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 90bbbf7ebb4d2a43276dad5f3d11b6f058f031c9 && git checkout 90bbbf7ebb4d2a43276dad5f3d11b6f058f031c9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 176b03582e600ee5628d21bff9422073c5a5530c a17094d9482575b014edce04224a0426e4e93712 && git checkout -B drift-repro 176b03582e600ee5628d21bff9422073c5a5530c && git merge --no-ff a17094d9482575b014edce04224a0426e4e93712
node scripts/docs-audit/affected-docs.mjs --json 176b03582e600ee5628d21bff9422073c5a5530c
|
…n the page The route's error table enumerated four outcomes and now has a fifth: a stored `reference` holding a non-string declares a target the route cannot read, and answers the sanitised fault rather than LOOKUP_TARGET_MISSING. The page said "could not be resolved from the field definition" for the missing-target code, which read as covering both — the distinction this routing exists to make. Found by hand, not by the drift bot: `rest-server.ts` yields no doc anchor, so pages documenting it are invisible to that run by its own declaration. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
|
The event names head Measured, ⛔ not pattern-matched
⇒ the push cancelled the in-flight run and the aggregator turned its cancelled members into a One difference from the sibling case, stated because it weakens the reassuranceOn PR #19076 the same artifact appeared and the superseding commit changed no TypeScript at all, so a genuine type error could not have hidden behind the supersession. ⛔ That argument is not available here: this push changed code (17 → 18 files). So the only support for 「artifact」 is the cancelled-member evidence and the three-second duration — and the actual verdict is whatever the four ⇒ if a real type failure exists it will appear there, and it will be this PR's to fix. This comment is not a claim that the PR is green. ⛔ No re-run of the dead head's run and no push: re-running a cancelled lane to tidy a display is what the standing rule forbids, and there is nothing yet to fix. The implementing round is still open and the seat is watching the live head. Generated by Claude Code |
Contract reviewHead reviewed: Reviewed-by: isolated at-tier reviewer commissioned by Tier established by the reviewer as its first act, before any review work, by grepping its own transcript: Verdict: CLAUSE-② PASS. All five blocking items pass. Reviewed in the reviewer's own worktree at the head above;
Disposition of the carrierBoth carriers were cleared seconds apart after this record was written, and this comment is the record the clear cites. The declaration stands as Findings 5, 7, 8 and 10 are non-blocking and are weighed on the card (#18550, comment 5733563414), together with three wording nits the seat deliberately did not push, because a push moves the head this verdict is bound to: the REST pin has no Generated by Claude Code |
Contract reviewServed-tier: 6/6 Supersedes comment 5733580409, which carried the same verdict and the same readings but failed the record contract twice: the head was under a key of this seat's own invention (「Head reviewed:」 rather than ① Derived judgmentsTen items reviewed by an isolated at-tier reviewer in its own worktree at the head above; Blocking items, all PASS:
Non-blocking findings: item 4 (no error code added; an existing sanitised ② Semver level
③ Boundary flagsNo security or permission boundary moves. No published contract surface is widened, no export, key or error code is added, and
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #18550
Clause-②: yes
Ruling letter E item 2 on #18095 asked for the loud refusal at every reader of
FieldSchema.reference. PR #18503 delivered it at the arbiter (referenceCarrierOf) and the lint target readers, and named the remainder itself under "What this PR does not close". This is that remainder, routed — with a per-site decision about absence vs unreadability, because in this design they are deliberately different answers.JOB ONE — the count, re-derived per line. ⛔ Nothing inherited.
Triage's instruction was 「⭐ 数是 9,⛔ 不是 PR 正文写的 10 —— 契约复核重新数过。⭐ 这正是本班刚立的那条:计数是会腐烂的读数 ⇒ 派发时再数一次,⛔ 不要把 9 或 10 任何一个往前抄。」 So neither number is carried forward here. What the re-derivation found is that 9 is not reachable at any single granularity — it mixes two.
Every cited line was printed at the card's stated merge base
2f11e2db7and re-located onorigin/main@0ec81857a:2f11e2db7)objectql/src/engine.ts:1305213096const ref = fdef.reference;objectql/src/engine.ts:1349113544const ref = fdef.reference;rest/src/rest-server.ts:1083510889referenceObject = def?.reference;metadata-protocol/src/seed-loader.ts:701701fieldDef.referencelint/src/validate-expressions.ts:380380const ref = def.reference;lint/src/validate-field-consumers.ts:552552const reference = strName(field.reference);lint/src/validate-object-references.ts:297297strName(field.reference),lint/src/validate-object-references.ts:316316strName(param.reference),lint/src/validate-sharing-rule-enforceability.ts:261261const ref = f.reference;verify/src/derive.ts:136136const ref = f?.reference;That is 10 cited LINES in 8 FILES. Neither is 9. The 9 is reachable only by mixing granularities, and triage's own parenthesis shows the mix: 「objectql ×2、rest、metadata-protocol、lint ×4、verify」 = 2 objectql LINES + 3 non-lint FILES + 4 lint FILES = 9, counting objectql per line and lint per file. PR #18503's own "10 sites" is its 11-line C3 list (the 10 above plus
validate-preset-comparands.ts:431) with one pair merged. Both numbers are arithmetic over the same list; the list is the fact, the count was the rotting reading.validate-preset-comparands.ts:431(now:450) is confirmed not residue, on the same reading the contract review gave: it readsverdict.meta?.reference, andmetais theGraphFieldslicegraphFieldOfbuilds — which PR Retire check-reference-carrier-shape; refuse an unreadablereferencecarrier at the reader #18503 routed. It is covered transitively, and so areobject-graph.ts:383and:386.objectql/src/engine.ts:9033(cd?.reference === parent.nameinbuildSummaryIndex) produces the same silent skip — "can't resolve the relationship — skip", so asummaryfield never recomputes. It is already classified, asengine.ts:8989in PR Retire check-reference-carrier-shape; refuse an unreadablereferencecarrier at the reader #18503's C2 list, a boundary that PR drew deliberately. Routing it would widen this card past its scope; it goes to the seat as a finding instead.The "four routed lint read sites", re-derived
The card paraphrases PR #18503 as delivering the refusal "at the arbiter plus the four lint read sites". The PR's own words are "A — changed here (6 read sites, 4 files)", and one of those four files is
packages/spec/src/data/field-value.zod.ts— the arbiter itself, not lint. Measured onorigin/main:referenceCarrierOfcalldata-model-rules.ts,object-graph.ts,validate-security-posture.ts)refOf,graphFieldOf,refOf)refOf(×4 indata-model-rules.ts, ×3 invalidate-security-posture.ts,graphFieldOf(×2 inobject-graph.tsSo four lint read sites is not a reading at any granularity: it is 3 files / 3 calls / 9 read sites, plus the arbiter's own file as the fourth FILE. The seat's lit control reproduces exactly (2 grep hits per file = 1 import + 1 call).
The C1 set, re-derived — the card is right, and the PR body is wrong in the other direction too
C1 was "explicit
typeof === 'string'narrowing — the same silence spelled differently". Re-derived repo-wide overpackages/*/src:The card's C1 list, checked one by one:
plugin-audit/src/audit-writers.ts:472, 527, 629, 693''/undefined, audit rows lose the targetrest/src/export-format.ts:170reference: undefinedin the export meta, soreferenceFieldNamesomits the column and it exports raw ids with no$expandcli/src/commands/doctor.ts:792spec/src/kernel/functional-completeness.ts:167(now:177)FIELD_RELATIONSHIP_WITHOUT_REFERENCE, severity error. Measured, not read: registering a fixture with an object carrier printed[Registry] Object "task" registered with 1 functionally-incomplete field(s) … account: [error] field/relationship-without-reference⇒ 6 genuine C1 sites, not 7.
And what the card's list gets wrong in the other direction — the thing it asked to be told:
typeof === 'string'narrowings, inline or through astrNamehelper that is one:validate-expressions.ts:380,validate-field-consumers.ts:552(strName=typeof v === 'string' && v.length > 0),validate-object-references.ts:297and:316,validate-sharing-rule-enforceability.ts:261,verify/derive.ts:136. The PR body's C1/C3 split reads as two populations; at six of ten sites it is one code shape sorted into two classes. Only four residue lines are a genuinely different shape: two truthiness gates (engine.ts×2), one truthiness-plus-cast (seed-loader.ts), one bare assignment with no narrowing at all (rest-server.ts).spec/src/automation/builtin-node-config.zod.ts:527—typeof field.reference === 'string' && field.reference.trim() !== ''inside asuperRefine, falling through toctx.addIssue(SCREEN_FIELD_LOOKUP_REFERENCE_REQUIRED). Sofunctional-completeness.tsis not the lone misclassification; there are two C1-SHAPED reporters, and ⛔ neither may be made to throw — a throw inside a refinement makessafeParsethrow instead of returning{success: false}.undefined(C2 by the PR's classification, ⛔ not touched here, reported as findings):plugin-approvals/src/approval-service.ts:5773doesString(f.reference), which turns an object carrier into the literal target name[object Object];service-analytics/src/plugin.ts:736returns the unnarrowed carrier;cli/src/commands/doctor.ts:709and:895push it into a dependency graph.The judgement per site — absence or unreadability, and the pin
referenceCarrierOfis the whole of the contract used here:undefined/null/''answerundefined(ABSENCE — a field is allowed to name no target;FieldSchema.referenceis.optional()andStrictFielddeclares it nullable), and any other shape throws aTypeErrornaming the shape and the fix. ⛔ No site throws on a falsy carrier. Every refusal pin has an absence partner and a positive control.objectqlplanCascadeAtomicityif (!ref)and then failed both name comparisons, so the child dropped out of the referencing set and'none'— the one verdict asserting nothing references this object — could be returned over a schema nobody could readengine-cascade-reference-carrier.test.ts"seam 1"objectqlcascadeDeleteRelationsrestpublic-form lookup pickercatch {}; routing alone would have been swallowed intoLOOKUP_TARGET_MISSING, so the field def is hoisted out and read after it. An object carrier was also forwarded verbatim asquery.objectintofindDatapublic-form-lookup-picker.test.ts, 4 casesmetadata-protocolbuildDependencyGraphas stringcast that asserted exactly what the truthiness guard had not checkedseed-loader-reference-carrier.test.tslintmasterDetailCountmaster_detailinvisible to the count, soparentwas reported unbound from metadata that declares a mastervalidate-expressions.test.tslintwalkObjectdisplayField edgedisplayFieldconsumer edge was never recorded, so a field a lookup DOES display was reported carrier-onlyvalidate-field-consumers.test.tslintfield target + action-param targetcheckreturns early onundefined, so the declaration reported NOTHING — not unknown-object, not missing-referencevalidate-object-references.test.tslintmasterOfcontrolled_by_parentdetail whose master IS declared answeredundefinedvalidate-sharing-rule-enforceability.test.tsverifyrelationTargetreferencetarget" — the exact trade this reader already refused to make for a rejected aliasderive.test.tsvalidate-preset-comparands.ts:450GraphFieldslice — no edit, measured not assumedengine.ts:9033buildSummaryIndexActionParamSchema.reference— one caveat, stated rather than buriedvalidate-object-references.ts:316reads an ActionParam, not a FieldSchema field. The two are one contract by the spec's own words —ActionParamSchema.reference's docblock: "Key name deliberately mirrorsFieldSchema.referenceso the same spelling", declaredSnakeCaseIdentifierSchema.optional(). The refusal text it now produces says "FieldSchema declares it as an optional STRING", which names the sibling schema rather than this one. ⛔ Widening the arbiter's message would mean editingpackages/spec, outside this card's declared file surface, so it is reported instead of done.Evidence
The silence, reproduced at the representative site, then refusing
objectqlcascade delete — the hot path, and the one whose wrong answer is a production behaviour change. Same probe, before and after the routing:No
restrictrefusal, noset_null, nothing logged, and the caller told the delete succeeded. The refusal now fires before any row is touched, becausedelete()callsplanCascadeAtomicityfirst.⭐ Two facts the probe forced, both worth recording: the engine's own WRITE path already refuses this shape (
insertrunsassertReferencesResolve→referenceTargetOf→ the same arbiter), so the child row had to be written straight through the driver — which is exactly the provenance the arbiter's docblock names (a rawregisterObject, a stored row rehydrated past its schema). And the registry's own completeness check printedfield/relationship-without-referencewhile the cascade stayed silent: the platform reported the shape in one channel and mis-read it in another.Ablation — the pins can fail, proved on disk by content hash
One routed read reverted through
scripts/ablation-replace.mjs(⛔ notsed -i), on the committed tree:Restored with the explicit form, verified independently afterwards:
git diff HEADempty andgit hash-objectequal to the HEAD blob. The redness is targeted — exactly the mutated seam's pin failed while seam 2 and all three absence controls stayed green, so the pins are per-site rather than one shared assertion.Tests
@objectstack/objectql@objectstack/rest@objectstack/metadata-protocol@objectstack/lint@objectstack/verifytypecheck(all five, incl. test layers)objectql's shrink-only test ledger held at 40 files / 234 errors / 65 pinned signatures — not raised⭐
check:type-check-debtre-measured all 4 DEBT ledger entries at the final head: 53 raw errors, none above its recorded number.Repo-wide lint — run, ⛔ not narrowed
pnpm lint(eslint . --no-inline-config) atbd1b3dcad: exit 0, 75s. No narrowing is claimed, so no narrowing evidence is owed.Gate census — derived from the REAL change set
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat the final head, every command run with$?captured before any pipe, then reconciled with--ran:pnpm check:dual-build-cjs-loads—PREREQUISITE NOT MET(exit 3), ⛔ neither a pass nor a finding. Its own words: "this gate reads built output, and some package has no dist/ … Runpnpm buildfirst. ⛔ This is NOT a pass: nothing was measured." 35 packages outside this card's build closure have nodist/; the prerequisite is a repo-wide build, which is what CI does before this step. Bounding fact, measurable from the diff: zeropackage.json,tsup/tsconfig,turbo.jsonor workflow files are touched — 17 changed paths, all.changeset/pluspackages/**/src.check:objectql-double-limitgraded the new test file'sfinddouble limit-blind. Fixed in the double (the caller's bound applied after the filter, by presence). ⛔ The baseline was not touched; the re-run printsbaseline key set verified against 0ec8185: no files added.check:type-check-debtexited 3 twice on its own prerequisite — first for an unbuilt@objectstack/driver-turso, then because the ablation's restore rewroteengine.tsand leftdist/older than source by mtime. Both satisfied by building, then exit 0. ⛔ No ledger entry was raised on an exit 3, which that gate forbids explicitly.Clause ② — the widening-tells reading, with its NOT MEASURED rows
node scripts/pm/check-widening-tells.mjs --declaration yes --diff …→ exit 0:yesthe gate examines no file. So the same diff was also run with--declaration noas a DIAGNOSTIC (⛔ not a declaration — the declaration isyes, and it is the seat's), to get the per-file reading:⇒ 17 NOT MEASURED rows, zero judged. ⛔ Not a clean reading, and reported as such: the gate's four tells cover Zod schema surfaces, closed sets, published export listings and registries, and this diff touches none of them.
--self-testpasses (510 cases).The declaration stands at
yeson the ground the seat's claim records — the observable behaviour at published runtime doors (rest-server.ts,objectqlcascade delete) changes — ⛔ and this PR does not review its own verdict.Changeset
One changeset,
minor× 5, with the level's reason stated rather than assumed.minorand notpatchbecause the declaration isClause-②: yesandcheck-changeset-no-major's level axis requires at least one moved published package gradedminoror above;minorand not a breaking grade because nothing conformant changes — a non-stringreferencecould not be authored, stored or parsed before this release either. That is the same grading and the same "Upgrading: nothing conformant changes" reasoning PR #18503's own changeset shipped for the same class of change, at the same launch-window convention.Docs — one page was falsified, and it was not on the bot's list
Added by the
domain:spec#3seat after the body's single dev write: this row did not exist when the body was written, and the dev does not PATCH a PR body.Docs Drift Check re-derived on the merge tree the bot names (
f3a03768de7b46fea7e8637158666569825c8f23, not the PR head), in a separate detached worktree so this branch never moved, reproducing the comment exactly: 28 docs, 7 release-owned, 21 hand-written, 9 anchors, 1 anchorless change, 38 package-mention pages.The 21 hand-written rows are clean, and the reason is worth stating: 20 of them came in through a single weak anchor — the literal
master_detail, which this diff only MOVED (the seed-loaderifwas rewritten) — and the eight symbol anchors (planCascadeAtomicity,cascadeDeleteRelations,masterDetailCount,masterOf,relationTarget,validateObjectReferences,walkObject,buildDependencyGraph) match no hand-written page at all. Each falsifiable statement in the 21 was re-read by hand and judged; none asserts the silence this change removes.The one page this change did falsify was invisible to that run, by the bot's own declaration:
packages/rest/src/rest-server.tsyields no doc anchor, so the page documenting that route could not appear on its list 「on this run or any run」. Found by a hand grep forLOOKUP_TARGET_MISSINGacrosscontent/docs:content/docs/ui/forms.mdx, whose public-form lookup picker error table read as covering both absence and unreadability — the exact distinction this routing exists to draw. Fixed here in two table cells: one clause on theobjectkey, and a new row for the500 INTERNAL_ERRORenvelope with the reason it is deliberately notLOOKUP_TARGET_MISSING. ⛔ Not broad doc rewriting.⛔ The 7 release-owned pages are read-only and byte-untouched (
git status content/docs/releases/empty). Each was checked for falsification and none is: six appear only via the weakmaster_detailliteral, andv17/17-1.mdxnamescascadeDeleteRelationsfor the registry read (#9002) rather than this carrier read. The generatedreferences/api/{contract,error-code-ledger}.mdxalso name the code but are auto-generated and this change adds no error code.Acceptance notes — noted, not filed (⛔ dev files no cards; these go to the seat)
The three-class findings above (a
String(f.reference)that invents[object Object]as a target name; the remaining C2 truthiness/equality readers,engine.ts:9033included; the arbiter message namingFieldSchemaat an ActionParam read site) are reported to the dispatching seat with dedup words, which runs dedup and files. Also noted and ⛔ not filed:{ reference: X.reference }wrapper form at the four lint sites and inverify/derive.tsis deliberate and is not stylistic drift: the literal.referenceread stays at the site so the validate-expressions / validate-security-posture 也有同形的 spec 不声明键的??别名读法(#5009 建议 3 的核对结果) #5017 receiver meta-test inpackages/lintkeeps its subject (it reads the rule's SOURCE to prove it readsreferenceand never an alias, and folding the read into a helper call disarms that scan silently — it went red on exactly that during this work), andrelationTarget's docblock makes the same key-spelling argument at length. The three runtime readers pass the field def directly, where the def is the natural argument and no such scan exists. Successor for the asymmetry: whoever routes the C2 population next.The open question this card carries — ⛔ not answered here
Whether arbiter + lint routing satisfied ruling item 2, or whether PR #18503 was owed the residue too, is recorded as the maintainer's to give. ⛔ This PR does not answer it and landing it is ⛔ not an answer. Two pieces of evidence bearing on it surfaced and are offered without a verdict: the ten residue lines were a measurable population at the time (each printed and read), which bears on whether they were reachable in that PR's scope; and six of the ten are the SAME code shape PR #18503 sorted into its deliberately-unchanged C1 class, so "the residue" and "the boundary" were not two populations but one, sorted twice.
Generated by Claude Code