feat(spec)!: a tree field's reference, when present, must name the declaring object — refused at parse otherwise (#14892) - #15979
Conversation
…text, showcase, docs Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
#14892) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
📓 Docs Drift CheckThis PR changes 2 package(s): 27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 129 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…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
|
CI note from the dispatching seat — Generated by Claude Code |
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:
What changed
packages/spec/src/data/object.zod.ts—refuseForeignTreeReference(ownName, fields, ctx), attached as a.superRefineonObjectSchemaBase(own name =name) and onObjectExtensionSchema(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 alookup). Zod 4 attaches checks in place, soObjectSchemaBasestays aZodObjectand.shape/create()'s unknown-key walk are untouched.packages/spec/src/kernel/functional-completeness.ts—hasDetectableParentField'streearm is nowtype === 'tree' && (reference absent || reference === own); the docblock's "Mirrored, not tightened" sentence is rewritten (spec is now the stricter of the two; objectui'sdetectParentFieldstill takes anytreefield — an objectui follow-up the seat files, not edited here). Theview/tree-without-parent-fieldmessage says what a detectabletreefield is.packages/spec/src/data/object.form.ts— the sharedreferencerow (still one row, samevisibleWhen) 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 disjointvisibleWhen(thedeleteBehaviorprecedent) or by one shared string; the ruling keeps the row shared, so the string carries thetreecase.packages/spec/src/data/field.zod.ts— thereferencedocblock and.describe()gain thetreesentence (a describe move ⇒content/docs/references/data/{field,object}.mdxandsystem/migration.mdxregenerated bycheck:generated --fix).examples/app-showcase/src/data/objects/field-zoo.object.ts—f_tree: { type: 'tree', label: 'Tree (self-reference)', reference: 'showcase_field_zoo' }; header comment aligned.packages/qa/dogfood/test/field-zoo.matrix.ts—REFERENCE_TARGETS.f_treenow seeds a secondshowcase_field_zoorow (name+ thef_master_detailproject seeded one entry earlier) instead of ashowcase_categoryrow, 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.content/docs/data-modeling/validation-rules.mdx(treeproperty 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'streerow, which still said**required**(the [finding] troubleshooting 文档把tree型与 lookup/master_detail 并列为「需要 reference」,但 lint RELATIONSHIP_TYPES 与 #13632 收窄都只盖后两型——tree 的 reference 语义(必需/默认自指/可选)未测未裁 #13928 half PR docs(content): atreefield does not requirereference— two pages said it did #14890 corrected on two other pages).docs/qa/platform-checklist/areas/records-forms.jsonfixture textf_tree→showcase_field_zoo (self-reference).content/docs/permissions/system-context.mdxanchors re-pointed bycheck-system-context-census --fix(pure line rot from the insertions inobject.zod.ts/field.zod.ts).packages/spec/src/data/tree-reference-self-only.test.ts(7 cases: self accepted throughsafeParseandcreate(); absent accepted, stilldeleteBehavior: 'set_null'andclassifyDottedFilterHead=relation; foreign refused with code + path + first-sentence names; one issue per foreign field;lookup/master_detailcontrol;FieldSchemaalone does NOT refuse — the door is the object; the extension door).functional-completeness.test.tsgains three predicate cases (self → silent; foreign →view/tree-without-parent-field; nameless object: absent → silent, with reference → flagged)..changeset/tree-reference-self-only.md:@objectstack/specminor,**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/mainandcheck-changeset-no-major.mjs --base origin/mainboth exit 0 on it.The two
skills/objectstack-data/rules/*.mdsurfaces move in a separate governed PR by the skills lane (field-types.md:91"Hierarchical self-reference",relationships.md:11"Self-reference" and its:132examplecategory → categoryare 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-lesstreebeingrelationand materialisingdeleteBehavioris coherent once everytreeis 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 pinTree at
origin/main0467941 (this branch's base), objectui at the pinned.objectui-shaa472b07167a39e55491109e864bb5a54027dcfbd.referenceexamples/app-showcase/src/data/objects/field-zoo.object.ts:108showcase_field_zooshowcase_categorypackages/objectql/src/query-expression-conformance.test.ts:75showcase_taskshowcase_taskpackages/qa/dogfood/test/derive-topology.test.ts:109/:120tree/cattree/catpackages/spec/src/kernel/functional-completeness.test.ts:231,packages/lint/src/validate-functional-completeness.test.ts:120,:195category/catpackages/spec/src/data/field.test.ts:935,:954;field-value.test.ts:56,:69FieldSchema/referenceTargetOffixturescategory/categories/ absentcontent/docs/data-modeling/field-types.mdx:363,skills/objectstack-data/rules/relationships.md:140category(prose examples)categorypackages/platform-objects/src/identity/sys-business-unit.object.ts:128sys_business_unitlookupto itself, not atree)packages/plugin-tree/src/ObjectTree.settledSchemaKeying-6481.test.tsx:62,:70business_unit/territorybusiness_unit/territorypackages/plugin-calendar/src/__tests__/ObjectCalendar.expandGate-6453.test.tsx:144visitreference_to: 'visit'(retired spelling)packages/plugin-grid/src/importServerPath.test.ts:73packages/core/src/utils/__tests__/expand-fields.test.ts:134relationalZoo)showcase_categorybuildExpandFields, never parsed by the specpackages/core/src/utils/__tests__/predicate-record.test.ts:29FIELDSmap)showcase_categorytoPredicateRecordfixturepackages/plugin-dashboard/src/__tests__/expandableFamily.identity-5692.test.ts:113objectSchema()has noname)nodespackages/plugin-detail/src/__tests__/expandableFamily.identity-5874.test.tsx:119dealreference_to: 'deals'deal≠deals) — expandable-type fixture, never parsed by the specNo foreign-referencing
treeauthor exists in this repo outside the showcase. The four objectui-pin rows marked foreign-shaped are client-side unit fixtures outsideObjectSchema'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)
ObjectSchemaBaseisstrictObject(...)(aZodObject) with no object-levelsuperRefine;ObjectSchemawraps it viaObject.assignoverridingparse/safeParse/create— one parse path, the refinement ridesObjectSchemaBaseso all three doors anddefineStack/ the meta-types route see it.LifecycleSchema(strictObject(...).superRefine(...)) is the in-file precedent for the idiom and thectx.addIssue({ code, path, message })shape.def.type === 'tree'alone at:317-320; tightened; the caller at thetype === 'tree'view rule keeps its behaviour (declared || hasDetectableParentField).helpTextis a plain string in the form DSL; per-value variance is expressed by row-splitting on disjointvisibleWhen(deleteBehavior, Field designer form still offers 'Set null' for a master_detail's deleteBehavior — a choice #9689 makes a publish-time rejection #11410). The ruling keeps the row shared, so the shared string carries thetreecase. No test pins the old "Target object name" text (grep acrosspackages/,examples/,content/, and the objectui pin: zero hits on the form row).validation-rules.mdx:543"Self-referencing; no automatic cycle check" stays true and unchanged. PR docs(content): atreefield does not requirereference— two pages said it did #14890 had correctedvalidation-rules.mdx:542(the summary table's Required Props cell) anddeployment/troubleshooting.mdx:78; thetreeproperty table at:297andfield-types.mdx:361still said required — both now say optional-and-self.not-required (no-migration-prescription)is accepted bycheck-adr-0087-registration.mjs(exit 0,--base origin/main), and the body carries no rewrite pair, table or## Migrationframing by design.check:generatedafter the rebuild reports every artifact fresh; the describe move regenerated the three reference pages;authorable-surfacedid not move (no authorable key changed). Nogen:schemain a MERGE state (main not merged: none of my files overlap feat(spec,rest): the authored label is the default locale's text — ResolveOptions.defaultLocale skips the fallback chain, and a chain-less caller no longer falls to a literal en (#15711) #15888 / feat(spec): the flowendnode declares its outcome —refusedwith an interpolatedmessage, and the run vocabulary gainsrefused#15889, which landed onspec/src/system/i18n-resolver.tsandautomation/*after this branch was cut).Verification (final commit b6f0313)
pnpm --filter @objectstack/spec buildunderos-verify-lock.sh—VERDICT command-exit 0(twice; the second after the last test edit, socheck:api-surfacereads a dist newer thansrc).pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2overtree-reference-self-only,functional-completeness,object,field,filter-dotted-head,field-value,object-strictness-batch20,alias-integrity,field-rows-option-description—Test 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 undertsconfig.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).tsximport offield-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 hadcheck:docsstale, regenerated with--fix)..superRefineonObjectSchemaBase(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 atraprunninggit checkout HEAD -- ABS_PATH; proof:git hash-objectequals the HEAD blob (9012ca36…),git diff HEAD --statempty, marker count 0. No build leg needed: the pin imports./object.zodfromsrc.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsfrom the merge base (98 families, pluscheck:platform-checklistfor 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-examples1,check:dual-build-cjs-loads/check:i18n/check:type-check-debt3 — each prints "nothing was measured").--ranreconciliation 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-loadsandcheck:skill-exampleseach exit 3 here — PREREQUISITE NOT MET (they read the built output of the wholepackages/*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 lintand the fullpnpm testover the 75 packagesturbo ls --affectedlists (every spec consumer) are CI's runs.Generated by Claude Code