@tekyzinc/gsd-t 5.21.10 → 5.23.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 +48 -0
- package/README.md +4 -3
- package/bin/gsd-t-estimate-sheet.cjs +270 -20
- package/bin/gsd-t-graph-edge-extract.cjs +113 -2
- package/bin/gsd-t-graph-exclude.cjs +81 -0
- package/bin/gsd-t-graph-freshness.cjs +5 -1
- package/bin/gsd-t-graph-index.cjs +42 -7
- package/bin/gsd-t-graph-query-cli.cjs +210 -20
- package/bin/gsd-t-graph-scip-upgrade.cjs +92 -12
- package/bin/gsd-t-model-tier-policy.cjs +3 -3
- package/bin/gsd-t-scip-reader.cjs +29 -2
- package/bin/gsd-t.js +26 -3
- package/commands/gsd-t-estimate-rescale.md +64 -0
- package/commands/gsd-t-estimate.md +10 -9
- package/commands/gsd-t-help.md +7 -1
- package/commands/gsd-t-status.md +1 -1
- package/package.json +1 -1
- package/scripts/gsd-t-graph-search-guard.js +52 -2
- package/scripts/statusline-command.sh +11 -2
- package/templates/CLAUDE-global.md +1 -1
- package/templates/estimate-config.json +7 -4
- package/templates/estimate-sheet-spec.md +45 -3
- package/templates/playbooks/tekyz-estimation-and-prd-playbook.md +8 -4
|
@@ -173,7 +173,7 @@ function extractImportedNames(importNode) {
|
|
|
173
173
|
* SCIP upgrade will replace these with fully-resolved funcIds.
|
|
174
174
|
*
|
|
175
175
|
* [RULE] who-calls-function-identity-disambiguated: src is the funcId of the
|
|
176
|
-
* enclosing function (or a synthetic
|
|
176
|
+
* enclosing function (or a synthetic route/anonymous caller id, else _toplevel); dst is the
|
|
177
177
|
* callee's best-effort funcId.
|
|
178
178
|
*/
|
|
179
179
|
function resolveCalleeName(fnNode) {
|
|
@@ -185,6 +185,57 @@ function resolveCalleeName(fnNode) {
|
|
|
185
185
|
return null;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// ── Anonymous callers (route handlers + callbacks) ───────────────────────────
|
|
189
|
+
//
|
|
190
|
+
// A call inside an anonymous function has no named caller. Before this, such a
|
|
191
|
+
// call was credited to `file#_toplevel` (and a const-arrow body was walked twice,
|
|
192
|
+
// crediting every call in it to _toplevel as well as to the real function). A
|
|
193
|
+
// Hono/Express route file is almost entirely `router.get('/p', mw, async (c) =>
|
|
194
|
+
// { helper() })`, so who-calls answered with one meaningless "_toplevel" caller
|
|
195
|
+
// or — when the dst never resolved — nothing at all.
|
|
196
|
+
//
|
|
197
|
+
// Now every anonymous body gets a caller identity that says where it is:
|
|
198
|
+
// route handler → `file#GET /locations/:id@3217` (method + path + line)
|
|
199
|
+
// other callback → the enclosing NAMED function, when there is one
|
|
200
|
+
// top-level cb → `file#anonymous@<line>`
|
|
201
|
+
// [RULE] anonymous-caller-synthesized-never-dropped
|
|
202
|
+
|
|
203
|
+
const ROUTE_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'del', 'all', 'options', 'head', 'use']);
|
|
204
|
+
|
|
205
|
+
function isAnonymousFn(node) {
|
|
206
|
+
if (!node) return false;
|
|
207
|
+
if (node.type === 'arrow_function') return true;
|
|
208
|
+
if (node.type === 'function' || node.type === 'function_expression') {
|
|
209
|
+
return !node.childForFieldName('name');
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* `router.get('/path', ...)` → "GET /path"; anything else → null.
|
|
216
|
+
* The method match is case-insensitive (a domain value, not an identifier).
|
|
217
|
+
*/
|
|
218
|
+
function routeLabel(fnNode, argsNode) {
|
|
219
|
+
if (!fnNode || fnNode.type !== 'member_expression' || !argsNode) return null;
|
|
220
|
+
const prop = fnNode.childForFieldName('property');
|
|
221
|
+
if (!prop) return null;
|
|
222
|
+
const method = prop.text.toLowerCase();
|
|
223
|
+
if (!ROUTE_METHODS.has(method)) return null;
|
|
224
|
+
const first = argsNode.namedChild(0);
|
|
225
|
+
if (!first || (first.type !== 'string' && first.type !== 'template_string')) return null;
|
|
226
|
+
return `${method.toUpperCase()} ${first.text.slice(1, -1)}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A registration with no anonymous handler (`router.get('/x', requireAuth(), handler)`)
|
|
231
|
+
* is only treated as a route when its first argument looks like a URL path, so a
|
|
232
|
+
* `cache.get('key', compute())` is never mistaken for one.
|
|
233
|
+
*/
|
|
234
|
+
function isRoutePath(firstArg) {
|
|
235
|
+
if (!firstArg) return false;
|
|
236
|
+
return firstArg.text.slice(1).startsWith('/');
|
|
237
|
+
}
|
|
238
|
+
|
|
188
239
|
// ── Python-specific extraction ────────────────────────────────────────────────
|
|
189
240
|
|
|
190
241
|
function walkPython(rootNode, relPath, entities, edges) {
|
|
@@ -298,6 +349,7 @@ function walkPython(rootNode, relPath, entities, edges) {
|
|
|
298
349
|
source: enclosingFuncId,
|
|
299
350
|
target: `UNRESOLVED#${calleeName}`,
|
|
300
351
|
line: node.startPosition.row + 1,
|
|
352
|
+
col: fn.startPosition.column, // [RULE] scip-tier-proportional
|
|
301
353
|
});
|
|
302
354
|
}
|
|
303
355
|
}
|
|
@@ -374,13 +426,58 @@ function walkTSJS(rootNode, relPath, entities, edges) {
|
|
|
374
426
|
source: srcId,
|
|
375
427
|
target: `UNRESOLVED#${calleeName}`,
|
|
376
428
|
line: node.startPosition.row + 1,
|
|
429
|
+
// callee column — lets the SCIP upgrader tell a call the compiler saw
|
|
430
|
+
// from one it never looked at. [RULE] scip-tier-proportional
|
|
431
|
+
col: fn.startPosition.column,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Route registration: the route itself becomes the caller, named by
|
|
436
|
+
// method + path + line — for its anonymous handler AND for every
|
|
437
|
+
// middleware call in its arguments (`requireAuth()`, `requireLocationTenant()`).
|
|
438
|
+
// Crediting middleware to the enclosing scope collapsed 95 routes into one
|
|
439
|
+
// `_toplevel` caller per file. [RULE] anonymous-caller-synthesized-never-dropped
|
|
440
|
+
// [RULE] route-middleware-args-credited-to-route
|
|
441
|
+
const label = routeLabel(fn, args);
|
|
442
|
+
const handlers = label ? args.namedChildren.filter(isAnonymousFn) : [];
|
|
443
|
+
const middlewareCalls = label && isRoutePath(args.namedChild(0))
|
|
444
|
+
? args.namedChildren.slice(1).filter((a) => a.type === 'call_expression') : [];
|
|
445
|
+
if (handlers.length || middlewareCalls.length) {
|
|
446
|
+
const line = node.startPosition.row + 1;
|
|
447
|
+
const routeId = `${relPath}#${label}@${line}`;
|
|
448
|
+
entities.push({
|
|
449
|
+
id: routeId,
|
|
450
|
+
name: label,
|
|
451
|
+
type: 'function',
|
|
452
|
+
line,
|
|
453
|
+
endLine: node.endPosition.row + 1,
|
|
454
|
+
exported: false,
|
|
455
|
+
synthetic: 'route-handler',
|
|
377
456
|
});
|
|
457
|
+
// Node objects are re-created per access, so match handlers by position.
|
|
458
|
+
const handlerStarts = new Set(handlers.map((h) => h.startIndex));
|
|
459
|
+
walk(fn, enclosingFuncId, enclosingClass);
|
|
460
|
+
for (let i = 0; i < args.childCount; i++) {
|
|
461
|
+
const arg = args.child(i);
|
|
462
|
+
if (!handlerStarts.has(arg.startIndex) || !isAnonymousFn(arg)) { walk(arg, routeId, enclosingClass); continue; }
|
|
463
|
+
for (let j = 0; j < arg.childCount; j++) walk(arg.child(j), routeId, enclosingClass);
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
378
466
|
}
|
|
379
467
|
}
|
|
380
468
|
|
|
381
469
|
// Fall through to walk children (the call_expression can contain more nodes)
|
|
382
470
|
}
|
|
383
471
|
|
|
472
|
+
// ── anonymous function with no named scope around it ─────────────────
|
|
473
|
+
// Inside a named function, a callback's calls belong to that function
|
|
474
|
+
// (enclosingFuncId passes through). Outside one, give it a located id.
|
|
475
|
+
if (!enclosingFuncId && isAnonymousFn(node)) {
|
|
476
|
+
const anonId = `${relPath}#anonymous@${node.startPosition.row + 1}`;
|
|
477
|
+
for (let i = 0; i < node.childCount; i++) walk(node.child(i), anonId, enclosingClass);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
384
481
|
// ── function_declaration ──────────────────────────────────────────────
|
|
385
482
|
if (t === 'function_declaration' || t === 'function') {
|
|
386
483
|
const nameNode = node.childForFieldName('name');
|
|
@@ -451,6 +548,7 @@ function walkTSJS(rootNode, relPath, entities, edges) {
|
|
|
451
548
|
// const foo = () => ... or const foo = function...
|
|
452
549
|
if (t === 'lexical_declaration' || t === 'variable_declaration') {
|
|
453
550
|
const isExp = isExportedNode(node);
|
|
551
|
+
const walkedValues = new Set(); // bodies already walked under their own funcId
|
|
454
552
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
455
553
|
const decl = node.namedChild(i);
|
|
456
554
|
if (decl.type === 'variable_declarator') {
|
|
@@ -474,10 +572,23 @@ function walkTSJS(rootNode, relPath, entities, edges) {
|
|
|
474
572
|
for (let j = 0; j < valueNode.childCount; j++) {
|
|
475
573
|
walk(valueNode.child(j), funcId, enclosingClass);
|
|
476
574
|
}
|
|
575
|
+
walkedValues.add(valueNode.startIndex);
|
|
477
576
|
}
|
|
478
577
|
}
|
|
479
578
|
}
|
|
480
|
-
//
|
|
579
|
+
// Walk the rest of the declaration, but NOT a function body already walked
|
|
580
|
+
// above — walking it again credited each of its calls to a second, wrong
|
|
581
|
+
// caller (_toplevel). [RULE] anonymous-caller-synthesized-never-dropped
|
|
582
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
583
|
+
const child = node.child(i);
|
|
584
|
+
if (child.type !== 'variable_declarator') { walk(child, enclosingFuncId, enclosingClass); continue; }
|
|
585
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
586
|
+
const part = child.child(j);
|
|
587
|
+
if (walkedValues.has(part.startIndex) && part.type !== 'identifier') continue;
|
|
588
|
+
walk(part, enclosingFuncId, enclosingClass);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
481
592
|
}
|
|
482
593
|
|
|
483
594
|
// ── export_statement (bare re-exports: export { x, y }) ──────────────
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* gsd-t-graph-exclude.cjs — per-project exclude list for the code graph.
|
|
4
|
+
*
|
|
5
|
+
* Some repos carry large trees that are not the production application —
|
|
6
|
+
* design exports, Figma-generated component dumps, prototypes, archived
|
|
7
|
+
* scripts. They bloat the index, slow SCIP, and put look-alike symbols into
|
|
8
|
+
* who-calls. A project lists them in `.gsd-t/graph-exclude.json`:
|
|
9
|
+
*
|
|
10
|
+
* { "exclude": ["design/", "figma-exports/**", "scripts/archive/", "**\/*.figma.tsx"] }
|
|
11
|
+
*
|
|
12
|
+
* Pattern rules (paths are repo-relative, forward slashes, matched case-insensitively):
|
|
13
|
+
* - no `*` → a path prefix: "design/" or "design" excludes that folder
|
|
14
|
+
* - `*` → any run of characters except "/"
|
|
15
|
+
* - `**` → any run of characters including "/"
|
|
16
|
+
*
|
|
17
|
+
* The indexer AND the freshness walker both read this module, so a file the
|
|
18
|
+
* indexer skipped never shows up as a phantom "new file" on the next query.
|
|
19
|
+
* [RULE] freshness-excludes-match-indexer-skipdirs
|
|
20
|
+
*
|
|
21
|
+
* A malformed file HALTS (throws): silently ignoring it would index exactly the
|
|
22
|
+
* trees the user asked to keep out, with nothing saying so.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const EXCLUDE_FILE = path.join('.gsd-t', 'graph-exclude.json');
|
|
29
|
+
const REGEX_SPECIALS = /[.+?^${}()|[\]\\]/g;
|
|
30
|
+
|
|
31
|
+
function toRegex(pattern) {
|
|
32
|
+
const p = String(pattern).trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '');
|
|
33
|
+
if (!p) return null;
|
|
34
|
+
if (!p.includes('*')) {
|
|
35
|
+
// Plain prefix: the path itself, or anything under it.
|
|
36
|
+
const base = p.replace(/\/+$/, '').replace(REGEX_SPECIALS, '\\$&');
|
|
37
|
+
return new RegExp(`^${base}(/|$)`, 'i');
|
|
38
|
+
}
|
|
39
|
+
let re = '';
|
|
40
|
+
for (let i = 0; i < p.length; i++) {
|
|
41
|
+
const c = p[i];
|
|
42
|
+
if (c === '*' && p[i + 1] === '*') {
|
|
43
|
+
i++;
|
|
44
|
+
if (p[i + 1] === '/') { i++; re += '(?:.*/)?'; } else re += '.*';
|
|
45
|
+
} else if (c === '*') re += '[^/]*';
|
|
46
|
+
else re += c.replace(REGEX_SPECIALS, '\\$&');
|
|
47
|
+
}
|
|
48
|
+
return new RegExp(`^${re}(/|$)`, 'i');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validateList(parsed) {
|
|
52
|
+
const list = parsed ? parsed.exclude : undefined;
|
|
53
|
+
if (!Array.isArray(list)) return false;
|
|
54
|
+
return list.every((x) => typeof x === 'string');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load the project's exclude list. Throws on a malformed file.
|
|
59
|
+
* @returns {{ patterns: string[], source: string|null, isExcluded: (relPath: string) => boolean }}
|
|
60
|
+
*/
|
|
61
|
+
function loadGraphExcludes(projectRoot) {
|
|
62
|
+
const file = path.join(projectRoot, EXCLUDE_FILE);
|
|
63
|
+
if (!fs.existsSync(file)) return { patterns: [], source: null, isExcluded: () => false };
|
|
64
|
+
let parsed;
|
|
65
|
+
try { parsed = JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
66
|
+
catch (e) { throw new Error(`${EXCLUDE_FILE} is not valid JSON (${e.message}) — fix it or delete it`); }
|
|
67
|
+
if (!validateList(parsed)) {
|
|
68
|
+
throw new Error(`${EXCLUDE_FILE} must be { "exclude": ["folder/", "glob/**", ...] }`);
|
|
69
|
+
}
|
|
70
|
+
const regexes = parsed.exclude.map(toRegex).filter(Boolean);
|
|
71
|
+
return {
|
|
72
|
+
patterns: parsed.exclude,
|
|
73
|
+
source: EXCLUDE_FILE,
|
|
74
|
+
isExcluded: (relPath) => {
|
|
75
|
+
const rel = String(relPath).split(path.sep).join('/');
|
|
76
|
+
return regexes.some((r) => r.test(rel));
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { loadGraphExcludes, EXCLUDE_FILE, _toRegex: toRegex };
|
|
@@ -220,7 +220,11 @@ function compute_touched_files(db, projectRoot) {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// Walk the current working tree
|
|
223
|
-
|
|
223
|
+
// Same exclude list as the indexer, so an excluded file is never a phantom ADD
|
|
224
|
+
// and a newly-excluded one drops out as a DELETE. [RULE] freshness-excludes-match-indexer-skipdirs
|
|
225
|
+
const { isExcluded } = require('./gsd-t-graph-exclude.cjs').loadGraphExcludes(projectRoot);
|
|
226
|
+
const liveFiles = walkTree(projectRoot)
|
|
227
|
+
.filter((abs) => !isExcluded(path.relative(projectRoot, abs)));
|
|
224
228
|
|
|
225
229
|
const liveRelSet = new Set();
|
|
226
230
|
const edits = [];
|
|
@@ -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
|
}
|
|
@@ -404,12 +407,16 @@ function parse_and_put(absPath, relPath, options) {
|
|
|
404
407
|
finalEntities = upgraded.entities;
|
|
405
408
|
finalEdges = upgraded.edges;
|
|
406
409
|
}
|
|
407
|
-
} else if (existingTier === 'compiler-accurate') {
|
|
410
|
+
} else if (existingTier === 'compiler-accurate' || existingTier === 'compiler-partial') {
|
|
408
411
|
// [RULE] reindex-tier-never-silently-downgraded — no SCIP context this call
|
|
409
412
|
// (e.g. a metadata-only re-index like M98's body end-line backfill). A file that
|
|
410
413
|
// WAS compiler-accurate must NOT silently drop to plain floor; label it STALE-SCIP
|
|
411
414
|
// so the tier reflects "previously accurate, not re-resolved" rather than a lie.
|
|
412
415
|
tier = 'tree-sitter-floor-STALE-SCIP';
|
|
416
|
+
} else if (existingTier === 'tree-sitter-floor-SCIP-MISSING') {
|
|
417
|
+
// Same principle: a file SCIP never indexed stays labelled as such until a
|
|
418
|
+
// full build (with SCIP) says otherwise. [RULE] scip-missing-file-detected-never-silent
|
|
419
|
+
tier = existingTier;
|
|
413
420
|
}
|
|
414
421
|
|
|
415
422
|
// Normalize edges to store schema (map from parser-floor shape to store shape)
|
|
@@ -530,8 +537,12 @@ function build_index(repoRoot, options) {
|
|
|
530
537
|
let edgeCount = 0;
|
|
531
538
|
let tierFloor = 0;
|
|
532
539
|
let tierUpgraded = 0;
|
|
540
|
+
let tierPartial = 0; // [RULE] scip-tier-proportional
|
|
541
|
+
let callEdges = 0;
|
|
542
|
+
let callEdgesUnresolved = 0;
|
|
533
543
|
let errors = 0;
|
|
534
544
|
const skippedFiles = [];
|
|
545
|
+
const scipMissing = []; // files SCIP ran for but produced no document for
|
|
535
546
|
|
|
536
547
|
// Stream: parse + put each file one at a time (never accumulate the full set)
|
|
537
548
|
for (const { absPath, relPath } of files) {
|
|
@@ -541,7 +552,14 @@ function build_index(repoRoot, options) {
|
|
|
541
552
|
entityCount += result.entities.length;
|
|
542
553
|
edgeCount += result.edges.length;
|
|
543
554
|
if (result.tier === 'compiler-accurate') tierUpgraded++;
|
|
555
|
+
else if (result.tier === 'compiler-partial') tierPartial++;
|
|
544
556
|
else tierFloor++;
|
|
557
|
+
for (const e of result.edges) {
|
|
558
|
+
if (e.kind !== 'call-site' && e.kind !== 'CALL') continue;
|
|
559
|
+
callEdges++;
|
|
560
|
+
if (String(e.target || e.dst).startsWith('UNRESOLVED#')) callEdgesUnresolved++;
|
|
561
|
+
}
|
|
562
|
+
if (result.tier === 'tree-sitter-floor-SCIP-MISSING') scipMissing.push(relPath);
|
|
545
563
|
if (typeof onProgress === 'function') {
|
|
546
564
|
onProgress({ file: relPath, tier: result.tier, fileCount, total: files.length });
|
|
547
565
|
}
|
|
@@ -592,11 +610,23 @@ function build_index(repoRoot, options) {
|
|
|
592
610
|
console.error(`\x1b[33m[IDX NOTICE]\x1b[0m ${scipNotice}`);
|
|
593
611
|
}
|
|
594
612
|
|
|
613
|
+
// A file the SCIP indexer skipped keeps every call target unresolved, so
|
|
614
|
+
// who-calls cannot see its callers. Loud, every build — not a fallback, a
|
|
615
|
+
// report. `gsd-t graph status` repeats it. [RULE] scip-missing-file-detected-never-silent
|
|
616
|
+
if (scipMissing.length) {
|
|
617
|
+
const shown = scipMissing.slice(0, 10).join(', ');
|
|
618
|
+
warn(`${scipMissing.length} file(s) not in the SCIP index — their call edges stay unresolved ` +
|
|
619
|
+
`(tier tree-sitter-floor-SCIP-MISSING): ${shown}${scipMissing.length > 10 ? ', …' : ''}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
595
622
|
return {
|
|
596
623
|
fileCount,
|
|
597
624
|
entityCount,
|
|
598
625
|
edgeCount,
|
|
599
|
-
tier: { floor: tierFloor, upgraded: tierUpgraded },
|
|
626
|
+
tier: { floor: tierFloor, upgraded: tierUpgraded, partial: tierPartial },
|
|
627
|
+
callEdges,
|
|
628
|
+
callEdgesUnresolved,
|
|
629
|
+
scipMissing,
|
|
600
630
|
scipAvailable: scipActive,
|
|
601
631
|
scipNotice,
|
|
602
632
|
errors,
|
|
@@ -648,7 +678,11 @@ if (require.main === module) {
|
|
|
648
678
|
});
|
|
649
679
|
|
|
650
680
|
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}`);
|
|
681
|
+
good(`Tiers — floor: ${result.tier.floor}, compiler-accurate: ${result.tier.upgraded}, compiler-partial: ${result.tier.partial}`);
|
|
682
|
+
if (result.callEdges) {
|
|
683
|
+
info(`Call edges unresolved: ${result.callEdgesUnresolved} of ${result.callEdges} ` +
|
|
684
|
+
`(${(100 * result.callEdgesUnresolved / result.callEdges).toFixed(1)}% — library calls such as console.log never resolve)`);
|
|
685
|
+
}
|
|
652
686
|
if (result.errors > 0) warn(`${result.errors} files had parse errors (skipped)`);
|
|
653
687
|
|
|
654
688
|
const envelope = {
|
|
@@ -660,6 +694,7 @@ if (require.main === module) {
|
|
|
660
694
|
entityCount: result.entityCount,
|
|
661
695
|
edgeCount: result.edgeCount,
|
|
662
696
|
tier: result.tier,
|
|
697
|
+
scipMissingCount: result.scipMissing.length,
|
|
663
698
|
errors: result.errors,
|
|
664
699
|
durationMs: result.durationMs,
|
|
665
700
|
};
|