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(); 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; } interface OpenRecord { readonly kind: RecordKind; readonly label: string; readonly state: string; readonly line: number; readonly fields: Map; } const HEADINGS: ReadonlyMap = 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; };