@tekyzinc/gsd-t 5.22.10 → 5.24.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.
@@ -196,18 +196,21 @@ function contentHash(absPath) {
196
196
 
197
197
  function enumerateFiles(root) {
198
198
  const results = [];
199
+ // Project exclude list (.gsd-t/graph-exclude.json) — ancillary trees such as
200
+ // design/Figma exports that are not the application. Throws on a malformed file.
201
+ const { isExcluded } = require('./gsd-t-graph-exclude.cjs').loadGraphExcludes(root);
199
202
  function walk(dir) {
200
203
  let entries;
201
204
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
202
205
  catch { return; }
203
206
  for (const e of entries) {
207
+ const absPath = path.join(dir, e.name);
208
+ const relPath = path.relative(root, absPath).split(path.sep).join('/');
204
209
  if (e.isDirectory()) {
205
- if (!shouldSkipDir(e.name)) walk(path.join(dir, e.name));
210
+ if (!shouldSkipDir(e.name) && !isExcluded(relPath)) walk(absPath);
206
211
  } else if (e.isFile()) {
207
212
  const ext = path.extname(e.name).toLowerCase();
208
- if (PARSED_EXTS.has(ext)) {
209
- const absPath = path.join(dir, e.name);
210
- const relPath = path.relative(root, absPath).split(path.sep).join('/');
213
+ if (PARSED_EXTS.has(ext) && !isExcluded(relPath)) {
211
214
  results.push({ absPath, relPath, ext });
212
215
  }
213
216
  }
@@ -261,7 +264,8 @@ function buildSchema(db) {
261
264
  file TEXT NOT NULL,
262
265
  name TEXT,
263
266
  func_id TEXT,
264
- end_line INTEGER
267
+ end_line INTEGER,
268
+ meta TEXT
265
269
  );
266
270
  CREATE TABLE IF NOT EXISTS edges (
267
271
  kind TEXT NOT NULL,
@@ -275,6 +279,18 @@ function buildSchema(db) {
275
279
  CREATE INDEX IF NOT EXISTS nodes_file ON nodes(file);
276
280
  `);
277
281
  migrateEndLine(db);
282
+ migrateMeta(db);
283
+ }
284
+
285
+ /**
286
+ * Idempotent migration: add `nodes.meta` (JSON) — a table's SQL name + columns,
287
+ * an enum's values. NULL for functions/classes. [RULE] drizzle-table-entity-and-usage-edges
288
+ */
289
+ function migrateMeta(db) {
290
+ const cols = db.prepare("PRAGMA table_info(nodes)").all();
291
+ if (!cols.some((c) => c.name === 'meta')) {
292
+ db.exec('ALTER TABLE nodes ADD COLUMN meta TEXT');
293
+ }
278
294
  }
279
295
 
280
296
  /**
@@ -310,11 +326,15 @@ function migrateEndLine(db) {
310
326
  */
311
327
  function getWriteStmts(db) {
312
328
  if (db.__m94WriteStmts) return db.__m94WriteStmts;
329
+ // A handle opened elsewhere (the freshness re-index opens the db without
330
+ // buildSchema) may point at an older graph — add the columns written below.
331
+ migrateEndLine(db);
332
+ migrateMeta(db);
313
333
  const stmts = {
314
334
  deleteNodes: db.prepare('DELETE FROM nodes WHERE file = ?'),
315
335
  deleteEdgesSrc: db.prepare("DELETE FROM edges WHERE src = ? OR src LIKE ? OR src = ?"),
316
336
  insFile: db.prepare('INSERT OR REPLACE INTO files (file, content_hash, tier, indexed_at) VALUES (?, ?, ?, ?)'),
317
- insNode: db.prepare('INSERT OR REPLACE INTO nodes (id, kind, tier, content_hash, file, name, func_id, end_line) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'),
337
+ insNode: db.prepare('INSERT OR REPLACE INTO nodes (id, kind, tier, content_hash, file, name, func_id, end_line, meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'),
318
338
  insEdge: db.prepare('INSERT INTO edges (kind, src, dst, partial) VALUES (?, ?, ?, ?)'),
319
339
  };
320
340
  // db.transaction() wrapper is also created once (it closes over the stmts).
@@ -324,7 +344,8 @@ function getWriteStmts(db) {
324
344
  stmts.deleteEdgesSrc.run(file, `${file}#%`, file);
325
345
  stmts.insFile.run(file, hash, tier, now);
326
346
  for (const entity of entities) {
327
- stmts.insNode.run(entity.id, entity.type || 'function', tier, hash, file, entity.name || null, entity.id, entity.endLine ?? null);
347
+ stmts.insNode.run(entity.id, entity.type || 'function', tier, hash, file, entity.name || null, entity.id, entity.endLine ?? null,
348
+ entity.meta === undefined ? null : JSON.stringify(entity.meta));
328
349
  }
329
350
  for (const edge of edges) {
330
351
  stmts.insEdge.run(edge.kind, edge.src, edge.dst, edge.partial ? 1 : 0);
@@ -404,12 +425,16 @@ function parse_and_put(absPath, relPath, options) {
404
425
  finalEntities = upgraded.entities;
405
426
  finalEdges = upgraded.edges;
406
427
  }
407
- } else if (existingTier === 'compiler-accurate') {
428
+ } else if (existingTier === 'compiler-accurate' || existingTier === 'compiler-partial') {
408
429
  // [RULE] reindex-tier-never-silently-downgraded — no SCIP context this call
409
430
  // (e.g. a metadata-only re-index like M98's body end-line backfill). A file that
410
431
  // WAS compiler-accurate must NOT silently drop to plain floor; label it STALE-SCIP
411
432
  // so the tier reflects "previously accurate, not re-resolved" rather than a lie.
412
433
  tier = 'tree-sitter-floor-STALE-SCIP';
434
+ } else if (existingTier === 'tree-sitter-floor-SCIP-MISSING') {
435
+ // Same principle: a file SCIP never indexed stays labelled as such until a
436
+ // full build (with SCIP) says otherwise. [RULE] scip-missing-file-detected-never-silent
437
+ tier = existingTier;
413
438
  }
414
439
 
415
440
  // Normalize edges to store schema (map from parser-floor shape to store shape)
@@ -452,6 +477,7 @@ function parse_and_put(absPath, relPath, options) {
452
477
  exported: e.exported,
453
478
  parentClass: e.parentClass,
454
479
  endLine: e.endLine ?? null, // M98 — function end line for body-slice
480
+ ...(e.meta === undefined ? {} : { meta: e.meta }), // table / enum: SQL name, columns, values
455
481
  }));
456
482
 
457
483
  if (db) {
@@ -530,8 +556,15 @@ function build_index(repoRoot, options) {
530
556
  let edgeCount = 0;
531
557
  let tierFloor = 0;
532
558
  let tierUpgraded = 0;
559
+ let tierPartial = 0; // [RULE] scip-tier-proportional
560
+ let callEdges = 0;
561
+ let callEdgesUnresolved = 0;
533
562
  let errors = 0;
534
563
  const skippedFiles = [];
564
+ const scipMissing = []; // files SCIP ran for but produced no document for
565
+ let tableCount = 0;
566
+ let enumCount = 0;
567
+ const tablesUnresolved = []; // [RULE] drizzle-table-shape-gap-named-never-skipped
535
568
 
536
569
  // Stream: parse + put each file one at a time (never accumulate the full set)
537
570
  for (const { absPath, relPath } of files) {
@@ -541,7 +574,19 @@ function build_index(repoRoot, options) {
541
574
  entityCount += result.entities.length;
542
575
  edgeCount += result.edges.length;
543
576
  if (result.tier === 'compiler-accurate') tierUpgraded++;
577
+ else if (result.tier === 'compiler-partial') tierPartial++;
544
578
  else tierFloor++;
579
+ for (const e of result.edges) {
580
+ if (e.kind !== 'call-site' && e.kind !== 'CALL') continue;
581
+ callEdges++;
582
+ if (String(e.target || e.dst).startsWith('UNRESOLVED#')) callEdgesUnresolved++;
583
+ }
584
+ if (result.tier === 'tree-sitter-floor-SCIP-MISSING') scipMissing.push(relPath);
585
+ for (const ent of result.entities) {
586
+ if (ent.type === 'table') tableCount++;
587
+ if (ent.type === 'enum') enumCount++;
588
+ if (ent.meta && ent.meta.unresolved) tablesUnresolved.push({ file: relPath, name: ent.name, problems: ent.meta.unresolved });
589
+ }
545
590
  if (typeof onProgress === 'function') {
546
591
  onProgress({ file: relPath, tier: result.tier, fileCount, total: files.length });
547
592
  }
@@ -552,6 +597,20 @@ function build_index(repoRoot, options) {
552
597
  }
553
598
  }
554
599
 
600
+ // A full build leaves exactly the enumerated set: rows for a file that is gone
601
+ // or newly excluded (e.g. GSD-T's copied bin/ tools) are pruned, not kept.
602
+ // [RULE] status-counts-files-table
603
+ const enumerated = new Set(files.map((f) => f.relPath));
604
+ const stale = db.prepare('SELECT file FROM files').all().map((r) => r.file).filter((f) => !enumerated.has(f));
605
+ if (stale.length) {
606
+ const w = getWriteStmts(db);
607
+ const delFile = db.prepare('DELETE FROM files WHERE file = ?');
608
+ db.transaction(() => {
609
+ for (const f of stale) { w.deleteNodes.run(f); w.deleteEdgesSrc.run(f, `${f}#%`, f); delFile.run(f); }
610
+ })();
611
+ info(`Pruned ${stale.length} file(s) no longer in the project (deleted or excluded)`);
612
+ }
613
+
555
614
  // Record the skipped set + parse-success-rate so a query whose edges live in a
556
615
  // skipped file can return a coverage flag ('result may be incomplete — N files
557
616
  // unparsed') instead of a bare empty set that reads as authoritative truth.
@@ -592,11 +651,35 @@ function build_index(repoRoot, options) {
592
651
  console.error(`\x1b[33m[IDX NOTICE]\x1b[0m ${scipNotice}`);
593
652
  }
594
653
 
654
+ // A file the SCIP indexer skipped keeps every call target unresolved, so
655
+ // who-calls cannot see its callers. Loud, every build — not a fallback, a
656
+ // report. `gsd-t graph status` repeats it. [RULE] scip-missing-file-detected-never-silent
657
+ if (scipMissing.length) {
658
+ const shown = scipMissing.slice(0, 10).join(', ');
659
+ warn(`${scipMissing.length} file(s) not in the SCIP index — their call edges stay unresolved ` +
660
+ `(tier tree-sitter-floor-SCIP-MISSING): ${shown}${scipMissing.length > 10 ? ', …' : ''}`);
661
+ }
662
+
663
+ // A table whose declaration had a shape the extractor could not read (a
664
+ // computed name, a spread of columns, a non-literal reference) is still an
665
+ // entity — but its columns or FKs are incomplete. Named every build, loud.
666
+ if (tablesUnresolved.length) {
667
+ warn(`${tablesUnresolved.length} table/enum declaration(s) only partly read — columns or foreign keys incomplete:`);
668
+ for (const t of tablesUnresolved.slice(0, 20)) warn(` ${t.file} ${t.name}: ${t.problems.join('; ')}`);
669
+ if (tablesUnresolved.length > 20) warn(` … ${tablesUnresolved.length - 20} more`);
670
+ }
671
+
595
672
  return {
596
673
  fileCount,
674
+ tableCount,
675
+ enumCount,
676
+ tablesUnresolved,
597
677
  entityCount,
598
678
  edgeCount,
599
- tier: { floor: tierFloor, upgraded: tierUpgraded },
679
+ tier: { floor: tierFloor, upgraded: tierUpgraded, partial: tierPartial },
680
+ callEdges,
681
+ callEdgesUnresolved,
682
+ scipMissing,
600
683
  scipAvailable: scipActive,
601
684
  scipNotice,
602
685
  errors,
@@ -648,7 +731,12 @@ if (require.main === module) {
648
731
  });
649
732
 
650
733
  good(`Indexed ${result.fileCount} files, ${result.entityCount} entities, ${result.edgeCount} edges in ${result.durationMs}ms`);
651
- good(`Tiers — floor: ${result.tier.floor}, compiler-accurate: ${result.tier.upgraded}`);
734
+ good(`Tiers — floor: ${result.tier.floor}, compiler-accurate: ${result.tier.upgraded}, compiler-partial: ${result.tier.partial}`);
735
+ if (result.callEdges) {
736
+ info(`Call edges unresolved: ${result.callEdgesUnresolved} of ${result.callEdges} ` +
737
+ `(${(100 * result.callEdgesUnresolved / result.callEdges).toFixed(1)}% — library calls such as console.log never resolve)`);
738
+ }
739
+ good(`Database tables: ${result.tableCount}, enums: ${result.enumCount}`);
652
740
  if (result.errors > 0) warn(`${result.errors} files had parse errors (skipped)`);
653
741
 
654
742
  const envelope = {
@@ -660,6 +748,10 @@ if (require.main === module) {
660
748
  entityCount: result.entityCount,
661
749
  edgeCount: result.edgeCount,
662
750
  tier: result.tier,
751
+ scipMissingCount: result.scipMissing.length,
752
+ tableCount: result.tableCount,
753
+ enumCount: result.enumCount,
754
+ tablesUnresolved: result.tablesUnresolved,
663
755
  errors: result.errors,
664
756
  durationMs: result.durationMs,
665
757
  };