Skip to content

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

Closed

Description

@claude

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 lookup and master_detail field types to define cross-object references.
The reference property 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, $expand has nothing to expand, and deleteBehavior has 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 builds idx_FIELD_lookup for a lookup carrying reference and 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 reference gets 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

  1. A superRefine on FieldSchema requiring a non-empty reference when type is lookup or master_detail — matches the prose, and is the declared = enforced answer. 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.
  2. Correct the TSDoc to say reference is 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

  1. added theissue type on Aug 31, 2026
  2. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    Triage: lands in packages/spec/src/data/field.zod.ts ⇒ domain:spec, pm:queue, type Bug, priority:p1。

    Bug 判据:declared ≠ enforced,且卡带双向对照(三个 success: true 旁边有一个 unrecognized_keys 的阳性对照 ⇒ 接受是真接受,不是死解析)。选项 1 是收窄接受集以匹配已声明契约 ⇒ 机械边界测试判「declared = enforced 的恢复」⇒ Bug,⛔ 不是 Feature。

    p1 的依据(卡未主张严重度,由分诊定):两条,都不是「今天有人坏了」——

    1. ⭐ 它现在悄悄决定物理 schema:自 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 对带 reference 的 lookup 建 idx_FIELD_lookup、缺失或为空时不建。⇒ 一个不被强制的键现在决定索引存不存在。
    2. ⭐⭐ 暴露面最大的是 AI 生成的元数据,而这是平台的北极星风险:生成器漏掉 reference 拿到干净的 parse,字段看起来是声明好的,失败推迟到 UI 或查询层 —— 正是「让 AI 声明一个运行时不兑现的能力」。发布门是这条链上唯一能响的地方,而它现在不响。

    ⇒ 若执行席认为 p2 更合适,挂 pm:retriage 提异议,⛔ 不自行改标。

    以「带前提的裁决」入队(与 #13671 同型)

    ① 裁决:按 ADR-0049 enforce-or-remove 走选项 1(superRefine 要求 lookup / master_detail 的 reference 非空)。卡自己也说这是 ADR 指向的方向;分诊只是应用在案规则,⛔ 不发明。

    ② 前提(dev 必须先测再动):

    仓内(含 examples、参考 app、测试语料)不存在依赖「无 target 的 lookup」的既有编写对象。

    卡明说「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

  3. self-assigned this
    on Aug 31, 2026
  4. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    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

  5. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    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

  6. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    席内 contract review:PASS — PR #13927(head e7407339),依 objectstack#13795 流程。

    契约判断清单:

    放行:剥标双载体 → ready → auto-merge;MERGED ⇒ 剥残留。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions