import * as fs from "node:fs"; import * as path from "node:path"; import type { UastLanguage } from "./uast-runtime-receipt.js"; export type StructuralNodeKind = "ScopeNode" | "ControlNode" | "StateNode" | "LeafNode"; export type StructuralPatchDecision = "surgical" | "blocked"; export interface StructuralNodeCoordinate { file: string; language: UastLanguage; nodeKind: StructuralNodeKind; languageKind: string; path: number[]; start: number; end: number; baseSha: string; } export interface StructuralPatchCheck { name: string; command: string; status: "pass" | "fail"; } export interface StructuralPatchReceiptOptions { taskLabel: string; target: StructuralNodeCoordinate; parserReceipt?: string; serializerReceipt?: string; checks: StructuralPatchCheck[]; rawStringPatch?: boolean; deployAttempted?: boolean; now?: Date; } export interface StructuralPatchReceipt { schema: "fable.structural_patch.receipt.v1"; createdAt: string; taskLabel: string; target: StructuralNodeCoordinate; parserReceipt?: string; serializerReceipt?: string; checks: StructuralPatchCheck[]; lockedSurroundingTree: boolean; decision: StructuralPatchDecision; reasons: string[]; skipped: string[]; deployAttempted: false; } function hasText(value: string | undefined): boolean { return Boolean(value?.trim()); } export function createStructuralPatchReceipt(opts: StructuralPatchReceiptOptions): StructuralPatchReceipt { const reasons: string[] = []; if (!hasText(opts.taskLabel)) reasons.push("missing task label"); if (!hasText(opts.target.file)) reasons.push("missing target file"); if (!hasText(opts.target.languageKind)) reasons.push("missing language node kind"); if (!hasText(opts.target.baseSha)) reasons.push("missing base sha"); if (opts.target.path.length === 0) reasons.push("missing node path coordinates"); if (opts.target.path.some((segment) => !Number.isInteger(segment) || segment < 0)) reasons.push("invalid node path coordinates"); if (!Number.isInteger(opts.target.start) || !Number.isInteger(opts.target.end) || opts.target.start < 0 || opts.target.end <= opts.target.start) { reasons.push("invalid target span"); } if (!hasText(opts.parserReceipt)) reasons.push("missing parser support receipt"); if (!hasText(opts.serializerReceipt)) reasons.push("missing serializer support receipt"); if (opts.checks.length === 0) reasons.push("missing validation checks"); if (opts.checks.some((check) => !hasText(check.name) || !hasText(check.command))) reasons.push("validation checks need names and commands"); if (opts.checks.some((check) => check.status !== "pass")) reasons.push("validation checks must pass"); if (opts.rawStringPatch) reasons.push("raw string patch is not surgical"); if (opts.deployAttempted) reasons.push("deploy is out of scope for structural patch receipts"); return { schema: "fable.structural_patch.receipt.v1", createdAt: (opts.now ?? new Date()).toISOString(), taskLabel: opts.taskLabel, target: opts.target, parserReceipt: opts.parserReceipt, serializerReceipt: opts.serializerReceipt, checks: opts.checks, lockedSurroundingTree: reasons.length === 0, decision: reasons.length === 0 ? "surgical" : "blocked", reasons, skipped: ["raw string concatenation", "full universal UAST runtime", "cross-language pristine writer", "deploy"], deployAttempted: false, }; } export function writeStructuralPatchReceipt(file: string, receipt: StructuralPatchReceipt): string { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`); return file; }