Skip to content

feat(core): declare overrideNotice on ActionDef, then narrow both param handlers' action?: any - #5644

Draft
os-sales wants to merge 3 commits into
mainfrom
claude/issue-5611-overridenotice-declaration
Draft

feat(core): declare overrideNotice on ActionDef, then narrow both param handlers' action?: any#5644
os-sales wants to merge 3 commits into
mainfrom
claude/issue-5611-overridenotice-declaration

Conversation

@os-sales

@os-salesos-sales commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5611

overrideNotice was produced, read, and declared nowhere. DeclaredActionsBar composes the dispatch as any and hands it to the runner through a dispatch as ActionDef cast (DeclaredActionsBar.tsx:305, :312); useConsoleActionRuntime's param-collection handler took action?: any. The key crossed the entire producer/reader seam without one declaration — which is why warnOnUnknownActionKeys told the author, in dev, that a key "no reader recognizes" was present, on every privileged-override dispatch, about a key two files actually read. Fixing that is restoring an invariant, not adding a feature.

Three commits, in the order the pin requires.

Note: GitHub's body sanitizer strips angle-bracket placeholders, so metavariables below are written in CAPS (_actions.NAME.description, git checkout BASE -- PATH) rather than in angle brackets.

1. Fresh diagnostics re-measured on the current tip, before anything was written

The card's three TS2339s were measured at 7a28e1e3f, before #5610 landed, and triage flagged them as stale in the direction that matters. Re-run on f1c27f037 by applying only the narrowing (no declaration), then reverting:

src/hooks/useConsoleActionRuntime.tsx(253,45): error TS2339: Property 'overrideNotice' does not exist on type 'ActionDef'.
src/hooks/useConsoleActionRuntime.tsx(253,83): error TS2339: Property 'overrideNotice' does not exist on type 'ActionDef'.
src/hooks/useConsoleActionRuntime.tsx(254,18): error TS2339: Property 'overrideNotice' does not exist on type 'ActionDef'.

overrideNotice-only, no fourth key ⇒ the grading is confirmed and nothing comes back to triage.

Details worth stating rather than assuming:

  • The probe was measured on both tsconfig projects. type-check is tsc --noEmit && tsc -p tsconfig.test.json, and the && short-circuits — the first run failing means the test project never ran. Measured separately, tsconfig.test.json yields the same three diagnostics and no test-call-site errors, so the list above is complete rather than truncated by a short-circuit.
  • Zero diagnostics on RecordDetailView.tsx. Its handler reads only objectName / params / name / label / description, all declared. The narrowing there is free — RecordDetailView carries a second copy of the param dialog's dead action?.title fallback #5610 already removed the dead title limb.
  • Baseline control: the unmodified tree type-checks green (exit 0), so the three errors are attributable to the narrowing and not to pre-existing debt.

2. Declaration + inventory, in one commit (e52146ddf)

actionKeys.pin.test.ts re-derives ACTION_DEF_KEYS from the interface's AST, so either half alone turns the pin red. Both move together.

Documented at the declaration as objectui dialect with no spec counterpart, and dialect in a stronger sense than description: that key at least derives from @object-ui/types' renderer view of an action, while this one is not authorable metadata at all — a host composes it in code. Hand-typed string precisely because there is no spec field to derive from. #5178's ruling that it must not be folded into description is restated there, since that is what makes a separate key correct rather than redundant: the reader resolves description through the _actions.NAME.description bundle key and prefers a bundle hit over the passed literal, and plugin-approvals ships exactly such an entry for approval_reject — so a safety notice routed through description would be silently replaced by ordinary "Reject this request?" copy in every locale that has the bundle.

This widens nothing. The key is already accepted at runtime through the dispatch as ActionDef cast; the declaration only makes the type layer say what already happens.

3. The narrowing (d787b6dfe)

Line numbers re-derived by code on the current tip rather than taken from the card — both unmoved: useConsoleActionRuntime.tsx:196 and RecordDetailView.tsx:500. ActionDef was already imported by both files, so no new import and no barrel export; it is one token per handler.

⛔ The rejected alternative stays rejected: casting the overrideNotice read at its use site would swap a visible any for an invisible cast and re-hide the undeclared key. Turned down on #4282, not rediscovered here.

Reverse verification — two controls, each with its own mutation leg, each measured failing

legmutationrebuilt?mutation proven on diskresult
1delete overrideNotice?: string; from ActionDefyes — @object-ui/core rebuilt, and the marker proven absent from dist/actions/ActionRunner.d.ts before reading anythingsource count 1 → 0; git diff --numstat = 0 1app-shell type-check exit 1, exactly the three TS2339s return
2delete only'overrideNotice' from ACTION_DEF_KEYSn/a — this pin reads source (TS AST + relative import), no dist involved; stated rather than skipped silentlysource count 1 → 0; git diff --numstat = 0 1pin fails by name: expected { missing: [ 'overrideNotice' ] } to deeply equal { missing: [], stale: [] } — 1 failed, 11 passed

Leg 1's rebuild is the load-bearing step: without it the ablation reads the pre-mutation .d.ts and reports "removing the declaration changed nothing", which is the false conclusion in the more dangerous direction. Both legs restore from a shell trap on EXIT INT TERM, and the restore was verified by re-deriving the anchor counts afterwards.

Changeset

check-changeset-presence.mjs is the authority. Before: ❌ 4 source file(s) of 2 released package(s) changed, and this change adds no changeset. After:

✅ 4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s): .changeset/action-override-notice-declaration-5611.md.

minor, never major (fixed group). This adds a declared field to a published interface, so the .d.ts does move — measured at the real dist/ path, both legs clean-built after deleting dist/andpackages/core/tsconfig.tsbuildinfo (verified to live outsidedist/ for this package, so clearing dist/ alone would have let tsc skip emit):

packages/core/dist/actions/ActionRunner.d.tsbase f1c27f037head
bytes3913641143 (+2007)
lines796828 (+32)
sha25681a74ca5…64daa795…
gzip (via stdin, so no filename in the header)1378314456 (+673)
overrideNotice?: string; lines01
whole dist/ tree hashfe020e37…12c18e85…

Compared by hash, not byte count. The head leg was measured again after the base leg and came back byte-identical to the pre-ablation head build, which is what rules out a stale artefact being read — and that check earned its keep: it caught a restore that had silently failed, because git checkout BASE -- PATHstages the base content, so the follow-up git checkout -- PATH restored from the index rather than from HEAD. Re-restored from HEAD and re-measured before any figure above was recorded.

Verification

All at final commit 8238b453d, and the union was re-run after the last commit.

  • @object-ui/core — build exit 0; type-check exit 0; vitest run packages/core/src/actions/Test Files 23 passed (23) / Tests 407 passed (407) (includes actionKeys.pin.test.ts and actionDef-closed-surface.test.ts)
  • @object-ui/app-shelltype-check exit 0 (both tsc projects ran, no short-circuit); vitest run packages/app-shell/src/hooks/__tests__/ packages/app-shell/src/views/__tests__/Test Files 40 passed (40) / Tests 393 passed (393)
  • All runs from the repo root — package-cwd vitest is refused by the repo's own guard (pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378).
  • Gates: check-action-forward-parity ✅ (owed sets unchanged — this key is in neither the spec vocabulary nor ui-action.ts, so it joins no surface's owed set), check-control-bytes ✅, check-type-check-coverage ✅, check-lint-coverage ✅ (46/46 packages linted, 0 with outstanding errors), check-changeset-presence / -no-major / -fixed

pnpm lint is a repo-wide farm CI owns; this is a declared narrowing to the changed files, with the evidence that the narrowing excluded nothing: (1) type-aware linting is not enabled — no projectService, parserOptions.project or recommendedTypeChecked anywhere in the config — so no rule reads cross-file type information and this diff cannot move the verdict of a file it does not touch; (2) file count read from --format json, not asserted: 4 files linted, 0 errors, 170 warnings; (3) every warning is a pre-existing @typescript-eslint/no-explicit-any, and zero fall on any added line (checked against the added ranges). The narrowing in fact removes two anys.

Known-broken gauges, noted and not touched: check-eager-closure-budget exits 2, check-doc-snippet-types exits 1.

Out of scope, filed not fixed

#5642 — the same warning's dev-console message states a fact step 3 retired ("ActionDef still carries [key: string]: any") and points the author at actionKeys.ts to add a field to an interface that lives in ActionRunner.ts. That second half has teeth: following it edits only the inventory, which is exactly the half-change the pin goes red on. Different defect class from this card (shipped prose vs. a missing declaration), so it is filed unassigned rather than folded in here.


Generated by Claude Code

os-salesand others added 3 commits August 21, 2026 22:14
… inventory
`overrideNotice` was produced, read, and declared nowhere. `DeclaredActionsBar`
composes the dispatch as `any` and hands it over through a `dispatch as
ActionDef` cast; `useConsoleActionRuntime`'s param-collection handler took
`action?: any`. The key therefore crossed the entire producer/reader seam
without one declaration, and `warnOnUnknownActionKeys` told the author, in dev,
that a key "no reader recognizes" was present — on every privileged-override
dispatch, about a key two files read.
Same shape objectstack#4075 step 3 promoted `description` out of, and for the
stated reason: authorable-or-host-composed, forwarded, read, and undeclared.
Declaring it widens nothing — the key is already accepted at runtime through
that cast; this only makes the type layer say so.
Steps 1 and 2 move together because `actionKeys.pin.test.ts` re-derives
`ACTION_DEF_KEYS` from the interface's AST: either half alone turns the pin red.
Documented at the declaration as objectui dialect with no spec counterpart, and
in a stronger sense than `description` — that key at least derives from
`@object-ui/types`' renderer view, while this one is not authorable metadata at
all. Hand-typed `string` precisely because there is no spec field to derive
FROM. objectui#5178's ruling that it must NOT be folded into `description` is
restated at the declaration, since that is what makes a separate key correct
rather than redundant.
Co-authored-by: Claude <noreply@anthropic.com>
… any` to `ActionDef`
The payoff of the declaration in the previous commit, and the half objectui#4282
measured and backed out rather than casting to compile. With `overrideNotice`
declared, the annotation is a one-token change on each handler — `ActionDef` was
already imported by both files, so no new import and no barrel export.
This is what makes the compiler cover these two files at all. Every other read in
both handlers (`objectName`, `params`, `name`, `label`, `description`) was already
a declared field; `overrideNotice` was the only key holding the `any` in place.
Line numbers re-derived on the current tip rather than taken from the card:
`useConsoleActionRuntime.tsx:196` and `RecordDetailView.tsx:500`, both unmoved.
The rejected alternative stays rejected: casting the `overrideNotice` read at its
use site would have swapped a visible `any` for an invisible cast and re-hidden
the undeclared key.
Co-authored-by: Claude <noreply@anthropic.com>
… narrowing
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3785.8 KB3867.2 KB
Main entry chunk (gzip)151.7 KB350 KB
Entry fileindex-CGHHcjjs.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 (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
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)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
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.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
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)507.00KB113.72KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)238.85KB60.13KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
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.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
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.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.65KB18.32KB
plugin-chatbot (index.js)181.41KB43.22KB
plugin-dashboard (index.js)128.33KB32.93KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.16KB60.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.50KB20.68KB
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)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
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)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.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-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

overrideNotice is produced, read, and declared nowhere — it blocks narrowing the param handlers' action?: any

1 participant

@os-sales