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 = 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) }; };