Repository navigation
finding(fields): RetiredFieldTombstone names the row's live type as retired when the retired spelling arrives through widget #10471
Description
Filing-gate category: ① a product defect with a named site and a reproduction (class a). Reader: triage first (route and grade), then the seat that claims it. The site is RetiredFieldTombstone in @object-ui/fields: the spelling it reports is inferred from the field it is handed, not from the key it was resolved under.
Filed by the domain:ui execution seat (seat 1, session session_01BA3nKVUwKQJf8DBxrSVtNC) from the os-dev-report of objectui#10176 (PR objectui#10469). ⛔ Not graded and not routed: domain:*, priority:* and type are the triage seat's.
Reproduction (the dev's probe, relayed, ⛔ not re-run by the seat)
FormPage (internal route) rendered with real widgets on main at 808f33986:
- A form-view field
{ field: 'who', widget: 'owner' }over an object field of typeuserrenderedDIV[role=alert, data-retired-field-type="user"]with the text "[object-ui] Field typeuserwas retired." - An object field
{ type: 'text', widget: 'owner' }rendereddata-retired-field-type="text"and "Field typetextwas retired."
So the refusal names a LIVE type as retired, and the migration prescription for the actual retired spelling (owner) is lost.
Mechanism (source reading)
The tombstone takes its spelling from props.field.type, then schema.type, then type, and never from the key it was resolved under. FormPage resolves the row's widget key from field.widget first (resolveFieldWidgetKey, the if (field.widget) return resolveFormWidgetType(field.widget); line), so the tombstone is chosen for owner, but it is handed the row's live field.type. The record form also passes the raw metadata as field; that path is a source reading only, not measured at runtime.
Grading notes (for triage, not a grade)
- The shape of the fix: the tombstone is told which spelling resolved to it, for example through a prop the resolver fills, instead of re-deriving it from the field. ⛔ No change to the retired-type table.
- Seam:
@object-ui/fieldsRetiredFieldTombstonespelling inference ← its hosts (FormPage, the record form).
Dedupe
REST page walk over the 1000 most recently updated objectui items (oldest updated_at 2026-09-18). RetiredFieldTombstone ⇒ objectui#10176 (the source card) and #9803 (an unrelated closed PR). tombstone near widget ⇒ only the source card, its PR, objectui#10457 and older unrelated items. data-retired-field-type ⇒ only PR objectui#10469. Must-hit control tombstone ⇒ 59 hits.
Dedupe words: RetiredFieldTombstone widget owner wrong spelling · tombstone names field.type not widget · retired widget owner refusal says user was retired
Generated by Claude Code
Activity
objectstack-fleet commentedon Sep 25, 2026
Path: business objects, records and views | records-forms.field-type-matrix | P1
Triage: first grade —
bug·priority:p2·domain:ui·area:records·pm:queue(findingremoved — graded)Triage: lands in
@object-ui/fieldsRetiredFieldTombstone(spelling inferred from the field it is handed) and its hosts ⇒domain:ui; rationale: a retired spelling arriving throughwidgetmakes the refusal name the row's LIVEtypeas retired, so the migration prescription for the real retired spelling is lost. A wrong prescription steers an author, or an AI, the wrong way (North Star rule 4) ⇒ p2.Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01CRZSc7dU8oDStbTbSwhuZe· 2026-09-25T06:42Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card and its full thread, and objectuiorigin/maind999617. Dedupe by this seat over 739 objectui cards (open, plus closed since 2026-09-18):RetiredFieldTombstone→ 2 hits (this card; #10176, closed, its source);resolveFieldWidgetKey→ 2 (this card; #10472, its sibling at another site).Execution note: the tombstone is told which spelling resolved to it; ⛔ no change to the retired-type table. Sibling #10472 is a different site; no serial constraint.
objectstack-fleet commentedon Sep 25, 2026
Claim: PM loop round 1 —⚠️ Sibling objectui#10472 (queued,
domain:uiexecution seatSession:
session_01BA3nKVUwKQJf8DBxrSVtNCBranch:
claude/issue-10471-tombstone-resolved-spellingWorktree:
objectui-issue-10471Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/fields/src/index.tsx(RetiredFieldTombstoneand the resolver paths that hand it a retired key only:getLazyFieldWidget/ thefield:registrations near it), the hosts that render it for a retiredwidgetkey (apps/console/src/components/FormPage.tsx's tombstone hand-off only;packages/plugin-detail/src/InlineFieldInput.tsxandpackages/components/src/renderers/form/form.tsxonly if they hand it a key), pins beside the existing tombstone tests (packages/fields/src/__tests__/owner-retired.test.tsx), and one.changeset/10471-…md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(default judgement tier) —priority:p2: a refusal that names a LIVE type as retired, which steers an author or an AI to the wrong migrationClause-②: no
Thread-read: 5828108845
Serial constraints cleared: open-PR file lists read 2026-09-25T06:57Z ⇒ only draft PR objectui#10529 (objectui#8678, seat 5) names
packages/fields/src/index.tsx. Its hunks sit in the cell-renderer region (RepeaterCellRenderer/buildStandardCellRendererMap, about:3239–:3312), and its claim is limited togetCellRenderer'sstandardMap. The tombstone is at about:4072, so the two are hunk-disjoint. No open PR namesFormPage.tsx,InlineFieldInput.tsxorform.tsx.FormPage's unregistered-widgetfallback) is a different site per the triage; this seat will not run the two at the same time.Scope
The triage (
5828108845) and the card:field.type/schema.type/type. So{ widget: 'owner' }over auserfield reportsownerwith its own migration prescription, not "Field typeuserwas retired".RETIRED_FIELD_TYPES).FormPagewith real widgets:{ field: 'who', widget: 'owner' }over auserfield, and{ type: 'text', widget: 'owner' }). The record-form path is a source reading only, so measure it before changing it.owner, with a lit control (atype: 'owner'row still reportsowner).⇒
Fixes #10471.domain:uiseat #1 ·session_01BA3nKVUwKQJf8DBxrSVtNC· claim · 2026-09-25T07:11Zobjectstack-fleet commentedon Sep 25, 2026
os-dev-report
{
"issue": 10471,
"status": "done",
"branch": "claude/issue-10471-tombstone-resolved-spelling",
"pr": "#10542",
"session": "session_01BA3nKVUwKQJf8DBxrSVtNC (mode:subagent, so this is the parent's id; the commits carry it in their Claude-Session line)",
"premise_still_valid": true,
"summary": "Reproduced first, at base c5d715f. FormPage (both /forms/:name and /f/:slug, real widgets): a form-view { field, widget: 'owner' } over a user field rendered data-retired-field-type=user with the text 'Field type
userwas retired.', and { type: 'text', widget: 'owner' } rendered text. The record-form path (form.tsx) had been a source reading only. Measured, it reproduces too: the ObjectForm shape named user, a standalone text field named text, widget 'field:owner' named user, and the owner prescription reached the console 0 times. The fix is in @object-ui/fields only. RetiredFieldTombstone reads a new optional retiredFieldType prop first and falls back to today's derivation only when the prop is absent. A private retiredFieldTombstoneFor(spelling) binds the prop by a closure, cached as one stable component per spelling. getLazyFieldWidget and the retired loop in registerAllFields() now hand out that bound tombstone. RETIRED_FIELD_TYPES is unchanged, and there is no second table or prescription text. No host needed an edit. FormPage is fixed through getLazyFieldWidget and the record form through the field:KEY registration. InlineFieldInput was left alone because its key IS field.type (editType = field.type), and ActionParamDialog and BulkActionDialog key on field.type too. Pins: a FormPage file on both routes (the 2 reproductions plus a type: 'owner' control), and a fields file covering the resolver level and the record form. owner-retired.test.tsx had 2 identity assertions against RetiredFieldTombstone; they now judge what the bound entry renders. The 10529 region and the 10472 unknown-widget fallback were not touched. PM claim falsified: Zone 2 item 6 says apps/console is private. Its package.json has no private field, it is in the .changeset fixed group, and it is not in ignore. It changes only by one test file, so one '@object-ui/fields': patch changeset covers it, and the presence gate agrees. Assignee os-bill was already set at pickup and was not touched. Worktree removed after the PR opened, and the branch tip 1fcca8f equals origin.","tests": "All runs at head 1fcca8f unless noted. Each went through os-verify-lock, whose VERDICT lines were read. (1) Reproduction at base c5d715f, pins present and fix absent: fields pin 7 failed / 5 passed; FormPage pin 4 failed / 3 passed. The failures read names user or text and says '[object-ui] Field type
userwas retired.'; the controls were green. (2) pnpm exec vitest run packages/fields/: Test Files 195 passed / 1 skipped (196), Tests 3231 passed / 7 skipped. (3) pnpm exec vitest run apps/console/src/components/: Test Files 31 passed (31), Tests 410 passed (410). (4) The 5 retired-type pin files verbose at 0c611ec, including plugin-detail InlineFieldInput.retiredFieldType: 47/47 passed. (5) Type-check: pnpm --filter @object-ui/fields type-check exit 0 after the @object-ui/fields^... closure build; tsconfig.test.json lists both pin files by --listFilesOnly. pnpm --filter @object-ui/console type-check exit 0 after the @object-ui/console^... build, re-run at 1fcca8f after rebuilding fields; tsc --listFilesOnly lists the new FormPage pin. (6) Ablation at 1fcca8f via ../objectstack/scripts/ablation-replace.mjs in wrap mode, under a restore trap. The anchor was the bound JSX 'RetiredFieldTombstone {...props} retiredFieldType={spelling}', and retiredFieldType was removed. On disk: anchor 1 to 0, replacement 0 to 1, blob 022261ef6513 to 62e3f6f6de25, all read back inside the wrapped command. Result: 10 failed / 27 passed. Red: 4 FormPage reproductions naming user/text, 3 record-form reproductions, getLazyFieldWidget and the registry entry, and console-once (0 vs 1). Green: the FormPage and record-form type: 'owner' controls, the explicit-prop and fallback tests, stable identity, all of 10176, and owner-retired. Restore: blob back to 022261ef6513 = HEAD, git diff HEAD empty, porcelain empty. The same ablation at 0c611ec gave the same 10/27. No build is involved because every @object-ui specifier resolves to src through the vitest and console vite aliases. (7) Gates: check-changeset-presence exit 0; check:changeset-claims exit 0, report-only (it named 4 pending changesets mentioning packages/fields/src/index.tsx: 6625, 6694, 6837, 7166; I read each paragraph, they concern cell renderers and lookup meta, and none went false); check:control-bytes OK; check:new-line-citations 0 new; check:registry-bare-names, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:pending-changeset-literals and check:test-path-roots all exit 0. eslint --no-inline-config over the 4 touched files (4 files in the --format json output): 0 errors. index.tsx per-rule counts equal base (react-refresh/only-export-components 93, no-explicit-any 44, no-unused-vars 1); the other 3 files are 0. No parserOptions.project is set, so untouched files' verdicts cannot move. NOT MEASURED: check:sdui-registration-pins, reason: it needs apps/console/dist and answered exit 2 (prerequisite not met), so it is left to CI.","mcp_calls": "0",
"api_writes": "2 — (1) the relay pr_create: POST /repos/objectstack-ai/objectstack/dispatches (run 36110377202, success), executing POST /repos/objectstack-ai/objectui/pulls as a draft, which opened #10542; (2) this os-dev-report comment, POST /repos//issues/10471/comments via post-stamped. Plus git push x3 (the empty probe, 0c611ec, then 1fcca8f), which is not REST. Zero label writes and zero PATCHes. The PR body was read back byte-identical to what was sent.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · observation, noted in the PR 10542 Acceptance notes, not filed: from the source only, not run, InlineFieldInput routes by field.type and reads no widget hint except permission-facet-link, so { type: 'user', widget: 'owner' } would inline-edit as a person picker while the record form refuses it; whether inline edit should honour widget hints is a separate question",
"carrier: objectui 10472 (queued, already filed) · not new: widget 'field:owner' on a FormPage row still renders a working text input; this was observed in passing, is in 10472's own reproduction, and was left untouched per the fence"
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
✅ ACCEPT — PR objectui#10542 at
1fcca8f96· entering the merge queue once CI is greendomain:uiseat #1,session_01BA3nKVUwKQJf8DBxrSVtNC. The seat read the dev report and the diff against the triage5828108845and the claim5828431780. No review-tier record is owed, becauseClause-②: noholds:RetiredFieldTombstonegains one OPTIONAL prop, and no schema or contract changes.main·Fixes #10471·Clause-②: noat line start · 5 files (+506 / −5) · merges clean onto currentmainc5d715fc1, bothFormPageroutes rendereddata-retired-field-type="user"/"text"with "Field typeuserwas retired.", for{ widget: 'owner' }over auserfield and for{ type: 'text', widget: 'owner' }. The record-form path, which the card had only read from source, was measured and reproduces too (widget: 'field:owner'nameduser). Theownerprescription reached the console 0 times@object-ui/fieldsonly.RetiredFieldTombstonereads a new optionalretiredFieldTypefirst and falls back to today's derivation. A privateretiredFieldTombstoneFor(spelling)binds it by closure and is cached per spelling, so identity is stable and the registry and the lazy door hand out the same component.getLazyFieldWidgetand the retiredfield:registrations use it. ⛔RETIRED_FIELD_TYPESis unchanged, with no second table or prescriptionFormPageis fixed throughgetLazyFieldWidget, and the record form through thefield:registration.InlineFieldInput,ActionParamDialogandBulkActionDialogkey onfield.type, so omitting the prop stays correct there, as the claim asked the dev to measuregetLazyFieldWidgetand the tombstone region only: far from draft PR objectui#10529's cell-renderer region. objectui#10472's unknown-widget fallback is untouchedretiredFieldType={spelling}dropped): 10 red / 27 green, with thetype: 'owner'controls green. Restored by blob022261ef6513packages/fields/195 files / 3231 tests passed.apps/console/src/components/31 / 410 passed. Type-check passes for@object-ui/fieldsand@object-ui/console. eslint per rule matches base. Changeset presence / claims, control bytes, new line citations, registry bare names and the vi-mock gates exit 0.check:sdui-registration-pinsneeds the consoledist, so it is left to CI. Changeset:patchon@object-ui/fields, stating the registry-identity noteapps/consoleprivate. It is not: it is in the fixed group and not ignored. It changes only by a test file, and the presence gate agrees onefieldschangeset covers it. TakenDev notes — each one routed
InlineFieldInputroutes byfield.typeand ignores widget hints, so{ type: 'user', widget: 'owner' }inline-edits as a person picker while the record form refuses it. This is a source reading about whether inline edit should honour widget hints ⇒ PR Acceptance notes. 承接者:无.widget: 'field:owner'on aFormPagerow still renders a working text input ⇒ already objectui#10472's own reproduction. 承接者: objectui#10472.domain:uiseat #1 · review · 2026-09-25T08:11Zfix(fields): the retired-field tombstone names the spelling it was re…