You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(fields): a lookup option label renders the referenced object's titleFormat ABOVE its declared nameField (and above the field's own displayField) — the order objectui#9436's ruling C1 reverses on the record page #10343
Filed by the domain:ui execution seat (seat #1, session session_01BA3nKVUwKQJf8DBxrSVtNC) from objectui#9436's dev stop report (5820114435, out_of_scope_findings, class b). The seat re-read the site on origin/main before filing. ⛔ Not graded and not routed: domain:*, priority:* and type are the triage seat's.
Fact
recordToOption in packages/fields/src/widgets/LookupField.tsx labels a fetched lookup option in this order:
templated ?? // formatRecordTitle(record, <referenced object's titleFormat>)
record[displayField] ?? // the lookup FIELD's own displayField
unified ?? // the unified resolver (declared nameField, …)
…
⇒ When the referenced object declares both nameField and titleFormat, the option shows the rendered template. It does NOT show the declared name field, and it does not show the field's own displayField either.
Contract
@objectstack/spec 17.4.0, the titleFormat describe: 「[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence.」
objectui#9436's ruling (class-1 ruling 5657441402, letter C1, ratified 5814246926, maintainer 「同意」): the declared pointer outranks titleFormat. The record page (the header, the record:details dedupe, DetailView) is being moved to that order under objectui#9436. Lookup options are a separate surface, with their own pins, and the ruling does not name them.
⛔ This was not probed at runtime. It is a code reading at origin/main.
⛔ The fix and the place of displayField are not chosen. The field-level displayField is an explicit per-field choice, and whether it should outrank the object's declared pointer is part of the answer. The record-page order from objectui#9436 is the reference.
⛔ LookupField.tsx is currently held by objectui#10223 (another seat). Serialise behind it.
Triage: lands in @object-ui/fields ⇒ domain:ui, bug, priority:p3, pm:blocked Blocked-by #10223 (finding removed — graded); rationale: an INHERITED branch of objectui#9436's ruling C1 (the declared pointer outranks titleFormat, maintainer 「同意」), which the spec's own titleFormat describe also states ("an explicit nameField now takes precedence"); the lookup option label still puts the rendered titleFormat first — above the object's nameField and even above the field's own displayField — so the same record reads one way on its record page and another in a lookup dropdown; LookupField.tsx is held by objectui#10223, so it serialises behind it.
Acting on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「帮我排查一下 blocked 卡片,哪些需要更新状态帮我更新。」; where — the maintainer's chat with session session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-24. ⛔ Not a claim and not a dispatch: the card returns to its lane's take order. Candidate surfaced by check-half-states.mjs H19 on this sweep.
Claim: PM loop round 4 — domain:ui execution seat 2
Session: session_014mXUNuFomfj24w7s1pZzhN
Branch: claude/issue-10343-lookup-option-label-order
Worktree: objectui-issue-10343
Domain: domain:ui
Seat: domain:ui#2
File surface: packages/fields/src/widgets/LookupField.tsx (recordToOption's label order only), tests beside it, one .changeset/10343-…md (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: opus — dispatch-gates --tier: 「no path-derived mandate … floor sonnet · default opus · ceiling fable」; an inherited branch of objectui#9436 ruling C1 ⇒ default judgement tier
Clause-②: no
Thread-read: 5825028737
Serial constraints cleared: the blocker objectui#10223 is closed and its PR objectui#10341 merged. The record-page order this card follows landed as ba0b61a (PR objectui#10358, objectui#9436). Open-PR file lists read 2026-09-25T02:57Z: packages/fields appears only as package.json in draft PR objectui#8941. Live claims: seat 1's objectui#10291 (SelectField / RadioField) and seat 4's objectui#10301 (index.tsx / GridField.tsx) do not name LookupField.tsx.
Clause-②: no — the label order of a rendered option changes to the ruled precedence. No schema, export or accept set moves.
{
"issue": 10343,
"status": "done",
"branch": "claude/issue-10343-lookup-option-label-order",
"pr": "https://github.com/objectstack-ai/objectui/pull/10479",
"session": "session_014mXUNuFomfj24w7s1pZzhN (mode:subagent, the dispatching seat's id)",
"premise_still_valid": true,
"summary": "recordToOption (packages/fields/src/widgets/LookupField.tsx) now labels every lookup option with @object-ui/core getRecordDisplayName(objectDef, row, { titleField: declaredDisplayField }), the same call LookupCellRenderer makes: the field's declared displayField, then the object's nameField, then titleFormat, then derivation; the widget's private template renderer (formatRecordTitle) is deleted, so a double-brace titleFormat placeholder now renders like its single-brace twin. The undeclared `name` default moves below the object's declarations when a schema is present: this order is forced, because keeping `name` above the template reddens two existing pins plus triage's pin 3 (ablation A1), and the spec's displayField describe names the default as the referenced object's name/title. Without a schema the path is unchanged. Guard (Zone 1): the record page (ba0b61a) also ranks an explicit per-view pointer (DetailView primaryField, header schema.title, core options.titleField) above the declared pointer, so both surfaces agree on displayField's position and no decision is owed. The only deviation is that the PR-assignee write was refused by the session classifier (see deviations); the seat owes it.",
"hypotheses": {
"H1": "CONFIRMED on origin/main ab50ccc (LookupField.tsx unchanged since 0348bc9): recordToOption computed `templated` from formatRecordTitle and put `templated ??` first, above shown[displayField] and the unified resolver.",
"H2": "ba0b61a exported core `declaredNameField` and reordered PageHeaderRenderer / DetailView.resolveDisplayTitle / record:details; it takes no per-field displayField. Reused instead: core getRecordDisplayName's `options.titleField` rung (step 0), which already takes a per-field pointer. The surfaces agree on displayField's position (explicit per-view or per-field pointer first), so the guard was not tripped.",
"H3": "Pins 1 and 2 RED on base (both got 'Globex / HT-001'), GREEN on head. Pin 3 (only titleFormat) is GREEN on base and head. It is RED under ablation A1, the name-first order.",
"H4": "PARTLY FALSIFIED. All 9 call sites go through recordToOption, and the selected-value chip has no resolver of its own (it renders option.label). But the mount-time hydration effect captures refObjectSchema before getObjectSchema resolves, and its eslint-disabled deps never re-run it. So a hydrated bare-id chip is labelled on the no-schema path: the probe read 'Acme' where the dropdown reads 'HT-001'. This is pre-existing and reported as finding 2; it is not fixed here because the fix is a design choice, not a mechanical one."
},
"files_changed": [
"packages/fields/src/widgets/LookupField.tsx (recordToOption label order; formatRecordTitle deleted; declaredDisplayField split from DEFAULT_DISPLAY_FIELD; 9 call sites and 6 dep arrays pass declaredDisplayField, drop refTitleFormat; handlePickerSelectRecords deps now list refObjectSchema)",
"packages/fields/src/widgets/LookupField.labelOrder-10343.test.tsx (new, 6 pins)",
".changeset/10343-lookup-option-label-order.md ('@object-ui/fields': patch)"
],
"tests": "BASE ab50ccc, with LookupField.tsx byte-equal to base: `pnpm exec vitest run --maxWorkers=2 packages/fields/src/widgets/LookupField.labelOrder-10343.test.tsx` gave 'Tests 4 failed | 2 passed (6)'. Pin 1 got 'Globex / HT-001', expected 'HT-001'. Pin 2 got 'Globex / HT-001', expected 'AC'. The double-brace control got '{Globex} / {HT-001}'. The chip pin got 'Globex / HT-001'. Pin 3 and the blank-pointer control passed. The lock verdict read 'VERDICT command-exit 1'. The pins file changed afterwards only by swapping any for unknown (runtime-neutral). HEAD 600c330 verbose run: 'Tests 6 passed (6)', 'VERDICT command-exit 0'. ABLATIONS at 7ce2686 via ../objectstack/scripts/ablation-replace.mjs (anchor hit x1, anchor count 1 to 0, blob changed; restore gave 'blob == HEAD' and 'git diff HEAD is empty'; no dist involved, since the test imports ./LookupField and vitest aliases @object-ui/core to src). A1 moved the name guess above the resolver: 'Tests 7 failed | 20 passed (27)' across the labelOrder, displayFls-10373 and candidateExpand-10223 files, exactly the predicted set (pins 1 and 3, both template controls, the chip, and 2 existing pins; pin 2 green). A2 dropped the titleField: 'Tests 1 failed | 5 passed (6)', pin 2 only, 'expected [ HT-001, HT-002 ] to deeply equal [ AC, IN ]'. FINAL HEAD 600c330: `turbo run build --filter=@object-ui/fields^... --concurrency=2` gave 'Tasks: 10 successful, 10 total' (cached). @object-ui/fields `type-check` (tsc --noEmit && tsc -p tsconfig.test.json) gave 'VERDICT command-exit 0', and --listFiles contains the new test. `pnpm exec vitest run --maxWorkers=2 --shard=N/4 packages/fields/` from the repo root, 4 shards: 47+47+47+46 = 187 files, equal to the package's tracked test-file count; 760 + 734 (7 skipped) + 623 + 984 = 3101 passed, 0 failed, each shard 'VERDICT command-exit 0'. Gates at 600c330: check-changeset-presence gave '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'. check-changeset-no-major and check-changeset-overwrite: OK. check:new-line-citations: 'VERDICT new-cross-file-line-citations: 0 new citation(s)'. check:control-bytes: 'OK (scanned 8517 tracked text file(s)'. check-pending-changeset-literals: OK. check-changeset-claims: 'No pending changeset names a file this change touches'. check-test-path-roots and check-vi-mock-specifiers/inherit/override-shape: OK. ESLint was narrowed to the 2 changed files, with a declared, proven narrowing: 0 errors. The population is eslint's own config (isPathIgnored false 2 of 2); the file count from --format json is 2. Invariance: eslint.config.js uses tseslint.configs.recommended (not type-checked) with no parserOptions or projectService, and 0 of 23 eslint-rules files read the fs (the control matcher hits scripts/check-control-bytes.mjs). Per rule against base on LookupField.tsx: no-explicit-any 41 to 39, exhaustive-deps 4 to 3, others equal. No doc or README gate is reached (no README, docs or exports touched). NOT MEASURED: repo-wide pnpm lint and full pnpm test, which belong to CI. CI state at report time: in_progress (not awaited).",
"gates": "check-changeset-presence, check-changeset-no-major, check-changeset-overwrite, check:new-line-citations, check:control-bytes, check-pending-changeset-literals, check-changeset-claims, check-test-path-roots, check-vi-mock-specifiers, check-vi-mock-inherit and check-vi-mock-override-shape: all exit 0 at 600c330. fields type-check: exit 0. fields tests: 4 of 4 shards exit 0.",
"deviations": [
"PR assignee NOT set. `node scripts/pm/label-write.mjs --repo objectstack-ai/objectui --issue 10479 --assign os-elon-musk` was refused by the session's auto-mode classifier, reason '[External System Writes]'. It was not retried by any other route. The seat owes it: PR #10479 assignee os-elon-musk.",
"Four lock acquisitions returned 99 (queue-timeout). They were re-queued under the same OS_VERIFY_LOCK_SLOT, and every measurement cited was read from a later acquired run.",
"Two throwaway probe tests ran while the working tree was dirty. They were deleted before the final-head runs, and all cited final numbers are from the clean tree at 600c330."
],
"mcp_calls": "0",
"api_writes": "2: (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches (run 36094574886, success), which executed POST /repos/objectstack-ai/objectui/pulls with draft=true, giving PR 10479, body read back byte-identical (9964 bytes, 0 angle brackets); (2) this os-dev-report comment via post-stamped.mjs: POST /repos/objectstack-ai/objectui/issues/10343/comments. The label-write assign attempt was refused by the classifier before any request (0 writes). git push is not REST.",
"open_questions": [],
"out_of_scope_findings": [
"class: b · The browse-all record picker's display column renders titleFormat above that column's own value and the object's nameField, and leaves double-brace placeholders' braces in place. Contract, @objectstack/spec 17.4.0 titleFormat describe: '[DEPRECATED → nameField (ADR-0079)] … an explicit nameField now takes precedence …' and, of the two placeholder spellings (double-brace and single-brace), 'the title renderers treat the two as equivalent'. Seam: spec:ObjectSchema.titleFormat → renderer:RecordPickerDialog display column (lookupColumnDisplay.tsx renderLookupColumnValue, step 1). Evidence: a throwaway probe at 600c330 read, for an object with nameField contract_no and titleFormat '{party} / {contract_no}', dropdown ['HT-001'] but picker name column 'Globex / HT-001'; for the double-brace template, picker '{Globex} / {HT-001}'. The existing pin in candidateExpand-10223 ('C-0 - samepick_task_0' in the name column) shows the template-over-column behaviour too. After PR 10479 the dropdown and the picker read differently on objects that declare both keys. · dedupe words: renderLookupColumnValue titleFormat · picker display column nameField · RecordPickerDialog title column template · lookup picker double brace placeholder",
"class: a · A lookup value hydrated from a bare id on mount is labelled without the referenced object's schema. The hydration effect in LookupField captures refObjectSchema (null) before getObjectSchema resolves; its deps are eslint-disabled and exclude it, and already-hydrated ids are never relabelled. Repro (throwaway probe at 600c330): value 'c1', object nameField contract_no, row name 'Acme', contract_no 'HT-001' gives the chip 'Remove Acme', while the dropdown option reads 'HT-001'. The same record's chip reads 'HT-001' right after picking it (handleSelect caches the option) and 'Acme' after a reload. Base behaviour is by code reading only (not probed). · dedupe words: LookupField hydration refObjectSchema stale · lookup chip label before schema loads · hydrated lookup label ignores nameField",
"carrier: none for either finding · both are noted in PR 10479's Acceptance notes and left for the seat to card; both sit in @object-ui/fields next to #10343 and are not blocked by it"
]
}
✅ ACCEPT: objectui#10479 at 600c330; ready + auto-merge in this act
domain:ui seat #2, session_014mXUNuFomfj24w7s1pZzhN. I checked this against the diff and CI, not against the report's own account. It executes the lookup branch of objectui#9436 ruling C1, using triage's order (5820783707).
recordToOption now labels an option with @object-ui/core's getRecordDisplayName(objectDef, row, { titleField: declaredDisplayField }). That is the same call the read cell's LookupCellRenderer makes. The resolver's ladder (read at head): the field's own declared displayField (or reference_field), then the object's nameField and its aliases, then titleFormat, then derivation
reuse, not copy
the widget's private template renderer is deleted, as triage asked, and core's formatTitleTemplate renders instead. For single-brace templates it behaves the same (same placeholder regex, separator class and strip pass). Double-brace placeholders now normalize, which is what spec 17.4.0's titleFormat describe rules
the name guess
the undeclared name default now sits below the object's declarations when a schema is present. Keeping it above would redden triage's pin 3 (ablation A1), and the spec's displayField describe says it "defaults to the referenced object's name/title". The no-schema path is byte-equivalent to the base
triage's guard
the record page (ba0b61a), the page header and this surface all rank an explicit per-view or per-field pointer above the declared pointer, and the declared pointer above the template. They agree, so no return to the card is owed
call sites
all 9 recordToOption calls pass declaredDisplayField and the schema. handlePickerSelectRecords now lists refObjectSchema in its deps, which closes a latent stale closure
FLS
every rung reads withoutDeniedFields(row, readable) (objectui#10373), and all 9 calls pass a gate
contract review
PASS at CONTRACT_REVIEW_TIER on this head, record 5826895480 (the seat read the reviewer transcript: every turn at tier)
semver
'@object-ui/fields': patch. AGENTS.md sets no patch-versus-minor rule for this, and the fixed group bumps minor regardless. The objectui#9436 siblings chose minor, so the two differ, but no rule is broken
governed
NOT GOVERNED (3 paths)
CI
43 check-runs on 600c330: 40 success, 3 skipped by design, 0 red
closing keywords
the body's only closing reference is Fixes #10343
Findings, each with its disposition
The picker's display column (lookupColumnDisplay.tsx) still renders titleFormat first. On an object declaring both keys, the dropdown now reads nameField while the picker column reads the template. This PR creates that mismatch: before it, both read the template. It is the ruling's next branch and outside this claim ⇒ carded by the seat in this act: objectui#10486.
On mount, a lookup value hydrated from a bare id is labelled before the referenced schema loads, and it is never relabelled. The chip can show name while the dropdown, cell and record page show nameField. This is pre-existing, and fewer surfaces disagree than on the base ⇒ carded by the seat in this act: objectui#10487.
The reviewer found a second upgrade effect the changeset does not name. When an object declares neither key and derivation picks a field other than name (for example, name is an auto_number), the label moves from name to the derived field. This matches the cell renderer ⇒ Acceptance notes.
The PR body says the changeset level is "as dispatched", but the claim named no level ⇒ Acceptance notes.
The PR assignee is not set. The dev's label-write was refused by its classifier. That is the seat's matter, not a landing condition ⇒ reported to the maintainer.
State
ready and auto-merge (squash) go through the relay ops pr_ready + automerge_enable in this act.
Activity
objectstack-fleet commentedon Sep 24, 2026
Blocked-by: #10223
分诊首次定级:
priority:p3·bug·domain:ui·pm:blocked—— 关联字段下拉选项的标签优先用已弃用的titleFormat,排在对象声明的nameField和字段自己的displayField前面,与记录页按 #9436 裁决 C1 改过的顺序相反;LookupField.tsx正被 #10223 持有,排在它后面Path:
packages/fields/src/widgets/LookupField.tsx(recordToOption:templated ?? record[displayField] ?? unified ?? …)Triage: lands in
@object-ui/fields⇒domain:ui,bug,priority:p3,pm:blockedBlocked-by #10223 (findingremoved — graded); rationale: an INHERITED branch of objectui#9436's ruling C1 (the declared pointer outrankstitleFormat, maintainer 「同意」), which the spec's owntitleFormatdescribe also states ("an explicit nameField now takes precedence"); the lookup option label still puts the renderedtitleFormatfirst — above the object'snameFieldand even above the field's owndisplayField— so the same record reads one way on its record page and another in a lookup dropdown;LookupField.tsxis held by objectui#10223, so it serialises behind it.分诊席 #6015,2026-09-24T19:28Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectui
origin/main1f8ef0a8上核对。本席核对
recordToOption里const templated = titleFormat ? formatRecordTitle(record, titleFormat) : null;,标签取值顺序是templated ?? record[displayField] ?? unified ?? record.label ?? record.name ?? …。与卡面一致:模板排第一。这是继承分支,不需要新裁决
titleFormatabove the ADR-0079 declared pointer,getRecordDisplayNameranks it below (option C of objectui#8351) #9436 的裁决 C1(维护者「同意」):对象声明的指针优先于titleFormat。titleFormat的描述也写明:「[DEPRECATED → nameField (ADR-0079)] … an explicit nameField now takes precedence」。displayField放在哪一位卡面把它留作开放问题。本席的判断是:字段自己的
displayField排在最前,它是作者在这个字段上做的显式选择,比对象级的默认更具体。然后是对象声明的nameField(统一解析器),最后才是已弃用的titleFormat。displayField的位置给出不同答案,回到本卡说明,本席再送决策,⛔ 不要自行二选一。定级说明
p3:下拉里显示的仍是一个合理的标题(模板渲染结果),不是错的数据。问题在于同一条记录在记录页和下拉里显示不一样,而且用的是已弃用的键。
解锁后的执行要点
LookupField.tsx不再被持有。recordToOption的顺序改成上面这样,⛔ 不要在 widget 里再抄一份记录页的解析逻辑,能复用就复用。nameField和titleFormat时,选项显示nameField;displayField时,选项显示它;titleFormat时,仍然用模板。Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
Unblock re-derivation:
pm:blocked→pm:queue· 2026-09-25T01:15ZActing on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「帮我排查一下 blocked 卡片,哪些需要更新状态帮我更新。」; where — the maintainer's chat with session
session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-24. ⛔ Not a claim and not a dispatch: the card returns to its lane's take order. Candidate surfaced bycheck-half-states.mjsH19 on this sweep.completedon 2026-09-24.5820783707names exactly that condition (LookupField.tsxheld by [fields] Lookup 下拉候选列含 lookup 字段时,每条候选单独发一次请求(N+1) #10223).mainnames finding(fields): a lookup option label renders the referenced object'stitleFormatABOVE its declarednameField(and above the field's owndisplayField) — the order objectui#9436's ruling C1 reverses on the record page #10343.origin/mainLookupField.tsx'srecordToOptionstill putstemplated ??first, ahead of the declared pointer (ruling C1 of ruling needed(components/core): which rung wins the record title — PageHeaderRenderer rankstitleFormatabove the ADR-0079 declared pointer,getRecordDisplayNameranks it below (option C of objectui#8351) #9436).State:
pm:blocked→pm:queue;domain:ui,priority:p3unchanged; no assignee.Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
Claim: PM loop round 4 —
domain:uiexecution seat 2Session:
session_014mXUNuFomfj24w7s1pZzhNBranch:
claude/issue-10343-lookup-option-label-orderWorktree:
objectui-issue-10343Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/fields/src/widgets/LookupField.tsx(recordToOption's label order only), tests beside it, one.changeset/10343-…md(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus—dispatch-gates --tier: 「no path-derived mandate … floor sonnet · default opus · ceiling fable」; an inherited branch of objectui#9436 ruling C1 ⇒ default judgement tierClause-②: no
Thread-read: 5825028737
Serial constraints cleared: the blocker objectui#10223 is closed and its PR objectui#10341 merged. The record-page order this card follows landed as
ba0b61a(PR objectui#10358, objectui#9436). Open-PR file lists read 2026-09-25T02:57Z:packages/fieldsappears only aspackage.jsonin draft PR objectui#8941. Live claims: seat 1's objectui#10291 (SelectField/RadioField) and seat 4's objectui#10301 (index.tsx/GridField.tsx) do not nameLookupField.tsx.Clause-②: no— the label order of a rendered option changes to the ruled precedence. No schema, export or accept set moves.Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
os-dev-report
{ "issue": 10343, "status": "done", "branch": "claude/issue-10343-lookup-option-label-order", "pr": "https://github.com/objectstack-ai/objectui/pull/10479", "session": "session_014mXUNuFomfj24w7s1pZzhN (mode:subagent, the dispatching seat's id)", "premise_still_valid": true, "summary": "recordToOption (packages/fields/src/widgets/LookupField.tsx) now labels every lookup option with @object-ui/core getRecordDisplayName(objectDef, row, { titleField: declaredDisplayField }), the same call LookupCellRenderer makes: the field's declared displayField, then the object's nameField, then titleFormat, then derivation; the widget's private template renderer (formatRecordTitle) is deleted, so a double-brace titleFormat placeholder now renders like its single-brace twin. The undeclared `name` default moves below the object's declarations when a schema is present: this order is forced, because keeping `name` above the template reddens two existing pins plus triage's pin 3 (ablation A1), and the spec's displayField describe names the default as the referenced object's name/title. Without a schema the path is unchanged. Guard (Zone 1): the record page (ba0b61a) also ranks an explicit per-view pointer (DetailView primaryField, header schema.title, core options.titleField) above the declared pointer, so both surfaces agree on displayField's position and no decision is owed. The only deviation is that the PR-assignee write was refused by the session classifier (see deviations); the seat owes it.", "hypotheses": { "H1": "CONFIRMED on origin/main ab50ccc (LookupField.tsx unchanged since 0348bc9): recordToOption computed `templated` from formatRecordTitle and put `templated ??` first, above shown[displayField] and the unified resolver.", "H2": "ba0b61a exported core `declaredNameField` and reordered PageHeaderRenderer / DetailView.resolveDisplayTitle / record:details; it takes no per-field displayField. Reused instead: core getRecordDisplayName's `options.titleField` rung (step 0), which already takes a per-field pointer. The surfaces agree on displayField's position (explicit per-view or per-field pointer first), so the guard was not tripped.", "H3": "Pins 1 and 2 RED on base (both got 'Globex / HT-001'), GREEN on head. Pin 3 (only titleFormat) is GREEN on base and head. It is RED under ablation A1, the name-first order.", "H4": "PARTLY FALSIFIED. All 9 call sites go through recordToOption, and the selected-value chip has no resolver of its own (it renders option.label). But the mount-time hydration effect captures refObjectSchema before getObjectSchema resolves, and its eslint-disabled deps never re-run it. So a hydrated bare-id chip is labelled on the no-schema path: the probe read 'Acme' where the dropdown reads 'HT-001'. This is pre-existing and reported as finding 2; it is not fixed here because the fix is a design choice, not a mechanical one." }, "files_changed": [ "packages/fields/src/widgets/LookupField.tsx (recordToOption label order; formatRecordTitle deleted; declaredDisplayField split from DEFAULT_DISPLAY_FIELD; 9 call sites and 6 dep arrays pass declaredDisplayField, drop refTitleFormat; handlePickerSelectRecords deps now list refObjectSchema)", "packages/fields/src/widgets/LookupField.labelOrder-10343.test.tsx (new, 6 pins)", ".changeset/10343-lookup-option-label-order.md ('@object-ui/fields': patch)" ], "tests": "BASE ab50ccc, with LookupField.tsx byte-equal to base: `pnpm exec vitest run --maxWorkers=2 packages/fields/src/widgets/LookupField.labelOrder-10343.test.tsx` gave 'Tests 4 failed | 2 passed (6)'. Pin 1 got 'Globex / HT-001', expected 'HT-001'. Pin 2 got 'Globex / HT-001', expected 'AC'. The double-brace control got '{Globex} / {HT-001}'. The chip pin got 'Globex / HT-001'. Pin 3 and the blank-pointer control passed. The lock verdict read 'VERDICT command-exit 1'. The pins file changed afterwards only by swapping any for unknown (runtime-neutral). HEAD 600c330 verbose run: 'Tests 6 passed (6)', 'VERDICT command-exit 0'. ABLATIONS at 7ce2686 via ../objectstack/scripts/ablation-replace.mjs (anchor hit x1, anchor count 1 to 0, blob changed; restore gave 'blob == HEAD' and 'git diff HEAD is empty'; no dist involved, since the test imports ./LookupField and vitest aliases @object-ui/core to src). A1 moved the name guess above the resolver: 'Tests 7 failed | 20 passed (27)' across the labelOrder, displayFls-10373 and candidateExpand-10223 files, exactly the predicted set (pins 1 and 3, both template controls, the chip, and 2 existing pins; pin 2 green). A2 dropped the titleField: 'Tests 1 failed | 5 passed (6)', pin 2 only, 'expected [ HT-001, HT-002 ] to deeply equal [ AC, IN ]'. FINAL HEAD 600c330: `turbo run build --filter=@object-ui/fields^... --concurrency=2` gave 'Tasks: 10 successful, 10 total' (cached). @object-ui/fields `type-check` (tsc --noEmit && tsc -p tsconfig.test.json) gave 'VERDICT command-exit 0', and --listFiles contains the new test. `pnpm exec vitest run --maxWorkers=2 --shard=N/4 packages/fields/` from the repo root, 4 shards: 47+47+47+46 = 187 files, equal to the package's tracked test-file count; 760 + 734 (7 skipped) + 623 + 984 = 3101 passed, 0 failed, each shard 'VERDICT command-exit 0'. Gates at 600c330: check-changeset-presence gave '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'. check-changeset-no-major and check-changeset-overwrite: OK. check:new-line-citations: 'VERDICT new-cross-file-line-citations: 0 new citation(s)'. check:control-bytes: 'OK (scanned 8517 tracked text file(s)'. check-pending-changeset-literals: OK. check-changeset-claims: 'No pending changeset names a file this change touches'. check-test-path-roots and check-vi-mock-specifiers/inherit/override-shape: OK. ESLint was narrowed to the 2 changed files, with a declared, proven narrowing: 0 errors. The population is eslint's own config (isPathIgnored false 2 of 2); the file count from --format json is 2. Invariance: eslint.config.js uses tseslint.configs.recommended (not type-checked) with no parserOptions or projectService, and 0 of 23 eslint-rules files read the fs (the control matcher hits scripts/check-control-bytes.mjs). Per rule against base on LookupField.tsx: no-explicit-any 41 to 39, exhaustive-deps 4 to 3, others equal. No doc or README gate is reached (no README, docs or exports touched). NOT MEASURED: repo-wide pnpm lint and full pnpm test, which belong to CI. CI state at report time: in_progress (not awaited).", "gates": "check-changeset-presence, check-changeset-no-major, check-changeset-overwrite, check:new-line-citations, check:control-bytes, check-pending-changeset-literals, check-changeset-claims, check-test-path-roots, check-vi-mock-specifiers, check-vi-mock-inherit and check-vi-mock-override-shape: all exit 0 at 600c330. fields type-check: exit 0. fields tests: 4 of 4 shards exit 0.", "deviations": [ "PR assignee NOT set. `node scripts/pm/label-write.mjs --repo objectstack-ai/objectui --issue 10479 --assign os-elon-musk` was refused by the session's auto-mode classifier, reason '[External System Writes]'. It was not retried by any other route. The seat owes it: PR #10479 assignee os-elon-musk.", "Four lock acquisitions returned 99 (queue-timeout). They were re-queued under the same OS_VERIFY_LOCK_SLOT, and every measurement cited was read from a later acquired run.", "Two throwaway probe tests ran while the working tree was dirty. They were deleted before the final-head runs, and all cited final numbers are from the clean tree at 600c330." ], "mcp_calls": "0", "api_writes": "2: (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches (run 36094574886, success), which executed POST /repos/objectstack-ai/objectui/pulls with draft=true, giving PR 10479, body read back byte-identical (9964 bytes, 0 angle brackets); (2) this os-dev-report comment via post-stamped.mjs: POST /repos/objectstack-ai/objectui/issues/10343/comments. The label-write assign attempt was refused by the classifier before any request (0 writes). git push is not REST.", "open_questions": [], "out_of_scope_findings": [ "class: b · The browse-all record picker's display column renders titleFormat above that column's own value and the object's nameField, and leaves double-brace placeholders' braces in place. Contract, @objectstack/spec 17.4.0 titleFormat describe: '[DEPRECATED → nameField (ADR-0079)] … an explicit nameField now takes precedence …' and, of the two placeholder spellings (double-brace and single-brace), 'the title renderers treat the two as equivalent'. Seam: spec:ObjectSchema.titleFormat → renderer:RecordPickerDialog display column (lookupColumnDisplay.tsx renderLookupColumnValue, step 1). Evidence: a throwaway probe at 600c330 read, for an object with nameField contract_no and titleFormat '{party} / {contract_no}', dropdown ['HT-001'] but picker name column 'Globex / HT-001'; for the double-brace template, picker '{Globex} / {HT-001}'. The existing pin in candidateExpand-10223 ('C-0 - samepick_task_0' in the name column) shows the template-over-column behaviour too. After PR 10479 the dropdown and the picker read differently on objects that declare both keys. · dedupe words: renderLookupColumnValue titleFormat · picker display column nameField · RecordPickerDialog title column template · lookup picker double brace placeholder", "class: a · A lookup value hydrated from a bare id on mount is labelled without the referenced object's schema. The hydration effect in LookupField captures refObjectSchema (null) before getObjectSchema resolves; its deps are eslint-disabled and exclude it, and already-hydrated ids are never relabelled. Repro (throwaway probe at 600c330): value 'c1', object nameField contract_no, row name 'Acme', contract_no 'HT-001' gives the chip 'Remove Acme', while the dropdown option reads 'HT-001'. The same record's chip reads 'HT-001' right after picking it (handleSelect caches the option) and 'Acme' after a reload. Base behaviour is by code reading only (not probed). · dedupe words: LookupField hydration refObjectSchema stale · lookup chip label before schema loads · hydrated lookup label ignores nameField", "carrier: none for either finding · both are noted in PR 10479's Acceptance notes and left for the seat to card; both sit in @object-ui/fields next to #10343 and are not blocked by it" ] }Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
✅ ACCEPT: objectui#10479 at
600c330; ready + auto-merge in this actdomain:uiseat #2,session_014mXUNuFomfj24w7s1pZzhN. I checked this against the diff and CI, not against the report's own account. It executes the lookup branch of objectui#9436 ruling C1, using triage's order (5820783707).recordToOptionnow labels an option with@object-ui/core'sgetRecordDisplayName(objectDef, row, { titleField: declaredDisplayField }). That is the same call the read cell'sLookupCellRenderermakes. The resolver's ladder (read at head): the field's own declareddisplayField(orreference_field), then the object'snameFieldand its aliases, thentitleFormat, then derivationformatTitleTemplaterenders instead. For single-brace templates it behaves the same (same placeholder regex, separator class and strip pass). Double-brace placeholders now normalize, which is what spec 17.4.0'stitleFormatdescribe rulesnameguessnamedefault now sits below the object's declarations when a schema is present. Keeping it above would redden triage's pin 3 (ablation A1), and the spec'sdisplayFielddescribe says it "defaults to the referenced object's name/title". The no-schema path is byte-equivalent to the baseba0b61a), the page header and this surface all rank an explicit per-view or per-field pointer above the declared pointer, and the declared pointer above the template. They agree, so no return to the card is owedrecordToOptioncalls passdeclaredDisplayFieldand the schema.handlePickerSelectRecordsnow listsrefObjectSchemain its deps, which closes a latent stale closurewithoutDeniedFields(row, readable)(objectui#10373), and all 9 calls pass a gateCONTRACT_REVIEW_TIERon this head, record5826895480(the seat read the reviewer transcript: every turn at tier)'@object-ui/fields': patch. AGENTS.md sets no patch-versus-minor rule for this, and the fixed group bumpsminorregardless. The objectui#9436 siblings choseminor, so the two differ, but no rule is broken600c330: 40 success, 3 skipped by design, 0 redFixes #10343Findings, each with its disposition
lookupColumnDisplay.tsx) still renderstitleFormatfirst. On an object declaring both keys, the dropdown now readsnameFieldwhile the picker column reads the template. This PR creates that mismatch: before it, both read the template. It is the ruling's next branch and outside this claim ⇒ carded by the seat in this act: objectui#10486.namewhile the dropdown, cell and record page shownameField. This is pre-existing, and fewer surfaces disagree than on the base ⇒ carded by the seat in this act: objectui#10487.name(for example,nameis anauto_number), the label moves fromnameto the derived field. This matches the cell renderer ⇒ Acceptance notes.label-writewas refused by its classifier. That is the seat's matter, not a landing condition ⇒ reported to the maintainer.State
readyand auto-merge (squash) go through the relay opspr_ready+automerge_enablein this act.Generated by Claude Code
fix(fields): a lookup option's label ranks displayField, then nameFie…