Uh oh!
There was an error while loading. Please reload this page.
refactor(app-shell): converge both ActionParam shadows onto the published type - #6536
Conversation
…he shadows Written first and observed RED against the unconverged tree: the census names ActionDefaultInspector.tsx:266 and ActionPreview.tsx:47, and the import half names both files. The census is the reverse-verified half on purpose. A module-local declaration is invisible from outside its module (objectui#5899), which is also why scripts/__tests__/one-authority-per-exported-name-6273.test.ts is green on all three sites: its matcher requires `export`. The key-set, strict-schema and I18nLabel assertions are direction guards on the published authority, which this card does not change and which therefore cannot fail before it. Refs #6329 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
…shed type
`ActionParam` was declared three times, not twice: `@object-ui/types` publishes
it (derived from the spec's `ActionParamSchema` input, with its own parity
suite), and ActionDefaultInspector.tsx and ActionPreview.tsx each carried a
module-local `interface` of the same name. app-shell already read the published
one elsewhere, so both locals were shadows. Deleted, not reconciled against
each other (family ruling 甲A1, 2026-08-25).
Neither shadow needed a member the published type lacks, so the published
surface is untouched. What they got wrong was the declaration:
- the inspector's `[k: string]: unknown` typed every key `unknown`, so a
commit of a key `ActionParamSchema` rejects BY NAME type-checked and failed
on save — and it made the two copies look compatible while they described
different authoring surfaces;
- the preview's `label?: string | { en?: string }` admitted the `en` tag and
no other, while its own `localize` has always read `Object.values(o)[0]`.
Two consequences of withdrawing the local `type?: string` in favour of
`ResolvableParamFieldType`: `renderFieldMock` loses its `long_text` / `integer`
branches (both belong to other vocabularies — the console form-builder dialect
and JSON Schema — so a param spelled either way is a parse rejection and could
never reach the preview), and the inspector narrows its dropdown commit through
the runtime witnesses `@object-ui/types` exports rather than writing the raw
DOM string.
Fixes#6329
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q✅ 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
|
os-support-ai
commented
Aug 26, 2026
ACCEPT — objectui#6329 ( The dispatch order's correction — three declarations, not the card's two, with the third being the published authority — was verified and acted on. Both stop conditions were tested and neither was reached, which is the part worth checking rather than assuming. Both stop conditions cleared by measurementStop 1 (no widening of the published surface). The union of the two shadows' members is a strict subset of the published key set. Confirmed from the tree: the face is four files and Stop 2 (no barrel edits). Neither ⭐⭐⭐ The blind-instrument warning was load-bearing, and the response was better than the warningMy order said the inspector twin's
Exactly right, and it is the distinction I missed: removing the blindness does not make the assertion provable by this PR, because the thing it would now measure is out of the PR's face. So the reverse-verified half is a census instead — and the pin is a TypeScript AST walk, not a grep, because this PR adds comments to both files naming ⭐⭐ It also explains why the sibling gate Ablation was per-site, and proved itPin committed first, then only Two behaviour-visible consequences, declared rather than buried
On |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6329
ActionParamwas declared three times, not twice as the card counted. The dispatch order corrected the count, and the correction changes the job: the authority already exists and is published, so this is not a pick between two local twins.packages/types/src/ui-action.ts:355export interface ActionParam— published, derived from the spec'sActionParamSchemainput, with its own parity suite.../metadata-admin/inspectors/ActionDefaultInspector.tsx:266interface, 7 members +[k: string]: unknown.../metadata-admin/previews/ActionPreview.tsx:47interface, 10 members, no index signatureapp-shell already read the published name by reference elsewhere (
src/utils/resolveActionParams.test.ts:21), so 2 and 3 were shadows. Both are deleted, not reconciled against each other — family ruling 甲A1, 2026-08-25.Stop condition 1 was not reached, and here is the measurement
Every member of both shadows is already declared on the published type.
ActionParamisOmit<z.input<typeof ActionParamSchema>, 'type'>plus a locally-narrowedtype, and the spec object carriesname·field·objectOverride·label·type·required·options·placeholder·helpText·defaultValue·multiple·accept·maxSize·reference·defaultFromRow·visible·requiresFeature. The union of the two shadows is a strict subset. Nothing was added, removed or changed inpackages/types/src/ui-action.ts— that file is not in this diff. Stop condition 2 likewise: neitherpreviews/index.tsnorinspectors/index.tsis touched (both shadows were unexported, so deleting them reaches no barrel).What the shadows actually got wrong — measured, not picked by taste
[k: string]: unknown. An index signature admits every key at typeunknown, sopatchParam(i, { referenceTo: 'account' })type-checked whileActionParamSchema—.strict(), withreferenceTonamed in its alias map — rejects it on save. Same shape as thedescriptiondefectFlowNodeInspector.specKeys.test.tsxrecords. It is also what made the two copies look compatible:options/helpText/defaultValuewere declared outright on one side and swallowed asunknownon the other.label?: string | { en?: string }admitted theentag and no other, while its ownlocalizehelper has always readObject.values(o)[0]. An inline locale map keyedfr-FRrendered correctly and failedtsc. Converging widens the declaration onto what the code already did; it does not widen what the runtime accepts. Both files'localizeis byte-identical and unchanged.type?: stringon both sides was the loosest guess, and withdrawing it in favour ofResolvableParamFieldType(the spec's 49-memberFieldTypeplus objectui's three declared aliases) exposed two dead branches inActionPreview.renderFieldMock:p.type === 'long_text'andp.type === 'integer'. Neither spelling is in that vocabulary —long_textbelongs to the console form-builder dialect (apps/console/src/components/FormPage.tsx:1368),integerto JSON Schema (ToolPreview.tsx,json-schema-to-fields.ts) — andActionParamSchema.typeis aFieldTypeenum under.strict(), so a param spelled either way is a parse rejection on the server and could never have reached the preview. Both branches are deleted with the measurement recorded in a comment.@object-ui/typesexports (ACTION_PARAM_FIELD_TYPES+OBJECTUI_LOCAL_PARAM_FIELD_TYPES) instead of writing the raw DOM string, andPARAM_TYPE_OPTSgained asatisfiescheck so a ninth option in a dialect the server refuses cannot be added silently. It rejects rather than coerces, so this is a boundary check, not a lenient fallback.The blind instrument, and what the pin measures instead
The dispatch order's warning is correct and was load-bearing: an index signature swallows
keyof, so a key-set assertion pointed at the inspector's copy could not fail. Here the index signature goes away in the convergence rather than being worked around — but that only makes the key-set half live, it does not make it reverse-verifiable, because after convergence the key set belongs to the published type, which this card does not change.The half that can fail before and pass after is a census, because a module-local declaration is invisible from outside its module. That is also why
scripts/__tests__/one-authority-per-exported-name-6273.test.tsis green on all three sites: its matcher requiresexport(the instrument hole #5899 is about). Sopackages/app-shell/src/views/metadata-admin/ActionParam.one-authority.test.tscarries:interface ActionParamin prose; the compiler sees a comment. Controls prove the discriminator rather than asserting it: prose / import / re-export / string-literal spellings find nothing, realinterfaceandtypedeclarations find one each, and the walker finds the one declaration that should exist when pointed atpackages/types/src/ui-action.ts(non-vacuity);string extends keyof ActionParammust befalse(the index signature, asserted away), every member both shadows declared must survive onkeyof ActionParam, a@ts-expect-erroronreferenceTo(its use is proof: an unused directive isTS2578), the full ten-member param parsing clean through the real strictActionParamSchema, and both authorizedI18nLabelforms accepted.Reverse verification
Written first, run against the unconverged tree, RED, naming both sites with the card's own line numbers:
Then the pin was committed first, and an ablation run from that committed state reverted only
ActionPreview.tsxto831be7285. Predicted direction — red, naming that file alone — matched:The mutation is proven on disk in three independent readings (
grep -cin both directions plus a blob hash differing from theHEADblob), never from an exit code. The restore leg is proven the same way and not by its exit code:git checkout HEAD -- <abs path>(never a baregit checkout --, which reads the polluted index), thengit hash-objectback to614802f3…andgit diff HEADempty.Verification, all at
e660fed22pnpm --filter @object-ui/app-shell type-check→tsc --noEmit && tsc -p tsconfig.test.json, exit 0. Proven to cover this change rather than assumed:tsc -p tsconfig.test.json --listFilesOnlylists all three files, the pin included — so the@ts-expect-errorabove is genuinely checked.pnpm exec vitest runover the pin, both named test files,packages/app-shell/src/utils, the siblingone-authority-per-exported-name-6273gate andpackages/types/src/__tests__→Test Files 94 passed (94)/Tests 1236 passed (1236).check-control-bytes·check-changeset-presence·check-changeset-no-major·check-changeset-fixed·check-changeset-overwrite·check-phantom-dependencies·check-spec-symbol-derivation·check-package-self-import— all green, each read from its own printed verdict line.eslint .inpackages/app-shell(exactly the jobturbo run lintruns for this package): 989 files, 0 errors, 2739 pre-existing warnings, and 0 errors / 0 warnings on all three changed files. The 989 is eslint's own resolution fromeslint.config.jsand the count is read from--format json, not estimated. The other 38 packages were not linted locally:eslint.config.jssets noparserOptions.project/projectService, so no rule reads cross-file type information and this diff cannot move a verdict in a package it does not touch. CI runs the farm.A
patchchangeset is included: the type convergence is internal, but two rendering paths changed, and under-declaring is the direction that cannot be corrected later.Generated by Claude Code
Generated by Claude Code