fix(components,plugin-grid,app-shell): one authority for ComboboxOption, NamedActionDef and OrgTranslate (objectui#6349 batch 3) - #7691
Conversation
…on, NamedActionDef and OrgTranslate
Batch 3 of the one-authority ledger burn-down. Three names come off
KNOWN_COLLISIONS (33 -> 30), each by re-pointing the losing file at the
one authority with a plain `export type { X } from` re-export, which the
gate does not count and which keeps every existing import resolving to
the same name:
- ComboboxOption: the component's `{ value, label }` was a strict subset
of the @object-ui/types declaration (`{ value, label, disabled? }`),
so packages/components/src/custom/combobox.tsx now re-exports it via
the `./form` subpath (the root barrel does not publish the name).
- NamedActionDef: identical in both plugin-grid folds;
resolveLegacyRowActions.ts is the authority, resolveBulkActions.ts
re-exports.
- OrgTranslate: identical in both organization helpers;
orgErrorMessage.ts is the authority, orgRoleLabel.ts re-exports.
FilterBuilderCondition / FilterGroup stay in the ledger, annotated: the
shapes disagree on id, value and operator, and the only dependency-legal
re-point retypes `operator`, which is the vocabulary objectui#7561 asks
a maintainer to rule on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
✅ 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
|
Contract review —
|
| leg | mutation | observed |
|---|---|---|
| A fresh | delete live DomProps row (blob 49574c8ac → 5c43b44ed) |
exit 1, DomProps — a NEW colliding name, 1 failed / 10 passed |
| B fresh via source | re-declare OrgTranslate in orgRoleLabel.ts (679e9b8e2 → 54865a348) |
exit 1, OrgTranslate — a NEW colliding name |
| C1 / C2 / C3 stale | re-add each converged row | exit 1, <name> — no longer collides at: listing both files each time |
| D (mine) | base's whole ledger (906488fbe) under head's sources |
exit 1, three no longer collides blocks naming all six files |
C1–C3 and D are the direct proof the three convergences are real source changes, not ledger edits.
Disclosed prediction miss (TS2739 predicted, TS2741 observed): RIGHT that nothing rested on it. Synthetic lit control on TS 6.0.3: Record<keyof X,1> missing one key → TS2741, missing two → TS2739. The author predicted the plural spelling for a single new key; direction, exit code and the assertion ("a consumer that exhausts the type breaks") were unaffected.
Verification union at head (my re-run): tsc --noEmit exit 0 for components, plugin-grid, app-shell against the fresh dist; type-check:scripts exit 0 (covers the edited gate file); eslint on the four changed source files exit 0; check-changeset-no-major ✅, check-changeset-presence ✅ (3 source file(s) of 3 released package(s) changed … 1 changeset), check-changeset-overwrite ✅; fence held (git diff --name-only = the 5 declared files); git ls-remote 'refs/heads/claude/issue-6349*' = 1 branch (the serial-batch constraint held).
② Semver grading
Declared: @object-ui/components: minor, @object-ui/plugin-grid: patch, @object-ui/app-shell: patch. Correct level: the same. AGENTS.md §版本号策略 (lines 236–241) pins objectui's major to @objectstack's and requires objectui's own breaking changes to be declared minor with the breaking semantics in the body; major is refused by scripts/check-changeset-no-major.mjs (passes here). The components change is a published declaration gaining an optional member — value-compatible in both directions, breaking only for exhaustive-keyof consumers — so it is minor on either reading, and the body spells out precisely that caveat plus the unread-disabled note. plugin-grid and app-shell change no reachable surface (measured in ①.3/①.4), so patch is right. The 40-package fixed group takes the highest bump, minor, exactly as batches 1 (#6936) and 2 (#7566) and the Kanban trio (#7643) landed ('@object-ui/types': minor); the cited #6574 precedent is likewise types: minor / plugin-list: minor / plugin-dashboard: patch. Every sentence of the changeset body I could measure is accurate.
③ Boundary-flag disposition
- PM ruling (comment 5548770074), route C —
FilterBuilderCondition/FilterGroupto filter-builder declares one operator vocabulary and renders another: the mirror refuses every dropdown id but three, and every id it accepts but three draws a blank trigger #7561: SOUND. Reproduced independently on the head tree: controltsc --noEmit -p packages/componentsexit 0, 0 errors; mutation (component's two declarations →export type { FilterBuilderCondition, FilterGroup } from "@object-ui/types", blobd27229586→f9ecb9515, 2 declarations → 0) exit 2, 29 errors, all infilter-builder.tsx: 2× TS2322Type 'string' is not assignable to type 'FilterBuilderOperator', 25× TS2339 (id/value/operator/fieldon the recursive union), 1× TS2345, 1× TS2353 ('id' does not exist in type 'FilterBuilderCondition'); restored, blob back tod27229586. Both headers claim one concept (filter-builder.tsx:46-48: "this is the same thing, so it takes the same name"); converging the name retypesoperator, which is filter-builder declares one operator vocabulary and renders another: the mirror refuses every dropdown id but three, and every id it accepts but three draws a blank trigger #7561's question; and the ruling does not even depend on direction — the opposite direction would retypeoperatoron the types side, equally filter-builder declares one operator vocabulary and renders another: the mirror refuses every dropdown id but three, and every id it accepts but three draws a blank trigger #7561's call. B (rename) is rightly refused:plugin-list/src/ListView.tsx:13andplugin-view/src/config/view-config-utils.ts:8importFilterGroupfrom@object-ui/components(verified), so a rename spends a published-name break on a distinction both files deny. One wording correction for the record:@object-ui/typesis not "zero dependencies" — it depends on@objectstack/spec ^17.0.0andzod ^4.4.3; it has zero in-workspace@object-ui/*dependencies, which is the property that makestypes → componentsa cycle. filter-builder declares one operator vocabulary and renders another: the mirror refuses every dropdown id but three, and every id it accepts but three draws a blank trigger #7561 is triaged and carries the coupling (comment 5548770124) — verified. - Stale card body / "36 vs 33": CONFIRMED.
ac8abb0f5ledger = 36;2c71482ea(PR refactor(types)!: rename the declarative Kanban trio, leaving the bare names to plugin-kanban #7643, 2026-09-04) removedKanbanCard/KanbanColumn/KanbanSchema; merge-base = 33; head = 30; the base ledger's own comment (:397) records the trio. Not a scope defect: all five batch-3 names were present at base and the exclusions still present (CalendarEvent,FilterBuilderOperator) were untouched; the excluded-but-gone names could not be breached. The PM's decision to leave the body as the record and re-derive the next batch from the ledger is right. - Clause-② determination: stands (①.1).
- Carrier half-state: PR carries
needs:contract-review; the card now does too (PM comment §3). Closed. out_of_scope_findings— Combobox never reads options[].disabled: @object-ui/types declares it and form.zod.ts validates it, the component renders every option enabled #7687 (Comboboxnever readsoptions[].disabled): acceptable here; this PR does not publish a new phantom. Verified:custom/combobox.tsxreads only the whole-controldisabled(:57/:68/:92),options.map(:114-122) forwardskey/value/onSelectonly;renderers/form/combobox.tsx:28passesschema.optionsstraight through. The member was already the published shape ofComboboxSchema.optionson the@object-ui/typesroot and is zod-validated; the components face was the narrower copy, and the only dependency-legal convergence makes types the authority — the member's visibility on the components face is the convergence itself. Behaviour lives in Combobox never reads options[].disabled: @object-ui/types declares it and form.zod.ts validates it, the component renders every option enabled #7687. Flag for the PM: Combobox never reads options[].disabled: @object-ui/types declares it and form.zod.ts validates it, the component renders every option enabled #7687 had no labels and no assignee when I read it — the same hole filter-builder declares one operator vocabulary and renders another: the mirror refuses every dropdown id but three, and every id it accepts but three draws a blank trigger #7561 was found in.- Docs:
content/docs/components/form/combobox.mdx:28-31documentsComboboxOption { value; label }in aplaintextfence no gate reads (the 7239 pin covers onlyComboboxSchema.disabled); it was already stale against the types face before this PR; the author's disclosed non-edit is acceptable, but whichever remedy Combobox never reads options[].disabled: @object-ui/types declares it and form.zod.ts validates it, the component renders every option enabled #7687 takes must update that fence in the same change. - PR is "behind" main: no overlap, clean merge-tree (①.7). Dev-report open question A/B/C: answered by the ruling (C). Non-blocking recommendation: after feat(types,mobile)!: retire the MobileResponsiveConfig and GestureConfig published types #7683 lands, publish
ComboboxOptionfrom the@object-ui/typesroot barrel (alongsideSelectOption/RadioOption) as its own types-surface card, so the authority is reachable from the barrel consumers look at first.
VERDICT: PASS
Generated by Claude Code
Refs #6349 — batch 3 of the one-authority ledger burn-down. Not a closing reference: 30 ledger rows remain after this PR and the card stays open as their owner.
Clause-②: yes — my own determination, not only the dispatch's. Re-pointing
ComboboxOptionmoves what the published@object-ui/componentsroot entry declares under that name (it gains the optionaldisabledmember), and a probe shows a literal the base declaration REFUSED is ACCEPTED at head. Draft,needs:contract-review, not ready, not enqueued, no auto-merge. Implemented by the os-dev seat in sessionsession_01KbJQ1y1J12nZxYzFWhP8Q3.Ledger reproduced on my base — 33, not 36
Merge-base
a472b0716.KNOWN_COLLISIONSprinted and counted by row: 33 entries. The sizing comment on the card read 36 atac8abb0f5; the three-row delta isKanbanCard/KanbanColumn/KanbanSchema, removed by PR #7643 (2c71482ea, 2026-09-04, after that reading), and the ledger's own inline comment already records them as converged. All five batch-3 names were still present on my base; of the exclusions,CalendarEventandFilterBuilderOperatorare present and the Kanban trio is already gone. After this PR: 30.The five entries — what was decided, and why
ComboboxOption@object-ui/types(packages/types/src/form.ts)packages/components/src/custom/combobox.tsxbecomesexport type { ComboboxOption } from "@object-ui/types/form"NamedActionDefpackages/plugin-grid/src/resolveLegacyRowActions.tsresolveBulkActions.tsimports and re-exports itEqualprobe exit 0); not on the package's public entryOrgTranslatepackages/app-shell/src/console/organizations/orgErrorMessage.tsorgRoleLabel.tsimports and re-exports itEqualprobe exit 0); not on the package's public entryFilterBuilderConditionFilterGroupWhy re-point rather than rename in all three: the card's rule is rename only where the shapes are genuinely different things. Two are identical and one is a subset whose header on both sides describes the same concept (the option a
ComboboxSchemacarries). The losing files keep exporting the name as a plain re-export, so an import from the losing module resolves to the SAME shape it always did — no consumer silently gets a different type. The./formsubpath is the door forComboboxOptionbecause the@object-ui/typesroot barrel does not publish that name (onlyComboboxSchema,SelectOption,RadioOption); adding it to the barrel would have touched a file another dispatch holds.The Filter pair is inseparable from objectui#7561 — measured, not judged
The dispatch warned that
FilterBuilderConditioncarries anoperatortyped by the excludedFilterBuilderOperator, and that if the NAME could not be re-pointed without touching the operator VOCABULARY it was a stop-and-report. It cannot:{ id: string; field: string; operator: string; value: string | number | boolean | array }andFilterGroup { id: string; logic; conditions: FilterBuilderCondition[] }. Types:{ field; operator: FilterBuilderOperator; value?: any }(noid) andFilterGroup { id?: string; logic; conditions: (FilterBuilderCondition | FilterGroup)[] }. Neither side is a subset of the other, yet BOTH headers claim one concept (the component's: "this is the same thing, so it takes the same name"; the types': "the shapeFilterBuilderreads"). One concept with drift is the re-point class, not the rename class.@object-ui/typeshas zero dependencies, so the only dependency-legal re-point is components → types.tsc --noEmit -p packages/componentson the committed tree — exit 0, 0 errors. Mutation: replace the component's two declarations with a re-export of the types pair (mutation proven on disk: 2 declarations → 0, re-export 0 → 1, blobd27229586→cfb2233e8), same tsc — exit 2, 29 errors: 2×Type 'string' is not assignable to type 'FilterBuilderOperator'(the dropdown's camelCase ids meeting the snake_case union), 25 errors onid/value/operatornot existing on the recursive union, 1× TS2353'id' does not exist in type 'FilterBuilderCondition'. Restored withgit checkout HEAD --on the absolute path under anEXIT INT TERMtrap;git diff HEADempty, blob back tod27229586. Direction and both predicted error classes matched.So converging the name retypes
operator, which is exactly the authority #7561 asks a maintainer to decide (its options 1 and 2 are "mirror follows dropdown" and "dropdown follows mirror"). The vocabulary-neutral alternative — declaring them two concepts and RENAMING one side — contradicts both headers, breaks a published name (plugin-listandplugin-viewimportFilterGroupfrom@object-ui/components), and would likely be undone by #7561's ruling. Both rows stay inKNOWN_COLLISIONSwith a comment saying why; the decision goes back to the PM in the report.Published surface — measured on the emitted declarations
Built
components,plugin-gridandapp-shellat base (a472b0716, snapshot of every.d.ts) and at head (76b737e41), full recursive diff:@object-ui/components: 208 vs 208 files; exactly ONE differs,dist/custom/combobox.d.ts— the interface becomesexport type { ComboboxOption } from '@object-ui/types/form'.dist/index.d.tsbyte-identical.@object-ui/plugin-grid: 31 vs 31; onlydist/resolveBulkActions.d.ts(interface → re-export of./resolveLegacyRowActions.js).dist/index.d.tsbyte-identical; the name was never on the entry.@object-ui/app-shell: 453 vs 453; onlydist/console/organizations/orgRoleLabel.d.ts(alias → re-export of./orgErrorMessage.js).dist/index.d.tsbyte-identical; not on the entry.Probes against the built base vs head
dist/index.d.tsof@object-ui/components, predictions written first:disabled: true— base exit 2 TS2353 ('disabled' does not exist in type 'ComboboxOption'), head exit 0. This is the clause-② content limb firing.Recordkeyed bykeyof ComboboxOptionwith the two old keys — base exit 0, head exit 2.Property 'disabled' is missing— the single-property spelling of the same diagnostic). Direction matched; the code did not; no assertion rested on the code. So: every VALUE that type-checked before still does (nothing narrows, no key changes type); a consumer that EXHAUSTS the type breaks. In-repokeyof ComboboxOptionconsumers: zero.value: 1— TS2322 on base AND head, same message.Note for the reviewer: the
Comboboxcomponent never readsoption.disabled, so the member now visible on the components face is the same phantom already declared on the types face. Behaviour unchanged here; recorded as objectui#7687 (out of scope: a behaviour defect, not a naming one).Grading:
@object-ui/componentsminor (a published declaration gaining a member — the #6574 / #6936 precedent),plugin-gridandapp-shellpatch.The gate is non-vacuous in both directions — five legs on the committed tree
Each leg: mutate the FACT not the assertion; prove the mutation on disk by anchored count AND
git hash-objectmovement off the HEAD blob; run the gate; restore withgit checkout HEAD --on an absolute path fromgit rev-parse --show-toplevelundertrap … EXIT INT TERM; prove the restore by emptygit diff HEADAND blob equality. Prediction written before each run.DomPropsrowDomProps — a NEW colliding name, 1 failed of 1149574c8ac→5c43b44ed→ restored)OrgTranslateinorgRoleLabel.tsin place of the re-exportOrgTranslate — a NEW colliding name679e9b8e2→ee4b0d8c8→ restored)ComboboxOptionrowno longer collides at:listing BOTH files (a single-file name has no observed entry)NamedActionDefrowOrgTranslaterowLegs C1–C3 are the direct proof that each convergence is real rather than a ledger edit. Final tree state after the battery: 0 modified paths.
Verification on the final commit
76b737e41Exit codes captured by redirect before any pipe; verdict lines are the tools' own.
pnpm exec vitest run --project unit scripts/__tests__/one-authority-per-exported-name-6273.test.ts—Test Files 1 passed (1),Tests 11 passed (11).scripts/__tests__/one-authority…,packages/components/src/custom/,packages/components/src/renderers/form/, the two plugin-gridresolve*tests,packages/app-shell/src/console/organizations/— BEFORETest Files 72 passed (72)/Tests 529 passed (529); AFTER 72 / 529.pnpm --filter @object-ui/components type-check,…/plugin-grid type-check,…/app-shell type-check(hyphenated; script echoed,tsc --noEmit && tsc -p tsconfig.test.json) — exit 0 each;--listFilesshows each edited source file in its program (1 hit each).pnpm run type-check:scripts(covers the edited ledger file) — exit 0.pnpm exec eslint .plain, whole repo — exit 0,11928 problems (0 errors, 11928 warnings).turbo run build— 43/43 successful; thencheck:readme-exports—✅ check-readme-exports: OK (… 421 self-imports judged (421 real, 0 wrong-path, 0 fabricated) …).check:control-bytes(6258 files OK) ·check:esm-specifiers·check:doc-types·check:spec-symbols·check:self-import·check:phantom-deps·check-changeset-presence(3 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)) ·check-changeset-no-major— all exit 0.check-governed-queue-guard --teston all five paths: NOT GOVERNED.Fences
git diff --name-only a472b0716..76b737e41:.changeset/6349-name-authority-batch-3.md,packages/app-shell/src/console/organizations/orgRoleLabel.ts,packages/components/src/custom/combobox.tsx,packages/plugin-grid/src/resolveBulkActions.ts,scripts/__tests__/one-authority-per-exported-name-6273.test.ts. No file underpackages/types/src/(so nocomplex.ts, noregistry.ts, nomobile.ts, neither barrel), nothing of #7546 / #7519 / #7662 / #7669. Only one #6349 branch exists on the remote.Placeholders in this body are spelled as words rather than angle-bracket shapes because GitHub's sanitizer strips tag-shaped fragments from stored bodies.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Generated by Claude Code