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