# tools/core/analyzers/board.analyzer.ts

> 266 lines of code and 89 definitions.

Tree: Coordination tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/coordination#file-coordination-tools-core-analyzers-board-analyzer-ts
Source text: https://banes-lab.com/assets/sources/source.bd97ed06457e8af94b37a3de6bd06184d63a6616a827f33656a8a7355674bf36.generated.txt

## Definitions

- `delimitersIn` (lexical_declaration, line 95, exported)
- `boardRecords` (lexical_declaration, line 276, exported)
- `activeSeats` (lexical_declaration, line 306, exported)
- `unresolvedAddressing` (lexical_declaration, line 16, exported)
- `closesRecord` (lexical_declaration, line 237)
- `stateOf` (lexical_declaration, line 307, exported)
- `runEnd` (lexical_declaration, line 56)
- `spansOf` (lexical_declaration, line 115)
- `itemSpanFlags` (lexical_declaration, line 153, exported)
- `advance` (lexical_declaration, line 255)
- `activeAgents` (lexical_declaration, line 315, exported)
- `AGENT_PREFIX` (lexical_declaration, line 4)
- `BROADCAST_META` (lexical_declaration, line 5)
- `DIRECTED_OPENER` (lexical_declaration, line 6)
- `BROADCAST_OPENER` (lexical_declaration, line 7)
- `OPENER_PREVIEW` (lexical_declaration, line 8)
- `LABEL_LIMIT` (lexical_declaration, line 9)
- `AddressingSite` (interface_declaration, line 11, exported)
- `lines` (lexical_declaration, line 17, exported)
- `text` (lexical_declaration, line 21, exported)
- `directed` (lexical_declaration, line 22, exported)
- `OPEN_MARKER` (lexical_declaration, line 31)
- `CLOSE_MARKER` (lexical_declaration, line 32)
- `MARKER_AGENT` (lexical_declaration, line 33)
- `MARKER_END` (lexical_declaration, line 34)
- `ORDINAL_SEPARATOR` (lexical_declaration, line 35)
- `FIELD_INDENT` (lexical_declaration, line 36)
- `Delimiter` (interface_declaration, line 38, exported)
- `isUpper` (lexical_declaration, line 44)
- `isLower` (lexical_declaration, line 48)
- `isDigit` (lexical_declaration, line 52)
- `cursor` (lexical_declaration, line 57)
- `agentAfter` (lexical_declaration, line 64)
- `at` (lexical_declaration, line 65)
- `start` (lexical_declaration, line 69)
- `upperEnd` (lexical_declaration, line 70)
- `nameEnd` (lexical_declaration, line 74)
- `name` (lexical_declaration, line 75)
- `ordinalEnd` (lexical_declaration, line 79)
- `delimiterOf` (lexical_declaration, line 83)
- `agent` (lexical_declaration, line 89)
- `delimiter` (lexical_declaration, line 100, exported)
- `Span` (interface_declaration, line 109)
- `open` (lexical_declaration, line 117)
- `from` (lexical_declaration, line 124)
- `enclosingRecord` (lexical_declaration, line 134, exported)
- `spans` (lexical_declaration, line 135, exported)
- `held` (lexical_declaration, line 138, exported)
- `width` (lexical_declaration, line 139, exported)
- `size` (lexical_declaration, line 142, exported)
- `flags` (lexical_declaration, line 154, exported)
- `openAt` (lexical_declaration, line 155, exported)
- `openKey` (lexical_declaration, line 156, exported)
- `isItem` (lexical_declaration, line 159, exported)
- `closesOpen` (lexical_declaration, line 160, exported)
- `RecordKind` (type_alias_declaration, line 175, exported)
- `BoardRecord` (interface_declaration, line 177, exported)
- `OpenRecord` (interface_declaration, line 185)
- `HEADINGS` (lexical_declaration, line 193)
- `headingOf` (lexical_declaration, line 198)
- `word` (lexical_declaration, line 205)
- `kind` (lexical_declaration, line 206)
- `rest` (lexical_declaration, line 211)
- `dash` (lexical_declaration, line 212)
- `label` (lexical_declaration, line 213)
- `state` (lexical_declaration, line 218)
- `fieldOf` (lexical_declaration, line 222)
- `colon` (lexical_declaration, line 228)
- `key` (lexical_declaration, line 233)
- `marks` (lexical_declaration, line 239)
- `named` (lexical_declaration, line 244)
- `space` (lexical_declaration, line 245)
- `letter` (lexical_declaration, line 246)
- `Advance` (interface_declaration, line 250)
- `heading` (lexical_declaration, line 256)
- `field` (lexical_declaration, line 266)
- `fields` (lexical_declaration, line 267)
- `isListOrHeading` (lexical_declaration, line 271)
- `trimmed` (lexical_declaration, line 272)
- `out` (lexical_declaration, line 277, exported)
- `fenced` (lexical_declaration, line 278, exported)
- `inItem` (lexical_declaration, line 279, exported)
- `current` (lexical_declaration, line 280, exported)
- `skipped` (lexical_declaration, line 283, exported)
- `step` (lexical_declaration, line 284, exported)
- `ACTIVE_STATE` (lexical_declaration, line 297, exported)
- `seatState` (lexical_declaration, line 299, exported)
- `bound` (lexical_declaration, line 300, exported)
- `allocating` (lexical_declaration, line 301, exported)

## Uses

- [tools/core/runners/fixture.runner.ts](https://banes-lab.com/source/coordination/tools/core/runners/fixture.runner.ts.md)

## Used by

- [tools/core/coordinators/checklist.coordinator.ts](https://banes-lab.com/source/coordination/tools/core/coordinators/checklist.coordinator.ts.md)
- [tools/core/formatters/board.formatter.ts](https://banes-lab.com/source/coordination/tools/core/formatters/board.formatter.ts.md)
- [tools/core/inspectors/board.inspector.ts](https://banes-lab.com/source/coordination/tools/core/inspectors/board.inspector.ts.md)
- [tools/core/runners/converge.runner.ts](https://banes-lab.com/source/coordination/tools/core/runners/converge.runner.ts.md)
- [tools/core/runners/venue.runner.ts](https://banes-lab.com/source/coordination/tools/core/runners/venue.runner.ts.md)
- [tools/core/steps/snapshot.step.ts](https://banes-lab.com/source/coordination/tools/core/steps/snapshot.step.ts.md)
- [tools/core/validators/converge.validator.ts](https://banes-lab.com/source/coordination/tools/core/validators/converge.validator.ts.md)
- [tools/core/validators/venue.validator.ts](https://banes-lab.com/source/coordination/tools/core/validators/venue.validator.ts.md)

## Source

```typescript
import { fencedFlags } from "../predicates/fence.predicate.ts";
import { indexedStates } from "../inspectors/index.inspector.ts";

const AGENT_PREFIX = "Agent ";
const BROADCAST_META = "to:*";
const DIRECTED_OPENER = "To ";
const BROADCAST_OPENER = "To ALL";
const OPENER_PREVIEW = 24;
const LABEL_LIMIT = 24;

export interface AddressingSite {
    readonly line: number;
    readonly opener: string;
}

export const unresolvedAddressing = function unresolvedAddressing(source: string): AddressingSite[] {
    const lines = source.split("\n");
    const out: AddressingSite[] = [];

    for (const [index, line] of lines.entries()) {
        const text = (lines[index + 1] ?? "").trim();
        const directed = text.startsWith(DIRECTED_OPENER) && !text.startsWith(BROADCAST_OPENER);
        if (line.includes(BROADCAST_META) && directed) {
            out.push({ line: index + 1, opener: text.slice(0, OPENER_PREVIEW) });
        }
    }

    return out;
};

const OPEN_MARKER = "┌";
const CLOSE_MARKER = "└";
const MARKER_AGENT = "AGENT ";
const MARKER_END = "END AGENT ";
const ORDINAL_SEPARATOR = "-";
const FIELD_INDENT = "  ";

export interface Delimiter {
    readonly agent: string;
    readonly open: boolean;
    readonly line: number;
}

const isUpper = function isUpper(char: string): boolean {
    return char >= "A" && char <= "Z";
};

const isLower = function isLower(char: string): boolean {
    return char >= "a" && char <= "z";
};

const isDigit = function isDigit(char: string): boolean {
    return char >= "0" && char <= "9";
};

const runEnd = function runEnd(text: string, from: number, accepts: (char: string) => boolean): number {
    let cursor = from;
    while (cursor < text.length && accepts(text.charAt(cursor))) {
        cursor += 1;
    }
    return cursor;
};

const agentAfter = function agentAfter(text: string, marker: string): string | null {
    const at = text.indexOf(marker);
    if (at === -1) {
        return null;
    }
    const start = at + marker.length;
    const upperEnd = runEnd(text, start, isUpper);
    if (upperEnd === start) {
        return null;
    }
    const nameEnd = runEnd(text, upperEnd, isLower);
    const name = text.slice(start, nameEnd);
    if (text.charAt(nameEnd) !== ORDINAL_SEPARATOR) {
        return name;
    }
    const ordinalEnd = runEnd(text, nameEnd + 1, isDigit);
    return ordinalEnd === nameEnd + 1 ? name : `${name}${ORDINAL_SEPARATOR}${text.slice(nameEnd + 1, ordinalEnd)}`;
};

const delimiterOf = function delimiterOf(line: string, index: number): Delimiter | null {
    if (line.startsWith(OPEN_MARKER)) {
        const agent = agentAfter(line, MARKER_AGENT);
        return agent === null ? null : { agent, line: index + 1, open: true };
    }
    if (line.startsWith(CLOSE_MARKER)) {
        const agent = agentAfter(line, MARKER_END);
        return agent === null ? null : { agent, line: index + 1, open: false };
    }
    return null;
};

export const delimitersIn = function delimitersIn(source: string): Delimiter[] {
    const out: Delimiter[] = [];
    const fenced = fencedFlags(source);

    for (const [index, line] of source.split("\n").entries()) {
        const delimiter = fenced[index] === true ? null : delimiterOf(line.trim(), index);
        if (delimiter !== null) {
            out.push(delimiter);
        }
    }

    return out;
};

interface Span {
    readonly agent: string;
    readonly from: number;
    readonly to: number;
}

const spansOf = function spansOf(source: string): Span[] {
    const spans: Span[] = [];
    const open = new Map<string, number>();

    for (const delimiter of delimitersIn(source)) {
        if (delimiter.open) {
            open.set(delimiter.agent, delimiter.line);
            continue;
        }
        const from = open.get(delimiter.agent);
        if (from !== undefined) {
            open.delete(delimiter.agent);
            spans.push({ agent: delimiter.agent, from, to: delimiter.line });
        }
    }

    return spans;
};

export const enclosingRecord = function enclosingRecord(source: string): (line: number) => string | null {
    const spans = spansOf(source);

    return (line: number): string | null => {
        let held: string | null = null;
        let width = Number.MAX_SAFE_INTEGER;

        for (const span of spans) {
            const size = span.to - span.from;
            if (line >= span.from && line <= span.to && size < width) {
                width = size;
                held = span.agent;
            }
        }

        return held;
    };
};

export const itemSpanFlags = function itemSpanFlags(source: string): boolean[] {
    const flags = source.split("\n").map(() => false);
    let openAt = -1;
    let openKey = "";

    for (const mark of delimitersIn(source)) {
        const isItem = mark.agent.includes(ORDINAL_SEPARATOR);
        const closesOpen = isItem && !mark.open && openAt !== -1 && mark.agent === openKey;
        if (isItem && mark.open && openAt === -1) {
            openAt = mark.line;
            openKey = mark.agent;
        }
        if (closesOpen) {
            flags.fill(true, openAt, mark.line - 1);
            openAt = -1;
            openKey = "";
        }
    }

    return flags;
};

export type RecordKind = "agent" | "gate";

export interface BoardRecord {
    readonly kind: RecordKind;
    readonly label: string;
    readonly state: string;
    readonly line: number;
    readonly fields: ReadonlyMap<string, string>;
}

interface OpenRecord {
    readonly kind: RecordKind;
    readonly label: string;
    readonly state: string;
    readonly line: number;
    readonly fields: Map<string, string>;
}

const HEADINGS: ReadonlyMap<string, RecordKind> = new Map([
    ["Agent", "agent"],
    ["Gate", "gate"],
]);

const headingOf = function headingOf(line: string): { kind: RecordKind; label: string; state: string } | null {
    const trimmed = line.trim();
    const space = trimmed.indexOf(" ");
    if (space === -1) {
        return null;
    }

    const word = trimmed.slice(0, space);
    const kind = HEADINGS.get(word);
    if (kind === undefined) {
        return null;
    }

    const rest = trimmed.slice(space + 1);
    const dash = rest.indexOf("—");
    const label = dash === -1 ? rest.trim() : rest.slice(0, dash).trim();
    if (label.length === 0 || label.length > LABEL_LIMIT) {
        return null;
    }

    const state = dash === -1 ? "" : rest.slice(dash + 1).trim();
    return { kind, label: `${word} ${label}`, state };
};

const fieldOf = function fieldOf(line: string): { key: string; value: string } | null {
    if (!line.startsWith(FIELD_INDENT) || line.charAt(FIELD_INDENT.length) === " ") {
        return null;
    }

    const trimmed = line.trim();
    const colon = trimmed.indexOf(":");
    if (colon <= 0) {
        return null;
    }

    const key = trimmed.slice(0, colon).trim();
    return key.includes(" ") ? null : { key, value: trimmed.slice(colon + 1).trim() };
};

const closesRecord = function closesRecord(line: string, label: string): boolean {
    const trimmed = line.trim();
    const marks = trimmed.indexOf(MARKER_END);
    if (!trimmed.startsWith(CLOSE_MARKER) || marks === -1) {
        return false;
    }

    const named = trimmed.slice(marks + MARKER_END.length).trim();
    const space = label.indexOf(" ");
    const letter = space === -1 ? label : label.slice(space + 1).trim();
    return named === letter;
};

interface Advance {
    readonly current: OpenRecord | null;
    readonly done: OpenRecord | null;
}

const advance = function advance(current: OpenRecord | null, line: string, index: number): Advance {
    const heading = headingOf(line);
    if (heading !== null) {
        return { current: { ...heading, fields: new Map(), line: index + 1 }, done: current };
    }
    if (current === null) {
        return { current: null, done: null };
    }
    if (closesRecord(line, current.label)) {
        return { current: null, done: current };
    }
    const field = fieldOf(line);
    const fields = field === null ? current.fields : new Map([...current.fields, [field.key, field.value]]);
    return { current: { ...current, fields }, done: null };
};

const isListOrHeading = function isListOrHeading(line: string): boolean {
    const trimmed = line.trim();
    return trimmed.startsWith("#") || trimmed.startsWith("-");
};

export const boardRecords = function boardRecords(source: string): BoardRecord[] {
    const out: BoardRecord[] = [];
    const fenced = fencedFlags(source);
    const inItem = itemSpanFlags(source);
    let current: OpenRecord | null = null;

    for (const [index, line] of source.split("\n").entries()) {
        const skipped = fenced[index] === true || inItem[index] === true || isListOrHeading(line);
        const step: Advance = skipped ? { current, done: null } : advance(current, line, index);
        ({ current } = step);
        if (step.done !== null) {
            out.push(step.done);
        }
    }

    if (current !== null) {
        out.push(current);
    }
    return out;
};

export const ACTIVE_STATE = "ACTIVE";

export const seatState = function seatState(index: string): (letter: string, marker: string) => string {
    const bound = indexedStates(index);
    const allocating = index.trim().length > 0;

    return (letter: string, marker: string): string => (allocating ? (bound.get(letter) ?? "") : marker);
};

export const activeSeats = function activeSeats(board: string, index: string): string[] {
    const stateOf = seatState(index);
    return boardRecords(board)
        .filter((record) => record.kind === "agent")
        .map((record) => ({ letter: record.label.slice(AGENT_PREFIX.length).trim(), state: record.state }))
        .filter(({ letter, state }) => letter.length > 0 && stateOf(letter, state) === ACTIVE_STATE)
        .map(({ letter }) => letter);
};

export const activeAgents = function activeAgents(board: string, index: string): number {
    return activeSeats(board, index).length;
};
```
