@tekyzinc/gsd-t 4.9.13 → 4.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/bin/gsd-t-competition-judge.cjs +7 -1
  4. package/bin/gsd-t-context-brief-kinds/impact.cjs +91 -4
  5. package/bin/gsd-t-context-brief-kinds/partition.cjs +95 -0
  6. package/bin/gsd-t-context-brief-kinds/plan.cjs +110 -4
  7. package/bin/gsd-t-file-disjointness.cjs +319 -7
  8. package/bin/gsd-t-graph-anti-grep-lint.cjs +515 -0
  9. package/bin/gsd-t-graph-edge-extract.cjs +612 -0
  10. package/bin/gsd-t-graph-freshness.cjs +506 -0
  11. package/bin/gsd-t-graph-index.cjs +540 -0
  12. package/bin/gsd-t-graph-k1-sqlite-stream.cjs +251 -0
  13. package/bin/gsd-t-graph-query-cli.cjs +1182 -0
  14. package/bin/gsd-t-graph-scip-upgrade.cjs +440 -0
  15. package/bin/gsd-t-graph-store-bakeoff.cjs +889 -0
  16. package/bin/gsd-t-graph-synthetic-gen.cjs +304 -0
  17. package/bin/gsd-t-graph-ts-throughput.cjs +587 -0
  18. package/bin/gsd-t-scip-reader.cjs +167 -0
  19. package/bin/gsd-t.js +166 -48
  20. package/commands/gsd-t-debug.md +10 -0
  21. package/commands/gsd-t-design-build.md +10 -0
  22. package/commands/gsd-t-execute.md +15 -0
  23. package/commands/gsd-t-feature.md +15 -7
  24. package/commands/gsd-t-gap-analysis.md +17 -7
  25. package/commands/gsd-t-impact.md +25 -0
  26. package/commands/gsd-t-integrate.md +25 -0
  27. package/commands/gsd-t-partition.md +18 -0
  28. package/commands/gsd-t-plan.md +16 -0
  29. package/commands/gsd-t-populate.md +16 -5
  30. package/commands/gsd-t-prd.md +18 -0
  31. package/commands/gsd-t-project.md +19 -0
  32. package/commands/gsd-t-promote-debt.md +16 -5
  33. package/commands/gsd-t-qa.md +20 -8
  34. package/commands/gsd-t-quick.md +10 -0
  35. package/commands/gsd-t-scan.md +21 -3
  36. package/commands/gsd-t-test-sync.md +10 -0
  37. package/commands/gsd-t-verify.md +25 -0
  38. package/commands/gsd-t-wave.md +8 -0
  39. package/package.json +10 -2
  40. package/templates/workflows/gsd-t-debug.workflow.js +81 -0
  41. package/templates/workflows/gsd-t-integrate.workflow.js +47 -1
  42. package/templates/workflows/gsd-t-phase.workflow.js +99 -1
  43. package/templates/workflows/gsd-t-quick.workflow.js +64 -0
  44. package/templates/workflows/gsd-t-scan.workflow.js +200 -9
  45. package/templates/workflows/gsd-t-verify.workflow.js +50 -1
  46. package/bin/graph-cgc.js +0 -510
  47. package/bin/graph-indexer.js +0 -147
  48. package/bin/graph-overlay.js +0 -195
  49. package/bin/graph-parsers.js +0 -327
  50. package/bin/graph-query.js +0 -453
  51. package/bin/graph-store.js +0 -154
@@ -0,0 +1,540 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * gsd-t-graph-index.cjs
6
+ *
7
+ * M94 D3-T2 — build_index + store write + per-file parse_and_put surface.
8
+ *
9
+ * Exposes:
10
+ * build_index(repoRoot, { dbPath?, onProgress? })
11
+ * Full-repo tree-sitter floor index → SQLite store via streaming writes.
12
+ * The indexer MUST stream — never load the whole graph into RAM
13
+ * (see K1 OOM finding in .gsd-t/spikes/k1-store-bakeoff-results.md).
14
+ *
15
+ * parse_and_put(absPath, relPath, { db, scip? })
16
+ * Per-file re-index surface that D4 (freshness) and D5 (query-cli re-index)
17
+ * call. Returns { entities, edges, tier, contentHash }.
18
+ * [RULE] reindex-tier-never-silently-downgraded: caller must pass the scip
19
+ * upgrader (or null); this function is deterministic — it does not decide
20
+ * the tier policy, it calls the tier labeller and records what it returns.
21
+ *
22
+ * openStore(dbPath) — open / create the SQLite graph store (better-sqlite3)
23
+ * closeStore(db) — close it
24
+ * putRecord(db, rec) — write a single file record (streaming-safe)
25
+ * getRecord(db, file) — read a single file record
26
+ * buildSchema(db) — idempotent schema creation (called by openStore)
27
+ *
28
+ * Store: SQLite (better-sqlite3), WAL mode, streaming inserts per K1 invariant.
29
+ * Schema per graph-store-schema-contract.md §Record shape:
30
+ * nodes table: (id TEXT PK, kind TEXT, tier TEXT, content_hash TEXT, file TEXT, name TEXT, func_id TEXT)
31
+ * edges table: (kind TEXT, src TEXT, dst TEXT, partial INTEGER)
32
+ * files table: (file TEXT PK, content_hash TEXT, tier TEXT, indexed_at TEXT)
33
+ *
34
+ * [RULE] accuracy-tier-labeled-never-silently-wrong
35
+ * [RULE] rust-cross-crate-flagged-partial
36
+ * [RULE] who-calls-function-identity-disambiguated (via edge-extract)
37
+ * [RULE] wave1-hard-gate-blocks-build (gate assumed cleared — K1=PICK/SQLite, K2=PASS)
38
+ *
39
+ * CLI usage:
40
+ * node bin/gsd-t-graph-index.cjs build [--repo <path>] [--db <path>]
41
+ * Emits JSON envelope on stdout, ANSI on stderr, exit 0=ok / 1=error.
42
+ */
43
+
44
+ const fs = require('fs');
45
+ const path = require('path');
46
+ const crypto = require('crypto');
47
+ const os = require('os');
48
+
49
+ // ── ANSI helpers ─────────────────────────────────────────────────────────────
50
+
51
+ const C = {
52
+ reset: '\x1b[0m',
53
+ green: '\x1b[32m',
54
+ red: '\x1b[31m',
55
+ yellow: '\x1b[33m',
56
+ cyan: '\x1b[36m',
57
+ bold: '\x1b[1m',
58
+ dim: '\x1b[2m',
59
+ };
60
+ function log(msg) { process.stderr.write(msg + '\n'); }
61
+ function info(msg) { log(`${C.cyan}[IDX]${C.reset} ${msg}`); }
62
+ function good(msg) { log(`${C.green}[IDX OK]${C.reset} ${msg}`); }
63
+ function warn(msg) { log(`${C.yellow}[IDX WARN]${C.reset} ${msg}`); }
64
+ function errLog(msg){ log(`${C.red}[IDX ERR]${C.reset} ${msg}`); }
65
+
66
+ // ── Source-file extensions + skip dirs (matches D2 / K2 probe) ───────────────
67
+
68
+ const PARSED_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py']);
69
+ const SKIP_DIRS = new Set([
70
+ 'node_modules', '.next', 'dist', 'build', '.git',
71
+ '.cache', '__pycache__', 'coverage', '.nyc_output',
72
+ 'out', '.turbo',
73
+ ]);
74
+
75
+ // ── Content hash ──────────────────────────────────────────────────────────────
76
+
77
+ /**
78
+ * [RULE] freshness-content-hash-not-git-sha: dirty-detection hashes file CONTENT.
79
+ */
80
+ function contentHash(absPath) {
81
+ const data = fs.readFileSync(absPath);
82
+ return crypto.createHash('sha256').update(data).digest('hex').slice(0, 16);
83
+ }
84
+
85
+ // ── File enumeration ──────────────────────────────────────────────────────────
86
+
87
+ function enumerateFiles(root) {
88
+ const results = [];
89
+ function walk(dir) {
90
+ let entries;
91
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
92
+ catch { return; }
93
+ for (const e of entries) {
94
+ if (e.isDirectory()) {
95
+ if (!SKIP_DIRS.has(e.name)) walk(path.join(dir, e.name));
96
+ } else if (e.isFile()) {
97
+ const ext = path.extname(e.name).toLowerCase();
98
+ if (PARSED_EXTS.has(ext)) {
99
+ const absPath = path.join(dir, e.name);
100
+ const relPath = path.relative(root, absPath).split(path.sep).join('/');
101
+ results.push({ absPath, relPath, ext });
102
+ }
103
+ }
104
+ }
105
+ }
106
+ walk(root);
107
+ return results;
108
+ }
109
+
110
+ // ── SQLite store ──────────────────────────────────────────────────────────────
111
+
112
+ /**
113
+ * Open (or create) the SQLite graph store.
114
+ * Returns a better-sqlite3 Database instance.
115
+ */
116
+ function openStore(dbPath) {
117
+ const Database = require('better-sqlite3');
118
+ const db = new Database(dbPath);
119
+ db.pragma('journal_mode = WAL');
120
+ db.pragma('synchronous = NORMAL');
121
+ buildSchema(db);
122
+ return db;
123
+ }
124
+
125
+ function closeStore(db) {
126
+ if (db && db.open) db.close();
127
+ }
128
+
129
+ /**
130
+ * Idempotent schema creation.
131
+ *
132
+ * Schema per graph-store-schema-contract.md §Record shape.
133
+ *
134
+ * files: one row per source file, carries the file-level tier + content_hash
135
+ * nodes: entity nodes (funcId-keyed functions/classes/exports)
136
+ * edges: import/require/call-site edges; partial flag for Rust cross-crate
137
+ */
138
+ function buildSchema(db) {
139
+ db.exec(`
140
+ CREATE TABLE IF NOT EXISTS files (
141
+ file TEXT PRIMARY KEY,
142
+ content_hash TEXT NOT NULL,
143
+ tier TEXT NOT NULL,
144
+ indexed_at TEXT NOT NULL
145
+ );
146
+ CREATE TABLE IF NOT EXISTS nodes (
147
+ id TEXT PRIMARY KEY,
148
+ kind TEXT NOT NULL,
149
+ tier TEXT NOT NULL,
150
+ content_hash TEXT NOT NULL,
151
+ file TEXT NOT NULL,
152
+ name TEXT,
153
+ func_id TEXT
154
+ );
155
+ CREATE TABLE IF NOT EXISTS edges (
156
+ kind TEXT NOT NULL,
157
+ src TEXT NOT NULL,
158
+ dst TEXT NOT NULL,
159
+ partial INTEGER NOT NULL DEFAULT 0
160
+ );
161
+ CREATE INDEX IF NOT EXISTS edges_dst ON edges(dst);
162
+ CREATE INDEX IF NOT EXISTS edges_src_kind ON edges(src, kind);
163
+ CREATE INDEX IF NOT EXISTS edges_kind_dst ON edges(kind, dst);
164
+ CREATE INDEX IF NOT EXISTS nodes_file ON nodes(file);
165
+ `);
166
+ }
167
+
168
+ /**
169
+ * Write a single file record to the store (streaming-safe — called per-file).
170
+ *
171
+ * Deletes old nodes/edges for this file first, then inserts the new set.
172
+ * WAL mode ensures atomic visibility (no torn reads during write).
173
+ *
174
+ * @param {object} db - better-sqlite3 Database
175
+ * @param {{ file, contentHash, entities, edges, tier }} rec
176
+ */
177
+ /**
178
+ * Prepare (once) and cache the per-file write statements on the db handle.
179
+ * better-sqlite3 statement preparation is expensive; re-preparing all 6
180
+ * statements on EVERY file (putRecord is called per-file across thousands of
181
+ * files) was the dominant cost — a full Atos index took 61 min almost entirely
182
+ * here. Preparing once and reusing drops it to seconds. The cache is keyed on
183
+ * the db handle so a fresh db re-prepares correctly.
184
+ */
185
+ function getWriteStmts(db) {
186
+ if (db.__m94WriteStmts) return db.__m94WriteStmts;
187
+ const stmts = {
188
+ deleteNodes: db.prepare('DELETE FROM nodes WHERE file = ?'),
189
+ deleteEdgesSrc: db.prepare("DELETE FROM edges WHERE src = ? OR src LIKE ? OR src = ?"),
190
+ insFile: db.prepare('INSERT OR REPLACE INTO files (file, content_hash, tier, indexed_at) VALUES (?, ?, ?, ?)'),
191
+ insNode: db.prepare('INSERT OR REPLACE INTO nodes (id, kind, tier, content_hash, file, name, func_id) VALUES (?, ?, ?, ?, ?, ?, ?)'),
192
+ insEdge: db.prepare('INSERT INTO edges (kind, src, dst, partial) VALUES (?, ?, ?, ?)'),
193
+ };
194
+ // db.transaction() wrapper is also created once (it closes over the stmts).
195
+ stmts.doWrite = db.transaction((rec) => {
196
+ const { file, contentHash: hash, entities, edges, tier, now } = rec;
197
+ stmts.deleteNodes.run(file);
198
+ stmts.deleteEdgesSrc.run(file, `${file}#%`, file);
199
+ stmts.insFile.run(file, hash, tier, now);
200
+ for (const entity of entities) {
201
+ stmts.insNode.run(entity.id, entity.type || 'function', tier, hash, file, entity.name || null, entity.id);
202
+ }
203
+ for (const edge of edges) {
204
+ stmts.insEdge.run(edge.kind, edge.src, edge.dst, edge.partial ? 1 : 0);
205
+ }
206
+ });
207
+ Object.defineProperty(db, '__m94WriteStmts', { value: stmts, enumerable: false });
208
+ return stmts;
209
+ }
210
+
211
+ function putRecord(db, rec) {
212
+ const { file, contentHash: hash, entities, edges, tier } = rec;
213
+ const now = new Date().toISOString();
214
+ // Prepared-statement bundle is created ONCE per db handle (see getWriteStmts).
215
+ getWriteStmts(db).doWrite({ file, contentHash: hash, entities, edges, tier, now });
216
+ }
217
+
218
+ /**
219
+ * Read a single file's record from the store.
220
+ * Returns { file, contentHash, tier, indexedAt } or null if not found.
221
+ */
222
+ function getRecord(db, file) {
223
+ const row = db.prepare('SELECT * FROM files WHERE file = ?').get(file);
224
+ if (!row) return null;
225
+ return {
226
+ file: row.file,
227
+ contentHash: row.content_hash,
228
+ tier: row.tier,
229
+ indexedAt: row.indexed_at,
230
+ };
231
+ }
232
+
233
+ // ── Per-file parse + put (the surface D4/D5 call) ────────────────────────────
234
+
235
+ /**
236
+ * Parse a single file and write it to the store.
237
+ *
238
+ * This is the per-file re-index surface that D4 (freshness) and D5 (query-cli)
239
+ * call when a file is stale or needs inline re-indexing.
240
+ *
241
+ * [RULE] reindex-tier-never-silently-downgraded: the scip upgrader (or null)
242
+ * is passed by the caller; this function is deterministic — it applies the
243
+ * tier policy that the upgrader declares and records it faithfully.
244
+ *
245
+ * @param {string} absPath - absolute path to the file
246
+ * @param {string} relPath - repo-relative POSIX path
247
+ * @param {{ db: Database, scip?: object, existingTier?: string }} options
248
+ * @returns {{ entities, edges, tier, contentHash }}
249
+ */
250
+ function parse_and_put(absPath, relPath, options) {
251
+ const { db, scip = null, existingTier = null } = options || {};
252
+
253
+ const { extractEdges } = require('./gsd-t-graph-edge-extract.cjs');
254
+ const { tryScipUpgrade } = scip || {};
255
+
256
+ // Extract via tree-sitter floor
257
+ const { entities, edges, loc } = extractEdges(absPath, relPath);
258
+ const hash = contentHash(absPath);
259
+
260
+ let finalEntities = entities;
261
+ let finalEdges = edges;
262
+ let tier = 'tree-sitter-floor';
263
+
264
+ // Optional SCIP upgrade
265
+ // [RULE] reindex-tier-never-silently-downgraded: pass existingTier as prevTier so
266
+ // the upgrader can produce 'tree-sitter-floor-STALE-SCIP' instead of plain floor
267
+ // when a previously compiler-accurate file loses its SCIP indexer.
268
+ if (typeof tryScipUpgrade === 'function') {
269
+ const upgraded = tryScipUpgrade(absPath, relPath, entities, edges, {
270
+ prevTier: existingTier,
271
+ projectRoot: scip.projectRoot || require('path').dirname(absPath),
272
+ resolver: scip.resolver || null,
273
+ });
274
+ if (upgraded) {
275
+ // ALWAYS use the tier returned by the upgrader — it encodes the full
276
+ // tier-preservation logic including STALE-SCIP (even when upgraded=false)
277
+ tier = upgraded.tier;
278
+ finalEntities = upgraded.entities;
279
+ finalEdges = upgraded.edges;
280
+ }
281
+ }
282
+
283
+ // Normalize edges to store schema (map from parser-floor shape to store shape)
284
+ const storeEdges = finalEdges.map(edge => {
285
+ if (edge.kind === 'import' || edge.kind === 'require') {
286
+ return {
287
+ kind: 'IMPORT',
288
+ src: edge.source || edge.src,
289
+ dst: edge.target || edge.dst,
290
+ partial: 0,
291
+ };
292
+ }
293
+ if (edge.kind === 'call-site') {
294
+ return {
295
+ kind: 'CALL',
296
+ src: edge.source || edge.src,
297
+ dst: edge.target || edge.dst,
298
+ partial: edge.partial ? 1 : 0,
299
+ };
300
+ }
301
+ // passthrough
302
+ return {
303
+ kind: edge.kind,
304
+ src: edge.source || edge.src,
305
+ dst: edge.target || edge.dst,
306
+ partial: edge.partial ? 1 : 0,
307
+ };
308
+ });
309
+
310
+ // Normalize entities to store schema
311
+ const storeEntities = finalEntities.map(e => ({
312
+ id: e.id,
313
+ type: e.type,
314
+ name: e.name,
315
+ file: relPath,
316
+ exported: e.exported,
317
+ parentClass: e.parentClass,
318
+ }));
319
+
320
+ if (db) {
321
+ putRecord(db, {
322
+ file: relPath,
323
+ contentHash: hash,
324
+ entities: storeEntities,
325
+ edges: storeEdges,
326
+ tier,
327
+ });
328
+ }
329
+
330
+ return { entities: storeEntities, edges: storeEdges, tier, contentHash: hash, loc };
331
+ }
332
+
333
+ // ── build_index (full-repo) ───────────────────────────────────────────────────
334
+
335
+ /**
336
+ * Full-repo index build.
337
+ *
338
+ * Streams per-file: enumerate → parse → store.put (never loads all in RAM).
339
+ * Calls the SCIP upgrader if present (per-language tier upgrade).
340
+ *
341
+ * @param {string} repoRoot - absolute path to the repo root
342
+ * @param {{ dbPath?, scip?, onProgress? }} options
343
+ * @returns {{ fileCount, entityCount, edgeCount, tier: { floor, upgraded }, durationMs }}
344
+ */
345
+ function build_index(repoRoot, options) {
346
+ const {
347
+ dbPath = path.join(repoRoot, '.gsd-t', 'graph.db'),
348
+ scip = null,
349
+ onProgress = null,
350
+ } = options || {};
351
+
352
+ // Ensure the parent directory exists
353
+ const dbDir = path.dirname(dbPath);
354
+ if (!fs.existsSync(dbDir)) {
355
+ fs.mkdirSync(dbDir, { recursive: true });
356
+ }
357
+
358
+ const db = openStore(dbPath);
359
+ const files = enumerateFiles(repoRoot);
360
+
361
+ // M95: build the repo-level SCIP resolver ONCE (run scip-typescript over the
362
+ // whole repo, read index.scip, build the symbol→funcId resolution maps). Each
363
+ // file's parse_and_put then resolves its call edges against this shared map.
364
+ // If the caller passed a `scip` object explicitly (tests inject mocks), respect
365
+ // it; otherwise auto-build from the upgrader.
366
+ let scipCtx = scip;
367
+ if (!scipCtx) {
368
+ const upg = require('./gsd-t-graph-scip-upgrade.cjs');
369
+ const resolver = upg.buildScipResolver(repoRoot);
370
+ scipCtx = {
371
+ tryScipUpgrade: upg.tryScipUpgrade,
372
+ resolver: resolver.ok ? resolver : null,
373
+ projectRoot: repoRoot,
374
+ };
375
+ if (resolver.ok) {
376
+ info(`SCIP resolver active (${resolver.scipPath}) — call edges will be resolved compiler-accurate.`);
377
+ } else {
378
+ info(`SCIP resolver unavailable (${resolver.reason}) — tree-sitter floor (approximate). Install scip-typescript for compiler-accurate call edges.`);
379
+ }
380
+ }
381
+
382
+ const t0 = Date.now();
383
+ let fileCount = 0;
384
+ let entityCount = 0;
385
+ let edgeCount = 0;
386
+ let tierFloor = 0;
387
+ let tierUpgraded = 0;
388
+ let errors = 0;
389
+ const skippedFiles = [];
390
+
391
+ // Stream: parse + put each file one at a time (never accumulate the full set)
392
+ for (const { absPath, relPath } of files) {
393
+ try {
394
+ const result = parse_and_put(absPath, relPath, { db, scip: scipCtx });
395
+ fileCount++;
396
+ entityCount += result.entities.length;
397
+ edgeCount += result.edges.length;
398
+ if (result.tier === 'compiler-accurate') tierUpgraded++;
399
+ else tierFloor++;
400
+ if (typeof onProgress === 'function') {
401
+ onProgress({ file: relPath, tier: result.tier, fileCount, total: files.length });
402
+ }
403
+ } catch (err) {
404
+ errors++;
405
+ skippedFiles.push({ file: relPath, reason: err.message });
406
+ warn(`Failed to index ${relPath}: ${err.message}`);
407
+ }
408
+ }
409
+
410
+ // Record the skipped set + parse-success-rate so a query whose edges live in a
411
+ // skipped file can return a coverage flag ('result may be incomplete — N files
412
+ // unparsed') instead of a bare empty set that reads as authoritative truth.
413
+ // A silently-missing edge is a WRONG answer (the no-wrong invariant).
414
+ const totalEnumerated = files.length;
415
+ const parseSuccessRate = totalEnumerated > 0 ? (totalEnumerated - errors) / totalEnumerated : 1;
416
+ const PARSE_RATE_FLOOR = 0.95;
417
+ const knownLimitation = parseSuccessRate < PARSE_RATE_FLOOR
418
+ ? `parse-success-rate ${(parseSuccessRate * 100).toFixed(1)}% is below the ${PARSE_RATE_FLOOR * 100}% floor — ${errors} of ${totalEnumerated} files unparsed; queries touching those files surface a coverage-incomplete flag`
419
+ : null;
420
+ // Persist the skipped set into the store so queries can consult it (best-effort;
421
+ // a meta table keyed by file).
422
+ try {
423
+ const meta = openStore(dbPath);
424
+ meta.exec('CREATE TABLE IF NOT EXISTS skipped_files (file TEXT PRIMARY KEY, reason TEXT)');
425
+ const insSkip = meta.prepare('INSERT OR REPLACE INTO skipped_files (file, reason) VALUES (?, ?)');
426
+ const tx = meta.transaction((rows) => { for (const r of rows) insSkip.run(r.file, r.reason); });
427
+ tx(skippedFiles);
428
+ closeStore(meta);
429
+ } catch { /* meta is best-effort; the result envelope still carries the set */ }
430
+
431
+ closeStore(db);
432
+
433
+ const durationMs = Date.now() - t0;
434
+
435
+ // R4-2 honesty: SCIP is an OPTIONAL accuracy upgrade. When no SCIP indexer was
436
+ // provided/available, EVERY file stays tree-sitter-floor (approximate edges) —
437
+ // that is correct (the graph never depends on SCIP to function), but it must be
438
+ // SURFACED LOUDLY, never a silent 0-upgrade that reads like full accuracy.
439
+ // SCIP is active when the resolver built successfully (auto-built or injected).
440
+ const scipActive = !!(scipCtx && scipCtx.resolver && scipCtx.resolver.ok);
441
+ let scipNotice = null;
442
+ if (!scipActive && tierUpgraded === 0) {
443
+ scipNotice =
444
+ 'SCIP indexer not available — all edges are tree-sitter-floor (approximate, not compiler-accurate). ' +
445
+ 'This is a working graph; for compiler-accurate TS/JS/Python edges, install a SCIP indexer ' +
446
+ '(e.g. scip-typescript) and re-index. The graph never requires SCIP to function.';
447
+ console.error(`\x1b[33m[IDX NOTICE]\x1b[0m ${scipNotice}`);
448
+ }
449
+
450
+ return {
451
+ fileCount,
452
+ entityCount,
453
+ edgeCount,
454
+ tier: { floor: tierFloor, upgraded: tierUpgraded },
455
+ scipAvailable: scipActive,
456
+ scipNotice,
457
+ errors,
458
+ skippedFiles,
459
+ parseSuccessRate,
460
+ knownLimitation,
461
+ durationMs,
462
+ dbPath,
463
+ };
464
+ }
465
+
466
+ // ── CLI entry point ───────────────────────────────────────────────────────────
467
+
468
+ if (require.main === module) {
469
+ const args = process.argv.slice(2);
470
+ const cmd = args[0] || 'build';
471
+
472
+ if (cmd !== 'build') {
473
+ errLog(`Unknown command: ${cmd}. Use: build`);
474
+ process.exit(1);
475
+ }
476
+
477
+ const repoIdx = args.indexOf('--repo');
478
+ const dbIdx = args.indexOf('--db');
479
+ const repoRoot = repoIdx !== -1 ? args[repoIdx + 1] : process.cwd();
480
+ const dbPath = dbIdx !== -1 ? args[dbIdx + 1] : path.join(repoRoot, '.gsd-t', 'graph.db');
481
+
482
+ if (!fs.existsSync(repoRoot)) {
483
+ errLog(`Repo root not found: ${repoRoot}`);
484
+ process.exit(1);
485
+ }
486
+
487
+ info(`build_index — repo: ${repoRoot}`);
488
+ info(`Store: ${dbPath}`);
489
+
490
+ // Load SCIP upgrader if available
491
+ let scip = null;
492
+ try {
493
+ scip = require('./gsd-t-graph-scip-upgrade.cjs');
494
+ } catch {
495
+ warn('SCIP upgrader not found — using tree-sitter-floor only');
496
+ }
497
+
498
+ const result = build_index(repoRoot, {
499
+ dbPath,
500
+ scip,
501
+ onProgress: ({ file, tier, fileCount, total }) => {
502
+ if (fileCount % 100 === 0) {
503
+ info(`[${fileCount}/${total}] ${tier === 'compiler-accurate' ? C.green : C.dim}${file}${C.reset}`);
504
+ }
505
+ },
506
+ });
507
+
508
+ good(`Indexed ${result.fileCount} files, ${result.entityCount} entities, ${result.edgeCount} edges in ${result.durationMs}ms`);
509
+ good(`Tiers — floor: ${result.tier.floor}, compiler-accurate: ${result.tier.upgraded}`);
510
+ if (result.errors > 0) warn(`${result.errors} files had parse errors (skipped)`);
511
+
512
+ const envelope = {
513
+ ok: true,
514
+ cmd: 'build',
515
+ repoRoot,
516
+ dbPath: result.dbPath,
517
+ fileCount: result.fileCount,
518
+ entityCount: result.entityCount,
519
+ edgeCount: result.edgeCount,
520
+ tier: result.tier,
521
+ errors: result.errors,
522
+ durationMs: result.durationMs,
523
+ };
524
+ console.log(JSON.stringify(envelope, null, 2));
525
+ process.exit(0);
526
+ }
527
+
528
+ module.exports = {
529
+ build_index,
530
+ parse_and_put,
531
+ openStore,
532
+ closeStore,
533
+ buildSchema,
534
+ putRecord,
535
+ getRecord,
536
+ contentHash,
537
+ enumerateFiles,
538
+ PARSED_EXTS,
539
+ SKIP_DIRS,
540
+ };