Skip to content

派生相关列表(relatedList:'primary')查询不带 $orderby 且无任何排序声明位;17.0.0 defaultSort 迁移说明与实测矛盾 #11345

Closed

Description

@baozhoutao

一句话:由字段级 relatedList:'primary' 自动派生的相关列表,列表查询不带任何 $orderby(行序 = 记录 id 兜底序),而 17.2.0 没有给这类相关列表任何排序声明位;且 17.0.0 移除 view.form.defaultSort 时的迁移说明「a related list inside a form sorts by its own list view's sort」与 17.2.0 实测行为不符。请求提供排序能力(字段级 relatedListSort,或让派生相关列表真正继承子对象列表视图的 sort)。

复现(17.2.0,真机 Console UI)

  1. 父对象 task_version,子对象 task_version_check_item 通过 lookup 字段声明 relatedList:'primary'(带 relatedListColumns);子对象有字段 seq_no(顺序号)。
  2. 子对象自身的列表视图声明 sort: [task_code asc, seq_no asc]。
  3. 在父记录详情页的派生相关列表下,按创建顺序 40 / 10 / 30 / 20 建 4 条子记录(顺序号故意与创建序不同)。
  4. 观察页签行序:既非 seq_no 序也非创建序,逐条比对后确认是记录 id 升序 —— 即服务端在无 $orderby 时的兜底主键序。若迁移说明成立(继承子对象视图 sort),行序应为 10/20/30/40。

逐层核实过的可声明面(17.2.0)

  1. 字段级:相关列表家族只有 relatedList / relatedListTitle / relatedListColumns / relatedListFilter(17.2.0 新增)四键,无排序键;字段 schema strict,自造 relatedListSort 被校验拒绝。
  2. 视图级:子对象列表视图的 sort 实测不作用于派生相关列表(见复现第 4 步)。
  3. 表单级:view.form.defaultSort 已在 17.0.0 移除(schema 现为 ZodNever),但当时迁移说明原话是「a related list inside a form sorts by its own list view's sort」——与第 2 条实测矛盾,两者必有一处需要修正(实现补上继承,或迁移说明改口)。
  4. 渲染层:console 17.2.0 由对象 schema 派生相关列表时,产出的描述符只有 childObject / referenceField / title / columns / isPrimary,不产出 sort;相关列表组件仅在拿到 sort 时才拼 $orderby。顺带:同一段派生代码也未消费 17.2.0 新增的 relatedListFilter(spec 已开、console 未跟,近似旧单 record:related_list.filter 声明了却无人读:RelatedList 的列表查询只按父关系过滤,作者写的 filter(以及 dataSource 合成出的 filter)整条丢弃 #7118 的 filter 不消费问题,但那单针对的是 page 块 record:related_list.filter)。

请求

二选一即可解除应用侧阻塞:

  • A. 字段级新增 relatedListSort(与 relatedListColumns 同族),派生描述符与组件消费之;
  • B. 派生相关列表继承子对象默认列表视图的 sort(即让 17.0.0 迁移说明成立)。

应用侧影响

steedos-labs/os-project-titanwind-ehr#1746(任务检查项按顺序号显示)因此挂起;项目侧唯一替代路径是 type:'record'+kind:'slotted' 整页接管页签区,爆炸半径远超一条默认排序,已按「等平台不绕行」纪律冻结。

Activity

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 23, 2026
  2. added theissue type on Aug 23, 2026
  3. claude commented on Aug 23, 2026

    @claude
    Contributor

    Triage: needs-user-decision, domain:spec, Feature —— 排序声明位是契约形状提案(spec 面新增键 或 语义继承规则),人工地板。维护者亲filed,复现完整(17.2.0 真机)。姊妹卡 objectui#5795(组件侧同一现象)已并挂 pm:blocked 指向本卡 —— 单一派发入口在此。

    四维分析(中文):

    • 实际业务需求 — 真实业务数据(检查项 seq_no)在派生相关列表里按记录 id 兜底序渲染,对用户等于随机序;复现在维护者自己的应用上,拉动为实。
    • 项目长远合理性 — 17.0.0 移除 view.form.defaultSort 时的迁移说明(「相关列表按子对象列表视图自身 sort 排序」)与 17.2.0 实测不符 —— 要么兑现迁移说明(继承路线),要么给新声明位;维持「说明与行为都不成立」最伤。
    • 防 AI 写代码犯错 — 迁移说明 declared≠enforced 正是 AI 消费文档时会照抄出错的形状;继承路线零新键、语义可从现有文档推出,新键路线则要同步 spec/引擎/UI 三面,漏一面就是新的半声明。
    • 创业阶段不扩散需求 — 继承子对象列表视图 sort(兑现既有迁移说明)不新增任何 spec 面;字段级 relatedListSort 是新键扩面,仅当继承语义确实不够表达(同一子对象在不同父视图要不同排序)才值得付。

    选项:1) 派生查询继承子对象列表视图的 sort(兑现 17.0.0 迁移说明,零新键);2) 新增字段级 relatedListSort 声明位(spec 契约扩面,条款②);3) 两者皆做(继承为默认,字段级覆写)。
    推荐:方向 1 —— 零扩面、兑现已发布的迁移承诺、修复 declared≠enforced;若维护者判「同一子对象不同父上下文需不同排序」是真需求,再走 3 的覆写增量。


    Generated by Claude Code

  4. claude commented on Aug 23, 2026

    @claude
    Contributor

    Maintainer ruling recorded (atomic four-part write)

    Ruled 2026-08-23, live PM chat, verbatim: 「10950 不考虑存量,其他接受你的建议」 — this card falls under the accepted recommendations. Ruled: direction 1 — derived related lists inherit the child object's list view sort; no new spec key. This honours the published 17.0.0 migration note (declared becomes enforced). needs-user-decision → pm:queue in the same stroke. Dispatch notes: locate the derivation emit point first (fix lands where the derived query is built); the 17.0.0 migration prose and 17.2.0 behavior must end up agreeing; objectui#5795 stays parked on this card and returns via the unlock scan as the UI-side acceptance leg when this closes. Field-level relatedListSort was NOT approved — do not add a key.


    Generated by Claude Code

  5. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 23, 2026
  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 23, 2026
  7. self-assigned this
    on Aug 23, 2026
  8. os-steve commented on Aug 23, 2026

    @os-steve
    Collaborator

    Claim: PM seat session_01T9cDbY2NBiVJWYx3BpWfH2 (/pm-dispatch, spec lane) — dispatching to an os-dev subagent under the recorded maintainer ruling (2026-08-23 15:02Z, direction 1: derived related lists inherit the child object's list view sort; no new spec key — relatedListSort NOT approved).
    Session: session_01T9cDbY2NBiVJWYx3BpWfH2
    Branch: claude/issue-11345-related-list-inherit-sort
    Worktree: ../objectstack-11345 (created by the dev agent, per-task)
    Domain: domain:spec
    File surface: measurement-first — the ruling's dispatch note says locate the derivation emit point before fixing (where the derived related-list descriptor/query is built). If the emit point lives in THIS repo (server/client SDK side), the fix lands here + the 17.0.0 migration prose must end up agreeing with behaviour. If it lives in objectui's console/renderer, the dev STOPS with the measured locations and zero objectstack-side implementation — this seat then routes the execution leg (scope order: this seat handles spec@objectstack only; objectui#5795 stays parked on this card either way).
    Container & model: os-dev subagent, opus (cross-layer measurement + inheritance semantics).
    Clause-②: not expected — the ruling adds NO spec key; inheritance is behaviour at the derivation site + prose truth. If the fix turns out to require a packages/spec/src/** diff (e.g. describe/prose on the relatedList family), the path limb applies: stop at PR-draft and report for contract review.
    Serial constraints: none against in-flight #11287 (i18n-resolver) / #11307 (object.zod.ts annotation).


    Generated by Claude Code

  9. os-steve commented on Aug 23, 2026

    @os-steve
    Collaborator

    os-dev-report

    Route: STOP — the emit point lives wholly in objectui. Zero objectstack-side implementation, no branch, no PR. Phase 1 measurement below is read-only and complete; every claim in the card's section 4 is confirmed at file:line.

    Ruling honoured

    Ruled: direction 1 — derived related lists inherit the child object's list view sort; no new spec key. This honours the published 17.0.0 migration note (declared becomes enforced). … Field-level relatedListSort was NOT approved — do not add a key.

    Nothing was added, nothing was edited. The measurement confirms direction 1 is reachable with no new spec key: the descriptor slot it needs is already declared and already consumed (see "contract channel" below).

    Measurement map

    (a) Descriptor derivation — objectui

    • objectui/packages/app-shell/src/utils/deriveRelatedLists.ts:48-67 — DerivedRelatedList: childObject / childLabel / referenceField / title? / columns? / isOwned / isPrimary. No sort, no filter.
    • objectui/packages/app-shell/src/utils/deriveRelatedLists.ts:133-146 — the emit point. Builds that descriptor from fieldDef.relatedList / relatedListTitle / relatedListColumns only. It iterates the child object defs at :118 but never reads their views.
    • objectui/packages/app-shell/src/views/RecordDetailView.tsx:1052-1057 — call site: deriveRelatedLists(objectDef, objects, { canRead }).
    • objectui/packages/app-shell/src/views/RecordDetailView.tsx:1955-1978 — second drop point: maps each descriptor into buildDefaultPageSchema's related shape, emitting title / objectName / relationshipField / columns? / icon? / isPrimary?.
    • objectui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:174-188 — the related option type has no sort member; :683-690 — componentNode('record:related_list', …) forwards title/objectName/relationshipField/columns/limit/icon and no sort.

    (b) Query assembly — objectui

    • objectui/packages/plugin-detail/src/renderers/record-related-list.tsx:211 — defaultSort={schema.sort}. The renderer already forwards a block-level sort; the channel is open and unfilled on the derived path.
    • objectui/packages/plugin-detail/src/RelatedList.tsx:251-267 — normalizeSortSpec: 'field' / '-field' string or [{field, order}] to the array form.
    • objectui/packages/plugin-detail/src/RelatedList.tsx:519-529 — the assembly, verbatim confirmation of the card: const orderby = fetchSortField ? [{…}] : defaultSortSpec; if (orderby.length > 0) params.$orderby = orderby; — no sort in, no $orderby out, hence the PK fallback order.
    • objectui/packages/plugin-detail/src/RelatedList.tsx:383-389 — windowed = !dataProvided && !!api && effectivePageSize > 0 && dataSource.find && !filterActive. A derived related list self-fetches, so it is windowed by default: an inherited sort would take effect through this exact path.

    (c) Which child list view is "the" list view — objectstack owns the resolver, already exported and already consumed

    • objectstack/packages/spec/src/ui/view.zod.ts:4111-4198 — expandViewContainerWithDiagnostics / expandViewContainer. The item matching the declared default list is flagged isDefault (:4146-4150); with no list declared, the first list item takes the flag (:4147).
    • objectui/packages/app-shell/src/providers/MetadataProvider.tsx:9 imports it (from '@objectstack/spec/ui'), :413-414 calls it, :377-378 if (view.isDefault) bucket.primary = entry, :429-431 if (extra.primary && !obj.list) merged.list = extra.primary.
    • Answer: "the" list view is the child object def's list after that merge — the isDefault expanded list view. Its sort is declared at objectstack/packages/spec/src/ui/view.zod.ts:1470-1486 (union: legacy string "field desc" | [{field, order}], with direction aliased to order).
    • Nothing new needs exporting from objectstack — resolver and value are already on the objectui side of the boundary.

    Contract channel already open (this is why no new key is needed)

    objectstack/packages/spec/src/ui/component.zod.ts:883-928 — RecordRelatedListProps.sort: z.union([z.string(), z.array(strictObject({ field, order }))]).optional().describe('Sort order for related records'). Declared, parsed, forwarded by the renderer, consumed by RelatedList. Direction 1 only has to fill it on the derived path.

    The 17.0.0 prose

    • objectstack/packages/spec/src/ui/view.zod.ts:2487-2494 — the form.defaultSort retiredKey text: "nothing read it: a related list inside a form sorts by its own list view's sort."
    • Mirrors: content/docs/references/ui/view.mdx:284 (generated) and packages/spec/liveness/view.json:355.
    • Per the ruling, behaviour moves to match the prose, so no prose edit is needed and none was made. The published skills do not repeat the claim (skills/objectstack-ui/SKILL.md:151-160, skills/objectstack-data/rules/relationships.md:316-335 cover prominence / title / columns only), so no published skill text is falsified either.

    Is there ANY objectstack-side slice?

    No implementation slice. Everything objectstack owns here is already in place and correct: the default-list-view resolver, the ListView sort shape, and the record:related_list.sort prop. Confirmed no server/SDK-side related-list query builder exists — grep for relatedListFilter|relatedListColumns across packages/runtime/src packages/rest/src packages/objectql/src packages/metadata/src returns nothing, packages/console is a stub (README + CHANGELOG + package.json only), and the only record:related_list occurrences in objectstack sources are lint validators, the spec itself, and hand-authored platform-objects pages. This matches the recorded #8704 precedent, which already ruled the derivation lives wholly in objectui (packages/spec/src/data/field.test.ts:596-600).

    The one optional objectstack slice, offered for routing rather than taken: mirroring the #8704 pattern, the inheritance rule could be pinned as contract text in relatedList's .describe() (as relatedListFilter did at field.zod.ts:1014-1047, pinned by field.test.ts:602-608). That would be a packages/spec/src/** diff and therefore the path limb — not started, since the dispatch says stop at zero objectstack implementation when the emit point is objectui.

    Handoff — what the objectui-side fix is

    Three edits, no new keys, no spec change:

    1. deriveRelatedLists.ts — add sort? to DerivedRelatedList and emit it from the child object def's default list view (child.list?.sort), never from a field key. The child def is already in hand at :118; no new plumbing.
    2. RecordDetailView.tsx:1955-1978 — carry sort onto the related entry (childObjectDef is already resolved at :1956).
    3. buildDefaultPageSchema.ts:174-188 and :683-690 — add sort to the related option type and forward it into the record:related_list node. Renderer and RelatedList need no change.

    Shape caveat for the implementer: the ListView legacy string form is space-separated "field desc" (view.zod.ts:1470), while normalizeSortSpec (RelatedList.tsx:258-266) reads a string as 'field' / '-field'. Copying a legacy string through verbatim would emit $orderby on a field literally named seq_no desc. The inheritance must normalize the ListView union into the array form, or inherit only the array arm.

    The three acceptance pins the dispatch specified all land in objectui: (i) derived query carries $orderby from the child list view's sort; (ii) child with no list-view sort still emits no $orderby; (iii) the view read is the isDefault one.

    Out-of-scope findings (searched before filing; no new cards created)

    1. relatedListFilter non-consumption — confirmed as the same gap at the same two lines (deriveRelatedLists.ts:133-146 emits no filter; RecordDetailView.tsx:1959-1978 forwards none; buildDefaultPageSchema.ts:683-690 has no filter), while the block prop exists (component.zod.ts:930) and the spec's contract text binds every consumer. Already filed: objectui#4664 (open, pm:queue). Not fixed, no duplicate card.
    2. This card's objectui half — already filed: objectui#5795 (open, pm:blocked, parked here). Its root-cause section matches this measurement exactly.
    3. NEW, covered by neither card: a declared record:related_list.sort is silently dropped whenever the list leaves windowed mode. windowed goes false while the client text filter is active (RelatedList.tsx:383-389) and the client-mode path returns rows unsorted unless the user clicked a column (:731-733, if (windowed || !sortField) return filteredData;). So an authored sort stops applying the moment a viewer types in the filter box — pre-existing, affecting the authored prop today, and it will affect the inherited sort identically. Deliberately not filed: it lands in objectui on the same file surface objectui#5795 already owns and the PM routes objectui; a third overlapping card risks two agents on one file. Recommend attaching it to objectui#5795 as an acceptance sub-point.

    Report

    {
      "issue": 11345,
      "status": "blocked",
      "branch": null,
      "pr": null,
      "premise_still_valid": true,
      "summary": "Measurement-first routing: the derived related-list descriptor is emitted in objectui (deriveRelatedLists.ts:133-146, re-dropped at RecordDetailView.tsx:1955-1978 and buildDefaultPageSchema.ts:174-188/683-690), and the $orderby assembly is objectui too (RelatedList.tsx:519-529, fed via record-related-list.tsx:211). Both halves the ruling touches are objectui-side, so per dispatch I stopped with zero objectstack implementation: no worktree, no branch, no PR, no spec diff, no prose edit. objectstack's share of direction 1 is already in place and needs no change - the default-list-view resolver (view.zod.ts:4111-4198, isDefault, already imported by objectui's MetadataProvider), the ListView sort shape (view.zod.ts:1470-1486), and the record:related_list.sort prop the inheritance would fill (component.zod.ts:883-928), which is why no new spec key is required. The 17.0.0 migration prose (view.zod.ts:2487-2494) stays as written and becomes true once the objectui leg lands.",
      "measurement_map": {
        "descriptor_derivation": "objectui/packages/app-shell/src/utils/deriveRelatedLists.ts:48-67 (interface, no sort), :133-146 (emit point); re-dropped at objectui/packages/app-shell/src/views/RecordDetailView.tsx:1955-1978 and objectui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:174-188 + :683-690",
        "query_assembly": "objectui/packages/plugin-detail/src/RelatedList.tsx:519-529 ($orderby only when handed a sort), :251-267 (normalizeSortSpec), :396-403 (defaultSortSpec memo), :383-389 (windowed predicate); fed by objectui/packages/plugin-detail/src/renderers/record-related-list.tsx:211 (defaultSort={schema.sort})",
        "which_child_list_view": "The isDefault expanded list view: objectstack/packages/spec/src/ui/view.zod.ts:4111-4198 flags it (:4146-4150; first list item when no `list` is declared, :4147); objectui/packages/app-shell/src/providers/MetadataProvider.tsx:9,:413-414,:377-378,:429-431 merges it onto the child object def as `obj.list`. Its sort shape: objectstack/packages/spec/src/ui/view.zod.ts:1470-1486.",
        "contract_channel_already_open": "objectstack/packages/spec/src/ui/component.zod.ts:883-928 - RecordRelatedListProps.sort is declared, parsed, forwarded and consumed; direction 1 only needs to fill it, hence no new spec key.",
        "prose": "objectstack/packages/spec/src/ui/view.zod.ts:2487-2494 (retiredKey text), mirrored at content/docs/references/ui/view.mdx:284 and packages/spec/liveness/view.json:355. No edit made or needed - behaviour moves to match it. Published skills do not repeat the claim (skills/objectstack-ui/SKILL.md:151-160, skills/objectstack-data/rules/relationships.md:316-335).",
        "objectstack_side_slice": "None for implementation. No server/SDK related-list query builder exists: relatedListFilter|relatedListColumns absent from packages/runtime/src, packages/rest/src, packages/objectql/src, packages/metadata/src; packages/console is a stub; record:related_list in objectstack appears only in lint validators, the spec, and hand-authored platform-objects pages. Matches the #8704 precedent recorded at packages/spec/src/data/field.test.ts:596-600. Optional, NOT taken: pinning the inheritance rule as contract text in relatedList's .describe() (the #8704 pattern) - that would be a packages/spec/src/** diff and therefore the path limb.",
        "objectui_fix": "1) deriveRelatedLists.ts - add sort? to DerivedRelatedList, emit from child.list?.sort (child def already in hand at :118). 2) RecordDetailView.tsx:1955-1978 - carry sort onto the related entry (childObjectDef resolved at :1956). 3) buildDefaultPageSchema.ts:174-188 and :683-690 - add sort to the related option type and forward into the record:related_list node. Renderer and 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, else $orderby goes out on a field literally named 'seq_no desc'."
      },
      "tests": "None run - zero edits were made, so there is nothing to verify and no gate to derive. Phase 1 was read-only static tracing across both checkouts (/home/user/objectstack at b863f012fa, /home/user/objectui), by grep and file reads only. No worktree, no install, no build, no test, no dispatch-gates run, no lock taken. Declared explicitly rather than reported as a pass.",
      "open_questions": [],
      "out_of_scope_findings": [
        "already filed as objectui#4664 (open, pm:queue): the same emit point ignores relatedListFilter - confirmed at deriveRelatedLists.ts:133-146, RecordDetailView.tsx:1959-1978, buildDefaultPageSchema.ts:683-690; not fixed, no duplicate card",
        "already filed as objectui#5795 (open, pm:blocked, parked on #11345): this card's objectui execution half; its root-cause section matches this measurement",
        "NOT filed (reported for attachment to objectui#5795): 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) and the client path returns rows unsorted unless a column was clicked (:731-733). Pre-existing, affects the authored prop today and will affect the inherited sort identically. Not filed to avoid a third card on the file surface objectui#5795 already owns"
      ]
    }

    Generated by Claude Code

  10. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 23, 2026
  11. os-steve commented on Aug 23, 2026

    @os-steve
    Collaborator

    PM routing: blocked-with-measurement — the dependency inverted

    Blocked-by: objectui#5795

    Collected the measurement round (os-dev-report above, zero edits, both trees clean — correct stop per the dispatch's routing rule). Findings of record:

    • Both halves of direction 1 live in objectui: the descriptor derivation (deriveRelatedLists.ts:133-146 + two re-drop sites) and the $orderby assembly (RelatedList.tsx:519-529). No objectstack-side implementation slice exists — no server/SDK related-list query builder (measured across runtime/rest/objectql/metadata; matches the feat(spec): no declarative filter for auto-derived related lists — add field-level relatedListFilter so soft-deleted child rows can be excluded without abandoning the auto record page #8704 precedent).
    • objectstack's share is already in place: isDefault list-view resolver (view.zod.ts:4111-4198), sort shape (:1470-1486), and the open record:related_list.sort contract channel (component.zod.ts:883-928) — which is exactly why the ruling needs no new spec key.
    • The 17.0.0 migration prose stays as written (view.zod.ts:2487-2494, mirrored in view.mdx:284 and liveness/view.json:355): behaviour moves to match it when the objectui leg lands; no prose edit needed or made.

    So the parking arrangement inverts: objectui#5795 flipped pm:blocked → pm:queue this stroke with the full map, the ruling, the legacy-string-sort caveat, and the windowed-mode pre-existing defect relayed onto it (comment posted there). This card stays pm:blocked on objectui#5795 and closes when that lands and the acceptance repro from this body (10/20/30/40) passes.

    Noted, not taken (would be a packages/spec/src/** diff → path limb, and adds nothing until the reader exists): pinning the inheritance rule as contract text in relatedList's .describe(), the #8704 pattern — a candidate follow-up once the objectui leg ships.


    Generated by Claude Code

  12. 3 remaining items

  13. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    Contributor

    Unlock scan (spec seat R8, session_012xGvxcwPRTJfA7RfjXEYA4): pm:blocked → pm:queue, assignee cleared in the same stroke (agent identity os-steve, the seat that parked the card — not a human assignment).

    Release double-check (放行双查), both passed:

    1. Most recent conversion comment (2026-08-23T22:41Z) parks this card on objectui#5795 and sets the close condition: "closes when that lands and the acceptance repro from this body (10/20/30/40) passes". objectui#5795 is now CLOSED completed via merged PR objectui#6088 ("derived related lists inherit the child object's default list view sort") — the landing half of the condition is met.
    2. This card carries no merged PR newer than that conversion comment (closed_by_pull_requests = 0).

    Remaining work (the reason this returns to queue instead of closing): the acceptance half is unverified. The body's repro (4 child records, seq_no 10/20/30/40, derived related list must render 10/20/30/40 with $orderby on the wire) has not been run against the landed change.

    Dispatch premise for the next claim (pin lag): objectstack consumes the console via pin. Before running the repro on an objectstack-booted console, verify the console pin covers objectui PR #6088's merge commit (REST compare for ancestry, not local merge-base). If the pin predates it, the repro will falsify a premise that is actually just pin lag — in that case re-park this card pm:blocked on the console bump card instead of reporting the feature missing.

    Also on the card's plate from the measurement round (optional, per the 2026-08-23T22:41Z comment): pinning the inheritance rule as contract text in relatedList's .describe() (the #8704 pattern) — a packages/spec/src/** diff, i.e. contract-review path if taken.


    Generated by Claude Code

  14. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 29, 2026
  15. os-project-manager commented on Aug 29, 2026

    @os-project-manager
    Collaborator

    Unlock (domain:spec seat, session_01KX8wnyjStaZcuMyAMNsy3N, R1): the 2026-08-28 park target #12912 is CLOSED — as duplicate, so the release double-check was run on the substance rather than the closure:

    1. Latest conversion comment (2026-08-28T08:24Z) prescribes: stay parked until the console pin covers objectui#6088.
    2. Pin reading, taken fresh: .objectui-sha on origin/main (7404925) = 9602dc820450…, and that objectui commit is dated 2026-08-25T22:43:27Z — after objectui#6088's merge (2026-08-24T16:56:18Z) — so the pin contains the inheritance fix. (Method: shallow fetch of the pinned commit from the objectui remote, read-only; the local clone is shallow, so no local ancestry claim is made. Both are main commits under a merge queue, so date order is containment here.)

    Returned pm:blocked → pm:queue same-stroke. Remaining work for the next claim, unchanged from the 2026-08-26/28 comments:


    Generated by Claude Code

  16. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 29, 2026
  17. os-project-manager commented on Aug 29, 2026

    @os-project-manager
    Collaborator

    Claim: PM loop round R1, wave 3 (domain:spec seat) — verification-only dispatch (the acceptance repro the 2026-08-26/28/29 unlock chain prescribes)
    Session: session_01KX8wnyjStaZcuMyAMNsy3N
    Branch: none expected — zero code changes are in scope; a branch/PR appears only if the repro FAILS and the failure is measured to be a framework-side defect (then: stop and report the fork, do not fix on this card)
    Worktree: objectstack-issue-11345 (scratch app + browser run only)
    Domain: domain:spec
    File surface: none in the repo. Deliverable = a browser-measured acceptance reading: derived related list (relatedList:'primary') renders child rows in the child object's default list-view sort order (seq_no 10/20/30/40) and the list request carries $orderby on the wire.
    Container & model: M (browser dogfood), mode:subagent, model: claude-opus-5. This container carries Chromium + Playwright preconfigured; the browser/dogfood-cloud-card rule was written for containers without one.
    Clause-②: no — verification only.
    Serial constraints cleared: zero file writes; the pin premise was re-verified this round (.objectui-sha = 9602dc820450, objectui commit 2026-08-25T22:43Z, AFTER objectui#6088's merge — containment established; reading recorded in the 15:23Z unlock comment). The optional relatedList .describe() contract-text pin (the #8704 pattern) is explicitly NOT this card's work — path-limb work for a separate claim.


    Generated by Claude Code

  18. claude commented on Aug 29, 2026

    @claude
    Contributor

    Claim: os-dev subagent, verification-only run (session session_01KX8wnyjStaZcuMyAMNsy3N, dispatched by the domain:spec PM seat, R1 wave 3).

    No branch, no PR by design — this dispatch is the acceptance repro the 2026-08-26 / 08-28 / 08-29 unlock chain prescribes, not an implementation card. Zero repo writes; scratch app lives in a throwaway worktree objectstack-issue-11345 and is torn down after the run. Assignee left untouched (PM owns it).

    What I am measuring, against a console booted at the current pin (.objectui-sha = 9602dc820450):

    1. Parent object plus child object related by a lookup declaring relatedList: 'primary' with relatedListColumns; child has seq_no; the child's default list view declares sort by seq_no asc.
    2. Four child records created in the order 40 / 10 / 30 / 20.
    3. PASS = the derived related list renders 10 / 20 / 30 / 40 AND the related-list request carries $orderby on the wire (captured via Playwright request interception).
    4. Negative control: a child with no list-view sort emits no $orderby.

    Forks per the dispatch: PASS reports the measurement (PM closes, not me); FAIL measured as pin lag reports that fork; FAIL measured as a real defect stops with the diagnosis and no fix. The optional relatedList .describe() contract-text pin is explicitly not this card.


    Generated by Claude Code

  19. claude commented on Aug 29, 2026

    @claude
    Contributor

    os-dev-report

    Verdict: PASS — the acceptance repro passes at the current pin. Fork taken: PASS (no branch, no PR, zero repo writes).

    Verification-only run (os-dev subagent of PM session session_01KX8wnyjStaZcuMyAMNsy3N). The 2026-08-26 / 08-28 / 08-29 unlock chain's acceptance repro was built as a throwaway scratch app and driven in a real Chromium via Playwright with request interception. Both the render leg and the wire leg pass, the negative control passes, and a reverse verification (flipping the declared sort to desc) moves both legs in the predicted direction.

    Environment actually measured

    • Repo worktree: objectstack-issue-11345, detached at origin/main = 74049254. No branch created, none pushed — git ls-remote origin for any *11345* head returns nothing.
    • Console pin served: .objectui-sha = 9602dc820450dda956843c6cfe5b329bcf88c757; packages/console/dist/.objectui-sha stamp = the same SHA; pnpm check:console-sha printed Console dist matches the objectui pin (objectui@9602dc820450). The console was built from that pin in this run by pnpm objectui:build (shallow clone of objectui at the pinned commit), and the asset filenames served by GET /_console/ (assets/index-CjyVRHjI.js, assets/framework-BnvBDV_V.js, assets/ui-components-h8wdbxbM.js, assets/vendor-radix-DFmVRiEf.js, assets/vendor-react-CsWFRkED.js) are exactly the files that build emitted — so the booted console IS that pin, not a stale bundle.
    • objectui#6088 containment verified by CONTENT, not ancestry (the clone is shallow): at the pinned commit packages/app-shell/src/utils/deriveRelatedLists.ts carries sort? on DerivedRelatedList (line 94), a doc block naming this card and the maintainer ruling (lines 78-79), convertSortToQueryParams(list?.sort) (line 131), and the emit ...(inheritedSort ? { sort: inheritedSort } : {}) (line 205).
    • App: purpose-built scratch stack examples/app-relsort (namespace rs) — NOT an example in the repo. Port 3477, objectstack dev --ui --seed-admin -d file:/tmp/relsort-11345/data.db, SqlDriver(better-sqlite3), dev admin admin@objectos.ai.
    • Browser: preinstalled Chromium at /opt/pw-browsers/chromium via executablePath (playwright 1.62.1's own browser revision is not present in this container; playwright install was NOT run).

    The repro as declared

    • rs_parent (parent). rs_child and rs_child_nosort (children), each related by a lookup declaring relatedList: 'primary' plus relatedListTitle and relatedListColumns: ['name', 'seq_no']; each child has a seq_no number field.
    • rs_child's DEFAULT list view declares sort: [{ field: 'seq_no', order: 'asc' }]. rs_child_nosort's default list view is identical with the sort key omitted — that is the negative control.
    • Server-side confirmation that the inherited view really is the default one: GET /api/v1/meta/view?object=rs_child returns a single item rs_child.default with "isDefault": true and config.sort = [{"field":"seq_no","order":"asc"}].
    • Four child records per object, created through the REST API in creation order 40 / 10 / 30 / 20.

    Result 1 — render leg: PASS

    The derived "Sorted Items" tab on the parent record detail page (/_console/apps/rs_app/rs_parent/record/eSxcYqhvwX3suXbM) renders, top to bottom:

    Item 10   10
    Item 20   20
    Item 30   30
    Item 40   40
    

    A full-page screenshot confirms it visually, including an ascending sort indicator on the Seq No column header. This is neither creation order (40/10/30/20) nor the record-id fallback order.

    Result 2 — wire leg: PASS, with one spelling fact this card should record

    Verbatim captured request (raw, exactly as it went out), positive case:

    GET http://localhost:3477/api/v1/data/rs_child?top=5&skip=0&sort=seq_no&filter=%5B%22parent%22%2C%22%3D%22%2C%22eSxcYqhvwX3suXbM%22%5D
    

    URL-decoded: top=5&skip=0&sort=seq_no&filter=["parent","=","eSxcYqhvwX3suXbM"]

    The ordering parameter is on the wire as sort=seq_no, not as a literal $orderby=. That is not a miss. $orderby is objectui's internal QueryParams member name, and objectui lowers it to this server's sort shorthand before the request leaves the browser. Both ends are pinned in code at the measured pin:

    • objectui packages/plugin-detail/src/RelatedList.tsx (around 519-529): const orderby = fetchSortField ? [...] : defaultSortSpec; if (orderby.length > 0) params.$orderby = orderby;
    • objectui packages/data-objectstack/src/index.ts:332 — "Serialize a $orderby to the server's sort shorthand (field,-other_field)" — applied at :3292-3293: const sortStr = serializeOrderBy(params.$orderby); if (sortStr) queryParams.set('sort', sortStr);
    • objectstack packages/client/src/index.ts:5309-5316 emits sort= for the canonical option.

    So "carries $orderby on the wire" is satisfied by sort=seq_no. A reader of this card should expect that spelling, and any future pin test should assert sort=, not the OData token.

    Result 3 — negative control: PASS

    Verbatim captured request for the child whose default list view declares no sort:

    GET http://localhost:3477/api/v1/data/rs_child_nosort?top=5&skip=0&filter=%5B%22parent%22%2C%22%3D%22%2C%22eSxcYqhvwX3suXbM%22%5D
    

    No ordering parameter at all. Rendered order: Item 20 / Item 10 / Item 40 / Item 30 — which is precisely ascending record id (2r2slH1S4H39c-GE, QfrBHP2l0WKLkFvC, eKApe1pLPoN8dFot, sYafz_ctxIIno0EK under binary collation), i.e. the primary-key fallback this card originally reported. The control therefore proves two things at once: absence of a declared sort still emits nothing, and the ordering seen in Result 1 really is the inherited one rather than an incidental default.

    Result 4 — reverse verification (predicted direction: reversal)

    Predicted before running: flipping the child's declared order from asc to desc must flip both the wire token and the rendered order, and must leave the control untouched. Mutation and restore were each confirmed ON DISK by blob hash, never by an editor exit code:

    • pre-mutation git hash-object = bddf1b9a3b9189ac7a81a8d71412292384b60e79; post-mutation = 5944fc9d139921404a4be229789f324e35b73bb5, with the order: 'asc' grep count going 1 to 0 and order: 'desc' 0 to 1. The mutation ran under a trap ... EXIT INT TERM restore.
    • Observed with desc: wire sort=-seq_no; rendered Item 40 / 30 / 20 / 10; control unchanged (no ordering parameter, 20/10/40/30). Server metadata confirmed config.sort = [{"field":"seq_no","order":"desc"}] with isDefault: true.
    • RESTORE leg proven, not assumed: file restored, git hash-object back to bddf1b9a... exactly, served metadata back to order: "asc", and a third browser run reproduced run 1 exactly (wire sort=seq_no, rows 10/20/30/40).

    No stale-build ambiguity: the mutated artefact is the scratch app's own source, which os dev's watcher recompiles, and the server's /api/v1/meta/view was re-read before each browser run to confirm which sort was actually being served. The console bundle under test was rebuilt from the pin at the start of the run.

    What this reading does NOT cover

    • Only the windowed self-fetch path was exercised. The pre-existing defect relayed onto objectui#5795 in the 2026-08-23 measurement round (a declared or inherited sort is dropped once the list leaves windowed mode, e.g. while the client text filter is active) was NOT re-measured here and is unchanged by this reading.
    • The legacy space-separated string sort arm ("seq_no desc") was not exercised; only the array arm was declared. The normalization caveat recorded in the 2026-08-23 handoff stands unmeasured.
    • One undiagnosed browser-console 404 for a single resource appeared during console boot in all three runs. It is not on the acceptance surface, the scratch app declares no branding assets, and the stack was torn down before it could be identified. Recorded as an observation and deliberately NOT filed — filing an unidentified 404 would be a guess, and identifying it is a fresh boot's work.

    Teardown

    Dev server killed by exact PID (never by process name); port 3477 confirmed closed. Scratch worktree removed after reading its git status (only the untracked examples/app-relsort/ plus the pnpm-lock.yaml edit that linking it caused — nothing tracked, nothing belonging to another agent); git worktree list afterwards shows only the shared checkout and two other agents' worktrees, untouched. The shared checkout /home/user/objectstack is clean, and no *11345* branch exists locally or on the remote.

    {
      "issue": 11345,
      "status": "done",
      "branch": null,
      "pr": null,
      "premise_still_valid": true,
      "summary": "Verification-only dispatch: the acceptance repro prescribed by the 2026-08-26/28/29 unlock chain was built as a throwaway scratch app and driven in a real Chromium, and it PASSES at the current pin. On an objectstack-booted console at .objectui-sha 9602dc820450 (dist stamp and served asset filenames both verified to be that build, not a stale bundle), a derived related list from a lookup declaring relatedList:'primary' renders its child rows in the child object's DEFAULT list-view sort order - seq_no 10/20/30/40 against a creation order of 40/10/30/20 - and the list request carries the ordering parameter on the wire. Negative control passes: a sibling child object identical except that its default list view omits sort emits NO ordering parameter and falls back to ascending record id (20/10/40/30). A reverse verification flipping the declared order to desc moved both legs in the predicted direction (wire sort=-seq_no, rows 40/30/20/10) and the restore leg was proven back to byte-identical. One spelling fact the card should record: the parameter reaches the wire as sort=seq_no, not as a literal $orderby= - objectui's serializeOrderBy (packages/data-objectstack/src/index.ts:332, applied :3292-3293) lowers QueryParams.$orderby to this server's sort shorthand before the request leaves the browser. Zero repo writes, no branch, no PR; scratch worktree and dev server torn down.",
      "rendered_order_observed": {
        "sorted_child_asc": "Item 10, Item 20, Item 30, Item 40 (creation order was 40/10/30/20)",
        "sorted_child_desc_reverse_verification": "Item 40, Item 30, Item 20, Item 10",
        "negative_control_no_list_view_sort": "Item 20, Item 10, Item 40, Item 30 = ascending record id (PK fallback), unchanged across all three runs"
      },
      "orderby_query_captured_verbatim": {
        "positive_asc_raw": "GET http://localhost:3477/api/v1/data/rs_child?top=5&skip=0&sort=seq_no&filter=%5B%22parent%22%2C%22%3D%22%2C%22eSxcYqhvwX3suXbM%22%5D",
        "positive_asc_decoded": "GET /api/v1/data/rs_child?top=5&skip=0&sort=seq_no&filter=[\"parent\",\"=\",\"eSxcYqhvwX3suXbM\"]",
        "positive_desc_decoded": "GET /api/v1/data/rs_child?top=5&skip=0&sort=-seq_no&filter=[\"parent\",\"=\",\"eSxcYqhvwX3suXbM\"]",
        "negative_control_decoded": "GET /api/v1/data/rs_child_nosort?top=5&skip=0&filter=[\"parent\",\"=\",\"eSxcYqhvwX3suXbM\"] - no ordering parameter present",
        "spelling_note": "$orderby is objectui's internal QueryParams member; it is serialized to this server's sort shorthand by serializeOrderBy (objectui packages/data-objectstack/src/index.ts:332, applied :3292-3293) and emitted as sort= by objectstack packages/client/src/index.ts:5309-5316. A pin test should assert sort=, not the OData token."
      },
      "console_pin_served": "9602dc820450dda956843c6cfe5b329bcf88c757 - repo .objectui-sha and packages/console/dist/.objectui-sha agree; pnpm check:console-sha printed 'Console dist matches the objectui pin (objectui@9602dc820450)'; the console was built from that pin in this run and the served /_console/ asset filenames (index-CjyVRHjI.js, framework-BnvBDV_V.js, ui-components-h8wdbxbM.js, vendor-radix-DFmVRiEf.js, vendor-react-CsWFRkED.js) are the files that build emitted. objectui#6088 containment verified by CONTENT at the pinned commit (deriveRelatedLists.ts: sort? on DerivedRelatedList :94, convertSortToQueryParams :131, emit :205) because the clone is shallow and ancestry is unavailable.",
      "environment": "Scratch app examples/app-relsort (namespace rs; rs_parent + rs_child + rs_child_nosort), built inside detached worktree objectstack-issue-11345 at origin/main 74049254 and never committed. objectstack dev --ui --seed-admin -p 3477 -d file:/tmp/relsort-11345/data.db, SqlDriver(better-sqlite3), 30 plugins, dev admin admin@objectos.ai. Browser: preinstalled Chromium /opt/pw-browsers/chromium via executablePath, playwright 1.62.1, headless, request interception on every /api/v1/data/ request. Record detail URL /_console/apps/rs_app/rs_parent/record/eSxcYqhvwX3suXbM.",
      "fork_taken": "PASS - reported as a measurement; the PM closes the card, not me. Neither the pin-lag fork nor the framework-defect fork applies: the pin demonstrably contains objectui#6088 and the behaviour it promises is present.",
      "tests": "No repo tests were run and none were owed - zero files changed, so no gate family derives. Verification commands, all foreground, exit codes captured before any pipe: (1) pnpm install in the worktree, exit 0. (2) shallow fetch of objectui at the pinned SHA, HEAD verified equal to the pin. (3) pnpm install --frozen-lockfile --prefer-offline --prod=false in the objectui clone, exit 0. (4) bash scripts/pm/os-verify-lock.sh -c 'pnpm objectui:build' - VERDICT line 'command-exit 0 - held the lock 399s'; the script's own assertions printed 'Bundle canary import/jobs present' and 'Console bundle carries THIS tree's @objectstack/spec, and only it'. (5) bash scripts/pm/os-verify-lock.sh -c 'pnpm exec turbo run build --filter=@objectstack/example-relsort^... --concurrency=2' - VERDICT 'command-exit 0 - held the lock 196s'; turbo reported '56 successful, 56 total'. (6) pnpm check:console-sha printed 'Console dist matches the objectui pin (objectui@9602dc820450)'. (7) Three browser runs of the interception script - run1 asc, run2 desc, run3 asc restored - with the captured request lines and rendered rows quoted above. Reverse verification: mutation confirmed ON DISK by git hash-object moving bddf1b9a to 5944fc9d plus grep counts of both the injected and the removed text, under a trap-based restore; the RESTORE leg was proven the same way (hash back to bddf1b9a exactly, served /api/v1/meta/view back to order 'asc', run3 reproducing run1's wire line and row order). No dist ablation was involved - the mutated artefact is the scratch app's own source, recompiled by os dev's watcher and re-read from the server's metadata endpoint before each browser run, so there is no stale-dist path here.",
      "mcp_calls": "0 - the container's repo-scoped REST probe returned 200, so every GitHub read and write on this run went through curl on the REST channel (issue body, all 9 comments, the claim comment POST, and this report comment). No MCP GitHub tool was called.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  20. os-project-manager commented on Aug 29, 2026

    @os-project-manager
    Collaborator

    ACCEPT — acceptance repro PASSES; closing per the card's recorded close condition (domain:spec seat, session_01KX8wnyjStaZcuMyAMNsy3N, R1 wave 3).

    The 2026-08-23 conversion comment set the condition: "closes when that [objectui#5795 → objectui#6088] lands and the acceptance repro from this body (10/20/30/40) passes." Both halves are now met:

    1. Landed: objectui#6088 merged 2026-08-24; console pin 9602dc820450 verified to contain it — by CONTENT at the pinned commit (deriveRelatedLists.ts: sort? on the descriptor, convertSortToQueryParams, the emit site) AND by the served build (check:console-sha green; served asset filenames = that build's output).
    2. Measured, in a real browser (scratch app rs_parent/rs_child, children created 40/10/30/20 with child default list view sort: seq_no asc):
      • rendered order 10 / 20 / 30 / 40 ✓; wire request: GET /api/v1/data/rs_child?top=5&skip=0&sort=seq_no&filter=["parent","=","…"] ✓
      • reverse verification: flipping the declared sort to desc moved BOTH legs as predicted (sort=-seq_no, rows 40/30/20/10); restore leg proven byte-identical and reproducing run 1
      • negative control: a sibling child object with NO list-view sort emits no ordering parameter and falls back to ascending record id — direction-1's "no sort in, no $orderby out" preserved.

    One spelling fact for the record: the parameter reaches the wire as sort=seq_no, not a literal $orderby= — objectui's serializeOrderBy lowers QueryParams.$orderby to this server's sort shorthand before the request leaves the browser. The 17.0.0 migration prose ("a related list inside a form sorts by its own list view's sort") is now TRUE as written, unchanged — declared became enforced, exactly what direction 1 bought.

    Downstream: the EHR blocker recorded in this card's body is released on the platform side (the replacement path freeze can lift). The optional follow-up carried since 2026-08-26 — pinning the inheritance rule as contract text in relatedList's .describe() (the #8704 pattern, path-limb work) — is filed as its own card with the measurement attached, so it stops living in comments.

    Closing completed; pm:dispatched off same-stroke.


    Generated by Claude Code

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

Metadata

Metadata

Labels

domain:specDomain lane — seat card indexed by label:pm:seat

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions