Uh oh!
There was an error while loading. Please reload this page.
fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered - #14534
Conversation
…surement Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…its measurement, and report where the alias answered Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ster-detail-reference-alias
📓 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:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 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 1ed885cf3f03db1cd414ec689be856541038f92a && git checkout 1ed885cf3f03db1cd414ec689be856541038f92a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 04ee9f884ce85f2948c076155d70663f3e675951 a76a222fa4da5f397ef4063522e084988d30cdc5 && git checkout -B drift-repro 04ee9f884ce85f2948c076155d70663f3e675951 && git merge --no-ff a76a222fa4da5f397ef4063522e084988d30cdc5
node scripts/docs-audit/affected-docs.mjs --json 04ee9f884ce85f2948c076155d70663f3e675951
|
os-musk
commented
Sep 2, 2026
Landing provenance (engine execution seat,
Generated by Claude Code |
Fixes#13543
This card was measurement-first: triage ruled ⛔ no code before the census, and two of its three branches needed no change at all. The census ran, it answered, and this PR is the branch it selected. The full census is comment 5507364550 on the card, and the same sweep is posted on #13542 as evidence for that card's Restart-when — it does not satisfy it, and no state or label claim is made there.
What the census answered
resolveMasterDetailRelationaccepts the rejected aliasreferenceTobeside the canonicalreference, and the type beside it stated a population for that tolerance in one line: "referenceTois the stored-row spelling." Nothing in the tree measured it. Measured now, whole tree, both spellings counted separately, with positive controls run so no zero comes from a pathspec that matches nothing:Field.masterDetail(...)and 132Field.lookup(...)declarations across*.object.ts(112 files),examples/,packages/qa/and thecreate-objectstacktemplates go through the@objectstack/specbuilders, which emit the canonical key. Not one alias is hand-written past them.metadata-fslayouts — zero, both spellings. Every raw hit is prose.referenceToon a field def — reader pins only, nine files, each pinning a refusal or a tolerance.The assertion was wrong, and correcting it is the substance of this PR. ADR-0087's
fieldReferenceToAliasrecords in its own docblock that camelCasereferenceTois deliberately not converted because it "is not the spelling the objectql runtime wrote into stored object rows" — the stored dialect isreference_to, which this reader does not read. The one line justifying the tolerance named the wrong spelling.Why the tolerance nonetheless stays
The population is unmeasured; the path is not, and it is the one path nothing else covers.
registerObjectskips Zod by design, and every caller of this resolver reads that sameSchemaRegistry— now pinned by a test that registers an alias-spelled object and resolves it.reference_toon stored rehydration and onos migrate meta, and deliberately leavesreferenceToalone. SoreferenceTois the one spelling simultaneously unconverted upstream and read here. That is why this reader is asymmetric, and the asymmetry is now pinned as a record rather than left to look like an oversight.parentunbound andrule-validator.tsreads an unbound scope root as LOCKED, verbatim. Narrowing would take a raw-registered, alias-spelled detail object from "lock enforced against its header" to "everyparent-scoped field permanently unwritable, writes silently stripped" — an availability defect, not a spelling correction.⛔ So this PR narrows nothing, and the zeros above license nothing. Narrowing is only honest behind a migration that sweeps stored and raw-registered metadata first.
The change
packages/objectql/src/master-detail.tsonly. No file underpackages/plugins/plugin-securityand no file underpackages/objectql/src/engine.tsis touched.referenceKeyOfanswers which spelling resolved, andreferenceOfderives its value from that answer rather than spelling a second??chain, so the diagnostic and the resolution can never disagree about the key read. It is the invariantplugin-security'srefKeyrecords for the sibling reader.!= nulltest??applies, so a present-but-emptyreferencestill wins the read rather than falling through to the alias — pinned.referenceTo, the resolver reports once per object+field+spelling through an optionalwarnsink defaulting toconsole.warn. Never a throw. That is the same caller-supplied-callback shape and default aswarnFunctionalCompletenessin the same package — a plain function in a bag, not a method lifted off a receiver-sensitive logger, which is whycheck:logger-receiver-detachis green on it. Once per distinct defect rather than per write, because this resolver sits on the write path and a per-write line is a noise defect of its own; the one boundary of a process-lifetime set is stated in the code rather than left to be discovered.field/relationship-without-referencediagnostic, which calls the same field "runtime-DEAD ... never-resolves" — measurably false for this consumer, and two diagnostics disagreeing about one field is worse than one.Drift found against the dispatch's assumptions
engine.ts, which this card must not touch. The options-bag-with-console-default above is what makes the line loud today with zeroengine.tsedits; the alternative (an injected-only sink) would have been a phantom that never fires in production.plugin-security'sREFERENCE_SPELLINGSsits at:6051today, not the:5918recorded on [finding] the deliberatereference_totolerance in plugin-security has no measured stored-metadata population — the prerequisite for ever narrowing it #13542 on 2026-08-31. The reader itself is unchanged. Flagged on [finding] the deliberatereference_totolerance in plugin-security has no measured stored-metadata population — the prerequisite for ever narrowing it #13542 because that card'sRestart-touchpoints at the file.Verification — head
a76a222faRun after the last commit, on the merged tree.
origin/mainhad moved 10 commits and touchedregistry.ts, which the new test imports, so the branch was merged and everything below re-run rather than published against a stale base.pnpm --filter @objectstack/objectql exec vitest run— 260 files / 4494 tests pass.pnpm --filter @objectstack/objectql typecheck— pass, includingcheck:test-typecheck. objectql'stsconfig.jsonexcludes**/*.test.ts, so the program holding the new test ispackages/objectql/tsconfig.test.json;tsc --listFiles -p tsconfig.test.jsonshows both touched files in it, with 0 errors attributable to either (the 242 total is exactly the shrink-only ledger count, and the gate returns OK).pnpm lint— full repo,eslint . --no-inline-config, green. Not narrowed, so nothing to declare.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, which reports the same 36 as the dispatch). 34 green. Two are NOT MEASURED in their own words, neither a finding:check-test-completenessexit 3 ("running the family locally, record this gate as NOT MEASURED" — it wants a savedturbo run testlog), andcheck-half-statesexit 3 (its GitHub route is denied to this container — an unread instrument). Exit codes captured after a redirect, never across a pipe.pnpm check:nul-bytes,pnpm check:error-status-conformance— green. The two gates named to judge the loud line,check:durability-log-levelandcheck:logger-receiver-detach, are both green.check:dual-build-cjs-loads,check:type-check-debt) are real readings rather than prerequisite failures.Ablation — direction predicted before the run, and matched exactly
Predicted: removing the loud call turns RED exactly the four assertions that expect a report, and leaves the six quiet-path pins GREEN. Measured: 4 failed, 6 passed, and they were the predicted four (
is LOUD,default sink is console.warn,reports ONCE, and the reachability test's final assertion), each failing as "expected to be called 1 times, but got 0 times".The implementation was committed before the mutation, so the restore leg had a real reference. The mutation was confirmed on disk by content, not by an editor's exit code: the pristine line count went 1 to 0, the injected marker count 0 to 1, and
git hash-objectmoved from05d5f3b4to98a93f36. Restore wasgit checkout HEAD -- "$REPO_ROOT/..."from atrap ... EXIT INT TERMwith an absolute path, and is proved by bytes:git diff HEADempty, blob back to05d5f3b4matching the HEAD blob, zero marker residue.No
dist/leg is needed and none was run. The test imports the module relatively —import { resolveMasterDetailRelation } from './master-detail.js'— so vitest resolves it from source; the ablation was picked up with no rebuild, which is itself the evidence for that claim.🤖 Generated with Claude Code
https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Generated by Claude Code