feat(prover): add whole-app React proof engine - #1471
Conversation
commit: |
|
React Doctor found 254 new issues in 157 files · 46 errors & 208 warnings · score 43 / 100 (Critical) · 0 fixed · vs Errors
208 warnings
158 more warnings not shown. Reviewed by React Doctor for commit |
Interactive terminal E2ERecorded from the built CLI at |
| propFlow.phase === phase && | ||
| propFlow.complete && | ||
| propFlow.callbackIds.length > 0 && | ||
| propFlow.callbackIds.every((callbackId) => callbackIds.includes(callbackId)), |
There was a problem hiding this comment.
React Doctor · react-doctor/js-set-map-lookups (warning)
This scales poorly because array.includes() inside a loop scans the whole list every time. Use a Set for constant-time lookups.
Fix → Use a Set or Map when you check for the same items over and over. Array.includes/find scans the whole list each time
| ); | ||
| } | ||
| if (hydration.status === ReactHydrationStatus.Unknown) { | ||
| const evidence = context.graph.hydrationRoots |
There was a problem hiding this comment.
React Doctor · react-doctor/js-combine-iterations (warning)
This loops over your list twice because .filter().map() makes two passes, so do it in one pass with .reduce() or a for...of loop
Fix → Combine .map().filter() style chains into one pass with .reduce() or a for...of loop, so you only loop over the list once
| const observer = resourceSymbol ? constructorsBySymbol.get(resourceSymbol) : null; | ||
| if (observer?.kind !== resourceKind) continue; | ||
| observer.activationCalls.push(callExpression); | ||
| if (!observers.includes(observer)) observers.push(observer); |
There was a problem hiding this comment.
React Doctor · react-doctor/js-set-map-lookups (warning)
This scales poorly because array.includes() inside a loop scans the whole list every time. Use a Set for constant-time lookups.
Fix → Use a Set or Map when you check for the same items over and over. Array.includes/find scans the whole list each time
| if (!listener) continue; | ||
| const ownerFunction = getEnclosingFunction(registrationCall); | ||
| const reachableOwner = ownerFunction | ||
| ? reachableFunctions.find( |
There was a problem hiding this comment.
React Doctor · react-doctor/js-index-maps (warning)
This gets slow as your list grows because array.find() runs inside a loop, so build a Map once before the loop for instant lookups
Fix → Build a Map once before the loop instead of calling array.find(...) inside it
| if (!observerActivation) continue; | ||
| const ownerFunction = getEnclosingFunction(observerActivation); | ||
| const reachableOwner = ownerFunction | ||
| ? reachableFunctions.find( |
There was a problem hiding this comment.
React Doctor · react-doctor/js-index-maps (warning)
This gets slow as your list grows because array.find() runs inside a loop, so build a Map once before the loop for instant lookups
Fix → Build a Map once before the loop instead of calling array.find(...) inside it
|
|
||
| useEffect(() => { | ||
| invokeTick(); | ||
| }, [invokeTick]); |
There was a problem hiding this comment.
React Doctor · react-doctor/exhaustive-deps (warning)
invokeTick is rebuilt every render, so useEffect runs every time.
Fix → Don't blindly add missing dependencies. Read the hook callback first.
Bad:
useEffect(() => {
setCount(count + 1);
}, [count]);
Better:
useEffect(() => {
setCount((currentCount) => currentCount + 1);
}, []);
If the missing value is recreated every render, move it inside the hook or stabilize it before adding it to deps.
|
|
||
| useEffect(() => { | ||
| invokeTick(); | ||
| }, [invokeTick]); |
There was a problem hiding this comment.
React Doctor · react-doctor/no-effect-with-fresh-deps (error)
Your useEffect runs every render because dep invokeTick is a new function built fresh each time, so === always fails.
Fix → Move the value inside the hook body and depend on its simple inputs instead, or wrap it in useMemo / useCallback so it stays the same between renders.
| const [enabled, setEnabled] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| setEnabled(!enabled); |
There was a problem hiding this comment.
React Doctor · react-doctor/no-self-updating-effect (warning)
setEnabled() updates enabled, which is also in this effect's dependency list. Guard the update or move the derivation out of the effect.
Fix → Break the loop: work the value out while rendering, set it in an event handler, or guard the update so it stops changing. See https://react.dev/learn/you-might-not-need-an-effect
| const [count, setCount] = useState(0); | ||
|
|
||
| useEffect(() => { | ||
| setCount(count + 1); |
There was a problem hiding this comment.
React Doctor · react-doctor/no-self-updating-effect (warning)
setCount() updates count, which is also in this effect's dependency list. Guard the update or move the derivation out of the effect.
Fix → Break the loop: work the value out while rendering, set it in an event handler, or guard the update so it stops changing. See https://react.dev/learn/you-might-not-need-an-effect
| const [count, setCount] = useState(0); | ||
|
|
||
| useEffect(() => { | ||
| setCount(count + 1); |
There was a problem hiding this comment.
React Doctor · react-doctor/rerender-functional-setstate (warning)
You can lose this update because setCount(count + ...) reads a stale value.
Fix → Use the callback form: setState(prev => prev + 1) to always read the latest value

What this adds
This PR adds a private
@react-doctor/proverpackage for whole-application React verification.It builds a TypeScript program, extracts React Compiler HIR/CFG facts, constructs a versioned semantic graph, evaluates 36 React proof obligations, and independently checks the resulting certificate.
proved: every discovered React unit satisfies every implemented obligation.refuted: the prover found a concrete source-level counterexample.incomplete: unsupported syntax, an opaque dependency, or an open runtime boundary prevented a proof.incompleteis a failed proof, not a pass.How it works
The semantic graph records component calls, renders, Hooks, callbacks, execution phases, state transitions, effects, resources, ReactNode slots, context providers, Suspense/Error Boundaries, hydration roots, memo comparators, and their cross-file relationships.
The checker does not trust the analyzer's final verdict. It recomputes graph references, reciprocal links, topology sources, protocol equations, every obligation status, report totals, and the application verdict.
Proof contracts added
1. Hook order, ownership, and component invocation
Hooks must have a valid React owner and execute in the same order on every render. Components must be rendered by React rather than called as ordinary functions.
Claims:
hook-order,hook-ownership,component-invocation,boundary-coverage.2. Render purity, refs, and component identity
Render must be deterministic and must not mutate props, state, refs, or external state. Component identity must not be recreated in render.
Claims:
render-purity,ref-access,component-identity.3. Effects, dependencies, cleanup, and scheduled work
The prover resolves captured reactive values, setup/cleanup alternatives, listener identity, timer handles, observer activations, and Strict Mode lifecycle repetition.
It refutes missing dependencies, mismatched listener identity/options, leaked timers or observers, render-time state cycles, and incomplete cleanup paths.
Claims:
effect-dependencies,effect-cleanup,effect-state-updates,scheduled-callback-lifetime.4. Async Effect ownership
Async completions must still belong to the Effect instance that writes their result.
Ungated post-
awaitor Promise-chain writes are refuted because an older request can overwrite newer state.Claim:
async-effect-ownership.5. Callback phases, Effect Events, and callable refs
Callbacks are tracked through local helpers, parameters, object properties, returns, JSX props, and synchronous higher-order calls. Each use is tied to its React phase.
Calling an Effect Event during render, exporting it, passing it to a child, or using a passively synchronized ref where commit freshness is required remains refuted or incomplete.
Claims:
effect-event-usage,callable-ref-freshness.6. Context and ReactNode topology
The prover distinguishes constructing a JSX value from actually rendering it. This prevents lexical JSX nesting from inventing provider, form, Suspense, Error Boundary, lifecycle, or hydration ancestry.
For effective renders, it follows direct children, named slots, aliases, fragments, portals, and project-local component edges while preserving provider/form/boundary frames.
Claims:
context-topology,react-node-flow.7. Reconciliation identity
List identity must follow the represented data rather than its current position.
Claim:
reconciliation-identity.8. Hook, reducer, and class state transitions
The prover resolves state/setter and reducer/dispatcher tuples, updater callbacks, execution roots, reducer totality, class lifecycle phases, direct mutations, and bounded update guards.
Direct
this.statemutation, impure updater callbacks, render-time dispatch, escaped setters/dispatchers, partial reducers, and unguardedcomponentDidUpdateloops are rejected.Claims:
hook-state-transitions,reducer-purity,reducer-transitions,class-construction,class-state-transitions.9. React 19 Actions, optimistic state, and forms
The graph models Transition Actions, Form Actions, Action State reducers, optimistic updates, and Form Status placement.
It checks that dispatches and optimistic writes occur inside modeled Actions, that Form Status has a real parent form, and that Transition work does not drive controlled inputs through an invalid deferred update.
Claims:
transition-actions,form-actions,action-state,optimistic-state,form-status.10. Imperative handles
The prover ties a
useImperativeHandlefactory to its ref, closed method set, dependencies, consumer binding, and invocation phase.Missing reactive dependencies, impure factories, open object spreads, shared refs, unresolved consumers, and stale methods do not receive a proof.
Claim:
imperative-handle.11. External stores
useSyncExternalStorerequires symmetric subscription cleanup, stable snapshots, and server/client snapshot equivalence during hydration.Fresh snapshot objects, missing cleanup, unresolved callback channels, and mismatched server snapshots are rejected.
Claim:
external-store-consistency.12. Lazy loading, Suspense, Error Boundaries, and
useThe prover follows lazy renders and Promise resources through component, helper, Hook, and ReactNode-slot edges to their catching boundaries.
Lazy declarations must have stable identity and valid loaders.
use(Promise)resources need stable cache identity, Suspense for pending state, and a valid Error Boundary for rejection.Claims:
lazy-suspense,error-boundary,use-resource.13. Controlled host elements
Intrinsic inputs, textareas, and selects are checked for stable controlled ownership and exact synchronous updates.
Known controlled/uncontrolled switches,
valueplusdefaultValue, absent/deferred/conditional writes, transformed values, and invalid file-input control are rejected.Claim:
host-control.14. Hydration equivalence
Source-visible server and client roots must resolve to the same tree, environment behavior, and
identifierPrefix.The prover refutes browser-global first-render branches, host-default locale formatting, unequal prefixes,
renderToStaticMarkuphydration, and hazards reached through children or custom Hooks. Framework-generated and dynamic roots remain incomplete.Claim:
hydration-equivalence.15.
React.memobailout equivalenceEvery path on which a custom comparator returns
truemust imply equality of every varying prop path observed by the component, including callbacks and nested values.The symbolic comparator model supports
===,!==,Object.is,&&,||,!, conditionals, immutable aliases, and early-return guards. It refutes omitted props, callbacks, nested paths, rest props, unsafe disjunctions, and comparisons of shared prototype methods instead of their rendered receivers. Opaque helpers and dynamic reads remain incomplete.Claims:
memo-equivalence,memo-dependencies.Certificate and schema
The independent checker rejects:
This revision uses report schema
30and semantic graph schema36.Corpus and validation
Runtime oracles reproduce selected failures in real React, including stale memo output, hydration recovery, reconciliation state transfer, listener leaks, Strict Mode replay, stale async writes, Suspense/Error Boundary behavior, and controlled-input ownership. Runtime evidence calibrates fixtures but never upgrades an incomplete static proof.
Scope
This package remains private at
0.0.0. It does not change the React Doctor CLI, score, config, public JSON report, GitHub Action, or telemetry, so no Changeset is included.A
provedresult covers the 36 implemented React contracts and the supported TypeScript/React subset. It does not prove application-specific business requirements, arbitrary third-party library behavior, framework-generated entrypoints, or unsupported syntax. Those remain explicitincompleteboundaries rather than implicit passes.The research basis and per-contract soundness ledger are in
packages/prover/research-log.md.