import { authorOf, closureText } from "../generators/board.generator.ts"; import { classesFiled, clauseExtended, clauseMissing, entryAdded, entryExists, entryNotFound, extendContended, extendMissingHistory, extendNotFound, historyContended, historyMissing, leadUndeclared, leadsInPlace, leadsMissing, leadsRepaired, repairContended, repairMissingHistory, } from "../strings/archive.strings.ts"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { appendSection } from "../formatters/text.formatter.ts"; import { closureRefusal } from "../validators/archive.validator.ts"; import { itemKindOf } from "../formatters/board.formatter.ts"; export interface ClosureRequest { readonly absolute: string; readonly archive: string; readonly index: string; readonly closes: string; readonly agent: string | null; readonly ref: string | null; } export interface ClosureOutcome { readonly refusal: string | null; readonly author: string; readonly label: string; readonly acknowledged: boolean; } export const JUDGEMENT_KIND = "judgement"; const ENTRY_MARKER = "### "; const ENTRY_LEADS = ["POPULATION:", "BOUNDARY:"]; const declaresLead = function declaresLead(body: string, lead: string): boolean { return body.split("\n").some((line) => line.startsWith(lead)); }; export interface EntryRequest { readonly archive: string; readonly heading: string; readonly body: string; } export interface EntryOutcome { readonly message: string; readonly code: number; } const HEADING_WORD = new Set("abcdefghijklmnopqrstuvwxyz0123456789-"); export const filedHeadings = function filedHeadings(source: string): string[] { const out: string[] = []; for (const line of source.split("\n")) { if (!line.startsWith(ENTRY_MARKER)) { continue; } const heading = line.slice(ENTRY_MARKER.length).trim(); if (heading.length > 0) { out.push(heading); } } return out; }; export const namesAClass = function namesAClass(heading: string): boolean { if (heading.length === 0) { return false; } for (const character of heading) { if (!HEADING_WORD.has(character)) { return false; } } return true; }; export const classHeadings = function classHeadings(source: string): string[] { return filedHeadings(source).filter((heading) => namesAClass(heading)); }; export const filedNotice = function filedNotice(source: string): string { const filed = filedHeadings(source); if (filed.length === 0) { return ""; } const classes = filed.filter((heading) => namesAClass(heading)); return classesFiled(classes, filed.length - classes.length); }; export const runEntry = function runEntry(request: EntryRequest): EntryOutcome { if (!existsSync(request.archive)) { return { code: 2, message: historyMissing(request.archive) }; } const before = readFileSync(request.archive, "utf8"); if (before.includes(`${ENTRY_MARKER}${request.heading}`)) { return { code: 2, message: `${entryExists(request.heading, request.archive)}${filedNotice(before)}` }; } const missing = ENTRY_LEADS.filter((lead) => !declaresLead(request.body, lead)); if (missing.length > 0) { return { code: 2, message: leadsMissing(missing) }; } const written = appendSection(before, `${ENTRY_MARKER}${request.heading}`, request.body); const witness = readFileSync(request.archive, "utf8"); if (witness !== before) { return { code: 2, message: historyContended(request.archive) }; } writeFileSync(request.archive, written, "utf8"); return { code: 0, message: `${entryAdded(request.heading, request.archive)}${filedNotice(before)}` }; }; const entryEnd = function entryEnd(lines: readonly string[], opens: number): number { const next = lines.findIndex((line, index) => index > opens && line.startsWith(ENTRY_MARKER)); return next === -1 ? lines.length : next; }; interface Promotion { readonly lines: string[]; readonly promoted: string[]; } const promoteLeads = function promoteLeads(line: string): Promotion { const lines: string[] = []; const promoted: string[] = []; let held = line; for (const lead of ENTRY_LEADS) { const at = held.indexOf(lead); if (at > 0) { lines.push(held.slice(0, at).trimEnd()); held = held.slice(at); promoted.push(lead); } } return { lines: [...lines, held], promoted }; }; export const runRepair = function runRepair(request: { archive: string; heading: string }): EntryOutcome { if (!existsSync(request.archive)) { return { code: 2, message: repairMissingHistory(request.archive) }; } const before = readFileSync(request.archive, "utf8"); const lines = before.split("\n"); const opens = lines.findIndex((line) => line.startsWith(`${ENTRY_MARKER}${request.heading}`)); if (opens === -1) { return { code: 2, message: `${entryNotFound(request.heading, request.archive)}${filedNotice(before)}` }; } const end = entryEnd(lines, opens); const promotions = lines.slice(opens + 1, end).map(promoteLeads); const promoted = promotions.flatMap((promotion) => promotion.promoted); const out = [...lines.slice(0, opens + 1), ...promotions.flatMap((promotion) => promotion.lines), ...lines.slice(end)]; if (promoted.length === 0) { return { code: 0, message: leadsInPlace(request.heading) }; } const witness = readFileSync(request.archive, "utf8"); if (witness !== before) { return { code: 2, message: repairContended(request.archive) }; } writeFileSync(request.archive, out.join("\n"), "utf8"); return { code: 0, message: leadsRepaired(promoted, request.heading) }; }; export interface ExtendRequest { readonly archive: string; readonly heading: string; readonly lead: string; readonly body: string; } const clauseEnd = function clauseEnd(lines: readonly string[], opens: number, lead: string): number { const end = entryEnd(lines, opens); const starts = lines.findIndex((line, index) => index > opens && index < end && line.startsWith(lead)); if (starts === -1) { return -1; } const blank = lines.findIndex((line, index) => index > starts && index < end && line.trim().length === 0); return (blank === -1 ? end : blank) - 1; }; export const runExtend = function runExtend(request: ExtendRequest): EntryOutcome { if (!existsSync(request.archive)) { return { code: 2, message: extendMissingHistory(request.archive) }; } if (!ENTRY_LEADS.includes(request.lead)) { return { code: 2, message: leadUndeclared(request.lead, ENTRY_LEADS) }; } const before = readFileSync(request.archive, "utf8"); const lines = before.split("\n"); const opens = lines.findIndex((line) => line.startsWith(`${ENTRY_MARKER}${request.heading}`)); if (opens === -1) { return { code: 2, message: `${extendNotFound(request.heading, request.archive)}${filedNotice(before)}` }; } const at = clauseEnd(lines, opens, request.lead); if (at === -1) { return { code: 2, message: clauseMissing(request.heading, request.lead) }; } const written = [...lines.slice(0, at + 1), request.body, ...lines.slice(at + 1)].join("\n"); const witness = readFileSync(request.archive, "utf8"); if (witness !== before) { return { code: 2, message: extendContended(request.archive) }; } writeFileSync(request.archive, written, "utf8"); return { code: 0, message: clauseExtended(request.lead, request.heading, request.archive) }; }; export const runClosure = function runClosure(request: ClosureRequest): ClosureOutcome { const carried = existsSync(request.archive) ? readFileSync(request.archive, "utf8") : ""; const source = readFileSync(request.absolute, "utf8"); const index = existsSync(request.index) ? readFileSync(request.index, "utf8") : ""; const text = closureText(request.closes, request.ref ?? "", source, request.agent ?? "", index); const kind = itemKindOf(source, request.closes); return { acknowledged: kind === JUDGEMENT_KIND, author: authorOf(request.closes), label: `handled by ${request.agent ?? ""}`, refusal: closureRefusal(request.closes, request.ref, request.agent, text, carried, kind), }; };