File size: 3,660 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#!/usr/bin/env node
// check-build-scope.mjs β€” guards against worktrees / cruft leaking into the
// TypeScript build scope and poisoning `next build`.
//
// Root cause of the 2026-06-25 build OOM/GC-livelock incident: `tsconfig.json`
// uses `include: ["**/*.ts","**/*.tsx","**/*.js","**/*.jsx"]` (recursive glob),
// and 69 git worktrees under `.claude/worktrees/` were NOT in `exclude` β€” so the
// TS scope ballooned to 355,215 files (vs 4,547 real source files) and `next build`
// processed ~70x the codebase, OOMing even at a 64 GB heap. The CI built fine
// because its checkout is clean.
//
// This gate counts the .ts/.tsx/.js/.jsx files that tsconfig's include would match
// (respecting its top-level exclude dirs) and FAILS if the count exceeds a
// threshold β€” catching a leak BEFORE it detonates the build. Heap size is NOT the
// fix for an over-large scope; a clean scope is.
//
// Usage: node scripts/check/check-build-scope.mjs [--max N] [--json]

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 args = process.argv.slice(2);
const MAX = Number(args[args.indexOf("--max") + 1]) || 12000;
const JSON_OUT = args.includes("--json");

const EXT = new Set([".ts", ".tsx", ".js", ".jsx"]);

// Read tsconfig.json exclude (the source of truth for what's out of scope).
const tsconfigPath = path.join(ROOT, "tsconfig.json");
let exclude = [];
try {
  exclude = JSON.parse(fs.readFileSync(tsconfigPath, "utf8")).exclude || [];
} catch {
  console.error("[build-scope] could not read tsconfig.json exclude β€” aborting");
  process.exit(2);
}
// Always skip VCS + the exclude dirs (normalise to bare top-level names).
const SKIP_DIRS = new Set([".git", ...exclude.map((e) => e.replace(/^\.\//, "").replace(/\/.*$/, ""))]);

let count = 0;
const byTop = {};
function walk(dir, top) {
  let entries;
  try {
    entries = fs.readdirSync(dir, { withFileTypes: true });
  } catch {
    return;
  }
  for (const e of entries) {
    if (e.isSymbolicLink()) continue; // don't follow symlinks (e.g. node_modules)
    const full = path.join(dir, e.name);
    if (e.isDirectory()) {
      walk(full, top);
    } else if (EXT.has(path.extname(e.name))) {
      count++;
      byTop[top] = (byTop[top] || 0) + 1;
    }
  }
}

for (const e of fs.readdirSync(ROOT, { withFileTypes: true })) {
  if (!e.isDirectory()) {
    if (EXT.has(path.extname(e.name))) {
      count++;
      byTop["(root)"] = (byTop["(root)"] || 0) + 1;
    }
    continue;
  }
  if (SKIP_DIRS.has(e.name)) continue;
  walk(path.join(ROOT, e.name), e.name);
}

if (JSON_OUT) {
  console.log(JSON.stringify({ count, max: MAX, byTop }, null, 2));
} else {
  console.log(`[build-scope] ${count} .ts/.tsx/.js/.jsx files in tsconfig scope (max ${MAX})`);
  const top = Object.entries(byTop)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 8);
  for (const [d, n] of top) console.log(`            ${String(n).padStart(7)}  ${d}`);
}

if (count > MAX) {
  console.error(
    `\n❌ [build-scope] scope of ${count} files exceeds ${MAX} β€” something is leaking into the\n` +
      `   tsconfig include scope (worktree, vendored copy, or build output). This poisons\n` +
      `   \`next build\` (OOM/GC-livelock). Add the offending dir to tsconfig.json "exclude"\n` +
      `   (and .dockerignore). Worktrees MUST live under .claude/worktrees/ (already excluded).\n` +
      `   Heap size does NOT fix this β€” a clean scope does. See incident 2026-06-25.`
  );
  process.exit(1);
}
console.log("βœ… [build-scope] OK β€” no leak into the build scope.");