Repository navigation
feat(spec): an object declares the business order of its related lists, and 'primary' places a list - #27215
Conversation
…elated lists ObjectSchema.relatedLists orders the record page's related lists; a field group's relatedList places one body table; relatedList: 'primary' places a list instead of ordering it, and is the default for an inline-edited master_detail child. defineStack refuses a name that matches no inbound relationship. deriveRelatedLists resolves the order in one function. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
…decision counts Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
…elatedLists Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
…-vr8ygc # Conflicts: # packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts
📓 Docs Drift CheckThis PR changes 4 package(s): 54 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 148 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 959ac8483df2eeb637151e79babcf1d71835dd7c && git checkout 959ac8483df2eeb637151e79babcf1d71835dd7c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 295699d51d1484a735989450238ec2e14c0c1104 897e8d89aff69b18cc77197d807f4cd03cda4d73 && git checkout -B drift-repro 295699d51d1484a735989450238ec2e14c0c1104 && git merge --no-ff 897e8d89aff69b18cc77197d807f4cd03cda4d73
node scripts/docs-audit/affected-docs.mjs --json 295699d51d1484a735989450238ec2e14c0c1104
|
…object publish door validateRelatedListRefs joins the reference-integrity suite on flow and object writes (rule related-list-ref-unknown, error). It writes the sentence defineStack writes, resolves children from the snapshot's objects and objectExtensions, and refuses a child the snapshot does not hold only on the whole collection (no package closure in force). fieldGroups is drilled in the object ledger, so fieldGroups[].relatedList has its own row, graded planned until the record page body layout draws it. Form help text and the changeset follow the new door. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
…-vr8ygc # Conflicts: # packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts
…rimary default Main's field inspector read an absent relatedList as shown. Under this branch an inline-edited master_detail with no relatedList is primary, so the control now reads the record page's own predicate (isMainContentList, now exported), clears the key when the author picks the default, and writes an explicit true for Show where the default is primary. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
Contract reviewServed-tier: Inputs read: card #27069 body and its two comments (claim 6465054400, os-dev-report 6467128510); epic #26676 round-3 comments 6459899198 and 6460478339; PR #27215 body, file list (27 files) and the net diff against the merge base Check-runs on ① Derived judgmentsAccept-set and public-surface changes the diff implies, each judged against the tree:
② Semver level
Changeset ③ Boundary flags
④ Design judgmentpackages/spec/src/data/object.zod.ts — against ruling A and its first consumer (
packages/spec/src/stack.zod.ts —
packages/spec/src/data/field.zod.ts — the
packages/spec/src/data/object.form.ts — against Studio's form renderer and the spec: holds. Both rows match the key shapes (string-tags for skills/objectstack-data/rules/relationships.md — against the spec on this head and the card's acceptance scenario: the tri-state bullets and the
skills/objectstack-ui/SKILL.md — the
Cross-cutting
Implemented-by: VERDICT: FAIL Two must-fixes, both one-sitting: M1 the ADR-0035 (and ADR-0085 §2) amended status line for the default and the re-reading this head ships; M2 the |
…roups, artifact-only stacks ADR-0035 gains an amended status line and a note on the two sentences this card changes (the inline-edit primary default, and 'primary' as placement); ADR-0085 section 2 notes the keys that entered under its admission test. validateSemanticRoles rules (b) and (d) skip a field group that carries relatedList, so os lint no longer tells an author to assign a field the parse refuses. collectRelatedListNameErrors runs before the object-count early return, so an artifact whose objects live only in package bodies is judged, once per object name. The two skills bullets, the changeset census of the three in-repo parents that move, and the RecordDetailView liveness citation follow. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
…ts relatedLists line Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
Contract reviewServed-tier: Delta review of the round-1 record Check-runs on Round-1 findings, each judged on this head
① Derived judgmentsJudgments 1, 2 (parse half), 3 (now including artifact-only stacks), 4, 5, 6, 7, 8, 9, 10 of the round-1 record stand on this head unchanged; re-read, not restated. New or changed on this head:
② Semver level
③ Boundary flags
④ Design judgmentpackages/lint/src/validate-related-list-refs.ts + reference-integrity-suite.ts — the door rule, tried against legitimate saves. It holds:
No must-fix. The rule's docblock sentence "with the fields their packages/spec/src/data/object.form.ts — the two rows and their help texts hold against the tree ("saving or validating the object refuses it" is now true at both doors). One contradiction inside the file:
skills/objectstack-ui/SKILL.md — the
packages/spec/src/data/object.zod.ts — holds, as in round 1; the gate it must co-exist with (M2) now agrees with it. The packages/spec/src/stack.zod.ts — holds; S3 closed as above. The artifact scope handed to the hoisted call is packages/spec/src/data/field.zod.ts — holds, as in round 1; ADR-0035 and ADR-0085 now record the re-reading (M1 closed). skills/objectstack-data/rules/relationships.md — holds; S1 closed. The code sample's comment ("read side: a body table (default with inlineEdit)") and the four bullets match Cross-cutting, observations not graded: the changeset's Implemented-by: VERDICT: PASS Every round-1 finding (M1, M2, S1-S4) is closed on this head in the tree, not only in the PR body. The door rule refuses nothing legitimate in the scenarios tried (package closure, extension-added FK, sibling dangling entries, child writes, flow writes); its universe is the registry's folded view, which is why the extension case holds. Two should-fixes remain, both copy inside |
… the layout paragraph names the inline-edit default Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
… relatedList as relationship names Test Core 4/8 failed on the object-key classification pin: relatedLists was an unclassified top-level key, so the served-object scrub would read it as a possible field reference and drop an entry whose child shares a denied field's name. Both keys name child relationships, never a field of the object, so they are kept; a test pins it against the denied-name fixture. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfMx1APzx1RoDRtUiC
席位核对 · ACCEPT (armed) ·
|
Requested by Yibo Wang · project thread
Fixes #27069
Clause-②: yes (widening)
Metadata-protocol: semantic
Skills-docs:
skills/objectstack-data/rules/relationships.md(therelatedListtri-state now reads as placement, plus one bullet teachingrelatedLists) and therelatedList: 'primary'bullet inskills/objectstack-ui/SKILL.md. Both files are net smaller (token ratchet green). The field-group placement entry is not taught yet: its reader is #27055, and the layout skill text is #27059's.维护者速读
relatedLists(按业务重要性排列相关列表,写子对象名或object.field),字段分组新增relatedList(让一个分组代表一张正文子表);relatedList: 'primary'改读为"放在正文"而非排序,带inlineEdit的master_detail子表默认即primary。inlineEdit且未写relatedList的明细子表,在今天的标签页布局里会升为独立标签页;写relatedList: true可保持原样。回滚 = revert 本 PR。本仓库内会变化的三个记录页:crm_opportunity、showcase_invoice、showcase_expense_report,各自的明细子表(inlineEdit: 'grid',未写relatedList)从共享 Related 标签页升为独立标签页。ui/packages/app-shell/src/utils/deriveRelatedLists.ts(resolveRelatedListOrder、isMainContentList)、packages/spec/src/stack.zod.ts的collectRelatedListNameErrors、packages/spec/src/data/object.zod.ts的两个新键与refuseFieldsInRelatedListGroups。pnpm --filter @objectstack/spec exec vitest run src/stack-related-lists-order.test.ts,预期 14 个用例全绿(含relatedLists: ['nope']被拒并列出现有的入向关系)。What changes
ObjectSchema.relatedLists?: string[](packages/spec/src/data/object.zod.ts, besidehighlightFields). Each entry is a child object name ('task', every list of that child) orobject.fieldfor one FK ('event.related_to'). The entry grammar is a regex on the element (snake_case, at most one dot), so the shape is judged at parse with no new refinement node. The.describe()says it orders the record page's related lists by business importance, that unlisted ones follow in the derived order, and that it orders only.ObjectFieldGroupSchema.relatedList?: string, same spelling. A group carrying it stands for that list's table in the details column. A field whosegroupnames such a group is refused at parse atfields.NAME.group(refuseFieldsInRelatedListGroups, inside the object's existingsuperRefine, so no new check node for the JSON Schema projection to drop).defineStack(collectRelatedListNameErrorsinstack.zod.ts, called fromvalidateCrossReferencesbesidecollectHydratedInlineColumnErrors). EveryrelatedListsentry and every grouprelatedListmust match an inboundlookup/master_detailfield of a child (the stack'sobjects, everypackages[].manifest.objects, and the fields theirobjectExtensionsadd). A miss is aSTACK_CROSS_REFERENCE_INVALIDissue naming the entry and the inbound relationships that exist, spelled as an entry may name them. Not judged: a child only a sibling package declares (artifactObjects), and a platform object. Audit FKs (created_by,updated_by,owner_id) are not inbound relationships, matching the reader. It runs before the object-count early return, so an artifact whose objects live only in package bodies is judged too, once per object name.packages/lint/src/validate-related-list-refs.ts,validateRelatedListRefs, rulerelated-list-ref-unknown,error): a new reference-integrity member onflowandobjectwrites. It writes the sentencedefineStackwrites (a test holds the two equal), resolves the children from the snapshot'sobjects(the registry plus the stored rows) andobjectExtensions, and refuses a child the snapshot does not hold only when no package closure is in force; under a closure that child is not judged. On the CLI an absent child is left todefineStack's artifact scope. A Studio / REST/meta/ MCP save ofrelatedLists: ['nope']is now 422INVALID_METADATA.validateSemanticRoles: rules (b)field-group-emptyand (d)field-group-shadowedskip a group that carriesrelatedList(a body table holds no fields), soos lintno longer tells the author to assign a field the parse refuses.readRelatedListModereads the record page's own predicate (isMainContentList, now exported), so an inline-editedmaster_detailwith norelatedListshows as Primary; picking the default clears the key, and Show there writesrelatedList: true.'primary'as placement); ADR-0085 §2 note on the keys that entered under its admission test.FieldSchema.relatedListTSDoc and.describe():'primary'means the record's main content (a body table; its own tab on the tabbed page) and no longer orders; absent on amaster_detailFK that setsinlineEdit, the list is'primary'by default, andtruekeeps it a side card.object.form.ts): arelatedListsstring-tags row afterhighlightFields, and arelatedListsub-row in thefieldGroupsrepeater. Labels and help texts in en, zh-CN, ja-JP and es-ES (platform-objectsmetadata-form bundles).deriveRelatedLists.ts: the one resolution functionresolveRelatedListOrder(lists, declared)replaces the plain owned-then-lookup tail. Listed lists first in the declared order (a list takes the position of the first entry naming it); unlisted ones after, in the derived order. An entry naming no list here places nothing.isMainContentListcomputesisPrimarywith the inline-edit default.buildDefaultTabsis unchanged: it filtersisPrimaryinto own tabs and the rest into the Related tab, both preserving this order, so "primarystill promotes an unlisted list" holds on the tabbed page.object.relatedListsrow,live(readerderiveRelatedLists→RecordDetailView, authoring judgescollectRelatedListNameErrorsand the door rule).object/fieldGroupsis drilled (10 child rows, the container's baseline row removed);fieldGroups.relatedListisplanned(reader 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, noauthorWarn, since nothing warns an author about it).field.relatedList/relatedListTitleciteRecordDetailView.tsx:1959, with a note on the placement reading. Object shard 76 live, 1 planned.data.json,content/docs/references/**(gen:docs, 4 files)..changeset/27069-related-lists-order.md:@objectstack/spec,@objectstack/platform-objects,@object-ui/app-shell,@objectstack/skills,@objectstack/lint,@objectstack/metadata-core, all minor.object-schema-fls-references.ts):relatedListsand a field group'srelatedListare classifiedkeep. They name child relationships, never a field of this object, so read as unclassified they would have over-masked an entry whose child shares a denied field's name. This is pinned against the denied-name fixture (897e8d89).Why
The maintainer's ruling (2026-10-28), verbatim, on 「元数据的开发者,可以根据业务的重要性自己指定顺序吗」 with options A (an ordered list on the parent object) / B (a number on each child FK) / C (full page only): 「A」. On a quote's line items: 「明细行的子表放在右侧合适嘛?」 → the body. On placement among the field groups: 「他如果做到放在基本信息下面,而条款和备注在后面」「如果有多个子表呢」.
On the declaration: the card said
Metadata-protocol: added. Two keys are added, and the diff also changes what an existing value means and what an absent value defaults to:relatedList: 'primary'becomes placement, and an absentrelatedListon an inline-editedmaster_detailFK now reads as'primary', which on today's tabbed record page moves such a child from the shared Related tab to its own tab. That is a changed meaning and a changed default, sosemantic, which outranksadded. The re-reading is semantic, not only documentation: the inline-edit default is a behaviour change a reader can see without editing any metadata.Readers and producers (measured on
origin/main0f1045b)deriveRelatedLists.tspushedmaster_detaillists, thenlookuplists, each in registry order;buildDefaultTabs(ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts) putisPrimarylists into their own tabs ahead of the Related tab. Nothing else readsrelatedList(git grepunderui/: the only branch on the value wasderiveRelatedLists.ts).highlightFields' existence check is a lint rule (validate-object-field-refs.ts) at the publish door andos validate, but it is a same-object check. A related-list name is cross-object, so it sits indefineStack'svalidateCrossReferences, the cross-object refusal family with the ADR-0112 envelope.compose-stacks-merge-collection-refusal.test.tsandcompose-stacks-collection-pipe-arm.test.ts(the derived object collection-key list gainsrelatedLists, plus a merge-refusal row), platform-objectsobject-lifecycle-panel-echo-decisions.test.ts(positive control +2, 690 → 692 on top of main) andobject-collapsed-sections-echo-decisions.test.ts(open leaves 118 → 122).Tests
packages/spec/src/stack-related-lists-order.test.ts(14 cases): shape accepts names andobject.field, refuses a non-array, a non-string, a non-snake and a two-dot entry; a field group withrelatedListand no fields parses, a field naming it as its group is refused atfields.name.group;defineStackaccepts child names and one FK of a child with several, refusesnopewithcode, a numericstatus, one issue naming the entry and'rlo_task', 'rlo_event.who_id', 'rlo_event.related_to', refuses a declared child with no FK and an unknown field, says "it has none" when there is no inbound relationship, sees an FK anobjectExtensionsentry adds, leaves a sibling-package child unjudged (and refuses it withoutartifactObjects), and judges a group'srelatedListthe same way.ui/packages/app-shell/src/utils/__tests__/deriveRelatedLists.relatedListsOrder-27069.test.ts(11 cases): the lead fixture's derived order;['task', 'campaign_member']renders tasks, campaign members, then the rest in derived order;object.fieldvs bare child name; first entry wins and an unknown entry places nothing;relatedList: falsestill hides a listed relationship; an unlistedprimarylist keeps its derived position (primary no longer orders); the inline-edit default on every arm,trueopting out, no default withoutinlineEditor on alookup.Acceptance notes
fieldGroups[].relatedListis declared ahead of its renderer (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). Until then such a group is not drawn, as every group without fields is not. It has its own liveness row,planned.relatedListsafter.6467460418).object.fieldpicks one.Verification
Final head
897e8d89a. It is08cebee83, which mergesorigin/maind745bb5e5with no conflict, plus one fix found in CI. Test Core 4/8 went red on08cebee83: metadata-core's object-key classification pin (object-schema-fls-references.test.ts) foundrelatedListsunclassified. The fix is in897e8d89a. The served-object FLS scrub now keepsrelatedListsand a group'srelatedListas relationship names, a new test pins that against the denied-name fixture, and the changeset now names@objectstack/metadata-core. Every run below was underos-verify-lock.Suites
912f48922(round-2 code, after the first main merge):@objectstack/lint: 173 files / 7549 passed.@objectstack/spec: 823 files / 23588 passed, 1 todo.@objectstack/metadata-protocol: 277 files / 21752 passed, 26 skipped.@objectstack/platform-objects: 96 files / 1361 passed.@object-ui/app-shell:utils/__tests__plus the RecordDetailView and inspector related-list files, 32 files / 448 passed, 8 skipped.8a549c1e2(second merge):check:generated --fixrebuilt spec and found nothing stale. Lint 173 files green. The three spec pin files (stack-related-lists-order, the twocompose-stackspins) are green, 101 tests.897e8d89a:@objectstack/metadata-core21 files / 845 passed.validate-related-list-refs.test.ts: 8 cases, including that its sentence matchesdefineStack's word for word.runtime-gate.object-writes.test.ts: 5 door cases with controls.protocol.runtime-authoring-gate.test.ts: a 422INVALID_METADATApin with a control that saves.validate-semantic-roles.test.ts: therelatedListgroup exemption, with a control.stack-related-lists-order.test.ts: now 15 cases, adding an artifact with objects only in package bodies, judged once per name.ObjectFieldInspector.relatedList-27054.test.tsx: the inline-edit default reads Primary, and Show writestrue.object-schema-fls-references.test.ts: relationship names are served.Gates
dispatch-gates --commandson08cebee83: 125 derived.check:generatedexited 1 for the same stale-dist reason.pnpm --filter @objectstack/spec buildandclient-reactbuild, those 7 were re-run and all exited 0.--ran: 125 run, 0 NOT-MEASURED (a derived zero), verdict exit 0.897e8d89athe derivation is unchanged (125). The gates that read the changeset were re-run and exited 0:check-changeset-no-major --base origin/main,check-empty-changeset --base origin/mainandcheck:pm-changeset-deadline-census.check-skills-token-ratchet,check-adr-links,check-adr-anchors, speccheck:liveness.!in the title and no(narrowing)arm.Skills readings (
pricedTokens, againstorigin/main):skills/objectstack-data/rules/relationships.md: 3873 → 3872 tokens, 418 → 418 lines.skills/objectstack-ui/SKILL.md: 3843 → 3840 tokens, 308 → 308 lines.Skills readings (
pricedTokens, the ratchet's own count, round 2):skills/objectstack-data/rules/relationships.md3873 → 3872 tokens (418 → 418 lines);skills/objectstack-ui/SKILL.md3843 → 3838 tokens (308 → 307 lines). Both net smaller.