Repository navigation
finding(plugin-dashboard, plugin-list): the two packages #4422 left unswept still erase every declared prop — and the #4438 guard cannot see them #4528
Description
Activity
CLAIM — session_017Qqyix2QcnpUC9XeYVDzx3, branch
claude/issue-4528-forwardref-prop-erasure. Dispatching a dev agent now.PM ruling (delegated decision authority; maintainer veto window open — record objections here):
- Measure both survivors first — the card states plugin-list's case is by-inspection, not measured. Reproduce the probe for BOTH (keyof ComponentProps is string|number; the declared prop reads any) before touching either; if plugin-list's shape differs, treat it on its own measurement.
- Fix = remove the accidental index signature so the DECLARED interface is what the type system serves. This must be a type-only change: zero runtime diff, proven by the untouched runtime suites.
- Repo-wide type-check is the canary: run the FULL turbo type-check. In-repo consumers that passed accidental extra props get fixed in the same PR (each pinned); if any consumer measurably RELIED on arbitrary-prop passthrough as a feature, STOP and report — that inverts the fix shape.
- Grading, decided here so it does not wobble: the interfaces have always DECLARED these props; the index signature erased them from the resolved type. Restoring what the interface documents is a FIX to the published contract, not a contract break — grade MINOR for both packages with this reasoning quoted in the changeset (major is forbidden under the fixed group and not warranted: no documented capability is removed;
any-typed accidental passthrough was never the documented surface). If measurement shows a plausible EXTERNAL consumer pattern that the repo's own docs/README endorse relying on the passthrough, STOP and report for a re-rule instead of shipping. - Guard must prove it can fail: refactor(components): the action keys publish UIActionSchema, and every forwardRef renderer annotates its props (#4418, #4422) #4438's guard is scoped to packages/components/src and structurally cannot see either survivor. Extend it (or add per-package siblings following its exact pattern) to cover these two components, with the discrimination proof run against the pre-fix shape — the [P2] objectui: every package type-checks its tests or declares why not — and guards must prove they can fail (measured: 355 errors across 19 packages) #4040 convention.
- Pins: both packages' runtime suites byte-stable; ComponentProps probes as new type-level tests (expect the declared signature, not any); the chore(types): plugin-dashboard's tests compile — the #4040 program closes at 40/40 #4530-added
(id: string | null)annotation in DashboardRenderer.domProps.test.tsx may become redundant — leave it unless it errors.
Mutual exclusion: #4537 (armed) owns plugin-dashboard's DatasetWidget test files + tsconfig.test.json — do not touch those; DashboardRenderer.tsx/ListView.tsx are free. ⛔ In-flight surfaces off-limits: #4271 (metadata-client.ts, PermissionMatrixEditor, celAuthoring), #4527 (ConditionBuilder, ConditionalFormattingEditor, five inspectors), #4272 (fields src, ObjectGrid date cells, ObjectGantt), #4006 (fields/plugin-editor config). If the type-check canary demands a fix inside any of those files, STOP and report rather than editing them.
Generated by Claude Code
Generated by Claude Code
- added 4 commits that reference this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 10, 2026 - added a commit that references this issue
on Sep 17, 2026
Observation-class finding, surfaced while wiring
@object-ui/plugin-dashboard's tests intotscfor #4040 (closing tranche). Nothing a user meets today; no fix proposed here.#4422 measured this exact hole in
packages/componentsand closed it via PR #4438. That issue ended with an explicit remainder:This is that grep. Two survivors.
The two files
Sweep method — every non-test source under
packages/*/srcthat mentionsforwardRefand declares[key: string]: any: 18 files, of which 16 are thepackages/componentsrenderers that #4438 already fixed.Why #4438's fix does not cover them
#4438's remedy is precise, and worth restating because it is what these two are missing. It moved the index signature OFF the
forwardReftype argument and ONTO the render function's parameter annotation:The type argument is now index-signature-free, so
PropsWithoutReftakes its identity branch and declared props survive; the parameter annotation still carries the signature, so the spread still type-checks. Both packages here still put the signature INSIDE the type argument, so both halves of the erasure are open.Measured, in the direction #4422 did not state
#4422 measured the erasure the render function sees. There is a second one, at every CALL SITE, and it is the one this card is really about:
ForwardRefExoticComponent's public props also come throughPropsWithoutRef, so a consumer writing JSX gets no checking at all.Measured on
DashboardRendererwith a throwaway probe compiled by the newpackages/plugin-dashboard/tsconfig.test.json(deliberate errors, so the compiler prints the resolved types):Reading those four in order:
keyof React.ComponentProps< typeof DashboardRenderer >isstring | number— every named prop erased;React.ComponentProps< typeof DashboardRenderer >['onWidgetClick']isany;DashboardRendererProps['onWidgetClick']is still((widgetId: string | null) => void) | undefined.So the declaration is right and no consumer is held to it. The live consequence in this repo:
DashboardRenderer.domProps.test.tsxwroteonWidgetClick={(id) => selections.push(id)}andidwas implicitlyany— a prop typo or a wrong-arity handler at anyDashboardRendererJSX call site passes today.packages/plugin-list/src/ListView.tsxis NOT separately probed: it is asserted here by inspection to carry the identical two ingredients at the file:line above, not measured. Whoever sizes the fix should measure it the same way rather than trusting this paragraph.The guard exists and is package-scoped
packages/components/src/__tests__/forwardref-props-annotation.guard.test.tspins #4438's shape, and its own header says so:It resolves its scan root as
path.resolve(here, '..'), i.e.packages/components/src. So the ratchet that keeps the 16 fixed files fixed structurally cannot see these two, and would not have caught them arriving.Directions, not a recommendation
ListViewPropsis exported andDashboardRendererPropsis exported, so moving the signature off the type argument is a public-type change worth a compile of the consumers.DashboardRendereralready destructures 14 named props before spreading the rest, so its real surface is closer to hand thancomponents' was. Note finding(plugin-dashboard):MetricWidgetProps/MetricCardPropsdeclare no DOM passthrough, so theid/role/aria-*the spread accepts at runtime are a type error for a TS consumer #4426 is the same package's adjacent DOM-passthrough finding and would likely be answered by the same work.packages/*/srcrather than leaving one package's ratchet to protect a repo-wide pattern — otherwise the third survivor arrives the same way these two did.Whichever is chosen, (3) is what stops this recurring: a fix without it leaves the pattern legal everywhere outside
packages/components.Refs #4422, #4438, #4426, #4040.
Generated by Claude Code