sigmap 8.6.0 → 8.7.1
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 +18 -0
- package/README.md +5 -5
- package/gen-context.js +445 -5
- package/llms-full.txt +7 -4
- package/llms.txt +4 -4
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/graph/call-graph.js +401 -0
- package/src/mcp/server.js +1 -1
- package/src/tracking/pricing.js +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.7.1] — 2026-07-05
|
|
14
|
+
|
|
15
|
+
Patch release — **multi-model cost savings + verified pricing.** The quality benchmark reported API input-cost savings for GPT-4o only; it now reports GPT-4o, Claude Sonnet, and Claude Haiku, and the hardcoded Claude rates were corrected to current published pricing.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Multi-model cost savings (#433, PR #434):** `scripts/run-quality-benchmark.mjs` adds Claude Haiku to the pricing model and emits a per-model "cost savings by model" table (GPT-4o · Claude Sonnet · Claude Haiku). `docs-vp/guide/quality-benchmark.md` now shows all three models in the cost table, headline, and frontmatter. Pricing verified 2026-07 (per 1M input tokens): GPT-4o $2.50, Claude Sonnet 5/4.6 $3.00, Claude Haiku 4.5 $1.00.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Stale Claude pricing (#433, PR #434):** `src/tracking/pricing.js` (the `sigmap gain` dashboard's cost assumptions) corrected to current published rates — Claude Haiku $0.80→$1.00 and Claude Opus $15.00→$5.00 (Opus 4.8). GPT-4o ($2.50) and Sonnet ($3.00) were already correct.
|
|
22
|
+
|
|
23
|
+
## [8.7.0] — 2026-07-05
|
|
24
|
+
|
|
25
|
+
Minor release — **method/caller-level call-graph (D4 v1): the graph goes from files to functions.** SigMap's dependency graph was file-level only (imports → `--impact`/`get_impact`). This adds **symbol-level** edges — *which function calls which function* — and the **method-level blast radius** of changing a single symbol. Deterministic, zero-dependency, JS/TS + Python. This is the plan's single hardest unbuilt gap and the biggest remaining lever on graph intelligence (v9.5 GR1).
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **Method/caller-level call-graph (#429, PR #430):** new `src/graph/call-graph.js` builds symbol edges keyed by `relPath#symbol` for **JS/TS + Python**. Definitions are extracted with real body ranges (brace-matching for JS, indentation for Python) over **comment- and string-masked** source, so braces/calls inside literals never create phantom edges. Each call site is resolved with **high precision** — a call binds to a definition of that name in the **same file** first, then in a **directly-imported file** (reusing the existing file-level import graph); names with no repo definition produce **no edge**, avoiding global name-collision noise. Public API: `buildCallGraph(cwd)` → `{ forward, reverse, defs }`, `methodImpact(symbol)` (transitive callers = method blast radius), `methodCallees(symbol)` (what it transitively calls), plus formatters mirroring `src/graph/impact.js`.
|
|
29
|
+
- **`--callers` / `--callees` CLI (#429, PR #430):** `sigmap --callers <symbol>` prints the method-level blast radius (every function that transitively calls `<symbol>`); `sigmap --callees <symbol>` prints what it calls. Both accept a bare name or a `file#symbol` id, support `--json` and `--depth <n>` (0 = unlimited), and mirror the `--impact` command. Documented in `--help`.
|
|
30
|
+
|
|
13
31
|
## [8.6.0] — 2026-07-05
|
|
14
32
|
|
|
15
33
|
Minor release — **Phase 1 "bank the A": the grounding moat's supporting surface.** Three master-plan items land together: a self-contained, third-party-runnable benchmark harness (G1), installed dependency version pins in the generated context header (D8), and `verify` promoted to a documented flagship command (G2). All zero-dependency, deterministic, and in-boundary.
|
package/README.md
CHANGED
|
@@ -59,8 +59,8 @@ That map is exactly what agentic grep is worst at: reproducible, auditable conte
|
|
|
59
59
|
<!--SM:whyMetrics-->
|
|
60
60
|
- **86.7% hit@5** — right file found in top 5 results (vs 13.6% baseline)
|
|
61
61
|
- **97.0% token reduction** — average across 21 real repos
|
|
62
|
-
- **
|
|
63
|
-
- **1.
|
|
62
|
+
- **67.8% task success rate** — up from 10% without context
|
|
63
|
+
- **1.46 prompts per task** — down from 2.84 (48.8% fewer retries)
|
|
64
64
|
<!--/SM:whyMetrics-->
|
|
65
65
|
- **<!--SM:languages-->33<!--/SM:languages--> languages supported** — TypeScript, Python, Go, Rust, Java, R, and more
|
|
66
66
|
- **No vendor lock-in** — works with any AI assistant or local LLM
|
|
@@ -120,13 +120,13 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
120
120
|
|
|
121
121
|
<!--SM:benchmarkBlock-->
|
|
122
122
|
```
|
|
123
|
-
Benchmark : sigmap-v8.
|
|
123
|
+
Benchmark : sigmap-v8.7-main (21 repositories, including R language)
|
|
124
124
|
Date : 2026-07-05
|
|
125
125
|
|
|
126
126
|
Hit@5 : 86.7% (baseline 13.6% — 6.4× lift)
|
|
127
127
|
Token reduction: 97.0% (across 21 repos)
|
|
128
|
-
Prompt reduction : 48.
|
|
129
|
-
Task success :
|
|
128
|
+
Prompt reduction : 48.8% (2.84 → 1.46 prompts per task)
|
|
129
|
+
Task success : 67.8% (baseline 10%)
|
|
130
130
|
Repos tested : 21 (JavaScript, Python, Go, Rust, Java, R, C++, C#, Dart, Swift, Ruby, PHP, Scala, Kotlin, and more)
|
|
131
131
|
```
|
|
132
132
|
<!--/SM:benchmarkBlock-->
|
package/gen-context.js
CHANGED
|
@@ -10453,6 +10453,411 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
10453
10453
|
|
|
10454
10454
|
};
|
|
10455
10455
|
|
|
10456
|
+
// ── ./src/graph/call-graph ──
|
|
10457
|
+
__factories["./src/graph/call-graph"] = function(module, exports) {
|
|
10458
|
+
|
|
10459
|
+
/**
|
|
10460
|
+
* Method/caller-level call-graph (D4 v1).
|
|
10461
|
+
*
|
|
10462
|
+
* Builds symbol-level edges — which function calls which function — for JS/TS
|
|
10463
|
+
* and Python. Deterministic, zero-dependency, regex + brace/indent matching.
|
|
10464
|
+
* Call sites are resolved with high precision: a call resolves to a definition
|
|
10465
|
+
* of that name in the *same file* first, then in a *directly-imported* file
|
|
10466
|
+
* (via the existing file-level import graph). Names that resolve to no repo
|
|
10467
|
+
* definition produce no edge — over-approximation noise is avoided.
|
|
10468
|
+
*
|
|
10469
|
+
* Symbol IDs are `relPath#symbolName` (forward-slashed, relative to cwd).
|
|
10470
|
+
*
|
|
10471
|
+
* @module src/graph/call-graph
|
|
10472
|
+
*/
|
|
10473
|
+
|
|
10474
|
+
const fs = require('fs');
|
|
10475
|
+
const path = require('path');
|
|
10476
|
+
const { build } = __require('./src/graph/builder');
|
|
10477
|
+
|
|
10478
|
+
const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
10479
|
+
const PY_EXTS = new Set(['.py', '.pyw']);
|
|
10480
|
+
|
|
10481
|
+
// Tokens that look like `name(` calls or definition headers but are language
|
|
10482
|
+
// keywords, not user symbols — never treated as a call or a definition.
|
|
10483
|
+
const NON_CALL = new Set([
|
|
10484
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'function', 'typeof',
|
|
10485
|
+
'await', 'new', 'super', 'else', 'do', 'with', 'yield', 'void', 'delete',
|
|
10486
|
+
'in', 'of', 'case', 'throw', 'print', 'and', 'or', 'not', 'assert',
|
|
10487
|
+
'lambda', 'class', 'def', 'elif', 'except', 'finally', 'raise', 'import',
|
|
10488
|
+
'from', 'global', 'nonlocal', 'del', 'pass', 'async', 'require', 'constructor',
|
|
10489
|
+
]);
|
|
10490
|
+
|
|
10491
|
+
function normalizePath(p) { return path.normalize(p).toLowerCase(); }
|
|
10492
|
+
function toRel(cwd, f) { return path.relative(cwd, f).replace(/\\/g, '/'); }
|
|
10493
|
+
function symId(cwd, absFile, name) { return `${toRel(cwd, absFile)}#${name}`; }
|
|
10494
|
+
|
|
10495
|
+
// ── Length- and newline-preserving maskers ──────────────────────────────────
|
|
10496
|
+
// Replace comment / string bodies with spaces so their braces, parens, and
|
|
10497
|
+
// call-looking tokens never confuse structure detection. Offsets stay aligned.
|
|
10498
|
+
|
|
10499
|
+
function maskJs(src) {
|
|
10500
|
+
const out = src.split('');
|
|
10501
|
+
const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
10502
|
+
let i = 0; const n = src.length;
|
|
10503
|
+
while (i < n) {
|
|
10504
|
+
const c = src[i], d = src[i + 1];
|
|
10505
|
+
if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
|
|
10506
|
+
if (c === '/' && d === '*') { let j = i + 2; while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j++; j = Math.min(n, j + 2); blank(i, j); i = j; continue; }
|
|
10507
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
10508
|
+
let j = i + 1;
|
|
10509
|
+
while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c) break; if (c !== '`' && src[j] === '\n') break; j++; }
|
|
10510
|
+
j = Math.min(n, j + 1); blank(i, j); i = j; continue;
|
|
10511
|
+
}
|
|
10512
|
+
i++;
|
|
10513
|
+
}
|
|
10514
|
+
return out.join('');
|
|
10515
|
+
}
|
|
10516
|
+
|
|
10517
|
+
function maskPy(src) {
|
|
10518
|
+
const out = src.split('');
|
|
10519
|
+
const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
10520
|
+
let i = 0; const n = src.length;
|
|
10521
|
+
while (i < n) {
|
|
10522
|
+
const c = src[i];
|
|
10523
|
+
if (c === '#') { let j = i + 1; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
|
|
10524
|
+
if (c === '"' || c === "'") {
|
|
10525
|
+
if (src.substr(i, 3) === c + c + c) {
|
|
10526
|
+
let j = i + 3; while (j < n && src.substr(j, 3) !== c + c + c) j++; j = Math.min(n, j + 3); blank(i, j); i = j; continue;
|
|
10527
|
+
}
|
|
10528
|
+
let j = i + 1; while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c || src[j] === '\n') break; j++; }
|
|
10529
|
+
j = Math.min(n, j + 1); blank(i, j); i = j; continue;
|
|
10530
|
+
}
|
|
10531
|
+
i++;
|
|
10532
|
+
}
|
|
10533
|
+
return out.join('');
|
|
10534
|
+
}
|
|
10535
|
+
|
|
10536
|
+
// ── Balanced-delimiter matchers (operate on masked source) ───────────────────
|
|
10537
|
+
function matchDelim(masked, openIdx, open, close) {
|
|
10538
|
+
let depth = 0;
|
|
10539
|
+
for (let i = openIdx; i < masked.length; i++) {
|
|
10540
|
+
if (masked[i] === open) depth++;
|
|
10541
|
+
else if (masked[i] === close) { depth--; if (depth === 0) return i; }
|
|
10542
|
+
}
|
|
10543
|
+
return masked.length - 1;
|
|
10544
|
+
}
|
|
10545
|
+
|
|
10546
|
+
function lineAt(src, idx) {
|
|
10547
|
+
let line = 1;
|
|
10548
|
+
const end = Math.min(idx, src.length);
|
|
10549
|
+
for (let i = 0; i < end; i++) if (src.charCodeAt(i) === 10) line++;
|
|
10550
|
+
return line;
|
|
10551
|
+
}
|
|
10552
|
+
|
|
10553
|
+
// ── Definition extraction ────────────────────────────────────────────────────
|
|
10554
|
+
// Each def: { name, line, bodyStart, bodyEnd } with char offsets into `masked`.
|
|
10555
|
+
|
|
10556
|
+
function jsDefs(masked) {
|
|
10557
|
+
const defs = [];
|
|
10558
|
+
const seen = new Set();
|
|
10559
|
+
const push = (name, headerIdx, bodyStart, bodyEnd) => {
|
|
10560
|
+
const key = name + ':' + bodyStart;
|
|
10561
|
+
if (NON_CALL.has(name) || seen.has(key)) return;
|
|
10562
|
+
seen.add(key);
|
|
10563
|
+
defs.push({ name, line: lineAt(masked, headerIdx), bodyStart, bodyEnd });
|
|
10564
|
+
};
|
|
10565
|
+
|
|
10566
|
+
// Locate the `{` body (or `=>` expression) that follows a param list `)`.
|
|
10567
|
+
const bodyAfterParams = (closeParen) => {
|
|
10568
|
+
let k = closeParen + 1;
|
|
10569
|
+
// skip a `=>`, return-type annotations, and whitespace up to `{` or a statement end
|
|
10570
|
+
while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n') k++;
|
|
10571
|
+
if (masked[k] === '{') { const end = matchDelim(masked, k, '{', '}'); return { bodyStart: k, bodyEnd: end }; }
|
|
10572
|
+
return null; // no braced body (interface/abstract/overload signature) — skip
|
|
10573
|
+
};
|
|
10574
|
+
|
|
10575
|
+
// 1) function declarations: (async) function name(...) { ... }
|
|
10576
|
+
const reFn = /\b(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)\s*\(/g;
|
|
10577
|
+
let m;
|
|
10578
|
+
while ((m = reFn.exec(masked)) !== null) {
|
|
10579
|
+
const paren = masked.indexOf('(', m.index + m[0].length - 1);
|
|
10580
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
10581
|
+
const body = bodyAfterParams(close);
|
|
10582
|
+
if (body) push(m[1], m.index, body.bodyStart, body.bodyEnd);
|
|
10583
|
+
}
|
|
10584
|
+
|
|
10585
|
+
// 2) arrow / function expressions: const name = (...) => { } | = function(...) { }
|
|
10586
|
+
const reArrow = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\b\s*\*?\s*[A-Za-z_$]*\s*)?\(/g;
|
|
10587
|
+
while ((m = reArrow.exec(masked)) !== null) {
|
|
10588
|
+
const paren = masked.indexOf('(', m.index + m[0].length - 1);
|
|
10589
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
10590
|
+
let k = close + 1;
|
|
10591
|
+
while (k < masked.length && /\s/.test(masked[k])) k++;
|
|
10592
|
+
if (masked[k] === '{') { push(m[1], m.index, k, matchDelim(masked, k, '{', '}')); continue; }
|
|
10593
|
+
if (masked[k] === '=' && masked[k + 1] === '>') {
|
|
10594
|
+
let j = k + 2; while (j < masked.length && /\s/.test(masked[j])) j++;
|
|
10595
|
+
if (masked[j] === '{') push(m[1], m.index, j, matchDelim(masked, j, '{', '}'));
|
|
10596
|
+
else { // single-expression arrow body → to end of statement
|
|
10597
|
+
let e = j; let d = 0;
|
|
10598
|
+
while (e < masked.length) { const ch = masked[e]; if (ch === '(' || ch === '[') d++; else if (ch === ')' || ch === ']') d--; else if ((ch === ';' || ch === '\n') && d <= 0) break; e++; }
|
|
10599
|
+
push(m[1], m.index, j, e);
|
|
10600
|
+
}
|
|
10601
|
+
}
|
|
10602
|
+
}
|
|
10603
|
+
|
|
10604
|
+
// 3) class methods: class X { name(...) { } }
|
|
10605
|
+
const reClass = /\bclass\s+[A-Za-z_$][\w$]*/g;
|
|
10606
|
+
while ((m = reClass.exec(masked)) !== null) {
|
|
10607
|
+
const brace = masked.indexOf('{', m.index);
|
|
10608
|
+
if (brace === -1) continue;
|
|
10609
|
+
const classEnd = matchDelim(masked, brace, '{', '}');
|
|
10610
|
+
const reMethod = /(?:^|\n)\s*(?:public\s+|private\s+|protected\s+|static\s+|readonly\s+|abstract\s+|async\s+|get\s+|set\s+|\*\s*)*([A-Za-z_$][\w$]*)\s*\(/g;
|
|
10611
|
+
reMethod.lastIndex = brace;
|
|
10612
|
+
let mm;
|
|
10613
|
+
while ((mm = reMethod.exec(masked)) !== null && mm.index < classEnd) {
|
|
10614
|
+
const paren = masked.indexOf('(', mm.index + mm[0].length - 1);
|
|
10615
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
10616
|
+
const body = bodyAfterParams(close);
|
|
10617
|
+
if (body && body.bodyEnd <= classEnd) push(mm[1], mm.index, body.bodyStart, body.bodyEnd);
|
|
10618
|
+
}
|
|
10619
|
+
}
|
|
10620
|
+
|
|
10621
|
+
return defs;
|
|
10622
|
+
}
|
|
10623
|
+
|
|
10624
|
+
function pyDefs(masked) {
|
|
10625
|
+
const defs = [];
|
|
10626
|
+
const lines = masked.split('\n');
|
|
10627
|
+
// precompute char offset of each line start
|
|
10628
|
+
const offsets = [0];
|
|
10629
|
+
for (let i = 0; i < lines.length; i++) offsets.push(offsets[i] + lines[i].length + 1);
|
|
10630
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10631
|
+
const m = lines[i].match(/^([ \t]*)(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/);
|
|
10632
|
+
if (!m) continue;
|
|
10633
|
+
const indent = m[1].length;
|
|
10634
|
+
let j = i + 1;
|
|
10635
|
+
for (; j < lines.length; j++) {
|
|
10636
|
+
const ln = lines[j];
|
|
10637
|
+
if (!ln.trim()) continue; // blank
|
|
10638
|
+
const ind = ln.match(/^[ \t]*/)[0].length;
|
|
10639
|
+
if (ind <= indent) break; // dedent → block ends
|
|
10640
|
+
}
|
|
10641
|
+
defs.push({ name: m[2], line: i + 1, bodyStart: offsets[i], bodyEnd: offsets[j] || masked.length });
|
|
10642
|
+
}
|
|
10643
|
+
return defs;
|
|
10644
|
+
}
|
|
10645
|
+
|
|
10646
|
+
function extractDefs(filePath, src) {
|
|
10647
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
10648
|
+
if (JS_EXTS.has(ext)) return jsDefs(maskJs(src));
|
|
10649
|
+
if (PY_EXTS.has(ext)) return pyDefs(maskPy(src));
|
|
10650
|
+
return null; // unsupported language
|
|
10651
|
+
}
|
|
10652
|
+
|
|
10653
|
+
// Collect `name(` call tokens within [start,end) of masked source.
|
|
10654
|
+
function callsInRange(masked, start, end) {
|
|
10655
|
+
const slice = masked.slice(start, end);
|
|
10656
|
+
const names = new Set();
|
|
10657
|
+
const re = /([A-Za-z_$][\w$]*)\s*\(/g;
|
|
10658
|
+
let m;
|
|
10659
|
+
while ((m = re.exec(slice)) !== null) {
|
|
10660
|
+
// skip a `.name(` method access (can't resolve the receiver deterministically)
|
|
10661
|
+
const before = slice[m.index - 1];
|
|
10662
|
+
if (before === '.') continue;
|
|
10663
|
+
if (!NON_CALL.has(m[1])) names.add(m[1]);
|
|
10664
|
+
}
|
|
10665
|
+
return names;
|
|
10666
|
+
}
|
|
10667
|
+
|
|
10668
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
10669
|
+
|
|
10670
|
+
function _walk(dir, excludeSet, out, depth) {
|
|
10671
|
+
if (depth > 8) return;
|
|
10672
|
+
let entries;
|
|
10673
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
10674
|
+
for (const e of entries) {
|
|
10675
|
+
if (excludeSet.has(e.name) || e.name.startsWith('.')) continue;
|
|
10676
|
+
const full = path.join(dir, e.name);
|
|
10677
|
+
if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
|
|
10678
|
+
else if (e.isFile()) {
|
|
10679
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
10680
|
+
if (JS_EXTS.has(ext) || PY_EXTS.has(ext)) out.push(full);
|
|
10681
|
+
}
|
|
10682
|
+
}
|
|
10683
|
+
}
|
|
10684
|
+
|
|
10685
|
+
/**
|
|
10686
|
+
* Build the method-level call-graph for a project.
|
|
10687
|
+
*
|
|
10688
|
+
* @param {string} cwd
|
|
10689
|
+
* @param {object} [opts]
|
|
10690
|
+
* @param {string[]} [opts.srcDirs=['src','app','lib']]
|
|
10691
|
+
* @param {string[]} [opts.exclude]
|
|
10692
|
+
* @param {string[]} [opts.files] explicit absolute file list (skips the walk)
|
|
10693
|
+
* @returns {{
|
|
10694
|
+
* forward: Map<string,string[]>, // callerId → calleeIds
|
|
10695
|
+
* reverse: Map<string,string[]>, // calleeId → callerIds
|
|
10696
|
+
* defs: Map<string,{file:string,name:string,line:number}>
|
|
10697
|
+
* }}
|
|
10698
|
+
*/
|
|
10699
|
+
function buildCallGraph(cwd, opts = {}) {
|
|
10700
|
+
const excludeSet = new Set(opts.exclude || ['node_modules', '.git', 'dist', 'build', 'coverage', 'vendor']);
|
|
10701
|
+
let files = opts.files ? opts.files.map((f) => path.resolve(f)) : [];
|
|
10702
|
+
if (!opts.files) {
|
|
10703
|
+
for (const sd of (opts.srcDirs || ['src', 'app', 'lib'])) {
|
|
10704
|
+
const abs = path.resolve(cwd, sd);
|
|
10705
|
+
if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0);
|
|
10706
|
+
}
|
|
10707
|
+
}
|
|
10708
|
+
|
|
10709
|
+
// File-level import graph (for precise call-site resolution). Keys normalized.
|
|
10710
|
+
let fileGraph;
|
|
10711
|
+
try { fileGraph = build(files, cwd); } catch (_) { fileGraph = { forward: new Map() }; }
|
|
10712
|
+
|
|
10713
|
+
// Per-file definitions + name→file lookups.
|
|
10714
|
+
const perFileDefs = new Map(); // absFile → def[]
|
|
10715
|
+
const defsByName = new Map(); // absFile → Map<name, symbolId[]>
|
|
10716
|
+
const normToAbs = new Map(); // normalized abs → abs
|
|
10717
|
+
const defs = new Map(); // symbolId → {file,name,line}
|
|
10718
|
+
|
|
10719
|
+
for (const f of files) {
|
|
10720
|
+
normToAbs.set(normalizePath(path.resolve(f)), path.resolve(f));
|
|
10721
|
+
let src;
|
|
10722
|
+
try { src = fs.readFileSync(f, 'utf8'); } catch (_) { continue; }
|
|
10723
|
+
const fileDefs = extractDefs(f, src);
|
|
10724
|
+
if (!fileDefs) continue;
|
|
10725
|
+
perFileDefs.set(f, fileDefs);
|
|
10726
|
+
const byName = new Map();
|
|
10727
|
+
for (const d of fileDefs) {
|
|
10728
|
+
const id = symId(cwd, f, d.name);
|
|
10729
|
+
defs.set(id, { file: toRel(cwd, f), name: d.name, line: d.line });
|
|
10730
|
+
if (!byName.has(d.name)) byName.set(d.name, []);
|
|
10731
|
+
byName.get(d.name).push(id);
|
|
10732
|
+
}
|
|
10733
|
+
defsByName.set(f, byName);
|
|
10734
|
+
}
|
|
10735
|
+
|
|
10736
|
+
const forward = new Map();
|
|
10737
|
+
const reverse = new Map();
|
|
10738
|
+
const addEdge = (from, to) => {
|
|
10739
|
+
if (from === to) return;
|
|
10740
|
+
if (!forward.has(from)) forward.set(from, new Set());
|
|
10741
|
+
forward.get(from).add(to);
|
|
10742
|
+
if (!reverse.has(to)) reverse.set(to, new Set());
|
|
10743
|
+
reverse.get(to).add(from);
|
|
10744
|
+
};
|
|
10745
|
+
|
|
10746
|
+
for (const [f, fileDefs] of perFileDefs.entries()) {
|
|
10747
|
+
const masked = JS_EXTS.has(path.extname(f).toLowerCase()) ? maskJs(fs.readFileSync(f, 'utf8')) : maskPy(fs.readFileSync(f, 'utf8'));
|
|
10748
|
+
// resolution scope: this file's defs, then directly-imported files' defs
|
|
10749
|
+
const importedAbs = (fileGraph.forward.get(normalizePath(path.resolve(f))) || [])
|
|
10750
|
+
.map((nf) => normToAbs.get(nf)).filter(Boolean);
|
|
10751
|
+
for (const d of fileDefs) {
|
|
10752
|
+
const callerId = symId(cwd, f, d.name);
|
|
10753
|
+
if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
|
|
10754
|
+
const callees = callsInRange(masked, d.bodyStart, d.bodyEnd);
|
|
10755
|
+
for (const nm of callees) {
|
|
10756
|
+
const local = (defsByName.get(f) || new Map()).get(nm);
|
|
10757
|
+
if (local && local.length) { for (const id of local) addEdge(callerId, id); continue; }
|
|
10758
|
+
for (const imp of importedAbs) {
|
|
10759
|
+
const ids = (defsByName.get(imp) || new Map()).get(nm);
|
|
10760
|
+
if (ids && ids.length) { for (const id of ids) addEdge(callerId, id); break; }
|
|
10761
|
+
}
|
|
10762
|
+
}
|
|
10763
|
+
}
|
|
10764
|
+
}
|
|
10765
|
+
|
|
10766
|
+
const toArr = (mapOfSets) => {
|
|
10767
|
+
const out = new Map();
|
|
10768
|
+
for (const [k, set] of mapOfSets.entries()) out.set(k, [...set]);
|
|
10769
|
+
return out;
|
|
10770
|
+
};
|
|
10771
|
+
return { forward: toArr(forward), reverse: toArr(reverse), defs };
|
|
10772
|
+
}
|
|
10773
|
+
|
|
10774
|
+
// Resolve a user-supplied symbol (bare name or full `file#name` id) to ids.
|
|
10775
|
+
function _resolveSymbol(symbol, defs) {
|
|
10776
|
+
if (defs.has(symbol)) return [symbol];
|
|
10777
|
+
const ids = [];
|
|
10778
|
+
for (const id of defs.keys()) if (id.slice(id.indexOf('#') + 1) === symbol) ids.push(id);
|
|
10779
|
+
return ids;
|
|
10780
|
+
}
|
|
10781
|
+
|
|
10782
|
+
// BFS over a graph map from seed ids up to maxDepth (0 = unlimited).
|
|
10783
|
+
function _bfs(seedIds, graph, maxDepth) {
|
|
10784
|
+
const direct = new Set();
|
|
10785
|
+
const transitive = new Set();
|
|
10786
|
+
const visited = new Set(seedIds);
|
|
10787
|
+
let frontier = [];
|
|
10788
|
+
for (const s of seedIds) for (const nb of (graph.get(s) || [])) if (!visited.has(nb)) { direct.add(nb); visited.add(nb); frontier.push(nb); }
|
|
10789
|
+
let depth = 1;
|
|
10790
|
+
while (frontier.length && (maxDepth === 0 || depth < maxDepth)) {
|
|
10791
|
+
const next = [];
|
|
10792
|
+
for (const node of frontier) for (const nb of (graph.get(node) || [])) if (!visited.has(nb)) { transitive.add(nb); visited.add(nb); next.push(nb); }
|
|
10793
|
+
frontier = next; depth++;
|
|
10794
|
+
}
|
|
10795
|
+
return { direct: [...direct], transitive: [...transitive] };
|
|
10796
|
+
}
|
|
10797
|
+
|
|
10798
|
+
/**
|
|
10799
|
+
* Method-level blast radius: everything that (transitively) calls `symbol`.
|
|
10800
|
+
*
|
|
10801
|
+
* @param {string} symbol bare name or `file#name`
|
|
10802
|
+
* @param {string} cwd
|
|
10803
|
+
* @param {object} [opts] { depth=0, ...buildCallGraph opts }
|
|
10804
|
+
* @returns {{ symbol:string, resolved:string[], direct:string[], transitive:string[], total:number, unresolved:boolean }}
|
|
10805
|
+
*/
|
|
10806
|
+
function methodImpact(symbol, cwd, opts = {}) {
|
|
10807
|
+
const graph = opts.graph || buildCallGraph(cwd, opts);
|
|
10808
|
+
const ids = _resolveSymbol(symbol, graph.defs);
|
|
10809
|
+
if (ids.length === 0) return { symbol, resolved: [], direct: [], transitive: [], total: 0, unresolved: true };
|
|
10810
|
+
const { direct, transitive } = _bfs(ids, graph.reverse, opts.depth || 0);
|
|
10811
|
+
return { symbol, resolved: ids, direct, transitive, total: direct.length + transitive.length, unresolved: false };
|
|
10812
|
+
}
|
|
10813
|
+
|
|
10814
|
+
/**
|
|
10815
|
+
* What `symbol` (transitively) calls.
|
|
10816
|
+
* @returns {{ symbol:string, resolved:string[], direct:string[], transitive:string[], total:number, unresolved:boolean }}
|
|
10817
|
+
*/
|
|
10818
|
+
function methodCallees(symbol, cwd, opts = {}) {
|
|
10819
|
+
const graph = opts.graph || buildCallGraph(cwd, opts);
|
|
10820
|
+
const ids = _resolveSymbol(symbol, graph.defs);
|
|
10821
|
+
if (ids.length === 0) return { symbol, resolved: [], direct: [], transitive: [], total: 0, unresolved: true };
|
|
10822
|
+
const { direct, transitive } = _bfs(ids, graph.forward, opts.depth || 0);
|
|
10823
|
+
return { symbol, resolved: ids, direct, transitive, total: direct.length + transitive.length, unresolved: false };
|
|
10824
|
+
}
|
|
10825
|
+
|
|
10826
|
+
// ── Formatters ───────────────────────────────────────────────────────────────
|
|
10827
|
+
function formatCallGraph(result, kind) {
|
|
10828
|
+
const verb = kind === 'callees' ? 'calls' : 'callers of';
|
|
10829
|
+
const lines = [`## ${kind === 'callees' ? 'Callees' : 'Callers'}: \`${result.symbol}\``, ''];
|
|
10830
|
+
if (result.unresolved) { lines.push('_symbol not found in the call-graph._'); return lines.join('\n'); }
|
|
10831
|
+
if (result.total === 0) {
|
|
10832
|
+
lines.push(kind === 'callees' ? '_calls no repo-defined symbols._' : '_no repo symbol calls this — zero method blast radius._');
|
|
10833
|
+
return lines.join('\n');
|
|
10834
|
+
}
|
|
10835
|
+
lines.push(`**Total ${verb}:** ${result.total}`, '');
|
|
10836
|
+
if (result.direct.length) { lines.push(`### Direct`); for (const id of result.direct) lines.push(`- \`${id}\``); lines.push(''); }
|
|
10837
|
+
if (result.transitive.length) { lines.push(`### Transitive`); for (const id of result.transitive) lines.push(`- \`${id}\``); lines.push(''); }
|
|
10838
|
+
return lines.join('\n');
|
|
10839
|
+
}
|
|
10840
|
+
|
|
10841
|
+
function formatCallGraphJSON(result, kind) {
|
|
10842
|
+
return {
|
|
10843
|
+
symbol: result.symbol,
|
|
10844
|
+
kind: kind === 'callees' ? 'callees' : 'callers',
|
|
10845
|
+
resolved: result.resolved,
|
|
10846
|
+
direct: result.direct,
|
|
10847
|
+
transitive: result.transitive,
|
|
10848
|
+
total: result.total,
|
|
10849
|
+
unresolved: result.unresolved,
|
|
10850
|
+
};
|
|
10851
|
+
}
|
|
10852
|
+
|
|
10853
|
+
module.exports = {
|
|
10854
|
+
buildCallGraph, methodImpact, methodCallees,
|
|
10855
|
+
formatCallGraph, formatCallGraphJSON,
|
|
10856
|
+
extractDefs, maskJs, maskPy,
|
|
10857
|
+
};
|
|
10858
|
+
|
|
10859
|
+
};
|
|
10860
|
+
|
|
10456
10861
|
// ── ./src/graph/impact ──
|
|
10457
10862
|
__factories["./src/graph/impact"] = function(module, exports) {
|
|
10458
10863
|
|
|
@@ -13111,7 +13516,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
13111
13516
|
|
|
13112
13517
|
const SERVER_INFO = {
|
|
13113
13518
|
name: 'sigmap',
|
|
13114
|
-
version: '8.
|
|
13519
|
+
version: '8.7.1',
|
|
13115
13520
|
description: 'SigMap MCP server — code signatures on demand',
|
|
13116
13521
|
};
|
|
13117
13522
|
|
|
@@ -16446,11 +16851,12 @@ __factories["./src/tracking/pricing"] = function(module, exports) {
|
|
|
16446
16851
|
* presented as exact. Zero npm dependencies.
|
|
16447
16852
|
*/
|
|
16448
16853
|
|
|
16449
|
-
// USD per 1,000,000 input tokens.
|
|
16854
|
+
// USD per 1,000,000 input tokens. Claude rates verified 2026-07 against
|
|
16855
|
+
// platform.claude.com (Opus 4.8 $5, Sonnet 5/4.6 $3, Haiku 4.5 $1); GPT-4o $2.50.
|
|
16450
16856
|
const PRICES = {
|
|
16451
16857
|
'claude-sonnet': 3.0,
|
|
16452
|
-
'claude-opus':
|
|
16453
|
-
'claude-haiku': 0
|
|
16858
|
+
'claude-opus': 5.0,
|
|
16859
|
+
'claude-haiku': 1.0,
|
|
16454
16860
|
'gpt-4o': 2.5,
|
|
16455
16861
|
'gpt-4o-mini': 0.15,
|
|
16456
16862
|
'gemini-1.5-pro': 1.25,
|
|
@@ -17679,7 +18085,7 @@ function __tryGit(args, opts = {}) {
|
|
|
17679
18085
|
catch (_) { return ''; }
|
|
17680
18086
|
}
|
|
17681
18087
|
|
|
17682
|
-
const VERSION = '8.
|
|
18088
|
+
const VERSION = '8.7.1';
|
|
17683
18089
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
17684
18090
|
|
|
17685
18091
|
function requireSourceOrBundled(key) {
|
|
@@ -19491,6 +19897,9 @@ Usage:
|
|
|
19491
19897
|
${cmd} --impact <file> Show every file impacted by changing <file>
|
|
19492
19898
|
${cmd} --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
|
|
19493
19899
|
${cmd} --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
|
|
19900
|
+
${cmd} --callers <symbol> Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS + Python)
|
|
19901
|
+
${cmd} --callees <symbol> Every repo function that <symbol> (transitively) calls
|
|
19902
|
+
${cmd} --callers <symbol> --json --depth <n> Call-graph edges as JSON (depth 0 = unlimited)
|
|
19494
19903
|
${cmd} verify <answer.md> Flagship grounding guard — flag fake files/tests/imports/symbols/npm-scripts in an AI answer (alias of verify-ai-output)
|
|
19495
19904
|
${cmd} verify <answer.md> --json Grounding report as JSON (exits 1 if issues)
|
|
19496
19905
|
${cmd} verify <answer.md> --report Write a standalone HTML report (red/amber/green)
|
|
@@ -22067,6 +22476,37 @@ function main() {
|
|
|
22067
22476
|
process.exit(0);
|
|
22068
22477
|
}
|
|
22069
22478
|
|
|
22479
|
+
// Method-level call-graph (D4): --callers <symbol> = method blast radius,
|
|
22480
|
+
// --callees <symbol> = what it calls. JS/TS + Python, deterministic.
|
|
22481
|
+
if (args.includes('--callers') || args.includes('--callees')) {
|
|
22482
|
+
try {
|
|
22483
|
+
const kind = args.includes('--callees') ? 'callees' : 'callers';
|
|
22484
|
+
const flag = kind === 'callees' ? '--callees' : '--callers';
|
|
22485
|
+
const symbol = (args[args.indexOf(flag) + 1] || '').trim();
|
|
22486
|
+
if (!symbol || symbol.startsWith('--')) {
|
|
22487
|
+
console.error(`[sigmap] ${flag} requires a symbol name (or file#symbol)`);
|
|
22488
|
+
console.error(` Example: node gen-context.js ${flag} extractFileDeps`);
|
|
22489
|
+
process.exit(1);
|
|
22490
|
+
}
|
|
22491
|
+
const { methodImpact, methodCallees, formatCallGraph, formatCallGraphJSON } = requireSourceOrBundled('./src/graph/call-graph');
|
|
22492
|
+
const depthIdx = args.indexOf('--depth');
|
|
22493
|
+
const depth = depthIdx >= 0 ? Math.max(0, parseInt(args[depthIdx + 1], 10) || 0) : 0;
|
|
22494
|
+
const srcDirs = (config && Array.isArray(config.srcDirs) && config.srcDirs.length) ? config.srcDirs : undefined;
|
|
22495
|
+
const result = kind === 'callees'
|
|
22496
|
+
? methodCallees(symbol, cwd, { depth, srcDirs })
|
|
22497
|
+
: methodImpact(symbol, cwd, { depth, srcDirs });
|
|
22498
|
+
if (args.includes('--json')) {
|
|
22499
|
+
process.stdout.write(JSON.stringify(formatCallGraphJSON(result, kind)) + '\n');
|
|
22500
|
+
} else {
|
|
22501
|
+
process.stdout.write(formatCallGraph(result, kind) + '\n');
|
|
22502
|
+
}
|
|
22503
|
+
} catch (err) {
|
|
22504
|
+
console.error(`[sigmap] call-graph error: ${err.message}`);
|
|
22505
|
+
process.exit(1);
|
|
22506
|
+
}
|
|
22507
|
+
process.exit(0);
|
|
22508
|
+
}
|
|
22509
|
+
|
|
22070
22510
|
if (args.includes('--report')) {
|
|
22071
22511
|
if (args.includes('--history')) {
|
|
22072
22512
|
try {
|
package/llms-full.txt
CHANGED
|
@@ -11,20 +11,20 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.7.1 | Benchmark: sigmap-v8.7-main (2026-07-05)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
20
|
+
## Core metrics (benchmark: sigmap-v8.7-main, 2026-07-05)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
24
24
|
| Retrieval hit@5 | 13.6% (random) | 86.7% (6.4× lift) |
|
|
25
25
|
| Token reduction | — | 97.0% average |
|
|
26
|
-
| Task success proxy | 10% |
|
|
27
|
-
| Prompts per task | 2.84 | 1.
|
|
26
|
+
| Task success proxy | 10% | 67.8% |
|
|
27
|
+
| Prompts per task | 2.84 | 1.46 (48.8% fewer) |
|
|
28
28
|
| Supported languages | — | 33 |
|
|
29
29
|
| MCP tools | — | 18 |
|
|
30
30
|
| npm runtime dependencies | — | 0 |
|
|
@@ -99,6 +99,9 @@ sigmap weights --json Learned weights as JSON
|
|
|
99
99
|
sigmap --impact <file> Show every file impacted by changing <file>
|
|
100
100
|
sigmap --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
|
|
101
101
|
sigmap --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
|
|
102
|
+
sigmap --callers <symbol> Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS + Python)
|
|
103
|
+
sigmap --callees <symbol> Every repo function that <symbol> (transitively) calls
|
|
104
|
+
sigmap --callers <symbol> --json --depth <n> Call-graph edges as JSON (depth 0 = unlimited)
|
|
102
105
|
sigmap verify <answer.md> Flagship grounding guard — flag fake files/tests/imports/symbols/npm-scripts in an AI answer (alias of verify-ai-output)
|
|
103
106
|
sigmap verify <answer.md> --json Grounding report as JSON (exits 1 if issues)
|
|
104
107
|
sigmap verify <answer.md> --report Write a standalone HTML report (red/amber/green)
|
package/llms.txt
CHANGED
|
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.7.1 | Benchmark: sigmap-v8.7-main (2026-07-05)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
@@ -23,12 +23,12 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
23
23
|
- No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
|
|
24
24
|
- Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
|
|
25
25
|
|
|
26
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
26
|
+
## Core metrics (benchmark: sigmap-v8.7-main, 2026-07-05)
|
|
27
27
|
|
|
28
28
|
- hit@5 retrieval: 86.7% vs 13.6% random baseline (6.4× lift)
|
|
29
29
|
- Token reduction: 97.0% average across benchmark repos
|
|
30
|
-
- Task success:
|
|
31
|
-
- Prompts per task: 1.
|
|
30
|
+
- Task success: 67.8% vs 10% without SigMap
|
|
31
|
+
- Prompts per task: 1.46 vs 2.84 baseline (48.8% fewer)
|
|
32
32
|
- Languages: 33 supported · MCP tools: 18
|
|
33
33
|
- Dependencies: zero npm runtime dependencies · fully offline
|
|
34
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.7.1",
|
|
4
4
|
"description": "97% token reduction for AI coding. Extracts function & class signatures with TF-IDF ranking to feed only the right files to Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP. Zero dependencies, runs offline via npx.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Method/caller-level call-graph (D4 v1).
|
|
5
|
+
*
|
|
6
|
+
* Builds symbol-level edges — which function calls which function — for JS/TS
|
|
7
|
+
* and Python. Deterministic, zero-dependency, regex + brace/indent matching.
|
|
8
|
+
* Call sites are resolved with high precision: a call resolves to a definition
|
|
9
|
+
* of that name in the *same file* first, then in a *directly-imported* file
|
|
10
|
+
* (via the existing file-level import graph). Names that resolve to no repo
|
|
11
|
+
* definition produce no edge — over-approximation noise is avoided.
|
|
12
|
+
*
|
|
13
|
+
* Symbol IDs are `relPath#symbolName` (forward-slashed, relative to cwd).
|
|
14
|
+
*
|
|
15
|
+
* @module src/graph/call-graph
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { build } = require('./builder');
|
|
21
|
+
|
|
22
|
+
const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
23
|
+
const PY_EXTS = new Set(['.py', '.pyw']);
|
|
24
|
+
|
|
25
|
+
// Tokens that look like `name(` calls or definition headers but are language
|
|
26
|
+
// keywords, not user symbols — never treated as a call or a definition.
|
|
27
|
+
const NON_CALL = new Set([
|
|
28
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'function', 'typeof',
|
|
29
|
+
'await', 'new', 'super', 'else', 'do', 'with', 'yield', 'void', 'delete',
|
|
30
|
+
'in', 'of', 'case', 'throw', 'print', 'and', 'or', 'not', 'assert',
|
|
31
|
+
'lambda', 'class', 'def', 'elif', 'except', 'finally', 'raise', 'import',
|
|
32
|
+
'from', 'global', 'nonlocal', 'del', 'pass', 'async', 'require', 'constructor',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function normalizePath(p) { return path.normalize(p).toLowerCase(); }
|
|
36
|
+
function toRel(cwd, f) { return path.relative(cwd, f).replace(/\\/g, '/'); }
|
|
37
|
+
function symId(cwd, absFile, name) { return `${toRel(cwd, absFile)}#${name}`; }
|
|
38
|
+
|
|
39
|
+
// ── Length- and newline-preserving maskers ──────────────────────────────────
|
|
40
|
+
// Replace comment / string bodies with spaces so their braces, parens, and
|
|
41
|
+
// call-looking tokens never confuse structure detection. Offsets stay aligned.
|
|
42
|
+
|
|
43
|
+
function maskJs(src) {
|
|
44
|
+
const out = src.split('');
|
|
45
|
+
const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
46
|
+
let i = 0; const n = src.length;
|
|
47
|
+
while (i < n) {
|
|
48
|
+
const c = src[i], d = src[i + 1];
|
|
49
|
+
if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
|
|
50
|
+
if (c === '/' && d === '*') { let j = i + 2; while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j++; j = Math.min(n, j + 2); blank(i, j); i = j; continue; }
|
|
51
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
52
|
+
let j = i + 1;
|
|
53
|
+
while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c) break; if (c !== '`' && src[j] === '\n') break; j++; }
|
|
54
|
+
j = Math.min(n, j + 1); blank(i, j); i = j; continue;
|
|
55
|
+
}
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
return out.join('');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function maskPy(src) {
|
|
62
|
+
const out = src.split('');
|
|
63
|
+
const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
64
|
+
let i = 0; const n = src.length;
|
|
65
|
+
while (i < n) {
|
|
66
|
+
const c = src[i];
|
|
67
|
+
if (c === '#') { let j = i + 1; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
|
|
68
|
+
if (c === '"' || c === "'") {
|
|
69
|
+
if (src.substr(i, 3) === c + c + c) {
|
|
70
|
+
let j = i + 3; while (j < n && src.substr(j, 3) !== c + c + c) j++; j = Math.min(n, j + 3); blank(i, j); i = j; continue;
|
|
71
|
+
}
|
|
72
|
+
let j = i + 1; while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c || src[j] === '\n') break; j++; }
|
|
73
|
+
j = Math.min(n, j + 1); blank(i, j); i = j; continue;
|
|
74
|
+
}
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
return out.join('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Balanced-delimiter matchers (operate on masked source) ───────────────────
|
|
81
|
+
function matchDelim(masked, openIdx, open, close) {
|
|
82
|
+
let depth = 0;
|
|
83
|
+
for (let i = openIdx; i < masked.length; i++) {
|
|
84
|
+
if (masked[i] === open) depth++;
|
|
85
|
+
else if (masked[i] === close) { depth--; if (depth === 0) return i; }
|
|
86
|
+
}
|
|
87
|
+
return masked.length - 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function lineAt(src, idx) {
|
|
91
|
+
let line = 1;
|
|
92
|
+
const end = Math.min(idx, src.length);
|
|
93
|
+
for (let i = 0; i < end; i++) if (src.charCodeAt(i) === 10) line++;
|
|
94
|
+
return line;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Definition extraction ────────────────────────────────────────────────────
|
|
98
|
+
// Each def: { name, line, bodyStart, bodyEnd } with char offsets into `masked`.
|
|
99
|
+
|
|
100
|
+
function jsDefs(masked) {
|
|
101
|
+
const defs = [];
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const push = (name, headerIdx, bodyStart, bodyEnd) => {
|
|
104
|
+
const key = name + ':' + bodyStart;
|
|
105
|
+
if (NON_CALL.has(name) || seen.has(key)) return;
|
|
106
|
+
seen.add(key);
|
|
107
|
+
defs.push({ name, line: lineAt(masked, headerIdx), bodyStart, bodyEnd });
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Locate the `{` body (or `=>` expression) that follows a param list `)`.
|
|
111
|
+
const bodyAfterParams = (closeParen) => {
|
|
112
|
+
let k = closeParen + 1;
|
|
113
|
+
// skip a `=>`, return-type annotations, and whitespace up to `{` or a statement end
|
|
114
|
+
while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n') k++;
|
|
115
|
+
if (masked[k] === '{') { const end = matchDelim(masked, k, '{', '}'); return { bodyStart: k, bodyEnd: end }; }
|
|
116
|
+
return null; // no braced body (interface/abstract/overload signature) — skip
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// 1) function declarations: (async) function name(...) { ... }
|
|
120
|
+
const reFn = /\b(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)\s*\(/g;
|
|
121
|
+
let m;
|
|
122
|
+
while ((m = reFn.exec(masked)) !== null) {
|
|
123
|
+
const paren = masked.indexOf('(', m.index + m[0].length - 1);
|
|
124
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
125
|
+
const body = bodyAfterParams(close);
|
|
126
|
+
if (body) push(m[1], m.index, body.bodyStart, body.bodyEnd);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 2) arrow / function expressions: const name = (...) => { } | = function(...) { }
|
|
130
|
+
const reArrow = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\b\s*\*?\s*[A-Za-z_$]*\s*)?\(/g;
|
|
131
|
+
while ((m = reArrow.exec(masked)) !== null) {
|
|
132
|
+
const paren = masked.indexOf('(', m.index + m[0].length - 1);
|
|
133
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
134
|
+
let k = close + 1;
|
|
135
|
+
while (k < masked.length && /\s/.test(masked[k])) k++;
|
|
136
|
+
if (masked[k] === '{') { push(m[1], m.index, k, matchDelim(masked, k, '{', '}')); continue; }
|
|
137
|
+
if (masked[k] === '=' && masked[k + 1] === '>') {
|
|
138
|
+
let j = k + 2; while (j < masked.length && /\s/.test(masked[j])) j++;
|
|
139
|
+
if (masked[j] === '{') push(m[1], m.index, j, matchDelim(masked, j, '{', '}'));
|
|
140
|
+
else { // single-expression arrow body → to end of statement
|
|
141
|
+
let e = j; let d = 0;
|
|
142
|
+
while (e < masked.length) { const ch = masked[e]; if (ch === '(' || ch === '[') d++; else if (ch === ')' || ch === ']') d--; else if ((ch === ';' || ch === '\n') && d <= 0) break; e++; }
|
|
143
|
+
push(m[1], m.index, j, e);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 3) class methods: class X { name(...) { } }
|
|
149
|
+
const reClass = /\bclass\s+[A-Za-z_$][\w$]*/g;
|
|
150
|
+
while ((m = reClass.exec(masked)) !== null) {
|
|
151
|
+
const brace = masked.indexOf('{', m.index);
|
|
152
|
+
if (brace === -1) continue;
|
|
153
|
+
const classEnd = matchDelim(masked, brace, '{', '}');
|
|
154
|
+
const reMethod = /(?:^|\n)\s*(?:public\s+|private\s+|protected\s+|static\s+|readonly\s+|abstract\s+|async\s+|get\s+|set\s+|\*\s*)*([A-Za-z_$][\w$]*)\s*\(/g;
|
|
155
|
+
reMethod.lastIndex = brace;
|
|
156
|
+
let mm;
|
|
157
|
+
while ((mm = reMethod.exec(masked)) !== null && mm.index < classEnd) {
|
|
158
|
+
const paren = masked.indexOf('(', mm.index + mm[0].length - 1);
|
|
159
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
160
|
+
const body = bodyAfterParams(close);
|
|
161
|
+
if (body && body.bodyEnd <= classEnd) push(mm[1], mm.index, body.bodyStart, body.bodyEnd);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return defs;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function pyDefs(masked) {
|
|
169
|
+
const defs = [];
|
|
170
|
+
const lines = masked.split('\n');
|
|
171
|
+
// precompute char offset of each line start
|
|
172
|
+
const offsets = [0];
|
|
173
|
+
for (let i = 0; i < lines.length; i++) offsets.push(offsets[i] + lines[i].length + 1);
|
|
174
|
+
for (let i = 0; i < lines.length; i++) {
|
|
175
|
+
const m = lines[i].match(/^([ \t]*)(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/);
|
|
176
|
+
if (!m) continue;
|
|
177
|
+
const indent = m[1].length;
|
|
178
|
+
let j = i + 1;
|
|
179
|
+
for (; j < lines.length; j++) {
|
|
180
|
+
const ln = lines[j];
|
|
181
|
+
if (!ln.trim()) continue; // blank
|
|
182
|
+
const ind = ln.match(/^[ \t]*/)[0].length;
|
|
183
|
+
if (ind <= indent) break; // dedent → block ends
|
|
184
|
+
}
|
|
185
|
+
defs.push({ name: m[2], line: i + 1, bodyStart: offsets[i], bodyEnd: offsets[j] || masked.length });
|
|
186
|
+
}
|
|
187
|
+
return defs;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function extractDefs(filePath, src) {
|
|
191
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
192
|
+
if (JS_EXTS.has(ext)) return jsDefs(maskJs(src));
|
|
193
|
+
if (PY_EXTS.has(ext)) return pyDefs(maskPy(src));
|
|
194
|
+
return null; // unsupported language
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Collect `name(` call tokens within [start,end) of masked source.
|
|
198
|
+
function callsInRange(masked, start, end) {
|
|
199
|
+
const slice = masked.slice(start, end);
|
|
200
|
+
const names = new Set();
|
|
201
|
+
const re = /([A-Za-z_$][\w$]*)\s*\(/g;
|
|
202
|
+
let m;
|
|
203
|
+
while ((m = re.exec(slice)) !== null) {
|
|
204
|
+
// skip a `.name(` method access (can't resolve the receiver deterministically)
|
|
205
|
+
const before = slice[m.index - 1];
|
|
206
|
+
if (before === '.') continue;
|
|
207
|
+
if (!NON_CALL.has(m[1])) names.add(m[1]);
|
|
208
|
+
}
|
|
209
|
+
return names;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
function _walk(dir, excludeSet, out, depth) {
|
|
215
|
+
if (depth > 8) return;
|
|
216
|
+
let entries;
|
|
217
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
218
|
+
for (const e of entries) {
|
|
219
|
+
if (excludeSet.has(e.name) || e.name.startsWith('.')) continue;
|
|
220
|
+
const full = path.join(dir, e.name);
|
|
221
|
+
if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
|
|
222
|
+
else if (e.isFile()) {
|
|
223
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
224
|
+
if (JS_EXTS.has(ext) || PY_EXTS.has(ext)) out.push(full);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Build the method-level call-graph for a project.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} cwd
|
|
233
|
+
* @param {object} [opts]
|
|
234
|
+
* @param {string[]} [opts.srcDirs=['src','app','lib']]
|
|
235
|
+
* @param {string[]} [opts.exclude]
|
|
236
|
+
* @param {string[]} [opts.files] explicit absolute file list (skips the walk)
|
|
237
|
+
* @returns {{
|
|
238
|
+
* forward: Map<string,string[]>, // callerId → calleeIds
|
|
239
|
+
* reverse: Map<string,string[]>, // calleeId → callerIds
|
|
240
|
+
* defs: Map<string,{file:string,name:string,line:number}>
|
|
241
|
+
* }}
|
|
242
|
+
*/
|
|
243
|
+
function buildCallGraph(cwd, opts = {}) {
|
|
244
|
+
const excludeSet = new Set(opts.exclude || ['node_modules', '.git', 'dist', 'build', 'coverage', 'vendor']);
|
|
245
|
+
let files = opts.files ? opts.files.map((f) => path.resolve(f)) : [];
|
|
246
|
+
if (!opts.files) {
|
|
247
|
+
for (const sd of (opts.srcDirs || ['src', 'app', 'lib'])) {
|
|
248
|
+
const abs = path.resolve(cwd, sd);
|
|
249
|
+
if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// File-level import graph (for precise call-site resolution). Keys normalized.
|
|
254
|
+
let fileGraph;
|
|
255
|
+
try { fileGraph = build(files, cwd); } catch (_) { fileGraph = { forward: new Map() }; }
|
|
256
|
+
|
|
257
|
+
// Per-file definitions + name→file lookups.
|
|
258
|
+
const perFileDefs = new Map(); // absFile → def[]
|
|
259
|
+
const defsByName = new Map(); // absFile → Map<name, symbolId[]>
|
|
260
|
+
const normToAbs = new Map(); // normalized abs → abs
|
|
261
|
+
const defs = new Map(); // symbolId → {file,name,line}
|
|
262
|
+
|
|
263
|
+
for (const f of files) {
|
|
264
|
+
normToAbs.set(normalizePath(path.resolve(f)), path.resolve(f));
|
|
265
|
+
let src;
|
|
266
|
+
try { src = fs.readFileSync(f, 'utf8'); } catch (_) { continue; }
|
|
267
|
+
const fileDefs = extractDefs(f, src);
|
|
268
|
+
if (!fileDefs) continue;
|
|
269
|
+
perFileDefs.set(f, fileDefs);
|
|
270
|
+
const byName = new Map();
|
|
271
|
+
for (const d of fileDefs) {
|
|
272
|
+
const id = symId(cwd, f, d.name);
|
|
273
|
+
defs.set(id, { file: toRel(cwd, f), name: d.name, line: d.line });
|
|
274
|
+
if (!byName.has(d.name)) byName.set(d.name, []);
|
|
275
|
+
byName.get(d.name).push(id);
|
|
276
|
+
}
|
|
277
|
+
defsByName.set(f, byName);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const forward = new Map();
|
|
281
|
+
const reverse = new Map();
|
|
282
|
+
const addEdge = (from, to) => {
|
|
283
|
+
if (from === to) return;
|
|
284
|
+
if (!forward.has(from)) forward.set(from, new Set());
|
|
285
|
+
forward.get(from).add(to);
|
|
286
|
+
if (!reverse.has(to)) reverse.set(to, new Set());
|
|
287
|
+
reverse.get(to).add(from);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
for (const [f, fileDefs] of perFileDefs.entries()) {
|
|
291
|
+
const masked = JS_EXTS.has(path.extname(f).toLowerCase()) ? maskJs(fs.readFileSync(f, 'utf8')) : maskPy(fs.readFileSync(f, 'utf8'));
|
|
292
|
+
// resolution scope: this file's defs, then directly-imported files' defs
|
|
293
|
+
const importedAbs = (fileGraph.forward.get(normalizePath(path.resolve(f))) || [])
|
|
294
|
+
.map((nf) => normToAbs.get(nf)).filter(Boolean);
|
|
295
|
+
for (const d of fileDefs) {
|
|
296
|
+
const callerId = symId(cwd, f, d.name);
|
|
297
|
+
if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
|
|
298
|
+
const callees = callsInRange(masked, d.bodyStart, d.bodyEnd);
|
|
299
|
+
for (const nm of callees) {
|
|
300
|
+
const local = (defsByName.get(f) || new Map()).get(nm);
|
|
301
|
+
if (local && local.length) { for (const id of local) addEdge(callerId, id); continue; }
|
|
302
|
+
for (const imp of importedAbs) {
|
|
303
|
+
const ids = (defsByName.get(imp) || new Map()).get(nm);
|
|
304
|
+
if (ids && ids.length) { for (const id of ids) addEdge(callerId, id); break; }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const toArr = (mapOfSets) => {
|
|
311
|
+
const out = new Map();
|
|
312
|
+
for (const [k, set] of mapOfSets.entries()) out.set(k, [...set]);
|
|
313
|
+
return out;
|
|
314
|
+
};
|
|
315
|
+
return { forward: toArr(forward), reverse: toArr(reverse), defs };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Resolve a user-supplied symbol (bare name or full `file#name` id) to ids.
|
|
319
|
+
function _resolveSymbol(symbol, defs) {
|
|
320
|
+
if (defs.has(symbol)) return [symbol];
|
|
321
|
+
const ids = [];
|
|
322
|
+
for (const id of defs.keys()) if (id.slice(id.indexOf('#') + 1) === symbol) ids.push(id);
|
|
323
|
+
return ids;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// BFS over a graph map from seed ids up to maxDepth (0 = unlimited).
|
|
327
|
+
function _bfs(seedIds, graph, maxDepth) {
|
|
328
|
+
const direct = new Set();
|
|
329
|
+
const transitive = new Set();
|
|
330
|
+
const visited = new Set(seedIds);
|
|
331
|
+
let frontier = [];
|
|
332
|
+
for (const s of seedIds) for (const nb of (graph.get(s) || [])) if (!visited.has(nb)) { direct.add(nb); visited.add(nb); frontier.push(nb); }
|
|
333
|
+
let depth = 1;
|
|
334
|
+
while (frontier.length && (maxDepth === 0 || depth < maxDepth)) {
|
|
335
|
+
const next = [];
|
|
336
|
+
for (const node of frontier) for (const nb of (graph.get(node) || [])) if (!visited.has(nb)) { transitive.add(nb); visited.add(nb); next.push(nb); }
|
|
337
|
+
frontier = next; depth++;
|
|
338
|
+
}
|
|
339
|
+
return { direct: [...direct], transitive: [...transitive] };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Method-level blast radius: everything that (transitively) calls `symbol`.
|
|
344
|
+
*
|
|
345
|
+
* @param {string} symbol bare name or `file#name`
|
|
346
|
+
* @param {string} cwd
|
|
347
|
+
* @param {object} [opts] { depth=0, ...buildCallGraph opts }
|
|
348
|
+
* @returns {{ symbol:string, resolved:string[], direct:string[], transitive:string[], total:number, unresolved:boolean }}
|
|
349
|
+
*/
|
|
350
|
+
function methodImpact(symbol, cwd, opts = {}) {
|
|
351
|
+
const graph = opts.graph || buildCallGraph(cwd, opts);
|
|
352
|
+
const ids = _resolveSymbol(symbol, graph.defs);
|
|
353
|
+
if (ids.length === 0) return { symbol, resolved: [], direct: [], transitive: [], total: 0, unresolved: true };
|
|
354
|
+
const { direct, transitive } = _bfs(ids, graph.reverse, opts.depth || 0);
|
|
355
|
+
return { symbol, resolved: ids, direct, transitive, total: direct.length + transitive.length, unresolved: false };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* What `symbol` (transitively) calls.
|
|
360
|
+
* @returns {{ symbol:string, resolved:string[], direct:string[], transitive:string[], total:number, unresolved:boolean }}
|
|
361
|
+
*/
|
|
362
|
+
function methodCallees(symbol, cwd, opts = {}) {
|
|
363
|
+
const graph = opts.graph || buildCallGraph(cwd, opts);
|
|
364
|
+
const ids = _resolveSymbol(symbol, graph.defs);
|
|
365
|
+
if (ids.length === 0) return { symbol, resolved: [], direct: [], transitive: [], total: 0, unresolved: true };
|
|
366
|
+
const { direct, transitive } = _bfs(ids, graph.forward, opts.depth || 0);
|
|
367
|
+
return { symbol, resolved: ids, direct, transitive, total: direct.length + transitive.length, unresolved: false };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Formatters ───────────────────────────────────────────────────────────────
|
|
371
|
+
function formatCallGraph(result, kind) {
|
|
372
|
+
const verb = kind === 'callees' ? 'calls' : 'callers of';
|
|
373
|
+
const lines = [`## ${kind === 'callees' ? 'Callees' : 'Callers'}: \`${result.symbol}\``, ''];
|
|
374
|
+
if (result.unresolved) { lines.push('_symbol not found in the call-graph._'); return lines.join('\n'); }
|
|
375
|
+
if (result.total === 0) {
|
|
376
|
+
lines.push(kind === 'callees' ? '_calls no repo-defined symbols._' : '_no repo symbol calls this — zero method blast radius._');
|
|
377
|
+
return lines.join('\n');
|
|
378
|
+
}
|
|
379
|
+
lines.push(`**Total ${verb}:** ${result.total}`, '');
|
|
380
|
+
if (result.direct.length) { lines.push(`### Direct`); for (const id of result.direct) lines.push(`- \`${id}\``); lines.push(''); }
|
|
381
|
+
if (result.transitive.length) { lines.push(`### Transitive`); for (const id of result.transitive) lines.push(`- \`${id}\``); lines.push(''); }
|
|
382
|
+
return lines.join('\n');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function formatCallGraphJSON(result, kind) {
|
|
386
|
+
return {
|
|
387
|
+
symbol: result.symbol,
|
|
388
|
+
kind: kind === 'callees' ? 'callees' : 'callers',
|
|
389
|
+
resolved: result.resolved,
|
|
390
|
+
direct: result.direct,
|
|
391
|
+
transitive: result.transitive,
|
|
392
|
+
total: result.total,
|
|
393
|
+
unresolved: result.unresolved,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
module.exports = {
|
|
398
|
+
buildCallGraph, methodImpact, methodCallees,
|
|
399
|
+
formatCallGraph, formatCallGraphJSON,
|
|
400
|
+
extractDefs, maskJs, maskPy,
|
|
401
|
+
};
|
package/src/mcp/server.js
CHANGED
package/src/tracking/pricing.js
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
* presented as exact. Zero npm dependencies.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
// USD per 1,000,000 input tokens.
|
|
13
|
+
// USD per 1,000,000 input tokens. Claude rates verified 2026-07 against
|
|
14
|
+
// platform.claude.com (Opus 4.8 $5, Sonnet 5/4.6 $3, Haiku 4.5 $1); GPT-4o $2.50.
|
|
14
15
|
const PRICES = {
|
|
15
16
|
'claude-sonnet': 3.0,
|
|
16
|
-
'claude-opus':
|
|
17
|
-
'claude-haiku': 0
|
|
17
|
+
'claude-opus': 5.0,
|
|
18
|
+
'claude-haiku': 1.0,
|
|
18
19
|
'gpt-4o': 2.5,
|
|
19
20
|
'gpt-4o-mini': 0.15,
|
|
20
21
|
'gemini-1.5-pro': 1.25,
|