Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

EDIT_WIDGETS still maps owner to UserField, so the grid's inline cell editor renders a working person picker for the retired spelling #4931

Closed

Description

@os-steve

Measured while implementing #4914's dispatched fast-follow (items 1-3 + 5, PR pending). Filed unassigned.

The face

packages/fields/src/FieldEditWidget.tsx:85 — EDIT_WIDGETS contains:

owner: UserField,

This is not one of the faces #4914 enumerates. That card lists FieldEditWidget.tsx:170 (the COMPACT_EDIT_TYPES set, cosmetic — trigger sizing) as item 11. Line 85 is the routing table itself, and it is live.

Why it is live rather than dead

hasFieldEditWidget(type) answers resolveInlineEditType(type) in EDIT_WIDGETS, and resolveInlineEditType returns the type unchanged when it is already a key (FieldEditWidget.tsx:144). So the alias table is never consulted and the retirement never applies:

hasFieldEditWidget('owner') === true

Measured on origin/main @ 69dbea20a (post-#4915). Every host built on FieldEditWidget therefore still offers a working person picker for a stored field typed owner, while the record form answers the same field with the tombstone refusal — the same two-surfaces-disagree shape that promoted #4914 item 5 out of the "inert" framing.

Directly observed while reverse-verifying the #4914 work: with the retired-spelling refusal branch removed from InlineFieldInput and 'owner' gone from its routing table, a retired field still rendered a picker trigger reading Select… — tombstone: false, plainTextInput: false, one role=button. Deleting a routing-table membership does not disable the type; delegation reaches the same widget by another road.

Scope of the exposure

InlineFieldInput is not affected once #4914's fast-follow lands: its refusal branch runs before both the routing switch and the delegation tail. The unprotected consumers are the ones that call FieldEditWidget / hasFieldEditWidget directly — the grid's inline cell editor is the one with a user-facing surface.

Suggested fix

Drop the owner key from EDIT_WIDGETS and let resolveInlineEditType fall through to the alias table, which already answers retired spellings with field:owner (field-type-alias.ts) — i.e. make this seam consult the retirement the same way resolveFormWidgetType does. Better still, have hasFieldEditWidget answer false for any key of RETIRED_FIELD_TYPES so the whole class closes at once rather than one spelling at a time. COMPACT_EDIT_TYPES (#4914 item 11) can be cleaned in the same stroke.

Note for whoever takes it: packages/plugin-detail/src/__tests__/InlineFieldInput.retiredFieldType.test.tsx (landing with #4914's fast-follow) deliberately does not pin today's hasFieldEditWidget('owner') === true in either direction, precisely so it does not go red on this fix.


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 17, 2026
  2. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor

    Claim: PM loop round 5
    Session: session_01DbRmJD3iPhXjKr6vhd4Qkv
    Branch: claude/issue-4931-edit-widgets-retired-gate
    Worktree: objectui-issue-4931
    Domain: repo:objectui
    File surface: packages/fields/src/FieldEditWidget.tsx (the EDIT_WIDGETS owner key, hasFieldEditWidget / resolveInlineEditType, and COMPACT_EDIT_TYPES — #4914 item 11 rides the same stroke per the card's suggestion) + tests (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: in-flight #4914 / PR #4932 (packages/types + plugin-detail — in landing) is disjoint from packages/fields; #4636 / PR #4639 (ranges + lockfile, parked) disjoint; no open dev PR touches FieldEditWidget.tsx (checked this round). Coupling declared: PR #4932's InlineFieldInput.retiredFieldType.test.tsx deliberately does NOT pin hasFieldEditWidget('owner') in either direction, so this fix cannot redden it — recorded on both cards. Grading note: promoted from finding under the standing A′ ruling (this is the last measured active-contradiction face; the fix restores the ruled invariant, no new decision required).


    Generated by Claude Code

  3. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor
    {
      "issue": 4931,
      "status": "done",
      "branch": "claude/issue-4931-edit-widgets-retired-gate",
      "pr": "https://github.com/objectstack-ai/objectui/pull/4942",
      "premise_still_valid": true,
      "summary": "Premise re-measured on my own branch (origin/main 25c8007d4, not the card's 69dbea20a) before any edit and confirmed exactly: hasFieldEditWidget('owner') === true, isInlineExcludedFieldType('owner') === false, and a direct FieldEditWidget call with type 'owner' rendered a working person picker (one role=button reading 'Select…', no tombstone). Implemented the endorsed fix as a CLASS closure keyed on the live RETIRED_FIELD_TYPES table, imported from ./field-type-alias rather than the ./index barrel (which re-exports this file — the barrel would cycle) and never copied: hasFieldEditWidget answers false ahead of every table lookup, so it holds even if a retired spelling is still a key of EDIT_WIDGETS; isInlineExcludedFieldType answers true; FieldEditWidget itself renders no control and writes the once-per-spelling prescription; and 'owner' is dropped from EDIT_WIDGETS and from the cosmetic COMPACT_EDIT_TYPES set (#4914 item 11). ONE THING MATERIALLY SHAPED THE FIX beyond the dispatch's four adjudicated points and should be read before review: closing the hasFieldEditWidget gate ALONE would have traded a visible wrong answer for a silent one. ObjectGrid.tsx:1989 forces a column to editable:false exactly when isFieldInlineEditable is false, and that predicate consults isInlineExcludedFieldType; the cell editor at :2674 returns null when hasFieldEditWidget is false, and the source comment there states that null 'lets DataTable fall back to its built-in text/number/date inputs'. So gate-1-only would have left the column editable and handed a stored person reference a PLAIN TEXT box that saves a bare string over it — the dispatch's own forbidden outcome. The isInlineExcludedFieldType half is therefore load-bearing, not decoration, and Leg B of the reverse verification proves it empirically. Gate 3 answers with the console prescription rather than a rendered RetiredFieldTombstone deliberately: this component's hosts are CELL-sized, and #4814 settled the same question the same way for the read path ('there is no visible alert a table CELL can carry without wrecking the row'); the visible refusal stays the record form's job, and returning null is this function's documented pre-existing contract for 'no dedicated widget', not a new fallback. The tombstone was also not importable here without a module cycle. File surface held exactly: FieldEditWidget.tsx plus tests of it, all inside packages/fields. The `user` path is untouched and pinned — picker, compact sizing, expanded-object resolution all unchanged.",
      "tests": "Union run at HEAD=e255f9123 (= final commit = pushed remote head; PR #4942 head sha confirmed e255f9123bf0af66a649b0bc8fc67ec29e6c4bd7). Repo-root path-filter vitest; every heavy run under `flock -E 99 -w 540 /tmp/os-heavy-verify.lock`; dependency closures built BEFORE every type-check verdict. RESULTS: `packages/fields/` -> 'Test Files 103 passed (103) / Tests 1705 passed (1705)'. The three REAL direct consumers (identified by grepping hasFieldEditWidget / FieldEditWidget / isInlineExcludedFieldType across packages+apps+examples): `packages/plugin-grid/ packages/plugin-kanban/ packages/plugin-detail/` -> 'Test Files 167 passed (167) / Tests 1514 passed (1514)' — this covers PR #4932's InlineFieldInput.retiredFieldType.test.tsx, which deliberately does not pin hasFieldEditWidget('owner') and is confirmed green, so the declared coupling held. type-check for @object-ui/fields, plugin-grid, plugin-kanban, plugin-detail -> all 'Done', with the script echoed as `tsc --noEmit && tsc -p tsconfig.test.json` (hyphenated `type-check`, so no zero-match trap). First type-check attempt FAILED on TS2307 for unbuilt @object-ui/mobile / @object-ui/permissions — the closure trap, not my change; green after `pnpm --filter '<pkg>^...' build` for the consumers. GRID CONSUMER MEASURED BEFORE/AFTER (the dispatch's explicit ask), via a throwaway probe in plugin-grid, since a permanent grid-side test would breach the declared file surface: isFieldInlineEditable({type:'owner'}) true -> FALSE, i.e. the column goes from editable to forced editable:false, so a stored owner field degrades to the grid's NORMAL READ-ONLY CELL — not a crash, not a string save; isFieldInlineEditable({type:'user'}) stays true. Read path unchanged from #4814 (text cell + prescription). REVERSE VERIFICATION, both legs from the COMMITTED state, direction predicted FIRST, and one of them a genuine reversal of the template expectation. Leg A (restore `owner: UserField` to EDIT_WIDGETS, keep the gates) — predicted ALL PINS STAY GREEN, because the gates key on the retirement table rather than on map absence, which IS the class-closure property; a red would have disproved it. Observed exactly that: 'Test Files 2 passed (2) / Tests 21 passed (21)'. Leg B (strip all three gates, keep the EDIT_WIDGETS deletion — the weaker spelling-only fix) — predicted road 1 and road 3 stay GREEN (hasFieldEditWidget is already false via map absence; the alias table still fires the prescription as a side effect of resolveInlineEditType) and EXACTLY the two exclusion pins redden. Observed exactly that: 'Tests 2 failed | 10 passed (12)', failing on `expected [ 'owner' ] to deeply equal []` and `expected false to be true`. Both legs restored with `git checkout e255f9123 -- ` + the edited path; `git diff HEAD` empty afterwards. FIXTURE TRIAGE — two existing fixtures pinned the exact branch removed, each triaged individually rather than batch-respelled: FieldEditWidget.test.ts:74 asserted hasFieldEditWidget('owner')===true in the relational-picker list (owner dropped, three survivors kept); complex-widgets.test.tsx 'owner field resolves an expanded-object value inline' was REPLACED/removed rather than re-spelled, because its value-shape contract is unchanged and still pinned by the identical `user` case immediately above it. I scanned by the rule's consumer radius, not by edited package (grepped the owner spelling across fields/plugin-grid/plugin-kanban/plugin-detail); every other hit is the field NAME `owner` (the surviving idiom) and none reddened. ONE OF MY OWN ASSERTIONS WAS FALSE and was corrected to measured reality rather than forced: hasFieldEditWidget('not-a-real-type') is TRUE, not false — an unknown type resolves through the alias table's `|| 'field:text'` tail onto the `text` editor. That measurement sharpens rather than weakens the card's point and the test now states it: the retired spelling is the ONE type the text tail does not catch, and had `owner` merely been dropped from the widget map it would have landed on exactly that text-box fallback. GATES derived from the actual changed paths and run on the final tree: check:control-bytes OK (4419 tracked text files), check:self-import OK, check:phantom-deps OK, check:published-dist OK (39 published packages), changeset:check OK, check:doc-types OK; plus a manual control-byte scan (grep -naP) of all five changed files: clean. Real patch changeset for @object-ui/fields (published behavior change; no exported signature changed, so this is a behavior narrowing rather than a type narrowing). No skip-changeset label created or applied — phantom in this repo per #4912.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Filing note (nothing new to file). Every residual owner face I encountered while working is already enumerated on the parent card #4914, so filing would have duplicated it: buildDefaultPageSchema.ts:433 is its item 6, and the two faces this PR closes are its item 11 (FieldEditWidget.tsx:170, COMPACT_EDIT_TYPES) and item 15 (FieldEditWidget.test.ts:74) — both can be struck from that card's list when this lands. I searched open issues before concluding that; no unrecorded defect was found.


    Generated by Claude Code


    Generated by Claude Code

  4. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor

    ACCEPT — PR #4942 (reviewer of record: PM session session_01DbRmJD3iPhXjKr6vhd4Qkv; readings taken on GitHub at head e255f912, not from the report).

    • Shape: draft → main, first line Fixes #4931; no closing keyword against parent owner survives as a live field-type in ~13 more code faces after #4814's retirement — including a published Zod enum that still accepts it #4914 — correct.
    • Scope (from changed files, 5): FieldEditWidget.tsx, the new class-level pin, two fixture triages, and a real patch changeset for @object-ui/fields. Exactly the claimed surface; the grid-consumer measurement was taken via a throwaway probe precisely so plugin-grid stayed untouched.
    • Implementation vs the adjudicated points: all four delivered, plus the discovery that makes the fix correct rather than merely compliant — gate 2 (isInlineExcludedFieldType → true) is load-bearing: ObjectGrid forces editable: false off that predicate, and without it gate 1 alone routes the cell to DataTable's built-in plain-text input, which saves a bare string over a stored person reference — the dispatch's own forbidden outcome, proven empirically by Leg B (exactly the two exclusion pins redden under the gate-1-only ablation). The gate imports the live table direct from field-type-alias (barrel would cycle) and answers ahead of every lookup, so Leg A's deliberately inverted prediction — restore the EDIT_WIDGETS key, all pins stay green — is the class-closure property demonstrated, not an anomaly.
    • Cell-sized loudness accepted: console prescription + render-nothing, per owner widget 拿不到表单下发的 dataSource,而 plugin-grid 认为它需要 —— 且 owner 根本不在 spec 的 FieldType 里(enforce-or-remove 待判) #4814's own settled answer for cell hosts ("no visible alert a table CELL can carry without wrecking the row"); the grid never reaches it anyway (gates 1–2 route to the normal read-only path, same disposition as password/formula/composite).
    • Fixture triage discipline: both old fixtures pinned the exact removed branch — one trimmed with the replacement fact named, one removed with its value-shape coverage shown to survive in the sibling user case. The corrected self-assertion (hasFieldEditWidget('not-a-real-type') === true via the text tail) is now the file's sharpest contrast: the retired spelling is the one type the text tail does not catch.
    • Evidence: fields 103/1705 green; the three real consumers (plugin-grid/kanban/detail) 167/1514 green including fix(types,plugin-detail): stop publishing the retired owner field type (#4914) #4932's deliberately-unpinned coupling test; type-checks green after closure builds; gates from changed paths all OK.

    Ready-flip + auto-merge after the six gate jobs report success at this head. On MERGE this closes the last measured active-contradiction face of the A′ retirement; what remains of the owner line is #4914's ordinary-cleanup residue.


    Generated by Claude Code

  5. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor

    Patch round — PR #4942, same claim, same branch. New head cb536762c (cb536762c3ad25c498e74b1fb91943031c1be4f5), was e255f9123.

    Cause confirmed as reported, and it is the happy kind. PR #4932 merged at 06:34Z, after my branch point, and shrank owner out of the published field-type unions in @object-ui/types. My four field={{ …, type: RETIRED }} literals stopped compiling. My local type-check was green only because the base predated that merge — the readings in my first report were honest for the tree they ran on, and stale the moment #4932 landed.

    Fix. Merged origin/main (8a79f2e91) into the branch — clean, no content conflicts, disjoint files as predicted. Rather than scatter four casts, the escape is confined to a single helper so strictness still holds everywhere else in the file:

    const retiredFieldLiteral = (name: string) => ({ name, type: RETIRED as never });

    Its docblock states that the refusal is the point, not an obstacle: the compiler now refuses this payload because the contract twin works as designed, that refusal is asserted head-on by packages/types/src/__tests__/owner-retired-contract-twins.test.ts, and this suite must still author the refused spelling because what it pins is the runtime disposition for a field already stored that way — the case no compile-time union can reach, and the whole reason this seam mattered.

    The escape is load-bearing, verified rather than assumed. Removing as never reproduces the failure at all four call sites, so my local env now reproduces CI:

    src/__tests__/FieldEditWidget.retiredFieldType.test.tsx(163,9): error TS2322: Type '{ name: string; type: string; }' is not assignable to type 'FieldMetadata'.
      … same at (187,9), (198,9), (219,11)
    

    Readings at cb536762c (heavy runs under flock -E 99 -w 540, closures built before every type-check verdict):

    • @object-ui/fields type-check → Done, both tsconfigs echoed (tsc --noEmit && tsc -p tsconfig.test.json) — the previously red gate.
    • type-check also green for @object-ui/types, plugin-grid, plugin-kanban, plugin-detail.
    • FieldEditWidget.retiredFieldType.test.tsx → Test Files 1 passed (1) / Tests 12 passed (12).
    • packages/fields/ → Test Files 104 passed (104) / Tests 1720 passed (1720) (was 103/1705; the merge added the slider-signature suite).
    • packages/plugin-grid/ packages/plugin-kanban/ packages/plugin-detail/ → Test Files 168 passed (168) / Tests 1524 passed (1524). This now includes fix(types,plugin-detail): stop publishing the retired owner field type (#4914) #4932's InlineFieldInput.retiredFieldType.test.tsx from main — green, so the declared coupling is confirmed against the real merged file rather than by reasoning.
    • Gates: check:control-bytes OK (4429 files), check:self-import OK, check:phantom-deps OK, changeset:check OK, check:doc-types OK; manual control-byte scan of the five changed files clean.

    FieldEditWidget.test.ts and complex-widgets.test.tsx needed no escape, as expected — they only remove owner; confirmed by the package type-checking clean.

    Delta is tests-only, so no changeset impact; the existing @object-ui/fields patch changeset stands. Diff vs main is still the same five files, and the implementation in FieldEditWidget.tsx is byte-unchanged from the reviewed head — every conclusion in the original report and PR body still holds.

    One process note worth recording, since it cost a cycle and is the exact hazard AGENTS.md names. After running the escape ablation I restored with git checkout -- on a file whose fix was not yet committed, which silently discarded the fix — exit 0, no warning. The suites I then ran still passed, because TypeScript types do not affect vitest execution, so the green was real but meaningless. It surfaced only when git commit reported "nothing to commit". Re-applied the edit, re-verified type-check, and committed before any further verification. No work was lost and nothing incorrect was pushed, but the lesson is the one already written down: commit the fix before reverse-verifying it, and never treat a runtime-suite pass as evidence about a type-level change.


    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions