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

> 197 lines of code and 66 definitions.

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

## Definitions

- `absentFields` (lexical_declaration, line 61)
- `isLetter` (lexical_declaration, line 130)
- `citesAt` (lexical_declaration, line 168)
- `selfCorrections` (lexical_declaration, line 196, exported)
- `venueSchemaGaps` (lexical_declaration, line 107, exported)
- `citesOwnLetter` (lexical_declaration, line 175)
- `positionsOutsideRecords` (lexical_declaration, line 226, exported)
- `fieldNameOf` (lexical_declaration, line 22)
- `venueFieldsFrom` (lexical_declaration, line 46, exported)
- `isDigit` (lexical_declaration, line 164)
- `claimKeyOf` (lexical_declaration, line 211)
- `recordStep` (lexical_declaration, line 89)
- `venueFieldAt` (lexical_declaration, line 84)
- `labelWord` (lexical_declaration, line 134)
- `depthChanges` (lexical_declaration, line 218)
- `POSITION_OPENER` (lexical_declaration, line 5)
- `FIELD_INDENT` (lexical_declaration, line 7)
- `SchemaGap` (interface_declaration, line 9, exported)
- `RECORD_OPENER` (lexical_declaration, line 16)
- `RECORD_CLOSER` (lexical_declaration, line 18)
- `DEEPER_INDENT` (lexical_declaration, line 20)
- `isRecordBoundary` (lexical_declaration, line 28)
- `recordFlags` (lexical_declaration, line 32)
- `next` (lexical_declaration, line 39)
- `inside` (lexical_declaration, line 49, exported)
- `fields` (lexical_declaration, line 51, exported)
- `eligible` (lexical_declaration, line 53, exported)
- `RecordState` (interface_declaration, line 72)
- `RecordStep` (interface_declaration, line 77)
- `NO_RECORD` (lexical_declaration, line 82)
- `indented` (lexical_declaration, line 85)
- `field` (lexical_declaration, line 97, exported)
- `declared` (lexical_declaration, line 108, exported)
- `state` (lexical_declaration, line 115, exported)
- `step` (lexical_declaration, line 118, exported)
- `CONTRADICTS_LEAD` (lexical_declaration, line 126)
- `FIELD_BLOCK_LEAD` (lexical_declaration, line 128)
- `trimmed` (lexical_declaration, line 135, exported)
- `colon` (lexical_declaration, line 136)
- `label` (lexical_declaration, line 137)
- `positionFieldLabels` (lexical_declaration, line 146, exported)
- `start` (lexical_declaration, line 148, exported)
- `words` (lexical_declaration, line 153, exported)
- `first` (lexical_declaration, line 154, exported)
- `block` (lexical_declaration, line 155, exported)
- `end` (lexical_declaration, line 156, exported)
- `SIGNED_WORD` (lexical_declaration, line 160)
- `SIGNED_LEAD` (lexical_declaration, line 162, exported)
- `before` (lexical_declaration, line 169)
- `cursor` (lexical_declaration, line 171)
- `index` (lexical_declaration, line 176)
- `contradictionClause` (lexical_declaration, line 186)
- `opens` (lexical_declaration, line 187)
- `after` (lexical_declaration, line 191)
- `stop` (lexical_declaration, line 192)
- `lines` (lexical_declaration, line 197, exported)
- `clause` (lexical_declaration, line 200, exported)
- `StrayPosition` (interface_declaration, line 206, exported)
- `rest` (lexical_declaration, line 212)
- `dash` (lexical_declaration, line 213)
- `key` (lexical_declaration, line 214)
- `fenced` (lexical_declaration, line 227, exported)
- `changes` (lexical_declaration, line 228, exported)
- `out` (lexical_declaration, line 229, exported)
- `depth` (lexical_declaration, line 230, exported)
- `stray` (lexical_declaration, line 234, 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)

## Used by

- [tools/rules/blocking.rule.ts](https://banes-lab.com/source/coordination/tools/rules/blocking.rule.ts.md)
- [tools/rules/surface.rule.ts](https://banes-lab.com/source/coordination/tools/rules/surface.rule.ts.md)
- [tools/rules/venue.rule.ts](https://banes-lab.com/source/coordination/tools/rules/venue.rule.ts.md)

## Source

```typescript
import { delimitersIn } from "../analyzers/board.analyzer.ts";
import { fencedFlags } from "../predicates/fence.predicate.ts";
import { itemSpans } from "../resolvers/sweep.resolver.ts";

const POSITION_OPENER = "Position ";

const FIELD_INDENT = "  ";

export interface SchemaGap {
    readonly record: string;
    readonly field: string;
    readonly declared: readonly string[];
    readonly state: "absent" | "undeclared";
}

const RECORD_OPENER = "Agent ";

const RECORD_CLOSER = "└───";

const DEEPER_INDENT = `${FIELD_INDENT} `;

const fieldNameOf = function fieldNameOf(trimmed: string): string | null {
    const colon = trimmed.indexOf(":");
    const field = colon <= 0 ? "" : trimmed.slice(0, colon).trim();
    return field.length === 0 || field.includes(" ") ? null : field;
};

const isRecordBoundary = function isRecordBoundary(trimmed: string): boolean {
    return trimmed.startsWith(RECORD_OPENER) || trimmed.startsWith(RECORD_CLOSER);
};

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

    for (const [index, line] of lines.entries()) {
        out.push(inside);
        const trimmed = line.trim();
        const next: boolean = trimmed.startsWith(RECORD_OPENER) || (inside && !trimmed.startsWith(RECORD_CLOSER));
        inside = fenced[index] === true ? next : inside;
    }

    return out;
};

export const venueFieldsFrom = function venueFieldsFrom(template: string): string[] {
    const lines = template.split("\n");
    const fenced = fencedFlags(template);
    const inside = recordFlags(lines, fenced);

    const fields = lines.flatMap((line, index) => {
        const trimmed = line.trim();
        const eligible = fenced[index] === true && inside[index] === true && !isRecordBoundary(trimmed);
        const field = eligible && line.startsWith(FIELD_INDENT) ? fieldNameOf(trimmed) : null;
        return field === null ? [] : [field];
    });

    return [...new Set(fields)];
};

const absentFields = function absentFields(
    record: string,
    seen: ReadonlySet<string>,
    declared: readonly string[],
): SchemaGap[] {
    if (record.length === 0) {
        return [];
    }
    return declared.filter((field) => !seen.has(field)).map((field) => ({ declared, field, record, state: "absent" }));
};

interface RecordState {
    readonly record: string;
    readonly seen: ReadonlySet<string>;
}

interface RecordStep {
    readonly gaps: readonly SchemaGap[];
    readonly next: RecordState;
}

const NO_RECORD: RecordState = { record: "", seen: new Set() };

const venueFieldAt = function venueFieldAt(line: string, record: string): string | null {
    const indented = record.length > 0 && line.startsWith(FIELD_INDENT) && !line.startsWith(DEEPER_INDENT);
    return indented ? fieldNameOf(line.trim()) : null;
};

const recordStep = function recordStep(state: RecordState, line: string, declared: readonly string[]): RecordStep {
    if (line.startsWith(RECORD_OPENER)) {
        return { gaps: absentFields(state.record, state.seen, declared), next: { record: line.trim(), seen: new Set() } };
    }
    if (line.startsWith(RECORD_CLOSER)) {
        return { gaps: absentFields(state.record, state.seen, declared), next: NO_RECORD };
    }

    const field = venueFieldAt(line, state.record);
    if (field === null) {
        return { gaps: [], next: state };
    }
    if (declared.includes(field)) {
        return { gaps: [], next: { ...state, seen: new Set([...state.seen, field]) } };
    }
    return { gaps: [{ declared, field, record: state.record, state: "undeclared" }], next: state };
};

export const venueSchemaGaps = function venueSchemaGaps(source: string, template: string): SchemaGap[] {
    const declared = venueFieldsFrom(template);
    if (declared.length === 0) {
        return [];
    }

    const fenced = fencedFlags(source);
    const out: SchemaGap[] = [];
    let state = NO_RECORD;

    for (const [index, line] of source.split("\n").entries()) {
        const step = fenced[index] === true ? { gaps: [], next: state } : recordStep(state, line, declared);
        out.push(...step.gaps);
        state = step.next;
    }

    return [...out, ...absentFields(state.record, state.seen, declared)];
};

const CONTRADICTS_LEAD = "Contradicts:";

const FIELD_BLOCK_LEAD = "carries these fields";

const isLetter = function isLetter(char: string): boolean {
    return (char >= "A" && char <= "Z") || (char >= "a" && char <= "z");
};

const labelWord = function labelWord(line: string): string {
    const trimmed = line.trim();
    const colon = trimmed.indexOf(":");
    const label = colon <= 0 ? "" : trimmed.slice(0, colon);
    for (const char of label) {
        if (!isLetter(char)) {
            return "";
        }
    }
    return label;
};

export const positionFieldLabels = function positionFieldLabels(template: string): string[] {
    const lines = template.split("\n");
    const start = lines.findIndex((line) => line.includes(FIELD_BLOCK_LEAD));
    if (start === -1) {
        return [];
    }

    const words = lines.slice(start + 1).map(labelWord);
    const first = words.findIndex((word) => word.length > 0 && word !== SIGNED_WORD);
    const block = first === -1 ? [] : words.slice(first);
    const end = block.indexOf("");
    return (end === -1 ? block : block.slice(0, end)).filter((word) => word !== SIGNED_WORD);
};

const SIGNED_WORD = "Signed";

export const SIGNED_LEAD = "Signed:";

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

const citesAt = function citesAt(clause: string, index: number, letter: string): boolean {
    const before = index === 0 ? " " : clause.charAt(index - 1);
    const after = index + letter.length;
    const cursor = clause.charAt(after) === "-" ? after + 1 : after;
    return !isLetter(before) && isDigit(clause.charAt(cursor));
};

const citesOwnLetter = function citesOwnLetter(clause: string, letter: string): boolean {
    let index = letter.length === 0 ? -1 : clause.indexOf(letter);
    while (index !== -1) {
        if (citesAt(clause, index, letter)) {
            return true;
        }
        index = clause.indexOf(letter, index + 1);
    }
    return false;
};

const contradictionClause = function contradictionClause(body: string): string | null {
    const opens = body.indexOf(CONTRADICTS_LEAD);
    if (opens === -1) {
        return null;
    }
    const after = body.slice(opens + CONTRADICTS_LEAD.length);
    const stop = after.indexOf(SIGNED_LEAD);
    return stop === -1 ? after : after.slice(0, stop);
};

export const selfCorrections = function selfCorrections(source: string): string[] {
    const lines = source.split("\n");
    return itemSpans(source)
        .filter((span) => {
            const clause = contradictionClause(lines.slice(span.from, span.through).join(" "));
            return clause !== null && citesOwnLetter(clause, span.agent);
        })
        .map((span) => span.key);
};

export interface StrayPosition {
    readonly key: string;
    readonly line: number;
}

const claimKeyOf = function claimKeyOf(line: string): string {
    const rest = line.slice(POSITION_OPENER.length);
    const dash = rest.indexOf(" —");
    const key = dash === -1 ? rest : rest.slice(0, dash);
    return key.trim();
};

const depthChanges = function depthChanges(source: string): Map<number, number> {
    const out = new Map<number, number>();
    for (const mark of delimitersIn(source)) {
        out.set(mark.line, (out.get(mark.line) ?? 0) + (mark.open ? 1 : -1));
    }
    return out;
};

export const positionsOutsideRecords = function positionsOutsideRecords(source: string): StrayPosition[] {
    const fenced = fencedFlags(source);
    const changes = depthChanges(source);
    const out: StrayPosition[] = [];
    let depth = 0;

    for (const [index, line] of source.split("\n").entries()) {
        depth += changes.get(index + 1) ?? 0;
        const stray = fenced[index] !== true && depth <= 0 && line.startsWith(POSITION_OPENER);
        if (stray) {
            out.push({ key: claimKeyOf(line), line: index + 1 });
        }
    }

    return out;
};
```
