Reorganize Blender Web execution around capabilities
This commit is contained in:
244
tools/web/reconcile-corrective-resign.mjs
Normal file
244
tools/web/reconcile-corrective-resign.mjs
Normal file
@@ -0,0 +1,244 @@
|
||||
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`);
|
||||
Reference in New Issue
Block a user