@tekyzinc/gsd-t 4.9.14 → 4.10.11

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 (52) hide show
  1. package/CHANGELOG.md +30 -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 +622 -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-require-store.cjs +89 -0
  19. package/bin/gsd-t-scip-reader.cjs +167 -0
  20. package/bin/gsd-t.js +170 -48
  21. package/commands/gsd-t-debug.md +10 -0
  22. package/commands/gsd-t-design-build.md +10 -0
  23. package/commands/gsd-t-execute.md +15 -0
  24. package/commands/gsd-t-feature.md +15 -7
  25. package/commands/gsd-t-gap-analysis.md +17 -7
  26. package/commands/gsd-t-impact.md +25 -0
  27. package/commands/gsd-t-integrate.md +25 -0
  28. package/commands/gsd-t-partition.md +18 -0
  29. package/commands/gsd-t-plan.md +16 -0
  30. package/commands/gsd-t-populate.md +16 -5
  31. package/commands/gsd-t-prd.md +18 -0
  32. package/commands/gsd-t-project.md +19 -0
  33. package/commands/gsd-t-promote-debt.md +16 -5
  34. package/commands/gsd-t-qa.md +20 -8
  35. package/commands/gsd-t-quick.md +10 -0
  36. package/commands/gsd-t-scan.md +21 -3
  37. package/commands/gsd-t-test-sync.md +10 -0
  38. package/commands/gsd-t-verify.md +25 -0
  39. package/commands/gsd-t-wave.md +8 -0
  40. package/package.json +12 -2
  41. package/templates/workflows/gsd-t-debug.workflow.js +81 -0
  42. package/templates/workflows/gsd-t-integrate.workflow.js +47 -1
  43. package/templates/workflows/gsd-t-phase.workflow.js +84 -0
  44. package/templates/workflows/gsd-t-quick.workflow.js +64 -0
  45. package/templates/workflows/gsd-t-scan.workflow.js +200 -9
  46. package/templates/workflows/gsd-t-verify.workflow.js +50 -1
  47. package/bin/graph-cgc.js +0 -510
  48. package/bin/graph-indexer.js +0 -147
  49. package/bin/graph-overlay.js +0 -195
  50. package/bin/graph-parsers.js +0 -327
  51. package/bin/graph-query.js +0 -453
  52. package/bin/graph-store.js +0 -154
@@ -0,0 +1,622 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * gsd-t-graph-edge-extract.cjs
6
+ *
7
+ * M94 D3-T1 — Fresh edge extraction on the tree-sitter floor.
8
+ *
9
+ * Extracts entities + edges from a single source file using tree-sitter
10
+ * (NOT lifted from bin/graph-parsers.js — built FRESH on tree-sitter).
11
+ *
12
+ * Taxonomy per graph-parser-floor-contract.md §Edge/entity taxonomy:
13
+ * - import : file → file ES-module import edge
14
+ * - require : file → file CommonJS require edge
15
+ * - export : exported symbol entity
16
+ * - function: function entity (def site; includes arrow functions assigned to const)
17
+ * - class : class entity (def site)
18
+ * - method : method entity (sub-kind of function, parentClass field)
19
+ * - call-site: function → function call edge (best-effort; keyed by funcId at BOTH ends)
20
+ *
21
+ * Output shape per graph-parser-floor-contract.md §Per-file parse output shape.
22
+ *
23
+ * [RULE] who-calls-function-identity-disambiguated: call-graph edges are keyed
24
+ * by funcId = "file#function" at BOTH endpoints; same-named functions across
25
+ * files are DISTINCT. Per graph-store-schema-contract.md §Function-identity key.
26
+ *
27
+ * Exported API (for D3's indexer and D4's freshness re-index):
28
+ * extractEdges(absPath, relPath) → { file, entities, edges, loc }
29
+ *
30
+ * CLI usage (for testing):
31
+ * node bin/gsd-t-graph-edge-extract.cjs <file> [--repo-root <root>]
32
+ * Emits JSON envelope on stdout, ANSI on stderr, exit 0=ok / 1=error.
33
+ */
34
+
35
+ const fs = require('fs');
36
+ const path = require('path');
37
+
38
+ // ── ANSI helpers ─────────────────────────────────────────────────────────────
39
+
40
+ const C = {
41
+ reset: '\x1b[0m',
42
+ green: '\x1b[32m',
43
+ red: '\x1b[31m',
44
+ yellow: '\x1b[33m',
45
+ cyan: '\x1b[36m',
46
+ };
47
+ function log(msg) { process.stderr.write(msg + '\n'); }
48
+ function info(msg) { log(`${C.cyan}[D3]${C.reset} ${msg}`); }
49
+ function warn(msg) { log(`${C.yellow}[D3 WARN]${C.reset} ${msg}`); }
50
+ function errLog(msg) { log(`${C.red}[D3 ERR]${C.reset} ${msg}`); }
51
+
52
+ // ── Source-file extensions parsed by the floor ───────────────────────────────
53
+
54
+ const PARSED_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py']);
55
+
56
+ // ── Tree-sitter parser lazy loader ───────────────────────────────────────────
57
+
58
+ let _parsersLoaded = false;
59
+ let Parser, TSGrammars, TSX, Python;
60
+ let tsAvailable = false;
61
+ let pythonAvailable = false;
62
+
63
+ function ensureParsers() {
64
+ if (_parsersLoaded) return;
65
+ _parsersLoaded = true;
66
+ // M96: resolve the tree-sitter native modules via the multi-location resolver so
67
+ // a COPIED extractor (in a project's bin/) finds them in the GSD-T global package,
68
+ // not the project's own (absent) node_modules. The TS grammar is the MANDATORY
69
+ // floor parser — if it cannot load, FAIL LOUD. A silent fall-through to
70
+ // tsAvailable=false produced an empty graph (0 nodes/edges) that looked like a
71
+ // successful build — the exact silent-degrade this milestone exists to kill.
72
+ const { requireGraphDep } = require('./gsd-t-require-store.cjs');
73
+ try {
74
+ Parser = requireGraphDep('tree-sitter');
75
+ TSGrammars = requireGraphDep('tree-sitter-typescript');
76
+ TSX = TSGrammars.tsx;
77
+ tsAvailable = true;
78
+ } catch (e) {
79
+ throw new Error(
80
+ `code-graph floor parser unavailable: ${e.message} — the graph cannot be built ` +
81
+ `without tree-sitter. Reinstall GSD-T (npx @tekyzinc/gsd-t install).`
82
+ );
83
+ }
84
+ try {
85
+ Python = requireGraphDep('tree-sitter-python');
86
+ pythonAvailable = true;
87
+ } catch {
88
+ /* Python optional — TS/JS still index */
89
+ }
90
+ }
91
+
92
+ function getGrammar(ext) {
93
+ switch (ext) {
94
+ case '.ts':
95
+ case '.mjs':
96
+ case '.cjs':
97
+ return tsAvailable ? TSGrammars.typescript : null;
98
+ case '.tsx':
99
+ case '.jsx':
100
+ return tsAvailable ? TSX : null;
101
+ case '.js':
102
+ return tsAvailable ? TSGrammars.typescript : null;
103
+ case '.py':
104
+ return pythonAvailable ? Python : null;
105
+ default:
106
+ return null;
107
+ }
108
+ }
109
+
110
+ // ── AST helpers ───────────────────────────────────────────────────────────────
111
+
112
+ /**
113
+ * Walk an AST node's ancestor chain; return true if any ancestor matches type.
114
+ */
115
+ function hasAncestorOfType(node, ...types) {
116
+ let p = node.parent;
117
+ while (p) {
118
+ if (types.includes(p.type)) return true;
119
+ p = p.parent;
120
+ }
121
+ return false;
122
+ }
123
+
124
+ /**
125
+ * Return true if the node is a direct child of an export_statement /
126
+ * export_declaration, or if it's a variable_declarator under an exported
127
+ * lexical/variable_declaration.
128
+ */
129
+ function isExportedNode(node) {
130
+ const p = node.parent;
131
+ if (!p) return false;
132
+ if (p.type === 'export_statement' || p.type === 'export_declaration') return true;
133
+ // const/let/var: parent is variable_declaration, grandparent is export_statement
134
+ if ((p.type === 'lexical_declaration' || p.type === 'variable_declaration') &&
135
+ p.parent && (p.parent.type === 'export_statement' || p.parent.type === 'export_declaration')) {
136
+ return true;
137
+ }
138
+ return false;
139
+ }
140
+
141
+ // ── Extract import names from an import_statement node ───────────────────────
142
+
143
+ function extractImportedNames(importNode) {
144
+ const names = [];
145
+ for (let i = 0; i < importNode.namedChildCount; i++) {
146
+ const child = importNode.namedChild(i);
147
+ if (child.type === 'import_clause') {
148
+ for (let j = 0; j < child.namedChildCount; j++) {
149
+ const sub = child.namedChild(j);
150
+ if (sub.type === 'named_imports') {
151
+ for (let k = 0; k < sub.namedChildCount; k++) {
152
+ const spec = sub.namedChild(k);
153
+ const nameNode = spec.childForFieldName('name') || spec;
154
+ if (nameNode) names.push(nameNode.text);
155
+ }
156
+ } else if (sub.type === 'identifier') {
157
+ names.push(sub.text); // default import
158
+ }
159
+ }
160
+ }
161
+ }
162
+ return names;
163
+ }
164
+
165
+ // ── Deduce callee funcId from a call_expression ──────────────────────────────
166
+
167
+ /**
168
+ * For a call_expression, return the best-effort callee target funcId.
169
+ *
170
+ * Strategy: the call expression's callee is an identifier or member_expression.
171
+ * We cannot statically resolve the callee's file — that requires SCIP.
172
+ * So the target funcId is "UNRESOLVED#<calleeName>" as a floor-tier placeholder.
173
+ * SCIP upgrade will replace these with fully-resolved funcIds.
174
+ *
175
+ * [RULE] who-calls-function-identity-disambiguated: src is the funcId of the
176
+ * enclosing function (or a synthetic _anon@line if top-level); dst is the
177
+ * callee's best-effort funcId.
178
+ */
179
+ function resolveCalleeName(fnNode) {
180
+ if (!fnNode) return null;
181
+ const t = fnNode.type;
182
+ if (t === 'identifier') return fnNode.text;
183
+ if (t === 'member_expression') return fnNode.text; // e.g. obj.method
184
+ if (t === 'subscript_expression') return null; // computed — unresolvable
185
+ return null;
186
+ }
187
+
188
+ // ── Python-specific extraction ────────────────────────────────────────────────
189
+
190
+ function walkPython(rootNode, relPath, entities, edges) {
191
+ function walk(node, enclosingFuncId) {
192
+ const t = node.type;
193
+
194
+ if (t === 'import_statement') {
195
+ // import foo, bar
196
+ for (let i = 0; i < node.namedChildCount; i++) {
197
+ const child = node.namedChild(i);
198
+ if (child.type === 'dotted_name') {
199
+ edges.push({
200
+ kind: 'require',
201
+ source: relPath,
202
+ target: child.text.replace(/\./g, '/'),
203
+ names: [],
204
+ line: node.startPosition.row + 1,
205
+ });
206
+ }
207
+ }
208
+ } else if (t === 'import_from_statement') {
209
+ // from foo import bar, baz / from .utils import x / from ..pkg.mod import y
210
+ const moduleNode = node.childForFieldName('module_name');
211
+ // Python relative imports use LEADING dots for package level: a single
212
+ // leading '.' = the current package (the file's own directory), '..' = the
213
+ // parent package, etc. A non-leading '.' is a submodule separator. The old
214
+ // code did a blind dot→slash replace, so `from .utils` became `/utils` (a
215
+ // bogus absolute id) instead of a './utils' relative specifier the query
216
+ // layer can resolve to a real file id. Translate leading dots to '../' levels.
217
+ let target = '?';
218
+ if (moduleNode) {
219
+ const raw = moduleNode.text; // e.g. '.utils', '..pkg.mod', 'django.db'
220
+ const lead = raw.match(/^\.+/);
221
+ if (lead) {
222
+ const dots = lead[0].length; // 1 = current pkg, 2 = parent, ...
223
+ const rest = raw.slice(dots).replace(/\./g, '/'); // submodule dots → slashes
224
+ // 1 dot → './rest' ; 2 dots → '../rest' ; 3 dots → '../../rest'
225
+ const up = '../'.repeat(dots - 1);
226
+ target = './' + up + rest; // a relative specifier the query layer resolves
227
+ } else {
228
+ target = raw.replace(/\./g, '/'); // absolute/package import (e.g. django/db)
229
+ }
230
+ }
231
+ const names = [];
232
+ for (let i = 0; i < node.namedChildCount; i++) {
233
+ const child = node.namedChild(i);
234
+ if (child.type === 'dotted_name' && child !== moduleNode) names.push(child.text);
235
+ if (child.type === 'import_list') {
236
+ for (let j = 0; j < child.namedChildCount; j++) {
237
+ names.push(child.namedChild(j).text);
238
+ }
239
+ }
240
+ }
241
+ edges.push({
242
+ kind: 'import',
243
+ source: relPath,
244
+ target,
245
+ names,
246
+ line: node.startPosition.row + 1,
247
+ });
248
+ } else if (t === 'function_definition') {
249
+ const nameNode = node.childForFieldName('name');
250
+ if (nameNode) {
251
+ const name = nameNode.text;
252
+ const funcId = `${relPath}#${name}@${node.startPosition.row + 1}`;
253
+ entities.push({
254
+ id: funcId,
255
+ name,
256
+ type: 'function',
257
+ line: node.startPosition.row + 1,
258
+ exported: false,
259
+ });
260
+ // walk body with this funcId as enclosing
261
+ const body = node.childForFieldName('body');
262
+ if (body) {
263
+ for (let i = 0; i < body.childCount; i++) {
264
+ walk(body.child(i), funcId);
265
+ }
266
+ }
267
+ return;
268
+ }
269
+ } else if (t === 'class_definition') {
270
+ const nameNode = node.childForFieldName('name');
271
+ if (nameNode) {
272
+ const name = nameNode.text;
273
+ entities.push({
274
+ id: `${relPath}#${name}@${node.startPosition.row + 1}`,
275
+ name,
276
+ type: 'class',
277
+ line: node.startPosition.row + 1,
278
+ exported: false,
279
+ });
280
+ const body = node.childForFieldName('body');
281
+ if (body) {
282
+ for (let i = 0; i < body.childCount; i++) {
283
+ walk(body.child(i), enclosingFuncId);
284
+ }
285
+ }
286
+ return;
287
+ }
288
+ } else if (t === 'call') {
289
+ // Python call node
290
+ const fn = node.childForFieldName('function');
291
+ if (fn && enclosingFuncId) {
292
+ const calleeName = resolveCalleeName(fn) || fn.text;
293
+ if (calleeName) {
294
+ edges.push({
295
+ kind: 'call-site',
296
+ source: enclosingFuncId,
297
+ target: `UNRESOLVED#${calleeName}`,
298
+ line: node.startPosition.row + 1,
299
+ });
300
+ }
301
+ }
302
+ }
303
+
304
+ for (let i = 0; i < node.childCount; i++) {
305
+ walk(node.child(i), enclosingFuncId);
306
+ }
307
+ }
308
+ walk(rootNode, null);
309
+ }
310
+
311
+ // ── TypeScript / JavaScript extraction ───────────────────────────────────────
312
+
313
+ /**
314
+ * Walk a TS/JS AST and extract entities + edges.
315
+ *
316
+ * Tracks the "enclosing function funcId" stack so call-site edges carry
317
+ * a file-qualified source funcId per [RULE] who-calls-function-identity-disambiguated.
318
+ */
319
+ function walkTSJS(rootNode, relPath, entities, edges) {
320
+ /**
321
+ * @param {object} node - tree-sitter ASTNode
322
+ * @param {string|null} enclosingFuncId - funcId of innermost function containing this node
323
+ * @param {string|null} enclosingClass - name of innermost class (for method parentClass)
324
+ */
325
+ function walk(node, enclosingFuncId, enclosingClass) {
326
+ const t = node.type;
327
+
328
+ // ── import_statement / import_declaration ─────────────────────────────
329
+ if (t === 'import_statement' || t === 'import_declaration') {
330
+ const sourceNode = node.childForFieldName('source');
331
+ if (sourceNode) {
332
+ const target = sourceNode.text.replace(/^['"`]|['"`]$/g, '');
333
+ const names = extractImportedNames(node);
334
+ edges.push({
335
+ kind: 'import',
336
+ source: relPath,
337
+ target,
338
+ names,
339
+ line: node.startPosition.row + 1,
340
+ });
341
+ }
342
+ return; // no children to recurse into for imports
343
+ }
344
+
345
+ // ── call_expression ───────────────────────────────────────────────────
346
+ if (t === 'call_expression') {
347
+ const fn = node.childForFieldName('function');
348
+ const args = node.childForFieldName('arguments');
349
+
350
+ // require('module')
351
+ if (fn && fn.text === 'require') {
352
+ if (args && args.namedChildCount > 0) {
353
+ const first = args.namedChild(0);
354
+ if (first && (first.type === 'string' || first.type === 'string_fragment')) {
355
+ const target = first.text.replace(/^['"`]|['"`]$/g, '');
356
+ edges.push({
357
+ kind: 'require',
358
+ source: relPath,
359
+ target,
360
+ names: [],
361
+ line: node.startPosition.row + 1,
362
+ });
363
+ }
364
+ }
365
+ } else if (fn) {
366
+ // General call-site edge — [RULE] who-calls-function-identity-disambiguated
367
+ const calleeName = resolveCalleeName(fn);
368
+ if (calleeName && calleeName !== 'require') {
369
+ const srcId = enclosingFuncId || `${relPath}#_toplevel`;
370
+ edges.push({
371
+ kind: 'call-site',
372
+ source: srcId,
373
+ target: `UNRESOLVED#${calleeName}`,
374
+ line: node.startPosition.row + 1,
375
+ });
376
+ }
377
+ }
378
+
379
+ // Fall through to walk children (the call_expression can contain more nodes)
380
+ }
381
+
382
+ // ── function_declaration ──────────────────────────────────────────────
383
+ if (t === 'function_declaration' || t === 'function') {
384
+ const nameNode = node.childForFieldName('name');
385
+ if (nameNode) {
386
+ const name = nameNode.text;
387
+ const funcId = `${relPath}#${name}@${node.startPosition.row + 1}`;
388
+ entities.push({
389
+ id: funcId,
390
+ name,
391
+ type: enclosingClass ? 'method' : 'function',
392
+ line: node.startPosition.row + 1,
393
+ exported: isExportedNode(node),
394
+ ...(enclosingClass ? { parentClass: enclosingClass } : {}),
395
+ });
396
+ // Walk body with this funcId
397
+ for (let i = 0; i < node.childCount; i++) {
398
+ walk(node.child(i), funcId, enclosingClass);
399
+ }
400
+ return;
401
+ }
402
+ }
403
+
404
+ // ── method_definition ─────────────────────────────────────────────────
405
+ if (t === 'method_definition') {
406
+ const nameNode = node.childForFieldName('name');
407
+ if (nameNode && nameNode.text !== 'constructor') {
408
+ const name = nameNode.text;
409
+ const funcId = `${relPath}#${name}@${node.startPosition.row + 1}`;
410
+ entities.push({
411
+ id: funcId,
412
+ name,
413
+ type: 'method',
414
+ line: node.startPosition.row + 1,
415
+ exported: true, // methods are implicitly exported via their class
416
+ parentClass: enclosingClass || undefined,
417
+ });
418
+ for (let i = 0; i < node.childCount; i++) {
419
+ walk(node.child(i), funcId, enclosingClass);
420
+ }
421
+ return;
422
+ }
423
+ }
424
+
425
+ // ── class_declaration / class ─────────────────────────────────────────
426
+ if (t === 'class_declaration' || t === 'class') {
427
+ const nameNode = node.childForFieldName('name');
428
+ if (nameNode) {
429
+ const name = nameNode.text;
430
+ entities.push({
431
+ id: `${relPath}#${name}@${node.startPosition.row + 1}`,
432
+ name,
433
+ type: 'class',
434
+ line: node.startPosition.row + 1,
435
+ exported: isExportedNode(node),
436
+ });
437
+ // Walk children with class context
438
+ for (let i = 0; i < node.childCount; i++) {
439
+ walk(node.child(i), enclosingFuncId, name);
440
+ }
441
+ return;
442
+ }
443
+ }
444
+
445
+ // ── lexical_declaration / variable_declaration ────────────────────────
446
+ // const foo = () => ... or const foo = function...
447
+ if (t === 'lexical_declaration' || t === 'variable_declaration') {
448
+ const isExp = isExportedNode(node);
449
+ for (let i = 0; i < node.namedChildCount; i++) {
450
+ const decl = node.namedChild(i);
451
+ if (decl.type === 'variable_declarator') {
452
+ const nameNode = decl.childForFieldName('name');
453
+ const valueNode = decl.childForFieldName('value');
454
+ if (nameNode && valueNode &&
455
+ (valueNode.type === 'arrow_function' ||
456
+ valueNode.type === 'function' ||
457
+ valueNode.type === 'function_expression')) {
458
+ const name = nameNode.text;
459
+ const funcId = `${relPath}#${name}@${decl.startPosition.row + 1}`;
460
+ entities.push({
461
+ id: funcId,
462
+ name,
463
+ type: 'function',
464
+ line: decl.startPosition.row + 1,
465
+ exported: isExp,
466
+ });
467
+ // Walk arrow/function body with this funcId
468
+ for (let j = 0; j < valueNode.childCount; j++) {
469
+ walk(valueNode.child(j), funcId, enclosingClass);
470
+ }
471
+ }
472
+ }
473
+ }
474
+ // Continue walking children for the declaration itself
475
+ }
476
+
477
+ // ── export_statement (bare re-exports: export { x, y }) ──────────────
478
+ if (t === 'export_statement') {
479
+ const declaration = node.childForFieldName('declaration');
480
+ if (!declaration) {
481
+ // export { x, y as z }
482
+ for (let i = 0; i < node.namedChildCount; i++) {
483
+ const child = node.namedChild(i);
484
+ if (child.type === 'export_clause') {
485
+ for (let j = 0; j < child.namedChildCount; j++) {
486
+ const spec = child.namedChild(j);
487
+ // export_specifier has field "name" (local name) and optional "alias"
488
+ const nameNode = spec.childForFieldName('name') || spec;
489
+ if (nameNode) {
490
+ entities.push({
491
+ id: `${relPath}#export:${nameNode.text}`,
492
+ name: nameNode.text,
493
+ type: 'export',
494
+ line: node.startPosition.row + 1,
495
+ exported: true,
496
+ });
497
+ }
498
+ }
499
+ }
500
+ }
501
+ }
502
+ // Re-export from another file: export { x } from './foo'
503
+ const sourceNode = node.childForFieldName('source');
504
+ if (sourceNode) {
505
+ const target = sourceNode.text.replace(/^['"`]|['"`]$/g, '');
506
+ edges.push({
507
+ kind: 'import',
508
+ source: relPath,
509
+ target,
510
+ names: [],
511
+ line: node.startPosition.row + 1,
512
+ });
513
+ }
514
+ }
515
+
516
+ // ── Walk children ─────────────────────────────────────────────────────
517
+ for (let i = 0; i < node.childCount; i++) {
518
+ walk(node.child(i), enclosingFuncId, enclosingClass);
519
+ }
520
+ }
521
+
522
+ walk(rootNode, null, null);
523
+ }
524
+
525
+ // ── Per-file extraction (exported API) ───────────────────────────────────────
526
+
527
+ /**
528
+ * Extract entities + edges from a single source file.
529
+ *
530
+ * @param {string} absPath - absolute path to the file
531
+ * @param {string} relPath - repo-relative POSIX path (the file's identity in the store)
532
+ * @returns {{ file: string, entities: Array, edges: Array, loc: number }}
533
+ *
534
+ * Per graph-parser-floor-contract.md §Per-file parse output shape.
535
+ */
536
+ function extractEdges(absPath, relPath) {
537
+ ensureParsers();
538
+
539
+ const ext = path.extname(absPath).toLowerCase();
540
+ const content = fs.readFileSync(absPath, 'utf8');
541
+ const loc = content.split('\n').length;
542
+
543
+ const grammar = getGrammar(ext);
544
+ if (!grammar) {
545
+ // Unsupported extension or parsers not available — return empty (no crash)
546
+ return { file: relPath, entities: [], edges: [], loc };
547
+ }
548
+
549
+ const entities = [];
550
+ const edges = [];
551
+
552
+ const parser = new Parser();
553
+ parser.setLanguage(grammar);
554
+ // tree-sitter 0.21's Node binding defaults to a 32 KB parse buffer and throws
555
+ // "Invalid argument" on larger source — silently dropping every file over
556
+ // ~32 KB (common in real repos: Atos has many). Pass an explicit bufferSize
557
+ // sized to the content (+ headroom) so large files index correctly.
558
+ const tree = parser.parse(content, null, {
559
+ bufferSize: Math.max(32 * 1024, content.length * 2 + 1024),
560
+ });
561
+
562
+ if (ext === '.py') {
563
+ walkPython(tree.rootNode, relPath, entities, edges);
564
+ } else {
565
+ walkTSJS(tree.rootNode, relPath, entities, edges);
566
+ }
567
+
568
+ return { file: relPath, entities, edges, loc };
569
+ }
570
+
571
+ // ── CLI entry point ──────────────────────────────────────────────────────────
572
+
573
+ if (require.main === module) {
574
+ const args = process.argv.slice(2);
575
+ const fileArg = args.find(a => !a.startsWith('--'));
576
+ const rootIdx = args.indexOf('--repo-root');
577
+ const repoRoot = rootIdx !== -1 ? args[rootIdx + 1] : process.cwd();
578
+
579
+ if (!fileArg) {
580
+ errLog('Usage: gsd-t-graph-edge-extract.cjs <file> [--repo-root <root>]');
581
+ process.exit(1);
582
+ }
583
+
584
+ const absPath = path.resolve(fileArg);
585
+ if (!fs.existsSync(absPath)) {
586
+ const envelope = { ok: false, error: 'file-not-found', file: fileArg };
587
+ console.log(JSON.stringify(envelope, null, 2));
588
+ process.exit(1);
589
+ }
590
+
591
+ const ext = path.extname(absPath).toLowerCase();
592
+ if (!PARSED_EXTS.has(ext)) {
593
+ const envelope = {
594
+ ok: false,
595
+ error: 'unsupported-extension',
596
+ file: fileArg,
597
+ ext,
598
+ supported: [...PARSED_EXTS],
599
+ };
600
+ console.log(JSON.stringify(envelope, null, 2));
601
+ process.exit(1);
602
+ }
603
+
604
+ const relPath = path.relative(repoRoot, absPath).split(path.sep).join('/');
605
+ info(`Extracting edges from: ${relPath}`);
606
+
607
+ const result = extractEdges(absPath, relPath);
608
+
609
+ const envelope = {
610
+ ok: true,
611
+ file: result.file,
612
+ loc: result.loc,
613
+ entityCount: result.entities.length,
614
+ edgeCount: result.edges.length,
615
+ entities: result.entities,
616
+ edges: result.edges,
617
+ };
618
+ console.log(JSON.stringify(envelope, null, 2));
619
+ process.exit(0);
620
+ }
621
+
622
+ module.exports = { extractEdges, PARSED_EXTS };