Files
workinf_Blender_Wasm/tools/web/generate-source-dynamic-inventory.mjs
mes123456 fa94ad14ed
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
perf(web): enforce batch-scoped source inventory checks
2026-09-03 15:06:48 +08:00

146 lines
7.4 KiB
JavaScript

import crypto from "node:crypto";
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 sourceRoot = path.join(root, "blender-5.2.0/source/blender");
const defaultOutput = path.join(root, "tests/golden/WBV2/dynamic-inventory.json");
const outputArgument = process.argv.indexOf("--output");
const outputPath = path.resolve(outputArgument >= 0 ? process.argv[outputArgument + 1] : defaultOutput);
const coverageBindingsPath = path.join(root, "tests/golden/WBV2/dynamic-coverage-bindings.json");
const sourceExtensions = /\.(?:c|cc|cpp|h|hh)$/u;
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const sourceRelative = (file) => path.relative(sourceRoot, file).replaceAll(path.sep, "/");
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
const sourceDirectories = fs.readdirSync(sourceRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name !== "tests" && entry.name !== "test")
.map((entry) => entry.name)
.sort(compare);
function collectFiles(directory, files) {
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => compare(left.name, right.name))) {
const file = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (entry.name === "tests" || entry.name === "test") continue;
collectFiles(file, files);
}
else if (entry.isFile() && sourceExtensions.test(entry.name)) files.push(file);
}
}
const files = [];
for (const directory of sourceDirectories) {
const absolute = path.join(sourceRoot, directory);
if (fs.existsSync(absolute)) collectFiles(absolute, files);
}
files.sort((left, right) => compare(sourceRelative(left), sourceRelative(right)));
const fileRecords = files.map((file) => {
const bytes = fs.readFileSync(file);
return { path: relative(file), sourcePath: sourceRelative(file), bytes: bytes.length, sha256: sha256(bytes), text: bytes.toString("utf8") };
});
const sourceTreeSha256 = sha256(fileRecords.map((file) => `${file.sourcePath}\0${file.sha256}\n`).join(""));
const units = new Map();
function familyFor(file, kind) {
if (kind === "web-command") return "WEB_ENGINE";
if (file.startsWith("editors/")) return "EDITOR";
if (file.startsWith("makesrna/")) return "RNA";
if (file.startsWith("nodes/")) return "NODES";
if (file.startsWith("modifiers/")) return "MODIFIER";
if (file.startsWith("animrig/")) return "ANIMATION";
if (file.startsWith("sequencer/")) return "SEQUENCER";
if (file.startsWith("blenkernel/")) return "CORE";
if (file.startsWith("windowmanager/")) return "WINDOW_MANAGER";
if (file.startsWith("web_engine/")) return "WEB_ENGINE";
return "OTHER";
}
function addUnit(kind, identifier, file, offset, evidence) {
const sourcePath = file.sourcePath;
const key = `${kind}:${identifier}`;
const location = { path: file.path, line: file.text.slice(0, offset).split("\n").length, evidence };
const existing = units.get(key);
if (existing) existing.locations.push(location);
else units.set(key, { id: key, kind, identifier, family: familyFor(sourcePath, kind), locations: [location] });
}
for (const file of fileRecords) {
for (const match of file.text.matchAll(/WM_operatortype_append\(\s*([A-Za-z0-9_]+)\s*\)/gu)) addUnit("operator", match[1], file, match.index, "WM_operatortype_append");
for (const match of file.text.matchAll(/WM_operatortype_append_macro\(\s*"([A-Za-z0-9_]+)"/gu)) addUnit("operator-macro", match[1], file, match.index, "WM_operatortype_append_macro");
for (const match of file.text.matchAll(/RNA_def_struct\s*\(\s*[^,]+,\s*"([^"]+)"/gu)) addUnit("rna-struct", match[1], file, match.index, "RNA_def_struct");
for (const match of file.text.matchAll(/(?:sh|cmp|geo|bke|common)_node_type_base(?:_custom)?\s*\([^,]+,\s*"([^"]+)"(?:_ustr)?/gu)) addUnit("node", match[1], file, match.index, "node_type_base");
// Only the public API dispatcher defines WebEngine command values. Other
// WebEngine sources compare `type` for shader/node metadata and must not be
// misclassified as transport commands.
if (file.sourcePath === "web_engine/web_engine_api.cpp") {
for (const match of file.text.matchAll(/type\s*==\s*"([A-Za-z][A-Za-z0-9_]*)"/gu)) addUnit("web-command", match[1], file, match.index, "command-dispatch");
}
if (file.sourcePath.startsWith("modifiers/intern/") && /^MOD_[A-Za-z0-9_]+\.cc$/u.test(path.basename(file.sourcePath))) {
addUnit("modifier-source", path.basename(file.sourcePath, ".cc"), file, 0, "modifier-source-file");
}
}
const sortedUnits = [...units.values()].map((unit) => ({
...unit,
locations: unit.locations.sort((left, right) => compare(`${left.path}:${left.line}`, `${right.path}:${right.line}`)),
coverageStatus: "UNVERIFIED",
})).sort((left, right) => compare(left.id, right.id));
const coverageBindings = fs.existsSync(coverageBindingsPath)
? JSON.parse(fs.readFileSync(coverageBindingsPath, "utf8"))
: [];
const bindingsByUnit = new Map(coverageBindings.map((binding) => [binding.unitId, binding]));
const coveredUnits = sortedUnits.map((unit) => bindingsByUnit.has(unit.id)
? { ...unit, coverageStatus: "VERIFIED", coverage: bindingsByUnit.get(unit.id) }
: unit);
const byKind = {};
const byFamily = {};
for (const unit of coveredUnits) {
byKind[unit.kind] = (byKind[unit.kind] ?? 0) + 1;
byFamily[unit.family] = (byFamily[unit.family] ?? 0) + 1;
}
const sourceBytes = fileRecords.reduce((total, file) => total + file.bytes, 0);
const inventory = {
schemaVersion: 1,
operation: "BLENDER_52_SOURCE_DYNAMIC_INVENTORY",
status: coverageBindings.length > 0 ? "SOURCE_DERIVED_WITH_EXPLICIT_COVERAGE" : "SOURCE_DERIVED_UNVERIFIED",
authority: {
sourceRoot: "blender-5.2.0/source/blender",
sourceTreeSha256,
sourceDirectories,
excludedDirectories: ["tests", "test"],
},
policy: {
initialLegacyEstimate: 6900,
planningEstimate: 5200,
hardTarget: false,
unitDefinition: "deduplicated source declarations for registered operators, RNA structs, node types, modifier source modules and WebEngine command dispatch values",
qualityGate: "source mapping and behavioral evidence are required before a unit is covered",
},
summary: {
sourceFiles: fileRecords.length,
sourceBytes,
unitCount: coveredUnits.length,
byKind,
byFamily,
planningEstimate: 5200,
estimateDelta: coveredUnits.length - 5200,
},
coverage: {
verifiedUnits: coverageBindings.length,
unverifiedUnits: coveredUnits.length - coverageBindings.length,
sourceMappingRequired: coveredUnits.length,
inventoryMutation: coverageBindings.length > 0 ? "explicit_source_case_bindings_only" : "forbidden",
...(coverageBindings.length > 0 ? { lastReconciledAt: "2026-09-01" } : {}),
},
sourceFiles: fileRecords.map(({ path: filePath, sourcePath, bytes, sha256: digest }) => ({ path: filePath, sourcePath, bytes, sha256: digest })),
units: coveredUnits,
coverageLedger: coverageBindings,
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(inventory, null, 2)}\n`);
process.stdout.write(`source-dynamic-inventory-generated files=${fileRecords.length} units=${sortedUnits.length} planningEstimate=5200 sourceTree=${sourceTreeSha256} output=${relative(outputPath)}\n`);