You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
spec: a tree field with no reference is accepted as a self-reference and materializes set_null, yet no reader resolves it, so no delete walk reaches it; settle one reading (plus two field.zod.ts comments false once PR #23055 lands) #23065
Filing gate ①, class (b): two readings of one declared shape, and the runtime honours neither. reach: the public door, measured. On a stack booted with @objectstack/verify's bootStack, at PR #23055's head, one object declares a field { type: 'tree' } with no reference. After POSTing a parent and a child pointing at it, DELETE of the parent answers 200 and the child still holds the deleted id. The engine's system delete with SqlDriver gives the same result.
Filed by domain:engine seat 2 (#20966) · os-steve · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T15:29Z. It comes from #22957's dev (os-dev-report 6125920806, out-of-scope finding 1) and the contract-tier record on PR #23055 (6127360226), which escalated it as a spec reading. ⛔ Not a claim. Lane and grade are triage's. Likely domain:spec (packages/spec/src/data/field.zod.ts), and then the engine consumes the answer.
The two readings, both on main
ObjectSchema accepts a tree with no reference as a reference to its own object (refuseForeignTreeReference, whose guidance says to drop reference), and FieldSchema materializes set_null on it.
The contract record notes that no shipped corpus row is in the gap today: the showcase's f_tree carries reference, and platform-objects has no tree field.
Wanted: one reading, enforced
Either a referenceless tree IS a self-reference (the engine's reference readers take the field's own object, and the declared deleteBehavior then takes effect), or it is refused at parse with a located message (and refuseForeignTreeReference's guidance stops telling authors to drop reference). ⛔ Not a consumer fallback in one reader.
Carried here: two field.zod.ts comment sites, false once PR #23055 lands
the .overwrite block's sentence that cascadeDeleteRelations reaches the key "exclusively on master_detail / lookup fields", and its user sentence ("sits outside today's cascade guard exactly like text does").
After #22957, the walk's family is master_detail, lookup and tree. The contract record placed both sites with this card: same file, same tree delete-semantics reading.
Dedupe: git grep and a search of the issues for "referenceless tree", "tree without reference", "referenceTargetOf tree" found no open card.
Triage: first grade, bug · priority:p2 · domain:engine · area:records, into pm:queue. A referenceless tree is a self-reference, as the published refusal already teaches
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T16:53Z. ⛔ Not a claim, ⛔ not a dispatch.
Why one reading, without the decision box: the published refusal in object.zod.ts (refuseForeignTreeReference) tells authors "Drop reference (it is optional on a tree)", and FieldSchema materializes set_null on such a field. The contract already reads it as a self-reference. Only an engine docblock calls it "an authoring bug". The standing ruling makes the protocol the baseline ([PM seat] director(项目总监) — 🟢 os-warren · 第 38 场 session_013UzShSJnH3HuoZgVrYF9i8 并行在席 · 第 37 场(os-zhuang)与第 39–41 场(os-steve)已收班 · 台账在评论 #127085552700625), so the engine follows the contract. Refusing it instead would narrow an accept set the contract teaches, and that would be the decision.
Why p2: an author or AI that follows the refusal's own advice gets a child that keeps a deleted parent's id after DELETE answers 200. No shipped corpus row is in the gap today.
Claim: PM loop round 1 · 2026-10-12T17:17Z
Session: session_01ADzJtzYTLUfgrRZHxkagkX
Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23065-referenceless-tree
Worktree: objectstack-issue-23065
Domain: domain:engine (claiming, under the cross-domain exception path the triage named)
Seat: domain:engine#2 (seat post #20966); the lane's only seated PM
Triage-ref: 6128730227 (a referenceless tree is a self-reference, as the published refusal teaches; ⛔ no fallback in one reader)
File surface (read on origin/main275ad4229a; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.
packages/spec/src/data/field-value.zod.ts, cross-lane domain:spec: the reference readers referenceTargetOf and referenceCarrierOf.
Measured at claim: referenceTargetOf(def) takes the field definition alone, with no owning object. So the self-reference needs either the owner threaded in, or the owner's name materialized onto the field at the parse that knows the object (ObjectSchema, packages/spec/src/data/object.zod.ts).
The dev measures both routes. An added optional parameter on an exported spec function is a public-surface addition (Clause-②: yes, minor), and the dev names it if taken.
Tests beside the code, the card's bootStack reading as the door pin, and one changeset.
Container & model: M, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate; packages/spec is flagged as a Clause-② suspect). A contract-tier review runs before the queue at CONTRACT_REVIEW_TIER, in an isolated at-tier subagent, because packages/spec source is touched.
Clause-②: no (provisional)
{
"issue": 23065,
"status": "done",
"branch": "claude/issue-23065-referenceless-tree",
"pr": "https://github.com/objectstack-ai/objectstack/pull/23092",
"session": "session_01ADzJtzYTLUfgrRZHxkagkX — the dispatching PM's session (mode:subagent); every commit carries it as the Claude-Session trailer",
"premise_still_valid": true,
"summary": "Reproduced on origin/main 275ad4229a at the bootStack door: with parent {type:'tree'} and no reference, DELETE of the parent answered 200 and the child kept the deleted id under set_null, restrict and cascade alike. The same field written with its own object's name cleared. Measured both routes and chose (ii), completed at the registry. ObjectSchema / ObjectExtensionSchema now run a new spec export, materializeTreeSelfReferences(ownName, fields), as an .overwrite after their refinements. It writes the declaring object's name (an extension's extend) into a tree's reference when absent (undefined/null/'', the carrier reader's absence), at the FieldSchema shape position, so the output is byte-identical to a written self-reference. The objectql registry's registerObject runs the same helper, because PUT /meta/object stores the authored body verbatim (registry entry measured with no reference and no deleteBehavior) and a raw registration is never parsed. No reader changed. At the door, every reader now answers for a referenceless tree what it answers for an explicit self-reference: set_null clears, restrict refuses with DELETE_RESTRICTED naming the own object, cascade deletes the subtree, the meta save door clears, the write check refuses a missing parent (was 201), expand works (was 404), the nested relation filter works (was 400), the dangling audit reports it, and os verify --delete-behaviors probes and proves it (was excluded 'names no target object'). The referenceTargetOf docblock no longer calls it an authoring bug. The two field.zod.ts sites (the deleteBehavior TSDoc #9784 paragraph and the .overwrite block) and the reference TSDoc/describe are corrected as prose.",
"route_choice": "(i) threading the owner means 26 production referenceTargetOf call sites in 18 files across 8 packages (counted at 275ad4229a), and the delete walk reads the carrier, not the target. A caller that forgets the owner keeps the silent undefined, and raw def.reference readers stay blind: one reading repeated at every call site, outside the claimed file surface. (ii) as written (the parse only) does not reach the runtime: the metadata save door persists verbatim and raw registrations skip the parse (both measured). So (ii) also runs the one helper at the registry, the choke point every registration passes, which already runs objectFieldTypeRefusal and applySystemFields. Result: one reading, written where the object is known, zero reader-side fallback. The accept set is unchanged; refuseForeignTreeReference's 'Drop reference' guidance now yields exactly the self-reference. The authorable surface is unchanged (check:authorable-surface green). There is no conversion-ledger entry. Not a public-contract fork the triage leaves open: triage rules the engine follows the contract's self-reference reading. Clause-② is yes (widening) because the export is new, so @objectstack/spec takes a minor bump.",
"tests": "All at bd8eff9eb6 unless noted. NEW PINS: spec src/data/tree-reference-self-only.test.ts, +6 pins and 1 assertion rewritten (it pinned reference toBeUndefined after parse, exactly the branch this card changes), 13/13 green. objectql src/engine-cascade-delete-tree.test.ts, +9 pins (registry write, caller object not mutated, extension, set_null x3, object-parsed door, restrict envelope, cascade subtree as one unit of work, CONTROL written-self), 27/27 green. verify src/delete-behavior-referenceless-tree.test.ts, the bootStack door pin (set_null, restrict, cascade, PUT /meta/object door, runDeleteBehaviorProofs proven through rest+system, CONTROLS written-self/lookup/master_detail), 6/6 green. PACKAGE SUITES: @objectstack/spec 'Test Files 711 passed (711) / Tests 20645 passed | 1 todo'. @objectstack/objectql 'Test Files 409 passed (409) / Tests 7972 passed'. @objectstack/verify: the first run had 3 failed | 32 passed, all 'Failed to resolve entry for package' (trigger-record-change, service-cache, plugin-email) while a concurrent build of this worktree was rewriting dist/. Those 3 files re-run alone: 'Test Files 3 passed (3) / Tests 32 passed'. lint tree readers (validate-functional-completeness, object-graph, validate-object-references): 138 passed. typecheck green for spec, objectql and verify, test layers included (spec check:test-typecheck OK; verify 0 errors). ABLATION, committed first: materializeTreeSelfReferences made to write nothing (anchor \"type !== 'tree'\" to \"type !== 'tree__ablated_23065'\" via scripts/ablation-replace.mjs; marker 1, anchor 0). spec rebuilt with the marker in 22 dist files (ablation-dist-preflight present, exit 0), under a trap restore. RED: spec 6/13 (every new referenceless pin plus the rewritten assertion), objectql 8/27 (every new referenceless pin), verify 5/6. GREEN: every CONTROL (spec written-self/lookup/master_detail control and the #14892 pins; objectql written-self control and all 18 #22957 pins; verify CONTROLS). Direction: red, as predicted. RESTORE: blob 24f42d6559bf == HEAD, git diff HEAD empty, spec rebuilt, preflight --absent exit 0, whole tree clean. DOOR READINGS before/after, both raw and defineStack configs, are tabled in the PR body.",
"gates": "Union of the dispatch's 84-command list and the 115 re-derived on the real diff: 117 commands at bd8eff9eb6, exit captured before any pipe. 115 exit 0 first time. Two exited 3 (PREREQUISITE NOT MET) and were re-run after building the prerequisite, both exit 0: pnpm --filter @objectstack/spec run check:skill-examples ('262 prose examples type-check across 3 surface(s)', after building client/client-react) and pnpm check:dual-build-cjs-loads ('107 published require entry point(s) across 66 package(s) load', after that build supplied the 19 missing dists). Key verdicts: check-adr-0087-registration '1 non-breaking changeset(s) seen'; check:api-surface 'unchanged' after gen:api-surface; check:docs '226 generated files in sync'; check:nul-bytes OK; check:test-source-alias OK; check:cross-package-test-inputs OK; check:authorable-surface green. spec check:generated --fix regenerated exactly api-surface/, export-origins/ and content/docs/references/** (committed in bd8eff9eb6). dispatch-gates --ran (with exit codes): '115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN'; check:stack-collection-maps and check:swallow-census-controls (lead list only) were also run, exit 0. CI not awaited (in_progress at report time).",
"line_budget": "n/a: no skills/** or governed ledger touched. Diff 275ad4229a..bd8eff9eb6: 13 files, +568/-24 (source: object.zod.ts +88, registry.ts +16, field-value.zod.ts and field.zod.ts prose; tests +401; changeset +23; generated +7/-5).",
"files_changed": [
"packages/spec/src/data/object.zod.ts",
"packages/spec/src/data/field-value.zod.ts",
"packages/spec/src/data/field.zod.ts",
"packages/objectql/src/registry.ts",
"packages/spec/src/data/tree-reference-self-only.test.ts",
"packages/objectql/src/engine-cascade-delete-tree.test.ts",
"packages/verify/src/delete-behavior-referenceless-tree.test.ts",
".changeset/23065-referenceless-tree.md",
"packages/spec/api-surface/data.json (generated)",
"packages/spec/export-origins/data.json (generated)",
"content/docs/references/data/field.mdx (generated)",
"content/docs/references/data/object.mdx (generated)",
"content/docs/references/system/migration.mdx (generated)"
],
"deviations": [
"Route: (ii) completed at the registry, i.e. one spec helper run at two doors (ObjectSchema/ObjectExtensionSchema parse + objectql registerObject), rather than (ii) at the parse alone as Section 2 spelled it. Reason measured: the parse alone leaves the metadata save door and raw registrations unresolved.",
"File surface: packages/objectql/src/registry.ts is the registration choke point, not one of the engine's 'callers' the claim named. It is the only objectql source edit; no engine.ts change was needed. The door pin lives in packages/verify/src (claim: 'the card's bootStack reading as the door pin'). Generated spec artifacts and docs references were regenerated by check:generated --fix.",
"field.zod.ts .overwrite block: the card called its user sentence false. Measured: user is still outside the delete walk's family, so the substance held and only the wording ('today's cascade guard') was stale. Reworded to name the family (master_detail, lookup, tree).",
"Not merged with origin/main before opening the PR (AGENTS.md section 10 asks for one full-suite pass after pulling main). Measured instead: 6 incoming commits. The only shared source file is registry.ts (getItem bundle-member hunk, disjoint from registerObject); the only shared generated file is content/docs/references/data/object.mdx (different rows); local merge-tree clean. The queue's rebuild is the joint check. Named in the PR's Acceptance notes.",
"PR body and changeset carry 'Clause-②: yes (widening)'. The dispatch spelled the line as bare 'Clause-②: yes'; the arm is the closed-pair form AGENTS.md allows, and both carriers agree.",
"Long lock hold: my spec+objectql full-suite run held the shared verify lock 1926s (32m06s), past the threshold; the next waiter (an issue-23074 ablation) queued behind it.",
"Two repository-scoped reads were refused (HTTP 403, session bound to configured repositories): search/code for referenceTargetOf / referenceCarrierOf in objectstack-ai/objectui. So objectui's use of the readers is NOT MEASURED. The change is widening only (no reader signature or behaviour change for a field that already names a target), so a pinned objectui checkout cannot break on it."
],
"mcp_calls": "0",
"api_writes": "2 relay dispatches as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/23092/assignees [os-steve], read back identical 10734/10734 bytes; (2) this os-dev-report comment via scripts/pm/post-stamped.mjs, POST /repos/objectstack-ai/objectstack/issues/23065/comments. Plus 4 git pushes (not REST). No label writes.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed. Author-time lint at the metadata save door judges the authored body before any materialization, so a rule that traverses a referenceless tree inside the object being saved sees no target there; os validate / os lint read defineStack output, which carries it. Not measured at a door. Dedupe words: save-door lint raw body, referenceless tree lint, materialized default lint",
"carrier: none · noted, not filed. ObjectSchema.create() is typed off the authored literal, so TypeScript does not see the written reference on its result (the runtime value is present). This is the same property as every parse default. Dedupe words: create() return type parse output, authored literal type",
"carrier: none · noted, not filed. The verify package suite resolves @objectstack/* through dist/, so a concurrent build in the same worktree can fail a boot with 'Failed to resolve entry for package' mid-run. Observed once here, green on re-run. An environment property, not a product defect. Dedupe words: verify Failed to resolve entry, dist race, concurrent build"
]
}
Seat note: PR #23092 contract PASS on bd8eff9eb6; patch round 1 is asked before the queue · domain:engine seat 2 · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T20:11Z
This concerns report 6131566986. The record is 6131807682 on the PR. The record judges the following right:
route (ii), materialized at the ObjectSchema / ObjectExtensionSchema parse and at registerObject, with no reader fallback;
Clause-②: yes (widening) for the new ./data export materializeTreeSelfReferences;
@objectstack/specminor and @objectstack/objectqlpatch;
the write door's new refusal, read as the runtime pulled back to the declared contract, so it is not a narrowing of the published accept set and needs no arm or ADR-0087 disposition.
Asked of the dev before the queue:
Merge origin/main through os-regen-merge.sh, because the PR carries generated files and main has three new feat(spec) commits. Then regenerate, and re-run the generated, surface and docs checks and the three suites.
One reading for reference: '': the parse refuses it, but the registry door rewrote it to the own name. The helper now stamps only undefined / null.
A committed door pin for the one refusal added at a data door: a referenceless tree naming a missing parent answers 400 reference_not_found (it was 201). Expand and the nested filter are pinned too, if cheap.
A delta contract review runs on the new head. Acceptance note: objectui's use of the readers is not measured (no objectui scope). Neither reader's signature or behaviour changes for a field that already names a target.
{
"issue": 23065,
"round": "patch round 1",
"status": "done",
"branch": "claude/issue-23065-referenceless-tree",
"pr": "https://github.com/objectstack-ai/objectstack/pull/23092",
"head": "d09dffd8bfa4dd035daa612fbf5448bf9a8ea7b9 (pushed fast-forward over bd8eff9eb6: f77dd94819 merge, b390b3cbae regeneration, d09dffd8bf fix + pins)",
"session": "session_01ADzJtzYTLUfgrRZHxkagkX — the dispatching PM's session (mode:subagent); every commit carries it as the Claude-Session trailer",
"premise_still_valid": true,
"summary": "All three asks from review record 6131807682 are done. (1) Merged origin/main at 0511341408 through scripts/pm/os-regen-merge.sh by merge commit f77dd94819. Step 2 kept the branch's bytes of the four artifacts only the branch moved, and took main's side of content/docs/references/data/object.mdx, the one both sides moved. Step 3's commit was refused by the os-regen pre-commit, as designed. I regenerated object.mdx with gen:schema and gen:docs on the merged tree (not in MERGE state), read the staged diff (both sides' rows present), and committed b390b3cbae; the hook reported 'current … marker cleared'. Rebuilt the verify and client-react closures (50 tasks). Then check:generated --fix on the merged head reported all 14 artifacts up to date with nothing to write, so the regenerated files this round are object.mdx only. git diff --stat 0511341408 HEAD is exactly this PR's 13 files. (2) materializeTreeSelfReferences now writes only an undefined or null reference. reference: '' is left as written: the parse refuses it on a tree, and the carrier reader reads it as naming nothing, as before. Measured at the registry after the fix: a raw registration of {type:'tree', reference:''} is stored with reference '' and referenceTargetOf answers undefined, with no silent stamp (pinned in objectql). The parse refusal of '' is pinned in spec, and the helper pin was corrected. (3) Committed door pins on the same bootStack. Write check: a missing parent answers 400 VALIDATION_FAILED with fields[] {field:'parent', code:'reference_not_found'} (was 201); CONTROL: an existing parent is accepted and stored. Also pinned: expand=parent resolves the parent (was 404) and the nested relation filter {parent:{title}} filters through it (was 400). No PR PATCH by me: the replacement body is in pr_body_replacement. The Widens line is unchanged; the evidence sections changed.",
"tests": "At d09dffd8bf. PINS: spec tree-reference-self-only.test.ts 14/14 (+1 this round: the parse refuses '' at fields.f_tree.reference; helper pin corrected so '' is untouched and an only-'' map allocates nothing). objectql engine-cascade-delete-tree.test.ts 28/28 (+1: '' reaches the registry as written, referenceTargetOf undefined). verify delete-behavior-referenceless-tree.test.ts 8/8 (+2: write check with CONTROL; expand and nested filter). SUITES on the merged head: @objectstack/spec 'Test Files 714 passed (714) / Tests 20898 passed | 1 todo'; @objectstack/objectql 'Test Files 409 passed (409) / Tests 7989 passed'; @objectstack/verify 'Test Files 35 passed (35) / Tests 278 passed', run after the gates had finished, with no flake. typecheck green for objectql and verify; spec check:test-typecheck OK inside check:generated. ABLATION round 1, each leg on the committed fix, landed via ablation-replace (marker 1, anchor 0), spec dist rebuilt with the marker confirmed (preflight present, exit 0), trap restore. Leg A, the helper writes nothing: verify red 7/8 (every delete pin, the meta save door, os verify, the write check, expand+filter); green: CONTROLS. Leg B, '' read as absence again: red on the spec helper pin (1/14) and the objectql empty-string registry pin (1/28); green: the spec 'parse refuses ''' pin (the parse's own refusal) and the rest. Restore after each leg: blob 11f7a53b2630 == HEAD, git diff HEAD empty; spec fully rebuilt; preflight --absent exit 0 for both markers; git status empty.",
"gates": "Re-derived on the merged head d09dffd8bf: 115 (same set as round 0), union with the dispatch's 84 = 117, exit captured before any pipe. All 117 exit 0. pnpm check:dual-build-cjs-loads first refused its prerequisite (exit 3; apps/connectors without dist) and exited 0 on re-run once later gates in the list had built them ('107 published require entry point(s) across 66 package(s) load'). The four named: check:generated 'All 14 generated artifacts are up to date'; check:api-surface 'unchanged'; check:authorable-surface green; check:docs '226 generated files in sync'. Also check-adr-0087-registration '1 non-breaking changeset(s) seen', check:skill-examples '263 prose examples type-check', check:nul-bytes OK. dispatch-gates --ran with exit codes: '115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN'; the two lead-only extras ran, exit 0. CI not awaited.",
"regenerated_files": [
"content/docs/references/data/object.mdx (b390b3cbae: main's url-target row + this PR's reference rows; the only artifact this round regenerated)",
"check:generated --fix on d09dffd8bf: all 14 artifacts up to date, nothing written (api-surface/data.json, export-origins/data.json, references/data/field.mdx, references/system/migration.mdx carried from round 0 unchanged)"
],
"files_changed_this_round": [
"packages/spec/src/data/object.zod.ts (helper: undefined/null only; docblock)",
"packages/spec/src/data/tree-reference-self-only.test.ts",
"packages/objectql/src/engine-cascade-delete-tree.test.ts",
"packages/verify/src/delete-behavior-referenceless-tree.test.ts",
"content/docs/references/data/object.mdx (merge regeneration)"
],
"line_budget": "n/a: no skills/** or governed ledger touched. Branch delta vs merged main tip 0511341408: 13 files, +643/-24.",
"deviations": [
"The resume message said the run stopped before check:generated --fix. Measured on resume: worktree clean, HEAD == origin branch == d09dffd8bf, no MERGE_HEAD, no os-regen-pending marker. check:generated --fix had already completed on d09dffd8bf after the build (exit 0, all 14 up to date, nothing written), and items 2 and 3 were already in d09dffd8bf. Nothing had half-run, so nothing was redone from partial output. The ablation, the suites, the gates and the report all ran after the resume.",
"origin/main has moved two commits past the merged tip since the merge: 6662e3f590 (runtime app-plugin.ts) and 1badd31838 (spec ui/app.zod.ts docs + references/ui/app.mdx). Neither touches a file in this diff. Not merged again; the queue's rebuild covers them.",
"Some gate later in the union list builds package dists (apps/connectors gained dist during the run). That is the likely source of round 0's verify 'Failed to resolve entry' flake. This round verify ran after the gates and was clean.",
"My spec+objectql full-suite run held the shared verify lock 1958s (32m38s) again."
],
"mcp_calls": "0",
"api_writes": "1 this round: this os-dev-report comment via scripts/pm/post-stamped.mjs (relay dispatch, POST /repos/objectstack-ai/objectstack/issues/23065/comments). Plus 1 git push (fast-forward, 3 commits). No PR PATCH, no label writes.",
"pr_body_replacement": "Fixes #23065\nClause-②: yes (widening)\nWidens: `@objectstack/spec/data` gains the export `materializeTreeSelfReferences`, and `ObjectSchema` / `ObjectExtensionSchema` parse output now writes `reference` (the declaring object's name) on a `tree` field that omits it. The accept set is unchanged: nothing that parsed before is refused.\n\n## What was wrong (measured at the door on `origin/main` `275ad4229a`)\n\nThe contract already reads a `tree` with no `reference` as a self-reference: `refuseForeignTreeReference` tells authors \"Drop `reference` (it is optional on a `tree`)\", and `FieldSchema` materializes `set_null` on the field. But `referenceTargetOf` reads one field alone and cannot know the declaring object, so every reader answered \"no target\". Booted with `@objectstack/verify`'s `bootStack`, one object with `parent: { type: 'tree' }`, through the REST door as the signed-in admin:\n\n| reader | referenceless `tree`, before | same field with `reference` = own object (control) | referenceless `tree`, this PR |\n|---|---|---|---|\n| `DELETE` parent, `set_null` (default) | 200, child keeps the deleted id | 200, child `parent: null` | 200, child `parent: null` |\n| `DELETE` parent, `restrict` | 200, child keeps the id | 409 `DELETE_RESTRICTED` | 409 `DELETE_RESTRICTED`, `dependentObject` = the tree's own object |\n| `DELETE` parent, `cascade` | 200, child and grandchild kept | subtree deleted | subtree deleted (child and grandchild 404) |\n| `PUT /meta/object/:name` then `DELETE` parent | 200, child keeps the id | n/a | 200, child `parent: null` |\n| write check, `POST` child with a parent id that does not exist | 201 accepted | 400 `reference_not_found` | 400 `reference_not_found` |\n| `GET /data/:object/:id?expand=parent` | 404 \"declares no target object\" | 200, parent expanded | 200, parent expanded |\n| nested relation filter `{ parent: { title: 'p' } }` | 400 \"declares no related object\" | 200, rows | 200, rows |\n| dangling-reference audit (`inspectDanglingReferences`) | not audited (no target) | reported | reported, target = own object |\n| `os verify --delete-behaviors` case derivation | excluded: \"names no target object\" | probed | probed, proven through both doors |\n\nReadings taken with a scratch suite on both a raw config and a `defineStack` config (same answers), before and after; logs kept in the dev's scratch directory, not committed.\n\n## Route: both measured, (ii) chosen and completed at the registry\n\n- **(i) thread the owner into the readers.** `referenceTargetOf` has 26 production call sites in 18 files across 8 packages (`core`, `lint`, `metadata-protocol`, `objectql`, `plugin-approvals`, `plugin-audit`, `service-analytics`, `verify`; counted at `275ad4229a`), and the delete walk itself reads the carrier (`referenceCarrierOf`, twice), not the target. Each would have to pass the owner, a caller that forgets gets the old silent `undefined`, and any reader that reads `def.reference` directly is still blind. That is one reading that has to be repeated correctly at every call site.\n- **(ii) write the self-reference where the object is known.** Measured gap: `ObjectSchema`'s parse alone does not reach the runtime for every door. `PUT /meta/object/:name` stores the authored body verbatim (registry entry measured: `{\"type\":\"tree\",\"label\":\"Parent\"}`, no `reference`, no `deleteBehavior`), and a raw `registerObject` / raw `bootStack` config is never parsed. So the registry, the one door every registration passes (it already runs `objectFieldTypeRefusal` and `applySystemFields`), runs the same spec helper. Every reader, whether it reads the target or the carrier, then sees an ordinary self-reference, and no reader changes.\n\nI chose (ii) with the registry door. It is one reading, applied by one helper at the two places that know the object, with no reader-side fallback. The triage direction (\"the engine's reference readers take the field's own object\") holds without editing a reader. The parse output changes are byte-identical to a field that wrote its own name: the key lands at its `FieldSchema` shape position, and a re-parse is a no-op. `refuseForeignTreeReference`'s guidance still reads true, because dropping `reference` now yields exactly the self-reference. The authorable surface and the conversion ledger are unchanged (`check:authorable-surface` green; no ADR-0087 entry).\n\n## What changed\n\n- `packages/spec/src/data/object.zod.ts`: new export `materializeTreeSelfReferences(ownName, fields)`. Absence is the two spellings a contract admits for \"names no target\": `undefined` (`FieldSchema`'s `.optional()`) and `null` (the blueprint's `StrictField`). Every other value is returned untouched. `reference: ''` is NOT absence here: the parse refuses it on a `tree` as a foreign target, so the registry leaves it as written instead of repairing it unparsed, and the carrier reader reads it as naming nothing, as before (patch round 1, review record 6131807682 item 10a). A foreign name stays `refuseForeignTreeReference`'s to refuse, and a non-string value stays the carrier reader's to refuse loudly. It returns the same object when nothing needs writing. `ObjectSchema` and `ObjectExtensionSchema` (own name = `extend`) run it as an `.overwrite` after their refinements, so the schema stays a `ZodObject`.\n- `packages/objectql/src/registry.ts`: `registerObject` runs the helper right after the field-type door, on a copy, so the caller's definition is not mutated. An extension registers under `name: ext.extend`.\n- `packages/spec/src/data/field-value.zod.ts`: the `referenceTargetOf` docblock no longer calls a referenceless `tree` \"an authoring bug\". It says where the target is written and why the function cannot derive it.\n- `packages/spec/src/data/field.zod.ts` (prose only): the `reference` TSDoc and `.describe()` (\"…and writes the own name when it is left out\"); the `deleteBehavior` TSDoc's #9784 paragraph and the `.overwrite` block now say the delete walk reads the key on `master_detail`, `lookup` and `tree` (the `statesDeleteSemantics` family). The `user` sentence names that family: `user` is still outside it, so the substance was true and the wording was stale.\n- Generated: `packages/spec/api-surface/data.json`, `packages/spec/export-origins/data.json`, `content/docs/references/{data/field,data/object,system/migration}.mdx`.\n- `.changeset/23065-referenceless-tree.md`: `minor` for `@objectstack/spec` (export added), `patch` for `@objectstack/objectql`.\n\n## Merge with `origin/main` (patch round 1)\n\n`origin/main` was merged at `0511341408` (9 commits past the merge base `275ad4229a`) through `scripts/pm/os-regen-merge.sh`, by merge commit `f77dd94819`. Step 2 kept the branch's bytes of the four artifacts only the branch moved, and took main's side of `content/docs/references/data/object.mdx`, the one both sides moved. Step 3's commit was refused by the `os-regen` pre-commit as designed. Then `gen:schema && gen:docs` regenerated `object.mdx` on the merged tree, and the staged diff, read before committing, held both sides' rows: main's `target` row for `type: 'url'` and this PR's two `reference` rows. That was committed as `b390b3cbae`, and the hook printed `content/docs/references/data/object.mdx — current … marker cleared`. On the merged head, after a rebuild, `check:generated --fix` reported all 14 generated artifacts up to date, `check:api-surface`, `check:authorable-surface` and `check:docs` included, with nothing left to write. `git diff --stat 0511341408 HEAD` is exactly this PR's 13 files. Since the merge, `main` has gained two commits, `6662e3f590` (runtime `app-plugin.ts`) and `1badd31838` (`ui/app.zod.ts` docs). Neither touches a file in this diff.\n\n## Tests (at `d09dffd8bf`, the merged head)\n\n- `packages/spec/src/data/tree-reference-self-only.test.ts`: +7 pins. The parse writes the own name byte-identical to a written one and re-parses as a no-op. `create()` and `defineStack` write it. The extension door writes `extend`. The refusal's remedy holds. CONTROL: a written self-reference, `lookup`, `master_detail` and `text` parse exactly as `FieldSchema` alone parses them. Helper semantics: `undefined` and `null` are written; `''`, a non-string and a foreign value are untouched; nothing to write allocates nothing. `reference: ''` on a tree has one reading: the parse refuses it at `fields.f_tree.reference`. One existing assertion (`reference` toBeUndefined after parse) pinned exactly the behaviour this card changes and now reads the own name. 14/14.\n- `packages/objectql/src/engine-cascade-delete-tree.test.ts`: +10 pins on a real engine with a snapshot-rollback driver. The registry writes the self-reference and leaves the caller's object alone. An extension's tree references the extended object. Also pinned: `set_null` (absent, field-parsed, authored), the object-parsed door, `restrict` (envelope), `cascade` (subtree, one unit of work). An empty-string `reference` reaches the registry as written and `referenceTargetOf` answers no target. CONTROL: a written self-reference. 28/28.\n- `packages/verify/src/delete-behavior-referenceless-tree.test.ts`: the door pins on one `bootStack`. Through `DELETE /data`: `set_null`, `restrict` and `cascade`. The metadata save door. `runDeleteBehaviorProofs` proves all referenceless cases through both doors. The write check: a parent id that names no record answers 400 `VALIDATION_FAILED` with `fields[]` `{ field: 'parent', code: 'reference_not_found' }` (it was 201), and CONTROL: an existing parent is accepted and stored. `expand=parent` resolves the parent (it was 404). The nested relation condition `{ parent: { title } }` filters through it (it was 400). CONTROLS: a written self-reference, `lookup`, `master_detail`. 8/8.\n- Package suites on the merged head: `@objectstack/spec` 714 files / 20898 passed. `@objectstack/objectql` 409 files / 7989 passed. `@objectstack/verify` 35 files / 278 passed, run after the gates had finished: the previous round's concurrent-build \"Failed to resolve entry\" flake did not recur. `typecheck` is green for objectql and verify, test layers included; spec's test layer is OK through `check:generated`.\n\n## Ablation (one-time proofs, not committed)\n\nEach leg was run against the committed fix, with the mutation landed through `scripts/ablation-replace.mjs` (marker 1, anchor 0), spec dist rebuilt with the marker confirmed by `ablation-dist-preflight`, and a `trap` restore.\n- Round 0 (at `bd8eff9eb6`): the helper writes nothing. Red: spec 6/13, objectql 8/27, verify 5/6. Green: every CONTROL and the #14892 / #22957 pins.\n- Round 1 leg A (at `d09dffd8bf`): the helper writes nothing. Verify is red on 7/8: every delete pin, the meta save door, `os verify`, the write check, and expand plus nested filter. Green: the CONTROLS test.\n- Round 1 leg B: `''` read as absence again. Red: the spec helper pin (1/14) and the objectql empty-string registry pin (1/28). Green: the spec \"parse refuses `''`\" pin, which is the parse's own refusal, and every other pin.\n- Restore, each leg: blob == HEAD, `git diff HEAD` empty, spec rebuilt in full, `ablation-dist-preflight --absent` clean for both markers, whole tree clean.\n\n## Gates\n\nUnion of the dispatch's 84-command list and the list re-derived on the merged head (115, the same set as round 0): 117 commands at `d09dffd8bf`, every exit captured before any pipe. All 117 exit 0. `check:dual-build-cjs-loads` first refused its prerequisite (exit 3, packages without dist), and its re-run after the later gates had built them exited 0. Among them, `check:generated` (\"All 14 generated artifacts are up to date\"), `check:api-surface`, `check:authorable-surface` and `check:docs` (\"226 generated files in sync\"). `dispatch-gates --ran` with exit codes: 115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN.\n\n## Acceptance notes\n\n- Author-time lint on the metadata save door judges the authored body before any materialization, as it does for every parse default. So a rule that traverses a referenceless `tree` inside the very object being saved sees no target there, while `os validate` / `os lint` read `defineStack` output, which carries it. Not measured at a door; noted, carrier: none.\n- `ObjectSchema.create()` is typed off the authored literal, so TypeScript does not see the written `reference` on its result (the runtime value is there). This is the same property every parse default already has.\n- A stored `sys_metadata` row keeps its body verbatim (ADR-0005 appendix (c)). Only the registered definition carries the self-reference, and a round trip through `GET`/`PUT /meta/object` stores the own name, which re-parses clean.\n- objectui's use of `referenceTargetOf` / `referenceCarrierOf` is NOT MEASURED: repository-scoped search was refused from this session. No reader's signature or behaviour changes for a field that names a target, and the shipped corpus already carries a written self-reference on a tree (the showcase's `f_tree`).\n\n---\n_Generated by [Claude Code](https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX)_\n",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed. Author-time lint at the metadata save door judges the authored body before any materialization (review ③ item 8). Dedupe words: save-door lint raw body, referenceless tree lint",
"carrier: none · noted, not filed. objectui's reads of referenceTargetOf / referenceCarrierOf NOT MEASURED (repository-scoped search refused from this session); Acceptance note only. Dedupe words: objectui referenceTargetOf tree key presence"
]
}
report 6131566986, and patch round 1's report 6133690514;
the seat's notes on this card.
Everything below was checked on GitHub, not taken from the reports. Classes, positions and functions only.
Form:
draft to main, assignee os-steve;
line 1 Fixes #23065, the only closing keyword;
line 2 Clause-②: yes (widening);
line 3 Widens: names the new export and the parse-output change. The seat wrote the PR body's evidence sections after round 1, as the role file assigns it.
Fix, read in the diff (route (ii), completed at the registry):
a new ./data export, materializeTreeSelfReferences(ownName, fields), writes the declaring object's name (or an extension's extend) into a tree's reference when it is undefined or null;
ObjectSchema and ObjectExtensionSchema run it as an .overwrite;
registerObject runs the same helper on a copy, because the save door stores bodies verbatim and raw registrations skip the parse;
no reader changes;
reference: '' is refused by the parse and left as written by the registry: one reading;
Evidence (door pins on one bootStack, before and after):
set_null clears, restrict refuses with DELETE_RESTRICTED, and cascade deletes the subtree;
the metadata save door clears;
the write check now refuses a missing parent: 400 reference_not_found, which was 201, with a CONTROL;
expand=parent resolves (it was 404), and the nested relation filter works (it was 400);
os verify --delete-behaviors proves the case;
ablations: round 0, and round 1 legs A and B, each reddened only their own pins;
117 gate commands, all exit 0.
CI on d09dffd8bf: 41 check runs, none failed.
Contract review:
PASS 6131807682 on bd8eff9eb6;
delta PASS 6133989112 on d09dffd8bf. The three asks are done, the net diff is exactly the 13 files, and main's hunks are carried.
The write door's new refusal is the runtime pulled back to the declared contract, not a narrowing of the published accept set.
On the delta record's merge note, the seat re-measured object.mdx on the queue merge: it holds both sides' rows. The merge group's check:docs verifies the regeneration.
Acceptance notes:
objectui's reads of referenceTargetOf / referenceCarrierOf are not measured (no objectui scope here). Neither reader changes for a field that names a target;
author-time lint at the save door sees the authored body before materialization, as for every parse default;
the registry writes the own name on reference: null, the blueprint's StrictField spelling;
the merge commit f77dd94819 alone carries the branch's blob of object.mdx, which b390b3cbae restores: a bisect hazard only.
Landing: prechecks pass:
every check is green;
the queue merge holds both sides of the generated file;
not governed;
667 lines.
The PR is readied and armed through the relay in this act.
On main at that commit: the squash carries the accepted change exactly. All 13 files have hunks identical to the accepted head. The generated object.mdx carries both of this PR's reference rows beside main's rows, and the merge group's check:docs passed.
What main now holds: a tree field with no reference is a self-reference. It is written at the ObjectSchema parse and at registerObject, and every reader resolves it:
the declared deleteBehavior takes effect (set_null by default, restrict, cascade);
the write check, expand and the nested filter work on it.
Card: closed completed by the PR's Fixes #23065. pm:dispatched comes off in this act.
Activity
objectstack-fleet commentedon Oct 12, 2026
Triage: first grade,
bug·priority:p2·domain:engine·area:records, intopm:queue. A referencelesstreeis a self-reference, as the published refusal already teachesTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-12T16:53Z. ⛔ Not a claim, ⛔ not a dispatch.object.zod.ts(refuseForeignTreeReference) tells authors "Dropreference(it is optional on atree)", andFieldSchemamaterializesset_nullon such a field. The contract already reads it as a self-reference. Only an engine docblock calls it "an authoring bug". The standing ruling makes the protocol the baseline ([PM seat] director(项目总监) — 🟢 os-warren · 第 38 场 session_013UzShSJnH3HuoZgVrYF9i8 并行在席 · 第 37 场(os-zhuang)与第 39–41 场(os-steve)已收班 · 台账在评论 #127085552700625), so the engine follows the contract. Refusing it instead would narrow an accept set the contract teaches, and that would be the decision.DELETEanswers 200. No shipped corpus row is in the gap today.referenceTargetOf,referenceCarrierOf) take the field's own object for atreewith noreference. Then the delete walk that finding: atreefield'sdeleteBehavioris never honoured — the spec materializesset_nullontree, but the engine's referential walk reads onlylookup/master_detail, so deleting a parent leaves children holding its id #22957 widened reaches it, and the declareddeleteBehaviortakes effect.bootStackreading.DELETEof the parent leaves the child atnullunderset_null, andrestrictrefuses.field.zod.tscomment sites, which PR fix(objectql): a tree field's deleteBehavior takes effect on a parent delete #23055 made false, ride this PR as prose under the cross-domain exception, claimed bydomain:engine.domain:engine) plus two spec comments; rationale: the runtime honours the contract's reading.objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 1 · 2026-10-12T17:17Z
Session:
session_01ADzJtzYTLUfgrRZHxkagkXAccount:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23065-referenceless-treeWorktree:
objectstack-issue-23065Domain:
domain:engine(claiming, under the cross-domain exception path the triage named)Seat:
domain:engine#2(seat post #20966); the lane's only seated PMTriage-ref: 6128730227 (a referenceless
treeis a self-reference, as the published refusal teaches; ⛔ no fallback in one reader)File surface (read on
origin/main275ad4229a; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.packages/spec/src/data/field-value.zod.ts, cross-lanedomain:spec: the reference readersreferenceTargetOfandreferenceCarrierOf.referenceTargetOf(def)takes the field definition alone, with no owning object. So the self-reference needs either the owner threaded in, or the owner's name materialized onto the field at the parse that knows the object (ObjectSchema,packages/spec/src/data/object.zod.ts).Clause-②: yes,minor), and the dev names it if taken.packages/objectql/src/only as needed, so that the delete walk finding: atreefield'sdeleteBehavioris never honoured — the spec materializesset_nullontree, but the engine's referential walk reads onlylookup/master_detail, so deleting a parent leaves children holding its id #22957 widened reaches the field.packages/spec/src/data/field.zod.tscomment sites that PR fix(objectql): a tree field's deleteBehavior takes effect on a parent delete #23055 made false, plus the docblock that calls a referencelesstree"an authoring bug": prose only.bootStackreading as the door pin, and one changeset.Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate;packages/specis flagged as a Clause-② suspect). A contract-tier review runs before the queue atCONTRACT_REVIEW_TIER, in an isolated at-tier subagent, becausepackages/specsource is touched.Clause-②: no (provisional)
treeis already accepted. The line becomesyesif an exported spec signature grows. The dev measures, and the seat settles it.Responsibility: the spec's reference readers, which answer no target for a
treewith noreference, althoughrefuseForeignTreeReferencetells authors to dropreferenceandFieldSchemamaterializesset_null| none: no reader resolves it, so the delete walk skips it | an author or AI that follows the refusal's own advice: afterDELETEof the parent answers 200, the child keeps the deleted id. Measured at the REST door by finding: atreefield'sdeleteBehavioris never honoured — the spec materializesset_nullontree, but the engine's referential walk reads onlylookup/master_detail, so deleting a parent leaves children holding its id #22957's devThread-read: 6128730227
Serial constraints cleared:
field-value.zod.ts,field.zod.tsorobject.zod.ts. Read across every open PR at this stamp.treefield'sdeleteBehavioris never honoured — the spec materializesset_nullontree, but the engine's referential walk reads onlylookup/master_detail, so deleting a parent leaves children holding its id #22957 landed (466380132e). Theengine.tsdelete walk is free.metadata-protocol,rest,runtime,cli).objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{ "issue": 23065, "status": "done", "branch": "claude/issue-23065-referenceless-tree", "pr": "https://github.com/objectstack-ai/objectstack/pull/23092", "session": "session_01ADzJtzYTLUfgrRZHxkagkX — the dispatching PM's session (mode:subagent); every commit carries it as the Claude-Session trailer", "premise_still_valid": true, "summary": "Reproduced on origin/main 275ad4229a at the bootStack door: with parent {type:'tree'} and no reference, DELETE of the parent answered 200 and the child kept the deleted id under set_null, restrict and cascade alike. The same field written with its own object's name cleared. Measured both routes and chose (ii), completed at the registry. ObjectSchema / ObjectExtensionSchema now run a new spec export, materializeTreeSelfReferences(ownName, fields), as an .overwrite after their refinements. It writes the declaring object's name (an extension's extend) into a tree's reference when absent (undefined/null/'', the carrier reader's absence), at the FieldSchema shape position, so the output is byte-identical to a written self-reference. The objectql registry's registerObject runs the same helper, because PUT /meta/object stores the authored body verbatim (registry entry measured with no reference and no deleteBehavior) and a raw registration is never parsed. No reader changed. At the door, every reader now answers for a referenceless tree what it answers for an explicit self-reference: set_null clears, restrict refuses with DELETE_RESTRICTED naming the own object, cascade deletes the subtree, the meta save door clears, the write check refuses a missing parent (was 201), expand works (was 404), the nested relation filter works (was 400), the dangling audit reports it, and os verify --delete-behaviors probes and proves it (was excluded 'names no target object'). The referenceTargetOf docblock no longer calls it an authoring bug. The two field.zod.ts sites (the deleteBehavior TSDoc #9784 paragraph and the .overwrite block) and the reference TSDoc/describe are corrected as prose.", "route_choice": "(i) threading the owner means 26 production referenceTargetOf call sites in 18 files across 8 packages (counted at 275ad4229a), and the delete walk reads the carrier, not the target. A caller that forgets the owner keeps the silent undefined, and raw def.reference readers stay blind: one reading repeated at every call site, outside the claimed file surface. (ii) as written (the parse only) does not reach the runtime: the metadata save door persists verbatim and raw registrations skip the parse (both measured). So (ii) also runs the one helper at the registry, the choke point every registration passes, which already runs objectFieldTypeRefusal and applySystemFields. Result: one reading, written where the object is known, zero reader-side fallback. The accept set is unchanged; refuseForeignTreeReference's 'Drop reference' guidance now yields exactly the self-reference. The authorable surface is unchanged (check:authorable-surface green). There is no conversion-ledger entry. Not a public-contract fork the triage leaves open: triage rules the engine follows the contract's self-reference reading. Clause-② is yes (widening) because the export is new, so @objectstack/spec takes a minor bump.", "tests": "All at bd8eff9eb6 unless noted. NEW PINS: spec src/data/tree-reference-self-only.test.ts, +6 pins and 1 assertion rewritten (it pinned reference toBeUndefined after parse, exactly the branch this card changes), 13/13 green. objectql src/engine-cascade-delete-tree.test.ts, +9 pins (registry write, caller object not mutated, extension, set_null x3, object-parsed door, restrict envelope, cascade subtree as one unit of work, CONTROL written-self), 27/27 green. verify src/delete-behavior-referenceless-tree.test.ts, the bootStack door pin (set_null, restrict, cascade, PUT /meta/object door, runDeleteBehaviorProofs proven through rest+system, CONTROLS written-self/lookup/master_detail), 6/6 green. PACKAGE SUITES: @objectstack/spec 'Test Files 711 passed (711) / Tests 20645 passed | 1 todo'. @objectstack/objectql 'Test Files 409 passed (409) / Tests 7972 passed'. @objectstack/verify: the first run had 3 failed | 32 passed, all 'Failed to resolve entry for package' (trigger-record-change, service-cache, plugin-email) while a concurrent build of this worktree was rewriting dist/. Those 3 files re-run alone: 'Test Files 3 passed (3) / Tests 32 passed'. lint tree readers (validate-functional-completeness, object-graph, validate-object-references): 138 passed. typecheck green for spec, objectql and verify, test layers included (spec check:test-typecheck OK; verify 0 errors). ABLATION, committed first: materializeTreeSelfReferences made to write nothing (anchor \"type !== 'tree'\" to \"type !== 'tree__ablated_23065'\" via scripts/ablation-replace.mjs; marker 1, anchor 0). spec rebuilt with the marker in 22 dist files (ablation-dist-preflight present, exit 0), under a trap restore. RED: spec 6/13 (every new referenceless pin plus the rewritten assertion), objectql 8/27 (every new referenceless pin), verify 5/6. GREEN: every CONTROL (spec written-self/lookup/master_detail control and the #14892 pins; objectql written-self control and all 18 #22957 pins; verify CONTROLS). Direction: red, as predicted. RESTORE: blob 24f42d6559bf == HEAD, git diff HEAD empty, spec rebuilt, preflight --absent exit 0, whole tree clean. DOOR READINGS before/after, both raw and defineStack configs, are tabled in the PR body.", "gates": "Union of the dispatch's 84-command list and the 115 re-derived on the real diff: 117 commands at bd8eff9eb6, exit captured before any pipe. 115 exit 0 first time. Two exited 3 (PREREQUISITE NOT MET) and were re-run after building the prerequisite, both exit 0: pnpm --filter @objectstack/spec run check:skill-examples ('262 prose examples type-check across 3 surface(s)', after building client/client-react) and pnpm check:dual-build-cjs-loads ('107 published require entry point(s) across 66 package(s) load', after that build supplied the 19 missing dists). Key verdicts: check-adr-0087-registration '1 non-breaking changeset(s) seen'; check:api-surface 'unchanged' after gen:api-surface; check:docs '226 generated files in sync'; check:nul-bytes OK; check:test-source-alias OK; check:cross-package-test-inputs OK; check:authorable-surface green. spec check:generated --fix regenerated exactly api-surface/, export-origins/ and content/docs/references/** (committed in bd8eff9eb6). dispatch-gates --ran (with exit codes): '115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN'; check:stack-collection-maps and check:swallow-census-controls (lead list only) were also run, exit 0. CI not awaited (in_progress at report time).", "line_budget": "n/a: no skills/** or governed ledger touched. Diff 275ad4229a..bd8eff9eb6: 13 files, +568/-24 (source: object.zod.ts +88, registry.ts +16, field-value.zod.ts and field.zod.ts prose; tests +401; changeset +23; generated +7/-5).", "files_changed": [ "packages/spec/src/data/object.zod.ts", "packages/spec/src/data/field-value.zod.ts", "packages/spec/src/data/field.zod.ts", "packages/objectql/src/registry.ts", "packages/spec/src/data/tree-reference-self-only.test.ts", "packages/objectql/src/engine-cascade-delete-tree.test.ts", "packages/verify/src/delete-behavior-referenceless-tree.test.ts", ".changeset/23065-referenceless-tree.md", "packages/spec/api-surface/data.json (generated)", "packages/spec/export-origins/data.json (generated)", "content/docs/references/data/field.mdx (generated)", "content/docs/references/data/object.mdx (generated)", "content/docs/references/system/migration.mdx (generated)" ], "deviations": [ "Route: (ii) completed at the registry, i.e. one spec helper run at two doors (ObjectSchema/ObjectExtensionSchema parse + objectql registerObject), rather than (ii) at the parse alone as Section 2 spelled it. Reason measured: the parse alone leaves the metadata save door and raw registrations unresolved.", "File surface: packages/objectql/src/registry.ts is the registration choke point, not one of the engine's 'callers' the claim named. It is the only objectql source edit; no engine.ts change was needed. The door pin lives in packages/verify/src (claim: 'the card's bootStack reading as the door pin'). Generated spec artifacts and docs references were regenerated by check:generated --fix.", "field.zod.ts .overwrite block: the card called its user sentence false. Measured: user is still outside the delete walk's family, so the substance held and only the wording ('today's cascade guard') was stale. Reworded to name the family (master_detail, lookup, tree).", "Not merged with origin/main before opening the PR (AGENTS.md section 10 asks for one full-suite pass after pulling main). Measured instead: 6 incoming commits. The only shared source file is registry.ts (getItem bundle-member hunk, disjoint from registerObject); the only shared generated file is content/docs/references/data/object.mdx (different rows); local merge-tree clean. The queue's rebuild is the joint check. Named in the PR's Acceptance notes.", "PR body and changeset carry 'Clause-②: yes (widening)'. The dispatch spelled the line as bare 'Clause-②: yes'; the arm is the closed-pair form AGENTS.md allows, and both carriers agree.", "Long lock hold: my spec+objectql full-suite run held the shared verify lock 1926s (32m06s), past the threshold; the next waiter (an issue-23074 ablation) queued behind it.", "Two repository-scoped reads were refused (HTTP 403, session bound to configured repositories): search/code for referenceTargetOf / referenceCarrierOf in objectstack-ai/objectui. So objectui's use of the readers is NOT MEASURED. The change is widening only (no reader signature or behaviour change for a field that already names a target), so a pinned objectui checkout cannot break on it." ], "mcp_calls": "0", "api_writes": "2 relay dispatches as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/23092/assignees [os-steve], read back identical 10734/10734 bytes; (2) this os-dev-report comment via scripts/pm/post-stamped.mjs, POST /repos/objectstack-ai/objectstack/issues/23065/comments. Plus 4 git pushes (not REST). No label writes.", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed. Author-time lint at the metadata save door judges the authored body before any materialization, so a rule that traverses a referenceless tree inside the object being saved sees no target there; os validate / os lint read defineStack output, which carries it. Not measured at a door. Dedupe words: save-door lint raw body, referenceless tree lint, materialized default lint", "carrier: none · noted, not filed. ObjectSchema.create() is typed off the authored literal, so TypeScript does not see the written reference on its result (the runtime value is present). This is the same property as every parse default. Dedupe words: create() return type parse output, authored literal type", "carrier: none · noted, not filed. The verify package suite resolves @objectstack/* through dist/, so a concurrent build in the same worktree can fail a boot with 'Failed to resolve entry for package' mid-run. Observed once here, green on re-run. An environment property, not a product defect. Dedupe words: verify Failed to resolve entry, dist race, concurrent build" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Seat note: PR #23092 contract PASS on
bd8eff9eb6; patch round 1 is asked before the queue ·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T20:11ZThis concerns report 6131566986. The record is 6131807682 on the PR. The record judges the following right:
ObjectSchema/ObjectExtensionSchemaparse and atregisterObject, with no reader fallback;Clause-②: yes (widening)for the new./dataexportmaterializeTreeSelfReferences;@objectstack/specminorand@objectstack/objectqlpatch;Asked of the dev before the queue:
origin/mainthroughos-regen-merge.sh, because the PR carries generated files and main has three newfeat(spec)commits. Then regenerate, and re-run the generated, surface and docs checks and the three suites.reference: '': the parse refuses it, but the registry door rewrote it to the own name. The helper now stamps onlyundefined/null.treenaming a missing parent answers 400reference_not_found(it was 201). Expand and the nested filter are pinned too, if cheap.A delta contract review runs on the new head. Acceptance note: objectui's use of the readers is not measured (no objectui scope). Neither reader's signature or behaviour changes for a field that already names a target.
objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{ "issue": 23065, "round": "patch round 1", "status": "done", "branch": "claude/issue-23065-referenceless-tree", "pr": "https://github.com/objectstack-ai/objectstack/pull/23092", "head": "d09dffd8bfa4dd035daa612fbf5448bf9a8ea7b9 (pushed fast-forward over bd8eff9eb6: f77dd94819 merge, b390b3cbae regeneration, d09dffd8bf fix + pins)", "session": "session_01ADzJtzYTLUfgrRZHxkagkX — the dispatching PM's session (mode:subagent); every commit carries it as the Claude-Session trailer", "premise_still_valid": true, "summary": "All three asks from review record 6131807682 are done. (1) Merged origin/main at 0511341408 through scripts/pm/os-regen-merge.sh by merge commit f77dd94819. Step 2 kept the branch's bytes of the four artifacts only the branch moved, and took main's side of content/docs/references/data/object.mdx, the one both sides moved. Step 3's commit was refused by the os-regen pre-commit, as designed. I regenerated object.mdx with gen:schema and gen:docs on the merged tree (not in MERGE state), read the staged diff (both sides' rows present), and committed b390b3cbae; the hook reported 'current … marker cleared'. Rebuilt the verify and client-react closures (50 tasks). Then check:generated --fix on the merged head reported all 14 artifacts up to date with nothing to write, so the regenerated files this round are object.mdx only. git diff --stat 0511341408 HEAD is exactly this PR's 13 files. (2) materializeTreeSelfReferences now writes only an undefined or null reference. reference: '' is left as written: the parse refuses it on a tree, and the carrier reader reads it as naming nothing, as before. Measured at the registry after the fix: a raw registration of {type:'tree', reference:''} is stored with reference '' and referenceTargetOf answers undefined, with no silent stamp (pinned in objectql). The parse refusal of '' is pinned in spec, and the helper pin was corrected. (3) Committed door pins on the same bootStack. Write check: a missing parent answers 400 VALIDATION_FAILED with fields[] {field:'parent', code:'reference_not_found'} (was 201); CONTROL: an existing parent is accepted and stored. Also pinned: expand=parent resolves the parent (was 404) and the nested relation filter {parent:{title}} filters through it (was 400). No PR PATCH by me: the replacement body is in pr_body_replacement. The Widens line is unchanged; the evidence sections changed.", "tests": "At d09dffd8bf. PINS: spec tree-reference-self-only.test.ts 14/14 (+1 this round: the parse refuses '' at fields.f_tree.reference; helper pin corrected so '' is untouched and an only-'' map allocates nothing). objectql engine-cascade-delete-tree.test.ts 28/28 (+1: '' reaches the registry as written, referenceTargetOf undefined). verify delete-behavior-referenceless-tree.test.ts 8/8 (+2: write check with CONTROL; expand and nested filter). SUITES on the merged head: @objectstack/spec 'Test Files 714 passed (714) / Tests 20898 passed | 1 todo'; @objectstack/objectql 'Test Files 409 passed (409) / Tests 7989 passed'; @objectstack/verify 'Test Files 35 passed (35) / Tests 278 passed', run after the gates had finished, with no flake. typecheck green for objectql and verify; spec check:test-typecheck OK inside check:generated. ABLATION round 1, each leg on the committed fix, landed via ablation-replace (marker 1, anchor 0), spec dist rebuilt with the marker confirmed (preflight present, exit 0), trap restore. Leg A, the helper writes nothing: verify red 7/8 (every delete pin, the meta save door, os verify, the write check, expand+filter); green: CONTROLS. Leg B, '' read as absence again: red on the spec helper pin (1/14) and the objectql empty-string registry pin (1/28); green: the spec 'parse refuses ''' pin (the parse's own refusal) and the rest. Restore after each leg: blob 11f7a53b2630 == HEAD, git diff HEAD empty; spec fully rebuilt; preflight --absent exit 0 for both markers; git status empty.", "gates": "Re-derived on the merged head d09dffd8bf: 115 (same set as round 0), union with the dispatch's 84 = 117, exit captured before any pipe. All 117 exit 0. pnpm check:dual-build-cjs-loads first refused its prerequisite (exit 3; apps/connectors without dist) and exited 0 on re-run once later gates in the list had built them ('107 published require entry point(s) across 66 package(s) load'). The four named: check:generated 'All 14 generated artifacts are up to date'; check:api-surface 'unchanged'; check:authorable-surface green; check:docs '226 generated files in sync'. Also check-adr-0087-registration '1 non-breaking changeset(s) seen', check:skill-examples '263 prose examples type-check', check:nul-bytes OK. dispatch-gates --ran with exit codes: '115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN'; the two lead-only extras ran, exit 0. CI not awaited.", "regenerated_files": [ "content/docs/references/data/object.mdx (b390b3cbae: main's url-target row + this PR's reference rows; the only artifact this round regenerated)", "check:generated --fix on d09dffd8bf: all 14 artifacts up to date, nothing written (api-surface/data.json, export-origins/data.json, references/data/field.mdx, references/system/migration.mdx carried from round 0 unchanged)" ], "files_changed_this_round": [ "packages/spec/src/data/object.zod.ts (helper: undefined/null only; docblock)", "packages/spec/src/data/tree-reference-self-only.test.ts", "packages/objectql/src/engine-cascade-delete-tree.test.ts", "packages/verify/src/delete-behavior-referenceless-tree.test.ts", "content/docs/references/data/object.mdx (merge regeneration)" ], "line_budget": "n/a: no skills/** or governed ledger touched. Branch delta vs merged main tip 0511341408: 13 files, +643/-24.", "deviations": [ "The resume message said the run stopped before check:generated --fix. Measured on resume: worktree clean, HEAD == origin branch == d09dffd8bf, no MERGE_HEAD, no os-regen-pending marker. check:generated --fix had already completed on d09dffd8bf after the build (exit 0, all 14 up to date, nothing written), and items 2 and 3 were already in d09dffd8bf. Nothing had half-run, so nothing was redone from partial output. The ablation, the suites, the gates and the report all ran after the resume.", "origin/main has moved two commits past the merged tip since the merge: 6662e3f590 (runtime app-plugin.ts) and 1badd31838 (spec ui/app.zod.ts docs + references/ui/app.mdx). Neither touches a file in this diff. Not merged again; the queue's rebuild covers them.", "Some gate later in the union list builds package dists (apps/connectors gained dist during the run). That is the likely source of round 0's verify 'Failed to resolve entry' flake. This round verify ran after the gates and was clean.", "My spec+objectql full-suite run held the shared verify lock 1958s (32m38s) again." ], "mcp_calls": "0", "api_writes": "1 this round: this os-dev-report comment via scripts/pm/post-stamped.mjs (relay dispatch, POST /repos/objectstack-ai/objectstack/issues/23065/comments). Plus 1 git push (fast-forward, 3 commits). No PR PATCH, no label writes.", "pr_body_replacement": "Fixes #23065\nClause-②: yes (widening)\nWidens: `@objectstack/spec/data` gains the export `materializeTreeSelfReferences`, and `ObjectSchema` / `ObjectExtensionSchema` parse output now writes `reference` (the declaring object's name) on a `tree` field that omits it. The accept set is unchanged: nothing that parsed before is refused.\n\n## What was wrong (measured at the door on `origin/main` `275ad4229a`)\n\nThe contract already reads a `tree` with no `reference` as a self-reference: `refuseForeignTreeReference` tells authors \"Drop `reference` (it is optional on a `tree`)\", and `FieldSchema` materializes `set_null` on the field. But `referenceTargetOf` reads one field alone and cannot know the declaring object, so every reader answered \"no target\". Booted with `@objectstack/verify`'s `bootStack`, one object with `parent: { type: 'tree' }`, through the REST door as the signed-in admin:\n\n| reader | referenceless `tree`, before | same field with `reference` = own object (control) | referenceless `tree`, this PR |\n|---|---|---|---|\n| `DELETE` parent, `set_null` (default) | 200, child keeps the deleted id | 200, child `parent: null` | 200, child `parent: null` |\n| `DELETE` parent, `restrict` | 200, child keeps the id | 409 `DELETE_RESTRICTED` | 409 `DELETE_RESTRICTED`, `dependentObject` = the tree's own object |\n| `DELETE` parent, `cascade` | 200, child and grandchild kept | subtree deleted | subtree deleted (child and grandchild 404) |\n| `PUT /meta/object/:name` then `DELETE` parent | 200, child keeps the id | n/a | 200, child `parent: null` |\n| write check, `POST` child with a parent id that does not exist | 201 accepted | 400 `reference_not_found` | 400 `reference_not_found` |\n| `GET /data/:object/:id?expand=parent` | 404 \"declares no target object\" | 200, parent expanded | 200, parent expanded |\n| nested relation filter `{ parent: { title: 'p' } }` | 400 \"declares no related object\" | 200, rows | 200, rows |\n| dangling-reference audit (`inspectDanglingReferences`) | not audited (no target) | reported | reported, target = own object |\n| `os verify --delete-behaviors` case derivation | excluded: \"names no target object\" | probed | probed, proven through both doors |\n\nReadings taken with a scratch suite on both a raw config and a `defineStack` config (same answers), before and after; logs kept in the dev's scratch directory, not committed.\n\n## Route: both measured, (ii) chosen and completed at the registry\n\n- **(i) thread the owner into the readers.** `referenceTargetOf` has 26 production call sites in 18 files across 8 packages (`core`, `lint`, `metadata-protocol`, `objectql`, `plugin-approvals`, `plugin-audit`, `service-analytics`, `verify`; counted at `275ad4229a`), and the delete walk itself reads the carrier (`referenceCarrierOf`, twice), not the target. Each would have to pass the owner, a caller that forgets gets the old silent `undefined`, and any reader that reads `def.reference` directly is still blind. That is one reading that has to be repeated correctly at every call site.\n- **(ii) write the self-reference where the object is known.** Measured gap: `ObjectSchema`'s parse alone does not reach the runtime for every door. `PUT /meta/object/:name` stores the authored body verbatim (registry entry measured: `{\"type\":\"tree\",\"label\":\"Parent\"}`, no `reference`, no `deleteBehavior`), and a raw `registerObject` / raw `bootStack` config is never parsed. So the registry, the one door every registration passes (it already runs `objectFieldTypeRefusal` and `applySystemFields`), runs the same spec helper. Every reader, whether it reads the target or the carrier, then sees an ordinary self-reference, and no reader changes.\n\nI chose (ii) with the registry door. It is one reading, applied by one helper at the two places that know the object, with no reader-side fallback. The triage direction (\"the engine's reference readers take the field's own object\") holds without editing a reader. The parse output changes are byte-identical to a field that wrote its own name: the key lands at its `FieldSchema` shape position, and a re-parse is a no-op. `refuseForeignTreeReference`'s guidance still reads true, because dropping `reference` now yields exactly the self-reference. The authorable surface and the conversion ledger are unchanged (`check:authorable-surface` green; no ADR-0087 entry).\n\n## What changed\n\n- `packages/spec/src/data/object.zod.ts`: new export `materializeTreeSelfReferences(ownName, fields)`. Absence is the two spellings a contract admits for \"names no target\": `undefined` (`FieldSchema`'s `.optional()`) and `null` (the blueprint's `StrictField`). Every other value is returned untouched. `reference: ''` is NOT absence here: the parse refuses it on a `tree` as a foreign target, so the registry leaves it as written instead of repairing it unparsed, and the carrier reader reads it as naming nothing, as before (patch round 1, review record 6131807682 item 10a). A foreign name stays `refuseForeignTreeReference`'s to refuse, and a non-string value stays the carrier reader's to refuse loudly. It returns the same object when nothing needs writing. `ObjectSchema` and `ObjectExtensionSchema` (own name = `extend`) run it as an `.overwrite` after their refinements, so the schema stays a `ZodObject`.\n- `packages/objectql/src/registry.ts`: `registerObject` runs the helper right after the field-type door, on a copy, so the caller's definition is not mutated. An extension registers under `name: ext.extend`.\n- `packages/spec/src/data/field-value.zod.ts`: the `referenceTargetOf` docblock no longer calls a referenceless `tree` \"an authoring bug\". It says where the target is written and why the function cannot derive it.\n- `packages/spec/src/data/field.zod.ts` (prose only): the `reference` TSDoc and `.describe()` (\"…and writes the own name when it is left out\"); the `deleteBehavior` TSDoc's #9784 paragraph and the `.overwrite` block now say the delete walk reads the key on `master_detail`, `lookup` and `tree` (the `statesDeleteSemantics` family). The `user` sentence names that family: `user` is still outside it, so the substance was true and the wording was stale.\n- Generated: `packages/spec/api-surface/data.json`, `packages/spec/export-origins/data.json`, `content/docs/references/{data/field,data/object,system/migration}.mdx`.\n- `.changeset/23065-referenceless-tree.md`: `minor` for `@objectstack/spec` (export added), `patch` for `@objectstack/objectql`.\n\n## Merge with `origin/main` (patch round 1)\n\n`origin/main` was merged at `0511341408` (9 commits past the merge base `275ad4229a`) through `scripts/pm/os-regen-merge.sh`, by merge commit `f77dd94819`. Step 2 kept the branch's bytes of the four artifacts only the branch moved, and took main's side of `content/docs/references/data/object.mdx`, the one both sides moved. Step 3's commit was refused by the `os-regen` pre-commit as designed. Then `gen:schema && gen:docs` regenerated `object.mdx` on the merged tree, and the staged diff, read before committing, held both sides' rows: main's `target` row for `type: 'url'` and this PR's two `reference` rows. That was committed as `b390b3cbae`, and the hook printed `content/docs/references/data/object.mdx — current … marker cleared`. On the merged head, after a rebuild, `check:generated --fix` reported all 14 generated artifacts up to date, `check:api-surface`, `check:authorable-surface` and `check:docs` included, with nothing left to write. `git diff --stat 0511341408 HEAD` is exactly this PR's 13 files. Since the merge, `main` has gained two commits, `6662e3f590` (runtime `app-plugin.ts`) and `1badd31838` (`ui/app.zod.ts` docs). Neither touches a file in this diff.\n\n## Tests (at `d09dffd8bf`, the merged head)\n\n- `packages/spec/src/data/tree-reference-self-only.test.ts`: +7 pins. The parse writes the own name byte-identical to a written one and re-parses as a no-op. `create()` and `defineStack` write it. The extension door writes `extend`. The refusal's remedy holds. CONTROL: a written self-reference, `lookup`, `master_detail` and `text` parse exactly as `FieldSchema` alone parses them. Helper semantics: `undefined` and `null` are written; `''`, a non-string and a foreign value are untouched; nothing to write allocates nothing. `reference: ''` on a tree has one reading: the parse refuses it at `fields.f_tree.reference`. One existing assertion (`reference` toBeUndefined after parse) pinned exactly the behaviour this card changes and now reads the own name. 14/14.\n- `packages/objectql/src/engine-cascade-delete-tree.test.ts`: +10 pins on a real engine with a snapshot-rollback driver. The registry writes the self-reference and leaves the caller's object alone. An extension's tree references the extended object. Also pinned: `set_null` (absent, field-parsed, authored), the object-parsed door, `restrict` (envelope), `cascade` (subtree, one unit of work). An empty-string `reference` reaches the registry as written and `referenceTargetOf` answers no target. CONTROL: a written self-reference. 28/28.\n- `packages/verify/src/delete-behavior-referenceless-tree.test.ts`: the door pins on one `bootStack`. Through `DELETE /data`: `set_null`, `restrict` and `cascade`. The metadata save door. `runDeleteBehaviorProofs` proves all referenceless cases through both doors. The write check: a parent id that names no record answers 400 `VALIDATION_FAILED` with `fields[]` `{ field: 'parent', code: 'reference_not_found' }` (it was 201), and CONTROL: an existing parent is accepted and stored. `expand=parent` resolves the parent (it was 404). The nested relation condition `{ parent: { title } }` filters through it (it was 400). CONTROLS: a written self-reference, `lookup`, `master_detail`. 8/8.\n- Package suites on the merged head: `@objectstack/spec` 714 files / 20898 passed. `@objectstack/objectql` 409 files / 7989 passed. `@objectstack/verify` 35 files / 278 passed, run after the gates had finished: the previous round's concurrent-build \"Failed to resolve entry\" flake did not recur. `typecheck` is green for objectql and verify, test layers included; spec's test layer is OK through `check:generated`.\n\n## Ablation (one-time proofs, not committed)\n\nEach leg was run against the committed fix, with the mutation landed through `scripts/ablation-replace.mjs` (marker 1, anchor 0), spec dist rebuilt with the marker confirmed by `ablation-dist-preflight`, and a `trap` restore.\n- Round 0 (at `bd8eff9eb6`): the helper writes nothing. Red: spec 6/13, objectql 8/27, verify 5/6. Green: every CONTROL and the #14892 / #22957 pins.\n- Round 1 leg A (at `d09dffd8bf`): the helper writes nothing. Verify is red on 7/8: every delete pin, the meta save door, `os verify`, the write check, and expand plus nested filter. Green: the CONTROLS test.\n- Round 1 leg B: `''` read as absence again. Red: the spec helper pin (1/14) and the objectql empty-string registry pin (1/28). Green: the spec \"parse refuses `''`\" pin, which is the parse's own refusal, and every other pin.\n- Restore, each leg: blob == HEAD, `git diff HEAD` empty, spec rebuilt in full, `ablation-dist-preflight --absent` clean for both markers, whole tree clean.\n\n## Gates\n\nUnion of the dispatch's 84-command list and the list re-derived on the merged head (115, the same set as round 0): 117 commands at `d09dffd8bf`, every exit captured before any pipe. All 117 exit 0. `check:dual-build-cjs-loads` first refused its prerequisite (exit 3, packages without dist), and its re-run after the later gates had built them exited 0. Among them, `check:generated` (\"All 14 generated artifacts are up to date\"), `check:api-surface`, `check:authorable-surface` and `check:docs` (\"226 generated files in sync\"). `dispatch-gates --ran` with exit codes: 115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN.\n\n## Acceptance notes\n\n- Author-time lint on the metadata save door judges the authored body before any materialization, as it does for every parse default. So a rule that traverses a referenceless `tree` inside the very object being saved sees no target there, while `os validate` / `os lint` read `defineStack` output, which carries it. Not measured at a door; noted, carrier: none.\n- `ObjectSchema.create()` is typed off the authored literal, so TypeScript does not see the written `reference` on its result (the runtime value is there). This is the same property every parse default already has.\n- A stored `sys_metadata` row keeps its body verbatim (ADR-0005 appendix (c)). Only the registered definition carries the self-reference, and a round trip through `GET`/`PUT /meta/object` stores the own name, which re-parses clean.\n- objectui's use of `referenceTargetOf` / `referenceCarrierOf` is NOT MEASURED: repository-scoped search was refused from this session. No reader's signature or behaviour changes for a field that names a target, and the shipped corpus already carries a written self-reference on a tree (the showcase's `f_tree`).\n\n---\n_Generated by [Claude Code](https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX)_\n", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed. Author-time lint at the metadata save door judges the authored body before any materialization (review ③ item 8). Dedupe words: save-door lint raw body, referenceless tree lint", "carrier: none · noted, not filed. objectui's reads of referenceTargetOf / referenceCarrierOf NOT MEASURED (repository-scoped search refused from this session); Acceptance note only. Dedupe words: objectui referenceTargetOf tree key presence" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
✅ ACCEPT: PR #23092 at
d09dffd8bf·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T22:40ZBasis:
Everything below was checked on GitHub, not taken from the reports. Classes, positions and functions only.
main, assigneeos-steve;Fixes #23065, the only closing keyword;Clause-②: yes (widening);Widens:names the new export and the parse-output change. The seat wrote the PR body's evidence sections after round 1, as the role file assigns it.check-governed-merges: clean);api-surface,export-originsandcontent/docs/references/**are in;content/docs/releases/;packages/specis cross-lane, declared on [PM seat] domain:spec — ⏳ vacant #6017../dataexport,materializeTreeSelfReferences(ownName, fields), writes the declaring object's name (or an extension'sextend) into atree'sreferencewhen it isundefinedornull;ObjectSchemaandObjectExtensionSchemarun it as an.overwrite;registerObjectruns the same helper on a copy, because the save door stores bodies verbatim and raw registrations skip the parse;reference: ''is refused by the parse and left as written by the registry: one reading;tree"an authoring bug", and the twofield.zod.tssites PR fix(objectql): a tree field's deleteBehavior takes effect on a parent delete #23055 made false, are corrected.bootStack, before and after):set_nullclears,restrictrefuses withDELETE_RESTRICTED, andcascadedeletes the subtree;reference_not_found, which was 201, with a CONTROL;expand=parentresolves (it was 404), and the nested relation filter works (it was 400);os verify --delete-behaviorsproves the case;d09dffd8bf: 41 check runs, none failed.bd8eff9eb6;d09dffd8bf. The three asks are done, the net diff is exactly the 13 files, and main's hunks are carried.object.mdxon the queue merge: it holds both sides' rows. The merge group'scheck:docsverifies the regeneration.referenceTargetOf/referenceCarrierOfare not measured (no objectui scope here). Neither reader changes for a field that names a target;reference: null, the blueprint'sStrictFieldspelling;f77dd94819alone carries the branch's blob ofobject.mdx, whichb390b3cbaerestores: a bisect hazard only.Landing: prechecks pass:
The PR is readied and armed through the relay in this act.
objectstack-fleet commentedon Oct 12, 2026
Landed: PR #23092 →
8464380248·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T23:16Z8464380248cf65b648a7e56208b41aa085df8f1e, with one parent,36b2889cdd. The ACCEPT is on this card. Contract review: PASS 6131807682, then delta PASS 6133989112 ond09dffd8bf.mainat that commit: the squash carries the accepted change exactly. All 13 files have hunks identical to the accepted head. The generatedobject.mdxcarries both of this PR'sreferencerows besidemain's rows, and the merge group'scheck:docspassed.mainnow holds: atreefield with noreferenceis a self-reference. It is written at theObjectSchemaparse and atregisterObject, and every reader resolves it:deleteBehaviortakes effect (set_nullby default,restrict,cascade);completedby the PR'sFixes #23065.pm:dispatchedcomes off in this act.