@tekyzinc/gsd-t 4.9.14 → 4.10.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/bin/gsd-t-competition-judge.cjs +7 -1
- package/bin/gsd-t-context-brief-kinds/impact.cjs +91 -4
- package/bin/gsd-t-context-brief-kinds/partition.cjs +95 -0
- package/bin/gsd-t-context-brief-kinds/plan.cjs +110 -4
- package/bin/gsd-t-file-disjointness.cjs +319 -7
- package/bin/gsd-t-graph-anti-grep-lint.cjs +515 -0
- package/bin/gsd-t-graph-edge-extract.cjs +612 -0
- package/bin/gsd-t-graph-freshness.cjs +506 -0
- package/bin/gsd-t-graph-index.cjs +540 -0
- package/bin/gsd-t-graph-k1-sqlite-stream.cjs +251 -0
- package/bin/gsd-t-graph-query-cli.cjs +1182 -0
- package/bin/gsd-t-graph-scip-upgrade.cjs +440 -0
- package/bin/gsd-t-graph-store-bakeoff.cjs +889 -0
- package/bin/gsd-t-graph-synthetic-gen.cjs +304 -0
- package/bin/gsd-t-graph-ts-throughput.cjs +587 -0
- package/bin/gsd-t-scip-reader.cjs +167 -0
- package/bin/gsd-t.js +166 -48
- package/commands/gsd-t-debug.md +10 -0
- package/commands/gsd-t-design-build.md +10 -0
- package/commands/gsd-t-execute.md +15 -0
- package/commands/gsd-t-feature.md +15 -7
- package/commands/gsd-t-gap-analysis.md +17 -7
- package/commands/gsd-t-impact.md +25 -0
- package/commands/gsd-t-integrate.md +25 -0
- package/commands/gsd-t-partition.md +18 -0
- package/commands/gsd-t-plan.md +16 -0
- package/commands/gsd-t-populate.md +16 -5
- package/commands/gsd-t-prd.md +18 -0
- package/commands/gsd-t-project.md +19 -0
- package/commands/gsd-t-promote-debt.md +16 -5
- package/commands/gsd-t-qa.md +20 -8
- package/commands/gsd-t-quick.md +10 -0
- package/commands/gsd-t-scan.md +21 -3
- package/commands/gsd-t-test-sync.md +10 -0
- package/commands/gsd-t-verify.md +25 -0
- package/commands/gsd-t-wave.md +8 -0
- package/package.json +10 -2
- package/templates/workflows/gsd-t-debug.workflow.js +81 -0
- package/templates/workflows/gsd-t-integrate.workflow.js +47 -1
- package/templates/workflows/gsd-t-phase.workflow.js +84 -0
- package/templates/workflows/gsd-t-quick.workflow.js +64 -0
- package/templates/workflows/gsd-t-scan.workflow.js +200 -9
- package/templates/workflows/gsd-t-verify.workflow.js +50 -1
- package/bin/graph-cgc.js +0 -510
- package/bin/graph-indexer.js +0 -147
- package/bin/graph-overlay.js +0 -195
- package/bin/graph-parsers.js +0 -327
- package/bin/graph-query.js +0 -453
- package/bin/graph-store.js +0 -154
|
@@ -0,0 +1,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
|
+
};
|