perf(web): add idempotent batch verification fast paths
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-09-03 20:29:51 +08:00
parent b849eb657b
commit b28b365f3a
19 changed files with 652 additions and 71 deletions

View File

@@ -16,19 +16,44 @@ const task = process.argv.includes("--task")
if (!task) throw new Error("unable to resolve current WBV2 task");
const parentManifest = queue.match(/\| parent manifest \| `([^`]+)` \|/u)?.[1];
if (!parentManifest) throw new Error("queue parent manifest is missing");
const inputPaths = [
const baseInputPaths = [
"docs/EXECUTION_QUEUE.md",
`docs/tasks/${task}.md`,
parentManifest,
`tests/golden/${task}/task-context.json`,
];
const inputHashFor = () => digest(inputPaths.map((file) => `${file}\0${fs.existsSync(abs(file)) ? digest(read(file)) : "MISSING"}\n`).join(""));
const inputHash = inputHashFor();
const statInput = (file) => {
try {
const stat = fs.statSync(abs(file), { bigint: true });
return { bytes: Number(stat.size), mtime_ns: stat.mtimeNs.toString(), exists: true };
}
catch (error) {
return { bytes: null, mtime_ns: null, exists: false, error: error?.code ?? "STAT_FAILED" };
}
};
const metadataComparable = (value) => value && value.exists === true
&& Number.isSafeInteger(value.bytes) && typeof value.mtime_ns === "string";
let cached = null;
try { cached = JSON.parse(fs.readFileSync(cachePath, "utf8")); }
catch { /* First use or an interrupted write. */ }
if (!process.argv.includes("--force") && cached?.status === "PASS" && cached.task === task && cached.inputHash === inputHash) {
process.stdout.write(`wbv2-context-cache-hit task=${task} inputHash=${inputHash}\n`);
const inputPaths = [...new Set([
...baseInputPaths,
...Object.keys(cached?.inputs ?? {}),
])];
const inputMetadata = Object.fromEntries(inputPaths.map((file) => [file, statInput(file)]));
const metadataUnchanged = (cachedInputs) => inputPaths.every((file) => {
const current = inputMetadata[file];
const cached = cachedInputs?.[file];
return metadataComparable(current) && metadataComparable(cached)
&& current.bytes === cached.bytes && current.mtime_ns === cached.mtime_ns;
});
const digestPath = (file) => {
try { return digest(read(file)); }
catch { return "MISSING"; }
};
const inputHashFor = (paths = inputPaths) => digest(paths.map((file) => `${file}\0${digestPath(file)}\n`).join(""));
if (!process.argv.includes("--force") && cached?.status === "PASS" && cached.task === task && metadataUnchanged(cached.inputs)) {
process.stdout.write(`CONTEXT_PACKET_REUSED task=${task} inputHash=${cached.inputHash ?? "UNKNOWN"}\n`);
process.exit(0);
}
const result = spawnSync(process.execPath, [abs("tools/web/print-task-context.mjs"), "--task", task], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 });
@@ -38,8 +63,21 @@ if ((result.status ?? 1) !== 0 || (check.status ?? 1) !== 0) {
process.exit(Math.max(result.status ?? 1, check.status ?? 1));
}
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
const refreshedInputHash = inputHashFor();
const payload = { schemaVersion: 1, status: "PASS", task, inputHash: refreshedInputHash, refreshedAt: new Date().toISOString() };
let contextMetadata = {};
try {
const context = JSON.parse(read(`tests/golden/${task}/task-context.json`));
contextMetadata = context.inputMetadata && typeof context.inputMetadata === "object" ? context.inputMetadata : {};
} catch { /* The task-context checker already reported the precise failure. */ }
const refreshedPaths = [...new Set([...inputPaths, ...Object.keys(contextMetadata)])];
const refreshedInputHash = inputHashFor(refreshedPaths);
const payload = {
schemaVersion: 2,
status: "PASS",
task,
inputHash: refreshedInputHash,
inputs: Object.fromEntries(refreshedPaths.map((file) => [file, { ...statInput(file), sha256: contextMetadata[file]?.sha256 ?? digestPath(file) }])),
refreshedAt: new Date().toISOString(),
};
const temporary = `${cachePath}.tmp-${process.pid}`;
fs.writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`);
fs.renameSync(temporary, cachePath);

View File

@@ -64,6 +64,25 @@ export function validateContextBundle(bundle, { current = true } = {}) {
if (context.commands.length > GOVERNANCE_LIMITS.maxTaskCommands) {
add(violations, "TASK_COMMANDS_TOO_WIDE", `count=${context.commands.length}`);
}
if (!context.inputMetadata || typeof context.inputMetadata !== "object") {
add(violations, "INPUT_METADATA_MISSING", context.task);
} else {
const requiredMetadata = [
relative(files.queuePath),
relative(files.taskPath),
relative(files.parentManifestPath),
...(fs.existsSync(files.parentStatusPath) ? [relative(files.parentStatusPath)] : []),
...context.inputPaths,
];
for (const file of [...new Set(requiredMetadata)]) {
const metadata = context.inputMetadata[file];
if (!metadata || typeof metadata !== "object" || metadata.exists !== true
|| !Number.isSafeInteger(metadata.bytes) || typeof metadata.mtime_ns !== "string"
|| !/^[a-f0-9]{64}$/u.test(metadata.sha256 ?? "")) {
add(violations, "INPUT_METADATA_INVALID", `${context.task}:${file}`);
}
}
}
const selection = context.inputSelection;
if (!selection || selection.schemaVersion !== 1 || !Array.isArray(selection.selected) || !Array.isArray(selection.excluded)) {

View File

@@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
const controlRoot = "tests/golden/WBV2";
const schedulerLockPath = `${controlRoot}/continuous-scheduler.lock`;
const schedulerCheckpointPath = `${controlRoot}/continuous-scheduler.json`;
const guardianLockPath = `${controlRoot}/continuous-guardian.lock`;
const supervisorLockPath = `${controlRoot}/continuous-supervisor.lock`;
const guardianCheckpointPath = `${controlRoot}/continuous-guardian.json`;
@@ -80,15 +82,17 @@ const pushVerification = (task) => {
};
const collect = () => {
const queue = currentPointer();
const scheduler = readJson(schedulerCheckpointPath);
const guardian = readJson(guardianCheckpointPath);
const supervisor = readJson(supervisorCheckpointPath);
const schedulerLock = lockInfo(schedulerLockPath);
const guardianLock = lockInfo(guardianLockPath);
const supervisorLock = lockInfo(supervisorLockPath);
const handoff = latestHandoff();
const active = guardianLock.alive && supervisorLock.alive;
const blocked = guardian?.state === "blocked" || supervisor?.state === "blocked";
const active = schedulerLock.alive || (guardianLock.alive && supervisorLock.alive);
const blocked = scheduler?.state === "blocked" || guardian?.state === "blocked" || supervisor?.state === "blocked";
const state = blocked ? "BLOCKED" : active ? "RUNNING" : "IDLE";
const phase = blocked ? "blocked" : supervisor?.state === "running" ? "task-or-gate" : supervisor?.state ?? "unknown";
const phase = blocked ? "blocked" : scheduler?.state === "running" ? "scheduler-task" : supervisor?.state === "running" ? "task-or-gate" : scheduler?.state ?? supervisor?.state ?? "unknown";
const observed = new Date().toISOString();
return {
schemaVersion: 1,
@@ -97,14 +101,15 @@ const collect = () => {
phase,
message: state === "RUNNING"
? `RUNNING: ${queue.task ?? "unknown task"} is owned by the guardian/supervisor process; wait for its handoff.`
: state === "BLOCKED" ? "BLOCKED: inspect the checkpoint reason before any write or retry." : "IDLE: no live guardian/supervisor lock was found.",
: state === "BLOCKED" ? "BLOCKED: inspect the scheduler/guardian/supervisor checkpoint reason before any write or retry." : "IDLE: no live scheduler, guardian or supervisor lock was found.",
queue,
scheduler: { pid: scheduler?.pid ?? schedulerLock.pid, alive: schedulerLock.alive, state: scheduler?.state ?? null, updatedAt: scheduler?.updatedAt ?? null, iteration: scheduler?.iteration ?? null, before: scheduler?.before ?? null, after: scheduler?.after ?? null, latestBatch: scheduler?.latestBatch ?? null },
guardian: { pid: guardian?.pid ?? guardianLock.pid, alive: guardianLock.alive, state: guardian?.state ?? null, updatedAt: guardian?.updatedAt ?? null, heartbeatAt: guardian?.heartbeatAt ?? null, restartCount: guardian?.restartCount ?? null },
supervisor: { pid: supervisor?.pid ?? supervisorLock.pid, alive: supervisorLock.alive, state: supervisor?.state ?? null, updatedAt: supervisor?.updatedAt ?? null, iteration: supervisor?.iteration ?? null, before: supervisor?.before ?? null, after: supervisor?.after ?? null },
latestHandoff: handoff,
latestPushVerification: pushVerification(handoff?.before ?? supervisor?.before?.task),
localHead: gitHead(),
checkpoints: { guardian: guardianCheckpointPath, supervisor: supervisorCheckpointPath },
checkpoints: { scheduler: schedulerCheckpointPath, guardian: guardianCheckpointPath, supervisor: supervisorCheckpointPath },
};
};
const format = (status) => [
@@ -112,6 +117,7 @@ const format = (status) => [
`阶段: ${status.phase}`,
`当前任务: ${status.queue.task ?? "unknown"}`,
`parent manifest: ${status.queue.parentManifest ?? "unknown"}`,
`scheduler: pid=${status.scheduler.pid ?? "none"} alive=${status.scheduler.alive} state=${status.scheduler.state ?? "unknown"} iteration=${status.scheduler.iteration ?? "none"} batch=${status.scheduler.latestBatch?.batchId ?? "none"} completed=${status.scheduler.latestBatch?.completed ?? false}`,
`guardian: pid=${status.guardian.pid ?? "none"} alive=${status.guardian.alive} state=${status.guardian.state ?? "unknown"} heartbeat=${status.guardian.heartbeatAt ?? "none"}`,
`supervisor: pid=${status.supervisor.pid ?? "none"} alive=${status.supervisor.alive} state=${status.supervisor.state ?? "unknown"} iteration=${status.supervisor.iteration ?? "none"}`,
`最近交接: ${status.latestHandoff ? `${status.latestHandoff.before} -> ${status.latestHandoff.after} (#${status.latestHandoff.iteration})` : "none"}`,

View File

@@ -58,20 +58,6 @@ function taskCommand(task) {
return match[1];
}
function typecheckProven(task) {
try {
const manifest = JSON.parse(read(`tests/golden/${task}/manifest.json`));
const receiptPath = manifest.artifacts?.moduleReceipt?.path;
if (!receiptPath) return false;
const receipt = JSON.parse(read(receiptPath));
return receipt.status === "PASS" && receipt.source?.unchanged === true
&& receipt.commands?.some((entry) => entry.exitCode === 0 && entry.command.trim() === "npm --prefix web run typecheck");
}
catch {
return false;
}
}
let iterations = 0;
const summary = [];
while (maxIterations === 0 || iterations < maxIterations) {
@@ -91,10 +77,10 @@ while (maxIterations === 0 || iterations < maxIterations) {
const after = currentTask();
const terminal = after.task === before.task && terminalTask(before.task);
if (after.task === before.task && !terminal) throw new Error(`${before.task} focused command exited 0 without advancing queue; log=${logPath}`);
const typecheckCommand = typecheckProven(before.task)
? "printf 'typecheck reused from hash-bound module receipt\\n'"
: "npm --prefix web run typecheck";
const gates = command(`node tools/web/check-context-governance.mjs && node tools/web/check-execution-control-plane.mjs --allow-unrunnable && node tools/web/check-v2-task-index.mjs && node tools/web/check-v2-handoff.mjs && ${typecheckCommand} && npm --prefix web run lint && git diff --check`, `tests/golden/${before.task}/continuous-gates.log`);
// Implementation cycles keep the post-handoff gate incremental. Full API,
// format, Chromium, performance and release gates belong to the explicit
// final audit and must not be repeated for every batch.
const gates = command(`python3 my-tools/validate_incremental_context.py --task ${before.task}`, `tests/golden/${before.task}/continuous-gates.log`);
if (gates.code !== 0) throw new Error(`${before.task} post-handoff gate failed (exit ${gates.code}); log=tests/golden/${before.task}/continuous-gates.log`);
iterations += 1;
summary.push({ task: before.task, command: focused, nextTask: terminal ? null : after.task, terminal, log: logPath });

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { recordFailure } from "./wbv2-failure-ledger.mjs";
import { createFailureLedgerSession } from "./wbv2-failure-ledger.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
@@ -11,6 +11,7 @@ const logPath = `${controlRoot}/continuous-guardian.log`;
const checkpointPath = `${controlRoot}/continuous-guardian.json`;
const supervisorCheckpointPath = `${controlRoot}/continuous-supervisor.json`;
const lockPath = `${controlRoot}/continuous-guardian.lock`;
const failureLedger = createFailureLedgerSession(root, { logPaths: [`${controlRoot}/continuous-guardian.log`] });
const argv = process.argv.slice(2);
const numericArg = (name, fallback) => {
@@ -173,7 +174,7 @@ try {
}
const decision = safeRestart(checkpoint, after);
if (!decision.ok) {
recordFailure(root, { task: before.task, phase: "guardian", reason: decision.reason, childExit: result.code });
failureLedger.record({ task: before.task, phase: "guardian", reason: decision.reason, childExit: result.code });
writeCheckpoint({ state: "blocked", restartCount, before, after, reason: decision.reason, supervisorExit: result.code });
appendLog(`blocked reason=${decision.reason}; no automatic replay`);
guardianExit = 2;
@@ -181,7 +182,7 @@ try {
}
restartCount += 1;
if (maxRestarts > 0 && restartCount > maxRestarts) {
recordFailure(root, { task: before.task, phase: "guardian", reason: "max_safe_restarts_exceeded", childExit: result.code });
failureLedger.record({ task: before.task, phase: "guardian", reason: "max_safe_restarts_exceeded", childExit: result.code });
writeCheckpoint({ state: "blocked", restartCount, before, after, reason: "max_safe_restarts_exceeded", supervisorExit: result.code });
appendLog("blocked reason=max_safe_restarts_exceeded");
guardianExit = 3;
@@ -194,12 +195,13 @@ try {
}
catch (error) {
guardianExit = 4;
try { recordFailure(root, { task: currentTask()?.task ?? "UNKNOWN", phase: "guardian", reason: "guardian_error", detail: error?.message ?? String(error) }); }
try { failureLedger.record({ task: currentTask()?.task ?? "UNKNOWN", phase: "guardian", reason: "guardian_error", detail: error?.message ?? String(error) }); }
catch { /* Preserve the original guardian failure. */ }
writeCheckpoint({ state: "blocked", restartCount, reason: "guardian_error", error: error?.message ?? String(error) });
appendLog(`blocked reason=guardian_error error=${error?.message ?? String(error)}`);
}
finally {
failureLedger.flush();
signalChild("SIGTERM");
releaseLock();
}

View File

@@ -4,6 +4,7 @@ import fs from "node:fs";
import path from "node:path";
import { execFileSync, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { batchContract, commandBudget, completedBatch } from "./wbv2-batch-contract.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
@@ -11,6 +12,7 @@ const read = (file) => fs.readFileSync(abs(file), "utf8");
const json = (file) => JSON.parse(read(file));
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
const dynamicInventoryPath = "tests/golden/WBV2/dynamic-inventory.json";
const out = `tests/golden/${process.argv[process.argv.indexOf("--task") + 1]}`;
const writeJson = (file, value) => {
fs.mkdirSync(path.dirname(abs(file)), { recursive: true });
fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`);
@@ -35,8 +37,28 @@ const parentId = record.dependsOn?.at(-1);
const parentPath = `tests/golden/${parentId}/manifest.json`;
const parent = json(parentPath);
if (parent.status !== "done" || parent.nextTask !== task) throw new Error(`module parent is not a completed handoff: ${parentId}`);
if (record.status !== "in_progress" || record.ready !== true) throw new Error(`module task ${task} is not active`);
if (!record.module.sourceBacked) throw new Error(`module ${record.module.moduleId} has no source-backed implementation`);
const contract = batchContract(task, record.module);
if (!record.batchId) record.batchId = contract.batchId;
if (!record.selectedRecordIds) record.selectedRecordIds = contract.selectedRecordIds;
if (!record.module.batchId) record.module.batchId = contract.batchId;
if (!record.module.selectedRecordIds) record.module.selectedRecordIds = contract.selectedRecordIds;
const commandContract = commandBudget(record.module.commands ?? []);
if (!commandContract.withinBudget) throw new Error(`task ${task} exceeds command budget: ${JSON.stringify(commandContract)}`);
const existingManifestPath = `${out}/manifest.json`;
if (fs.existsSync(abs(existingManifestPath))) {
try {
const existingManifest = json(existingManifestPath);
const legacyCompleted = existingManifest.status === "done"
&& existingManifest.nextTask === null
&& JSON.stringify(existingManifest.selectedRecordIds ?? record.module.selectedRecordIds ?? []) === JSON.stringify(contract.selectedRecordIds);
if (completedBatch(existingManifest, task, record.module) || legacyCompleted) {
process.stdout.write(`module-batch-idempotent task=${task} batchId=${contract.batchId} plannerRequired=true\n`);
process.exit(0);
}
} catch { /* A partial manifest is not evidence and follows the normal path. */ }
}
if (record.status !== "in_progress" || record.ready !== true) throw new Error(`module task ${task} is not active`);
for (const source of record.module.sourcePaths ?? []) if (!fs.existsSync(abs(source))) throw new Error(`module source is missing: ${source}`);
const sourceAuditPaths = (record.module.sourcePaths ?? []).filter((source) =>
/^(blender-5\.2\.0\/source\/blender\/|web\/protocol\/|web\/app\/src\/(?!vendor\/))/u.test(source)
@@ -48,7 +70,6 @@ const reusedReceipts = (record.module.reuseReceipts ?? []).map((receipt) => ({
reportSha256: sha(receipt.report),
}));
const out = `tests/golden/${task}`;
const reportPath = `${out}/module-receipt.json`;
const statusPath = `docs/status/${task}.md`;
const logPath = `${out}/module-run.log`;
@@ -130,7 +151,11 @@ const report = {
schemaVersion: 1,
task,
operation: "MODULE_FUNCTION_EXECUTION",
phase: failed || !sourceUnchanged ? "BLOCKED" : "CONTINUE",
moduleId: record.module.moduleId,
batchId: contract.batchId,
selectedRecordIds: contract.selectedRecordIds,
commandBudget: commandContract,
status: failed || !sourceUnchanged ? "BLOCKED" : "PASS",
source: { paths: record.module.sourcePaths, auditedPaths: sourceAuditPaths, before: sourceBefore, after: sourceAfter, unchanged: sourceUnchanged },
reusedReceipts,
@@ -159,7 +184,7 @@ if (failed || !sourceUnchanged) {
const manifestPath = `${out}/manifest.json`;
const evidenceArtifacts = Object.fromEntries((record.module.evidencePaths ?? []).filter((file) => fs.existsSync(abs(file))).map((file, item) => [`evidence${item + 1}`, { path: file, sha256: sha(file) }]));
const artifacts = { parentManifest: { path: parentPath, sha256: sha(parentPath) }, ...Object.fromEntries(record.module.sourcePaths.map((source, item) => [`source${item + 1}`, { path: source, sha256: sha(source) }])), ...evidenceArtifacts, moduleReceipt: { path: reportPath, sha256: sha(reportPath) }, status: { path: statusPath, sha256: sha(statusPath) }, log: { path: logPath, sha256: sha(logPath) }, plan: { path: index.source.path, sha256: sha(index.source.path) }, taskContext: { path: `${out}/task-context.json`, sha256: sha(`${out}/task-context.json`) } };
writeJson(manifestPath, { schemaVersion: 1, task, parentTask: parentId, operation: "MODULE_FUNCTION_EXECUTION", taskClass: "MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "BLENDER_WEB_FUNCTION_MODULE", artifacts, assertions: { moduleId: record.module.moduleId, sourceBacked: true, positiveCases: record.module.positive.length, negativeCases: record.module.negative.length, recoveryCases: record.module.recovery.length, commandCount: results.length, reusedReceiptCount: reusedReceipts.length, sourceUnchanged, fullFamilyParity: false, dynamicInventoryPath, dynamicSourceTreeSha256: dynamicInventory.authority.sourceTreeSha256, dynamicUnitCount: dynamicInventory.summary.unitCount, dynamicVerifiedUnits: dynamicInventory.coverage.verifiedUnits, dynamicUnverifiedUnits: dynamicInventory.coverage.unverifiedUnits, planningEstimate: dynamicInventory.policy.planningEstimate, historicalInventoryTotal: 6900, historicalEffectiveCompleted: 290, historicalEffectivePending: 6609, historicalReproduction: false }, nextTask });
writeJson(manifestPath, { schemaVersion: 1, task, parentTask: parentId, operation: "MODULE_FUNCTION_EXECUTION", phase: "CONTINUE", taskClass: "MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "BLENDER_WEB_FUNCTION_MODULE", batchId: contract.batchId, selectedRecordIds: contract.selectedRecordIds, artifacts, assertions: { moduleId: record.module.moduleId, batchId: contract.batchId, selectedRecordIds: contract.selectedRecordIds, sourceBacked: true, positiveCases: record.module.positive.length, negativeCases: record.module.negative.length, recoveryCases: record.module.recovery.length, commandCount: results.length, commandBudget, reusedReceiptCount: reusedReceipts.length, sourceUnchanged, fullFamilyParity: false, dynamicInventoryPath, dynamicSourceTreeSha256: dynamicInventory.authority.sourceTreeSha256, dynamicUnitCount: dynamicInventory.summary.unitCount, dynamicVerifiedUnits: dynamicInventory.coverage.verifiedUnits, dynamicUnverifiedUnits: dynamicInventory.coverage.unverifiedUnits, planningEstimate: dynamicInventory.policy.planningEstimate, historicalInventoryTotal: 6900, historicalEffectiveCompleted: 290, historicalEffectivePending: 6609, historicalReproduction: false }, nextTask });
if (record.module.coverageBindings?.length) {
execFileSync(process.execPath, [abs("tools/web/bind-wbv2-module-coverage.mjs"), "--task", task], { cwd: root, stdio: "inherit" });
execFileSync(process.execPath, [abs("tools/web/finalize-wbv2-module-batch.mjs"), "--task", task], { cwd: root, stdio: "inherit" });

View File

@@ -3,7 +3,7 @@ import crypto from "node:crypto";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { recordFailure } from "./wbv2-failure-ledger.mjs";
import { createFailureLedgerSession } from "./wbv2-failure-ledger.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const controlRoot = "tests/golden/WBV2";
@@ -11,6 +11,10 @@ const checkpointPath = `${controlRoot}/continuous-scheduler.json`;
const lockPath = `${controlRoot}/continuous-scheduler.lock`;
const logPath = `${controlRoot}/continuous-scheduler.log`;
const abs = (file) => path.join(root, file);
const failureLedger = createFailureLedgerSession(root, {
logPaths: [`${controlRoot}/continuous-scheduler.log`, `${controlRoot}/continuous-supervisor.log`, `${controlRoot}/continuous-guardian.log`],
});
let latestBatch = null;
const argValue = (name, fallback) => {
const index = process.argv.indexOf(name);
@@ -60,6 +64,7 @@ function writeCheckpoint(payload) {
pid: process.pid,
updatedAt: new Date().toISOString(),
...payload,
latestBatch: payload.latestBatch ?? latestBatch,
};
const stable = (value) => { const { pid, updatedAt, ...rest } = value ?? {}; return rest; };
try {
@@ -72,6 +77,43 @@ function writeCheckpoint(payload) {
fs.renameSync(abs(temporaryPath), abs(checkpointPath));
}
function batchInfo(pointer) {
if (!pointer?.task) return null;
let manifest = {};
try { manifest = JSON.parse(read(`tests/golden/${pointer.task}/manifest.json`)); }
catch { /* The active batch may not have produced a manifest yet. */ }
let card = "";
try { card = read(`docs/tasks/${pointer.task}.md`); }
catch { /* Readiness reports the missing card separately. */ }
const batchId = manifest.batchId ?? card.match(/^- batchId: `?([^`\n]+)`?/mu)?.[1] ?? null;
let selectedRecordIds = Array.isArray(manifest.selectedRecordIds) ? manifest.selectedRecordIds : [];
if (selectedRecordIds.length === 0) {
try {
const selected = card.match(/^- selectedRecordIds:\s*(\[[^\n]+\])/mu)?.[1];
selectedRecordIds = selected ? JSON.parse(selected) : [];
} catch { selectedRecordIds = []; }
}
return {
task: pointer.task,
batchId,
selectedRecordIds,
completed: manifest.status === "done" && manifest.nextTask === null,
};
}
function planNextBatch(pointer) {
const result = spawnSync("python3", [abs("my-tools/authorize_next_wbv2_task.py"), "--write"], {
cwd: root,
encoding: "utf8",
maxBuffer: 8 * 1024 * 1024,
});
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
if (output) appendLog(`planner task=${pointer?.task ?? "NONE"} exit=${result.status ?? 1}\n${output}`);
if ((result.status ?? 1) !== 0 && /no configured unresolved Blender source batch remains/u.test(output)) return { terminal: true };
if ((result.status ?? 1) !== 0) throw new Error(`next batch planner failed: ${output}`);
return { terminal: false };
}
function appendLog(message) {
fs.appendFileSync(abs(logPath), `[${new Date().toISOString()}] ${message}\n`);
}
@@ -91,10 +133,10 @@ function refreshContinuation(task) {
if (!fs.existsSync(continuationPath)) return false;
const pointer = currentTask();
const index = JSON.parse(read("tests/golden/WBV2/task-index.json"));
const plan = JSON.parse(read(index.source.path));
const taskIndex = plan.tasks.findIndex((candidate) => candidate.id === task);
const terminal = terminalTask(task);
const nextTask = terminal ? null : plan.tasks[taskIndex + 1]?.id ?? null;
// The compact task index already contains the direct successor. Reading the
// full plan here adds no evidence and used to dominate short handoff cycles.
const nextTask = terminal ? null : index.entries?.[task]?.next ?? null;
const inputFiles = ["docs/EXECUTION_QUEUE.md", `docs/tasks/${task}.md`, pointer.parentManifest, `tests/golden/${task}/task-context.json`];
const fingerprint = crypto.createHash("sha256").update(inputFiles.map((file) => `${file}\0${crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex")}\n`).join("")).digest("hex");
try {
@@ -246,7 +288,9 @@ let iteration = 0;
let completed = false;
try {
appendLog(`started pid=${process.pid} maxIterations=${maxIterations} commit=${commitEnabled} push=${pushEnabled}`);
writeCheckpoint({ state: "started", iteration, current: currentTask(), maxIterations, commit: commitEnabled, push: pushEnabled });
const initial = currentTask();
latestBatch = batchInfo(initial);
writeCheckpoint({ state: "started", iteration, current: initial, maxIterations, commit: commitEnabled, push: pushEnabled });
while (maxIterations === 0 || iteration < maxIterations) {
if (stopRequested) {
writeCheckpoint({ state: "stopped", iteration, current: currentTask(), reason: stopSignal });
@@ -254,6 +298,7 @@ try {
break;
}
const before = currentTask();
latestBatch = batchInfo(before);
if (!before) {
writeCheckpoint({ state: "complete", iteration, current: null, reason: "queue_closed" });
appendLog(`complete queue_closed iteration=${iteration}`);
@@ -266,14 +311,24 @@ try {
try {
const index = JSON.parse(read("tests/golden/WBV2/task-index.json"));
if (index.activeTask === null && index.entries?.[before.task]) {
writeCheckpoint({ state: "complete", iteration, current: before, reason: "v2_plan_terminal" });
const completedBatchInfo = { ...batchInfo(before), completed: true };
latestBatch = completedBatchInfo;
const planned = preflightOnly ? { terminal: true } : planNextBatch(before);
if (!planned.terminal) {
const afterPlan = currentTask();
latestBatch = batchInfo(afterPlan);
writeCheckpoint({ state: "planned_next_batch", iteration, current: afterPlan, before, reason: "completed_batch_planned" });
appendLog(`planned next batch before=${before.task} after=${afterPlan.task}`);
continue;
}
writeCheckpoint({ state: "complete", iteration, current: before, reason: "v2_plan_terminal", latestBatch: completedBatchInfo });
appendLog(`complete v2_plan_terminal task=${before.task} iteration=${iteration}`);
completed = true;
break;
}
}
catch (error) {
recordFailure(root, { task: before.task, phase: "scheduler", reason: "terminal_plan_check_failed", detail: error?.message ?? String(error) });
failureLedger.record({ task: before.task, phase: "scheduler", reason: "terminal_plan_check_failed", detail: error?.message ?? String(error) });
writeCheckpoint({ state: "blocked", iteration, before, reason: "terminal_plan_check_failed", error: error?.message ?? String(error) });
appendLog(`blocked task=${before.task} reason=terminal_plan_check_failed error=${error?.message ?? String(error)}`);
process.stderr.write(`scheduler-blocked task=${before.task} reason=terminal_plan_check_failed\n`);
@@ -290,7 +345,7 @@ try {
writeCheckpoint({ state: "preflight", iteration, before, readiness });
appendLog(`preflight task=${before.task} class=${readiness.taskClass ?? "UNKNOWN"} ok=${readiness.ok} reason=${readiness.reason ?? "ready"}`);
if (!readiness.ok) {
recordFailure(root, { task: before.task, phase: "scheduler", reason: readiness.reason });
failureLedger.record({ task: before.task, phase: "scheduler", reason: readiness.reason });
writeCheckpoint({ state: "blocked", iteration, before, readiness });
appendLog(`blocked task=${before.task} reason=${readiness.reason}`);
process.stderr.write(`scheduler-blocked task=${before.task} reason=${readiness.reason}\n`);
@@ -306,7 +361,7 @@ try {
refreshTaskContext(before.task);
}
catch (error) {
recordFailure(root, { task: before.task, phase: "scheduler", reason: "context_rotation_failed", detail: error?.message ?? String(error) });
failureLedger.record({ task: before.task, phase: "scheduler", reason: "context_rotation_failed", detail: error?.message ?? String(error) });
writeCheckpoint({ state: "blocked", iteration, before, reason: "context_rotation_failed", error: error?.message ?? String(error) });
process.stderr.write(`scheduler-blocked task=${before.task} reason=context_rotation_failed\n`);
exitCode = 2;
@@ -315,6 +370,7 @@ try {
writeCheckpoint({ state: "running", iteration: iteration + 1, before, readiness });
const child = runOne(before);
const after = currentTask();
latestBatch = batchInfo(after);
appendLog(`child task=${before.task} exit=${child.code} signal=${child.signal ?? "none"} after=${after?.task ?? "NONE"} log=${child.outputPath}`);
if (stopRequested) {
writeCheckpoint({ state: "stopped", iteration, before, after, reason: stopSignal, childExit: child.code });
@@ -323,7 +379,7 @@ try {
}
if (child.code !== 0) {
const reason = after?.task !== before.task ? "child_failed_after_queue_advance" : "child_failed_same_task";
recordFailure(root, { task: before.task, phase: "scheduler", reason, childExit: child.code });
failureLedger.record({ task: before.task, phase: "scheduler", reason, childExit: child.code });
writeCheckpoint({ state: "blocked", iteration, before, after, reason, childExit: child.code, childLog: child.outputPath });
appendLog(`blocked task=${before.task} reason=${reason}`);
process.stderr.write(`scheduler-blocked task=${before.task} reason=${reason} childExit=${child.code}\n`);
@@ -335,7 +391,7 @@ try {
if (refreshContinuation(after?.task ?? before.task) && commitEnabled) commitContinuation();
}
catch (error) {
recordFailure(root, { task: before.task, phase: "scheduler", reason: "continuation_rotation_failed", detail: error?.message ?? String(error) });
failureLedger.record({ task: before.task, phase: "scheduler", reason: "continuation_rotation_failed", detail: error?.message ?? String(error) });
writeCheckpoint({ state: "blocked", iteration, before, after, reason: "continuation_rotation_failed", error: error?.message ?? String(error) });
process.stderr.write(`scheduler-blocked task=${before.task} reason=continuation_rotation_failed\n`);
exitCode = 5;
@@ -348,7 +404,7 @@ try {
break;
}
if (!after || after.task === before.task) {
recordFailure(root, { task: before.task, phase: "scheduler", reason: "child_exited_without_queue_advance", childExit: child.code });
failureLedger.record({ task: before.task, phase: "scheduler", reason: "child_exited_without_queue_advance", childExit: child.code });
writeCheckpoint({ state: "blocked", iteration, before, after, reason: "child_exited_without_queue_advance", childExit: child.code, childLog: child.outputPath });
appendLog(`blocked task=${before.task} reason=child_exited_without_queue_advance`);
process.stderr.write(`scheduler-blocked task=${before.task} reason=child_exited_without_queue_advance\n`);
@@ -372,12 +428,13 @@ try {
}
catch (error) {
exitCode = 5;
try { recordFailure(root, { task: currentTask()?.task ?? "UNKNOWN", phase: "scheduler", reason: "scheduler_error", detail: error?.message ?? String(error) }); }
try { failureLedger.record({ task: currentTask()?.task ?? "UNKNOWN", phase: "scheduler", reason: "scheduler_error", detail: error?.message ?? String(error) }); }
catch { /* Preserve the original scheduler failure. */ }
writeCheckpoint({ state: "blocked", iteration, reason: "scheduler_error", error: error?.message ?? String(error) });
appendLog(`blocked reason=scheduler_error error=${error?.message ?? String(error)}`);
}
finally {
failureLedger.flush();
releaseLock();
}
process.exitCode = exitCode;

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { recordFailure } from "./wbv2-failure-ledger.mjs";
import { createFailureLedgerSession } from "./wbv2-failure-ledger.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
@@ -10,6 +10,7 @@ const supervisorRoot = "tests/golden/WBV2";
const supervisorLog = `${supervisorRoot}/continuous-supervisor.log`;
const checkpointPath = `${supervisorRoot}/continuous-supervisor.json`;
const lockPath = `${supervisorRoot}/continuous-supervisor.lock`;
const failureLedger = createFailureLedgerSession(root, { logPaths: [`${supervisorRoot}/continuous-supervisor.log`] });
const valueArg = (name, fallback) => {
const index = process.argv.indexOf(name);
@@ -158,7 +159,7 @@ try {
}
if (child.code !== 0) {
const reason = after.task === before.task ? "child_failed_same_task" : "child_failed_after_queue_advance";
recordFailure(root, { task: before.task, phase: "supervisor", reason, childExit: child.code });
failureLedger.record({ task: before.task, phase: "supervisor", reason, childExit: child.code });
writeCheckpoint({ state: "blocked", iteration: iteration + 1, before, after, reason, childExit: child.code });
exitCode = 3;
break;
@@ -170,7 +171,7 @@ try {
break;
}
if (after.task === before.task) {
recordFailure(root, { task: before.task, phase: "supervisor", reason: "child_exited_without_queue_advance", childExit: child.code });
failureLedger.record({ task: before.task, phase: "supervisor", reason: "child_exited_without_queue_advance", childExit: child.code });
writeCheckpoint({ state: "blocked", iteration: iteration + 1, before, after, reason: "child_exited_without_queue_advance", childExit: child.code });
exitCode = 4;
break;
@@ -193,6 +194,7 @@ try {
}
}
finally {
failureLedger.flush();
releaseLock();
}
process.exitCode = exitCode;

View File

@@ -22,6 +22,19 @@ const json = (file) => JSON.parse(read(file));
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const tokenEstimate = (value) => Math.ceil(Buffer.byteLength(value, "utf8") / 4);
function inputMetadata(file, includeHash = true) {
const relativePath = relative(file);
let stat;
try {
stat = fs.statSync(file, { bigint: true });
} catch (error) {
return { path: relativePath, exists: false, bytes: null, mtime_ns: null, sha256: null, error: error?.code ?? "STAT_FAILED" };
}
const metadata = { path: relativePath, exists: true, bytes: Number(stat.size), mtime_ns: stat.mtimeNs.toString() };
if (includeHash) metadata.sha256 = sha256File(file);
return metadata;
}
const queuePath = path.join(root, "docs/EXECUTION_QUEUE.md");
const planPath = path.join(root, "tests/golden/M15-03A/next-task-plan.json");
const legacyTaskIndexPath = path.join(root, "tests/golden/M15-03A/task-index.json");
@@ -367,6 +380,13 @@ export function buildTaskContext(requestedTask) {
const ownOutputDir = path.join(root, "tests/golden", task);
const generatedPaths = [relative(ownOutputDir), relative(ownManifestPath), relative(ownStatusPath)];
const selectedInputs = selectInputPaths(candidatePaths, { sourceBytes, sourceTokens, reservedPaths, generatedPaths });
const metadataPaths = [...new Set([
relative(queuePath),
relative(taskPath),
relative(parentManifestPath),
...(fs.existsSync(parentStatusPath) ? [relative(parentStatusPath)] : []),
...selectedInputs.inputPaths,
])].map((candidate) => path.join(root, candidate));
const nextTask = ownManifest?.nextTask ?? nextFromPlan(task) ?? null;
const context = {
schemaVersion: 1,
@@ -381,6 +401,7 @@ export function buildTaskContext(requestedTask) {
},
commands,
inputPaths: selectedInputs.inputPaths,
inputMetadata: Object.fromEntries(metadataPaths.map((file) => [relative(file), inputMetadata(file)])),
inputSelection: selectedInputs.inputSelection,
exitCriteria: entry?.exitCriteria ?? ["focused command exits 0", "report and manifest are hash-bound", "failure path does not publish a late result"],
nextTask,

View File

@@ -0,0 +1,60 @@
import crypto from "node:crypto";
export const COMMAND_BUDGET = Object.freeze({
maxCommands: 48,
maxReadCommands: 16,
});
/** Return the explicit source/RNA record IDs selected for one module batch. */
export function selectedRecordIds(module = {}) {
const values = module.selectedRecordIds ?? module.coverageBindings ?? module.sourceUnits ?? [];
return [...new Set((Array.isArray(values) ? values : []).filter((value) => typeof value === "string" && value.length > 0))];
}
/** A batch identity is stable across retries and changes when selection changes. */
export function batchIdFor(task, module = {}) {
const ids = selectedRecordIds(module);
const moduleId = module.moduleId ?? "UNKNOWN_MODULE";
const digest = crypto.createHash("sha256").update(JSON.stringify(ids)).digest("hex").slice(0, 16);
return module.batchId ?? `${task}:${moduleId}:${digest}`;
}
export function batchContract(task, module = {}) {
const selected = selectedRecordIds(module);
return {
batchId: batchIdFor(task, module),
selectedRecordIds: selected,
selectedCount: selected.length,
};
}
export function completedBatch(manifest, task, module = {}) {
const expected = batchContract(task, module);
return manifest?.status === "done"
&& manifest.batchId === expected.batchId
&& JSON.stringify(manifest.selectedRecordIds ?? []) === JSON.stringify(expected.selectedRecordIds);
}
export function commandBudget(commands = []) {
const values = Array.isArray(commands) ? commands.filter((value) => typeof value === "string") : [];
const readCommands = values.filter((value) => /\b(?:cat|head|tail|sed|rg|find|ls|stat)\b/u.test(value));
return {
maxCommands: COMMAND_BUDGET.maxCommands,
maxReadCommands: COMMAND_BUDGET.maxReadCommands,
commandCount: values.length,
readCommandCount: readCommands.length,
withinBudget: values.length <= COMMAND_BUDGET.maxCommands && readCommands.length <= COMMAND_BUDGET.maxReadCommands,
};
}
export function adaptiveBatchSize({ currentSize, history = [], minSize = 1, maxSize = currentSize, remaining = Number.MAX_SAFE_INTEGER }) {
const size = Math.max(minSize, Math.min(maxSize, currentSize));
const recent = history.filter((item) => Number.isFinite(item.durationMs) && Number.isFinite(item.commands)).slice(-3);
let next = size;
if (recent.some((item) => item.timeout === true || item.durationMs > 900_000 || item.commands > COMMAND_BUDGET.maxCommands)) {
next = Math.floor(size / 2);
} else if (recent.length > 0 && recent.every((item) => item.durationMs < 300_000 && item.commands < 24 && item.focusedGateFailure !== true)) {
next = size + 4;
}
return Math.max(minSize, Math.min(maxSize, remaining, next));
}

View File

@@ -5,7 +5,87 @@ import path from "node:path";
const ledgerPath = (root) => path.join(root, "tests/golden/WBV2/failure-ledger.json");
const keyFor = (entry) => crypto.createHash("sha256").update(JSON.stringify({ task: entry.task, phase: entry.phase, reason: entry.reason, childExit: entry.childExit ?? null })).digest("hex");
const readLedger = (root) => {
try {
const value = JSON.parse(fs.readFileSync(ledgerPath(root), "utf8"));
return {
schemaVersion: 2,
entries: Array.isArray(value.entries) ? value.entries : [],
hydratedLogs: Array.isArray(value.hydratedLogs) ? value.hydratedLogs : [],
};
}
catch {
return { schemaVersion: 2, entries: [], hydratedLogs: [] };
}
};
export function createFailureLedgerSession(root, { logPaths = [] } = {}) {
const ledger = readLedger(root);
const existing = new Set(ledger.entries.map((entry) => entry.key).filter(Boolean));
const hydrated = new Set(ledger.hydratedLogs);
let dirty = false;
// Recovery is deliberately narrow: only explicit cycle.completed events can
// add entries, and each log is imported at most once.
for (const relative of logPaths) {
if (hydrated.has(relative)) continue;
const file = path.join(root, relative);
if (!fs.existsSync(file)) continue;
let source;
try { source = fs.readFileSync(file, "utf8"); }
catch { continue; }
if (!/cycle\.completed/u.test(source)) continue;
for (const line of source.split("\n")) {
if (!/cycle\.completed/u.test(line)) continue;
let event;
try {
const start = line.indexOf("{");
event = JSON.parse(start >= 0 ? line.slice(start) : line);
}
catch { event = null; }
const entry = event?.failure ?? event?.entry;
if (!entry || typeof entry !== "object" || !entry.task || !entry.phase || !entry.reason) continue;
const key = keyFor(entry);
if (existing.has(key)) continue;
ledger.entries.push({ ...entry, key, recordedAt: event.recordedAt ?? new Date().toISOString(), hydratedFrom: relative });
existing.add(key);
dirty = true;
}
hydrated.add(relative);
dirty = true;
}
const record = (entry) => {
const key = keyFor(entry);
if (existing.has(key)) return false;
ledger.entries.push({ ...entry, key, recordedAt: new Date().toISOString() });
ledger.entries = ledger.entries.slice(-200);
existing.add(key);
dirty = true;
return true;
};
const flush = () => {
if (!dirty) return false;
const file = ledgerPath(root);
fs.mkdirSync(path.dirname(file), { recursive: true });
const temporary = `${file}.tmp-${process.pid}`;
fs.writeFileSync(temporary, `${JSON.stringify({ ...ledger, hydratedLogs: [...hydrated] }, null, 2)}\n`);
fs.renameSync(temporary, file);
dirty = false;
return true;
};
return { record, flush, ledger };
}
export function recordFailure(root, entry) {
const session = createFailureLedgerSession(root);
const recorded = session.record(entry);
session.flush();
return recorded;
}
/* Legacy callers should migrate to createFailureLedgerSession so a cycle only writes once. */
export function _legacyRecordFailure(root, entry) {
const file = ledgerPath(root);
let ledger = { schemaVersion: 1, entries: [] };
try { ledger = JSON.parse(fs.readFileSync(file, "utf8")); }