# rules/eslint/closure-allowlist-entries-are-wired.eslint.rule.ts

> 176 lines of code and 32 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-closure-allowlist-entries-are-wired-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/closure-allowlist-entries-are-wired.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/centralized-strings.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/centralized-strings.eslint.rule.ts.md) and before [rules/eslint/closure-barrel-must-be-glob.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-barrel-must-be-glob.eslint.rule.ts.md).

## Definitions

- `readEntries` (lexical_declaration, line 51)
- `fileExportsSymbol` (lexical_declaration, line 111)
- `referencedElsewhere` (lexical_declaration, line 119)
- `verdictFor` (lexical_declaration, line 124)
- `create` (method_definition, line 156, exported)
- `program` (method_definition, line 161, exported)
- `ROOT` (lexical_declaration, line 19)
- `ANCHOR_FILE` (lexical_declaration, line 20)
- `AllowlistEntry` (interface_declaration, line 22)
- `SourceIndex` (interface_declaration, line 27)
- `walk` (lexical_declaration, line 33)
- `out` (lexical_declaration, line 37)
- `full` (lexical_declaration, line 39)
- `split` (lexical_declaration, line 53)
- `buildIndex` (lexical_declaration, line 61)
- `rootFiles` (lexical_declaration, line 62)
- `files` (lexical_declaration, line 63)
- `importedBasenames` (lexical_declaration, line 64)
- `globSuffixes` (lexical_declaration, line 65)
- `bodies` (lexical_declaration, line 66)
- `spec` (lexical_declaration, line 83)
- `moduleIsLive` (lexical_declaration, line 90)
- `base` (lexical_declaration, line 91)
- `EXPORT_KEYWORDS` (lexical_declaration, line 95)
- `text` (lexical_declaration, line 112)
- `target` (lexical_declaration, line 120)
- `live` (lexical_declaration, line 128)
- `Emit` (type_alias_declaration, line 138)
- `EMITTERS` (lexical_declaration, line 140)
- `index` (lexical_declaration, line 162, exported)
- `messageId` (lexical_declaration, line 164, exported)
- `payload` (lexical_declaration, line 168, exported)

## Uses

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

## Records this file checks

- [Zombie Code](https://banes-lab.com/records/architecture/zombie-code.md)

## Source

```typescript
import {
    BUILD_CONFIG_FILES,
    BUILD_SCRIPT_ROOT,
    FOUNDATION_SUBJECT,
    MEMBER_ROOT,
    SCRIPT_ROOT,
    TEST_ROOT,
    normalizePath,
    projectFiles,
} from "../../shared/resolvers/anchor.resolver.ts";
import type { LocalRule, RuleContext, RuleListener, RuleNode } from "../../types/rule.types.ts";
import { basename, join } from "node:path";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { STAGED_FUTURE_ALLOWLIST } from "../../shared/allowlists/export.allowlist.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";
import { resolveFile } from "../../shared/matchers/filename.matcher.ts";

const ROOT = MEMBER_ROOT;
const ANCHOR_FILE = resolveFile(FOUNDATION_SUBJECT, "registry", projectFiles());

interface AllowlistEntry {
    file: string;
    symbol: string;
}

interface SourceIndex {
    bodies: { path: string; text: string }[];
    globSuffixes: string[];
    importedBasenames: Set<string>;
}

const walk = function walk(dir: string): string[] {
    if (!existsSync(dir)) {
        return [];
    }
    const out: string[] = [];
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
        const full = join(dir, entry.name);
        if (entry.isDirectory()) {
            out.push(...walk(full));
            continue;
        }
        if (entry.name.endsWith(".ts")) {
            out.push(full);
        }
    }
    return out;
};

const readEntries = function readEntries(): AllowlistEntry[] {
    return STAGED_FUTURE_ALLOWLIST.map((entry) => {
        const split = entry.file.indexOf("::");
        if (split === -1) {
            return { file: entry.file, symbol: "" };
        }
        return { file: entry.file.slice(0, split), symbol: entry.file.slice(split + 2) };
    });
};

const buildIndex = function buildIndex(): SourceIndex {
    const rootFiles = BUILD_CONFIG_FILES.map((name) => join(ROOT, name)).filter((p) => existsSync(p));
    const files = [...walk(ROOT), ...walk(TEST_ROOT), ...walk(SCRIPT_ROOT), ...walk(BUILD_SCRIPT_ROOT), ...rootFiles];
    const importedBasenames = new Set<string>();
    const globSuffixes: string[] = [];
    const bodies: { path: string; text: string }[] = [];
    for (const file of files) {
        const text = readFileSync(file, "utf8");
        bodies.push({ path: normalizePath(file), text });
        for (const part of text.split('from "').slice(1)) {
            const spec = part.slice(0, part.indexOf('"'));
            if (spec.endsWith(".ts")) {
                importedBasenames.add(basename(spec));
            }
        }
        for (const part of text.split('import "').slice(1)) {
            const spec = part.slice(0, part.indexOf('"'));
            if (spec.endsWith(".ts")) {
                importedBasenames.add(basename(spec));
            }
        }
        for (const part of text.split('import.meta.glob("./').slice(1)) {
            const spec = part.slice(0, part.indexOf('"'));
            globSuffixes.push(spec.split("*").join(""));
        }
    }
    return { bodies, globSuffixes, importedBasenames };
};

const moduleIsLive = function moduleIsLive(file: string, index: SourceIndex): boolean {
    const base = basename(file);
    return index.importedBasenames.has(base) || index.globSuffixes.some((suffix) => base.endsWith(suffix));
};

const EXPORT_KEYWORDS = [
    "export const ",
    "export let ",
    "export function ",
    "export async function ",
    "export function* ",
    "export async function* ",
    "export class ",
    "export abstract class ",
    "export interface ",
    "export type ",
    "export enum ",
    "export declare const ",
    "export declare function ",
];

const fileExportsSymbol = function fileExportsSymbol(entry: AllowlistEntry): boolean {
    const text = readFileSync(join(ROOT, entry.file), "utf8");
    if (EXPORT_KEYWORDS.some((keyword) => text.includes(keyword + entry.symbol))) {
        return true;
    }
    return text.includes("export {") && text.includes(entry.symbol);
};

const referencedElsewhere = function referencedElsewhere(entry: AllowlistEntry, index: SourceIndex): boolean {
    const target = normalizePath(join(ROOT, entry.file));
    return index.bodies.some((body) => body.path !== target && body.text.includes(entry.symbol));
};

const verdictFor = function verdictFor(entry: AllowlistEntry, index: SourceIndex): string | null {
    if (!existsSync(join(ROOT, entry.file))) {
        return "stale";
    }
    const live = moduleIsLive(entry.file, index);
    if (entry.symbol === "") {
        return live ? null : "unwired";
    }
    if (!fileExportsSymbol(entry)) {
        return "gone";
    }
    return live || referencedElsewhere(entry, index) ? null : "orphan";
};

type Emit = (context: RuleContext, payload: Record<string, string>, at: RuleNode) => void;

const EMITTERS: Record<string, Emit> = {
    gone: (context, payload, at) => {
        context.report({ data: payload, messageId: "gone", node: at });
    },
    orphan: (context, payload, at) => {
        context.report({ data: payload, messageId: "orphan", node: at });
    },
    stale: (context, payload, at) => {
        context.report({ data: payload, messageId: "stale", node: at });
    },
    unwired: (context, payload, at) => {
        context.report({ data: payload, messageId: "unwired", node: at });
    },
};

export default {
    create(context: RuleContext): RuleListener {
        if (!normalizePath(context.filename).endsWith(ANCHOR_FILE)) {
            return {};
        }
        return listener({
            program(_view, node) {
                const index = buildIndex();
                for (const entry of readEntries()) {
                    const messageId = verdictFor(entry, index);
                    if (messageId === null) {
                        continue;
                    }
                    const payload = { file: entry.file, symbol: entry.symbol };
                    EMITTERS[messageId]?.(context, payload, node);
                }
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: ["architecture:zombie-code"], enforces: [] }),
            description:
                "The staged-future allowlist may only hold wired capability: an export whose module something imports, directly or through a glob barrel, waiting on a consumer. Both entry shapes are held to that bar — a `file::symbol` entry names one export, a bare `file` entry covers the whole module.",
        },
        messages: {
            gone: "Allowlist entry `{{file}}::{{symbol}}` names a symbol `{{file}}` does not export. Delete or update the entry.",
            orphan: "Allowlist entry `{{file}}::{{symbol}}` is not wired: nothing imports `{{file}}`, directly or through a glob barrel. Delete the module and the entry.",
            stale: "Allowlist entry `{{file}}` points at a file that does not exist. Delete the entry.",
            unwired:
                "Allowlist entry `{{file}}` covers a whole file that nothing imports, directly or through a glob barrel. Wire it into a consumer, or delete the module and the entry.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
