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.

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

Closed
1 of 1 issue completed

Description

@os-steve

Measured while implementing #4814 (ruling A′ — retirement of the owner widget alias). Filed unassigned, out of that card's scope; recording it rather than widening the ruled batch into 8 packages.

What the A′ ruling scoped vs. what is actually there

The A′ ruling (comment 5310924344) scoped "all three code consumers" plus the fourth face app-shell/src/utils/paramToField.ts, working from the fork report's map (comment 5308145625). That map was built by sweeping for field:owner and widget-registration usages. Re-sweeping on the implementation branch for owner in the field-type namespace (i.e. type === 'owner' predicate branches, not the field name owner) finds substantially more.

PR for #4814 retired these five: field-type-alias.ts, index.tsx (widget map + cell renderer), plugin-grid/bulkParamToField.ts, app-shell/paramToField.ts, and app-shell/paramValueShape.ts (that last one surfaced only because its own drift guard turned red).

All of the following were present at the fork report's own base commit 1ef236e18 — verified with git show 1ef236e18:path. They are pre-existing faces the sweep did not reach, not consumers that appeared since.

Residual faces

Published contract surfaces (these are the ones that matter):

  1. packages/types/src/zod/reports.zod.ts:49 — a runtime Zod enum for ReportFieldSchema.type that still lists 'owner' as legal. After owner widget 拿不到表单下发的 dataSource,而 plugin-grid 认为它需要 —— 且 owner 根本不在 spec 的 FieldType 里(enforce-or-remove 待判) #4814 this is a published validator blessing a spelling the renderer now refuses.
  2. packages/types/src/reports.ts:105 — the TS twin of the above.
  3. packages/types/src/field-types.ts:584 — UserFieldMetadata.type: 'user' | 'owner', i.e. published .d.ts autocomplete still offers owner.

Note #4814 shrank the three doc unions (content/docs/fields/user.mdx, filter-builder.mdx:27, report-schema.mdx:99) as ruled. Items 1-3 are the code twins of those exact docs, so the repo currently has docs and code disagreeing in the opposite direction from before.

Predicate branches that treat owner as a lookup-like/user-like type (dead rather than broken after the retirement, but each is a drift channel of the kind the ruling exists to close):

  1. packages/components/src/custom/filter-builder.tsx:707,982,1442
  2. packages/plugin-detail/src/InlineFieldInput.tsx:107,290
  3. packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:433
  4. packages/plugin-view/src/config/view-config-utils.ts:133
  5. packages/plugin-dashboard/src/ObjectDataTable.tsx:102
  6. packages/plugin-dashboard/src/recordFields.tsx:34
  7. packages/plugin-list/src/UserFilters.tsx:49,505
  8. packages/fields/src/FieldEditWidget.tsx:170

Test fixtures still spelling it:

  1. packages/plugin-list/src/__tests__/list-offered-operator-expressible-parity.test.ts:100
  2. packages/plugin-detail/src/__tests__/inlineEditTypeCoverage.test.tsx:234
  3. packages/components/src/__tests__/filter-builder-field-switch-value.test.tsx:451
  4. packages/fields/src/FieldEditWidget.test.ts:74

None of 4-15 turn red from #4814 — they were all green on the implementation branch, which is precisely why they need filing rather than discovering later.

Why this was not folded into #4814

Three reasons, offered for triage rather than as a settled call:

  • it spans 8 packages well outside that card's claimed file surface, with in-flight-agent collision risk that was never checked for those files;
  • the ruled batch is coherent and strictly-better on its own: the retired spelling now fails loudly, so the residual faces are surfaced by the tombstone rather than hidden by it;
  • items 1-3 in particular are a contract decision (does @object-ui/types mirror @objectstack/spec's closed FieldType?), which overlaps Unify hand-written @object-ui/types zod with @objectstack/spec/ui (ListViewSchema drift) #2231 and is not an implementer's call.

Suggested disposition

Items 1-3 look worth doing promptly and together — a published runtime validator that accepts a spelling the renderer refuses is exactly the declared-but-unenforced shape ADR-0049 targets. Items 4-15 are mechanical dead-branch cleanup and could ride one follow-up PR.

Related: #4814 (this measurement's origin), #4855 (the coverage-guard gap — its premise should be re-read against #4814's landed state, since that PR removed owner from CELL_RENDERER_TYPES while the form half still omits user), #2231 (types/spec zod unification).


Generated by Claude Code

Activity

  1. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor

    PM disposition (session session_01DbRmJD3iPhXjKr6vhd4Qkv, 2026-08-17), under the #4814 ACCEPT. Split by the same test the A′ ruling used — active contradiction vs dead code:

    Serial note for whoever dispatches: land AFTER PR #4915 (the tombstone is the behavioral half this shrink cites), and packages/types is currently untouched by any in-flight claim (checked this round).


    Generated by Claude Code

  2. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor

    Disposition amendment (PM session session_01DbRmJD3iPhXjKr6vhd4Qkv, 2026-08-17) — superseding one line of my split above, on the patch-round measurement recorded in the dev's addendum (comment 5312190915) and the #4814 final report.

    Item 5 (InlineFieldInput) moves from "dead branch, ordinary cleanup" into the dispatchable fast-follow scope. The patch round disproved the inert framing: INLINE_ROUTED_FIELD_TYPES (InlineFieldInput.tsx:107) and the isUserRef branch (:290) are consulted at runtime against a STORED field's actual type — so a field still typed owner gets a working person picker in inline edit while the record form renders the tombstone refusal. Two edit surfaces for one field now disagree, which is worse than either uniform outcome and exactly the drift A′ was chartered to end.

    Fast-follow scope is therefore items 1–3 + 5 (the published contract twins in @object-ui/types + both InlineFieldInput spots); items 4 and 6–15 stay recorded here as ordinary cleanup. The InlineFieldInput half should dispose consistently with the tombstone (a stored owner field in inline edit should meet the same loud refusal — or at minimum the same console prescription — not a silently working picker the form contradicts). Serial: after PR #4915 merges; packages/plugin-detail(InlineFieldInput's home per the measurement) and packages/types are untouched by in-flight claims as of this round.


    Generated by Claude Code

  3. self-assigned this
    on Aug 17, 2026
  4. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor

    Claim: PM loop round 4
    Session: session_01DbRmJD3iPhXjKr6vhd4Qkv
    Branch: claude/issue-4914-owner-contract-twins
    Worktree: objectui-issue-4914
    Domain: repo:objectui
    File surface (the dispatched fast-follow scope ONLY — items 1–3 + 5 per the PM disposition comments above): packages/types/src/zod/reports.zod.ts + packages/types/src/reports.ts + packages/types/src/field-types.ts + packages/plugin-detail/src/**/InlineFieldInput.tsx (the INLINE_ROUTED_FIELD_TYPES set and the isUserRef branch) + tests. Items 4 and 6–15 stay on this card, ⛔ not in this dispatch (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: PR #4915 (the tombstone this shrink cites) MERGED 05:38Z — branch from post-merge main. In-flight checked this round: #4636 / PR #4639 (package.json ranges + lockfile, parked awaiting #4911) — disjoint; no open dev PR touches packages/types or plugin-detail's InlineFieldInput. Overlap check owed at implementation time: #2231 / #2890 (ListView legacy-vocabulary phases) may own edits in packages/types — dev reads those cards first and reports rather than collides if the same lines are claimed there.


    Generated by Claude Code

  5. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor
    {
      "issue": 4914,
      "status": "done",
      "branch": "claude/issue-4914-owner-contract-twins",
      "pr": "https://github.com/objectstack-ai/objectui/pull/4932",
      "premise_still_valid": true,
      "summary": "Implemented the dispatched fast-follow scope in full: items 1-3 (the three published contract twins in `@object-ui/types` — the runtime `ReportFieldSchema.type` Zod enum plus its TS twins `ReportField['type']` and `UserFieldMetadata['type']`) and item 5 (`InlineFieldInput`). All four measured faces were re-verified present on post-#4915 main before any edit; none had moved or disappeared. `InlineFieldInput` now answers a retired spelling with the SAME `RetiredFieldTombstone` the record form gives, read live from `@object-ui/fields`' `RETIRED_FIELD_TYPES` rather than copied, with `owner` removed from `INLINE_ROUTED_FIELD_TYPES` and from the `isUserRef` branch in lockstep. One finding materially changed the shape of the fix and should be read before review: the dispatch's premise that deleting the two members would leave a 'silent removal / quiet fallback' is WRONG in a way that matters — deleting them alone changes NOTHING an author could see. `hasFieldEditWidget('owner')` is still `true` because `packages/fields/src/FieldEditWidget.tsx:85` maps `owner: UserField` in `EDIT_WIDGETS`, and `resolveInlineEditType` returns a type unchanged when it is already a key, so the alias table is never consulted. With the routing member gone the type reaches the SAME person picker down the delegation road. Measured directly under ablation: `tombstone: false`, `plainTextInput: false`, one `role=button` reading 'Select…'. So the refusal branch is the only behavior-changing half; the two deletions are lockstep hygiene. `EDIT_WIDGETS:85` is NOT among the faces this card enumerates (item 11 names `FieldEditWidget.tsx:170`, the cosmetic `COMPACT_EDIT_TYPES` set) — filed as #4931, a sub-issue of this card, and left untouched per the file surface. #2231 / #2890 overlap check owed by the claim: NO collision — both scope `zod/objectql.zod.ts` (ListView vocabulary) and `zod/views.zod.ts` (DetailView), neither names the three report/field-type files. File surface stayed exactly within the claim; items 4 and 6-15 untouched.",
      "tests": "Union run at HEAD=ddbf4d92e (= final commit = pushed remote head; PR head sha confirmed ddbf4d92e). Repo-root path-filter vitest; heavy runs under `flock -E 99 -w 540 /tmp/os-heavy-verify.lock`; dependency closures built BEFORE any type-check verdict (`pnpm --filter '@object-ui/types^...' --filter '@object-ui/plugin-detail^...' build`). RESULTS: `packages/types/` + `packages/plugin-detail/` -> 'Test Files 119 passed (119) / Tests 1230 passed (1230)'. type-check (hyphenated script name, echoed in output as `tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json`, so no zero-match trap) for @object-ui/types and @object-ui/plugin-detail -> both 'Done'. DOWNSTREAM SWEEP, direction stated: this is a contract NARROWING, so the DEPENDENTS direction is the one owed — `pnpm --filter '...@object-ui/types' build` then `type-check` (prefix ellipsis = downstream consumers) -> ALL GREEN, covering plugin-report (the real `ReportField` consumer), plugin-view, plugin-list, plugin-designer, plugin-calendar, plugin-gantt, plugin-kanban, app-shell, runner, apps/console, apps/site, examples/schema-catalog, examples/console-starter, examples/byo-backend-console. Repo-wide sweep for report fixtures spelling `type: 'owner'`: ZERO outside the fields package's own tombstone tests, so no fixture triage was owed. Gates derived from the actual changed paths: check:control-bytes OK (4413 tracked text files), check:doc-types OK (143 mdx / 632 code blocks / 558 type literals), check:self-import OK, check:phantom-deps OK, check:published-dist OK (39 published packages), changeset:check OK. REVERSE VERIFICATION, both legs run from the COMMITTED state, direction predicted first. Leg A (restore 'owner' to all three type faces) — predicted the Zod pin red on the refusal test and the two TS pins red at TYPE-CHECK not vitest, in the INVERTED shape (the expected error disappears rather than an illegal value being accepted). Observed exactly that: vitest 'Tests 1 failed | 4 passed (5)' with `expect(result.success).toBe(false)` receiving true, and `tsc` -> 'owner-retired-contract-twins.test.ts(104,7): error TS2578: Unused @ts-expect-error directive.' plus the same at (119,7) — which also proves `tsconfig.test.json` really enforces the type-level pins. Leg B (drop the refusal branch, keep both deletions) — predicted the retired field reaches a WORKING PERSON PICKER via delegation, so tombstone assertions redden while the plain-text-input assertion stays green. Observed: 'Tests 4 failed | 6 passed (10)', failing on `expected [ one button element ] to have a length of +0 but got 1`, and a direct probe returned `{ tombstone: false, plainTextInput: false, buttons: 1, firstButtonText: 'Select…', anyInput: false }`. Both legs restored byte-identically via `git checkout ddbf4d92e -- ` + the edited paths (`git diff HEAD` empty afterwards; final tree confirmed 0 files differing from the commit). No dist-ablation concern: these suites import from source, not dist. Two of my own pins failed on first run and were corrected to measured reality rather than forced: the zod v4 `invalid_value` issue carries the ALLOWED `values` and no echo of the input, so the envelope assertion was rewritten to pin code + path + the offered vocabulary; and the `isKnownFieldType(retired) === false` assertion was simply false — that discovery is what surfaced #4931.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #4931 (sub-issue of #4914): `packages/fields/src/FieldEditWidget.tsx:85` maps `owner: UserField` in `EDIT_WIDGETS`, so `hasFieldEditWidget('owner')` is true and every host calling FieldEditWidget directly — the grid's inline cell editor being the user-facing one — still renders a working person picker for the retired spelling while the record form refuses it. This is NOT one of the 15 faces #4914 enumerates (item 11 names the same file's line 170, the cosmetic COMPACT_EDIT_TYPES set); line 85 is the routing table itself. Suggested fix noted on the card: have `hasFieldEditWidget` answer false for any key of RETIRED_FIELD_TYPES so the whole class closes at once. `InlineFieldInput` is unaffected once this PR lands (its refusal runs before both roads), and the new pin deliberately does not assert today's `hasFieldEditWidget('owner')` value in either direction so it will not go red on that fix."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  6. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor

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

    • Shape: draft → main, first line Part of #4914 — correct: items 4 and 6–15 stay open on this card; the merge must not close it.
    • Scope (from changed files, 8): exactly the dispatched fast-follow surface — the three @object-ui/types twins (Zod enum + two published unions, each with a lockstep retirement comment), InlineFieldInput (refusal branch + both member deletions), three test files, and a real 2-package patch changeset. The Unify hand-written @object-ui/types zod with @objectstack/spec/ui (ListViewSchema drift) #2231/Migrate the remaining ListView legacy vocabulary to spec-canonical keys, and audit ObjectView/DetailView (#2231 phases 4–5) #2890 overlap check came back clean with the evidence stated (they scope objectql.zod.ts/views.zod.ts, not the report/field-type files).
    • Dispatch premise correction accepted — and it improves the fix: "deleting the members would be a silent removal" was wrong; measured, the type reaches the SAME picker via the hasFieldEditWidget delegation road (EDIT_WIDGETS still maps owner: UserField). The refusal branch is therefore the load-bearing half, the deletions are lockstep hygiene, and both source comments say so. The newly exposed 16th face (FieldEditWidget.tsx:85, the grid inline cell editor's road) is correctly filed as sub-issue EDIT_WIDGETS still maps owner to UserField, so the grid's inline cell editor renders a working person picker for the retired spelling #4931 and correctly left out of this diff.
    • Test discipline: Zod pinned by refusal ENVELOPE (one type-path invalid_value issue; offered vocabulary lacks owner, keeps user; sibling-vocabulary over-reach guard) — with the zod-4 message-shape correction measured rather than forced; TS twins pinned by @ts-expect-error with the ablation's TS2578 proving tsconfig.test.json enforces them; InlineFieldInput pins BOTH wrong outcomes (working picker AND quiet text box), once-per-spelling dedupe, expanded-value case, and a structural pin quantified over the whole RETIRED_FIELD_TYPES table. The deliberate non-assertion on hasFieldEditWidget is the right call — a pin on today's value would redden on EDIT_WIDGETS still maps owner to UserField, so the grid's inline cell editor renders a working person picker for the retired spelling #4931's own fix.
    • Evidence: types+plugin-detail 119 files / 1230 tests green; full DEPENDENTS-direction sweep green (the direction a contract narrowing owes); zero fixtures authoring type: 'owner' outside the tombstone tests; both reverse-verification legs with directions (including the inverted TS shape) predicted first, restored byte-identically.

    Ready-flip + auto-merge after the six gate jobs report success at this head. This card stays OPEN — remaining on it: #4931 (the delegation-road face, graded next), and items 4 / 6–15 as ordinary cleanup.


    Generated by Claude Code

  7. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor

    Part-of closeout (PM session session_01DbRmJD3iPhXjKr6vhd4Qkv, 2026-08-17 06:47Z). PR #4932 MERGED at 06:34Z — this card stays open by design. Ledger:

    Label: pm:dispatched → pm:queue for the residue; assignee cleared (the fast-follow claim is fulfilled; the residue is unclaimed).


    Generated by Claude Code

  8. 9 remaining items

  9. removed their assignment
    on Aug 18, 2026
  10. yinlianghui commented on Aug 18, 2026

    @yinlianghui
    Contributor

    PM 处置:残余批转决策箱(session_01GTRjn8xBqp75dk7kFupVRt,objectui 分片;标签 pm:queue → needs-user-decision,assignee 清除 —— 认领以前提门报告履行,零代码零 PR)

    采信席位复测报告(评论 5324769751):「items 4/6–10 是死支、删除零行为变化」的卡面判断被推翻,不派清扫。要点:

    1. 归谬成立:六个面是对开放后端词汇表的防御性归一化 —— reference/picklist/money 等同样不在 spec 闭合 FieldType 里却从未退役,按卡面论证它们也「死」了,显然为假。实测 owner 在这些谓词上与 never-retired 别名回答完全一致(operators 走 user 桶、$expand 主动请求、isLookupType true)。
    2. 删除 = 静默降级:六面无一读 RETIRED_FIELD_TYPES,机械删除给存量 owner 字段的既不是 tombstone 也不是 lookup 路,而是 filter picker → 裸 id 框、$expand 掉 → 原始 FK id —— 逐字就是退役表 docblock 说它要防的失效形。反向验证:删 item 4 即有正钉红(filter-builder-lookup-empty-options.test.tsx:95,231 —— 卡面未列的第 16 面)。
    3. 账本更正:items 11+15 已随 PR fix(fields): close the FieldEditWidget delegation road for retired field types #4942(EDIT_WIDGETS still maps owner to UserField, so the grid's inline cell editor renders a working person picker for the retired spelling #4931)交付、item 13 已随 PR fix(types,plugin-detail): stop publishing the retired owner field type (#4914) #4932 —— 派发的 11 面中 3 面早已完成;items 12/14 是真零变化 fixture 但不出部分 PR(生产谓词仍特判 owner 时删断言 = 净减覆盖),随最终裁定走。

    待裁(A/B/C,席位推荐 B,PM 附议):

    • A 机械删除:把可见矛盾换成不可见降级,两轴皆低 —— 不推荐。
    • B 六面前置共享 isRetiredFieldType 门(@object-ui/fields 出口,fire reportRetiredFieldType 一次取既定处置):declared=enforced 覆盖全部消费路径,同形已被 fix(types,plugin-detail): stop publishing the retired owner field type (#4914) #4932/fix(fields): close the FieldEditWidget delegation road for retired field types #4942 两次采纳,5 包中等成本。
    • C 判 not-a-defect:这些层是外来后端词汇的兼容归一化,A′ 退役的是 objectui 自己的 SDUI 创作词汇,无权管辖后端叫什么 —— 内部自洽但与 A′ 存在张力。
    • 单一分界问题(决定 B vs C,维护者的裁定):后端词汇归一化层里出现 owner,算 authoring error(→B 该拒)还是合法外来输入(→C 该容)?若裁 C,items 4/6–10 就地关闭;若裁 B,一张 5 包实施卡随裁定入队。

    Generated by Claude Code

  11. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Maintainer ruling (2026-08-18, live PM chat with the triage seat, session session_01EUZUjvnLsqwHVkXkTv8TKQ) — recorded verbatim: 「4914 B」.

    Ruled: option B — the shared retirement gate. Export a single isRetiredFieldType(t) gate from @object-ui/fields and place it ahead of each of the six live predicate faces (items 4, 6, 7, 8, 9, 10): a retired spelling fires reportRetiredFieldType once and takes the established disposition, instead of riding the lookup-family paths or silently degrading. This closes the whole class at once (the #4932/#4942 shape), and answers the boundary question on record: owner arriving through a backend-vocabulary normalizer is an authoring error to refuse loudly, not legitimate foreign input to tolerate.

    Scope notes from the measurement (comment 5324769751, adopted):

    Returned to pm:queue for dispatch as the implementation card (the measurement stays on-card as its spec).


    Generated by Claude Code

  12. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  13. self-assigned this
    on Aug 22, 2026
  14. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    Claim — domain:ui execution seat, round 15.

    • Session: session_012u2pRjcqAYtoEjgr3wwhnK
    • Branch: claude/issue-4914-retired-field-type-gate

    Dispatching the implementation card under the maintainer's ruling of 2026-08-18, recorded verbatim as 「4914 B」: export a single isRetiredFieldType(t) gate from @object-ui/fields and place it ahead of each of the six live predicate faces (items 4, 6, 7, 8, 9, 10), so a retired spelling fires reportRetiredFieldType once and takes the established disposition rather than riding the lookup-family paths or degrading silently.

    The boundary question is answered on record and the brief quotes it: owner arriving through a backend-vocabulary normalizer is an authoring error to refuse loudly, not legitimate foreign input to tolerate.

    Scope, from the adopted measurement (comment 5324769751):

    The prior seat's premise-gate report is the spec for this card, and it stands: these six faces are live, not dead, and mechanical deletion would trade a visible contradiction for a silent degradation. That is the finding the ruling is answering.


    Generated by Claude Code

  15. claude commented on Aug 22, 2026

    @claude
    Contributor
    {
      "issue": 4914,
      "status": "done",
      "branch": "claude/issue-4914-retired-field-type-gate",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5693",
      "premise_still_valid": true,
      "summary": "Implemented the ruled scope end-to-end: a single `isRetiredFieldType(t)` gate exported from `@object-ui/fields`, placed ahead of all six live predicate faces (items 4, 6, 7, 8, 9, 10), plus the three fixture inversions (items 12, 14 and the 16th face) in the SAME PR. The premise-gate report (comment 5324769751) is this card's spec and it HELD on re-verification: every one of the six faces was present and live on origin/main 3b147a367, and each answered a retired spelling exactly as that report measured. Line drift found and re-verified: item 4 is at 925/1220/1730 (card said 707/982/1442), item 7 at 141 (card: 133), item 8 at 183 (card: 102); items 6/9/10/12/14 and the 16th face are unmoved at 433 / 34 / 49+505 / 100 / 451 / 95+231. ONE THING THE RULING DID NOT ANTICIPATE, and the only place this PR departs from its literal wording: the ruling homes the gate in `@object-ui/fields`, but `@object-ui/components` is one of the six consumers and `@object-ui/fields` DEPENDS on `@object-ui/components` (54 source files import from it), so a shared table could not live in `fields` — it is a package cycle. A second copy inside `components` was the obvious alternative and is exactly wrong: two tables means two `reportedRetiredTypes` sets, so one spelling logs TWICE and the ruling's own 'fires once' clause would have been false on day one. So RETIRED_FIELD_TYPES, its reporter, the reset seam and the new gate are homed in `@object-ui/core` (which imports only `@object-ui/types`, and already owns the sibling cross-package field-type vocabulary EXPANDABLE_FIELD_TYPES) and RE-EXPORTED from `@object-ui/fields`. The ruling's stated requirement therefore holds literally — `import { isRetiredFieldType } from '@object-ui/fields'` resolves, and that is pinned as a test and proved through the TypeScript checker against the built .d.ts. No package gained a new dependency; all seven already carried `@object-ui/core`. Flagged as an open question below so the reviewer confirms the re-homing rather than discovering it. Disposition per face: the retired spelling now gets the answer an UNRECOGNISED spelling gets, plus one console line — not an empty operator bucket (that draws a blank trigger, objectui#4768's defect, and would strand a stored filter row) and not a disappearing chip. `retypeFilterValue` is deliberately NOT gated: a refusal that cleared the row would destroy a stored value on the way to showing the author the prescription. The `owner` members were also deleted from the six lists as lockstep hygiene — measured to be behaviourally inert once the gate is ahead (leg 1a/1b show the gate is the load-bearing half), and the PR says so rather than implying the deletions did the work. Item 6 needed the gate on TWO doors, not the one the card names: `deriveHighlightFields` also selects by NAME through a `preferred` list whose FIRST entry is `owner`, so gating only the type-driven loop would have left the exact shape this card is about — a field named `owner` still typed `owner` — reaching the strip through the other door with the fix reading as complete. The surviving idiom `{ type: 'user', name: 'owner' }` keeps its slot, pinned. Also deleted the local `isRetiredFieldType` that #4942 added to FieldEditWidget.tsx: it is now the same function object, which is what makes 'a single gate' true rather than aspirational. Items 11/15/13 untouched as ruled — item 13's remaining `owner` mentions are the explanatory comments recording why the spelling left ALL_TYPES, left as archive. Fenced-out paths were not touched and no collision was found: this diff shares no file with #4891, #4807 (the row-predicate `record.*`/`data.*` axis is disjoint from these field-type predicates — verified, no shared lines), #4834 or #4773; `content/docs/**` and `apps/site/**` are entirely untouched. FILE SURFACE (16 files, 7 packages): core/src/utils/retired-field-types.ts (new), core/src/index.ts, core/src/utils/__tests__/retired-field-types.test.ts (new), fields/src/field-type-alias.ts, fields/src/index.tsx, fields/src/FieldEditWidget.tsx, components/src/custom/filter-builder.tsx, components/src/__tests__/filter-builder-retired-field-type.test.tsx (new), components/src/__tests__/filter-builder-lookup-empty-options.test.tsx, components/src/__tests__/filter-builder-field-switch-value.test.tsx, plugin-detail/src/synth/buildDefaultPageSchema.ts + its new test, plugin-view/src/config/view-config-utils.ts + its new test, plugin-dashboard/src/ObjectDataTable.tsx, plugin-dashboard/src/recordFields.tsx + one new test for both, plugin-list/src/UserFilters.tsx + its new test, plugin-list/src/__tests__/list-offered-operator-expressible-parity.test.ts, and .changeset/retired-field-type-gate-4914.md (7-package patch). BOOKKEEPING NOTE for the PM: by my audit every enumerated item on this card is now addressed (1-3+5+13 by #4932, 11+15 by #4942, 4/6-10/12/14 plus the 16th face here). I still opened with `Part of #4914` rather than a closing keyword — this card carries a multi-round PM ledger and the maintainer ruling being implemented, so retiring it is a deliberate reviewer call, not a merge side effect.",
      "tests": "ALL readings at HEAD=427cf160f (= the final commit = the pushed remote head = PR head sha, confirmed by reading the PR back from the API). Heavy legs ran through `/home/user/objectstack/scripts/pm/os-verify-lock.sh`; verdicts read from its own VERDICT lines, exit codes captured before any pipe (`cmd > log 2>&1; echo EXIT=$?`), never from a bare `$?` after a `tail`. TARGETED SUITE (canonical repo-root invocation, `pnpm exec vitest run --reporter=verbose --maxWorkers=2 <paths>`, no `--`): 'Test Files 13 passed (13) / Tests 201 passed (201)' — the six new face suites, the three inverted fixtures, and the three pre-existing retirement suites (owner-retired, FieldEditWidget.retiredFieldType, InlineFieldInput.retiredFieldType) that read the table this PR re-homes. REPO-WIDE BUILD: `turbo run build --filter='!@object-ui/site' --concurrency=2` -> 'Tasks: 43 successful, 43 total' (BUILD_EXIT=0, 332s). REPO-WIDE TYPE-CHECK: `turbo run type-check --concurrency=2` -> 'Tasks: 81 successful, 81 total' (TYPECHECK_EXIT=0, 334s). REPO-WIDE LINT: `turbo run lint --concurrency=2` -> 'Tasks: 47 successful, 47 total' (LINT_EXIT=0, 209s) — the full farm, so no narrowing is claimed and none is owed. DEPENDENTS DIRECTION, stated: this is a contract change, so the DEPENDENTS direction is what is owed. I did not hand-pick a filter list; I ran a provable superset with the principle stated — the whole repo minus the docs site contains, by construction, both the dependency closure and the dependent set of every changed package. (A first attempt at a narrower superset, `--filter '...@object-ui/core' --filter '@object-ui/core^...'`, FAILED at BUILD_EXIT=2 and the failure is instructive: it misses `@object-ui/react-runtime` and `@object-ui/providers`, which are neither dependents nor dependencies of core. That is exactly the kind of hole a hand-derived filter leaves, so I widened rather than patched it.) GATES, quoting each gate's own printed verdict line, not `$?`: check:phantom-deps EXIT=0 'Every in-scope import is declared by the package that publishes it.'; check:self-import EXIT=0 'No package names itself inside its own src/.'; check-control-bytes EXIT=0 'check-control-bytes: OK (scanned 4734 tracked text file(s); skipped 85 binary).'; check-lint-coverage EXIT=0 'lint coverage: 46/46 packages linted, 0 with outstanding errors'; check-type-check-coverage EXIT=0 'type-check coverage: 45/46 via type-check ... test type-check coverage: 41/41 packages compile their tests'; check-changeset-fixed EXIT=0 'All workspace packages are in the changeset fixed group.'; check-changeset-no-major EXIT=0; check-spec-symbols EXIT=0; check:esm-specifiers / check:i18n-keys / check:i18n-drift / check:i18n-dead-keys all EXIT=0. Skipped the three known-broken gates named in the dispatch (check:eager-closure, check:doc-snippets, check:published-dist) and check:doc-types (no docs path touched). PUBLISHED SURFACE, MEASURED: `dist/` AND every `tsconfig*.tsbuildinfo` deleted in BOTH trees before rebuilding (script prints 'tsbuildinfo left after clean: 0' for each — a surviving buildinfo makes composite tsc skip emit, which would have made the 'after' manifest a copy of the 'before'), then a clean `--filter '@object-ui/core...' --filter '@object-ui/fields...' build` in a comparison worktree at origin/main 3b147a367 and in this branch. Whole-tree `.d.ts` sha256 manifests: 166 -> 167 files, and EXACTLY FOUR entries move — core/dist/index.d.ts, the new core/dist/utils/retired-field-types.d.ts, fields/dist/field-type-alias.d.ts, fields/dist/index.d.ts. Nothing else in either published tree changed. Content delta on @object-ui/fields is PURELY ADDITIVE: the three existing names survive as re-exports (`export { RETIRED_FIELD_TYPES, isRetiredFieldType, reportRetiredFieldType, resetRetiredFieldTypeReports, } from '@object-ui/core';`), `isRetiredFieldType` is added. REACHABILITY THROUGH THE CHECKER, not a barrel grep (`export *` and `export type { X } from '...'` both propagate a symbol without restating its shape): a throwaway consumer inside packages/plugin-dashboard (which depends on both packages) imported all four names from '@object-ui/fields', bound each to its expected type, and asserted `typeof gateFromCore` and `typeof gateFromFields` mutually assignable -> `tsc --noEmit` PROBE_TSC_EXIT=0 with no output. The probe carries a negative control (`@ts-expect-error` on a misspelled name) and was itself FALSIFIED to prove it can fail: making that name resolve produced 'probe-4914/probe.ts(21,1): error TS2578: Unused @ts-expect-error directive.' (NEGCONTROL_TSC_EXIT=2). Probe deleted; `git status --porcelain` empty afterwards. ABLATION — 8 legs, direction PREDICTED BEFORE each run, every leg's mutation CONFIRMED ON DISK by injected and removed occurrence COUNTS (python str.count, not `grep -c` which counts lines) plus a byte-length delta, restored under `trap restore EXIT INT TERM`, and verified byte-identical afterwards. NO REBUILD IS OWED and here is why, stated rather than assumed: the root vitest.config.mts aliases every `@object-ui/*` specifier to the sibling package's `src/` (lines 255-281), so these suites execute the SOURCE that was mutated — which the redness below demonstrates directly rather than by argument; nothing here resolves through `dist/`. RESULTS vs predictions, all matched: (1a) gate removed from operatorsForFieldType + `owner` restored to lookupLikeTypes -> predicted bucket and 'once' pins red, controls green; observed 'Tests 5 failed | 95 passed (100)' across 3 files, red on 'answers a retired spelling with the unknown-spelling bucket, not the lookup one', 'says why, exactly once', 'closes the CLASS', plus the item-14 and item-12 inversions. (1b) gate removed from renderValueInput + the `type === \"owner\"` conjunct restored -> predicted the picker returns; observed 'Tests 3 failed | 19 passed (22)', red on 'draws no picker for the retired column' and both 16th-face inversions. (2) gate removed from deriveHighlightFields -> predicted both doors red, live controls green; observed 'Tests 4 failed | 2 passed (6)', red on BOTH the `preferred` NAME pass and the type-driven pass. (3) gate removed from normalizeFieldType -> predicted red with 'select' received; observed 'Tests 4 failed | 2 passed (6)', the two open-vocabulary controls green. (4) gate removed from computeLookupExpand -> observed 'Tests 3 failed | 5 passed (8)', isLookupType block green. (5) gate removed from isLookupType -> observed 'Tests 2 failed | 6 passed (8)', computeLookupExpand block green — the two faces are separately attributable. (6) gate removed from UserFilters -> observed 'Tests 2 failed | 4 passed (6)', picker returns. (7) the DEDUPE itself removed (`if (!reportedRetiredTypes.has(fieldType))` -> `if (true)`) -> predicted every 'once' pin red everywhere; observed 'Tests 8 failed | 36 passed (44)' across all six files including the cross-package 'the toolbar face and the filter-builder face share the dedupe set'. Final restore: 'SHA256-MANIFEST-IDENTICAL: all 7 files byte-identical to the committed state' and `git status --porcelain` empty. ONE VOID LEG, REPORTED NOT SILENTLY RETRIED: leg 2's FIRST attempt was rejected by my own on-disk check, which demanded that the removed text's count DROP. That check is invalid for an ADDITIVE mutation — putting `'owner'` back into a list injects a string that CONTAINS the needle, so the count legitimately stays at 1 ('removed-text count 1->1'). The mutation HAD applied; the instrument was wrong, so leg 2 produced NO reading on that pass and I recorded it as unrun rather than reading numbers from it. The check now demands the drop only when the needle is not a substring of the replacement (and always demands a byte-length change); leg 2 was re-run under the corrected instrument and its numbers above are from that run, with the confirmation line now reading 'removed-text count 1->1 (needle-is-substring-of-replacement=True); bytes 41827->41836'. ONE TEST CORRECTED TO MEASURED REALITY rather than forced: the first spelling of the item-4 non-vacuity control looked for `lookup-picker-assignee` and failed — with no `dataSource` configured, LookupValuePicker renders its BY-HAND id input instead of the search trigger. The control was rewritten to assert the artifact that actually exists, and the comment records why.",
      "open_questions": [
        {
          "question": "The ruling says 'export a single `isRetiredFieldType(t)` gate from `@object-ui/fields`'. `@object-ui/components` is one of the six ruled consumers and `@object-ui/fields` depends on `@object-ui/components`, so the shared table cannot physically live in `fields`. I resolved it by homing the implementation in `@object-ui/core` and re-exporting the full set from `@object-ui/fields`. Confirm this is the intended reading of the ruling rather than a re-litigation of it.",
          "options": [
            "A - keep as implemented: implementation in `@object-ui/core`, re-exported from `@object-ui/fields`. The ruling's stated contract holds literally (the `@object-ui/fields` import resolves, pinned by a test and proved through the TS checker); one table, one dedupe set, one function object; no package gained a dependency; `.d.ts` delta on `@object-ui/fields` is purely additive.",
            "B - a second copy of the table inside `@object-ui/components`. Keeps the table's only home in `fields`, but means two `reportedRetiredTypes` sets, so one spelling logs TWICE — the ruling's own 'fires once' clause is false on day one. Measured, not hypothetical: ablation leg 7 shows exactly what a broken dedupe looks like across all six files.",
            "C - drop item 4 (the filter builder) from the gated set and leave `@object-ui/components` un-gated. Honours the letter of 'exported from `@object-ui/fields`' but abandons the face the premise-gate measurement proved most live — `operatorsForFieldType('owner')` matched the `user` bucket item for item, and the 16th face is a positive pin on that exact conjunct."
          ],
          "recommendation": "A. Real business need: the traffic being refused is real — a stored/backend column typed with the retired spelling, which is why #4915/#4932/#4942 exist at all; B and C serve no need either does not. Long-term soundness: A is the only option where 'retired' is declared in one place and enforced on every consumer path, and it removes a copy (FieldEditWidget's local gate) rather than adding one — B adds a copy the repo has already been burned by four times (#4770/#4790/#4815/#5312), and C leaves the retirement table's authority stopping at a package boundary so the next retired spelling redoes this whole sweep. Making AI-written metadata hard to get wrong: A gives the author a prescription at the moment they write the spelling, on every path; B gives them the same prescription twice, which trains people to ignore it; C leaves the filter builder silently tolerant, which is exactly where a generated error would hide. Startup scope discipline: A adds no vocabulary, no new dependency edge and no new package — it MOVES three existing exports one layer down and adds one function, and the measured `.d.ts` delta is four files. The only thing A costs is that the gate's implementation file is not physically in the package the ruling named, which the re-export makes unobservable to every consumer."
        }
      ],
      "out_of_scope_findings": [
        "filed as #5692: `packages/plugin-dashboard` holds two MORE private copies of the reference-bearing field family — `LOOKUP_TYPES` in recordFields.tsx and the inline `isLookup` inside `computeLookupExpand` in ObjectDataTable.tsx — neither deriving from nor pinned against `@object-ui/core`'s EXPANDABLE_FIELD_TYPES, which is documented as THE reference-bearing family and already re-pointed two other consumers at itself with identity pins. The sets are not in a subset relation in either direction: `tree` is only in core's, `reference` only in the dashboard's, so today a hierarchy column is $expand-ed by the form/grid road and not by the dashboard table. This also falsifies #5312's closing claim that `paramToField` was the LAST private copy. Not fixed here: converting them changes behaviour in both directions and needs its own call on which set is right; the gate this PR adds runs AHEAD of both predicates and is unaffected either way."
      ]
    }

    Generated by Claude Code

  16. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    Closeout — every enumerated item on this card is now addressed

    Closed by the domain:ui execution seat (session session_012u2pRjcqAYtoEjgr3wwhnK). PR #5693 opened with Part of #4914 rather than a closing keyword, deliberately — this card carries a multi-round PM ledger and the maintainer ruling being implemented, so retiring it is a reviewer decision, not a merge side effect. Taking that decision here.

    Ledger

    items delivered by
    1–3 (the published contract twins in @object-ui/types), 5 (InlineFieldInput), 13 #4932
    11, 15 (FieldEditWidget COMPACT_EDIT_TYPES + its test) #4942 (from sub-issue #4931)
    4, 6, 7, 8, 9, 10 (the six live predicate faces), 12, 14 (fixtures, inverted), and the 16th face the card never enumerated #5693

    Fifteen items plus the face that was found by measurement rather than by the original filing. Item 13's remaining owner mentions are the explanatory comments recording why the spelling left ALL_TYPES — deliberately left as archive, not an omission.

    What the last PR settled

    The maintainer's 2026-08-18 ruling — recorded verbatim as 「4914 B」 — asked for a single isRetiredFieldType(t) gate exported from @object-ui/fields, firing once, placed ahead of the six faces, with the boundary question answered on record: owner arriving through a backend-vocabulary normalizer is an authoring error to refuse loudly, not legitimate foreign input to tolerate.

    Implementing it surfaced something the ruling could not have anticipated: @object-ui/fields depends on @object-ui/components, and components is one of the six ruled consumers — so a shared table homed in fields is a package cycle. Verified independently against origin/main: packages/fields/package.json carries "@object-ui/components": "workspace:*", with 54 files under packages/fields/src/** importing from it, while packages/core depends only on @object-ui/types.

    Resolved by homing the table in @object-ui/core — which already owns the sibling cross-package vocabulary EXPANDABLE_FIELD_TYPES — and re-exporting the full set from @object-ui/fields. All three of the ruling's clauses hold literally: one function object (the local gate #4942 had added to FieldEditWidget.tsx was deleted — measured: one occurrence on main, zero on the branch), the @object-ui/fields import resolves, and there is one reportedRetiredTypes set so a spelling reports once.

    The alternative — a second copy inside components — would have made the ruling's own "fires once" clause false on day one, and that was measured rather than argued: ablation leg 7 removes the dedupe and reddens 8 assertions across all six files.

    One thing the card itself did not contain

    Item 6's deriveHighlightFields selects fields through two doors, and the card names one. The second selects by name, through a preferred list whose first entry is owner — so gating only the type-driven loop would have left the exact shape this card is about, a field named owner still typed owner, reaching the strip through the other door with the fix reading as complete. Both doors are gated and both go red under ablation; the legitimate idiom { type: 'user', name: 'owner' } keeps its slot, pinned.

    Carried forward

    #5692 — packages/plugin-dashboard holds two further private copies of the reference-bearing field family (LOOKUP_TYPES in recordFields.tsx, the inline isLookup in ObjectDataTable.tsx), neither derived from nor pinned against @object-ui/core's EXPANDABLE_FIELD_TYPES. The sets are not in a subset relation in either direction — tree only in core's, reference only in the dashboard's — so today a hierarchy column is $expand-ed by the form/grid road and not by the dashboard table. It also falsifies #5312's closing claim that paramToField was the last private copy. Not fixed with the gate: converting them changes behaviour in both directions and needs its own call on which set is right, and the gate runs ahead of both predicates regardless.

    Closing as completed.


    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

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions