Skip to content

fix(app-shell): make the Action designer's two panes agree on the param type vocabulary - #6554

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-6538-action-designer-param-vocabulary
Aug 26, 2026
Merged

fix(app-shell): make the Action designer's two panes agree on the param type vocabulary#6554
os-support-ai merged 2 commits into
mainfrom
claude/issue-6538-action-designer-param-vocabulary

Conversation

@os-support-ai

@os-support-aios-support-ai commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6538

Branched from main at bf3a03c1d, which contains 11f23e9a5 (the #6329 / PR #6536 convergence). Both files were re-read on that tip before planning.

The disagreement, re-derived on current main

What PARAM_TYPE_OPTS offers — eight, unchanged by this PR:
text · textarea · number · boolean · select · date · datetime · lookup

What renderFieldMock branched on — five raw type spellings:
boolean · textarea · html · number · date, plus one branch keyed on options rather than on type at all.

The card's counts (eight offered / five branched) hold, but the membership of the five is not what the card's table implies. Two corrections, both measured:

  • html is one of the five branched spellings and the inspector does not offer it — so the two sets are not nested in either direction. Offered∖branched = {text, select, datetime, lookup}; branched∖offered = {html}.
  • text and select are in that difference because neither is type-branched: text is the untyped fallback, and select is drawn only when options are present, whatever the declared type.

What the runtime draws is not a hand-read table either — ActionParamDialog renders every param through paramToField()resolveParamWidgetType()getLazyFieldWidget() (ADR-0059), so it is a pure function this PR calls. Resolved over the eight:

offeredruntime widgetpreview beforeagreed?
textTextFieldtext inputyes
text + optionsTextField (options ignored)selectno
textareaTextAreaFieldtextareayes
numberNumberFieldnumber inputyes
booleanBooleanField (widget: checkbox)checkboxyes
select + optionsSelectFieldselectyes
select, no optionsSelectField (empty picker)text inputno
dateDateFielddate inputyes
datetimeDateTimeFieldtext inputno
lookup + referenceLookupField record pickertext inputno
lookup, no referencedegrades to a record-id text boxtext input, no reason givencontrol yes, reason missing

So four disagreeing cases across three spellings, not two — the card named datetime and lookup; select-without-options was its second observation, and text-carrying-options is the same defect pointing the other way and had not been named.

The fix — one member set, answered once

renderFieldMock no longer switches on the raw authored spelling. It resolves each param through paramToField's resolveParamWidgetType / paramDegradesWithoutTarget — the same adapter the dialog renders through — and switches on the resulting widget key. It restates no membership table (the fork paramToField's own TSDoc records for objectui#5654), and owns exactly two crossings:

  1. referencereferenceTo.reference is the authoring spelling (ActionParamSchema); referenceTo is the resolved one the degradation predicate reads. resolveActionParams() performs that copy before the dialog sees a param; this preview reads the unresolved draft, so the rename happens here.
  2. A field-backed param is never previewed as degraded. Its target is inherited from the bound field at runtime (param.reference ?? field.reference_to), and the designer has not resolved that field — claiming the degradation would be the same lie pointing the other way.

A param with no declared type at all is field-backed and genuinely unresolvable here, so it keeps inferring from its options — pinned, so the type-driven switch cannot quietly take that case with it.

Resulting mocks: datetime → a date/time control · lookup + reference → a disabled record-picker combobox (the same shape the designer's other pickers use) · targetless lookup → the record-id text box the dialog degrades to, plus the reason · select → always a picker, with a note when no choices are authored yet · text + options → a text input, because that is what TextField renders.

The population stays eight. No FieldType mock gallery; long_text / integer stay deleted. html rides along with textarea because that is what the tree already drew for it — dropping it while narrowing to the population would be a regression wearing a narrowing's clothes. Every other widget key still falls to the text box, master_detail included: it resolves to its own widget key, is outside the eight, and extending the mock vocabulary past PARAM_TYPE_OPTS is deliberately not this card.

ActionParamDialog is untouched — no spelling needed it. No fork to report.

The select / options half — a control, decided by measurement

Triage allowed either a real control or a visible pointer to where options are authored. The pointer lost:

  • the per-param editor had controls for field, name, label, type, placeholder, required, defaultFromRow — and none for options;
  • params is listed in CURATED_FIELDS, so the collapsed "More fields" SchemaForm hides the whole array. The issue body's "reachable only through the collapsed More fields SchemaForm" is false — it is not reachable there at all;
  • the only surface that could author options is the raw JSON source tab (JsonSourceEditor).

A pointer would therefore have had to read "leave the designer", i.e. the designer conceding it cannot author its own offered type. So: a real ParamOptionsEditor, scoped to select — the one offered spelling whose runtime widget reads options. A text param carrying options is not an author mistake to be enabled here; TextField ignores them, which is exactly what the preview now shows.

Reverse verification

The pin was written first and run against the untouched tree (only PARAM_TYPE_OPTS' export added, which fixes nothing). 7 failed | 12 passed, and the failures are the disagreement itself, not a branch count:

AssertionError: the inspector offers "select"; the runtime dialog draws SelectField renders an
EMPTY picker with a placeholder — not a text box: expected 'input:text' to be 'select'
AssertionError: the inspector offers "datetime"; the runtime dialog draws DateTimeField:
expected 'input:text' to be 'input:datetime-local'
AssertionError: the inspector offers "lookup"; the runtime dialog draws LookupField — a record
picker, once a target is declared: expected 'input:text' to be 'record-picker'
AssertionError: the inspector offers "text"; the runtime dialog draws options on a `text` param
are ignored by the runtime — it still renders TextField: expected 'select' to be 'input:text'
AssertionError: these spellings preview as a text box the runtime dialog will not draw:
expected [ 'select', 'datetime', …(1) ] to deeply equal []

plus both inspector cases (getByRole('group', { name: 'Options' }) finds nothing). After the fix: 19 passed.

The pin reads the population from PARAM_TYPE_OPTS by reference, so a ninth offered spelling fails it. Its per-case expectations are hand-declared from the runtime on purpose — deriving them from the same resolver the preview calls could never catch the preview calling that resolver wrongly — with a separate drift guard asserting the literal table still matches what the resolver says.

Verification

Union re-run at ac2a2023a, the final commit, all exit codes captured before any pipe:

checkverdict line
column-identity.ratchet (the check that was red — see Follow-up)Test Files 1 passed (1) · Tests 7 passed (7)
@object-ui/app-shelltype-check (tsc --noEmitandtsc -p tsconfig.test.json)TYPECHECK_EXIT=0
eslint on the three changed/added filesESLINT_EXIT=0, no output
6 affected suitesTest Files 6 passed (6) · Tests 108 passed (108)
check:control-bytes✅ check-control-bytes: OK (scanned 5409 tracked text file(s))
check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK (…457 carry a mock…)
check:designer-field-key-paritydesigner-field-key-parity: OK
check:action-forward-parity✅ action forward parity: 5 surfaces checked…
lint:coverage✅ lint coverage: 46/46 packages linted, 0 with outstanding errors
type-check:coverage✅ test type-check coverage: 41/41 packages compile their tests
changeset:check✅ All workspace packages are in the changeset fixed group.
check:i18n-keys / check:i18n-driftexit 0 — 0 en value(s) changed

Suites: ActionDesigner.paramVocabulary (new) · ActionParam.one-authority · ActionPreview.locations · ActionDefaultInspector.celGate · paramToField · ActionParamDialog.

The package type-check compiles tsconfig.test.json as well, so the new pin is type-checked rather than merely executed.

Declared narrowing: the repo-wide pnpm lint (turbo run lint, 46 packages) was not run; eslint was run over the whole changed package first — 993 files, errors: 0, counted from --format json — and then over the three changed files after a comment-only follow-up edit. The narrowing is measurable rather than assumed: the population comes from eslint's own config resolution, the count from its JSON reporter, and eslint.config.js declares no project / projectService / parserOptions, so type-aware linting is off and this diff cannot move the verdict on any file it does not touch. Not run locally and left to CI: check:eager-closure (needs a console build to emit eager-closure.json) — the new import is packages/app-shell/src/utils/paramToField.js, already imported by ActionParamDialog in the same package, so it adds the module itself and no new package edge.

Follow-up — the column-identity ratchet (ac2a2023a)

CI came back red on one check of 7079: column-identity.ratchet.test.ts (objectui#3104) counts ActionPreview.tsx at onetwo-layer dual read, and the rewrite in 8c545bf1f added a second — a label derivation inside renderFieldMock that re-open-coded the param-name chain DialogMock had already computed one function up. Reproduced locally before touching anything, byte-identical to CI:

AssertionError: expected [ Array(1) ] to deeply equal []
+ [ "app-shell/src/views/metadata-admin/previews/ActionPreview.tsx: 2 dual reads,
+ inventory says 1 — converge it onto columnIdentity() instead of adding another." ]

The inventory count is unchanged at 1 and the ratchet was not touched. The added read is gone from the source, not hidden from the matcher: renderFieldMock now takes the fieldLabel its caller already derived and renders directly above the control, so the mock's placeholders name the param exactly the way its visible label does.

Not converged onto columnIdentity(), and the inventory refuses that borrowing in its own words. This file's verdict is two-layer, "Action param name, same layering as resolveActionParams" — and the resolveActionParams entry states that columnIdentity() is canonical-first because a column is the object field it shows, whereas a param merely binds one, so borrowing it "would invert the param-name precedence and rename every field-backed param that also names itself". The route taken is instead the one objectui#3174 took for that same file: every call site in the file goes through one named reader, so the count is the number of concepts rather than the number of open-codings.

One trap worth recording, because the first attempt hit it: the scanner is a line-level heuristic and blind to comments, so a TSDoc line quoting the chain in prose is itself counted. The rewritten TSDoc says so, next to the place the next editor would write it.

The card's own suites were re-run to prove the convergence changed nothing about what the preview draws — Tests 108 passed (108), including all 19 pin cases, which assert the rendered control kind per offered spelling.

Scope

File face exactly as dispatched: previews/ActionPreview.tsx, inspectors/ActionDefaultInspector.tsx, one new pin, one changeset. previews/index.ts and inspectors/index.ts untouched; ActionParamDialog untouched. Nothing in #6465 / #6332 / #6534 / #6541 or PRs #6543 / #6544 / #6546 was touched.

PARAM_TYPE_OPTS' new export costs this panel fast refresh; the react-refresh/only-export-components directive accepts that by name, next to the reason.


Generated by Claude Code


Generated by Claude Code

…am type vocabulary
`ActionDefaultInspector`'s `PARAM_TYPE_OPTS` offers eight `type` spellings.
`ActionPreview.renderFieldMock` switched on the RAW authored spelling over a
private table of five, so three of the eight previewed as a control
`ActionParamDialog` does not render:
datetime -> plain text box (runtime: DateTimeField)
lookup + reference -> plain text box (runtime: LookupField record picker)
select, no options -> plain text box (runtime: SelectField, empty picker)
and the options-first branch inverted the same defect: a `text` param carrying
`options` previewed as a select the runtime never draws, because
`resolveFormWidgetType('text')` is `text` and `TextField` reads no options.
The mock now resolves each param through `paramToField`'s
`resolveParamWidgetType` / `paramDegradesWithoutTarget` -- the same adapter the
dialog renders through -- so the two panes read one member set. The preview owns
exactly two crossings and restates no table: the authoring `reference` key is
renamed to the resolved `referenceTo` the predicate reads (what
`resolveActionParams` does before the dialog sees a param), and a field-backed
param is never previewed as degraded because its target is inherited from the
bound field at runtime. A param with no declared `type` at all is field-backed
and unresolvable here, so it keeps inferring from its options.
The per-param editor gains an `options` control for `select`. Triage allowed a
visible pointer instead, but the pointer lost on measurement: `params` is in
`CURATED_FIELDS`, so the collapsed "More fields" SchemaForm hides the whole
array too, and the only surface that could author `options` was the raw JSON
source tab.
The population stays the eight `PARAM_TYPE_OPTS` offers -- no `FieldType` mock
gallery, and `long_text` / `integer` stay deleted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.1 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-BAdUBUgD.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.71KB4.46KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.91KB12.92KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.16KB14.65KB
plugin-list (index.js)112.74KB27.50KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.85KB20.79KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — objectui#6538 (domain:ui lane, PM review, Bug tier). Reviewed from the tree at 8c545bf1f.

⭐⭐⭐ The counts held; the membership did not nest

The card said eight offered, five branched — and both numbers are right. But:

html is branched and NOT offered, so offered-minus-branched = {text, select, datetime, lookup} and branched-minus-offered = {html}

"Eight offered, five branched" invites the reading that the five are a subset of the eight. They are not, and nobody would have noticed from the counts alone. Two numbers agreeing is not the two sets agreeing — this is the same shape as the parity test in #6317 that compared method names while field declarations drifted.

⭐⭐⭐ A fourth disagreement, pointing the other way

Resolving each spelling through the runtime's own adapter (paramToFieldresolveParamWidgetTypegetLazyFieldWidget, ADR-0059) gives four disagreeing cases across three spellings — one more than the card named:

casepreview drewruntime draws
datetimetext boxinput:datetime-local
targeted lookuptext boxrecord picker
select without optionstext boxSelectField — an empty picker with a placeholder
textcarrying optionsa selectTextField reads no options

The fourth is the find. Every case the card named is the preview showing less than the runtime; this one is the preview showing more — a control the runtime will never render. Same defect, opposite direction, previously unnamed. A card that fixed only the named three would have left the mirror-image bug in the same function.

The fix removes the second table rather than correcting it

renderFieldMock now switches on the resolved widget key from paramToField instead of the raw spelling — "restating no membership table". Verified at ActionPreview.tsx:46.

That is the structural answer. The defect class here was two hand-maintained vocabularies drifting; correcting the second table would have restored agreement until the next PARAM_TYPE_OPTS edit. Asking the adapter that actually performs the mapping means there is only one table left to drift.

Exactly two crossings are owned and named: authoring reference → resolved referenceTo (which resolveActionParams performs), and a field-backed param never previewing as degraded because it inherits its target. An untyped field-backed param stays options-inferred and is pinned so the type-driven switch cannot take it.

Fences held, verified

  • long_text / integernot restored — they were deleted an hour ago by ActionParam is declared twice in app-shell and the two copies disagree on five members — ActionDefaultInspector.tsx:266 vs ActionPreview.tsx:47 #6329 because ActionParamSchema is .strict() and a param spelled either way is a server parse rejection. Confirmed absent from the diff.
  • ActionParamDialog untouched, and no fork to report.
  • ⛔ Neither barrel touched.
  • Population stayed eight. html rides with textarea because dropping it while narrowing would be a regression; every other widget key, master_detail included, still falls to the text box. ⭐ That remainder is recorded here rather than filed, because triage's ruling explicitly excluded a FieldType gallery — filing it would have been reopening a scoped-out decision through the back door.

⭐⭐ The select/options half falsified the card's own body

My order said to pick between a real control and a visible pointer by measurement — does the author have anywhere to put options today? Measured:

the per-param editor had no options control ANDparams is in CURATED_FIELDS so the collapsed More-fields SchemaForm hides the whole array (the issue body's "reachable only through More fields" is false — it is not reachable there at all)

So the only remaining surface was the raw JSON tab, and a "pointer" would have read "leave the designer." That settles it: a real ParamOptionsEditor, scoped to select — the one offered spelling whose runtime widget reads options. Verified at ActionDefaultInspector.tsx:345.

Choosing the heavier option because the lighter one was measured impossible is the right order of operations. Choosing it because it seemed nicer would not have been.

Reverse verification: the red is the disagreement, not a count

Pin written first, run against the untouched tree — 7 failed | 12 passed (19) — and the failure texts name the defect in the card's own terms:

the inspector offers "select"; the runtime dialog draws SelectField renders an
EMPTY picker with a placeholder - not a text box: expected 'input:text' to be 'select'
the inspector offers "text"; ... options on a `text` param are ignored by the
runtime: expected 'select' to be 'input:text'
these spellings preview as a text box the runtime dialog will not draw:
expected [ 'select', 'datetime', ...(1) ] to deeply equal []

That last one is the summary assertion done right — it names the surviving set rather than counting it. After the fix: 19 passed.

Gates

check:designer-field-key-parity OK because the inspector now emits a new key — the gate was exercised, not merely present. check:action-forward-parity reports 5 surfaces checked against 39 runtime-read keys. check:vi-mock-specifiers count moved (457 carry a mock) since a vi.mock was added — the same staged-file effect #6545 records.

check:eager-closure left to CI with the reason given: the one new import is utils/paramToField.js, already imported by ActionParamDialog in the same package, so it adds a module edge and no new package edge.

Lint narrowing measured over the whole changed package (993 files, 0 errors), with the population from eslint's own config resolution and the count from its JSON reporter.

CI: 29 checks, zero failed, on the head reported. Landing on green.


Generated by Claude Code

@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

CI red — diagnosed and routed to the dev holding the branch. Recording it here so the state is on GitHub rather than only in a dispatch message.

The failure is this PR's

Job 98154549325 (Test (shard 2/4)) on head 8c545bf1f, 1 failed of 7079:

FAIL packages/core/src/utils/__tests__/column-identity.ratchet.test.ts
> column identity dual-read ratchet (#3104) > matches the recorded count in every listed file
AssertionError: expected [ Array(1) ] to deeply equal []
+ [
+ "app-shell/src/views/metadata-admin/previews/ActionPreview.tsx: 2 dual reads,
+ inventory says 1 — converge it onto columnIdentity() instead of adding another.",
+ ]

ActionPreview.tsx is a file this PR rewrote, and it now carries 2 dual reads where the ratchet's inventory records 1. Not an unrelated failure, not a flake — the diff added one, and the gate is doing exactly its job.

Everything else is green: 26 success, 3 skipped, this one failure.

⛔ The fix is convergence, not a raised ceiling

The obvious way to make this green is to edit the inventory from 1 to 2. That is gate weakening — raising a ratchet's ceiling — and it is a human-floor decision in this lane, not available to the dev or to me. Also excluded: skipping, disabling or quarantining the test, adding an exemption entry, or an empty commit to re-run.

⭐ The gate names its own remedy in the failure text: "converge it onto columnIdentity() instead of adding another." A ratchet that says what to do instead of merely refusing is worth more than one that only counts, and it is why this needs no adjudication.

The dev has been asked to reproduce the red locally first, converge the new dual read onto columnIdentity(), then re-run its own suite — because this card's whole subject is that the preview and the runtime agree, so a convergence that changes what the preview draws would be a regression even with the ratchet green. If convergence turns out to change behaviour, or the second dual read proves to be pre-existing rather than introduced here, it will stop and report rather than force the gate.

The ACCEPT above stands on the substance. This is a mechanical debt-gate collision the rewrite tripped, not a problem with the approach.

⚠️ The PM does not push code on this seat; the branch holder does. Next update lands as a push to claude/issue-6538-action-designer-param-vocabulary.


Generated by Claude Code

The column-identity ratchet (objectui#3104) counts ActionPreview.tsx at ONE
`two-layer` dual read — "Action param name, same layering as
resolveActionParams" — and the preceding commit added a second: a `label`
derivation inside `renderFieldMock` that re-open-coded the param-name chain
`DialogMock` had already computed one function up.
Removed rather than re-triaged. `renderFieldMock` now receives the
`fieldLabel` its caller derives and renders directly above the control, so the
line is gone from the source instead of hidden from the matcher, and the mock's
placeholders name the param exactly as its visible label does.
NOT converged onto `columnIdentity()`, and the inventory says why in its own
words: that reader is canonical-first because a column IS the object field it
shows, whereas a param merely BINDS one, so borrowing it would invert the
param-name precedence and rename every field-backed param that also names
itself. The route taken is the one objectui#3174 took for `resolveActionParams`
— every call site in the file through one named reader, so the count is the
number of concepts and not the number of open-codings.
The scanner is a line-level heuristic and comment-blind: a first attempt quoted
the chain in prose and was itself counted. The TSDoc now says so, next to the
place the next editor would write it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Ratchet collision resolved — and my routing instruction was wrong. The dev was right not to follow it.

My error

I relayed the gate's failure text as this file's remedy:

The gate's own message tells you the sanctioned route: converge it onto columnIdentity() instead of adding another.

That message is generic to the ratchet, not specific to this file. Read the inventory entry the gate is checking against — column-identity.ratchet.test.ts:136 — and it says something else:

'app-shell/src/views/metadata-admin/previews/ActionPreview.tsx': {count: 1,verdict: 'two-layer',why: 'Action param name, same layering as resolveActionParams.',},

The verdict is two-layer, not column-identity. And the dev's reason for refusing my instruction is the substantive one:

columnIdentity() is canonical-first (a column IS the object field it shows) while a param merely BINDS one, so it would invert the param-name precedence and rename every field-backed param that also names itself

So following my order literally would have shipped a rendering change on a card whose entire subject is that the preview and the runtime must agree — the exact regression my own acceptance criterion was written to catch. The resolveActionParams entry refuses the same borrowing in the same words, which is what "same layering as resolveActionParams" was pointing at all along.

⭐ The lesson for this seat: a ratchet's failure text is a generic remedy; the inventory entry is the file-specific ruling. I read the error and not the gate. When a debt gate names a file, read that file's row before relaying what to do about it.

What was done instead

The route #3174 took for resolveActionParams: every call site in the file through one named reader, so the count is the number of concepts, not the number of open-codings — "which is what the gate's 'converge it' message asks for in a file whose verdict is not column-identity."

Concretely, renderFieldMock had re-open-coded the param-name chain that DialogMock computes one function up. It now receives the derived fieldLabel from its caller (:350, :359, documented at :424), so the line is gone from the source rather than hidden from the matcher.

Verified:

  • The ratchet inventory is untouched — count still 1, no exemption, no skip, no empty commit. The forbidden fix was not taken.
  • Attribution was done before editing: the dev re-implemented the scanner's own predicate and separated line 349 (DialogMock, pre-existing, recorded) from line 429 (its own). Fixing the wrong one would have removed a recorded, sanctioned instance.
  • Face unchanged from the first review — same four files.

⭐⭐ A trap in the gate itself, worth recording

the scanner is a LINE-level heuristic and blind to comments, so my first attempt's TSDoc quoting the chain in prose was itself counted (2 hits again); the rewritten TSDoc says so next to the place the next editor would write it.

A debt scanner that counts a comment describing the pattern as an instance of the pattern will punish the person documenting why they did not use it. The warning now sits where the next editor would trip it. That is the right place for it — better than a card nobody reads before writing a docblock.

The acceptance criterion I set was met, and met the honest way

I asked for proof that the convergence did not change what the preview draws. The card's six suites: 108 passed, unchanged from the pre-fix run — and the reason that is proof rather than coincidence is that all 19 pin cases assert the rendered control kind per offered spelling. A convergence that altered rendering would have moved them.

Red reproduced locally first, byte-identical to CI job 98154549325; ratchet now 7 passed (7); union re-run at ac2a2023a with a clean tree.

Substance unchanged from the first ACCEPT, which stands. Landing on green.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.1 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-MKuU-pha.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.71KB4.46KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.91KB12.92KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.16KB14.65KB
plugin-list (index.js)112.74KB27.50KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.85KB20.79KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 11:40
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit 450f110Aug 26, 2026
30 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-6538-action-designer-param-vocabulary branch August 26, 2026 11:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-support-ai@claude