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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Auto-derived related lists have no default-sort surface — derivation emits no sort, component skips $orderby, rows fall to PK order #5795
Surface: record detail page → auto-derived related list tab (master-detail child object)
Phenomenon
An auto-derived related list has no way to declare a default sort. The rows render in record-id order (the server's primary-key fallback), which for business data is effectively random. Downstream case: a "task version" detail page shows its "check items" tab where items carry an explicit seq_no (10/20/30/40…) — the tab renders them 20/30/10/40.
Root cause (verified against 17.2.0 dist)
The console's related-list derivation produces columns / title / summary for the tab, but never produces sort;
The related-list component only appends $orderby to the query when it is handed a sort — so the derived list's query goes out without $orderby and falls to the PK order;
There is no declaration surface anywhere to fill the gap:
the field-level relatedList* keys (relatedList, relatedListTitle, relatedListColumns, relatedListFilter) have no sort key, and the schema is $strict — adding one is rejected;
the child object's own list view sort is not consulted by the derivation (verified: child default view sorted by seq_no, related list still id-order);
view.form.defaultSort was removed in 17.0.0.
Same derivation code path as #4664 (which is about it not consuming relatedListFilter) — the derivation emits neither filter nor sort.
Repro
Master object A, child object B (master_detail lookup to A) with a number field seq_no;
Create 4 B-records under one A in shuffled order so seq_no = 20/30/10/40 by creation sequence;
Open A's record page → B tab: rows show 20/30/10/40 (id order), not 10/20/30/40;
Set B's default list view sort: [{field:'seq_no', order:'asc'}] → B's own list obeys it, the related list tab does not; network shows the related-list request carries no $orderby.
Triage: parked pm:blocked (line in body) on objectstack-ai/objectstack#11345 — the maintainer-filed contract question (where does a derived related list's sort declaration live: field-level relatedListSort, inherit the child list view's sort, or none) is now in the decision inbox with a four-prism block. This card is the component-side consumer of whatever is ruled; contract-first, the spec/engine half decides the shape. domain:ui, Feature.
Unblock relay from the objectstack spec seat (session_01T9cDbY2NBiVJWYx3BpWfH2): the block inverted — this card is now the implementation leg.pm:blocked → pm:queue in this stroke.
The ruling is in (objectstack#11345, maintainer, 2026-08-23 15:02Z): direction 1 — derived related lists inherit the child object's default list view sort. No new spec key; field-level relatedListSort was explicitly not approved.
Measurement (full map in the os-dev-report on objectstack#11345, comment 5388863970) puts both halves in this repo: descriptor emit at app-shell/src/utils/deriveRelatedLists.ts:133-146 (re-dropped at RecordDetailView.tsx:1955-1978 and plugin-detail/src/synth/buildDefaultPageSchema.ts:174-188/683-690); $orderby assembly at plugin-detail/src/RelatedList.tsx:519-529, fed via renderers/record-related-list.tsx:211. objectstack's share is already in place and needs no change: the isDefault expanded-list-view resolver (spec/src/ui/view.zod.ts:4111-4198, already merged onto the child def as obj.list by MetadataProvider.tsx:413-414), the ListView sort shape (view.zod.ts:1470-1486), and the declared-parsed-consumed record:related_list.sort prop (component.zod.ts:883-928) — the inheritance only needs to fill it.
Sketch from the measurement (yours to refine): emit sort from child.list?.sort in deriveRelatedLists.ts (child def already in hand at :118); carry it at the two re-drop sites; renderer/RelatedList unchanged. ⚠️ Caveat: ListView legacy string sort is space-separated 'field desc' (view.zod.ts:1470) while normalizeSortSpec reads 'field'/'-field' (RelatedList.tsx:258-266) — normalize to the array form or inherit only the array arm, or $orderby goes out on a field literally named seq_no desc.
Attach, don't lose: pre-existing on this file surface — a declared record:related_list.sort is dropped whenever the list leaves windowed mode (windowed goes false while the client text filter is active, RelatedList.tsx:383-389; the client path returns rows unsorted unless a column was clicked, :731-733). Affects the authored prop today and will affect the inherited sort identically. Also Auto-derived related lists consume the new field-level relatedListFilter declaration — list query AND-composed, tab badge honors the same filter #4664 (same emit point ignores relatedListFilter) was re-confirmed by this measurement.
objectstack#11345 is now pm:blocked on THIS card and closes when this lands and the acceptance leg passes (the 10/20/30/40 repro from its body); the 17.0.0 migration prose then becomes true as written, no prose edit needed.
Claim: PM loop round 38 (refill slot)
Session: session_01CSoz9uGhaaSgiq3hshtN7L
Branch: claude/issue-5795-related-list-inherit-sort
Worktree: objectui-issue-5795
Domain: domain:ui
File surface: packages/app-shell/src/utils/deriveRelatedLists.ts, packages/app-shell/src/views/RecordDetailView.tsx, packages/plugin-detail/src/synth/buildDefaultPageSchema.ts + tests + a changeset. RelatedList.tsx and renderers/record-related-list.tsx are read-only — the relay says the renderer needs no change; if you find it does, ⛔ stop and report.
Container & model: M, mode:subagent, model: opus
Clause-②: no new key — the ruling explicitly declined one. But a derived descriptor gains a populated sort where it had none, so a host observes new ordering. Declare it. ⚠️Serial:#5874's rework is live in packages/app-shell/src/utils/ (resolveActionParams.ts, paramToField.ts). Different files — ⛔ do not touch either. #5725 is live in app-shell/src/views/metadata-admin/previews/ — ⛔ stay out. Enqueued and landing shortly: #6053, #6060, #6061, #6064, #6066, #6068, #6072.
⛔ RULED — objectstack#11345, maintainer 2026-08-23 15:02Z, direction 1
derived related lists inherit the child object's default list view sort. NO new spec key; field-level relatedListSort was explicitly not approved.
⭐ The block inverted: this card is now the implementation leg, and objectstack#11345 is pm:blocked on THIS card. It closes when this lands and its acceptance leg passes (the 10/20/30/40 repro in its body), and the 17.0.0 migration prose then becomes true as written — no prose edit needed. ⛔ Do not touch objectstack.
The measurement is already done — use it, but re-derive before editing
From the spec seat's relay (full map in the os-dev-report on objectstack#11345, comment 5388863970):
Emit:app-shell/src/utils/deriveRelatedLists.ts:133-146 — the child def is already in hand at :118.
Two re-drop sites:RecordDetailView.tsx:1955-1978 and plugin-detail/src/synth/buildDefaultPageSchema.ts:174-188 / 683-690.
$orderby assembly:RelatedList.tsx:519-529, fed via renderers/record-related-list.tsx:211 — unchanged.
objectstack's share is already in place and needs no change: the isDefault expanded-list-view resolver (merged onto the child def as obj.list by MetadataProvider.tsx:413-414), the ListView sort shape, and the declared-parsed-consumed record:related_list.sort prop. The inheritance only needs to fill it.
Sketch (yours to refine): emit sort from child.list?.sort in deriveRelatedLists.ts; carry it at the two re-drop sites.
⚠️ The caveat that will bite, carried verbatim
ListView legacy string sort is space-separated 'field desc' while normalizeSortSpec reads 'field' / '-field' (RelatedList.tsx:258-266) — normalize to the array form or inherit only the array arm, or $orderby goes out on a field literally named seq_no desc.
⛔ Do not inherit the string arm unnormalised. ✅ Whichever route you take, pin it: a child list view declaring the legacy string form must produce a correct $orderby, or be provably skipped.
⚠️ Attach, don't lose — a pre-existing defect on this exact surface
a declared record:related_list.sort is dropped whenever the list leaves windowed mode (windowed goes false while the client text filter is active, RelatedList.tsx:383-389; the client path returns rows unsorted unless a column was clicked, :731-733). Affects the authored prop today and will affect the inherited sort identically.
⛔ Not yours to fix — RelatedList.tsx is read-only here. ✅ But pin the interaction so the inherited sort's disappearance under the client filter is a recorded, deliberate fact rather than a surprise, and say in the report that the inherited sort inherits this hole too.
⚠️ Also re-confirmed by that measurement: #4664 (the same emit point ignores relatedListFilter). ⛔ Separate card — do not fold.
child.list is populated by MetadataProvider.tsx:413-414 in objectstack's half.⚠️ Verify it is actually present at deriveRelatedLists.ts:118 at the moment you read it — an async metadata arrival that lands after derivation would make the inheritance silently empty, and an empty inherited sort is indistinguishable from today's behaviour.
Verification
Direction predicted before running: a child object with a default list view carrying sort must produce a derived related list whose $orderby reflects it; revert the emit and show the pin goes red. Assert the $orderby that reaches the data source, not the descriptor — the descriptor is three hops from the wire and two of those hops are the re-drop sites.
Counter-probes: (a) a child with no default list view sort must still produce a working related list with no$orderby — otherwise inheritance is satisfiable by inventing an order; (b) an authoredrecord:related_list.sort must still win / behave exactly as today — ⛔ the ruling adds inheritance, it does not change precedence, and if it turns out precedence is undefined here, stop and report.
Prove every mutation on disk by grepping injected and separately removed text; ⚠️print the landing site, ⚠️assert anchor uniqueness before writing, and ⚠️ restore under trap … EXIT INT TERM with a cwd-independent command (all three measured on other cards in this round).
⚠️Run app-shell path-filtered — whole-package vitest is ~784 s and holds the shared lock; ⛔ do not run it. Gates by name with exit codes: type-check for @object-ui/app-shell and @object-ui/plugin-detail (⚠️ hyphenated; echo the script name; distinguish the unbuilt-closure red from the missing-node_modules red), path-filtered vitest, eslint with merge-base delta, and — ⚠️new standing rule, measured on #5874 in this round — pnpm check:esm-specifiers if you add ANY new relative import to a published package; it runs inside the Type Check CI job under a name that does not look like one, and a hand-derived gate set misses it.
Changeset required. Draft PR, Fixes #5795. ⛔ Do not mark ready, ⛔ do not enqueue, ⛔ no auto-merge — the PM lands it. Post the terminal report here (plain-text first-line marker os-dev-report; ⛔ avoid short angle-bracket placeholders in prose) and return it.
{
"issue": 5795,
"status": "done",
"branch": "claude/issue-5795-related-list-inherit-sort",
"pr": "https://github.com/objectstack-ai/objectui/pull/6088",
"premise_still_valid": true,
"summary": "Implements the objectstack#11345 ruling (maintainer 2026-08-23, direction 1): a derived related list inherits the child object's default list view sort, with NO new spec key. deriveRelatedLists emits `sort` from the child def's merged default list view; RecordDetailView and buildDefaultTabs' relatedNode carry it to the `record:related_list` node; RelatedList.tsx and renderers/record-related-list.tsx are unchanged, as the relay predicted. The two `sort` surfaces share a union but mean different things by its string arm (ListView's legacy space-separated 'seq_no desc' vs the related list's 'field'/'-field'), so the inherited value is normalized to the array arm ONCE at the derivation, through @object-ui/core's convertSortToQueryParams — the repo's single definition of both dialects — rather than by a tolerant reader downstream. Three PM mechanism assumptions were measured and two moved: five of the nine relay line references had drifted (RecordDetailView +11, others -3..+2), and MetadataProvider.tsx is in THIS repo, not objectstack. A THIRD re-drop site exists (buildDefaultPageSchema.ts:940, into record:reference_rail entries) and is deliberately excluded because ReferenceRailEntrySchema is $strict with no `sort` — the objectui#5494 class — pinned as its own leg."
,
"tests": "All at final commit 5d599be9c on a clean tree (union re-run after the last commit).\n\nNEW: app-shell/src/utils/__tests__/deriveRelatedLists.inheritSort.test.ts (9), app-shell/src/views/RecordDetailView.relatedListInheritedSort-5795.test.tsx (5, the end-to-end $orderby pin through the real page), plugin-detail/src/synth/__tests__/buildDefaultPageSchema.relatedSort.test.ts (6), plugin-detail/src/__tests__/RelatedList.sortDroppedOutsideWindowed.test.tsx (4, the recorded hole).\n\nGATES (exit captured before any pipe; each quotes the gate's own line):\n- `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell' --filter '@object-ui/plugin-detail' type-check` EXIT=0 — `Scope: 2 of 47 workspace projects`, `packages/app-shell type-check$ tsc --noEmit && tsc -p tsconfig.test.json` -> `Done`, same for plugin-detail. Non-zero scope + echoed script name rules out the silent zero-match green.\n- dependency-closure build (`--filter '@object-ui/app-shell^...' --filter '@object-ui/plugin-detail^...' build`) EXIT=0, under the shared verify lock (`VERDICT command-exit 0 - held 121s - waited 77s`).\n- path-filtered vitest, 12 files incl. the pre-existing neighbours (deriveRelatedLists.test.ts, all synth tests, serverpagination, speclimit, headerSort, relationalSort, unmaterializedSort): `Test Files 12 passed (12)` / `Tests 172 passed (172)`, EXIT=0. Whole-package app-shell vitest NOT run, per dispatch.\n- eslint over the merge-base delta (7 files), plain `eslint` with no --no-inline-config: EXIT=0, zero error lines (one real `prefer-const` error found and fixed; the remaining 217 are pre-existing `no-explicit-any` warnings).\n- check:esm-specifiers 0 (run per the new standing rule, though the one new import is a package specifier, not relative), check:phantom-deps 0, check:self-import 0, check:control-bytes 0 (plus a direct `grep -naP` control-byte sweep of the delta), check:spec-symbols 0, check-changeset-presence 0 (`3 source file(s) of 2 released package(s) changed ... declares 1 changeset(s)`), check-changeset-no-major 0.\n- DECLARED NARROWING: check:eager-closure NOT run — it refuses as a *broken gauge* without apps/console/dist/eager-closure.json from a console `vite build`, which is CI's run. Safe to leave there rather than merely unrun: the one new import edge is deriveRelatedLists.ts -> @object-ui/core, and app-shell/src/utils/index.ts (the barrel that re-exports deriveRelatedLists) ALREADY imports @object-ui/core, so the edge adds nothing to that closure.\n\nREVERSE VERIFICATION — direction predicted in writing BEFORE running, three ablation legs, every cell matched. No rebuild leg needed or skipped: vitest.config.mts aliases @object-ui/app-shell and @object-ui/plugin-detail to their src/, so every subject resolves from SOURCE and no dist stands between an edit and a run (verified in the config, not assumed). Each leg asserted the mutation on disk by EXACT-SUBSTRING count before (want 1) and after (want 0) plus `git diff --numstat` = `0 1`, printed its landing site, asserted anchor uniqueness before writing, and restored under `trap ... EXIT INT TERM` with a cwd-independent `git -C` command; each leg reported `RESTORED ... 0 dirty entries`.\n L1 remove the emit -> inheritSort 7 failed/2 passed; wire 3 failed/2 passed; synth 6 passed; hole-pin 4 passed\n L2 remove re-drop site 1 -> inheritSort 9 passed; wire 3 failed/2 passed; synth 6 passed\n L3 remove re-drop site 2 -> inheritSort 9 passed; wire 3 failed/2 passed; synth 3 failed/3 passed\nThe legs that stayed GREEN were predicted too: the counter-probes assert ABSENCE, so a counter-probe going red would mean it was really a second subject. Only the wire test goes red on all three legs — which is exactly why the subject is the $orderby reaching dataSource.find and not the descriptor.\n\nCOUNTER-PROBES: (a) a child with no default list-view sort produces a working, scoped, WINDOWED related list carrying NO $orderby — key absent, not present-and-empty — with $filter and $top asserted as live controls so 'nothing inherited' cannot read as 'nothing fetched'; an unusable sort (number, bare string array, entry without `field`) is dropped, never guessed. (b) PRECEDENCE IS DEFINED, so no stop-and-report was needed: a hand-authored page carries its own record:related_list node and never enters this synthesizer (RecordDetailView synthesizes only when no page is assigned), and within the synthesized path the ONE producer of related[].sort is the inheritance while the synthesizer is a pure carrier. An authored sort behaves exactly as before; both facts are recorded in the synth test.\n\nA harness note worth carrying: the Related tab is opened via the URL (`?tab=related`, ADR-0054 C3) rather than a synthetic click — the list self-fetches when its tab mounts, and the child object is queried TWICE on that page (the tab strip's `$top:1,$count:true` count probe carries no ordering by design), so the helper selects the windowed page fetch explicitly. Without that, every counter-probe would have passed by reading the count probe."
,
"open_questions": [],
"out_of_scope_findings": [
"NOT filed, already open as objectui#4286: a synthesized Reference Rail page is unpersistable — the `aside` region carries a `className` that PageRegionSchema refuses. Re-confirmed here and measured on that branch with NO `sort` anywhere, so it is independent of this change; it is why the third-site leg asserts entry shape instead of parsing the whole page.",
"NOT filed, already open as objectui#4664: the same emit point ignores `relatedListFilter`. Re-confirmed by this measurement and deliberately not folded in, per dispatch.",
"No new issues filed. Searched open issues for both findings before deciding (keyword + file path); both already exist."
]
}
objectstack untouched. objectstack#11345 is pm:blocked on this card; per the relay it closes when this lands and its 10/20/30/40 acceptance leg passes, and the 17.0.0 migration prose becomes true as written with no prose edit.
The attached hole, restated so it is not lost: a declared or inherited record:related_list.sort is dropped whenever the list leaves windowed mode (the client text filter). Pre-existing on the authored prop; the inherited sort inherits it identically, and now reaches users who authored nothing. RelatedList.tsx was out of this card's file surface, so it is recorded in RelatedList.sortDroppedOutsideWindowed.test.tsx rather than fixed — those assertions pin today's behaviour and should go red when someone closes it. Worth its own card.
A trap I hit and fixed in my own tooling, in case it is worth a standing note: my first patch helper used JS String.replace, and a replacement string containing a $ followed by a backtick (a JSDoc citation of the field-name regex) was reinterpreted as the $\`` substitution pattern — splicing the entire preceding file back in. git diff --statstill read as a plausible insert-only diff (+149) and the injected-marker greps still counted 1. Only a structural check (the file's header block appearing twice) caught it. The helper now splices withslice()and neverreplace()`.
PR #6088 at 5d599be9c. Gates read once at 16:36:30Z, armed off this run's own started_at (16:23:52Z, ~12½ min).
22 checks, 19 success + 3 skipped, 0 failing, 0 still running — read as every check green, not the required subset. Not a governed surface; marked ready and enqueued through the merge queue. ⛔ Nothing merged by hand.
Two of my dispatch assumptions were wrong, and the dev measured rather than inherited
⚠️Five of the nine line references I relayed had drifted (RecordDetailView +11, others −3..+2), and ⚠️I said MetadataProvider.tsx lives in objectstack — it is in this repo. Both corrected by measurement on the merge-base. That is the third and fourth PM assumption falsified this round, which is the system working as intended: the dispatch's job is to name what I believe so it can be checked, ⛔ not to be right.
The find the card did not name
A third re-drop site exists — buildDefaultPageSchema.ts:940, into record:reference_rail entries — and it is deliberately excluded because ReferenceRailEntrySchema is $strict with no sort key. ✅ That is the objectui#5494 class, the same one #6066 landed on this morning, and it is pinned as its own leg rather than silently skipped. A silent skip here would have read as "two sites, both handled"; the pin makes the exclusion a measured fact with a reason.
The design call, and why it is the right one
The two sort surfaces share a union but mean different things by its string arm — ListView's legacy space-separated 'seq_no desc' versus the related list's 'field' / '-field'. The inherited value is normalised to the array arm once, at the derivation, through @object-ui/core's convertSortToQueryParams — the repo's single definition of both dialects — ⛔ rather than by a tolerant reader downstream. ✅ A tolerant reader is how you get a third dialect; normalising at the producer is how you get one.
And the ruling was executed as ruled: no new spec key (objectstack#11345, maintainer 2026-08-23, direction 1). RelatedList.tsx and renderers/record-related-list.tsx are unchanged, exactly as the relay predicted.
Reverse verification — three legs, every cell predicted in writing first
leg
inheritSort
wire
synth
L1 remove the emit
7 failed / 2 passed
3 failed / 2
6 passed
L2 remove re-drop site 1
9 passed
3 failed / 2
6 passed
L3 remove re-drop site 2
9 passed
3 failed / 2
3 failed / 3
⚠️The greens were predicted too, which is the part that makes this a measurement rather than a fishing trip: the counter-probes assert absence, so a counter-probe going red would have meant it was really a second subject. Only the wire test goes red on all three legs — which is precisely why the subject is the $orderby reaching dataSource.find, ⛔ not the descriptor.
No rebuild leg was needed and that was verified, not assumed: vitest.config.mts aliases both packages to their src/, so no dist/ stands between an edit and a run. ✅ Stating why a step is absent beats omitting it silently.
The harness note that would otherwise have been a false green
⚠️The child object is queried TWICE on that page. The tab strip's count probe ($top:1, $count:true) carries no ordering by design, so the helper selects the windowed page fetch explicitly. Without that, every counter-probe would have passed by reading the count probe — a green that looks identical to a correct result. ✅ Recorded in the report rather than buried in the harness.
Also right: the Related tab is opened via the URL (?tab=related, ADR-0054 C3) rather than a synthetic click, so the list self-fetches when its tab mounts — the real path, not a simulated one.
Precedence — no stop-and-report needed, and the reason is on the record
A hand-authored page carries its own record:related_list node and never enters this synthesizer (RecordDetailView synthesizes only when no page is assigned); within the synthesized path the one producer of related[].sort is the inheritance, and the synthesizer is a pure carrier. An authored sort behaves exactly as before. ✅ Both facts pinned in the synth test rather than argued in prose.
Counter-probe on the negative case
A child with no default list-view sort produces a working, scoped, windowed related list carrying no $orderby — key absent, ⛔ not present-and-empty — with $filter and $top asserted as live controls, so "nothing inherited" cannot read as "nothing fetched". An unusable sort (a number, a bare string array, an entry without field) is dropped, never guessed.
Declared narrowing, correctly reasoned
check:eager-closure was not run: it refuses as a broken gauge without apps/console/dist/eager-closure.json from a console vite build, which is CI's job. ✅ Safe to leave there rather than merely unrun — the one new import edge is deriveRelatedLists.ts → @object-ui/core, and app-shell/src/utils/index.ts (the barrel re-exporting it) already imports @object-ui/core, so the edge adds nothing to that closure. That is an argument, not a shrug.
Out of scope, and correctly not re-filed
Both adjacent defects were re-confirmed and found to be already open — objectui#4286 (a synthesized Reference Rail page is unpersistable: the aside region carries a className that PageRegionSchema refuses) and objectui#4664 (the same emit point ignores relatedListFilter). ✅ Searched before deciding, by keyword and file path; nothing duplicated.
Downstream: objectstack#11345 is pm:blocked on this card and unblocks on merge.
Activity
claude commentedon Aug 23, 2026
Triage: parked
pm:blocked(line in body) on objectstack-ai/objectstack#11345 — the maintainer-filed contract question (where does a derived related list's sort declaration live: field-levelrelatedListSort, inherit the child list view'ssort, or none) is now in the decision inbox with a four-prism block. This card is the component-side consumer of whatever is ruled; contract-first, the spec/engine half decides the shape.domain:ui, Feature.Generated by Claude Code
os-steve commentedon Aug 23, 2026
Unblock relay from the objectstack spec seat (session_01T9cDbY2NBiVJWYx3BpWfH2): the block inverted — this card is now the implementation leg.
pm:blocked→pm:queuein this stroke.sort. No new spec key; field-levelrelatedListSortwas explicitly not approved.app-shell/src/utils/deriveRelatedLists.ts:133-146(re-dropped atRecordDetailView.tsx:1955-1978andplugin-detail/src/synth/buildDefaultPageSchema.ts:174-188/683-690);$orderbyassembly atplugin-detail/src/RelatedList.tsx:519-529, fed viarenderers/record-related-list.tsx:211. objectstack's share is already in place and needs no change: theisDefaultexpanded-list-view resolver (spec/src/ui/view.zod.ts:4111-4198, already merged onto the child def asobj.listbyMetadataProvider.tsx:413-414), the ListView sort shape (view.zod.ts:1470-1486), and the declared-parsed-consumedrecord:related_list.sortprop (component.zod.ts:883-928) — the inheritance only needs to fill it.sortfromchild.list?.sortinderiveRelatedLists.ts(child def already in hand at :118); carry it at the two re-drop sites; renderer/RelatedList unchanged.'field desc'(view.zod.ts:1470) whilenormalizeSortSpecreads'field'/'-field'(RelatedList.tsx:258-266) — normalize to the array form or inherit only the array arm, or$orderbygoes out on a field literally namedseq_no desc.record:related_list.sortis dropped whenever the list leaves windowed mode (windowed goes false while the client text filter is active,RelatedList.tsx:383-389; the client path returns rows unsorted unless a column was clicked, :731-733). Affects the authored prop today and will affect the inherited sort identically. Also Auto-derived related lists consume the new field-levelrelatedListFilterdeclaration — list query AND-composed, tab badge honors the same filter #4664 (same emit point ignoresrelatedListFilter) was re-confirmed by this measurement.pm:blockedon THIS card and closes when this lands and the acceptance leg passes (the 10/20/30/40 repro from its body); the 17.0.0 migration prose then becomes true as written, no prose edit needed.Generated by Claude Code
yinlianghui commentedon Aug 24, 2026
Claim: PM loop round 38 (refill slot)
⚠️ Serial: #5874's rework is live in
Session:
session_01CSoz9uGhaaSgiq3hshtN7LBranch:
claude/issue-5795-related-list-inherit-sortWorktree:
objectui-issue-5795Domain:
domain:uiFile surface:
packages/app-shell/src/utils/deriveRelatedLists.ts,packages/app-shell/src/views/RecordDetailView.tsx,packages/plugin-detail/src/synth/buildDefaultPageSchema.ts+ tests + a changeset.RelatedList.tsxandrenderers/record-related-list.tsxare read-only — the relay says the renderer needs no change; if you find it does, ⛔ stop and report.Container & model:
M,mode:subagent,model: opusClause-②: no new key — the ruling explicitly declined one. But a derived descriptor gains a populated
sortwhere it had none, so a host observes new ordering. Declare it.packages/app-shell/src/utils/(resolveActionParams.ts,paramToField.ts). Different files — ⛔ do not touch either. #5725 is live inapp-shell/src/views/metadata-admin/previews/— ⛔ stay out. Enqueued and landing shortly: #6053, #6060, #6061, #6064, #6066, #6068, #6072.⛔ RULED — objectstack#11345, maintainer 2026-08-23 15:02Z, direction 1
⭐ The block inverted: this card is now the implementation leg, and objectstack#11345 is
pm:blockedon THIS card. It closes when this lands and its acceptance leg passes (the 10/20/30/40 repro in its body), and the 17.0.0 migration prose then becomes true as written — no prose edit needed. ⛔ Do not touch objectstack.The measurement is already done — use it, but re-derive before editing
From the spec seat's relay (full map in the os-dev-report on objectstack#11345, comment 5388863970):
app-shell/src/utils/deriveRelatedLists.ts:133-146— the child def is already in hand at:118.RecordDetailView.tsx:1955-1978andplugin-detail/src/synth/buildDefaultPageSchema.ts:174-188/683-690.$orderbyassembly:RelatedList.tsx:519-529, fed viarenderers/record-related-list.tsx:211— unchanged.isDefaultexpanded-list-view resolver (merged onto the child def asobj.listbyMetadataProvider.tsx:413-414), the ListView sort shape, and the declared-parsed-consumedrecord:related_list.sortprop. The inheritance only needs to fill it.Sketch (yours to refine): emit
sortfromchild.list?.sortinderiveRelatedLists.ts; carry it at the two re-drop sites.⛔ Do not inherit the string arm unnormalised. ✅ Whichever route you take, pin it: a child list view declaring the legacy string form must produce a correct
$orderby, or be provably skipped.⛔ Not yours to fix —
RelatedList.tsxis read-only here. ✅ But pin the interaction so the inherited sort's disappearance under the client filter is a recorded, deliberate fact rather than a surprise, and say in the report that the inherited sort inherits this hole too.relatedListFilter). ⛔ Separate card — do not fold.PM mechanism assumptions — measure, do not assume
app-shell: fix(console): trimhidden: truefields from the approvals drawer summary card #6031, refactor(app-shell): type RecordDetailView's confirm handler as the published ConfirmationHandler #6035;plugin-detail: fix(plugin-detail,i18n): localize record:path's container labels and announce its goal terminus #6000, fix(plugin-detail): give record:path one stage classification both rows read #6015, test(plugin-detail): pin record:alert's CTA honours resultDialog through the shared runner #6039). ⛔ Re-derive all five line references on your merge-base and report the delta.visibleWhenbinds nocurrent_user— position-gated visibility works on pages and per-option rules, but silently fail-opens on form fields #6010 hit today. Search for the drop, do not trust the list.child.listis populated byMetadataProvider.tsx:413-414in objectstack's half.deriveRelatedLists.ts:118at the moment you read it — an async metadata arrival that lands after derivation would make the inheritance silently empty, and an empty inherited sort is indistinguishable from today's behaviour.Verification
Direction predicted before running: a child object with a default list view carrying
sortmust produce a derived related list whose$orderbyreflects it; revert the emit and show the pin goes red. Assert the$orderbythat reaches the data source, not the descriptor — the descriptor is three hops from the wire and two of those hops are the re-drop sites.Counter-probes: (a) a child with no default list view sort must still produce a working related list with no
$orderby— otherwise inheritance is satisfiable by inventing an order; (b) an authoredrecord:related_list.sortmust still win / behave exactly as today — ⛔ the ruling adds inheritance, it does not change precedence, and if it turns out precedence is undefined here, stop and report.Prove every mutation on disk by grepping injected and separately removed text;⚠️ print the landing site, ⚠️ assert anchor uniqueness before writing, and ⚠️ restore under
trap … EXIT INT TERMwith a cwd-independent command (all three measured on other cards in this round).type-checkfor@object-ui/app-shelland@object-ui/plugin-detail(node_modulesred), path-filtered vitest, eslint with merge-base delta, and —pnpm check:esm-specifiersif you add ANY new relative import to a published package; it runs inside theType CheckCI job under a name that does not look like one, and a hand-derived gate set misses it.Changeset required. Draft PR,
Fixes #5795. ⛔ Do not mark ready, ⛔ do not enqueue, ⛔ no auto-merge — the PM lands it. Post the terminal report here (plain-text first-line markeros-dev-report; ⛔ avoid short angle-bracket placeholders in prose) and return it.Generated by Claude Code
feat(app-shell,plugin-detail): derived related lists inherit the chil…
test(plugin-detail): prefer-const in the windowed-sort pin's fake bac…
yinlianghui commentedon Aug 24, 2026
os-dev-report
{ "issue": 5795, "status": "done", "branch": "claude/issue-5795-related-list-inherit-sort", "pr": "https://github.com/objectstack-ai/objectui/pull/6088", "premise_still_valid": true, "summary": "Implements the objectstack#11345 ruling (maintainer 2026-08-23, direction 1): a derived related list inherits the child object's default list view sort, with NO new spec key. deriveRelatedLists emits `sort` from the child def's merged default list view; RecordDetailView and buildDefaultTabs' relatedNode carry it to the `record:related_list` node; RelatedList.tsx and renderers/record-related-list.tsx are unchanged, as the relay predicted. The two `sort` surfaces share a union but mean different things by its string arm (ListView's legacy space-separated 'seq_no desc' vs the related list's 'field'/'-field'), so the inherited value is normalized to the array arm ONCE at the derivation, through @object-ui/core's convertSortToQueryParams — the repo's single definition of both dialects — rather than by a tolerant reader downstream. Three PM mechanism assumptions were measured and two moved: five of the nine relay line references had drifted (RecordDetailView +11, others -3..+2), and MetadataProvider.tsx is in THIS repo, not objectstack. A THIRD re-drop site exists (buildDefaultPageSchema.ts:940, into record:reference_rail entries) and is deliberately excluded because ReferenceRailEntrySchema is $strict with no `sort` — the objectui#5494 class — pinned as its own leg." , "tests": "All at final commit 5d599be9c on a clean tree (union re-run after the last commit).\n\nNEW: app-shell/src/utils/__tests__/deriveRelatedLists.inheritSort.test.ts (9), app-shell/src/views/RecordDetailView.relatedListInheritedSort-5795.test.tsx (5, the end-to-end $orderby pin through the real page), plugin-detail/src/synth/__tests__/buildDefaultPageSchema.relatedSort.test.ts (6), plugin-detail/src/__tests__/RelatedList.sortDroppedOutsideWindowed.test.tsx (4, the recorded hole).\n\nGATES (exit captured before any pipe; each quotes the gate's own line):\n- `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell' --filter '@object-ui/plugin-detail' type-check` EXIT=0 — `Scope: 2 of 47 workspace projects`, `packages/app-shell type-check$ tsc --noEmit && tsc -p tsconfig.test.json` -> `Done`, same for plugin-detail. Non-zero scope + echoed script name rules out the silent zero-match green.\n- dependency-closure build (`--filter '@object-ui/app-shell^...' --filter '@object-ui/plugin-detail^...' build`) EXIT=0, under the shared verify lock (`VERDICT command-exit 0 - held 121s - waited 77s`).\n- path-filtered vitest, 12 files incl. the pre-existing neighbours (deriveRelatedLists.test.ts, all synth tests, serverpagination, speclimit, headerSort, relationalSort, unmaterializedSort): `Test Files 12 passed (12)` / `Tests 172 passed (172)`, EXIT=0. Whole-package app-shell vitest NOT run, per dispatch.\n- eslint over the merge-base delta (7 files), plain `eslint` with no --no-inline-config: EXIT=0, zero error lines (one real `prefer-const` error found and fixed; the remaining 217 are pre-existing `no-explicit-any` warnings).\n- check:esm-specifiers 0 (run per the new standing rule, though the one new import is a package specifier, not relative), check:phantom-deps 0, check:self-import 0, check:control-bytes 0 (plus a direct `grep -naP` control-byte sweep of the delta), check:spec-symbols 0, check-changeset-presence 0 (`3 source file(s) of 2 released package(s) changed ... declares 1 changeset(s)`), check-changeset-no-major 0.\n- DECLARED NARROWING: check:eager-closure NOT run — it refuses as a *broken gauge* without apps/console/dist/eager-closure.json from a console `vite build`, which is CI's run. Safe to leave there rather than merely unrun: the one new import edge is deriveRelatedLists.ts -> @object-ui/core, and app-shell/src/utils/index.ts (the barrel that re-exports deriveRelatedLists) ALREADY imports @object-ui/core, so the edge adds nothing to that closure.\n\nREVERSE VERIFICATION — direction predicted in writing BEFORE running, three ablation legs, every cell matched. No rebuild leg needed or skipped: vitest.config.mts aliases @object-ui/app-shell and @object-ui/plugin-detail to their src/, so every subject resolves from SOURCE and no dist stands between an edit and a run (verified in the config, not assumed). Each leg asserted the mutation on disk by EXACT-SUBSTRING count before (want 1) and after (want 0) plus `git diff --numstat` = `0 1`, printed its landing site, asserted anchor uniqueness before writing, and restored under `trap ... EXIT INT TERM` with a cwd-independent `git -C` command; each leg reported `RESTORED ... 0 dirty entries`.\n L1 remove the emit -> inheritSort 7 failed/2 passed; wire 3 failed/2 passed; synth 6 passed; hole-pin 4 passed\n L2 remove re-drop site 1 -> inheritSort 9 passed; wire 3 failed/2 passed; synth 6 passed\n L3 remove re-drop site 2 -> inheritSort 9 passed; wire 3 failed/2 passed; synth 3 failed/3 passed\nThe legs that stayed GREEN were predicted too: the counter-probes assert ABSENCE, so a counter-probe going red would mean it was really a second subject. Only the wire test goes red on all three legs — which is exactly why the subject is the $orderby reaching dataSource.find and not the descriptor.\n\nCOUNTER-PROBES: (a) a child with no default list-view sort produces a working, scoped, WINDOWED related list carrying NO $orderby — key absent, not present-and-empty — with $filter and $top asserted as live controls so 'nothing inherited' cannot read as 'nothing fetched'; an unusable sort (number, bare string array, entry without `field`) is dropped, never guessed. (b) PRECEDENCE IS DEFINED, so no stop-and-report was needed: a hand-authored page carries its own record:related_list node and never enters this synthesizer (RecordDetailView synthesizes only when no page is assigned), and within the synthesized path the ONE producer of related[].sort is the inheritance while the synthesizer is a pure carrier. An authored sort behaves exactly as before; both facts are recorded in the synth test.\n\nA harness note worth carrying: the Related tab is opened via the URL (`?tab=related`, ADR-0054 C3) rather than a synthetic click — the list self-fetches when its tab mounts, and the child object is queried TWICE on that page (the tab strip's `$top:1,$count:true` count probe carries no ordering by design), so the helper selects the windowed page fetch explicitly. Without that, every counter-probe would have passed by reading the count probe." , "open_questions": [], "out_of_scope_findings": [ "NOT filed, already open as objectui#4286: a synthesized Reference Rail page is unpersistable — the `aside` region carries a `className` that PageRegionSchema refuses. Re-confirmed here and measured on that branch with NO `sort` anywhere, so it is independent of this change; it is why the third-site leg asserts entry shape instead of parsing the whole page.", "NOT filed, already open as objectui#4664: the same emit point ignores `relatedListFilter`. Re-confirmed by this measurement and deliberately not folded in, per dispatch.", "No new issues filed. Searched open issues for both findings before deciding (keyword + file path); both already exist." ] }Notes for the PM, outside the JSON:
mergeable_state: behind(main moved during the run); the PM owns the update.pm:blockedon this card; per the relay it closes when this lands and its 10/20/30/40 acceptance leg passes, and the 17.0.0 migration prose becomes true as written with no prose edit.record:related_list.sortis dropped whenever the list leaves windowed mode (the client text filter). Pre-existing on the authored prop; the inherited sort inherits it identically, and now reaches users who authored nothing.RelatedList.tsxwas out of this card's file surface, so it is recorded inRelatedList.sortDroppedOutsideWindowed.test.tsxrather than fixed — those assertions pin today's behaviour and should go red when someone closes it. Worth its own card.String.replace, and a replacement string containing a$followed by a backtick (a JSDoc citation of the field-name regex) was reinterpreted as the$\`` substitution pattern — splicing the entire preceding file back in.git diff --statstill read as a plausible insert-only diff (+149) and the injected-marker greps still counted 1. Only a structural check (the file's header block appearing twice) caught it. The helper now splices withslice()and neverreplace()`.Generated by Claude Code
yinlianghui commentedon Aug 24, 2026
PM review — ACCEPT, enqueued
PR #6088 at
5d599be9c. Gates read once at 16:36:30Z, armed off this run's ownstarted_at(16:23:52Z, ~12½ min).22 checks, 19 success + 3 skipped, 0 failing, 0 still running — read as every check green, not the required subset. Not a governed surface; marked ready and enqueued through the merge queue. ⛔ Nothing merged by hand.
Two of my dispatch assumptions were wrong, and the dev measured rather than inherited
RecordDetailView+11, others −3..+2), andMetadataProvider.tsxlives inobjectstack— it is in this repo. Both corrected by measurement on the merge-base. That is the third and fourth PM assumption falsified this round, which is the system working as intended: the dispatch's job is to name what I believe so it can be checked, ⛔ not to be right.The find the card did not name
A third re-drop site exists —
buildDefaultPageSchema.ts:940, intorecord:reference_railentries — and it is deliberately excluded becauseReferenceRailEntrySchemais$strictwith nosortkey. ✅ That is the objectui#5494 class, the same one #6066 landed on this morning, and it is pinned as its own leg rather than silently skipped. A silent skip here would have read as "two sites, both handled"; the pin makes the exclusion a measured fact with a reason.The design call, and why it is the right one
The two
sortsurfaces share a union but mean different things by its string arm —ListView's legacy space-separated'seq_no desc'versus the related list's'field'/'-field'. The inherited value is normalised to the array arm once, at the derivation, through@object-ui/core'sconvertSortToQueryParams— the repo's single definition of both dialects — ⛔ rather than by a tolerant reader downstream. ✅ A tolerant reader is how you get a third dialect; normalising at the producer is how you get one.And the ruling was executed as ruled: no new spec key (objectstack#11345, maintainer 2026-08-23, direction 1).
RelatedList.tsxandrenderers/record-related-list.tsxare unchanged, exactly as the relay predicted.Reverse verification — three legs, every cell predicted in writing first
$orderbyreachingdataSource.find, ⛔ not the descriptor.No rebuild leg was needed and that was verified, not assumed:
vitest.config.mtsaliases both packages to theirsrc/, so nodist/stands between an edit and a run. ✅ Stating why a step is absent beats omitting it silently.The harness note that would otherwise have been a false green
$top:1, $count:true) carries no ordering by design, so the helper selects the windowed page fetch explicitly. Without that, every counter-probe would have passed by reading the count probe — a green that looks identical to a correct result. ✅ Recorded in the report rather than buried in the harness.Also right: the Related tab is opened via the URL (
?tab=related, ADR-0054 C3) rather than a synthetic click, so the list self-fetches when its tab mounts — the real path, not a simulated one.Precedence — no stop-and-report needed, and the reason is on the record
A hand-authored page carries its own
record:related_listnode and never enters this synthesizer (RecordDetailViewsynthesizes only when no page is assigned); within the synthesized path the one producer ofrelated[].sortis the inheritance, and the synthesizer is a pure carrier. An authored sort behaves exactly as before. ✅ Both facts pinned in the synth test rather than argued in prose.Counter-probe on the negative case
A child with no default list-view sort produces a working, scoped, windowed related list carrying no
$orderby— key absent, ⛔ not present-and-empty — with$filterand$topasserted as live controls, so "nothing inherited" cannot read as "nothing fetched". An unusable sort (a number, a bare string array, an entry withoutfield) is dropped, never guessed.Declared narrowing, correctly reasoned
check:eager-closurewas not run: it refuses as a broken gauge withoutapps/console/dist/eager-closure.jsonfrom a consolevite build, which is CI's job. ✅ Safe to leave there rather than merely unrun — the one new import edge isderiveRelatedLists.ts → @object-ui/core, andapp-shell/src/utils/index.ts(the barrel re-exporting it) already imports@object-ui/core, so the edge adds nothing to that closure. That is an argument, not a shrug.Out of scope, and correctly not re-filed
Both adjacent defects were re-confirmed and found to be already open — objectui#4286 (a synthesized Reference Rail page is unpersistable: the
asideregion carries aclassNamethatPageRegionSchemarefuses) and objectui#4664 (the same emit point ignoresrelatedListFilter). ✅ Searched before deciding, by keyword and file path; nothing duplicated.Downstream: objectstack#11345 is
pm:blockedon this card and unblocks on merge.Generated by Claude Code
feat(app-shell,plugin-detail): derived related lists inherit the chil…