feat(web): batch modifier reader coverage
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 11:11:20 +08:00
parent 2542f82785
commit 61b27cdebe
72 changed files with 23097 additions and 208 deletions

View File

@@ -0,0 +1,114 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
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 arg = (name, fallback = null) => {
const index = process.argv.indexOf(name);
return index >= 0 ? process.argv[index + 1] : fallback;
};
const task = arg("--task");
if (!task) throw new Error("usage: node tools/web/bind-wbv2-module-coverage.mjs --task <task-id>");
const manifestPath = `tests/golden/${task}/manifest.json`;
const productionPath = arg("--production", `tests/golden/${task}/modifier-reader-batch-production.json`);
const manifest = json(manifestPath);
assert.equal(manifest.status, "done");
const moduleRecord = Object.values(json("tests/golden/WBV2-P1100-000/module-execution-plan.json").tasks ?? {})
.find((candidate) => candidate.id === task);
const planPath = manifest.artifacts.plan.path;
const plan = json(planPath);
const taskRecord = plan.tasks.find((candidate) => candidate.id === task);
const module = taskRecord?.module;
const requestedUnits = (arg("--units") ?? "").split(",").map((value) => value.trim()).filter(Boolean);
const coverageBindings = requestedUnits.length > 0 ? requestedUnits : module?.coverageBindings ?? [];
const capabilityId = module?.moduleId ?? arg("--capability", task);
if (!coverageBindings.length) throw new Error(`task ${task} has no coverageBindings`);
const production = json(productionPath);
assert.equal(production.browser, "chromium");
assert.equal(production.queueMutation, false);
assert.equal(production.error, undefined);
assert.equal(production.saveReopenExact, true);
assert.equal(production.deterministicExact, true);
const inventoryPath = "tests/golden/WBV2/dynamic-inventory.json";
const inventory = json(inventoryPath);
assert.equal(production.fixtureSha256, sha(production.fixture));
const bindingsPath = "tests/golden/WBV2/dynamic-coverage-bindings.json";
const existing = fs.existsSync(abs(bindingsPath)) ? json(bindingsPath) : [];
const byId = new Map(existing.map((binding) => [binding.unitId, binding]));
for (const unitId of coverageBindings) {
const unit = inventory.units.find((candidate) => candidate.id === unitId);
assert.ok(unit, `dynamic unit missing: ${unitId}`);
if (unit.coverageStatus === "VERIFIED") {
assert.equal(byId.get(unitId)?.task, task, `dynamic unit already belongs to another task: ${unitId}`);
}
const sourceLocation = unit.locations?.[0];
assert.ok(sourceLocation?.path, `source location missing: ${unitId}`);
byId.set(unitId, {
unitId,
task,
capabilityId,
sourceLocation,
evidenceManifest: { path: manifestPath, sha256: sha(manifestPath) },
productionEvidence: { path: productionPath, sha256: sha(productionPath) },
sourceTreeSha256: inventory.authority.sourceTreeSha256,
status: "VERIFIED",
});
}
const bindings = [...byId.values()];
const verifiedIds = new Set(bindings.map((binding) => binding.unitId));
const updatedUnits = inventory.units.map((unit) => verifiedIds.has(unit.id)
? { ...unit, coverageStatus: "VERIFIED", coverage: bindings.find((binding) => binding.unitId === unit.id) }
: { ...unit, coverageStatus: "UNVERIFIED", ...(unit.coverage ? {} : {}) });
const verifiedUnits = updatedUnits.filter((unit) => unit.coverageStatus === "VERIFIED").length;
const updated = {
...inventory,
coverage: { ...inventory.coverage, verifiedUnits, unverifiedUnits: inventory.summary.unitCount - verifiedUnits, sourceMappingRequired: inventory.summary.unitCount, inventoryMutation: "explicit_source_case_bindings_only", lastReconciledAt: "2026-09-01" },
units: updatedUnits,
coverageLedger: bindings,
};
fs.writeFileSync(abs(bindingsPath), `${JSON.stringify(bindings, null, 2)}\n`);
fs.writeFileSync(abs(inventoryPath), `${JSON.stringify(updated, null, 2)}\n`);
const coverageManifestPath = "tests/golden/WBV2/dynamic-coverage-manifest.json";
if (fs.existsSync(abs(coverageManifestPath))) {
const coverageManifest = json(coverageManifestPath);
coverageManifest.artifacts.inventory.sha256 = sha(inventoryPath);
coverageManifest.artifacts.bindings.sha256 = sha(bindingsPath);
coverageManifest.coverage = { ...coverageManifest.coverage, unitCount: inventory.summary.unitCount, verifiedUnits, unverifiedUnits: inventory.summary.unitCount - verifiedUnits, boundUnitDelta: coverageBindings.length, sourceTreeSha256: inventory.authority.sourceTreeSha256 };
fs.writeFileSync(abs(coverageManifestPath), `${JSON.stringify(coverageManifest, null, 2)}\n`);
}
const inventoryManifestPath = "tests/golden/WBV2/dynamic-inventory-manifest.json";
if (fs.existsSync(abs(inventoryManifestPath))) {
const inventoryManifest = json(inventoryManifestPath);
if (inventoryManifest.artifacts?.inventory) inventoryManifest.artifacts.inventory.sha256 = sha(inventoryPath);
if (inventoryManifest.inventory) {
inventoryManifest.inventory.verifiedUnits = verifiedUnits;
inventoryManifest.inventory.unverifiedUnits = inventory.summary.unitCount - verifiedUnits;
inventoryManifest.inventory.boundCaseDelta = coverageBindings.length;
}
fs.writeFileSync(abs(inventoryManifestPath), `${JSON.stringify(inventoryManifest, null, 2)}\n`);
}
const auditPath = "tests/golden/WBV2/dynamic-coverage-audit.json";
if (fs.existsSync(abs(auditPath))) {
const audit = json(auditPath);
const coverageByKind = {};
const coverageByFamily = {};
for (const unit of updatedUnits) {
const kind = coverageByKind[unit.kind] ??= { verified: 0, unverified: 0 };
const family = coverageByFamily[unit.family] ??= { verified: 0, unverified: 0 };
const key = unit.coverageStatus === "VERIFIED" ? "verified" : "unverified";
kind[key] += 1;
family[key] += 1;
}
audit.dynamicInventory = { ...(audit.dynamicInventory ?? {}), path: inventoryPath, afterSha256: sha(inventoryPath), unitCount: inventory.summary.unitCount, verifiedUnits, unverifiedUnits: inventory.summary.unitCount - verifiedUnits };
audit.explicitBindings = bindings;
audit.coverageByKind = coverageByKind;
audit.coverageByFamily = coverageByFamily;
fs.writeFileSync(abs(auditPath), `${JSON.stringify(audit, null, 2)}\n`);
}
process.stdout.write(`wbv2-module-coverage-bound task=${task} module=${capabilityId} added=${coverageBindings.length} verified=${verifiedUnits} unverified=${inventory.summary.unitCount - verifiedUnits}\n`);

View File

@@ -29,7 +29,19 @@ fi
export WEB_EMSCRIPTEN_VERSION
export EM_CONFIG="${web_workspace_root}/.emscripten-web"
export EM_CACHE="${web_workspace_root}/.emcache"
# Prefer RAM-backed compiler caches on Linux hosts with enough tmpfs space.
# The cache only contains derived Emscripten artifacts; evidence and source
# outputs remain in the repository. Callers can override either path.
if [[ -d /dev/shm && -w /dev/shm ]]; then
web_ram_cache_root="${WEB_RAM_CACHE_ROOT-/dev/shm/blender-wasm-${UID}}"
mkdir -p "${web_ram_cache_root}/emcache" "${web_ram_cache_root}/tmp"
export EM_CACHE="${EM_CACHE-${web_ram_cache_root}/emcache}"
export TMPDIR="${TMPDIR-${web_ram_cache_root}/tmp}"
else
export EM_CACHE="${EM_CACHE-${web_workspace_root}/.emcache}"
fi
export CMAKE_BUILD_PARALLEL_LEVEL="${CMAKE_BUILD_PARALLEL_LEVEL-8}"
export NINJAFLAGS="${NINJAFLAGS--j8}"
export WEB_EMSCRIPTEN_CFLAGS="-sENVIRONMENT=web,worker -sMODULARIZE=1 -sEXPORT_ES6=1"
export WEB_EMSCRIPTEN_LDFLAGS="-sALLOW_MEMORY_GROWTH=1 -sFILESYSTEM=1"

View File

@@ -0,0 +1,111 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { execFileSync, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
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 writeJson = (file, value) => {
fs.mkdirSync(path.dirname(abs(file)), { recursive: true });
fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`);
};
const writeText = (file, value) => {
fs.mkdirSync(path.dirname(abs(file)), { recursive: true });
fs.writeFileSync(abs(file), value);
};
const taskArg = process.argv.indexOf("--task");
const task = taskArg >= 0 ? process.argv[taskArg + 1] : null;
if (!task || !process.argv.includes("--write")) throw new Error("usage: node tools/web/run-wbv2-module-task.mjs --task <task-id> --write");
const queue = read("docs/EXECUTION_QUEUE.md");
const currentTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const index = json("tests/golden/WBV2/task-index.json");
const plan = json(index.source.path);
const dynamicInventory = json(dynamicInventoryPath);
const record = plan.tasks.find((candidate) => candidate.id === task);
if (currentTask !== task) throw new Error(`module task is not the authoritative queue task: ${currentTask}!=${task}`);
if (!record?.module) throw new Error(`task ${task} is not a module task`);
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`);
for (const source of record.module.sourcePaths ?? []) if (!fs.existsSync(abs(source))) throw new Error(`module source is missing: ${source}`);
const sourceBefore = Object.fromEntries((record.module.sourcePaths ?? []).map((source) => [source, sha(source)]));
const reusedReceipts = (record.module.reuseReceipts ?? []).map((receipt) => ({
...receipt,
manifestSha256: sha(receipt.manifest),
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`;
const results = [];
let failed = false;
for (const command of record.module.commands ?? []) {
const result = spawnSync("bash", ["-lc", command], { cwd: root, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
results.push({ command, exitCode: result.status ?? 1, output });
if ((result.status ?? 1) !== 0) { failed = true; break; }
}
fs.mkdirSync(abs(out), { recursive: true });
writeText(logPath, results.map((result) => `> ${result.command}\n${result.output}`).join("\n"));
const sourceAfter = Object.fromEntries((record.module.sourcePaths ?? []).map((source) => [source, sha(source)]));
const sourceUnchanged = JSON.stringify(sourceBefore) === JSON.stringify(sourceAfter);
const report = {
schemaVersion: 1,
task,
operation: "MODULE_FUNCTION_EXECUTION",
moduleId: record.module.moduleId,
status: failed || !sourceUnchanged ? "BLOCKED" : "PASS",
source: { paths: record.module.sourcePaths, before: sourceBefore, after: sourceAfter, unchanged: sourceUnchanged },
reusedReceipts,
cases: { positive: record.module.positive, negative: record.module.negative, recovery: record.module.recovery },
commands: results.map(({ command, exitCode }) => ({ command, exitCode })),
verification: { required: plan.verificationPolicy.required, chromiumOnly: plan.verificationPolicy.browser === "chromium_only", fullFamilyParity: false },
inventory: { historical: plan.inventory, dynamic: { path: dynamicInventoryPath, sha256: sha(dynamicInventoryPath), sourceTreeSha256: dynamicInventory.authority.sourceTreeSha256, unitCount: dynamicInventory.summary.unitCount, planningEstimate: dynamicInventory.policy.planningEstimate, hardTarget: dynamicInventory.policy.hardTarget, verifiedUnits: dynamicInventory.coverage.verifiedUnits, unverifiedUnits: dynamicInventory.coverage.unverifiedUnits } },
nextTask: failed || !sourceUnchanged ? task : plan.tasks[plan.tasks.findIndex((candidate) => candidate.id === task) + 1]?.id ?? null,
};
writeJson(reportPath, report);
if (failed || !sourceUnchanged) {
writeText(statusPath, `# ${task} status\n\n- task: \`${task}\`; module: \`${record.module.moduleId}\`; status: \`in_progress\`\n- module execution blocked; no queue advance or parity claim\n- source unchanged: \`${sourceUnchanged}\`; failed command: \`${results.find((result) => result.exitCode !== 0)?.command ?? "source hash drift"}\`\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\n- nextTask: \`${task}\`\n`);
process.exitCode = 2;
} else {
const nextTask = report.nextTask;
const planIndex = plan.tasks.findIndex((candidate) => candidate.id === task);
plan.tasks = plan.tasks.map((candidate, index) => index === planIndex ? { ...candidate, status: "done", ready: false } : candidate.id === nextTask ? { ...candidate, status: "in_progress", ready: true } : { ...candidate, ready: false });
if (!nextTask) plan.status = "COMPLETED_MODULE_EXECUTION";
writeJson(index.source.path, plan);
execFileSync(process.execPath, [abs("tools/web/generate-v2-task-index.mjs"), "--plan-path", abs(index.source.path)], { cwd: root, stdio: "inherit" });
let updatedQueue = read("docs/EXECUTION_QUEUE.md");
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- 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`) } };
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 });
if (record.module.coverageBindings?.length) {
execFileSync(process.execPath, [abs("tools/web/bind-wbv2-module-coverage.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}`);
}