fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered - #14534

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias
Sep 2, 2026
Merged

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered#14534
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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

resolveMasterDetailRelation accepts the rejected alias referenceTo beside the canonical reference, and the type beside it stated a population for that tolerance in one line: "referenceTo is 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:

  • Authored declarations — zero, both spellings. All 8 Field.masterDetail(...) and 132 Field.lookup(...) declarations across *.object.ts (112 files), examples/, packages/qa/ and the create-objectstack templates go through the @objectstack/spec builders, which emit the canonical key. Not one alias is hand-written past them.
  • Stored-metadata seeds, JSON/YAML fixtures, metadata-fs layouts — zero, both spellings. Every raw hit is prose.
  • In-tree referenceTo on a field def — reader pins only, nine files, each pinning a refusal or a tolerance.
  • Metadata at rest in a live deployment — NOT MEASURED. No command in this repository reaches it. The zeros are zeros for the tree, not the world.

The assertion was wrong, and correcting it is the substance of this PR. ADR-0087's fieldReferenceToAlias records in its own docblock that camelCase referenceTo is deliberately not converted because it "is not the spelling the objectql runtime wrote into stored object rows" — the stored dialect is reference_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.

  • A raw registerObject skips Zod by design, and every caller of this resolver reads that same SchemaRegistry — now pinned by a test that registers an alias-spelled object and resolves it.
  • The conversion layer normalises reference_to on stored rehydration and on os migrate meta, and deliberately leaves referenceTo alone. So referenceTo is 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.
  • Two of this resolver's four callers fail closed. An unresolved relation leaves parent unbound and rule-validator.ts reads an unbound scope root as LOCKED, verbatim. Narrowing would take a raw-registered, alias-spelled detail object from "lock enforced against its header" to "every parent-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.ts only. No file under packages/plugins/plugin-security and no file under packages/objectql/src/engine.ts is touched.

  • The module docblock carries the measurement — which corpus, what count, which path — in place of the assertion.
  • referenceKeyOf answers which spelling resolved, and referenceOf derives 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 invariant plugin-security's refKey records for the sibling reader.
  • No behaviour change. The key is selected with the same != null test ?? applies, so a present-but-empty reference still wins the read rather than falling through to the alias — pinned.
  • Loud where the alias answered. When the returned relation resolved from referenceTo, the resolver reports once per object+field+spelling through an optional warn sink defaulting to console.warn. Never a throw. That is the same caller-supplied-callback shape and default as warnFunctionalCompleteness in the same package — a plain function in a bag, not a method lifted off a receiver-sensitive logger, which is why check:logger-receiver-detach is 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.
  • The report also corrects the registration-time field/relationship-without-reference diagnostic, 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

Verification — head a76a222fa

Run after the last commit, on the merged tree. origin/main had moved 10 commits and touched registry.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 run260 files / 4494 tests pass.
  • pnpm --filter @objectstack/objectql typecheck — pass, including check:test-typecheck. objectql's tsconfig.json excludes **/*.test.ts, so the program holding the new test is packages/objectql/tsconfig.test.json; tsc --listFiles -p tsconfig.test.json shows 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.
  • Gate union: 36 families, re-derived on this tree (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-completeness exit 3 ("running the family locally, record this gate as NOT MEASURED" — it wants a saved turbo run test log), and check-half-states exit 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-level and check:logger-receiver-detach, are both green.
  • Workspace build 70/70, so the two build-dependent gates (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-object moved from 05d5f3b4 to 98a93f36. Restore was git checkout HEAD -- "$REPO_ROOT/..." from a trap ... EXIT INT TERM with an absolute path, and is proved by bytes: git diff HEAD empty, blob back to 05d5f3b4 matching 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 10 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ui/forms.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 04ee9f884ce85f2948c076155d70663f3e675951packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1ed885cf3f03db1cd414ec689be856541038f92a — the merge of head a76a222fa4da5f397ef4063522e084988d30cdc5 into base 04ee9f884ce85f2948c076155d70663f3e675951, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 04ee9f884ce85f2948c076155d70663f3e675951 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session_0112hMx9hjJ9BgB28X97DS68) — flipped to ready at 13:11Z and auto-merge (squash) armed at 13:12Z on head a76a222fa.


Generated by Claude Code

Merged via the queue into main with commit aaa4e65Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13543-master-detail-reference-alias branch September 2, 2026 13:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] a fourth tolerant alias reader — master-detail.ts accepts referenceTo, with its stored-row population asserted but never measured

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered - #14534

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias
Sep 2, 2026
Merged

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered#14534
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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

resolveMasterDetailRelation accepts the rejected alias referenceTo beside the canonical reference, and the type beside it stated a population for that tolerance in one line: "referenceTo is 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:

  • Authored declarations — zero, both spellings. All 8 Field.masterDetail(...) and 132 Field.lookup(...) declarations across *.object.ts (112 files), examples/, packages/qa/ and the create-objectstack templates go through the @objectstack/spec builders, which emit the canonical key. Not one alias is hand-written past them.
  • Stored-metadata seeds, JSON/YAML fixtures, metadata-fs layouts — zero, both spellings. Every raw hit is prose.
  • In-tree referenceTo on a field def — reader pins only, nine files, each pinning a refusal or a tolerance.
  • Metadata at rest in a live deployment — NOT MEASURED. No command in this repository reaches it. The zeros are zeros for the tree, not the world.

The assertion was wrong, and correcting it is the substance of this PR. ADR-0087's fieldReferenceToAlias records in its own docblock that camelCase referenceTo is deliberately not converted because it "is not the spelling the objectql runtime wrote into stored object rows" — the stored dialect is reference_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.

  • A raw registerObject skips Zod by design, and every caller of this resolver reads that same SchemaRegistry — now pinned by a test that registers an alias-spelled object and resolves it.
  • The conversion layer normalises reference_to on stored rehydration and on os migrate meta, and deliberately leaves referenceTo alone. So referenceTo is 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.
  • Two of this resolver's four callers fail closed. An unresolved relation leaves parent unbound and rule-validator.ts reads an unbound scope root as LOCKED, verbatim. Narrowing would take a raw-registered, alias-spelled detail object from "lock enforced against its header" to "every parent-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.ts only. No file under packages/plugins/plugin-security and no file under packages/objectql/src/engine.ts is touched.

  • The module docblock carries the measurement — which corpus, what count, which path — in place of the assertion.
  • referenceKeyOf answers which spelling resolved, and referenceOf derives 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 invariant plugin-security's refKey records for the sibling reader.
  • No behaviour change. The key is selected with the same != null test ?? applies, so a present-but-empty reference still wins the read rather than falling through to the alias — pinned.
  • Loud where the alias answered. When the returned relation resolved from referenceTo, the resolver reports once per object+field+spelling through an optional warn sink defaulting to console.warn. Never a throw. That is the same caller-supplied-callback shape and default as warnFunctionalCompleteness in the same package — a plain function in a bag, not a method lifted off a receiver-sensitive logger, which is why check:logger-receiver-detach is 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.
  • The report also corrects the registration-time field/relationship-without-reference diagnostic, 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

Verification — head a76a222fa

Run after the last commit, on the merged tree. origin/main had moved 10 commits and touched registry.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 run260 files / 4494 tests pass.
  • pnpm --filter @objectstack/objectql typecheck — pass, including check:test-typecheck. objectql's tsconfig.json excludes **/*.test.ts, so the program holding the new test is packages/objectql/tsconfig.test.json; tsc --listFiles -p tsconfig.test.json shows 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.
  • Gate union: 36 families, re-derived on this tree (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-completeness exit 3 ("running the family locally, record this gate as NOT MEASURED" — it wants a saved turbo run test log), and check-half-states exit 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-level and check:logger-receiver-detach, are both green.
  • Workspace build 70/70, so the two build-dependent gates (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-object moved from 05d5f3b4 to 98a93f36. Restore was git checkout HEAD -- "$REPO_ROOT/..." from a trap ... EXIT INT TERM with an absolute path, and is proved by bytes: git diff HEAD empty, blob back to 05d5f3b4 matching 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 10 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ui/forms.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 04ee9f884ce85f2948c076155d70663f3e675951packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1ed885cf3f03db1cd414ec689be856541038f92a — the merge of head a76a222fa4da5f397ef4063522e084988d30cdc5 into base 04ee9f884ce85f2948c076155d70663f3e675951, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 04ee9f884ce85f2948c076155d70663f3e675951 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session_0112hMx9hjJ9BgB28X97DS68) — flipped to ready at 13:11Z and auto-merge (squash) armed at 13:12Z on head a76a222fa.


Generated by Claude Code

Merged via the queue into main with commit aaa4e65Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13543-master-detail-reference-alias branch September 2, 2026 13:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] a fourth tolerant alias reader — master-detail.ts accepts referenceTo, with its stored-row population asserted but never measured

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered - #14534

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias
Sep 2, 2026
Merged

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered#14534
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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

resolveMasterDetailRelation accepts the rejected alias referenceTo beside the canonical reference, and the type beside it stated a population for that tolerance in one line: "referenceTo is 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:

  • Authored declarations — zero, both spellings. All 8 Field.masterDetail(...) and 132 Field.lookup(...) declarations across *.object.ts (112 files), examples/, packages/qa/ and the create-objectstack templates go through the @objectstack/spec builders, which emit the canonical key. Not one alias is hand-written past them.
  • Stored-metadata seeds, JSON/YAML fixtures, metadata-fs layouts — zero, both spellings. Every raw hit is prose.
  • In-tree referenceTo on a field def — reader pins only, nine files, each pinning a refusal or a tolerance.
  • Metadata at rest in a live deployment — NOT MEASURED. No command in this repository reaches it. The zeros are zeros for the tree, not the world.

The assertion was wrong, and correcting it is the substance of this PR. ADR-0087's fieldReferenceToAlias records in its own docblock that camelCase referenceTo is deliberately not converted because it "is not the spelling the objectql runtime wrote into stored object rows" — the stored dialect is reference_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.

  • A raw registerObject skips Zod by design, and every caller of this resolver reads that same SchemaRegistry — now pinned by a test that registers an alias-spelled object and resolves it.
  • The conversion layer normalises reference_to on stored rehydration and on os migrate meta, and deliberately leaves referenceTo alone. So referenceTo is 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.
  • Two of this resolver's four callers fail closed. An unresolved relation leaves parent unbound and rule-validator.ts reads an unbound scope root as LOCKED, verbatim. Narrowing would take a raw-registered, alias-spelled detail object from "lock enforced against its header" to "every parent-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.ts only. No file under packages/plugins/plugin-security and no file under packages/objectql/src/engine.ts is touched.

  • The module docblock carries the measurement — which corpus, what count, which path — in place of the assertion.
  • referenceKeyOf answers which spelling resolved, and referenceOf derives 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 invariant plugin-security's refKey records for the sibling reader.
  • No behaviour change. The key is selected with the same != null test ?? applies, so a present-but-empty reference still wins the read rather than falling through to the alias — pinned.
  • Loud where the alias answered. When the returned relation resolved from referenceTo, the resolver reports once per object+field+spelling through an optional warn sink defaulting to console.warn. Never a throw. That is the same caller-supplied-callback shape and default as warnFunctionalCompleteness in the same package — a plain function in a bag, not a method lifted off a receiver-sensitive logger, which is why check:logger-receiver-detach is 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.
  • The report also corrects the registration-time field/relationship-without-reference diagnostic, 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

Verification — head a76a222fa

Run after the last commit, on the merged tree. origin/main had moved 10 commits and touched registry.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 run260 files / 4494 tests pass.
  • pnpm --filter @objectstack/objectql typecheck — pass, including check:test-typecheck. objectql's tsconfig.json excludes **/*.test.ts, so the program holding the new test is packages/objectql/tsconfig.test.json; tsc --listFiles -p tsconfig.test.json shows 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.
  • Gate union: 36 families, re-derived on this tree (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-completeness exit 3 ("running the family locally, record this gate as NOT MEASURED" — it wants a saved turbo run test log), and check-half-states exit 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-level and check:logger-receiver-detach, are both green.
  • Workspace build 70/70, so the two build-dependent gates (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-object moved from 05d5f3b4 to 98a93f36. Restore was git checkout HEAD -- "$REPO_ROOT/..." from a trap ... EXIT INT TERM with an absolute path, and is proved by bytes: git diff HEAD empty, blob back to 05d5f3b4 matching 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 10 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ui/forms.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 04ee9f884ce85f2948c076155d70663f3e675951packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1ed885cf3f03db1cd414ec689be856541038f92a — the merge of head a76a222fa4da5f397ef4063522e084988d30cdc5 into base 04ee9f884ce85f2948c076155d70663f3e675951, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 04ee9f884ce85f2948c076155d70663f3e675951 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session_0112hMx9hjJ9BgB28X97DS68) — flipped to ready at 13:11Z and auto-merge (squash) armed at 13:12Z on head a76a222fa.


Generated by Claude Code

Merged via the queue into main with commit aaa4e65Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13543-master-detail-reference-alias branch September 2, 2026 13:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] a fourth tolerant alias reader — master-detail.ts accepts referenceTo, with its stored-row population asserted but never measured

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered - #14534

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias
Sep 2, 2026
Merged

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered#14534
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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

resolveMasterDetailRelation accepts the rejected alias referenceTo beside the canonical reference, and the type beside it stated a population for that tolerance in one line: "referenceTo is 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:

  • Authored declarations — zero, both spellings. All 8 Field.masterDetail(...) and 132 Field.lookup(...) declarations across *.object.ts (112 files), examples/, packages/qa/ and the create-objectstack templates go through the @objectstack/spec builders, which emit the canonical key. Not one alias is hand-written past them.
  • Stored-metadata seeds, JSON/YAML fixtures, metadata-fs layouts — zero, both spellings. Every raw hit is prose.
  • In-tree referenceTo on a field def — reader pins only, nine files, each pinning a refusal or a tolerance.
  • Metadata at rest in a live deployment — NOT MEASURED. No command in this repository reaches it. The zeros are zeros for the tree, not the world.

The assertion was wrong, and correcting it is the substance of this PR. ADR-0087's fieldReferenceToAlias records in its own docblock that camelCase referenceTo is deliberately not converted because it "is not the spelling the objectql runtime wrote into stored object rows" — the stored dialect is reference_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.

  • A raw registerObject skips Zod by design, and every caller of this resolver reads that same SchemaRegistry — now pinned by a test that registers an alias-spelled object and resolves it.
  • The conversion layer normalises reference_to on stored rehydration and on os migrate meta, and deliberately leaves referenceTo alone. So referenceTo is 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.
  • Two of this resolver's four callers fail closed. An unresolved relation leaves parent unbound and rule-validator.ts reads an unbound scope root as LOCKED, verbatim. Narrowing would take a raw-registered, alias-spelled detail object from "lock enforced against its header" to "every parent-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.ts only. No file under packages/plugins/plugin-security and no file under packages/objectql/src/engine.ts is touched.

  • The module docblock carries the measurement — which corpus, what count, which path — in place of the assertion.
  • referenceKeyOf answers which spelling resolved, and referenceOf derives 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 invariant plugin-security's refKey records for the sibling reader.
  • No behaviour change. The key is selected with the same != null test ?? applies, so a present-but-empty reference still wins the read rather than falling through to the alias — pinned.
  • Loud where the alias answered. When the returned relation resolved from referenceTo, the resolver reports once per object+field+spelling through an optional warn sink defaulting to console.warn. Never a throw. That is the same caller-supplied-callback shape and default as warnFunctionalCompleteness in the same package — a plain function in a bag, not a method lifted off a receiver-sensitive logger, which is why check:logger-receiver-detach is 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.
  • The report also corrects the registration-time field/relationship-without-reference diagnostic, 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

Verification — head a76a222fa

Run after the last commit, on the merged tree. origin/main had moved 10 commits and touched registry.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 run260 files / 4494 tests pass.
  • pnpm --filter @objectstack/objectql typecheck — pass, including check:test-typecheck. objectql's tsconfig.json excludes **/*.test.ts, so the program holding the new test is packages/objectql/tsconfig.test.json; tsc --listFiles -p tsconfig.test.json shows 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.
  • Gate union: 36 families, re-derived on this tree (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-completeness exit 3 ("running the family locally, record this gate as NOT MEASURED" — it wants a saved turbo run test log), and check-half-states exit 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-level and check:logger-receiver-detach, are both green.
  • Workspace build 70/70, so the two build-dependent gates (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-object moved from 05d5f3b4 to 98a93f36. Restore was git checkout HEAD -- "$REPO_ROOT/..." from a trap ... EXIT INT TERM with an absolute path, and is proved by bytes: git diff HEAD empty, blob back to 05d5f3b4 matching 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 10 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ui/forms.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 04ee9f884ce85f2948c076155d70663f3e675951packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1ed885cf3f03db1cd414ec689be856541038f92a — the merge of head a76a222fa4da5f397ef4063522e084988d30cdc5 into base 04ee9f884ce85f2948c076155d70663f3e675951, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 04ee9f884ce85f2948c076155d70663f3e675951 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session_0112hMx9hjJ9BgB28X97DS68) — flipped to ready at 13:11Z and auto-merge (squash) armed at 13:12Z on head a76a222fa.


Generated by Claude Code

Merged via the queue into main with commit aaa4e65Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13543-master-detail-reference-alias branch September 2, 2026 13:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] a fourth tolerant alias reader — master-detail.ts accepts referenceTo, with its stored-row population asserted but never measured

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered - #14534

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias
Sep 2, 2026
Merged

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered#14534
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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

resolveMasterDetailRelation accepts the rejected alias referenceTo beside the canonical reference, and the type beside it stated a population for that tolerance in one line: "referenceTo is 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:

  • Authored declarations — zero, both spellings. All 8 Field.masterDetail(...) and 132 Field.lookup(...) declarations across *.object.ts (112 files), examples/, packages/qa/ and the create-objectstack templates go through the @objectstack/spec builders, which emit the canonical key. Not one alias is hand-written past them.
  • Stored-metadata seeds, JSON/YAML fixtures, metadata-fs layouts — zero, both spellings. Every raw hit is prose.
  • In-tree referenceTo on a field def — reader pins only, nine files, each pinning a refusal or a tolerance.
  • Metadata at rest in a live deployment — NOT MEASURED. No command in this repository reaches it. The zeros are zeros for the tree, not the world.

The assertion was wrong, and correcting it is the substance of this PR. ADR-0087's fieldReferenceToAlias records in its own docblock that camelCase referenceTo is deliberately not converted because it "is not the spelling the objectql runtime wrote into stored object rows" — the stored dialect is reference_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.

  • A raw registerObject skips Zod by design, and every caller of this resolver reads that same SchemaRegistry — now pinned by a test that registers an alias-spelled object and resolves it.
  • The conversion layer normalises reference_to on stored rehydration and on os migrate meta, and deliberately leaves referenceTo alone. So referenceTo is 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.
  • Two of this resolver's four callers fail closed. An unresolved relation leaves parent unbound and rule-validator.ts reads an unbound scope root as LOCKED, verbatim. Narrowing would take a raw-registered, alias-spelled detail object from "lock enforced against its header" to "every parent-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.ts only. No file under packages/plugins/plugin-security and no file under packages/objectql/src/engine.ts is touched.

  • The module docblock carries the measurement — which corpus, what count, which path — in place of the assertion.
  • referenceKeyOf answers which spelling resolved, and referenceOf derives 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 invariant plugin-security's refKey records for the sibling reader.
  • No behaviour change. The key is selected with the same != null test ?? applies, so a present-but-empty reference still wins the read rather than falling through to the alias — pinned.
  • Loud where the alias answered. When the returned relation resolved from referenceTo, the resolver reports once per object+field+spelling through an optional warn sink defaulting to console.warn. Never a throw. That is the same caller-supplied-callback shape and default as warnFunctionalCompleteness in the same package — a plain function in a bag, not a method lifted off a receiver-sensitive logger, which is why check:logger-receiver-detach is 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.
  • The report also corrects the registration-time field/relationship-without-reference diagnostic, 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

Verification — head a76a222fa

Run after the last commit, on the merged tree. origin/main had moved 10 commits and touched registry.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 run260 files / 4494 tests pass.
  • pnpm --filter @objectstack/objectql typecheck — pass, including check:test-typecheck. objectql's tsconfig.json excludes **/*.test.ts, so the program holding the new test is packages/objectql/tsconfig.test.json; tsc --listFiles -p tsconfig.test.json shows 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.
  • Gate union: 36 families, re-derived on this tree (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-completeness exit 3 ("running the family locally, record this gate as NOT MEASURED" — it wants a saved turbo run test log), and check-half-states exit 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-level and check:logger-receiver-detach, are both green.
  • Workspace build 70/70, so the two build-dependent gates (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-object moved from 05d5f3b4 to 98a93f36. Restore was git checkout HEAD -- "$REPO_ROOT/..." from a trap ... EXIT INT TERM with an absolute path, and is proved by bytes: git diff HEAD empty, blob back to 05d5f3b4 matching 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 10 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ui/forms.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 04ee9f884ce85f2948c076155d70663f3e675951packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1ed885cf3f03db1cd414ec689be856541038f92a — the merge of head a76a222fa4da5f397ef4063522e084988d30cdc5 into base 04ee9f884ce85f2948c076155d70663f3e675951, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 04ee9f884ce85f2948c076155d70663f3e675951 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session_0112hMx9hjJ9BgB28X97DS68) — flipped to ready at 13:11Z and auto-merge (squash) armed at 13:12Z on head a76a222fa.


Generated by Claude Code

Merged via the queue into main with commit aaa4e65Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13543-master-detail-reference-alias branch September 2, 2026 13:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] a fourth tolerant alias reader — master-detail.ts accepts referenceTo, with its stored-row population asserted but never measured

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered - #14534

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias
Sep 2, 2026
Merged

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered#14534
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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

resolveMasterDetailRelation accepts the rejected alias referenceTo beside the canonical reference, and the type beside it stated a population for that tolerance in one line: "referenceTo is 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:

  • Authored declarations — zero, both spellings. All 8 Field.masterDetail(...) and 132 Field.lookup(...) declarations across *.object.ts (112 files), examples/, packages/qa/ and the create-objectstack templates go through the @objectstack/spec builders, which emit the canonical key. Not one alias is hand-written past them.
  • Stored-metadata seeds, JSON/YAML fixtures, metadata-fs layouts — zero, both spellings. Every raw hit is prose.
  • In-tree referenceTo on a field def — reader pins only, nine files, each pinning a refusal or a tolerance.
  • Metadata at rest in a live deployment — NOT MEASURED. No command in this repository reaches it. The zeros are zeros for the tree, not the world.

The assertion was wrong, and correcting it is the substance of this PR. ADR-0087's fieldReferenceToAlias records in its own docblock that camelCase referenceTo is deliberately not converted because it "is not the spelling the objectql runtime wrote into stored object rows" — the stored dialect is reference_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.

  • A raw registerObject skips Zod by design, and every caller of this resolver reads that same SchemaRegistry — now pinned by a test that registers an alias-spelled object and resolves it.
  • The conversion layer normalises reference_to on stored rehydration and on os migrate meta, and deliberately leaves referenceTo alone. So referenceTo is 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.
  • Two of this resolver's four callers fail closed. An unresolved relation leaves parent unbound and rule-validator.ts reads an unbound scope root as LOCKED, verbatim. Narrowing would take a raw-registered, alias-spelled detail object from "lock enforced against its header" to "every parent-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.ts only. No file under packages/plugins/plugin-security and no file under packages/objectql/src/engine.ts is touched.

  • The module docblock carries the measurement — which corpus, what count, which path — in place of the assertion.
  • referenceKeyOf answers which spelling resolved, and referenceOf derives 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 invariant plugin-security's refKey records for the sibling reader.
  • No behaviour change. The key is selected with the same != null test ?? applies, so a present-but-empty reference still wins the read rather than falling through to the alias — pinned.
  • Loud where the alias answered. When the returned relation resolved from referenceTo, the resolver reports once per object+field+spelling through an optional warn sink defaulting to console.warn. Never a throw. That is the same caller-supplied-callback shape and default as warnFunctionalCompleteness in the same package — a plain function in a bag, not a method lifted off a receiver-sensitive logger, which is why check:logger-receiver-detach is 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.
  • The report also corrects the registration-time field/relationship-without-reference diagnostic, 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

Verification — head a76a222fa

Run after the last commit, on the merged tree. origin/main had moved 10 commits and touched registry.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 run260 files / 4494 tests pass.
  • pnpm --filter @objectstack/objectql typecheck — pass, including check:test-typecheck. objectql's tsconfig.json excludes **/*.test.ts, so the program holding the new test is packages/objectql/tsconfig.test.json; tsc --listFiles -p tsconfig.test.json shows 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.
  • Gate union: 36 families, re-derived on this tree (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-completeness exit 3 ("running the family locally, record this gate as NOT MEASURED" — it wants a saved turbo run test log), and check-half-states exit 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-level and check:logger-receiver-detach, are both green.
  • Workspace build 70/70, so the two build-dependent gates (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-object moved from 05d5f3b4 to 98a93f36. Restore was git checkout HEAD -- "$REPO_ROOT/..." from a trap ... EXIT INT TERM with an absolute path, and is proved by bytes: git diff HEAD empty, blob back to 05d5f3b4 matching 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 10 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ui/forms.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 04ee9f884ce85f2948c076155d70663f3e675951packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1ed885cf3f03db1cd414ec689be856541038f92a — the merge of head a76a222fa4da5f397ef4063522e084988d30cdc5 into base 04ee9f884ce85f2948c076155d70663f3e675951, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 04ee9f884ce85f2948c076155d70663f3e675951 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session_0112hMx9hjJ9BgB28X97DS68) — flipped to ready at 13:11Z and auto-merge (squash) armed at 13:12Z on head a76a222fa.


Generated by Claude Code

Merged via the queue into main with commit aaa4e65Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13543-master-detail-reference-alias branch September 2, 2026 13:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] a fourth tolerant alias reader — master-detail.ts accepts referenceTo, with its stored-row population asserted but never measured

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered - #14534

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias
Sep 2, 2026
Merged

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered#14534
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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

resolveMasterDetailRelation accepts the rejected alias referenceTo beside the canonical reference, and the type beside it stated a population for that tolerance in one line: "referenceTo is 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:

  • Authored declarations — zero, both spellings. All 8 Field.masterDetail(...) and 132 Field.lookup(...) declarations across *.object.ts (112 files), examples/, packages/qa/ and the create-objectstack templates go through the @objectstack/spec builders, which emit the canonical key. Not one alias is hand-written past them.
  • Stored-metadata seeds, JSON/YAML fixtures, metadata-fs layouts — zero, both spellings. Every raw hit is prose.
  • In-tree referenceTo on a field def — reader pins only, nine files, each pinning a refusal or a tolerance.
  • Metadata at rest in a live deployment — NOT MEASURED. No command in this repository reaches it. The zeros are zeros for the tree, not the world.

The assertion was wrong, and correcting it is the substance of this PR. ADR-0087's fieldReferenceToAlias records in its own docblock that camelCase referenceTo is deliberately not converted because it "is not the spelling the objectql runtime wrote into stored object rows" — the stored dialect is reference_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.

  • A raw registerObject skips Zod by design, and every caller of this resolver reads that same SchemaRegistry — now pinned by a test that registers an alias-spelled object and resolves it.
  • The conversion layer normalises reference_to on stored rehydration and on os migrate meta, and deliberately leaves referenceTo alone. So referenceTo is 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.
  • Two of this resolver's four callers fail closed. An unresolved relation leaves parent unbound and rule-validator.ts reads an unbound scope root as LOCKED, verbatim. Narrowing would take a raw-registered, alias-spelled detail object from "lock enforced against its header" to "every parent-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.ts only. No file under packages/plugins/plugin-security and no file under packages/objectql/src/engine.ts is touched.

  • The module docblock carries the measurement — which corpus, what count, which path — in place of the assertion.
  • referenceKeyOf answers which spelling resolved, and referenceOf derives 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 invariant plugin-security's refKey records for the sibling reader.
  • No behaviour change. The key is selected with the same != null test ?? applies, so a present-but-empty reference still wins the read rather than falling through to the alias — pinned.
  • Loud where the alias answered. When the returned relation resolved from referenceTo, the resolver reports once per object+field+spelling through an optional warn sink defaulting to console.warn. Never a throw. That is the same caller-supplied-callback shape and default as warnFunctionalCompleteness in the same package — a plain function in a bag, not a method lifted off a receiver-sensitive logger, which is why check:logger-receiver-detach is 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.
  • The report also corrects the registration-time field/relationship-without-reference diagnostic, 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

Verification — head a76a222fa

Run after the last commit, on the merged tree. origin/main had moved 10 commits and touched registry.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 run260 files / 4494 tests pass.
  • pnpm --filter @objectstack/objectql typecheck — pass, including check:test-typecheck. objectql's tsconfig.json excludes **/*.test.ts, so the program holding the new test is packages/objectql/tsconfig.test.json; tsc --listFiles -p tsconfig.test.json shows 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.
  • Gate union: 36 families, re-derived on this tree (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-completeness exit 3 ("running the family locally, record this gate as NOT MEASURED" — it wants a saved turbo run test log), and check-half-states exit 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-level and check:logger-receiver-detach, are both green.
  • Workspace build 70/70, so the two build-dependent gates (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-object moved from 05d5f3b4 to 98a93f36. Restore was git checkout HEAD -- "$REPO_ROOT/..." from a trap ... EXIT INT TERM with an absolute path, and is proved by bytes: git diff HEAD empty, blob back to 05d5f3b4 matching 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 10 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ui/forms.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 04ee9f884ce85f2948c076155d70663f3e675951packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1ed885cf3f03db1cd414ec689be856541038f92a — the merge of head a76a222fa4da5f397ef4063522e084988d30cdc5 into base 04ee9f884ce85f2948c076155d70663f3e675951, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 04ee9f884ce85f2948c076155d70663f3e675951 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session_0112hMx9hjJ9BgB28X97DS68) — flipped to ready at 13:11Z and auto-merge (squash) armed at 13:12Z on head a76a222fa.


Generated by Claude Code

Merged via the queue into main with commit aaa4e65Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13543-master-detail-reference-alias branch September 2, 2026 13:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] a fourth tolerant alias reader — master-detail.ts accepts referenceTo, with its stored-row population asserted but never measured

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered - #14534

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias
Sep 2, 2026
Merged

fix(objectql): the fourth tolerant alias reader — master-detail's referenceTo tolerance recorded with its measurement, and loud where the alias answered#14534
os-musk merged 3 commits into
mainfrom
claude/issue-13543-master-detail-reference-alias

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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

resolveMasterDetailRelation accepts the rejected alias referenceTo beside the canonical reference, and the type beside it stated a population for that tolerance in one line: "referenceTo is 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:

  • Authored declarations — zero, both spellings. All 8 Field.masterDetail(...) and 132 Field.lookup(...) declarations across *.object.ts (112 files), examples/, packages/qa/ and the create-objectstack templates go through the @objectstack/spec builders, which emit the canonical key. Not one alias is hand-written past them.
  • Stored-metadata seeds, JSON/YAML fixtures, metadata-fs layouts — zero, both spellings. Every raw hit is prose.
  • In-tree referenceTo on a field def — reader pins only, nine files, each pinning a refusal or a tolerance.
  • Metadata at rest in a live deployment — NOT MEASURED. No command in this repository reaches it. The zeros are zeros for the tree, not the world.

The assertion was wrong, and correcting it is the substance of this PR. ADR-0087's fieldReferenceToAlias records in its own docblock that camelCase referenceTo is deliberately not converted because it "is not the spelling the objectql runtime wrote into stored object rows" — the stored dialect is reference_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.

  • A raw registerObject skips Zod by design, and every caller of this resolver reads that same SchemaRegistry — now pinned by a test that registers an alias-spelled object and resolves it.
  • The conversion layer normalises reference_to on stored rehydration and on os migrate meta, and deliberately leaves referenceTo alone. So referenceTo is 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.
  • Two of this resolver's four callers fail closed. An unresolved relation leaves parent unbound and rule-validator.ts reads an unbound scope root as LOCKED, verbatim. Narrowing would take a raw-registered, alias-spelled detail object from "lock enforced against its header" to "every parent-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.ts only. No file under packages/plugins/plugin-security and no file under packages/objectql/src/engine.ts is touched.

  • The module docblock carries the measurement — which corpus, what count, which path — in place of the assertion.
  • referenceKeyOf answers which spelling resolved, and referenceOf derives 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 invariant plugin-security's refKey records for the sibling reader.
  • No behaviour change. The key is selected with the same != null test ?? applies, so a present-but-empty reference still wins the read rather than falling through to the alias — pinned.
  • Loud where the alias answered. When the returned relation resolved from referenceTo, the resolver reports once per object+field+spelling through an optional warn sink defaulting to console.warn. Never a throw. That is the same caller-supplied-callback shape and default as warnFunctionalCompleteness in the same package — a plain function in a bag, not a method lifted off a receiver-sensitive logger, which is why check:logger-receiver-detach is 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.
  • The report also corrects the registration-time field/relationship-without-reference diagnostic, 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

Verification — head a76a222fa

Run after the last commit, on the merged tree. origin/main had moved 10 commits and touched registry.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 run260 files / 4494 tests pass.
  • pnpm --filter @objectstack/objectql typecheck — pass, including check:test-typecheck. objectql's tsconfig.json excludes **/*.test.ts, so the program holding the new test is packages/objectql/tsconfig.test.json; tsc --listFiles -p tsconfig.test.json shows 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.
  • Gate union: 36 families, re-derived on this tree (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-completeness exit 3 ("running the family locally, record this gate as NOT MEASURED" — it wants a saved turbo run test log), and check-half-states exit 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-level and check:logger-receiver-detach, are both green.
  • Workspace build 70/70, so the two build-dependent gates (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-object moved from 05d5f3b4 to 98a93f36. Restore was git checkout HEAD -- "$REPO_ROOT/..." from a trap ... EXIT INT TERM with an absolute path, and is proved by bytes: git diff HEAD empty, blob back to 05d5f3b4 matching 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 10 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ui/forms.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via referenceTo (literal, a string literal in REFERENCE_SPELLINGS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 04ee9f884ce85f2948c076155d70663f3e675951packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1ed885cf3f03db1cd414ec689be856541038f92a — the merge of head a76a222fa4da5f397ef4063522e084988d30cdc5 into base 04ee9f884ce85f2948c076155d70663f3e675951, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 04ee9f884ce85f2948c076155d70663f3e675951 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session_0112hMx9hjJ9BgB28X97DS68) — flipped to ready at 13:11Z and auto-merge (squash) armed at 13:12Z on head a76a222fa.


Generated by Claude Code

Merged via the queue into main with commit aaa4e65Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13543-master-detail-reference-alias branch September 2, 2026 13:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] a fourth tolerant alias reader — master-detail.ts accepts referenceTo, with its stored-row population asserted but never measured

2 participants

@os-musk@claude