# rules/eslint/closure-no-import-cycles.eslint.rule.ts

> 139 lines of code and 36 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-closure-no-import-cycles-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/closure-no-import-cycles.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/closure-no-double-cast.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-no-double-cast.eslint.rule.ts.md) and before [rules/eslint/closure-no-inline-exported-types.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-no-inline-exported-types.eslint.rule.ts.md).

## Definitions

- `addEdge` (lexical_declaration, line 12)
- `visit` (lexical_declaration, line 56)
- `step` (lexical_declaration, line 59)
- `cycleThrough` (lexical_declaration, line 40)
- `buildGraph` (lexical_declaration, line 10)
- `findCycles` (lexical_declaration, line 51)
- `create` (method_definition, line 121, exported)
- `FileGraph` (type_alias_declaration, line 8)
- `graph` (lexical_declaration, line 11)
- `resolved` (lexical_declaration, line 13)
- `targets` (lexical_declaration, line 17)
- `WHITE` (lexical_declaration, line 30)
- `GRAY` (lexical_declaration, line 31)
- `BLACK` (lexical_declaration, line 32)
- `Frame` (interface_declaration, line 34)
- `path` (lexical_declaration, line 41)
- `cursor` (lexical_declaration, line 42)
- `color` (lexical_declaration, line 52)
- `parent` (lexical_declaration, line 53)
- `cycles` (lexical_declaration, line 54)
- `stack` (lexical_declaration, line 57)
- `target` (lexical_declaration, line 61)
- `seen` (lexical_declaration, line 68)
- `frame` (lexical_declaration, line 80)
- `GRAPH` (lexical_declaration, line 96)
- `CYCLES` (lexical_declaration, line 97)
- `FILES_IN_CYCLES` (lexical_declaration, line 99)
- `byFile` (lexical_declaration, line 100)
- `repr` (lexical_declaration, line 102)
- `chains` (lexical_declaration, line 104)
- `MEMBER_PREFIX` (lexical_declaration, line 112)
- `relFromMember` (lexical_declaration, line 114)
- `norm` (lexical_declaration, line 115)
- `idx` (lexical_declaration, line 116)
- `rel` (lexical_declaration, line 122, exported)
- `program` (method_definition, line 124, exported)

## Uses

- [shared/factories/listener.factory.ts](https://banes-lab.com/source/governance/shared/factories/listener.factory.ts.md)

## Records this file grounds

- [Directed Acyclic Graph](https://banes-lab.com/records/architecture/directed-acyclic-graph.md)

## Records this file checks

- [Directed Acyclic Graph](https://banes-lab.com/records/architecture/directed-acyclic-graph.md)
- [Circular Dependency](https://banes-lab.com/records/architecture/circular-dependency.md)

## Source

```typescript
import { GOVERNED_ROOT, normalizePath } from "../../shared/resolvers/anchor.resolver.ts";
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { loadClosureGraph, normalizeImport } from "../../shared/loaders/graph.loader.ts";
import type { ClosureGraph } from "../../types/closure.types.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";

type FileGraph = Map<string, Set<string>>;

const buildGraph = function buildGraph(closure: ClosureGraph): FileGraph {
    const graph: FileGraph = new Map();
    const addEdge = function addEdge(from: string, importStr: string): void {
        const resolved = normalizeImport(from, importStr);
        if (resolved === null) {
            return;
        }
        const targets = graph.get(from) ?? new Set<string>();
        targets.add(resolved);
        graph.set(from, targets);
    };
    for (const imp of closure.imports) {
        addEdge(imp.file, imp.from);
    }
    for (const imp of closure.sideEffectImports) {
        addEdge(imp.file, imp.from);
    }
    return graph;
};

const WHITE = 0;
const GRAY = 1;
const BLACK = 2;

interface Frame {
    idx: number;
    node: string;
    targets: string[] | null;
}

const cycleThrough = function cycleThrough(parent: Map<string, string>, from: string, target: string): string[] {
    const path = [target];
    let cursor: string | undefined = from;
    while (cursor !== undefined && cursor !== target) {
        path.push(cursor);
        cursor = parent.get(cursor);
    }
    path.push(target);
    return path.reverse();
};

const findCycles = function findCycles(graph: FileGraph): string[][] {
    const color = new Map<string, number>();
    const parent = new Map<string, string>();
    const cycles: string[][] = [];

    const visit = function visit(start: string): void {
        const stack: Frame[] = [{ idx: 0, node: start, targets: null }];
        color.set(start, GRAY);
        const step = function step(frame: Frame): void {
            frame.targets ??= [...(graph.get(frame.node) ?? [])];
            const target = frame.targets[frame.idx];
            if (target === undefined) {
                color.set(frame.node, BLACK);
                stack.pop();
                return;
            }
            frame.idx += 1;
            const seen = color.get(target) ?? WHITE;
            if (seen === GRAY) {
                cycles.push(cycleThrough(parent, frame.node, target));
                return;
            }
            if (seen === WHITE) {
                parent.set(target, frame.node);
                color.set(target, GRAY);
                stack.push({ idx: 0, node: target, targets: null });
            }
        };
        while (stack.length > 0) {
            const frame = stack.at(-1);
            if (frame === undefined) {
                break;
            }
            step(frame);
        }
    };

    for (const node of graph.keys()) {
        if ((color.get(node) ?? WHITE) === WHITE) {
            visit(node);
        }
    }
    return cycles;
};

const GRAPH = loadClosureGraph();
const CYCLES = GRAPH === null ? [] : findCycles(buildGraph(GRAPH));

const FILES_IN_CYCLES = ((): Map<string, string[]> => {
    const byFile = new Map<string, string[]>();
    for (const cycle of CYCLES) {
        const repr = cycle.join(" → ");
        for (const file of cycle) {
            const chains = byFile.get(file) ?? [];
            chains.push(repr);
            byFile.set(file, chains);
        }
    }
    return byFile;
})();

const MEMBER_PREFIX = `${normalizePath(GOVERNED_ROOT)}/`;

const relFromMember = function relFromMember(filepath: string): string {
    const norm = normalizePath(filepath);
    const idx = norm.indexOf(MEMBER_PREFIX);
    return idx === -1 ? norm : norm.slice(idx + MEMBER_PREFIX.length);
};

export default {
    create(context: RuleContext): RuleListener {
        const rel = relFromMember(context.filename);
        return listener({
            program(_view, node) {
                if (GRAPH === null) {
                    context.report({ messageId: "graphMissing", node });
                    return;
                }
                for (const chain of FILES_IN_CYCLES.get(rel) ?? []) {
                    context.report({ data: { chain }, messageId: "cycle", node });
                }
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({
                detects: ["architecture:circular-dependency"],
                enforces: ["architecture:directed-acyclic-graph"],
            }),
            description:
                "Detects circular imports in the codebase. A file A that imports (directly or transitively) a file B that imports A creates a cycle — modules cannot be loaded deterministically, tree-shaking breaks, and the dependency direction is muddled. Cycles indicate misplaced shared concerns; lift the cycled state into a third file both can import.",
        },
        messages: {
            cycle: "File participates in an import cycle: `{{ chain }}`. Lift the shared concern into a third file both ends can import without circling back.",
            graphMissing:
                "closure-graph missing. Run `npm run verify` to regenerate. Fail-close until the graph is available.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
