# rules/eslint/closure-registry-consumer-naming.eslint.rule.ts

> 100 lines of code and 21 definitions.

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

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [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) and before [rules/eslint/closure-rule-declarations-resolve.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-rule-declarations-resolve.eslint.rule.ts.md).

## Definitions

- `hasStandardConsumer` (lexical_declaration, line 37)
- `registrarFiles` (lexical_declaration, line 20)
- `matchesSuffix` (lexical_declaration, line 33)
- `findNonStandardRegistries` (lexical_declaration, line 46)
- `create` (method_definition, line 61, exported)
- `functionDeclaration` (method_definition, line 78, exported)
- `REGISTER_VERB` (lexical_declaration, line 11)
- `STANDARD_PREFIXES` (lexical_declaration, line 12)
- `Offender` (interface_declaration, line 14)
- `byFn` (lexical_declaration, line 21)
- `files` (lexical_declaration, line 26)
- `consumerSuffix` (lexical_declaration, line 40)
- `offenders` (lexical_declaration, line 47)
- `suffix` (lexical_declaration, line 49)
- `GRAPH` (lexical_declaration, line 57)
- `OFFENDERS` (lexical_declaration, line 58)
- `program` (method_definition, line 64, exported)
- `filename` (lexical_declaration, line 72, exported)
- `here` (lexical_declaration, line 73, exported)
- `id` (lexical_declaration, line 79, exported)
- `name` (lexical_declaration, line 83, exported)

## Uses

- [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

- [Intent-Revealing Interface](https://banes-lab.com/records/architecture/intent-revealing-interface.md)
- [Registry Pattern](https://banes-lab.com/records/architecture/registry-pattern.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 { listener } from "../../shared/factories/listener.factory.ts";
import { loadClosureGraph } from "../../shared/loaders/graph.loader.ts";
import { normalizePath } from "../../shared/resolvers/anchor.resolver.ts";
import { suffixAfterVerb } from "../../shared/manifests/verb.manifest.ts";

const REGISTER_VERB = "register";
const STANDARD_PREFIXES = ["get", "list", "has"];

interface Offender {
    files: string[];
    fn: string;
    suffix: string;
}

const registrarFiles = function registrarFiles(closure: ClosureGraph): Map<string, Set<string>> {
    const byFn = new Map<string, Set<string>>();
    for (const r of closure.registers) {
        if (INSTANCE_SCOPED_REGISTRARS.has(r.fn)) {
            continue;
        }
        const files = byFn.get(r.fn) ?? new Set<string>();
        files.add(r.file);
        byFn.set(r.fn, files);
    }
    return byFn;
};

const matchesSuffix = function matchesSuffix(consumerSuffix: string, suffix: string): boolean {
    return consumerSuffix === suffix || consumerSuffix === `${suffix}s` || `${consumerSuffix}s` === suffix;
};

const hasStandardConsumer = function hasStandardConsumer(closure: ClosureGraph, suffix: string): boolean {
    return closure.consumers.some((c) =>
        STANDARD_PREFIXES.some((prefix) => {
            const consumerSuffix = suffixAfterVerb(c.fn, prefix);
            return consumerSuffix !== null && matchesSuffix(consumerSuffix, suffix);
        }),
    );
};

const findNonStandardRegistries = function findNonStandardRegistries(closure: ClosureGraph): Offender[] {
    const offenders: Offender[] = [];
    for (const [fn, files] of registrarFiles(closure)) {
        const suffix = suffixAfterVerb(fn, REGISTER_VERB);
        if (suffix !== null && !hasStandardConsumer(closure, suffix)) {
            offenders.push({ files: [...files], fn, suffix });
        }
    }
    return offenders;
};

const GRAPH = loadClosureGraph();
const OFFENDERS = GRAPH === null ? [] : findNonStandardRegistries(GRAPH);

export default {
    create(context: RuleContext): RuleListener {
        if (GRAPH === null) {
            return listener({
                program(_view, node) {
                    context.report({ messageId: "graphMissing", node });
                },
            });
        }
        if (OFFENDERS.length === 0) {
            return {};
        }
        const filename = normalizePath(context.filename);
        const here = OFFENDERS.filter((off) => off.files.some((f) => filename.endsWith(f)));
        if (here.length === 0) {
            return {};
        }
        return listener({
            functionDeclaration(view, node) {
                const id = nodeAt(view, "id");
                if (!isType(id, "Identifier")) {
                    return;
                }
                const name = nameOf(id);
                for (const off of here.filter((entry) => entry.fn === name)) {
                    context.report({ data: { suffix: off.suffix }, messageId: "nonStandard", node });
                }
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({
                detects: [],
                enforces: ["architecture:intent-revealing-interface", "architecture:registry-pattern"],
            }),
            description:
                "Every registry must expose at least one consumer named to the standard shape — `getX(id)`, `listXs()` or `hasX(id)`. A bespoke selector name defeats every cross-cutting tool that finds registries by that convention, this workspace's own rules included, so the registry becomes invisible to the checks meant to govern it. A bespoke selector is added ALONGSIDE the standard trio, never in place of it. A registrar that attaches to one instance rather than a shared table is classified in the instance-registrar allowlist, which is data the mechanism reads rather than a name written into this rule.",
        },
        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.",
            nonStandard:
                "Registry `{{ suffix }}` has no standard consumer (get{{ suffix }} / list{{ suffix }}s / has{{ suffix }}). Bespoke verb-suffix consumers defeat audit tooling. Add a standard accessor; the bespoke one can stay alongside.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
