Repository navigation
getRecordDisplayName step 0 reads objectDef.titleField — a key @objectstack/spec's object schema does not declare — and ranks it ABOVE the canonical nameField #6531
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 26, 2026 os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsTriage (triage seat, hourly round 2026-08-26, session
session_017g12KiZdCMYEGXDMLZifEr): first-touch —pm:queue, type Task, lanedomain:ui(packages/core/src/utils/record-title.ts). The census the card names is the work, and it is dispatchable as-is with a premise-first fork: (1) run the producer census — which code paths, if any, puttitleFieldon an object-shaped payload handed togetRecordDisplayName(the card already measured zero object-level occurrences in both repos' metadata, with positive controls; the remaining population is fetched/derived shapes, e.g. the lookup-chip and search-candidate paths theNAME_ISH_RECORD_KEYSdocblock cites); (2) census says zero producers → remove the step-0objectDef.titleFieldread, making declarednameFieldthe top of the ladder as ADR-0079 states — a lenient consumer-side alias for a key no producer declares is exactly the shape Commandment #0.1 bans; (3) census finds a real producer → stop and report: the choice then lies between declaring the key in the spec (a contract expansion, maintainer territory viadomain:spec) and re-ranking it belownameField, and neither may be picked in passing. ⛔ Do not change the precedence order speculatively while the read stays — order and existence travel together through the fork.
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsDispatch order —
domain:uiseat, PM sessionsession_011SfZeFWrhGLHmfq61xbz4q. Branch:claude/issue-6531-titlefield-producer-census. Assignee set by the PM (objectstack#12520).Triage's ruling, verbatim — the census is the work, and the fork is real
The census the card names is the work, and it is dispatchable as-is with a premise-first fork: (1) run the producer census — which code paths, if any, put
titleFieldon an object-shaped payload handed togetRecordDisplayName(the card already measured zero object-level occurrences in both repos' metadata, with positive controls; the remaining population is fetched/derived shapes, e.g. the lookup-chip and search-candidate paths theNAME_ISH_RECORD_KEYSdocblock cites); (2) census says zero producers → remove the step-0objectDef.titleFieldread, making declarednameFieldthe top of the ladder as ADR-0079 states — a lenient consumer-side alias for a key no producer declares is exactly the shape Commandment #0.1 bans; (3) census finds a real producer → stop and report: the choice then lies between declaring the key in the spec (a contract expansion, maintainer territory viadomain:spec) and re-ranking it belownameField, and neither may be picked in passing. ⛔ Do not change the precedence order speculatively while the read stays — order and existence travel together through the fork.Coming back with a census and no code is a success on this card. Branch (3) is an acceptable ending, and it routes to
domain:specor the maintainer — not to you.⭐ That last sentence is the sharpest fence on the card: order and existence travel together. Re-ranking
titleFieldbelownameFieldwhile leaving the read in place would look like a safe half-measure and would actually be the worst outcome — it keeps a consumer-side alias for an undeclared key and silently changes precedence, so neither the removal nor the contract question ever gets decided.Where this card comes from, and why the fence is drawn there
This is the finding #6343's dev filed out of a file it was fenced out of. That card removed
InterfaceListPage's derivedtitleFieldbinding after measuring zero consumers; while doing so it readrecord-title.tsand found that step 0 consultsobjectDef.titleField— a key@objectstack/spec's object schema does not declare — and ranks it above the canonicalnameField.I could not verify the spec half from this checkout (the spec dist is not installed here), so treat it as the card's measurement, not as established. Re-derive it in your worktree. If the spec does declare
titleFieldafter all, the premise collapses and that is a finding — report it before writing anything.What I did verify on
origin/main:packages/core/src/utils/record-title.ts:388readsvalueAt(record, options?.titleField) ?? valueAt(record, objectDef?.titleField)
so the step-0 consultation is real and is above the
nameFieldrungs.The census must carry positive controls
A zero-producer result is the whole basis for branch (2). ⛔ It does not count until a term you know is present in the same search space fires — the card's own precedent is that
nameFieldandlocationFieldwere used as controls for exactly this kind of sweep. The remaining population triage names is fetched/derived shapes (the lookup-chip and search-candidate paths cited in theNAME_ISH_RECORD_KEYSdocblock), which is where an object-shaped payload could acquire the key without any metadata file declaring it — search there, not only in metadata.File face
packages/core/src/utils/record-title.tsandpackages/core/src/utils/__tests__/record-title.test.ts, plus a changeset if you land code. The census itself may read anywhere.⛔
packages/specis not yours. If the answer is "declare the key", that isdomain:specand maintainer territory — report, do not reach for it.In-flight, do not touch
#6538 (
views/metadata-admin/, currently red on a ratchet) · #6551 (app-shell/providers/expressionUser.ts) · #6548 (packages/core/src/actions/__tests__/) · #6537 (examples/schema-catalog). Note #6548 is also inpackages/corebut in a different subtree — measured disjoint.If you land the removal, reverse verification required: a pin written first that fails while step 0 still reads the undeclared key, and mutation proven on disk rather than inferred from an exit code.
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsClaim —
domain:uidev seat, sessionsession_011SfZeFWrhGLHmfq61xbz4q(dispatched by the PM order above; same session, dev subagent seat). Branch:claude/issue-6531-titlefield-producer-census.Starting the premise-first fork: re-derive the spec half (does
@objectstack/spec's object schema declaretitleField?) in a fresh worktree with the dist actually installed, then run the producer census over fetched/derived shapes with positive controls. Will report the census before writing any code.
Generated by Claude Code
os-dev-report
{ "issue": 6531, "status": "done", "branch": "claude/issue-6531-titlefield-producer-census", "pr": "https://github.com/objectstack-ai/objectui/pull/6560", "premise_still_valid": true, "summary": "The census came back ZERO producers, and the premise re-derived STRONGER than the card stated: @objectstack/spec's object schema is not merely silent on titleField, it REJECTS it. Measured against @objectstack/spec@17.2.0 (the dist this repo resolves), ObjectSchema.safeParse({name,fields,titleField:'name'}) fails with `unrecognized_keys` — the same issue code a nonsense key gets — while nameField/displayNameField/titleFormat all parse and survive; ObjectSchemaBase is a strictObject (object.zod.ts:1540). So branch (2) of the fork: removed the step-0 `objectDef.titleField` leg from getRecordDisplayName, making the declared nameField the top of the object ladder per ADR-0079. Order and existence travelled together as the card required — precedence was not touched independently. The DECLARED half of step 0 (options.titleField, a real key on all six ui/*Config view schemas) is untouched and pinned by a CONTROL test green in both ablation legs. Also updated the three docblocks that described the old ladder, including the NAME_ISH_RECORD_KEYS parenthetical ('search candidates that carry only name/titleField') that was the strongest-looking evidence for reading 2 and the one claim that did not survive contact with the code.", "tests": "Union re-run at final HEAD 7d896350b, all via scripts/pm/os-verify-lock.sh; verdict lines quoted from the gates themselves, exit codes captured before any pipe.\n· @object-ui/core type-check (script echo confirmed: `> tsc --noEmit && tsc -p tsconfig.test.json`) EXIT_typecheck=0. Coverage measured not assumed: tsc --listFiles shows 1 hit each for record-title.ts and record-title.test.ts, so 'typecheck clean' covers this diff.\n· @object-ui/core tests (root-cwd vitest, path filter — never `pnpm --filter <pkg> test`, which the repo's vitest guard rejects): 'Test Files 103 passed (103)' / 'Tests 2079 passed (2079)'.\n· consumer tests 'Test Files 3 passed (3)' / 'Tests 33 passed (33)' — packages/react useRecordSearch.test.ts, plugin-map ObjectMap.markerTitle.test.tsx, app-shell InterfaceListPage.mapConfig.test.tsx. Selection derived, not guessed: every test file in the repo was scanned for a fixture pairing titleField with an object-def pointer (nameField/displayNameField/titleFormat); exactly four matched, three outside packages/core, all run.\n· @object-ui/core lint (the package's own CI command, complete): '✖ 513 problems (0 errors, 513 warnings)', EXIT_core_lint=0. All warnings pre-existing no-explicit-any; zero added lines contain `any`.\n· changeset gates: '✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)' · '✅ All workspace packages are in the changeset fixed group.' · '✅ No changeset declares a `major` bump.' · '✅ No pre-existing changeset was modified or deleted.'\n· '✅ check-control-bytes: OK (scanned 5418 tracked text file(s); skipped 85 binary).' plus a self-scan of the three changed files with grep -naP for raw control bytes: no hits.\nREVERSE VERIFICATION — pin written and COMMITTED FIRST (2996cdfd6) and red against the unmodified resolver; then re-ablated from the committed fixed state. Mutation proven on disk by content hash and anchored greps in BOTH directions, never by an exit code: HEAD blob c91fd4db922eb10c87064d2e7eac029a9eea4f27 == pre-mutation hash; post-mutation injected-text count=1, removed-text count=0, hash moved to 4a2a2cb0dea9044b2dfca9e23e000851113330e2 ('MUTATION CONFIRMED ON DISK'). Ablated run: 3 pins red ('expected \\'Undeclared Alias\\' to be \\'Canonical Name\\'' etc), 'Tests 3 failed | 72 passed (75)'. Expected direction was plain red and that is what it did. Restore leg given the SAME treatment: git checkout HEAD -- <abs path> (never a bare `git checkout --`, which reads the possibly-polluted index), hash back to c91fd4db..., git diff HEAD empty ('RESTORE CONFIRMED'), so no later measurement ran on a mutated tree. Script carried trap '<restore>' EXIT INT TERM with an absolute REPO_ROOT.\nNO REBUILD PARTICIPATES AND THAT IS CORRECT HERE: root vitest.config.mts aliases @object-ui/core to packages/core/src, so every test — mine and every consumer's — exercises resolver SOURCE. A dist-based ablation in this repo would measure nothing.\nONE OF MY OWN PINS WAS WRONG FIRST, reported rather than quietly re-run: the 'only pointer' case initially used legacy_title and stayed red after the removal. The fixture was at fault, not the code — step 4b(ii)'s *_title affix rung answers legacy_title on its own. Renamed to `headline`, name-ish by neither 4b rung, and the reason is recorded in the test.", "open_questions": [], "out_of_scope_findings": [ "filed as #6557: five more consumer-side reads of the same undeclared object-level `titleField` survive — app-shell ObjectView.tsx lines 164/2211/2228/2242/2264 (view-config fallbacks) plus the useRecordSearch memo key at line 191 and its phantom fixture. NOT folded in: the bounded-in-place exemption fails condition 2 — deleting those legs changes what a VIEW is configured with, and the replacement (drop it / re-point at nameField / hand the seam to the resolver as ObjectMap does) is a decision this census does not settle; options 1 and 2 differ observably.", "filed as #6558 (label `finding`): a dead pre-ADR-0079 getRecordDisplayName copy still exported from apps/console/src/utils.ts — zero importers, but same name and signature one import away from any console surface needing a title; it ranks titleFormat first (the ordering ADR-0079 Phase 2 inverted) and never reads nameField.", "not filed, recorded here as census output: the record-title.test.ts pin named \"honors objectDef.titleField when set\" was a PHANTOM — `name` is also a NAME_ISH_RECORD_KEYS entry, so step 4b answered it identically with the step-0 read deleted, and the very next test asserts the same string from the same record without the key. Retired inside this PR's file face rather than filed." ] }
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsPost-merge verification on
main(domain:uiseat, sessionsession_011SfZeFWrhGLHmfq61xbz4q). PR #6560 landed asc00bf2861.1. The object-level read is gone, the view-level one survives. This is the whole point of the card and both halves are measured, not one:
valueAt(record, objectDef?.titleField)→ 0 occurrences;valueAt(record, options?.titleField)→ 1, untouched.
The same key name stays declared on all six view configs and rejected on objects. That asymmetry is what made this a declared-equals-enforced restoration rather than a breaking removal, and it survived the queue intact.
2. The
CONTROLpin is onmain— the case asserting the declared view-leveloptions.titleFieldstill wins at step 0. That is the assertion that stops the removal over-reaching, and it would be the first casualty of a careless follow-up.3. The phantom pin is retired.
⚠️ Worth recording how I nearly mis-called this: a bare count of the old pin name"honors objectDef.titleField when set"returns 1, not 0, which reads as "the retired pin is still there." It is not. The single hit is atrecord-title.test.ts:149, inside the comment that explains why the pin was retired — noit()carries that name.That is my catalogued failure ⑥ — a string hit is not the claim — and it is the second time today it has nearly produced a false alarm from this seat. The claim is "no test is named that"; a comment quoting the old name matches the same bytes. The surviving string is the record of the retirement, which is exactly what should survive: without it, the next editor re-adds a fixture whose green means nothing.
Card closed as completed.
Generated by Claude Code
Filed unassigned by the dev seat working objectui#6343 (session
session_011SfZeFWrhGLHmfq61xbz4q), measured while establishing what the derivedoptions.titleFieldat the interface-page map seam actually outranks. ⛔ Not touched by that PR:packages/core/src/utils/record-title.tswas explicitly fenced out of its file face, and a precedence change there is a decision, not a mechanical repair. Duplicate scan run before filing: 247 open objectui issues enumerated via the REST list endpoint and grepped fortitleField/record-title/nameField/display name; no hits on this subject (positive control: the grep fires on #4716 and the enumeration contains #6343 itself).What the code does
packages/core/src/utils/record-title.ts, step 0:So an object-level
titleFieldis consulted before step 1'sobjectDef.nameField, which ADR-0079 Phase 2 made the canonical record-title pointer, and before the deprecateddisplayNameFieldalias at step 2.What the spec declares
@objectstack/spec's object schema (packages/spec/src/data/object.zod.ts:2040-2050) declares exactly three record-title keys on an object:nameField— "[ADR-0079] Canonical primary title field";displayNameField— "[DEPRECATED → nameField] … Accepted as an alias";titleFormat— "[DEPRECATED → nameField (ADR-0079)] Render-only title template".There is no object-level
titleField. Measured across both repos' metadata: everytitleFieldinobjectstack/examplesis view-level (gallery / calendar / timeline / gantt / map config, all of which the spec does declare —view.zod.ts:785, 799, 1124, 1166, 1267); zero object-level occurrences. Positive control for that zero:nameFieldfires in the same search space (examples/app-todo/src/objects/task.object.ts:209), andlocationFieldfires inexamples/app-showcase.Why this is a card and not a cleanup
Two readings, and they lead to opposite edits — which is why nobody should pick one in passing:
objectDef.titleField, so the read is a lenient consumer-side alias for a key no producer declares — the exact shape AGENTS.md Commandment #0.1 bans, and a live invitation for AI-authored metadata to spell the pointer three ways. Removing it makes the declarednameFieldthe top of the ladder, as ADR-0079 says it is.getRecordDisplayNameis deliberately defensive (objectDefisany; steps 4b/5 exist for partial shapes), and lookup chips / search candidates are documented as arriving with "onlyname/titleField" (see theNAME_ISH_RECORD_KEYSdocblock in the same file). If some fetched shape really does carrytitleField, deleting the read silently degrades those titles.Reading 2 needs a census that this card does not have: which producers, if any, put
titleFieldon an object-shaped payload. That census is the work here, and its answer decides between "remove the read" and "declare the key in the spec".Either way the ORDER is separately questionable: even if the read stays, an undeclared/legacy key ranking above the canonical declared pointer inverts the same governed-authority default objectui#6343 restored one seam up.
Refs: objectui#6343 (where this was measured, and the seam that fed step 0 a derived name) · ADR-0079 ·
packages/spec/src/data/object.zod.ts:2040-2050(objectstack).