sigmap 8.13.0 → 8.16.0
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/AGENTS.md +128 -120
- package/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/gen-context.js +285 -33
- package/llms-full.txt +5 -5
- package/llms.txt +2 -2
- package/package.json +2 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/defaults.js +2 -0
- package/src/evidence/pack.js +50 -14
- package/src/graph/call-graph.js +177 -11
- package/src/mcp/handlers.js +10 -1
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +1 -1
- package/src/retrieval/ranker.js +28 -0
package/gen-context.js
CHANGED
|
@@ -1531,6 +1531,8 @@ __factories["./src/config/defaults"] = function(module, exports) {
|
|
|
1531
1531
|
topK: 10,
|
|
1532
1532
|
// Multiplier applied to recently-changed files (>1 boosts them up)
|
|
1533
1533
|
recencyBoost: 1.5,
|
|
1534
|
+
// Boost files call-graph-connected to query matches (opt-in, measure-gated)
|
|
1535
|
+
callGraphBoost: false,
|
|
1534
1536
|
},
|
|
1535
1537
|
|
|
1536
1538
|
// Impact layer settings (v2.5)
|
|
@@ -4731,12 +4733,15 @@ __factories["./src/eval/usefulness-scorer"] = function(module, exports) {
|
|
|
4731
4733
|
__factories["./src/evidence/pack"] = function(module, exports) {
|
|
4732
4734
|
|
|
4733
4735
|
/**
|
|
4734
|
-
* Evidence Pack
|
|
4736
|
+
* Evidence Pack v2 (v8.0 E1; schema v2 adds a published schema URL, per-file
|
|
4737
|
+
* multi-factor risk labels, measured test-discovery provenance, and generator
|
|
4738
|
+
* identity — all additive over v1).
|
|
4735
4739
|
*
|
|
4736
4740
|
* A deterministic, machine-consumable signature-and-evidence map. Replaces the
|
|
4737
4741
|
* "paste this into your prompt" workflow with a byte-stable JSON artifact that
|
|
4738
4742
|
* an agent or CI can ingest directly — every entry anchored to a real file,
|
|
4739
|
-
* symbol, and line range.
|
|
4743
|
+
* symbol, and line range. The published schema lives at
|
|
4744
|
+
* docs-vp/public/schemas/evidence-pack-2.json (sigmap.io/schemas/).
|
|
4740
4745
|
*
|
|
4741
4746
|
* Composed entirely from shipped zero-dep modules:
|
|
4742
4747
|
* - retrieval/ranker → ranked files, scores, signals
|
|
@@ -4756,10 +4761,21 @@ __factories["./src/evidence/pack"] = function(module, exports) {
|
|
|
4756
4761
|
const { buildSigIndex, rank, detectIntent } = __require('./src/retrieval/ranker');
|
|
4757
4762
|
const { scan } = __require('./src/security/scanner');
|
|
4758
4763
|
|
|
4759
|
-
const SCHEMA_VERSION = '
|
|
4764
|
+
const SCHEMA_VERSION = '2.0';
|
|
4765
|
+
const SCHEMA_URL = 'https://sigmap.io/schemas/evidence-pack-2.json';
|
|
4760
4766
|
const DEFAULT_BUDGET = 6000;
|
|
4761
4767
|
const DEFAULT_TOP = 12;
|
|
4762
4768
|
|
|
4769
|
+
// Measured accuracy of the stem-affix test-discovery method (C2). Constants
|
|
4770
|
+
// are sourced from the committed benchmarks/reports/test-discovery.json and
|
|
4771
|
+
// guarded by a test that fails on drift — re-run `npm run
|
|
4772
|
+
// benchmark:test-discovery` and update together, never hand-invent.
|
|
4773
|
+
const TEST_DISCOVERY = {
|
|
4774
|
+
method: 'stem-affix-match',
|
|
4775
|
+
measured: { f1: 0.98, precision: 0.971, recall: 0.988, pairs: 3701, repos: 28 },
|
|
4776
|
+
benchmark: 'npm run benchmark:test-discovery',
|
|
4777
|
+
};
|
|
4778
|
+
|
|
4763
4779
|
const GENERATED_RE = /(^|\/)(dist|build|out|vendor|node_modules)\/|\.(generated|min|bundle)\.|\.(pb|_pb)\.|\.pb\.go$|_pb2\.py$/;
|
|
4764
4780
|
const TEST_RE = /(^|\/)(tests?|__tests__|spec|specs)\/|\.(test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(go|py|rb)$/;
|
|
4765
4781
|
const CONFIG_RE = /\.(json|ya?ml|toml|ini|conf|config|properties|env)$|(^|\/)(\.?[a-z]+rc)$|\.config\.[a-z]+$/i;
|
|
@@ -4798,16 +4814,29 @@ __factories["./src/evidence/pack"] = function(module, exports) {
|
|
|
4798
4814
|
* @returns {'generated'|'test'|'migration'|'payment'|'auth'|'security'|'config'|'public-api'|'source'}
|
|
4799
4815
|
*/
|
|
4800
4816
|
function riskLabelFor(relPath) {
|
|
4817
|
+
return riskFactorsFor(relPath)[0];
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4820
|
+
/**
|
|
4821
|
+
* Every risk category a file matches, in the same strict precedence order
|
|
4822
|
+
* riskLabelFor uses (schema v2). Where v1 collapsed a migration touching
|
|
4823
|
+
* payments to `migration`, the factors list carries both. Always non-empty:
|
|
4824
|
+
* a file matching nothing is `['source']`.
|
|
4825
|
+
* @param {string} relPath
|
|
4826
|
+
* @returns {string[]}
|
|
4827
|
+
*/
|
|
4828
|
+
function riskFactorsFor(relPath) {
|
|
4801
4829
|
const p = relPath.replace(/\\/g, '/');
|
|
4802
|
-
|
|
4803
|
-
if (
|
|
4804
|
-
if (
|
|
4805
|
-
if (
|
|
4806
|
-
if (
|
|
4807
|
-
if (
|
|
4808
|
-
if (
|
|
4809
|
-
if (
|
|
4810
|
-
|
|
4830
|
+
const factors = [];
|
|
4831
|
+
if (GENERATED_RE.test(p)) factors.push('generated');
|
|
4832
|
+
if (TEST_RE.test(p)) factors.push('test');
|
|
4833
|
+
if (MIGRATION_RE.test(p)) factors.push('migration');
|
|
4834
|
+
if (PAYMENT_RE.test(p)) factors.push('payment');
|
|
4835
|
+
if (AUTH_RE.test(p)) factors.push('auth');
|
|
4836
|
+
if (SECURITY_RE.test(p)) factors.push('security');
|
|
4837
|
+
if (CONFIG_RE.test(p)) factors.push('config');
|
|
4838
|
+
if (PUBLIC_API_RE.test(p)) factors.push('public-api');
|
|
4839
|
+
return factors.length ? factors : ['source'];
|
|
4811
4840
|
}
|
|
4812
4841
|
|
|
4813
4842
|
/** Filename stem (basename minus the first extension chain). */
|
|
@@ -4939,6 +4968,7 @@ __factories["./src/evidence/pack"] = function(module, exports) {
|
|
|
4939
4968
|
if (start !== null) sourceLines.push({ symbol, start, end });
|
|
4940
4969
|
}
|
|
4941
4970
|
|
|
4971
|
+
const riskFactors = riskFactorsFor(r.file);
|
|
4942
4972
|
files.push({
|
|
4943
4973
|
path: r.file,
|
|
4944
4974
|
symbols,
|
|
@@ -4946,7 +4976,8 @@ __factories["./src/evidence/pack"] = function(module, exports) {
|
|
|
4946
4976
|
confidence: maxScore > 0 ? Math.round((r.score / maxScore) * 100) / 100 : 0,
|
|
4947
4977
|
sourceLines,
|
|
4948
4978
|
relatedTests: findRelatedTests(r.file, allFiles),
|
|
4949
|
-
riskLabel:
|
|
4979
|
+
riskLabel: riskFactors[0],
|
|
4980
|
+
riskFactors,
|
|
4950
4981
|
});
|
|
4951
4982
|
}
|
|
4952
4983
|
|
|
@@ -4955,11 +4986,14 @@ __factories["./src/evidence/pack"] = function(module, exports) {
|
|
|
4955
4986
|
|
|
4956
4987
|
const pack = {
|
|
4957
4988
|
schemaVersion: SCHEMA_VERSION,
|
|
4989
|
+
schemaUrl: SCHEMA_URL,
|
|
4990
|
+
generator: { name: 'sigmap', version: typeof opts.version === 'string' ? opts.version : null },
|
|
4958
4991
|
query,
|
|
4959
4992
|
intent,
|
|
4960
4993
|
files,
|
|
4961
4994
|
tokenBudget: { limit: budget, used, remaining: Math.max(0, budget - used) },
|
|
4962
4995
|
droppedFiles,
|
|
4996
|
+
testDiscovery: TEST_DISCOVERY,
|
|
4963
4997
|
grounding: {
|
|
4964
4998
|
symbolCount,
|
|
4965
4999
|
anchoredSymbols,
|
|
@@ -5001,7 +5035,8 @@ __factories["./src/evidence/pack"] = function(module, exports) {
|
|
|
5001
5035
|
L.push('');
|
|
5002
5036
|
|
|
5003
5037
|
for (const f of pack.files) {
|
|
5004
|
-
|
|
5038
|
+
const risk = (f.riskFactors && f.riskFactors.length > 1) ? f.riskFactors.join(' + ') : f.riskLabel;
|
|
5039
|
+
L.push(`## \`${f.path}\` _(${risk}, confidence ${f.confidence})_`);
|
|
5005
5040
|
L.push(`_${f.reason}_`);
|
|
5006
5041
|
if (f.relatedTests.length) L.push(`Related tests: ${f.relatedTests.map((t) => `\`${t}\``).join(', ')}`);
|
|
5007
5042
|
L.push('');
|
|
@@ -5026,8 +5061,11 @@ __factories["./src/evidence/pack"] = function(module, exports) {
|
|
|
5026
5061
|
formatMarkdown,
|
|
5027
5062
|
parseAnchor,
|
|
5028
5063
|
riskLabelFor,
|
|
5064
|
+
riskFactorsFor,
|
|
5029
5065
|
findRelatedTests,
|
|
5030
5066
|
SCHEMA_VERSION,
|
|
5067
|
+
SCHEMA_URL,
|
|
5068
|
+
TEST_DISCOVERY,
|
|
5031
5069
|
};
|
|
5032
5070
|
|
|
5033
5071
|
};
|
|
@@ -10942,14 +10980,16 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
10942
10980
|
__factories["./src/graph/call-graph"] = function(module, exports) {
|
|
10943
10981
|
|
|
10944
10982
|
/**
|
|
10945
|
-
* Method/caller-level call-graph (D4 v1).
|
|
10983
|
+
* Method/caller-level call-graph (D4 v1, languages expanded in GR1).
|
|
10946
10984
|
*
|
|
10947
|
-
* Builds symbol-level edges — which function calls which function — for JS/TS
|
|
10948
|
-
* and
|
|
10949
|
-
* Call sites are resolved with high precision: a call
|
|
10950
|
-
* of that name in the *same file* first, then in a
|
|
10951
|
-
* (via the existing file-level import graph). Names
|
|
10952
|
-
* definition produce no edge — over-approximation
|
|
10985
|
+
* Builds symbol-level edges — which function calls which function — for JS/TS,
|
|
10986
|
+
* Python, Java, Go, and Rust. Deterministic, zero-dependency, regex +
|
|
10987
|
+
* brace/indent matching. Call sites are resolved with high precision: a call
|
|
10988
|
+
* resolves to a definition of that name in the *same file* first, then in a
|
|
10989
|
+
* *directly-imported* file (via the existing file-level import graph). Names
|
|
10990
|
+
* that resolve to no repo definition produce no edge — over-approximation
|
|
10991
|
+
* noise is avoided. Constructs that can't be parsed dependency-free are
|
|
10992
|
+
* skipped (less fidelity, never a parser dep).
|
|
10953
10993
|
*
|
|
10954
10994
|
* Symbol IDs are `relPath#symbolName` (forward-slashed, relative to cwd).
|
|
10955
10995
|
*
|
|
@@ -10962,6 +11002,9 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
10962
11002
|
|
|
10963
11003
|
const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
10964
11004
|
const PY_EXTS = new Set(['.py', '.pyw']);
|
|
11005
|
+
const JAVA_EXTS = new Set(['.java']);
|
|
11006
|
+
const GO_EXTS = new Set(['.go']);
|
|
11007
|
+
const RS_EXTS = new Set(['.rs']);
|
|
10965
11008
|
|
|
10966
11009
|
// Tokens that look like `name(` calls or definition headers but are language
|
|
10967
11010
|
// keywords, not user symbols — never treated as a call or a definition.
|
|
@@ -10971,6 +11014,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
10971
11014
|
'in', 'of', 'case', 'throw', 'print', 'and', 'or', 'not', 'assert',
|
|
10972
11015
|
'lambda', 'class', 'def', 'elif', 'except', 'finally', 'raise', 'import',
|
|
10973
11016
|
'from', 'global', 'nonlocal', 'del', 'pass', 'async', 'require', 'constructor',
|
|
11017
|
+
'synchronized',
|
|
10974
11018
|
]);
|
|
10975
11019
|
|
|
10976
11020
|
function normalizePath(p) { return path.normalize(p).toLowerCase(); }
|
|
@@ -10999,6 +11043,32 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
10999
11043
|
return out.join('');
|
|
11000
11044
|
}
|
|
11001
11045
|
|
|
11046
|
+
// Rust: `//`, `/* */`, and `"..."` mask like JS, but a bare `'` is usually a
|
|
11047
|
+
// lifetime (`'a`), not a string — masking to the "closing" quote would corrupt
|
|
11048
|
+
// offsets. Only char literals (`'x'`, `'\n'`) are masked; lifetimes pass through.
|
|
11049
|
+
function maskRust(src) {
|
|
11050
|
+
const out = src.split('');
|
|
11051
|
+
const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
11052
|
+
let i = 0; const n = src.length;
|
|
11053
|
+
while (i < n) {
|
|
11054
|
+
const c = src[i], d = src[i + 1];
|
|
11055
|
+
if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
|
|
11056
|
+
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; }
|
|
11057
|
+
if (c === '"') {
|
|
11058
|
+
let j = i + 1;
|
|
11059
|
+
while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === '"') break; j++; }
|
|
11060
|
+
j = Math.min(n, j + 1); blank(i, j); i = j; continue;
|
|
11061
|
+
}
|
|
11062
|
+
if (c === "'") {
|
|
11063
|
+
if (d === '\\' && src[i + 3] === "'") { blank(i, i + 4); i += 4; continue; } // '\n'
|
|
11064
|
+
if (d !== undefined && src[i + 2] === "'") { blank(i, i + 3); i += 3; continue; } // 'x'
|
|
11065
|
+
i++; continue; // lifetime `'a` — leave untouched
|
|
11066
|
+
}
|
|
11067
|
+
i++;
|
|
11068
|
+
}
|
|
11069
|
+
return out.join('');
|
|
11070
|
+
}
|
|
11071
|
+
|
|
11002
11072
|
function maskPy(src) {
|
|
11003
11073
|
const out = src.split('');
|
|
11004
11074
|
const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
@@ -11128,10 +11198,98 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
11128
11198
|
return defs;
|
|
11129
11199
|
}
|
|
11130
11200
|
|
|
11201
|
+
// Go: func name(...) { } | func (r Recv) name(...) (T, error) { }
|
|
11202
|
+
// The return list may itself be parenthesized, so scan past it to the body `{`.
|
|
11203
|
+
function goDefs(masked) {
|
|
11204
|
+
const defs = [];
|
|
11205
|
+
const re = /(?:^|\n)func\s+(?:\([^)\n]*\)\s*)?([A-Za-z_]\w*)\s*\(/g;
|
|
11206
|
+
let m;
|
|
11207
|
+
while ((m = re.exec(masked)) !== null) {
|
|
11208
|
+
const paren = masked.indexOf('(', m.index + m[0].length - 1);
|
|
11209
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
11210
|
+
let k = close + 1;
|
|
11211
|
+
let depth = 0;
|
|
11212
|
+
while (k < masked.length) {
|
|
11213
|
+
const ch = masked[k];
|
|
11214
|
+
if (ch === '(') depth++;
|
|
11215
|
+
else if (ch === ')') depth--;
|
|
11216
|
+
else if (ch === '{' && depth === 0) break;
|
|
11217
|
+
else if (ch === '\n' && depth === 0) { k = -1; break; } // no body on this header
|
|
11218
|
+
k++;
|
|
11219
|
+
}
|
|
11220
|
+
if (k === -1 || k >= masked.length) continue;
|
|
11221
|
+
defs.push({ name: m[1], line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
|
|
11222
|
+
}
|
|
11223
|
+
return defs;
|
|
11224
|
+
}
|
|
11225
|
+
|
|
11226
|
+
// Java: methods + constructors with braced bodies. Statement-shaped matches
|
|
11227
|
+
// (calls, control flow) are rejected because their `)` is followed by `;`,
|
|
11228
|
+
// and keyword headers (`if`, `while`, …) fall to the NON_CALL guard.
|
|
11229
|
+
function javaDefs(masked) {
|
|
11230
|
+
const defs = [];
|
|
11231
|
+
const seen = new Set();
|
|
11232
|
+
const re = /(?:^|\n)[ \t]*((?:(?:public|private|protected|static|final|abstract|synchronized|native|default|strictfp)\s+)*)(?:<[^>\n]{0,80}>\s*)?(?:[\w$][\w$.<>\[\],?\s]*?\s+)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
11233
|
+
let m;
|
|
11234
|
+
while ((m = re.exec(masked)) !== null) {
|
|
11235
|
+
const name = m[2];
|
|
11236
|
+
if (NON_CALL.has(name)) continue;
|
|
11237
|
+
// `new Foo() { … }` anonymous classes are uses, not definitions.
|
|
11238
|
+
const before = masked.slice(Math.max(0, m.index), m.index + m[0].length - name.length - 1);
|
|
11239
|
+
if (/\bnew\s*$/.test(before)) continue;
|
|
11240
|
+
const paren = masked.indexOf('(', m.index + m[0].length - 1);
|
|
11241
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
11242
|
+
// skip `throws A, B` up to the body `{` (same line — multi-line headers are skipped)
|
|
11243
|
+
let k = close + 1;
|
|
11244
|
+
while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n' && masked[k] !== '=') k++;
|
|
11245
|
+
if (masked[k] !== '{') continue;
|
|
11246
|
+
const key = name + ':' + k;
|
|
11247
|
+
if (seen.has(key)) continue;
|
|
11248
|
+
seen.add(key);
|
|
11249
|
+
defs.push({ name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
|
|
11250
|
+
}
|
|
11251
|
+
return defs;
|
|
11252
|
+
}
|
|
11253
|
+
|
|
11254
|
+
// Rust: fn name(...) { } | fn name<T>(...) -> T where … { } — inside or
|
|
11255
|
+
// outside impl/trait blocks. A `;` before the body brace (trait declaration)
|
|
11256
|
+
// means no body: skipped.
|
|
11257
|
+
function rustDefs(masked) {
|
|
11258
|
+
const defs = [];
|
|
11259
|
+
const re = /\bfn\s+([A-Za-z_]\w*)/g;
|
|
11260
|
+
let m;
|
|
11261
|
+
while ((m = re.exec(masked)) !== null) {
|
|
11262
|
+
let k = m.index + m[0].length;
|
|
11263
|
+
while (k < masked.length && /\s/.test(masked[k])) k++;
|
|
11264
|
+
if (masked[k] === '<') k = matchDelim(masked, k, '<', '>') + 1;
|
|
11265
|
+
while (k < masked.length && /\s/.test(masked[k])) k++;
|
|
11266
|
+
if (masked[k] !== '(') continue;
|
|
11267
|
+
const close = matchDelim(masked, k, '(', ')');
|
|
11268
|
+
// return type / where clause may span lines; stop at body `{` or decl `;`
|
|
11269
|
+
let b = close + 1;
|
|
11270
|
+
while (b < masked.length && masked[b] !== '{' && masked[b] !== ';') b++;
|
|
11271
|
+
if (masked[b] !== '{') continue;
|
|
11272
|
+
defs.push({ name: m[1], line: lineAt(masked, m.index), bodyStart: b, bodyEnd: matchDelim(masked, b, '{', '}') });
|
|
11273
|
+
}
|
|
11274
|
+
return defs;
|
|
11275
|
+
}
|
|
11276
|
+
|
|
11277
|
+
// Pick the masker whose comment/string syntax matches the language.
|
|
11278
|
+
// Java and Go share JS syntax (Go raw strings mask like template literals).
|
|
11279
|
+
function maskFor(filePath, src) {
|
|
11280
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
11281
|
+
if (PY_EXTS.has(ext)) return maskPy(src);
|
|
11282
|
+
if (RS_EXTS.has(ext)) return maskRust(src);
|
|
11283
|
+
return maskJs(src);
|
|
11284
|
+
}
|
|
11285
|
+
|
|
11131
11286
|
function extractDefs(filePath, src) {
|
|
11132
11287
|
const ext = path.extname(filePath).toLowerCase();
|
|
11133
11288
|
if (JS_EXTS.has(ext)) return jsDefs(maskJs(src));
|
|
11134
11289
|
if (PY_EXTS.has(ext)) return pyDefs(maskPy(src));
|
|
11290
|
+
if (JAVA_EXTS.has(ext)) return javaDefs(maskJs(src));
|
|
11291
|
+
if (GO_EXTS.has(ext)) return goDefs(maskJs(src));
|
|
11292
|
+
if (RS_EXTS.has(ext)) return rustDefs(maskRust(src));
|
|
11135
11293
|
return null; // unsupported language
|
|
11136
11294
|
}
|
|
11137
11295
|
|
|
@@ -11162,7 +11320,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
11162
11320
|
if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
|
|
11163
11321
|
else if (e.isFile()) {
|
|
11164
11322
|
const ext = path.extname(e.name).toLowerCase();
|
|
11165
|
-
if (JS_EXTS.has(ext) || PY_EXTS.has(ext)) out.push(full);
|
|
11323
|
+
if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext) || RS_EXTS.has(ext)) out.push(full);
|
|
11166
11324
|
}
|
|
11167
11325
|
}
|
|
11168
11326
|
}
|
|
@@ -11229,10 +11387,20 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
11229
11387
|
};
|
|
11230
11388
|
|
|
11231
11389
|
for (const [f, fileDefs] of perFileDefs.entries()) {
|
|
11232
|
-
const masked =
|
|
11390
|
+
const masked = maskFor(f, fs.readFileSync(f, 'utf8'));
|
|
11233
11391
|
// resolution scope: this file's defs, then directly-imported files' defs
|
|
11234
11392
|
const importedAbs = (fileGraph.forward.get(normalizePath(path.resolve(f))) || [])
|
|
11235
11393
|
.map((nf) => normToAbs.get(nf)).filter(Boolean);
|
|
11394
|
+
// Go/Java: same-package symbols are visible with no import statement, and
|
|
11395
|
+
// a package is (in practice) a directory — extend the scope to same-dir
|
|
11396
|
+
// same-language siblings. Sorted for deterministic resolution order.
|
|
11397
|
+
const ext = path.extname(f).toLowerCase();
|
|
11398
|
+
if (GO_EXTS.has(ext) || JAVA_EXTS.has(ext)) {
|
|
11399
|
+
const siblings = [...perFileDefs.keys()]
|
|
11400
|
+
.filter((o) => o !== f && path.dirname(o) === path.dirname(f) && path.extname(o).toLowerCase() === ext)
|
|
11401
|
+
.sort();
|
|
11402
|
+
importedAbs.push(...siblings);
|
|
11403
|
+
}
|
|
11236
11404
|
for (const d of fileDefs) {
|
|
11237
11405
|
const callerId = symId(cwd, f, d.name);
|
|
11238
11406
|
if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
|
|
@@ -11256,6 +11424,42 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
11256
11424
|
return { forward: toArr(forward), reverse: toArr(reverse), defs };
|
|
11257
11425
|
}
|
|
11258
11426
|
|
|
11427
|
+
/**
|
|
11428
|
+
* Collapse the symbol-level call-graph to FILE-level bidirectional edges for
|
|
11429
|
+
* the ranker's neighbor boost (opt-in via `retrieval.callGraphBoost`). A file
|
|
11430
|
+
* whose functions call into — or are called by — another file gets an edge in
|
|
11431
|
+
* both directions. Keys are `path.resolve`-form absolute paths (matching the
|
|
11432
|
+
* ranker's lookups); entries and neighbor lists are sorted for determinism.
|
|
11433
|
+
*
|
|
11434
|
+
* @param {string} cwd
|
|
11435
|
+
* @param {object} [opts] { graph } to inject a prebuilt call graph (tests)
|
|
11436
|
+
* @returns {{ forward: Map<string,string[]> }}
|
|
11437
|
+
*/
|
|
11438
|
+
function buildCallFileGraph(cwd, opts = {}) {
|
|
11439
|
+
const graph = opts.graph || buildCallGraph(cwd, opts);
|
|
11440
|
+
const edges = new Map(); // absFile → Set<absFile>
|
|
11441
|
+
const add = (a, b) => {
|
|
11442
|
+
if (a === b) return;
|
|
11443
|
+
if (!edges.has(a)) edges.set(a, new Set());
|
|
11444
|
+
edges.get(a).add(b);
|
|
11445
|
+
};
|
|
11446
|
+
for (const [callerId, calleeIds] of graph.forward.entries()) {
|
|
11447
|
+
const callerDef = graph.defs.get(callerId);
|
|
11448
|
+
if (!callerDef) continue;
|
|
11449
|
+
for (const calleeId of calleeIds) {
|
|
11450
|
+
const calleeDef = graph.defs.get(calleeId);
|
|
11451
|
+
if (!calleeDef || calleeDef.file === callerDef.file) continue;
|
|
11452
|
+
const a = path.resolve(cwd, callerDef.file);
|
|
11453
|
+
const b = path.resolve(cwd, calleeDef.file);
|
|
11454
|
+
add(a, b);
|
|
11455
|
+
add(b, a);
|
|
11456
|
+
}
|
|
11457
|
+
}
|
|
11458
|
+
const forward = new Map();
|
|
11459
|
+
for (const k of [...edges.keys()].sort()) forward.set(k, [...edges.get(k)].sort());
|
|
11460
|
+
return { forward };
|
|
11461
|
+
}
|
|
11462
|
+
|
|
11259
11463
|
// Resolve a user-supplied symbol (bare name or full `file#name` id) to ids.
|
|
11260
11464
|
function _resolveSymbol(symbol, defs) {
|
|
11261
11465
|
if (defs.has(symbol)) return [symbol];
|
|
@@ -11336,9 +11540,9 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
11336
11540
|
}
|
|
11337
11541
|
|
|
11338
11542
|
module.exports = {
|
|
11339
|
-
buildCallGraph, methodImpact, methodCallees,
|
|
11543
|
+
buildCallGraph, buildCallFileGraph, methodImpact, methodCallees,
|
|
11340
11544
|
formatCallGraph, formatCallGraphJSON,
|
|
11341
|
-
extractDefs, maskJs, maskPy,
|
|
11545
|
+
extractDefs, maskJs, maskPy, maskRust,
|
|
11342
11546
|
};
|
|
11343
11547
|
|
|
11344
11548
|
};
|
|
@@ -13488,7 +13692,16 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
13488
13692
|
// Build dependency graph for neighbor boost — non-fatal if it fails
|
|
13489
13693
|
let graph = null;
|
|
13490
13694
|
try { graph = buildFromCwd(cwd); } catch (_) {}
|
|
13491
|
-
|
|
13695
|
+
// Opt-in call-graph neighbor boost (retrieval.callGraphBoost) — non-fatal
|
|
13696
|
+
let callGraph = null;
|
|
13697
|
+
try {
|
|
13698
|
+
const { loadConfig } = __require('./src/config/loader');
|
|
13699
|
+
const retrieval = loadConfig(cwd).retrieval;
|
|
13700
|
+
if (retrieval && retrieval.callGraphBoost) {
|
|
13701
|
+
callGraph = __require('./src/graph/call-graph').buildCallFileGraph(cwd);
|
|
13702
|
+
}
|
|
13703
|
+
} catch (_) {}
|
|
13704
|
+
const results = rank(args.query, index, { topK, cwd, graph, callGraph });
|
|
13492
13705
|
return formatRankTable(results, args.query);
|
|
13493
13706
|
} catch (err) {
|
|
13494
13707
|
return `_query_context failed: ${err.message}_`;
|
|
@@ -14201,7 +14414,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14201
14414
|
|
|
14202
14415
|
const SERVER_INFO = {
|
|
14203
14416
|
name: 'sigmap',
|
|
14204
|
-
version: '8.
|
|
14417
|
+
version: '8.16.0',
|
|
14205
14418
|
description: 'SigMap MCP server — code signatures on demand',
|
|
14206
14419
|
};
|
|
14207
14420
|
|
|
@@ -14482,7 +14695,7 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
14482
14695
|
'Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — ' +
|
|
14483
14696
|
'or, with direction "callees", every repo function it calls. Finer-grained than the ' +
|
|
14484
14697
|
'file-level get_impact: tells an agent which functions break, not just which files. ' +
|
|
14485
|
-
'JS/TS
|
|
14698
|
+
'JS/TS, Python, Java, Go, and Rust call-graph; deterministic, no LLM.',
|
|
14486
14699
|
inputSchema: {
|
|
14487
14700
|
type: 'object',
|
|
14488
14701
|
properties: {
|
|
@@ -15266,6 +15479,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
15266
15479
|
const GRAPH_BOOST_AMOUNTS = {
|
|
15267
15480
|
hop1: 0.40, // direct import neighbor of a file with score > 0
|
|
15268
15481
|
hop2: 0.15, // 2 hops away (transitive), with decay
|
|
15482
|
+
callHop: 0.30, // call-graph file neighbor (opt-in retrieval.callGraphBoost)
|
|
15269
15483
|
};
|
|
15270
15484
|
|
|
15271
15485
|
// Intent-specific weight adjustments
|
|
@@ -15398,6 +15612,8 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
15398
15612
|
* @param {object} [opts.weights] - override scoring weights
|
|
15399
15613
|
* @param {string} [opts.cwd] - project root for learned ranking weights
|
|
15400
15614
|
* @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
|
|
15615
|
+
* @param {{ forward: Map<string,string[]> }} [opts.callGraph] - file-level call-graph edges
|
|
15616
|
+
* (from buildCallFileGraph) for the opt-in call-neighbor boost
|
|
15401
15617
|
* @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
|
|
15402
15618
|
*/
|
|
15403
15619
|
function rank(query, sigIndex, opts) {
|
|
@@ -15521,6 +15737,31 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
15521
15737
|
}
|
|
15522
15738
|
}
|
|
15523
15739
|
|
|
15740
|
+
// Call-graph neighbor boost (opt-in via retrieval.callGraphBoost): a file
|
|
15741
|
+
// whose functions call into — or are called by — a positively-scored file is
|
|
15742
|
+
// relevant even when no import edge exists (Go/Java same-package, dynamic
|
|
15743
|
+
// dispatch). Single hop; seeds snapshotted first so boosts never cascade.
|
|
15744
|
+
const callGraph = (opts && opts.callGraph && opts.callGraph.forward instanceof Map) ? opts.callGraph : null;
|
|
15745
|
+
if (callGraph && cwd) {
|
|
15746
|
+
const path = require('path');
|
|
15747
|
+
const relToIdx = new Map();
|
|
15748
|
+
for (let i = 0; i < scored.length; i++) relToIdx.set(scored[i].file, i);
|
|
15749
|
+
const hubs = _computeHubs(callGraph);
|
|
15750
|
+
const seeds = scored.filter((e) => e.score > 0).map((e) => e.file);
|
|
15751
|
+
for (const file of seeds) {
|
|
15752
|
+
const abs = path.resolve(cwd, file);
|
|
15753
|
+
for (const neighborAbs of (callGraph.forward.get(abs) || [])) {
|
|
15754
|
+
if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
|
|
15755
|
+
const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
|
|
15756
|
+
const idx = relToIdx.get(neighborRel);
|
|
15757
|
+
if (idx !== undefined && scored[idx].file !== file) {
|
|
15758
|
+
scored[idx].score += GRAPH_BOOST_AMOUNTS.callHop;
|
|
15759
|
+
scored[idx].signals.callGraphBoost = (scored[idx].signals.callGraphBoost || 0) + GRAPH_BOOST_AMOUNTS.callHop;
|
|
15760
|
+
}
|
|
15761
|
+
}
|
|
15762
|
+
}
|
|
15763
|
+
}
|
|
15764
|
+
|
|
15524
15765
|
// Compute confidence levels based on score distribution
|
|
15525
15766
|
if (scored.length > 0) {
|
|
15526
15767
|
const scores = scored.map(s => s.score);
|
|
@@ -19214,7 +19455,7 @@ function __tryGit(args, opts = {}) {
|
|
|
19214
19455
|
catch (_) { return ''; }
|
|
19215
19456
|
}
|
|
19216
19457
|
|
|
19217
|
-
const VERSION = '8.
|
|
19458
|
+
const VERSION = '8.16.0';
|
|
19218
19459
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
19219
19460
|
|
|
19220
19461
|
function requireSourceOrBundled(key) {
|
|
@@ -21075,7 +21316,7 @@ Usage:
|
|
|
21075
21316
|
${cmd} --impact <file> Show every file impacted by changing <file>
|
|
21076
21317
|
${cmd} --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
|
|
21077
21318
|
${cmd} --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
|
|
21078
|
-
${cmd} --callers <symbol> Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS
|
|
21319
|
+
${cmd} --callers <symbol> Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS, Python, Java, Go, Rust)
|
|
21079
21320
|
${cmd} --callees <symbol> Every repo function that <symbol> (transitively) calls
|
|
21080
21321
|
${cmd} --callers <symbol> --json --depth <n> Call-graph edges as JSON (depth 0 = unlimited)
|
|
21081
21322
|
${cmd} verify <answer.md> Flagship grounding guard — flag fake files/tests/imports/symbols/npm-scripts in an AI answer (alias of verify-ai-output)
|
|
@@ -21537,7 +21778,13 @@ function main() {
|
|
|
21537
21778
|
} catch (_) { /* squeeze is best-effort — never break ask */ }
|
|
21538
21779
|
}
|
|
21539
21780
|
|
|
21540
|
-
|
|
21781
|
+
// Opt-in call-graph neighbor boost (retrieval.callGraphBoost) — non-fatal
|
|
21782
|
+
let askCallGraph = null;
|
|
21783
|
+
if (config && config.retrieval && config.retrieval.callGraphBoost) {
|
|
21784
|
+
try { askCallGraph = requireSourceOrBundled('./src/graph/call-graph').buildCallFileGraph(cwd); } catch (_) {}
|
|
21785
|
+
}
|
|
21786
|
+
|
|
21787
|
+
let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd, callGraph: askCallGraph });
|
|
21541
21788
|
|
|
21542
21789
|
// v6.10: Workspace scoping — infer package from query and apply boost
|
|
21543
21790
|
const workspaces = detectWorkspaces(cwd);
|
|
@@ -21694,7 +21941,7 @@ function main() {
|
|
|
21694
21941
|
|
|
21695
21942
|
const { buildEvidencePack, formatJSON, formatMarkdown } = requireSourceOrBundled('./src/evidence/pack');
|
|
21696
21943
|
|
|
21697
|
-
const opts = {};
|
|
21944
|
+
const opts = { version: VERSION };
|
|
21698
21945
|
const topIdx = args.indexOf('--top');
|
|
21699
21946
|
if (topIdx !== -1 && args[topIdx + 1]) opts.top = parseInt(args[topIdx + 1], 10);
|
|
21700
21947
|
const budgetIdx = args.indexOf('--budget');
|
|
@@ -23709,7 +23956,12 @@ function main() {
|
|
|
23709
23956
|
const topK = topIdx >= 0 ? Math.min(Math.max(1, parseInt(args[topIdx + 1], 10) || 10), 25)
|
|
23710
23957
|
: ((config && config.retrieval && config.retrieval.topK) || 10);
|
|
23711
23958
|
const recencyBoost = (config && config.retrieval && config.retrieval.recencyBoost) || 1.5;
|
|
23712
|
-
|
|
23959
|
+
// Opt-in call-graph neighbor boost (retrieval.callGraphBoost) — non-fatal
|
|
23960
|
+
let queryCallGraph = null;
|
|
23961
|
+
if (config && config.retrieval && config.retrieval.callGraphBoost) {
|
|
23962
|
+
try { queryCallGraph = requireSourceOrBundled('./src/graph/call-graph').buildCallFileGraph(cwd); } catch (_) {}
|
|
23963
|
+
}
|
|
23964
|
+
const results = rank(query, index, { topK, recencyBoost, cwd, callGraph: queryCallGraph });
|
|
23713
23965
|
if (args.includes('--context')) {
|
|
23714
23966
|
const miniCtx = buildMiniContext(results, cwd);
|
|
23715
23967
|
const ctxOut = path.join(cwd, '.context', 'query-context.md');
|
package/llms-full.txt
CHANGED
|
@@ -11,13 +11,13 @@ 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.16.0 | Benchmark: sigmap-v8.16-main (2026-07-11)
|
|
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.16-main, 2026-07-11)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
@@ -101,7 +101,7 @@ sigmap weights --json Learned weights as JSON
|
|
|
101
101
|
sigmap --impact <file> Show every file impacted by changing <file>
|
|
102
102
|
sigmap --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
|
|
103
103
|
sigmap --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
|
|
104
|
-
sigmap --callers <symbol> Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS
|
|
104
|
+
sigmap --callers <symbol> Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS, Python, Java, Go, Rust)
|
|
105
105
|
sigmap --callees <symbol> Every repo function that <symbol> (transitively) calls
|
|
106
106
|
sigmap --callers <symbol> --json --depth <n> Call-graph edges as JSON (depth 0 = unlimited)
|
|
107
107
|
sigmap verify <answer.md> Flagship grounding guard — flag fake files/tests/imports/symbols/npm-scripts in an AI answer (alias of verify-ai-output)
|
|
@@ -210,7 +210,7 @@ Input: { query: string, topK?: number }
|
|
|
210
210
|
|
|
211
211
|
### get_method_impact
|
|
212
212
|
|
|
213
|
-
Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — or, with direction "callees", every repo function it calls. Finer-grained than the file-level get_impact: tells an agent which functions break, not just which files. JS/TS
|
|
213
|
+
Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — or, with direction "callees", every repo function it calls. Finer-grained than the file-level get_impact: tells an agent which functions break, not just which files. JS/TS, Python, Java, Go, and Rust call-graph; deterministic, no LLM.
|
|
214
214
|
|
|
215
215
|
```
|
|
216
216
|
Input: { symbol: string, direction?: string, depth?: number }
|
|
@@ -343,7 +343,7 @@ testCoverage = false
|
|
|
343
343
|
testDirs = ["tests","test","__tests__","spec"]
|
|
344
344
|
sigCache = false
|
|
345
345
|
impactRadius = false
|
|
346
|
-
retrieval = {"topK":10,"recencyBoost":1.5}
|
|
346
|
+
retrieval = {"topK":10,"recencyBoost":1.5,"callGraphBoost":false}
|
|
347
347
|
impact = {"depth":3,"includeSigs":true}
|
|
348
348
|
```
|
|
349
349
|
|
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.16.0 | Benchmark: sigmap-v8.16-main (2026-07-11)
|
|
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,7 +23,7 @@ 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.16-main, 2026-07-11)
|
|
27
27
|
|
|
28
28
|
- hit@5 retrieval: 87.8% vs 13.6% random baseline (6.5× lift)
|
|
29
29
|
- Token reduction: 97.0% average across benchmark repos
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.16.0",
|
|
4
4
|
"description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"benchmark:squeeze": "node scripts/run-squeeze-benchmark.mjs --save",
|
|
31
31
|
"benchmark:test-discovery": "node scripts/run-test-discovery-benchmark.mjs --save",
|
|
32
32
|
"benchmark:terse": "node scripts/run-terse-benchmark.mjs --save",
|
|
33
|
+
"benchmark:callgraph-boost": "node scripts/run-callgraph-boost-benchmark.mjs --save",
|
|
33
34
|
"validate:squeeze": "node scripts/run-squeeze-benchmark.mjs --gate",
|
|
34
35
|
"health": "node gen-context.js --health",
|
|
35
36
|
"map": "node gen-project-map.js",
|
package/src/config/defaults.js
CHANGED
|
@@ -147,6 +147,8 @@ const DEFAULTS = {
|
|
|
147
147
|
topK: 10,
|
|
148
148
|
// Multiplier applied to recently-changed files (>1 boosts them up)
|
|
149
149
|
recencyBoost: 1.5,
|
|
150
|
+
// Boost files call-graph-connected to query matches (opt-in, measure-gated)
|
|
151
|
+
callGraphBoost: false,
|
|
150
152
|
},
|
|
151
153
|
|
|
152
154
|
// Impact layer settings (v2.5)
|