Files
workinf_Blender_Wasm/tools/web/generate-blender-feature-route-catalog.mjs
mes123456 e9bfef0c5a
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
feat(web): complete Blender route runtime execution
2026-09-07 00:12:32 +08:00

105 lines
5.9 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const readJson = (file) => JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
const inventory = readJson("tests/golden/WBV2/dynamic-inventory.json");
const bindings = readJson("tests/golden/WBV2/dynamic-coverage-bindings.json");
const verified = new Set(bindings.filter((entry) => entry.status === "VERIFIED").map((entry) => entry.unitId));
const browserPrefixes = new Set(["WM", "SCREEN", "FILE", "VIEW2D", "UI", "OUTLINER", "CONSOLE", "INFO", "PREFERENCES", "TEXT", "BUTTONS", "SPREADSHEET", "WORKSPACE", "GIZMOGROUP", "VIEW3D"]);
const browserActionGroups = {
OPEN: { command: "openBlendFile", description: "Browser file picker and native Main open workflow" },
SAVE: { command: "saveBlend", description: "Native Main save and browser download workflow" },
WORKSPACE: { command: "switchWorkspace", description: "Existing workspace and window layout workflow" },
SELECTION: { command: "selectObject", description: "Existing Outliner/viewport selection workflow" },
VIEW: { command: "focusViewport", description: "Existing viewport navigation and view workflow" },
EDITOR: { command: "focusEditor", description: "Existing editor focus/context workflow" },
};
function prefix(identifier) {
return identifier.split(/_(?:OT|MT)_/u, 1)[0];
}
function classify(unit) {
if (unit.kind === "web-command") return ["WEB_MAIN", "typed WebEngine command"];
if (unit.kind === "node") return ["WEB_MAIN", "shared native node graph route"];
if (unit.kind === "rna-struct") return ["WEB_MAIN", "bounded RNA reflection route"];
if (unit.kind === "modifier-source") {
return unit.identifier === "MOD_none" || unit.identifier === "MOD_ui_common"
? ["BROWSER_EQUIVALENT", "non-runtime modifier sentinel or desktop UI helper"]
: ["WEB_MAIN", "metadata-driven native modifier stack route"];
}
if (verified.has(unit.id)) return ["WEB_MAIN", "existing exact native Main binding"];
const operatorPrefix = prefix(unit.identifier);
if (operatorPrefix === "WM" && /_XR_/u.test(unit.identifier)) return ["UNAVAILABLE", "XR window-manager context is outside the Web runtime"];
if (browserPrefixes.has(operatorPrefix)) return ["BROWSER_EQUIVALENT", "browser-owned editor, window, view or file interaction"];
return ["SERVER_NATIVE", "background Blender operator job with bounded typed properties"];
}
function browserActionGroup(unit) {
const identifier = String(unit.identifier ?? "");
const prefix = identifier.split(/_(?:OT|MT)_/u, 1)[0];
const hasToken = (tokens) => tokens.some((token) => new RegExp(`(?:^|[_-])${token}(?:$|[_-])`, "iu").test(identifier));
if (hasToken(["open", "load", "read", "import", "browse", "directory", "filepath", "find_missing", "recent"])) return "OPEN";
if (hasToken(["save", "write", "export", "pack", "unpack"])) return "SAVE";
if (prefix === "WORKSPACE" || prefix === "SCREEN" || hasToken(["window", "workspace", "screen", "area", "region"])) return "WORKSPACE";
if (hasToken(["select", "deselect", "box_select", "lasso", "circle_select", "pick"])) return "SELECTION";
if (prefix === "VIEW2D" || prefix === "VIEW3D" || prefix === "GIZMOGROUP" || hasToken(["view", "zoom", "pan", "orbit", "navigate", "cursor", "gizmo", "frame"])) return "VIEW";
return "EDITOR";
}
const entries = inventory.units.map((unit) => {
const [route, reason] = classify(unit);
return {
id: unit.id,
kind: unit.kind,
identifier: unit.identifier,
family: unit.family,
route,
reason,
...(route === "SERVER_NATIVE" ? { httpRoute: "/api/server-operator-job" } : {}),
...(route === "BROWSER_EQUIVALENT" ? { browserActionGroup: browserActionGroup(unit) } : {}),
evidenceStatus: verified.has(unit.id) ? "VERIFIED" : "UNVERIFIED",
locations: unit.locations,
};
});
const counts = Object.fromEntries([...new Set(entries.map((entry) => entry.route))].sort().map((route) => [route, entries.filter((entry) => entry.route === route).length]));
const verifiedUnits = entries.filter((entry) => entry.evidenceStatus === "VERIFIED").length;
const output = {
schemaVersion: 1,
sourceInventory: "tests/golden/WBV2/dynamic-inventory.json",
sourceTreeSha256: inventory.sourceFiles?.treeSha256 ?? inventory.authority?.sourceTreeSha256 ?? null,
unitCount: entries.length,
classifiedCount: entries.length,
unclassifiedCount: 0,
counts,
behavioralEvidence: {
status: verifiedUnits === entries.length ? "VERIFIED" : "PARTIAL",
verifiedUnits,
unverifiedUnits: entries.length - verifiedUnits,
policy: "route classification does not replace exact behavioral evidence",
},
routes: {
WEB_MAIN: "typed native Main/WASM or shared reflection route",
SERVER_NATIVE: "bounded Blender background operator job",
BROWSER_EQUIVALENT: "browser-native replacement for desktop UI/context",
UNAVAILABLE: "platform capability has no Web equivalent",
},
browserActionGroups,
runtimeAudit: {
schemaVersion: 1,
command: "featureRouteRuntimeAudit",
status: "STATIC_DECLARATION",
webMain: { policy: "every web-command entry must be present in web-engine capabilities" },
serverNative: { httpRoute: "/api/server-operator-job", policy: "every SERVER_NATIVE entry declares the bounded HTTP provider" },
browserEquivalent: { groups: Object.keys(browserActionGroups), policy: "every BROWSER_EQUIVALENT entry declares an actionable browser group" },
behavioralEvidence: { separate: true, policy: "behavioral evidence remains independent from route assignment" },
},
entries,
};
const outputPath = path.join(root, "web/app/src/capabilities/blender-feature-route-catalog.json");
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`);
process.stdout.write(`blender-feature-routes-written units=${entries.length} routes=${JSON.stringify(counts)} output=${path.relative(root, outputPath)}\n`);