# tools/rules/binding.rule.ts

> 209 lines of code and 40 definitions.

Tree: Coordination tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/coordination#file-coordination-tools-rules-binding-rule-ts
Source text: https://banes-lab.com/assets/sources/source.00a9d4997c6902a99e820c24a7adf812f07ac92343f3ff435d0c0d8bc8c3b167.generated.txt

## Definitions

- `undeclaredFinding` (lexical_declaration, line 160)
- `unstated` (lexical_declaration, line 181)
- `finding` (lexical_declaration, line 9)
- `misSectioned` (lexical_declaration, line 34)
- `unhonoured` (lexical_declaration, line 51)
- `blocksOf` (lexical_declaration, line 125)
- `honoredIn` (lexical_declaration, line 141)
- `consumerFindings` (lexical_declaration, line 174)
- `declaredIn` (lexical_declaration, line 111)
- `healBinding` (lexical_declaration, line 193)
- `drifted` (lexical_declaration, line 76)
- `SlotKey` (interface_declaration, line 93)
- `slotKey` (lexical_declaration, line 98)
- `body` (lexical_declaration, line 99)
- `dot` (lexical_declaration, line 100)
- `Declared` (interface_declaration, line 104)
- `keyed` (lexical_declaration, line 113)
- `blocks` (lexical_declaration, line 126)
- `held` (lexical_declaration, line 127)
- `Slot` (type_alias_declaration, line 154)
- `firstOf` (lexical_declaration, line 156)
- `parsed` (lexical_declaration, line 161)
- `elsewhere` (lexical_declaration, line 166)
- `lines` (lexical_declaration, line 175)
- `honored` (lexical_declaration, line 176)
- `slots` (lexical_declaration, line 177)
- `firstSeen` (lexical_declaration, line 178)
- `firstUnmentioned` (lexical_declaration, line 179)
- `state` (lexical_declaration, line 182)
- `open` (lexical_declaration, line 183)
- `onDisk` (lexical_declaration, line 194)
- `rule` (lexical_declaration, line 203, exported)
- `check` (method_definition, line 204, exported)
- `rendered` (lexical_declaration, line 205, exported)
- `healed` (lexical_declaration, line 206, exported)
- `drift` (lexical_declaration, line 207, exported)
- `declared` (lexical_declaration, line 213, exported)
- `scoped` (lexical_declaration, line 214, exported)
- `findings` (lexical_declaration, line 215, exported)
- `names` (lexical_declaration, line 221, exported)

## Uses

- [tools/core/analyzers/binding.analyzer.ts](https://banes-lab.com/source/coordination/tools/core/analyzers/binding.analyzer.ts.md)
- [tools/core/generators/binding.generator.ts](https://banes-lab.com/source/coordination/tools/core/generators/binding.generator.ts.md)

## Source

```typescript
import { BINDING_PATH, SLOT_CONSUMERS } from "../core/constants/binding.constants.ts";
import { type Resolution, enumeratesVocabulary, slotStates, slotsIn } from "../core/analyzers/binding.analyzer.ts";
import type { RuleContext, RuleDeclaration, RuleResult } from "../core/types/rule.types.ts";
import { renderBinding, writeBinding } from "../core/generators/binding.generator.ts";
import type { Finding } from "../core/types/segment.types.ts";
import { sameDocument } from "../core/normalizers/document.normalizer.ts";
import { underRoots } from "../core/filters/scope.filter.ts";

const finding = function finding(path: string, line: number, slot: string): Finding {
    return {
        actual: `${slot} is consumed but the adapter binding does not resolve it`,
        expected: `${slot} declared in ${BINDING_PATH}, or resolved ABSENT`,
        healed: false,
        line,
        locus: slot,
        path,
        remediation: {
            action: "declare",
            decide: `an abstract slot resolves against the adapter binding or resolves ABSENT, and an ABSENT slot means the branch using it does not run — which is declared rather than faked. Two artifacts can carry the repair and only one of them is this finding's subject: either ${path} stops naming ${slot}, or ${BINDING_PATH} binds it to its project value or declares it ABSENT. Deciding which is the judgement, so neither is offered as the target`,
            deterministic: false,
            from: slot,
            target: path,
            to: BINDING_PATH,
        },
        rule: "binding/unresolvedSlot",
        stack: [
            { check: "slot", resolved: slot },
            { check: "binding", resolved: BINDING_PATH },
            { check: "resolution", resolved: "absent" },
        ],
    };
};

const misSectioned = function misSectioned(path: string, line: number, slot: string, actual: string): Finding {
    return {
        actual: `${slot} names a section that does not declare it, and ${actual} does`,
        expected: actual,
        healed: false,
        line,
        locus: slot,
        path,
        remediation: { action: "rename", decide: "", deterministic: true, from: slot, target: path, to: actual },
        rule: "binding/slotInWrongSection",
        stack: [
            { check: "slot", resolved: slot },
            { check: "declared", resolved: actual },
        ],
    };
};

const unhonoured = function unhonoured(path: string, line: number, slot: string, state: Resolution): Finding {
    return {
        actual: `${slot} is consumed as though it resolved, and the adapter resolves it ${state}`,
        expected: `the consuming line names ${state}, so the branch reading ${slot} does not run`,
        healed: false,
        line,
        locus: slot,
        path,
        remediation: {
            action: "declare",
            decide: `a slot has three resolution states and only one of them lets its branch run. The adapter is checked for whether a slot is DECLARED; nothing checks whether the CONSUMER honors what it declares, so a spec reading a non-resolving slot as though it resolved passes every existing check — which makes a gate satisfiable only by fabricating the evidence it demands. Two repairs are available and choosing between them is the judgement: ${path} states the ${state} resolution at the consuming line and does not run that branch, or it stops naming ${slot} at all. ABSENT means this deployment has no analogue and the branch is skipped; DEFERRED means the branch is BLOCKED rather than skipped, and collapsing the second into the first answers a different question in the right shape`,
            deterministic: false,
            from: slot,
            target: path,
            to: state,
        },
        rule: "binding/stateNotHonoured",
        stack: [
            { check: "slot", resolved: slot },
            { check: "binding", resolved: BINDING_PATH },
            { check: "resolution", resolved: state },
        ],
    };
};

const drifted = function drifted(path: string): Finding {
    return {
        actual: "the adapter binding on disk differs from the configuration it is rendered from",
        expected: "the rendered binding",
        healed: true,
        line: 0,
        locus: "adapter binding",
        path,
        remediation: { action: "delete", decide: "", deterministic: true, from: path, target: path, to: null },
        rule: "binding/bindingDrift",
        stack: [
            { check: "source", resolved: "the surface configuration" },
            { check: "rendered", resolved: "differs from the document on disk" },
        ],
    };
};

interface SlotKey {
    readonly namespace: string;
    readonly key: string;
}

const slotKey = function slotKey(name: string): SlotKey | null {
    const body = name.slice(1, -1);
    const dot = body.indexOf(".");
    return dot <= 0 ? null : { key: body.slice(dot + 1), namespace: body.slice(0, dot) };
};

interface Declared {
    readonly names: ReadonlySet<string>;
    readonly namespaces: ReadonlySet<string>;
    readonly byKey: ReadonlyMap<string, string>;
    readonly states: ReadonlyMap<string, Resolution>;
}

const declaredIn = function declaredIn(rendered: string): Declared {
    const names = slotsIn(rendered).map((slot) => slot.name);
    const keyed = names.flatMap((name) => {
        const parsed = slotKey(name);
        return parsed === null ? [] : [{ name, ...parsed }];
    });
    return {
        byKey: new Map(keyed.map((entry) => [entry.key, entry.name])),
        names: new Set(names),
        namespaces: new Set(keyed.map((entry) => entry.namespace)),
        states: slotStates(rendered),
    };
};

const blocksOf = function blocksOf(lines: readonly string[]): string[] {
    const blocks: string[] = [];
    let held: string[] = [];

    for (const line of [...lines, ""]) {
        if (line.trim().length > 0) {
            held.push(line);
        } else {
            blocks.push(held.join("\n"));
            held = [];
        }
    }

    return blocks;
};

const honoredIn = function honoredIn(lines: readonly string[], states: ReadonlyMap<string, Resolution>): Set<string> {
    return new Set(
        blocksOf(lines).flatMap((text) =>
            slotsIn(text)
                .filter((use) => {
                    const state = states.get(use.name);
                    return state !== undefined && text.includes(state);
                })
                .map((use) => use.name),
        ),
    );
};

type Slot = ReturnType<typeof slotsIn>[number];

const firstOf = function firstOf(slots: readonly Slot[]): Set<Slot> {
    return new Set(slots.filter((slot, index) => slots.findIndex((other) => other.name === slot.name) === index));
};

const undeclaredFinding = function undeclaredFinding(path: string, slot: Slot, declared: Declared): Finding[] {
    const parsed = slotKey(slot.name);
    if (declared.names.has(slot.name) || parsed === null || !declared.namespaces.has(parsed.namespace)) {
        return [];
    }

    const elsewhere = declared.byKey.get(parsed.key);
    return [
        elsewhere === undefined
            ? finding(path, slot.line, slot.name)
            : misSectioned(path, slot.line, slot.name, elsewhere),
    ];
};

const consumerFindings = function consumerFindings(path: string, source: string, declared: Declared): Finding[] {
    const lines = source.split("\n");
    const honored = honoredIn(lines, declared.states);
    const slots = slotsIn(source);
    const firstSeen = firstOf(slots);
    const firstUnmentioned = firstOf(slots.filter((slot) => !enumeratesVocabulary(lines[slot.line - 1] ?? "")));

    const unstated = (slot: Slot): Finding[] => {
        const state = declared.states.get(slot.name);
        const open = state !== undefined && state !== "RESOLVED" && !honored.has(slot.name);
        return open && firstUnmentioned.has(slot) ? [unhonoured(path, slot.line, slot.name, state)] : [];
    };

    return slots.flatMap((slot) => [
        ...unstated(slot),
        ...(firstSeen.has(slot) ? undeclaredFinding(path, slot, declared) : []),
    ]);
};

const healBinding = function healBinding(context: RuleContext, rendered: string, fix: boolean): string[] {
    const onDisk = context.exists(BINDING_PATH) ? context.read(BINDING_PATH) : null;
    const drift = onDisk === null || !sameDocument(onDisk, rendered);
    if (!drift || !fix) {
        return [];
    }
    writeBinding(context.repoRoot, rendered);
    return [BINDING_PATH];
};

export const rule: RuleDeclaration = {
    check(context: RuleContext, fix: boolean): RuleResult {
        const rendered = renderBinding();
        const healed = healBinding(context, rendered, fix);
        const drift = healed.length > 0 ? [drifted(BINDING_PATH)] : [];

        if (!context.paths.includes(BINDING_PATH)) {
            return { derivations: { binding: "rendered" }, findings: drift, healed };
        }

        const declared = declaredIn(rendered);
        const scoped = underRoots(context.paths, SLOT_CONSUMERS);
        const findings = [
            ...scoped
                .filter((path) => path !== BINDING_PATH)
                .flatMap((path) => consumerFindings(path, context.read(path), declared)),
            ...drift,
        ];
        const names = [...declared.names].toSorted((left, right) => left.localeCompare(right, "en"));

        return { derivations: { consumers: scoped.length, declared: names }, findings, healed };
    },
    extensions: [".md"],
    heals: true,
    invariant:
        "every abstract slot a consumer names resolves through the adapter binding, and the binding is rendered from the configuration rather than authored beside it",
    jurisdiction: "taxonomy",
    kinds: ["unresolvedSlot", "slotInWrongSection", "stateNotHonoured", "bindingDrift"],

    reads: [BINDING_PATH],

    stage: "content",
};
```
