Skip to content

feat(spec)!: a tree field's reference, when present, must name the declaring object — refused at parse otherwise (#14892) - #15979

Draft
claude[bot] wants to merge 4 commits into
mainfrom
claude/issue-14892-tree-reference-self-only
Draft

feat(spec)!: a tree field's reference, when present, must name the declaring object — refused at parse otherwise (#14892)#15979
claude[bot] wants to merge 4 commits into
mainfrom
claude/issue-14892-tree-reference-self-only

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #14892

Maintainer ruling recorded on the card (comment 5548738608, director seat, 2026-09-05; provenance: maintainer, decision batch #42, verbatim 「13753 我让别人处理了,其他同意」), executed as ruled:

Ruled: A. FieldSchema refuses type: 'tree' with a reference naming any object other than the declaring one (a superRefine at the object level, where the own name is known; the message names both). reference stays optional — under A it is a redundant self-annotation, which is what makes the fifth reading coherent: a self-referential hierarchy is a relation and its cascade deleteBehavior is exactly the intended semantics. hasDetectableParentField's tree arm is tightened to the same rule. The designer's help text for tree says "this object" (the form row stays shared; only the text and validation change). showcase_field_zoo.f_tree is corrected to self-reference. Not taken: B (a free target — a designer form-reuse artefact promoted to semantics, and it would require defining rendering, cycle detection and delete semantics for cross-object trees), C (prose only).

What changed

  • packages/spec/src/data/object.zod.tsrefuseForeignTreeReference(ownName, fields, ctx), attached as a .superRefine on ObjectSchemaBase (own name = name) and on ObjectExtensionSchema (own name = extend, so an extension cannot merge the shape the object refuses — a small widening of the ruling's file face, declared here). The issue: code: 'custom', path ['fields', FIELD, 'reference'], first sentence naming the field, the declaring object and the object it wrongly points at; the remedy sentence names the three ways out (drop it, self-reference, or a lookup). Zod 4 attaches checks in place, so ObjectSchemaBase stays a ZodObject and .shape / create()'s unknown-key walk are untouched.
  • packages/spec/src/kernel/functional-completeness.tshasDetectableParentField's tree arm is now type === 'tree' && (reference absent || reference === own); the docblock's "Mirrored, not tightened" sentence is rewritten (spec is now the stricter of the two; objectui's detectParentField still takes any tree field — an objectui follow-up the seat files, not edited here). The view/tree-without-parent-field message says what a detectable tree field is.
  • packages/spec/src/data/object.form.ts — the shared reference row (still one row, same visibleWhen) now reads: "Target object name. For a tree field it is optional and, if given, must be this object (a tree is a hierarchy within its own object — link a different object with a lookup)". The registry expresses per-value help text either by row-splitting on disjoint visibleWhen (the deleteBehavior precedent) or by one shared string; the ruling keeps the row shared, so the string carries the tree case.
  • packages/spec/src/data/field.zod.ts — the reference docblock and .describe() gain the tree sentence (a describe move ⇒ content/docs/references/data/{field,object}.mdx and system/migration.mdx regenerated by check:generated --fix).
  • examples/app-showcase/src/data/objects/field-zoo.object.tsf_tree: { type: 'tree', label: 'Tree (self-reference)', reference: 'showcase_field_zoo' }; header comment aligned.
  • packages/qa/dogfood/test/field-zoo.matrix.tsREFERENCE_TARGETS.f_tree now seeds a second showcase_field_zoo row (name + the f_master_detail project seeded one entry earlier) instead of a showcase_category row, since data: a lookup accepts an id that does not exist in the referenced object — including the RBAC permission-set link tables #4441 refuses a reference to a row that does not exist in the referenced object.
  • Docscontent/docs/data-modeling/validation-rules.mdx (tree property table and default-constraints sentence: optional; if given, must be this object), field-type-decision-tree.mdx (one callout after the relationship table), and — a same-semantics sweep addition — field-types.mdx's tree row, which still said **required** (the [finding] troubleshooting 文档把 tree 型与 lookup/master_detail 并列为「需要 reference」,但 lint RELATIONSHIP_TYPES 与 #13632 收窄都只盖后两型——tree 的 reference 语义(必需/默认自指/可选)未测未裁 #13928 half PR docs(content): a tree field does not require reference — two pages said it did #14890 corrected on two other pages). docs/qa/platform-checklist/areas/records-forms.json fixture text f_tree→showcase_field_zoo (self-reference). content/docs/permissions/system-context.mdx anchors re-pointed by check-system-context-census --fix (pure line rot from the insertions in object.zod.ts / field.zod.ts).
  • Pins — new packages/spec/src/data/tree-reference-self-only.test.ts (7 cases: self accepted through safeParse and create(); absent accepted, still deleteBehavior: 'set_null' and classifyDottedFilterHead = relation; foreign refused with code + path + first-sentence names; one issue per foreign field; lookup/master_detail control; FieldSchema alone does NOT refuse — the door is the object; the extension door). functional-completeness.test.ts gains three predicate cases (self → silent; foreign → view/tree-without-parent-field; nameless object: absent → silent, with reference → flagged).
  • Changeset.changeset/tree-reference-self-only.md: @objectstack/spec minor, **BREAKING** banner, adr-0087: not-required (no-migration-prescription), and the verbatim caveat "Out-of-repo cross-object trees are NOT MEASURED". check-adr-0087-registration.mjs --base origin/main and check-changeset-no-major.mjs --base origin/main both exit 0 on it.

The two skills/objectstack-data/rules/*.md surfaces move in a separate governed PR by the skills lane (field-types.md:91 "Hierarchical self-reference", relationships.md:11 "Self-reference" and its :132 example category → category are already self-reference wording; nothing there is edited here). #13928 is not reopened by this change (its docs half landed in PR #14890); the fifth reading (comment 5522566920) is answered by this change: a reference-less tree being relation and materialising deleteBehavior is coherent once every tree is self-referential by contract. #9689 / ADR-0122 are context only and remain open where they are.

Author census — every in-repo type: 'tree' field author, and the objectui pin

Tree at origin/main 0467941 (this branch's base), objectui at the pinned .objectui-sha a472b07167a39e55491109e864bb5a54027dcfbd.

author declaring object reference class
examples/app-showcase/src/data/objects/field-zoo.object.ts:108 showcase_field_zoo was showcase_category foreign — the ruled one, corrected to self
packages/objectql/src/query-expression-conformance.test.ts:75 showcase_task showcase_task self
packages/qa/dogfood/test/derive-topology.test.ts:109 / :120 tree / cat tree / cat self
packages/spec/src/kernel/functional-completeness.test.ts:231, packages/lint/src/validate-functional-completeness.test.ts:120, :195 category / cat absent absent
packages/spec/src/data/field.test.ts:935, :954; field-value.test.ts:56, :69 none — field-level FieldSchema / referenceTargetOf fixtures category / categories / absent field-level, no declaring object (outside the object door's reach; kept as they are, and pinned as such)
content/docs/data-modeling/field-types.mdx:363, skills/objectstack-data/rules/relationships.md:140 category (prose examples) category self
packages/platform-objects/src/identity/sys-business-unit.object.ts:128 sys_business_unit (a lookup to itself, not a tree) not a tree author
objectui packages/plugin-tree/src/ObjectTree.settledSchemaKeying-6481.test.tsx:62, :70 business_unit / territory business_unit / territory self
objectui packages/plugin-calendar/src/__tests__/ObjectCalendar.expandGate-6453.test.tsx:144 visit reference_to: 'visit' (retired spelling) self
objectui packages/plugin-grid/src/importServerPath.test.ts:73 (array fixture) absent absent
objectui packages/core/src/utils/__tests__/expand-fields.test.ts:134 none (a bare fields map, relationalZoo) showcase_category foreign-shaped — mirrors the shipped showcase example; unit fixture for buildExpandFields, never parsed by the spec
objectui packages/core/src/utils/__tests__/predicate-record.test.ts:29 none (a bare FIELDS map) showcase_category foreign-shaped — same mirror; toPredicateRecord fixture
objectui packages/plugin-dashboard/src/__tests__/expandableFamily.identity-5692.test.ts:113 none (objectSchema() has no name) nodes unclassifiable — no declaring name; expandable-type fixture
objectui packages/plugin-detail/src/__tests__/expandableFamily.identity-5874.test.tsx:119 deal reference_to: 'deals' foreign-shaped (dealdeals) — expandable-type fixture, never parsed by the spec

No foreign-referencing tree author exists in this repo outside the showcase. The four objectui-pin rows marked foreign-shaped are client-side unit fixtures outside ObjectSchema's reach (they exercise expand/predicate plumbing by field TYPE); they are listed on the card for the PM's judgment per the dispatch's stop condition and do not change what this PR does.

Mechanism readings (M1–M8)

Verification (final commit b6f0313)

  • pnpm --filter @objectstack/spec build under os-verify-lock.shVERDICT command-exit 0 (twice; the second after the last test edit, so check:api-surface reads a dist newer than src).
  • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 over tree-reference-self-only, functional-completeness, object, field, filter-dotted-head, field-value, object-strictness-batch20, alias-integrity, field-rows-option-descriptionTest Files 8 passed (8) · Tests 593 passed (593); the new file alone after the typed-create() fix: Tests 7 passed (7).
  • pnpm --filter @objectstack/spec typecheck (tsc + check:scripts-typecheck + check:test-typecheck) — VERDICT command-exit 0; check:test-typecheck: OK — 54 file(s) / 261 error(s) / 145 pinned signature (the ratchet unchanged; the new test file compiles clean under tsconfig.test.json).
  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-functional-completeness.test.ts src/object-graph.test.ts (lint built) — Test Files 2 passed (2) · Tests 50 passed (50).
  • Corrected showcase object parsed through the BUILT spec (tsx import of field-zoo.object.ts): f_tree{"type":"tree","reference":"showcase_field_zoo","deleteBehavior":"set_null",…}.
  • pnpm --filter @objectstack/spec check:generated — after the rebuild: all 15 artifacts fresh (the earlier run had check:docs stale, regenerated with --fix).
  • Reverse verification (ablation), from the committed state: removed the .superRefine on ObjectSchemaBase (on-disk proof: anchor line count 1 → 0, marker count 0 → 1), re-ran the new pin file under the lock — Tests 2 failed | 5 passed (7): exactly the two refusal pins turned red (foreign refused; one issue per foreign field), the accepted-shape pins and the extension-door pin stayed green. Restored by a trap running git checkout HEAD -- ABS_PATH; proof: git hash-object equals the HEAD blob (9012ca36…), git diff HEAD --stat empty, marker count 0. No build leg needed: the pin imports ./object.zod from src.
  • Gates derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands from the merge base (98 families, plus check:platform-checklist for the checklist JSON), re-run in full on the final commit b6f0313 after the lint/formula build: every command exit 0 except the four prerequisite-not-met families declared below (check:skill-examples 1, check:dual-build-cjs-loads / check:i18n / check:type-check-debt 3 — each prints "nothing was measured"). --ran reconciliation over that record: 0 UNRUN. Every exit code was captured redirect-first, never through a pipe.

Declared to CI (narrowing, stated): pnpm check:i18n, pnpm check:type-check-debt, pnpm check:dual-build-cjs-loads and check:skill-examples each exit 3 here — PREREQUISITE NOT MET (they read the built output of the whole packages/* closure / client-react's 35-package closure), which is NOT MEASURED locally, not red; the diff touches none of their subjects (no i18n bundle, no ledger number, no package entry, no skill). The dogfood HTTP round-trip (packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.ts, the consumer of the matrix edit) boots the showcase over a 66-package closure and is declared to the Dogfood Regression Gate; the seed body was checked against the zoo's declared required fields (name, f_master_detail). pnpm lint and the full pnpm test over the 75 packages turbo ls --affected lists (every spec consumer) are CI's runs.


Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 7 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 f7db8f4fd268a86a08c62ae4894cf7417720f8c9.

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

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 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 — 129 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 f7db8f4fd268a86a08c62ae4894cf7417720f8c9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6f45ff617c5fe1f173b1960942ec1c639e7f5c21 — the merge of head 91e253208305ad76bedd0352bb03576ee6a93e6b into base f7db8f4fd268a86a08c62ae4894cf7417720f8c9, 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 6f45ff617c5fe1f173b1960942ec1c639e7f5c21 && git checkout 6f45ff617c5fe1f173b1960942ec1c639e7f5c21
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f7db8f4fd268a86a08c62ae4894cf7417720f8c9 91e253208305ad76bedd0352bb03576ee6a93e6b && git checkout -B drift-repro f7db8f4fd268a86a08c62ae4894cf7417720f8c9 && git merge --no-ff 91e253208305ad76bedd0352bb03576ee6a93e6b

node scripts/docs-audit/affected-docs.mjs --json f7db8f4fd268a86a08c62ae4894cf7417720f8c9

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

…the shared reference row's help text (#14892)

node scripts/check-i18n-bundles.mjs --write — the only bundle that moved; the
`reference` describe lives in no bundle. Clears the red `pnpm check:i18n` on
PR #15979's head b6f0313 (Type Check · consumer gates).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

CI note from the dispatching seat — Lint & Repo Gates is red on this PR for a reason that is not this PR's. The failing step is Merge-driver wiring gate (pnpm check:merge-driver), and it fails identically on main (aa6ba0623, push run 33979882868 at 17:26Z) and on every run since ~17:22Z in every lane: the check-regen-pending.mjs --self-test fixture runs pnpm -s in an unpinned throwaway project, and Corepack now resolves pnpm latest = 12.3.4, whose CLI rejects -s. Root cause and the local reproduction are on the anchor card #15992 (priority:p0, domain:devx); the one-file fix is dispatched from there as a fix-forward that jumps the queue, and this PR needs no change for it — CI re-runs against main once the fix lands. The TypeScript Type Check / Type Check · consumer gates failure on b6f0313bb WAS this PR's (the regenerated en.metadata-forms bundle) and is fixed on 91e253208. The seat re-reviews the delta, re-cycles the contract-review carriers at the new head, and flips this PR to ready + auto-merge once every check is green.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

1 participant