import * as fs from "node:fs"; import * as path from "node:path"; import type { PlanPhase } from "./plan-receipt.js"; export interface ResearchMapReceipt { schema: "fable.research_map.receipt.v1"; createdAt: string; taskLabel: string; task: string; constraints: string[]; existingPrimitives: string[]; recommendedPlan: PlanPhase[]; skipped: string[]; decision: "ready" | "blocked"; reasons: string[]; deployAttempted: false; } export interface ResearchMapOptions { taskLabel: string; task: string; constraints?: string[]; existingPrimitives?: string[]; recommendedPlan: PlanPhase[]; skipped?: string[]; now?: Date; } const FORBIDDEN_SCOPE = ["deploy", "dashboard", "vendor", "vendoring", "new model architecture"]; export function createResearchMapReceipt(opts: ResearchMapOptions): ResearchMapReceipt { const reasons: string[] = []; if (!opts.taskLabel.trim()) reasons.push("missing task label"); if (!opts.task.trim()) reasons.push("missing task"); if (opts.recommendedPlan.length === 0) reasons.push("missing recommended phases"); if (opts.recommendedPlan.some((phase) => phase.acceptanceCriteria.length === 0)) reasons.push("each phase needs acceptance criteria"); if (opts.recommendedPlan.some((phase) => phase.verification.length === 0)) reasons.push("each phase needs verification steps"); const searchable = [opts.task, opts.taskLabel, ...opts.recommendedPlan.flatMap((phase) => [phase.name, phase.goal])].join("\n").toLowerCase(); const forbidden = FORBIDDEN_SCOPE.filter((term) => searchable.includes(term)); if (forbidden.length > 0) reasons.push(`forbidden scope: ${forbidden.join(", ")}`); return { schema: "fable.research_map.receipt.v1", createdAt: (opts.now ?? new Date()).toISOString(), taskLabel: opts.taskLabel, task: opts.task, constraints: opts.constraints ?? [], existingPrimitives: opts.existingPrimitives ?? [], recommendedPlan: opts.recommendedPlan, skipped: opts.skipped ?? [], decision: reasons.length === 0 ? "ready" : "blocked", reasons, deployAttempted: false, }; } export function writeResearchMapReceipt(file: string, receipt: ResearchMapReceipt): string { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`); return file; }