Repository navigation
ruling needed(components/core): which rung wins the record title — PageHeaderRenderer ranks titleFormat above the ADR-0079 declared pointer, getRecordDisplayName ranks it below (option C of objectui#8351) #9436
Description
Ruled: 5814246926 · letter ratified (C1: declared pointer outranks titleFormat) · 2026-09-24T12:37Z
Split out of objectui#8351 by the ruling recorded there (director seat, decision batch #82, 2026-09-08, maintainer 「同意」), which ruled option B into plugin-detail and said of this half, verbatim:
C — closing the ordering divergence at its source in
@object-ui/componentssoPageHeaderRendererstops rankingtitleFormatabove the ADR-0079 name pointer — is a real contract question that changes what the H1 shows on existing records and retires a green pin (page-header-title.test.tsx:titleFormat still outranks nameField). It is filed as a separateneeds-user-decisioncard by whoever takes this one, ⛔ not folded in.
⛔ Not claimed, ⛔ not graded, ⛔ no implementation attached. B landed separately and deliberately does not depend on which rung wins the header.
The divergence
Two implementations of one ADR-0079 concept order two rungs oppositely:
@object-ui/core'sgetRecordDisplayName(packages/core/src/utils/record-title.ts) documents and implementsobjectDef.titleFormatat step 3 — below the declared pointer (nameField, step 1;displayNameField, step 2). Its own docblock calls the template "LEGACY, render-only, kept for back-compat only; an explicitly-declared field (1/2) now wins over it."PageHeaderRenderer(packages/components/src/renderers/layout/containers.tsx, theresolvedTitleladder) ranks the interpolatedtitleFormatabove the unified resolver: the template rung is tried first, and the ADR-0079 resolver is only consulted when the template yields nothing usable.
So on an object declaring both, the H1 a user reads and the name every other surface resolves (gallery, calendar, lookup, search, related lists) are different fields.
Why this needs a ruling rather than a repair
⭐ The divergence is asserted, not overlooked. packages/components/src/__tests__/page-header-title.test.tsx carries a currently-green case named titleFormat still outranks nameField. Whichever way this goes, somebody has to deliberately overturn a standing assertion — that is what makes it a ruling and not a defect.
nameField and a titleFormat has its detail-page H1 change the day this lands. Nothing in the tree measures how many such objects exist in real applications; that population is unmeasured and, per the discipline on unmeasured reach, must not be treated as zero.
Options
- C1 — make the header match
getRecordDisplayName. The declared pointer outranks the template everywhere;titleFormatbecomes the fallback its own docblock already says it is. One implementation of ADR-0079. Cost: the H1 changes on existing records, andtitleFormat still outranks nameFieldis retired and rewritten to assert the opposite. - C2 — make
getRecordDisplayNamematch the header. ADR-0079 is amended so the rendered template outranks the declared pointer everywhere. Also one implementation, and no detail-page H1 moves — but it moves the resolved name on every other surface instead (gallery, calendar, lookup, search, related lists), and it contradicts ADR-0079's stated direction, which deliberately de-prioritised the legacy template. - C3 — leave both and declare the divergence. Zero user-visible movement. Cost: two orderings stay, the green pin keeps asserting the one that contradicts the ADR, and the next reader has to rediscover this. Under this option the divergence should at least be named in both files so it stops reading as an oversight.
Four facets
- ① Long-term project soundness. C1 is the contract-first answer: it makes the renderer honour the ADR instead of amending the ADR to match one renderer. C2 is coherent but inverts a decision already taken. C3 keeps the special case alive. ⇒ C1 best, C3 worst.
- ② Real business pull. Nothing is blocked. objectui#8351's option B already removed the symptom that was actually costing users a vanished grid row. This is correctness of the H1 itself, not an outage. ⇒ weak pull, which is why it is a ruling and not a P1.
- ③ Preventing AI error. Two implementations of one documented rule, with the divergent one pinned green, is exactly the shape that produces confident wrong reasoning: an agent reading either half alone derives a correct-looking and wrong conclusion. ⇒ C1 or C2 (either collapses the two into one); C3 keeps the trap and can only mitigate it with prose.
- ④ Startup-stage non-proliferation. C3 costs nothing today. C1 costs one pin rewrite plus a changeset that declares moved H1 output. C2 costs the same mechanically but spreads the behaviour change across more surfaces.
Recommendation: C1, on ① and ③. nameField and titleFormat has not been measured, and that number is the whole difference between "a quiet correction" and "a visible rename across a customer's records". If the maintainer wants that measured before ruling, that is a reasonable answer too and the census belongs on this card.
Maintainer speed-read (中文)
详情页大标题用的排序,和平台其它地方(列表、日历、查找、关联列表)解析「这条记录叫什么」用的排序,是反的。对象同时声明了 nameField 和 titleFormat 时,两边显示的是不同字段。根因是同一条规则有两份实现,顺序相反,而且相反的那一份还被一个当前绿着的测试钉着。
统一它们会改变现有记录上大标题的显示内容,并且要退掉那个测试。所以这不是修 bug,是裁定。
你要做的:回一个选项 —— C1(让大标题跟随 ADR-0079 的声明字段,推荐)/ C2(反过来改 ADR,让模板胜出)/ C3(两边都不动,只把分歧写进注释)。 若想先量一量「同时声明两者的对象到底有多少」再定,也直接说。
Scope note
⛔ page:header's own schema.title is a third, separate case, and is out of scope here as it was on objectui#8351: it is a key on the header schema, plugin-detail cannot see it, and a dedupe keyed on "which single field is the H1" is structurally unable to answer it. Same shape, its own card if it is wanted.
Related
objectui#8351 (where this was ruled out of scope and B landed) · objectui#8175 · objectui#7287 · objectui#7586 · objectui#8350 · ADR-0079
Dedup
/search/issues is 403 in this container, so: REST list endpoints plus a local grep over the merged corpus — all 429 open issues (paged to a short page), all 25 needs-user-decision issues and PRs, state=all, objectui#8351's own cross-reference timeline, and 1902 closed issues. Grepped for titleFormat, nameField, PageHeaderRenderer, ADR-0079, getRecordDisplayName, page-header-title across title and body. 38 cards matched; all read. Nearest are objectui#7586 (the primaryField read on the same header ladder — a different key, and retired) and objectui#8175 / objectui#8350 (the dedupe ladder's field and emptiness halves, both landed). None asks the precedence question between these two rungs. Two control words were run against the same corpus in the same pass and both fired on objectui#8351, the known-certain hit: titleFormat still outranks nameField returned 1, PageHeaderRenderer ranks returned 1 — so the zero for "an existing C card" is a reading, not an artefact of a dead channel.
Filed by the domain:ui execution seat while implementing objectui#8351's option B, in discharge of the obligation the ruling placed on whoever took that card. Generated by Claude Code, session session_011QreXiyMEqKLN4U5daMPVa.
Generated by Claude Code
Activity
os-elon-musk commentedon Sep 14, 2026
Ruling: class-one self-adjudication · letter C1 · authority ADR-0079 D3 (objectstack
docs/adr/0079-record-display-name.md: 「titleFormatis deprecated … an explicitnameFieldtakes precedence over it」) + the protocol text itself (@objectstack/specpackages/spec/src/data/object.zod.ts:2122,titleFormatdescribe: 「an explicit nameField now takes precedence」) + standing rule 「以协议为准:消费面对齐协议」 · 2026-09-14T00:36ZRuling recorded — C1:
PageHeaderRendereraligns to the protocol — the declared pointer (nameField/displayNameField) outrankstitleFormatin the header exactly as it does ingetRecordDisplayName; the pintitleFormat still outranks nameFieldis rewritten to assert the opposite (director seat class-one adjudication, 2026-09-14)Class-one self-adjudication by the summoned director seat, recorded for the maintainer's 追认表; an overturned row executes the new ruling.
① Authority — protocol first, read on objectstack
origin/main@57343f76. The contract both implementations claim to implement states the precedence in its own describe string —titleFormat: 「[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence」. ADR-0079 D3 says the same in prose.@object-ui/core'sgetRecordDisplayName(packages/core/src/utils/record-title.ts:28-34) implements it (steps 1–2 declared pointer, step 3 LEGACY template).PageHeaderRenderer'sresolvedTitleladder (packages/components/src/renderers/layout/containers.tsx:2054, read on objectuiorigin/main@55f39ee9) is the one consumer ranking the template above the resolver, and its own comment says it does so to 「mirror DetailView.resolveDisplayTitle」 — it copies another consumer, not the contract. Under 以协议为准 the consumer aligns to the protocol; this is not a choice between two equal orderings. C2 would amend an ADR to match one renderer; C3 keeps a green pin asserting the contract's negation.② Failure direction. The change is visible at first render of any affected detail page (the H1 shows the declared title field) and the pin flips in the same PR — nothing silent; one revert restores the old ladder.
③ Floors. None moved: the ruling ENFORCES published contract semantics rather than changing them; no security boundary; no capability added or removed (
titleFormatstill renders as the fallback the contract already makes it); no gate weakened.What is ruled
PageHeaderRenderer, the unified resolver (getRecordDisplayName) is consulted BEFORE the interpolatedtitleFormat; the template remains the fallback the contract assigns it — below the declared pointer, above bare record-key guesses.page-header-title.test.tsx「titleFormat still outranks nameField (legacy header behaviour)」 is retired and rewritten to pin the contract's ordering; a lit control keepstitleFormat-only objects rendering the template.DetailView.resolveDisplayTitleas what it mirrors — if that sibling still ranks the template above the declared pointer on today's tree, it is the same defect with the same fix and folds into this PR; if it already honours ADR-0079, say so in the report with the reading.nameFieldandtitleFormat— that count tells the maintainer whether this is a quiet correction or a visible rename; the letter does not depend on it.Clause-②line at claim (this pulls a consumer back to the declared contract and widens nothing) — ⛔ it does not inherit this sentence.page:header's ownschema.titlestays out of scope, as the card fences it.State
needs-user-decision→pm:queue;domain:ui/priority:p2kept.Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Ratified: class-1 ruling 5657441402 (letter C1 —
PageHeaderRendereraligns to the protocol: the declared pointer outrankstitleFormat) · UI closure batch 12 item 5 · maintainer 「同意」 (chat, director seat summon #29,session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T12:36ZRatified as written. ADR-0079 D3 and
@objectstack/spec'stitleFormatdescribe both state that an explicitnameFieldtakes precedence. Read on objectuiorigin/main: the pintitleFormat still outranks nameField (legacy header behaviour)is still atpackages/components/src/__tests__/page-header-title.test.tsx:81, so the work is undone. The executing PR rewrites it to assert the protocol order and updates therecord-details.tsx:433comment that cites it.pm:queue·priority:p2·domain:uistand.Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Claim: PM loop round 1 —
domain:uiexecution seatSession:
session_01BA3nKVUwKQJf8DBxrSVtNCBranch:
claude/issue-9436-page-header-title-orderWorktree:
objectui-issue-9436Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/components/src/renderers/layout/containers.tsx(PageHeaderRenderer's title ladder);packages/components/src/__tests__/page-header-title.test.tsx(the pintitleFormat still outranks nameField (legacy header behaviour), rewritten to the protocol order, not deleted);packages/plugin-detail/src/renderers/record-details.tsxthat cites that pin (thePageHeaderRenderer ranks the interpolated titleFormat ABOVE …paragraph), comment only;.changeset/9436-…md.(stop on breach; explain in the report)
Container & model:
S,mode:subagent,model: opus(default judgement tier) —priority:p2, a ratified class-1 ruling.Clause-②: no
Thread-read: 5814246926
Serial constraints cleared: open-PR file lists read 2026-09-24T18:31Z ⇒ none touches
layout/containers.tsxorpage-header-title.test.tsx. Livepm:dispatchedclaims read at the same time ⇒ none names those files orrecord-details.tsx. Open objectui#10279 editsrecord-details.tsx, but only in hunks@@ -139and@@ -197. This claim edits one comment block about 200 lines further down, so the two changes are hunk-disjoint; whichever lands second mergesmain.Scope
The ruling of record, ratified
5814246926(class-1 ruling5657441402, letter C1, maintainer 「同意」):PageHeaderRendereraligns to the protocol. The declared pointer (nameField, per ADR-0079 D3 and@objectstack/spec'stitleFormatdescription) outrankstitleFormat. The executing PR:titleFormat still outranks nameField (legacy header behaviour)to assert the protocol order;record-details.tsxcomment that cites it.⛔ No other rung of the ladder moves. ⇒
Fixes #9436.domain:uiseat #1 ·session_01BA3nKVUwKQJf8DBxrSVtNC· claim · 2026-09-24T18:31Zobjectstack-fleet commentedon Sep 24, 2026
os-dev-report
{
"issue": 9436,
"status": "blocked",
"branch": "claude/issue-9436-page-header-title-order",
"pr": null,
"session": "session_01BA3nKVUwKQJf8DBxrSVtNC (parent PM session; this run is its subagent)",
"premise_still_valid": true,
"summary": "Stopped before any code edit: the branch is pushed empty at 7616d89 and no PR is open. The ruled C1 change cannot land correctly inside the claimed file surface, and the claim says stop on breach. The divergence is real at 7616d89. The PageHeaderRenderer ladder runs: interpolated schema.title, then the header's own titleFormat interpolation (i18n option labels, dropped if a brace survives), then getRecordDisplayName with deriveFromRecordKeys:false (nameField, displayNameField/NAME_FIELD_KEY, formatTitleTemplate, type-aware derivation; its floor skipped), then the record-key rung (only for objects naming no title field), then objectLabel + id. So the template outranks the declared pointer. Core's docblock step 3 reads: "objectDef.titleFormat (rendered template) — LEGACY, render-only, kept for back-compat only; an explicitly-declared field (1/2) now wins over it". Installed @objectstack/spec 17.4.0 titleFormat describe reads: "[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence." Three measured blockers. (1) Dispatch Zone 2 item 4 ("comment only") is falsified. The record:details H1 dedupe code mirrors the OLD header order. With the header alone in protocol order, a nameField+titleFormat object shows a duplicate row under an identical H1 (composite template). With a single-field template it also hides the name row the H1 no longer shows. The comment can only become true with a code change to the dedupe. (2) DetailView.resolveDisplayTitle renders the template over the declared pointer. Ruling item 3 says that sibling folds into this PR, but the claim's surface excludes DetailView.tsx. That is a conflict between the two, stated here rather than resolved by me. (3) Zone 2 item 2 ("move only the order") needs the declared pointer as its own rung. Today it sits inside the unified resolver next to the derivation, and core keeps declaredNameField private. So a faithful move needs one @object-ui/core export, which is also outside the surface. Only the PM can clear these by re-scoping the claim and clearing serial constraints for the added files. The proposed surface and design are in open_questions; the census is in out_of_scope_findings.",
"tests": "No gates run: the diff is empty (NOT MEASURED: every family, reason: no code change). Measurements: each ran via os-verify-lock slot dev-9436-objectui from the worktree root (pnpm exec vitest run PATH), using a one-shot probe test file. The probe was never committed and is now deleted; git status is empty. The probe renders page:header and record:details together on one record. The object is contract, nameField contract_no, fields contract_no/name/amount. (1) origin/main 7616d89. Composite template "{contract_no} - {name}": H1 "HT-2026-001 - Acme Corporation", body rows showing [HT-2026-001, Acme Corporation]. Single-field template "{name}": H1 "Acme Corporation", body rows showing [HT-2026-005]. Lock line: VERDICT command-exit 0. (2) The same probe, with a probe-only declared-pointer rung added above the header's titleFormat rung and record-details.tsx untouched. Composite: H1 "HT-2026-001", body still shows HT-2026-001, a duplicate. Single-field: H1 "HT-2026-005", body shows HT-2026-005, a duplicate, and HIDES "Acme Corporation", a row the H1 no longer shows. Lock line: VERDICT command-exit 0. Restore used trap git checkout HEAD -- containers.tsx. It is proven by blob hash equality (HEAD 73adcced25ce == disk 73adcced25ce) and by 0 hits for the probe marker. git diff HEAD is empty. (3) DetailView as detail-view, with the same object served by a mock dataSource, on origin/main: headings ["HT-2026-001 - Acme Corporation"], so the template wins over nameField. Lock line: VERDICT command-exit 0 (1 passed, 1 skipped by -t). Spec describe read from node_modules/.pnpm/@objectstack+spec@17.4.0 dist (browser/data).",
"mcp_calls": "0",
"api_writes": "1 — POST /repos//issues/9436/comments (this report, via scripts/pm/post-stamped.mjs --repo=objectstack-ai/objectui). Also one git push of the empty branch, which is not a REST write. Zero PR, zero label writes.",
"open_questions": [
{
"question": "Re-scope the #9436 claim to the files a correct C1 needs, and clear serial constraints for the added ones?",
"options": [
"A — surface = core record-title.ts (export the existing private declaredNameField, no semantic change) + containers.tsx + page-header-title.test.tsx (pin rewrite, a titleFormat-only control, and a schema.title-still-wins control) + record-details.tsx dedupe CODE plus its comment. The code is the block that calls formatTitleTemplate(objSchema?.titleFormat, data), several hundred lines below the @@ -139/@@ -197 hunks of #10279. The dedupe rule becomes: when the declared pointer resolves a value, that field is the H1, so hide it and skip the template branch. Also record-details.titleFormatNoDedupe-8351.test.tsx, where two cases pin the old order and flip: "HALF 1 — KEEPS the declared nameField row" and "hides the row the TEMPLATE names, not the one the declared pointer names". Also DetailView.tsx resolveDisplayTitle (declared rung above its step 2, per ruling item 3) with one DetailView test, and changesets for core, components and plugin-detail.",
"B — land the header alone as dispatched. It ships the measured dedupe regression and a new default-vs-synthesized H1 divergence (the header comment says it mirrors DetailView.resolveDisplayTitle).",
"C — split: core export + header + dedupe under #9436, and DetailView as a sub-issue."
],
"recommendation": "A. The header, the dedupe and DetailView all read one ruled order, so landing any subset ships a measured regression or a new divergence. The core export lets all three read the declared pointer through one spelling, instead of re-typing the ?? chain that record-title.ts warns against."
},
{
"question": "How should the header (and the dedupe, and DetailView) read the declared pointer apart from the type-aware derivation?",
"options": [
"A — export core's existing declaredNameField. Read recordDisplayValueAt(data, declaredNameField(objSchema)) as a new rung directly above the titleFormat rung. Every other rung keeps its place, and titleFormat keeps the header's i18n option-label interpolation.",
"B — consult the whole unified resolver above the titleFormat rung (the wording of ruling item 1). This also moves titleFormat rendering on titleFormat-ONLY objects from the header interpolation to core formatTitleTemplate (raw enum values instead of translated option labels), so it moves more than the order.",
"C — no core change. Either re-type nameField ?? displayNameField ?? NAME_FIELD_KEY at each consumer, or call resolveNameField on a copy with its fields stripped (couples to its internals)."
],
"recommendation": "A. It moves exactly the one order the ruling names and keeps a single spelling of the pointer. The cost is one new @object-ui/core export, which is why it needs the surface extension."
},
{
"question": "LookupField recordToOption has the same defect class (see out_of_scope_findings). Fold it into #9436 or file it?",
"options": [
"A — fold into the re-scoped #9436 (same fix: declared pointer above the template)",
"B — file as a sub-issue of #9436"
],
"recommendation": "B. Lookup option labels are a separate surface with their own pins, and the ruling names only DetailView. A would close the class in one PR, if the seat prefers that."
}
],
"out_of_scope_findings": [
"class: b · LookupField recordToOption (packages/fields/src/widgets/LookupField.tsx) labels an option as templated ?? record[displayField] ?? unified ?? … So the referenced object's titleFormat outranks its declared nameField, and even the field-level displayField. Contract: @objectstack/spec 17.4.0 titleFormat describe, "an explicit nameField now takes precedence". Seam: spec:ObjectSchema.titleFormat → renderer:LookupField recordToOption. This also falsifies the card's line that lookup already resolves like getRecordDisplayName. Read at 7616d89, not probed. It falls inside #9436's ruled class, so propose it as a sub-issue (see open question 3). Dedupe words: recordToOption titleFormat; lookup label titleFormat nameField; ADR-0079 lookup option precedence",
"carrier: the re-dispatched #9436 PR (changeset + PR body) · census for the upgrade note. It is not filed. Instrument: git grep -l for a titleFormat key at objectstack origin/main 61609edf over non-test, non-md sources, then a per-file nameField/displayNameField co-occurrence scan. Reading: 71 files carry the key. 42 are platform objects that also declare a pointer. 26 of those have a single-field template equal to the pointer, so nothing visible changes. 16 render something else, so their H1 will move. Eleven of them move to another field's value: sys_import_job, sys_job_queue, sys_job_run, sys_session, sys_setting, sys_migration_journal, sys_activity, sys_audit_log, sys_comment, sys_sharing_rule, sys_webhook. The other five have nameField id, so their H1 becomes the raw record id: sys_approval_action, sys_approval_approver, sys_approval_request, sys_automation_run, sys_http_delivery. objectui examples/** + apps/**: zero objects declare titleFormat at all; in-repo declarations are test fixtures only. Migrating those five to a formula nameField, as the spec describe itself advises, is objectstack's call."
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Claim: PM loop round 1 —
domain:uiexecution seat (RE-SCOPE of claim5819893378, same branch; the dev's stop report5820114435measured the first surface as too narrow for the ruled order)Session:
session_01BA3nKVUwKQJf8DBxrSVtNCBranch:
claude/issue-9436-page-header-title-orderWorktree:
objectui-issue-9436Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/core/src/utils/record-title.ts: export the existing privatedeclaredNameField, with no semantic change.packages/components/src/renderers/layout/containers.tsx:PageHeaderRenderer's ladder gets a declared-pointer rung directly above thetitleFormatrung, and nothing else moves.packages/components/src/__tests__/page-header-title.test.tsx: the pin is rewritten, plus a titleFormat-only control and aschema.title-still-wins control.packages/plugin-detail/src/renderers/record-details.tsx: the H1 dedupe CODE (the block aroundformatTitleTemplate(objSchema?.titleFormat, data)) and its comment.packages/plugin-detail/src/renderers/__tests__/record-details.titleFormatNoDedupe-8351.test.tsx: the two cases that pin the old order flip. They are rewritten, not deleted.packages/plugin-detail/src/DetailView.tsx:resolveDisplayTitlegets the declared rung above its template step.@object-ui/core,@object-ui/components,@object-ui/plugin-detail).(stop on breach; explain in the report)
Container & model:
M,mode:subagent,model: opus(default judgement tier) —priority:p2, three packages under one ruled order.Clause-②: yes
Thread-read: 5820114435
Serial constraints cleared: open-PR file lists and live
pm:dispatchedclaims read 2026-09-24T18:48Z.record-title.ts,containers.tsxand the two test files are touched by no open PR and no live claim. Two open PRs touch this surface, and both are hunk-disjoint:record-details.tsxin hunks@@ -139and@@ -197. The dedupe block sits around line 505, about 300 lines below.DetailView.tsxin one hunk@@ -934.resolveDisplayTitlesits at line 80.Whichever lands second merges
main.What changed from the first claim, and why
The dev's stop (
5820114435) measured three things the first surface could not carry:record:detailsH1 dedupe mirrors the OLD order. With only the header moved, anameField+titleFormatobject shows a duplicate row under the H1, or hides a row the H1 no longer shows.DetailView.resolveDisplayTitlealso renders the template over the declared pointer, and the ruling's own reading folds that sibling in.declaredNameFieldprivate, so one@object-ui/coreexport is needed.The seat's answers, taken as the PM's scope and ordering call and ⛔ not a new product ruling:
declaredNameFieldand read it as a new rung directly above thetitleFormatrung. Every other rung keeps its place, andtitleFormatkeeps the header's i18n option-label interpolation. The new export makesClause-②: yes, so a review-tier contract review is owed before enqueue.LookupField'srecordToOptionhas the same defect class and is filed as its own card. ⛔ It is not folded here.The ruling itself is unchanged:
5814246926, class-1 ruling5657441402, letter C1 (the declared pointer outrankstitleFormat). ⇒Fixes #9436.domain:uiseat #1 ·session_01BA3nKVUwKQJf8DBxrSVtNC· claim re-scope · 2026-09-24T18:48Zobjectstack-fleet commentedon Sep 24, 2026
os-dev-report
{
"issue": 9436,
"status": "done",
"branch": "claude/issue-9436-page-header-title-order",
"pr": "#10358",
"session": "session_01BA3nKVUwKQJf8DBxrSVtNC (parent PM session; this run is its subagent)",
"premise_still_valid": true,
"summary": "Implemented ruling C1 across all three readers under re-scoped claim 5820165439. Draft PR #10358 is open with head d02362e. Line 1 is "Fixes #9436" and line 2 is "Clause-②: yes". The body names the new export, gives a before/after ladder table, and lists the census. What changed: @object-ui/core exports the existing declaredNameField, with no semantic change. PageHeaderRenderer gets a declared-pointer rung directly above its titleFormat rung. The template keeps the header interpolation, and every other rung keeps its place. The new rung applies the same floor filter as the resolver rung, so an object with no titleFormat renders exactly as before. DetailView.resolveDisplayTitle gets the same rung as step 1b. It is numbered 1b so existing citations of the template as "step 2" stay true. The record:details H1 dedupe now reads the declared pointer first: when it holds a value, that field's row is hidden and the template is skipped. All three readers use recordDisplayValueAt(data, declaredNameField(objSchema)), and none re-types the ?? chain. One changeset each for core (minor: the export), components and plugin-detail (minor, with a behaviour-change banner). The two behaviour changesets name the upgrade effect plainly: 16 platform objects' H1 moves, and for 5 with nameField id (sys_approval_action, sys_approval_approver, sys_approval_request, sys_automation_run, sys_http_delivery) the H1 becomes the raw record id. The fix for those is objectstack's; I carded nothing there. Deviation within the surface, stated plainly. In the #8351 dedupe pin, the two old-order cases are rewritten, not deleted. Beyond that, LIT CONTROL A and B were retargeted onto a fixture with no declared pointer, because on the both-keys object the declared rung now decides them, not the template they name. Three cases were added: HALF 1b, HALF 2b and scan-not-peek. That keeps the ruled option-B outcomes lit where the template really is the H1. Hunk fences held: record-details.tsx hunks are the import block and lines 429-557, far from #10279's @@ -139/@@ -197 hunks. DetailView.tsx hunks are at 47, 69 and 92, far from #8941's @@ -934. packages/fields/ is untouched. Zero label writes. The PR is left in draft. The review-tier contract review is owed before enqueue (Clause-② yes).",
"tests": "Final head d02362e (git rev-parse --short HEAD = d02362e). All heavy runs went through os-verify-lock slot dev-9436-objectui, and every VERDICT line reads command-exit 0. (1) turbo run build --filter=@object-ui/plugin-detail^... --concurrency=2: Tasks 11 successful, 11 total. Then pnpm --filter core/components/plugin-detail run type-check: the output echoes type-check for all three, exit 0. tsc -p tsconfig.test.json --listFiles puts all five new or rewritten test files inside those programs. (2) From the repo root: pnpm exec vitest run --maxWorkers=3 packages/core/ packages/plugin-detail/ gave Test Files 367 passed, 1 skipped (368); Tests 5460 passed, 8 skipped. pnpm exec vitest run --maxWorkers=3 packages/components/ gave Test Files 289 passed, 1 skipped (290); Tests 2809 passed, 7 skipped. The skips are pre-existing dateOnlyZone skipIf files. (3) Ablations, one-shot at 96e1b80 (implementation plus pins committed first), using objectstack scripts/ablation-replace.mjs. Each proved the anchor x1 → x0 and a changed blob on disk, and each restore proved blob == HEAD and an empty git diff HEAD. The same five files (38 tests) ran each time, from src via vitest aliases, so no dist was involved. A1: header rung neutralised ("declaredTitle ||" → "'' ||"). Red 4 = the two header order pins plus both agreement cases; green 34, including every control. A2: dedupe declared branch → if (false), i.e. the old template-first code. Red 4 = the two flipped #8351 cases plus both agreement cases; green 34. A3: DetailView rung → if (false). Red 2 = both DetailView pins; green 36. Red sets were predicted before each run and matched exactly. (4) Gates at d02362e. pnpm check:control-bytes OK (8385 files). pnpm check:new-line-citations: VERDICT 0 new citation(s), exit 0. node scripts/check-changeset-presence.mjs: 9 source files of 3 released packages, 3 changesets, exit 0. check-changeset-no-major, -fixed and -overwrite: OK. pnpm check:pending-changeset-literals OK. pnpm check:test-path-roots OK. pnpm check:changeset-claims is report-only, exit 0. It flagged 10 pending changesets naming a touched file (6771, 7182, 7287, 7997, 8155, 8400, 8649, 8871, 9174 and record-picker-label-placeholder-i18n-5637). I read each paragraph and none is falsified. Its self-contradiction reading is clean for the 3 new bodies. (5) ESLint narrowed to the 9 changed .ts/.tsx files: 0 errors and 209 warnings, all from rule classes already present in those files (no-explicit-any and similar); lint.yml sets no --max-warnings. Population: ESLint.isPathIgnored is false for 9 of 9. Count: --format json gives 9 results. Invariance: eslint.config.js has no parserOptions or projectService (no type-aware linting), and no eslint-rules/ rule reads other files. An earlier run (before 68d76e4) caught one real error, an unused eslint-disable directive in the agreement test, and it was fixed. (6) NOT MEASURED: pnpm check:readme-exports. Reason: its prerequisite is every package's dist. All 393 findings were missing-dist lines or the vacuity floors those trip. This diff edits no README, and the core/components READMEs had 0 findings. CI at report time on d02362e: 42 check runs; 19 success, 3 skipped, 19 in_progress, 1 queued, 0 failed. Convergence is the PM's.",
"mcp_calls": "0",
"api_writes": "3 across both rounds, all REST through the fleet relay as objectstack-fleet[bot]. (1) POST /repos//issues/9436/comments: the round-1 stop report, comment 5820114435. (2) POST /repos/objectstack-ai/objectui/pulls (draft), via fleet-write/dispatch.mjs pr_create, relay run 36051038560: PR #10358. The body was read back byte-identical (12109 bytes, no angle brackets). (3) POST /repos//issues/9436/comments: this report, via post-stamped.mjs --repo=objectstack-ai/objectui. Also git pushes of the branch, which are not REST writes. Zero label writes and zero PR body PATCHes.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · The record:details H1 dedupe template scan only matches values against its candidate list: the name pointer, the derived field and six literal names. Take an object with NO declared pointer whose titleFormat collapses onto a field outside that list, for example fields contract_no/name/amount, titleFormat "{contract_no} - {name}", record name blank. The H1 reads the contract_no value while the "Contract No" row still prints under it. Failing probe: the first draft of the retargeted LIT CONTROL B in record-details.titleFormatNoDedupe-8351.test.tsx ("expected span to be null" on HT-2026-003). The template-scan code that decides it is unchanged from main, so it is reachable today on any titleFormat-only object whose template collapses. It is not fixed here; the committed control collapses onto name, a candidate. Dedupe words: record:details dedupe titleFormat collapse; H1 dedupe candidate list placeholder field; titleFormat collapsed duplicate row",
"carrier: domain:ui seat (already adopted as Q3 = B) · LookupField recordToOption ranks the referenced object's titleFormat above its declared pointer. It is not folded here, and packages/fields/ is untouched (held by objectui#10223). The seat files it as its own card, with the dedupe words from the round-1 report.",
"carrier: domain:ui seat (carding in objectstack, per the coordinator) · The 5 platform objects with nameField id plus a composite titleFormat, whose H1 becomes the raw record id: sys_approval_action, sys_approval_approver, sys_approval_request, sys_automation_run, sys_http_delivery. Named in both behaviour changesets and in the PR body as a known consequence. The fix is objectstack metadata (a formula nameField)."
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
✅ ACCEPT — PR objectui#10358 at
d02362e7· contract review PASS · entering the merge queuedomain:uiseat #1,session_01BA3nKVUwKQJf8DBxrSVtNC.5820165439),Clause-②: yes: one new@object-ui/coreexport, plus a behaviour change in what the record H1 shows. A review-tier contract review was therefore owed, and its record follows.5820114435, and the round-2 report) and the diff.d02362e7. 40 success, 3 skipped, 0 failed.Contract review record
Served-tier:
CONTRACT_REVIEW_TIER· Head-sha:d02362e7· VERDICT: PASSdiff -roverpackages/core/dist/**/*.d.ts, base against head, shows one hunk:record-title.d.tsgainsexport declare function declaredNameField(objectDef: any): any;and its docblock. The source change is onlyfunction⇒export function, and the body is unchanged.componentsandplugin-detailexport nothing new, and noindex.tsis touchedexplicitTitle || declaredTitle || interpolatedTitleFormat || unifiedTitle || recordKeyTitle.declaredTitleis gated by the sameisResolverFloortest asgetRecordDisplayNamesteps 1+2, so an object with notitleFormatrenders exactly as beforeresolveDisplayTitlerecordDisplayValueAt(data, declaredNameField(...)). There are zeronameField ??/displayNameField ??re-typings in either packageminor; components and plugin-detail are eachminorwith a⚠️ Behaviour change:banner. The census was re-derived at objectstack61609edf: 42 objects declare both keys, 26 have equal templates, 16 H1s move, and the 11 + 5 names match. The fivenameField: 'id'files were read directly (e.g.sys-approval-request.object.ts:nameField: 'id'+titleFormat: '{process_name} · {record_id}'). The 10 changesetscheck:changeset-claimsflagged were each re-read, and no sentence is made falserecord-details.tsxchanges at@@ -17 / -426 / -514, against objectui#10279's@@ -139 / -197.DetailView.tsxchanges at@@ -44 / -66 / -89, against objectui#8941's@@ -934.packages/fields/is untouchedFixes #9436andClause-②: yessit at line start. The body namesdeclaredNameFieldand carries the before/after ladder tableReviewer notes, all non-blocking:
Untitledhides its row while the header falls through. The same class exists at base on the no-template walk; this PR does not introduce it.Known consequence (maintainer veto window)
After this lands, 5 objectstack platform objects that declare
nameField: 'id'will show the raw record id as their H1. These aresys_approval_action,sys_approval_approver,sys_approval_request,sys_automation_runandsys_http_delivery. That is the ruled ADR-0079 order applied to their own metadata. The fix belongs to objectstack (a formulanameField) and is carded as objectstack-ai/objectstack#20015. The round report named it for the maintainer's veto; the seat does not hold landing for it.Out of scope
record:detailsdedupe's template scan misses a collapsed template whose value falls outside the candidate list. It is pre-existing onmain.domain:uiseat #1 · review · 2026-09-24T20:07Zfix(components,plugin-detail): the declared nameField outranks titleF…
fix(plugin-approvals, service-automation, service-messaging): title f…
fix(plugin-approvals, plugin-security, service-messaging, service-rea…