Files
workinf_Blender_Wasm/tools/web/reconcile-corrective-state.mjs
mes123456 a081c68c87
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Reorganize Blender Web execution around capabilities
2026-08-24 18:37:46 -04:00

210 lines
11 KiB
JavaScript

import crypto from "node:crypto";
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const queuePath = path.join(root, "docs/EXECUTION_QUEUE.md");
const planPath = path.join(root, "tests/golden/M15-03A/next-task-plan.json");
const catalogPath = path.join(root, "tests/golden/M15-03A/task-catalog.jsonl");
const indexPath = path.join(root, "tests/golden/M15-03A/task-index.json");
const completionPath = path.join(root, "tests/golden/M15-03A/completed-gap-tasks.json");
const correctivePlanPath = path.join(root, "docs/BLENDER_WASM_CORRECTIVE_TASK_PLAN.json");
const read = (file) => fs.readFileSync(file, "utf8");
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
const json = (file) => JSON.parse(read(file));
function arg(name) {
const index = process.argv.indexOf(name);
return index >= 0 ? process.argv[index + 1] : undefined;
}
const evidenceRootArg = arg("--evidence-root");
const reportArg = arg("--report");
const outputRootArg = arg("--output-root");
const dryRun = process.argv.includes("--dry-run");
const write = process.argv.includes("--write");
if (!evidenceRootArg || !reportArg || dryRun === write) {
throw new Error("usage: node tools/web/reconcile-corrective-state.mjs --evidence-root tests/golden --dry-run|--write --report path [--output-root path]");
}
const evidenceRoot = path.resolve(root, evidenceRootArg);
const reportPath = path.resolve(root, reportArg);
const correctiveRoot = path.join(root, "tests/golden/corrective");
const inside = (file, parent) => file === parent || file.startsWith(`${parent}${path.sep}`);
if (!inside(reportPath, correctiveRoot)) throw new Error("report must stay under tests/golden/corrective");
if (write && !outputRootArg) throw new Error("--write requires --output-root");
const outputRoot = outputRootArg ? path.resolve(root, outputRootArg) : null;
if (outputRoot && !inside(outputRoot, correctiveRoot)) throw new Error("output-root must stay under tests/golden/corrective");
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 taskPlan = json(planPath);
const taskToGap = new Map(taskPlan.tasks.map((task) => [task.id, task.gapId]));
const sourcePaths = [
queuePath,
path.join(root, parentManifestPath),
completionPath,
planPath,
catalogPath,
indexPath,
correctivePlanPath,
];
const sourceHashes = Object.fromEntries(sourcePaths.map((file) => [relative(file), sha256(file)]));
const issues = [];
const issueCounts = new Map();
const issueTasks = new Map();
const issuePaths = new Map();
const addIssue = (issue) => {
issues.push(issue);
issueCounts.set(issue.code, (issueCounts.get(issue.code) ?? 0) + 1);
if (issue.task) {
const tasks = issueTasks.get(issue.code) ?? new Set();
tasks.add(issue.task);
issueTasks.set(issue.code, tasks);
}
if (issue.path) {
const paths = issuePaths.get(issue.code) ?? new Set();
paths.add(issue.path);
issuePaths.set(issue.code, paths);
}
};
const records = [];
const taskDirs = fs.readdirSync(evidenceRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && /^M16-GAP-\d{5}$/u.test(entry.name))
.map((entry) => entry.name)
.sort();
for (const directoryName of taskDirs) {
const directory = path.join(evidenceRoot, directoryName);
const manifestPath = path.join(directory, "manifest.json");
let manifest;
try { manifest = json(manifestPath); } catch (error) {
addIssue({ code: "MANIFEST_INVALID", task: directoryName, detail: error.message });
continue;
}
const task = manifest.task;
if (task !== directoryName) addIssue({ code: "TASK_DIRECTORY_MISMATCH", task: directoryName, detail: manifest.task });
const statusPath = path.join(root, "docs/status", `${task}.md`);
const contextPath = path.join(directory, "task-context.json");
const statusExists = fs.existsSync(statusPath);
const contextExists = fs.existsSync(contextPath);
if (!statusExists) addIssue({ code: "MISSING_STATUS", task });
if (!contextExists) addIssue({ code: "MISSING_TASK_CONTEXT", task });
if (statusExists) {
const statusText = read(statusPath);
if (!new RegExp(`status\\s*:\\s*${manifest.status}`, "iu").test(statusText)) {
addIssue({ code: "STATUS_MISMATCH", task, detail: `manifest=${manifest.status}` });
}
}
if (contextExists) {
try {
const context = json(contextPath);
if (context.task !== task) addIssue({ code: "CONTEXT_TASK_MISMATCH", task, detail: context.task });
if (context.parentTask !== manifest.parentTask) addIssue({ code: "CONTEXT_PARENT_MISMATCH", task, detail: `${context.parentTask}!=${manifest.parentTask}` });
} catch (error) {
addIssue({ code: "CONTEXT_INVALID", task, detail: error.message });
}
}
const artifactIssues = [];
for (const artifact of Object.values(manifest.artifacts ?? {})) {
if (!artifact?.path || !artifact?.sha256) {
artifactIssues.push({ code: "ARTIFACT_DECLARATION_INVALID" });
continue;
}
const artifactPath = path.join(root, artifact.path);
if (!fs.existsSync(artifactPath)) artifactIssues.push({ code: "ARTIFACT_MISSING", path: artifact.path });
else if (sha256(artifactPath) !== artifact.sha256) artifactIssues.push({ code: "ARTIFACT_HASH_DRIFT", path: artifact.path });
}
for (const issue of artifactIssues) addIssue({ ...issue, task });
const completeEvidence = manifest.status === "done" && statusExists && contextExists && artifactIssues.length === 0;
records.push({ task, gapId: taskToGap.get(task) ?? null, parentTask: manifest.parentTask, status: manifest.status, completeEvidence });
}
const active = records.filter((record) => record.status === "in_progress");
if (active.length !== 1) addIssue({ code: "ACTIVE_TASK_COUNT", detail: `count=${active.length}` });
if (parentManifest.nextTask !== queueTask) addIssue({ code: "PARENT_NEXT_TASK_MISMATCH", detail: `${parentManifest.nextTask}!=${queueTask}` });
if (active[0]?.task !== queueTask) addIssue({ code: "ACTIVE_TASK_MISMATCH", detail: `${active[0]?.task ?? "NONE"}!=${queueTask}` });
const generatedPlanTempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "blender-corrective-plan-"));
const generatedPlanTemp = path.join(generatedPlanTempRoot, "next-task-plan.json");
try {
execFileSync(process.execPath, [path.join(root, "tools/web/generate-blender-next-task-plan.mjs"), generatedPlanTemp], { cwd: root, stdio: "ignore" });
if (!fs.readFileSync(generatedPlanTemp).equals(fs.readFileSync(planPath))) {
addIssue({ code: "PLAN_NONDETERMINISTIC", path: relative(planPath), detail: "generator output differs from committed plan" });
}
} catch (error) {
addIssue({ code: "PLAN_GENERATION_FAILED", detail: error.message });
} finally {
fs.rmSync(generatedPlanTempRoot, { recursive: true, force: true });
}
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 registeredCompletedTasks = json(completionPath);
const eligibleSet = new Set(eligibleCompletedGaps);
const registeredSet = new Set(registeredCompletedTasks);
const manifestDoneTasks = records.filter((record) => record.status === "done").map((record) => record.task).sort();
const manifestDoneGaps = records.filter((record) => record.status === "done" && 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 repairPlan = {
actions: [
{ code: "MISSING_TASK_CONTEXT", priority: "P1", action: "repair only when the task parent pointer is independently valid; otherwise preserve BLOCKED", writes: ["corrective evidence only"] },
{ code: "ARTIFACT_HASH_DRIFT", priority: "P0", action: "obtain an immutable source snapshot or classify the task BLOCKED; never rewrite historical manifests", writes: [] },
{ code: "STATUS_MISMATCH", priority: "P0", action: "compare manifest/status provenance and stop; do not normalize by hand", writes: [] },
{ code: "PARENT_NEXT_TASK_MISMATCH", priority: "P0", action: "repair through the repository generator only after evidence review", writes: [] },
{ code: "PLAN_NONDETERMINISTIC", priority: "P0", action: "rebuild generator inputs in staging and compare canonical bytes", writes: ["corrective evidence only"] },
],
approvalRequired: true,
queueMutationAllowed: false,
};
const digestInput = JSON.stringify({ sourceHashes, records, issueSummary, eligibleCompletedTaskIds, eligibleCompletedGaps, registeredCompletedTasks });
const inputDigest = crypto.createHash("sha256").update(digestInput).digest("hex");
const report = {
schemaVersion: 1,
operation: "CORRECTIVE_STATE_RECONCILIATION",
mode: dryRun ? "dry-run" : "write",
status: issues.length === 0 ? "PASS" : "BLOCKED",
queueMutation: false,
queue: { currentTask: queueTask, parentManifest: parentManifestPath, parentNextTask: parentManifest.nextTask },
inputTaskCount: records.length,
manifestDoneCount: manifestDoneTasks.length,
registeredCompletedCount: registeredCompletedTasks.length,
eligibleCompletedTaskIds,
eligibleCompletedGaps,
registryComparison: {
eligibleNotRegistered: eligibleCompletedGaps.filter((gap) => !registeredSet.has(gap)),
registeredNotEligible: registeredCompletedTasks.filter((gap) => !eligibleSet.has(gap)),
manifestDoneNotEligible: manifestDoneGaps.filter((gap) => !eligibleSet.has(gap)),
},
candidateActiveTask: active.length === 1 ? active[0].task : null,
sourceHashes,
inputDigest,
issueSummary,
issueCount: issues.length,
issues,
repairPlan,
writeOutcome: write ? (issues.length === 0 ? "CANDIDATE_NOT_IMPLEMENTED" : "NO_FILES_WRITTEN") : "DRY_RUN_ONLY",
};
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
if (write && issues.length === 0) {
fs.mkdirSync(outputRoot, { recursive: true });
fs.writeFileSync(path.join(outputRoot, "completion-registry.candidate.json"), `${JSON.stringify({ schemaVersion: 1, tasks: eligibleCompletedGaps }, null, 2)}\n`);
}
process.stdout.write(`corrective-reconcile-${report.status.toLowerCase()} mode=${report.mode} tasks=${records.length} eligible=${eligibleCompletedGaps.length} issues=${issues.length} report=${relative(reportPath)}\n`);
if (issues.length !== 0) process.exitCode = 2;