105 lines
5.9 KiB
JavaScript
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`);
|