Skip to content

Guard every static import cycle in server/services with a shrinking baseline - #5921

Merged
atomantic merged 2 commits into
mainfrom
claim/issue-5693
Sep 2, 2026
Merged

Guard every static import cycle in server/services with a shrinking baseline#5921
atomantic merged 2 commits into
mainfrom
claim/issue-5693

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Why components rather than rendered rings

findImportCycles is depth-first, so which rings it names depends on where the walk enters a component — and it enters wherever readdirSync put the first file, i.e. filesystem order, not alphabetical. That is fine for an "is this empty?" assertion and unusable as a baseline: the same untouched graph would produce a different list on another machine, so CI would go red over a change nobody made. Strongly-connected components are a property of the edges alone. They are also the truer picture — the DFS walk names 8 modules in the autopilot ring; the component is 22.

Test plan

  • cd server && npm test — the new suite passes (5 tests); agentImportCycles, twinImportCycles, lib/index.test.js and lib/generatedManifests.test.js stay green.
  • Acceptance probe 1 (a new cycle must fail). Temporarily added import './cosForgeSpawnGate.js' to agentState.js, closing a two-module ring. The guard failed and named both the component and the ring that closed it. Reverted.
  • Acceptance probe 2 (a stale baseline entry must fail). Temporarily removed the taskLearning/routing.jsmetrics.js back-edge without touching the baseline. The other direction failed, naming #5916 and the entry to delete. Reverted.
  • The suite carries its own negative controls: a hand-built 3-node cyclic graph must be reported, an acyclic graph must report nothing, a self-importing module must be reported, and the services graph is size-checked so a resolver gap cannot read as a clean sweep.
  • Nine unrelated suites (imageGen routes, sprites atlas, videoGen, trellis normal bake, public/peer-sync auth integration, setup-data-drift) fail identically on main without this branch — pre-existing local-environment failures, not introduced here.

Closes #5693

…king baseline (#5693)

The two existing cycle guards (agentImportCycles #2837/#3450,
twinImportCycles #5687) each fail only on a ring touching their own
cluster, so the rest of a 1000-module directory had nothing stopping the
next one. A static ESM cycle is a boot-order hazard: whichever member
evaluates first sees `undefined` for the others' bindings, and no
behavior test notices until an unrelated import-order change surfaces it.

serviceImportCycles.test.js now covers the whole directory against a
baseline of the five components still live, each recorded against the
issue that removes it (#5916-#5920). Both directions are asserted: an
unlisted component fails, and a listed one that is no longer detected
fails too, so the list can only shrink and a fixed cycle cannot leave a
stale entry behind.

The baseline is keyed on strongly-connected components, not on the rings
findImportCycles renders. That walk reports whichever rings it closes
from wherever it enters a component, and it enters wherever readdirSync
put the first file - filesystem order, not alphabetical - so the same
untouched graph yields a different list on another machine. Components
are a property of the edges alone. They are also the truer picture: the
DFS walk names 8 modules in the autopilot ring; the component is 22.

findImportCycleComponents joins the shared parser in
lib/staticImportGraph.js rather than getting its own module, for the
reason that file's header already gives - two copies of a structural
matcher is how a guard rots.
The comparator returned 1 for equal elements, which is not a strict weak ordering. SCCs partition the nodes so a tie cannot arise today, but a sort that lies about equality is not something a determinism-critical helper should carry. Compared by code point rather than localeCompare — locale-dependent order is the machine-to-machine variation this function exists to keep out of a baseline.
@atomantic
atomantic merged commit df083e0 into main Sep 2, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-5693 branch September 2, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Widen the import-cycle guard from one cluster to all of server/services with a shrinking baseline

1 participant