@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.
- package/CHANGELOG.md +32 -0
- package/README.md +3 -2
- package/bin/gsd-t-estimate-sheet.cjs +206 -28
- package/bin/gsd-t-graph-edge-extract.cjs +454 -2
- package/bin/gsd-t-graph-exclude.cjs +106 -0
- package/bin/gsd-t-graph-freshness.cjs +16 -5
- package/bin/gsd-t-graph-index.cjs +102 -10
- package/bin/gsd-t-graph-query-cli.cjs +546 -27
- package/bin/gsd-t-graph-scip-upgrade.cjs +92 -12
- package/bin/gsd-t-scip-reader.cjs +29 -2
- package/bin/gsd-t.js +43 -6
- package/commands/gsd-t-estimate-rescale.md +64 -0
- package/commands/gsd-t-estimate.md +2 -2
- package/commands/gsd-t-help.md +6 -0
- package/package.json +1 -1
- package/scripts/gsd-t-graph-search-guard.js +98 -4
- package/scripts/statusline-command.sh +11 -2
- package/templates/CLAUDE-global.md +1 -1
- package/templates/estimate-config.json +2 -2
- package/templates/estimate-sheet-spec.md +27 -7
- package/templates/playbooks/tekyz-estimation-and-prd-playbook.md +2 -2
|
@@ -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(
|
|
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
|
};
|