Skip to content

fix(app-shell): title the param dialog from label alone - #5609

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-4282-action-title-fallback
Aug 21, 2026
Merged

fix(app-shell): title the param dialog from label alone#5609
os-sales merged 1 commit into
mainfrom
claude/issue-4282-action-title-fallback

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#4282

Deletes the || action?.title fallback from the console param-collection
dialog's title, leaving title: action?.label — one key, matching the
description line directly below it.

Premise re-verified before deleting anything

The card was filed against origin/main @ 4cb0562b5; re-audited here against
7a28e1e3f. Every zero is paired with a control probe on a term known to be
present, so a zero result is a measurement rather than a broken command.

Producer surfaceprobe: titlecontrolverdict
@objectstack/specActionSchema (walked live at spec 17.0.0, 44 keys)absentdescription present, label presentno producer
@object-ui/coreSPEC_ACTION_KEYS0 entriesdescription 2, label 2no producer
@object-ui/coreACTION_DEF_KEYS / the ActionDef interface0 fieldsdescription field presentno producer
@object-ui/typesui-action.ts1 hit, prose only (titleFormat in a doc comment, :273)description 4no producer
@object-ui/typescrud.tsActionSchema2 hits, both non-action: retired-confirm prose (:136) and the nested dialog.title (:163)description 3no producer
@object-ui/typesBaseSchema (base.ts)1 hit — and it belongs to HTMLAttributes (:599), not BaseSchemadescription 12no producer, and none inherited
action:button / action:icon / action:group / action:menu1 / 1 / 1 / 2 hits, all prose in commentslabel 14-16, description 2-4 eachnone forwards it

The spec walk is the pin test's own zod-internals walk, run against the
installed @objectstack/spec@17.0.0:

total keys : 44
probe title : false
control description: true
control label : true

Sweeping every ActionDef construction site in the workspace for a title: /
.title = assignment found no producer either. No new producer has appeared
since the card was filed
— the limb was still unreachable, and is now gone.

The any narrowing: attempted, measured, backed out

ParamCollectionHandler already types its second parameter as ActionDef, and
ActionDef is already imported by this file — so the narrowing needed no new
import and no barrel export. It was applied and type-checked. It surfaces real
type debt, in a different package:

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 is the opposite of title: it has a live producer
(DeclaredActionsBar.tsx:305 sets it on the dispatch, then casts to ActionDef)
and a live reader (this handler), but is declared on no action surface — exactly
the shape #5178 introduced and #4046 step 3 promoted description out of. The
correct fix is to declare it on ActionDef in @object-ui/core and move
ACTION_DEF_KEYS with it, since actionKeys.pin.test.ts re-derives that list
from the interface's AST. That is another package plus a pinned inventory and its
pin test, so the narrowing is backed out here and filed separately with these
diagnostics. Casting the overrideNotice read to make the narrowing compile was
rejected deliberately: it would swap a visible any for an invisible cast and
re-hide an undeclared key, which is the thing this card is about.

Evidence: a reachability argument, plus a pin that bites

useConsoleActionRuntime.paramDialogTitle.test.tsx pins the reader, not the
deletion — the handler's action parameter is still any, so nothing in the
compiler stops the alias being reinstated by the next reader who sees an untitled
dialog and reaches for a second key.

Reverse-verified by mutating the fix away on disk, with a
trap ... EXIT INT TERM restore. The mutation was proved on disk with anchored
counts in both directions before anything was read:

anchor 'title: action?.label,' count before : 1
removed 'title: action?.label,' after : 0
injected 'action?.label || action?.title,' : 1
sha256 before mutation : ba79c42c…3cd212
sha256 after mutation : 774f6e82…4ddbd0 (differs -> not a no-op)

Direction observed, as predicted: red, on the one test that pins the removal.

AssertionError: expected 'Should Not Win' to be undefined
Test Files 1 failed (1)
Tests 1 failed | 3 passed (4)
TRAP: restored packages/app-shell/src/hooks/useConsoleActionRuntime.tsx

The other three stayed green by design — the third is a legibility control
(label wins when both are present), green before and after, so a lone red on
test 2 reads unambiguously as "the limb is back". After the trap:
sha256 back to ba79c42c…3cd212, git status --porcelain empty.

Gates — all on 87015140a, the final commit

Exit codes captured before any pipe; each gate quoted by its own verdict line.

Gateexitits own verdict
@object-ui/app-shell type-check0tsc --noEmit && tsc -p tsconfig.test.json (script name echoed — not a zero-match filter)
@object-ui/app-shell lint0✖ 2538 problems (0 errors, 2538 warnings) — warnings pre-existing repo-wide
vitest run (observable set, 57 files)0Test Files 57 passed (57) · Tests 631 passed (631)
vitest run (transitive mounters, 30 files)0Test Files 30 passed (30) · Tests 271 passed (271)
check-control-bytes.mjs0OK (scanned 4655 tracked text file(s); skipped 85 binary)
check-changeset-presence.mjs02 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs0No changeset declares a 'major' bump.
check-changeset-fixed.mjs0All workspace packages are in the changeset fixed group.
check-type-check-coverage.mjs045/46 via type-check … 41/41 packages compile their tests
check-lint-coverage.mjs046/46 packages linted, 0 with outstanding errors
check-action-forward-parity.mjs05 surfaces checked against 40 runtime-read keys from 4 consumers
check-i18n-call-site-keys.mjs0Every in-scope call-site key resolves against the en pack (2918 keys)
check-i18n-en-drift.mjs0No en value changed in this range.
check-phantom-dependencies.mjs0Every in-scope import is declared by the package that publishes it.
check-package-self-import.mjs0No package names itself inside its own src/.
check-spec-symbol-derivation.mjs01289 files scanned against 4912 spec export names
check-skills-paths.mjs0OK (95/96 stated path(s) resolve across 18 guide file(s); 1 baselined)

check-action-forward-parity is the one worth naming: it derives its owed key
set from what the runtime reads, so deleting a read shrinks that derived set.
It is green at 40 runtime-read keys across 4 consumers.

Declared narrowing on the test run

packages/app-shell has 483 test files; the full package suite does not fit
inside this environment's 10-minute foreground cap (57 files took 90s, so 483
projects to roughly 12-13 minutes, and one attempt was already killed at the cap).
Instead of the whole package, the run covers a provable superset of everything
that can observe the change
— 87 files, 902 tests, all green:

  1. the population was read from the code, not guessed: every app-shell test that
    names useConsoleActionRuntime, every test that names paramDialogState or
    ActionParamDialog, every *.ratchet.test.ts, the whole hooks/__tests__
    directory, plus every test importing any of the six modules that mount the
    hook (ConsoleShell, AppContent, PageView, ObjectView, FlowRunner,
    DeclaredActionsBar, RecordDetailView) — the transitive mounters that would
    not show up in a grep for the hook's name;
  2. the counts are vitest's own: 57 passed (57) and 30 passed (30);
  3. invariance: the change alters one expression inside paramCollectionHandler,
    whose only value flows into the param dialog's title prop. A test outside
    that set neither renders the dialog nor reaches the hook by any import path,
    so no excluded file's verdict can move.

CI runs the full farm in 4 shards regardless.

Not touched, deliberately

Two things in the same defect class sit outside this card's file fence and are
filed separately rather than folded in:

  • RecordDetailView.tsx:539 carries a second, near-identical
    paramCollectionHandler with the same title: action?.label || action?.title
    and the same action?: any. Different directory, and another PR in this round
    is touching views/ — not fixed here.
  • the overrideNotice declaration that the narrowing needs, above.

A repo-wide sweep confirms those are the only two live reads; the two remaining
hits (action-menu.tsx:223, action-forward-parity.test.tsx:21) are prose in
comments, and both stay accurate for the RecordDetailView site until it is
cleaned up too.

Refs #4046, #4192, #5178.

Generated by Claude Code


Generated by Claude Code

…282)
`useConsoleActionRuntime`'s param-collection dialog read
`title: action?.label || action?.title`. The right-hand side could not be
reached by authored metadata: `title` is declared on no action surface in the
ecosystem — absent from `@objectstack/spec`'s `ActionSchema` (44 keys walked at
spec 17.0.0), from `@object-ui/core`'s `ActionDef` and its pinned
`ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS` inventories, and from `@object-ui/types`'
renderer view (`ui-action.ts`) and `crud.ts` `ActionSchema` / `BaseSchema` — and
none of the four action renderers (`action:button`, `action:icon`,
`action:group`, `action:menu`) forwards it. A fallback that cannot fire is the
"declared is not enforced" shape objectstack#4075 exists to reduce.
The line now reads exactly one key, matching the `description` line directly
below it. Nothing a user hits changes.
`useConsoleActionRuntime.paramDialogTitle.test.tsx` pins the reader rather than
the deletion: the handler's `action` parameter is still `any`, so nothing in the
compiler stops the alias being reinstated by the next reader who sees an
untitled dialog and reaches for a second key. An action carrying `title` and no
`label` must open an untitled dialog.
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3785.7 KB3867.2 KB
Main entry chunk (gzip)151.6 KB350 KB
Entry fileindex-DS3FQrNw.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)506.99KB113.73KB
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.72KB18.35KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.36KB32.95KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.15KB60.89KB
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)1.45KB0.83KB
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

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT (card #4282)

Gates. 22 named check runs read individually for completed + success at 87015140a: 19 success, 3 skippedTest (coverage), the unexpanded Test (coverage shard …/4) matrix placeholder, and dependabot. All four real shards, Type Check, Lint, Build & E2E, Build Docs, Bundle Analysis, Live E2E, both doc type checks and all three changeset checks green.

The probes are the deliverable here, and they hold up

Deleting a fallback is only safe if it truly cannot fire, and the evidence is a set of negative results — the failure mode being a search that returns zero because it was written wrong. Every zero here is paired with a control on a term known to be present, which is what turns them into measurements:

  • @objectstack/spec's ActionSchema walked live at spec 17.0.0 through the pin test's own zod-internals walk: total keys: 44 / probe title: false / control description: true / control label: true
  • SPEC_ACTION_KEYS + ACTION_DEF_KEYS: title 0, controls description 2 / label 2
  • ui-action.ts: title 1, and it is prose in a titleFormat doc comment, not a declaration
  • crud.ts: title 2, both non-top-level (retired-confirm prose, nested dialog.title)
  • base.ts: title 1, belonging to HTMLAttributes, not BaseSchema
  • all four action renderers: every title hit is prose in a comment; controls label 14–16 and description 2–4 each

Repo-wide that leaves exactly two live reads. One is the limb this PR removes. The other is RecordDetailView.tsx:539, which is the finding below.

The instruction I care most about was followed

The dispatch said to attempt the anyActionDef narrowing but not to force it. It produced three TS2339s on overrideNotice — a key with a live producer (DeclaredActionsBar.tsx:305) and a live reader but no declaration anywhere — and fixing that means declaring it in @object-ui/core and moving the pinned ACTION_DEF_KEYS with its AST-derived pin test, i.e. another package and another card. It was backed out, the one-line deletion kept, and the diagnostics filed as #5611.

That is the right call and the right shape of report: an attempt with a measured reason for stopping beats either forcing it through or never trying.

One note on which evidence actually bears weight

check-action-forward-parity derives its owed set from what the runtime reads, so deleting a read shrinks that set — meaning it is structurally guaranteed to pass under this diff and cannot detect an over-deletion. It is the right gate to run because it is the one most likely to be affected, but its green is not independent confirmation. The probes above are what carry the safety argument. Worth stating so nobody later reads that row as the proof.

Rulings on your two questions

#5610 — A, and the sequencing blocker has already cleared. Queue it as a follow-up rather than widening this fence. Note that the contention I named in your dispatch was packages/app-shell's barrels and views/metadata-admin/, held by #5597 and #5599 — both merged before you asked. So RecordDetailView.tsx is uncontended now and #5610 is eligible for the next batch immediately, with this PR's pin as the template.

Duplicate footer — A, leave it. Same ruling I gave on #5604 and for the same reason: the body has already been through entity encoding once, and re-saving it risks double-escaping the quoted gate output for a cosmetic gain.

Declared narrowing accepted. The full 483-file app-shell suite exceeds the container's foreground cap, and rather than quietly running less you built a provable superset of the observable population — 87 files, 902 tests — with the population read from the code (every test naming the hook, the dialog, the ratchets, plus every test importing any of the six modules that mount it). That is the correct way to narrow.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 21, 2026 17:38
@os-sales
os-sales added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 76ceb1eAug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-4282-action-title-fallback branch August 21, 2026 17:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The param dialog's action?.title fallback reads a key no author can write and no producer sets

2 participants

@os-sales@claude