Found while breaking a two-module cycle in src/connectors/gst for #169.
What
src/background contains a runtime import cycle cluster spanning roughly seven modules, with about
15 distinct cycle paths:
download-observer
-> filed-returns-durable-download-reconciler
-> filed-returns-target-download-attempt
-> filed-returns-target-review
-> single-period-bundle-cleanup
-> single-period-zip
-> staged-zip
-> back into the cluster
These are value-level edges, not type-only.
Why it matters
Same latent shape as the cycle fixed in #169, with a larger blast radius. A cycle is safe only while
no module in it touches another's bindings at module top level. Add one top-level const derived
across the cycle and you get a TDZ crash under a real Node ESM load — and tsc will not catch it,
because it resolves like a bundler.
This is not theoretical for this repository: scripts/create-live-run-evidence-template.mjs loads
connector modules through a genuine Node ESM await import("….ts") under
--experimental-strip-types. Any script that ever reaches into src/background the same way would
be exposed.
Existing guard, and why it is scoped
#169 adds tests/connectors/filed-returns-module-graph.test.ts, which detects runtime cycles by
shape and names the offending path. It is deliberately scoped to src/connectors/gst because
pointing it at src fails immediately on this cluster.
Widening that guard to src/background is the natural completion of this issue — but the cluster
has to be broken first, or the guard has to start with a recorded, shrinking allowlist so it cannot
silently accept new cycles while the old ones are worked off.
Suggested approach
Prefer extracting shared leaf modules (the shape used in #169: move the vocabulary into a module
that imports nothing, then re-export so no caller changes) over adding lazy imports, which hide the
cycle rather than removing it.
Found while breaking a two-module cycle in
src/connectors/gstfor #169.What
src/backgroundcontains a runtime import cycle cluster spanning roughly seven modules, with about15 distinct cycle paths:
These are value-level edges, not type-only.
Why it matters
Same latent shape as the cycle fixed in #169, with a larger blast radius. A cycle is safe only while
no module in it touches another's bindings at module top level. Add one top-level
constderivedacross the cycle and you get a TDZ crash under a real Node ESM load — and
tscwill not catch it,because it resolves like a bundler.
This is not theoretical for this repository:
scripts/create-live-run-evidence-template.mjsloadsconnector modules through a genuine Node ESM
await import("….ts")under--experimental-strip-types. Any script that ever reaches intosrc/backgroundthe same way wouldbe exposed.
Existing guard, and why it is scoped
#169 adds
tests/connectors/filed-returns-module-graph.test.ts, which detects runtime cycles byshape and names the offending path. It is deliberately scoped to
src/connectors/gstbecausepointing it at
srcfails immediately on this cluster.Widening that guard to
src/backgroundis the natural completion of this issue — but the clusterhas to be broken first, or the guard has to start with a recorded, shrinking allowlist so it cannot
silently accept new cycles while the old ones are worked off.
Suggested approach
Prefer extracting shared leaf modules (the shape used in #169: move the vocabulary into a module
that imports nothing, then re-export so no caller changes) over adding lazy imports, which hide the
cycle rather than removing it.