Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-kanban,plugin-detail,app-shell): converge four private copies of the reference-bearing field family, and correct the "fourth and last" claim - #6064
Merged
yinlianghui merged 2 commits intoAug 24, 2026
Conversation
…ies of the reference-bearing field family Four faces restated `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES` inline, none deriving from it and none pinned against it, and every one diverged from it in BOTH directions: all four lacked `user` and `tree`, and all four carried a fifth spelling `reference` that no producer can emit. Each now reads the published family and carries an identity pin (a spy on its `has`) so a member-identical private copy fails rather than quietly re-forking the table. `resolveActionParams` also lacked `master_detail`, so a field-backed `master_detail` action param inherited no picker config and degraded to the "paste a record id" text input #3405 exists to prevent. It keeps answering for `reference` through the one alias table in `paramToField`, which the dialog still accepts from params already authored with it — the same expression `paramToField` evaluates one step later, so the half that populates a param's picker config and the half that forwards it can no longer disagree. Also corrects the two now-false "fourth and last private copy" claims in `paramToField.ts` / `paramToField.test.ts`, pointing at the census in the family's canonical home rather than restating a count locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
Contributor
✅ 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
|
…nsion `check:esm-specifiers` (inside the Type Check CI job) caught the relative specifier this PR added: Node's ESM resolver does not extension-search relative specifiers and app-shell's build emits them unchanged, so the published entry could not be imported under plain Node. Part of #5874 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
Contributor
✅ 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
|
yinlianghui
marked this pull request as ready for review
August 24, 2026 15:58
Uh oh!
There was an error while loading. Please reload this page.
yinlianghui
deleted the
claude/issue-5874-expandable-field-family-convergence
branch
August 24, 2026 16:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#5874
Fixes#5875
Folded per the PM ruling on #5874: #5875's false claim is about the very copies#5874 converges, so landing the convergence alone would leave
paramToField.tsasserting a count the same PR had just made wrong again — in the file the next consolidation reads first.Verified at
011293ac6(the final commit; the gate union below was re-run on that tree).What changed
@object-ui/core'sEXPANDABLE_FIELD_TYPESis the published reference-bearing field family. Four faces restated it inline, none deriving from it and none pinned against it. Each now reads it and carries an identity pin — a spy on itshas— so a member-identical private copy fails rather than quietly re-forking the table. No face copies the set.plugin-kanban/src/ObjectKanban.tsx:345lookup || master_detail || referenceisExpandableFieldType(def)plugin-detail/src/RecordDetailDrawer.tsx:227lookup || master_detail || referenceisExpandableFieldType(def)plugin-detail/src/HeaderHighlight.tsx:147reference || lookup || master_detail, insideisWideEXPANDABLE_FIELD_TYPES.has(resolvedType), the other three disjuncts untouchedapp-shell/src/utils/resolveActionParams.ts:489lookup || referenceEXPANDABLE_FIELD_TYPES.has(resolveParamWidgetType(resolvedType))Line-reference delta vs the card: none. All four anchors were re-derived on the merge-base (
b65fe911d) and each is repo-wide unique — asserted before writing, over 3648 source files, because four of these five restatements are near-identical disjunctions.ObjectKanban.tsxlands at 345 rather than the card's 346 only because the card cited the line carrying thereferenceterm inside a two-line statement.The per-face judgement on
user/tree— restoration on all fourThe card explicitly did not pre-empt this. Decided per face, with the read site as the evidence:
RecordDetailDrawer— the guard forces a field read-only, and the file's own stated reason is "we don't yet wire a relation picker inside the drawer's inline editor — showing them as plain text inputs would let users overwrite the relation with a free-form string." That applies touser/treeverbatim: same foreign-key storage. Restoration.HeaderHighlight— the guard grants the wide layout basis because "an editor (select / date / lookup) needs more room than a KPI number." The component's owndataSourceprop doc already nameslookup/usertogether as "the reference (lookup / user) editors". Restoration.resolveActionParams— the guard populates the picker configparamToFieldforwards to the lookup-field widget. Without it a param reaches the dialog with noreferenceToand degrades to a plain record-id text input. Restoration.ObjectKanban— no widening is possible; see the finding below.None of the four would be widened, so nothing was stopped and reported under that rule.
resolveActionParamsis the odd one — treated as its own judgementIt diverged on three members, not two: it also lacked
master_detail, so a field-backedmaster_detailaction param inherited no picker config at all and degraded to the unexplained "paste a UUID" box #3405 exists to prevent.And it is the one face where
referencedoes not drop, which is a measurement rather than an omission.referenceis refused by the spec'sActionParamSchematoo (pinned withlookup/master_detail/user/treeaccepted as live controls and retiredownerplus a nonsense spelling refused as dead ones) — but the dialog still accepts it from params already authored with it, via thePARAM_TYPE_ALIASESlegacy dialect inparamToField.ts, which folds it tolookup. Dropping the branch outright would have silently degraded such a param to a text input.So this face asks the shared family over the widget key that one alias table produces — read, not re-copied. That is deliberately the same expression
paramToField()evaluates one step later: this half populates a param's picker config and that half forwards it, so the two can no longer disagree. Measured over the entire specFieldTypevocabulary plus every alias key, exactly five inputs land in the family —lookup,master_detail,tree,user,reference— so nothing else widened.HeaderHighlight's widerisWidedisjunction — measured first, and pinnedisWideis two independent rules OR'd together. Only the reference-bearing one moved;email/url/textareaare this surface's own list (they widen because the value doesn't fit, not because of a picker) and are preserved verbatim. Three tests pin that they are still wide and that they are still not family members, so a later sweep cannot quietly absorb them.The
referencedrop — measured, with live and dead controlsAgainst
@objectstack/spec's closedFieldType(49 options):lookup/master_detail/user/treepresent,referenceabsent, retiredownerand a nonsense spelling absent. OnHeaderHighlightit is unreachable from the authored side too —HighlightField['type']is a closed TS union without it. Kept as an executable pin in each test file, so the day the spec adds the spelling these go red and the membership question reopens instead of the drop staying correct-by-accident.The #5875 half — prose only, no new number
paramToField.tssaid "This face held the fourth and last private copy";paramToField.test.tssaid "This module held the FOURTH and last hand-maintained answer." Both now say fourth converted, and neither states a replacement count — writing "the seventh and last" would re-create the defect with a bigger integer. Both point at the census and its falsification in the family's canonical home (packages/core/src/utils/expand-fields.ts), which is untouched here — it is read-only, already corrected by #5692.Verification — the identity pin is the deliverable
Three new test files, 44 assertions. Ablation direction was predicted before running: replace a converged face with a member-identical private copy (
new Set(['lookup', 'master_detail', 'tree', 'user'])) and its identity pin goes red while the member-set assertion stays green. Each file carries that contrast as an executable test, since it is the whole reason the pin is on identity.Observed, per face — mutation proven on disk both ways (injected text present and removed text absent) before each read, restore under a
trap … EXIT INT TERMwith a cwd-independent command, all four confirmed byte-identical to their pre-ablation copies afterwards andgit diff HEAD --statempty:resolveActionParamsRecordDetailDrawerHeaderHighlightObjectKanbanOne of these pins did not work at first, and the ablation is what caught it. The kanban pin, written as a bare
vi.spyOn(EXPANDABLE_FIELD_TYPES, 'has').mock.callscheck, passed its own ablation 6/6 green —ObjectKanban.tsxalso importsbuildExpandFields, which consults the same shared object once per schema field on every render, so "some call happened" was satisfied by a face that never converged. The pin now filters recorded calls by the calling stack frame (measured: 3 calls frombuildExpandFields, 1 fromresolveDisplay) and fails as designed. The file keeps the un-attributed version as a green-either-way test documenting why the filter is there.Counter-probes. (a) The restoration half is behaviourally checked on three faces and must be able to fail:
user/treefields are now read-only in the drawer,user/treechips now get the wide basis, andmaster_detail/user/treeparams now inherit all nine picker keys. (b) A type genuinely outside the family is still not reference-bearing —text/selectstay editable, aselectchip stays narrow,select/textparams carry no picker config — so "converge" is not satisfiable by treating everything as a relation.Out of scope, filed
#6063 —
ObjectKanban'sresolveDisplayguard is unreachable:if (isLookup && isOpaqueId(raw)) return undefined;is immediately followed byif (isOpaqueId(raw)) return undefined;, which subsumes it for every input. So no membership delta on that face is observable in either direction, and its test file records that absence rather than writing a counter-probe that cannot fail. That is a redundant guard, not a forked table — a different defect class — and the fix needs a decision (delete the dead branch, or narrow the over-broad line), so it is filed rather than fixed here. Converging the copy stays correct on its own terms: the day the guard is made live it reads the family instead of a stale literal.ActionParamDialog.tsx:351was not swept — it is a different rule (degraded-hint, not reference-bearing) and is already #5654.Gates, by name with exit codes
Re-run at
011293ac6. Dependency closures built first —pnpm --filterwith the per-package upstream-closure selector (name followed by caret-ellipsis) thenbuild, exit 0.pnpm check:esm-specifierstype-check—@object-ui/plugin-kanban/plugin-detail/app-shelllint(plaineslint .) — same three packagespackages/plugin-kanban/ packages/plugin-detail/packages/app-shell/src/utils/+ the 5ActionParamDialog/DeclaredActionsBarconsumersRecordDetailView/RelatedRecordActionsBridgeconsumer filescheck:phantom-deps/check:self-import/check:spec-symbols/check:action-forward-parity/check:icon-record-names/check:i18n-keyscheck-changeset-presence/check-changeset-no-major/check-changeset-fixed/check-control-bytes/check-lint-coverage/check-type-check-coveragecheck:esm-specifierswas missing from the first gate set, and that is the reusable lesson. The set was re-derived by hand frompackage.jsonand.github/workflows/— correct practice here, since this repo has no dispatch-gate deriver — but it was derived against top-level script names, and this gate runs as a step inside theType CheckCI job under a name that does not read like a type check. The second pass enumerated that job's own step list instead, which is what addedcheck:esm-specifiers,check:spec-symbols,check:action-forward-parity,check:icon-record-namesandcheck:i18n-keysabove. Rule worth carrying: any PR adding a new relative import to a published package must runcheck:esm-specifiers, and a hand-derived gate set should be checked against the CI job's step list, not only against top-level script names.The gate's verdict is repo-wide across all 39 published ESM packages, so it also clears
plugin-kanbanandplugin-detail. Confirmed independently: the only relative import this branch adds to published source is the one fixed here — every other added import is either a package specifier (@object-ui/core) or lives in the three new test files, which no published build emits.app-shell vitest was path-filtered, never whole-package (~784 s on the shared lock), and every invocation was the root-form
pnpm exec vitest runwith explicit paths — no--filterform anywhere.type-checkis hyphenated and each run echoed its script name, so none of them was a zero-match silent pass. Every exit code was captured before any pipe.Changeset:
.changeset/5874-expandable-field-family-convergence.md—patchon all three packages, since each face's accepted field set changes observably.⛔ Draft on purpose — the PM lands this. Not marked ready, not enqueued, no auto-merge.
Generated by Claude Code