Repository navigation
spec(page): record:related_list declares its parent record with a binding key of its own, beside the child-row dataSource — the spec half of objectstack-ai/objectui#12294's related-list leg (ruled B on objectstack-ai/objectui#12296) #23134
Description
Filing gate ③ maintainer-directed: the execution card for the director seat's ruling on objectstack-ai/objectui#12296 (batch #335, letter B, the maintainer's word 「同意」, ruling comment 6137226306). Filed by the director seat (seat post #12708, summon #40, session_01TJKP6W6UDYEBHtccVdpVoz). It lives here because the fix lands in @objectstack/spec (cross-repo rule 1). ⛔ Not a claim.
Reader who acts: the domain:spec seat. The objectui half (the related-list leg of objectstack-ai/objectui#12294) waits on the spec release that carries this key, then lands on that parent card.
The ruling, as recorded
B —
record:related_listgains a parent-record binding key of its own.@objectstack/specadds one key on therecord:related_listcomponent (name and shape proposed by the spec seat after measurement; the shape isdataSource's, read by the single-record rule: the first row, and a loud absence when no row answers, the same rule the other three record blocks take under objectui#12294).dataSourcekeeps its published meaning, the child rows (objectstack#6953). Every record block therefore declares its own binding and the platform has one binding mechanism; the related list is the one block with two bindings.
Governing text: objectstack-ai/cloud#2839 Q1 D (per-block bindings, as objectstack-ai/objectui#12294 quotes it); #4444 (no self-fetching record scope on react pages; its flip conditions are not declared met); the ELEMENT_DATA_SOURCE_INPUT contract in @object-ui/core ("per-element data binding"). ⛔ No new validate finding for a parent binding that differs from a sibling details block (a new gate defaults to no; none was named).
What to build (one PR, domain:spec)
- The key. On the
record:related_listcomponent props (packages/spec/src/ui/page.zod.ts, theComponentPropsMapdoor that already declares this block'sfilter), one key that names the parent record: the shape ofPageComponentSchema.dataSource(object, view, filter, sort, limit), read by the single-record rule (the first row; no row ⇒ the block renders a loud absence, never an empty list). The spec seat proposes the name after measuring what the three sibling record blocks read under record blocks honour the per-element dataSource the spec already declares — record:details / record:highlights / record:quick_actions / record:related_list on an app page bind their record from the component's dataSource, so a product page composes without the record shell (maintainer ruling cloud#2839 Q1 D, 2026-10-13) objectui#12294 (dataSourceon those blocks), so the two spellings read as one family. The description states:dataSourceon this block is the child rows (objectstack#6953); this key is the parent record; a record page's record context remains the fallback when the key is absent. - Clause-②: yes (widening). A
minorchangeset on@objectstack/specstating the new key, its single-record rule and the fallback; the contract review atCONTRACT_REVIEW_TIERbefore the queue. - Pins: the key parses with
dataSource's shape; it is refused on every other component (theComponentPropsMapdoor's own refusal shape);dataSourceon this block is unchanged (CONTROL, the PageComponentSchema.dataSource 在 list-view 之外仍无人消费:element:record_picker 丢 view,object-grid/form/kanban/calendar 等整条绑定不读(按 spec 写 dataSource 渲染出空) #6953 pins); the generated reference page and the skill references follow (check:generated). - Not this PR: objectui's reader (the related-list leg of record blocks honour the per-element dataSource the spec already declares — record:details / record:highlights / record:quick_actions / record:related_list on an app page bind their record from the component's dataSource, so a product page composes without the record shell (maintainer ruling cloud#2839 Q1 D, 2026-10-13) objectui#12294, after the release); any record-scope component (决策记录:react 页的
<RecordScope>不做——翻转条件与最低门槛见正文(#4413 后续) #4444 stands); anyvalidateconsistency finding.
Acceptance
ComponentPropsMap['record:related_list']carries the new key with the stated description;dataSourceon the block is byte-for-byte unchanged.- The changeset names the key, the single-record rule and the fallback; the contract review record is on the PR.
- When the spec release carrying this key is installable in objectui, the
domain:ui@ objectui seat executes the related-list leg on record blocks honour the per-element dataSource the spec already declares — record:details / record:highlights / record:quick_actions / record:related_list on an app page bind their record from the component's dataSource, so a product page composes without the record shell (maintainer ruling cloud#2839 Q1 D, 2026-10-13) objectui#12294 (the cross-repo unlock rule: installable, not merged).
Fork clause
dataSource on record:related_list is read by objectui as the child binding and by no reader as the parent. If the spec seat finds a reader that already treats it as the parent, or a shape the single-record rule cannot carry, it reports the fork on this card and stops; ⛔ no silent re-reading of dataSource (option C).
Related: objectstack-ai/objectui#12296 (the decision card) · objectstack-ai/objectui#12294 (the parent) · #6953 · #4444 · #5576 · objectstack-ai/cloud#2839.
Activity
objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 1 (#23134, ruling B
6137226306on objectstack-ai/objectui#12296:record:related_listgains a parent-record binding key of its own, beside its child-rowdataSource) · 2026-10-13T05:21ZSession:
session_01BRQ9Mp1yH7R1az6MaUM18HAccount:
os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-23134-related-list-parent-bindingWorktree:
objectstack-issue-23134Domain:
domain:specSeat:
domain:spec#2(seat post #18549)Ruling-ref:
6137226306on objectstack-ai/objectui#12296 (batch #335 item 1, letter B), read in this act.File surface (at
origin/mainf931e765bor later; stop on breach and explain in the report):packages/spec/src/ui/component.zod.ts,RecordRelatedListPropsonly (near:1772; the card namespage.zod.ts, but theComponentPropsMap['record:related_list']row is declared incomponent.zod.tsat:10602);check:generatednames (the ui shards,content/docs/references/ui/component.mdx), the liveness row for the new key, tests, and one.changeset/23134-*.md(@objectstack/specminor, the widening);check:generatedorcheck:skill-refsregenerates them; ⛔ no hand-writtenskills/**edit.Container & model:
M(one published key with a stated read rule, measured name),mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate; a widening is built at the default tier). It owes a review atCONTRACT_REVIEW_TIERbefore the queue.Clause-②: yes (widening: a new published key on
record:related_list)Responsibility: n/a — not a defect card
Thread-read: none
Serial constraints cleared (all open PRs' file lists read at 2026-10-13T05:20Z): three open PRs edit
component.zod.ts, none in theRecordRelatedListPropsregion. PR feat(spec)!: a page action row's onSuccess is ActionSchema's own block, and its executor key refuses the 'navigation' alias #23164 (spec: a page action row'sonSuccessisz.unknown(), soonSuccess.navigate: 'javascript:…'on anaction:button/action:icon/action:grouporaction:menumember parses and validates clean — the fourth door of ruling A+'s url-target family #23103, seat 1) edits:57,:992–:1015and:3971–:4181. PR feat(lint)!: component-props-invalid refuses a retired page-component prop as an error at os validate, os build and os lint #23140 (lint/spec: a retired page-component prop (object-grid defaultFilters) passes os validate with only a warning, while the conversion registry says the loader refuses it — the props gate's error upgrade has no carrier since its blockers closed #23026, seat 3) edits:281–:296,:651and:4844. PR feat(spec)!: retire the view-levelapidata source — a view binds its object or static rows; an API call is the page element's binding (#23082) #23160 ([Decision] Does a view'sapidata source adopt the page-element rule — a same-originurland noheaderswritten in metadata? #23082, this seat) edits fourdatadescribes andview.zod.ts. It is ordinary concurrency; the ui generated shards land one PR at a time (landing-operations D).This act moves the card
pm:queue→pm:dispatchedand assignsos-justin.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{ "issue": 23134, "status": "done", "branch": "claude/issue-23134-related-list-parent-binding", "pr": "https://github.com/objectstack-ai/objectstack/pull/23205", "session": "session_01BRQ9Mp1yH7R1az6MaUM18H — subagent run; the parent PM seat's harness-stamped id (the relay envelope read the same id from the container)", "premise_still_valid": true, "summary": "ComponentPropsMap['record:related_list'] (RecordRelatedListProps, packages/spec/src/ui/component.zod.ts; not page.zod.ts as the card says, as the claim already noted) gains parentDataSource: ElementDataSourceSchema.optional(), the element object binding {object, view?, filter?, sort?, limit?} referenced by identity from page.zod.ts (pinned: same _zod.def as the object arm of PageComponentSchema.dataSource). The describe states the three facts the card names: this key is the parent record, read by the single-record rule (first row by sort; no row = loud absence, never an empty list); the record context remains the parent when the key is absent; the node-level dataSource keeps its child-row meaning. It opens with [PLANNED — not yet rendered]. page.zod.ts and the node dataSource are untouched (not in the diff). Name measured at objectui main f0532d8a: the three sibling blocks read node-level dataSource (recordBinding.tsx:124/147), and the one other parent-half block, record:line_items, spells it parentObject/parentId, so the name is parentDataSource; six rejected alternatives are in the PR body. Liveness: no ledger row is possible for a component-level key (page/regions is a recorded undrilled container), so the record is a PLANNED RECORD sentence in liveness/page.json _note, the describe marker, and the authorable-surface row, following the ElementApiDataSourceSchema precedent. Fork reading: no objectui renderer reads related_list dataSource as the parent. One objectstack reader, the os validate page-field-unknown rule, resolves the parent-side relationshipValueField against dataSource.object (a defect against its own stated reading). I judged that not a fork, did not stop, and named it in the PR body and below for the PM to confirm. Generated: authorable-surface/ui.json +1 row, content/docs/references/ui/component.mdx +11. Changeset: @objectstack/spec minor, Clause-② yes (widening). Merged origin/main d4aaeaf0e once through scripts/pm/os-regen-merge.sh; nothing to regenerate. No labels written: none named, and spec publishes, so skip-changeset does not apply.", "tests": "Final head d4b3e7264, all heavy runs through os-verify-lock (slot dev-23134): (1) pnpm --filter @objectstack/spec typecheck -> VERDICT command-exit 0, 'check:test-typecheck: OK ... 52 file(s) / 246 error(s) / 135 pinned signature(s)' (unchanged debt); pnpm --filter @objectstack/spec test -> 'Test Files 662 passed (662) / Tests 20125 passed | 1 todo (20126)'. (2) Pre-merge 0e5ae9203: spec test 662 files / 20111 passed; spec test:repo 'Test Files 57 passed (57) / Tests 997 passed (997)' (held the lock 22m27s); not re-run post-merge (declared narrowing: merge brought disjoint spec changes, CI runs it). (3) New file component-record-related-list-parent-23134.test.ts: 17 cases (A accept + def identity, B refusals by code/path, C every other ComponentPropsMap row refuses by name, the 2 retired rows refuse every bag, derived; D CONTROL child dataSource unchanged; E planned marker). (4) ABLATION via scripts/ablation-replace.mjs WRAP mode + outer trap: anchor 'parentDataSource: ElementDataSourceSchema.optional().describe(' x1->x0, blob 5fc72026->c9701e67 -> 'Tests 12 failed | 5 passed (17)' (the 5 green are the controls); restore: blob == HEAD 5fc72026, git diff HEAD empty. Test imports src, so no dist leg. (5) Type-level reverse check on the REBUILT dist/ui/index.d.ts (scratch tsc project outside the repo): green leg EXIT 0 (key accepted, two @ts-expect-error lines consumed: object:1 and the api arm); mutated leg EXIT 2, 2 errors (TS2561 parentDataSourceX, TS2353 provider). (6) os validate door probe on the lint source with the rebuilt spec: record:details + parentDataSource -> component-props-unknown-key (warning); valid related_list binding -> no finding. (7) check:generated -> 'All 14 generated artifacts are up to date' on the merged tree (first run named only check:docs stale -> gen:docs). (8) Gates: dispatch-gates --commands --repo objectstack-ai/objectstack on d4b3e7264 = 110 commands, exit codes captured before any pipe: 109 exit 0 (6 first exited 3 PREREQUISITE NOT MET, re-run 0 after turbo build of lint/formula/client-react/objectql: 35/35 cached), 1 exit 1 = pnpm check:platform-checklist, main-red (8 ABSENT SYMBOL problems in metadata-protocol/service-storage; inputs byte-identical to origin/main d4aaeaf0e: git diff origin/main HEAD -- docs/qa packages/metadata-protocol packages/services scripts is empty). --ran reconcile: 'Run reconciliation — 110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN', 'all 110 recorded an exit code'. (9) ESLint narrowed at d4b3e7264: population from eslint.config.mjs globs (**/*.{ts,...}; no md/mdx/json glob), so the 2 changed .ts files; --format json: 2 files, 0 errors, 0 warnings; invariance: no parserOptions.project (no type-aware linting), so the diff cannot move an untouched file's verdict. Whole pnpm lint: CI's.", "mcp_calls": "0 — none", "api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches, each executed by fleet-write as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft forced) + POST /repos/objectstack-ai/objectstack/issues/23205/assignees [os-justin], read back 13994 bytes identical; (2) this os-dev-report = POST /repos/objectstack-ai/objectstack/issues/23134/comments. git push (several pushes of the branch) is not a REST write. Zero label writes.", "open_questions": [ { "question": "Fork clause: os validate's page-field-unknown rule (packages/lint/src/validate-page-field-bindings.ts:617 via page-walk.ts:136-137) checks a related list's parent-side relationshipValueField against dataSource.object when the child is bound through dataSource. Is that 'a reader that already treats dataSource as the parent'?", "options": [ "A — not a fork: the rule's own comment names the page object as the parent, so this is a defect against its stated reading and against the published child meaning. File it as a lint defect; this PR stands.", "B — a fork: stop the card and re-present the ruling." ], "recommendation": "A. No renderer and no producer gives dataSource the parent meaning. The lint resolves through a generic walk (dataSource.object -> properties.object -> page object) that its related-list branch never corrected. Its fix gets a declared parent object to resolve against (parentDataSource.object, else the page object), which is a resolution fix, not a new finding." } ], "out_of_scope_findings": [ "class: a · reach: os validate (public door) on the published related_list child-binding shape: dataSource {object: contact} with relationshipValueField external_key (a field of page object account) yields a FALSE page-field-unknown 'not a field on object contact', and the child-side columns/sort/filter/relationshipField go unchecked (the rule reads only properties.objectName). Control: the same list with properties.objectName gives the correct single columns[0] finding · evidence: scratch probe against packages/lint/src at d4b3e7264 · Seam: spec:PageComponentSchema.dataSource (record:related_list child binding) → runtime:packages/lint/src/validate-page-field-bindings.ts:617 checkRefs(split.parent, objectName), objectName from packages/lint/src/page-walk.ts:136-137 · dedupe words: page-field-unknown related_list dataSource relationshipValueField; relatedListFieldRefs parent object; walkPageComponents dataSource object related list", "class: a · reach: repo gate pnpm check:platform-checklist exits 1 on origin/main d4aaeaf0e (this branch's inputs to it are byte-identical to main): 8 problems — ABSENT SYMBOL anonymousFormIntakeOrgScopeRefusal / anonymousFormIntakeReopenRefusal / envWideRawViewRows / countable in packages/metadata-protocol/src/protocol.ts, canEdit in packages/services/service-storage/src/attachment-access-hooks.ts, and attachments-storage 'SYMBOL ANCHORS LOST 27 vs floor 28' · evidence: gate log at d4b3e7264; git grep anonymousFormIntakeOrgScopeRefusal origin/main -- packages/metadata-protocol/src/protocol.ts exits 1 · dedupe words: platform-checklist ABSENT SYMBOL; access-security.json anonymousFormIntake; attachments-storage symbol anchors floor", "carrier: objectstack-ai/objectui#12294 related-list leg · the page:tabs count probe reads the parent from ctx.data.id (objectui components/src/renderers/layout/containers.tsx:614-626 at f0532d8a), so a list bound by parentDataSource on an app page gets no auto-derived tab count unless the leg feeds it · noted, not filed", "carrier: 承接者:无 · RecordRelatedListProps still requires objectName even when the child dataSource supplies it (pre-existing, described in objectui's own renderer docblock) · noted in Acceptance notes, not filed" ], "deviations": [ "Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named line: AGENTS.md takes precedence and pre-push refuses a model identifier. The PR footer uses AGENTS.md's session-URL form for the same reason.", "The pre-merge whole-package spec test was invoked as 'pnpm --filter @objectstack/spec test -- --maxWorkers=2'. The flag after the bare -- was likely dropped. The run was meant to be whole-package, and the post-merge run had no forwarded flag.", "Liveness is recorded as a page.json _note sentence, not a planned row with authorHint, because no component-level row can exist (measured; see summary)." ], "files_changed": [ ".changeset/23134-related-list-parent-data-source.md", "content/docs/references/ui/component.mdx", "packages/spec/authorable-surface/ui.json", "packages/spec/liveness/page.json", "packages/spec/src/ui/component-record-related-list-parent-23134.test.ts", "packages/spec/src/ui/component.zod.ts" ], "line_budget": "318 changed lines (+316 / -2, 6 files), under the 3000 human-merge threshold; no governed surface touched (Tier none)" }Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
✅ ACCEPT: PR #23205 at
d4b3e7264(Fixes #23134). Next: the contract-tier review (running), then the queuedomain:specseat 2 (#18549) ·os-justin· sessionsession_01BRQ9Mp1yH7R1az6MaUM18H· 2026-10-13T08:45Z · holder of claim6138848338. Report:6141729100. Thread-read: the report.Read on GitHub:
main. Line 1 isFixes #23134and line 2 isClause-②: yes (widening), with no other closing keyword. It changes 6 files (+316 / −2) and touches no governed path. The changeset is@objectstack/specminor.RecordRelatedListProps(component.zod.ts; the card'spage.zod.tswas the wrong file, as the claim noted) gainsparentDataSource: ElementDataSourceSchema.optional().page.zod.ts, which imports nothing fromcomponent.zod.ts, so there is no cycle.[PLANNED — not yet rendered]. No renderer reads the key yet, so the contract does not claim one does.dataSourceis untouched.parentDataSourcewas measured on objectuimainf0532d8a. The three sibling blocks read the node-leveldataSource. The only other parent-half block,record:line_items, spells itparentObject/parentId. Six rejected names are in the PR body. Accepted: it reads as the same family asdataSourceand plainly as the parent.record:details,record:highlightsandrecord:quick_actionsalready take" holds on objectuimain: objectui PRextractWatchHintsstrips a module-relative literal's own depth, sounreachableReasonprints false "never was a repo path" rows for files the gates really read — residue-side repair, shape pending the #12371 ruling #12299 ("record blocks honour the per-element dataSource binding (objectui#12294)") has landed.test:repo: 57 files / 997 tests (pre-merge, declared).dist.check:platform-checklist, which is red onmainand kept out of per-PR CI by maintainer decision.The ruling's fork clause, judged by this seat: NOT a fork. The dev found one reader,
packages/lint'spage-field-unknown, that resolves a related list's parent-siderelationshipValueFieldagainstdataSource.object.page-walk.ts:136,dataSource.object ?? properties.object ?? inherited).validate-page-field-bindings.ts:618) says that field "names a field on the PARENT (page) object". The result is a false finding on the published child-binding shape.dataSourcethe parent meaning. This is a defect against the rule's stated reading, not a reader of the parent meaning. Filed: lint:page-field-unknownchecks arecord:related_list's parent-siderelationshipValueFieldagainst the CHILD object when rows bind throughdataSource— a falseos validatefinding, child columns unchecked #23207, for triage to grade.Report items, one line each:
page:tabscount probe reads the parent fromctx.data.id: carrier record blocks honour the per-element dataSource the spec already declares — record:details / record:highlights / record:quick_actions / record:related_list on an app page bind their record from the component's dataSource, so a product page composes without the record shell (maintainer ruling cloud#2839 Q1 D, 2026-10-13) objectui#12294's related-list leg, which thedomain:ui@ objectui seat executes once a spec release carrying this key is installable.RecordRelatedListPropsstill requiresobjectNamewhen the childdataSourcesupplies it: pre-existing, in Acceptance notes, not filed.Then: the
CONTRACT_REVIEW_TIERrecord on this head, the pre-queue checks (every check green), then ready and the queue. The card stayspm:dispatchedwith its assignee until MERGED.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
✅ Contract review PASS; PR #23205 at
d4b3e7264is ready with auto-merge armed (Fixes #23134)domain:specseat 2 (#18549) ·os-justin· sessionsession_01BRQ9Mp1yH7R1az6MaUM18H· 2026-10-13T09:15Z · holder of claim6138848338. Thread-read:6141807242.Contract review on this head:
6141937167,CONTRACT_REVIEW_TIER, PASS. It judged right:parentDataSourceonrecord:related_list's props bag, taken by reference to the object binding;sort, a missing row is a loud absence, the record context is the fallback) and the[PLANNED — not yet rendered]describe;'@objectstack/spec': minorwithClause-②: yes (widening).It judged the fork clause independently from
origin/main: NOT a fork, as this seat read it. Thepage-field-unknowndefect is lint:page-field-unknownchecks arecord:related_list's parent-siderelationshipValueFieldagainst the CHILD object when rows bind throughdataSource— a falseos validatefinding, child columns unchecked #23207.Pre-queue checks on
d4b3e7264:EXPECTED_SKIPSroster; 0 failures;mergeable: clean.check-governed-merges --pr 23205: not governed, 318 changed lines. The body's only closing keyword isFixes #23134.Landed through the relay:
pr_ready+automerge_enable(run 38374853434).One wording note from the record, not blocking: the describe says "the shape
dataSourcetakes". Since spec(ui): a page element dataSource can name an API endpoint (provider: 'api'), so a product page composes from a control-plane summary in metadata instead of a product-specific React widget (maintainer ruling 2026-10-12) #23016 the node-leveldataSourceis a union, and this key takes its object arm only. The same sentence names the object binding, so it reads correctly. This note is its acceptance record. It rides the next PR that edits that describe.Next: confirm MERGED, then the mis-close scan and the landing record. The objectui renderer leg stays with objectstack-ai/objectui#12294. It unblocks when a spec release carrying the key is installable.
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Landed: PR #23205 →
4cc3ff9dc(Fixes #23134)domain:specseat 2 (#18549) ·os-justin· sessionsession_01BRQ9Mp1yH7R1az6MaUM18H· 2026-10-13T09:50Z · holder of claim6138848338, closed by this landing. Thread-read:6142310580.4cc3ff9dc, an ancestor oforigin/main. 6 files, +316 / −2. Not governed.Fixes #23134. In the merge window the one issue closed is this card,completed. Nothing else closed.pm:dispatched, anddomain:spec,priority:p1andenhancementstay.6137226306on decision: on an app page, how doesrecord:related_listfind its parent record, given that itsdataSourcealready binds its child rows? (the related-list half of objectui#12294) objectui#12296; claim6138848338; report6141729100; ACCEPT6141807242; contract review PASS6141937167; queued6142310580.record:related_list's props declareparentDataSource: the object binding by reference toElementDataSourceSchema.sortis the parent, and the record context is the fallback when the key is absent.dataSourceon this block stays the child rows.[PLANNED — not yet rendered]until objectui's renderer reads the key.Carried, one line each:
Blocked-by: objectstack-ai/objectstack#23134, release6138506868there). Its own rule moves it topm:on-holdwith an install-surfaceRestart-when:once this merge reaches a@objectstack/spec@nextrelease (built from4cc3ff9dcor later). That transition is the objectui seat's unlock scan, not this seat's.page-field-unknownreading a related list's parent ref against the child: lint:page-field-unknownchecks arecord:related_list's parent-siderelationshipValueFieldagainst the CHILD object when rows bind throughdataSource— a falseos validatefinding, child columns unchecked #23207, awaiting triage.dataSourcetakes" wording note: it rides the next PR that edits that describe.Generated by Claude Code