Files
FreeCADWasmDev/web/tools/audit-geometry-boundary.mjs
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

115 lines
6.0 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
const webRoot = fileURLToPath(new URL("../", import.meta.url));
// React views and scene hosts are display-layer owners: Three.js is permitted
// there; Facades/Workers must continue to consume OCCT WASM mesh payloads.
const displayOwners = new Set([
"src/main.ts", "src/import-render-bridge.ts", "src/render-object-registry.ts", "src/scene-session-registry.ts",
"src/bim-frame-view.tsx", "src/draft-geometry-task.tsx", "src/openscad-desktop-view.tsx",
"src/scene-camera-commands.ts", "src/scene-command-host.ts", "src/scene-overlap-picker.ts",
"src/techdraw-active-view.tsx", "src/templatepymod-desktop-task.tsx", "src/test-view-example-task.tsx",
"src/web-view-task.tsx",
]);
const proceduralGeometry = new Set(["BoxGeometry", "SphereGeometry", "CylinderGeometry", "ConeGeometry", "ExtrudeGeometry",
"ShapeGeometry", "TorusGeometry", "TorusKnotGeometry", "LatheGeometry", "TubeGeometry", "PolyhedronGeometry",
"TetrahedronGeometry", "OctahedronGeometry", "DodecahedronGeometry", "IcosahedronGeometry"]);
const alternateGeometry = /^(?:@jscad\/|jscad(?:$|\/)|three-csg(?:-ts)?(?:$|\/)|three-bvh-csg(?:$|\/)|manifold-3d(?:$|\/))/;
export function inspectGeometrySource(filename, source) {
const tree = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true,
filename.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
const findings = [];
const namespaces = new Set();
const named = new Map();
let usesThree = false;
const add = (node, code, message) => findings.push({ path: filename, line: tree.getLineAndCharacterOfPosition(node.getStart(tree)).line + 1, code, message });
const dependency = (node, value) => {
if (alternateGeometry.test(value)) add(node, "alternate_geometry_backend", `Use the provided OCCT backend, not ${value}`);
if (value === "three" || value.startsWith("three/")) {
usesThree = true;
if (!displayOwners.has(filename)) add(node, "three_outside_display", "Three.js belongs to the reviewed display layer, not a Facade or Worker");
}
};
for (const statement of tree.statements) {
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && !statement.importClause?.isTypeOnly) {
const value = statement.moduleSpecifier.text;
dependency(statement, value);
if (value === "three") {
const binding = statement.importClause?.namedBindings;
if (binding && ts.isNamespaceImport(binding)) namespaces.add(binding.name.text);
if (binding && ts.isNamedImports(binding)) for (const item of binding.elements) {
if (!item.isTypeOnly) named.set(item.name.text, item.propertyName?.text ?? item.name.text);
}
}
}
if (ts.isExportDeclaration(statement) && !statement.isTypeOnly && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) dependency(statement, statement.moduleSpecifier.text);
}
const visit = node => {
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword
|| ts.isIdentifier(node.expression) && node.expression.text === "require")) {
if (node.arguments[0] && ts.isStringLiteral(node.arguments[0])) dependency(node, node.arguments[0].text);
}
if (ts.isNewExpression(node)) {
const target = node.expression;
const name = ts.isIdentifier(target) ? named.get(target.text)
: ts.isPropertyAccessExpression(target) && ts.isIdentifier(target.expression) && namespaces.has(target.expression.text) ? target.name.text : undefined;
if (proceduralGeometry.has(name)) add(node, "three_procedural_cad", `${name} cannot replace native CAD geometry; render the WASM mesh payload`);
}
ts.forEachChild(node, visit);
};
visit(tree);
return { findings, usesThree };
}
export function auditGeometryBoundary(root = webRoot) {
const started = performance.now();
const findings = [];
const renderers = [];
let sourceFiles = 0;
const visit = directory => {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const file = path.join(directory, entry.name);
if (entry.isDirectory()) visit(file);
else if (entry.isFile() && /\.(?:ts|tsx|js|mjs)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
const name = path.relative(root, file).split(path.sep).join("/");
const result = inspectGeometrySource(name, fs.readFileSync(file, "utf8"));
sourceFiles += 1;
findings.push(...result.findings);
if (result.usesThree) renderers.push(name);
}
}
};
visit(path.join(root, "src"));
const artifacts = ["public/kernel/FreeCADWebKernel.js", "public/kernel/FreeCADWebKernel.wasm"].map(name => {
const file = path.join(root, name);
let bytes = 0;
try {
bytes = fs.statSync(file).size;
if (!bytes) throw new Error("empty artifact");
if (name.endsWith(".wasm")) {
const descriptor = fs.openSync(file, "r");
const header = Buffer.alloc(8);
try { fs.readSync(descriptor, header, 0, 8, 0); } finally { fs.closeSync(descriptor); }
if (!header.equals(Buffer.from([0, 97, 115, 109, 1, 0, 0, 0]))) throw new Error("invalid WASM header");
}
} catch (error) {
findings.push({ path: name, code: "provided_kernel_unavailable", message: error.message });
}
return { path: name, bytes };
});
return { schema: "freecad-web-geometry-boundary/v1", status: findings.length ? "failed" : "checked",
sourceFiles, renderers: renderers.sort(), artifacts, findings,
singleRuntimeVerified: false,
scope: "Static frontend ownership and supplied artifact checks only; not proof of all native algorithms or a single WASM instance.",
totalSeconds: Number(((performance.now() - started) / 1000).toFixed(3)) };
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const report = auditGeometryBoundary();
console.log(JSON.stringify(report));
process.exitCode = report.findings.length ? 1 : 0;
}