Repository navigation
The param dialog's action?.title fallback reads a key no author can write and no producer sets #4282
Description
Activity
Findings triage (first grade): HOLD — stays
finding.Premise verified this round on
origin/main@6d8231c: theaction?.label || action?.titlefallback inuseConsoleActionRuntime.tsxstill reads a key no schema declares and no renderer forwards — a dead right-hand side by the card's own four-source audit.Why held: dormant (unreachable via authored metadata, zero user impact), and the adjacent in-flight card is likely to moot it — #4046 (
pm:dispatched) deletesActionDef's index signature, after whichaction?.titleplausibly stops compiling and gets cleaned in that PR or its immediate follow-up. Restart condition: re-check when #4046's PR lands; if the read survives it, promote as a one-line cleanup then.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Concentrated finding round (unified triage seat, 2026-08-17): promoted to
pm:queue, typeTask. Dead-limb removal per the card's own (correct) preference: no declared or producedtitleexists anywhere on the action surface, so|| action?.titlecannot fire from authored metadata. Scope: delete the limb; narrowing the handler'saction?: anytoActionDefis the valuable half — attempt it in the same PR and report if it surfaces real type debt rather than forcing it. Premise check at claim: re-grep for any newtitleproducer onorigin/main. S / sonnet.
Generated by Claude Code
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 21, 2026 Claim. PM seat
domain:ui(seat post #5560), round 6. Sessionsession_012u2pRjcqAYtoEjgr3wwhnK. Branchclaude/issue-4282-action-title-fallback.pm:queue→pm:dispatched.Route ruled — dead limb out; the
anynarrowing is a probe, not a commitment. Delete|| action?.title, leavingtitle: action?.label, matching thedescriptionline directly below which reads exactly one key. Re-greporigin/mainfor any newtitleproducer first and report the count — if one has appeared since the card was filed, stop and say so rather than deleting a limb that now fires.On the second half: attempt narrowing
paramCollectionHandler'saction?: anytoActionDefin the same PR, but do not force it. If it surfaces real type debt beyond a line or two, back it out, keep the one-line deletion, and file the narrowing as a separate finding with the diagnostics you saw. A dead-limb removal that lands cleanly is worth more than a narrowing that drags unrelated files in.
Generated by Claude Code
{ "issue": 4282, "status": "done", "branch": "claude/issue-4282-action-title-fallback", "pr": "https://github.com/objectstack-ai/objectui/pull/5609", "premise_still_valid": true, "summary": "Deleted `|| action?.title` from useConsoleActionRuntime's param-collection dialog title (packages/app-shell/src/hooks/useConsoleActionRuntime.tsx:259), leaving `title: action?.label` — one key, matching the `description` line below it — plus a comment recording why, a 4-case pin test, and a patch changeset. Premise re-verified on origin/main @ 7a28e1e3f: NO new `title` producer has appeared since the card was filed. The `any` narrowing was attempted, type-checked, and BACKED OUT: it produces three TS2339s on `overrideNotice`, a key with a live producer (DeclaredActionsBar.tsx:305) and a live reader but no declaration anywhere — fixing it means declaring it on ActionDef in @object-ui/core plus moving the pinned ACTION_DEF_KEYS and its AST-derived pin test, i.e. another package, outside this card's fence. Filed as #5611. Also found and filed: RecordDetailView.tsx:539 carries a SECOND, near-identical paramCollectionHandler with the same dead limb and the same `action?: any` (#5610) — outside the hooks fence, and the dispatch flagged concurrent work in views/, so not touched.", "tests": "All gates on final commit 87015140a (= pushed head), exit codes captured before any pipe, each quoted by its own verdict line. PREMISE PROBES (every zero paired with a control): @objectstack/spec ActionSchema walked live at spec 17.0.0 via 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 (prose only, `titleFormat` doc comment :273), control description 4; crud.ts ActionSchema -> title 2 (retired-confirm prose :136, nested dialog.title :163 — neither top-level), control description 3; base.ts -> title 1 and it belongs to HTMLAttributes :599 not BaseSchema, control description 12; four action renderers action:button/icon/group/menu -> title 1/1/1/2 ALL prose in comments, controls label 14-16 and description 2-4 each. Repo-wide sweep found exactly 2 live reads (the one removed + RecordDetailView.tsx:539) and 2 prose mentions. GATES: `pnpm --filter @object-ui/app-shell type-check` EXIT=0 (output echoed `tsc --noEmit && tsc -p tsconfig.test.json`, so the filter matched — not a zero-match no-op); `lint` EXIT=0, verdict `✖ 2538 problems (0 errors, 2538 warnings)`; vitest from repo root, observable set 57 files EXIT=0 `Test Files 57 passed (57) / Tests 631 passed (631)`; transitive mounters 30 files EXIT=0 `Test Files 30 passed (30) / Tests 271 passed (271)`; check-control-bytes EXIT=0 `OK (scanned 4655 tracked text file(s); skipped 85 binary)`; check-changeset-presence EXIT=0 `2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)`; check-changeset-no-major EXIT=0 `No changeset declares a 'major' bump.`; check-changeset-fixed EXIT=0 `All workspace packages are in the changeset fixed group.`; check-type-check-coverage EXIT=0 `45/46 via type-check … 41/41 packages compile their tests`; check-lint-coverage EXIT=0 `46/46 packages linted, 0 with outstanding errors`; check-action-forward-parity EXIT=0 `5 surfaces checked against 40 runtime-read keys from 4 consumers` (the load-bearing one — it derives its owed set FROM what the runtime reads, so deleting a read shrinks that set); check-i18n-call-site-keys EXIT=0; check-i18n-en-drift EXIT=0 `No en value changed in this range.`; check-phantom-dependencies EXIT=0; check-package-self-import EXIT=0; check-spec-symbol-derivation EXIT=0; check-skills-paths EXIT=0. REVERSE-VERIFICATION of the new pin: mutated the fix away on disk under `trap '...' EXIT INT TERM`; mutation proved on disk in BOTH directions before reading anything — anchor `title: action?.label,` 1 -> 0, injected `action?.label || action?.title,` 0 -> 1, sha256 ba79c42c…3cd212 -> 774f6e82…4ddbd0 (differs, so not a no-op), git diff --stat non-empty. Direction observed matched the prediction: RED — `AssertionError: expected 'Should Not Win' to be undefined`, `Test Files 1 failed (1)`, `Tests 1 failed | 3 passed (4)`; the 3 that stayed green are by design (one is a legibility control that is green before and after, so a lone red reads unambiguously as 'the limb is back'). Trap fired, sha256 back to ba79c42c…3cd212, `git status --porcelain` empty. No build/dist is involved — this is a source-resolved React hook under vitest, not an exports-resolved dist ablation. DECLARED NARROWING: packages/app-shell has 483 test files and the full package suite does not fit the container's 10-minute foreground cap (one attempt was killed at the cap with exit 143; 57 files take 90s, so 483 projects to ~12-13 min). Ran a PROVABLE SUPERSET of the observable population instead — 87 files / 902 tests, all green: (1) population read from the code not guessed — every app-shell test naming useConsoleActionRuntime, every test naming paramDialogState or ActionParamDialog, every *.ratchet.test.ts, all of hooks/__tests__, plus every test importing any of the 6 modules that mount the hook (ConsoleShell, AppContent, PageView, ObjectView, FlowRunner, DeclaredActionsBar, RecordDetailView) so transitive mounters are covered; (2) counts are vitest's own output; (3) invariance — the change alters one expression whose only value flows into the param dialog's title prop, and no excluded file renders that dialog or reaches the hook by any import path. CI runs the full farm in 4 shards regardless. All heavy runs went through scripts/pm/os-verify-lock.sh; one exception to record honestly — the reverse-verification vitest run (single file, ~9s) was run outside the lock.", "open_questions": [ { "question": "RecordDetailView.tsx:539 carries a second, near-identical paramCollectionHandler with the SAME dead limb. It is outside this card's file fence (hooks/**) and the dispatch flagged a concurrent PR touching views/, so I filed it as #5610 rather than folding it in. Does the PM want it queued now as an immediate follow-up, or held until the views/ PR lands?", "options": [ "A — queue #5610 next round once the concurrent views/ PR has landed; it is a byte-identical mirror of what just landed here, and the pin test added by this PR is the template", "B — fold it into this PR by widening the fence now (rejected here: I cannot verify no other claim holds RecordDetailView.tsx, and the dispatch named views/ as contended)", "C — leave it as a dormant finding indefinitely (it is genuinely unreachable, same as this one was)" ], "recommendation": "A. Same defect class, mechanical fix, correct shape already pinned by this PR's evidence — but the fence exists because views/ is contended this round, so sequencing beats widening. It is cheap and the template now exists, so it should not sit as a finding." }, { "question": "PR body has a duplicated attribution footer — the platform's create path appended its own session-URL footer after the one I wrote. Should I edit it out?", "options": [ "A — leave it; cosmetic only", "B — edit the body to remove the duplicate" ], "recommendation": "A. The body already went through GitHub's HTML-entity encoding once (&& is stored as an entity); re-saving it risks double-encoding every code block for a purely cosmetic gain. Left alone deliberately, not overlooked." } ], "out_of_scope_findings": [ "filed as #5610: RecordDetailView.tsx:539 carries a second, near-identical paramCollectionHandler with the same dead `title: action?.label || action?.title` limb and the same `action?: any` — same defect class, outside the hooks/** fence, with the full reachability re-measurement and the suggested mirror fix", "filed as #5611: `overrideNotice` has a live producer (DeclaredActionsBar.tsx:305) and a live reader (useConsoleActionRuntime.tsx:253-254) but is declared on no action surface — it is the sole blocker for narrowing both param handlers' `action?: any` to ActionDef, carries the three real TS2339 diagnostics, and also makes warnOnUnknownActionKeys falsely report it in dev on every privileged-override dispatch" ] }
Generated by Claude Code
Found while implementing #4046 (deleting
ActionDef's index signature). Observation-class: dormant, nothing a user hits today. Filed rather than fixed — out of that card's scope.The read
packages/app-shell/src/hooks/useConsoleActionRuntime.tsx:209, titling the param-collection dialog:titleis not declared anywhere an action can carry it:@objectstack/spec'sActionSchema—SPEC_ACTION_KEYSinpackages/core/src/actions/actionKeys.tsis re-derived from the schema's own shape byactionKeys.pin.test.ts, and has notitle;@object-ui/types' renderer view (UIActionSchema,ui-action.ts), which is where the siblingdescriptionis declared (:521);@object-ui/types'crud.tsActionSchemaor itsBaseSchema;action:button,action:icon,action:group,action:menueach forwardlabelanddescription, none forwardstitle.So the right-hand side of that
||cannot be reached by authored metadata. It is live only for a host that hand-builds an action object in code and puts atitleon it, which nothing in this repo does.Why it stayed invisible, and why it still is
The parameter is
action?: any(paramCollectionHandler,:167), so the read type-checks regardless. That is also why #4046 did not surface it: closingActionDefcannot reach a binding typedany. Worth recording explicitly — the natural assumption after #4046 is "unknown action keys are now compile errors", and this is a class that is not.Disposition
Two readings, and the choice is a judgement call rather than a defect:
|| action?.title, leavingtitle: action?.label. Matches thedescriptionline directly below it, which reads exactly one key.titleexplicitly is a shape worth supporting, it should be declared onActionDeflikedescriptionnow is, and the handler'sanynarrowed toActionDefso the compiler covers this file at all.Slight preference for the first: no producer sets it, and a fallback that cannot fire is the "declared ≠ enforced" shape #4046 exists to reduce. But it is one token either way, and narrowing that
anyis the larger and more valuable half of option two.Anchors
Verified on
origin/main@4cb0562b5.packages/app-shell/src/hooks/useConsoleActionRuntime.tsx:167—action?: anypackages/app-shell/src/hooks/useConsoleActionRuntime.tsx:209-210— thetitle/descriptionpairpackages/types/src/ui-action.ts:521—description?: string, the declared siblingRefs #4046, #4192.
Generated by Claude Code