fix(plugin-detail,plugin-form): let the declaration type the two surviving useRecordContext bindings - #9334
Conversation
…dContext bindings The `record:activity` renderer and the line-items panel each bound the whole record context through a type assertion, so every member read below them was `any` regardless of how those reads were written — which is why removing a narrower assertion at one read site changed nothing the compiler answers. Drop both assertions and let `RecordContextValue` do the typing, and pin the shape with a census keyed on the BINDING rather than on an identifier name, carrying compile-time controls that prove the declaration really resolves instead of degrading to `any`. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Measured while removing the whole-context assertion above it: dropping the `record?.recordId` assertion is TS2345 (`string | number` is not assignable to `string`), not a redundancy — `RecordContextValue.recordId` is declared wider than the parent id `buildMasterDetailEditBatch` accepts. It did nothing at all while the binding was `any`; it does real work now, and the next reader needs to know which. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
…ught, and declare the level `ReturnType` of an overloaded `readdirSync` resolves to the LAST overload (the Buffer one), and `ReturnType` of a generic hook does not instantiate its type parameters the way a bare call site does — both surfaced as `tsc` errors in the pin itself rather than as a quietly passing assertion, which is what the compile-time half is there to do. The "not any" clause is now written first and separately, since a plain `extends` check is satisfied by `any`. The changeset declares no release: every emitted `.js`, `.css` and `.d.ts` of both touched packages is byte-identical to the base tree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
✅ 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
|
Contract review — PR objectui#9334 @ head
|
| check | reading |
|---|---|
CI at 61d8ea70d |
36/36 settled — 0 failing, 0 pending |
--pair 9334 |
✓ both carriers agree |
| both whole-context assertions gone | ✓ verified by content: useRecordContext() as any 1 → 0 in record-activity.tsx, and the LineItemsPanel binding is now bare |
| the inner cast handled, not deleted | ✓ kept, with the measurement written at the site |
| empty changeset | ✓ and the level is a measurement, not a judgement |
⛔ the five packages/fields sites untouched |
✓ |
| No-Touch zone | ✓ not touched |
⭐⭐ The inner cast is the finding, and you measured it instead of guessing
Triage named LineItemsPanel.tsx:98 and said it would turn out to be either pure redundancy or a real mismatch. It is the second, and you have the diagnostic:
TS2345: Argument of type 'string | number' is not assignable to parameter of type 'string'
because RecordContextValue.recordId is declared string | number | null | undefined while buildMasterDetailEditBatch takes a string parent id.
⭐ And the comment you left says the thing a later reader most needs and would never reconstruct: the assertion only BECAME load-bearing when the outer one was removed. While the binding was any it did nothing at all. ⇒ a future seat reading "a cast on a typed binding" will not delete it as leftovers.
⛔ And you did not put the outer cast back to make it green — which was the explicit trap in the dispatch. The residue is objectui#9333 instead, correctly, because both repairs move runtime bytes for a numeric primary key (String() coercion changes the id sent; widening masterDetailTx's parameter is that module's contract) and this card is type-side with no runtime effect.
⭐⭐ "No release" is a measurement, and that is rare
172 dist files, of which 171 are byte-identical by sha256; every emitted
.js,.cssand.d.tsis among them. The single difference is one declaration sourcemap, whosemappingsshift because an explanatory comment was added above an unchanged statement.
⇒ the changeset level is derived from the built artefacts, not argued from "it's only types". ⭐ And the reasoning behind it is the right one — a type assertion erases, and both renderers export an explicitly annotated React.FC, so nothing inferred reaches the emitted declarations. ⛔ Most no-release claims in this repo are assertions; this one is a reading.
⭐ The ablation was not a formality, exactly as the dispatch demanded
The risk on a pure cast-removal card is a pin that stays green whether or not the cast is there. Yours reddens: one as any restored in record-activity.tsx only, mutation proved on disk before any result was read (fixed-form 1→0, asserted-form 0→1, grep-printed at line 99), pin red naming that one file, and the other 8 controls stayed green ⇒ the redness is the census, not a broken control. Restore by blob hash (9d7d23b07a39… == HEAD blob) plus a clean git diff HEAD.
⭐⭐ Two controls proving resolution is real — and two that fired on their own author
- A misspelled member on the repaired binding ⇒
TS2551: Property 'objectNam' does not exist on type 'RecordContextValue'. ⇒ the types resolve; the harness is not degrading them toany. - The pin's own
@ts-expect-errorcorrected ⇒TS2578: Unused '@ts-expect-error' directive. ⇒ the compile-time half can fire.
⭐ And the honest part: two clauses fired on you while you were writing them — ReturnType of an overloaded readdirSync picks the Buffer overload; ReturnType of a generic hook does not instantiate like a bare call site — and you repaired them rather than weakening them. ⛔ The easy move there is to loosen the assertion until it stops complaining, which would have left a pin that measures nothing.
The census keys on the binding
The re-run covered packages/ + apps/ + examples/ + e2e/ + scripts/ and returned exactly the two — ⭐ a scope, not a sample — and every member read off each is declared. That is the discriminant the card asked for, applied rather than quoted.
NOT MEASURED, correctly declared
The first lock attempt returned exit 99 queue-timeout behind another lane's app-shell suite ⇒ recorded as NOT MEASURED, re-acquired on the same slot, and the run that completed is the one reported (346 files / 3472 tests).
The two noted-not-filed items are correctly placed
The pin forbids a type assertion on a useRecordContext() binding but would not catch a non-null assertion — strictly narrower (it removes the | null, not the typing) and nothing in the tree uses it today. And check:test-path-roots' 384 unclassified roots is the gate reporting its own declared blind spot, untouched by this diff. ⛔ Neither is a card.
Carriers
Cleared in one stroke on both limbs — PR objectui#9334 and card objectui#9304 — each read back.
Generated by Claude Code
Fixes #9304
Two whole-context assertions on
useRecordContext()survived objectui#9197, and on therecord:activityrenderer they made that PR's own cast removal cosmetic: theconst ctxbinding one scope above was alreadyany, soctx?.dataSourcewasanywhether or not the read carried an assertion of its own. Both bindings now take their type fromRecordContextValue.The change
packages/plugin-detail/src/renderers/record-activity.tsxconst ctxbinding carried a whole-context assertion to ANYpackages/plugin-form/src/LineItemsPanel.tsxconst recordbinding carried the samePlus one new pin and one changeset. No other file moves.
The audit, re-run on this branch rather than taken from the card
⭐ Keyed on the BINDING, not on an identifier name — the card's methodological rule, adopted by triage as an acceptance condition, and the reason a cast list keyed on the name
ctxonce matched the real total by coincidence while overlapping on only six of eleven entries.Every
useRecordContext(...)call inpackages,apps,examples,e2eandscriptswas enumerated and classified. Exactly two were asserted, and they are the two the card names. The discriminant — is every member read off this binding declared onRecordContextValue? — holds for both:record-activity.tsxreadsobjectName,data(.id/._id),recordIdanddataSource. All declared.LineItemsPanel.tsxreadsobjectNameandrecordId. Both declared.⛔ The five
packages/fieldssites were not touched: they readSchemaRendererContext, a different declaration, and they are objectui#7912's face.⭐ What the inner
LineItemsPanelassertion turned out to be — a REAL type mismatch, not redundancyThe site the card did not name is the parent-id arm,
record?.recordIdasserted tostring | undefined. While the binding above it wasanythe assertion did nothing at all. Measured on this branch with the outer assertion gone and this one deleted:⇒ it is now load-bearing:
RecordContextValue.recordIdis declaredstring | number | null | undefined,buildMasterDetailEditBatchtakes astringparent id, and this assertion is the only thing that makes them meet. ⛔ It was not silently deleted, and ⛔ the outer assertion was not put back because of it. It is kept, with a comment at the site saying what it now carries and what it hides, because both repairs move runtime bytes for a numeric primary key — coercing withString()changes the id this panel sends, and widening the helper's parameter ismasterDetailTx's contract rather than this one's. Filed separately as objectui#9333.Instruments
Written before the code, so the "unmodified" arm is the real base tree.
packages/react/src/context/__tests__/useRecordContext.bindingCast.pin.test.ts, two halves:as anyrestored inrecord-activity.tsx, nothing elsegit diff HEADtscRED:error TS2551: Property 'objectNam' does not exist on type 'RecordContextValue'. Did you mean 'objectName'?tscRED:error TS2578: Unused '@ts-expect-error' directive.RecordContextValueis named by the checker with its two parameters, and a tag-shaped fragment does not survive this platform's body sanitiser even inside backticks.The last row is the answer to "could this harness pass while measuring nothing": it cannot, because the directive fails as unused the moment the checker stops refusing a member that is not there. Two of the compile-time clauses fired on their own author during authoring —
ReturnTypeof an overloadedreaddirSyncpicks the Buffer overload, andReturnTypeof a generic hook does not instantiate like a bare call site — and were repaired rather than weakened.⭐ The release level, measured rather than judged
The changeset declares no release (empty frontmatter, the explicit exemption
check-changeset-presence.mjsnames). A type assertion erases, and both renderers export an explicitly annotated component, so nothing inferred reaches the emitted declarations. Building@object-ui/plugin-detailand@object-ui/plugin-formfrom the base tree and from this one:The single difference is a declaration sourcemap whose mappings shift because a comment was added above an unchanged statement. ⇒ nothing a consumer can resolve, import or execute changes.
Verification union, and why it is what it is
@object-ui/reacthas 28 direct dependents,plugin-detail6,plugin-form8 —apps/siteandapps/consoleamong them, andapps/sitedoes declaretype-check. The roottsconfig.jsonpathsblock aliases onlytypes,core,react-runtime,sdui-parser,protocolandconsoleto source trees; none of the three changed packages is aliased, so every dependent compiles them through their published declarations — which the table above shows are byte-identical.packages/react's owndistis untouched: the only file added there lives under__tests__, which its build project excludes.Run green on this branch:
pnpm --filter @object-ui/plugin-detail --filter @object-ui/plugin-form --filter @object-ui/react run type-check... run lint(0 errors; the warning stream is pre-existing)pnpm exec vitest runon the pin file, from the repo rootcheck:control-bytes·check:test-path-roots·check:published-tsconfig-exclude·type-check:coverage·check:unreferenced-sources·check:phantom-deps·check:vi-mock-specifiers·check:lint-rule-coverage·check-changeset-fixed·check-changeset-no-major·check-changeset-presence·check-new-cross-file-line-citations(0 new) ·check:changeset-claims·check-governed-queue-guard --test(NOT GOVERNED)The three affected packages' full suites, from the repo root through the shared verify lock:
queue-timeout (exit 99)after nine minutes behind another lane'sapp-shellsuite. That is a not-measured reading rather than a red one; the numbers above come from the re-acquisition, which ran to completion.Sequencing
objectui#7912 / PR objectui#9310 is in flight on
SchemaRendererContextType. The boundary was confirmed rather than assumed: that PR's 53-file face was read and is disjoint from this one's four. Its nearest approach is a sibling file in the same directory — a different pin test next to this one — not a shared file. ⇒ no blocking relation.Acceptance notes
string | number | null | undefinedtostring; a numeric primary key is permitted byRecordContextValueand byDataSource.update, and it would reach the batch helper typed as something it is not. Needs a decision between coercing and widening, both of which move runtime bytes.| null, not the typing — and nothing in the tree uses it today. The successor that would touch this is the next card in this family, which reads the same pin.check:test-path-rootsreports 384 unclassified roots tree-wide. Pre-existing, unrelated to this diff, and the gate says so itself.Dedup boundary
MCP
search_issuesanswered with a rate-limit error for this session's identity. The duplicate check for objectui#9333 therefore ran over the repo-scoped REST open issues list, fully paginated (449 issues, five pages, page six empty), with objectui#9304 carried as a known-hit control that came back lit. ⛔ No text query over CLOSED cards was possible, so a closed duplicate would not have been seen.Drafted by an
os-devseat with Claude Code; sessionhttps://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ.Generated by Claude Code
Generated by Claude Code