115 lines
6.0 KiB
JavaScript
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;
|
|
}
|