Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-detail): let buildConflict consume the narrowing it is handed - #6585
Conversation
InlineEditSaveBar narrows correctly at the call site and then handed the narrowed value to a callback typed `err: any`, which discards it. The predicate's return type was inert at its only in-repo consumer. Measured first: buildConflict reads exactly two things off `err` -- currentRecord and currentVersion -- and the narrowed type declares both, at types ConcurrentUpdateConflict accepts unchanged. Nothing forced the `any`, so the parameter now takes the predicate's narrowed type and the `as Record<string, unknown> | null` cast it made necessary is gone. The type is DERIVED from the predicate rather than restated, so a future narrowing that stops covering a read is a red build instead of a fresh `any`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
Type-only change: emitted JS and the published dist/index.d.ts are byte-identical before and after, so an empty frontmatter is the honest declaration rather than a workaround. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
os-support-ai
commented
Aug 26, 2026
Reviewer note on one thing this PR deliberately did not change. With I left it. Removing Flagging it rather than leaving a reviewer to wonder why it is still there. Happy to drop the Generated by Claude Code Generated by Claude Code |
✅ 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
|
os-support-ai
commented
Aug 26, 2026
ACCEPT on the diff. ⏸ Landing held until every check is green — zero failures, 10 still running at Both ⛔ exclusions verified from the file listThree files: the changeset, The answer chosen, and why the mechanism is better than what was asked forThe dispatch authorised both outcomes and required the measurement to pick. It picked take the narrowed type, on the grounds that What was not asked for is how the coupling was made: :86typeNarrowedBy<F>=Fextends(arg: unknown)=>arg is infer N ? N : never;
:87typeConcurrentUpdateErrorShape=NarrowedBy<typeofisConcurrentUpdateError>;
:97err: ConcurrentUpdateErrorShape,
:157constbuildConflict=React.useCallback<BuildConflict>(The parameter type is inferred from the predicate's own signature rather than restated from it, so the two cannot drift apart. Restating the shape would have re-created a smaller version of the very defect this card is about — two declarations of one thing, free to disagree. This closes it structurally instead. The The ablation that decides itType-level claims are the hard case, because a green
That is the proof the change converts a silent hole into a compile error, rather than merely being green. The no-rebuild question was established rather than waved past — The empty changeset is earned, not assumedPublished surface unchanged, measured: building the package before and after leaves 51 of 52 dist artefacts byte-identical, including The deliberate non-change is the right call and was flagged rather than taken. Three corrections offered, all accepted
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6477
InlineEditSaveBarnarrows correctly at the call site —if (isConcurrentUpdateError(err) && canAtomic)— and then handed the narrowed value to a callback typederr: any, which discards it. The predicate's return type was inert at its only in-repo consumer, one line after the boundary was drawn.Step 0: the premise is live
The card was blocked behind #6421, whose PR #6474 landed on this very predicate, so the first question was whether that merge had already fixed the dependant. It had not. On
origin/mainat12402a9b8:The question was decided by measurement, not preference
Both answers were authorised: take the narrowed type, or keep the
anyand write down what forces it.buildConflictreads exactly two things offerr, and the narrowed type declares both, at typesConcurrentUpdateConflictaccepts unchanged:err?.currentRecordcurrentRecord?: Record<string, unknown> | nullcurrentRecord?: Record<string, unknown> | nullerr?.currentVersioncurrentVersion?: stringcurrentVersion?: stringNothing forced the
any, so option one was selected. As the card predicted, theas Record<string, unknown> | nullcast then becomes redundant and is deleted — the two shapes agree, so there was no finding to report on that front.exactOptionalPropertyTypesis off repo-wide, sostring | undefinedflowing intocurrentVersion?: stringneeds no ceremony.The type is derived from the predicate rather than restated:
A hand-copied literal shape would be a second declaration free to drift from the predicate — precisely the disagreement this card exists to rule out. Derived, the compiler re-checks every read against whatever the predicate currently promises.
isConcurrentUpdateErroris untouchedPR #6474 deliberately made that narrowed type honest (
code?: 'CONCURRENT_UPDATE', optional because thenamelimb carries nocode). Nothing here widens or otherwise edits it — this PR only changes a module-localReact.useCallbackparameter.The published surface is unchanged, and that is measured rather than asserted. Building the package before and after and comparing all 52 emitted
.js/.cjs/.d.tsartefacts leaves 51 byte-identical, includingdist/index.js,dist/index.umd.cjsanddist/index.d.ts. The one delta isdist/InlineEditSaveBar.d.ts, which gains theBuildConflicttype the pin test reads. The package'sexportsmap declares only"."(no wildcard subpaths), so that file is unresolvable by any consumer, andsrc/index.tsxlists published names explicitly and does not re-export it —grep -c BuildConflict dist/index.d.tsis0. Hence the changeset with empty frontmatter: this releases nothing.The pins can fail — shown, not claimed
"Type-check is green" is not coverage, so both failure modes were ablated. Each mutation was confirmed on disk before measuring, and each restore confirmed byte-identical to
HEADviagit hash-object.Ablation A — the parameter goes back to
any. Mutating the declared error parameter toany:Those are
_ErrParamIsNotAnyand_ErrParamIsTheNarrowedType, the two DISAGREES pins.Ablation B — the narrowed type stops covering a read. Deleting
currentVersion?: stringfrom the predicate's narrowed type turns the source itself red:Under
err: anyboth reads compiled silently and would have beenundefinedat runtime. That contrast is the whole point of the card.The pin file is really in the compiler's program —
tsc -p tsconfig.test.json --listFileslists both it andsrc/InlineEditSaveBar.tsx(as source, not via a built.d.ts), so "type-check is clean" is a statement about these assertions and not a vacuous pass.Known residual, stated rather than papered over: the compile-time pins bite on the exported
BuildConflicttype, which the component applies viaReact.useCallback<BuildConflict>. A revert that deleted the generic and re-annotated the lambdaerr: anywould leaveBuildConflictcorrect but unused and this file green. Nothing reachable from a test can observe a callback local to a component body; the runtime block covers that flank by driving the real component through a real 409.The runtime half also closes a genuine gap: the existing suite covers the
codelimb only, so nothing previously proved a code-less (name-limb) error reachesbuildConflictat all. The new test asserts both reads arrive in the dialog — the racer's value fromcurrentRecord, andcurrentVersionin the audit line.Gates, each quoting its own verdict, on
a31b806b0pnpm --filter @object-ui/plugin-detail type-checkcommand-exit 0(bothtsc --noEmitandtsc -p tsconfig.test.json)vitest run— 4 affected files, ona31b806b0command-exit 0·Test Files 4 passed (4)·Tests 17 passed (17)vitest run packages/plugin-detail/— whole package, on14bf45c12command-exit 0·Test Files 111 passed (111)·Tests 1036 passed (1036)pnpm --filter @object-ui/plugin-detail lintcommand-exit 0— 0 errors, 861 warnings (pre-existing baseline)node scripts/check-changeset-presence.mjsnode scripts/check-changeset-fixed.mjsnode scripts/check-changeset-no-major.mjsmajorbump"node scripts/check-control-bytes.mjsnode scripts/check-vi-mock-specifiers.mjsnode scripts/check-lint-coverage.mjsnode scripts/check-type-check-coverage.mjstype-check" · "41/41 packages compile their tests"node scripts/check-readme-exports.mjspnpm build; all 297 unjudged self-imports are unbuilt sibling packages ("type entry not on disk"),plugin-detailandBuildConflictappear 0 times. CI builds everything.The whole-package vitest run is cited against
14bf45c12; the only later commit adds.changeset/*.md, which no test reads. The narrowed 4-file run above was re-run on the final head.Declared narrowing: repo-wide
eslint .is CI's run. Locally the affected package was linted with its ownlintscript — the same commandturbo run lintinvokes for it. Population comes from eslint's own config, not a guess:--format jsonreports 164 files linted, 0 errors. The verdict for untouched files cannot move, because type-aware linting is not configured (noproject/projectServiceineslint.config.js), so no file outside the diff can change judgement.Measured side effect worth recording: this deletes one
@typescript-eslint/no-explicit-anywarning fromInlineEditSaveBar.tsx(8 → 7 on the same file, measured both ways), which is exactly theerr: anythe card names.Generated by Claude Code