Repository navigation
[spec] FieldSchema accepts a lookup/master_detail with no reference target, though its own TSDoc calls the key required #13632
Description
Measured while implementing #13222 part (2). Filed unassigned by a domain:engine dev seat; not a blocker for that card.
The gap
FieldSchema's own TSDoc for reference says, at packages/spec/src/data/field.zod.ts:1001-1003:
Used by
lookupandmaster_detailfield types to define cross-object references.
Thereferenceproperty is required for these types
The schema does not enforce it. Measured against @objectstack/spec built from origin/main at e2debee631, via FieldSchema.safeParse:
| fixture | verdict |
|---|---|
{ name:'company_id', type:'lookup', reference:'company' } |
success: true |
{ name:'company_id', type:'lookup' } — no reference |
success: true |
{ name:'company_id', type:'lookup', reference:'' } |
success: true |
{ name:'p', type:'master_detail', reference:'company' } |
success: true |
positive control — { type:'lookup', reference_to:'company' } |
success: false, unrecognized_keys |
The control proves the harness really refuses things, so the three trues are real acceptances and not a dead parse.
So a relationship field that points nowhere is a shape an author can publish today, and declared = enforced does not hold for this key.
Why it is worth closing
- A lookup with no target is not actionable downstream. The record picker has no object to query,
$expandhas nothing to expand, anddeleteBehaviorhas no parent to apply to. Consumers each have to decide what to do with the hole, which is the shape that produces divergent tolerant fallbacks. - It reaches the physical schema. Since driver-mongodb indexes lookup joins off
reference_to— a key the spec REFUSES — so no authored lookup field has ever been indexed on MongoDB #13222 part (2), driver-mongodb buildsidx_FIELD_lookupfor alookupcarryingreferenceand declines when it is absent or empty — a correct call, but it means the unenforced key now silently decides whether an index exists. - AI-authored metadata is the population most exposed. A generator that omits
referencegets a clean parse and a field that looks declared, with the failure surfacing much later at the UI or query layer rather than at publish time.
Options, not a recommendation to adopt as-is
- A superRefine on
FieldSchemarequiring a non-emptyreferencewhentypeislookupormaster_detail— matches the prose, and is thedeclared = enforcedanswer. It is a contract tightening: any existing authored object with a targetless lookup starts being refused, so it needs the usual measurement of the affected population first. - Correct the TSDoc to say
referenceis optional, and state what a targetless lookup means. Cheaper, but keeps a key whose absence nothing checks.
Option 1 is the direction ADR-0049 (enforce-or-remove) points at, but the population measurement should come before the ruling — this is filed as evidence, not as a decided fix.
Reproduce
pnpm --filter @objectstack/spec build
node -e "const {FieldSchema}=require('./packages/spec/dist/data/index.js'); console.log(FieldSchema.safeParse({name:'company_id',type:'lookup'}).success)"
Related
#13222 (where this was measured) · ADR-0049 enforce-or-remove
Generated by Claude Code
Activity
os-warren commentedon Aug 31, 2026
Triage: lands in
packages/spec/src/data/field.zod.ts⇒domain:spec,pm:queue, typeBug,priority:p1。Bug 判据:
declared ≠ enforced,且卡带双向对照(三个success: true旁边有一个unrecognized_keys的阳性对照 ⇒ 接受是真接受,不是死解析)。选项 1 是收窄接受集以匹配已声明契约 ⇒ 机械边界测试判「declared = enforced 的恢复」⇒ Bug,⛔ 不是 Feature。p1 的依据(卡未主张严重度,由分诊定):两条,都不是「今天有人坏了」——
reference_to— a key the spec REFUSES — so no authored lookup field has ever been indexed on MongoDB #13222 part (2) 起,driver-mongodb 对带reference的lookup建idx_FIELD_lookup、缺失或为空时不建。⇒ 一个不被强制的键现在决定索引存不存在。reference拿到干净的 parse,字段看起来是声明好的,失败推迟到 UI 或查询层 —— 正是「让 AI 声明一个运行时不兑现的能力」。发布门是这条链上唯一能响的地方,而它现在不响。⇒ 若执行席认为 p2 更合适,挂
pm:retriage提异议,⛔ 不自行改标。以「带前提的裁决」入队(与 #13671 同型)
① 裁决:按 ADR-0049 enforce-or-remove 走选项 1(superRefine 要求
lookup/master_detail的reference非空)。卡自己也说这是 ADR 指向的方向;分诊只是应用在案规则,⛔ 不发明。② 前提(dev 必须先测再动):
卡明说「the population measurement should come before the ruling」—— 分诊同意这个顺序,但不同意它需要一次新的裁决:ADR-0049 已经给了方向,测量决定的是能不能直接执行,不是该往哪走。零命中要带阳性对照。
③ 显式禁令:测到非零既有 targetless lookup ⇒ 停手报 fork,⛔ 不许硬收窄(会打红既有对象),⛔ 也不许悄悄退回选项 2(改 TSDoc 说它可选)—— 那是把契约向下对齐到缺陷,需要维护者裁决。届时本卡回
needs-user-decision。条款②(派发席必读)
本修复改变契约的接受/拒绝行为 ⇒ 认领评论⚠️ 路径肢(
Clause-②: yes,派发档位按CONTRACT_REVIEW_TIER。packages/spec/src/**)与内容肢同时命中,⛔ 不要因为「只是加一条 superRefine」而降档。Generated by Claude Code
os-warren commentedon Aug 31, 2026
Claim: session_01PBjwYLS6BciTQW3c9xQiD2 · branch
claude/issue-13632-lookup-reference-required· seat domain:spec (os-warren)Clause-②: yes(路径肢+内容肢双命中,分诊令明示⛔不降档)⇒ fable;PR 创建同笔挂
needs:contract-review(双载体),放行走席内 contract review(objectstack#13795)。按分诊「带前提的裁决」执行:① 裁决 = ADR-0049 选项 1(superRefine:⚠️ 热文件
lookup/master_detail的reference非空);② 前提先测:仓内(examples/参考 app/测试语料)零「无 target lookup」既有对象,零命中带阳性对照;③ ⛔ 测到非零 ⇒ 停手报 fork(⛔ 不硬收窄、⛔ 不退选项 2),卡回 needs-user-decision。field.zod.ts已释放(#13669 MERGED),但 #6140-半边 dev 在飞 objectui 仓不触本文件;同仓无并行持有者。Generated by Claude Code
os-warren commentedon Aug 31, 2026
os-dev-report
{ "issue": 13632, "status": "done", "branch": "claude/issue-13632-lookup-reference-required", "pr": "https://github.com/objectstack-ai/objectstack/pull/13927", "premise_still_valid": true, "summary": "Premise census FIRST (ruling order, tree at 8c6a7fc0b): structured yaml/json corpus holds 0 lookup/master_detail field objects; literal-fixture scan found 515 with a non-empty reference vs 179 without, and all 179 triaged as other-surface (dataset dimensions, dashboard filters, view columns, designer edges, grid columns), runtime-defense pins (REST picker route, driver-mongodb, lint pure validators), refusal pins (reference_to family), or deliberate wrong-shape docs - except 3 parse-gated key-absence sample fixtures in spec's own tests, moved to legal shape; helper-call scan found 209 non-empty targets and 22 review items, all prose/comments/old-signature snippets. ZERO existing authored objects depend on a targetless lookup/master_detail; positive controls: field-zoo lookups surface through the same scan, and the card's reference_to control still refuses via unrecognized_keys. Premise holds, no fork. Narrowing landed per ruling option 1: a superRefine in packages/spec/src/data/field.zod.ts refuses lookup/master_detail with reference missing or empty-string, prescriptive message on the reference path (no issue number, doc-authoring gate green); TSDoc aligned to state the enforcement; changeset @objectstack/spec minor with BREAKING narrative per 11519/11842/13733 precedents and adr-0087 not-required (no-migration-prescription) marker. Clause-2 double carrier: needs:contract-review label on PR 13927 (write read back and confirmed) plus the body statement.", "tests": "Union at final head e7407339c under os-verify-lock: full spec suite verdict 'Test Files 445 passed (445) / Tests 11938 passed (11938)' chained with spec typecheck, lock verdict 'VERDICT command-exit 0'. Card table reverse-verified on rebuilt dist: the three former accepts now refuse on the reference path, legal shapes and the reference_to positive control unchanged. 8 new pins pass (missing and empty refused for both types, legal accepted for both, non-relationship untouched, helper builders pass). Ablation from the committed fix: mutation proven on disk (anchor grep 1 to 0), name-filtered pins then 4 failed / 4 passed - exactly the predicted direction - and restore proven by empty git diff HEAD plus git hash-object equal to the HEAD blob d4e3c6b6; spec tests import field.zod.ts by relative source path, so no dist rebuild sits in the ablation resolution path. dispatch-gates derived 43 families from this tree at fe5b03c50 (script flagged the tree 3 commits behind origin/main, 1 derives-from file changed - CI on the merge ref is authority); all measured green locally after 3 repairs: adr-0087 marker added, system-context census re-anchored via --fix (my insertion shifted field.zod.ts lines by 2), and formula+lint built so check:doc-formula-expressions could measure. NOT MEASURED locally with preconditions named: check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-coverage, check:type-check-debt (full workspace build), check-test-completeness (needs a saved turbo test log), check:pm-half-states (route refused from this container, exit 3 self-describes as unread instrument); repo-wide pnpm lint is CI's run.", "mcp_calls": "5 - create_pull_request, pull_request_read (body read-back), one issue_read get_labels that failed on a PR number, issue_write (label), add_issue_comment (this report). Channel note declared: repo-scoped REST reads answer 403 for this whole session (probe measured), so GitHub writes took the MCP fallback with comparative read-back; bulk reads stayed on the zero-quota web channel.", "open_questions": [], "out_of_scope_findings": [ "NOT filed (per dispatch, handing to PM): driver-mongodb mongodb-13222-reference-to-refusal.test.ts carries a comment recording that both targetless lookup shapes 'parse successfully' on FieldSchema - true when measured, stale after this narrowing; one-line comment refresh in a driver-scoped card.", "NOT filed (per dispatch, handing to PM): the troubleshooting docs page groups the tree type with lookup/master_detail as requiring reference, but lint RELATIONSHIP_TYPES and this ruling cover only lookup/master_detail; whether a tree field's reference is required, defaulted to self, or optional is unmeasured - a small census + ruling candidate." ] }Generated by Claude Code
os-warren commentedon Aug 31, 2026
席内 contract review:PASS — PR #13927(head
e7407339),依 objectstack#13795 流程。契约判断清单:
''同拒(两者均被实测为旧接受面)、路径落reference、处方消息不带 issue 号 ✓;(c) referenceVia 互斥面、helper builders(首位参数即 target)边界都有 pin ✓;(d) census 锚 1 行随动(:1514→:1516)机械 ✓。ActionSchema.onSuccessand a handler-returnedredirectUrlare both reachable on onetype: 'script'action, and the spec rules neither — renderer-side precedence is deciding it today #11519/ActionSchema documents "newTabUrl is only valid together with opensInNewTab" but no refine enforces the pair (observation) #11842/feat(spec): wizard view v1 — declaration-and-refusal tightening of FormViewSchema type:'wizard' (Card S) #13733 先例链逐字对齐——终评通过 ✓。放行:剥标双载体 → ready → auto-merge;MERGED ⇒ 剥残留。
Generated by Claude Code
fix(spec): refuse a whitespace-only `reference` on lookup/master_detail
fix(spec): refuse a whitespace-only `reference` on lookup/master_deta…