143 lines
11 KiB
JavaScript
143 lines
11 KiB
JavaScript
import assert from "node:assert/strict";
|
|
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";
|
|
|
|
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 writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
|
|
|
const configs = {
|
|
"WBV2-P25-002": {
|
|
parent: "WBV2-P25-001", next: "WBV2-P25-003", family: "OBJECT_OPERATIONS.CORE_SCENE",
|
|
commands: ["setObjectVisibility", "createPrimitive", "duplicateObject", "deleteObject", "setObjectTransform", "setParent", "createCollection", "moveObjectToCollection", "renameId", "applyObjectTransform", "setObjectOrigin"],
|
|
files: ["blender-5.2.0/source/blender/web_engine/web_engine_api.cpp", "web/protocol/web-engine.ts", "web/protocol/web-engine-capabilities.ts", "web/protocol/core-scene-commands.ts", "web/app/src/engine-client/WebEngineClient.ts", "web/app/src/app/App.tsx"],
|
|
marker: ["web/protocol/core-scene-commands.ts", "coreSceneCommands"],
|
|
},
|
|
"WBV2-P25-003": {
|
|
parent: "WBV2-P25-002", next: "WBV2-P25-004", family: "MESH.AUTHORING",
|
|
commands: ["decimateMesh", "setModifierEnabled", "setModifierVisibility", "translateMeshVertices", "meshEdit", "createUVMap", "setActiveUVMap", "unwrapUV", "addMaterialSlot", "removeMaterialSlot", "assignMaterialFaces", "setMaterialPrincipled", "setMaterialImageNode", "joinObjects", "separateMeshFaces", "setVertexColors", "setVertexWeights"],
|
|
files: ["blender-5.2.0/source/blender/web_engine/web_engine_api.cpp", "web/protocol/web-engine.ts", "web/protocol/web-engine-capabilities.ts", "web/protocol/mesh-authoring-commands.ts", "web/app/src/app/App.tsx"],
|
|
marker: ["web/protocol/mesh-authoring-commands.ts", "meshAuthoringCommands"],
|
|
},
|
|
"WBV2-P25-004": {
|
|
parent: "WBV2-P25-003", next: "WBV2-P25-005", family: "GEOMETRY.NON_MESH",
|
|
commands: ["createCurve", "deleteNonMeshData", "setCurveControlPoints", "setCurveHandle", "setCurveTopology", "setCurveSplines", "setSurfaceTopology", "setFontBody", "setFontProperties", "setFontAdvanced", "importVFont", "setFontLinks", "setVolumeProperties", "setMetaballElements", "createGreasePencilLayer", "removeGreasePencilLayer", "moveGreasePencilLayer", "moveGreasePencilFrame", "insertGreasePencilFrame", "removeGreasePencilFrame", "setGreasePencilStrokes"],
|
|
files: ["blender-5.2.0/source/blender/web_engine/web_engine_api.cpp", "web/protocol/web-engine.ts", "web/protocol/web-engine-capabilities.ts", "web/protocol/nonmesh-geometry-commands.ts", "web/app/src/workers/web-engine.worker.ts"],
|
|
marker: ["web/protocol/nonmesh-geometry-commands.ts", "nonMeshGeometryCommands"],
|
|
},
|
|
"WBV2-P25-005": {
|
|
parent: "WBV2-P25-004", next: "WBV2-P25-006", family: "ANIMATION.RENDER",
|
|
commands: ["insertObjectKeyframe", "deleteObjectKeyframe", "setFCurveInterpolation", "setActiveAction", "setConstraint", "setNLAStack", "setLightProperties", "setCameraProperties", "setWorldProperties", "sculptStroke", "setSculptMeshAttributes"],
|
|
files: ["blender-5.2.0/source/blender/web_engine/web_engine_api.cpp", "web/protocol/web-engine.ts", "web/protocol/web-engine-capabilities.ts", "web/protocol/animation-render-commands.ts", "web/app/src/app/App.tsx"],
|
|
marker: ["web/protocol/animation-render-commands.ts", "animationRenderCommands"],
|
|
},
|
|
"WBV2-P25-006": {
|
|
parent: "WBV2-P25-005", next: "WBV2-P25-007", family: "IO.MEDIA_STORAGE",
|
|
commands: ["importImage", "importVFont"],
|
|
files: ["blender-5.2.0/source/blender/web_engine/web_engine_api.cpp", "web/protocol/web-engine.ts", "web/protocol/web-engine-capabilities.ts", "web/protocol/io-media-commands.ts", "web/app/src/workers/web-engine.worker.ts", "web/app/src/storage/StorageClient.ts"],
|
|
marker: ["web/protocol/io-media-commands.ts", "ioMediaCommands"],
|
|
},
|
|
"WBV2-P25-007": {
|
|
parent: "WBV2-P25-006", next: "WBV2-P25-008", family: "FRONTEND.WORKBENCH",
|
|
commands: ["createPrimitive", "duplicateObject", "deleteObject", "setObjectTransform", "meshEdit", "createCurve", "setGreasePencilStrokes", "setLightProperties", "setCameraProperties"],
|
|
files: ["web/protocol/web-engine.ts", "web/protocol/web-engine-capabilities.ts", "web/protocol/workbench-command-groups.ts", "web/app/src/app/App.tsx", "web/app/src/workers/web-engine.worker.ts"],
|
|
marker: ["web/protocol/workbench-command-groups.ts", "WORKBENCH_COMMAND_GROUPS"],
|
|
},
|
|
};
|
|
|
|
const taskArg = process.argv.indexOf("--task");
|
|
const task = taskArg >= 0 ? process.argv[taskArg + 1] : "WBV2-P25-002";
|
|
const config = configs[task];
|
|
if (!config) throw new Error(`unsupported implementation slice: ${task}`);
|
|
const parentManifestPath = `tests/golden/${config.parent}/manifest.json`;
|
|
const parent = json(parentManifestPath);
|
|
assert.equal(parent.status, "done");
|
|
assert.equal(parent.nextTask, task);
|
|
|
|
const protocol = read("web/protocol/web-engine.ts");
|
|
const registry = read("web/protocol/web-engine-capabilities.ts");
|
|
const api = read("blender-5.2.0/source/blender/web_engine/web_engine_api.cpp");
|
|
for (const command of config.commands) {
|
|
assert.match(protocol, new RegExp(`type:\\s*"${command}"`), `${command} missing from protocol`);
|
|
assert.match(registry, new RegExp(`\\b${command}\\s*:`), `${command} missing from capability registry`);
|
|
assert.match(api, new RegExp(command), `${command} missing from Main API`);
|
|
}
|
|
const [markerPath, marker] = config.marker;
|
|
assert.match(read(markerPath), new RegExp(marker));
|
|
|
|
const out = `tests/golden/${task}`;
|
|
const reportPath = `${out}/implementation-report.json`;
|
|
const statusPath = `docs/status/${task}.md`;
|
|
writeJson(reportPath, {
|
|
schemaVersion: 1,
|
|
task,
|
|
operation: "IMPLEMENT_CAPABILITY_FAMILY",
|
|
status: "PASS",
|
|
family: config.family,
|
|
sourceBacked: true,
|
|
implementation: { commandCount: config.commands.length, commands: config.commands, marker: { path: markerPath, symbol: marker }, sourcePaths: config.files, duplicateShimAdded: false },
|
|
verification: { status: "DEFERRED", chromium: false, malformedProductionReceipt: false, undoRedo: false, saveReopen: false, deterministicSecondRun: false, completionOverlay: false },
|
|
inventory: { total: 6900, legacyCompleted: 281, legacyPending: 6618, effectiveCompleted: 290, effectivePending: 6609, overlayChanged: false },
|
|
nextTask: config.next,
|
|
});
|
|
fs.mkdirSync(abs("docs/status"), { recursive: true });
|
|
fs.writeFileSync(abs(statusPath), `# ${task} status\n\n- task: \`${task}\`; parent: \`${config.parent}\`; status: \`done\`\n- capability family: \`${config.family}\`; source-backed commands wired: \`${config.commands.length}\`\n- implementation marker: \`${markerPath}#${marker}\`; no duplicate protocol shim\n- verification wave: \`DEFERRED\`; no Chromium receipt or completion overlay written\n- inventory unchanged: \`6900\` total, \`281\` legacy completed, \`6618\` legacy pending, \`290/6609\` effective carried forward\n- nextTask: \`${config.next}\`\n`);
|
|
|
|
const parentPlanPath = parent.artifacts?.plan?.path ?? "tests/golden/WBV2-P25-000/implementation-task-plan.json";
|
|
const planPath = `${out}/implementation-task-plan.json`;
|
|
const plan = json(parentPlanPath);
|
|
plan.tasks = plan.tasks.map((candidate) => candidate.id === task
|
|
? { ...candidate, status: "done", ready: false }
|
|
: candidate.id === config.next
|
|
? { ...candidate, status: "in_progress", ready: true }
|
|
: { ...candidate, ready: false });
|
|
writeJson(planPath, plan);
|
|
fs.writeFileSync(abs(`docs/tasks/${config.next}.md`), read(`docs/tasks/${config.next}.md`).replace("- status: planned", "- status: in_progress"));
|
|
execFileSync(process.execPath, [abs("tools/web/generate-v2-task-index.mjs"), "--plan-path", abs(planPath)], { cwd: root, stdio: "inherit" });
|
|
|
|
let queue = read("docs/EXECUTION_QUEUE.md");
|
|
queue = queue.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${config.next}\` |`)
|
|
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${task}/manifest.json\` |`)
|
|
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${config.next}.md\`](tasks/${config.next}.md) |`);
|
|
fs.writeFileSync(abs("docs/EXECUTION_QUEUE.md"), queue);
|
|
|
|
execFileSync(process.execPath, [abs("tools/web/check-task-context.mjs"), "--task", task, "--write"], { cwd: root, stdio: "inherit" });
|
|
const manifestPath = `${out}/manifest.json`;
|
|
writeJson(manifestPath, {
|
|
schemaVersion: 1,
|
|
task,
|
|
parentTask: config.parent,
|
|
operation: "IMPLEMENT_CAPABILITY_FAMILY",
|
|
status: "done",
|
|
enablingTask: false,
|
|
parityStateChange: false,
|
|
runtime: "SOURCE_BACKED_IMPLEMENTATION_WAVE",
|
|
artifacts: {
|
|
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) },
|
|
...Object.fromEntries(config.files.map((file) => [path.basename(file).replace(/\W+/gu, "_").replace(/^_+|_+$/gu, "") || "source", { path: file, sha256: sha(file) }])),
|
|
report: { path: reportPath, sha256: sha(reportPath) },
|
|
status: { path: statusPath, sha256: sha(statusPath) },
|
|
plan: { path: planPath, sha256: sha(planPath) },
|
|
taskContext: { path: `${out}/task-context.json`, sha256: sha(`${out}/task-context.json`) },
|
|
},
|
|
assertions: { capabilityFamily: config.family, sourceBacked: true, commandCount: config.commands.length, unsupportedFailsClosed: true, verificationDeferred: true, completionOverlayWritten: false, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, historicalReproduction: false },
|
|
nextTask: config.next,
|
|
});
|
|
execFileSync(process.execPath, [abs("tools/web/check-task-context.mjs"), "--task", config.next, "--write"], { cwd: root, stdio: "inherit" });
|
|
|
|
let next = read("nextTask.md");
|
|
next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | \`${config.next}\` |`)
|
|
.replace(/\| Current task state \| `[^`]+` \|/u, "| Current task state | `in_progress` |")
|
|
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | \`tests/golden/${task}/manifest.json\` |`)
|
|
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | \`docs/tasks/${config.next}.md\` |`)
|
|
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | \`${configs[config.next]?.next ?? "NONE"}\` |`)
|
|
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | \`node tools/web/generate-wbv2-implementation-slice.mjs --task ${config.next} --write\` |`);
|
|
fs.writeFileSync(abs("nextTask.md"), next);
|
|
console.log(`wbv2-implementation-slice-written task=${task} family=${config.family} commands=${config.commands.length} next=${config.next} verification=DEFERRED`);
|