Skip to content

fix(spec): require a non-empty reference on lookup/master_detail fields - #13927

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required
Aug 31, 2026
Merged

fix(spec): require a non-empty reference on lookup/master_detail fields#13927
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13632

Contract review: this PR carries needs:contract-review (Clause-② — it changes contract accept/reject behaviour in packages/spec), stated here as the second carrier alongside the label.

What

FieldSchema now refuses type: 'lookup' / type: 'master_detail' whose reference is missing or the empty string, via a new check in the existing superRefine chain of packages/spec/src/data/field.zod.ts — the ruling on the card (ADR-0049 enforce-or-remove, option 1). The refusal is prescriptive on the reference path: it names the type, the key, the expected shape (a snake_case target object name) and the fix, and carries no issue number (doc-authoring discipline). The key's TSDoc — which has always called reference required on these two types — now also states the enforcement. No key is removed, renamed or re-shaped: this is an accept-set narrowing only. Empty string and absence are refused alike (the card measured both as accepted before).

Premise measurement (ruling ②, taken BEFORE the tightening)

Census over the whole tree at 8c6a7fc, three channels:

  • Structured metadata (all tracked yaml/json parsed and walked): 0 field objects with type lookup/master_detail at all, hence 0 targetless.
  • Literal object fixtures in ts/js/md/mdx (balanced-brace block extraction around every type: 'lookup' / type: 'master_detail' occurrence): 515 with a non-empty reference (positive), 179 without — every one triaged below; 0 are authored objects that FieldSchema parses.
  • Helper calls Field.lookup(...) / Field.masterDetail(...) (both take the target as a required first positional argument): 209 with a non-empty string-literal target; 22 flagged for review, all resolved as comments, docstrings, markdown API tables, or old-signature blog snippets — 0 real targetless calls.

Positive controls proving the pipeline reads real hits: the showcase field-zoo's f_lookup (target showcase_account) and f_master_detail (target showcase_project) surface through the same scan; the card's own reference_to control still refuses with unrecognized_keys after the change.

Verdict: zero existing authored objects depend on a targetless lookup/master_detail. The premise holds; the narrowing was executed (no fork). Corroborating: lint's relationship/missing-reference (error severity, non-empty check over exactly lookup + master_detail) has long graded the same hole an error, and the troubleshooting docs page already documents missing reference as a validation failure — the publish seam was the one door left open.

The 179 literal negatives, triaged (three-way, per the wizard-tightening precedent)

  • Different surface, untouched (the bulk): analytics dataset dimension descriptors, dashboard filter descriptors, UI view column descriptors, object-designer edge styles, inline-grid columns, action/bulk-action param shapes — each parses under its own schema, not FieldSchema. Also untouched: REST route runtime-defense fixtures (public-form-lookup-picker pins what the route does with stored rows that deliberately never passed FieldSchema, including its own LOOKUP_TARGET_MISSING guard), driver fixtures fed straight to the driver (below), lint fixtures fed to pure stack-to-findings validators, and docs that deliberately show the wrong shape as wrong (the field-types skill guide and the troubleshooting page).
  • Moved to legal shape (3 sites — the only parse-gated targetless fixtures in the tree, found red by the spec suite): the key-absence sample loops that push a bare type set through FieldSchema.parse — the maxLength/minLength absence pins in field.test.ts and the materialization pin in field-autonumber-default-format.test.ts. Their subject is key-absence propagation, not the lookup accept surface; the lookup sample now carries reference: 'company', other types unchanged.
  • Refusal pins, unaffected: reference_to / referenceTo / target alias fixtures across drivers, lint and spec already refuse via unrecognized_keys and still do.

Blast radius noted, deliberately untouched

driver-mongodb (#13222): syncCollectionSchema declining to build the lookup index for a targetless def stays in place as runtime defense for stored/legacy artifacts, and its tests still pass — but the comment inside its "does not index a lookup that declares no target" case, which records that both targetless shapes "parse successfully" on FieldSchema, is now stale as a statement about the current schema. Recorded here for triage rather than edited: driver code and driver tests are out of this card's scope per the dispatch.

Verification

  • Reverse verification of the card's measurement table on the rebuilt dist: the three former accepts now refuse on the reference path; the two legal shapes and the reference_to positive control are unchanged.
  • New pins (8): missing refused and empty refused for both types, legal accepted for both types (the positive-control leg), non-relationship types untouched, helper builders pass. Name-filtered run: 8 passed.
  • Ablation, from the committed fix: mutation proven on disk (anchor grep 1 to 0), name-filtered pins then 4 failed / 4 passed — exactly the four refusal pins red, accepts green — and restoration proven by an empty git diff HEAD plus git hash-object equal to the HEAD blob (d4e3c6b6). The spec suite imports field.zod.ts by relative source path, so no dist rebuild sits in this ablation's resolution path.
  • Union at final head e740733 (git rev-parse --short HEAD from the same tree the run measured): full @objectstack/spec suite — verdict line "Test Files 445 passed (445) / Tests 11938 passed (11938)" — chained with pnpm --filter @objectstack/spec typecheck (tsc + scripts + tests programs), lock entry-point verdict "VERDICT command-exit 0" covering both.
  • Derived gates (dispatch-gates from this tree at fe5b03c; the script itself flags this tree as 3 commits behind origin/main with 1 derives-from file changed across the range — CI on the merge ref is the authority): 43 families derived; measured green locally, including check:authorable-surface, check:strictness-ledger, check:docs, check:doc-authoring, check:dispatcher-error-vocabulary, check:engine-double-contract, check:where-matcher and the changeset family, after three repairs — the adr-0087 marker below, a system-context census re-anchor (--fix; the superRefine insertion shifted field.zod.ts line numbers by two), and building @objectstack/formula + @objectstack/lint so check:doc-formula-expressions could measure (then green).
  • NOT MEASURED locally, preconditions named (CI-owned): check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-coverage, check:type-check-debt (all need the full workspace closure built), check-test-completeness (needs a saved turbo test log), check:pm-half-states (route refused from this container; its exit 3 self-describes as an unread instrument). Repo-wide pnpm lint is CI's run.

Changeset / ADR-0087

@objectstack/specminor with the BREAKING narrative, per the #11519 / #11842 / #13733 narrowing precedents. ADR-0087 disposition: not-required (no-migration-prescription) — no tombstone exists, the parse refusal is the channel that reaches an affected author, and which target a targetless relationship was meant to point at is authoring intent no migration entry can decide; marker in the changeset body.

Generated by Claude Code


Generated by Claude Code

…r_detail
superRefine on the relationship types (ADR-0049 declared = enforced): a
lookup / master_detail whose reference is missing or '' is refused at
parse with a prescriptive message on the reference path. TSDoc aligned;
pins for missing/empty/legal on both types; the three key-absence sample
fixtures that used a bare lookup move to legal shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
…sus line
The changeset gains the not-required (no-migration-prescription) marker
mirroring the #11519/#11842/#13733 narrowing precedents; the superRefine
insertion shifted field.zod.ts line numbers, so the system-context page
anchor is re-anchored by the census --fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 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 — 128 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 55519d50363147da03df0af068fb82fcc6d1d8c1packageMentionDocs.

Which tree this was computed on

This run read content/docs from e80b881f55da1c500d8d261d222ec98dc6c9156c — the merge of head e7407339c968d096bf4e6f945117036dae250fbb into base 55519d50363147da03df0af068fb82fcc6d1d8c1, 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 e80b881f55da1c500d8d261d222ec98dc6c9156c && git checkout e80b881f55da1c500d8d261d222ec98dc6c9156c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55519d50363147da03df0af068fb82fcc6d1d8c1 e7407339c968d096bf4e6f945117036dae250fbb && git checkout -B drift-repro 55519d50363147da03df0af068fb82fcc6d1d8c1 && git merge --no-ff e7407339c968d096bf4e6f945117036dae250fbb
node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1

⚠️ 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 55519d50363147da03df0af068fb82fcc6d1d8c1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 31, 2026
@os-warrenos-warren added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling protocol:data labels Aug 31, 2026 — with Claude
@os-warren
os-warren marked this pull request as ready for review August 31, 2026 17:12
@os-warren
os-warren enabled auto-merge August 31, 2026 17:12
@os-warren
os-warren added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 0fb8760Aug 31, 2026
56 checks passed
@os-warren
os-warren deleted the claude/issue-13632-lookup-reference-required branch August 31, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[spec] FieldSchema accepts a lookup/master_detail with no reference target, though its own TSDoc calls the key required

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(spec): require a non-empty reference on lookup/master_detail fields by os-warren · Pull Request #13927 · objectstack-ai/objectstack · GitHub
Skip to content

fix(spec): require a non-empty reference on lookup/master_detail fields - #13927

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required
Aug 31, 2026
Merged

fix(spec): require a non-empty reference on lookup/master_detail fields#13927
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13632

Contract review: this PR carries needs:contract-review (Clause-② — it changes contract accept/reject behaviour in packages/spec), stated here as the second carrier alongside the label.

What

FieldSchema now refuses type: 'lookup' / type: 'master_detail' whose reference is missing or the empty string, via a new check in the existing superRefine chain of packages/spec/src/data/field.zod.ts — the ruling on the card (ADR-0049 enforce-or-remove, option 1). The refusal is prescriptive on the reference path: it names the type, the key, the expected shape (a snake_case target object name) and the fix, and carries no issue number (doc-authoring discipline). The key's TSDoc — which has always called reference required on these two types — now also states the enforcement. No key is removed, renamed or re-shaped: this is an accept-set narrowing only. Empty string and absence are refused alike (the card measured both as accepted before).

Premise measurement (ruling ②, taken BEFORE the tightening)

Census over the whole tree at 8c6a7fc, three channels:

  • Structured metadata (all tracked yaml/json parsed and walked): 0 field objects with type lookup/master_detail at all, hence 0 targetless.
  • Literal object fixtures in ts/js/md/mdx (balanced-brace block extraction around every type: 'lookup' / type: 'master_detail' occurrence): 515 with a non-empty reference (positive), 179 without — every one triaged below; 0 are authored objects that FieldSchema parses.
  • Helper calls Field.lookup(...) / Field.masterDetail(...) (both take the target as a required first positional argument): 209 with a non-empty string-literal target; 22 flagged for review, all resolved as comments, docstrings, markdown API tables, or old-signature blog snippets — 0 real targetless calls.

Positive controls proving the pipeline reads real hits: the showcase field-zoo's f_lookup (target showcase_account) and f_master_detail (target showcase_project) surface through the same scan; the card's own reference_to control still refuses with unrecognized_keys after the change.

Verdict: zero existing authored objects depend on a targetless lookup/master_detail. The premise holds; the narrowing was executed (no fork). Corroborating: lint's relationship/missing-reference (error severity, non-empty check over exactly lookup + master_detail) has long graded the same hole an error, and the troubleshooting docs page already documents missing reference as a validation failure — the publish seam was the one door left open.

The 179 literal negatives, triaged (three-way, per the wizard-tightening precedent)

  • Different surface, untouched (the bulk): analytics dataset dimension descriptors, dashboard filter descriptors, UI view column descriptors, object-designer edge styles, inline-grid columns, action/bulk-action param shapes — each parses under its own schema, not FieldSchema. Also untouched: REST route runtime-defense fixtures (public-form-lookup-picker pins what the route does with stored rows that deliberately never passed FieldSchema, including its own LOOKUP_TARGET_MISSING guard), driver fixtures fed straight to the driver (below), lint fixtures fed to pure stack-to-findings validators, and docs that deliberately show the wrong shape as wrong (the field-types skill guide and the troubleshooting page).
  • Moved to legal shape (3 sites — the only parse-gated targetless fixtures in the tree, found red by the spec suite): the key-absence sample loops that push a bare type set through FieldSchema.parse — the maxLength/minLength absence pins in field.test.ts and the materialization pin in field-autonumber-default-format.test.ts. Their subject is key-absence propagation, not the lookup accept surface; the lookup sample now carries reference: 'company', other types unchanged.
  • Refusal pins, unaffected: reference_to / referenceTo / target alias fixtures across drivers, lint and spec already refuse via unrecognized_keys and still do.

Blast radius noted, deliberately untouched

driver-mongodb (#13222): syncCollectionSchema declining to build the lookup index for a targetless def stays in place as runtime defense for stored/legacy artifacts, and its tests still pass — but the comment inside its "does not index a lookup that declares no target" case, which records that both targetless shapes "parse successfully" on FieldSchema, is now stale as a statement about the current schema. Recorded here for triage rather than edited: driver code and driver tests are out of this card's scope per the dispatch.

Verification

  • Reverse verification of the card's measurement table on the rebuilt dist: the three former accepts now refuse on the reference path; the two legal shapes and the reference_to positive control are unchanged.
  • New pins (8): missing refused and empty refused for both types, legal accepted for both types (the positive-control leg), non-relationship types untouched, helper builders pass. Name-filtered run: 8 passed.
  • Ablation, from the committed fix: mutation proven on disk (anchor grep 1 to 0), name-filtered pins then 4 failed / 4 passed — exactly the four refusal pins red, accepts green — and restoration proven by an empty git diff HEAD plus git hash-object equal to the HEAD blob (d4e3c6b6). The spec suite imports field.zod.ts by relative source path, so no dist rebuild sits in this ablation's resolution path.
  • Union at final head e740733 (git rev-parse --short HEAD from the same tree the run measured): full @objectstack/spec suite — verdict line "Test Files 445 passed (445) / Tests 11938 passed (11938)" — chained with pnpm --filter @objectstack/spec typecheck (tsc + scripts + tests programs), lock entry-point verdict "VERDICT command-exit 0" covering both.
  • Derived gates (dispatch-gates from this tree at fe5b03c; the script itself flags this tree as 3 commits behind origin/main with 1 derives-from file changed across the range — CI on the merge ref is the authority): 43 families derived; measured green locally, including check:authorable-surface, check:strictness-ledger, check:docs, check:doc-authoring, check:dispatcher-error-vocabulary, check:engine-double-contract, check:where-matcher and the changeset family, after three repairs — the adr-0087 marker below, a system-context census re-anchor (--fix; the superRefine insertion shifted field.zod.ts line numbers by two), and building @objectstack/formula + @objectstack/lint so check:doc-formula-expressions could measure (then green).
  • NOT MEASURED locally, preconditions named (CI-owned): check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-coverage, check:type-check-debt (all need the full workspace closure built), check-test-completeness (needs a saved turbo test log), check:pm-half-states (route refused from this container; its exit 3 self-describes as an unread instrument). Repo-wide pnpm lint is CI's run.

Changeset / ADR-0087

@objectstack/specminor with the BREAKING narrative, per the #11519 / #11842 / #13733 narrowing precedents. ADR-0087 disposition: not-required (no-migration-prescription) — no tombstone exists, the parse refusal is the channel that reaches an affected author, and which target a targetless relationship was meant to point at is authoring intent no migration entry can decide; marker in the changeset body.

Generated by Claude Code


Generated by Claude Code

…r_detail
superRefine on the relationship types (ADR-0049 declared = enforced): a
lookup / master_detail whose reference is missing or '' is refused at
parse with a prescriptive message on the reference path. TSDoc aligned;
pins for missing/empty/legal on both types; the three key-absence sample
fixtures that used a bare lookup move to legal shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
…sus line
The changeset gains the not-required (no-migration-prescription) marker
mirroring the #11519/#11842/#13733 narrowing precedents; the superRefine
insertion shifted field.zod.ts line numbers, so the system-context page
anchor is re-anchored by the census --fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 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 — 128 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 55519d50363147da03df0af068fb82fcc6d1d8c1packageMentionDocs.

Which tree this was computed on

This run read content/docs from e80b881f55da1c500d8d261d222ec98dc6c9156c — the merge of head e7407339c968d096bf4e6f945117036dae250fbb into base 55519d50363147da03df0af068fb82fcc6d1d8c1, 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 e80b881f55da1c500d8d261d222ec98dc6c9156c && git checkout e80b881f55da1c500d8d261d222ec98dc6c9156c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55519d50363147da03df0af068fb82fcc6d1d8c1 e7407339c968d096bf4e6f945117036dae250fbb && git checkout -B drift-repro 55519d50363147da03df0af068fb82fcc6d1d8c1 && git merge --no-ff e7407339c968d096bf4e6f945117036dae250fbb
node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1

⚠️ 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 55519d50363147da03df0af068fb82fcc6d1d8c1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 31, 2026
@os-warrenos-warren added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling protocol:data labels Aug 31, 2026 — with Claude
@os-warren
os-warren marked this pull request as ready for review August 31, 2026 17:12
@os-warren
os-warren enabled auto-merge August 31, 2026 17:12
@os-warren
os-warren added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 0fb8760Aug 31, 2026
56 checks passed
@os-warren
os-warren deleted the claude/issue-13632-lookup-reference-required branch August 31, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[spec] FieldSchema accepts a lookup/master_detail with no reference target, though its own TSDoc calls the key required

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(spec): require a non-empty reference on lookup/master_detail fields by os-warren · Pull Request #13927 · objectstack-ai/objectstack · GitHub
Skip to content

fix(spec): require a non-empty reference on lookup/master_detail fields - #13927

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required
Aug 31, 2026
Merged

fix(spec): require a non-empty reference on lookup/master_detail fields#13927
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13632

Contract review: this PR carries needs:contract-review (Clause-② — it changes contract accept/reject behaviour in packages/spec), stated here as the second carrier alongside the label.

What

FieldSchema now refuses type: 'lookup' / type: 'master_detail' whose reference is missing or the empty string, via a new check in the existing superRefine chain of packages/spec/src/data/field.zod.ts — the ruling on the card (ADR-0049 enforce-or-remove, option 1). The refusal is prescriptive on the reference path: it names the type, the key, the expected shape (a snake_case target object name) and the fix, and carries no issue number (doc-authoring discipline). The key's TSDoc — which has always called reference required on these two types — now also states the enforcement. No key is removed, renamed or re-shaped: this is an accept-set narrowing only. Empty string and absence are refused alike (the card measured both as accepted before).

Premise measurement (ruling ②, taken BEFORE the tightening)

Census over the whole tree at 8c6a7fc, three channels:

  • Structured metadata (all tracked yaml/json parsed and walked): 0 field objects with type lookup/master_detail at all, hence 0 targetless.
  • Literal object fixtures in ts/js/md/mdx (balanced-brace block extraction around every type: 'lookup' / type: 'master_detail' occurrence): 515 with a non-empty reference (positive), 179 without — every one triaged below; 0 are authored objects that FieldSchema parses.
  • Helper calls Field.lookup(...) / Field.masterDetail(...) (both take the target as a required first positional argument): 209 with a non-empty string-literal target; 22 flagged for review, all resolved as comments, docstrings, markdown API tables, or old-signature blog snippets — 0 real targetless calls.

Positive controls proving the pipeline reads real hits: the showcase field-zoo's f_lookup (target showcase_account) and f_master_detail (target showcase_project) surface through the same scan; the card's own reference_to control still refuses with unrecognized_keys after the change.

Verdict: zero existing authored objects depend on a targetless lookup/master_detail. The premise holds; the narrowing was executed (no fork). Corroborating: lint's relationship/missing-reference (error severity, non-empty check over exactly lookup + master_detail) has long graded the same hole an error, and the troubleshooting docs page already documents missing reference as a validation failure — the publish seam was the one door left open.

The 179 literal negatives, triaged (three-way, per the wizard-tightening precedent)

  • Different surface, untouched (the bulk): analytics dataset dimension descriptors, dashboard filter descriptors, UI view column descriptors, object-designer edge styles, inline-grid columns, action/bulk-action param shapes — each parses under its own schema, not FieldSchema. Also untouched: REST route runtime-defense fixtures (public-form-lookup-picker pins what the route does with stored rows that deliberately never passed FieldSchema, including its own LOOKUP_TARGET_MISSING guard), driver fixtures fed straight to the driver (below), lint fixtures fed to pure stack-to-findings validators, and docs that deliberately show the wrong shape as wrong (the field-types skill guide and the troubleshooting page).
  • Moved to legal shape (3 sites — the only parse-gated targetless fixtures in the tree, found red by the spec suite): the key-absence sample loops that push a bare type set through FieldSchema.parse — the maxLength/minLength absence pins in field.test.ts and the materialization pin in field-autonumber-default-format.test.ts. Their subject is key-absence propagation, not the lookup accept surface; the lookup sample now carries reference: 'company', other types unchanged.
  • Refusal pins, unaffected: reference_to / referenceTo / target alias fixtures across drivers, lint and spec already refuse via unrecognized_keys and still do.

Blast radius noted, deliberately untouched

driver-mongodb (#13222): syncCollectionSchema declining to build the lookup index for a targetless def stays in place as runtime defense for stored/legacy artifacts, and its tests still pass — but the comment inside its "does not index a lookup that declares no target" case, which records that both targetless shapes "parse successfully" on FieldSchema, is now stale as a statement about the current schema. Recorded here for triage rather than edited: driver code and driver tests are out of this card's scope per the dispatch.

Verification

  • Reverse verification of the card's measurement table on the rebuilt dist: the three former accepts now refuse on the reference path; the two legal shapes and the reference_to positive control are unchanged.
  • New pins (8): missing refused and empty refused for both types, legal accepted for both types (the positive-control leg), non-relationship types untouched, helper builders pass. Name-filtered run: 8 passed.
  • Ablation, from the committed fix: mutation proven on disk (anchor grep 1 to 0), name-filtered pins then 4 failed / 4 passed — exactly the four refusal pins red, accepts green — and restoration proven by an empty git diff HEAD plus git hash-object equal to the HEAD blob (d4e3c6b6). The spec suite imports field.zod.ts by relative source path, so no dist rebuild sits in this ablation's resolution path.
  • Union at final head e740733 (git rev-parse --short HEAD from the same tree the run measured): full @objectstack/spec suite — verdict line "Test Files 445 passed (445) / Tests 11938 passed (11938)" — chained with pnpm --filter @objectstack/spec typecheck (tsc + scripts + tests programs), lock entry-point verdict "VERDICT command-exit 0" covering both.
  • Derived gates (dispatch-gates from this tree at fe5b03c; the script itself flags this tree as 3 commits behind origin/main with 1 derives-from file changed across the range — CI on the merge ref is the authority): 43 families derived; measured green locally, including check:authorable-surface, check:strictness-ledger, check:docs, check:doc-authoring, check:dispatcher-error-vocabulary, check:engine-double-contract, check:where-matcher and the changeset family, after three repairs — the adr-0087 marker below, a system-context census re-anchor (--fix; the superRefine insertion shifted field.zod.ts line numbers by two), and building @objectstack/formula + @objectstack/lint so check:doc-formula-expressions could measure (then green).
  • NOT MEASURED locally, preconditions named (CI-owned): check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-coverage, check:type-check-debt (all need the full workspace closure built), check-test-completeness (needs a saved turbo test log), check:pm-half-states (route refused from this container; its exit 3 self-describes as an unread instrument). Repo-wide pnpm lint is CI's run.

Changeset / ADR-0087

@objectstack/specminor with the BREAKING narrative, per the #11519 / #11842 / #13733 narrowing precedents. ADR-0087 disposition: not-required (no-migration-prescription) — no tombstone exists, the parse refusal is the channel that reaches an affected author, and which target a targetless relationship was meant to point at is authoring intent no migration entry can decide; marker in the changeset body.

Generated by Claude Code


Generated by Claude Code

…r_detail
superRefine on the relationship types (ADR-0049 declared = enforced): a
lookup / master_detail whose reference is missing or '' is refused at
parse with a prescriptive message on the reference path. TSDoc aligned;
pins for missing/empty/legal on both types; the three key-absence sample
fixtures that used a bare lookup move to legal shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
…sus line
The changeset gains the not-required (no-migration-prescription) marker
mirroring the #11519/#11842/#13733 narrowing precedents; the superRefine
insertion shifted field.zod.ts line numbers, so the system-context page
anchor is re-anchored by the census --fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 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 — 128 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 55519d50363147da03df0af068fb82fcc6d1d8c1packageMentionDocs.

Which tree this was computed on

This run read content/docs from e80b881f55da1c500d8d261d222ec98dc6c9156c — the merge of head e7407339c968d096bf4e6f945117036dae250fbb into base 55519d50363147da03df0af068fb82fcc6d1d8c1, 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 e80b881f55da1c500d8d261d222ec98dc6c9156c && git checkout e80b881f55da1c500d8d261d222ec98dc6c9156c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55519d50363147da03df0af068fb82fcc6d1d8c1 e7407339c968d096bf4e6f945117036dae250fbb && git checkout -B drift-repro 55519d50363147da03df0af068fb82fcc6d1d8c1 && git merge --no-ff e7407339c968d096bf4e6f945117036dae250fbb
node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1

⚠️ 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 55519d50363147da03df0af068fb82fcc6d1d8c1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 31, 2026
@os-warrenos-warren added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling protocol:data labels Aug 31, 2026 — with Claude
@os-warren
os-warren marked this pull request as ready for review August 31, 2026 17:12
@os-warren
os-warren enabled auto-merge August 31, 2026 17:12
@os-warren
os-warren added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 0fb8760Aug 31, 2026
56 checks passed
@os-warren
os-warren deleted the claude/issue-13632-lookup-reference-required branch August 31, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[spec] FieldSchema accepts a lookup/master_detail with no reference target, though its own TSDoc calls the key required

2 participants

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

fix(spec): require a non-empty reference on lookup/master_detail fields - #13927

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required
Aug 31, 2026
Merged

fix(spec): require a non-empty reference on lookup/master_detail fields#13927
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13632

Contract review: this PR carries needs:contract-review (Clause-② — it changes contract accept/reject behaviour in packages/spec), stated here as the second carrier alongside the label.

What

FieldSchema now refuses type: 'lookup' / type: 'master_detail' whose reference is missing or the empty string, via a new check in the existing superRefine chain of packages/spec/src/data/field.zod.ts — the ruling on the card (ADR-0049 enforce-or-remove, option 1). The refusal is prescriptive on the reference path: it names the type, the key, the expected shape (a snake_case target object name) and the fix, and carries no issue number (doc-authoring discipline). The key's TSDoc — which has always called reference required on these two types — now also states the enforcement. No key is removed, renamed or re-shaped: this is an accept-set narrowing only. Empty string and absence are refused alike (the card measured both as accepted before).

Premise measurement (ruling ②, taken BEFORE the tightening)

Census over the whole tree at 8c6a7fc, three channels:

  • Structured metadata (all tracked yaml/json parsed and walked): 0 field objects with type lookup/master_detail at all, hence 0 targetless.
  • Literal object fixtures in ts/js/md/mdx (balanced-brace block extraction around every type: 'lookup' / type: 'master_detail' occurrence): 515 with a non-empty reference (positive), 179 without — every one triaged below; 0 are authored objects that FieldSchema parses.
  • Helper calls Field.lookup(...) / Field.masterDetail(...) (both take the target as a required first positional argument): 209 with a non-empty string-literal target; 22 flagged for review, all resolved as comments, docstrings, markdown API tables, or old-signature blog snippets — 0 real targetless calls.

Positive controls proving the pipeline reads real hits: the showcase field-zoo's f_lookup (target showcase_account) and f_master_detail (target showcase_project) surface through the same scan; the card's own reference_to control still refuses with unrecognized_keys after the change.

Verdict: zero existing authored objects depend on a targetless lookup/master_detail. The premise holds; the narrowing was executed (no fork). Corroborating: lint's relationship/missing-reference (error severity, non-empty check over exactly lookup + master_detail) has long graded the same hole an error, and the troubleshooting docs page already documents missing reference as a validation failure — the publish seam was the one door left open.

The 179 literal negatives, triaged (three-way, per the wizard-tightening precedent)

  • Different surface, untouched (the bulk): analytics dataset dimension descriptors, dashboard filter descriptors, UI view column descriptors, object-designer edge styles, inline-grid columns, action/bulk-action param shapes — each parses under its own schema, not FieldSchema. Also untouched: REST route runtime-defense fixtures (public-form-lookup-picker pins what the route does with stored rows that deliberately never passed FieldSchema, including its own LOOKUP_TARGET_MISSING guard), driver fixtures fed straight to the driver (below), lint fixtures fed to pure stack-to-findings validators, and docs that deliberately show the wrong shape as wrong (the field-types skill guide and the troubleshooting page).
  • Moved to legal shape (3 sites — the only parse-gated targetless fixtures in the tree, found red by the spec suite): the key-absence sample loops that push a bare type set through FieldSchema.parse — the maxLength/minLength absence pins in field.test.ts and the materialization pin in field-autonumber-default-format.test.ts. Their subject is key-absence propagation, not the lookup accept surface; the lookup sample now carries reference: 'company', other types unchanged.
  • Refusal pins, unaffected: reference_to / referenceTo / target alias fixtures across drivers, lint and spec already refuse via unrecognized_keys and still do.

Blast radius noted, deliberately untouched

driver-mongodb (#13222): syncCollectionSchema declining to build the lookup index for a targetless def stays in place as runtime defense for stored/legacy artifacts, and its tests still pass — but the comment inside its "does not index a lookup that declares no target" case, which records that both targetless shapes "parse successfully" on FieldSchema, is now stale as a statement about the current schema. Recorded here for triage rather than edited: driver code and driver tests are out of this card's scope per the dispatch.

Verification

  • Reverse verification of the card's measurement table on the rebuilt dist: the three former accepts now refuse on the reference path; the two legal shapes and the reference_to positive control are unchanged.
  • New pins (8): missing refused and empty refused for both types, legal accepted for both types (the positive-control leg), non-relationship types untouched, helper builders pass. Name-filtered run: 8 passed.
  • Ablation, from the committed fix: mutation proven on disk (anchor grep 1 to 0), name-filtered pins then 4 failed / 4 passed — exactly the four refusal pins red, accepts green — and restoration proven by an empty git diff HEAD plus git hash-object equal to the HEAD blob (d4e3c6b6). The spec suite imports field.zod.ts by relative source path, so no dist rebuild sits in this ablation's resolution path.
  • Union at final head e740733 (git rev-parse --short HEAD from the same tree the run measured): full @objectstack/spec suite — verdict line "Test Files 445 passed (445) / Tests 11938 passed (11938)" — chained with pnpm --filter @objectstack/spec typecheck (tsc + scripts + tests programs), lock entry-point verdict "VERDICT command-exit 0" covering both.
  • Derived gates (dispatch-gates from this tree at fe5b03c; the script itself flags this tree as 3 commits behind origin/main with 1 derives-from file changed across the range — CI on the merge ref is the authority): 43 families derived; measured green locally, including check:authorable-surface, check:strictness-ledger, check:docs, check:doc-authoring, check:dispatcher-error-vocabulary, check:engine-double-contract, check:where-matcher and the changeset family, after three repairs — the adr-0087 marker below, a system-context census re-anchor (--fix; the superRefine insertion shifted field.zod.ts line numbers by two), and building @objectstack/formula + @objectstack/lint so check:doc-formula-expressions could measure (then green).
  • NOT MEASURED locally, preconditions named (CI-owned): check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-coverage, check:type-check-debt (all need the full workspace closure built), check-test-completeness (needs a saved turbo test log), check:pm-half-states (route refused from this container; its exit 3 self-describes as an unread instrument). Repo-wide pnpm lint is CI's run.

Changeset / ADR-0087

@objectstack/specminor with the BREAKING narrative, per the #11519 / #11842 / #13733 narrowing precedents. ADR-0087 disposition: not-required (no-migration-prescription) — no tombstone exists, the parse refusal is the channel that reaches an affected author, and which target a targetless relationship was meant to point at is authoring intent no migration entry can decide; marker in the changeset body.

Generated by Claude Code


Generated by Claude Code

…r_detail
superRefine on the relationship types (ADR-0049 declared = enforced): a
lookup / master_detail whose reference is missing or '' is refused at
parse with a prescriptive message on the reference path. TSDoc aligned;
pins for missing/empty/legal on both types; the three key-absence sample
fixtures that used a bare lookup move to legal shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
…sus line
The changeset gains the not-required (no-migration-prescription) marker
mirroring the #11519/#11842/#13733 narrowing precedents; the superRefine
insertion shifted field.zod.ts line numbers, so the system-context page
anchor is re-anchored by the census --fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 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 — 128 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 55519d50363147da03df0af068fb82fcc6d1d8c1packageMentionDocs.

Which tree this was computed on

This run read content/docs from e80b881f55da1c500d8d261d222ec98dc6c9156c — the merge of head e7407339c968d096bf4e6f945117036dae250fbb into base 55519d50363147da03df0af068fb82fcc6d1d8c1, 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 e80b881f55da1c500d8d261d222ec98dc6c9156c && git checkout e80b881f55da1c500d8d261d222ec98dc6c9156c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55519d50363147da03df0af068fb82fcc6d1d8c1 e7407339c968d096bf4e6f945117036dae250fbb && git checkout -B drift-repro 55519d50363147da03df0af068fb82fcc6d1d8c1 && git merge --no-ff e7407339c968d096bf4e6f945117036dae250fbb
node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1

⚠️ 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 55519d50363147da03df0af068fb82fcc6d1d8c1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 31, 2026
@os-warrenos-warren added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling protocol:data labels Aug 31, 2026 — with Claude
@os-warren
os-warren marked this pull request as ready for review August 31, 2026 17:12
@os-warren
os-warren enabled auto-merge August 31, 2026 17:12
@os-warren
os-warren added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 0fb8760Aug 31, 2026
56 checks passed
@os-warren
os-warren deleted the claude/issue-13632-lookup-reference-required branch August 31, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[spec] FieldSchema accepts a lookup/master_detail with no reference target, though its own TSDoc calls the key required

2 participants

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

fix(spec): require a non-empty reference on lookup/master_detail fields - #13927

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required
Aug 31, 2026
Merged

fix(spec): require a non-empty reference on lookup/master_detail fields#13927
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13632

Contract review: this PR carries needs:contract-review (Clause-② — it changes contract accept/reject behaviour in packages/spec), stated here as the second carrier alongside the label.

What

FieldSchema now refuses type: 'lookup' / type: 'master_detail' whose reference is missing or the empty string, via a new check in the existing superRefine chain of packages/spec/src/data/field.zod.ts — the ruling on the card (ADR-0049 enforce-or-remove, option 1). The refusal is prescriptive on the reference path: it names the type, the key, the expected shape (a snake_case target object name) and the fix, and carries no issue number (doc-authoring discipline). The key's TSDoc — which has always called reference required on these two types — now also states the enforcement. No key is removed, renamed or re-shaped: this is an accept-set narrowing only. Empty string and absence are refused alike (the card measured both as accepted before).

Premise measurement (ruling ②, taken BEFORE the tightening)

Census over the whole tree at 8c6a7fc, three channels:

  • Structured metadata (all tracked yaml/json parsed and walked): 0 field objects with type lookup/master_detail at all, hence 0 targetless.
  • Literal object fixtures in ts/js/md/mdx (balanced-brace block extraction around every type: 'lookup' / type: 'master_detail' occurrence): 515 with a non-empty reference (positive), 179 without — every one triaged below; 0 are authored objects that FieldSchema parses.
  • Helper calls Field.lookup(...) / Field.masterDetail(...) (both take the target as a required first positional argument): 209 with a non-empty string-literal target; 22 flagged for review, all resolved as comments, docstrings, markdown API tables, or old-signature blog snippets — 0 real targetless calls.

Positive controls proving the pipeline reads real hits: the showcase field-zoo's f_lookup (target showcase_account) and f_master_detail (target showcase_project) surface through the same scan; the card's own reference_to control still refuses with unrecognized_keys after the change.

Verdict: zero existing authored objects depend on a targetless lookup/master_detail. The premise holds; the narrowing was executed (no fork). Corroborating: lint's relationship/missing-reference (error severity, non-empty check over exactly lookup + master_detail) has long graded the same hole an error, and the troubleshooting docs page already documents missing reference as a validation failure — the publish seam was the one door left open.

The 179 literal negatives, triaged (three-way, per the wizard-tightening precedent)

  • Different surface, untouched (the bulk): analytics dataset dimension descriptors, dashboard filter descriptors, UI view column descriptors, object-designer edge styles, inline-grid columns, action/bulk-action param shapes — each parses under its own schema, not FieldSchema. Also untouched: REST route runtime-defense fixtures (public-form-lookup-picker pins what the route does with stored rows that deliberately never passed FieldSchema, including its own LOOKUP_TARGET_MISSING guard), driver fixtures fed straight to the driver (below), lint fixtures fed to pure stack-to-findings validators, and docs that deliberately show the wrong shape as wrong (the field-types skill guide and the troubleshooting page).
  • Moved to legal shape (3 sites — the only parse-gated targetless fixtures in the tree, found red by the spec suite): the key-absence sample loops that push a bare type set through FieldSchema.parse — the maxLength/minLength absence pins in field.test.ts and the materialization pin in field-autonumber-default-format.test.ts. Their subject is key-absence propagation, not the lookup accept surface; the lookup sample now carries reference: 'company', other types unchanged.
  • Refusal pins, unaffected: reference_to / referenceTo / target alias fixtures across drivers, lint and spec already refuse via unrecognized_keys and still do.

Blast radius noted, deliberately untouched

driver-mongodb (#13222): syncCollectionSchema declining to build the lookup index for a targetless def stays in place as runtime defense for stored/legacy artifacts, and its tests still pass — but the comment inside its "does not index a lookup that declares no target" case, which records that both targetless shapes "parse successfully" on FieldSchema, is now stale as a statement about the current schema. Recorded here for triage rather than edited: driver code and driver tests are out of this card's scope per the dispatch.

Verification

  • Reverse verification of the card's measurement table on the rebuilt dist: the three former accepts now refuse on the reference path; the two legal shapes and the reference_to positive control are unchanged.
  • New pins (8): missing refused and empty refused for both types, legal accepted for both types (the positive-control leg), non-relationship types untouched, helper builders pass. Name-filtered run: 8 passed.
  • Ablation, from the committed fix: mutation proven on disk (anchor grep 1 to 0), name-filtered pins then 4 failed / 4 passed — exactly the four refusal pins red, accepts green — and restoration proven by an empty git diff HEAD plus git hash-object equal to the HEAD blob (d4e3c6b6). The spec suite imports field.zod.ts by relative source path, so no dist rebuild sits in this ablation's resolution path.
  • Union at final head e740733 (git rev-parse --short HEAD from the same tree the run measured): full @objectstack/spec suite — verdict line "Test Files 445 passed (445) / Tests 11938 passed (11938)" — chained with pnpm --filter @objectstack/spec typecheck (tsc + scripts + tests programs), lock entry-point verdict "VERDICT command-exit 0" covering both.
  • Derived gates (dispatch-gates from this tree at fe5b03c; the script itself flags this tree as 3 commits behind origin/main with 1 derives-from file changed across the range — CI on the merge ref is the authority): 43 families derived; measured green locally, including check:authorable-surface, check:strictness-ledger, check:docs, check:doc-authoring, check:dispatcher-error-vocabulary, check:engine-double-contract, check:where-matcher and the changeset family, after three repairs — the adr-0087 marker below, a system-context census re-anchor (--fix; the superRefine insertion shifted field.zod.ts line numbers by two), and building @objectstack/formula + @objectstack/lint so check:doc-formula-expressions could measure (then green).
  • NOT MEASURED locally, preconditions named (CI-owned): check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-coverage, check:type-check-debt (all need the full workspace closure built), check-test-completeness (needs a saved turbo test log), check:pm-half-states (route refused from this container; its exit 3 self-describes as an unread instrument). Repo-wide pnpm lint is CI's run.

Changeset / ADR-0087

@objectstack/specminor with the BREAKING narrative, per the #11519 / #11842 / #13733 narrowing precedents. ADR-0087 disposition: not-required (no-migration-prescription) — no tombstone exists, the parse refusal is the channel that reaches an affected author, and which target a targetless relationship was meant to point at is authoring intent no migration entry can decide; marker in the changeset body.

Generated by Claude Code


Generated by Claude Code

…r_detail
superRefine on the relationship types (ADR-0049 declared = enforced): a
lookup / master_detail whose reference is missing or '' is refused at
parse with a prescriptive message on the reference path. TSDoc aligned;
pins for missing/empty/legal on both types; the three key-absence sample
fixtures that used a bare lookup move to legal shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
…sus line
The changeset gains the not-required (no-migration-prescription) marker
mirroring the #11519/#11842/#13733 narrowing precedents; the superRefine
insertion shifted field.zod.ts line numbers, so the system-context page
anchor is re-anchored by the census --fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 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 — 128 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 55519d50363147da03df0af068fb82fcc6d1d8c1packageMentionDocs.

Which tree this was computed on

This run read content/docs from e80b881f55da1c500d8d261d222ec98dc6c9156c — the merge of head e7407339c968d096bf4e6f945117036dae250fbb into base 55519d50363147da03df0af068fb82fcc6d1d8c1, 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 e80b881f55da1c500d8d261d222ec98dc6c9156c && git checkout e80b881f55da1c500d8d261d222ec98dc6c9156c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55519d50363147da03df0af068fb82fcc6d1d8c1 e7407339c968d096bf4e6f945117036dae250fbb && git checkout -B drift-repro 55519d50363147da03df0af068fb82fcc6d1d8c1 && git merge --no-ff e7407339c968d096bf4e6f945117036dae250fbb
node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1

⚠️ 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 55519d50363147da03df0af068fb82fcc6d1d8c1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 31, 2026
@os-warrenos-warren added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling protocol:data labels Aug 31, 2026 — with Claude
@os-warren
os-warren marked this pull request as ready for review August 31, 2026 17:12
@os-warren
os-warren enabled auto-merge August 31, 2026 17:12
@os-warren
os-warren added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 0fb8760Aug 31, 2026
56 checks passed
@os-warren
os-warren deleted the claude/issue-13632-lookup-reference-required branch August 31, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[spec] FieldSchema accepts a lookup/master_detail with no reference target, though its own TSDoc calls the key required

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(spec): require a non-empty reference on lookup/master_detail fields by os-warren · Pull Request #13927 · objectstack-ai/objectstack · GitHub
Skip to content

fix(spec): require a non-empty reference on lookup/master_detail fields - #13927

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required
Aug 31, 2026
Merged

fix(spec): require a non-empty reference on lookup/master_detail fields#13927
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13632

Contract review: this PR carries needs:contract-review (Clause-② — it changes contract accept/reject behaviour in packages/spec), stated here as the second carrier alongside the label.

What

FieldSchema now refuses type: 'lookup' / type: 'master_detail' whose reference is missing or the empty string, via a new check in the existing superRefine chain of packages/spec/src/data/field.zod.ts — the ruling on the card (ADR-0049 enforce-or-remove, option 1). The refusal is prescriptive on the reference path: it names the type, the key, the expected shape (a snake_case target object name) and the fix, and carries no issue number (doc-authoring discipline). The key's TSDoc — which has always called reference required on these two types — now also states the enforcement. No key is removed, renamed or re-shaped: this is an accept-set narrowing only. Empty string and absence are refused alike (the card measured both as accepted before).

Premise measurement (ruling ②, taken BEFORE the tightening)

Census over the whole tree at 8c6a7fc, three channels:

  • Structured metadata (all tracked yaml/json parsed and walked): 0 field objects with type lookup/master_detail at all, hence 0 targetless.
  • Literal object fixtures in ts/js/md/mdx (balanced-brace block extraction around every type: 'lookup' / type: 'master_detail' occurrence): 515 with a non-empty reference (positive), 179 without — every one triaged below; 0 are authored objects that FieldSchema parses.
  • Helper calls Field.lookup(...) / Field.masterDetail(...) (both take the target as a required first positional argument): 209 with a non-empty string-literal target; 22 flagged for review, all resolved as comments, docstrings, markdown API tables, or old-signature blog snippets — 0 real targetless calls.

Positive controls proving the pipeline reads real hits: the showcase field-zoo's f_lookup (target showcase_account) and f_master_detail (target showcase_project) surface through the same scan; the card's own reference_to control still refuses with unrecognized_keys after the change.

Verdict: zero existing authored objects depend on a targetless lookup/master_detail. The premise holds; the narrowing was executed (no fork). Corroborating: lint's relationship/missing-reference (error severity, non-empty check over exactly lookup + master_detail) has long graded the same hole an error, and the troubleshooting docs page already documents missing reference as a validation failure — the publish seam was the one door left open.

The 179 literal negatives, triaged (three-way, per the wizard-tightening precedent)

  • Different surface, untouched (the bulk): analytics dataset dimension descriptors, dashboard filter descriptors, UI view column descriptors, object-designer edge styles, inline-grid columns, action/bulk-action param shapes — each parses under its own schema, not FieldSchema. Also untouched: REST route runtime-defense fixtures (public-form-lookup-picker pins what the route does with stored rows that deliberately never passed FieldSchema, including its own LOOKUP_TARGET_MISSING guard), driver fixtures fed straight to the driver (below), lint fixtures fed to pure stack-to-findings validators, and docs that deliberately show the wrong shape as wrong (the field-types skill guide and the troubleshooting page).
  • Moved to legal shape (3 sites — the only parse-gated targetless fixtures in the tree, found red by the spec suite): the key-absence sample loops that push a bare type set through FieldSchema.parse — the maxLength/minLength absence pins in field.test.ts and the materialization pin in field-autonumber-default-format.test.ts. Their subject is key-absence propagation, not the lookup accept surface; the lookup sample now carries reference: 'company', other types unchanged.
  • Refusal pins, unaffected: reference_to / referenceTo / target alias fixtures across drivers, lint and spec already refuse via unrecognized_keys and still do.

Blast radius noted, deliberately untouched

driver-mongodb (#13222): syncCollectionSchema declining to build the lookup index for a targetless def stays in place as runtime defense for stored/legacy artifacts, and its tests still pass — but the comment inside its "does not index a lookup that declares no target" case, which records that both targetless shapes "parse successfully" on FieldSchema, is now stale as a statement about the current schema. Recorded here for triage rather than edited: driver code and driver tests are out of this card's scope per the dispatch.

Verification

  • Reverse verification of the card's measurement table on the rebuilt dist: the three former accepts now refuse on the reference path; the two legal shapes and the reference_to positive control are unchanged.
  • New pins (8): missing refused and empty refused for both types, legal accepted for both types (the positive-control leg), non-relationship types untouched, helper builders pass. Name-filtered run: 8 passed.
  • Ablation, from the committed fix: mutation proven on disk (anchor grep 1 to 0), name-filtered pins then 4 failed / 4 passed — exactly the four refusal pins red, accepts green — and restoration proven by an empty git diff HEAD plus git hash-object equal to the HEAD blob (d4e3c6b6). The spec suite imports field.zod.ts by relative source path, so no dist rebuild sits in this ablation's resolution path.
  • Union at final head e740733 (git rev-parse --short HEAD from the same tree the run measured): full @objectstack/spec suite — verdict line "Test Files 445 passed (445) / Tests 11938 passed (11938)" — chained with pnpm --filter @objectstack/spec typecheck (tsc + scripts + tests programs), lock entry-point verdict "VERDICT command-exit 0" covering both.
  • Derived gates (dispatch-gates from this tree at fe5b03c; the script itself flags this tree as 3 commits behind origin/main with 1 derives-from file changed across the range — CI on the merge ref is the authority): 43 families derived; measured green locally, including check:authorable-surface, check:strictness-ledger, check:docs, check:doc-authoring, check:dispatcher-error-vocabulary, check:engine-double-contract, check:where-matcher and the changeset family, after three repairs — the adr-0087 marker below, a system-context census re-anchor (--fix; the superRefine insertion shifted field.zod.ts line numbers by two), and building @objectstack/formula + @objectstack/lint so check:doc-formula-expressions could measure (then green).
  • NOT MEASURED locally, preconditions named (CI-owned): check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-coverage, check:type-check-debt (all need the full workspace closure built), check-test-completeness (needs a saved turbo test log), check:pm-half-states (route refused from this container; its exit 3 self-describes as an unread instrument). Repo-wide pnpm lint is CI's run.

Changeset / ADR-0087

@objectstack/specminor with the BREAKING narrative, per the #11519 / #11842 / #13733 narrowing precedents. ADR-0087 disposition: not-required (no-migration-prescription) — no tombstone exists, the parse refusal is the channel that reaches an affected author, and which target a targetless relationship was meant to point at is authoring intent no migration entry can decide; marker in the changeset body.

Generated by Claude Code


Generated by Claude Code

…r_detail
superRefine on the relationship types (ADR-0049 declared = enforced): a
lookup / master_detail whose reference is missing or '' is refused at
parse with a prescriptive message on the reference path. TSDoc aligned;
pins for missing/empty/legal on both types; the three key-absence sample
fixtures that used a bare lookup move to legal shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
…sus line
The changeset gains the not-required (no-migration-prescription) marker
mirroring the #11519/#11842/#13733 narrowing precedents; the superRefine
insertion shifted field.zod.ts line numbers, so the system-context page
anchor is re-anchored by the census --fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 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 — 128 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 55519d50363147da03df0af068fb82fcc6d1d8c1packageMentionDocs.

Which tree this was computed on

This run read content/docs from e80b881f55da1c500d8d261d222ec98dc6c9156c — the merge of head e7407339c968d096bf4e6f945117036dae250fbb into base 55519d50363147da03df0af068fb82fcc6d1d8c1, 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 e80b881f55da1c500d8d261d222ec98dc6c9156c && git checkout e80b881f55da1c500d8d261d222ec98dc6c9156c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55519d50363147da03df0af068fb82fcc6d1d8c1 e7407339c968d096bf4e6f945117036dae250fbb && git checkout -B drift-repro 55519d50363147da03df0af068fb82fcc6d1d8c1 && git merge --no-ff e7407339c968d096bf4e6f945117036dae250fbb
node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1

⚠️ 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 55519d50363147da03df0af068fb82fcc6d1d8c1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 31, 2026
@os-warrenos-warren added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling protocol:data labels Aug 31, 2026 — with Claude
@os-warren
os-warren marked this pull request as ready for review August 31, 2026 17:12
@os-warren
os-warren enabled auto-merge August 31, 2026 17:12
@os-warren
os-warren added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 0fb8760Aug 31, 2026
56 checks passed
@os-warren
os-warren deleted the claude/issue-13632-lookup-reference-required branch August 31, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[spec] FieldSchema accepts a lookup/master_detail with no reference target, though its own TSDoc calls the key required

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(spec): require a non-empty reference on lookup/master_detail fields by os-warren · Pull Request #13927 · objectstack-ai/objectstack · GitHub
Skip to content

fix(spec): require a non-empty reference on lookup/master_detail fields - #13927

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required
Aug 31, 2026
Merged

fix(spec): require a non-empty reference on lookup/master_detail fields#13927
os-warren merged 2 commits into
mainfrom
claude/issue-13632-lookup-reference-required

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13632

Contract review: this PR carries needs:contract-review (Clause-② — it changes contract accept/reject behaviour in packages/spec), stated here as the second carrier alongside the label.

What

FieldSchema now refuses type: 'lookup' / type: 'master_detail' whose reference is missing or the empty string, via a new check in the existing superRefine chain of packages/spec/src/data/field.zod.ts — the ruling on the card (ADR-0049 enforce-or-remove, option 1). The refusal is prescriptive on the reference path: it names the type, the key, the expected shape (a snake_case target object name) and the fix, and carries no issue number (doc-authoring discipline). The key's TSDoc — which has always called reference required on these two types — now also states the enforcement. No key is removed, renamed or re-shaped: this is an accept-set narrowing only. Empty string and absence are refused alike (the card measured both as accepted before).

Premise measurement (ruling ②, taken BEFORE the tightening)

Census over the whole tree at 8c6a7fc, three channels:

  • Structured metadata (all tracked yaml/json parsed and walked): 0 field objects with type lookup/master_detail at all, hence 0 targetless.
  • Literal object fixtures in ts/js/md/mdx (balanced-brace block extraction around every type: 'lookup' / type: 'master_detail' occurrence): 515 with a non-empty reference (positive), 179 without — every one triaged below; 0 are authored objects that FieldSchema parses.
  • Helper calls Field.lookup(...) / Field.masterDetail(...) (both take the target as a required first positional argument): 209 with a non-empty string-literal target; 22 flagged for review, all resolved as comments, docstrings, markdown API tables, or old-signature blog snippets — 0 real targetless calls.

Positive controls proving the pipeline reads real hits: the showcase field-zoo's f_lookup (target showcase_account) and f_master_detail (target showcase_project) surface through the same scan; the card's own reference_to control still refuses with unrecognized_keys after the change.

Verdict: zero existing authored objects depend on a targetless lookup/master_detail. The premise holds; the narrowing was executed (no fork). Corroborating: lint's relationship/missing-reference (error severity, non-empty check over exactly lookup + master_detail) has long graded the same hole an error, and the troubleshooting docs page already documents missing reference as a validation failure — the publish seam was the one door left open.

The 179 literal negatives, triaged (three-way, per the wizard-tightening precedent)

  • Different surface, untouched (the bulk): analytics dataset dimension descriptors, dashboard filter descriptors, UI view column descriptors, object-designer edge styles, inline-grid columns, action/bulk-action param shapes — each parses under its own schema, not FieldSchema. Also untouched: REST route runtime-defense fixtures (public-form-lookup-picker pins what the route does with stored rows that deliberately never passed FieldSchema, including its own LOOKUP_TARGET_MISSING guard), driver fixtures fed straight to the driver (below), lint fixtures fed to pure stack-to-findings validators, and docs that deliberately show the wrong shape as wrong (the field-types skill guide and the troubleshooting page).
  • Moved to legal shape (3 sites — the only parse-gated targetless fixtures in the tree, found red by the spec suite): the key-absence sample loops that push a bare type set through FieldSchema.parse — the maxLength/minLength absence pins in field.test.ts and the materialization pin in field-autonumber-default-format.test.ts. Their subject is key-absence propagation, not the lookup accept surface; the lookup sample now carries reference: 'company', other types unchanged.
  • Refusal pins, unaffected: reference_to / referenceTo / target alias fixtures across drivers, lint and spec already refuse via unrecognized_keys and still do.

Blast radius noted, deliberately untouched

driver-mongodb (#13222): syncCollectionSchema declining to build the lookup index for a targetless def stays in place as runtime defense for stored/legacy artifacts, and its tests still pass — but the comment inside its "does not index a lookup that declares no target" case, which records that both targetless shapes "parse successfully" on FieldSchema, is now stale as a statement about the current schema. Recorded here for triage rather than edited: driver code and driver tests are out of this card's scope per the dispatch.

Verification

  • Reverse verification of the card's measurement table on the rebuilt dist: the three former accepts now refuse on the reference path; the two legal shapes and the reference_to positive control are unchanged.
  • New pins (8): missing refused and empty refused for both types, legal accepted for both types (the positive-control leg), non-relationship types untouched, helper builders pass. Name-filtered run: 8 passed.
  • Ablation, from the committed fix: mutation proven on disk (anchor grep 1 to 0), name-filtered pins then 4 failed / 4 passed — exactly the four refusal pins red, accepts green — and restoration proven by an empty git diff HEAD plus git hash-object equal to the HEAD blob (d4e3c6b6). The spec suite imports field.zod.ts by relative source path, so no dist rebuild sits in this ablation's resolution path.
  • Union at final head e740733 (git rev-parse --short HEAD from the same tree the run measured): full @objectstack/spec suite — verdict line "Test Files 445 passed (445) / Tests 11938 passed (11938)" — chained with pnpm --filter @objectstack/spec typecheck (tsc + scripts + tests programs), lock entry-point verdict "VERDICT command-exit 0" covering both.
  • Derived gates (dispatch-gates from this tree at fe5b03c; the script itself flags this tree as 3 commits behind origin/main with 1 derives-from file changed across the range — CI on the merge ref is the authority): 43 families derived; measured green locally, including check:authorable-surface, check:strictness-ledger, check:docs, check:doc-authoring, check:dispatcher-error-vocabulary, check:engine-double-contract, check:where-matcher and the changeset family, after three repairs — the adr-0087 marker below, a system-context census re-anchor (--fix; the superRefine insertion shifted field.zod.ts line numbers by two), and building @objectstack/formula + @objectstack/lint so check:doc-formula-expressions could measure (then green).
  • NOT MEASURED locally, preconditions named (CI-owned): check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-coverage, check:type-check-debt (all need the full workspace closure built), check-test-completeness (needs a saved turbo test log), check:pm-half-states (route refused from this container; its exit 3 self-describes as an unread instrument). Repo-wide pnpm lint is CI's run.

Changeset / ADR-0087

@objectstack/specminor with the BREAKING narrative, per the #11519 / #11842 / #13733 narrowing precedents. ADR-0087 disposition: not-required (no-migration-prescription) — no tombstone exists, the parse refusal is the channel that reaches an affected author, and which target a targetless relationship was meant to point at is authoring intent no migration entry can decide; marker in the changeset body.

Generated by Claude Code


Generated by Claude Code

…r_detail
superRefine on the relationship types (ADR-0049 declared = enforced): a
lookup / master_detail whose reference is missing or '' is refused at
parse with a prescriptive message on the reference path. TSDoc aligned;
pins for missing/empty/legal on both types; the three key-absence sample
fixtures that used a bare lookup move to legal shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
…sus line
The changeset gains the not-required (no-migration-prescription) marker
mirroring the #11519/#11842/#13733 narrowing precedents; the superRefine
insertion shifted field.zod.ts line numbers, so the system-context page
anchor is re-anchored by the census --fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 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 — 128 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 55519d50363147da03df0af068fb82fcc6d1d8c1packageMentionDocs.

Which tree this was computed on

This run read content/docs from e80b881f55da1c500d8d261d222ec98dc6c9156c — the merge of head e7407339c968d096bf4e6f945117036dae250fbb into base 55519d50363147da03df0af068fb82fcc6d1d8c1, 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 e80b881f55da1c500d8d261d222ec98dc6c9156c && git checkout e80b881f55da1c500d8d261d222ec98dc6c9156c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55519d50363147da03df0af068fb82fcc6d1d8c1 e7407339c968d096bf4e6f945117036dae250fbb && git checkout -B drift-repro 55519d50363147da03df0af068fb82fcc6d1d8c1 && git merge --no-ff e7407339c968d096bf4e6f945117036dae250fbb
node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1

⚠️ 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 55519d50363147da03df0af068fb82fcc6d1d8c1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 31, 2026
@os-warrenos-warren added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling protocol:data labels Aug 31, 2026 — with Claude
@os-warren
os-warren marked this pull request as ready for review August 31, 2026 17:12
@os-warren
os-warren enabled auto-merge August 31, 2026 17:12
@os-warren
os-warren added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 0fb8760Aug 31, 2026
56 checks passed
@os-warren
os-warren deleted the claude/issue-13632-lookup-reference-required branch August 31, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[spec] FieldSchema accepts a lookup/master_detail with no reference target, though its own TSDoc calls the key required

2 participants

@os-warren@claude