From 07e63efa255861bf9f5b71a5620dc8e6243beff8 Mon Sep 17 00:00:00 2001 From: mes123456 Date: Thu, 3 Sep 2026 15:50:39 +0800 Subject: [PATCH] perf(web): centralize batch handoff rotation --- my-tools/authorize_next_wbv2_task.py | 1 - tools/web/check-wbv2-context-cache.mjs | 10 ++-- tools/web/run-wbv2-continuous.mjs | 7 --- tools/web/run-wbv2-module-task.mjs | 10 ---- tools/web/run-wbv2-scheduler.mjs | 72 ++++++++++++++++++++++++++ tools/web/run-wbv2-supervisor.mjs | 8 --- 6 files changed, 78 insertions(+), 30 deletions(-) diff --git a/my-tools/authorize_next_wbv2_task.py b/my-tools/authorize_next_wbv2_task.py index 908b5f819..a8f699c32 100644 --- a/my-tools/authorize_next_wbv2_task.py +++ b/my-tools/authorize_next_wbv2_task.py @@ -451,7 +451,6 @@ def main() -> int: # The root is a completed queue-enable record. Preparing its context again # wastes budget and can fail solely because the parent manifest grew; only # the active implementation batch needs a packet. - run_node("tools/web/check-task-context.mjs", "--task", module_task, "--write") print(f"wbv2-successor-authorized root={root_task} next={module_task} module={batch['moduleId']} batchSize={len(batch['units'])} reused={len(batch.get('candidateUnits', batch['units'])) - len(batch['units'])} inventoryDelta=0") return 0 diff --git a/tools/web/check-wbv2-context-cache.mjs b/tools/web/check-wbv2-context-cache.mjs index a7b5ba1fe..50a48dbdf 100644 --- a/tools/web/check-wbv2-context-cache.mjs +++ b/tools/web/check-wbv2-context-cache.mjs @@ -22,7 +22,8 @@ const inputPaths = [ parentManifest, `tests/golden/${task}/task-context.json`, ]; -const inputHash = digest(inputPaths.map((file) => `${file}\0${digest(read(file))}\n`).join("")); +const inputHashFor = () => digest(inputPaths.map((file) => `${file}\0${fs.existsSync(abs(file)) ? digest(read(file)) : "MISSING"}\n`).join("")); +const inputHash = inputHashFor(); let cached = null; try { cached = JSON.parse(fs.readFileSync(cachePath, "utf8")); } catch { /* First use or an interrupted write. */ } @@ -31,14 +32,15 @@ if (!process.argv.includes("--force") && cached?.status === "PASS" && cached.tas 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 }); -const check = spawnSync(process.execPath, [abs("tools/web/check-task-context.mjs"), "--task", task], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); +const check = spawnSync(process.execPath, [abs("tools/web/check-task-context.mjs"), "--task", task, "--write"], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); if ((result.status ?? 1) !== 0 || (check.status ?? 1) !== 0) { process.stderr.write(`${result.stdout ?? ""}${result.stderr ?? ""}${check.stdout ?? ""}${check.stderr ?? ""}`); process.exit(Math.max(result.status ?? 1, check.status ?? 1)); } fs.mkdirSync(path.dirname(cachePath), { recursive: true }); -const payload = { schemaVersion: 1, status: "PASS", task, inputHash, refreshedAt: new Date().toISOString() }; +const refreshedInputHash = inputHashFor(); +const payload = { schemaVersion: 1, status: "PASS", task, inputHash: refreshedInputHash, refreshedAt: new Date().toISOString() }; const temporary = `${cachePath}.tmp-${process.pid}`; fs.writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`); fs.renameSync(temporary, cachePath); -process.stdout.write(`wbv2-context-cache-refresh task=${task} inputHash=${inputHash}\n`); +process.stdout.write(`wbv2-context-cache-refresh task=${task} inputHash=${refreshedInputHash}\n`); diff --git a/tools/web/run-wbv2-continuous.mjs b/tools/web/run-wbv2-continuous.mjs index ac436a746..fd2afc13d 100644 --- a/tools/web/run-wbv2-continuous.mjs +++ b/tools/web/run-wbv2-continuous.mjs @@ -58,11 +58,6 @@ function taskCommand(task) { return match[1]; } -function assertContext() { - const result = command("node tools/web/check-wbv2-context-cache.mjs", "tests/golden/WBV2/continuous-context.log"); - if (result.code !== 0) throw new Error(`context gate failed (exit ${result.code})`); -} - function typecheckProven(task) { try { const manifest = JSON.parse(read(`tests/golden/${task}/manifest.json`)); @@ -81,7 +76,6 @@ let iterations = 0; const summary = []; while (maxIterations === 0 || iterations < maxIterations) { const before = currentTask(); - assertContext(); // A completed V2 plan intentionally leaves the final task as an auditable // pointer. Do not replay its implementation command on later unattended // passes; close the runner cleanly at the terminal boundary. @@ -112,7 +106,6 @@ while (maxIterations === 0 || iterations < maxIterations) { : []; const handoffPaths = [ "docs/EXECUTION_QUEUE.md", - "nextTask.md", `docs/tasks/${before.task}.md`, ...(terminal ? [] : [`docs/tasks/${after.task}.md`]), `docs/status/${before.task}.md`, diff --git a/tools/web/run-wbv2-module-task.mjs b/tools/web/run-wbv2-module-task.mjs index fbf7637ba..7b0a5fd1c 100644 --- a/tools/web/run-wbv2-module-task.mjs +++ b/tools/web/run-wbv2-module-task.mjs @@ -153,7 +153,6 @@ if (failed || !sourceUnchanged) { if (nextTask) updatedQueue = updatedQueue.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${nextTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${task}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${nextTask}.md\`](tasks/${nextTask}.md) |`); writeText("docs/EXECUTION_QUEUE.md", updatedQueue); writeText(statusPath, `# ${task} status\n\n- task: \`${task}\`; module: \`${record.module.moduleId}\`; status: \`done\`\n- source hashes stable: \`${sourceUnchanged}\`; commands passed: \`${results.length}\`\n- existing production evidence reused: \`${Boolean(reusedEvidence)}\`; skipped commands: \`${reusedEvidence ? results.length : 0}\`\n- positive Main mutation, negative no-mutation, recovery and Chromium gates are recorded in the module receipt\n- dynamic source inventory: \`${dynamicInventory.summary.unitCount}\` units; verified \`${dynamicInventory.coverage.verifiedUnits}\`; unverified \`${dynamicInventory.coverage.unverifiedUnits}\`; planning estimate \`${dynamicInventory.policy.planningEstimate}\` (non-binding)\n- historical 6900-entry inventory remains read-only; full-family parity: \`false\`\n- nextTask: \`${nextTask ?? "NONE"}\`\n`); - execFileSync(process.execPath, [abs("tools/web/check-task-context.mjs"), "--task", task, "--write"], { cwd: root, stdio: "inherit" }); 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`) } }; @@ -162,14 +161,5 @@ if (failed || !sourceUnchanged) { 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" }); } - if (nextTask) execFileSync(process.execPath, [abs("tools/web/check-task-context.mjs"), "--task", nextTask, "--write"], { cwd: root, stdio: "inherit" }); - let next = read("nextTask.md"); - const tick = String.fromCharCode(96); - const snapshotTask = nextTask ?? task; - const snapshotState = nextTask ? "in_progress" : "done"; - const snapshotNext = nextTask ? plan.tasks[plan.tasks.findIndex((candidate) => candidate.id === nextTask) + 1]?.id ?? "NONE" : "NONE"; - const snapshotCommand = nextTask ? `node tools/web/run-wbv2-module-task.mjs --task ${nextTask} --write` : "node tools/web/check-wbv2-module-execution.mjs"; - next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${snapshotTask}${tick} |`).replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}${snapshotState}${tick} |`).replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${task}/manifest.json${tick} |`).replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${snapshotTask}.md${tick} |`).replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}${snapshotNext}${tick} |`).replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}${snapshotCommand}${tick} |`); - writeText("nextTask.md", next); console.log(`module-task-complete task=${task} module=${record.module.moduleId} next=${nextTask} commands=${results.length}`); } diff --git a/tools/web/run-wbv2-scheduler.mjs b/tools/web/run-wbv2-scheduler.mjs index aa612f619..f3748c326 100644 --- a/tools/web/run-wbv2-scheduler.mjs +++ b/tools/web/run-wbv2-scheduler.mjs @@ -1,4 +1,5 @@ import fs from "node:fs"; +import crypto from "node:crypto"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; @@ -79,6 +80,57 @@ function read(file) { return fs.readFileSync(abs(file), "utf8"); } +function refreshTaskContext(task) { + const result = spawnSync(process.execPath, [abs("tools/web/check-wbv2-context-cache.mjs"), "--task", task], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); + if ((result.status ?? 1) !== 0) throw new Error(`context rotation failed for ${task}: ${result.stderr ?? result.stdout ?? "unknown error"}`); +} + +function refreshContinuation(task) { + const continuationPath = abs("nextTask.md"); + const markerPath = abs("tests/golden/WBV2/continuation-snapshot.json"); + 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; + 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 { + const marker = JSON.parse(fs.readFileSync(markerPath, "utf8")); + if (marker.task === task && marker.fingerprint === fingerprint) return false; + } + catch { /* First rotation or an interrupted marker write. */ } + const tick = String.fromCharCode(96); + let next = fs.readFileSync(continuationPath, "utf8"); + const snapshotTask = task; + const snapshotState = terminal ? "done" : "in_progress"; + const snapshotNext = nextTask ?? "NONE"; + const snapshotCommand = nextTask ? `node tools/web/run-wbv2-module-task.mjs --task ${nextTask} --write` : "node tools/web/check-wbv2-module-execution.mjs"; + next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${snapshotTask}${tick} |`).replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}${snapshotState}${tick} |`).replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${task}/manifest.json${tick} |`).replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${snapshotTask}.md${tick} |`).replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}${snapshotNext}${tick} |`).replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}${snapshotCommand}${tick} |`); + fs.writeFileSync(continuationPath, next); + const marker = { schemaVersion: 1, task, fingerprint, refreshedAt: new Date().toISOString(), nextTask: snapshotNext }; + const temporary = `${markerPath}.tmp-${process.pid}`; + fs.writeFileSync(temporary, `${JSON.stringify(marker, null, 2)}\n`); + fs.renameSync(temporary, markerPath); + return true; +} + +function commitContinuation() { + const add = spawnSync("git", ["add", "-f", "nextTask.md", "tests/golden/WBV2/continuation-snapshot.json"], { cwd: root, encoding: "utf8" }); + if ((add.status ?? 1) !== 0) throw new Error(`continuation staging failed: ${add.stderr ?? add.stdout ?? "unknown error"}`); + const pending = spawnSync("git", ["diff", "--cached", "--quiet"], { cwd: root, encoding: "utf8" }); + if ((pending.status ?? 0) === 1) { + const commit = spawnSync("git", ["commit", "-m", "chore(web): rotate continuation snapshot"], { cwd: root, encoding: "utf8" }); + if ((commit.status ?? 1) !== 0) throw new Error(`continuation commit failed: ${commit.stderr ?? commit.stdout ?? "unknown error"}`); + if (pushEnabled) { + const push = spawnSync("git", ["push", "origin", "main"], { cwd: root, encoding: "utf8" }); + if ((push.status ?? 1) !== 0) throw new Error(`continuation push failed: ${push.stderr ?? push.stdout ?? "unknown error"}`); + } + } +} + function currentTask() { const queue = read("docs/EXECUTION_QUEUE.md"); const task = queue.match(/\| 当前任务 \| `(WBV2-P\d+-\d+)` \|/u)?.[1]; @@ -250,6 +302,16 @@ try { process.stdout.write(`scheduler-preflight-ready task=${before.task} class=${readiness.taskClass}\n`); break; } + try { + refreshTaskContext(before.task); + } + catch (error) { + recordFailure(root, { 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; + break; + } writeCheckpoint({ state: "running", iteration: iteration + 1, before, readiness }); const child = runOne(before); const after = currentTask(); @@ -268,6 +330,16 @@ try { exitCode = 3; break; } + 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) }); + 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; + break; + } if (after && after.task === before.task && terminalTask(before.task)) { writeCheckpoint({ state: "complete", iteration: iteration + 1, before, after, childExit: child.code, reason: "terminal_module_complete", childLog: child.outputPath }); completed = true; diff --git a/tools/web/run-wbv2-supervisor.mjs b/tools/web/run-wbv2-supervisor.mjs index 3b2e25207..59fc7e864 100644 --- a/tools/web/run-wbv2-supervisor.mjs +++ b/tools/web/run-wbv2-supervisor.mjs @@ -148,14 +148,6 @@ try { break; } const before = currentTask(); - const context = run(process.execPath, ["tools/web/check-wbv2-context-cache.mjs", "--task", before.task]); - appendLog(`preflight task=${before.task} contextExit=${context.code}`); - if (context.code !== 0) { - recordFailure(root, { task: before.task, phase: "supervisor", reason: "preflight_failed", childExit: context.code }); - writeCheckpoint({ state: "blocked", iteration, current: before, reason: "preflight_failed", contextExit: context.code }); - exitCode = 2; - break; - } writeCheckpoint({ state: "running", iteration: iteration + 1, before, child: childArgs.join(" ") }); const child = run(process.execPath, childArgs); appendLog(`child task=${before.task} exit=${child.code}\n${child.output}`);