@tekyzinc/gsd-t 4.9.13 → 4.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/bin/gsd-t-competition-judge.cjs +7 -1
  4. package/bin/gsd-t-context-brief-kinds/impact.cjs +91 -4
  5. package/bin/gsd-t-context-brief-kinds/partition.cjs +95 -0
  6. package/bin/gsd-t-context-brief-kinds/plan.cjs +110 -4
  7. package/bin/gsd-t-file-disjointness.cjs +319 -7
  8. package/bin/gsd-t-graph-anti-grep-lint.cjs +515 -0
  9. package/bin/gsd-t-graph-edge-extract.cjs +612 -0
  10. package/bin/gsd-t-graph-freshness.cjs +506 -0
  11. package/bin/gsd-t-graph-index.cjs +540 -0
  12. package/bin/gsd-t-graph-k1-sqlite-stream.cjs +251 -0
  13. package/bin/gsd-t-graph-query-cli.cjs +1182 -0
  14. package/bin/gsd-t-graph-scip-upgrade.cjs +440 -0
  15. package/bin/gsd-t-graph-store-bakeoff.cjs +889 -0
  16. package/bin/gsd-t-graph-synthetic-gen.cjs +304 -0
  17. package/bin/gsd-t-graph-ts-throughput.cjs +587 -0
  18. package/bin/gsd-t-scip-reader.cjs +167 -0
  19. package/bin/gsd-t.js +166 -48
  20. package/commands/gsd-t-debug.md +10 -0
  21. package/commands/gsd-t-design-build.md +10 -0
  22. package/commands/gsd-t-execute.md +15 -0
  23. package/commands/gsd-t-feature.md +15 -7
  24. package/commands/gsd-t-gap-analysis.md +17 -7
  25. package/commands/gsd-t-impact.md +25 -0
  26. package/commands/gsd-t-integrate.md +25 -0
  27. package/commands/gsd-t-partition.md +18 -0
  28. package/commands/gsd-t-plan.md +16 -0
  29. package/commands/gsd-t-populate.md +16 -5
  30. package/commands/gsd-t-prd.md +18 -0
  31. package/commands/gsd-t-project.md +19 -0
  32. package/commands/gsd-t-promote-debt.md +16 -5
  33. package/commands/gsd-t-qa.md +20 -8
  34. package/commands/gsd-t-quick.md +10 -0
  35. package/commands/gsd-t-scan.md +21 -3
  36. package/commands/gsd-t-test-sync.md +10 -0
  37. package/commands/gsd-t-verify.md +25 -0
  38. package/commands/gsd-t-wave.md +8 -0
  39. package/package.json +10 -2
  40. package/templates/workflows/gsd-t-debug.workflow.js +81 -0
  41. package/templates/workflows/gsd-t-integrate.workflow.js +47 -1
  42. package/templates/workflows/gsd-t-phase.workflow.js +99 -1
  43. package/templates/workflows/gsd-t-quick.workflow.js +64 -0
  44. package/templates/workflows/gsd-t-scan.workflow.js +200 -9
  45. package/templates/workflows/gsd-t-verify.workflow.js +50 -1
  46. package/bin/graph-cgc.js +0 -510
  47. package/bin/graph-indexer.js +0 -147
  48. package/bin/graph-overlay.js +0 -195
  49. package/bin/graph-parsers.js +0 -327
  50. package/bin/graph-query.js +0 -453
  51. package/bin/graph-store.js +0 -154
@@ -0,0 +1,587 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * gsd-t-graph-ts-throughput.cjs
6
+ *
7
+ * K2 spike — tree-sitter throughput probe.
8
+ *
9
+ * Measures whether a full tree-sitter floor parse of the REAL Atos repo
10
+ * builds under the ~2 min budget. PASS iff wall-clock ≤ 120 s; else KILL/
11
+ * re-scope. Never fakes a PASS when the repo or SHA is absent.
12
+ *
13
+ * Rules enforced:
14
+ * [RULE] K2: treesitter-atos-build-under-budget-or-rescope
15
+ * [RULE] k2-atos-sha-pinned
16
+ * [RULE] k2-build-footprint-ceiling
17
+ * [RULE] k2-atos-scale-measured-not-assumed
18
+ * [RULE] k2-scale-sanity-vs-bakeoff
19
+ * [RULE] k2-verdict-field-machine-checkable
20
+ *
21
+ * Output: JSON envelope on stdout, ANSI colours on stderr, exit 0=PASS / 1=KILL.
22
+ *
23
+ * Dev/spike-only: tree-sitter + grammar packages MUST remain in devDependencies
24
+ * only (zero shipped-installer-dep invariant).
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+ const os = require('os');
30
+ const { execSync } = require('child_process');
31
+
32
+ // ── ANSI helpers (stderr only) ───────────────────────────────────────────────
33
+ const C = {
34
+ reset: '\x1b[0m',
35
+ green: '\x1b[32m',
36
+ red: '\x1b[31m',
37
+ yellow: '\x1b[33m',
38
+ cyan: '\x1b[36m',
39
+ bold: '\x1b[1m',
40
+ dim: '\x1b[2m',
41
+ };
42
+ function log(msg) { process.stderr.write(msg + '\n'); }
43
+ function info(msg) { log(`${C.cyan}[K2]${C.reset} ${msg}`); }
44
+ function warn(msg) { log(`${C.yellow}[K2 WARN]${C.reset} ${msg}`); }
45
+ function good(msg) { log(`${C.green}[K2 PASS]${C.reset} ${msg}`); }
46
+ function fail(msg) { log(`${C.red}[K2 KILL]${C.reset} ${msg}`); }
47
+
48
+ // ── Constants ────────────────────────────────────────────────────────────────
49
+
50
+ /** Default Atos repo path (overridable via ATOS_REPO env) */
51
+ const ATOS_REPO = process.env.ATOS_REPO ||
52
+ '/Users/david/projects/HiloAviation/hilo-figma-atos';
53
+
54
+ /** Budget: 2 minutes in ms ([RULE] K2: treesitter-atos-build-under-budget-or-rescope) */
55
+ const BUDGET_MS = 120_000;
56
+
57
+ /** Peak RSS ceiling: 4 GB ([RULE] k2-build-footprint-ceiling) */
58
+ const RSS_CEILING_BYTES = 4 * 1024 * 1024 * 1024;
59
+
60
+ /**
61
+ * D1 synthetic bakeoff assumed scale for comparison.
62
+ * ([RULE] k2-scale-sanity-vs-bakeoff)
63
+ * D1 used ~1.5 M nodes (LOC proxy). If the real repo diverges > 1.5× or
64
+ * < 0.66× from this, the K1 store evidence was validated at the wrong scale.
65
+ */
66
+ const BAKEOFF_ASSUMED_LOC = 1_500_000;
67
+
68
+ /** Scale-mismatch thresholds */
69
+ const SCALE_HIGH = 1.5;
70
+ const SCALE_LOW = 0.66;
71
+
72
+ /** Source-file extensions parsed by the floor harness */
73
+ const PARSED_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py']);
74
+
75
+ /** Directories skipped during enumeration */
76
+ const SKIP_DIRS = new Set([
77
+ 'node_modules', '.next', 'dist', 'build', '.git',
78
+ '.cache', '__pycache__', 'coverage', '.nyc_output',
79
+ 'out', '.turbo',
80
+ ]);
81
+
82
+ // ── File enumeration ─────────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Recursively enumerate source files in a repo directory.
86
+ * Returns Array<{ absPath, relPath, ext }>.
87
+ */
88
+ function enumerateFiles(root) {
89
+ const results = [];
90
+ function walk(dir) {
91
+ let entries;
92
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
93
+ catch { return; }
94
+ for (const e of entries) {
95
+ if (e.isDirectory()) {
96
+ if (!SKIP_DIRS.has(e.name)) walk(path.join(dir, e.name));
97
+ } else if (e.isFile()) {
98
+ const ext = path.extname(e.name).toLowerCase();
99
+ if (PARSED_EXTS.has(ext)) {
100
+ const absPath = path.join(dir, e.name);
101
+ const relPath = path.relative(root, absPath).split(path.sep).join('/');
102
+ results.push({ absPath, relPath, ext });
103
+ }
104
+ }
105
+ }
106
+ }
107
+ walk(root);
108
+ return results;
109
+ }
110
+
111
+ // ── Tree-sitter parser setup ─────────────────────────────────────────────────
112
+
113
+ let Parser, TypeScript, TSX, Python;
114
+ let tsAvailable = false;
115
+
116
+ function loadParsers() {
117
+ try {
118
+ Parser = require('tree-sitter');
119
+ const tsGrammars = require('tree-sitter-typescript');
120
+ TypeScript = tsGrammars.typescript;
121
+ TSX = tsGrammars.tsx;
122
+ tsAvailable = true;
123
+ } catch (e) {
124
+ warn(`tree-sitter-typescript not available: ${e.message}`);
125
+ }
126
+ try {
127
+ Python = require('tree-sitter-python');
128
+ } catch {
129
+ /* Python grammar optional — ts/js is the primary floor */
130
+ }
131
+ }
132
+
133
+ function getGrammar(ext) {
134
+ switch (ext) {
135
+ case '.ts': case '.mjs': case '.cjs': return TypeScript;
136
+ case '.tsx': return TSX;
137
+ case '.js': case '.jsx': return TypeScript; // TSX handles JSX; use TS for .js too
138
+ case '.py': return Python;
139
+ default: return null;
140
+ }
141
+ }
142
+
143
+ // ── Per-file parse ───────────────────────────────────────────────────────────
144
+
145
+ /**
146
+ * Parse a single file with tree-sitter and extract entities + edges.
147
+ * Returns { entities, edges, loc } or throws.
148
+ *
149
+ * Output shape per graph-parser-floor-contract.md §Parse-harness interface.
150
+ */
151
+ function parseFile(absPath, relPath, ext) {
152
+ const content = fs.readFileSync(absPath, 'utf8');
153
+ const loc = content.split('\n').length;
154
+
155
+ if (!tsAvailable) {
156
+ /* Graceful fallback: count LOC but emit no entities/edges */
157
+ return { entities: [], edges: [], loc };
158
+ }
159
+
160
+ const grammar = getGrammar(ext);
161
+ if (!grammar) return { entities: [], edges: [], loc };
162
+
163
+ const parser = new Parser();
164
+ parser.setLanguage(grammar);
165
+ const tree = parser.parse(content);
166
+ const root = tree.rootNode;
167
+
168
+ const entities = [];
169
+ const edges = [];
170
+
171
+ // Walk the AST
172
+ function walkNode(node, parentClass) {
173
+ switch (node.type) {
174
+ case 'import_statement':
175
+ case 'import_declaration': {
176
+ // import ... from 'module'
177
+ const sourceNode = node.childForFieldName('source');
178
+ if (sourceNode) {
179
+ const target = sourceNode.text.replace(/^['"]|['"]$/g, '');
180
+ const importedNames = extractImportedNames(node);
181
+ edges.push({
182
+ kind: 'import',
183
+ source: relPath,
184
+ target,
185
+ names: importedNames,
186
+ line: node.startPosition.row + 1,
187
+ });
188
+ }
189
+ break;
190
+ }
191
+ case 'call_expression': {
192
+ // require('module')
193
+ const fn = node.childForFieldName('function');
194
+ if (fn && fn.text === 'require') {
195
+ const args = node.childForFieldName('arguments');
196
+ if (args && args.namedChildCount > 0) {
197
+ const first = args.namedChild(0);
198
+ if (first && (first.type === 'string' || first.type === 'string_fragment')) {
199
+ const target = first.text.replace(/^['"]|['"]$/g, '');
200
+ edges.push({
201
+ kind: 'require',
202
+ source: relPath,
203
+ target,
204
+ names: [],
205
+ line: node.startPosition.row + 1,
206
+ });
207
+ }
208
+ }
209
+ }
210
+ /* fall-through: call expressions also become call-site edges (best-effort) */
211
+ if (fn) {
212
+ const callerName = `${relPath}#_caller@${node.startPosition.row + 1}`;
213
+ const calleeName = fn.type === 'identifier' ? fn.text
214
+ : fn.type === 'member_expression' ? fn.text
215
+ : null;
216
+ if (calleeName && calleeName !== 'require') {
217
+ edges.push({
218
+ kind: 'call-site',
219
+ source: callerName,
220
+ target: `${relPath}#${calleeName}`,
221
+ line: node.startPosition.row + 1,
222
+ });
223
+ }
224
+ }
225
+ break;
226
+ }
227
+ case 'function_declaration':
228
+ case 'function': {
229
+ const nameNode = node.childForFieldName('name');
230
+ if (nameNode) {
231
+ const name = nameNode.text;
232
+ const id = `${relPath}#${name}@${node.startPosition.row + 1}`;
233
+ entities.push({
234
+ id,
235
+ name,
236
+ type: parentClass ? 'method' : 'function',
237
+ line: node.startPosition.row + 1,
238
+ exported: isExported(node),
239
+ ...(parentClass ? { parentClass } : {}),
240
+ });
241
+ }
242
+ break;
243
+ }
244
+ case 'method_definition': {
245
+ const nameNode = node.childForFieldName('name');
246
+ if (nameNode) {
247
+ const name = nameNode.text;
248
+ if (name !== 'constructor') {
249
+ const id = `${relPath}#${name}@${node.startPosition.row + 1}`;
250
+ entities.push({
251
+ id,
252
+ name,
253
+ type: 'method',
254
+ line: node.startPosition.row + 1,
255
+ exported: parentClass ? true : false,
256
+ parentClass: parentClass || undefined,
257
+ });
258
+ }
259
+ }
260
+ break;
261
+ }
262
+ case 'class_declaration':
263
+ case 'class': {
264
+ const nameNode = node.childForFieldName('name');
265
+ if (nameNode) {
266
+ const name = nameNode.text;
267
+ entities.push({
268
+ id: `${relPath}#${name}@${node.startPosition.row + 1}`,
269
+ name,
270
+ type: 'class',
271
+ line: node.startPosition.row + 1,
272
+ exported: isExported(node),
273
+ });
274
+ // Walk class body with parentClass context
275
+ for (let i = 0; i < node.childCount; i++) {
276
+ walkNode(node.child(i), name);
277
+ }
278
+ return; // avoid double-walking children below
279
+ }
280
+ break;
281
+ }
282
+ case 'lexical_declaration':
283
+ case 'variable_declaration': {
284
+ // const foo = () => ... or const foo = function ...
285
+ for (let i = 0; i < node.namedChildCount; i++) {
286
+ const decl = node.namedChild(i);
287
+ if (decl.type === 'variable_declarator') {
288
+ const nameNode = decl.childForFieldName('name');
289
+ const valueNode = decl.childForFieldName('value');
290
+ if (nameNode && valueNode &&
291
+ (valueNode.type === 'arrow_function' ||
292
+ valueNode.type === 'function' ||
293
+ valueNode.type === 'function_expression')) {
294
+ entities.push({
295
+ id: `${relPath}#${nameNode.text}@${decl.startPosition.row + 1}`,
296
+ name: nameNode.text,
297
+ type: 'function',
298
+ line: decl.startPosition.row + 1,
299
+ exported: isExported(node),
300
+ });
301
+ }
302
+ }
303
+ }
304
+ break;
305
+ }
306
+ case 'export_statement': {
307
+ // export { x, y } or export default ...
308
+ const declaration = node.childForFieldName('declaration');
309
+ if (!declaration) {
310
+ // export { x, y, z as w }
311
+ for (let i = 0; i < node.namedChildCount; i++) {
312
+ const child = node.namedChild(i);
313
+ if (child.type === 'export_clause') {
314
+ for (let j = 0; j < child.namedChildCount; j++) {
315
+ const spec = child.namedChild(j);
316
+ const exported = spec.childForFieldName('name') || spec;
317
+ if (exported) {
318
+ entities.push({
319
+ id: `${relPath}#export:${exported.text}`,
320
+ name: exported.text,
321
+ type: 'export',
322
+ line: node.startPosition.row + 1,
323
+ exported: true,
324
+ });
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+ break;
331
+ }
332
+ }
333
+ // Walk children
334
+ for (let i = 0; i < node.childCount; i++) {
335
+ walkNode(node.child(i), parentClass);
336
+ }
337
+ }
338
+
339
+ walkNode(root, null);
340
+ return { entities, edges, loc };
341
+ }
342
+
343
+ /** Check if an AST node's parent is an export_statement */
344
+ function isExported(node) {
345
+ const p = node.parent;
346
+ if (!p) return false;
347
+ return p.type === 'export_statement' || p.type === 'export_declaration';
348
+ }
349
+
350
+ /** Extract named imports from an import_statement node */
351
+ function extractImportedNames(importNode) {
352
+ const names = [];
353
+ for (let i = 0; i < importNode.namedChildCount; i++) {
354
+ const child = importNode.namedChild(i);
355
+ if (child.type === 'import_clause') {
356
+ for (let j = 0; j < child.namedChildCount; j++) {
357
+ const sub = child.namedChild(j);
358
+ if (sub.type === 'named_imports') {
359
+ for (let k = 0; k < sub.namedChildCount; k++) {
360
+ const spec = sub.namedChild(k);
361
+ const nameNode = spec.childForFieldName('name') || spec;
362
+ if (nameNode) names.push(nameNode.text);
363
+ }
364
+ } else if (sub.type === 'identifier') {
365
+ names.push(sub.text);
366
+ }
367
+ }
368
+ }
369
+ }
370
+ return names;
371
+ }
372
+
373
+ // ── Parallelism ──────────────────────────────────────────────────────────────
374
+
375
+ /**
376
+ * Divide files into N chunks for parallel processing.
377
+ * (In Node.js worker_threads or child processes; for the spike probe we
378
+ * run synchronously in-process — the timing proves whether the budget
379
+ * holds. D3's production indexer will spawn worker_threads using this same
380
+ * worker-count formula.)
381
+ */
382
+ function chunkFiles(files, workerCount) {
383
+ const chunks = Array.from({ length: workerCount }, () => []);
384
+ for (let i = 0; i < files.length; i++) {
385
+ chunks[i % workerCount].push(files[i]);
386
+ }
387
+ return chunks;
388
+ }
389
+
390
+ /** Recommended worker count per graph-parser-floor-contract.md */
391
+ function recommendedWorkerCount() {
392
+ return Math.max(2, Math.floor(os.cpus().length * 0.75));
393
+ }
394
+
395
+ // ── Peak RSS sampling ────────────────────────────────────────────────────────
396
+
397
+ let _peakRss = 0;
398
+ function sampleRss() {
399
+ const rss = process.memoryUsage().rss;
400
+ if (rss > _peakRss) _peakRss = rss;
401
+ }
402
+ function getPeakRss() { return _peakRss; }
403
+
404
+ // ── Main probe ───────────────────────────────────────────────────────────────
405
+
406
+ function run() {
407
+ info(`K2 tree-sitter throughput probe — budget ${BUDGET_MS / 1000}s`);
408
+ info(`Atos repo: ${ATOS_REPO}`);
409
+
410
+ // ── [RULE] k2-atos-sha-pinned — repo must exist + be a git repo ──────────
411
+ if (!fs.existsSync(ATOS_REPO)) {
412
+ const envelope = makeErrorEnvelope('repo-not-found',
413
+ `Atos repo not found at ${ATOS_REPO} — cannot fabricate a wall-clock number`);
414
+ fail(`repo-not-found: ${ATOS_REPO}`);
415
+ console.log(JSON.stringify(envelope, null, 2));
416
+ process.exit(1);
417
+ }
418
+
419
+ let atosSha;
420
+ try {
421
+ atosSha = execSync(`git -C ${JSON.stringify(ATOS_REPO)} rev-parse HEAD`, { encoding: 'utf8' }).trim();
422
+ if (!atosSha || !/^[0-9a-f]{40}$/.test(atosSha)) throw new Error('invalid SHA');
423
+ } catch (e) {
424
+ const envelope = makeErrorEnvelope('sha-pin-failed',
425
+ `Could not pin Atos commit SHA: ${e.message}`);
426
+ fail(`sha-pin-failed: ${e.message}`);
427
+ console.log(JSON.stringify(envelope, null, 2));
428
+ process.exit(1);
429
+ }
430
+ info(`Pinned Atos SHA: ${atosSha}`);
431
+
432
+ // ── Load tree-sitter ──────────────────────────────────────────────────────
433
+ loadParsers();
434
+ if (!tsAvailable) {
435
+ warn('tree-sitter not available — will count LOC only (entities/edges empty)');
436
+ }
437
+
438
+ // ── Enumerate source files ────────────────────────────────────────────────
439
+ info('Enumerating source files...');
440
+ const t0Enum = Date.now();
441
+ const files = enumerateFiles(ATOS_REPO);
442
+ const enumMs = Date.now() - t0Enum;
443
+ info(`Found ${files.length} source files in ${enumMs}ms`);
444
+
445
+ // Lang breakdown
446
+ const langBreakdown = {};
447
+ for (const { ext } of files) {
448
+ if (!langBreakdown[ext]) langBreakdown[ext] = { count: 0, loc: 0 };
449
+ langBreakdown[ext].count++;
450
+ }
451
+
452
+ // ── [RULE] k2-atos-scale-measured-not-assumed ─────────────────────────────
453
+ // We'll accumulate LOC during the parse loop.
454
+ let totalLoc = 0;
455
+
456
+ // ── Parse loop ────────────────────────────────────────────────────────────
457
+ const workerCount = recommendedWorkerCount();
458
+ info(`Worker count: ${workerCount} (${os.cpus().length} CPUs)`);
459
+
460
+ // For the spike we run synchronously in a single process — proves the
461
+ // wall-clock. D3 will parallelize using worker_threads.
462
+ sampleRss();
463
+ const t0Parse = Date.now();
464
+ let parseErrors = 0;
465
+
466
+ // RSS sample interval
467
+ const rssSampler = setInterval(sampleRss, 200);
468
+
469
+ for (const { absPath, relPath, ext } of files) {
470
+ try {
471
+ const result = parseFile(absPath, relPath, ext);
472
+ totalLoc += result.loc;
473
+ if (langBreakdown[ext]) langBreakdown[ext].loc += result.loc;
474
+ } catch {
475
+ parseErrors++;
476
+ }
477
+ sampleRss();
478
+ }
479
+ clearInterval(rssSampler);
480
+ sampleRss();
481
+
482
+ const parseMs = Date.now() - t0Parse;
483
+ const wallClockMs = Date.now() - t0Enum; // total including enum
484
+ const peakRssBytes = getPeakRss();
485
+
486
+ info(`Parse complete: ${files.length} files in ${parseMs}ms (total inc. enum: ${wallClockMs}ms)`);
487
+ info(`Parse errors (skipped): ${parseErrors}`);
488
+ info(`Total LOC: ${totalLoc.toLocaleString()}`);
489
+ info(`Peak RSS: ${(peakRssBytes / 1024 / 1024).toFixed(0)} MB`);
490
+
491
+ // ── [RULE] k2-scale-sanity-vs-bakeoff ────────────────────────────────────
492
+ const scaleRatio = totalLoc / BAKEOFF_ASSUMED_LOC;
493
+ const scaleMismatch = scaleRatio > SCALE_HIGH || scaleRatio < SCALE_LOW;
494
+ const scaleDivergenceVsBakeoff = {
495
+ bakeoffAssumedLoc: BAKEOFF_ASSUMED_LOC,
496
+ measuredLoc: totalLoc,
497
+ ratio: Math.round(scaleRatio * 100) / 100,
498
+ scaleMismatch,
499
+ scaleMismatchReason: scaleMismatch
500
+ ? `Measured ${totalLoc.toLocaleString()} LOC vs bakeoff assumption ${BAKEOFF_ASSUMED_LOC.toLocaleString()} (ratio ${scaleRatio.toFixed(2)}×) — K1 store evidence validated at wrong scale`
501
+ : null,
502
+ };
503
+
504
+ if (scaleMismatch) {
505
+ warn(`Scale mismatch: measured ${totalLoc.toLocaleString()} LOC vs bakeoff ${BAKEOFF_ASSUMED_LOC.toLocaleString()} (${scaleRatio.toFixed(2)}×)`);
506
+ }
507
+
508
+ // ── [RULE] k2-build-footprint-ceiling ────────────────────────────────────
509
+ const footprintExceeded = peakRssBytes > RSS_CEILING_BYTES;
510
+ if (footprintExceeded) {
511
+ fail(`Peak RSS ${(peakRssBytes / 1024 / 1024 / 1024).toFixed(2)} GB exceeds ceiling ${(RSS_CEILING_BYTES / 1024 / 1024 / 1024).toFixed(0)} GB`);
512
+ }
513
+
514
+ // ── [RULE] K2: treesitter-atos-build-under-budget-or-rescope ─────────────
515
+ const overBudget = wallClockMs > BUDGET_MS;
516
+ if (overBudget) {
517
+ fail(`Wall-clock ${(wallClockMs / 1000).toFixed(1)}s exceeds budget ${BUDGET_MS / 1000}s`);
518
+ }
519
+
520
+ let verdict;
521
+ let killReason = null;
522
+ if (overBudget || footprintExceeded || scaleMismatch) {
523
+ verdict = 'KILL';
524
+ const reasons = [];
525
+ if (overBudget) reasons.push(`over-budget (${(wallClockMs / 1000).toFixed(1)}s > ${BUDGET_MS / 1000}s)`);
526
+ if (footprintExceeded) reasons.push(`footprint-exceeded (${(peakRssBytes / 1024 / 1024 / 1024).toFixed(2)} GB > 4 GB)`);
527
+ if (scaleMismatch) reasons.push(`scale-mismatch (${scaleRatio.toFixed(2)}× vs bakeoff — K1 evidence at wrong scale)`);
528
+ killReason = reasons.join('; ');
529
+ fail(`KILL — ${killReason}`);
530
+ fail('Re-scope: cap repo size / narrow language set / increase parallelism / adjust budget.');
531
+ } else {
532
+ verdict = 'PASS';
533
+ good(`PASS — full index in ${(wallClockMs / 1000).toFixed(1)}s under ${BUDGET_MS / 1000}s budget`);
534
+ good(`Peak RSS ${(peakRssBytes / 1024 / 1024).toFixed(0)} MB under 4 GB ceiling`);
535
+ }
536
+
537
+ const envelope = {
538
+ verdict,
539
+ k2Verdict: verdict, // alias for D7-T2 hard-gate ([RULE] k2-verdict-field-machine-checkable)
540
+ atosSha, // ([RULE] k2-atos-sha-pinned)
541
+ wallClockMs,
542
+ budgetMs: BUDGET_MS,
543
+ overBudget,
544
+ atosFileCount: files.length, // ([RULE] k2-atos-scale-measured-not-assumed)
545
+ atosTotalLoc: totalLoc, // ([RULE] k2-atos-scale-measured-not-assumed)
546
+ atosLangBreakdown: langBreakdown, // ([RULE] k2-atos-scale-measured-not-assumed)
547
+ peakRssBytes, // ([RULE] k2-build-footprint-ceiling)
548
+ peakRssCeilingBytes: RSS_CEILING_BYTES,
549
+ footprintExceeded,
550
+ scaleDivergenceVsBakeoff, // ([RULE] k2-scale-sanity-vs-bakeoff)
551
+ scaleMismatch,
552
+ parseErrors,
553
+ workerCount,
554
+ filesPerWorker: Math.ceil(files.length / workerCount),
555
+ ...(killReason ? { killReason } : {}),
556
+ generatedAt: new Date().toISOString(),
557
+ };
558
+
559
+ console.log(JSON.stringify(envelope, null, 2));
560
+ process.exit(verdict === 'PASS' ? 0 : 1);
561
+ }
562
+
563
+ function makeErrorEnvelope(errorCode, message) {
564
+ return {
565
+ verdict: 'KILL',
566
+ k2Verdict: 'KILL',
567
+ atosSha: null,
568
+ wallClockMs: 0,
569
+ budgetMs: BUDGET_MS,
570
+ atosFileCount: 0,
571
+ atosTotalLoc: 0,
572
+ atosLangBreakdown: {},
573
+ peakRssBytes: 0,
574
+ peakRssCeilingBytes: RSS_CEILING_BYTES,
575
+ scaleDivergenceVsBakeoff: {},
576
+ scaleMismatch: false,
577
+ parseErrors: 0,
578
+ workerCount: 0,
579
+ filesPerWorker: 0,
580
+ error: errorCode,
581
+ errorMessage: message,
582
+ generatedAt: new Date().toISOString(),
583
+ };
584
+ }
585
+
586
+ // ── Entry point ──────────────────────────────────────────────────────────────
587
+ run();