Files
workinf_Blender_Wasm/tools/web/reconcile-corrective-resign.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

245 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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`);