import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { buildTaskContext } from "./task-context-lib.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const correctiveRoot = path.join(root, "tests/golden/corrective"); const read = (file) => fs.readFileSync(file, "utf8"); const json = (file) => JSON.parse(read(file)); const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); const inside = (file, parent) => file === parent || file.startsWith(`${parent}${path.sep}`); function arg(name) { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : undefined; } const evidenceRootArg = arg("--evidence-root"); const outputRootArg = arg("--output-root"); const reportArg = arg("--report"); const approved = process.argv.includes("--approve-current-worktree"); if (!evidenceRootArg || !outputRootArg || !reportArg || !approved) { throw new Error("usage: node tools/web/reconcile-corrective-resign.mjs --evidence-root tests/golden --output-root tests/golden/corrective/C0-002/staged --report path --approve-current-worktree"); } const evidenceRoot = path.resolve(root, evidenceRootArg); const outputRoot = path.resolve(root, outputRootArg); const reportPath = path.resolve(root, reportArg); if (!inside(evidenceRoot, root) || !inside(outputRoot, correctiveRoot) || !inside(reportPath, correctiveRoot)) { throw new Error("evidence, output and report paths are outside the allowed repository roots"); } const queuePath = path.join(root, "docs/EXECUTION_QUEUE.md"); const queue = read(queuePath); const queueTask = queue.match(/\|\s*当前任务\s*\|\s*`([^`]+)`/u)?.[1]; const parentManifestPath = queue.match(/\|\s*parent manifest\s*\|\s*`([^`]+)`/iu)?.[1]; if (!queueTask || !parentManifestPath) throw new Error("queue must declare current task and parent manifest"); const parentManifest = json(path.join(root, parentManifestPath)); const planPath = path.join(root, "tests/golden/M15-03A/next-task-plan.json"); const plan = json(planPath); const taskToGap = new Map(plan.tasks.map((task) => [task.id, task.gapId])); const issueCounts = new Map(); const issueTasks = new Map(); const issuePaths = new Map(); const issues = []; const addIssue = (code, fields = {}) => { issues.push({ code, ...fields }); issueCounts.set(code, (issueCounts.get(code) ?? 0) + 1); if (fields.task) { const values = issueTasks.get(code) ?? new Set(); values.add(fields.task); issueTasks.set(code, values); } if (fields.path) { const values = issuePaths.get(code) ?? new Set(); values.add(fields.path); issuePaths.set(code, values); } }; const taskDirs = fs.readdirSync(evidenceRoot, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && /^M16-GAP-\d{5}$/u.test(entry.name)) .map((entry) => entry.name) .sort(); const records = []; const resignedManifests = []; const repairedContexts = []; const repairedParents = []; const snapshotPaths = new Set(); function taskCardFields(task) { const cardPath = path.join(root, "docs/tasks", `${task}.md`); const source = fs.existsSync(cardPath) ? read(cardPath) : ""; const get = (name) => source.match(new RegExp(`^[-*]?\\s*\\x60?${name}\\x60?\\s*[::]\\s*\\x60?([^\\n\\x60]+)`, "imu"))?.[1]?.trim(); return { source, path: cardPath, task: get("task"), parent: get("parent"), status: get("status"), gap: get("gap"), ownerFamily: get("ownerFamily"), implementationClass: get("targetImplementationClass") }; } function syntheticContext(task, manifest, parent) { const fields = taskCardFields(task); const entry = plan.tasks.find((value) => value.id === task); const taskPath = fields.path; const parentPath = path.join(root, "tests/golden", parent.task, "manifest.json"); const parentStatusPath = path.join(root, "docs/status", `${parent.task}.md`); const fixture = entry?.fixture?.path ?? null; const context = { schemaVersion: 1, task, parentTask: manifest.parentTask, status: fields.status ?? entry?.state ?? "pending", goal: fields.source.match(/^##\s+目标\s*\n([\s\S]*?)(?=^##\s|$)/mu)?.[1].trim() ?? `完成 ${entry?.gapId ?? task} 的最小可观察切片`, scope: { gap: fields.gap ?? entry?.gapId ?? "NOT_APPLICABLE", ownerFamily: fields.ownerFamily ?? entry?.ownerFamily ?? "NOT_APPLICABLE", implementationClass: fields.implementationClass ?? entry?.targetImplementationClass ?? "NOT_APPLICABLE", }, commands: [entry?.desktopCommand, entry?.webCommand, entry?.comparator].filter(Boolean), inputPaths: [], inputSelection: { schemaVersion: 1, limits: { maxFiles: 12, evidenceBytes: 8192, contextRemainingBytes: 0, contextRemainingTokens: 0 }, source: { bytes: Buffer.byteLength(queue) + Buffer.byteLength(fields.source) + Buffer.byteLength(read(parentPath)), tokens: 0 }, selected: [], excluded: fixture ? [{ path: fixture, bytes: null, reason: "EVIDENCE_BYTE_BUDGET" }] : [], totals: { files: 0, bytes: 0, tokens: 0 } }, exitCriteria: entry?.exitCriteria ?? ["focused command exits 0", "report and manifest are hash-bound"], nextTask: manifest.nextTask ?? entry?.id ?? null, sourceDocuments: { queue: relative(queuePath), taskCard: relative(taskPath), taskIndex: "tests/golden/M15-03A/task-index.json", parentManifest: relative(parentPath), parentStatus: fs.existsSync(parentStatusPath) ? relative(parentStatusPath) : "NOT_APPLICABLE" }, readPolicy: { required: [relative(queuePath), relative(taskPath), relative(parentPath), fs.existsSync(parentStatusPath) ? relative(parentStatusPath) : "NOT_APPLICABLE"], machineOnly: ["tests/golden/M15-03A/task-index.json", "tests/golden/M15-03A/task-catalog.jsonl"], optionalByNeed: [], forbiddenByDefault: [] }, parent: { manifest: { schemaVersion: parent.schemaVersion, task: parent.task, parentTask: parent.parentTask, status: parent.status ?? "done", nextTask: task, artifactCount: Object.keys(parent.artifacts ?? {}).length }, statusSummary: fs.existsSync(parentStatusPath) ? read(parentStatusPath).split("\n").filter(Boolean).slice(0, 8) : [] }, budgets: { queueBytes: 4096, taskBytes: 8192, parentManifestBytes: 24576, parentStatusBytes: 6144, evidenceFiles: 12, evidenceBytes: 8192, contextTokens: 3500, contextEnvelopeTokens: 1024, taskLines: fields.source ? fields.source.split("\n").length : 0, taskTokens: Math.ceil(Buffer.byteLength(fields.source) / 4) }, }; return context; } for (const task of taskDirs) { const directory = path.join(evidenceRoot, task); const manifestPath = path.join(directory, "manifest.json"); const manifest = json(manifestPath); const statusPath = path.join(root, "docs/status", `${task}.md`); const contextPath = path.join(directory, "task-context.json"); if (!fs.existsSync(statusPath)) addIssue("MISSING_STATUS", { task }); if (!fs.existsSync(contextPath)) addIssue("MISSING_TASK_CONTEXT", { task, repaired: true }); const candidate = structuredClone(manifest); const drift = []; for (const artifact of Object.values(candidate.artifacts ?? {})) { if (!artifact?.path || !artifact?.sha256) continue; const artifactPath = path.join(root, artifact.path); if (!fs.existsSync(artifactPath)) { addIssue("ARTIFACT_MISSING", { task, path: artifact.path }); continue; } const actual = sha256(artifactPath); snapshotPaths.add(artifact.path); if (actual !== artifact.sha256) { drift.push({ path: artifact.path, originalSha256: artifact.sha256, resignedSha256: actual }); artifact.sha256 = actual; addIssue("ARTIFACT_HASH_RESIGNED", { task, path: artifact.path }); } } if (manifest.parentTask) { const parentPath = path.join(root, "tests/golden", manifest.parentTask, "manifest.json"); if (fs.existsSync(parentPath)) { const parent = json(parentPath); if (parent.nextTask !== task) { repairedParents.push({ task, parentTask: manifest.parentTask, originalNextTask: parent.nextTask, resignedNextTask: task }); addIssue("PARENT_POINTER_RESIGNED", { task, parent: manifest.parentTask, originalNextTask: parent.nextTask }); } } } candidate.reconciliation = { mode: "CURRENT_WORKTREE_RESIGN", authorized: true, artifactDrift: drift }; resignedManifests.push({ task, manifest: candidate }); let context; if (fs.existsSync(contextPath)) { context = json(contextPath); } else { try { context = buildTaskContext(task).context; } catch { const parent = manifest.parentTask ? json(path.join(root, "tests/golden", manifest.parentTask, "manifest.json")) : manifest; context = syntheticContext(task, manifest, parent); } repairedContexts.push({ task, context }); } records.push({ task, gapId: taskToGap.get(task) ?? null, parentTask: manifest.parentTask, status: manifest.status, completeEvidence: manifest.status === "done" && fs.existsSync(statusPath), resigned: true }); } if (parentManifest.nextTask !== queueTask) addIssue("PARENT_NEXT_TASK_MISMATCH", { detail: `${parentManifest.nextTask}!=${queueTask}` }); const active = records.filter((record) => record.status === "in_progress"); if (active.length !== 1) addIssue("ACTIVE_TASK_COUNT", { detail: `count=${active.length}` }); if (active[0]?.task !== queueTask) addIssue("ACTIVE_TASK_MISMATCH", { detail: `${active[0]?.task ?? "NONE"}!=${queueTask}` }); const eligibleCompletedTaskIds = records.filter((record) => record.completeEvidence).map((record) => record.task).sort(); const eligibleCompletedGaps = records.filter((record) => record.completeEvidence && record.gapId).map((record) => record.gapId).sort(); const issueSummary = [...issueCounts.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([code, count]) => ({ code, count, taskCount: issueTasks.get(code)?.size ?? 0, sampleTasks: [...(issueTasks.get(code) ?? [])].sort().slice(0, 5), pathCount: issuePaths.get(code)?.size ?? 0, samplePaths: [...(issuePaths.get(code) ?? [])].sort().slice(0, 5) })); const sourcePaths = [queuePath, path.join(root, parentManifestPath), path.join(root, "tests/golden/M15-03A/completed-gap-tasks.json"), planPath, path.join(root, "tests/golden/M15-03A/task-catalog.jsonl"), path.join(root, "tests/golden/M15-03A/task-index.json")]; const sourceHashes = Object.fromEntries(sourcePaths.map((file) => [relative(file), sha256(file)])); const inputDigest = digest({ sourceHashes, records, eligibleCompletedTaskIds, eligibleCompletedGaps, resignedManifests: resignedManifests.map(({ task, manifest }) => ({ task, artifacts: manifest.artifacts, reconciliation: manifest.reconciliation })), repairedContexts: repairedContexts.map(({ task }) => task), repairedParents }); const report = { schemaVersion: 1, operation: "CORRECTIVE_STATE_RECONCILIATION_RESIGN", mode: "write", status: "BLOCKED", queueMutation: false, authorization: { mode: "CURRENT_WORKTREE_RESIGN", userApproved: true, legacyFilesOverwritten: false }, queue: { currentTask: queueTask, parentManifest: parentManifestPath, parentNextTask: parentManifest.nextTask }, inputTaskCount: records.length, candidateActiveTask: active.length === 1 ? active[0].task : null, sourceHashes, inputDigest, issueSummary, issueCount: issues.length, issues, registryComparison: { candidateCompletedTaskCount: eligibleCompletedTaskIds.length, candidateCompletedGapCount: eligibleCompletedGaps.length, previousRegistryPath: "tests/golden/M15-03A/completed-gap-tasks.json" }, repairPlan: { approvalRequired: false, queueMutationAllowed: false, actions: ["re-sign current artifact hashes in staged manifests", "repair contexts with valid indexed parent data", "record M16-GAP-00001 parent pointer as a staged repair", "regenerate registry, plan, catalog, index and active card in staging"] }, writeOutcome: "NOT_STARTED", commandExitCodes: { resign: null, validation: null, planCheck: null, indexCheck: null, contextCheck: null, governanceCheck: null, diffCheck: null } }; for (const entry of ["resigned-manifests", "resigned-context", "source-snapshot", "completed-gap-tasks.candidate.json", "next-task-plan.candidate.json", "next-task-plan.candidate.second.json", "task-index", "task-cards"]) { fs.rmSync(path.join(outputRoot, entry), { recursive: true, force: true }); } fs.mkdirSync(outputRoot, { recursive: true }); const manifestRoot = path.join(outputRoot, "resigned-manifests"); for (const { task, manifest } of resignedManifests) { const file = path.join(manifestRoot, task, "manifest.json"); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`); } for (const parentRepair of repairedParents) { const source = path.join(root, "tests/golden", parentRepair.parentTask, "manifest.json"); const candidate = json(source); candidate.nextTask = parentRepair.resignedNextTask; candidate.reconciliation = { mode: "CURRENT_WORKTREE_RESIGN", authorized: true, parentPointerRepair: parentRepair }; const file = path.join(manifestRoot, parentRepair.parentTask, "manifest.json"); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(candidate, null, 2)}\n`); } const contextRoot = path.join(outputRoot, "resigned-context"); for (const { task, context } of repairedContexts) { const file = path.join(contextRoot, task, "task-context.json"); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(context, null, 2)}\n`); } const completionPath = path.join(outputRoot, "completed-gap-tasks.candidate.json"); fs.writeFileSync(completionPath, `${JSON.stringify(eligibleCompletedGaps, null, 2)}\n`); const snapshotRoot = path.join(outputRoot, "source-snapshot"); const snapshotFiles = []; for (const artifactPath of [...snapshotPaths].sort()) { const source = path.join(root, artifactPath); const destination = path.join(snapshotRoot, "files", artifactPath); fs.mkdirSync(path.dirname(destination), { recursive: true }); fs.copyFileSync(source, destination); snapshotFiles.push({ path: artifactPath, snapshotPath: relative(destination), sha256: sha256(source), bytes: fs.statSync(source).size }); } fs.writeFileSync(path.join(snapshotRoot, "snapshot-manifest.json"), `${JSON.stringify({ schemaVersion: 1, mode: "CURRENT_WORKTREE_RESIGN", authorized: true, files: snapshotFiles }, null, 2)}\n`); const candidatePlan = path.join(outputRoot, "next-task-plan.candidate.json"); const candidatePlanSecond = path.join(outputRoot, "next-task-plan.candidate.second.json"); const generator = path.join(root, "tools/web/generate-blender-next-task-plan.mjs"); execFileSync(process.execPath, [generator, candidatePlan, "--completion-path", completionPath], { cwd: root, stdio: "ignore" }); execFileSync(process.execPath, [generator, candidatePlanSecond, "--completion-path", completionPath], { cwd: root, stdio: "ignore" }); if (!fs.readFileSync(candidatePlan).equals(fs.readFileSync(candidatePlanSecond))) { addIssue("CANDIDATE_PLAN_NONDETERMINISTIC"); } else { fs.rmSync(candidatePlanSecond, { force: true }); } const indexDir = path.join(outputRoot, "task-index"); execFileSync(process.execPath, [path.join(root, "tools/web/generate-task-index.mjs"), "--plan-path", candidatePlan, "--output-dir", indexDir], { cwd: root, stdio: "ignore" }); const candidatePlanData = json(candidatePlan); const activeTask = candidatePlanData.firstTask; const activeCardSource = activeTask && fs.existsSync(path.join(root, "docs/tasks", `${activeTask}.md`)) ? read(path.join(root, "docs/tasks", `${activeTask}.md`)) : ""; if (activeTask && activeCardSource) { const cardPath = path.join(outputRoot, "task-cards", `${activeTask}.md`); fs.mkdirSync(path.dirname(cardPath), { recursive: true }); fs.writeFileSync(cardPath, activeCardSource); } report.status = issues.some((issue) => issue.code === "ARTIFACT_MISSING" || issue.code === "CANDIDATE_PLAN_NONDETERMINISTIC") ? "BLOCKED" : "PASS_WITH_REPAIR"; report.writeOutcome = report.status === "PASS_WITH_REPAIR" ? "STAGED_RE_SIGN_CANDIDATE" : "NO_FILES_WRITTEN"; report.issueSummary = [...issueCounts.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([code, count]) => ({ code, count, taskCount: issueTasks.get(code)?.size ?? 0, sampleTasks: [...(issueTasks.get(code) ?? [])].sort().slice(0, 5), pathCount: issuePaths.get(code)?.size ?? 0, samplePaths: [...(issuePaths.get(code) ?? [])].sort().slice(0, 5) })); report.issueCount = issues.length; report.candidates = { completionRegistry: relative(completionPath), plan: relative(candidatePlan), taskIndex: relative(path.join(indexDir, "task-index.json")), catalog: relative(path.join(indexDir, "task-catalog.jsonl")), activeTaskCard: activeTask ? relative(path.join(outputRoot, "task-cards", `${activeTask}.md`)) : null, sourceSnapshot: relative(path.join(snapshotRoot, "snapshot-manifest.json")), sourceSnapshotFileCount: snapshotFiles.length, resignedManifestCount: resignedManifests.length, repairedContextCount: repairedContexts.length, repairedParentCount: repairedParents.length }; fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`corrective-resign-${report.status.toLowerCase()} tasks=${records.length} completed=${eligibleCompletedGaps.length} issues=${issues.length} output=${relative(outputRoot)}\n`);