@tekyzinc/gsd-t 4.9.14 → 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.
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/bin/gsd-t-competition-judge.cjs +7 -1
- package/bin/gsd-t-context-brief-kinds/impact.cjs +91 -4
- package/bin/gsd-t-context-brief-kinds/partition.cjs +95 -0
- package/bin/gsd-t-context-brief-kinds/plan.cjs +110 -4
- package/bin/gsd-t-file-disjointness.cjs +319 -7
- package/bin/gsd-t-graph-anti-grep-lint.cjs +515 -0
- package/bin/gsd-t-graph-edge-extract.cjs +612 -0
- package/bin/gsd-t-graph-freshness.cjs +506 -0
- package/bin/gsd-t-graph-index.cjs +540 -0
- package/bin/gsd-t-graph-k1-sqlite-stream.cjs +251 -0
- package/bin/gsd-t-graph-query-cli.cjs +1182 -0
- package/bin/gsd-t-graph-scip-upgrade.cjs +440 -0
- package/bin/gsd-t-graph-store-bakeoff.cjs +889 -0
- package/bin/gsd-t-graph-synthetic-gen.cjs +304 -0
- package/bin/gsd-t-graph-ts-throughput.cjs +587 -0
- package/bin/gsd-t-scip-reader.cjs +167 -0
- package/bin/gsd-t.js +166 -48
- package/commands/gsd-t-debug.md +10 -0
- package/commands/gsd-t-design-build.md +10 -0
- package/commands/gsd-t-execute.md +15 -0
- package/commands/gsd-t-feature.md +15 -7
- package/commands/gsd-t-gap-analysis.md +17 -7
- package/commands/gsd-t-impact.md +25 -0
- package/commands/gsd-t-integrate.md +25 -0
- package/commands/gsd-t-partition.md +18 -0
- package/commands/gsd-t-plan.md +16 -0
- package/commands/gsd-t-populate.md +16 -5
- package/commands/gsd-t-prd.md +18 -0
- package/commands/gsd-t-project.md +19 -0
- package/commands/gsd-t-promote-debt.md +16 -5
- package/commands/gsd-t-qa.md +20 -8
- package/commands/gsd-t-quick.md +10 -0
- package/commands/gsd-t-scan.md +21 -3
- package/commands/gsd-t-test-sync.md +10 -0
- package/commands/gsd-t-verify.md +25 -0
- package/commands/gsd-t-wave.md +8 -0
- package/package.json +10 -2
- package/templates/workflows/gsd-t-debug.workflow.js +81 -0
- package/templates/workflows/gsd-t-integrate.workflow.js +47 -1
- package/templates/workflows/gsd-t-phase.workflow.js +84 -0
- package/templates/workflows/gsd-t-quick.workflow.js +64 -0
- package/templates/workflows/gsd-t-scan.workflow.js +200 -9
- package/templates/workflows/gsd-t-verify.workflow.js +50 -1
- package/bin/graph-cgc.js +0 -510
- package/bin/graph-indexer.js +0 -147
- package/bin/graph-overlay.js +0 -195
- package/bin/graph-parsers.js +0 -327
- package/bin/graph-query.js +0 -453
- 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();
|