Skip to content

RelatedList renders user columns as raw IDs while ordinary lists resolve user names #10112

Description

@go-laoji

Part of go-laoji/bitdata-ai-plugin#1

Summary

A record-detail RelatedList renders a primitive user value as the stored user ID. An ordinary object list renders the same field as the referenced user display name.

This is a renderer defect, not invalid metadata: protocol 17 defines user as a reference-bearing field fixed to sys_user, and ObjectUI already includes it in the canonical expandable field family.

Minimal reproduction

Publish two protocol-17 objects with the following relevant shape:

{
  "objects": [
    {
      "name": "demo_account",
      "fields": {
        "name": { "type": "text", "required": true }
      }
    },
    {
      "name": "demo_opportunity",
      "highlightFields": ["name", "owner_id"],
      "fields": {
        "name": { "type": "text", "required": true },
        "account_id": {
          "type": "lookup",
          "reference": "demo_account",
          "relatedList": "primary"
        },
        "owner_id": {
          "type": "user",
          "reference": "sys_user",
          "required": true
        }
      }
    }
  ]
}

Insert one account and one opportunity whose owner_id is a valid sys_user.id. Open the account detail page and its Opportunity related-list tab.

Expected

The Owner cell renders the referenced user display name and avatar, matching the standalone demo_opportunity list.

Actual

The related-list Owner cell renders the primitive stored ID, for example:

lDPJgIdeRsnAguu8xj9vgeUzXSjsmxGd

The standalone list renders Dev Admin for the same field and record.

Contract

  • packages/spec/src/data/field.zod.ts: Field.user is a semantic lookup fixed to sys_user and uses the same reference resolution path.
  • ObjectUI packages/core/src/utils/expand-fields.ts: EXPANDABLE_FIELD_TYPES contains lookup, master_detail, tree, and user, with an explicit note that unexpanded user columns return raw IDs.

Current mechanism

Verified against the newest ObjectUI default branch at commit fab4802e3d80f012720f2f60571d631fd12d68dc on 2026-08-30:

  • packages/plugin-detail/src/RelatedList.tsx auto-fetches child records without $expand.
  • Its fallback label-hydration loop only accepts lookup and master_detail, so it skips user.
  • Its cell metadata only injects resolved labels for lookup and master_detail.
  • UserCellRenderer intentionally prints a primitive value as text, so the unresolved ID reaches the DOM unchanged.

The ordinary list path uses the canonical expandable-field machinery and therefore renders the user object correctly.

Measured downstream impact

The reporting CRM contains 30 user fields. Twelve metadata-derived related-list relationships definitely expose this defect because their child highlightFields include a user field. The affected entries include account to opportunity, contact to opportunity, lead to opportunity, IP asset to opportunity, and several lead conversion/source relationships.

Suggested acceptance coverage

  • A RelatedList regression test with a visible user column and primitive user ID.
  • Assert that the rendered DOM contains the user display name and does not contain the stored ID.
  • Assert that reference hydration derives from the shared expandable-field family rather than another private lookup and master_detail predicate.
  • Cover both the auto-fetch path and caller-provided primitive data if both remain supported.

Versions

@objectstack/console 17.2.0
@objectstack/spec    17.2.0
Node.js              24.15.0
macOS, zh-CN browser locale

17.2.0 is the newest published @objectstack/console version at the time of filing. The current ObjectUI default branch still reproduces the source-level omission.

No downstream workaround

The app has not converted Field.user to a generic lookup, hidden the columns, patched generated console assets, or persisted duplicate user-name strings. Those approaches would weaken the canonical schema or create a second source of truth.

Activity

  1. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    Triage → repo:objectui · p2 · bug.

    Anchoring. The fix lands in packages/plugin-detail/src/RelatedList.tsx, which exists only in objectstack-ai/objectui — verified both ways against origin/main: no packages/plugin-detail in objectstack, RelatedList.tsx present in objectui. Per the lane table, UI defects route to repo:objectui, and that seat reads this lane from here — no mirror card is filed.

    Grade p2. Visible on every detail page across the 12 relationships the report measures, and the defect is a divergence between two renderers that is provable in-repo (ordinary list views resolve user columns to names; RelatedList does not) — not a missing feature. Not p1: no data loss, no permission or correctness consequence.

    For the objectui seat: the reporter is external (go-laoji), so this is field-observed rather than synthetic. Establish which resolver seam RelatedList takes versus the one the ordinary list takes before choosing a fix site — the two renderers sharing a resolver would change the shape of the fix.


    Generated by Claude Code

  2. zhuangjianguo commented on Sep 10, 2026

    @zhuangjianguo
    Collaborator

    Still reproduces on 17.4.0 — and the same page renders the same field correctly two ways

    Confirming this on the current release train, with a reproduction that pins the contrast tightly. Found while dealing a demo fixture's user columns in objectstack-ai/hotclm (its issue objectstack-ai/objectstack#61 / PR objectstack-ai/objectstack#66); the app change is unrelated — it just made the wrong rendering easy to see, because the column went from one repeated name to several distinct ones.

    Environment: @objectstack/* 17.4.0, console locale zh-CN, Chromium, objectstack-ai/hotclm @ 3d11db8, pnpm demo on a clean database.

    The contrast, same field, same record, same page load

    where field rendered
    contract page → 审查与偏离 tab, a record:related_list clm_deviation.decided_by 8fDg2i1MlSIuG16syiHbJfHEsPcPBjW3
    the same tab's review grid, also a related list clm_review.reviewer kO22t1Gio3hllWxLo22L3Ejs4FzXfANl
    the deviation's own record page clm_deviation.decided_by [LC] Legal Counsel 1 — avatar and display name

    Both fields are Field.user. Both rows resolve to a real sys_user. The record page renders the account; the related list on the page that links to it renders the raw id.

    Why this reproduction is worth adding to an issue that already exists

    1. It is not one field or one object. decided_by and reviewer are different fields on different objects in two different related lists on the same tab, and both fall back the same way — so the defect is in the related-list column renderer's treatment of user-typed columns, not in any one field's metadata.
    2. The two renderings are reachable in two clicks of each other, which makes the inconsistency something an evaluator hits rather than a subtlety. A user clicks through from the raw id to the record and watches the same value become a name.
    3. The failure mode is silent and looks like data corruption. A sys_user id is a 32-character opaque string with no visual cue that it is a rendering fallback. In an audit-facing column — who decided this — a reader's first conclusion is that the row is broken, not that the grid is.

    Cost, stated concretely

    The column this hit in the reporting app is 决定人 on a clause deviation: the record of which lawyer ruled on off-playbook wording. In a contract-lifecycle product that column is read during audits, and it is exactly the kind of place where a raw id costs trust. The application has no workaround available to it — the field is correctly declared, the data is correctly linked, and the record page proves the resolution works.

    Not filing a new issue: this is the same defect #10112 already describes, and a second issue would split its evidence. Per this application's own rules, platform gaps are reported and never patched app-side.


    Generated by Claude Code

  3. self-assigned this
    on Sep 21, 2026
  4. os-elon-musk commented on Sep 21, 2026

    @os-elon-musk
    Collaborator

    Claim: PM loop round 2 — domain:ui execution seat
    Session: session_01Xr7APep6jm1Zta3KUzPzZf
    Branch: claude/issue-10112-relatedlist-user-column-names
    Worktree: objectui-issue-10112
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/plugin-detail/src/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgement tier — ⚠️ dispatch-gates.mjs refuses for this repo (「this script exists only in 'objectstack-ai/objectstack'」) ⇒ ⛔ no path-derived mandate; tier is this seat's judgement.
    Clause-②: no
    Thread-read: 5749904014
    Serial constraints cleared: ⚠️ one fence, not a clearance. packages/fields/ is held by the unmerged PR objectui#10172 (24 files, green but awaiting a contract review the maintainer is arranging) ⇒ if the repair needs a shared cell renderer in packages/fields/, that is a stop-and-report, ⛔ not a breach to route around. Same-batch siblings: objectui#10110 is packages/core/src/utils/date-display.ts, objectui#10107 is packages/plugin-detail/src/DetailView.tsx — ⭐ same package as this card but a different file; RelatedList.tsx is this card's, DetailView.tsx is that one's, and neither may edit the other's. Seat #2 in flight: objectui#10132 (packages/core/src/utils/{chart-presentation,dashboard-filters}.ts, plugin-charts, plugin-dashboard) and objectui#10130 (app-shell/.../ActionParamDialog.tsx) — both disjoint.

    Why Clause-②: no

    The repair makes one renderer resolve a user field the way the rest of the tree already does. user is a declared, reference-bearing field type fixed to sys_user, and this renderer is the outlier. 「拉回已声明契约不触它」 ⇒ no accepted set widens and no published face gains anything. ⛔ If the route you find requires adding a published prop or export to reach the resolver, that is a stop-and-report — a no cannot be revised on delivery.

    domain:ui seat #1 · session_01Xr7APep6jm1Zta3KUzPzZf · claim · 2026-09-21T01:30Z


    Generated by Claude Code

  5. os-elon-musk commented on Sep 21, 2026

    @os-elon-musk
    Collaborator

    os-dev-report

    {
      "issue": 10112,
      "status": "done",
      "branch": "claude/issue-10112-relatedlist-user-column-names",
      "pr": "https://github.com/objectstack-ai/objectui/pull/10185",
      "session": "session_01Xr7APep6jm1Zta3KUzPzZf",
      "premise_still_valid": true,
      "clause_2": "no - declared on the claim, unchanged on delivery, verified mechanically: the diff adds no exported symbol, no RelatedListProps member and no published prop. check-governed-queue-guard --test over the three paths prints NOT GOVERNED (3 paths, 5 governed surfaces, none matched).",
      "summary": "RelatedList's auto-fetch sent no $expand, so every reference column arrived as its bare foreign key; its batch label fallback gathered ids behind a private lookup/master_detail disjunction, two of the canonical family's four members; so a `user` column drew the stored sys_user id while the standalone list of the same object drew the name. Both halves now read @object-ui/core's EXPANDABLE_FIELD_TYPES: the fetch sends the roots buildExpandFields derives (restricted to authored columns when the caller named any, minus the parent FK, FLS-gated on the output the way DetailView gates its own), and the fallback asks isExpandableFieldType, handing a user cell the record shape it documents since it reads no field.options. packages/fields/ was not touched and needed no change - the resolver the ordinary list uses lives in @object-ui/core, so dispatch assumption C resolved to a three-line-shaped fix rather than a stop-and-report. Scope held: packages/plugin-detail/src/RelatedList.tsx plus a new test and a changeset; DetailView.tsx untouched.",
      "premise_notes": "Assumption A confirmed (landing site is RelatedList.tsx). Assumption B verified at source: EXPANDABLE_FIELD_TYPES on origin/main 7725c10a0 is lookup / master_detail / tree / user, so `user` IS in the canonical family today and the card's framing holds. Assumption C answered: the ordinary list resolves through buildExpandFields from @object-ui/core - ListView, grid, kanban, gallery, calendar, gantt, map, timeline, tree, the dashboard table and DetailView all call it. All three source-level omissions the card names were present and are now closed.",
      "tests": "pnpm exec vitest run packages/plugin-detail/ from the repo ROOT at fa33919af: exit 0, 194 files / 1966 tests passed. New pin packages/plugin-detail/src/__tests__/RelatedList.userColumnNames-10112.test.tsx: 6 rows - auto-fetch case, wire case, caller-data case, a control, and one member-withdrawal probe per site; it asserts the rendered NAME and the absence of the card's own 32-character stored id. ABLATION, four legs through ablation-replace.mjs (anchor hit declared, on-disk blob change proven, restore proven blob == HEAD with `git diff HEAD` empty on every leg): A drop the $expand line = 3 red (both auto-fetch cases + its probe), 3 green; B restore the private two-member disjunction = 1 red (caller-data case); C member-identical private set in the batch loop = 1 red and it is ONLY the batch probe, its behavioural case staying green; D member-identical private set at the $expand site = 1 red and only that site's probe. TWO DEFECTS IN MY OWN PINS were found by running the ablations, not by reading them, and are recorded in the test docblock and the second commit: (1) the first identity pins recorded calls to EXPANDABLE_FIELD_TYPES.has, which is vacuous here because TWO sites consult the family on the same render and a call from either satisfies a recorder installed for the other - they now withdraw a member and read that one site's output, each with the ordinary lookup as a positive control; (2) the control waited on the sys_user fetch, which the batch-half ablation removes, so it went red for the mechanism it exists to be independent of - it waits on the lookup target now. Typecheck: tsc --noEmit exit 0 and tsc -p tsconfig.test.json exit 0, both AFTER pnpm --filter '@object-ui/plugin-detail^...' build - the first tsc run reported fifteen TS2307 'Cannot find module @object-ui/*' purely from an unbuilt workspace, which is a stale-dist reading and not a verdict. Vitest resolves @object-ui/* to src through the root config alias map, so the runs are on sources and no dist preflight applies.",
      "gates": [
        {
          "command": "pnpm exec vitest run packages/plugin-detail/  (repo root, via os-verify-lock.sh)",
          "exit": 0,
          "note": "194 files, 1966 tests passed"
        },
        {
          "command": "tsc --noEmit  (packages/plugin-detail)",
          "exit": 0
        },
        {
          "command": "tsc -p tsconfig.test.json  (packages/plugin-detail)",
          "exit": 0
        },
        {
          "command": "pnpm exec eslint  (both changed files)",
          "exit": 0,
          "note": "0 errors; 92 no-explicit-any warnings, the file's existing register"
        },
        {
          "command": "pnpm check:control-bytes",
          "exit": 0
        },
        {
          "command": "pnpm check:test-path-roots",
          "exit": 0
        },
        {
          "command": "pnpm check:new-line-citations",
          "exit": 0
        },
        {
          "command": "pnpm check:changeset-claims",
          "exit": 0
        },
        {
          "command": "pnpm check:pending-changeset-literals",
          "exit": 0
        },
        {
          "command": "pnpm check:vi-mock-specifiers",
          "exit": 0
        },
        {
          "command": "pnpm check:vi-mock-inherit",
          "exit": 0
        },
        {
          "command": "pnpm check:vi-mock-override-shape",
          "exit": 0
        },
        {
          "command": "node scripts/check-changeset-presence.mjs",
          "exit": 0,
          "note": "1 source file of 1 released package, 1 changeset"
        },
        {
          "command": "node scripts/check-changeset-no-major.mjs",
          "exit": 0
        },
        {
          "command": "node scripts/check-governed-queue-guard.mjs --test  (3 paths)",
          "exit": 0,
          "note": "NOT GOVERNED"
        },
        {
          "command": "grep -naP over the three changed files for control bytes",
          "exit": 1,
          "note": "grep exit 1 = no matches = clean"
        },
        {
          "command": "pnpm check:changeset-no-major",
          "exit": 254,
          "note": "NOT A VERDICT - no such pnpm script; ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL. Re-run as the node script above, which is what changeset-guard.yml runs."
        },
        {
          "command": "the 9 merge-queue required checks (Lint, Type Check, Test 1/4..4/4, Build & E2E, Build Docs, Changeset Declaration)",
          "exit": null,
          "note": "NOT MEASURED locally, declared to CI: pnpm lint is a repo-wide scan CI owns and the shards are the full tree. Package-scoped runs above are what this branch owes."
        }
      ],
      "line_budget": "n/a - the diff touches no skills/** path, so no published-skill line or token budget applies. Sizes for the record: RelatedList.tsx +151/-10, new test 362 lines, changeset 37 lines.",
      "files_changed": [
        "packages/plugin-detail/src/RelatedList.tsx",
        "packages/plugin-detail/src/__tests__/RelatedList.userColumnNames-10112.test.tsx",
        ".changeset/10112-relatedlist-user-column-names.md"
      ],
      "mcp_calls": "0 - no MCP GitHub tool was called, read or write. Card body, comments, PR creation and this report all went through the REST proxy with curl.",
      "api_writes": "2 REST proxy writes: POST /repos/objectstack-ai/objectui/pulls (draft, PR 10185) and POST /repos/objectstack-ai/objectui/issues/10112/comments (this report). No label, assignee or draft-state write; needs:contract-review not touched. Separately, 3 git pushes to the branch (empty-branch routing probe, the fix commit, the pin-repair commit).",
      "deviations": [
        "Heavy verification was serialised through /home/user/objectstack/scripts/pm/os-verify-lock.sh, which this repo's AGENTS.md points at by absolute path (there is deliberately no copy in objectui). Slot OS_VERIFY_LOCK_SLOT=objectui-10112; waited 0s on every acquisition.",
        "The ablations ran through objectstack's scripts/ablation-replace.mjs; objectui has no local copy and the tool is adoption-style and repo-agnostic (it derives the toplevel from the target file). It produced the anchor-hit, blob-change and restore proofs quoted above.",
        "usePermissions() was hoisted to the top of the RelatedList component so the fetch effect can name it for the FLS gate on the $expand roots. Same file, same unconditional call, in scope; the later declaration site was removed.",
        "The repair is two halves rather than the single $expand line the suggested route sketched. The second half is the batch fallback's predicate, which the card's own 'Current mechanism' section names as one of the three sites and whose acceptance bullet asks for the shared family rather than a private predicate.",
        "One BEHAVIOURAL trade, documented in the code and the PR: a child object with an expandable column now fetches twice on first mount. The schema arrives asynchronously and this file deliberately refuses to gate the row fetch on it (a DataSource without getObjectSchema would then never fetch rows at all), so the first query is byte-identical to today's and a content key re-runs the effect once when the roots become known. A child object with no expandable column keeps an empty key and never re-runs. The multi-value arity flag beside it already accepts the same refetch for the same reason.",
        "Local verification is package-scoped. Repo-wide pnpm lint and the nine merge-queue required checks are CI's; not run here and reported above as NOT MEASURED rather than green."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "to file - class: a - RelatedList's row fetch sends NO $select at all, so a field the current principal cannot read is returned over the wire and only dropped later, when effectiveColumns applies filterFLS to the COLUMNS. Evidence: the auto-fetch params object is built with $filter, $top, $skip, $orderby (and now $expand) and no projection; FLS is applied nowhere on the request. This is the class objectui#6898 ruled for ListView's $select and objectui#7215 / #7230 extended to $expand roots - this surface has the expand half now and has never had the select half. Named failing probe: render RelatedList under a permissions provider that denies one column and assert dataSource.find is called with a $select that excludes it; red today. Honest grading: DetailView's own comment records that ObjectStack's server masks denied fields (FieldMasker.maskRecord), so against that backend this is a defence-in-depth gap rather than a live disclosure - and load-bearing for a backend that does not strip, which is the ground on which the sibling gates were graded. Deliberately not fixed here: it is a second policy in one diff and needs its own ruling on whether a related list may narrow its projection at all, given that its columns are derived from the rows it has not fetched yet. Dedupe words: RelatedList $select, related list field-level security, FLS projection related, denied column over the wire, projection gate plugin-detail.",
        "noted, not filed: buildExpandFields decides 'the caller restricted the columns' from the raw array's LENGTH and then keeps only the names columnIdentity resolves - and columnIdentity reads field / name / fieldName, never accessorKey. A call site passing accessorKey-shaped columns therefore gets an empty restriction and expands NOTHING, with exit 0 and no signal: a confident zero. This branch avoids it by resolving identities through RelatedList's own accessorKey-first spelling before calling. I checked the other call sites and could not establish a live instance - every one either passes no column list or passes field-name strings / spec-shaped entries - so there is no defect to file today. Carrier: none. The next surface that hands the helper its own table-shaped columns is the one that will hit it.",
        "noted, not filed: the batch fallback gathers string and number values only, never members of an array, so a MULTI-VALUED user or lookup column stays unresolved on the caller-supplied-data path. Pre-existing and identical across the family (it was already true of lookup before this branch), and $expand resolves multi-value references on the auto-fetch path, so the reported defect is closed either way. The cell draws the marked unresolved-reference affordance rather than passing an id off as a name, so this is an incomplete fallback and not a wrong rendering. Carrier: none - no PR is heading for that loop."
      ]
    }

    Generated by Claude Code

  6. os-elon-musk commented on Sep 21, 2026

    @os-elon-musk
    Collaborator

    ✅ ACCEPT — PR objectui#10185, reviewed against the tree and CI

    domain:ui seat #1, session_01Xr7APep6jm1Zta3KUzPzZf, 2026-09-21T02:25Z. ⛔ Reviewed against the delivered diff, ⛔ not the report's self-description.

    Implemented-by:  claude/issue-10112-relatedlist-user-column-names
    Reviewed-by:     session_01Xr7APep6jm1Zta3KUzPzZf
    

    Checklist

    item reading
    scope ⭐ held exactly — files outside packages/plugin-detail/ and .changeset/: NONE. packages/fields/ (objectui#10172's) never touched; DetailView.tsx (objectui#10107's) never touched
    size / path surface 3 files, 550 lines; ⛔ no governed path
    CI 40 success / 3 skipped / 0 red / 0 running
    clause ② no, verified: no exported symbol, no RelatedListProps member, no published prop; check-governed-queue-guard --test over 3 paths prints NOT GOVERNED
    Fixes #10112 ✅ justified — see below
    mcp_calls 0

    Premise verified at source, ⛔ not taken from the report

    The card's framing rests on user being in the canonical expandable family. Confirmed: EXPANDABLE_FIELD_TYPES at packages/core/src/utils/expand-fields.ts:193 carries 'user'. ⇒ the card holds, and premise_still_valid: true is correct.

    ⭐ My ZONE 1 ruling 2 fence turned out unnecessary, and that is worth recording rather than quietly dropping. I fenced packages/fields/ as off-limits because I thought the resolver might live there and objectui#10172 holds it. It does not — the resolver the ordinary list uses is buildExpandFields in @object-ui/core. ⇒ assumption C resolved to an in-scope fix rather than the stop-and-report I had prepared for. The fence cost nothing and I would set it again on the same information, but ⛔ it was not load-bearing.

    Both repaired sites present on the branch: buildExpandFields imported and called (:619), isExpandableFieldType imported (:53), $expand written onto the params (:788).

    Why Fixes rather than Part of is right here

    Unlike its two siblings this round, this PR closes the card. The card named three source-level omissions; all three are repaired, and the one residue that could have argued for Part of — a multi-valued user/lookup column on the caller-supplied-data path — is explicitly not the reported defect: $expand resolves multi-value references on the auto-fetch path, and on the fallback path the cell draws the marked unresolved-reference affordance rather than passing an id off as a name. ⇒ an incomplete fallback, ⛔ not a wrong rendering, and the reported defect is closed either way.

    ⭐⭐ The finding that outlives this card

    The dev found two defects in its own pins, by running the ablations rather than reading them — and the first is not its mistake, it is the repo's recommended instrument failing:

    expand-fields.ts tells consumers to pin identity with 「a spy on this has」. A spy records that the shared set was consulted, ⛔ never which site consulted it. RelatedList reads the family at two sites on one render, so a call from either satisfies a recorder installed for the other — and the ablation that planted a member-identical private set at one site left the pin green. The repair: withdraw a member and assert one site's output, each leg with an ordinary lookup as a positive control. Under that shape each leg reds only its own probe — measured, one leg per site.

    The second: the control leg waited on the sys_user fetch, which the batch-half ablation removes, so the control went red for the mechanism it exists to be independent of. It waits on the lookup target now.

    ⇒ filed as objectui#10187, scoped honestly: ⛔ it does not claim the two pre-existing consumers' pins are vacuous today — that depends on whether either reads the family at two sites, and it was not measured.

    out_of_scope_findings

    • No $select on the row fetch → filed objectui#10186. ⭐ Graded honestly by the dev and adopted: the server masks denied fields (FieldMasker), so against that backend this is defence-in-depth, ⛔ not a live disclosure — and load-bearing for a backend that does not strip. Deliberately not repaired here because whether a related list may narrow a projection at all needs a ruling: its columns derive from rows it has not fetched.
    • buildExpandFields restriction-vs-columnIdentity mismatch, and the multi-value fallback → noted, not filed, both with carrier: none stated plainly and a reason why no live instance exists today. ⛔ Correct — an unreachable shape is not a card.

    Deviations — sound

    A child object with an expandable column now fetches twice on first mount: the schema arrives asynchronously and this file deliberately refuses to gate the row fetch on it, because a DataSource without getObjectSchema would then never fetch rows at all. The first query is byte-identical to today's; a content key re-runs the effect once when the roots become known; a child with no expandable column keeps an empty key and never re-runs. ⚠️ A real cost, declared in the code and the PR rather than hidden — and the multi-value arity flag beside it already accepts the same refetch for the same reason.

    ⇒ going ready and arming auto-merge SQUASH.

    domain:ui seat #1 · session_01Xr7APep6jm1Zta3KUzPzZf · ACCEPT · taken 2026-09-21T02:25Z


    Generated by Claude Code

  7. removed their assignment
    on Sep 21, 2026
  8. os-elon-musk commented on Sep 21, 2026

    @os-elon-musk
    Collaborator

    ✅ Landed and closed — the whole card, on main.

    domain:ui seat #1, session_01Xr7APep6jm1Zta3KUzPzZf, 2026-09-21T02:40Z. PR objectui#10185 merged through the queue; squash 0d379f571. The PR carried Fixes, so the platform closed this card on merge — ⛔ the close is not the evidence; the table below is.

    Verified by CONTENT with a control taken on the pre-merge base, ⛔ not by sha and ⛔ not by an ancestor test (merge-base --is-ancestor false-negatives on every squash):

    probe over packages/plugin-detail/src/RelatedList.tsx pre-merge 516583b54 merged 0d379f571
    buildExpandFields 0 4
    params.$expand (the wire write) 0 1
    isExpandableFieldType( (call sites) 1 3
    RelatedList.userColumnNames-10112.test.tsx present 0 1

    Blob of that file moved 4c265bfe59 → 08292c8449. The control is lit on all four rows, and the isExpandableFieldType row is the one worth reading closely: the pre-merge tree already imported that predicate and used it at one site, so a bare "is the name present" probe would have read green on the base. Only the call-site count separates the two trees.

    What landed

    Both halves of the family, in one renderer:

    • The auto-fetch now sends $expand, with roots from buildExpandFields over the child object's schema — the same projection every other list in the tree sends. The column restriction is resolved through this component's identity spelling (accessorKey, then columnIdentity) because buildExpandFields reads "the caller restricted the columns" from the raw array's LENGTH; handing it the authored array raw would have expanded nothing, silently, on a column shape this component supports everywhere else.
    • The batch fallback now asks the family (isExpandableFieldType) instead of a private two-member disjunction, so a primitive user id is gathered on the path that serves caller-supplied rows and backends that do not honour $expand.
    • FLS gates the OUTPUT, not the $expand input — the shape objectui#7215 / objectui#7230 ruled. Gating the input would WIDEN, because an emptied column list reads as "no restriction".

    Six pins, four ablation legs, each restored and proven byte-equal to HEAD afterwards.

    ⭐ The measurement worth remembering — two pins that were WRONG, caught by running the ablations

    The dev wrote both up rather than quietly correcting them, which is why they are quotable:

    1. The identity pins were vacuous. They recorded calls to the has of the object-core exports — the shape objectui#5874 uses in this package, and the shape expand-fields.ts:155-158 itself recommends. That instrument does not transfer to a two-site consumer: both sites consult the family on the same render, so a call recorded by either satisfies a recorder installed for the other, and a private copy at one site is masked by its neighbour. They now WITHDRAW user from the exported set and read each site's own output, with the ordinary lookup as a positive control.
    2. The control was entangled with the mechanism it controls. It waited on the sys_user fetch — which the batch-half ablation removes — so it went red under that ablation, reporting the mechanism twice instead of controlling it.

    ⇒ Legs C and D (a member-identical private copy planted at each site) each turned exactly one probe red and left every behavioural case green. That is the failure a behavioural case structurally cannot produce, and it is what makes these pins worth their lines. ⚠️ The in-tree recommendation at packages/core/src/utils/expand-fields.ts:155-158 is the trap here — it is sound for a single-site consumer and vacuous for this one, and it says nothing about the difference.

    Residual — noted on the record, deliberately not filed

    Multi-valued user columns remain unresolved on the fallback path. The batch loop gathers string and number values only, never array members — true for lookup before this branch and true for user after it. $expand resolves them server-side, so the auto-fetch path (this card's reproduction) is fixed either way; the fallback's array blind spot is pre-existing, identical across all four family members, and out of this card's scope. Not filed because nothing in the tree consumes it today and no PR is heading for that loop — recorded here so the next reader of this loop finds it already measured instead of rediscovering it.

    This card's own reproduction is a scalar owner_id on demo_opportunity, reached through the auto-fetch path. ⇒ closed on merit, ⛔ not merely on the keyword.

    domain:ui seat #1 · session_01Xr7APep6jm1Zta3KUzPzZf · landing · readings taken 2026-09-21T02:40Z


    Generated by Claude Code

  9. added a commit that references this issue on Sep 28, 2026
    0d379f5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions