# tools/core/validators/converge.validator.ts

> 562 lines of code and 149 definitions.

Tree: Coordination tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/coordination#file-coordination-tools-core-validators-converge-validator-ts
Source text: https://banes-lab.com/assets/sources/source.de91ac6788bda4a7e0a2df7092ad97cce03736e86836591bd10fad320a0e61d5.generated.txt

## Definitions

- `clauseLines` (lexical_declaration, line 239, exported)
- `receiverResolves` (lexical_declaration, line 384, exported)
- `opensSection` (lexical_declaration, line 194)
- `addressesSuccessor` (lexical_declaration, line 363, exported)
- `isDigit` (lexical_declaration, line 64)
- `isSectionBoundary` (lexical_declaration, line 167)
- `isStatedClause` (lexical_declaration, line 190)
- `venueInvariant` (lexical_declaration, line 356, exported)
- `fieldOf` (lexical_declaration, line 465)
- `successorDetail` (lexical_declaration, line 559)
- `absorptionDetail` (lexical_declaration, line 591)
- `clauseSection` (lexical_declaration, line 210)
- `unarrivedDeferrals` (lexical_declaration, line 367, exported)
- `absorptionEdge` (lexical_declaration, line 606)
- `isHeadingCharacter` (lexical_declaration, line 68)
- `isAllDigits` (lexical_declaration, line 263)
- `needsEdge` (lexical_declaration, line 469)
- `durableEdge` (lexical_declaration, line 502)
- `successorEdge` (lexical_declaration, line 576)
- `directivesEdge` (lexical_declaration, line 581)
- `inheritanceDetail` (lexical_declaration, line 616)
- `inheritanceEdge` (lexical_declaration, line 623)
- `declaredHeading` (lexical_declaration, line 72, exported)
- `receiverOf` (lexical_declaration, line 146)
- `unemphasised` (lexical_declaration, line 157)
- `deferredClauses` (lexical_declaration, line 230)
- `isSignOffLetter` (lexical_declaration, line 420)
- `signaturesEdge` (lexical_declaration, line 477)
- `matches` (lexical_declaration, line 521)
- `inheritsIntoSuccessor` (lexical_declaration, line 551)
- `absorbed` (lexical_declaration, line 587)
- `unoriginated` (lexical_declaration, line 624)
- `convergenceEdges` (lexical_declaration, line 633, exported)
- `successorView` (lexical_declaration, line 526)
- `insideFlags` (lexical_declaration, line 198)
- `strandedDeferrals` (lexical_declaration, line 400, exported)
- `durableFailure` (lexical_declaration, line 493)
- `headingOf` (lexical_declaration, line 505)
- `headingIsWhole` (lexical_declaration, line 76, exported)
- `clauseOf` (lexical_declaration, line 185)
- `isTaskId` (lexical_declaration, line 272)
- `unoriginatedInheritance` (lexical_declaration, line 377, exported)
- `signOffEntry` (lexical_declaration, line 425)
- `venueRecords` (lexical_declaration, line 438, exported)
- `durableDeclared` (lexical_declaration, line 489)
- `successorName` (lexical_declaration, line 519)
- `successorHolds` (lexical_declaration, line 555)
- `SIGN_OFF_ABSENT` (lexical_declaration, line 44)
- `NEEDS_FIELD` (lexical_declaration, line 46)
- `DURABLE_FIELD` (lexical_declaration, line 48)
- `INHERITED_HEADING` (lexical_declaration, line 50)
- `PLACEHOLDER` (lexical_declaration, line 52)
- `LIST_ITEM` (lexical_declaration, line 54)
- `AGENT_LABEL` (lexical_declaration, line 56)
- `SIGN_OFF_COLON_LIMIT` (lexical_declaration, line 58)
- `SIGN_OFF_LETTER_LIMIT` (lexical_declaration, line 60)
- `EMPHASIS` (lexical_declaration, line 62)
- `CONVERGENCE_STEPS` (lexical_declaration, line 86, exported)
- `ARCHIVE_STEP` (lexical_declaration, line 96, exported)
- `DESTRUCTIVE_STEPS` (lexical_declaration, line 98, exported)
- `ConvergenceStep` (type_alias_declaration, line 100, exported)
- `Edge` (interface_declaration, line 102, exported)
- `SUCCESSOR_FIELD` (lexical_declaration, line 109)
- `DEFERRED_HEADING` (lexical_declaration, line 111)
- `DIRECTIVES_HEADING` (lexical_declaration, line 113)
- `declaredSuccessor` (lexical_declaration, line 115, exported)
- `fenced` (lexical_declaration, line 116, exported)
- `line` (lexical_declaration, line 117, exported)
- `value` (lexical_declaration, line 118, exported)
- `CLAUSE_ARROW` (lexical_declaration, line 122)
- `DeferredSection` (interface_declaration, line 124)
- `ClauseLine` (interface_declaration, line 130)
- `ClauseSection` (interface_declaration, line 135)
- `StrandedDeferral` (interface_declaration, line 141, exported)
- `rest` (lexical_declaration, line 152)
- `comma` (lexical_declaration, line 153)
- `held` (lexical_declaration, line 158)
- `BANNER` (lexical_declaration, line 165)
- `DEFERRED_SECTION` (lexical_declaration, line 171, exported)
- `DEFERRAL_ARROW` (lexical_declaration, line 173, exported)
- `sectionBound` (lexical_declaration, line 175, exported)
- `boundaries` (lexical_declaration, line 177, exported)
- `from` (lexical_declaration, line 178, exported)
- `arrow` (lexical_declaration, line 186)
- `out` (lexical_declaration, line 199)
- `inside` (lexical_declaration, line 212)
- `items` (lexical_declaration, line 213)
- `clause` (lexical_declaration, line 224)
- `section` (lexical_declaration, line 231)
- `DISTRIBUTES_FIELD` (lexical_declaration, line 243, exported)
- `CLOSES_FIELD` (lexical_declaration, line 245)
- `OPEN_TASK` (lexical_declaration, line 247)
- `Absorption` (interface_declaration, line 249, exported)
- `PlannedSurface` (interface_declaration, line 256)
- `taskIdOf` (lexical_declaration, line 276)
- `plannedSurface` (lexical_declaration, line 280)
- `closesLine` (lexical_declaration, line 284)
- `absorptionState` (lexical_declaration, line 297, exported)
- `planning` (lexical_declaration, line 298, exported)
- `declaring` (lexical_declaration, line 303, exported)
- `[first]` (lexical_declaration, line 306, exported)
- `Distribution` (interface_declaration, line 319, exported)
- `declaredDistributions` (lexical_declaration, line 325, exported)
- `trimmed` (lexical_declaration, line 333, exported)
- `declares` (lexical_declaration, line 334, exported)
- `AWAITING_LINE` (lexical_declaration, line 340)
- `hasReadMark` (lexical_declaration, line 342, exported)
- `base` (lexical_declaration, line 357, exported)
- `deferred` (lexical_declaration, line 378, exported)
- `SIGN_OFF_BANNER` (lexical_declaration, line 412, exported)
- `signOffSection` (lexical_declaration, line 414, exported)
- `lines` (lexical_declaration, line 415, exported)
- `opens` (lexical_declaration, line 416, exported)
- `first` (lexical_declaration, line 421)
- `colon` (lexical_declaration, line 426)
- `letter` (lexical_declaration, line 427)
- `signOffLines` (lexical_declaration, line 431)
- `entries` (lexical_declaration, line 432)
- `successorOf` (lexical_declaration, line 446)
- `stem` (lexical_declaration, line 447, exported)
- `dot` (lexical_declaration, line 448, exported)
- `ordinal` (lexical_declaration, line 449)
- `Records` (type_alias_declaration, line 453)
- `SuccessorView` (interface_declaration, line 455)
- `unstated` (lexical_declaration, line 470)
- `signatures` (lexical_declaration, line 478)
- `outstanding` (lexical_declaration, line 479)
- `signed` (lexical_declaration, line 480)
- `undeclared` (lexical_declaration, line 503)
- `stated` (lexical_declaration, line 504)
- `headings` (lexical_declaration, line 507)
- `unresolved` (lexical_declaration, line 508)
- `vacuous` (lexical_declaration, line 513)
- `failing` (lexical_declaration, line 515)
- `declared` (lexical_declaration, line 533)
- `successor` (lexical_declaration, line 534)
- `absolute` (lexical_declaration, line 535)
- `successorText` (lexical_declaration, line 536)
- `inherited` (lexical_declaration, line 572)
- `live` (lexical_declaration, line 582)
- `holds` (lexical_declaration, line 583)
- `absorption` (lexical_declaration, line 607)
- `records` (lexical_declaration, line 643, exported)
- `convened` (lexical_declaration, line 644, exported)
- `letters` (lexical_declaration, line 645, exported)
- `uncovened` (lexical_declaration, line 646, exported)
- `view` (lexical_declaration, line 647, exported)
- `successorExists` (lexical_declaration, line 660, exported)
- `wanted` (lexical_declaration, line 665, exported)

## Uses

- [tools/core/analyzers/board.analyzer.ts](https://banes-lab.com/source/coordination/tools/core/analyzers/board.analyzer.ts.md)
- [tools/core/strings/converge.strings.ts](https://banes-lab.com/source/coordination/tools/core/strings/converge.strings.ts.md)

## Used by

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

## Source

```typescript
import {
    CHECK_ABSORPTION,
    CHECK_DIRECTIVES,
    CHECK_DURABLE,
    CHECK_INHERITANCE,
    CHECK_NEEDS,
    CHECK_SIGNATURES,
    CHECK_SUCCESSOR,
    DIRECTIVES_HOLD,
    INHERITANCE_HOLDS,
    NOBODY_TAKING_PART,
    NO_SUCCESSOR,
    SIGNATURES_HOLD,
    absorptionDuplicated,
    absorptionHolds,
    absorptionNoCloses,
    absorptionOpen,
    absorptionUndistributed,
    deferredUnanswered,
    deferredUndeclared,
    durableHold,
    durableUndeclared,
    durableUnresolved,
    needsHold,
    needsMissing,
    successorArrived,
    successorMissing,
    successorScheduled,
    unarrivedClauses,
    undischarged,
    unoriginated as unoriginatedMessage,
    unsigned,
} from "../strings/converge.strings.ts";
import { activeSeats, boardRecords } from "../analyzers/board.analyzer.ts";

import { basename, resolve } from "node:path";

import { existsSync, readFileSync, readdirSync } from "node:fs";

import { BLOCKING_SUFFIX } from "../constants/blocking.constants.ts";
import { fencedFlags } from "../predicates/fence.predicate.ts";
import { surfacePath } from "../../../config/surface.config.ts";

const SIGN_OFF_ABSENT = "—";

const NEEDS_FIELD = "Needs";

const DURABLE_FIELD = "Durable";

const INHERITED_HEADING = "INHERITED";

const PLACEHOLDER = "<";

const LIST_ITEM = "- ";

const AGENT_LABEL = "Agent ";

const SIGN_OFF_COLON_LIMIT = 4;

const SIGN_OFF_LETTER_LIMIT = 2;

const EMPHASIS = ["*", "`", "_"];

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

const isHeadingCharacter = function isHeadingCharacter(character: string): boolean {
    return (character >= "a" && character <= "z") || isDigit(character) || character === "-";
};

export const declaredHeading = function declaredHeading(field: string): string {
    return field.trim();
};

export const headingIsWhole = function headingIsWhole(field: string): boolean {
    const trimmed = field.trim();
    for (const character of trimmed) {
        if (!isHeadingCharacter(character)) {
            return false;
        }
    }
    return trimmed.length > 0;
};

export const CONVERGENCE_STEPS = [
    "needs",
    "signatures",
    "durable",
    "directives",
    "absorption",
    "inheritance",
    "successor",
] as const;

export const ARCHIVE_STEP = "archive";

export const DESTRUCTIVE_STEPS: readonly string[] = [...CONVERGENCE_STEPS, ARCHIVE_STEP];

export type ConvergenceStep = (typeof CONVERGENCE_STEPS)[number];

export interface Edge {
    readonly step: string;
    readonly edge: string;
    readonly holds: boolean;
    readonly detail: string;
}

const SUCCESSOR_FIELD = "SUCCESSOR:";

const DEFERRED_HEADING = "DEFERRED";

const DIRECTIVES_HEADING = "DIRECTIVES";

export const declaredSuccessor = function declaredSuccessor(venue: string): string {
    const fenced = fencedFlags(venue);
    const line = venue.split("\n").find((text, index) => fenced[index] !== true && text.trim().startsWith(SUCCESSOR_FIELD));
    const value = line === undefined ? "" : line.trim().slice(SUCCESSOR_FIELD.length).trim();
    return value.startsWith(PLACEHOLDER) ? "" : value;
};

const CLAUSE_ARROW = "→";

interface DeferredSection {
    readonly declared: boolean;
    readonly answered: boolean;
    readonly clauses: readonly string[];
}

interface ClauseLine {
    readonly clause: string;
    readonly receiver: string;
}

interface ClauseSection {
    readonly declared: boolean;
    readonly answered: boolean;
    readonly lines: readonly ClauseLine[];
}

export interface StrandedDeferral {
    readonly clause: string;
    readonly receiver: string;
}

const receiverOf = function receiverOf(line: string): string {
    const arrow = line.indexOf(CLAUSE_ARROW);
    if (arrow === -1) {
        return "";
    }

    const rest = line.slice(arrow + CLAUSE_ARROW.length).trim();
    const comma = rest.indexOf(",");
    return (comma === -1 ? rest : rest.slice(0, comma)).trim();
};

const unemphasised = function unemphasised(text: string): string {
    let held = text;
    for (const mark of EMPHASIS) {
        held = held.replaceAll(mark, "");
    }
    return held.trim();
};

const BANNER = "═";

const isSectionBoundary = function isSectionBoundary(trimmed: string): boolean {
    return trimmed.startsWith("#") || trimmed.startsWith(BANNER);
};

export const DEFERRED_SECTION = DEFERRED_HEADING;

export const DEFERRAL_ARROW = CLAUSE_ARROW;

export const sectionBound = function sectionBound(venue: string, heading: string): { from: number; to: number } | null {
    const lines = venue.split("\n");
    const boundaries = [...lines.keys()].filter((index) => isSectionBoundary((lines[index] ?? "").trim()));
    const from = boundaries.find((index) => (lines[index] ?? "").includes(heading));
    if (from === undefined) {
        return null;
    }
    return { from, to: boundaries.find((index) => index > from) ?? lines.length };
};

const clauseOf = function clauseOf(trimmed: string): string {
    const arrow = trimmed.indexOf(CLAUSE_ARROW);
    return unemphasised(arrow === -1 ? trimmed.slice(LIST_ITEM.length) : trimmed.slice(LIST_ITEM.length, arrow));
};

const isStatedClause = function isStatedClause(clause: string): boolean {
    return clause.length > 0 && !clause.startsWith(PLACEHOLDER) && clause !== SIGN_OFF_ABSENT;
};

const opensSection = function opensSection(trimmed: string, heading: string): boolean {
    return isSectionBoundary(trimmed) && trimmed.includes(heading);
};

const insideFlags = function insideFlags(lines: readonly string[], heading: string): boolean[] {
    const out: boolean[] = [];
    let inside = false;

    for (const trimmed of lines) {
        out.push(inside);
        inside = opensSection(trimmed, heading) || (inside && !isSectionBoundary(trimmed));
    }

    return out;
};

const clauseSection = function clauseSection(venue: string, heading: string): ClauseSection {
    const lines = venue.split("\n").map((line) => line.trim());
    const inside = insideFlags(lines, heading);
    const items = lines.filter(
        (trimmed, index) => inside[index] === true && !isSectionBoundary(trimmed) && trimmed.startsWith(LIST_ITEM),
    );

    return {
        answered: items.some((trimmed) => {
            const clause = clauseOf(trimmed);
            return isStatedClause(clause) || clause === SIGN_OFF_ABSENT;
        }),
        declared: lines.some((trimmed) => opensSection(trimmed, heading)),
        lines: items.flatMap((trimmed) => {
            const clause = clauseOf(trimmed);
            return isStatedClause(clause) ? [{ clause, receiver: receiverOf(trimmed) }] : [];
        }),
    };
};

const deferredClauses = function deferredClauses(venue: string): DeferredSection {
    const section = clauseSection(venue, DEFERRED_HEADING);
    return {
        answered: section.answered,
        clauses: section.lines.map((entry) => entry.clause),
        declared: section.declared,
    };
};

export const clauseLines = function clauseLines(venue: string, heading: string): readonly ClauseLine[] {
    return clauseSection(venue, heading).lines;
};

export const DISTRIBUTES_FIELD = "DISTRIBUTES:";

const CLOSES_FIELD = "CLOSES:";

const OPEN_TASK = "- [ ]";

export interface Absorption {
    readonly checklist: string;
    readonly open: readonly string[];
    readonly closes: string;
    readonly declaring: readonly string[];
}

interface PlannedSurface {
    readonly entry: string;
    readonly declares: boolean;
    readonly closes: string;
    readonly open: readonly string[];
}

const isAllDigits = function isAllDigits(text: string): boolean {
    for (const character of text) {
        if (!isDigit(character)) {
            return false;
        }
    }
    return text.length > 0;
};

const isTaskId = function isTaskId(token: string): boolean {
    return token.includes(".") && isAllDigits(token.replaceAll(".", ""));
};

const taskIdOf = function taskIdOf(trimmed: string): string {
    return trimmed.slice(OPEN_TASK.length).trim().split(" ")[0] ?? "";
};

const plannedSurface = function plannedSurface(planning: string, entry: string, venueName: string): PlannedSurface {
    const lines = readFileSync(resolve(planning, entry), "utf8")
        .split("\n")
        .map((line) => line.trim());
    const closesLine = lines.findLast((line) => line.startsWith(CLOSES_FIELD));

    return {
        closes: closesLine === undefined ? "" : closesLine.slice(CLOSES_FIELD.length).trim(),
        declares: lines.some((line) => line.startsWith(DISTRIBUTES_FIELD) && line.includes(venueName)),
        entry,
        open: lines
            .filter((line) => line.startsWith(OPEN_TASK))
            .map(taskIdOf)
            .filter(isTaskId),
    };
};

export const absorptionState = function absorptionState(repoRoot: string, venueName: string): Absorption | null {
    const planning = resolve(repoRoot, surfacePath("planning"));
    if (!existsSync(planning)) {
        return null;
    }

    const declaring = readdirSync(planning)
        .map((entry) => plannedSurface(planning, entry, venueName))
        .filter((surface) => surface.declares);
    const [first] = declaring;
    if (first === undefined) {
        return null;
    }

    return {
        checklist: first.entry,
        closes: first.closes,
        declaring: declaring.map((surface) => surface.entry),
        open: first.open.filter((id) => id !== first.closes),
    };
};

export interface Distribution {
    readonly path: string;
    readonly line: number;
    readonly declares: string;
}

export const declaredDistributions = function declaredDistributions(
    surfaces: readonly string[],
    read: (path: string) => string,
): Distribution[] {
    return surfaces.flatMap((path) =>
        read(path)
            .split("\n")
            .flatMap((line, index) => {
                const trimmed = line.trim();
                const declares = trimmed.startsWith(DISTRIBUTES_FIELD) ? trimmed.slice(DISTRIBUTES_FIELD.length).trim() : "";
                return declares.length === 0 || declares.startsWith(PLACEHOLDER) ? [] : [{ declares, line: index + 1, path }];
            }),
    );
};

const AWAITING_LINE = "READ AND AWAITING:";

export const hasReadMark = function hasReadMark(venue: string): boolean {
    return venue
        .split("\n")
        .map((line) => line.trim())
        .filter((line) => line.startsWith(AWAITING_LINE))
        .some((line) =>
            line
                .slice(AWAITING_LINE.length)
                .split(" ")
                .map((token) => token.trim())
                .some((letter) => letter.length > 0 && letter !== SIGN_OFF_ABSENT),
        );
};

export const venueInvariant = function venueInvariant(name: string): string {
    const base = basename(name);
    const stem = base.endsWith(BLOCKING_SUFFIX) ? base.slice(0, base.length - BLOCKING_SUFFIX.length) : base;
    const dot = stem.lastIndexOf(".");
    return dot === -1 ? stem : stem.slice(0, dot);
};

export const addressesSuccessor = function addressesSuccessor(receiver: string, successorName: string): boolean {
    return receiver.length > 0 && successorName.length > 0 && venueInvariant(successorName) === receiver;
};

export const unarrivedDeferrals = function unarrivedDeferrals(
    venue: string,
    successorText: string,
    successorName = "",
): readonly ClauseLine[] {
    return clauseLines(venue, DEFERRED_HEADING)
        .filter((entry) => addressesSuccessor(entry.receiver, successorName))
        .filter((entry) => !successorText.includes(entry.clause));
};

export const unoriginatedInheritance = function unoriginatedInheritance(venue: string, successorText: string): string[] {
    const deferred = clauseLines(venue, DEFERRED_HEADING).map((entry) => entry.clause);
    return clauseLines(successorText, INHERITED_HEADING)
        .map((entry) => entry.clause)
        .filter((clause) => !deferred.some((name) => name.includes(clause) || clause.includes(name)));
};

export const receiverResolves = function receiverResolves(
    receiver: string,
    board: string,
    index: string,
    venueNames: readonly string[],
    planned: readonly string[] = [],
): boolean {
    if (receiver.length === 0) {
        return false;
    }
    if (new Set(activeSeats(board, index)).has(receiver)) {
        return true;
    }
    return venueNames.some((name) => venueInvariant(name) === receiver) || planned.includes(receiver);
};

export const strandedDeferrals = function strandedDeferrals(
    venue: string,
    board: string,
    index: string,
    venueNames: readonly string[],
    planned: readonly string[] = [],
): StrandedDeferral[] {
    return clauseLines(venue, DEFERRED_HEADING)
        .filter((entry) => entry.receiver === entry.clause || !receiverResolves(entry.receiver, board, index, venueNames, planned))
        .map((entry) => ({ clause: entry.clause, receiver: entry.receiver }));
};

export const SIGN_OFF_BANNER = "═══════════════════ SIGN-OFF";

export const signOffSection = function signOffSection(venue: string): string[] {
    const lines = venue.split("\n");
    const opens = lines.findIndex((line) => line.startsWith(SIGN_OFF_BANNER));
    return opens === -1 ? [] : lines.slice(opens + 1);
};

const isSignOffLetter = function isSignOffLetter(letter: string): boolean {
    const first = letter.charAt(0);
    return letter.length > 0 && letter.length <= SIGN_OFF_LETTER_LIMIT && first >= "A" && first <= "Z";
};

const signOffEntry = function signOffEntry(line: string): [string, string] | null {
    const colon = line.indexOf(":");
    const letter = colon <= 0 || colon > SIGN_OFF_COLON_LIMIT ? "" : line.slice(0, colon).trim();
    return isSignOffLetter(letter) ? [letter, line.slice(colon + 1).trim()] : null;
};

const signOffLines = function signOffLines(venue: string): Map<string, string> {
    const entries = signOffSection(venue)
        .map(signOffEntry)
        .filter((entry): entry is [string, string] => entry !== null);
    return new Map(entries.toReversed());
};

export const venueRecords = function venueRecords(venue: string): Map<string, ReadonlyMap<string, string>> {
    return new Map(
        boardRecords(venue)
            .filter((record) => record.kind === "agent")
            .map((record): [string, ReadonlyMap<string, string>] => [record.label.slice(AGENT_LABEL.length).trim(), record.fields]),
    );
};

const successorOf = function successorOf(target: string): string {
    const stem = target.slice(0, target.length - BLOCKING_SUFFIX.length);
    const dot = stem.lastIndexOf(".");
    const ordinal = dot === -1 ? Number.NaN : Number(stem.slice(dot + 1));
    return Number.isFinite(ordinal) ? `${String(ordinal + 1)}${BLOCKING_SUFFIX}` : "";
};

type Records = ReadonlyMap<string, ReadonlyMap<string, string>>;

interface SuccessorView {
    readonly declared: string;
    readonly successor: string;
    readonly successorText: string;
    readonly deferred: DeferredSection;
    readonly unarrived: readonly string[];
    readonly elsewhere: readonly string[];
    readonly scheduled: boolean;
}

const fieldOf = function fieldOf(records: Records, letter: string, field: string): string {
    return (records.get(letter)?.get(field) ?? "").trim();
};

const needsEdge = function needsEdge(letters: readonly string[], uncovened: readonly string[], records: Records): Edge {
    const unstated = letters.filter((letter) => fieldOf(records, letter, NEEDS_FIELD).length === 0);
    const vacuous = letters.length === 0;
    const holds = !vacuous && unstated.length === 0;
    const failing = vacuous ? NOBODY_TAKING_PART : needsMissing(unstated);
    return { detail: holds ? needsHold(letters.length, uncovened) : failing, edge: CHECK_NEEDS, holds, step: "needs" };
};

const signaturesEdge = function signaturesEdge(letters: readonly string[], venue: string): Edge {
    const signatures = signOffLines(venue);
    const outstanding = letters.filter((letter) => {
        const signed = (signatures.get(letter) ?? "").trim();
        return signed.length === 0 || signed === SIGN_OFF_ABSENT;
    });
    const vacuous = letters.length === 0;
    const holds = !vacuous && outstanding.length === 0;
    const failing = vacuous ? NOBODY_TAKING_PART : unsigned(outstanding);
    return { detail: holds ? SIGNATURES_HOLD : failing, edge: CHECK_SIGNATURES, holds, step: "signatures" };
};

const durableDeclared = function durableDeclared(records: Records, letter: string): boolean {
    return records.get(letter)?.has(DURABLE_FIELD) === true && fieldOf(records, letter, DURABLE_FIELD).length > 0;
};

const durableFailure = function durableFailure(undeclared: readonly string[], unresolved: readonly string[]): string {
    return [
        undeclared.length === 0 ? "" : durableUndeclared(undeclared),
        unresolved.length === 0 ? "" : durableUnresolved(unresolved),
    ]
        .filter((part) => part.length > 0)
        .join("; ");
};

const durableEdge = function durableEdge(letters: readonly string[], records: Records, archive: string): Edge {
    const undeclared = letters.filter((letter) => !durableDeclared(records, letter));
    const stated = letters.filter((letter) => durableDeclared(records, letter));
    const headingOf = (letter: string): string => declaredHeading(fieldOf(records, letter, DURABLE_FIELD));

    const headings = stated.map((letter) => (headingOf(letter) === SIGN_OFF_ABSENT ? `${letter} → none` : headingOf(letter)));
    const unresolved = stated
        .filter((letter) => headingOf(letter) !== SIGN_OFF_ABSENT)
        .filter((letter) => !headingIsWhole(headingOf(letter)) || !archive.includes(headingOf(letter)))
        .map((letter) => `${letter} → ${headingOf(letter)}`);

    const vacuous = letters.length === 0;
    const holds = !vacuous && undeclared.length === 0 && unresolved.length === 0;
    const failing = vacuous ? NOBODY_TAKING_PART : durableFailure(undeclared, unresolved);
    return { detail: holds ? durableHold(headings) : failing, edge: CHECK_DURABLE, holds, step: "durable" };
};

const successorName = function successorName(target: string, declared: string, siblings: readonly string[]): string {
    const wanted = successorOf(target);
    const matches = (name: string): boolean =>
        declared.length > 0 ? basename(name).startsWith(declared) : name.endsWith(wanted);
    return siblings.find((name) => matches(name) && name !== target) ?? "";
};

const successorView = function successorView(
    repoRoot: string,
    target: string,
    venue: string,
    siblings: readonly string[],
    planned: readonly string[],
): SuccessorView {
    const declared = declaredSuccessor(venue);
    const successor = successorName(target, declared, siblings);
    const absolute = successor.length === 0 ? "" : resolve(repoRoot, successor);
    const successorText = absolute.length > 0 && existsSync(absolute) ? readFileSync(absolute, "utf8") : "";

    return {
        declared,
        deferred: deferredClauses(venue),
        elsewhere: clauseLines(venue, DEFERRED_HEADING)
            .filter((entry) => successor.length > 0 && !addressesSuccessor(entry.receiver, successor))
            .map((entry) => `${entry.clause} → ${entry.receiver}`),
        scheduled: declared.length > 0 && planned.includes(declared),
        successor,
        successorText,
        unarrived: unarrivedDeferrals(venue, successorText, successor).map((entry) => entry.clause),
    };
};

const inheritsIntoSuccessor = function inheritsIntoSuccessor(view: SuccessorView): boolean {
    return view.successor.length > 0 && view.successorText.includes(INHERITED_HEADING) && view.unarrived.length === 0;
};

const successorHolds = function successorHolds(view: SuccessorView): boolean {
    return view.deferred.declared && view.deferred.answered && (view.scheduled || inheritsIntoSuccessor(view));
};

const successorDetail = function successorDetail(view: SuccessorView, holds: boolean): string {
    if (holds && view.scheduled && view.successor.length === 0) {
        return successorScheduled(view.declared);
    }
    if (holds) {
        return successorArrived(view.successor, view.elsewhere, view.deferred.clauses);
    }
    if (!view.deferred.declared) {
        return deferredUndeclared(DEFERRED_HEADING);
    }
    if (!view.deferred.answered) {
        return deferredUnanswered(DEFERRED_HEADING);
    }
    const inherited = view.successor.length > 0 && view.successorText.includes(INHERITED_HEADING);
    return inherited ? unarrivedClauses(view.unarrived) : successorMissing(INHERITED_HEADING, view.declared);
};

const successorEdge = function successorEdge(view: SuccessorView): Edge {
    const holds = successorHolds(view);
    return { detail: successorDetail(view, holds), edge: CHECK_SUCCESSOR, holds, step: "successor" };
};

const directivesEdge = function directivesEdge(venue: string): Edge {
    const live = clauseLines(venue, DIRECTIVES_HEADING).map((entry) => entry.clause);
    const holds = live.length === 0;
    return { detail: holds ? DIRECTIVES_HOLD : undischarged(live), edge: CHECK_DIRECTIVES, holds, step: "directives" };
};

const absorbed = function absorbed(absorption: Absorption | null): boolean {
    return absorption?.declaring.length === 1 && absorption.closes.length > 0 && absorption.open.length === 0;
};

const absorptionDetail = function absorptionDetail(absorption: Absorption | null, target: string): string {
    if (absorption === null) {
        return absorptionUndistributed(basename(target), DISTRIBUTES_FIELD);
    }
    if (absorption.declaring.length > 1) {
        return absorptionDuplicated(absorption.declaring);
    }
    if (absorption.closes.length === 0) {
        return absorptionNoCloses(absorption.checklist, CLOSES_FIELD);
    }
    return absorption.open.length === 0
        ? absorptionHolds(absorption.checklist, absorption.closes)
        : absorptionOpen(absorption.checklist, absorption.open);
};

const absorptionEdge = function absorptionEdge(repoRoot: string, target: string): Edge {
    const absorption = absorptionState(repoRoot, basename(target));
    return {
        detail: absorptionDetail(absorption, target),
        edge: CHECK_ABSORPTION,
        holds: absorbed(absorption),
        step: "absorption",
    };
};

const inheritanceDetail = function inheritanceDetail(successor: string, unoriginated: readonly string[]): string {
    if (successor.length === 0) {
        return NO_SUCCESSOR;
    }
    return unoriginated.length === 0 ? INHERITANCE_HOLDS : unoriginatedMessage(unoriginated);
};

const inheritanceEdge = function inheritanceEdge(view: SuccessorView, venue: string): Edge {
    const unoriginated = unoriginatedInheritance(venue, view.successorText);
    return {
        detail: inheritanceDetail(view.successor, unoriginated),
        edge: CHECK_INHERITANCE,
        holds: view.successor.length === 0 || unoriginated.length === 0,
        step: "inheritance",
    };
};

export const convergenceEdges = function convergenceEdges(
    repoRoot: string,
    target: string,
    venue: string,
    board: string,
    index: string,
    archive: string,
    siblings: readonly string[],
    plannedInvariants: readonly string[] = [],
): Edge[] {
    const records = venueRecords(venue);
    const convened = activeSeats(board, index);
    const letters = convened.filter((letter) => records.has(letter));
    const uncovened = convened.filter((letter) => !records.has(letter));
    const view = successorView(repoRoot, target, venue, siblings, plannedInvariants);

    return [
        needsEdge(letters, uncovened, records),
        signaturesEdge(letters, venue),
        durableEdge(letters, records, archive),
        successorEdge(view),
        directivesEdge(venue),
        absorptionEdge(repoRoot, target),
        inheritanceEdge(view, venue),
    ];
};

export const successorExists = function successorExists(
    repoRoot: string,
    target: string,
    siblings: readonly string[],
): boolean {
    const wanted = successorOf(target);
    return siblings.some((name) => name.endsWith(wanted) && name !== target && existsSync(resolve(repoRoot, name)));
};
```
