Repository navigation
RelatedList renders user columns as raw IDs while ordinary lists resolve user names #10112
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on 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 commented
on Sep 10, 2026 CollaboratorMore actionsStill 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
where field rendered contract page → 审查与偏离 tab, a record:related_listclm_deviation.decided_by8fDg2i1MlSIuG16syiHbJfHEsPcPBjW3the same tab's review grid, also a related list clm_review.reviewerkO22t1Gio3hllWxLo22L3Ejs4FzXfANlthe deviation's own record page clm_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
- It is not one field or one object.
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. - 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.
- The failure mode is silent and looks like data corruption. A
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
- It is not one field or one object.
os-elon-musk commented
on Sep 21, 2026 CollaboratorMore actionsClaim: PM loop round 2 —
domain:uiexecution 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.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:⚠️ 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 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:30Z
Generated by Claude Code
os-elon-musk commented
on Sep 21, 2026 CollaboratorMore actionsos-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 commented
on Sep 21, 2026 CollaboratorMore actions✅ 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.Implemented-by: claude/issue-10112-relatedlist-user-column-names Reviewed-by: session_01Xr7APep6jm1Zta3KUzPzZfChecklist
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 touchedsize / path surface 3 files, 550 lines; ⛔ no governed path CI 40 success / 3 skipped / 0 red / 0 running clause ② no, verified: no exported symbol, noRelatedListPropsmember, no published prop;check-governed-queue-guard --testover 3 paths prints NOT GOVERNEDFixes #10112✅ justified — see below mcp_calls0 Premise 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- No
$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
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.⚠️ 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:uiseat #1 ·session_01Xr7APep6jm1Zta3KUzPzZf· ACCEPT · taken 2026-09-21T02:25Z
Generated by Claude Code
- No
os-elon-musk commented
on Sep 21, 2026 CollaboratorMore actions✅ 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):probe over packages/plugin-detail/src/RelatedList.tsxpre-merge 516583b54merged 0d379f571buildExpandFields0 4 params.$expand(the wire write)0 1 isExpandableFieldType((call sites)1 3 RelatedList.userColumnNames-10112.test.tsxpresent0 1 Blob 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:
- The auto-fetch now sends
$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. - The batch fallback now asks the family (
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. - FLS gates the OUTPUT, not the
$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:
- The identity pins were vacuous. They recorded calls to the
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. - The control was entangled with the mechanism it controls. It waited on the
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 atpackages/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:40Z
Generated by Claude Code
- The auto-fetch now sends
- added a commit that references this issue
on Sep 28, 2026
Part of go-laoji/bitdata-ai-plugin#1
Summary
A record-detail
RelatedListrenders a primitiveuservalue 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
useras a reference-bearing field fixed tosys_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_idis a validsys_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_opportunitylist.Actual
The related-list Owner cell renders the primitive stored ID, for example:
The standalone list renders
Dev Adminfor 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.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
fab4802e3d80f012720f2f60571d631fd12d68dcon 2026-08-30:packages/plugin-detail/src/RelatedList.tsxauto-fetches child records without$expand.lookupandmaster_detail, so it skipsuser.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
highlightFieldsinclude 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
RelatedListregression test with a visibleusercolumn and primitive user ID.lookupandmaster_detailpredicate.Versions
17.2.0is the newest published@objectstack/consoleversion 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.userto 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.