Squashed 'third_party/pi/' content from commit d981de122
git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
140
scripts/check-entry-graphs.mjs
Normal file
140
scripts/check-entry-graphs.mjs
Normal file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Entry points are cost contracts.
|
||||
*
|
||||
* A package's `exports` map is the only place that says which modules are public, and one stray
|
||||
* `export *` can silently make a narrow entry drag an entire barrel: importing a 1-file pure
|
||||
* function through a barrel costs ~37 MB of evaluated module graph, and nothing fails until someone
|
||||
* measures a process. This walks the value-import graph of every declared entry point and enforces a
|
||||
* budget per entry, so that regression fails at commit time instead.
|
||||
*
|
||||
* Only value imports count. `import type` / `export type` are erased before Node sees them.
|
||||
*/
|
||||
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
/** Workspace package name -> its source root, so cross-package imports are followed. */
|
||||
const WORKSPACE = {
|
||||
"@earendil-works/chord": "packages/chord/src",
|
||||
"@earendil-works/pi-ai": "packages/ai/src",
|
||||
"@earendil-works/pi-agent-core": "packages/agent/src",
|
||||
"@earendil-works/pi-telemetry": "packages/telemetry/src",
|
||||
"@earendil-works/pi-tui": "packages/tui/src",
|
||||
};
|
||||
|
||||
/**
|
||||
* Budgets are deliberate. `.` and `./node` are batteries-included entries and stay unbounded; every
|
||||
* narrow entry states the graph it is allowed to reach.
|
||||
*/
|
||||
const BUDGETS = {
|
||||
"packages/ai": {
|
||||
"./utils/*": { maxFiles: 3, forbid: ["providers/", "api/", "index.ts"] },
|
||||
},
|
||||
"packages/agent": {
|
||||
"./harness/runtime/reducer": { maxFiles: 1 },
|
||||
"./harness/context": { maxFiles: 6, forbid: ["harness/runtime/", "harness/execution/", "packages/ai/"] },
|
||||
"./harness/env/nodejs": { maxFiles: 5, forbid: ["packages/ai/", "harness/runtime/"] },
|
||||
"./harness/session": { maxFiles: 25, forbid: ["harness/runtime/", "harness/execution/", "packages/ai/src/index.ts"] },
|
||||
},
|
||||
};
|
||||
|
||||
const SPEC = /(?:^|\n)\s*(?:import|export)\s+(?!type\s)([^;]*?\sfrom\s*)?["']([^"']+)["']/g;
|
||||
|
||||
function resolveSpec(spec, fromFile) {
|
||||
if (spec.startsWith("node:")) return null;
|
||||
if (spec.startsWith(".")) {
|
||||
const base = resolve(dirname(fromFile), spec);
|
||||
for (const candidate of [base, `${base}.ts`, `${base}/index.ts`]) {
|
||||
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
for (const [name, src] of Object.entries(WORKSPACE)) {
|
||||
if (spec === name) return resolve(ROOT, src, "index.ts");
|
||||
if (!spec.startsWith(`${name}/`)) continue;
|
||||
const tail = spec.slice(name.length + 1);
|
||||
for (const candidate of [`${tail}.ts`, `${tail}/index.ts`, tail]) {
|
||||
const file = resolve(ROOT, src, candidate);
|
||||
if (existsSync(file) && statSync(file).isFile()) return file;
|
||||
}
|
||||
}
|
||||
return null; // external dependency: not part of the workspace graph
|
||||
}
|
||||
|
||||
function walk(entryFile) {
|
||||
const seen = new Set();
|
||||
const queue = [entryFile];
|
||||
while (queue.length > 0) {
|
||||
const file = queue.pop();
|
||||
if (seen.has(file) || file.endsWith(".json")) continue;
|
||||
seen.add(file);
|
||||
for (const match of readFileSync(file, "utf8").matchAll(SPEC)) {
|
||||
const target = resolveSpec(match[2], file);
|
||||
if (target) queue.push(target);
|
||||
}
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
|
||||
/** `./dist/harness/context.js` in the exports map is `src/harness/context.ts` on disk. */
|
||||
function sourceFor(pkgDir, distPath) {
|
||||
const rel = distPath.replace(/^\.\/dist\//, "").replace(/\.js$/, ".ts");
|
||||
const file = resolve(ROOT, pkgDir, "src", rel);
|
||||
return existsSync(file) ? file : undefined;
|
||||
}
|
||||
|
||||
function expand(pkgDir, entry, target) {
|
||||
if (!entry.includes("*")) return [[entry, target]];
|
||||
const dir = resolve(ROOT, pkgDir, "src", dirname(target.replace(/^\.\/dist\//, "")));
|
||||
if (!existsSync(dir)) return [];
|
||||
return readdirSync(dir)
|
||||
.filter((name) => name.endsWith(".ts"))
|
||||
.map((name) => [entry.replace("*", name.replace(/\.ts$/, "")), target.replace("*", name.replace(/\.ts$/, ""))]);
|
||||
}
|
||||
|
||||
let failures = 0;
|
||||
for (const [pkgDir, budgets] of Object.entries(BUDGETS)) {
|
||||
const manifest = JSON.parse(readFileSync(resolve(ROOT, pkgDir, "package.json"), "utf8"));
|
||||
for (const [entry, budget] of Object.entries(budgets)) {
|
||||
const declared = manifest.exports?.[entry];
|
||||
if (!declared) {
|
||||
console.error(`${pkgDir} declares no export "${entry}" but a budget exists for it`);
|
||||
failures += 1;
|
||||
continue;
|
||||
}
|
||||
const target = typeof declared === "string" ? declared : declared.import;
|
||||
for (const [name, distPath] of expand(pkgDir, entry, target)) {
|
||||
const source = sourceFor(pkgDir, distPath);
|
||||
if (!source) {
|
||||
console.error(`${pkgDir} export "${name}" points at ${distPath}, which has no source file`);
|
||||
failures += 1;
|
||||
continue;
|
||||
}
|
||||
const graph = [...walk(source)].map((file) => relative(ROOT, file));
|
||||
if (graph.length > budget.maxFiles) {
|
||||
console.error(
|
||||
`${pkgDir} export "${name}" reaches ${graph.length} files, budget ${budget.maxFiles}\n` +
|
||||
graph.map((file) => ` ${file}`).join("\n"),
|
||||
);
|
||||
failures += 1;
|
||||
}
|
||||
for (const pattern of budget.forbid ?? []) {
|
||||
const hit = graph.filter((file) => file.includes(pattern));
|
||||
if (hit.length > 0) {
|
||||
console.error(`${pkgDir} export "${name}" must not reach ${pattern}:\n${hit.map((f) => ` ${f}`).join("\n")}`);
|
||||
failures += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
console.error(`\n${failures} entry-point budget violation(s).`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Entry point graphs are within budget.");
|
||||
Reference in New Issue
Block a user