# rules/eslint/require-signal-guard.eslint.rule.ts

> 79 lines of code and 17 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-require-signal-guard-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/require-signal-guard.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/require-self-registration.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/require-self-registration.eslint.rule.ts.md) and before [rules/eslint/require-snapshot-coverage.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/require-snapshot-coverage.eslint.rule.ts.md).

## Definitions

- `asNode` (lexical_declaration, line 23)
- `isNode` (lexical_declaration, line 19)
- `makeOnDeclarator` (lexical_declaration, line 42)
- `AstNode` (interface_declaration, line 6)
- `SPAWN_CALLEE` (lexical_declaration, line 13)
- `GUARD_NAMES` (lexical_declaration, line 14)
- `MESSAGE` (lexical_declaration, line 16)
- `GuardState` (interface_declaration, line 27)
- `makeNoteIdentifier` (lexical_declaration, line 33)
- `name` (lexical_declaration, line 35)
- `makeOnCall` (lexical_declaration, line 50)
- `call` (lexical_declaration, line 52)
- `makeOnProgramExit` (lexical_declaration, line 59)
- `requireSignalGuard` (lexical_declaration, line 67)
- `create` (method_definition, line 68)
- `state` (lexical_declaration, line 69)
- `handlers` (lexical_declaration, line 70)

## Records this file checks

- [Error Handling](https://banes-lab.com/records/architecture/error-handling.md)
- [Unobservable Failure](https://banes-lab.com/records/architecture/unobservable-failure.md)

## Source

```typescript
import type { Rule } from "eslint";
import { defineCheck } from "@govlab/context/check";

defineCheck({ detects: ["architecture:unobservable-failure"], enforces: ["architecture:error-handling"] });

interface AstNode {
    type: string;
    name?: string;
    callee?: AstNode;
    id?: AstNode;
}

const SPAWN_CALLEE = "spawnTool";
const GUARD_NAMES = new Set(["signalKilled", "signalDeathResult", "terminalOf", "passOf"]);

const MESSAGE =
    "A spawned tool that dies by a signal reports no exit status, and a caller that gates on that status reads the killed run as a clean one. Check the spawn result for signal death before any exit status gates it, by classifying the result for a terminal outcome or by testing for signal death and returning the signal-death result. [require_signal_guard]";

const isNode = function isNode(value: unknown): value is AstNode {
    return value !== null && typeof value === "object" && "type" in value;
};

const asNode = function asNode(value: unknown): AstNode | null {
    return isNode(value) ? value : null;
};

interface GuardState {
    declaresSpawn: boolean;
    sawGuard: boolean;
    spawnNode: Rule.Node | null;
}

const makeNoteIdentifier = function makeNoteIdentifier(state: GuardState): (node: Rule.Node) => void {
    return function noteIdentifier(node: Rule.Node): void {
        const name = asNode(node)?.name;
        if (name !== undefined && GUARD_NAMES.has(name)) {
            state.sawGuard = true;
        }
    };
};

const makeOnDeclarator = function makeOnDeclarator(state: GuardState): (node: Rule.Node) => void {
    return function onDeclarator(node: Rule.Node): void {
        if (asNode(node)?.id?.name === SPAWN_CALLEE) {
            state.declaresSpawn = true;
        }
    };
};

const makeOnCall = function makeOnCall(state: GuardState): (node: Rule.Node) => void {
    return function onCall(node: Rule.Node): void {
        const call = asNode(node);
        if (state.spawnNode === null && call?.callee?.type === "Identifier" && call.callee.name === SPAWN_CALLEE) {
            state.spawnNode = node;
        }
    };
};

const makeOnProgramExit = function makeOnProgramExit(context: Rule.RuleContext, state: GuardState): () => void {
    return function onProgramExit(): void {
        if (state.spawnNode !== null && !state.sawGuard && !state.declaresSpawn) {
            context.report({ messageId: "guard", node: state.spawnNode });
        }
    };
};

const requireSignalGuard: Rule.RuleModule = {
    create(context: Rule.RuleContext): Rule.RuleListener {
        const state: GuardState = { declaresSpawn: false, sawGuard: false, spawnNode: null };
        const handlers: [string, (node: Rule.Node) => void][] = [
            ["CallExpression", makeOnCall(state)],
            ["Identifier", makeNoteIdentifier(state)],
            ["VariableDeclarator", makeOnDeclarator(state)],
            ["Program:exit", makeOnProgramExit(context, state)],
        ];
        return Object.fromEntries(handlers);
    },
    meta: {
        docs: {
            description:
                "A spawned child that dies by signal reports no exit status, so coercing that absent status to success folds a killed process into the clean path and parses its missing output as zero findings — a false all-clear, the worst outcome a gate can produce. Check for signal death immediately after the spawn's error branch, before any status coercion.",
        },
        messages: { guard: MESSAGE },
        schema: [],
        type: "problem",
    },
};

export default {
    plugins: { "govlab-quality": { rules: { "require-signal-guard": requireSignalGuard } } },
    tool: "eslint",
};
```
