You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Nothing in the repository measures what the transform cache does across repeated build lifecycles. Every gate the package has drives either exactly one build or none, so a defect that only appears at the second pass is invisible to all of them. That is why #1300 — a whole-project transform on every rebuild, on every host whose buildStart repeats — shipped through a suite that already asserts the one-compile invariant in eleven other ways.
Evidence
Corrected during implementation. The first version of this body said the perf scenarios drive exactly one build lifecycle. They drive two, a warm-up and a measured one. The blindness is real and in fact sharper than first described: their plugin runs == 1 held only because the second pass discarded the first pass's generation, so the harness was measuring the per-pass clear rather than gating against it, and no boundary that discarded a valid compile could ever have failed one of them.
Every beginTtscTransformBuild call site in the repository, and what it drives:
Call site
Passes driven
Why it cannot gate
perf scenarios A/B (measure)
2
the run log is truncated after the warm-up, so the assertion measures whether the second pass recompiled
The Vite lifecycle scenarios in tests/.../adapter-vite-lifecycle.ts do drive buildStart twice, but only to model overlapping plugin containers during a dev-server restart, and only under command: "serve". No case anywhere drives pass, pass, pass on one cache and asks how many times the project was compiled.
The perf harness's own README states its invariants as plugin runs == 1, stats/file, lstats/file, and bounded probes per module. All four are within-one-pass quantities, and the first of them is satisfied by the clear rather than despite it. There is no across-passes quantity, so no gate could observe a per-pass clear.
The suite is otherwise thorough about this cache: test_transformttsc_caches_one_compile_across_a_multi_file_project, ..._avoids_rehashing_the_project_for_each_first_module_delivery, ..._completeness_narrows_persistent_cache_validation, ..._complete_validation_proves_each_input_once, and the real-envelope cases all pin the one-compile invariant inside a pass. The dimension that was missing is the only one #1300 lives in.
The gate has to live in tests/test-unplugin, not only in experimental/unplugin-perf: the perf harness is not selected by scripts/ci/validation-plan.cjs for any lane and is a manual measurement. A change to packages/unplugin/** selects the bundler defenses lane, which runs @ttsc/test-unplugin and @ttsc/test-metro.
The counting instrument already exists and is trustworthy: the fixture producers in transform-project-cache.ts and real-native-envelope.ts append one byte per whole-project compile, so programRuns() is a real count of native host invocations rather than a proxy.
Approach
Add across-passes coverage in tests/test-unplugin/src/features/, one case per file as the testing contract requires, driving several lifecycles over one cache and asserting the compile count after each. The matrix the class needs:
Case
Passes
Expected compiles
repeated passes, nothing edited
3
1
a pass that edits a delivered module's own source
2
2
a pass that edits a type-only input the bundler erased from its graph
2
2
a pass that changes project membership (a file created or removed)
2
2
a pass that edits a file the generation never declared as an input
2
1
a module delivered twice inside one pass
1
still revalidates on the second delivery
Drive at least one of them through a real bundler watch session as well, not just the core API, so the mapping from a host's own rebuild signal to a pass boundary is covered end to end. tests/.../adapter-webpack.ts already has a working polling-watch harness for exactly this.
Add the matching across-passes scenario to experimental/unplugin-perf too, so the manual measurement reports the quantity its README claims to be about. Its guarded invariant should be compiles-across-passes, alongside the existing per-delivery budgets.
Acceptance and verification
Positive: each row of the matrix above holds.
Negative: the "nothing edited" row must fail against the pre-fix adapter, and the four "must recompile" rows must fail against an adapter that never invalidates. A gate that passes both ways is not a gate.
Depends on #1300 and the vite build --watch disposal issue: the positive rows only pass once the generation survives a pass boundary. Land in the same change.
Problem
Nothing in the repository measures what the transform cache does across repeated build lifecycles. Every gate the package has drives either exactly one build or none, so a defect that only appears at the second pass is invisible to all of them. That is why #1300 — a whole-project transform on every rebuild, on every host whose
buildStartrepeats — shipped through a suite that already asserts the one-compile invariant in eleven other ways.Evidence
Every
beginTtscTransformBuildcall site in the repository, and what it drives:measure)measureGraphBuild)tests/.../transform-project-cache.ts:1445, 1495, 3167tests/.../real-native-envelope.ts:379(assertCoreLifecycle)packages/unplugin/src/bun.ts:160-161The Vite lifecycle scenarios in
tests/.../adapter-vite-lifecycle.tsdo drivebuildStarttwice, but only to model overlapping plugin containers during a dev-server restart, and only undercommand: "serve". No case anywhere drives pass, pass, pass on one cache and asks how many times the project was compiled.The perf harness's own README states its invariants as
plugin runs == 1, stats/file, lstats/file, and bounded probes per module. All four are within-one-pass quantities, and the first of them is satisfied by the clear rather than despite it. There is no across-passes quantity, so no gate could observe a per-pass clear.The suite is otherwise thorough about this cache:
test_transformttsc_caches_one_compile_across_a_multi_file_project,..._avoids_rehashing_the_project_for_each_first_module_delivery,..._completeness_narrows_persistent_cache_validation,..._complete_validation_proves_each_input_once, and the real-envelope cases all pin the one-compile invariant inside a pass. The dimension that was missing is the only one #1300 lives in.Consequence surface
tests/test-unplugin, not only inexperimental/unplugin-perf: the perf harness is not selected byscripts/ci/validation-plan.cjsfor any lane and is a manual measurement. A change topackages/unplugin/**selects thebundler defenseslane, which runs@ttsc/test-unpluginand@ttsc/test-metro.transform-project-cache.tsandreal-native-envelope.tsappend one byte per whole-project compile, soprogramRuns()is a real count of native host invocations rather than a proxy.Approach
Add across-passes coverage in
tests/test-unplugin/src/features/, one case per file as the testing contract requires, driving several lifecycles over one cache and asserting the compile count after each. The matrix the class needs:Drive at least one of them through a real bundler watch session as well, not just the core API, so the mapping from a host's own rebuild signal to a pass boundary is covered end to end.
tests/.../adapter-webpack.tsalready has a working polling-watch harness for exactly this.Add the matching across-passes scenario to
experimental/unplugin-perftoo, so the manual measurement reports the quantity its README claims to be about. Its guarded invariant should be compiles-across-passes, alongside the existing per-delivery budgets.Acceptance and verification
pnpm --filter @ttsc/test-unplugin start -- --include=<case>.pnpm --filter @ttsc/test-unplugin start && pnpm --filter @ttsc/test-metro start.Coordination
Depends on #1300 and the
vite build --watchdisposal issue: the positive rows only pass once the generation survives a pass boundary. Land in the same change.