# rules/eslint/closure-register-needs-consumer.eslint.rule.ts

> 114 lines of code and 20 definitions.

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

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/closure-paths-via-ssot.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-paths-via-ssot.eslint.rule.ts.md) and before [rules/eslint/closure-register-verb-reserved.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-register-verb-reserved.eslint.rule.ts.md).

## Definitions

- `coversSuffix` (lexical_declaration, line 48)
- `registrarSuffixes` (lexical_declaration, line 14)
- `findOrphanRegistries` (lexical_declaration, line 64)
- `create` (method_definition, line 79, exported)
- `callExpression` (method_definition, line 91, exported)
- `REGISTER_VERB` (lexical_declaration, line 11)
- `CONSUMER_VERBS` (lexical_declaration, line 12)
- `counts` (lexical_declaration, line 15)
- `consumerSuffixes` (lexical_declaration, line 28)
- `suffixes` (lexical_declaration, line 29)
- `lowered` (lexical_declaration, line 49)
- `consumed` (lexical_declaration, line 65)
- `orphans` (lexical_declaration, line 66)
- `GRAPH` (lexical_declaration, line 75)
- `ORPHANS` (lexical_declaration, line 76)
- `program` (method_definition, line 82, exported)
- `callee` (lexical_declaration, line 92, exported)
- `suffix` (lexical_declaration, line 96, exported)
- `count` (lexical_declaration, line 97, exported)
- `payload` (lexical_declaration, line 101, exported)

## Uses

- [codemods/analyzers/identifier.analyzer.ts](https://banes-lab.com/source/governance/codemods/analyzers/identifier.analyzer.ts.md)
- [shared/factories/listener.factory.ts](https://banes-lab.com/source/governance/shared/factories/listener.factory.ts.md)
- [shared/selectors/syntax.selector.ts](https://banes-lab.com/source/governance/shared/selectors/syntax.selector.ts.md)

## Records this file checks

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

## Source

```typescript
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { isType, nameOf, nodeAt } from "../../shared/selectors/syntax.selector.ts";
import type { ClosureGraph } from "../../types/closure.types.ts";
import { INSTANCE_SCOPED_REGISTRARS } from "../../shared/allowlists/registrar.allowlist.ts";
import { defineCheck } from "@govlab/context/check";
import { isUpperAlpha } from "@govlab/constants";
import { listener } from "../../shared/factories/listener.factory.ts";
import { loadClosureGraph } from "../../shared/loaders/graph.loader.ts";
import { suffixAfterVerb } from "../../shared/manifests/verb.manifest.ts";

const REGISTER_VERB = "register";
const CONSUMER_VERBS = ["get", "list", "has", "emit", "dispatch"];

const registrarSuffixes = function registrarSuffixes(closure: ClosureGraph): Map<string, number> {
    const counts = new Map<string, number>();
    for (const r of closure.registers) {
        if (INSTANCE_SCOPED_REGISTRARS.has(r.fn)) {
            continue;
        }
        const suffix = suffixAfterVerb(r.fn, REGISTER_VERB);
        if (suffix !== null) {
            counts.set(suffix, (counts.get(suffix) ?? 0) + 1);
        }
    }
    return counts;
};

const consumerSuffixes = function consumerSuffixes(closure: ClosureGraph): Set<string> {
    const suffixes = new Set<string>();
    for (const c of closure.consumers) {
        for (const verb of CONSUMER_VERBS) {
            const suffix = suffixAfterVerb(c.fn, verb);
            if (suffix === null) {
                continue;
            }
            suffixes.add(suffix);
            if (suffix.endsWith("es")) {
                suffixes.add(suffix.slice(0, -2));
            }
            if (suffix.endsWith("s")) {
                suffixes.add(suffix.slice(0, -1));
            }
        }
    }
    return suffixes;
};

const coversSuffix = function coversSuffix(consumed: ReadonlySet<string>, suffix: string): boolean {
    const lowered = suffix.charAt(0).toLowerCase() + suffix.slice(1);
    if (consumed.has(suffix) || consumed.has(lowered)) {
        return true;
    }
    for (const candidate of consumed) {
        if (candidate.length === 0 || candidate.length >= suffix.length) {
            continue;
        }
        if (suffix.startsWith(candidate) && isUpperAlpha(suffix.charAt(candidate.length))) {
            return true;
        }
    }
    return false;
};

const findOrphanRegistries = function findOrphanRegistries(closure: ClosureGraph): Map<string, number> {
    const consumed = consumerSuffixes(closure);
    const orphans = new Map<string, number>();
    for (const [suffix, count] of registrarSuffixes(closure)) {
        if (!coversSuffix(consumed, suffix)) {
            orphans.set(suffix, count);
        }
    }
    return orphans;
};

const GRAPH = loadClosureGraph();
const ORPHANS = GRAPH === null ? new Map<string, number>() : findOrphanRegistries(GRAPH);

export default {
    create(context: RuleContext): RuleListener {
        if (GRAPH === null) {
            return listener({
                program(_view, node) {
                    context.report({ messageId: "graphMissing", node });
                },
            });
        }
        if (ORPHANS.size === 0) {
            return {};
        }
        return listener({
            callExpression(view, node) {
                const callee = nodeAt(view, "callee");
                if (!isType(callee, "Identifier")) {
                    return;
                }
                const suffix = suffixAfterVerb(nameOf(callee), REGISTER_VERB);
                const count = suffix === null ? undefined : ORPHANS.get(suffix);
                if (suffix === null || count === undefined) {
                    return;
                }
                const payload = { count: String(count), suffix };
                context.report({ data: payload, messageId: "orphanRegistry", node });
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: ["architecture:zombie-code"], enforces: ["architecture:registry-pattern"] }),
            description:
                "Every dispatch-table registry (anything declared via `registerX(...)`) MUST have at least one consumer site (`getX(...)`, `listXs()`, `hasX(...)`). Write-only registries are dead infrastructure — consumers bypass them via direct import OR the registry serves zero purpose. Either delete the registry's consumer API, OR migrate every consumer to dispatch via `getX(id)`.",
        },
        messages: {
            graphMissing:
                "The closure graph is missing, so this rule fails closed rather than passing vacuously. Run the gate, whose auto-fix stage rebuilds the graph before linting reads it.",
            orphanRegistry:
                "Registry `{{ suffix }}` is write-only: {{ count }} register{{ suffix }}() call(s), 0 get{{ suffix }}/list{{ suffix }}s/has{{ suffix }} consumer sites. Either DELETE the consumer API and treat as a roster, OR migrate every consumer to dispatch via the registry. Cannot have both halves living simultaneously.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
