Part of #57. Revisits the rejected core of proposals #2/#3 (see spike #59) — but as a deliberate, opt-in, multi-framework capability, not a default that weakens the gate.
Goal
Speed up the pre-push capture by re-capturing only the surfaces a change can affect, and reusing the committed base maps for the rest — without ever silently missing a regression. Build a dependency graph that, given the changed files in a commit, returns the set of surfaces whose rendered output could have changed (or all). Support the major frameworks; start with React (the hardest).
The hard constraint (why this is subtle, not a quick win)
In the committed-map model, a wrong answer is silent and fatal: if the graph says a surface is unaffected and it actually changed, its stale committed map matches the base, the diff is empty, and the regression ships green. So the graph must over-approximate — when in doubt, re-capture. It is only sound if:
- Any non-page-local style change forces
all — a CSS reset, a :root/theme token, a global stylesheet, a CSS-in-JS global, a Tailwind/theme config change, a design-system primitive. These cascade everywhere.
- The component→surface graph is complete for anything it claims is unaffected — including dynamic
import(), React.lazy, context/providers, render props, children, and component registries/CMS-driven composition. A missed edge = a missed regression.
- Runtime theming / data-driven styling that isn't visible in the static graph is treated conservatively.
For most PRs (which touch shared CSS/tokens) the sound answer is often all, so the realistic win is on changes provably scoped to a single surface's own component + styles.
Proposed shape
- Opt-in, default off. The full-coverage gate stays the default and the source of truth.
- Pluggable framework adapters. A small interface
affectedSurfaces(changedFiles, surfaces) → Set<key> | 'all'; ship a React/TS adapter first (lean on an existing module-graph tool — dependency-cruiser/madge — rather than reinventing it), then others.
- Conservative core, framework-agnostic. The "global change → all" classifier and the committed-map plumbing (capture subset, reuse committed base for the rest) live in core; only the import-graph resolution is per-framework.
- Trust-but-verify safety net.
main (or a scheduled run) always captures all surfaces, so any graph hole is caught at the latest on merge, not hidden forever. Document that a PR-time miss can still slip to merge — the residual risk the adopter accepts.
- Loud about what it skipped. Log every surface it chose not to re-capture and why, so a reviewer can sanity-check.
Open questions to resolve in the spike
- Can "is this style change global?" be classified soundly across CSS Modules / styled-components / Tailwind / vanilla / CSS-in-JS? If not reliably, the conservative default is
all on any CSS/config change — does that leave enough win?
- How complete can a static React graph be against dynamic composition, and how do we fail closed (→
all) when resolution is uncertain?
- Does the trust-but-verify-on-main safety net make PR-time selection an acceptable risk, or is consumer-declared mapping (the maintainer asserts completeness) the only honest opt-in?
- Per-framework adapter API + which existing graph tool to build on.
Acceptance
A design (and a React/TS proof-of-concept) that is opt-in, conservative (fails to all), framework-pluggable, and loud about skips, with the trust-but-verify net — or a documented decision that selective remap stays consumer-declared. The default gate must remain 100%-sound regardless.
Context: as of 3.0.0 the gate ships full coverage — every surface captured, changes determined by measuring the map (the map is the oracle), not by guessing. This issue is the path to making it faster on large apps without giving that up.
Part of #57. Revisits the rejected core of proposals #2/#3 (see spike #59) — but as a deliberate, opt-in, multi-framework capability, not a default that weakens the gate.
Goal
Speed up the pre-push capture by re-capturing only the surfaces a change can affect, and reusing the committed base maps for the rest — without ever silently missing a regression. Build a dependency graph that, given the changed files in a commit, returns the set of surfaces whose rendered output could have changed (or
all). Support the major frameworks; start with React (the hardest).The hard constraint (why this is subtle, not a quick win)
In the committed-map model, a wrong answer is silent and fatal: if the graph says a surface is unaffected and it actually changed, its stale committed map matches the base, the diff is empty, and the regression ships green. So the graph must over-approximate — when in doubt, re-capture. It is only sound if:
all— a CSS reset, a:root/theme token, a global stylesheet, a CSS-in-JS global, a Tailwind/theme config change, a design-system primitive. These cascade everywhere.import(),React.lazy, context/providers, render props,children, and component registries/CMS-driven composition. A missed edge = a missed regression.For most PRs (which touch shared CSS/tokens) the sound answer is often
all, so the realistic win is on changes provably scoped to a single surface's own component + styles.Proposed shape
affectedSurfaces(changedFiles, surfaces) → Set<key> | 'all'; ship a React/TS adapter first (lean on an existing module-graph tool —dependency-cruiser/madge— rather than reinventing it), then others.main(or a scheduled run) always captures all surfaces, so any graph hole is caught at the latest on merge, not hidden forever. Document that a PR-time miss can still slip to merge — the residual risk the adopter accepts.Open questions to resolve in the spike
allon any CSS/config change — does that leave enough win?all) when resolution is uncertain?Acceptance
A design (and a React/TS proof-of-concept) that is opt-in, conservative (fails to
all), framework-pluggable, and loud about skips, with the trust-but-verify net — or a documented decision that selective remap stays consumer-declared. The default gate must remain 100%-sound regardless.