fable-agent/src/fable5/structural-patch-receipt.ts

95 lines
3.6 KiB
TypeScript

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