Uh oh!
There was an error while loading. Please reload this page.
fix(core): drop the undeclared objectDef.titleField read from getRecordDisplayName step 0 - #6560
Conversation
…itleField` Written BEFORE the fix as the reverse verification for objectui#6531. All three negative pins fail against the current resolver with `expected 'Undeclared Alias' to be …`, i.e. the step-0 `objectDef.titleField` read beating the canonical `nameField`. Also retires a phantom pin: the old "honors objectDef.titleField when set (object-level title hint)" asserted a title that `NAME_ISH_RECORD_KEYS` step 4b resolves identically, so it never measured the step-0 read at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
…itle ladder
`getRecordDisplayName` step 0 consulted `objectDef.titleField` and ranked it
ABOVE the `nameField` ADR-0079 Phase 2 made the canonical record-title
pointer. `@objectstack/spec`'s object schema is a `strictObject`:
`ObjectSchema.safeParse({ …, titleField })` fails with `unrecognized_keys` —
the same code a nonsense key gets — while `nameField`, `displayNameField` and
`titleFormat` all parse. A producer census across both repos found nothing
that puts the key on an object-shaped payload, so the read was a
consumer-side alias for a key no producer can ship (AGENTS.md Commandment
#0.1), inverting the governed-authority default on top of it.
The DECLARED half of step 0 is untouched: `options.titleField` is a real view
key (`ui/CalendarConfig`, `ui/GalleryConfig`, `ui/GanttConfig`,
`ui/ListMapConfig`, `ui/ObjectKanbanProps`, `ui/TimelineConfig`) and still
wins, so no view loses its author-chosen title field.
Fixes an accidental fixture bug in the pin committed just before: the
"only pointer" case used `legacy_title`, which step 4b(ii)'s `*_title` affix
rung answers on its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q…ce fix Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-support-ai
commented
Aug 26, 2026
ACCEPT — PM review of #6531, done from both repos' trees rather than from the report. The census answered zero, and the premise came back stronger than the cardThe card said the spec's object schema "does not declare"
Strict plus absent means an object payload carrying the key fails with The half that keeps the fix from over-reachingYour line "undeclared at the object level ≠ undeclared everywhere" is the crux, and I verified every name on it.
So the same key name is declared on views and rejected on objects, and the fix removes exactly one of those two reads. The The pin that never measured its own name⭐⭐⭐ This is the most valuable thing in the PR, above the fix. The old case was called "honors objectDef.titleField when set" and its fixture was A pin that passes for a reason unrelated to its name is worse than no pin, because it reads as coverage. That is the same failure class as a one-directional parity gate, and finding one inside your own card's file face is the kind of thing a census is actually for. ⭐⭐ And you caught your own bad fixture and reported it instead of quietly re-running: the "only pointer" case first used VerificationThe mutation-on-disk proof in both directions (content hash moved, anchored greps counted in both directions, restore leg hashed back to the HEAD blob with Spin-offs, correctly handled#6557 filed unlabelled and #6558 with Landing on green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6531
The census was the work, and it came back zero
The card asked which producers, if any, put
titleFieldon an object-shaped payload handed togetRecordDisplayName. Nothing does — and the contract is stronger than "does not declare it".The spec half, re-derived rather than assumed. The PM could not verify this from the shared checkout (no spec dist installed there). Measured in this worktree against
@objectstack/spec@17.2.0— the exact dist this repo resolves — by callingObjectSchema.safeParsedirectly:nameField: 'name'(control)displayNameField: 'name'(control)titleFormat: '{name}'(control){dialect:'template',source:'{name}'}titleField: 'name'(subject)unrecognized_keystotallyMadeUpKey: 'x'(negative control)unrecognized_keysThe subject fails with the same issue code as a nonsense key.
ObjectSchemaBaseis astrictObject(packages/spec/src/data/object.zod.ts:1540), shut by objectstack#4001 precisely becauseparse()used to drop unknown keys in silence. The declared object shape has 42 top-level keys;titleFieldis not one of them, and the only key matching/title/iistitleFormat.So object-level
titleFieldis not merely undeclared — no spec-compliant producer can ship it. The premise held, and hardened.The producer half. Every population, each with a positive control that fired:
packages/spec/authorable-surface/*.json(machine-generated ledger)data/ObjectdeclaresnameField,displayNameField,titleFormat. All sixtitleFieldentries in the entire surface areui/*Configview keys.data/Object:nameFieldfires indata.jsonnameFieldfires inobjectstack/examples/app-todo/src/objects/task.object.ts.titleField =, both repos.label =fires 24× (objectui), 23× (objectstack)titleField:in objectui non-test srcgetObjectSchemaimplementation in objectui (10, incl. the realdata-objectstackadapter)normalizeSchemaReferenceKeysandapplyFieldWidgetOverrides.titleFieldfires inplugin-gantt/demo/main.tsx— on the gantt view schema, not the object schema that file'sgetObjectSchemareturns@objectstack/objectql#titleFieldOfreadsnameField→displayNameFieldand nothing elsetitleFieldOfyet takes its pointer fromnameField— "titleField" is the concept,nameFieldis the keyThe two paths the
NAME_ISH_RECORD_KEYSdocblock cited, which triage named as the remaining population, were traced by hand rather than grepped:useRecordSearch) — theobjectDefis either an entry from the app'sobjectsmetadata array or the stub the hook synthesizes for a hit whose object is not in that array:{ name: objectName }. Neither carries the key.LookupField) — theobjectDefisdataSource.getObjectSchema(referenceTo), passed through verbatim.That docblock's own wording ("lightweight search candidates that carry only
name/titleField") was the strongest-looking evidence for reading 2, and it is the one thing here that did not survive contact with the code. It is rewritten in this PR to say what the shape actually is.Verdict: branch (2). The read was a consumer-side alias for a key no producer can ship — AGENTS.md Commandment #0.1 — ranked above the
nameFieldADR-0079 Phase 2 made canonical.What changed
packages/core/src/utils/record-title.ts— step 0 loses its second leg:Order and existence travelled together, as the card required. The precedence was not touched independently: the undeclared key is gone, and with it the inversion.
nameFieldis now the top of the object-level ladder, exactly as ADR-0079 states.The declared half of step 0 is untouched.
titleFieldis a real spec key — on views (ui/CalendarConfig,ui/GalleryConfig,ui/GanttConfig,ui/ListMapConfig,ui/ObjectKanbanProps,ui/TimelineConfig) — and views hand it in asoptions.titleField. That leg still outranks everything, so no view loses its author-chosen title field. A dedicated CONTROL test pins it in both directions of the ablation below.The three docblocks that described the old ladder (module header,
getRecordDisplayNameprecedence list,NAME_ISH_RECORD_KEYS) were updated with it; a stale ladder in a docblock is how the next reader re-adds the leg.Reverse verification — pin first, mutation proven on disk
The pin was written and committed before the fix (
2996cdfd6), and it failed against the unmodified resolver.Then, from the committed fixed state, the read was put back and the tree re-measured. The mutation was proven on disk by content hash and anchored greps in both directions, never by an exit code:
Expected direction was plain red, and that is what it did. The
options.titleFieldCONTROL stayed green in both legs, which is what isolates the removal from an over-reach. The restore leg was proven the same way as the mutation leg — byte identity against the HEAD blob, plus an emptygit diff HEAD— so no later measurement in this branch ran on a mutated tree. Restore usedgit checkout HEAD -- <path>with an absolute repo root, never a baregit checkout --(which reads from a possibly-polluted index).Note for anyone re-running this:
@object-ui/coreis aliased topackages/core/srcin the rootvitest.config.mts, so tests exercise resolver source; nodistbuild participates and adist-based ablation here would measure nothing.Two fixture corrections, both worth naming
A phantom pin retired.
record-title.test.tscarriedIt never measured that.
nameis also aNAME_ISH_RECORD_KEYSentry, so step 4b answers identically with the step-0 read deleted — and the very next test asserts the same string from the same record without the key. A pin that passes for a reason unrelated to its own name is how an undeclared key survives a rewrite. Replaced by a fixture that states what it actually covers, with the real negative pins in a new block.One of my own pins was wrong first, and is reported rather than quietly fixed. The "only pointer" case initially used
legacy_title; it stayed red after the removal. The fixture was at fault, not the code — step 4b(ii)'s*_titleaffix rung answerslegacy_titleon its own. Renamed toheadline, which is name-ish by neither 4b rung, so the pin now isolates what it claims. The comment in the test records this so the field name is not "simplified" back later.Verification
Union re-run at final HEAD
7d896350b, all through the shared verify lock:@object-ui/coretype-check> tsc --noEmit && tsc -p tsconfig.test.json→ exit 0@object-ui/coretestsTest Files 103 passed (103)·Tests 2079 passed (2079)Test Files 3 passed (3)·Tests 33 passed (33)@object-ui/corelint✖ 513 problems (0 errors, 513 warnings)→ exit 0check-changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)check-changeset-fixed✅ All workspace packages are in the changeset fixed group.check-changeset-no-major✅ No changeset declares a `major` bump.check-changeset-overwrite✅ No pre-existing changeset was modified or deleted.check-control-bytes✅ check-control-bytes: OK (scanned 5418 tracked text file(s); skipped 85 binary).Typecheck coverage is measured, not assumed.
--listFilesconfirms both edited files are in the compiled program (1 hit each), so "typecheck clean" is a statement about this diff and not about a set that excludes it.Consumer selection is derived, not guessed. The only shape this change can move is an
objectDefcarryingtitleField. Every test file in the repo was scanned for a fixture pairingtitleFieldwith an object-def pointer (nameField/displayNameField/titleFormat); that returned exactly four files, and the three outsidepackages/coreare the three run above. All pass.Lint narrowing, declared. Repo-wide
pnpm lintisturbo run lint— each package's owneslint .— and CI runs it in full regardless. Narrowed here to the affected package's own CI command, run complete:files: ['**/*.{ts,tsx}'];packages/corehas no local override.--format json: 198 files linted, 0 errors, 513 warnings, and both edited files are present in that population.tseslint.configs.recommendedwith noparserOptions.project/projectService— linting is not type-aware, so each file's verdict is a function of its own bytes and the shared config. This diff changes no config file and no file outsidepackages/core, so no untouched file's verdict can move.All 513 warnings are the pre-existing
@typescript-eslint/no-explicit-anyfamily (the resolver is deliberatelyany-typed for partialobjectDefshapes). None is introduced here: zero added lines containany.Out of scope, filed
titleFieldsurvive #6531 — inObjectViewview-config fallbacks and auseRecordSearchmemo key #6557 — five more consumer-side reads of the same undeclared key survive:ObjectView.tsxlines 164 / 2211 / 2228 / 2242 / 2264 (view-config fallbacks), plus theuseRecordSearchmemo key at line 191 and its fixture. Not folded in: deleting those legs changes what a view is configured with, and the honest replacement (drop it · re-point atnameField· hand the seam to the resolver asObjectMapdoes) is a decision this census does not settle. Options 1 and 2 differ observably.getRecordDisplayNamecopy still sits exported inapps/console/src/utils.ts#6558(finding) — a dead pre-ADR-0079getRecordDisplayNamecopy still exported fromapps/console/src/utils.ts. Zero importers, but same name and signature, one import away from any console surface that needs a title.#6557and#6558are not addressed here and remain open.Generated by Claude Code