# tools/core/inspectors/board.inspector.ts

> 214 lines of code and 41 definitions.

Tree: Coordination tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/coordination#file-coordination-tools-core-inspectors-board-inspector-ts
Source text: https://banes-lab.com/assets/sources/source.ddfe87ffcf2a034614035c85251165ac4a0efecf2baa26a1918dfc76bc0d93ab.generated.txt

## Definitions

- `drift` (lexical_declaration, line 95)
- `checkTemplateDrift` (lexical_declaration, line 116, exported)
- `checkItemAddressing` (lexical_declaration, line 143, exported)
- `checkItemLetters` (lexical_declaration, line 184, exported)
- `checkReadable` (lexical_declaration, line 124, exported)
- `readerSet` (lexical_declaration, line 25, exported)
- `checkIndex` (lexical_declaration, line 62, exported)
- `checkItemFences` (lexical_declaration, line 221, exported)
- `recordSpans` (lexical_declaration, line 162)
- `fenceCounts` (lexical_declaration, line 207)
- `ORDINAL_SEPARATOR` (lexical_declaration, line 9)
- `FenceCounts` (interface_declaration, line 11)
- `ADDRESS_PREFIX` (lexical_declaration, line 17)
- `ADDRESS_TERMINATOR` (lexical_declaration, line 19)
- `EVERYONE` (lexical_declaration, line 21)
- `ROLE_PREFIX` (lexical_declaration, line 23)
- `trimmed` (lexical_declaration, line 26, exported)
- `end` (lexical_declaration, line 31, exported)
- `body` (lexical_declaration, line 36, exported)
- `name` (lexical_declaration, line 44, exported)
- `EXCERPT` (lexical_declaration, line 55)
- `letterOfLabel` (lexical_declaration, line 57, exported)
- `space` (lexical_declaration, line 58, exported)
- `{ letters, duplicates }` (lexical_declaration, line 66, exported)
- `duplicated` (lexical_declaration, line 68, exported)
- `unindexed` (lexical_declaration, line 79, exported)
- `missing` (lexical_declaration, line 97)
- `extra` (lexical_declaration, line 98)
- `contract` (lexical_declaration, line 117, exported)
- `RecordSpan` (interface_declaration, line 156)
- `opens` (lexical_declaration, line 163)
- `counted` (lexical_declaration, line 164)
- `out` (lexical_declaration, line 165, exported)
- `from` (lexical_declaration, line 169)
- `records` (lexical_declaration, line 185, exported)
- `line` (lexical_declaration, line 188, exported)
- `holder` (lexical_declaration, line 189, exported)
- `seen` (lexical_declaration, line 208)
- `held` (lexical_declaration, line 210)
- `unstamped` (lexical_declaration, line 222, exported)
- `malformed` (lexical_declaration, line 235, exported)

## Uses

- [tools/core/analyzers/board.analyzer.ts](https://banes-lab.com/source/coordination/tools/core/analyzers/board.analyzer.ts.md)
- [tools/core/resolvers/sweep.resolver.ts](https://banes-lab.com/source/coordination/tools/core/resolvers/sweep.resolver.ts.md)
- [tools/core/validators/board.validator.ts](https://banes-lab.com/source/coordination/tools/core/validators/board.validator.ts.md)

## Used by

- [tools/core/validators/board.validator.ts](https://banes-lab.com/source/coordination/tools/core/validators/board.validator.ts.md)
- [tools/rules/board.rule.ts](https://banes-lab.com/source/coordination/tools/rules/board.rule.ts.md)

## Source

```typescript
import { AGENT_FIELDS, GATE_FIELDS, READ_BUDGET_CHARS } from "../constants/board.constants.ts";
import { delimitersIn, unresolvedAddressing } from "../analyzers/board.analyzer.ts";
import type { Finding } from "../types/segment.types.ts";
import { boardFinding } from "../validators/board.validator.ts";
import { indexedLetters } from "./index.inspector.ts";
import { itemSpans } from "../resolvers/sweep.resolver.ts";
import { readBoardContract } from "../readers/board.reader.ts";

const ORDINAL_SEPARATOR = "-";

interface FenceCounts {
    opens: number;
    closes: number;
    line: number;
}

const ADDRESS_PREFIX = "To ";

const ADDRESS_TERMINATOR = "—";

const EVERYONE = "ALL";

const ROLE_PREFIX = "WHOEVER";

export const readerSet = function readerSet(line: string): string[] {
    const trimmed = line.trim();
    if (!trimmed.startsWith(ADDRESS_PREFIX)) {
        return [];
    }

    const end = trimmed.indexOf(ADDRESS_TERMINATOR);
    if (end === -1) {
        return [];
    }

    const body = trimmed.slice(ADDRESS_PREFIX.length, end);
    if (body.includes(ROLE_PREFIX) || body.includes(EVERYONE)) {
        return [];
    }

    const out = new Set<string>();
    for (const part of body.split(",")) {
        for (const word of part.split(" ")) {
            const name = word.trim();
            if (name.length !== 1 || name < "A" || name > "Z") {
                continue;
            }
            out.add(name);
        }
    }

    return [...out];
};

const EXCERPT = 40;

export const letterOfLabel = function letterOfLabel(label: string): string {
    const space = label.indexOf(" ");
    return space === -1 ? label : label.slice(space + 1);
};

export const checkIndex = function checkIndex(
    records: readonly { kind: string; label: string; line: number }[],
    index: string,
): Finding[] {
    const { letters, duplicates } = indexedLetters(index);

    const duplicated = duplicates.map((letter) =>
        boardFinding(
            "duplicateIndexBinding",
            1,
            letter,
            `the index binds ${letter} more than once`,
            `${letter} bound to exactly one role`,
            "a letter is bound to a role for the life of the project and is never reused, because every item, row, citation and changelog line that ever named it resolves through the index — two bindings silently re-point half of them, and nothing errors",
        ),
    );

    const unindexed = records
        .filter((record) => record.kind === "agent" && !letters.has(letterOfLabel(record.label)))
        .map((record) =>
            boardFinding(
                "unindexedAgent",
                record.line,
                letterOfLabel(record.label),
                `${record.label} writes under a letter the index does not bind`,
                `${letterOfLabel(record.label)} carrying an index row before its first write`,
                "a letter is claimed by adding the index row, never by using it. Writing under an unindexed letter is the same construct as a task citing an agent the board does not declare — it reads as governed and resolves to nothing, and every citation written against it resolves to nothing too",
            ),
        );

    return [...duplicated, ...unindexed];
};

const drift = function drift(declared: readonly string[], derived: readonly string[], kind: string): Finding[] {
    const held = new Set(derived);
    const missing = declared.filter((field) => !held.has(field));
    const extra = derived.filter((field) => !declared.includes(field));

    if (missing.length === 0 && extra.length === 0) {
        return [];
    }

    return [
        boardFinding(
            "templateDrift",
            1,
            kind,
            `the comms template declares ${derived.join(", ")} for a ${kind} record`,
            declared.join(", "),
            "the template is what a new board is built FROM, so a schema transcribed in the gate and restated in the template is two copies of one contract with nothing keeping them equal — and the drift surfaces only when somebody raises a board and it fails on its first run. The checklist gate already derives its contract from its own template for exactly this reason; a board raised from a drifted template is non-conformant at birth",
        ),
    ];
};

export const checkTemplateDrift = function checkTemplateDrift(template: string): Finding[] {
    const contract = readBoardContract(template);
    return [
        ...(contract.agentFields.length > 0 ? drift(AGENT_FIELDS, contract.agentFields, "agent") : []),
        ...(contract.gateFields.length > 0 ? drift(GATE_FIELDS, contract.gateFields, "gate") : []),
    ];
};

export const checkReadable = function checkReadable(source: string): Finding[] {
    return source
        .split("\n")
        .flatMap((line, index) =>
            line.length <= READ_BUDGET_CHARS
                ? []
                : [
                      boardFinding(
                          "unreadableField",
                          index + 1,
                          line.slice(0, EXCERPT),
                          `one field carries ${String(line.length)} characters, past what a single read can consume`,
                          `no field longer than ${String(READ_BUDGET_CHARS)} characters`,
                          "the board is read whole before any agent acts, and an oversized board is read in parts until it is whole — but a part is never smaller than one field, so a field past the read budget removes the last granularity the protocol has left and makes reading it whole impossible rather than merely expensive; the rule that every other board rule depends on then stops holding while every one of them still reports green",
                      ),
                  ],
        );
};

export const checkItemAddressing = function checkItemAddressing(source: string): Finding[] {
    return unresolvedAddressing(source).map((site) =>
        boardFinding(
            "unresolvedAddressing",
            site.line,
            site.opener,
            "an item names an addressee in its text while its metadata resolved to everyone",
            "the metadata carries the letters the text names",
            "the sweep closes an item once every ADDRESSEE has posted later than it, so an item whose addressing failed to parse is addressed to everyone and is swept by nobody — it accumulates while looking correctly filed. The failure is silent in the direction that costs most: the writer sees their item land, the reader still receives it, and only the automatic drain quietly stops applying. A parser that rejects the punctuation every writer actually uses degrades to broadcast rather than erroring, which is why the construct is checked on the surface instead of trusted at the tool",
        ),
    );
};

interface RecordSpan {
    readonly agent: string;
    readonly from: number;
    readonly through: number;
}

const recordSpans = function recordSpans(source: string): RecordSpan[] {
    const opens = new Map<string, number>();
    const counted = new Map<string, number>();
    const out: RecordSpan[] = [];

    for (const mark of delimitersIn(source).filter((each) => !each.agent.includes(ORDINAL_SEPARATOR))) {
        counted.set(mark.agent, (counted.get(mark.agent) ?? 0) + 1);
        const from = opens.get(mark.agent);

        if (mark.open) {
            opens.set(mark.agent, mark.line);
            continue;
        }
        if (from !== undefined) {
            opens.delete(mark.agent);
            out.push({ agent: mark.agent, from, through: mark.line });
        }
    }

    return out.filter((span) => counted.get(span.agent) === 2);
};

export const checkItemLetters = function checkItemLetters(source: string): Finding[] {
    const records = recordSpans(source);

    return itemSpans(source).flatMap((item) => {
        const line = item.from + 1;
        const holder = records.find((record) => record.from < line && record.through > line);
        if (holder === undefined || holder.agent === item.agent) {
            return [];
        }

        return [
            boardFinding(
                "foreignItemLetter",
                line,
                item.key,
                `item ${item.key} sits inside the record of ${holder.agent}, so its key letter and its holder disagree`,
                `item ${item.key} sits inside the record of ${item.agent}`,
                "one writer per record and one letter per item are the same claim read from two operands, and NOTHING COMPARES THEM — the fence-pair check counts each key's own markers and passes a perfectly formed item wherever it happens to sit, while the interleaving check matches a foreign FENCE and an item fence is not foreign to itself. So an item written into a peer's record is well-formed by both, and every mechanism keyed on the letter then reads a different answer depending on which operand it took: the drain resolves the item by its own key and finds it, the record owner reads it as content of their block and may not remove it, and an anchored edit on the holder's span covers text the holder never wrote. Move the item into the record its key names, or reopen it under the letter whose record it sits in — the two operands are both cheap to change and only their AGREEMENT is load-bearing. THIS DOES NOT REACH FOREIGN PROSE: text written into a peer's record with no fence at all carries no letter to compare, so it is invisible to this check and to every other, which is the reason the item mechanism needs a fence rather than a convention",
            ),
        ];
    });
};

const fenceCounts = function fenceCounts(source: string): Map<string, FenceCounts> {
    const seen = new Map<string, FenceCounts>();
    for (const mark of delimitersIn(source).filter((each) => each.agent.includes(ORDINAL_SEPARATOR))) {
        const held = seen.get(mark.agent) ?? { closes: 0, line: mark.line, opens: 0 };
        if (mark.open) {
            held.opens += 1;
        } else {
            held.closes += 1;
        }
        seen.set(mark.agent, held);
    }
    return seen;
};

export const checkItemFences = function checkItemFences(source: string): Finding[] {
    const unstamped = itemSpans(source)
        .filter((span) => span.at === 0)
        .map((span) =>
            boardFinding(
                "unstampedItem",
                span.from + 1,
                span.key,
                `item ${span.key} carries no allocated stamp`,
                `item ${span.key} opened by the tool, which allocates the stamp`,
                "the auto-sweep keys an item by the stamp the tool allocates, so a hand-opened fence is one the sweep can never take — and a hand edit is the sanctioned fallback when the tool is down, which makes that fallback a residue generator on the surface the tool exists to keep clean. The item is not stranded: a handler still drops it by id with an extraction, and this finding is what makes the explicit drain necessary rather than optional",
            ),
        );

    const malformed = [...fenceCounts(source)]
        .filter(([, counts]) => counts.opens !== 1 || counts.closes !== 1)
        .map(([key, counts]) =>
            boardFinding(
                "malformedItemFence",
                counts.line,
                key,
                `item ${key} carries ${String(counts.opens)} open and ${String(counts.closes)} close markers`,
                `item ${key} carries exactly one of each`,
                "the span tool returns a span only when exactly one open and one close exist for a key, so a duplicated or unclosed item fence makes that item permanently unremovable BY TOOL — and the repair is manual, on the one surface the item mechanism exists to stop repairing by hand. At item granularity there are roughly ten times as many fences as at record level, so the failure that never mattered before becomes the common one",
            ),
        );

    return [...unstamped, ...malformed];
};
```
