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`);