Repository navigation
RelatedList renders user columns as raw IDs while ordinary lists resolve user names #10112
Description
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.useris a semantic lookup fixed tosys_userand uses the same reference resolution path.- ObjectUI
packages/core/src/utils/expand-fields.ts:EXPANDABLE_FIELD_TYPEScontainslookup,master_detail,tree, anduser, 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.tsxauto-fetches child records without$expand.- Its fallback label-hydration loop only accepts
lookupandmaster_detail, so it skipsuser. - Its cell metadata only injects resolved labels for
lookupandmaster_detail. UserCellRendererintentionally 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
RelatedListregression test with a visibleusercolumn 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
lookupandmaster_detailpredicate. - 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
os-warren commentedon Aug 31, 2026
Triage →
repo:objectui· p2 · bug.Anchoring. The fix lands in
packages/plugin-detail/src/RelatedList.tsx, which exists only inobjectstack-ai/objectui— verified both ways againstorigin/main: nopackages/plugin-detailin objectstack,RelatedList.tsxpresent in objectui. Per the lane table, UI defects route torepo: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
usercolumns 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
zhuangjianguo commentedon Sep 10, 2026
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 localezh-CN, Chromium,objectstack-ai/hotclm@3d11db8,pnpm demoon a clean database.The contrast, same field, same record, same page load
record:related_listclm_deviation.decided_by8fDg2i1MlSIuG16syiHbJfHEsPcPBjW3clm_review.reviewerkO22t1Gio3hllWxLo22L3Ejs4FzXfANlclm_deviation.decided_by[LC] Legal Counsel 1— avatar and display nameBoth fields are
Field.user. Both rows resolve to a realsys_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
decided_byandreviewerare 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.sys_userid 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
os-elon-musk commentedon Sep 21, 2026
Claim: PM loop round 2 —⚠️ ⚠️ one fence, not a clearance.
domain:uiexecution seatSession:
session_01Xr7APep6jm1Zta3KUzPzZfBranch:
claude/issue-10112-relatedlist-user-column-namesWorktree:
objectui-issue-10112Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/plugin-detail/src/(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default judgement tier—dispatch-gates.mjsrefuses 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:
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 inpackages/fields/, that is a stop-and-report, ⛔ not a breach to route around. Same-batch siblings: objectui#10110 ispackages/core/src/utils/date-display.ts, objectui#10107 ispackages/plugin-detail/src/DetailView.tsx— ⭐ same package as this card but a different file;RelatedList.tsxis this card's,DetailView.tsxis 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-②: noThe repair makes one renderer resolve a
userfield the way the rest of the tree already does.useris a declared, reference-bearing field type fixed tosys_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 — anocannot be revised on delivery.domain:uiseat #1 ·session_01Xr7APep6jm1Zta3KUzPzZf· claim · 2026-09-21T01:30ZGenerated by Claude Code
os-elon-musk commentedon Sep 21, 2026
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
os-elon-musk commentedon Sep 21, 2026
✅ ACCEPT — PR objectui#10185, reviewed against the tree and CI
domain:uiseat #1,session_01Xr7APep6jm1Zta3KUzPzZf, 2026-09-21T02:25Z. ⛔ Reviewed against the delivered diff, ⛔ not the report's self-description.Checklist
packages/plugin-detail/and.changeset/: NONE.packages/fields/(objectui#10172's) never touched;DetailView.tsx(objectui#10107's) never touchedno, verified: no exported symbol, noRelatedListPropsmember, no published prop;check-governed-queue-guard --testover 3 paths prints NOT GOVERNEDFixes #10112mcp_callsPremise verified at source, ⛔ not taken from the report
The card's framing rests on
userbeing in the canonical expandable family. Confirmed:EXPANDABLE_FIELD_TYPESatpackages/core/src/utils/expand-fields.ts:193carries'user'. ⇒ the card holds, andpremise_still_valid: trueis 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 isbuildExpandFieldsin@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:
buildExpandFieldsimported and called (:619),isExpandableFieldTypeimported (:53),$expandwritten onto the params (:788).Why
Fixesrather thanPart ofis right hereUnlike 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:$expandresolves 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.tstells consumers to pin identity with 「a spy on thishas」. A spy records that the shared set was consulted, ⛔ never which site consulted it.RelatedListreads 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 ordinarylookupas 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_userfetch, 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$selecton 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.buildExpandFieldsrestriction-vs-columnIdentitymismatch, 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⚠️ 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.
getObjectSchemawould 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.⇒ going ready and arming auto-merge SQUASH.
domain:uiseat #1 ·session_01Xr7APep6jm1Zta3KUzPzZf· ACCEPT · taken 2026-09-21T02:25ZGenerated by Claude Code
os-elon-musk commentedon Sep 21, 2026
✅ Landed and closed — the whole card, on
main.domain:uiseat #1,session_01Xr7APep6jm1Zta3KUzPzZf, 2026-09-21T02:40Z. PR objectui#10185 merged through the queue; squash0d379f571. The PR carriedFixes, 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-ancestorfalse-negatives on every squash):packages/plugin-detail/src/RelatedList.tsx516583b540d379f571buildExpandFieldsparams.$expand(the wire write)isExpandableFieldType((call sites)RelatedList.userColumnNames-10112.test.tsxpresentBlob of that file moved
4c265bfe59→08292c8449. The control is lit on all four rows, and theisExpandableFieldTyperow 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:
$expand, with roots frombuildExpandFieldsover 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, thencolumnIdentity) becausebuildExpandFieldsreads "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.isExpandableFieldType) instead of a private two-member disjunction, so a primitiveuserid is gathered on the path that serves caller-supplied rows and backends that do not honour$expand.$expandinput — 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:
hasof the object-core exports — the shape objectui#5874 uses in this package, and the shapeexpand-fields.ts:155-158itself 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 WITHDRAWuserfrom the exported set and read each site's own output, with the ordinary lookup as a positive control.sys_userfetch — 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-158is 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
usercolumns remain unresolved on the fallback path. The batch loop gathers string and number values only, never array members — true forlookupbefore this branch and true foruserafter it.$expandresolves 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_idondemo_opportunity, reached through the auto-fetch path. ⇒ closed on merit, ⛔ not merely on the keyword.domain:uiseat #1 ·session_01Xr7APep6jm1Zta3KUzPzZf· landing · readings taken 2026-09-21T02:40ZGenerated by Claude Code
fix(plugin-detail): a related list resolves `user` columns to names, …