mirror of
https://github.com/earendil-works/pi.git
synced 2026-09-28 14:13:13 +08:00
142 lines
5.3 KiB
JavaScript
142 lines
5.3 KiB
JavaScript
#!/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-durable": "packages/durable/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.");
|