# tools/core/runners/agenda.runner.ts

> 210 lines of code and 48 definitions.

Tree: Coordination tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/coordination#file-coordination-tools-core-runners-agenda-runner-ts
Source text: https://banes-lab.com/assets/sources/source.4287ffa7cdb952bd7b2168739ce34139b595d10c0b554f437f99bed4e98e305b.generated.txt

## Definitions

- `oneLine` (lexical_declaration, line 120, exported)
- `invariantCell` (lexical_declaration, line 140)
- `stateCell` (lexical_declaration, line 145)
- `renderSchedule` (lexical_declaration, line 154, exported)
- `runAgendaRow` (lexical_declaration, line 228, exported)
- `liveVenues` (lexical_declaration, line 48)
- `healSchedule` (lexical_declaration, line 184, exported)
- `driftedRows` (lexical_declaration, line 199, exported)
- `SCHEDULE_HEADER` (lexical_declaration, line 18, exported)
- `SCHEDULE_RULE` (lexical_declaration, line 20, exported)
- `ROW_MARKER` (lexical_declaration, line 22)
- `MARKED_FIELD` (lexical_declaration, line 24)
- `PLAN_TERMINATOR` (lexical_declaration, line 26)
- `AgendaRequest` (interface_declaration, line 28, exported)
- `AgendaOutcome` (interface_declaration, line 36, exported)
- `ScheduleReading` (interface_declaration, line 41, exported)
- `prefix` (lexical_declaration, line 49)
- `entries` (lexical_declaration, line 55)
- `archivedVenues` (lexical_declaration, line 62)
- `root` (lexical_declaration, line 63)
- `hasMarkedLetter` (lexical_declaration, line 73)
- `absolute` (lexical_declaration, line 74)
- `readSchedule` (lexical_declaration, line 89, exported)
- `live` (lexical_declaration, line 90, exported)
- `archived` (lexical_declaration, line 91, exported)
- `onRoot` (lexical_declaration, line 94, exported)
- `marked` (lexical_declaration, line 96, exported)
- `inArchive` (lexical_declaration, line 105, exported)
- `CELL_BREAKS` (lexical_declaration, line 118)
- `out` (lexical_declaration, line 121, exported)
- `pending` (lexical_declaration, line 122, exported)
- `named` (lexical_declaration, line 141)
- `{ note }` (lexical_declaration, line 146)
- `TableBounds` (interface_declaration, line 165, exported)
- `scheduleBounds` (lexical_declaration, line 170, exported)
- `from` (lexical_declaration, line 171, exported)
- `to` (lexical_declaration, line 176, exported)
- `held` (lexical_declaration, line 191, exported)
- `bounds` (lexical_declaration, line 203, exported)
- `rendered` (lexical_declaration, line 208, exported)
- `planEntry` (lexical_declaration, line 214)
- `fields` (lexical_declaration, line 215)
- `before` (lexical_declaration, line 233, exported)
- `lines` (lexical_declaration, line 239, exported)
- `at` (lexical_declaration, line 240, exported)
- `index` (lexical_declaration, line 241, exported)
- `written` (lexical_declaration, line 252, exported)
- `witness` (lexical_declaration, line 254, exported)

## Uses

- [tools/core/normalizers/document.normalizer.ts](https://banes-lab.com/source/coordination/tools/core/normalizers/document.normalizer.ts.md)
- [tools/core/resolvers/sweep.resolver.ts](https://banes-lab.com/source/coordination/tools/core/resolvers/sweep.resolver.ts.md)
- [tools/core/strings/agenda.strings.ts](https://banes-lab.com/source/coordination/tools/core/strings/agenda.strings.ts.md)

## Source

```typescript
import { BLOCKING_SUFFIX, VENUE_ARCHIVE } from "../constants/blocking.constants.ts";
import { type SchedulePlan, type ScheduleState, schedule } from "../../../config/agenda.config.ts";

import {
    agendaRowAdded,
    invariantTaken,
    planContended,
    planFileMissing,
    planTerminatorMissing,
} from "../strings/agenda.strings.ts";
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { surfacePath, surfacePrefix } from "../../../config/surface.config.ts";
import { letters } from "./converge.runner.ts";
import { openVenues } from "../resolvers/sweep.resolver.ts";
import { resolve } from "node:path";
import { sameRow } from "../normalizers/document.normalizer.ts";

export const SCHEDULE_HEADER = "| planned | invariant | what it must establish | state |";

export const SCHEDULE_RULE = "|---|---|---|---|";

const ROW_MARKER = "|";

const MARKED_FIELD = "READ AND AWAITING:";

const PLAN_TERMINATOR = "]);";

export interface AgendaRequest {
    readonly plan: string;
    readonly invariant: string;
    readonly establishes: string;
    readonly ordinal: string;
    readonly note: string;
}

export interface AgendaOutcome {
    readonly message: string;
    readonly code: number;
}

export interface ScheduleReading {
    readonly plan: SchedulePlan;
    readonly state: ScheduleState;
    readonly derived: boolean;
    readonly evidence: string;
}

const liveVenues = function liveVenues(repoRoot: string): string[] {
    const prefix = surfacePrefix();
    const root = resolve(repoRoot, prefix);
    if (!existsSync(root)) {
        return [];
    }

    const entries = readdirSync(root, { withFileTypes: true })
        .filter((entry) => entry.isFile())
        .map((entry) => (prefix.length === 0 ? entry.name : `${prefix}/${entry.name}`));

    return openVenues(entries, VENUE_ARCHIVE, BLOCKING_SUFFIX);
};

const archivedVenues = function archivedVenues(repoRoot: string): string[] {
    const root = resolve(repoRoot, surfacePath("venue_archive"));
    if (!existsSync(root)) {
        return [];
    }

    return readdirSync(root, { withFileTypes: true })
        .filter((entry) => entry.isFile() && entry.name.endsWith(BLOCKING_SUFFIX))
        .map((entry) => entry.name);
};

const hasMarkedLetter = function hasMarkedLetter(repoRoot: string, venue: string): boolean {
    const absolute = resolve(repoRoot, venue);
    if (!existsSync(absolute)) {
        return false;
    }

    for (const line of readFileSync(absolute, "utf8").split("\n")) {
        if (!line.startsWith(MARKED_FIELD)) {
            continue;
        }
        return letters(line, MARKED_FIELD).length > 0;
    }

    return false;
};

export const readSchedule = function readSchedule(repoRoot: string): ScheduleReading[] {
    const live = liveVenues(repoRoot);
    const archived = archivedVenues(repoRoot);

    return schedule.map((plan) => {
        const onRoot = live.find((venue) => venue.includes(plan.invariant));
        if (onRoot !== undefined) {
            const marked = hasMarkedLetter(repoRoot, onRoot);
            return {
                derived: true,
                evidence: `${onRoot} stands at the active root and its roster carries ${marked ? "a marked letter" : "no marked letter"}`,
                plan,
                state: marked ? "open" : "created",
            };
        }

        const inArchive = archived.find((venue) => venue.includes(plan.invariant));
        if (inArchive !== undefined) {
            return { derived: true, evidence: `${inArchive} resolves under the archive root`, plan, state: "archived" };
        }

        if (plan.declaredState !== undefined) {
            return { derived: false, evidence: plan.declaredBecause ?? "", plan, state: plan.declaredState };
        }

        return { derived: true, evidence: "neither listing holds a venue for it", plan, state: "planned" };
    });
};

const CELL_BREAKS: ReadonlySet<string> = new Set(["\n", "\r", "\t", " "]);

export const oneLine = function oneLine(value: string): string {
    let out = "";
    let pending = false;

    for (const character of value) {
        if (CELL_BREAKS.has(character)) {
            pending = out.length > 0;
            continue;
        }

        if (pending) {
            out += " ";
        }
        pending = false;
        out += character;
    }

    return out;
};

const invariantCell = function invariantCell(plan: SchedulePlan): string {
    const named = `\`${plan.invariant}\``;
    return plan.merged === undefined ? named : `${named} **merged with** \`${plan.merged}\``;
};

const stateCell = function stateCell(reading: ScheduleReading): string {
    const { note } = reading.plan;
    if (note === undefined || note.length === 0) {
        return reading.state;
    }

    return `\`${reading.state}\` — ${note}`;
};

export const renderSchedule = function renderSchedule(readings: readonly ScheduleReading[]): string[] {
    return [
        SCHEDULE_HEADER,
        SCHEDULE_RULE,
        ...readings.map(
            (reading) =>
                `| ${oneLine(reading.plan.ordinal)} | ${oneLine(invariantCell(reading.plan))} | ${oneLine(reading.plan.establishes)} | ${oneLine(stateCell(reading))} |`,
        ),
    ];
};

export interface TableBounds {
    readonly from: number;
    readonly to: number;
}

export const scheduleBounds = function scheduleBounds(lines: readonly string[]): TableBounds | null {
    const from = lines.findIndex((line) => sameRow(line, SCHEDULE_HEADER));
    if (from === -1) {
        return null;
    }

    let to = from;
    while (to + 1 < lines.length && (lines[to + 1] ?? "").trim().startsWith(ROW_MARKER)) {
        to += 1;
    }

    return { from, to };
};

export const healSchedule = function healSchedule(source: string, rendered: readonly string[]): string | null {
    const lines = source.split("\n");
    const bounds = scheduleBounds(lines);
    if (bounds === null) {
        return null;
    }

    const held = lines.slice(bounds.from, bounds.to + 1);
    if (held.length === rendered.length && held.every((line, index) => sameRow(line, rendered[index] ?? ""))) {
        return null;
    }

    return [...lines.slice(0, bounds.from), ...rendered, ...lines.slice(bounds.to + 1)].join("\n");
};

export const driftedRows = function driftedRows(
    lines: readonly string[],
    readings: readonly ScheduleReading[],
): ScheduleReading[] {
    const bounds = scheduleBounds(lines);
    if (bounds === null) {
        return [];
    }

    const rendered = renderSchedule(readings);
    return readings.filter(
        (_reading, index) => !sameRow(lines[bounds.from + 2 + index] ?? "", rendered[index + 2] ?? ""),
    );
};

const planEntry = function planEntry(request: AgendaRequest): string {
    const fields = [
        `        ordinal: ${JSON.stringify(request.ordinal)},`,
        `        invariant: ${JSON.stringify(request.invariant)},`,
        `        establishes: ${JSON.stringify(request.establishes)},`,
    ];

    if (request.note.length > 0) {
        fields.push(`        note: ${JSON.stringify(request.note)},`);
    }

    return ["    {", ...fields, "    },"].join("\n");
};

export const runAgendaRow = function runAgendaRow(request: AgendaRequest): AgendaOutcome {
    if (!existsSync(request.plan)) {
        return { code: 2, message: planFileMissing(request.plan) };
    }

    const before = readFileSync(request.plan, "utf8");

    if (schedule.some((row) => row.invariant === request.invariant)) {
        return { code: 2, message: invariantTaken(request.invariant) };
    }

    const lines = before.split("\n");
    let at = -1;
    for (let index = lines.length - 1; index >= 0; index -= 1) {
        if ((lines[index] ?? "").trim() === PLAN_TERMINATOR) {
            at = index;
            break;
        }
    }

    if (at === -1) {
        return { code: 2, message: planTerminatorMissing(request.plan) };
    }

    const written = [...lines.slice(0, at), planEntry(request), ...lines.slice(at)].join("\n");

    const witness = readFileSync(request.plan, "utf8");
    if (witness !== before) {
        return { code: 2, message: planContended(request.plan) };
    }

    writeFileSync(request.plan, written, "utf8");

    return { code: 0, message: agendaRowAdded(request.invariant, request.plan) };
};
```
