# rules/eslint/closure-event-system-has-trajectory.eslint.rule.ts

> 80 lines of code and 17 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-closure-event-system-has-trajectory-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/closure-event-system-has-trajectory.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/closure-discriminated-union-must-be-exhaustive.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-discriminated-union-must-be-exhaustive.eslint.rule.ts.md) and before [rules/eslint/closure-lifecycle-handle-must-be-stored.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-lifecycle-handle-must-be-stored.eslint.rule.ts.md).

## Definitions

- `hasOnlyPlaceholderEvents` (lexical_declaration, line 31)
- `declaresRealEvents` (lexical_declaration, line 48)
- `isEventTypesFile` (lexical_declaration, line 21)
- `create` (method_definition, line 56, exported)
- `program` (method_definition, line 61, exported)
- `LISTENER_REGISTRAR` (lexical_declaration, line 11)
- `PAYLOAD_MAP` (lexical_declaration, line 12)
- `EVENT_SUBJECT` (lexical_declaration, line 13)
- `GRAPH` (lexical_declaration, line 15)
- `listenerRegistryFile` (lexical_declaration, line 17)
- `countSubscribers` (lexical_declaration, line 25)
- `ownDefinition` (lexical_declaration, line 26)
- `external` (lexical_declaration, line 27)
- `members` (lexical_declaration, line 35)
- `annotation` (lexical_declaration, line 43)
- `declaration` (lexical_declaration, line 50)
- `wired` (lexical_declaration, line 66, exported)

## Uses

- [shared/factories/listener.factory.ts](https://banes-lab.com/source/governance/shared/factories/listener.factory.ts.md)
- [shared/matchers/filename.matcher.ts](https://banes-lab.com/source/governance/shared/matchers/filename.matcher.ts.md)
- [shared/resolvers/anchor.resolver.ts](https://banes-lab.com/source/governance/shared/resolvers/anchor.resolver.ts.md)
- [shared/selectors/syntax.selector.ts](https://banes-lab.com/source/governance/shared/selectors/syntax.selector.ts.md)

## Records this file checks

- [Speculative Generality](https://banes-lab.com/records/architecture/speculative-generality.md)

## Source

```typescript
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { isType, nameOf, nodeAt, nodesAt } from "../../shared/selectors/syntax.selector.ts";
import { normalizePath, projectFiles } from "../../shared/resolvers/anchor.resolver.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import type { ClosureGraph } from "../../types/closure.types.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";
import { loadClosureGraph } from "../../shared/loaders/graph.loader.ts";
import { resolveFile } from "../../shared/matchers/filename.matcher.ts";

const LISTENER_REGISTRAR = "registerEventListener";
const PAYLOAD_MAP = "EventPayloadMap";
const EVENT_SUBJECT = "event";

const GRAPH = loadClosureGraph();

const listenerRegistryFile = function listenerRegistryFile(closure: ClosureGraph): string {
    return closure.exports.find((e) => e.name === LISTENER_REGISTRAR)?.file ?? "";
};

const isEventTypesFile = function isEventTypesFile(filename: string): boolean {
    return normalizePath(filename).endsWith(`/${resolveFile(EVENT_SUBJECT, "types", projectFiles())}`);
};

const countSubscribers = function countSubscribers(closure: ClosureGraph): number {
    const ownDefinition = listenerRegistryFile(closure);
    const external = closure.subscribes.filter((s) => ownDefinition === "" || s.file !== ownDefinition).length;
    return external + closure.eventActivity.filter((e) => e.fn === LISTENER_REGISTRAR).length;
};

const hasOnlyPlaceholderEvents = function hasOnlyPlaceholderEvents(node: AstNode | null): boolean {
    if (!isType(node, "TSInterfaceDeclaration") || nameOf(nodeAt(node, "id")) !== PAYLOAD_MAP) {
        return false;
    }
    const members = nodesAt(nodeAt(node, "body"), "body");
    if (members.length === 0) {
        return true;
    }
    return members.every((m) => {
        if (m.type !== "TSPropertySignature") {
            return false;
        }
        const annotation = nodeAt(nodeAt(m, "typeAnnotation"), "typeAnnotation");
        return isType(annotation, "TSNeverKeyword");
    });
};

const declaresRealEvents = function declaresRealEvents(program: AstNode): boolean {
    return nodesAt(program, "body").some((stmt) => {
        const declaration = stmt.type === "ExportNamedDeclaration" ? nodeAt(stmt, "declaration") : stmt;
        return isType(declaration, "TSInterfaceDeclaration") && !hasOnlyPlaceholderEvents(declaration);
    });
};

export default {
    create(context: RuleContext): RuleListener {
        if (!isEventTypesFile(context.filename)) {
            return {};
        }
        return listener({
            program(view, node) {
                if (GRAPH === null) {
                    context.report({ messageId: "graphMissing", node });
                    return;
                }
                const wired = countSubscribers(GRAPH) > 0 || GRAPH.emits.length > 0;
                if (!declaresRealEvents(view) || !wired) {
                    context.report({ messageId: "zeroTrajectory", node });
                }
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: ["architecture:speculative-generality"], enforces: [] }),
            description:
                "Where an event surface exists, it must have trajectory: at least one real, non-`never` payload declared in the payload map, AND at least one emit or subscribe site outside the registrar's own definition.",
        },
        messages: {
            graphMissing:
                "The closure graph is missing. Run the gate, whose auto-fix stage rebuilds the graph before linting reads it.",
            zeroTrajectory:
                "The event system is a zero-trajectory scaffold: its payload map holds only placeholders, or there is no emit site and no external subscribe site. Wire the first event end to end — payload entry, id, emit, listener — or delete the surface.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
