95 lines
3.6 KiB
TypeScript
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;
|
|
}
|