Skip to content

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

Closed

Description

@objectstack-fleet

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

  1. ObjectSchema gains relatedLists?: string[] (packages/spec/src/data/object.zod.ts, beside highlightFields): an ordered list of inbound relationships, each entry a child object name ('task') or object.field when 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 inbound lookup / master_detail field of the object is refused at defineStack with 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 where highlightFields' field-existence check runs and sit beside it).
  2. 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 — a primary list is drawn in the record's body as a full-width table (the details column, after the field sections), not as a side card; a master_detail FK with inlineEdit is primary by default (maintainer, 2026-10-28, on a quote's line items: 「明细行的子表放在右侧合适嘛?」 → the body). relatedLists orders the body tables among themselves and the side cards among themselves. The describe() of relatedList says so; the objectstack-ui skill bullet follows.
    Placement of a body table among the field groups (maintainer, 2026-10-28: 「他如果做到放在基本信息下面,而条款和备注在后面」「如果有多个子表呢」): ObjectFieldGroupSchema gains an optional relatedList?: string (same naming as relatedLists entries — child object, or object.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 with relatedList carries no fields and no field may name it as its group (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, in relatedLists order). 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: false still hides regardless of the list.
  3. 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.
  4. Liveness: the key has a consumer in the same PR (the console's record page), so the ledger row is live, not planned; check:authorable-surface baseline regenerated; the objectstack-data skill's object rules gain one bullet (governed skills/** → Tier H).
  5. 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

  1. safeParse accepts relatedLists: ['task', 'event.related_to']; defineStack refuses relatedLists: ['nope'] with code + status and a message naming nope and the inbound relationships; a non-array / non-string entry fails the shape.
  2. A lead with relatedLists: ['task', 'campaign_member'] renders tasks, campaign members, then the rest in the derived order; primary still promotes an unlisted list.
  3. 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)

  1. Record-page model (2026-10-27): 「1. 一个对象一个默认打开方式:默认抽屉(带展开整页),page 只给显式声明的对象,modal 退役,split / popover 变成用户个人偏好;抽屉和整页是同一张页在两个宽度。」
  2. 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.
  3. On the related column's cost: 「关联都放在右侧,记录打开时是不是会很慢,因为需要加载所有的关联子表。」 — the related column loads counts first, rows on demand, and never blocks the details.
  4. On mobile: 「还要考虑手机端的效果。」
  5. 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 the tabs slot and the dead BuildPageOptions hints; 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

  1. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-28T09:10Z
    Session: session_01WGufzfhobWEgUUrjz4rrbh
    Account: os-support-ai
    Branch: claude/project-thread-vr8ygc (card thread session_01WGufzfMx1APzx1RoDRtUiC. The platform assigns the thread's branch and the thread can push only that one, so the branch is written as measured instead of claude/issue-27069-<slug>.)
    Worktree: objectstack-issue-27069
    Domain: domain:spec
    Seat: domain:spec#1
    File surface:

  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-28T13:58Z

    PR #27215 merged at c63ce2ad, which is an ancestor of origin/main. On 37 of the 44 paths the PR touched, the merge tree matches the accepted head 897e8d89 byte 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 on main in between. Those 7 are field.zod.ts, liveness/field.json, field.mdx and 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, or object.field for one FK. A field group can declare relatedList, so that it stands for a body table; it is planned until #27055 draws it. relatedList: 'primary' now means placement in the record's main content. An inline-edited master_detail child is primary by default, so on the tabbed page crm_opportunity, showcase_invoice and showcase_expense_report each gain their own line-items tab. Dangling names are refused at defineStack and 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 removes pm:dispatched.

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specenhancementNew feature or requestpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions