chore: checkpoint web Blender migration source, tools and docs
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

Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
mes123456
2026-11-10 11:18:01 +08:00
parent 122142c26c
commit f3e21c6b98
6887 changed files with 257154 additions and 12294 deletions

21
tools/web/.probe-dom.mjs Normal file
View File

@@ -0,0 +1,21 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const dump = await page.evaluate(() => ({
ariaLabels: [...document.querySelectorAll("[aria-label]")].map((n) => n.getAttribute("aria-label")),
buttons: [...document.querySelectorAll("button")].map((n) => (n.textContent || n.getAttribute("aria-label") || "").trim()).filter(Boolean).slice(0, 80),
dataAttrs: [...new Set([...document.querySelectorAll("*")].flatMap((n) => [...n.attributes].map((a) => a.name).filter((x) => x.startsWith("data-"))))],
workspaces: [...document.querySelectorAll("nav[aria-label]")].map((nav) => ({ nav: nav.getAttribute("aria-label"), tabs: [...nav.querySelectorAll("button")].map((b) => (b.textContent || "").trim()) })),
mainAttrs: [...document.querySelectorAll("main")].map((m) => [...m.attributes].map((a) => `${a.name}=${a.value}`)),
}));
console.log(JSON.stringify(dump, null, 1).slice(0, 6000));
await browser.close();

27
tools/web/.probe10.mjs Normal file
View File

@@ -0,0 +1,27 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = () => page.evaluate(() => ({ rev: document.querySelector("main")?.getAttribute("data-current-main-revision"), stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent, diag: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,120) }));
const click = (l) => page.evaluate((v) => { const b = document.querySelector(`[aria-label="${v}"]`); b?.click(); return Boolean(b); }, l);
await click("添加立方体"); await page.waitForTimeout(9000);
console.log("ADD", JSON.stringify(await st()));
await click("撤销"); await page.waitForTimeout(9000);
console.log("UNDO1", JSON.stringify(await st()));
await click("撤销"); await page.waitForTimeout(9000);
console.log("UNDO2", JSON.stringify(await st()));
// open undo history dialog
await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="主菜单"] button')].find(b => (b.textContent||"").trim() === "编辑")?.click());
await page.waitForTimeout(1500);
await page.evaluate(() => [...document.querySelectorAll("button")].find(b => (b.textContent||"").trim() === "撤销历史")?.click());
await page.waitForTimeout(3000);
console.log("HISTORY-DIALOG", JSON.stringify(await page.evaluate(() => { const d = document.querySelector('[role="dialog"]'); return d ? [...d.querySelectorAll("li,button,tr")].map(n => (n.textContent||"").trim()).filter(Boolean).slice(0,25) : null; })));
await browser.close();

21
tools/web/.probe11.mjs Normal file
View File

@@ -0,0 +1,21 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const errs = []; page.on("console", m => { if (m.type()==="error") errs.push(m.text().slice(0,300)); });
await page.goto("http://127.0.0.1:5403/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const ws = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), diag: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }));
for (const name of ["Shading", "UV Editing", "Layout"]) {
await page.evaluate((v) => { const b = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find(n => (n.textContent||"").trim() === v); b?.click(); }, name);
await page.waitForTimeout(12000);
console.log(name, JSON.stringify(await ws()));
}
console.log("ERRS", JSON.stringify(errs.slice(-6)));
await browser.close();

30
tools/web/.probe12.mjs Normal file
View File

@@ -0,0 +1,30 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const click = (l) => page.evaluate((v) => { const b = document.querySelector(`[aria-label="${v}"]`); b?.click(); return Boolean(b); }, l);
const stats = () => page.evaluate(() => (document.querySelector('[data-testid="scene-stats"]')?.textContent ?? "").match(/Objects (\d+)/)?.[1] ?? "?");
const hist = async () => {
await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="主菜单"] button')].find(b => (b.textContent||"").trim() === "编辑")?.click());
await page.waitForTimeout(1200);
await page.evaluate(() => [...document.querySelectorAll("button")].find(b => (b.textContent||"").trim() === "撤销历史")?.click());
await page.waitForTimeout(2500);
const items = await page.evaluate(() => { const d = document.querySelector('[role="dialog"]'); return d ? [...d.querySelectorAll("li,button,tr")].map(n => (n.textContent||"").trim()).filter(Boolean).slice(0,30) : null; });
await page.keyboard.press("Escape"); await page.waitForTimeout(1500);
return items;
};
await click("添加立方体"); await page.waitForTimeout(9000);
console.log("ADD objects=", await stats(), "history=", JSON.stringify(await hist()));
for (let i = 1; i <= 4; i++) {
await click("撤销"); await page.waitForTimeout(9000);
console.log("UNDO"+i+" objects=", await stats(), "history=", JSON.stringify(await hist()));
}
await browser.close();

28
tools/web/.probe13.mjs Normal file
View File

@@ -0,0 +1,28 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const logs = []; page.on("console", m => logs.push(m.type()+": "+m.text().slice(0,400)));
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const before = logs.length;
await page.evaluate(() => { const b = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find(n => (n.textContent||"").trim() === "Shading"); b?.click(); });
await page.waitForTimeout(12000);
const state = await page.evaluate(() => ({
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
statusBar: document.querySelector('[data-testid="engine-status"]')?.textContent,
bodyText: (document.body.innerText||"").split("\n").filter(l => /失败|错误|error|unavailable|无法/i.test(l)).slice(0,8),
}));
console.log("STATE", JSON.stringify(state));
console.log("NEWLOGS", JSON.stringify(logs.slice(before).slice(0,10), null, 1));
// confirm user impact: can we still add a cube after the wedge?
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
await page.waitForTimeout(6000);
console.log("AFTER-ADD", JSON.stringify(await page.evaluate(() => ({ stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
await browser.close();

30
tools/web/.probe14.mjs Normal file
View File

@@ -0,0 +1,30 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = () => page.evaluate(() => ({
ws: document.querySelector("main")?.getAttribute("data-workspace"),
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
inputReady: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-input-ready"),
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,120),
}));
await page.evaluate(() => { const b = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find(n => (n.textContent||"").trim() === "Shading"); b?.click(); });
await page.waitForTimeout(8000);
console.log("SHADING", JSON.stringify(await st()));
// click into the 3D viewport area to activate it
const box = await page.evaluate(() => { const c = document.querySelector('canvas[aria-label="Blender desktop"]'); const r = c?.getBoundingClientRect(); return r ? {x:r.x+r.width*0.5, y:r.y+r.height*0.5} : null; });
if (box) { await page.mouse.click(box.x, box.y); }
for (const t of [3000, 6000, 12000]) { await page.waitForTimeout(t === 3000 ? 3000 : t === 6000 ? 3000 : 6000); console.log("AFTER-CLICK-"+t, JSON.stringify(await st())); }
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
await page.waitForTimeout(6000);
console.log("ADD", JSON.stringify(await st()));
await browser.close();

22
tools/web/.probe15.mjs Normal file
View File

@@ -0,0 +1,22 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
await page.evaluate(() => { const b = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find(n => (n.textContent||"").trim() === "Shading"); b?.click(); });
await page.waitForTimeout(10000);
console.log("TEXT", JSON.stringify((await page.evaluate(() => document.body.innerText)).slice(0, 1500)));
console.log("ERROREL", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('[role=alert],.native-error,[class*=error]')].map(n => (n.textContent||"").trim().slice(0,300)).filter(Boolean))));
// try Escape then retry edit
await page.keyboard.press("Escape"); await page.waitForTimeout(3000);
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
await page.waitForTimeout(6000);
console.log("AFTER-ESC", JSON.stringify(await page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display") }))));
await browser.close();

34
tools/web/.probe16.mjs Normal file
View File

@@ -0,0 +1,34 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = () => page.evaluate(() => ({
ws: document.querySelector("main")?.getAttribute("data-workspace"),
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,140),
}));
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
await go("Shading"); await page.waitForTimeout(8000);
console.log("S1", JSON.stringify(await st()));
// retry switching repeatedly with increasing waits: does the python flag clear?
for (const w of [4000, 8000, 15000, 25000]) {
await go("Layout"); await page.waitForTimeout(w);
console.log("TRY-"+w, JSON.stringify(await st()));
}
// and does a same-candidate fresh reload switch away from Shading directly?
await page.reload({ waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
for (const n of ["Shading","UV Editing","Layout","Modeling"]) { await go(n); await page.waitForTimeout(9000); console.log("SEQ-"+n, JSON.stringify(await st())); }
await browser.close();

28
tools/web/.probe17.mjs Normal file
View File

@@ -0,0 +1,28 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5404/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = () => page.evaluate(() => ({
ws: document.querySelector("main")?.getAttribute("data-workspace"),
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,300),
}));
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
// A: which workspaces can be entered and left
for (const n of ["Modeling","Scripting","Layout"]) { await go(n); await page.waitForTimeout(7000); console.log("A-"+n, JSON.stringify(await st())); }
await go("Shading"); await page.waitForTimeout(9000);
console.log("B-Shading", JSON.stringify(await st()));
await go("Layout"); await page.waitForTimeout(7000);
console.log("B-Leave", JSON.stringify(await st()));
// C: does an explicit python exec also report the same active handle?
console.log("C", JSON.stringify(await st()));
await browser.close();

28
tools/web/.probe18.mjs Normal file
View File

@@ -0,0 +1,28 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5404/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = () => page.evaluate(() => ({
ws: document.querySelector("main")?.getAttribute("data-workspace"),
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,240),
}));
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
// 1) enter Shading from a NON-Shading workspace (Modeling) and leave
await go("Modeling"); await page.waitForTimeout(8000); console.log("M", JSON.stringify(await st()));
await go("Shading"); await page.waitForTimeout(9000); console.log("M->S", JSON.stringify(await st()));
await go("Modeling"); await page.waitForTimeout(8000); console.log("S->M", JSON.stringify(await st()));
// 2) leave Shading a second time after failure
await go("Shading"); await page.waitForTimeout(7000);
await go("Layout"); await page.waitForTimeout(8000); console.log("S->L", JSON.stringify(await st()));
await go("Scripting"); await page.waitForTimeout(8000); console.log("S->Scr", JSON.stringify(await st()));
await browser.close();

22
tools/web/.probe19.mjs Normal file
View File

@@ -0,0 +1,22 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent }));
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
// Compare workspaces that also host node editors: Texture Paint, Compositing, Geometry Nodes
for (const n of ["Texture Paint","Compositing","Geometry Nodes","Shading"]) {
await go(n); await page.waitForTimeout(9000);
const entered = await st();
await go("Layout"); await page.waitForTimeout(8000);
console.log(n, "enter=", JSON.stringify(entered), "leave=", JSON.stringify(await st()));
}
await browser.close();

47
tools/web/.probe2.mjs Normal file
View File

@@ -0,0 +1,47 @@
import { createRequire } from "node:module";
import fs from "node:fs";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const snap = () => page.evaluate(() => {
const m = document.querySelector("main");
return {
ws: m?.getAttribute("data-workspace"),
rev: m?.getAttribute("data-current-main-revision"),
dirty: m?.getAttribute("data-dirty"),
area: m?.getAttribute("data-editor-workflow-area"),
stats: document.querySelector('[data-testid="scene-stats"]')?.textContent,
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
saveStatus: m?.getAttribute("data-user-action-save-status"),
openStatus: m?.getAttribute("data-user-action-open-status"),
};
});
console.log("A-label aria:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[aria-label]")].map(n => n.getAttribute("aria-label")))));
console.log("A-workspaces:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="工作区列表"] button')].map(b => (b.textContent||"").trim()))));
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
await page.waitForTimeout(4000);
console.log("B-after-add:", JSON.stringify(await snap()));
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
await page.waitForTimeout(4000);
console.log("B-after-undo:", JSON.stringify(await snap()));
await page.evaluate(() => document.querySelector('[aria-label="重做"]')?.click());
await page.waitForTimeout(4000);
console.log("B-after-redo:", JSON.stringify(await snap()));
const dlP = page.waitForEvent("download", { timeout: 40000 });
await page.evaluate(() => document.querySelector('[aria-label="保存项目"]')?.click());
const dl = await dlP;
const p = await dl.path();
const bytes = p ? fs.readFileSync(p) : Buffer.alloc(0);
console.log("C-download:", dl.suggestedFilename(), bytes.length, "path=", Boolean(p));
console.log("C-after-save:", JSON.stringify(await snap()));
await page.getByTestId("blend-file-input").setInputFiles(p);
for (let i = 0; i < 12; i++) { await page.waitForTimeout(3000); console.log("C-reopen-"+i+":", JSON.stringify(await snap())); }
await browser.close();

27
tools/web/.probe20.mjs Normal file
View File

@@ -0,0 +1,27 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const errs = []; page.on("console", m => { if (m.type()==="error" || /worker|Worker/.test(m.text())) errs.push(m.type()+": "+m.text().slice(0,300)); });
page.on("pageerror", e => errs.push("pageerror: "+String(e).slice(0,300)));
await page.goto("http://127.0.0.1:5404/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const full = () => page.evaluate(() => {
const m = document.querySelector("main");
const attrs = {}; for (const a of m.attributes) if (/worker|recovery|engine|main-revision|diagnostic/.test(a.name)) attrs[a.name] = a.value.slice(0,120);
return { ws: m.getAttribute("data-workspace"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, code: m.getAttribute("data-last-diagnostic-code"), detail: (m.getAttribute("data-last-diagnostic-detail")||"").slice(0,240), attrs };
});
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
const before = errs.length;
await go("Compositing"); await page.waitForTimeout(9000);
console.log("ENTER", JSON.stringify(await full()));
await go("Layout"); await page.waitForTimeout(8000);
console.log("LEAVE", JSON.stringify(await full()));
console.log("ERRS", JSON.stringify(errs.slice(before).slice(0,8), null, 1));
await browser.close();

25
tools/web/.probe21.mjs Normal file
View File

@@ -0,0 +1,25 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5404/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }));
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
await page.evaluate(() => document.querySelector('[data-testid="native-web-tools-toggle"]')?.click());
await page.waitForTimeout(4000);
console.log("WEBMODE", JSON.stringify(await st()));
await go("Shading"); await page.waitForTimeout(9000);
console.log("WEB-Shading", JSON.stringify(await st()));
await page.evaluate(() => document.querySelector('[data-testid="desktop-file-transfer-entry"]')?.click());
await page.waitForTimeout(6000);
console.log("BACK-NATIVE", JSON.stringify(await st()));
await go("Layout"); await page.waitForTimeout(8000);
console.log("NATIVE-Layout", JSON.stringify(await st()));
await browser.close();

25
tools/web/.probe22.mjs Normal file
View File

@@ -0,0 +1,25 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const py = []; page.on("console", m => { const t = m.text(); if (/WBV2_PY_|REENTRANT/.test(t)) py.push(t.slice(0,160)); });
await page.goto("http://127.0.0.1:5407/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200) }));
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
const mark = async (label) => { await page.waitForTimeout(300); py.push("--- " + label + " ---"); };
await go("Modeling"); await page.waitForTimeout(7000); await mark("after-entering-Modeling");
await go("Shading"); await page.waitForTimeout(9000); await mark("after-entering-Shading");
await go("Layout"); await page.waitForTimeout(8000); await mark("after-leaving-Shading");
console.log("STATE", JSON.stringify(await st()));
const enter = py.filter(l => l.startsWith("WBV2_PY_ENTER")).length;
const leave = py.filter(l => l.startsWith("WBV2_PY_LEAVE")).length;
console.log("COUNTS enter=", enter, "leave=", leave);
console.log(JSON.stringify(py.slice(-30), null, 1));
await browser.close();

38
tools/web/.probe23.mjs Normal file
View File

@@ -0,0 +1,38 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5406/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = (tag) => page.evaluate((t) => ({
tag: t,
ws: document.querySelector("main")?.getAttribute("data-workspace"),
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
objects: (document.querySelector('[data-testid="scene-stats"]')?.textContent ?? "").match(/Objects (\d+)/)?.[1] ?? "?",
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,220),
}), tag);
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
const click = (l) => page.evaluate((v) => { const b = document.querySelector(`[aria-label="${v}"]`); b?.click(); return Boolean(b); }, l);
// A: does an EDIT fail inside a node workspace (no detach involved)?
await go("Shading"); await page.waitForTimeout(9000);
console.log(JSON.stringify(await st("shading-entered")));
await click("添加立方体"); await page.waitForTimeout(7000);
console.log(JSON.stringify(await st("edit-inside-shading")));
await click("撤销"); await page.waitForTimeout(7000);
console.log(JSON.stringify(await st("undo-inside-shading")));
// B: does a fresh load still edit fine? (control)
await page.reload({ waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
await click("添加立方体"); await page.waitForTimeout(7000);
console.log(JSON.stringify(await st("control-layout-edit")));
await browser.close();

25
tools/web/.probe24.mjs Normal file
View File

@@ -0,0 +1,25 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5406/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
// Clean baseline: does entering Shading from Layout itself already set the wedge?
console.log("BEFORE", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), count: document.querySelector("main")?.getAttribute("data-diagnostic-count"), code: document.querySelector("main")?.getAttribute("data-last-diagnostic-code") }))));
await go("Shading"); await page.waitForTimeout(9000);
console.log("AFTER-ENTER", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), count: document.querySelector("main")?.getAttribute("data-diagnostic-count"), code: document.querySelector("main")?.getAttribute("data-last-diagnostic-code"), detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
// does the wedge clear if we wait and interact with the viewport first?
const box = await page.evaluate(() => { const c = document.querySelector('canvas[aria-label="Blender desktop"]'); const r = c?.getBoundingClientRect(); return r ? { x: r.x + r.width * 0.5, y: r.y + r.height * 0.5 } : null; });
if (box) await page.mouse.click(box.x, box.y);
await page.waitForTimeout(6000);
console.log("AFTER-CLICK", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), count: document.querySelector("main")?.getAttribute("data-diagnostic-count") }))));
await go("Layout"); await page.waitForTimeout(8000);
console.log("AFTER-LEAVE", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
await browser.close();

22
tools/web/.probe25.mjs Normal file
View File

@@ -0,0 +1,22 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5406/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
const st = (t) => page.evaluate((tag) => ({ tag, ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), count: document.querySelector("main")?.getAttribute("data-diagnostic-count"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200) }), t);
console.log(JSON.stringify(await st("baseline-layout")));
// detach without ever entering Shading
await page.evaluate(() => document.querySelector('[data-testid="native-web-tools-toggle"]')?.click());
await page.waitForTimeout(6000);
console.log(JSON.stringify(await st("after-toggle-web-tools")));
await page.evaluate(() => document.querySelector('[data-testid="desktop-file-transfer-entry"]')?.click());
await page.waitForTimeout(8000);
console.log(JSON.stringify(await st("after-reattach")));
await browser.close();

23
tools/web/.probe26.mjs Normal file
View File

@@ -0,0 +1,23 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const t0 = Date.now(); const log = [];
page.on("console", m => { const t = m.text(); if (/PYTHON_|REENTRANT|sRGB|storage buffer/.test(t)) log.push(((Date.now()-t0)/1000).toFixed(1)+"s "+m.type()+": "+t.slice(0,200)); });
await page.goto("http://127.0.0.1:5407/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
log.push("=== switch to Shading ===");
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Shading"); b?.click(); });
await page.waitForTimeout(12000);
log.push("=== switch to Layout ===");
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Layout"); b?.click(); });
await page.waitForTimeout(9000);
console.log(JSON.stringify(log.slice(0, 24), null, 1));
console.log("STATE", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200) }))));
await browser.close();

34
tools/web/.probe27.mjs Normal file
View File

@@ -0,0 +1,34 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const marks = []; page.on("console", m => { const t = m.text(); if (/WBV2_PY_RECLAIM|REENTRANT/.test(t)) marks.push(m.type()+": "+t.slice(0,220)); });
await page.goto("http://127.0.0.1:5408/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = (t) => page.evaluate((tag) => ({ tag, ws: document.querySelector("main")?.getAttribute("data-workspace"), objects: (document.querySelector('[data-testid="scene-stats"]')?.textContent ?? "").match(/Objects (\d+)/)?.[1] ?? "?", engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,220) }), t);
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
const click = (l) => page.evaluate((v) => { const b = document.querySelector(`[aria-label="${v}"]`); b?.click(); return Boolean(b); }, l);
// A: baseline edit
await click("添加立方体"); await page.waitForTimeout(7000);
console.log(JSON.stringify(await st("baseline-add")));
// B: enter Shading, edit inside
await go("Shading"); await page.waitForTimeout(9000);
console.log(JSON.stringify(await st("shading-entered")));
// C: leave Shading — the old wedge point
await go("Layout"); await page.waitForTimeout(9000);
console.log(JSON.stringify(await st("left-shading")));
// D: edit after leaving
await click("添加立方体"); await page.waitForTimeout(7000);
console.log(JSON.stringify(await st("add-after-leave")));
// E: back into Shading and out again
await go("Shading"); await page.waitForTimeout(8000);
await go("Modeling"); await page.waitForTimeout(8000);
console.log(JSON.stringify(await st("second-round")));
console.log("RECLAIM-LOGS", JSON.stringify(marks.slice(0, 8), null, 1));
await browser.close();

31
tools/web/.probe28.mjs Normal file
View File

@@ -0,0 +1,31 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5408/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
// console python must still execute after the reclaim change
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Scripting"); b?.click(); });
await page.waitForFunction(() => document.querySelector('[data-workspace="Scripting"]') !== null, undefined, { timeout: 40000 }).catch(() => {});
await page.waitForTimeout(6000);
const console_ = await page.evaluate(() => document.querySelector('[aria-label="Python Console"] textarea, .wasm-python-console textarea, textarea[aria-label*="Console"]') !== null);
if (console_) {
const box = await page.locator('[aria-label="Python Console"] textarea, .wasm-python-console textarea, textarea[aria-label*="Console"]').first();
await box.click();
await box.fill("print(1 + 1)");
await page.keyboard.press("Enter");
await page.waitForTimeout(6000);
}
console.log("PYTHON", JSON.stringify(await page.evaluate(() => ({
consoleMounted: Boolean(document.querySelector('[aria-label="Python Console"]')),
echoed: (document.body.innerText.match(/\n2\b/) || [])[0] ?? null,
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160),
}))));
await browser.close();

34
tools/web/.probe29.mjs Normal file
View File

@@ -0,0 +1,34 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Scripting"); b?.click(); });
await page.waitForFunction(() => document.querySelector("main")?.getAttribute("data-workspace") === "Scripting", undefined, { timeout: 60000 });
await page.waitForFunction(() => document.querySelector(".wasm-python-console") !== null, undefined, { timeout: 60000 });
const input = await page.evaluate(() => {
const host = document.querySelector(".wasm-python-console");
const el = host?.querySelector("textarea, input[type=text]");
return el ? { tag: el.tagName, aria: el.getAttribute("aria-label"), placeholder: el.getAttribute("placeholder") } : null;
});
console.log("INPUT", JSON.stringify(input));
if (input) {
const loc = page.locator(".wasm-python-console textarea, .wasm-python-console input[type=text]").first();
await loc.click();
await loc.fill("print(1 + 1)");
await page.keyboard.press("Enter");
await page.waitForTimeout(7000);
}
console.log("RESULT", JSON.stringify(await page.evaluate(() => ({
text: (document.querySelector(".wasm-python-console")?.innerText ?? "").slice(-300),
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160),
}))));
await browser.close();

29
tools/web/.probe3.mjs Normal file
View File

@@ -0,0 +1,29 @@
import { createRequire } from "node:module";
import fs from "node:fs"; import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
console.log("NAVS:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("nav")].map(n => ({ label: n.getAttribute("aria-label"), cls: n.className, buttons: [...n.querySelectorAll("button")].map(b => (b.textContent||"").trim()).slice(0,20) })))));
console.log("AREAS:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[data-editor-workflow-area],[data-area],[data-workflow-area]")].map(n => ({ tag: n.tagName, id: n.getAttribute("data-editor-workflow-area") ?? n.getAttribute("data-area") ?? n.getAttribute("data-workflow-area"), cls: n.className?.toString().slice(0,60) })))));
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
await page.waitForTimeout(3000);
console.log("MENU:", JSON.stringify(await page.evaluate(() => { const m = document.querySelector('[data-testid="scene-stats"]'); return { stats: m?.textContent, undoDisabled: document.querySelector('[aria-label="撤销"]')?.disabled ?? null, redoDisabled: document.querySelector('[aria-label="重做"]')?.disabled ?? null }; })));
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
await page.waitForTimeout(4000);
console.log("AFTERUNDO:", JSON.stringify(await page.evaluate(() => ({ stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
const dlP = page.waitForEvent("download", { timeout: 40000 });
await page.evaluate(() => document.querySelector('[aria-label="保存项目"]')?.click());
const dl = await dlP; const p = await dl.path();
const dir = fs.mkdtempSync("/tmp/wbv2-reopen-"); const named = path.join(dir, "saved.blend");
fs.copyFileSync(p, named);
console.log("SAVED:", dl.suggestedFilename(), fs.statSync(named).size);
await page.getByTestId("blend-file-input").setInputFiles(named);
for (let i = 0; i < 6; i++) { await page.waitForTimeout(3000); console.log("REOPEN"+i+":", JSON.stringify(await page.evaluate(() => ({ stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent })))); }
await browser.close();

18
tools/web/.probe30.mjs Normal file
View File

@@ -0,0 +1,18 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5408/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Scripting"); b?.click(); });
await page.waitForTimeout(12000);
console.log("WS", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,180) }))));
console.log("AREAS", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[aria-label]")].map(n=>n.getAttribute("aria-label")).filter(l=>/Console|Python|Editor|节点/.test(l)).slice(0,15))));
console.log("CLASSES", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[class*=console],[class*=python]")].map(n=>n.className?.toString().slice(0,60)).slice(0,10))));
await browser.close();

27
tools/web/.probe31.mjs Normal file
View File

@@ -0,0 +1,27 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = (t) => page.evaluate((tag) => ({ tag, ws: document.querySelector("main")?.getAttribute("data-workspace"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,220) }), t);
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); return Boolean(b); }, n);
// UV Editing reached directly from Layout (no Shading involved)
console.log("uv-from-layout-click", await go("UV Editing"));
await page.waitForTimeout(10000);
console.log(JSON.stringify(await st("uv-from-layout")));
// Shading -> UV Editing
await go("Shading"); await page.waitForTimeout(8000);
console.log(JSON.stringify(await st("shading")));
console.log("uv-from-shading-click", await go("UV Editing"));
await page.waitForTimeout(10000);
console.log(JSON.stringify(await st("uv-from-shading")));
// what does the workspace tab list contain for UV Editing?
console.log("TABS", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="工作区"] button')].map(b => ({ n: (b.textContent||"").trim(), disabled: b.disabled, cls: b.className.slice(0,40) })))));
await browser.close();

24
tools/web/.probe32.mjs Normal file
View File

@@ -0,0 +1,24 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const goBack = () => page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Layout"); b?.click(); });
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
const st = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent }));
const results = [];
for (const name of ["Animation","Compositing","Geometry Nodes","Rendering","Sculpting","Texture Paint","UV Editing","Scripting"]) {
await goBack(); await page.waitForTimeout(6000);
await go(name); await page.waitForTimeout(11000);
const s = await st();
results.push({ name, ws: s.ws, engine: (s.engine||"").slice(0,90) });
}
console.log(JSON.stringify(results, null, 1));
await browser.close();

21
tools/web/.probe33.mjs Normal file
View File

@@ -0,0 +1,21 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5408/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
const st = (t) => page.evaluate((tag) => ({ tag, ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,180) }), t);
// Geometry Nodes alone from Layout
await go("Geometry Nodes"); await page.waitForTimeout(12000);
console.log(JSON.stringify(await st("geometry-nodes-alone")));
// then try leaving
await go("Layout"); await page.waitForTimeout(9000);
console.log(JSON.stringify(await st("leave-geometry-nodes")));
await browser.close();

19
tools/web/.probe34.mjs Normal file
View File

@@ -0,0 +1,19 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const log = []; page.on("console", m => { if (m.type()==="error" || /Traceback|Error|error/.test(m.text())) log.push(m.text().slice(0,600)); });
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
log.length = 0;
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="UV Editing"); b?.click(); });
await page.waitForTimeout(14000);
console.log("DETAIL", JSON.stringify(await page.evaluate(() => (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,260))));
console.log("LOG", JSON.stringify(log.slice(0, 14), null, 1));
await browser.close();

22
tools/web/.probe35.mjs Normal file
View File

@@ -0,0 +1,22 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Scripting"); b?.click(); });
await page.waitForTimeout(15000);
console.log(JSON.stringify(await page.evaluate(() => ({
ws: document.querySelector("main")?.getAttribute("data-workspace"),
area: document.querySelector("main")?.getAttribute("data-editor-workflow-area"),
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200),
textareas: [...document.querySelectorAll("textarea")].map(t => ({ aria: t.getAttribute("aria-label"), cls: t.className.slice(0,50), placeholder: t.getAttribute("placeholder") })),
consoleish: [...document.querySelectorAll("[class*=console],[data-testid*=console],[aria-label*=Console],[aria-label*=控制台]")].map(n => ({ tag: n.tagName, aria: n.getAttribute("aria-label"), cls: (n.className||"").toString().slice(0,60) })),
})), null, 1));
await browser.close();

35
tools/web/.probe36.mjs Normal file
View File

@@ -0,0 +1,35 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
// Switch to Web business tools so the DOM console mounts, then Scripting.
await page.evaluate(() => document.querySelector('[data-testid="native-web-tools-toggle"]')?.click());
await page.waitForTimeout(3000);
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Scripting"); b?.click(); });
await page.waitForTimeout(6000);
const input = await page.evaluate(() => {
const el = document.querySelector('.wasm-python-console textarea, .wasm-python-console input, [aria-label="Python Console command"]');
return el ? { tag: el.tagName, aria: el.getAttribute('aria-label'), readOnly: el.getAttribute('readonly') !== null } : null;
});
console.log("INPUT", JSON.stringify(input));
if (input && !input.readOnly) {
const loc = page.locator('.wasm-python-console textarea, .wasm-python-console input, [aria-label="Python Console command"]').first();
await loc.click();
await loc.fill("print(1 + 1)");
await page.keyboard.press("Enter");
await page.waitForTimeout(7000);
}
console.log("RESULT", JSON.stringify(await page.evaluate(() => ({
consoleText: (document.querySelector('.wasm-python-console')?.innerText ?? '').slice(-240),
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160),
}))));
await browser.close();

19
tools/web/.probe37.mjs Normal file
View File

@@ -0,0 +1,19 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const log = []; page.on("console", m => { const t=m.text(); if (/shader|cannot convert|non-constant|unsupported/.test(t)) log.push(t.slice(0,400)); });
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
// Force compilation of material and world shaders the same way the receipt does.
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Rendering"); b?.click(); });
await page.waitForTimeout(15000);
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Shading"); b?.click(); });
await page.waitForTimeout(15000);
console.log(JSON.stringify(log.slice(0, 20), null, 1));
await browser.close();

29
tools/web/.probe4.mjs Normal file
View File

@@ -0,0 +1,29 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
const page = await ctx.newPage();
const logs = []; page.on("console", m => logs.push(`${m.type()}: ${m.text()}`.slice(0, 400)));
page.on("pageerror", e => logs.push("pageerror: " + String(e).slice(0, 300)));
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = () => page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, diag: document.querySelector("main")?.getAttribute("data-diagnostic-count"), rev: document.querySelector("main")?.getAttribute("data-current-main-revision"), stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, workspace: document.querySelector("main")?.getAttribute("data-workspace"), area: document.querySelector("main")?.getAttribute("data-editor-workflow-area") }));
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
await page.waitForTimeout(3000);
console.log("ADD:", JSON.stringify(await st()));
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
await page.waitForTimeout(5000);
console.log("UNDO:", JSON.stringify(await st()));
console.log("BANNER:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[role=alert],[data-testid*=error],[class*=error],[class*=diagnostic]")].map(n => (n.textContent||"").trim().slice(0,200)).filter(Boolean).slice(0,8))));
console.log("LOGS:", JSON.stringify(logs.slice(-14), null, 1));
for (const name of ["Modeling", "Scripting", "Layout"]) {
await page.evaluate((v) => { const b = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find(n => (n.textContent||"").trim() === v); b?.click(); }, name);
await page.waitForTimeout(4000);
console.log("WS-"+name+":", JSON.stringify(await st()), "areas:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[data-editor-workflow-area]")].map(n => n.getAttribute("data-editor-workflow-area")))));
}
await browser.close();

15
tools/web/.probe40.mjs Normal file
View File

@@ -0,0 +1,15 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
console.log("TREE", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => ({ name: n.getAttribute('aria-label') ?? (n.textContent||'').trim().slice(0,30), level: n.getAttribute('aria-level'), parent: n.getAttribute('data-parent-id'), expanded: n.getAttribute('aria-expanded') })).slice(0, 20))));
console.log("STATS", JSON.stringify(await page.evaluate(() => ({ stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
await browser.close();

34
tools/web/.probe41.mjs Normal file
View File

@@ -0,0 +1,34 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
// A: is the Decimate panel present and enabled?
const panel = await page.evaluate(() => {
const h = [...document.querySelectorAll('.property-section h3')].find(n => (n.textContent||'').trim() === 'Decimate');
const section = h?.closest('.property-section');
if (!section) return { found: false };
const btn = (label) => { const b = section.querySelector(`[aria-label="${label}"]`); return b ? { disabled: b.disabled } : null; };
return { found: true, summary: section.querySelector('.modifier-summary span')?.textContent?.trim(), preview: btn('预览 Decimate'), apply: btn('应用 Decimate') };
});
console.log("PANEL", JSON.stringify(panel));
// B: preview and read counts
if (panel.found && panel.preview && !panel.preview.disabled) {
await page.getByRole('button', { name: '预览 Decimate', exact: true }).click();
try {
await page.waitForFunction(() => /三角形/.test(document.querySelector('[data-testid="simplify-triangle-counts"]')?.textContent ?? ''), undefined, { timeout: 90000 });
} catch (e) { console.log("PREVIEW-TIMEOUT"); }
console.log("COUNTS", JSON.stringify(await page.evaluate(() => ({
counts: document.querySelector('[data-testid="simplify-triangle-counts"]')?.textContent,
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160),
}))));
}
await browser.close();

32
tools/web/.probe42.mjs Normal file
View File

@@ -0,0 +1,32 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
const tree = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => ({ name: n.getAttribute('aria-label') ?? (n.textContent||'').trim().slice(0,26), level: n.getAttribute('aria-level'), parent: n.getAttribute('data-parent-id') })));
console.log("BEFORE", JSON.stringify(await tree()));
// select Payload, then ctrl-select NewParent via the tree
const clickTree = async (name, mods = []) => {
const el = page.getByRole('treeitem', { name, exact: true }).first().locator('.tree-node-name').first();
await el.click({ modifiers: mods });
await page.waitForTimeout(2500);
};
await clickTree('Payload');
await clickTree('NewParent', ['Control']);
console.log("SELECTED", JSON.stringify(await page.evaluate(() => document.querySelector('main')?.getAttribute('data-selected-object-ids'))));
await page.getByRole('button', { name: '物体', exact: true }).click();
await page.waitForTimeout(800);
await page.getByRole('menuitem', { name: '父级', exact: true }).click();
await page.waitForTimeout(800);
await page.getByRole('menuitem', { name: '设定父级', exact: true }).click();
await page.waitForTimeout(6000);
console.log("AFTER", JSON.stringify(await tree()));
console.log("ENGINE", JSON.stringify(await page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
await browser.close();

17
tools/web/.probe43.mjs Normal file
View File

@@ -0,0 +1,17 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
console.log(JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => ({
aria: n.getAttribute('aria-label'), text: (n.textContent||'').trim().slice(0,28), level: n.getAttribute('aria-level'),
nodeId: n.getAttribute('data-node-id'), parent: n.getAttribute('data-parent-id'), dataId: n.getAttribute('data-data-id')
}))), null, 1));
await browser.close();

16
tools/web/.probe44.mjs Normal file
View File

@@ -0,0 +1,16 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
const names = await page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => n.getAttribute('aria-label')));
console.log("COUNT", names.length, JSON.stringify(names));
console.log("HAS-PAYLOAD", names.includes("Payload"));
await browser.close();

26
tools/web/.probe45.mjs Normal file
View File

@@ -0,0 +1,26 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
// Read the authoritative snapshot directly from the worker.
const snap = await page.evaluate(() => new Promise((resolve, reject) => {
const worker = window.__nativeWorkflowEngine;
if (!worker) return reject(new Error("no engine"));
const requestId = "probe-" + Math.random();
const timer = setTimeout(() => reject(new Error("timeout")), 30000);
const receive = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); worker.removeEventListener("message", receive); resolve(data); } };
worker.addEventListener("message", receive);
worker.postMessage({ requestId, command: { type: "snapshot" } });
}));
const s = snap.result?.snapshot;
console.log("NODES", JSON.stringify((s?.nodes ?? []).map(n => ({ name: n.name, type: n.type, parent: n.parentId }))));
console.log("COLLECTIONS", JSON.stringify((s?.collections ?? []).map(c => ({ name: c.name, id: c.id }))));
await browser.close();

20
tools/web/.probe46.mjs Normal file
View File

@@ -0,0 +1,20 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
const list = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => (n.getAttribute('aria-label') ?? '').trim()).filter(Boolean));
console.log("BEFORE", JSON.stringify(await list()));
for (const name of ['Mount', 'RootRig', 'Parts']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(3000); }
}
console.log("AFTER-EXPAND", JSON.stringify(await list()));
await browser.close();

36
tools/web/.probe47.mjs Normal file
View File

@@ -0,0 +1,36 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
// expand parents so the drop target is visible
for (const name of ['Parts','Mount','RootRig']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2500); }
}
const rows = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => ({ name: (n.getAttribute('aria-label')||'').trim(), level: n.getAttribute('aria-level'), node: n.getAttribute('data-node-id'), parent: n.getAttribute('data-parent-id'), draggable: n.getAttribute('draggable') })).filter(r => r.name));
console.log("BEFORE", JSON.stringify(await rows()));
const draggable = await page.evaluate(() => document.querySelectorAll('[role="treeitem"][draggable="true"]').length);
console.log("DRAGGABLE-ROWS", draggable);
// HTML5 drag: Mount -> NewParent
const res = await page.evaluate(() => {
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
const src = find('Mount'), dst = find('NewParent');
if (!src || !dst) return { error: 'missing node', src: Boolean(src), dst: Boolean(dst) };
const dt = new DataTransfer();
const fire = (el, type) => el.dispatchEvent(new DragEvent(type, { bubbles: true, cancelable: true, dataTransfer: dt }));
fire(src, 'dragstart'); fire(dst, 'dragover'); fire(dst, 'drop'); fire(src, 'dragend');
return { payload: dt.getData('application/x-web-blender-objects') };
});
console.log("DRAG", JSON.stringify(res));
await page.waitForTimeout(7000);
console.log("AFTER", JSON.stringify(await rows()));
console.log("ENGINE", JSON.stringify(await page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200) }))));
await browser.close();

57
tools/web/.probe48.mjs Normal file
View File

@@ -0,0 +1,57 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
for (const name of ['Parts','Mount','RootRig']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2500); }
}
const state = () => page.evaluate(() => ({
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200),
tree: [...document.querySelectorAll('[role="treeitem"]')].map(n => `${n.getAttribute('aria-level')}:${(n.getAttribute('aria-label')||'').trim()}`).filter(x => !x.endsWith(':')),
}));
console.log("A-start", JSON.stringify(await state()));
// drag Mount -> NewParent
const drag = await page.evaluate(() => {
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
const src = find('Mount'), dst = find('NewParent');
if (!src || !dst) return { error: 'missing' };
const dt = new DataTransfer();
const fire = (el, type) => el.dispatchEvent(new DragEvent(type, { bubbles: true, cancelable: true, dataTransfer: dt }));
fire(src, 'dragstart'); fire(dst, 'dragover');
const dropHandled = !dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
fire(src, 'dragend');
return { payload: dt.getData('application/x-web-blender-objects'), dropHandled };
});
console.log("drag", JSON.stringify(drag));
await page.waitForTimeout(8000);
console.log("B-after-drag", JSON.stringify(await state()));
// control: menu-based reparent of Payload -> NewParent
await page.evaluate(() => {
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
find('Payload')?.querySelector('.tree-node-name')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
await page.waitForTimeout(2500);
await page.evaluate(() => {
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
find('NewParent')?.querySelector('.tree-node-name')?.dispatchEvent(new MouseEvent('click', { bubbles: true, ctrlKey: true }));
});
await page.waitForTimeout(2500);
console.log("C-selected", JSON.stringify(await page.evaluate(() => document.querySelector('main')?.getAttribute('data-selected-object-ids'))));
await page.getByRole('button', { name: '物体', exact: true }).click();
await page.waitForTimeout(800);
await page.getByRole('menuitem', { name: '父级', exact: true }).click();
await page.waitForTimeout(800);
await page.getByRole('menuitem', { name: '设定父级', exact: true }).click();
await page.waitForTimeout(8000);
console.log("D-after-menu", JSON.stringify(await state()));
await browser.close();

51
tools/web/.probe49.mjs Normal file
View File

@@ -0,0 +1,51 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
for (const name of ['Parts','Mount','RootRig']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
}
const tree = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => `${n.getAttribute('aria-level')}:${(n.getAttribute('aria-label')||'').trim()}`).filter(x => !x.endsWith(':')).join(" | "));
const worldOf = async (name) => {
await page.evaluate((n) => {
const el = [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
el?.querySelector('.tree-node-name')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}, name);
await page.waitForTimeout(2500);
return page.evaluate(() => {
const read = (label) => { const el = document.querySelector(`input[aria-label="${label}"]`); return el ? el.value : null; };
return { x: read('位置 X'), y: read('位置 Y'), z: read('位置 Z'), loc: [...document.querySelectorAll('.property-section')].map(s => s.textContent||'').find(t => /Location|位置/.test(t))?.slice(0, 120) };
});
};
console.log("TREE-BEFORE", await tree());
console.log("MOUNT-BEFORE", JSON.stringify(await worldOf('Mount')));
// drag Mount -> NewParent
const drag = await page.evaluate(() => {
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
const src = find('Mount'), dst = find('NewParent');
if (!src || !dst) return { error: 'missing' };
const dt = new DataTransfer();
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
const handled = !dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
src.dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: dt }));
return { handled, payload: dt.getData('application/x-web-blender-objects') };
});
console.log("DRAG", JSON.stringify(drag));
await page.waitForTimeout(8000);
// expand NewParent to reveal the child
const expand = page.getByRole('button', { name: '展开 NewParent', exact: true }).first();
if (await expand.count()) { await expand.click(); await page.waitForTimeout(3500); }
console.log("TREE-AFTER", await tree());
console.log("MOUNT-AFTER", JSON.stringify(await worldOf('Mount')));
console.log("ENGINE", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent, d: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
await browser.close();

27
tools/web/.probe5.mjs Normal file
View File

@@ -0,0 +1,27 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const full = () => page.evaluate(() => {
const m = document.querySelector("main");
return { engine: document.querySelector('[data-testid="engine-status"]')?.textContent, rev: m?.getAttribute("data-current-main-revision"), diag: m?.getAttribute("data-diagnostic-count"), text: (document.body.innerText || "").split("\n").filter(l => /diagnos|error|failed|失败|诊断/i.test(l)).slice(0, 10) };
});
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
await page.waitForTimeout(3000);
console.log("ADD", JSON.stringify(await full()));
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
await page.waitForTimeout(5000);
console.log("UNDO", JSON.stringify(await full()));
// try Edit > Undo menu instead
await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="主菜单"] button')].find(b => (b.textContent||"").trim() === "编辑")?.click());
await page.waitForTimeout(1500);
console.log("EDITMENU", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('[role="menu"] button, .menu-popup button, [data-menu-item]')].map(b => (b.textContent||"").trim()).filter(Boolean).slice(0, 25))));
await browser.close();

67
tools/web/.probe50.mjs Normal file
View File

@@ -0,0 +1,67 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.addInitScript(() => {
const registry = [];
window.__combinedWorkers = registry;
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); registry.push({ url: String(a[0]), worker: w }); return w; } });
});
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
await page.evaluate(() => {
const w = window.__combinedWorkers.map(x => x.worker).find(x => true);
window.__engine = window.__combinedWorkers.find(x => /web-engine\.worker/.test(x.url))?.worker;
});
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
const worker = window.__engine;
if (!worker) return reject(new Error('no engine worker'));
const requestId = 'p-' + Math.random();
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
const receive = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); worker.removeEventListener('message', receive); resolve(data); } };
worker.addEventListener('message', receive);
worker.postMessage({ requestId, command: { type: 'snapshot' } });
}));
const read = async (label) => {
const reply = await snap();
const s = reply.result?.snapshot;
const node = (s?.nodes ?? []).find(n => n.name === 'Mount');
const parent = node?.parentId ? (s.nodes.find(n => n.id === node.parentId)?.name) : null;
return { label, rev: s?.revision, mountParent: parent, local: node?.localMatrix?.map(v => Number(v.toFixed(4))), world: node?.worldMatrix?.map(v => Number(v.toFixed(4))) };
};
console.log("BEFORE", JSON.stringify(await read("before")));
for (const name of ['Parts','Mount','RootRig']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
}
await page.evaluate(() => {
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
const src = find('Mount'), dst = find('NewParent');
const dt = new DataTransfer();
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
src.dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: dt }));
});
await page.waitForTimeout(9000);
console.log("AFTER", JSON.stringify(await read("after")));
// collection drop should be rejected
const before = await snap();
await page.evaluate(() => {
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
const src = find('Payload'), dst = find('Archive');
const dt = new DataTransfer();
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
});
await page.waitForTimeout(7000);
const after = await snap();
console.log("COLLECTION-DROP-REV", JSON.stringify({ before: before.result?.snapshot?.revision, after: after.result?.snapshot?.revision }));
await browser.close();

44
tools/web/.probe51.mjs Normal file
View File

@@ -0,0 +1,44 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
// native desktop mode: no ?ui=web
await page.goto("http://127.0.0.1:5412/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(4000);
const tree = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => `${n.getAttribute('aria-level')}:${(n.getAttribute('aria-label')||'').trim()}`).filter(x => !x.endsWith(':')).join(" | "));
const draggableCount = await page.evaluate(() => document.querySelectorAll('[role="treeitem"][draggable="true"]').length);
console.log("NATIVE-DRAGGABLE", draggableCount);
console.log("NATIVE-TREE", await tree());
for (const name of ['Parts','Mount','RootRig']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2500); }
}
const drag = await page.evaluate(() => {
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
const src = find('Mount'), dst = find('NewParent');
if (!src || !dst) return { error: 'missing' };
const dt = new DataTransfer();
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
const handled = !dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
src.dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: dt }));
return { handled, payload: dt.getData('application/x-web-blender-objects') };
});
console.log("NATIVE-DRAG", JSON.stringify(drag));
await page.waitForTimeout(9000);
const expand = page.getByRole('button', { name: '展开 NewParent', exact: true }).first();
if (await expand.count()) { await expand.click(); await page.waitForTimeout(3500); }
console.log("NATIVE-TREE-AFTER", await tree());
console.log("NATIVE-ENGINE", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent, d: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
// undo
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
await page.waitForTimeout(9000);
console.log("NATIVE-AFTER-UNDO", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
console.log("NATIVE-TREE-UNDO", await tree());
await browser.close();

21
tools/web/.probe52.mjs Normal file
View File

@@ -0,0 +1,21 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5412/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(5000);
console.log(JSON.stringify(await page.evaluate(() => ({
treeItems: document.querySelectorAll('[role="treeitem"]').length,
outlinerTrees: document.querySelectorAll('[role="tree"]').length,
nativeOverlay: Boolean(document.querySelector('section[aria-label="Blender 桌面"]')),
overlayVisible: (() => { const n = document.querySelector('section[aria-label="Blender 桌面"]'); if (!n) return null; const s = getComputedStyle(n); return { visibility: s.visibility, pointerEvents: s.pointerEvents, zIndex: s.zIndex }; })(),
canvas: (() => { const c = document.querySelector('canvas[aria-label="Blender desktop"]'); if (!c) return null; const r = c.getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height, display: c.getAttribute('data-native-display') }; })(),
areas: (() => { const w = document.querySelector('main')?.getAttribute('data-editor-workflow-area'); return w; })(),
}))));
await browser.close();

35
tools/web/.probe53.mjs Normal file
View File

@@ -0,0 +1,35 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
for (const name of ['Parts','Mount','RootRig']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
}
const tree = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => `${n.getAttribute('aria-level')}:${(n.getAttribute('aria-label')||'').trim()}`).filter(x => !x.endsWith(':')).join(" | "));
await page.evaluate(() => {
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
const src = find('Mount'), dst = find('NewParent'); const dt = new DataTransfer();
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
});
await page.waitForTimeout(9000);
console.log("AFTER-DRAG", await tree());
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
await page.waitForTimeout(9000);
console.log("AFTER-UNDO-1", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
console.log("TREE-UNDO-1", await tree());
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
await page.waitForTimeout(9000);
console.log("AFTER-UNDO-2", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
console.log("TREE-UNDO-2", await tree());
await browser.close();

23
tools/web/.probe54.mjs Normal file
View File

@@ -0,0 +1,23 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5412/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(5000);
// The native workspace's area layout lives in the snapshot's editor workflow.
const layout = await page.evaluate(() => {
const text = document.querySelector('[data-testid="editor-workflow-context"]')?.textContent;
return text ?? null;
});
console.log("CTX", layout);
console.log("AREAS", JSON.stringify(await page.evaluate(() => {
const nodes = [...document.querySelectorAll('[data-workspace-id]')];
return nodes.map(n => n.textContent).slice(0, 16);
})));
await browser.close();

31
tools/web/.probe55.mjs Normal file
View File

@@ -0,0 +1,31 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.addInitScript(() => {
const reg = []; window.__w = reg;
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
});
await page.goto("http://127.0.0.1:5412/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(4000);
const snap = await page.evaluate(() => new Promise((resolve, reject) => {
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
if (!worker) return reject(new Error('no engine'));
const requestId = 'p-' + Math.random();
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); window.removeEventListener('message', recv); resolve(data); } };
worker.addEventListener('message', recv);
worker.postMessage({ requestId, command: { type: 'snapshot' } });
}));
const wf = snap.result?.snapshot?.editorWorkflow;
console.log("STATUS", wf?.status ?? "none");
for (const ws of (wf?.workspaces ?? []).slice(0, 3)) {
console.log("WS", ws.name, JSON.stringify(ws.areas.map(a => ({ e: a.editor, r: a.rect, active: a.id === ws.activeAreaId }))));
}
await browser.close();

42
tools/web/.probe56.mjs Normal file
View File

@@ -0,0 +1,42 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
for (const name of ['Parts','Mount','RootRig']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
}
// select Mount and KeyLight, then drag both onto NewParent
await page.evaluate(() => {
const find = (n) => [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
find('Mount')?.querySelector('.tree-node-name')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
await page.waitForTimeout(2000);
await page.evaluate(() => {
const find = (n) => [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
find('KeyLight')?.querySelector('.tree-node-name')?.dispatchEvent(new MouseEvent('click', { bubbles: true, ctrlKey: true }));
});
await page.waitForTimeout(2000);
console.log("SELECTED", await page.evaluate(() => document.querySelector('main')?.getAttribute('data-selected-object-ids')));
const drag = await page.evaluate(() => {
const find = (n) => [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
const src = [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === 'Mount');
const dst = find('NewParent'); const dt = new DataTransfer();
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
return dt.getData('application/x-web-blender-objects');
});
console.log("PAYLOAD", drag);
await page.waitForTimeout(9000);
console.log("TREE", await page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => `${n.getAttribute('aria-level')}:${(n.getAttribute('aria-label')||'').trim()}`).filter(x => !x.endsWith(':')).join(" | ")));
console.log("ENGINE", await page.evaluate(() => document.querySelector('[data-testid="engine-status"]')?.textContent));
await browser.close();

52
tools/web/.probe57.mjs Normal file
View File

@@ -0,0 +1,52 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.addInitScript(() => {
const reg = []; window.__w = reg;
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
});
await page.goto("http://127.0.0.1:5412/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(5000);
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
if (!worker) return reject(new Error('no engine'));
const requestId = 'p-' + Math.random();
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
worker.addEventListener('message', recv);
worker.postMessage({ requestId, command: { type: 'snapshot' } });
}));
const state = async () => {
const r = await snap();
const s = r.result?.snapshot;
const byName = Object.fromEntries((s?.nodes ?? []).map(n => [n.name, n.parentId ? (s.nodes.find(x => x.id === n.parentId)?.name) : null]));
return { rev: s?.revision, parents: byName };
};
console.log("BEFORE", JSON.stringify(await state()));
// Real mouse drag inside the native outliner area (normalized rect from the workflow).
const rect = { x: 0.8234986945169713, y: 0.8174757281553398, w: 0.17650130548302867, h: 0.18252427184466025 };
const vw = 1440, vh = 900;
const nx = (fx) => vw * (rect.x + rect.w * fx);
const ny = (fy) => vh * (rect.y + rect.h * fy);
console.log("AREA-PX", JSON.stringify({ x0: nx(0), y0: ny(0), x1: nx(1), y1: ny(1) }));
// Activate the outliner by clicking an empty part of it first.
await page.mouse.click(nx(0.5), ny(0.9));
await page.waitForTimeout(2500);
await page.mouse.move(nx(0.5), ny(0.25));
await page.waitForTimeout(800);
await page.mouse.down();
await page.waitForTimeout(600);
await page.mouse.move(nx(0.5), ny(0.4), { steps: 12 });
await page.waitForTimeout(600);
await page.mouse.up();
await page.waitForTimeout(6000);
console.log("AFTER-NATIVE-DRAG", JSON.stringify(await state()));
console.log("UI", JSON.stringify(await page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,180), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display") }))));
await browser.close();

35
tools/web/.probe58.mjs Normal file
View File

@@ -0,0 +1,35 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.addInitScript(() => {
const reg = []; window.__w = reg;
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
});
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3500);
const snap = await page.evaluate(() => new Promise((resolve, reject) => {
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
if (!worker) return reject(new Error('no engine'));
const requestId = 'p-' + Math.random();
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
worker.addEventListener('message', recv);
worker.postMessage({ requestId, command: { type: 'snapshot' } });
}));
const s = snap.result?.snapshot;
const wf = s?.editorWorkflow;
const ws = wf?.workspaces?.find(w => w.id === wf.context.workspaceId);
for (const area of (ws?.areas ?? [])) {
if (area.space?.outliner) {
console.log("OUTLINER-ROWS", JSON.stringify(area.space.outliner.rows.map(r => ({ label: r.label, depth: r.depth, objectId: r.objectId, remapId: r.remapId })), null, 1));
}
}
console.log("COLLECTIONS", JSON.stringify((s?.collections ?? []).map(c => ({ id: c.id, name: c.name, master: c.isMaster, children: c.childCollectionIds, objects: c.objectIds }))));
await browser.close();

56
tools/web/.probe59.mjs Normal file
View File

@@ -0,0 +1,56 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("/home/mes123456/workinf_Blender_Wasm/web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.addInitScript(() => {
const reg = []; window.__w = reg;
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
});
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3500);
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
if (!worker) return reject(new Error('no engine'));
const requestId = 'p-' + Math.random();
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
worker.addEventListener('message', recv);
worker.postMessage({ requestId, command: { type: 'snapshot' } });
}));
const state = async () => {
const r = await snap(); const s = r.result?.snapshot;
const coll = Object.fromEntries((s?.collections ?? []).map(c => [c.name, c.objectIds.filter(Boolean).map(id => id.replace('object:', ''))]));
const parents = Object.fromEntries((s?.nodes ?? []).map(n => [n.name, n.parentId ? s.nodes.find(x => x.id === n.parentId)?.name : null]));
return { rev: s?.revision, coll, parents };
};
console.log("BEFORE", JSON.stringify(await state()));
for (const name of ['Parts','Mount','RootRig']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
}
const drag = async (src, dst, mods = {}) => page.evaluate(({ srcName, dstName, ctrl }) => {
const find = (n) => [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
const s = find(srcName), d = find(dstName);
if (!s || !d) return { error: 'missing', s: Boolean(s), d: Boolean(d) };
const dt = new DataTransfer();
s.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
d.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt, ctrlKey: ctrl }));
const handled = !d.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt, ctrlKey: ctrl }));
return { handled };
}, { srcName: src, dstName: dst, ctrl: Boolean(mods.ctrl) });
// A: object -> collection row (Archive)
console.log("DRAG-TO-COLLECTION", JSON.stringify(await drag('Mount', 'Archive')));
await page.waitForTimeout(8000);
console.log("AFTER-COLLECTION", JSON.stringify(await state()));
// B: drop onto itself must do nothing
console.log("DRAG-SELF", JSON.stringify(await drag('Payload', 'Payload')));
await page.waitForTimeout(5000);
console.log("AFTER-SELF", JSON.stringify(await state()));
console.log("ENGINE", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent, d: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
await browser.close();

37
tools/web/.probe6.mjs Normal file
View File

@@ -0,0 +1,37 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const d = () => page.evaluate(() => {
const m = document.querySelector("main");
const keys = [...m.attributes].map(a => a.name).filter(k => /diagnostic/.test(k));
return { engine: document.querySelector('[data-testid="engine-status"]')?.textContent, rev: m.getAttribute("data-current-main-revision"), code: m.getAttribute("data-last-diagnostic-code"), detail: (m.getAttribute("data-last-diagnostic-detail")||"").slice(0,600), keys };
});
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
await page.waitForTimeout(3000);
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
await page.waitForTimeout(5000);
console.log("TOOLBAR-UNDO", JSON.stringify(await d(), null, 1));
// reload, then use Edit > Undo menu path
await page.reload({ waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
await page.waitForTimeout(3000);
console.log("BEFORE-MENU", JSON.stringify(await d()));
await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="主菜单"] button')].find(b => (b.textContent||"").trim() === "编辑")?.click());
await page.waitForTimeout(1200);
await page.evaluate(() => { const it = [...document.querySelectorAll("button")].find(b => (b.textContent||"").trim() === "撤销Ctrl Z"); it?.click(); });
await page.waitForTimeout(5000);
console.log("MENU-UNDO", JSON.stringify(await d(), null, 1));
await browser.close();

49
tools/web/.probe60.mjs Normal file
View File

@@ -0,0 +1,49 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)("/home/mes123456/workinf_Blender_Wasm/web/node_modules/playwright");
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.addInitScript(() => {
const reg = []; window.__w = reg;
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
});
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("/home/mes123456/workinf_Blender_Wasm/tests/files/web/hierarchy_simplify_combined.blend"));
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3500);
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
const requestId = 'p-' + Math.random();
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
worker.addEventListener('message', recv);
worker.postMessage({ requestId, command: { type: 'snapshot' } });
}));
const parents = async () => {
const r = await snap(); const s = r.result?.snapshot;
return { rev: s?.revision, p: Object.fromEntries((s?.nodes ?? []).map(n => [n.name, n.parentId ? s.nodes.find(x => x.id === n.parentId)?.name : null])) };
};
for (const name of ['Parts','Mount','RootRig']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2000); }
}
const drag = (src, dst, shift) => page.evaluate(({ s: srcName, d: dstName, shiftKey }) => {
const find = (n) => [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
const s = find(srcName), d = find(dstName);
const dt = new DataTransfer();
s.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
d.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey }));
const handled = !d.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey }));
return handled;
}, { s: src, d: dst, shiftKey: shift });
console.log("BEFORE", JSON.stringify(await parents()));
console.log("PLAIN-DROP", await drag('Mount', 'NewParent', false));
await page.waitForTimeout(6000);
console.log("AFTER-PLAIN", JSON.stringify(await parents()));
console.log("SHIFT-DROP", await drag('Mount', 'NewParent', true));
await page.waitForTimeout(8000);
console.log("AFTER-SHIFT", JSON.stringify(await parents()));
await browser.close();

52
tools/web/.probe61.mjs Normal file
View File

@@ -0,0 +1,52 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)("/home/mes123456/workinf_Blender_Wasm/web/node_modules/playwright");
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.addInitScript(() => {
const reg = []; window.__w = reg;
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
});
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles("/home/mes123456/workinf_Blender_Wasm/tests/files/web/hierarchy_simplify_combined.blend");
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3500);
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
const requestId = 'p-' + Math.random();
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
worker.addEventListener('message', recv);
worker.postMessage({ requestId, command: { type: 'snapshot' } });
}));
const parents = async () => {
const r = await snap(); const s = r.result?.snapshot;
return { rev: s?.revision, p: Object.fromEntries((s?.nodes ?? []).map(n => [n.name, n.parentId ? s.nodes.find(x => x.id === n.parentId)?.name : null])) };
};
for (const name of ['Parts','Mount','RootRig']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
}
console.log("BEFORE", JSON.stringify(await parents()));
// Drop Payload onto empty container space with and without Shift.
const drop = (name, shift) => page.evaluate(({ n, shiftKey }) => {
const row = [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
const tree = document.querySelector('.outliner-tree');
if (!row || !tree) return { error: 'missing', rows: [...document.querySelectorAll('[role=\"treeitem\"]')].map(x => (x.getAttribute('aria-label')||'').trim()).filter(Boolean) };
const dt = new DataTransfer();
row.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
// target the container itself, not a row: dispatch directly on it
tree.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey }));
const handled = !tree.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey }));
return { handled, payload: dt.getData('application/x-web-blender-objects') };
}, { n: name, shiftKey: shift });
console.log("DROP-NO-SHIFT", JSON.stringify(await drop('Payload', false)));
await page.waitForTimeout(5000);
console.log("AFTER-NO-SHIFT", JSON.stringify(await parents()));
console.log("DROP-SHIFT", JSON.stringify(await drop('Payload', true)));
await page.waitForTimeout(8000);
console.log("AFTER-SHIFT", JSON.stringify(await parents()));
await browser.close();

45
tools/web/.probe62.mjs Normal file
View File

@@ -0,0 +1,45 @@
import { createRequire } from "node:module";
const { chromium } = createRequire(import.meta.url)("/home/mes123456/workinf_Blender_Wasm/web/node_modules/playwright");
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.addInitScript(() => {
const reg = []; window.__w = reg;
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
});
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles("/home/mes123456/workinf_Blender_Wasm/tests/files/web/hierarchy_simplify_combined.blend");
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3500);
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
const requestId = 'p-' + Math.random();
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
worker.addEventListener('message', recv);
worker.postMessage({ requestId, command: { type: 'snapshot' } });
}));
const payload = async () => {
const r = await snap(); const s = r.result?.snapshot;
const n = (s?.nodes ?? []).find(x => x.name === 'Payload');
return { rev: s?.revision, parent: n?.parentId ? s.nodes.find(x => x.id === n.parentId)?.name : null,
world: n?.worldMatrix?.map(v => Number(v.toFixed(4))) };
};
for (const name of ['Parts','Mount','RootRig']) {
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2000); }
}
console.log("BEFORE", JSON.stringify(await payload()));
await page.evaluate(() => {
const row = [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === 'Payload');
const tree = document.querySelector('.outliner-tree');
const dt = new DataTransfer();
row.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
tree.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey: true }));
tree.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey: true }));
});
await page.waitForTimeout(8000);
console.log("AFTER-CLEAR", JSON.stringify(await payload()));
await browser.close();

18
tools/web/.probe63.mjs Normal file
View File

@@ -0,0 +1,18 @@
import { createRequire } from "node:module";
const { chromium } = createRequire(import.meta.url)("/home/mes123456/workinf_Blender_Wasm/web/node_modules/playwright");
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles("/home/mes123456/workinf_Blender_Wasm/tests/files/web/hierarchy_simplify_combined.blend");
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(3500);
console.log("STATUS", JSON.stringify(await page.evaluate(() => ({
status: document.querySelector('[data-testid="editor-workflow-status"]')?.textContent,
context: document.querySelector('[data-testid="editor-workflow-context"]')?.textContent,
count: document.querySelector('[data-testid="editor-workflow-workspace-count"]')?.textContent,
mainStatus: document.querySelector('main')?.getAttribute('data-editor-workflow-status'),
}))));
await browser.close();

23
tools/web/.probe7.mjs Normal file
View File

@@ -0,0 +1,23 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const out = [];
for (const delay of [500, 2000, 5000, 10000]) {
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
await page.waitForTimeout(delay);
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
await page.waitForTimeout(4000);
const r = await page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,120) }));
out.push({ delay, ...r });
await browser.close();
}
console.log(JSON.stringify(out, null, 1));

31
tools/web/.probe8.mjs Normal file
View File

@@ -0,0 +1,31 @@
import { createRequire } from "node:module";
import fs from "node:fs"; import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const ws = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), area: document.querySelector("main")?.getAttribute("data-editor-workflow-area") }));
console.log("START", JSON.stringify(await ws()));
const clickWs = (name) => page.evaluate((v) => {
const btns = [...document.querySelectorAll('nav[aria-label="工作区"] button')];
const b = btns.find(n => (n.textContent||"").trim() === v);
if (!b) return { found: false, names: btns.map(n => (n.textContent||"").trim()) };
b.click();
return { found: true, disabled: b.disabled, cls: b.className };
}, name);
console.log("CLICK-Modeling", JSON.stringify(await clickWs("Modeling")));
for (const t of [1000, 2000, 4000, 8000]) { await page.waitForTimeout(t === 1000 ? 1000 : t - (t === 2000 ? 1000 : t === 4000 ? 2000 : 4000)); console.log("T"+t, JSON.stringify(await ws())); }
console.log("F3TAP", JSON.stringify(await page.evaluate(() => { const b = document.querySelector('[aria-label="操作搜索"]'); b?.click(); return Boolean(b); })));
await page.waitForTimeout(2000);
await page.keyboard.press("Escape");
await page.waitForTimeout(1500);
console.log("CLICK-Scripting", JSON.stringify(await clickWs("Scripting")));
await page.waitForTimeout(5000);
console.log("AFTER-Scripting", JSON.stringify(await ws()));
await browser.close();

31
tools/web/.probe9.mjs Normal file
View File

@@ -0,0 +1,31 @@
import { createRequire } from "node:module";
import path from "node:path";
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
await page.waitForTimeout(2000);
const st = () => page.evaluate(() => ({ rev: document.querySelector("main")?.getAttribute("data-current-main-revision"), stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent }));
const click = (l) => page.evaluate((v) => { const b = document.querySelector(`[aria-label="${v}"]`); b?.click(); return Boolean(b); }, l);
console.log("S0", JSON.stringify(await st()));
await click("添加立方体"); await page.waitForTimeout(8000);
console.log("S1-add", JSON.stringify(await st()));
const rev1 = (await st()).rev;
await click("撤销"); await page.waitForTimeout(5000);
console.log("S2-undo", JSON.stringify(await st()));
await click("重做"); await page.waitForTimeout(5000);
console.log("S3-redo", JSON.stringify(await st()));
await page.waitForTimeout(6000);
const revA = (await st()).rev;
await click("复制对象"); await page.waitForTimeout(6000);
console.log("S4-dup", JSON.stringify(await st()));
await click("删除对象"); await page.waitForTimeout(6000);
console.log("S5-del", JSON.stringify(await st()));
await page.waitForTimeout(6000);
console.log("S6-quiet", JSON.stringify(await st()));
await browser.close();

View File

@@ -0,0 +1,277 @@
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 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 sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const rootTask = "WBV2-P1129-000";
const moduleTask = "WBV2-P1129-001";
const parentTask = "WBV2-P1128-000";
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const queuePath = "docs/EXECUTION_QUEUE.md";
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const parent = json(parentManifestPath);
const index = json("tests/golden/WBV2/task-index.json");
const plan = json(planPath);
const prepared = plan.tasks.some((task) => task.id === rootTask || task.id === moduleTask);
if (prepared) {
assert.equal(queueTask, rootTask, `expected resumable authorization checkpoint ${rootTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, rootTask);
assert.equal(index.activeTask, moduleTask);
} else {
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, null);
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
assert.equal(index.activeTask, null);
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
}
const module = {
moduleId: "CORE_SCENE_GRAPH_SETTINGS",
sourceBacked: true,
capabilities: ["CAP-SCENE-COMPOSITOR-SETTINGS", "CAP-SCENE-PERFORMANCE-SETTINGS", "CAP-SCENE-SAMPLING-SETTINGS"],
sourcePaths: [
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
"web/protocol/web-engine.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/app/src/app/App.tsx",
"web/tests/e2e/wbv2-scene-settings-production.spec.ts",
],
reuseReceipts: [
{ manifest: "tests/golden/WBV2-P65-002/manifest.json", report: "tests/golden/WBV2-P65-002/implementation-report.json" },
{ manifest: "tests/golden/WBV2-P66-002/manifest.json", report: "tests/golden/WBV2-P66-002/implementation-report.json" },
{ manifest: "tests/golden/WBV2-P67-002/manifest.json", report: "tests/golden/WBV2-P67-002/implementation-report.json" },
],
positive: ["compositor device/precision mutation", "performance thread/persistence mutation", "sampling filter/dither mutation"],
negative: ["invalid sampling range", "unknown scene target", "malformed property set"],
recovery: ["undo", "redo", "save/reopen", "deterministic scene settings snapshot"],
commands: [
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5344 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-scene-settings-production.spec.ts",
],
fullFamilyParity: false,
};
const rootRecord = {
id: rootTask,
phase: "1129",
title: "authorize_scene_settings_module",
kind: "enabling",
owner: "PROGRAM",
dependsOn: [parentTask],
parityId: null,
implementationClass: "QUEUE_ONLY",
parallelGroup: "module-root-1129",
gate: "MODULE",
card: `docs/tasks/${rootTask}.md`,
behavior: "Authorize one source-backed Core Scene Graph settings module after the terminal P1128 module epoch.",
outputs: ["successor authorization", "module plan", "manifest", "status"],
acceptance: ["P1128 evidence remains hash-bound", "existing receipts are reused", "inventory remains immutable", "one source-backed module is active"],
commands: ["node tools/web/check-execution-control-plane.mjs"],
status: "done",
ready: false,
queueMutationAllowed: true,
};
const moduleRecord = {
id: moduleTask,
phase: "1129",
title: "verify_scene_settings_module",
kind: "module",
owner: "CORE_SCENE",
dependsOn: [rootTask],
parityId: module.moduleId,
implementationClass: "LOCAL_EXACT",
parallelGroup: "module-core_scene_graph_settings",
gate: "MODULE",
card: `docs/tasks/${moduleTask}.md`,
behavior: "Verify the existing Blender Main scene compositor, performance and sampling setting transactions as one aggregate module.",
outputs: ["module receipt", "negative matrix", "manifest", "status"],
acceptance: ["source and protocol hashes are bound", "positive Main mutations are proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen receipts are exact", "full-family parity remains explicit"],
status: "in_progress",
ready: true,
queueMutationAllowed: true,
module,
};
if (!prepared) {
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
plan.modules.push({ id: module.moduleId, task: moduleTask, owner: "CORE_SCENE", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
plan.tasks.push(rootRecord, moduleRecord);
writeJson(planPath, plan);
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: module.moduleId, priorTerminalEpoch: "WBV2-P1128-000" };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
} else {
const existingRoot = plan.tasks.find((task) => task.id === rootTask);
if (existingRoot) existingRoot.commands = rootRecord.commands;
writeJson(planPath, plan);
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
}
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize scene settings module
- task: ${rootTask}
- parent: ${parentTask}
- status: done
- gap: workflow:authorize_scene_settings_module
- ownerFamily: PROGRAM
- targetImplementationClass: QUEUE_ONLY
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1129.mjs\`
## 目标
Authorize one source-backed Core Scene Graph settings module while preserving all P1128 evidence and the immutable inventory.
## 输入与范围
- Parent manifest: \`${parentManifestPath}\`
- Existing receipts: \`tests/golden/WBV2-P65-002/\`, \`tests/golden/WBV2-P66-002/\`, \`tests/golden/WBV2-P67-002/\`
- New module: \`${moduleTask}\`
## 验收
- P1128 source and receipt hashes remain bound
- existing scene-setting implementation receipts are reused without rerun
- exactly one source-backed module is active
- inventory remains \`6900\` total and effective \`290/6609\`
- missing source, unsupported capability, malformed input, hash drift or failed gate keeps the module in_progress
- node tools/web/check-execution-control-plane.mjs
## 交付与回滚
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${module.moduleId}\`
- P1128 evidence reused; no implementation or inventory mutation claimed
- nextTask: \`${moduleTask}\`
`);
let updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, {
schemaVersion: 1,
task: rootTask,
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
status: "PASS",
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
successor: { task: moduleTask, moduleId: module.moduleId, sourceBacked: true, noRepeatedImplementation: true },
inventory: plan.inventory,
nextTask: moduleTask,
});
writeJson(`tests/golden/${rootTask}/manifest.json`, {
schemaVersion: 1,
task: rootTask,
parentTask,
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
status: "done",
enablingTask: true,
parityStateChange: false,
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
artifacts: {
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) },
plan: { path: planPath, sha256: sha(planPath) },
report: { path: rootReportPath, sha256: sha(rootReportPath) },
status: { path: rootStatusPath, sha256: sha(rootStatusPath) },
taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
},
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, fullHistoricalReproduction: false },
nextTask: moduleTask,
});
updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify scene settings module
- task: ${moduleTask}
- parent: ${rootTask}
- status: in_progress
- gap: workflow:core_scene_graph_settings
- ownerFamily: CORE_SCENE
- targetImplementationClass: LOCAL_EXACT
- moduleId: ${module.moduleId}
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
## 目标
Verify the existing Blender Main compositor, performance and sampling setting transactions as one source-backed module. This adds aggregate production evidence without duplicating the P1128 movie implementation or older scene-setting receipts.
## 输入与范围
- Source paths:
${module.sourcePaths.map((file) => `- \`${file}\``).join("\n")}
- Reused receipts:
${module.reuseReceipts.map((item) => `- \`${item.manifest}\` + \`${item.report}\``).join("\n")}
- Production test: \`web/tests/e2e/wbv2-scene-settings-production.spec.ts\`
## 验收
- Positive: ${module.positive.join(", ")}
- Negative: ${module.negative.join(", ")}
- Recovery: ${module.recovery.join(", ")}
- Commands:
${module.commands.map((command) => `- \`${command}\``).join("\n")}
- Missing source, hash drift, malformed input or browser failure remains in_progress and does not advance the queue.
## 交付与回滚
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
- Status: \`docs/status/${moduleTask}.md\`
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
`);
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
let next = read("nextTask.md");
const tick = String.fromCharCode(96);
next = next
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
writeText("nextTask.md", next);
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${module.moduleId}\n`);

View File

@@ -0,0 +1,245 @@
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 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 sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const rootTask = "WBV2-P1130-000";
const moduleTask = "WBV2-P1130-001";
const parentTask = "WBV2-P1129-001";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const queuePath = "docs/EXECUTION_QUEUE.md";
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const parent = json(parentManifestPath);
const index = json("tests/golden/WBV2/task-index.json");
const plan = json(planPath);
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, null);
assert.equal(index.activeTask, null);
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
const module = {
moduleId: "UI_EDITOR_VIEW2D_STATE",
sourceBacked: true,
capabilities: ["CAP-EDITOR-VIEW2D-CUR", "CAP-EDITOR-VIEW2D-MASK", "CAP-EDITOR-VIEW2D-FAIL-CLOSED"],
sourcePaths: [
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
"web/protocol/editor-view2d.ts",
"web/app/src/workers/editor-view2d-test.worker.ts",
"web/app/src/app/App.tsx",
"web/tests/e2e/ui-editor-view2d-production.spec.ts",
],
reuseReceipts: [],
positive: ["Graph main-region View2D cur retention", "Graph main-region pixel mask retention", "reader/protocol exact state"],
negative: ["inverted View2D bounds", "malformed mask range", "missing View2D payload"],
recovery: ["immutable base editor workflow", "worker termination", "deterministic repeated parse", "save/reopen reader compatibility"],
commands: [
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5346 npm --prefix web run test:e2e -- --workers=1 tests/e2e/ui-editor-view2d-production.spec.ts",
],
fullFamilyParity: false,
};
const rootRecord = {
id: rootTask,
phase: "1130",
title: "authorize_editor_view2d_module",
kind: "enabling",
owner: "PROGRAM",
dependsOn: [parentTask],
parityId: null,
implementationClass: "QUEUE_ONLY",
parallelGroup: "module-root-1130",
gate: "MODULE",
card: `docs/tasks/${rootTask}.md`,
behavior: "Authorize one source-backed UI editor View2D reader/protocol module after the P1129 scene settings module.",
outputs: ["successor authorization", "module plan", "manifest", "status"],
acceptance: ["P1129 evidence remains hash-bound", "P1102 editor workflow source remains unchanged", "inventory remains immutable", "one source-backed module is active"],
commands: ["node tools/web/check-execution-control-plane.mjs"],
status: "done",
ready: false,
queueMutationAllowed: true,
};
const moduleRecord = {
id: moduleTask,
phase: "1130",
title: "verify_editor_view2d_module",
kind: "module",
owner: "EDITOR",
dependsOn: [rootTask],
parityId: module.moduleId,
implementationClass: "LOCAL_EXACT",
parallelGroup: "module-ui_editor_view2d_state",
gate: "MODULE",
card: `docs/tasks/${moduleTask}.md`,
behavior: "Preserve Blender Graph/Dope Sheet View2D cur and mask state through the Web editor protocol without changing the existing editor workflow parser.",
outputs: ["module receipt", "negative matrix", "manifest", "status"],
acceptance: ["source and protocol hashes are bound", "positive reader state is proven", "negative no-publish and recovery are proven", "Chromium and save/reopen compatibility are exact", "full-family parity remains explicit"],
status: "in_progress",
ready: true,
queueMutationAllowed: true,
module,
};
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
plan.modules.push({ id: module.moduleId, task: moduleTask, owner: "EDITOR", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: [], fullFamilyParity: false });
plan.tasks.push(rootRecord, moduleRecord);
writeJson(planPath, plan);
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: module.moduleId, priorModule: "WBV2-P1129-001" };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize editor View2D module
- task: ${rootTask}
- parent: ${parentTask}
- status: done
- gap: workflow:authorize_editor_view2d_module
- ownerFamily: PROGRAM
- targetImplementationClass: QUEUE_ONLY
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1130.mjs\`
## 目标
Authorize a source-backed UI editor View2D reader/protocol module while preserving the P1129 receipt and the immutable inventory.
## 输入与范围
- Parent manifest: \`${parentManifestPath}\`
- Blender reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`
- New module: \`${moduleTask}\`
## 验收
- P1129 source and receipt hashes remain bound
- P1102 editor workflow source remains unchanged
- exactly one source-backed module is active
- malformed, unsupported, missing source or hash-drift input keeps the module in_progress
- node tools/web/check-execution-control-plane.mjs
## 交付与回滚
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${module.moduleId}\`
- P1129 evidence reused; P1102 editor workflow source remains unchanged
- nextTask: \`${moduleTask}\`
`);
let updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, {
schemaVersion: 1,
task: rootTask,
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
status: "PASS",
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
successor: { task: moduleTask, moduleId: module.moduleId, sourceBacked: true, noRepeatedImplementation: true },
inventory: plan.inventory,
nextTask: moduleTask,
});
writeJson(`tests/golden/${rootTask}/manifest.json`, {
schemaVersion: 1,
task: rootTask,
parentTask,
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
status: "done",
enablingTask: true,
parityStateChange: false,
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
artifacts: {
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) },
plan: { path: planPath, sha256: sha(planPath) },
report: { path: rootReportPath, sha256: sha(rootReportPath) },
status: { path: rootStatusPath, sha256: sha(rootStatusPath) },
taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
},
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, fullHistoricalReproduction: false },
nextTask: moduleTask,
});
updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify editor View2D module
- task: ${moduleTask}
- parent: ${rootTask}
- status: in_progress
- gap: workflow:ui_editor_view2d_state
- ownerFamily: EDITOR
- targetImplementationClass: LOCAL_EXACT
- moduleId: ${module.moduleId}
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
## 目标
Preserve the Blender Graph/Dope Sheet View2D \`cur\` and pixel \`mask\` state through a dedicated protocol adapter. The existing P1102 editor workflow parser remains untouched so its source hash and receipt stay reusable.
## 输入与范围
- Source paths:
${module.sourcePaths.map((file) => `- \`${file}\``).join("\n")}
- Production test: \`web/tests/e2e/ui-editor-view2d-production.spec.ts\`
## 验收
- Positive: ${module.positive.join(", ")}
- Negative: ${module.negative.join(", ")}
- Recovery: ${module.recovery.join(", ")}
- Commands:
${module.commands.map((command) => `- \`${command}\``).join("\n")}
- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.
## 交付与回滚
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
- Status: \`docs/status/${moduleTask}.md\`
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
`);
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
let next = read("nextTask.md");
const tick = String.fromCharCode(96);
next = next
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
writeText("nextTask.md", next);
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${module.moduleId}\n`);

View File

@@ -0,0 +1,261 @@
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 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 sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const rootTask = "WBV2-P1131-000";
const moduleTask = "WBV2-P1131-001";
const parentTask = "WBV2-P1130-001";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const queuePath = "docs/EXECUTION_QUEUE.md";
const moduleId = "CURVE_TOPOLOGY_EDITOR";
const sourcePaths = [
"blender-5.2.0/source/blender/editors/curve/curve_ops.cc",
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
"web/protocol/curve-topology-editor.ts",
"web/app/src/workers/curve-topology-contract-test.worker.ts",
"web/app/src/app/App.tsx",
"web/tests/e2e/curve-topology-contract.spec.ts",
"web/tests/e2e/curve-topology-operator.spec.ts",
];
const commands = [
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5347 npm --prefix web run test:e2e -- --workers=1 tests/e2e/curve-topology-contract.spec.ts tests/e2e/curve-topology-operator.spec.ts",
];
const positive = ["TOGGLE_CYCLIC Main mutation", "operator gate exposes only verified capability", "save/reopen preserves cyclic spline state"];
const negative = ["unknown curve operator", "stale revision claim", "selection and payload budget overflow"];
const recovery = ["undo", "redo", "save/reopen", "worker termination and deterministic contract parse"];
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const parent = json(parentManifestPath);
const plan = json(planPath);
const index = json("tests/golden/WBV2/task-index.json");
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, null);
assert.equal(index.activeTask, null);
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
const module = {
moduleId,
sourceBacked: true,
capabilities: ["CAP-CURVE-TOPOLOGY-TOGGLE-CYCLIC", "CAP-CURVE-TOPOLOGY-GATE", "CAP-CURVE-TOPOLOGY-FAIL-CLOSED"],
sourcePaths,
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }],
positive,
negative,
recovery,
commands,
fullFamilyParity: false,
};
const rootRecord = {
id: rootTask,
phase: "1131",
title: "authorize_curve_topology_module",
kind: "enabling",
owner: "PROGRAM",
dependsOn: [parentTask],
parityId: null,
implementationClass: "QUEUE_ONLY",
parallelGroup: "module-root-1131",
gate: "MODULE",
card: `docs/tasks/${rootTask}.md`,
behavior: "Authorize one source-backed Curve topology editor module after the P1130 View2D module.",
outputs: ["successor authorization", "module plan", "manifest", "status"],
acceptance: ["P1130 evidence remains hash-bound", "Curve topology source and protocol are present", "inventory remains immutable", "one source-backed module is active"],
commands: ["node tools/web/check-execution-control-plane.mjs"],
status: "done",
ready: false,
queueMutationAllowed: true,
};
const moduleRecord = {
id: moduleTask,
phase: "1131",
title: "verify_curve_topology_module",
kind: "module",
owner: "EDITOR",
dependsOn: [rootTask],
parityId: moduleId,
implementationClass: "LOCAL_EXACT",
parallelGroup: "module-curve_topology_editor",
gate: "MODULE",
card: `docs/tasks/${moduleTask}.md`,
behavior: "Verify the Blender Curve TOGGLE_CYCLIC topology transaction through the existing Main, protocol, Worker and frontend path.",
outputs: ["module receipt", "negative matrix", "manifest", "status"],
acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
status: "in_progress",
ready: true,
queueMutationAllowed: true,
module,
};
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
plan.modules.push({ id: moduleId, task: moduleTask, owner: "EDITOR", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
plan.tasks.push(rootRecord, moduleRecord);
writeJson(planPath, plan);
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Curve topology module
- task: ${rootTask}
- parent: ${parentTask}
- status: done
- gap: workflow:authorize_curve_topology_module
- ownerFamily: PROGRAM
- targetImplementationClass: QUEUE_ONLY
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1131.mjs\`
## 目标
Authorize one source-backed Curve topology editor module while preserving the P1130 receipt and immutable inventory.
## 输入与范围
- Parent manifest: \`${parentManifestPath}\`
- Blender source: \`blender-5.2.0/source/blender/editors/curve/curve_ops.cc\`
- Web protocol: \`web/protocol/curve-topology-editor.ts\`
- New module: \`${moduleTask}\`
## 验收
- P1130 source and receipt hashes remain bound
- Curve Main, protocol, Worker and frontend sources are present
- exactly one source-backed module is active
- malformed, unsupported, missing source or hash-drift input keeps the module in_progress
- node tools/web/check-execution-control-plane.mjs
## 交付与回滚
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`
- P1130 evidence reused; original 6900-entry inventory remains immutable at effective 290/6609
- nextTask: \`${moduleTask}\`
`);
let updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, {
schemaVersion: 1,
task: rootTask,
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
status: "PASS",
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true },
inventory: { ...plan.inventory, boundCaseDelta: 0 },
nextTask: moduleTask,
});
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
writeJson(rootManifestPath, {
schemaVersion: 1,
task: rootTask,
parentTask,
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
status: "done",
enablingTask: true,
parityStateChange: false,
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
artifacts: {
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) },
plan: { path: planPath, sha256: sha(planPath) },
report: { path: rootReportPath, sha256: sha(rootReportPath) },
status: { path: rootStatusPath, sha256: sha(rootStatusPath) },
taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
},
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, fullHistoricalReproduction: false },
nextTask: moduleTask,
});
updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Curve topology module
- task: ${moduleTask}
- parent: ${rootTask}
- status: in_progress
- gap: workflow:curve_topology_editor
- ownerFamily: EDITOR
- targetImplementationClass: LOCAL_EXACT
- moduleId: ${moduleId}
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
## 目标
Verify the source-backed Blender Curve TOGGLE_CYCLIC transaction through Main, protocol, Worker and frontend paths without duplicating the existing non-mesh geometry implementation.
## 输入与范围
- Source paths:
${sourcePaths.map((file) => `- \`${file}\``).join("\n")}
- Reused receipts:
- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`
- Production tests: \`web/tests/e2e/curve-topology-contract.spec.ts\`, \`web/tests/e2e/curve-topology-operator.spec.ts\`
## 验收
- Positive: ${positive.join(", ")}
- Negative: ${negative.join(", ")}
- Recovery: ${recovery.join(", ")}
- Commands:
${commands.map((command) => `- \`${command}\``).join("\n")}
- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.
## 交付与回滚
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
- Status: \`docs/status/${moduleTask}.md\`
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
`);
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
let next = read("nextTask.md");
const tick = String.fromCharCode(96);
next = next
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
writeText("nextTask.md", next);
run(["tools/web/check-execution-control-plane.mjs"]);
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,235 @@
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 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 sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const rootTask = "WBV2-P1132-000";
const moduleTask = "WBV2-P1132-001";
const parentTask = "WBV2-P1131-001";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const moduleId = "CURVE_HANDLE_GIZMO";
const sourcePaths = [
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
"web/protocol/nonmesh-interaction.ts",
"web/app/src/workers/nonmesh-interaction-test.worker.ts",
"web/app/src/app/App.tsx",
"web/app/src/three-adapter/nonmesh.ts",
"web/app/src/three-adapter/viewport.ts",
"web/app/src/three-adapter/offscreen-viewport.ts",
"web/tests/e2e/wbv2-curve-handle-production.spec.ts",
];
const commands = [
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5348 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-curve-handle-production.spec.ts",
];
const dynamic = json(dynamicPath);
const dynamicSummary = {
path: dynamicPath,
sha256: sha(dynamicPath),
sourceTreeSha256: dynamic.authority.sourceTreeSha256,
unitCount: dynamic.summary.unitCount,
planningEstimate: dynamic.policy.planningEstimate,
hardTarget: dynamic.policy.hardTarget,
verifiedUnits: dynamic.coverage.verifiedUnits,
unverifiedUnits: dynamic.coverage.unverifiedUnits,
};
const queuePath = "docs/EXECUTION_QUEUE.md";
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const parent = json(parentManifestPath);
const plan = json(planPath);
const index = json("tests/golden/WBV2/task-index.json");
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, null);
assert.equal(index.activeTask, null);
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
const module = {
moduleId,
sourceBacked: true,
capabilities: ["CAP-CURVE-HANDLE-MAIN", "CAP-CURVE-HANDLE-GIZMO-PREVIEW", "CAP-CURVE-HANDLE-FAIL-CLOSED"],
sourcePaths,
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }],
positive: ["Curve handle position Main mutation", "local handle gizmo preview", "save/reopen preserves handle state"],
negative: ["invalid handle identity", "stale base revision", "malformed handle position"],
recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic preview"],
commands,
fullFamilyParity: false,
};
const rootRecord = {
id: rootTask, phase: "1132", title: "authorize_curve_handle_module", kind: "enabling", owner: "PROGRAM",
dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1132", gate: "MODULE",
card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Curve handle gizmo module after the P1131 topology module.",
outputs: ["successor authorization", "module plan", "manifest", "status"],
acceptance: ["P1131 evidence remains hash-bound", "Curve handle source and protocol are present", "inventory remains immutable", "one source-backed module is active"],
commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true,
};
const moduleRecord = {
id: moduleTask, phase: "1132", title: "verify_curve_handle_module", kind: "module", owner: "EDITOR", dependsOn: [rootTask],
parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-curve_handle_gizmo", gate: "MODULE",
card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Curve handle position edits through Blender Main, protocol, Worker, renderer preview and frontend gizmo.",
outputs: ["module receipt", "negative matrix", "manifest", "status"],
acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
status: "in_progress", ready: true, queueMutationAllowed: true, module,
};
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
plan.modules.push({ id: moduleId, task: moduleTask, owner: "EDITOR", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
plan.tasks.push(rootRecord, moduleRecord);
writeJson(planPath, plan);
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Curve handle module
- task: ${rootTask}
- parent: ${parentTask}
- status: done
- gap: workflow:authorize_curve_handle_module
- ownerFamily: PROGRAM
- targetImplementationClass: QUEUE_ONLY
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1132.mjs\`
## 目标
Authorize one source-backed Curve handle gizmo module while preserving P1131 evidence and the historical inventory.
## 输入与范围
- Parent manifest: \`${parentManifestPath}\`
- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`
- Web protocol: \`web/protocol/nonmesh-interaction.ts\`
- New module: \`${moduleTask}\`
## 验收
- P1131 source and receipt hashes remain bound
- Curve handle Main/protocol/Worker/frontend sources are present
- exactly one source-backed module is active
- malformed, unsupported, missing source or hash-drift input keeps the module in_progress
- node tools/web/check-execution-control-plane.mjs
## 交付与回滚
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`
- P1131 evidence reused; historical 6900-entry inventory remains immutable
- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)
- nextTask: \`${moduleTask}\`
`);
let updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, {
schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS",
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true },
inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask,
});
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
writeJson(rootManifestPath, {
schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false,
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
artifacts: {
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) },
report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
},
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false },
nextTask: moduleTask,
});
updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Curve handle module
- task: ${moduleTask}
- parent: ${rootTask}
- status: in_progress
- gap: workflow:curve_handle_gizmo
- ownerFamily: EDITOR
- targetImplementationClass: LOCAL_EXACT
- moduleId: ${moduleId}
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
## 目标
Verify the source-backed Curve handle gizmo position transaction through Blender Main, protocol, Worker, renderer preview and frontend paths.
## 输入与范围
- Source paths:
${sourcePaths.map((file) => `- \`${file}\``).join("\n")}
- Reused receipts:
- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`
- Production test: \`web/tests/e2e/wbv2-curve-handle-production.spec.ts\`
## 验收
- Positive: ${module.positive.join(", ")}
- Negative: ${module.negative.join(", ")}
- Recovery: ${module.recovery.join(", ")}
- Commands:
${commands.map((command) => `- \`${command}\``).join("\n")}
- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.
## 交付与回滚
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
- Status: \`docs/status/${moduleTask}.md\`
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
`);
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
let next = read("nextTask.md");
const tick = String.fromCharCode(96);
next = next
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
writeText("nextTask.md", next);
run(["tools/web/check-execution-control-plane.mjs"]);
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,231 @@
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 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 sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const rootTask = "WBV2-P1133-000";
const moduleTask = "WBV2-P1133-001";
const parentTask = "WBV2-P1132-001";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const moduleId = "CURVE_SPLINES_TRANSACTION";
const sourcePaths = [
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
"web/protocol/web-engine.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/tests/e2e/wbv2-curve-splines-production.spec.ts",
];
const commands = [
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5349 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-curve-splines-production.spec.ts",
];
const dynamic = json(dynamicPath);
const dynamicSummary = {
path: dynamicPath,
sha256: sha(dynamicPath),
sourceTreeSha256: dynamic.authority.sourceTreeSha256,
unitCount: dynamic.summary.unitCount,
planningEstimate: dynamic.policy.planningEstimate,
hardTarget: dynamic.policy.hardTarget,
verifiedUnits: dynamic.coverage.verifiedUnits,
unverifiedUnits: dynamic.coverage.unverifiedUnits,
};
const queuePath = "docs/EXECUTION_QUEUE.md";
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const parent = json(parentManifestPath);
const plan = json(planPath);
const index = json("tests/golden/WBV2/task-index.json");
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, null);
assert.equal(index.activeTask, null);
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
const module = {
moduleId,
sourceBacked: true,
capabilities: ["CAP-CURVE-SPLINES-MAIN", "CAP-CURVE-SPLINES-RECOVERY", "CAP-CURVE-SPLINES-FAIL-CLOSED"],
sourcePaths,
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }],
positive: ["complete Curve spline Main transaction", "POLY/BEZIER descriptor retention", "save/reopen preserves spline state"],
negative: ["invalid spline offsets", "stale base revision", "mismatched handle payload"],
recovery: ["undo", "redo", "save/reopen", "deterministic binary-geometry transaction"],
commands,
fullFamilyParity: false,
};
const rootRecord = {
id: rootTask, phase: "1133", title: "authorize_curve_splines_module", kind: "enabling", owner: "PROGRAM",
dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1133", gate: "MODULE",
card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Curve spline transaction module after the P1132 handle module.",
outputs: ["successor authorization", "module plan", "manifest", "status"],
acceptance: ["P1132 evidence remains hash-bound", "Curve spline source and protocol are present", "inventory remains immutable", "one source-backed module is active"],
commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true,
};
const moduleRecord = {
id: moduleTask, phase: "1133", title: "verify_curve_splines_module", kind: "module", owner: "EDITOR", dependsOn: [rootTask],
parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-curve_splines_transaction", gate: "MODULE",
card: `docs/tasks/${moduleTask}.md`, behavior: "Verify complete Curve spline replacement through Blender Main, protocol, Worker and Chromium recovery.",
outputs: ["module receipt", "negative matrix", "manifest", "status"],
acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
status: "in_progress", ready: true, queueMutationAllowed: true, module,
};
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
plan.modules.push({ id: moduleId, task: moduleTask, owner: "EDITOR", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
plan.tasks.push(rootRecord, moduleRecord);
writeJson(planPath, plan);
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Curve splines module
- task: ${rootTask}
- parent: ${parentTask}
- status: done
- gap: workflow:authorize_curve_splines_module
- ownerFamily: PROGRAM
- targetImplementationClass: QUEUE_ONLY
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1133.mjs\`
## 目标
Authorize one source-backed Curve spline transaction module while preserving P1132 evidence and the historical inventory.
## 输入与范围
- Parent manifest: \`${parentManifestPath}\`
- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`
- Web protocol: \`web/protocol/web-engine.ts\`
- New module: \`${moduleTask}\`
## 验收
- P1132 source and receipt hashes remain bound
- Curve spline Main, protocol and Worker sources are present
- exactly one source-backed module is active
- malformed, unsupported, missing source or hash-drift input keeps the module in_progress
- node tools/web/check-execution-control-plane.mjs
## 交付与回滚
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`
- P1132 evidence reused; historical 6900-entry inventory remains immutable
- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)
- nextTask: \`${moduleTask}\`
`);
let updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, {
schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS",
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true },
inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask,
});
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
writeJson(rootManifestPath, {
schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false,
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
artifacts: {
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) },
report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
},
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false },
nextTask: moduleTask,
});
updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Curve splines module
- task: ${moduleTask}
- parent: ${rootTask}
- status: in_progress
- gap: workflow:curve_splines_transaction
- ownerFamily: EDITOR
- targetImplementationClass: LOCAL_EXACT
- moduleId: ${moduleId}
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
## 目标
Verify the source-backed Curve spline replacement transaction through Blender Main, protocol, Worker and Chromium recovery paths.
## 输入与范围
- Source paths:
${sourcePaths.map((file) => `- \`${file}\``).join("\n")}
- Reused receipts:
- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`
- Production test: \`web/tests/e2e/wbv2-curve-splines-production.spec.ts\`
## 验收
- Positive: ${module.positive.join(", ")}
- Negative: ${module.negative.join(", ")}
- Recovery: ${module.recovery.join(", ")}
- Commands:
${commands.map((command) => `- \`${command}\``).join("\n")}
- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.
## 交付与回滚
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
- Status: \`docs/status/${moduleTask}.md\`
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
`);
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
let next = read("nextTask.md");
const tick = String.fromCharCode(96);
next = next
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
writeText("nextTask.md", next);
run(["tools/web/check-execution-control-plane.mjs"]);
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,223 @@
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 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 sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const rootTask = "WBV2-P1134-000";
const moduleTask = "WBV2-P1134-001";
const parentTask = "WBV2-P1133-001";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const moduleId = "SURFACE_TOPOLOGY_TRANSACTION";
const sourcePaths = [
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
"web/protocol/web-engine.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/tests/e2e/wbv2-surface-topology-production.spec.ts",
];
const commands = [
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5350 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-surface-topology-production.spec.ts",
];
const dynamic = json(dynamicPath);
const dynamicSummary = {
path: dynamicPath, sha256: sha(dynamicPath), sourceTreeSha256: dynamic.authority.sourceTreeSha256,
unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget,
verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits,
};
const queuePath = "docs/EXECUTION_QUEUE.md";
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const parent = json(parentManifestPath);
const plan = json(planPath);
const index = json("tests/golden/WBV2/task-index.json");
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, null);
assert.equal(index.activeTask, null);
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
const module = {
moduleId, sourceBacked: true,
capabilities: ["CAP-SURFACE-TOPOLOGY-MAIN", "CAP-SURFACE-NURBS-BOUNDS", "CAP-SURFACE-FAIL-CLOSED"],
sourcePaths,
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }],
positive: ["Surface topology Main transaction", "NURBS dimensions and weights retention", "save/reopen preserves surface state"],
negative: ["invalid surface dimensions", "stale base revision", "control grid budget mismatch"],
recovery: ["undo", "redo", "save/reopen", "deterministic surface transaction"],
commands, fullFamilyParity: false,
};
const rootRecord = {
id: rootTask, phase: "1134", title: "authorize_surface_topology_module", kind: "enabling", owner: "PROGRAM",
dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1134", gate: "MODULE",
card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Surface topology transaction module after the P1133 Curve spline module.",
outputs: ["successor authorization", "module plan", "manifest", "status"],
acceptance: ["P1133 evidence remains hash-bound", "Surface topology source and protocol are present", "inventory remains immutable", "one source-backed module is active"],
commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true,
};
const moduleRecord = {
id: moduleTask, phase: "1134", title: "verify_surface_topology_module", kind: "module", owner: "GEOMETRY", dependsOn: [rootTask],
parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-surface_topology_transaction", gate: "MODULE",
card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Surface NURBS topology replacement through Blender Main, protocol, Worker and Chromium recovery.",
outputs: ["module receipt", "negative matrix", "manifest", "status"],
acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
status: "in_progress", ready: true, queueMutationAllowed: true, module,
};
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
plan.modules.push({ id: moduleId, task: moduleTask, owner: "GEOMETRY", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
plan.tasks.push(rootRecord, moduleRecord);
writeJson(planPath, plan);
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Surface topology module
- task: ${rootTask}
- parent: ${parentTask}
- status: done
- gap: workflow:authorize_surface_topology_module
- ownerFamily: PROGRAM
- targetImplementationClass: QUEUE_ONLY
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1134.mjs\`
## 目标
Authorize one source-backed Surface topology transaction module while preserving P1133 evidence and the historical inventory.
## 输入与范围
- Parent manifest: \`${parentManifestPath}\`
- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`
- Web protocol: \`web/protocol/web-engine.ts\`
- New module: \`${moduleTask}\`
## 验收
- P1133 source and receipt hashes remain bound
- Surface topology Main/protocol/Worker sources are present
- exactly one source-backed module is active
- malformed, unsupported, missing source or hash-drift input keeps the module in_progress
- node tools/web/check-execution-control-plane.mjs
## 交付与回滚
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`
- P1133 evidence reused; historical 6900-entry inventory remains immutable
- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)
- nextTask: \`${moduleTask}\`
`);
let updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, {
schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS",
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true },
inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask,
});
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
writeJson(rootManifestPath, {
schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false,
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
artifacts: {
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) },
report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
},
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false },
nextTask: moduleTask,
});
updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Surface topology module
- task: ${moduleTask}
- parent: ${rootTask}
- status: in_progress
- gap: workflow:surface_topology_transaction
- ownerFamily: GEOMETRY
- targetImplementationClass: LOCAL_EXACT
- moduleId: ${moduleId}
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
## 目标
Verify the source-backed Surface NURBS topology replacement through Blender Main, protocol, Worker and Chromium recovery paths.
## 输入与范围
- Source paths:
${sourcePaths.map((file) => `- \`${file}\``).join("\n")}
- Reused receipts:
- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`
- Production test: \`web/tests/e2e/wbv2-surface-topology-production.spec.ts\`
## 验收
- Positive: ${module.positive.join(", ")}
- Negative: ${module.negative.join(", ")}
- Recovery: ${module.recovery.join(", ")}
- Commands:
${commands.map((command) => `- \`${command}\``).join("\n")}
- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.
## 交付与回滚
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
- Status: \`docs/status/${moduleTask}.md\`
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
`);
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
let next = read("nextTask.md");
const tick = String.fromCharCode(96);
next = next
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
writeText("nextTask.md", next);
run(["tools/web/check-execution-control-plane.mjs"]);
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,88 @@
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 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 sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const rootTask = "WBV2-P1135-000";
const moduleTask = "WBV2-P1135-001";
const parentTask = "WBV2-P1134-001";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const moduleId = "FONT_BODY_TRANSACTION";
const sourcePaths = [
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
"web/protocol/web-engine.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/app/src/app/App.tsx",
"web/tests/e2e/wbv2-font-body-production.spec.ts",
];
const commands = [
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5351 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-font-body-production.spec.ts",
];
const dynamic = json(dynamicPath);
const dynamicSummary = { path: dynamicPath, sha256: sha(dynamicPath), sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
const queuePath = "docs/EXECUTION_QUEUE.md";
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const parent = json(parentManifestPath);
const plan = json(planPath);
const index = json("tests/golden/WBV2/task-index.json");
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, null);
assert.equal(index.activeTask, null);
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-FONT-BODY-MAIN", "CAP-FONT-BODY-RECOVERY", "CAP-FONT-BODY-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }], positive: ["Font body Main mutation", "UTF-8 text retention", "save/reopen preserves text"], negative: ["unknown Font data ID", "body byte budget overflow", "malformed body payload"], recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic text state"], commands, fullFamilyParity: false };
const rootRecord = { id: rootTask, phase: "1135", title: "authorize_font_body_module", kind: "enabling", owner: "PROGRAM", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1135", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Font body transaction module after the P1134 Surface topology module.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1134 evidence remains hash-bound", "Font body source and protocol are present", "inventory remains immutable", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
const moduleRecord = { id: moduleTask, phase: "1135", title: "verify_font_body_module", kind: "module", owner: "GEOMETRY", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-font_body_transaction", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Font body replacement through Blender Main, protocol, Worker and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
plan.modules.push({ id: moduleId, task: moduleTask, owner: "GEOMETRY", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
plan.tasks.push(rootRecord, moduleRecord);
writeJson(planPath, plan);
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Font body module\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- gap: workflow:authorize_font_body_module\n- ownerFamily: PROGRAM\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1135.mjs\`\n\n## 目标\n\nAuthorize one source-backed Font body transaction module while preserving P1134 evidence and the historical inventory.\n\n## 输入与范围\n\n- Parent manifest: \`${parentManifestPath}\`\n- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`\n- Web protocol: \`web/protocol/web-engine.ts\`\n- New module: \`${moduleTask}\`\n\n## 验收\n\n- P1134 source and receipt hashes remain bound\n- Font body Main/protocol/Worker sources are present\n- exactly one source-backed module is active\n- malformed, unsupported, missing source or hash-drift input keeps the module in_progress\n- node tools/web/check-execution-control-plane.mjs\n\n## 交付与回滚\n\n- Manifest: \`tests/golden/${rootTask}/manifest.json\`\n- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.\n`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1134 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
writeJson(rootManifestPath, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Font body module\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:font_body_transaction\n- ownerFamily: GEOMETRY\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\nVerify the source-backed Font body replacement through Blender Main, protocol, Worker and Chromium recovery paths.\n\n## 输入与范围\n\n- Source paths:\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- Reused receipts:\n- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`\n- Production test: \`web/tests/e2e/wbv2-font-body-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`\n- Manifest: \`tests/golden/${moduleTask}/manifest.json\`\n- Status: \`docs/status/${moduleTask}.md\`\n- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.\n`);
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
let next = read("nextTask.md");
const tick = String.fromCharCode(96);
next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`).replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`).replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`).replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`).replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`).replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
writeText("nextTask.md", next);
run(["tools/web/check-execution-control-plane.mjs"]);
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,87 @@
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 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 sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const rootTask = "WBV2-P1136-000";
const moduleTask = "WBV2-P1136-001";
const parentTask = "WBV2-P1135-001";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const moduleId = "FONT_PROPERTIES_TRANSACTION";
const sourcePaths = [
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
"web/protocol/web-engine.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/app/src/app/App.tsx",
"web/tests/e2e/wbv2-font-properties-production.spec.ts",
];
const commands = [
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5352 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-font-properties-production.spec.ts",
];
const dynamic = json(dynamicPath);
const dynamicSummary = { path: dynamicPath, sha256: sha(dynamicPath), sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
const queuePath = "docs/EXECUTION_QUEUE.md";
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const parent = json(parentManifestPath);
const plan = json(planPath);
const index = json("tests/golden/WBV2/task-index.json");
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, null);
assert.equal(index.activeTask, null);
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-FONT-PROPERTIES-MAIN", "CAP-FONT-PROPERTIES-RECOVERY", "CAP-FONT-PROPERTIES-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }], positive: ["Font property Main mutation", "bounded alignment/extrude fields", "save/reopen preserves properties"], negative: ["unknown Font data ID", "invalid alignment enum", "out-of-range numeric property"], recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic property state"], commands, fullFamilyParity: false };
const rootRecord = { id: rootTask, phase: "1136", title: "authorize_font_properties_module", kind: "enabling", owner: "PROGRAM", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1136", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Font properties transaction module after the P1135 Font body module.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1135 evidence remains hash-bound", "Font property source and protocol are present", "inventory remains immutable", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
const moduleRecord = { id: moduleTask, phase: "1136", title: "verify_font_properties_module", kind: "module", owner: "GEOMETRY", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-font_properties_transaction", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Font property replacement through Blender Main, protocol, Worker and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
plan.modules.push({ id: moduleId, task: moduleTask, owner: "GEOMETRY", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
plan.tasks.push(rootRecord, moduleRecord);
writeJson(planPath, plan);
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Font properties module\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- gap: workflow:authorize_font_properties_module\n- ownerFamily: PROGRAM\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1136.mjs\`\n\n## 目标\n\nAuthorize one source-backed Font properties transaction module while preserving P1135 evidence and the historical inventory.\n\n## 输入与范围\n\n- Parent manifest: \`${parentManifestPath}\`\n- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`\n- Web protocol: \`web/protocol/web-engine.ts\`\n- New module: \`${moduleTask}\`\n\n## 验收\n\n- P1135 source and receipt hashes remain bound\n- Font property Main/protocol/Worker sources are present\n- exactly one source-backed module is active\n- malformed, unsupported, missing source or hash-drift input keeps the module in_progress\n- node tools/web/check-execution-control-plane.mjs\n\n## 交付与回滚\n\n- Manifest: \`tests/golden/${rootTask}/manifest.json\`\n- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.\n`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1135 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
writeJson(rootManifestPath, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Font properties module\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:font_properties_transaction\n- ownerFamily: GEOMETRY\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\nVerify the source-backed Font property replacement through Blender Main, protocol, Worker and Chromium recovery paths.\n\n## 输入与范围\n\n- Source paths:\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- Reused receipts:\n- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`\n- Production test: \`web/tests/e2e/wbv2-font-properties-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`\n- Manifest: \`tests/golden/${moduleTask}/manifest.json\`\n- Status: \`docs/status/${moduleTask}.md\`\n- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.\n`);
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
let next = read("nextTask.md"); const tick = String.fromCharCode(96);
next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`).replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`).replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`).replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`).replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`).replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
writeText("nextTask.md", next);
run(["tools/web/check-execution-control-plane.mjs"]);
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,87 @@
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 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 sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const rootTask = "WBV2-P1137-000";
const moduleTask = "WBV2-P1137-001";
const parentTask = "WBV2-P1136-001";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const moduleId = "METABALL_ELEMENTS_TRANSACTION";
const sourcePaths = [
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
"web/protocol/web-engine.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/app/src/app/App.tsx",
"web/tests/e2e/wbv2-metaball-elements-production.spec.ts",
];
const commands = [
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5353 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-metaball-elements-production.spec.ts",
];
const dynamic = json(dynamicPath);
const dynamicSummary = { path: dynamicPath, sha256: sha(dynamicPath), sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
const queuePath = "docs/EXECUTION_QUEUE.md";
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const parent = json(parentManifestPath);
const plan = json(planPath);
const index = json("tests/golden/WBV2/task-index.json");
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, null);
assert.equal(index.activeTask, null);
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-METABALL-ELEMENTS-MAIN", "CAP-METABALL-ELEMENTS-RECOVERY", "CAP-METABALL-ELEMENTS-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }], positive: ["Metaball element Main mutation", "bounded position/radius/scale retention", "save/reopen preserves elements"], negative: ["unknown Metaball data ID", "invalid radius", "element count topology mismatch"], recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic element state"], commands, fullFamilyParity: false };
const rootRecord = { id: rootTask, phase: "1137", title: "authorize_metaball_elements_module", kind: "enabling", owner: "PROGRAM", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1137", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Metaball element transaction module after the P1136 Font properties module.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1136 evidence remains hash-bound", "Metaball element source and protocol are present", "inventory remains immutable", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
const moduleRecord = { id: moduleTask, phase: "1137", title: "verify_metaball_elements_module", kind: "module", owner: "GEOMETRY", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-metaball_elements_transaction", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Metaball element replacement through Blender Main, protocol, Worker and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
plan.modules.push({ id: moduleId, task: moduleTask, owner: "GEOMETRY", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
plan.tasks.push(rootRecord, moduleRecord);
writeJson(planPath, plan);
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Metaball elements module\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- gap: workflow:authorize_metaball_elements_module\n- ownerFamily: PROGRAM\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1137.mjs\`\n\n## 目标\n\nAuthorize one source-backed Metaball element transaction module while preserving P1136 evidence and the historical inventory.\n\n## 输入与范围\n\n- Parent manifest: \`${parentManifestPath}\`\n- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`\n- Web protocol: \`web/protocol/web-engine.ts\`\n- New module: \`${moduleTask}\`\n\n## 验收\n\n- P1136 source and receipt hashes remain bound\n- Metaball element Main/protocol/Worker sources are present\n- exactly one source-backed module is active\n- malformed, unsupported, missing source or hash-drift input keeps the module in_progress\n- node tools/web/check-execution-control-plane.mjs\n\n## 交付与回滚\n\n- Manifest: \`tests/golden/${rootTask}/manifest.json\`\n- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.\n`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1136 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
writeJson(rootManifestPath, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Metaball elements module\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:metaball_elements_transaction\n- ownerFamily: GEOMETRY\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\nVerify the source-backed Metaball element replacement through Blender Main, protocol, Worker and Chromium recovery paths.\n\n## 输入与范围\n\n- Source paths:\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- Reused receipts:\n- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`\n- Production test: \`web/tests/e2e/wbv2-metaball-elements-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`\n- Manifest: \`tests/golden/${moduleTask}/manifest.json\`\n- Status: \`docs/status/${moduleTask}.md\`\n- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.\n`);
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
let next = read("nextTask.md"); const tick = String.fromCharCode(96);
next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`).replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`).replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`).replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`).replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`).replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
writeText("nextTask.md", next);
run(["tools/web/check-execution-control-plane.mjs"]);
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,153 @@
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 writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const rootTask = "WBV2-P1138-000";
const moduleTask = "WBV2-P1138-001";
const parentTask = "WBV2-P1137-001";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const moduleId = "TEXT_SOURCE_TRANSACTION";
const sourcePaths = [
"blender-5.2.0/source/blender/blenkernel/intern/idtype.cc",
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
"web/protocol/web-engine.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/app/src/app/App.tsx",
"web/tests/e2e/wbv2-text-source-production.spec.ts",
"web/app/src/vendor/blender/web_engine.wasm",
];
const commands = [
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5353 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-text-source-production.spec.ts",
];
const queuePath = "docs/EXECUTION_QUEUE.md";
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const parent = json(parentManifestPath);
const plan = json(planPath);
const index = json("tests/golden/WBV2/task-index.json");
const dynamic = json(dynamicPath);
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, null);
assert.equal(index.activeTask, null);
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
const dynamicSummary = {
path: dynamicPath,
sha256: sha(dynamicPath),
sourceTreeSha256: dynamic.authority.sourceTreeSha256,
unitCount: dynamic.summary.unitCount,
planningEstimate: dynamic.policy.planningEstimate,
hardTarget: dynamic.policy.hardTarget,
verifiedUnits: dynamic.coverage.verifiedUnits,
unverifiedUnits: dynamic.coverage.unverifiedUnits,
};
const module = {
moduleId,
sourceBacked: true,
capabilities: ["CAP-TEXT-SOURCE-MAIN", "CAP-TEXT-SOURCE-RECOVERY", "CAP-TEXT-SOURCE-FAIL-CLOSED"],
sourcePaths,
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1137-001/manifest.json", report: "tests/golden/WBV2-P1137-001/module-receipt.json" }],
positive: ["Text source Main mutation", "ID_TXT registration in WITH_WEB", "save/reopen preserves source"],
negative: ["unknown Text data ID", "source over 1 MiB", "linked/read-only Text"],
recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic source state"],
commands,
fullFamilyParity: false,
};
const rootRecord = {
id: rootTask, phase: "1138", title: "authorize_text_source_module", kind: "enabling", owner: "SCRIPTING",
dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1138", gate: "MODULE",
card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed Text datablock mutation after the P1137 Metaball module.",
outputs: ["successor authorization", "module plan", "manifest", "status"],
acceptance: ["P1137 evidence remains hash-bound", "ID_TXT is registered for WITH_WEB", "inventory remains source-derived", "one source-backed module is active"],
commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true,
};
const moduleRecord = {
id: moduleTask, phase: "1138", title: "verify_text_source_module", kind: "module", owner: "SCRIPTING", dependsOn: [rootTask], parityId: moduleId,
implementationClass: "LOCAL_EXACT", parallelGroup: "module-text_source_transaction", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`,
behavior: "Verify Text source replacement through Blender Main, protocol, Worker and Chromium recovery.",
outputs: ["module receipt", "negative matrix", "manifest", "status"],
acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
status: "in_progress", ready: true, queueMutationAllowed: true, module,
};
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
plan.modules.push({ id: moduleId, task: moduleTask, owner: "SCRIPTING", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
plan.tasks.push(rootRecord, moduleRecord);
writeJson(planPath, plan);
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Text source module\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- gap: workflow:authorize_text_source_module\n- ownerFamily: SCRIPTING\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1138.mjs\`\n\n## 目标\n\nAuthorize one source-backed Text datablock transaction module while preserving P1137 evidence and the source-derived inventory.\n\n## 输入与范围\n\n- Parent manifest: \`${parentManifestPath}\`\n- Blender ID registry: \`blender-5.2.0/source/blender/blenkernel/intern/idtype.cc\`\n- Blender Main mutation: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`\n- New module: \`${moduleTask}\`\n\n## 验收\n\n- P1137 source and receipt hashes remain bound\n- ID_TXT is registered in the WITH_WEB source path\n- exactly one source-backed module is active\n- malformed, unsupported, missing source or hash-drift input keeps the module in_progress\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- Manifest: \`tests/golden/${rootTask}/manifest.json\`\n- Rollback: restore ${parentTask} as terminal queue parent and leave ${moduleTask} unstarted.\n`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1137 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
let updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, {
schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS",
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true },
inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask,
});
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
writeJson(rootManifestPath, {
schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true,
parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
artifacts: {
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) },
report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
},
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false },
nextTask: moduleTask,
});
updatedQueue = read(queuePath)
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Text source module\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:text_source_transaction\n- ownerFamily: SCRIPTING\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\nVerify Text source replacement through Blender Main, protocol, Worker and Chromium recovery paths.\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- Fixture: \`tests/files/web/generated/M16-GAP-00024-datablock-Text.blend\`\n- Production test: \`web/tests/e2e/wbv2-text-source-production.spec.ts\`\n- Reused receipt: \`tests/golden/WBV2-P1137-001/manifest.json\` + \`tests/golden/WBV2-P1137-001/module-receipt.json\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`\n- Manifest: \`tests/golden/${moduleTask}/manifest.json\`\n- Status: \`docs/status/${moduleTask}.md\`\n- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.\n`);
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
let next = read("nextTask.md");
const tick = String.fromCharCode(96);
next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
writeText("nextTask.md", next);
run("tools/web/check-execution-control-plane.mjs");
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,99 @@
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 writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const rootTask = "WBV2-P1139-000";
const moduleTask = "WBV2-P1139-001";
const parentTask = "WBV2-P1138-001";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const moduleId = "TEXTURE_PROPERTIES_TRANSACTION";
const sourcePaths = [
"blender-5.2.0/source/blender/blenkernel/intern/idtype.cc",
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
"web/protocol/web-engine.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/app/src/app/App.tsx",
"web/tests/e2e/wbv2-texture-properties-production.spec.ts",
"web/app/src/vendor/blender/web_engine.wasm",
];
const commands = [
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5353 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-texture-properties-production.spec.ts",
];
const queuePath = "docs/EXECUTION_QUEUE.md";
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
const parent = json(parentManifestPath);
const plan = json(planPath);
const index = json("tests/golden/WBV2/task-index.json");
const dynamic = json(dynamicPath);
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
assert.equal(parent.status, "done");
assert.equal(parent.nextTask, null);
assert.equal(index.activeTask, null);
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
const dynamicSummary = { path: dynamicPath, sha256: sha(dynamicPath), sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
const module = {
moduleId, sourceBacked: true,
capabilities: ["CAP-TEXTURE-PROPERTIES-MAIN", "CAP-TEXTURE-PROPERTIES-RECOVERY", "CAP-TEXTURE-PROPERTIES-FAIL-CLOSED"],
sourcePaths,
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1138-001/manifest.json", report: "tests/golden/WBV2-P1138-001/module-receipt.json" }],
positive: ["Texture property Main mutation", "ID_TE registration in WITH_WEB", "save/reopen preserves properties"],
negative: ["unknown Texture data ID", "noise depth outside Blender bounds", "non-finite numeric property"],
recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic property state"], commands, fullFamilyParity: false,
};
const rootRecord = { id: rootTask, phase: "1139", title: "authorize_texture_properties_module", kind: "enabling", owner: "TEXTURE", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1139", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed Texture property mutation module after P1138 Text.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1138 evidence remains hash-bound", "ID_TE is registered for WITH_WEB", "inventory remains source-derived", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
const moduleRecord = { id: moduleTask, phase: "1139", title: "verify_texture_properties_module", kind: "module", owner: "TEXTURE", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-texture_properties_transaction", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Texture property replacement through Blender Main, protocol, Worker and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
plan.modules.push({ id: moduleId, task: moduleTask, owner: "TEXTURE", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
plan.tasks.push(rootRecord, moduleRecord);
writeJson(planPath, plan);
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Texture properties module\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- gap: workflow:authorize_texture_properties_module\n- ownerFamily: TEXTURE\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1139.mjs\`\n\n## 目标\n\nAuthorize one source-backed Texture property transaction while preserving P1138 evidence and the source-derived inventory.\n\n## 输入与范围\n\n- Parent manifest: \`${parentManifestPath}\`\n- Blender ID registry: \`blender-5.2.0/source/blender/blenkernel/intern/idtype.cc\`\n- Blender Main mutation: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`\n- New module: \`${moduleTask}\`\n\n## 验收\n\n- P1138 source and receipt hashes remain bound\n- ID_TE is registered in the WITH_WEB source path\n- exactly one source-backed module is active\n- malformed, unsupported, missing source or hash-drift input keeps the module in_progress\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- Manifest: \`tests/golden/${rootTask}/manifest.json\`\n- Rollback: restore ${parentTask} as terminal queue parent and leave ${moduleTask} unstarted.\n`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1138 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
writeJson(rootManifestPath, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Texture properties module\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:texture_properties_transaction\n- ownerFamily: TEXTURE\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\nVerify Texture property replacement through Blender Main, protocol, Worker and Chromium recovery paths.\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- Fixture: \`tests/files/web/generated/M16-GAP-00025-datablock-Texture.blend\`\n- Production test: \`web/tests/e2e/wbv2-texture-properties-production.spec.ts\`\n- Reused receipt: \`tests/golden/WBV2-P1138-001/manifest.json\` + \`tests/golden/WBV2-P1138-001/module-receipt.json\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`\n- Manifest: \`tests/golden/${moduleTask}/manifest.json\`\n- Status: \`docs/status/${moduleTask}.md\`\n- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.\n`);
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
let next = read("nextTask.md"); const tick = String.fromCharCode(96);
next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`).replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`).replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`).replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`).replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`).replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
writeText("nextTask.md", next);
run("tools/web/check-execution-control-plane.mjs");
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,103 @@
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 writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const parentTask = "WBV2-P1139-001";
const rootTask = "WBV2-P1140-000";
const moduleTask = "WBV2-P1140-001";
const moduleId = "ACTION_GROUP_READER";
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const sourcePaths = [
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
"web/protocol/scene-ir.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/app/src/engine-client/WebEngineClient.ts",
"web/tests/e2e/wbv2-action-group-reader-production.spec.ts",
"web/app/src/vendor/blender/web_engine.wasm",
];
const commands = [
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5356 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-action-group-reader-production.spec.ts",
];
const queuePath = "docs/EXECUTION_QUEUE.md";
const queue = read(queuePath);
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
assert.ok(queueTask === parentTask || queueTask === rootTask || queueTask === moduleTask);
const parent = json(parentManifestPath);
assert.equal(parent.status, "done");
assert.ok(parent.nextTask === null || parent.nextTask === rootTask);
const plan = json(planPath);
assert.ok(plan.status === "COMPLETED_MODULE_EXECUTION" || plan.status === "ACTIVE_MODULE_EXECUTION_QUEUE");
const dynamic = json(dynamicPath);
const dynamicSummary = { path: dynamicPath, sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
const module = {
moduleId,
sourceBacked: true,
capabilities: ["CAP-ACTION-GROUP-READER", "CAP-ACTION-GROUP-RECOVERY", "CAP-ACTION-GROUP-FAIL-CLOSED"],
sourcePaths,
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1112-000/manifest.json", report: "tests/golden/WBV2-P1112-000/module-receipt.json" }],
positive: ["ActionGroup Main reader exposes group and expanded metadata", "three grouped location F-Curves remain grouped", "save/reopen preserves group metadata"],
negative: ["malformed blend is rejected without Main mutation", "missing animation target is fail-closed", "unsupported group payload is not synthesized"],
recovery: ["undo", "redo", "save/reopen", "worker termination and deterministic group state"],
commands,
fullFamilyParity: false,
};
const rootRecord = {
id: rootTask, phase: "1140", title: "authorize_action_group_reader_module", kind: "enabling", owner: "ANIMATION", dependsOn: [parentTask], parityId: null,
implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1140", gate: "MODULE", card: `docs/tasks/${rootTask}.md`,
behavior: "Authorize a source-backed ActionGroup reader and recovery module after P1139 Texture.", outputs: ["successor authorization", "module plan", "manifest", "status"],
acceptance: ["P1139 evidence remains hash-bound", "ActionGroup has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"],
commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true,
};
const moduleRecord = {
id: moduleTask, phase: "1140", title: "verify_action_group_reader_module", kind: "module", owner: "ANIMATION", dependsOn: [rootTask], parityId: moduleId,
implementationClass: "LOCAL_EXACT", parallelGroup: "module-action_group_reader", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`,
behavior: "Verify ActionGroup metadata through Blender Main reader, protocol and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"],
acceptance: ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
status: "in_progress", ready: true, queueMutationAllowed: true, module,
};
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
if (!plan.modules.some((entry) => entry.id === moduleId)) plan.modules.push({ id: moduleId, task: moduleTask, owner: "ANIMATION", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
if (!plan.tasks.some((entry) => entry.id === rootTask)) plan.tasks.push(rootRecord);
if (!plan.tasks.some((entry) => entry.id === moduleTask)) plan.tasks.push(moduleRecord);
writeJson(planPath, plan);
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize ActionGroup reader\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- ownerFamily: ANIMATION\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1140.mjs\`\n\n## 目标\n\n授权一个 source-backed ActionGroup reader/recovery 模块。\n\n## 输入与范围\n\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_action.cc\` (ActionGroup)\n- Reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`\n- Chromium: \`web/tests/e2e/wbv2-action-group-reader-production.spec.ts\`\n\n## 验收\n\n- P1139 evidence stays hash-bound; one source-backed module active; malformed or hash-drift input remains \`in_progress\`.\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- 交付 P1140 manifests/status/receipt;回滚恢复 P1139 终态指针。\n`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1139 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
writeJson(`tests/golden/${rootTask}/manifest.json`, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify ActionGroup reader\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:action_group_reader\n- ownerFamily: ANIMATION\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\n验证 Blender Main 的 ActionGroup 元数据在 Chromium recovery/save-reopen 中保持一致。\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_action.cc\` (ActionGroup)\n- Fixture: \`tests/files/web/generated/M16-GAP-00161-operator-anim.channels_group.blend\`\n- Production test: \`web/tests/e2e/wbv2-action-group-reader-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- JS simulation is forbidden; all behavior must come from Blender Main/WASM.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`; Manifest: \`tests/golden/${moduleTask}/manifest.json\`; Status: \`docs/status/${moduleTask}.md\`.\n`);
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
run("tools/web/check-execution-control-plane.mjs");
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,82 @@
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 writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const parentTask = "WBV2-P1140-001";
const rootTask = "WBV2-P1141-000";
const moduleTask = "WBV2-P1141-001";
const moduleId = "ACTION_POSE_MARKERS_READER";
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const sourcePaths = [
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
"web/protocol/scene-ir.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/app/src/engine-client/WebEngineClient.ts",
"web/tests/e2e/wbv2-action-pose-markers-production.spec.ts",
"web/app/src/vendor/blender/web_engine.wasm",
];
const commands = [
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5359 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-action-pose-markers-production.spec.ts",
];
const queuePath = "docs/EXECUTION_QUEUE.md";
const queueTask = read(queuePath).match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
assert.ok([parentTask, rootTask, moduleTask].includes(queueTask));
const parent = json(parentManifestPath);
assert.equal(parent.status, "done");
assert.ok(parent.nextTask === null || parent.nextTask === rootTask);
const plan = json(planPath);
assert.ok(["COMPLETED_MODULE_EXECUTION", "ACTIVE_MODULE_EXECUTION_QUEUE"].includes(plan.status));
const dynamic = json(dynamicPath);
const dynamicSummary = { path: dynamicPath, sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
const module = {
moduleId, sourceBacked: true, capabilities: ["CAP-ACTION-POSE-MARKERS-READER", "CAP-ACTION-POSE-MARKERS-RECOVERY", "CAP-ACTION-POSE-MARKERS-FAIL-CLOSED"], sourcePaths,
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1140-001/manifest.json", report: "tests/golden/WBV2-P1140-001/module-receipt.json" }],
positive: ["ActionPoseMarkers Main reader exposes marker names and frames", "three Blender pose markers remain ordered", "save/reopen preserves marker metadata"],
negative: ["malformed blend is rejected without Main mutation", "missing animation target is fail-closed", "non-finite marker frame is not synthesized"],
recovery: ["undo", "redo", "save/reopen", "worker termination and deterministic marker state"], commands, fullFamilyParity: false,
};
const rootRecord = { id: rootTask, phase: "1141", title: "authorize_action_pose_markers_module", kind: "enabling", owner: "ANIMATION", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1141", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed ActionPoseMarkers reader and recovery module after P1140.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1140 evidence remains hash-bound", "ActionPoseMarkers has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
const moduleRecord = { id: moduleTask, phase: "1141", title: "verify_action_pose_markers_reader_module", kind: "module", owner: "ANIMATION", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-action_pose_markers_reader", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify ActionPoseMarkers metadata through Blender Main reader, protocol and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
if (!plan.modules.some((entry) => entry.id === moduleId)) plan.modules.push({ id: moduleId, task: moduleTask, owner: "ANIMATION", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
if (!plan.tasks.some((entry) => entry.id === rootTask)) plan.tasks.push(rootRecord);
if (!plan.tasks.some((entry) => entry.id === moduleTask)) plan.tasks.push(moduleRecord);
writeJson(planPath, plan);
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize ActionPoseMarkers reader\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- ownerFamily: ANIMATION\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1141.mjs\`\n\n## 目标\n\n授权一个 source-backed ActionPoseMarkers reader/recovery 模块。\n\n## 输入与范围\n\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_action.cc\` (ActionPoseMarkers)\n- Reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`\n- Chromium: \`web/tests/e2e/wbv2-action-pose-markers-production.spec.ts\`\n\n## 验收\n\n- P1140 evidence stays hash-bound; one source-backed module active; malformed or hash-drift input remains \`in_progress\`.\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- 交付 P1141 manifests/status/receipt;回滚恢复 P1140 终态指针。\n`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1140 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
writeJson(`tests/golden/${rootTask}/manifest.json`, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify ActionPoseMarkers reader\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:action_pose_markers_reader\n- ownerFamily: ANIMATION\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\n验证 Blender Main 的 ActionPoseMarkers 元数据在 Chromium recovery/save-reopen 中保持一致。\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_action.cc\` (ActionPoseMarkers)\n- Fixture: \`tests/files/web/generated/M16-GAP-00161-action-pose-markers.blend\`\n- Production test: \`web/tests/e2e/wbv2-action-pose-markers-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- JS simulation is forbidden; all behavior must come from Blender Main/WASM.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`; Manifest: \`tests/golden/${moduleTask}/manifest.json\`; Status: \`docs/status/${moduleTask}.md\`.\n`);
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
run("tools/web/check-execution-control-plane.mjs");
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,76 @@
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 writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const parentTask = "WBV2-P1141-001";
const rootTask = "WBV2-P1142-000";
const moduleTask = "WBV2-P1142-001";
const moduleId = "FCURVE_SAMPLE_READER";
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const sourcePaths = [
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
"web/protocol/scene-ir.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/app/src/engine-client/WebEngineClient.ts",
"web/tests/e2e/wbv2-fcurve-sample-production.spec.ts",
"web/app/src/vendor/blender/web_engine.wasm",
];
const commands = [
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5365 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-fcurve-sample-production.spec.ts",
];
const queuePath = "docs/EXECUTION_QUEUE.md";
const queueTask = read(queuePath).match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
assert.ok([parentTask, rootTask, moduleTask].includes(queueTask));
const parent = json(parentManifestPath);
assert.equal(parent.status, "done");
assert.ok(parent.nextTask === null || parent.nextTask === rootTask);
const plan = json(planPath);
assert.ok(["COMPLETED_MODULE_EXECUTION", "ACTIVE_MODULE_EXECUTION_QUEUE"].includes(plan.status));
const dynamic = json(dynamicPath);
const dynamicSummary = { path: dynamicPath, sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-FCURVE-SAMPLE-READER", "CAP-FCURVE-SAMPLE-RECOVERY", "CAP-FCURVE-SAMPLE-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1141-001/manifest.json", report: "tests/golden/WBV2-P1141-001/module-receipt.json" }], positive: ["FCurveSample Main reader exposes sampled source and values", "four native FPoint samples remain ordered", "save/reopen preserves sampled data"], negative: ["malformed blend is rejected without Main mutation", "missing sampled payload is fail-closed", "non-finite sampled value is not synthesized"], recovery: ["undo", "redo", "save/reopen", "worker termination and deterministic sampled state"], commands, fullFamilyParity: false };
const rootRecord = { id: rootTask, phase: "1142", title: "authorize_fcurve_sample_reader_module", kind: "enabling", owner: "ANIMATION", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1142", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed FCurveSample reader and recovery module after P1141.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1141 evidence remains hash-bound", "FCurveSample has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
const moduleRecord = { id: moduleTask, phase: "1142", title: "verify_fcurve_sample_reader_module", kind: "module", owner: "ANIMATION", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-fcurve_sample_reader", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify FCurveSample data through Blender Main reader, protocol and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
if (!plan.modules.some((entry) => entry.id === moduleId)) plan.modules.push({ id: moduleId, task: moduleTask, owner: "ANIMATION", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
if (!plan.tasks.some((entry) => entry.id === rootTask)) plan.tasks.push(rootRecord);
if (!plan.tasks.some((entry) => entry.id === moduleTask)) plan.tasks.push(moduleRecord);
writeJson(planPath, plan);
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize FCurveSample reader\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- ownerFamily: ANIMATION\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1142.mjs\`\n\n## 目标\n\n授权一个 source-backed FCurveSample reader/recovery 模块。\n\n## 输入与范围\n\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (FCurveSample)\n- Reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`\n- Chromium: \`web/tests/e2e/wbv2-fcurve-sample-production.spec.ts\`\n\n## 验收\n\n- P1141 evidence stays hash-bound; one source-backed module active; malformed or hash-drift input remains \`in_progress\`.\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- 交付 P1142 manifests/status/receipt;回滚恢复 P1141 终态指针。\n`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1141 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
writeJson(`tests/golden/${rootTask}/manifest.json`, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify FCurveSample reader\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:fcurve_sample_reader\n- ownerFamily: ANIMATION\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\n验证 Blender Main 的 FCurveSample sampled 数据在 Chromium recovery/save-reopen 中保持一致。\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (FCurveSample)\n- Fixture: \`tests/files/web/generated/M16-GAP-00161-action-fcurve-samples.blend\`\n- Production test: \`web/tests/e2e/wbv2-fcurve-sample-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- JS simulation is forbidden; all behavior must come from Blender Main/WASM.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`; Manifest: \`tests/golden/${moduleTask}/manifest.json\`; Status: \`docs/status/${moduleTask}.md\`.\n`);
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
run("tools/web/check-execution-control-plane.mjs");
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,76 @@
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 writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const parentTask = "WBV2-P1142-001";
const rootTask = "WBV2-P1143-000";
const moduleTask = "WBV2-P1143-001";
const moduleId = "FCURVE_MODIFIER_READER";
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const sourcePaths = [
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
"web/protocol/scene-ir.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/app/src/engine-client/WebEngineClient.ts",
"web/tests/e2e/wbv2-fcurve-modifier-production.spec.ts",
"web/app/src/vendor/blender/web_engine.wasm",
];
const commands = [
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5368 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-fcurve-modifier-production.spec.ts",
];
const queuePath = "docs/EXECUTION_QUEUE.md";
const queueTask = read(queuePath).match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
assert.ok([parentTask, rootTask, moduleTask].includes(queueTask));
const parent = json(parentManifestPath);
assert.equal(parent.status, "done");
assert.ok(parent.nextTask === null || parent.nextTask === rootTask);
const plan = json(planPath);
assert.ok(["COMPLETED_MODULE_EXECUTION", "ACTIVE_MODULE_EXECUTION_QUEUE"].includes(plan.status));
const dynamic = json(dynamicPath);
const dynamicSummary = { path: dynamicPath, sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-FCURVE-MODIFIER-READER", "CAP-FCURVE-MODIFIER-RECOVERY", "CAP-FCURVE-MODIFIER-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1142-001/manifest.json", report: "tests/golden/WBV2-P1142-001/module-receipt.json" }], positive: ["FCurveModifiers Main reader exposes native modifier stack", "FModifierCycles preserves type/name/flags/influence", "save/reopen preserves modifier metadata"], negative: ["malformed blend is rejected without Main mutation", "missing modifier payload is fail-closed", "unsupported modifier type is not synthesized"], recovery: ["undo", "redo", "save/reopen", "worker termination and deterministic modifier state"], commands, fullFamilyParity: false };
const rootRecord = { id: rootTask, phase: "1143", title: "authorize_fcurve_modifier_reader_module", kind: "enabling", owner: "ANIMATION", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1143", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed FCurve modifier reader and recovery module after P1142.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1142 evidence remains hash-bound", "FCurveModifiers has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
const moduleRecord = { id: moduleTask, phase: "1143", title: "verify_fcurve_modifier_reader_module", kind: "module", owner: "ANIMATION", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-fcurve_modifier_reader", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify FCurve modifier metadata through Blender Main reader, protocol and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
if (!plan.modules.some((entry) => entry.id === moduleId)) plan.modules.push({ id: moduleId, task: moduleTask, owner: "ANIMATION", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
if (!plan.tasks.some((entry) => entry.id === rootTask)) plan.tasks.push(rootRecord);
if (!plan.tasks.some((entry) => entry.id === moduleTask)) plan.tasks.push(moduleRecord);
writeJson(planPath, plan);
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize FCurve modifier reader\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- ownerFamily: ANIMATION\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1143.mjs\`\n\n## 目标\n\n授权一个 source-backed FCurve modifier reader/recovery 模块。\n\n## 输入与范围\n\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (FCurveModifiers/FModifier/FModifierCycles)\n- Reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`\n- Chromium: \`web/tests/e2e/wbv2-fcurve-sample-production.spec.ts\`\n\n## 验收\n\n- P1142 evidence stays hash-bound; one source-backed module active; malformed or hash-drift input remains \`in_progress\`.\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- 交付 P1143 manifests/status/receipt;回滚恢复 P1142 终态指针。\n`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1142 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
writeJson(`tests/golden/${rootTask}/manifest.json`, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify FCurve modifier reader\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:fcurve_modifier_reader\n- ownerFamily: ANIMATION\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\n验证 Blender Main 的 FCurve modifier 栈在 Chromium recovery/save-reopen 中保持一致。\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (FCurveModifiers/FModifier/FModifierCycles)\n- Fixture: \`tests/files/web/generated/M16-GAP-00161-action-fcurve-modifier.blend\`\n- Production test: \`web/tests/e2e/wbv2-fcurve-sample-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- JS simulation is forbidden; all behavior must come from Blender Main/WASM.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`; Manifest: \`tests/golden/${moduleTask}/manifest.json\`; Status: \`docs/status/${moduleTask}.md\`.\n`);
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
run("tools/web/check-execution-control-plane.mjs");
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,76 @@
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 writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
const parentTask = "WBV2-P1143-001";
const rootTask = "WBV2-P1144-000";
const moduleTask = "WBV2-P1144-001";
const moduleId = "DRIVER_READER";
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
const sourcePaths = [
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
"web/protocol/scene-ir.ts",
"web/app/src/workers/web-engine.worker.ts",
"web/app/src/engine-client/WebEngineClient.ts",
"web/tests/e2e/wbv2-driver-reader-production.spec.ts",
"web/app/src/vendor/blender/web_engine.wasm",
];
const commands = [
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-driver-reader-production.spec.ts",
];
const queuePath = "docs/EXECUTION_QUEUE.md";
const queueTask = read(queuePath).match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
assert.ok([parentTask, rootTask, moduleTask].includes(queueTask));
const parent = json(parentManifestPath);
assert.equal(parent.status, "done");
assert.ok(parent.nextTask === null || parent.nextTask === rootTask);
const plan = json(planPath);
assert.ok(["COMPLETED_MODULE_EXECUTION", "ACTIVE_MODULE_EXECUTION_QUEUE"].includes(plan.status));
const dynamic = json(dynamicPath);
const dynamicSummary = { path: dynamicPath, sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-DRIVER-READER", "CAP-DRIVER-RECOVERY", "CAP-DRIVER-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1143-001/manifest.json", report: "tests/golden/WBV2-P1143-001/module-receipt.json" }], positive: ["AnimData driver list is read from Blender Main", "Driver preserves scripted expression and variables", "save/reopen preserves driver metadata"], negative: ["malformed blend is rejected without Main mutation", "missing driver target is fail-closed", "non-finite driver influence is not synthesized"], recovery: ["undo", "redo", "save/reopen", "worker termination and deterministic driver state"], commands, fullFamilyParity: false };
const rootRecord = { id: rootTask, phase: "1144", title: "authorize_driver_reader_module", kind: "enabling", owner: "ANIMATION", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1144", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed Driver reader and recovery module after P1143.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1143 evidence remains hash-bound", "Driver has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
const moduleRecord = { id: moduleTask, phase: "1144", title: "verify_driver_reader_module", kind: "module", owner: "ANIMATION", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-driver_reader", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Driver metadata through Blender Main reader, protocol and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
if (!plan.modules.some((entry) => entry.id === moduleId)) plan.modules.push({ id: moduleId, task: moduleTask, owner: "ANIMATION", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
if (!plan.tasks.some((entry) => entry.id === rootTask)) plan.tasks.push(rootRecord);
if (!plan.tasks.some((entry) => entry.id === moduleTask)) plan.tasks.push(moduleRecord);
writeJson(planPath, plan);
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
parent.nextTask = rootTask;
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
parent.artifacts.plan.sha256 = sha(planPath);
writeJson(parentManifestPath, parent);
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Driver reader\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- ownerFamily: ANIMATION\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1144.mjs\`\n\n## 目标\n\n授权一个 source-backed Driver reader/recovery 模块。\n\n## 输入与范围\n\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (Driver)\n- Reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`\n- Chromium: \`web/tests/e2e/wbv2-driver-reader-production.spec.ts\`\n\n## 验收\n\n- P1143 evidence stays hash-bound; one source-backed module active; malformed or hash-drift input remains \`in_progress\`.\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- 交付 P1144 manifests/status/receipt;回滚恢复 P1143 终态指针。\n`);
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1143 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
writeText(queuePath, updatedQueue);
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
const rootStatusPath = `docs/status/${rootTask}.md`;
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
writeJson(`tests/golden/${rootTask}/manifest.json`, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
writeText(queuePath, updatedQueue);
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Driver reader\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:driver_reader\n- ownerFamily: ANIMATION\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\n验证 Blender Main 的 Driver/AnimData 元数据在 Chromium recovery/save-reopen 中保持一致。\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (Driver/AnimDataDrivers)\n- Fixture: \`tests/files/web/generated/M16-GAP-00161-driver.blend\`\n- Production test: \`web/tests/e2e/wbv2-driver-reader-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- JS simulation is forbidden; all behavior must come from Blender Main/WASM.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`; Manifest: \`tests/golden/${moduleTask}/manifest.json\`; Status: \`docs/status/${moduleTask}.md\`.\n`);
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
run("tools/web/check-execution-control-plane.mjs");
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);

View File

@@ -0,0 +1,6 @@
abi <abi/4.0>,
include <tunables/global>
profile blender_web_bwrap /usr/bin/bwrap flags=(unconfined) {
userns,
}

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Produce the changed WASM and web bundle without running acceptance commands."""
import argparse
import datetime
import hashlib
import json
import os
from pathlib import Path
import subprocess
ROOT = Path(__file__).resolve().parents[2]
def publish(destination, data):
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_name(destination.name + f'.tmp-{os.getpid()}')
temporary.write_bytes(data)
temporary.replace(destination)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', default='tests/golden/WBV2-P1424-001/desktop-workflow-build')
args = parser.parse_args()
output = ROOT / args.output / datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
output.mkdir(parents=True, exist_ok=False)
report = {'schemaVersion': 1, 'task': 'WBV2-P1424-001', 'status': 'BUILDING',
'validation': 'NOT_RUN', 'acceptance': 'NOT_CLAIMED', 'steps': []}
try:
commands = [
*(([('cpython', ['python3', 'tools/web/build-webengine-python.py'], ROOT)]) if not (ROOT / 'build_web_python313/build/libpython3.13.a').exists() else []),
('wasm', ['bash', '-c', 'source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine --parallel 4'], ROOT),
]
for name, command, directory in commands:
with (output / (name + '.log')).open('w') as log:
completed = subprocess.run(command, cwd=directory, stdout=log, stderr=subprocess.STDOUT)
report['steps'].append({'name': name, 'exitCode': completed.returncode})
if completed.returncode: raise RuntimeError(name + ' build failed; see ' + str(output))
build = ROOT / 'build_web_blender6/bin'
manifest_path = ROOT / 'web/app/public/wasm-module-manifest.json'
manifest = json.loads(manifest_path.read_text())
report['artifacts'] = {}
for name in ('web_engine.js', 'web_engine.wasm'):
data = (build / name).read_bytes()
digest = hashlib.sha256(data).hexdigest()
for directory in ('web/app/src/vendor/blender', 'web/app/public/vendor/blender'):
publish(ROOT / directory / name, data)
for module in manifest['modules']:
for resource in module.get('resources', {}).values():
if resource.get('url') == '/vendor/blender/' + name: resource['sha256'] = digest
report['artifacts'][name] = {'sha256': digest, 'bytes': len(data)}
publish(manifest_path, (json.dumps(manifest, indent=2) + '\n').encode())
with (output / 'web.log').open('w') as log:
completed = subprocess.run(['node', 'tools/web/incremental-web-build.mjs'], cwd=ROOT,
env=dict(os.environ, WBV2_SKIP_TYPECHECK='1'), stdout=log, stderr=subprocess.STDOUT)
report['steps'].append({'name': 'web', 'exitCode': completed.returncode})
if completed.returncode: raise RuntimeError('web build failed; see ' + str(output))
report['status'] = 'BUILT_UNVERIFIED'
except BaseException as error:
report.update(status='BUILD_INCOMPLETE', error=str(error))
raise
finally:
publish(output / 'build.json', (json.dumps(report, indent=2) + '\n').encode())
print(json.dumps({'directory': str(output), **report}), flush=True)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Assemble an unpublished desktop application; never run acceptance or typecheck."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
def active_native_builder(native):
for process in Path('/proc').glob('[0-9]*'):
try:
if (process / 'cwd').resolve() == native.parent and (process / 'comm').read_text().strip() in {'ninja', 'make', 'gmake', 'cmake', 'wasm-ld', 'wasm-opt'}:
return process.name
except OSError:
continue
def write_json(path, value):
path.write_text(json.dumps(value, indent=2) + "\n")
def share_candidate_assets(resources, application):
cache = resources.parent.parent / ".candidate-assets"
cache.mkdir(exist_ok=True)
for source in resources.rglob("*"):
target = application / source.relative_to(resources)
if source.is_symlink() or target.is_symlink() or not source.is_file() or not target.is_file():
continue
if (source.stat().st_mode & 0o777) != (target.stat().st_mode & 0o777):
continue
with source.open("rb") as stream:
digest = hashlib.file_digest(stream, "sha256").hexdigest()
with target.open("rb") as stream:
if hashlib.file_digest(stream, "sha256").hexdigest() != digest:
continue
anchor = cache / f"{digest}-{source.stat().st_mode & 0o777:o}"
if not anchor.exists():
os.link(source, anchor)
with anchor.open("rb") as stream:
if anchor.is_symlink() or hashlib.file_digest(stream, "sha256").hexdigest() != digest:
raise RuntimeError(f"Candidate asset cache differs: {anchor.name}")
for path in (source, target):
if os.path.samefile(anchor, path):
continue
temporary = path.with_name(f".{path.name}.share-{os.getpid()}")
os.link(anchor, temporary)
os.replace(temporary, path)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", required=True, help="New local candidate directory")
parser.add_argument("--native-bin", default="build_web_blender6/bin",
help="Directory containing web_engine.js and web_engine.wasm, or either file")
args = parser.parse_args()
root = Path(__file__).resolve().parents[2]
output = (root / args.output).resolve()
native = (root / args.native_bin).resolve()
if native.is_file() and native.name in {"web_engine.js", "web_engine.wasm"}:
native = native.parent
# Reject a wrong input before copying the public tree or leaving a partial
# candidate. This is packaging input handling, not runtime verification.
missing = [name for name in ("web_engine.js", "web_engine.wasm")
if not (native / name).is_file()]
if missing:
parser.error(f"Native directory {native} is missing: {', '.join(missing)}")
# A fresh destination preserves both previous candidates and published assets.
if builder := active_native_builder(native):
parser.error(f"Native build PID {builder} is still active; retain pending edits and package after it exits")
native_stamps = {name: (native / name).stat() for name in ("web_engine.js", "web_engine.wasm")}
output.mkdir(parents=True, exist_ok=False)
resources = output / "public"
subprocess.run(["cp", "-a", "--reflink=auto", str(root / "web/app/public"),
str(resources)], check=True)
vendor = resources / "vendor/blender"
hashes = {}
for name in ("web_engine.js", "web_engine.wasm"):
shutil.copy2(native / name, vendor / name)
with (vendor / name).open("rb") as stream:
hashes[name] = hashlib.file_digest(stream, "sha256").hexdigest()
# Startup scripts, fonts, color management and Python's library are embedded
# by the native target. Retain the source startup resources with the candidate
# as well, without replacing any source or published runtime files.
shutil.copytree(root / "blender-5.2.0/scripts/startup", output / "scripts/startup")
shutil.copytree(root / "blender-5.2.0/scripts/presets", output / "scripts/presets")
module_path = resources / "wasm-module-manifest.json"
modules = json.loads(module_path.read_text())
for module in modules["modules"]:
resource = module["resources"].get("single")
if resource and resource["fileName"] == "web_engine.wasm":
resource.update(url="/vendor/blender/web_engine.wasm", sha256=hashes["web_engine.wasm"])
write_json(module_path, modules)
engine_path = resources / "engine-manifest.json"
engine = json.loads(engine_path.read_text())
for variant in engine["variants"]:
if variant["id"] == "single":
for kind, name in (("js", "web_engine.js"), ("wasm", "web_engine.wasm")):
variant["resources"][kind].update(url=f"/vendor/blender/{name}", sha256=hashes[name])
write_json(engine_path, engine)
env = dict(os.environ, WEB_DESKTOP_CANDIDATE_PUBLIC=str(resources),
WEB_DESKTOP_CANDIDATE_FACTORY=str(vendor / "web_engine.js"))
# Vite emits frontend and Worker from current source; no test/typecheck wrapper.
subprocess.run(["node", "node_modules/vite/bin/vite.js", "build", "--config",
"app/vite.config.ts", "--outDir", str(output / "app")],
cwd=root / "web", env=env, check=True)
share_candidate_assets(resources, output / "app")
if active_native_builder(native):
parser.error("Native build started during packaging; no candidate manifest published; retain pending edits")
for name, before in native_stamps.items():
after = (native / name).stat()
if (before.st_ino, before.st_size, before.st_mtime_ns) != (after.st_ino, after.st_size, after.st_mtime_ns):
parser.error(f"Native input changed during packaging: {name}; no candidate manifest published")
write_json(output / "candidate.json", {
"stageId": "main-desktop", "status": "implemented_unverified",
"scopeComplete": False, "nativeUIComplete": False,
"entry": "app/index.html", "nativeBin": str(native), "sha256": hashes,
"runtimeURL": "/vendor/blender/web_engine.wasm",
"startupResources": "embedded in WASM; source copy in scripts/",
"deployment": "not_performed", "validation": "not_run",
})
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Build WM/RNA objects and link an isolated candidate from existing native libraries."""
import datetime
import hashlib
import json
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
root=Path(__file__).resolve().parents[2]
if '--configured' not in sys.argv:
result=subprocess.run(['bash','-c','source tools/web/native-build-env.sh && exec "$1" "$2" --configured',
'_',sys.executable,str(Path(__file__).resolve())],cwd=root)
sys.exit(result.returncode)
build=root/'build_blender_native'
candidate=build/'desktop-adapter'
directory=root/'tests/golden/WBV2-P1424-001/native-desktop-adapter-build'/datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
directory.mkdir(parents=True)
for name in ('objects','lib','bin','generated'):(candidate/name).mkdir(parents=True,exist_ok=True)
report={'schemaVersion':1,'status':'BUILDING','validation':'NOT_RUN','steps':[],'strategy':'REUSE_EXISTING_LIBRARIES_NO_DNA_LAYOUT_CHANGE'}
def run(name,command,cwd=build):
with (directory/(name+'.log')).open('w') as log:
result=subprocess.run(command,cwd=cwd,env=dict(os.environ,WBV2_SKIP_TYPECHECK='1'),stdout=log,stderr=subprocess.STDOUT)
report['steps'].append({'name':name,'exitCode':result.returncode})
if result.returncode:raise RuntimeError(name+' artifact build failed')
def digest(file):
sha=hashlib.sha256()
with file.open('rb') as stream:
for data in iter(lambda:stream.read(1024*1024),b''):sha.update(data)
return {'sha256':sha.hexdigest(),'bytes':file.stat().st_size}
def link_command(target):
output=subprocess.check_output(['ninja','-t','commands',target],cwd=build,text=True)
tokens=shlex.split(output.strip().splitlines()[-1])
if tokens[:2]==[':','&&']:tokens=tokens[2:]
if '&&' in tokens:tokens=tokens[:tokens.index('&&')]
if not tokens or 'g++' not in Path(tokens[0]).name or '-o' not in tokens:raise RuntimeError('NATIVE_LINK_RULE_UNSUPPORTED')
return tokens
try:
rules=['CXX_COMPILER__makesrna_unscanned_Release','CXX_COMPILER__bf_rna_unscanned_Release','CXX_COMPILER__bf_windowmanager_unscanned_Release']
commands=json.loads(subprocess.check_output(['ninja','-t','compdb',*rules],cwd=build,text=True))
def compile_object(suffix,name,source=None):
entry=next(item for item in commands if item['file'].endswith(suffix))
args=shlex.split(entry['command']);destination=candidate/'objects'/name
for flag in ('-o','-MT','-MF'):
if flag in args:args[args.index(flag)+1]=str(destination)+('.d' if flag=='-MF' else '')
if source:args[args.index('-c')+1]=str(source)
args.insert(1,'-I'+str(candidate/'generated'))
run(name,args)
return destination,entry['output']
generator_object,old_generator_object=compile_object('/rna_wm_api.cc','rna_wm_api.cc.o')
args=link_command('bin/makesrna');args=[str(generator_object) if value==old_generator_object else value for value in args]
generator=candidate/'bin/makesrna';args[args.index('-o')+1]=str(generator);run('link-makesrna',args)
run('generate-rna',[str(generator),str(root/'blender-5.2.0/source/blender/makesdna')+'/',str(candidate/'generated')+'/',str(candidate/'generated')+'/'])
wm_object,_=compile_object('/wm_window.cc','wm_window.cc.o')
rna_object,_=compile_object('/rna_wm_gen.cc','rna_wm_gen.cc.o',candidate/'generated/rna_wm_gen.cc')
replacements={};report['reusedArchives']={}
for name,object_file in [('libbf_windowmanager.a',wm_object),('libbf_rna.a',rna_object)]:
archive=build/'lib'/name;report['reusedArchives'][name]=digest(archive)
target=candidate/'lib'/name;shutil.copyfile(archive,target)
run('archive-'+name,['ar','rcs',str(target),str(object_file)])
replacements['lib/'+name]=str(target)
args=link_command('bin/blender');args=[replacements.get(value,value) for value in args]
pending=candidate/'bin/blender.pending';args[args.index('-o')+1]=str(pending)
run('link-blender',args)
runtime=candidate/'bin/5.2'
if not runtime.exists():runtime.symlink_to(build/'bin/5.2',target_is_directory=True)
pending.replace(candidate/'bin/blender')
run('web',['node','tools/web/incremental-web-build.mjs'],root)
report.update(status='BUILT_UNVERIFIED',nativeBinary=digest(candidate/'bin/blender'),binary=str(candidate/'bin/blender'))
except BaseException as error:
report.update(status='BUILD_INCOMPLETE',error=str(error));raise
finally:
(directory/'build.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps({'directory':str(directory),**report}),flush=True)

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Produce the bounded evidence capture plan for WBV2-P1424-001.
This planner is intentionally side-effect free: implementation phase records the
exact Chromium/native workflows to run later, without claiming they passed.
"""
import json
from pathlib import Path
TASK = "WBV2-P1424-001"
CONTRACTS = Path("tests/golden") / TASK / "behavior-contracts.json"
OUT = Path("tests/golden") / TASK / "runtime-evidence-plan.json"
def main():
data = json.loads(CONTRACTS.read_text())
plan = {
"schemaVersion": 1,
"task": TASK,
"status": "PENDING_USER_RUNTIME_VALIDATION",
"runner": "Chromium only",
"sourceContracts": str(CONTRACTS),
"steps": [],
}
for contract in data["contracts"]:
module = contract["module"]
plan["steps"].append({
"module": module,
"positive": contract["positive"],
"negative": contract["negative"],
"recovery": contract["recovery"],
"requiredArtifacts": [
f"{module.lower()}-before.blend",
f"{module.lower()}-after.blend",
f"{module.lower()}-negative.blend",
f"{module.lower()}-undo.blend",
f"{module.lower()}-redo.blend",
f"{module.lower()}-reopened.blend",
f"{module.lower()}-chromium.json",
],
"result": "NOT_RUN",
})
OUT.write_text(json.dumps(plan, indent=2, ensure_ascii=False) + "\n")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Build Blender's pinned Alembic for the existing Web CacheFile reader."""
import hashlib
from pathlib import Path
import re
import subprocess
import tarfile
import urllib.request
ROOT = Path(__file__).resolve().parents[2]
BASE = ROOT / "build_web_alembic"
OCIO = ROOT / "build_web_ocio/install"
def main():
versions = (ROOT / "blender-5.2.0/build_files/build_environment/cmake/versions.cmake").read_text()
version = re.search(r"set\(ALEMBIC_VERSION ([^)]+)\)", versions)[1]
expected = re.search(r"set\(ALEMBIC_HASH ([^)]+)\)", versions)[1]
algorithm = re.search(r"set\(ALEMBIC_HASH_TYPE ([^)]+)\)", versions)[1].lower()
if not (OCIO / "lib/cmake/Imath/ImathConfig.cmake").is_file():
raise SystemExit("Reuse/build the Web OCIO Imath installation first")
BASE.mkdir(exist_ok=True)
source = BASE / ("alembic-" + version)
if not (source / "CMakeLists.txt").is_file():
archive = BASE / ("alembic-" + version + ".tar.gz")
if not archive.is_file():
with urllib.request.urlopen(
"https://codeload.github.com/alembic/alembic/tar.gz/" + version,
timeout=60,
) as response:
data = response.read()
if hashlib.new(algorithm, data).hexdigest() != expected:
raise SystemExit("Alembic download differs from Blender's pinned source hash")
archive.write_bytes(data)
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != expected:
raise SystemExit("Alembic archive differs from Blender's pinned source hash")
with tarfile.open(archive) as package:
package.extractall(BASE, filter="data")
toolchain = ROOT / ".emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"
subprocess.run([
"cmake", "-S", str(source), "-B", str(BASE / "build"), "-G", "Ninja",
"-DCMAKE_TOOLCHAIN_FILE=" + str(toolchain),
"-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_LIBDIR=lib",
"-DCMAKE_INSTALL_PREFIX=" + str(BASE / "install"),
"-DCMAKE_PREFIX_PATH=" + str(OCIO), "-DCMAKE_FIND_ROOT_PATH=" + str(OCIO),
"-DImath_DIR=" + str(OCIO / "lib/cmake/Imath"),
"-DCMAKE_CXX_FLAGS=-fexceptions", "-DALEMBIC_SHARED_LIBS=OFF",
"-DUSE_HDF5=OFF", "-DUSE_TESTS=OFF", "-DUSE_BINARIES=OFF",
"-DUSE_EXAMPLES=OFF", "-DUSE_PYALEMBIC=OFF", "-DUSE_MAYA=OFF",
"-DUSE_ARNOLD=OFF", "-DUSE_PRMAN=OFF",
], cwd=ROOT, check=True)
subprocess.run(["cmake", "--build", str(BASE / "build"), "--target", "install",
"--parallel", "8"], cwd=ROOT, check=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Build Blender-pinned FFmpeg audio libraries for the embedded Audaspace plugin."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tarfile
import urllib.request
ROOT = Path(__file__).resolve().parents[2]
VERSION = "8.1"
SHA256 = "c07039598df7d64d3c8b42c4e25b1959fc908621c6f6c2946881133f3b27eda2"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path)
parser.add_argument("--jobs", type=int, default=3)
args = parser.parse_args()
base = ROOT / "build_web_ffmpeg"
base.mkdir(exist_ok=True)
source = args.source.resolve() if args.source else base / f"ffmpeg-{VERSION}"
if not (source / "configure").is_file():
if args.source:
raise FileNotFoundError(source)
filename = f"ffmpeg-{VERSION}.tar.bz2"
caches = [base / filename, Path.home() / "working/packages" / filename,
Path.home() / "working/build_oiio_deps/downloads" / filename]
archive = next((path for path in caches if path.is_file()), caches[0])
if not archive.is_file():
partial = archive.with_suffix(".partial")
with urllib.request.urlopen(f"https://ffmpeg.org/releases/{filename}", timeout=60) as response, partial.open("wb") as out:
shutil.copyfileobj(response, out)
partial.replace(archive)
if hashlib.sha256(archive.read_bytes()).hexdigest() != SHA256:
raise RuntimeError("Blender-pinned FFmpeg archive digest mismatch")
with tarfile.open(archive) as package:
package.extractall(base, filter="data")
if (source / "RELEASE").read_text().strip() != VERSION:
raise RuntimeError("Expected Blender-pinned FFmpeg " + VERSION)
prefix = base / "install"
build = base / "build"
build.mkdir(exist_ok=True)
em = ROOT / ".emsdk/upstream/emscripten"
codecs = ROOT / "build_web_sndfile/install"
env = dict(os.environ)
env["PATH"] = str(em) + os.pathsep + env["PATH"]
env["PKG_CONFIG_LIBDIR"] = str(codecs / "lib/pkgconfig")
env["PKG_CONFIG_PATH"] = ""
command = [str(source / "configure"), f"--prefix={prefix}",
"--target-os=none", "--arch=wasm32", "--enable-cross-compile",
"--cc=" + str(em / "emcc"), "--cxx=" + str(em / "em++"),
"--ar=" + str(em / "emar"), "--ranlib=" + str(em / "emranlib"),
"--nm=" + str(em.parent / "bin/llvm-nm"),
f"--extra-cflags=-O2 -fPIC -I{codecs / 'include'}",
f"--extra-ldflags=-L{codecs / 'lib'}", "--disable-asm", "--disable-x86asm",
"--disable-pthreads", "--disable-w32threads", "--disable-os2threads",
"--disable-network", "--disable-autodetect", "--disable-everything",
"--disable-programs", "--disable-doc", "--disable-debug",
"--disable-avdevice", "--disable-avfilter", "--disable-swscale",
"--disable-shared", "--enable-static", "--enable-small",
"--enable-protocol=file", "--enable-libmp3lame", "--enable-libopus",
"--enable-libvorbis", "--pkg-config-flags=--static",
"--enable-encoder=aac,ac3,mp2,flac,libmp3lame,libopus,libvorbis,pcm_u8,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_f64le",
"--enable-decoder=aac,ac3,mp2,mp3,flac,opus,vorbis,pcm_u8,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_f64le",
"--enable-parser=aac,ac3,mpegaudio,flac,opus,vorbis",
"--enable-muxer=adts,ac3,mp2,mp3,flac,matroska,ogg,wav",
"--enable-demuxer=aac,ac3,mp3,flac,matroska,ogg,wav,mov",
"--enable-bsf=aac_adtstoasc,extract_extradata"]
settings = json.dumps([command, env["PKG_CONFIG_LIBDIR"]])
stamp = build / "web-configure.json"
if not stamp.is_file() or stamp.read_text() != settings:
subprocess.run(command, cwd=build, env=env, check=True)
stamp.write_text(settings)
subprocess.run(["make", f"-j{args.jobs}", "install"], cwd=build, env=env, check=True)
artifacts = {str(path.relative_to(ROOT)): hashlib.sha256(path.read_bytes()).hexdigest()
for path in sorted((prefix / "lib").glob("*.a"))}
(base / "build-manifest.json").write_text(json.dumps({
"version": VERSION, "source": str(source), "upstreamArchiveSha256": SHA256,
"configure": command, "artifacts": artifacts,
"validation": "NOT_RUN", "acceptance": "NOT_CLAIMED"}, indent=2) + "\n")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Build upstream NumPy for the existing static CPython/WASM runtime."""
import hashlib, json, os, pathlib, subprocess, tarfile, urllib.request, zipfile
ROOT = pathlib.Path(__file__).resolve().parents[2]
BASE = ROOT / 'build_web_numpy'
SOURCE = BASE / 'numpy-2.3.4'
BUILD = BASE / 'build'
PYTHON = ROOT / '.deps/native/python/bin/python3.13'
BASE.mkdir(exist_ok=True)
archive = BASE / 'numpy-2.3.4.tar.gz'
if not archive.exists():
urllib.request.urlretrieve('https://github.com/numpy/numpy/releases/download/v2.3.4/numpy-2.3.4.tar.gz', archive)
if hashlib.sha256(archive.read_bytes()).hexdigest() != 'a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a':
raise RuntimeError('Pinned NumPy source hash mismatch')
if not SOURCE.exists():
with tarfile.open(archive) as package: package.extractall(BASE, filter='data')
import re
# Shared modules isolate legacy random's 32-bit ABI; static modules need namespacing.
random_meson = SOURCE / 'numpy/random/meson.build'
symbols = sorted(set(re.findall(r'\b(random_[A-Za-z0-9_]+)\s*\(', (SOURCE / 'numpy/_core/include/numpy/random/distributions.h').read_text())))
symbols.append('random_geometric_inversion') # Exported helper absent from distributions.h.
legacy_flags = "['-DNP_RANDOM_LEGACY=1'" + ''.join(f", '-D{name}=web_numpy_legacy_{name}'" for name in symbols) + ']'
meson_source = re.sub(r"\['-DNP_RANDOM_LEGACY=1'(?:, '-Drandom_\w+=web_numpy_legacy_random_\w+')*\]", "['-DNP_RANDOM_LEGACY=1']", random_meson.read_text())
if "['-DNP_RANDOM_LEGACY=1']" in meson_source: random_meson.write_text(meson_source.replace("['-DNP_RANDOM_LEGACY=1']", legacy_flags))
elif legacy_flags not in meson_source: raise RuntimeError('Unexpected NumPy legacy random build declaration')
env = dict(os.environ)
env['PATH'] = str(PYTHON.parent) + ':' + env['PATH']
env['LD_LIBRARY_PATH'] = str(ROOT / '.deps/native/python/lib') + ':' + env.get('LD_LIBRARY_PATH', '')
pkgconfig = BASE / 'pkgconfig'
pkgconfig.mkdir(exist_ok=True)
env['PKG_CONFIG_LIBDIR'] = str(pkgconfig)
env['PKG_CONFIG_PATH'] = ''
(pkgconfig / 'python-3.13.pc').write_text(f'Name: Python\nDescription: WebEngine CPython\nVersion: 3.13\nLibs:\nCflags: -I{ROOT}/build_web_python313/build -I{ROOT}/build_web_python313/source/Include\n')
cross = BASE / 'emscripten.cross'
original = (SOURCE / 'tools/ci/emscripten/emscripten.meson.cross').read_text()
original = original.replace('[properties]\n', f"[properties]\npkg_config_libdir = '{pkgconfig}'\n")
binaries = f"c = 'emcc'\ncpp = 'em++'\nar = 'emar'\npython = '{PYTHON}'\ncython = '{PYTHON.parent}/cython'\n"
cross.write_text(original.replace('[binaries]\n', '[binaries]\n' + binaries) + "\n[built-in options]\nc_args = ['-fexceptions', '-DPY_CALL_TRAMPOLINE=1']\ncpp_args = ['-fexceptions', '-DPY_CALL_TRAMPOLINE=1']\n")
def run(name, command, **kwargs):
with (BASE / (name + '.log')).open('w') as log:
subprocess.run(command, cwd=ROOT, env=env, stdout=log, stderr=subprocess.STDOUT, check=True, **kwargs)
meson = [str(PYTHON), str(SOURCE / 'vendored-meson/meson/meson.py')]
if not (BUILD / 'build.ninja').exists():
run('configure', meson + ['setup', str(BUILD), str(SOURCE), '--cross-file=' + str(cross),
'--buildtype=release', '-Dblas=none', '-Dlapack=none', '-Dcpu-baseline=none', '-Dcpu-dispatch=none'])
else:
run('reconfigure', meson + ['setup', '--reconfigure', str(BUILD), str(SOURCE)])
commands = json.loads((BUILD / 'compile_commands.json').read_text())
python_commands = [entry['command'] for entry in commands if '.so.p/' in entry['output'] and entry['output'].endswith('.o')]
if not python_commands or any(f'-I{ROOT}/build_web_python313/build ' not in command for command in python_commands):
raise RuntimeError('NumPy extension compilation must use target CPython pyconfig.h')
plan = json.loads((BUILD / 'meson-info/intro-install_plan.json').read_text())
modules = [pathlib.Path(p).relative_to(BUILD) for p, v in plan['targets'].items() if v['tag'] == 'runtime']
inputs = set()
for module in modules:
query = subprocess.check_output(['ninja', '-C', str(BUILD), '-t', 'query', str(module)], env=env, text=True)
inputs.update(line.strip().lstrip('| ') for line in query.split(' outputs:')[0].splitlines() if line.strip().endswith(('.o', '.a')))
if len(modules) != 13 or not inputs:
raise RuntimeError('Unexpected NumPy runtime module graph; inspect upstream install plan')
(BUILD / 'webengine-static.ninja').write_text('include build.ninja\nbuild webengine_numpy_objects: phony ' + ' '.join(sorted(inputs)) + '\n')
run('objects', ['ninja', '-C', str(BUILD), '-f', 'webengine-static.ninja', '-j4', 'webengine_numpy_objects'])
library = BASE / 'libnumpy.a'
mri = 'CREATE ' + str(library) + '\n'
mri += ''.join(('ADDLIB ' if name.endswith('.a') else 'ADDMOD ') + str(BUILD / name) + '\n' for name in sorted(inputs))
run('archive', ['emar', '-M'], input=mri + 'SAVE\nEND\n', text=True)
files = {}
for kind, entries in plan.items():
if kind == 'targets': continue
for name, entry in entries.items():
if entry['tag'] != 'python-runtime': continue
source = pathlib.Path(name)
destination = entry['destination'].split('}/', 1)[1]
if source.is_file(): files[destination] = source
else:
for file in sorted(source.rglob('*')):
relative = file.relative_to(source)
if not file.is_file() or any(part in entry.get('exclude_dirs', []) + ['__pycache__'] for part in relative.parts): continue
if relative.as_posix() not in entry.get('exclude_files', []): files[destination + '/' + relative.as_posix()] = file
with zipfile.ZipFile(BASE / 'numpy.zip', 'w', compression=zipfile.ZIP_STORED) as package:
for name, file in sorted(files.items()): package.write(file, name)
package.write(SOURCE / 'LICENSE.txt', 'numpy/LICENSE.txt')
registrations = [('.'.join(module.parts[:-1] + (module.name.split('.')[0],)), module.name.split('.')[0]) for module in modules]
header = '#pragma once\n#include <Python.h>\n'
header += ''.join(f'extern "C" PyObject *PyInit_{symbol}();\n' for name, symbol in registrations)
header += 'inline int web_engine_register_numpy() {\n' + ''.join(f' if (PyImport_AppendInittab("{name}", PyInit_{symbol}) < 0) return -1;\n' for name, symbol in registrations) + ' return 0;\n}\n'
(BASE / 'webengine_numpy.hh').write_text(header)
bound = [archive, pathlib.Path(__file__), cross, random_meson, ROOT / 'build_web_python313/build/pyconfig.h', ROOT / 'build_web_python313/build/libpython3.13.a', library, BASE / 'numpy.zip', BASE / 'webengine_numpy.hh']
report = dict(status='BUILT_UNVERIFIED', target='wasm32-emscripten-cpython313-static', modules=[name for name, _ in registrations], artifacts={str(file.relative_to(ROOT)): hashlib.sha256(file.read_bytes()).hexdigest() for file in bound})
(BASE / 'build.json').write_text(json.dumps(report, indent=2) + '\n')

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Build OpenColorIO and its dependencies from the supplied local Blender sources."""
import argparse
from pathlib import Path
import subprocess
ROOT = Path(__file__).resolve().parents[2]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sources", type=Path,
default=Path.home() / "working/build_oiio_deps/build")
parser.add_argument("--jobs", type=int, default=4)
parser.add_argument("--package", action="append",
choices=("zlib", "expat", "yamlcpp", "pystring", "imath",
"minizipng", "opencolorio"))
args = parser.parse_args()
base = ROOT / "build_web_ocio"
prefix = base / "install"
packages = [
("zlib", "", ["ZLIB_BUILD_EXAMPLES=OFF"]),
("expat", "expat", ["EXPAT_BUILD_TOOLS=OFF", "EXPAT_BUILD_EXAMPLES=OFF",
"EXPAT_BUILD_TESTS=OFF", "EXPAT_SHARED_LIBS=OFF",
"EXPAT_BUILD_DOCS=OFF",
# CPython namespaces its Expat API but not these internals.
"CMAKE_C_FLAGS="
"-Dg_reparseDeferralEnabledDefault=WebOCIO_reparseDeferralEnabledDefault "
"-D_INTERNAL_trim_to_complete_utf8_characters="
"WebOCIO_trim_to_complete_utf8_characters"]),
("yamlcpp", "", ["YAML_CPP_BUILD_TESTS=OFF", "YAML_CPP_BUILD_TOOLS=OFF",
"YAML_CPP_FORMAT_SOURCE=OFF", "YAML_CPP_INSTALL=ON"]),
("pystring", "", []),
("imath", "", ["IMATH_BUILD_TESTS=OFF", "IMATH_BUILD_PYTHON=OFF",
"IMATH_INSTALL_PKG_CONFIG=ON"]),
("minizipng", "", ["MZ_COMPAT=OFF", "MZ_BZIP2=OFF", "MZ_LZMA=OFF",
"MZ_ZSTD=OFF", "MZ_OPENSSL=OFF", "MZ_LIBBSD=OFF",
"MZ_PKCRYPT=OFF", "MZ_WZAES=OFF", "MZ_ICONV=OFF",
"MZ_FETCH_LIBS=OFF", "MZ_BUILD_TESTS=OFF",
"MZ_BUILD_UNIT_TESTS=OFF"]),
("opencolorio", "", ["OCIO_BUILD_APPS=OFF", "OCIO_BUILD_TESTS=OFF",
"OCIO_BUILD_GPU_TESTS=OFF", "OCIO_BUILD_PYTHON=OFF",
"OCIO_BUILD_DOCS=OFF", "OCIO_USE_SIMD=OFF",
"OCIO_INSTALL_EXT_PACKAGES=NONE",
"OCIO_INSTALL_EXT_LIBS=OFF"]),
]
for name, subdir, options in packages:
if args.package and name not in args.package:
continue
source = args.sources / name / "src" / ("external_" + name) / subdir
build = base / name
if name == "zlib":
# This pinned copy already has distinct Emscripten archive names.
source = ROOT / "blender-5.2.0/extern/zlib"
build = base / "zlib-web"
configure = [
"emcmake", "cmake", "-S", str(source), "-B", str(build), "-G", "Ninja",
"-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_LIBDIR=lib",
"-DCMAKE_INSTALL_PREFIX=" + str(prefix),
"-DCMAKE_PREFIX_PATH=" + str(prefix),
"-DCMAKE_FIND_ROOT_PATH=" + str(prefix),
"-DCMAKE_CXX_FLAGS=-fexceptions", "-DBUILD_SHARED_LIBS=OFF",
"-DBUILD_TESTING=OFF",
*["-D" + option for option in options],
]
print("Building local dependency: " + name, flush=True)
subprocess.run(configure, cwd=ROOT, check=True)
subprocess.run(["cmake", "--build", str(build), "--target", "install",
"--parallel", str(args.jobs)], cwd=ROOT, check=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Build pinned native image codecs and OpenImageIO for Blender WASM."""
import hashlib, re, subprocess, sys, tarfile, urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
BASE = ROOT / 'build_web_oiio'
PREFIX = BASE / 'install'
VERSIONS = (ROOT / 'blender-5.2.0/build_files/build_environment/cmake/versions.cmake').read_text()
def value(key):
return re.search(r'set\(' + key + r' ([^)]+)\)', VERSIONS)[1]
def source(name):
version = value(name + '_VERSION')
filename = value(name + '_FILE').replace('${' + name + '_VERSION}', version)
url = value(name + '_URI').replace('${' + name + '_VERSION}', version).replace('http:', 'https:')
expected, algorithm = value(name+'_HASH'), value(name+'_HASH_TYPE').lower()
if name == 'PNG' and version == '1.6.58':
filename = 'libpng-3061454d980de7d53608f594194cfac722721d2a.tar.gz'
url = 'https://codeload.github.com/pnggroup/libpng/tar.gz/3061454d980de7d53608f594194cfac722721d2a'
expected, algorithm = 'a2569b7553971e36e86c8f75b9d1b5abbe92c436e05169afe3f0072e937f3536', 'sha256'
archive = BASE / filename
if not archive.exists():
try:
with urllib.request.urlopen(url, timeout=60) as response: data = response.read()
except OSError:
with urllib.request.urlopen('https://download.blender.org/dependencies/' + filename, timeout=60) as response: data = response.read()
if hashlib.new(algorithm, data).hexdigest() != expected:
raise SystemExit(name + ' download differs from Blender pinned hash')
archive.write_bytes(data)
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != expected: raise SystemExit(name + ' archive hash mismatch')
with tarfile.open(archive) as package:
directory = BASE / package.getmembers()[0].name.split('/')[0]
if not (directory / 'CMakeLists.txt').exists(): package.extractall(BASE, filter='data')
return directory
def build(name, directory, options):
if len(sys.argv)>1 and name not in sys.argv[1:]: return
toolchain = ROOT / '.emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake'
prefixes = ';'.join(str(p) for p in (PREFIX, ROOT/'build_web_openexr/install', ROOT/'build_web_ocio/install'))
subprocess.run(['cmake', '-S', str(directory), '-B', str(BASE/name), '-G', 'Ninja',
'-DCMAKE_TOOLCHAIN_FILE='+str(toolchain), '-DCMAKE_BUILD_TYPE=Release',
'-DCMAKE_INSTALL_PREFIX='+str(PREFIX), '-DCMAKE_INSTALL_LIBDIR='+str(PREFIX/'lib'),
'-DCMAKE_PREFIX_PATH='+prefixes, '-DCMAKE_FIND_ROOT_PATH='+prefixes,
'-DCMAKE_CXX_FLAGS=-fexceptions', '-DBUILD_SHARED_LIBS=OFF', '-DBUILD_TESTING=OFF',
'-DZLIB_LIBRARY='+str(ROOT/'build_web_blender6/lib/libzstatic.a'),
'-DZLIB_INCLUDE_DIR='+str(ROOT/'blender-5.2.0/extern/zlib'), *options], cwd=ROOT, check=True)
subprocess.run(['cmake','--build',str(BASE/name),'--target','install','--parallel','8'],cwd=ROOT,check=True)
def main():
BASE.mkdir(exist_ok=True)
build('jpeg',source('JPEG'),['-DENABLE_SHARED=OFF','-DENABLE_STATIC=ON','-DWITH_SIMD=OFF','-DENABLE_TESTING=OFF'])
build('png',source('PNG'),['-DPNG_SHARED=OFF','-DPNG_TESTS=OFF','-DPNG_TOOLS=OFF','-DPNG_EXECUTABLES=OFF'])
build('tiff',source('TIFF'),['-Dtiff-tools=OFF','-Dtiff-tests=OFF','-Dtiff-contrib=OFF','-Dtiff-docs=OFF','-Dtiff-deprecated=OFF','-Dlibdeflate=OFF','-Dlzma=OFF','-Dzstd=OFF','-Dwebp=OFF','-Djbig=OFF'])
robin = source('ROBINMAP')
fmt = BASE/'fmt'; fmt.mkdir(exist_ok=True)
(fmt/'fmtConfigVersion.cmake').write_text('set(PACKAGE_VERSION "10.1.0")\nset(PACKAGE_VERSION_COMPATIBLE TRUE)\n')
(fmt/'fmtConfig.cmake').write_text('add_library(fmt::fmt-header-only INTERFACE IMPORTED)\nset_target_properties(fmt::fmt-header-only PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "'+str(ROOT/'blender-5.2.0/extern/fmt/include')+'" INTERFACE_COMPILE_DEFINITIONS "FMT_HEADER_ONLY;FMT_CONSTEVAL=;FMT_USE_CONSTEXPR=0;FMT_USE_NONTYPE_TEMPLATE_ARGS=0")\n')
oiio=source('OPENIMAGEIO')
platform=oiio/'src/libutil/sysutil.cpp'; text=platform.read_text()
text=text.replace('#include <thread>', '#include <thread>\n#ifdef __EMSCRIPTEN__\n# include <unistd.h>\n#endif',1) if '#ifdef __EMSCRIPTEN__' not in text else text
text=text.replace('#elif defined(__GNU__) || defined(__OpenBSD__) || defined(_WIN32)', '#elif defined(__GNU__) || defined(__OpenBSD__) || defined(_WIN32) || defined(__EMSCRIPTEN__)')
text=text.replace('# include <unistd.h>', '# include <unistd.h>\n# include <emscripten/heap.h>',1) if '<emscripten/heap.h>' not in text else text
text=text.replace('Sysutil::physical_memory()\n{\n#if defined(__linux__)', 'Sysutil::physical_memory()\n{\n#if defined(__EMSCRIPTEN__)\n return emscripten_get_heap_max();\n#elif defined(__linux__)')
platform.write_text(text)
strings=oiio/'src/libutil/strutil.cpp'; text=strings.read_text()
text=text.replace('|| defined(__GLIBC__)\n', '|| defined(__GLIBC__) || defined(__EMSCRIPTEN__)\n')
strings.write_text(text.replace('|| defined(__GLIBC__) || defined(__NetBSD__)\n', '|| defined(__GLIBC__) || defined(__NetBSD__) || defined(__EMSCRIPTEN__)\n'))
build('oiio',oiio,[
'-DCMAKE_PROJECT_INCLUDE='+str(ROOT/'tools/web/oiio-wasm-dependencies.cmake'),
'-Dfmt_DIR='+str(fmt), '-DROBINMAP_INCLUDE_DIR='+str(robin/'include'),
'-DOIIO_BUILD_TOOLS=OFF','-DOIIO_BUILD_TESTS=OFF','-DUSE_PYTHON=OFF','-DINSTALL_DOCS=OFF','-DEMBEDPLUGINS=ON',
'-DOpenImageIO_BUILD_MISSING_DEPS=none','-DUSE_SIMD=0','-DUSE_TBB=OFF','-DUSE_OPENCOLORIO=ON',
'-DUSE_FFMPEG=OFF','-DUSE_OPENVDB=OFF','-DUSE_FREETYPE=OFF','-DUSE_OPENCV=OFF','-DUSE_OPENJPEG=OFF','-DUSE_WEBP=OFF',
'-DUSE_JXL=OFF','-DUSE_LIBHEIF=OFF','-DUSE_LIBRAW=OFF','-DUSE_LIBUHDR=OFF','-DUSE_PTEX=OFF','-DUSE_DCMTK=OFF'])
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Build Blender's pinned OpenEXR with the existing WASM Imath installation."""
import hashlib
import re
import shutil
import subprocess
import tarfile
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
BASE = ROOT / 'build_web_openexr'
IMATH = ROOT / 'build_web_ocio/install'
def main():
versions = (ROOT / 'blender-5.2.0/build_files/build_environment/cmake/versions.cmake').read_text()
value = lambda name: re.search(r'set\(OPENEXR_' + name + r' ([^)]+)\)', versions)[1]
version, expected, algorithm = value('VERSION'), value('HASH'), value('HASH_TYPE').lower()
if not (IMATH / 'lib/cmake/Imath/ImathConfig.cmake').is_file():
raise SystemExit('Reuse/build the Web OCIO Imath installation first')
BASE.mkdir(exist_ok=True)
archive = BASE / ('openexr-' + version + '.tar.gz')
if not archive.is_file():
with urllib.request.urlopen('https://codeload.github.com/AcademySoftwareFoundation/openexr/tar.gz/refs/tags/v' + version, timeout=60) as response:
data = response.read()
if hashlib.new(algorithm, data).hexdigest() != expected:
raise SystemExit('OpenEXR download differs from Blender pinned source hash')
archive.write_bytes(data)
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != expected:
raise SystemExit('OpenEXR archive differs from Blender pinned source hash')
source = BASE / ('openexr-' + version)
if not (source / 'CMakeLists.txt').is_file():
with tarfile.open(archive) as package: package.extractall(BASE, filter='data')
toolchain = ROOT / '.emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake'
subprocess.run(['cmake', '-S', str(source), '-B', str(BASE / 'build'), '-G', 'Ninja',
'-DCMAKE_TOOLCHAIN_FILE=' + str(toolchain), '-DCMAKE_BUILD_TYPE=Release',
'-DCMAKE_INSTALL_PREFIX=' + str(BASE / 'install'), '-DCMAKE_INSTALL_LIBDIR=lib',
'-DCMAKE_PREFIX_PATH=' + str(IMATH), '-DCMAKE_FIND_ROOT_PATH=' + str(IMATH),
'-DImath_DIR=' + str(IMATH / 'lib/cmake/Imath'), '-DCMAKE_CXX_FLAGS=-fexceptions',
'-DCMAKE_C_FLAGS=', '-DBUILD_SHARED_LIBS=OFF', '-DBUILD_TESTING=OFF',
'-DOPENEXR_ENABLE_THREADING=OFF', '-DBUILD_WEBSITE=OFF', '-DOPENEXR_INSTALL_DOCS=OFF',
'-DOPENEXR_BUILD_TOOLS=OFF', '-DOPENEXR_BUILD_EXAMPLES=OFF', '-DOPENEXR_FORCE_INTERNAL_DEFLATE=ON'], cwd=ROOT, check=True)
subprocess.run(['cmake', '--build', str(BASE / 'build'), '--target', 'install', '--parallel', '8'], cwd=ROOT, check=True)
shutil.copy2(BASE / 'build/external/OpenJPH/src/core/libopenjph.a', BASE / 'install/lib/libopenjph.a')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Build the local desktop-version CPython archive for the WebEngine memory."""
import argparse
import datetime
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import zipfile
ROOT = Path(__file__).resolve().parents[2]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path, default=Path('/home/mes123456/working/build_oiio_deps/build/python/src/external_python'))
args = parser.parse_args()
base = ROOT / 'build_web_python313'
source, build = base / 'source', base / 'build'
report_dir = ROOT / 'tests/golden/WBV2-P1424-001/embedded-python-build' / datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
report_dir.mkdir(parents=True)
report = dict(status='BUILDING', validation='NOT_RUN', source=str(args.source), steps=[])
try:
patchlevel = (args.source / 'Include/patchlevel.h').read_text()
if '#define PY_VERSION "3.13.13"' not in patchlevel:
raise ValueError('The embedded runtime requires the supplied Blender CPython 3.13.13 source')
report['version'] = '3.13.13'
report['sourceIdentity'] = {name: hashlib.sha256((args.source / name).read_bytes()).hexdigest()
for name in ['Include/patchlevel.h', 'configure', 'LICENSE']}
if not source.exists():
shutil.copytree(args.source, source, ignore=shutil.ignore_patterns(
'*.o', '*.a', '*.so', '*.so.*', '*.pyc', '__pycache__', '.git', 'build',
'python', 'Makefile', 'pyconfig.h', 'config.log', 'config.status', 'config.cache',
'pybuilddir.txt', '_bootstrap_python', '_freeze_module', 'Setup.local', 'Setup.stdlib', 'Setup.bootstrap', 'config.c'))
# Keep the copied upstream source reproducible without rebuilding or
# replacing the archive on every implementation cycle.
if 'web_engine_python_unraisable' not in (source / 'Python/errors.c').read_text():
subprocess.run(['patch', '-p1', '-i',
str(ROOT / 'tools/web/patches/cpython-3.13-webengine-unraisable.patch')],
cwd=source, check=True)
build.mkdir(parents=True, exist_ok=True)
build_python = ROOT / '.deps/native/python/bin/python3.13'
env = dict(os.environ)
env['CFLAGS'] = (env.get('CFLAGS', '') + ' -DPY_CALL_TRAMPOLINE=1').strip()
env['LD_LIBRARY_PATH'] = str(ROOT / '.deps/native/python/lib') + ':' + env.get('LD_LIBRARY_PATH', '')
env['PYTHONHOME'] = str(ROOT / '.deps/native/python')
def run(name, command):
with (report_dir / (name + '.log')).open('w') as log:
result = subprocess.run(command, cwd=ROOT, env=env, stdout=log, stderr=subprocess.STDOUT)
report['steps'].append(dict(name=name, exitCode=result.returncode))
if result.returncode:
raise RuntimeError(name + ' failed; see ' + str(report_dir))
if not (build / 'Makefile').exists() or 'PY_CALL_TRAMPOLINE' not in (build / 'Makefile').read_text():
# Shell arguments stay positional; source paths are never interpolated as shell code.
run('configure', ['bash', '-c',
'source tools/web/emscripten-env.sh && cd "$1" && CONFIG_SITE="$2/Tools/wasm/config.site-wasm32-emscripten" emconfigure "$2/configure" --host=wasm32-unknown-emscripten --build=x86_64-pc-linux-gnu --with-emscripten-target=browser --disable-wasm-pthreads --disable-wasm-dynamic-linking --disable-shared --without-ensurepip --disable-test-modules --prefix=/python --with-build-python="$3"',
'web-python-build', str(build), str(source), str(build_python)])
configuration = build / 'pyconfig.h'
if '#define PY_CALL_TRAMPOLINE 1' not in configuration.read_text():
configuration.write_text(configuration.read_text() + '\n#define PY_CALL_TRAMPOLINE 1\n')
frozen = build / 'Python/frozen_modules/getpath.h'
frozen.parent.mkdir(parents=True, exist_ok=True)
if not frozen.exists():
run('freeze-getpath', [str(build_python), str(source / 'Programs/_freeze_module.py'),
'getpath', str(source / 'Modules/getpath.py'), str(frozen)])
run('archive', ['bash', '-c', 'source tools/web/emscripten-env.sh && emmake make -C "$1" -j4 libpython3.13.a', 'web-python-build', str(build)])
stdlib = base / 'python313.zip'
with zipfile.ZipFile(stdlib, 'w', compression=zipfile.ZIP_STORED) as archive:
for file in sorted((source / 'Lib').rglob('*.py')):
relative = file.relative_to(source / 'Lib')
if any(part in {'test', 'tests', '__pycache__', 'idlelib', 'tkinter', 'turtledemo', 'ensurepip'} for part in relative.parts):
continue
archive.write(file, relative.as_posix())
report['artifacts'] = {}
for file in [build / 'libpython3.13.a', build / 'pyconfig.h', stdlib,
build / 'Modules/expat/libexpat.a', build / 'Modules/_hacl/libHacl_Hash_SHA2.a',
build / 'Modules/_decimal/libmpdec/libmpdec.a']:
data = file.read_bytes()
report['artifacts'][str(file.relative_to(ROOT))] = dict(bytes=len(data), sha256=hashlib.sha256(data).hexdigest())
report['status'] = 'BUILT_UNVERIFIED'
license_dir = ROOT / 'web/app/public/vendor/blender'
license_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(source / 'LICENSE', license_dir / 'CPYTHON-LICENSE.txt')
except BaseException as error:
report.update(status='BUILD_INCOMPLETE', error=str(error))
raise
finally:
(report_dir / 'build.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps(dict(directory=str(report_dir), **report)), flush=True)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""Build pinned libsndfile/Xiph/MPEG codecs for native WebEngine audio."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess
import shutil
import tarfile
import urllib.request
ROOT = Path(__file__).resolve().parents[2]
VERSION = "1.2.2"
# Blender build_files/build_environment/cmake/versions.cmake.
ARCHIVE_MD5 = "04e2e6f726da7c5dc87f8cf72f250d04"
# Debian's unmodified upstream tag archive of the same version (different
# packaging from the upstream release tarball), retained for offline reuse.
TAG_ARCHIVE_SHA256 = "ffe12ef8add3eaca876f04087734e6e8e029350082f3251f565fa9da55b52121"
# Same releases and digests as Blender's versions.cmake. All algorithms remain
# in the upstream native libraries; no browser-side encoding implementation.
CODECS = (
("ogg", "libogg-1.3.5", ".tar.gz",
"0eb4b4b9420a0f51db142ba3f9c64b333f826532dc0f48c6410ae51f4799b664", []),
("vorbis", "libvorbis-1.3.7", ".tar.gz",
"0e982409a9c3fc82ee06e08205b1355e5c6aa4c36bca58146ef399621b0ce5ab", []),
("flac", "flac-1.4.2", ".tar.xz",
"e322d58a1f48d23d9dd38f432672865f6f79e73a6f9cc5a5f57fcaa83eb5a8e4",
["-DBUILD_CXXLIBS=OFF", "-DBUILD_PROGRAMS=OFF", "-DBUILD_EXAMPLES=OFF",
"-DBUILD_DOCS=OFF", "-DBUILD_MANPAGES=OFF", "-DWITH_OGG=ON"]),
("opus", "opus-1.3.1", ".tar.gz",
"65b58e1e25b2a114157014736a3d9dfeaad8d41be1c8179866f144a2fb44ff9d",
["-DOPUS_BUILD_PROGRAMS=OFF", "-DOPUS_BUILD_TESTING=OFF",
"-DOPUS_INSTALL_PKG_CONFIG_MODULE=ON", "-DOPUS_STACK_PROTECTOR=OFF"]),
)
# LAME matches Blender's versions.cmake. Blender uses FFmpeg for decoding and
# does not pin mpg123: pin the original upstream archive required by libsndfile
# separately. Digest from Debian's mpg123_1.32.10-1+deb13u1.dsc source metadata.
MPEG_CODECS = (
("lame-3.100", ".tar.gz", "md5", "83e260acbe4389b54fe08e0bdbf7cddb",
"https://deb.debian.org/debian/pool/main/l/lame/lame_3.100.orig.tar.gz",
["--disable-frontend", "--disable-decoder", "--disable-analyzer-hooks", "--disable-nasm"]),
("mpg123-1.32.10", ".tar.xz", "sha256",
"98867a796fa1eeb161847d211734d585ff61c56712fbc333de416fea7b59de29",
"https://deb.debian.org/debian/pool/main/m/mpg123/mpg123_1.32.10.orig.tar.xz",
["--with-cpu=generic", "--disable-components", "--enable-libmpg123", "--disable-network"]),
)
def build_mpeg(base, prefix, jobs, env):
emscripten = ROOT / ".emsdk/upstream/emscripten"
for release, suffix, algorithm, digest, url, options in MPEG_CODECS:
source = base / release
if not (source / "configure").is_file():
filename = release + suffix
cached = [base / filename, Path.home() / "working/packages" / filename,
Path.home() / "working/build_oiio_deps/downloads" / filename]
archive = next((path for path in cached if path.is_file()), None)
if archive is None:
archive = cached[0]
partial = archive.with_suffix(".partial")
with urllib.request.urlopen(url, timeout=60) as response, partial.open("wb") as out:
shutil.copyfileobj(response, out)
partial.replace(archive)
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != digest:
raise RuntimeError(f"Pinned {release} archive digest mismatch")
with tarfile.open(archive) as package:
package.extractall(base, filter="data")
build = base / (release + "-build")
build.mkdir(exist_ok=True)
if release.startswith("lame-"):
# LAME 3.100's 2017 GNU config helper predates Emscripten. Reuse
# the helper shipped with our embedded CPython, not a fake host
# triplet which could enable native CPU assembly or ELF tooling.
shutil.copy2(ROOT / "build_web_python313/source/config.sub", source / "config.sub")
configure = [str(emscripten / "emconfigure"), str(source / "configure"),
"--host=wasm32-unknown-emscripten", f"--prefix={prefix}",
"--disable-shared", "--enable-static", *options]
codec_env = dict(env, CFLAGS="-O2 -fPIC", CXXFLAGS="-O2 -fPIC")
# Autoconf source lists remain upstream-owned; only rerun configuration
# after build inputs change, leaving existing codec objects reusable.
settings = json.dumps([configure, codec_env["CFLAGS"], codec_env["CXXFLAGS"]])
stamp = build / "web-configure.json"
if not stamp.is_file() or stamp.read_text() != settings:
subprocess.run(configure, cwd=build, env=codec_env, check=True)
stamp.write_text(settings)
subprocess.run([str(emscripten / "emmake"), "make", f"-j{jobs}", "install"],
cwd=build, env=codec_env, check=True)
def build_codecs(base, prefix, jobs, common, env):
for family, release, suffix, digest, options in CODECS:
source = base / release
if not (source / "CMakeLists.txt").is_file():
filename = release + suffix
cached = [base / filename, Path.home() / "working/packages" / filename]
archive = next((path for path in cached if path.is_file()), None)
if archive is None:
archive = cached[0]
partial = archive.with_suffix(".partial")
url = f"https://downloads.xiph.org/releases/{family}/{filename}"
with urllib.request.urlopen(url, timeout=60) as response, partial.open("wb") as out:
shutil.copyfileobj(response, out)
partial.replace(archive)
if hashlib.sha256(archive.read_bytes()).hexdigest() != digest:
raise RuntimeError(f"Blender-pinned {release} archive digest mismatch")
with tarfile.open(archive) as package:
package.extractall(base, filter="data")
if family == "opus" and not (source / "opus_buildtype.cmake").is_file():
# The pinned 1.3.1 release omits this build-type defaults helper.
# We set CMAKE_BUILD_TYPE explicitly. Keep the original source list
# and configuration; only make the absent defaults include optional.
cmake = source / "CMakeLists.txt"
cmake.write_text(cmake.read_text().replace(
"include(opus_buildtype.cmake)",
"include(opus_buildtype.cmake OPTIONAL)"))
build = base / (family + "-build")
subprocess.run(["cmake", "-S", str(source), "-B", str(build), *common,
"-DBUILD_TESTING=OFF", *options], cwd=ROOT, env=env, check=True)
subprocess.run(["cmake", "--build", str(build), "--target", "install",
"--parallel", str(jobs)], cwd=ROOT, env=env, check=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path)
parser.add_argument("--archive", type=Path)
parser.add_argument("--jobs", type=int, default=3)
args = parser.parse_args()
base = ROOT / "build_web_sndfile"
base.mkdir(exist_ok=True)
source = args.source
if source is not None and not (source / "CMakeLists.txt").is_file():
raise FileNotFoundError(f"No libsndfile source at {source}")
if source is None:
local = (Path.home() / "working/build_oiio_deps/build/sndfile/src/"
"external_sndfile")
source = local if (local / "CMakeLists.txt").is_file() else base / f"libsndfile-{VERSION}"
if not (source / "CMakeLists.txt").is_file():
if args.archive:
archive = args.archive
else:
cached = [Path.home() / f"working/packages/libsndfile-{VERSION}.tar.xz",
base / f"libsndfile-{VERSION}.tar.xz",
base / f"libsndfile_{VERSION}.orig.tar.gz"]
archive = next((path for path in cached if path.is_file()), None)
if archive is None:
archive = cached[-1]
url = ("https://deb.debian.org/debian/pool/main/libs/libsndfile/"
f"libsndfile_{VERSION}.orig.tar.gz")
partial = archive.with_suffix(".partial")
with urllib.request.urlopen(url, timeout=45) as response, partial.open("wb") as out:
shutil.copyfileobj(response, out)
partial.replace(archive)
contents = archive.read_bytes()
if (hashlib.md5(contents).hexdigest() != ARCHIVE_MD5 and
hashlib.sha256(contents).hexdigest() != TAG_ARCHIVE_SHA256):
raise RuntimeError("Blender-pinned libsndfile archive digest mismatch")
with tarfile.open(archive) as package:
package.extractall(base, filter="data")
source = base / f"libsndfile-{VERSION}"
prefix = base / "install"
common = [
"-G", "Ninja",
"-DCMAKE_TOOLCHAIN_FILE=" + str(ROOT / ".emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"),
"-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_PREFIX=" + str(prefix),
"-DEMSCRIPTEN_SYSTEM_PROCESSOR=wasm32",
"-DCMAKE_INSTALL_LIBDIR=lib", "-DBUILD_SHARED_LIBS=OFF",
"-DCMAKE_PREFIX_PATH=" + str(prefix),
"-DCMAKE_FIND_ROOT_PATH=" + str(prefix),
]
# Never let host pkg-config select ELF libraries for a WASM link.
env = dict(os.environ, PKG_CONFIG_LIBDIR=str(prefix / "lib/pkgconfig"),
PKG_CONFIG_PATH=str(prefix / "lib/pkgconfig"))
build_codecs(base, prefix, args.jobs, common, env)
build_mpeg(base, prefix, args.jobs, env)
# libsndfile 1.2.2 only exposes Vorbis quality, although libvorbis already
# implements managed bitrate encoding. Keep this native extension in a
# reproducible source patch and install its command declaration with the
# archive, so Audaspace cannot silently use an unpatched system header.
marker = "SFC_WEB_SET_VORBIS_BITRATE"
patched_header = marker in (source / "include/sndfile.h").read_text()
patched_encoder = marker in (source / "src/ogg_vorbis.c").read_text()
if patched_header != patched_encoder:
raise RuntimeError("Incomplete WebEngine Vorbis patch in libsndfile source")
if not patched_header:
subprocess.run(["patch", "--batch", "--forward", "-p1", "-i",
str(ROOT / "tools/web/patches/libsndfile-vorbis-bitrate.patch")],
cwd=source, check=True)
configure = [
"cmake", "-S", str(source), "-B", str(base / "build"), *common,
"-DBUILD_PROGRAMS=OFF", "-DBUILD_EXAMPLES=OFF", "-DBUILD_TESTING=OFF",
"-DENABLE_EXTERNAL_LIBS=ON", "-DENABLE_MPEG=ON",
"-DINSTALL_MANPAGES=OFF", "-DINSTALL_PKGCONFIG_MODULE=ON",
]
# Pin every find-module input, including entries cached by the old PCM build.
for library, variable in (("ogg", "OGG"), ("vorbis", "Vorbis_Vorbis"),
("vorbisenc", "Vorbis_Enc"), ("vorbisfile", "Vorbis_File"),
("FLAC", "FLAC"), ("opus", "OPUS"),
("mp3lame", "MP3LAME"), ("mpg123", "mpg123")):
configure += [f"-D{variable}_LIBRARY={prefix}/lib/lib{library}.a",
f"-D{variable}_INCLUDE_DIR={prefix}/include"]
subprocess.run(configure, cwd=ROOT, env=env, check=True)
subprocess.run(["cmake", "--build", str(base / "build"), "--target", "install",
"--parallel", str(args.jobs)], cwd=ROOT, env=env, check=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""Build pinned native dependencies for CacheFile USD_create_handle in WebEngine.
The TBB stage is independently resumable. This builds the OpenUSD core and
original static pxr Python extensions, not the Blender reader.
No tests or application candidates are built; existing objects are retained.
"""
import argparse
import hashlib
import os
from pathlib import Path
import re
import subprocess
import tarfile
import urllib.request
import zipfile
ROOT = Path(__file__).resolve().parents[2]
BASE = ROOT / "build_web_usd"
INSTALL = BASE / "install"
TOOLCHAIN = ROOT / ".emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"
VERSIONS = ROOT / "blender-5.2.0/build_files/build_environment/cmake/versions.cmake"
def source_for(name, repository, directory):
versions = VERSIONS.read_text()
def value(suffix):
return re.search(r"set\(" + name + "_" + suffix + r" ([^)]+)\)", versions)[1]
version = value("VERSION")
tag = version if version.startswith("v") else "v" + version
filename = ("oneTBB-" if name == "TBB" else "usd-") + tag + ".tar.gz"
archive = BASE / filename
expected = value("HASH")
algorithm = value("HASH_TYPE").lower()
if not archive.exists():
with urllib.request.urlopen(
"https://codeload.github.com/" + repository + "/tar.gz/refs/tags/" + tag,
timeout=60,
) as response:
data = response.read()
if hashlib.new(algorithm, data).hexdigest() != expected:
raise SystemExit(name + " download differs from Blender's pinned hash")
archive.write_bytes(data)
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != expected:
raise SystemExit(name + " archive differs from Blender's pinned hash")
source = BASE / (directory + "-" + version.removeprefix("v"))
if not (source / "CMakeLists.txt").exists():
with tarfile.open(archive) as package:
package.extractall(BASE, filter="data")
return source, version
def configure(source, build, options):
env = os.environ.copy()
host_python = ROOT / ".deps/native/python"
env["LD_LIBRARY_PATH"] = str(host_python / "lib") + ":" + env.get("LD_LIBRARY_PATH", "")
subprocess.run([
"cmake", "-S", str(source), "-B", str(build), "-G", "Ninja",
"-DCMAKE_TOOLCHAIN_FILE=" + str(TOOLCHAIN),
"-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_LIBDIR=lib",
"-DCMAKE_INSTALL_PREFIX=" + str(INSTALL),
"-DCMAKE_CXX_FLAGS=-fexceptions -fsigned-char",
"-DBUILD_SHARED_LIBS=OFF", *options,
], cwd=ROOT, env=env, check=True)
def build_target(build, target):
subprocess.run(["cmake", "--build", str(build), "--target", target,
"--parallel", "8"], cwd=ROOT, check=True)
def adapt_web_source(source):
# These adaptations preserve upstream behavior and ABI assertions. They
# must also be present in installed headers consumed by Blender.
edits = {
"pxr/base/tf/pyObjWrapper.h": [
('#include "pxr/base/arch/pragmas.h"',
'#include "pxr/base/arch/pragmas.h"\n\n#include <memory>'),
('static constexpr std::size_t Size = 16;\n'
' static constexpr std::size_t Align = 8;',
'// Match the native shared_ptr member on wasm32 as well as 64-bit hosts.\n'
' static constexpr std::size_t Size = sizeof(std::shared_ptr<void>);\n'
' static constexpr std::size_t Align = alignof(std::shared_ptr<void>);'),
],
"cmake/defaults/gccclangshareddefaults.cmake": [
('if(CMAKE_USE_PTHREADS_INIT)\n',
'if(CMAKE_USE_PTHREADS_INIT AND NOT (EMSCRIPTEN AND EMSCRIPTEN_WITHOUT_PTHREAD))\n'),
],
"cmake/macros/Private.cmake": [
('if(NOT TARGET shared_libs)\n message(STATUS "Skipping Python module',
'if(NOT TARGET shared_libs AND NOT PXR_WEB_STATIC_PYTHON)\n message(STATUS "Skipping Python module'),
(' # Add the module target.\n add_library(${LIBRARY_NAME}\n SHARED',
' # Embedded CPython loads original wrappers through its builtin table.\n'
' set(pythonLibraryType SHARED)\n'
' if(PXR_WEB_STATIC_PYTHON)\n'
' set(pythonLibraryType STATIC)\n'
' endif()\n'
' # Add the module target.\n add_library(${LIBRARY_NAME}\n ${pythonLibraryType}'),
(' # Accumulate Python module names.\n',
' if(PXR_WEB_STATIC_PYTHON)\n'
' set_property(GLOBAL APPEND PROPERTY PXR_WEB_PYTHON_MODULES\n'
' "${pyModuleName}|${LIBRARY_NAME}")\n'
' endif()\n\n # Accumulate Python module names.\n'),
(' TARGETS ${LIBRARY_NAME}\n LIBRARY DESTINATION ${libInstallPrefix}',
' TARGETS ${LIBRARY_NAME}\n ARCHIVE DESTINATION ${libInstallPrefix}\n'
' LIBRARY DESTINATION ${libInstallPrefix}'),
],
"pxr/CMakeLists.txt": [
('pxr_core_epilogue()\n',
'pxr_core_epilogue()\n\n'
'include("${PROJECT_SOURCE_DIR}/cmake/macros/WebStaticPython.cmake")\n'),
],
"cmake/macros/Public.cmake": [
('if(TARGET shared_libs AND PXR_ENABLE_PYTHON_SUPPORT)',
'if((TARGET shared_libs OR PXR_WEB_STATIC_PYTHON) AND PXR_ENABLE_PYTHON_SUPPORT)'),
],
}
for relative, replacements in edits.items():
path = source / relative
text = path.read_text()
original = text
for before, after in replacements:
if after in text:
continue
if text.count(before) != 1:
raise SystemExit("OpenUSD source differs at " + relative)
text = text.replace(before, after, 1)
if text != original:
path.write_text(text)
helper = source / "cmake/macros/WebStaticPython.cmake"
contents = (ROOT / "tools/web/openusd-static-python.cmake").read_text()
if not helper.exists() or helper.read_text() != contents:
helper.write_text(contents)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--stage", choices=("tbb", "usd", "all"), default="all")
parser.add_argument("--usd-target", default="install",
help="Original OpenUSD target; install includes schema/plugin resources")
args = parser.parse_args()
BASE.mkdir(exist_ok=True)
if args.stage in ("tbb", "all"):
source, _ = source_for("TBB", "uxlfoundation/oneTBB", "oneTBB")
build = BASE / "tbb-build"
configure(source, build, [
"-DEMSCRIPTEN_WITHOUT_PTHREAD=ON", "-DTBB_TEST=OFF",
"-DTBB_STRICT=OFF", "-DTBBMALLOC_BUILD=OFF",
"-DTBBMALLOC_PROXY_BUILD=OFF", "-DTBB_ENABLE_IPO=OFF",
])
build_target(build, "install")
if args.stage in ("usd", "all"):
if not (INSTALL / "lib/cmake/TBB/TBBConfig.cmake").exists():
raise SystemExit("Build the pinned Web TBB stage first")
source, version = source_for("USD", "PixarAnimationStudios/OpenUSD", "OpenUSD")
adapt_web_source(source)
python = ROOT / "build_web_python313"
if not (python / "build/libpython3.13.a").exists():
raise SystemExit("Reuse/build the embedded Web CPython 3.13 archive first")
# FindPython expects pyconfig.h beside Python.h. Preserve CPython's
# separate source/build trees with a local header overlay, using the
# WASM configuration rather than the host interpreter's ABI.
python_include = BASE / "python-include"
python_include.mkdir(exist_ok=True)
for header in (python / "source/Include").iterdir():
destination = python_include / header.name
if not destination.exists():
destination.symlink_to(header, target_is_directory=header.is_dir())
config = python_include / "pyconfig.h"
if not config.exists():
config.symlink_to(python / "build/pyconfig.h")
build = BASE / "usd-build"
configure(source, build, [
"-DCMAKE_CXX_FLAGS=-fexceptions -fsigned-char -I" + str(python / "build"),
"-DCMAKE_PREFIX_PATH=" + str(INSTALL),
"-DCMAKE_FIND_ROOT_PATH=" + str(INSTALL),
"-DTBB_DIR=" + str(INSTALL / "lib/cmake/TBB"),
"-DOPENSUBDIV_ROOT_DIR=" + str(ROOT / "blender-5.2.0/extern/opensubdiv-source"),
"-DOPENSUBDIV_OSDCPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdCPU.a"),
"-DOPENSUBDIV_OSDGPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdGPU.a"),
"-DEMSCRIPTEN_WITHOUT_PTHREAD=ON",
# OpenUSD retains static registration through LINK_LIBRARY.
# Emscripten/wasm-ld supports these flags, but CMake 3.28's
# Emscripten platform does not declare the feature itself.
"-DCMAKE_LINK_LIBRARY_USING_WHOLE_ARCHIVE_SUPPORTED=TRUE",
"-DCMAKE_LINK_LIBRARY_USING_WHOLE_ARCHIVE=-Wl,--whole-archive;<LINK_ITEM>;-Wl,--no-whole-archive",
"-DPXR_FIND_TBB_IN_CONFIG=ON", "-DPXR_BUILD_MONOLITHIC=ON",
"-DPXR_SET_INTERNAL_NAMESPACE=pxrBlender_v" + version.replace(".", "_"),
"-DPXR_ENABLE_PYTHON_SUPPORT=ON",
"-DPXR_WEB_STATIC_PYTHON=ON",
"-DPython3_EXECUTABLE=" + str(ROOT / ".deps/native/python/bin/python3.13"),
"-DPython3_INCLUDE_DIR=" + str(python_include),
# CMake 3.28 validates its previous internal include path before
# applying an updated public path during incremental configure.
"-D_Python3_INCLUDE_DIR=" + str(python_include),
"-DPython3_LIBRARY=" + str(python / "build/libpython3.13.a"),
"-DPXR_PY_UNDEFINED_DYNAMIC_LOOKUP=ON",
"-DPXR_BUILD_TESTS=OFF", "-DPXR_BUILD_EXAMPLES=OFF",
"-DPXR_BUILD_TUTORIALS=OFF", "-DPXR_BUILD_USD_TOOLS=OFF",
"-DPXR_BUILD_USDVIEW=OFF",
# Blender's Web USD reader consumes the native shape adapters.
# Reuse the existing Emscripten OpenSubdiv package from the main
# build tree; do not silently disable imaging and leave headers out
# of the install set.
"-DPXR_BUILD_IMAGING=ON",
"-DPXR_BUILD_USD_IMAGING=ON",
"-DOPENSUBDIV_INCLUDE_DIR=" + str(ROOT / "blender-5.2.0/extern/opensubdiv-source"),
"-DOPENSUBDIV_CPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdCPU.a"),
"-DOPENSUBDIV_GPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdGPU.a"),
"-DPXR_BUILD_USD_VALIDATION=OFF",
"-DPXR_BUILD_EXEC=OFF", "-DPXR_ENABLE_GL_SUPPORT=OFF",
"-DPXR_ENABLE_VULKAN_SUPPORT=OFF", "-DPXR_ENABLE_OPENVDB_SUPPORT=OFF",
"-DPXR_ENABLE_MATERIALX_SUPPORT=OFF", "-DPXR_ENABLE_OSL_SUPPORT=OFF",
"-DPXR_BUILD_DOCUMENTATION=OFF", "-DPXR_VALIDATE_GENERATED_CODE=OFF",
])
build_target(build, args.usd_target)
if args.usd_target == "install":
# Only Python resources belong in the embedded package. Static
# wrapper archives in lib/python are linked, never shipped as data.
package = INSTALL / "lib/python"
output = INSTALL / "lib/webengine-pxr.zip"
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive:
for path in sorted(package.rglob("*.py")):
archive.write(path, path.relative_to(package))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,63 @@
"""Capture one native GUI quit plus independent background readback; never claim release acceptance."""
import argparse, datetime, hashlib, json, os, pathlib, shutil, signal, subprocess
ROOT = pathlib.Path(__file__).resolve().parents[2]
parser = argparse.ArgumentParser()
parser.add_argument("--blender", default=str(ROOT / "build_blender_native/bin/blender"))
parser.add_argument("--input", required=True); parser.add_argument("--output", required=True)
parser.add_argument("--timeout", type=int, default=300)
args = parser.parse_args(); native_binary = pathlib.Path(args.blender).resolve(); asset_input = pathlib.Path(args.input).resolve(); output = pathlib.Path(args.output).resolve()
def sha(path):
digest = hashlib.sha256()
with pathlib.Path(path).open("rb") as stream:
for chunk in iter(lambda: stream.read(1048576), b""): digest.update(chunk)
return digest.hexdigest()
producer = ROOT / "web/tests/fixtures/native-last-session-prepare.py"; reader = ROOT / "web/tests/fixtures/native-last-session-readback.py"
companion = asset_input.with_name("ExternalImage.png")
assert 0 < args.timeout <= 1800 and asset_input.suffix.lower() == ".blend" and all(path.is_file() for path in (native_binary, asset_input, companion, producer, reader)) and os.access(native_binary, os.X_OK), "Native executable, source fixtures, input blend and original image companion are required"
environment = dict(os.environ); gui_prefix = []
if not (environment.get("DISPLAY") or environment.get("WAYLAND_DISPLAY")):
xvfb = shutil.which("xvfb-run"); assert xvfb, "GUI quit capture requires a live display or existing xvfb-run"
gui_prefix = [xvfb, "-a"]; environment.setdefault("LIBGL_ALWAYS_SOFTWARE", "1")
output.mkdir(parents=True, exist_ok=False); assets = output / "assets"; assets.mkdir()
working_input = assets / asset_input.name; shutil.copy2(asset_input, working_input); shutil.copy2(companion, assets / companion.name)
work = output / "native-work"; receipt_path = output / "desktop-baseline.json"
state = {"schemaVersion": 1, "kind": "desktop-native-quit-baseline", "phase": "PREPARED", "acceptance": "NOT_CLAIMED", "workDir": str(work), "nativeBinary": {"path": str(native_binary), "sha256": sha(native_binary)}, "sources": {name: {"path": str(path), "sha256": sha(path)} for name, path in (("producer", producer), ("reader", reader), ("capture", pathlib.Path(__file__).resolve()))}, "input": {"source": str(asset_input), "path": str(working_input), "sha256": sha(working_input), "companion": {"source": str(companion), "path": str(assets / companion.name), "sha256": sha(assets / companion.name)}}, "steps": {}}
def persist():
temporary = receipt_path.with_suffix(".tmp"); temporary.write_text(json.dumps(state, indent=2) + "\n"); temporary.replace(receipt_path)
def terminate_owned(process):
for sig, grace in ((signal.SIGTERM, 5), (signal.SIGKILL, 5)):
try: os.killpg(process.pid, sig)
except ProcessLookupError: pass
try: process.wait(timeout=grace); return
except subprocess.TimeoutExpired: continue
state["cleanupPendingPid"] = process.pid; persist(); raise RuntimeError(f"Owned process {process.pid} remains live after bounded cleanup")
def run_step(label, command):
log = output / f"{label}.log"; step = {"command": command, "log": str(log), "startedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), "timeoutSeconds": args.timeout}; state["steps"][label] = step; state["phase"] = label.upper() + "_RUNNING"; persist()
with log.open("w") as stream:
process = subprocess.Popen(command, cwd=ROOT, env=environment, stdout=stream, stderr=subprocess.STDOUT, start_new_session=True)
step["pid"] = process.pid; persist()
try: step["exitCode"] = process.wait(timeout=args.timeout)
except subprocess.TimeoutExpired:
step["timedOut"] = True; terminate_owned(process); step["exitCode"] = process.returncode
except BaseException:
terminate_owned(process); raise
step["finishedAt"] = datetime.datetime.now(datetime.timezone.utc).isoformat(); step["logSha256"] = sha(log); persist()
if step.get("timedOut") or step["exitCode"] != 0: raise RuntimeError(f"{label} failed or timed out; see {log}")
def capture():
persist()
run_step("gui-producer", gui_prefix + [str(native_binary), "--factory-startup", "--disable-autoexec", "--python-exit-code", "1", "--python", str(producer), "--", "--input", str(working_input), "--work-dir", str(work)])
quit_file = work / "native-temp/quit.blend"; assert quit_file.is_file() and quit_file.stat().st_size > 0, "GUI exit did not produce quit.blend"; state["quitFile"] = {"path": str(quit_file), "bytes": quit_file.stat().st_size, "sha256": sha(quit_file)}; persist()
run_step("background-reader", [str(native_binary), "--background", "--factory-startup", "--disable-autoexec", "--python-exit-code", "1", "--python", str(reader), "--", "--work-dir", str(work)])
readback_path = work / "native-last-session-readback.json"; readback = json.loads(readback_path.read_text())
assert readback["kind"] == "desktop-native-quit-readback" and readback["phase"] == "NATIVE_QUIT_READBACK_CAPTURED" and readback["nativeBinary"] == str(native_binary) and readback["quitFile"] == state["quitFile"] and sha(quit_file) == state["quitFile"]["sha256"]
state["readback"] = {"path": str(readback_path), "sha256": sha(readback_path)}; state["version"] = readback["version"]; state["platform"] = readback["platform"]
assert readback["inputSha256"] == state["input"]["sha256"] and readback["producerSha256"] == state["sources"]["producer"]["sha256"] and readback["readerSha256"] == state["sources"]["reader"]["sha256"] and sha(work / "native-last-session-expected.json") == readback["expectedReceiptSha256"]
assert sha(native_binary) == state["nativeBinary"]["sha256"] and all(sha(item["path"]) == item["sha256"] for item in state["sources"].values()), "Native/source inputs changed during capture"
assert sha(asset_input) == sha(working_input) == state["input"]["sha256"] and sha(companion) == sha(assets / companion.name) == state["input"]["companion"]["sha256"], "Original or working assets changed"
state["phase"] = "DESKTOP_BASELINE_CAPTURED"; state["finishedAt"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
persist(); print(str(receipt_path), flush=True)
def capture_interrupted(signum, frame): raise KeyboardInterrupt(f"Capture interrupted by signal {signum}")
signal.signal(signal.SIGTERM, capture_interrupted)
try: capture()
except BaseException as error:
state["phase"] = "FAILED_WITH_OWNED_PROCESS_PENDING" if state.get("cleanupPendingPid") else "FAILED"; state["error"] = str(error); persist(); raise

View File

@@ -0,0 +1,8 @@
import assert from 'node:assert/strict'; import { AutosaveScheduler } from '../../web/app/src/storage/autosave.ts';
const timers = new Map(); let serial = 0; globalThis.window = { setTimeout(callback) { timers.set(++serial, callback); return serial; }, clearTimeout(id) { timers.delete(id); } };
const fire = async () => { for (const [id, callback] of [...timers]) { timers.delete(id); callback(); } await Promise.resolve(); };
const deferred = () => { let release; const promise = new Promise(resolve => { release = resolve; }); return { promise, release }; };
const scheduler = new AutosaveScheduler(), edit = deferred(); let entered = 0, commits = 0; scheduler.schedule(async isCurrent => { entered++; await edit.promise; if (isCurrent()) commits++; }, 1);
await fire(); assert.equal(entered, 1); scheduler.cancel(); edit.release(); await scheduler.whenIdle(); assert.equal(commits, 0, 'new edit cancels waiting checkpoint');
scheduler.schedule(() => { commits++; }, 1); await fire(); await scheduler.whenIdle(); assert.equal(commits, 1, 'recovery reschedules'); scheduler.schedule(() => { commits++; }, 1); scheduler.cancel(); await fire(); assert.equal(commits, 1, 'timer cancellation prevents dispatch');
const write = deferred(); let writing = false, finished = false; scheduler.schedule(async () => { writing = true; await write.promise; finished = true; }, 1); await fire(); assert.equal(writing, true); scheduler.cancel(); assert.equal(finished, false); write.release(); await scheduler.whenIdle(); assert.equal(finished, true, 'active write retains ownership until finished'); scheduler.dispose(); console.log(JSON.stringify({status:'PASS',checks:['waiting callback cancellation','recovery reschedule','timer cancellation','active write completion']}));

View File

@@ -11,6 +11,9 @@ assert.deepEqual(new Set(catalog.entries.map((entry) => entry.id)), new Set(inve
assert.ok(catalog.entries.every((entry) => ["WEB_MAIN", "SERVER_NATIVE", "BROWSER_EQUIVALENT", "UNAVAILABLE"].includes(entry.route)));
const browserGroups = new Set(Object.keys(catalog.browserActionGroups ?? {}));
assert.ok(catalog.entries.filter((entry) => entry.route === "BROWSER_EQUIVALENT").every((entry) => browserGroups.has(entry.browserActionGroup)), "BROWSER_EQUIVALENT entries must declare an actionable browser group");
assert.equal(catalog.entries.find((entry) => entry.identifier === "WM_OT_save_auto_save")?.browserActionGroup, "AUTOSAVE"); assert.equal(catalog.browserActionGroups.AUTOSAVE.command, "saveNativeAutoSave");
assert.equal(catalog.entries.find((entry) => entry.identifier === "WM_OT_recover_auto_save")?.browserActionGroup, "RECOVERY"); assert.equal(catalog.browserActionGroups.RECOVERY.command, "recoverNativeAutoSave");
assert.equal(catalog.entries.find((entry) => entry.identifier === "WM_OT_recover_last_session")?.browserActionGroup, "LAST_SESSION"); assert.equal(catalog.browserActionGroups.LAST_SESSION.command, "recoverLastSession");
assert.ok(catalog.entries.filter((entry) => entry.route === "SERVER_NATIVE").every((entry) => entry.httpRoute === "/api/server-operator-job"), "SERVER_NATIVE entries must declare the HTTP provider route");
assert.equal(catalog.behavioralEvidence.verifiedUnits + catalog.behavioralEvidence.unverifiedUnits, catalog.unitCount);
assert.equal(catalog.behavioralEvidence.verifiedUnits, 1025);

View File

@@ -25,8 +25,9 @@ if (index) {
} finally {
fs.closeSync(catalogHandle);
}
if (active.length !== 1) violations.push({ code: "INDEX_ACTIVE_TASK_COUNT", detail: `count=${active.length}` });
if (current && active[0]?.[0] !== bundle.context.task) violations.push({ code: "INDEX_ACTIVE_TASK_MISMATCH", detail: `${active[0]?.[0] ?? "NONE"}!=${bundle.context.task}` });
const terminal = index.activeTask === null && bundle.context.nextTask === null;
if (active.length !== (terminal ? 0 : 1)) violations.push({ code: "INDEX_ACTIVE_TASK_COUNT", detail: `count=${active.length}` });
if (current && !terminal && active[0]?.[0] !== bundle.context.task) violations.push({ code: "INDEX_ACTIVE_TASK_MISMATCH", detail: `${active[0]?.[0] ?? "NONE"}!=${bundle.context.task}` });
}
assert.equal(violations.length, 0, `context governance violations: ${JSON.stringify(violations)}`);

View File

@@ -33,8 +33,16 @@ const favicon = await fetch(`${origin}/favicon.svg`);
assert.equal(favicon.status, 200);
assert.equal(favicon.headers.get("content-type"), "image/svg+xml");
const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH ?? "/usr/bin/google-chrome-stable" });
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, ignoreHTTPSErrors: false });
const browser = await chromium.launch({
headless: true,
executablePath: process.env.CHROME_PATH ?? "/usr/bin/google-chrome-stable",
/* Match web/playwright.config.ts: without a WebGPU/Dawn adapter the native
* GPU surface logs three init errors that are environment, not product. */
args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader",
"--enable-unsafe-webgpu", "--enable-dawn-features=allow_unsafe_apis",
"--use-webgpu-adapter=swiftshader"],
});
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, ignoreHTTPSErrors: false, acceptDownloads: true });
const page = await context.newPage();
const consoleErrors = [];
const requestFailures = [];
@@ -45,17 +53,236 @@ page.on("response", (item) => { if (item.status() >= 400) badResponses.push(`${i
await page.goto(`${origin}/`, { waitUntil: "domcontentloaded", timeout: 60_000 });
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120_000 });
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
await page.waitForFunction(() => Number(document.body.innerText.match(/Objects (\d+)/)?.[1] ?? 0) > 0, undefined, { timeout: 120_000 });
const beforeObjects = Number(await page.locator("body").evaluate((node) => node.innerText.match(/Objects (\d+)/)?.[1] ?? 0));
await page.getByRole("button", { name: "添加立方体", exact: true }).click();
await page.waitForFunction((before) => Number(document.body.innerText.match(/Objects (\d+)/)?.[1] ?? 0) === before + 1, beforeObjects, { timeout: 60_000 });
/* Wait for the fixture open itself. The startup General template also leaves a
* non-empty object count, so counting objects alone lets a still-running
* template open overwrite the fixture and the later mutation never lands. */
/* The startup General template also reaches "Engine: SceneIR" with three
* objects (Cube, Camera, Light), so neither the status text nor the object
* count identifies the fixture open: waiting on those let the template
* replace the fixture and the later mutation was discarded with the retired
* Main. basic_scene.blend names its objects BasicArea/BasicCamera/BasicCube,
* so wait until one of them is the active object. */
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120_000 });
await page.waitForFunction(() => /Engine: SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120_000 });
await page.waitForTimeout(2000);
const sceneStats = async () => page.locator('[data-testid="scene-stats"]').evaluate((node) => node.textContent ?? "");
const objectCountFrom = (text) => Number(text.match(/Objects (\d+)/)?.[1] ?? 0);
const beforeObjects = objectCountFrom(await sceneStats());
/* The product toolbar sits under the native desktop overlay
* (section[aria-label="Blender 桌面"], position fixed, z-index 20), so a
* normal actionability click is intercepted. Dispatch on the element itself:
* this is the same button a user reaches after switching to Web 业务工具. */
const steps = [];
const started = Date.now();
const record = (name, status, detail) => steps.push({ name, status, durationMs: Date.now() - started, detail });
record("startup", "PASS", { title: await page.title(), secureContext: await page.evaluate(() => window.isSecureContext) });
record("open_fixture", "PASS", { stats: await sceneStats(), objects: beforeObjects });
/* A synthetic pointer click lands on the native overlay that covers the Web
* toolbar. Dispatch on the button element so the same handler a user reaches
* after switching to Web 业务工具 runs. */
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
await page.waitForFunction((before) => Number((document.querySelector('[data-testid="scene-stats"]')?.textContent ?? "").match(/Objects (\d+)/)?.[1] ?? 0) === before + 1, beforeObjects, { timeout: 60_000 });
const state = await page.evaluate(() => ({ secureContext: window.isSecureContext, crossOriginIsolated: window.crossOriginIsolated, body: document.body.innerText, wasm: performance.getEntriesByType("resource").filter((item) => item.name.includes(".wasm")).map((item) => item.name) }));
assert.equal(state.secureContext, true);
assert.equal(state.crossOriginIsolated, true);
assert.equal(Number(state.body.match(/Objects (\d+)/)?.[1]), beforeObjects + 1);
assert.equal(objectCountFrom(await sceneStats()), beforeObjects + 1);
if (consoleErrors.length) process.stderr.write(`deployed-console-errors ${JSON.stringify(consoleErrors.slice(0, 6))}\n`);
/* Kept as the hard gate for the boot path only. The remaining steps each
* record the console errors they observe in their own detail, so a later
* failure cannot hide behind this early assertion. */
assert.equal(consoleErrors.length, 0);
assert.equal(requestFailures.length, 0);
assert.equal(badResponses.length, 0);
record("add_cube", "PASS", { objects: beforeObjects + 1, consoleErrors: consoleErrors.length });
/* The remaining report steps keep the same names and count as the historical
* 11-step receipt so the coverage denominator does not shrink. Each step
* records what actually happened; a step that cannot run is SKIP with the
* reason, never PASS. */
const sceneObjects = async () => Number((await sceneStats()).match(/Objects (\d+)/)?.[1] ?? -1);
const clickLabel = (label) => page.evaluate((value) => {
const element = document.querySelector(`[aria-label="${value}"]`);
if (!element) return false;
element.click();
return true;
}, label);
/* Workspace switches land asynchronously: an observed probe reached Modeling
* only between 2s and 4s after the click, so poll the attribute instead of
* sleeping a fixed interval that can sit right on the boundary. */
const switchWorkspace = async (name, timeout = 30000) => {
await page.evaluate((value) => {
const tab = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find((node) => (node.textContent ?? '').trim() === value);
tab?.click();
}, name);
try {
await page.waitForFunction((value) => document.querySelector('main')?.getAttribute('data-workspace') === value, name, { timeout });
await page.waitForTimeout(1200);
return true;
}
catch { return false; }
};
/* Native gestures keep engine.native_gesture_history set until the owned WM
* handler retires, and capture_history_entry rejects edits meanwhile. Retry
* the whole command rather than sleeping blindly; the attempt count is the
* measured release window and is reported, not hidden. */
const runEdit = async (label, attempts = 8, settle = 2000) => {
for (let attempt = 1; attempt <= attempts; attempt++) {
await clickLabel(label);
await page.waitForTimeout(settle);
const status = await page.evaluate(() => document.querySelector('[data-testid="engine-status"]')?.textContent ?? '');
if (!status.includes('command failed')) return { ok: true, attempts: attempt, status };
}
return { ok: false, attempts, status: 'command failed' };
};
const surface = () => page.evaluate(() => ({
workspace: document.querySelector('main')?.getAttribute('data-workspace') ?? null,
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute('data-native-display') ?? null,
area: document.querySelector('main')?.getAttribute('data-editor-workflow-area') ?? null,
editors: [...new Set([...document.querySelectorAll('[data-editor-workflow-area]')].map((node) => node.getAttribute('data-editor-workflow-area')))],
}));
try {
/* One toolbar add currently lands two entries in the undo stack
* (history reads "createPrimitive" then "Native UI"), so a single Undo only
* retires the empty native entry. Press until the object count drops and
* report how many presses that took: that count is the defect. */
const undoBase = await sceneObjects();
let undoPresses = 0, undone = undoBase;
for (; undoPresses < 3; undoPresses++) {
await clickLabel('撤销');
await page.waitForTimeout(6000);
undone = await sceneObjects();
if (undone !== undoBase) break;
}
undoPresses += 1;
let redoPresses = 0, redone = undone;
for (; redoPresses < 3; redoPresses++) {
await clickLabel('重做');
await page.waitForTimeout(6000);
redone = await sceneObjects();
if (redone === undoBase) break;
}
redoPresses += 1;
record('undo_redo', undone === undoBase - 1 && redone === undoBase ? 'PASS' : 'FAIL', { undoBase, undone, redone, undoPresses, redoPresses, defect: undoPresses > 1 ? 'one add operator pushes two history entries; the extra press is the empty native entry, not the operator' : null });
}
catch (error) { record('undo_redo', 'FAIL', { error: String(error).slice(0, 200) }); }
try {
const duplicateBase = await sceneObjects();
const duplicate = await runEdit('复制对象');
const duplicated = duplicate.ok ? await sceneObjects() : duplicateBase;
const remove = await runEdit('删除对象');
const deleted = remove.ok ? await sceneObjects() : duplicated;
record('duplicate_delete', duplicated === duplicateBase + 1 && deleted === duplicateBase ? 'PASS' : 'FAIL', { duplicateBase, duplicated, deleted, duplicate, remove });
}
catch (error) { record('duplicate_delete', 'FAIL', { error: String(error).slice(0, 200) }); }
try {
const beforeSave = await sceneObjects();
const downloading = page.waitForEvent('download', { timeout: 30000 });
await clickLabel('保存项目');
const download = await downloading;
const savedPath = await download.path();
const savedBytes = savedPath ? fs.readFileSync(savedPath) : Buffer.alloc(0);
/* Playwright stages downloads under a random extension-less name. The blend
* format route resolves on the file name, so reopening the staged path fails
* with "IO: format route unavailable". Reopen a .blend-named copy instead. */
const reopenDir = fs.mkdtempSync('/tmp/wbv2-reopen-');
const reopenPath = path.join(reopenDir, download.suggestedFilename());
fs.writeFileSync(reopenPath, savedBytes);
await page.getByTestId('blend-file-input').setInputFiles(reopenPath);
await page.waitForFunction(() => /Engine: SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ''), undefined, { timeout: 120000 });
await page.waitForTimeout(3000);
const reopenedObjects = await sceneObjects();
record('save_reopen', savedBytes.length > 0 && reopenedObjects === beforeSave ? 'PASS' : 'FAIL', {
suggestedFilename: download.suggestedFilename(),
byteLength: savedBytes.length,
sha256: crypto.createHash('sha256').update(savedBytes).digest('hex'),
reopenedObjects,
});
}
catch (error) { record('save_reopen', 'FAIL', { error: String(error).slice(0, 200) }); }
try {
await clickLabel('操作搜索');
await page.waitForSelector('.operator-search-dialog', { timeout: 20000 });
const form = await page.evaluate(() => {
const dialog = document.querySelector('.operator-search-dialog');
return { present: Boolean(dialog), controls: dialog ? dialog.querySelectorAll('.operator-property-form input, .operator-property-form select').length : 0, results: dialog ? dialog.querySelectorAll('[role="listbox"] button').length : 0 };
});
await page.keyboard.press('Escape');
await page.waitForTimeout(800);
record('f3_local_operator_properties', form.present ? 'PASS' : 'FAIL', form);
}
catch (error) { record('f3_local_operator_properties', 'FAIL', { error: String(error).slice(0, 200) }); }
try {
const response = await fetch(`${origin}/api/server-operator-job`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-server-operator-action': 'describe' },
body: JSON.stringify({ operator: 'mesh.primitive_cube_add' }),
});
const text = await response.text();
let properties = null;
try { properties = JSON.parse(text)?.result?.operator?.properties?.length ?? null; } catch { properties = null; }
record('f3_server_operator_properties', response.status === 200 && properties ? 'PASS' : 'FAIL', { status: response.status, properties, body: text.slice(0, 200) });
}
catch (error) { record('f3_server_operator_properties', 'FAIL', { error: String(error).slice(0, 200) }); }
try {
const beforeSplit = await surface();
const switched = await switchWorkspace('Modeling');
const afterSplit = await surface();
record('browser_equivalent_area_split', switched && afterSplit.workspace === 'Modeling' && afterSplit.area !== beforeSplit.area ? 'PASS' : 'FAIL', { before: beforeSplit, after: afterSplit });
}
catch (error) { record('browser_equivalent_area_split', 'FAIL', { error: String(error).slice(0, 200) }); }
try {
const tabs = await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="工作区"] button')].map((node) => (node.textContent ?? '').trim()).filter(Boolean));
const visited = [];
for (const name of ['Layout', 'Modeling', 'Scripting', 'Shading', 'UV Editing']) {
const switched = await switchWorkspace(name);
visited.push({ name, switched, ...(await surface()) });
}
await switchWorkspace('Layout');
record('workspace_surfaces', visited.every((entry) => entry.switched && entry.workspace === entry.name && entry.area?.includes(entry.name)) ? 'PASS' : 'FAIL', { scope: 'workspace identity and editor area identity; not full functional verification', tabs, visited });
}
catch (error) { record('workspace_surfaces', 'FAIL', { error: String(error).slice(0, 200) }); }
try {
const render = await page.evaluate(() => ({
canvases: [...document.querySelectorAll('canvas')].map((canvas) => ({ aria: canvas.getAttribute('aria-label'), width: canvas.width, height: canvas.height })),
bodyWidth: document.body.clientWidth,
viewportWidth: window.innerWidth,
wasmResources: performance.getEntriesByType('resource').filter((entry) => entry.name.includes('.wasm')).map((entry) => entry.name),
}));
record('desktop_render', render.canvases.some((canvas) => canvas.width > 0 && canvas.height > 0) ? 'PASS' : 'FAIL', render);
}
catch (error) { record('desktop_render', 'FAIL', { error: String(error).slice(0, 200) }); }
await context.close();
await browser.close();
process.stdout.write(`deployed-web-blender-ok origin=${origin} wasm=${expectedWasmSha256} range=206 mainMutation=${beforeObjects}->${beforeObjects + 1} chromium=ready\n`);
/* Bind the receipt to the exact source tree and candidate so a completion
* evaluation can tell this run apart from an older deployment snapshot. */
const reportPath = process.env.CHROMIUM_REPORT_PATH ?? null;
if (reportPath) {
const candidate = process.env.WBV2_PRODUCTION_CANDIDATE ?? null;
const candidateSha256 = candidate && fs.existsSync(candidate) ? crypto.createHash("sha256").update(fs.readFileSync(candidate)).digest("hex") : null;
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
const inventory = JSON.parse(fs.readFileSync(path.resolve("tests/golden/WBV2/dynamic-inventory.json"), "utf8"));
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, `${JSON.stringify({
observedAt: new Date().toISOString(),
origin,
browser: "Chromium",
sourceTreeSha256: inventory.authority.sourceTreeSha256,
candidate,
candidateSha256,
engineWasmSha256: expectedWasmSha256,
steps,
errors: consoleErrors,
failedRequests: requestFailures,
badResponses,
}, null, 2)}\n`);
}
process.stdout.write(`deployed-web-blender-ok origin=${origin} wasm=${expectedWasmSha256} range=206 mainMutation=${beforeObjects}->${beforeObjects + 1} chromium=ready steps=${steps.length}\n`);

View File

@@ -23,7 +23,7 @@ assert.deepEqual(contract.responseHeaders.allResponses, {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Resource-Policy": "same-origin",
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self' blob:; media-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
});
const routes = Object.fromEntries(contract.responseHeaders.routes.map((route) => [route.id, route]));
assert.deepEqual(routes["entry-document"], { id: "entry-document", patterns: ["/", "/index.html"], cacheControl: "no-cache" });

View File

@@ -178,6 +178,17 @@ try {
assert.notEqual(changed.headers.get("etag"), etagFirst);
assert.deepEqual(await body(changed), replacement);
const privatePassword = crypto.randomBytes(24).toString("hex");
const privateServer = createDeploymentHttpServer({ root, contract, accessUsername: "test-owner", accessPassword: privatePassword });
const privateOrigin = await listenDeploymentHttpServer(privateServer), privateHeaders = { Authorization: `Basic ${Buffer.from(`test-owner:${privatePassword}`).toString("base64")}` };
try {
assert.equal((await fetch(privateOrigin)).status, 401);
assert.equal((await fetch(privateOrigin, { headers: { Authorization: "Basic invalid" } })).status, 401);
for (const route of ["server-operator-job", "blender-workflow", "blender-session", "blender-resources"]) assert.equal((await fetch(`${privateOrigin}/api/${route}`, { method: "POST" })).status, 401);
const authorized = await fetch(privateOrigin, { headers: privateHeaders }); assert.equal(authorized.status, 200); assert.equal(authorized.headers.get("cache-control"), "private, no-store"); assert.equal(authorized.headers.get("vary"), "Authorization"); assert.deepEqual(await body(authorized), fixtures.get("/index.html"));
assert.equal((await fetch(`${privateOrigin}/api/server-operator-job`, { method: "POST", headers: { ...privateHeaders, Origin: "https://untrusted.invalid" } })).status, 403);
await assert.rejects(listenDeploymentHttpServer(createDeploymentHttpServer({ root, contract, accessPassword: "" }), { host: "0.0.0.0" }), /WEB_BLENDER_PASSWORD/);
} finally { await new Promise((resolve) => privateServer.close(resolve)); }
process.stdout.write("deployment-http-ok mime=10 head=4 ranges=5 invalidRanges=6 resumeSha256=verified\n");
}
finally {

View File

@@ -34,6 +34,8 @@ let catalog = [];
let plan = null;
let matrix = null;
let completed = [];
let dynamicInventory = null;
let dynamicInventoryManifest = null;
try {
queue = parseQueue();
@@ -46,23 +48,39 @@ try {
const capabilityIndex = queue.currentTask.startsWith("WBV2-");
index = json(capabilityIndex ? "tests/golden/WBV2/task-index.json" : "tests/golden/M15-03A/task-index.json");
if (index.activeTask !== queue.currentTask) addIssue(issues, "INDEX_ACTIVE_TASK_MISMATCH", `${index.activeTask}!=${queue.currentTask}`);
const currentManifestPath = path.join(root, "tests/golden", queue.currentTask, "manifest.json");
const currentManifest = fs.existsSync(currentManifestPath) ? JSON.parse(fs.readFileSync(currentManifestPath, "utf8")) : null;
const terminal = index.activeTask === null && currentManifest?.status === "done" && currentManifest?.nextTask === null;
if (!terminal && index.activeTask !== queue.currentTask) addIssue(issues, "INDEX_ACTIVE_TASK_MISMATCH", `${index.activeTask ?? "NONE"}!=${queue.currentTask}`);
const catalogPath = path.join(root, index.catalog.path);
if (!fs.existsSync(catalogPath)) addIssue(issues, "CATALOG_MISSING", index.catalog.path);
else {
catalog = fs.readFileSync(catalogPath, "utf8").trimEnd().split("\n").filter(Boolean).map((line) => JSON.parse(line));
const active = catalog.filter((entry) => entry.state === "active");
if (active.length !== 1) addIssue(issues, "ACTIVE_TASK_COUNT", `count=${active.length}`);
else if (active[0].id !== queue.currentTask) addIssue(issues, "CATALOG_ACTIVE_TASK_MISMATCH", `${active[0].id}!=${queue.currentTask}`);
if (terminal ? active.length !== 0 : active.length !== 1) addIssue(issues, "ACTIVE_TASK_COUNT", `count=${active.length}`);
else if (!terminal && active[0].id !== queue.currentTask) addIssue(issues, "CATALOG_ACTIVE_TASK_MISMATCH", `${active[0].id}!=${queue.currentTask}`);
}
plan = json("tests/golden/M15-03A/next-task-plan.json");
matrix = json("docs/BLENDER_WASM_CAPABILITY_MATRIX_TEMPLATE.json");
completed = json("tests/golden/M15-03A/completed-gap-tasks.json");
dynamicInventory = json("tests/golden/WBV2/dynamic-inventory.json");
dynamicInventoryManifest = json("tests/golden/WBV2/dynamic-inventory-manifest.json");
} catch (error) {
addIssue(issues, "CONTROL_PLANE_READ_ERROR", error instanceof Error ? error.message : String(error));
}
if (!dynamicInventory || !dynamicInventoryManifest) addIssue(issues, "DYNAMIC_INVENTORY_MISSING", "tests/golden/WBV2/dynamic-inventory.json and manifest");
else {
if (dynamicInventory.operation !== "BLENDER_52_SOURCE_DYNAMIC_INVENTORY" || !["SOURCE_DERIVED_UNVERIFIED", "SOURCE_DERIVED_WITH_EXPLICIT_COVERAGE"].includes(dynamicInventory.status)) addIssue(issues, "DYNAMIC_INVENTORY_INVALID", "operation/status");
if (dynamicInventory.policy?.hardTarget !== false || dynamicInventory.policy?.planningEstimate !== 5200) addIssue(issues, "DYNAMIC_INVENTORY_POLICY_INVALID", "planning estimate must remain non-binding");
if ((dynamicInventory.coverage?.verifiedUnits ?? 0) + (dynamicInventory.coverage?.unverifiedUnits ?? 0) !== dynamicInventory.summary?.unitCount) addIssue(issues, "DYNAMIC_INVENTORY_COVERAGE_INVALID", "verified plus unverified must equal source unit count");
if (dynamicInventoryManifest.operation !== "BLENDER_52_SOURCE_DYNAMIC_INVENTORY" || dynamicInventoryManifest.status !== "done") addIssue(issues, "DYNAMIC_INVENTORY_MANIFEST_INVALID", "operation/status");
for (const artifact of Object.values(dynamicInventoryManifest.artifacts ?? {})) {
if (!fs.existsSync(path.join(root, artifact.path)) || sha256(fs.readFileSync(path.join(root, artifact.path))) !== artifact.sha256) addIssue(issues, "DYNAMIC_INVENTORY_MANIFEST_HASH_DRIFT", artifact.path);
}
}
const active = catalog.find((entry) => entry.state === "active") ?? null;
let legacyCoverage = null;
try {
@@ -124,6 +142,17 @@ const report = {
productiveLane: capabilities.filter((entry) => entry.lifecycleStatus === "done" && ["L2", "L3", "L4", "L5"].includes(entry.parityLevel)).length,
rule: "one capability task may close multiple legacy cases; legacy cases remain regression coverage",
},
dynamicInventory: dynamicInventory ? {
path: "tests/golden/WBV2/dynamic-inventory.json",
manifestPath: "tests/golden/WBV2/dynamic-inventory-manifest.json",
sourceTreeSha256: dynamicInventory.authority.sourceTreeSha256,
sourceFiles: dynamicInventory.summary.sourceFiles,
unitCount: dynamicInventory.summary.unitCount,
planningEstimate: dynamicInventory.policy.planningEstimate,
hardTarget: dynamicInventory.policy.hardTarget,
verifiedUnits: dynamicInventory.coverage.verifiedUnits,
unverifiedUnits: dynamicInventory.coverage.unverifiedUnits,
} : null,
capabilityClusters,
issues,
remediation: issues.map(({ code }) => ({

View File

@@ -26,13 +26,20 @@ function geometry(meshId, positions) {
}
try {
const exporterSource = fs.readFileSync(path.join(root, "web/protocol/glb-export.ts"), "utf8");
const transpiled = ts.transpileModule(exporterSource, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
fileName: "glb-export.ts",
});
const transpile = (sourceName, outputName) => {
const source = fs.readFileSync(path.join(root, "web/protocol", sourceName), "utf8");
const transpiled = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
fileName: sourceName,
});
fs.writeFileSync(path.join(temporary, outputName), transpiled.outputText);
};
// glb-export imports the non-mesh mapper at runtime; materialize that small
// dependency chain beside the transpiled exporter for the standalone checker.
transpile("nonmesh-binary.ts", "nonmesh-binary.js");
transpile("nonmesh-export.ts", "nonmesh-export.js");
transpile("glb-export.ts", "glb-export.cjs");
const exporterPath = path.join(temporary, "glb-export.cjs");
fs.writeFileSync(exporterPath, transpiled.outputText);
const { exportGLB } = await import(pathToFileURL(exporterPath).href);
const roundTripPositions = [0, 0, 0, 1, 0, 0, 0, 1, 0];

Some files were not shown because too many files have changed in this diff Show More