Repository navigation
spec: an object declares the business order of its related lists — relatedLists: ['task', 'event', …], an ordered list of child relationships like highlightFields is of fields; unlisted ones follow the derived order; a name that matches no inbound relationship is refused #27069
Description
Part of the record-page-v3 epic #26676 (v3 child 9). Contract card (spec lane), Clause-②: yes (widening), Metadata-protocol: added; contract review at CONTRACT_REVIEW_TIER before the queue.
Source (origin/main 7bb8084590)
The order of a record page's related lists is not authorable: deriveRelatedLists.ts:262-328 buckets master_detail children before lookup children and, inside each bucket, follows the object registry's order (package declaration / served order); buildDefaultTabs (buildDefaultPageSchema.ts:1003-1012, ADR-0085 rule Z) only promotes relatedList: 'primary' lists ahead of the rest. HotCRM declares no primary, so a lead's related column is 「owned children in declaration order, then lookup children in declaration order」 — an order nobody chose. The maintainer's ruling (2026-10-28), verbatim: on the question 「元数据的开发者,可以根据业务的重要性自己指定顺序吗」 with options A (an ordered list on the parent object) / B (a number on each child FK) / C (full page only): 「A」.
Target
ObjectSchemagainsrelatedLists?: string[](packages/spec/src/data/object.zod.ts, besidehighlightFields): an ordered list of inbound relationships, each entry a child object name ('task') orobject.fieldwhen the child has more than one FK to this object ('event.related_to'); describe() says it orders the related lists of the record page by business importance; strict names: an entry that matches no inboundlookup/master_detailfield of the object is refused atdefineStackwith the ADR-0112 envelope naming the entry and the inbound relationships that exist (a cross-object check at compose time, where the registry is known — measure wherehighlightFields' field-existence check runs and sit beside it).- Resolution (one pure function in
deriveRelatedLists.ts, replacing the registry-order tail): listed relationships first in the declared order; unlisted ones after, in today's derived order (owned → lookup, declaration order).relatedList: 'primary'no longer orders; its meaning becomes PLACEMENT — aprimarylist is drawn in the record's body as a full-width table (the details column, after the field sections), not as a side card; amaster_detailFK withinlineEditisprimaryby default (maintainer, 2026-10-28, on a quote's line items: 「明细行的子表放在右侧合适嘛?」 → the body).relatedListsorders the body tables among themselves and the side cards among themselves. The describe() ofrelatedListsays so; the objectstack-ui skill bullet follows.
Placement of a body table among the field groups (maintainer, 2026-10-28: 「他如果做到放在基本信息下面,而条款和备注在后面」「如果有多个子表呢」):ObjectFieldGroupSchemagains an optionalrelatedList?: string(same naming asrelatedListsentries — child object, orobject.field) so a group entry can stand for a body table instead of fields:{ name: 'lines', label: '报价明细', relatedList: 'crm_quote_line_item.crm_quote' }places that table at that position in the details column; a group withrelatedListcarries nofieldsand no field may name it as itsgroup(refused). Body tables not placed this way follow the default rule (console record page v3: frameless detail sections, one merged timeline (discussion + activity + field changes), compact related cards, header 1 primary + 2 secondary + ⋯ — the synthesized default page, drawn from the object's roles; the dead BuildPageOptions hints go #27055: stacked after the first field group, inrelatedListsorder). Studio's field-group designer draws such an entry as a draggable row (Studio: the object and field inspectors author what the v3 record page reads —relatedList(show / hide / primary), related-list columns / filter / limit on the block, and a new record page starts empty instead of a copy of the synthesized regions #27054).relatedList: falsestill hides regardless of the list. - The reader's drag order (console record page v3: frameless detail sections, one merged timeline (discussion + activity + field changes), compact related cards, header 1 primary + 2 secondary + ⋯ — the synthesized default page, drawn from the object's roles; the dead BuildPageOptions hints go #27055 / console: one workspace-preference module (record open mode, drawer width, timeline / related column toggles) read per user × object, with a shared panel-toggle toolbar on the record page — defaults derived from the object, never metadata #27052) overrides this per user; 「恢复默认顺序」 returns to it.
- Liveness: the key has a consumer in the same PR (the console's record page), so the ledger row is
live, notplanned;check:authorable-surfacebaseline regenerated; the objectstack-data skill's object rules gain one bullet (governedskills/**→ Tier H). - HotCRM is not changed here; a follow-up on hotcrm declares the order for lead / opportunity / account once the spec ships.
File surface
packages/spec/src/data/object.zod.ts (+ form object.form.ts row for Studio), the authorable-surface baseline, liveness ledger, spec tests; ui/packages/app-shell/src/utils/deriveRelatedLists.ts + tests; skills/objectstack-data/rules/*.md (one bullet); changesets (@objectstack/spec minor, added).
Acceptance
safeParseacceptsrelatedLists: ['task', 'event.related_to'];defineStackrefusesrelatedLists: ['nope']withcode+statusand a message namingnopeand the inbound relationships; a non-array / non-string entry fails the shape.- A lead with
relatedLists: ['task', 'campaign_member']renders tasks, campaign members, then the rest in the derived order;primarystill promotes an unlisted list. - Studio control is Studio: the object and field inspectors author what the v3 record page reads —
relatedList(show / hide / primary), related-list columns / filter / limit on the block, and a new record page starts empty instead of a copy of the synthesized regions #27054's; the layout card console record page v3: frameless detail sections, one merged timeline (discussion + activity + field changes), compact related cards, header 1 primary + 2 secondary + ⋯ — the synthesized default page, drawn from the object's roles; the dead BuildPageOptions hints go #27055 reads the resolved order.
Rulings (maintainer, chat in the filing session, 2026-10-28 — verbatim, not re-decidable here)
- Record-page model (2026-10-27): 「1. 一个对象一个默认打开方式:默认抽屉(带展开整页),
page只给显式声明的对象,modal退役,split / popover 变成用户个人偏好;抽屉和整页是同一张页在两个宽度。」 - On the resident timeline column: 「时间线是不是太突出了,这个只适合部分对象吧?适合做所有业务对象的默认布局吗」 and then 「时间线是否显示,是否可以切换,比如当前版本的 Claude code,顶部有很多切换图标」 — the column's presence is a per-user toggle with an object-derived default, never metadata.
- On the related column's cost: 「关联都放在右侧,记录打开时是不是会很慢,因为需要加载所有的关联子表。」 — the related column loads counts first, rows on demand, and never blocks the details.
- On mobile: 「还要考虑手机端的效果。」
- On the definition surfaces: the PM's keep / narrow / retire table was put to the maintainer (keep object roles, relationship flags, six slotted slots and
kind: full; retire thetabsslot and the deadBuildPageOptionshints; fold the quick-look path into the one record page) and approved with the instruction in the footer.
Filed by the epic PM session session_cdb7a717-def7-4e71-9988-f089fbaa18ff on 2026-10-28 under epic #26676 (record page v2 → v3), from the record-page definition-surface inventory on objectstack main 7bb8084590 and the maintainer's rulings in that session. The maintainer's instruction to file and dispatch, verbatim: 「同意,以上界面相关的需要我确认的你负责立卡派发,如果有可能可以启动云会话,开发完你来确认。」 Design evidence: the design canvas boards 「记录页 v3」 row (RecordPageV3, RecordPageV3NoTimeline, RecordPageRef, RecordDrawerV3, RecordMobile) — exported to the never-merged branch named in the epic parent once this batch is filed.
Activity
objectstack-fleet commentedon Oct 28, 2026
Claim: PM loop round 2 · 2026-10-28T09:10Z
Session:
session_01WGufzfhobWEgUUrjz4rrbhAccount:
os-support-aiBranch:
claude/project-thread-vr8ygc(card threadsession_01WGufzfMx1APzx1RoDRtUiC. The platform assigns the thread's branch and the thread can push only that one, so the branch is written as measured instead ofclaude/issue-27069-<slug>.)Worktree:
objectstack-issue-27069Domain:
domain:specSeat:
domain:spec#1File surface:
packages/spec/src/data/object.zod.ts:ObjectSchema.relatedLists?: string[], plus a compose-time strict-name check besidehighlightFields' existence check, using the ADR-0112 envelope.ObjectFieldGroupSchema.relatedList?: string, refused when combined withfieldsor named as a field'sgroup. TherelatedListfield flag's describe() re-reads'primary'as body placement.object.form.ts: the Studio row. The editor itself is Studio: the object and field inspectors author what the v3 record page reads —relatedList(show / hide / primary), related-list columns / filter / limit on the block, and a new record page starts empty instead of a copy of the synthesized regions #27054's, in the epic session.ui/packages/app-shell/src/utils/deriveRelatedLists.ts: one pure resolution function. Listed relationships come first in the declared order, then unlisted ones in today's derived order.primaryplaces a list in the body, and amaster_detailFK withinlineEditisprimaryby default. Tests cover it.liveliveness row naming this consumer, the oneskills/objectstack-data/**bullet (Tier H), spec tests, the pins the change moves, and root.changesetentries.Container & model:
M,mode:thread,model: TIER_DEFAULT,effort: highClause-②: yes (widening). New optional keys. The dev states at step 3 whether re-reading
'primary'from ordering to placement makesMetadata-protocolsemanticrather thanadded.Metadata-protocol: added (subject to the dev's step 3 reading above)
Responsibility: n/a. A child of the record-page v3 epic epic: record page v2 — one default open mode per object (drawer by default, page by declaration; modal / split / popover retired as metadata); the drawer and the page are one page at two widths; related records open in place #26676, filed by the epic PM under the maintainer's ruling 「A」 quoted in the card.
Thread-read: none
Premise: the card measured
7bb8084590, and the dev re-measures onorigin/main0f1045bcbefore the first edit.Serial constraints cleared: yes. spec: a slotted record page keeps six slots — header, actions, alerts, highlights, details, discussion — and the
tabsslot is retired; a page that needs its own tab strip is akind: fullpage #27053 (seat 1) runs in parallel and may touchbuildDefaultPageSchema.ts; whichever lands second mergesmain. Studio: the object and field inspectors author what the v3 record page reads —relatedList(show / hide / primary), related-list columns / filter / limit on the block, and a new record page starts empty instead of a copy of the synthesized regions #27054 and console record page v3: frameless detail sections, one merged timeline (discussion + activity + field changes), compact related cards, header 1 primary + 2 secondary + ⋯ — the synthesized default page, drawn from the object's roles; the dead BuildPageOptions hints go #27055 (epic session and ui lane) read this card's resolved order.Ruling-ref: the maintainer ruling quoted in the card (2026-10-28, letter A) and epic round 3
6459899198, update6460478339objectstack-fleet commentedon Oct 28, 2026
os-dev-report
{ "issue": 27069, "status": "done", "branch": "claude/project-thread-vr8ygc", "pr": "https://github.com/objectstack-ai/objectstack/pull/27215", "session": "session_01WGufzfMx1APzx1RoDRtUiC", "premise_still_valid": true, "summary": "Head d53bd3312 (merge of origin/main into 1e4906889, branch off 0f1045bc; premise reproduced there: deriveRelatedLists pushed master_detail then lookup lists in registry order, nothing authorable). Items 1, 2 and 4 done within the card's file surface. ObjectSchema.relatedLists (child name or object.field, regex-shaped, no new refinement node) beside highlightFields; ObjectFieldGroupSchema.relatedList, and a field whose group names such a group is refused at parse (inside the object's existing superRefine). Strict names at defineStack: collectRelatedListNameErrors in validateCrossReferences refuses an entry or group relatedList matching no inbound lookup/master_detail FK (objects, package bodies, objectExtensions fields) with STACK_CROSS_REFERENCE_INVALID, naming the entry and the inbound relationships; sibling-package children (artifactObjects) and platform objects unjudged. deriveRelatedLists: resolveRelatedListOrder is the one resolution (declared first, first entry wins, unlisted in derived order); isMainContentList makes an absent relatedList on an inlineEdit master_detail FK primary. relatedList describe()/TSDoc re-read as placement. Studio form rows (relatedLists, fieldGroups.relatedList) with en/zh-CN/ja-JP/es-ES labels; liveness object.relatedLists live; field.relatedList re-cited; objectstack-data relationships bullet and objectstack-ui bullet (both files net smaller). Declaration: Clause-② yes (widening), Metadata-protocol semantic (not the card's added): 'primary' changes meaning and an absent relatedList on an inline-edited master_detail now defaults to primary, which moves such a child to its own tab on today's tabbed page; semantic outranks added. Changeset minor for spec, platform-objects, app-shell, skills. PR body: /mnt/project-files/issue-27069/pr-body.md.", "tests": "Under the lock. Branch head 1e4906889: spec 741 files, 13 collection-key pin failures moved by the new array key, fixed (675e8d03d), then green; platform-objects 96 files / 1361 passed after echo-decision pins 689->691 and 118->122; metadata-protocol 1 failed (CARD_PROPERTY_COUNTS object 48->49), fixed, file 18/18; app-shell every test file importing RecordDetailView or deriveRelatedLists 178 files / 1402 passed (narrowed; CI Test UI runs the rest); plugin-detail 322 files / 2941 passed, 18 skipped; spec + platform-objects typecheck and app-shell type-check exit 0. Merge head d53bd3312 (conflict: positive-control pin, resolved to 692 = main's 690 + 2): install, pnpm build exit 0; check:generated 14/14 current; spec 742 files / 22263 passed (1 todo); platform-objects 96 / 1361; metadata-protocol 277 files / 21750 passed, 26 skipped; app-shell utils + plugin-detail synth 35 files / 572 passed. New: stack-related-lists-order.test.ts 14 cases (envelope code + numeric status + named entry and relationships); deriveRelatedLists.relatedListsOrder-27069.test.ts 11 cases. No ablation run.", "gates": "dispatch-gates --commands: 121 derived; 120 exit 0 on 36b767ddf, check:durability-log-level exit 0 on 1e4906889; --ran: 121 run, 0 NOT-MEASURED, 0 UNRUN. check-skills-token-ratchet red once (+26 relationships.md), paid by deletions in the same file. Merge head: ratchet, check:i18n, check:liveness, check-adr-0087-registration, check-empty-changeset exit 0; check-changeset-no-major with the PR body as event exit 0 (semantic, all minor). CI on the PR: not read at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "2: issue_read x2 (27069 get, get_comments), 0 writes", "api_writes": "3 via the relay: pr_create (#27215, 12716 bytes read back identical, assignee os-support-ai), pr_request_reviewers (os-support-ai), and this os-dev-report comment", "open_questions": [], "out_of_scope_findings": [ "class: c · reach: a one-object save (Studio object form, REST /meta, MCP) of relatedLists: ['nope'] is accepted, because the strict-name check is cross-object and runs only at defineStack (os validate / os build); the reader then places nothing for it · dedupe: relatedLists publish door dangling name, validate-object-field-refs · carrier: seat 1 to file or route to the lint lane", "boundary: fieldGroups[].relatedList is declared ahead of its renderer (#27055 body layout); until then a relatedList group is not drawn, like any group without fields · carrier: #27055", "boundary: ADR-0035's read-side table says 'primary' promotes to its own tab; true on today's tabbed page, needs its amended status line when #27055 ships body placement (Tier H) · carrier: #27055 or the epic session" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
Landed ·
domain:specseat 1 (#6017) · sessionsession_01WGufzfhobWEgUUrjz4rrbh· 2026-10-28T13:58ZPR #27215 merged at
c63ce2ad, which is an ancestor oforigin/main. On 37 of the 44 paths the PR touched, the merge tree matches the accepted head897e8d89byte for byte. On the other 7, the PR's net hunks are identical between the merge-base and the merge, and the bytes differ only because other work landed onmainin between. Those 7 arefield.zod.ts,liveness/field.json,field.mdxand the four generated metadata-form translation bundles.An object can now declare
relatedLists, which orders the record page's related lists by business importance. An entry is a child object name, orobject.fieldfor one FK. A field group can declarerelatedList, so that it stands for a body table; it isplanneduntil #27055 draws it.relatedList: 'primary'now means placement in the record's main content. An inline-editedmaster_detailchild isprimaryby default, so on the tabbed pagecrm_opportunity,showcase_invoiceandshowcase_expense_reporteach gain their own line-items tab. Dangling names are refused atdefineStackand at the publish door (related-list-ref-unknown). ADR-0035 is amended and ADR-0085 carries a §2 note.The card closed through
Fixes #27069, and this seat removespm:dispatched.