@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.
- package/CHANGELOG.md +30 -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 +622 -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-require-store.cjs +89 -0
- package/bin/gsd-t-scip-reader.cjs +167 -0
- package/bin/gsd-t.js +170 -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 +12 -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,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 };
|