sigmap 8.11.0 → 8.13.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 +99 -80
- package/CHANGELOG.md +18 -0
- package/README.md +3 -3
- package/gen-context.js +516 -10
- package/llms-full.txt +13 -4
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/graph/blast-radius.js +132 -0
- package/src/mcp/handlers.js +23 -1
- package/src/mcp/server.js +3 -2
- package/src/mcp/tools.js +34 -5
- package/src/review/pr-evidence.js +17 -0
- package/src/review/review-pr.js +23 -1
- package/src/wiki/generate.js +250 -0
package/gen-context.js
CHANGED
|
@@ -10304,6 +10304,142 @@ __factories["./src/format/verify-report"] = function(module, exports) {
|
|
|
10304
10304
|
|
|
10305
10305
|
};
|
|
10306
10306
|
|
|
10307
|
+
// ── ./src/graph/blast-radius ──
|
|
10308
|
+
__factories["./src/graph/blast-radius"] = function(module, exports) {
|
|
10309
|
+
|
|
10310
|
+
/**
|
|
10311
|
+
* Method-level blast-radius scoring (GR2).
|
|
10312
|
+
*
|
|
10313
|
+
* Consumes the D4 call-graph (src/graph/call-graph.js): for each changed file,
|
|
10314
|
+
* resolve the functions it defines, BFS the reverse call edges, and score how
|
|
10315
|
+
* much of the codebase transitively calls into the change. The score is a
|
|
10316
|
+
* documented deterministic formula — no heuristics that vary run to run — so
|
|
10317
|
+
* review-pr findings and PR Evidence lines are byte-stable for a fixed tree.
|
|
10318
|
+
*
|
|
10319
|
+
* Score: min(100, direct×4 + transitive×1). Tiers:
|
|
10320
|
+
* 0 → none · 1–9 → low · 10–29 → medium · 30–59 → high · 60+ → critical
|
|
10321
|
+
*/
|
|
10322
|
+
|
|
10323
|
+
const path = require('path');
|
|
10324
|
+
const { buildCallGraph } = __require('./src/graph/call-graph');
|
|
10325
|
+
|
|
10326
|
+
const DIRECT_WEIGHT = 4;
|
|
10327
|
+
const TRANSITIVE_WEIGHT = 1;
|
|
10328
|
+
const IMPACTED_FUNCTIONS_CAP = 12;
|
|
10329
|
+
|
|
10330
|
+
const TEST_FILE_RE = /\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.(py|go)$|(^|\/)(tests?|__tests__|spec)\//;
|
|
10331
|
+
|
|
10332
|
+
function tierFor(score) {
|
|
10333
|
+
if (score >= 60) return 'critical';
|
|
10334
|
+
if (score >= 30) return 'high';
|
|
10335
|
+
if (score >= 10) return 'medium';
|
|
10336
|
+
if (score >= 1) return 'low';
|
|
10337
|
+
return 'none';
|
|
10338
|
+
}
|
|
10339
|
+
|
|
10340
|
+
function _normRel(p) {
|
|
10341
|
+
return String(p).replace(/\\/g, '/');
|
|
10342
|
+
}
|
|
10343
|
+
|
|
10344
|
+
// BFS the reverse edges from a set of symbol ids; returns direct/transitive id sets.
|
|
10345
|
+
function _bfs(seedIds, reverse, maxDepth) {
|
|
10346
|
+
const direct = new Set();
|
|
10347
|
+
const transitive = new Set();
|
|
10348
|
+
const visited = new Set(seedIds);
|
|
10349
|
+
let frontier = [];
|
|
10350
|
+
for (const s of seedIds) {
|
|
10351
|
+
for (const nb of (reverse.get(s) || [])) {
|
|
10352
|
+
if (!visited.has(nb)) { direct.add(nb); visited.add(nb); frontier.push(nb); }
|
|
10353
|
+
}
|
|
10354
|
+
}
|
|
10355
|
+
let depth = 1;
|
|
10356
|
+
while (frontier.length && (maxDepth === 0 || depth < maxDepth)) {
|
|
10357
|
+
const next = [];
|
|
10358
|
+
for (const node of frontier) {
|
|
10359
|
+
for (const nb of (reverse.get(node) || [])) {
|
|
10360
|
+
if (!visited.has(nb)) { transitive.add(nb); visited.add(nb); next.push(nb); }
|
|
10361
|
+
}
|
|
10362
|
+
}
|
|
10363
|
+
frontier = next;
|
|
10364
|
+
depth++;
|
|
10365
|
+
}
|
|
10366
|
+
return { direct, transitive };
|
|
10367
|
+
}
|
|
10368
|
+
|
|
10369
|
+
/**
|
|
10370
|
+
* Score the method-level blast radius of a changed-file list.
|
|
10371
|
+
*
|
|
10372
|
+
* @param {string[]} changedFiles repo-relative paths
|
|
10373
|
+
* @param {string} cwd
|
|
10374
|
+
* @param {object} [opts]
|
|
10375
|
+
* @param {object} [opts.graph] injected call graph (tests); else built from cwd
|
|
10376
|
+
* @param {number} [opts.depth=0] BFS depth limit (0 = unlimited)
|
|
10377
|
+
* @returns {{
|
|
10378
|
+
* available: boolean,
|
|
10379
|
+
* files: Array<{ file:string, symbols:number, directCallers:number,
|
|
10380
|
+
* transitiveCallers:number, testCallers:number,
|
|
10381
|
+
* impactedFunctions:string[], score:number, tier:string }>,
|
|
10382
|
+
* aggregate: { score:number, tier:string, impactedFunctions:number }
|
|
10383
|
+
* }}
|
|
10384
|
+
*/
|
|
10385
|
+
function methodBlastRadius(changedFiles, cwd, opts = {}) {
|
|
10386
|
+
const empty = { available: false, files: [], aggregate: { score: 0, tier: 'none', impactedFunctions: 0 } };
|
|
10387
|
+
let graph;
|
|
10388
|
+
try {
|
|
10389
|
+
graph = opts.graph || buildCallGraph(cwd, opts);
|
|
10390
|
+
} catch (_) {
|
|
10391
|
+
return empty;
|
|
10392
|
+
}
|
|
10393
|
+
if (!graph || !graph.defs || graph.defs.size === 0) return empty;
|
|
10394
|
+
|
|
10395
|
+
// Group defined symbol ids by their (normalized) defining file.
|
|
10396
|
+
const idsByFile = new Map();
|
|
10397
|
+
for (const [id, def] of graph.defs.entries()) {
|
|
10398
|
+
const rel = _normRel(def.file);
|
|
10399
|
+
if (!idsByFile.has(rel)) idsByFile.set(rel, []);
|
|
10400
|
+
idsByFile.get(rel).push(id);
|
|
10401
|
+
}
|
|
10402
|
+
|
|
10403
|
+
const depth = Number.isFinite(opts.depth) ? opts.depth : 0;
|
|
10404
|
+
const files = [];
|
|
10405
|
+
const allImpacted = new Set();
|
|
10406
|
+
|
|
10407
|
+
for (const changed of (changedFiles || []).map(_normRel).sort()) {
|
|
10408
|
+
const ids = idsByFile.get(changed);
|
|
10409
|
+
if (!ids || !ids.length) continue;
|
|
10410
|
+
const { direct, transitive } = _bfs(ids, graph.reverse, depth);
|
|
10411
|
+
const impacted = [...direct, ...transitive].sort();
|
|
10412
|
+
for (const id of impacted) allImpacted.add(id);
|
|
10413
|
+
const testCallers = impacted.filter((id) => {
|
|
10414
|
+
const def = graph.defs.get(id);
|
|
10415
|
+
return def && TEST_FILE_RE.test(_normRel(def.file));
|
|
10416
|
+
}).length;
|
|
10417
|
+
const score = Math.min(100, direct.size * DIRECT_WEIGHT + transitive.size * TRANSITIVE_WEIGHT);
|
|
10418
|
+
files.push({
|
|
10419
|
+
file: changed,
|
|
10420
|
+
symbols: ids.length,
|
|
10421
|
+
directCallers: direct.size,
|
|
10422
|
+
transitiveCallers: transitive.size,
|
|
10423
|
+
testCallers,
|
|
10424
|
+
impactedFunctions: impacted.slice(0, IMPACTED_FUNCTIONS_CAP),
|
|
10425
|
+
score,
|
|
10426
|
+
tier: tierFor(score),
|
|
10427
|
+
});
|
|
10428
|
+
}
|
|
10429
|
+
|
|
10430
|
+
if (!files.length) return empty;
|
|
10431
|
+
const maxScore = files.reduce((m, f) => Math.max(m, f.score), 0);
|
|
10432
|
+
return {
|
|
10433
|
+
available: true,
|
|
10434
|
+
files,
|
|
10435
|
+
aggregate: { score: maxScore, tier: tierFor(maxScore), impactedFunctions: allImpacted.size },
|
|
10436
|
+
};
|
|
10437
|
+
}
|
|
10438
|
+
|
|
10439
|
+
module.exports = { methodBlastRadius, tierFor, DIRECT_WEIGHT, TRANSITIVE_WEIGHT };
|
|
10440
|
+
|
|
10441
|
+
};
|
|
10442
|
+
|
|
10307
10443
|
// ── ./src/graph/builder ──
|
|
10308
10444
|
__factories["./src/graph/builder"] = function(module, exports) {
|
|
10309
10445
|
|
|
@@ -13359,6 +13495,28 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
13359
13495
|
}
|
|
13360
13496
|
}
|
|
13361
13497
|
|
|
13498
|
+
/**
|
|
13499
|
+
* get_method_impact({ symbol, direction?, depth? }) → string
|
|
13500
|
+
*
|
|
13501
|
+
* Method-level blast radius (GR2): every function that transitively calls
|
|
13502
|
+
* `symbol` (direction 'callers', default), or everything it calls ('callees').
|
|
13503
|
+
*/
|
|
13504
|
+
function getMethodImpact(args, cwd) {
|
|
13505
|
+
if (!args || !args.symbol) return 'Missing required argument: symbol';
|
|
13506
|
+
|
|
13507
|
+
try {
|
|
13508
|
+
const { methodImpact, methodCallees, formatCallGraph } = __require('./src/graph/call-graph');
|
|
13509
|
+
const kind = args.direction === 'callees' ? 'callees' : 'callers';
|
|
13510
|
+
const depth = Math.max(0, parseInt(args.depth, 10) || 0);
|
|
13511
|
+
const result = kind === 'callees'
|
|
13512
|
+
? methodCallees(args.symbol, cwd, { depth })
|
|
13513
|
+
: methodImpact(args.symbol, cwd, { depth });
|
|
13514
|
+
return formatCallGraph(result, kind);
|
|
13515
|
+
} catch (err) {
|
|
13516
|
+
return `_get_method_impact failed: ${err.message}_`;
|
|
13517
|
+
}
|
|
13518
|
+
}
|
|
13519
|
+
|
|
13362
13520
|
/**
|
|
13363
13521
|
* get_impact({ file, depth? }) → string
|
|
13364
13522
|
*
|
|
@@ -13872,7 +14030,7 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
13872
14030
|
return header + sq.squeezed;
|
|
13873
14031
|
}
|
|
13874
14032
|
|
|
13875
|
-
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput };
|
|
14033
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput };
|
|
13876
14034
|
|
|
13877
14035
|
};
|
|
13878
14036
|
|
|
@@ -14039,11 +14197,11 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14039
14197
|
|
|
14040
14198
|
const readline = require('readline');
|
|
14041
14199
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
14042
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } = __require('./src/mcp/handlers');
|
|
14200
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } = __require('./src/mcp/handlers');
|
|
14043
14201
|
|
|
14044
14202
|
const SERVER_INFO = {
|
|
14045
14203
|
name: 'sigmap',
|
|
14046
|
-
version: '8.
|
|
14204
|
+
version: '8.13.0',
|
|
14047
14205
|
description: 'SigMap MCP server — code signatures on demand',
|
|
14048
14206
|
};
|
|
14049
14207
|
|
|
@@ -14099,6 +14257,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14099
14257
|
else if (name === 'explain_file') text = explainFile(args, cwd);
|
|
14100
14258
|
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
14101
14259
|
else if (name === 'query_context') text = queryContext(args, cwd);
|
|
14260
|
+
else if (name === 'get_method_impact') text = getMethodImpact(args, cwd);
|
|
14102
14261
|
else if (name === 'get_impact') text = getImpact(args, cwd);
|
|
14103
14262
|
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
14104
14263
|
else if (name === 'read_memory') text = readMemory(args, cwd);
|
|
@@ -14170,12 +14329,12 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14170
14329
|
__factories["./src/mcp/tools"] = function(module, exports) {
|
|
14171
14330
|
|
|
14172
14331
|
/**
|
|
14173
|
-
* MCP tool definitions for SigMap (
|
|
14332
|
+
* MCP tool definitions for SigMap (20 tools).
|
|
14174
14333
|
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
14175
|
-
* explain_file, list_modules, query_context,
|
|
14176
|
-
* get_callee_signatures, sigmap_notify_file_created,
|
|
14177
|
-
* sigmap_notify_file_deleted, get_diff_context,
|
|
14178
|
-
* verify_suggestion, squeeze_output.
|
|
14334
|
+
* explain_file, list_modules, query_context, get_method_impact, get_impact,
|
|
14335
|
+
* get_lines, read_memory, get_callee_signatures, sigmap_notify_file_created,
|
|
14336
|
+
* sigmap_notify_symbol_added, sigmap_notify_file_deleted, get_diff_context,
|
|
14337
|
+
* get_architecture_overview, verify_suggestion, squeeze_output.
|
|
14179
14338
|
*/
|
|
14180
14339
|
|
|
14181
14340
|
const TOOLS = [
|
|
@@ -14317,6 +14476,35 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
14317
14476
|
required: ['query'],
|
|
14318
14477
|
},
|
|
14319
14478
|
},
|
|
14479
|
+
{
|
|
14480
|
+
name: 'get_method_impact',
|
|
14481
|
+
description:
|
|
14482
|
+
'Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — ' +
|
|
14483
|
+
'or, with direction "callees", every repo function it calls. Finer-grained than the ' +
|
|
14484
|
+
'file-level get_impact: tells an agent which functions break, not just which files. ' +
|
|
14485
|
+
'JS/TS + Python call-graph; deterministic, no LLM.',
|
|
14486
|
+
inputSchema: {
|
|
14487
|
+
type: 'object',
|
|
14488
|
+
properties: {
|
|
14489
|
+
symbol: {
|
|
14490
|
+
type: 'string',
|
|
14491
|
+
description:
|
|
14492
|
+
'Function/method name (e.g. "validateToken") or a full "file#name" id ' +
|
|
14493
|
+
'(e.g. "src/auth/session.js#validateToken") to disambiguate.',
|
|
14494
|
+
},
|
|
14495
|
+
direction: {
|
|
14496
|
+
type: 'string',
|
|
14497
|
+
enum: ['callers', 'callees'],
|
|
14498
|
+
description: '"callers" (default) = blast radius; "callees" = what the symbol calls.',
|
|
14499
|
+
},
|
|
14500
|
+
depth: {
|
|
14501
|
+
type: 'number',
|
|
14502
|
+
description: 'BFS depth limit (default 0 = unlimited).',
|
|
14503
|
+
},
|
|
14504
|
+
},
|
|
14505
|
+
required: ['symbol'],
|
|
14506
|
+
},
|
|
14507
|
+
},
|
|
14320
14508
|
{
|
|
14321
14509
|
name: 'get_impact',
|
|
14322
14510
|
description:
|
|
@@ -15723,6 +15911,12 @@ __factories["./src/review/pr-evidence"] = function(module, exports) {
|
|
|
15723
15911
|
impactByFile = new Map(analyzeImpact(srcPaths, cwd, { depth }).map((r) => [r.file, r.impact]));
|
|
15724
15912
|
} catch (_) { /* graph optional */ }
|
|
15725
15913
|
|
|
15914
|
+
// GR2: method-level blast radius per changed file (reviewPr already computed
|
|
15915
|
+
// it when the call graph resolved — reuse, don't rebuild the graph).
|
|
15916
|
+
const methodBlastByFile = new Map(
|
|
15917
|
+
(review.methodBlast && review.methodBlast.files || []).map((m) => [m.file, m])
|
|
15918
|
+
);
|
|
15919
|
+
|
|
15726
15920
|
const fileReports = files.map((f) => {
|
|
15727
15921
|
const deleted = f.status === 'D';
|
|
15728
15922
|
let signatures = [];
|
|
@@ -15731,6 +15925,7 @@ __factories["./src/review/pr-evidence"] = function(module, exports) {
|
|
|
15731
15925
|
}
|
|
15732
15926
|
const impact = impactByFile.get(f.path) || null;
|
|
15733
15927
|
return {
|
|
15928
|
+
methodBlast: methodBlastByFile.get(f.path.replace(/\\/g, '/')) || null,
|
|
15734
15929
|
path: f.path,
|
|
15735
15930
|
status: f.status,
|
|
15736
15931
|
riskLabel: riskLabelFor(f.path),
|
|
@@ -15773,6 +15968,7 @@ __factories["./src/review/pr-evidence"] = function(module, exports) {
|
|
|
15773
15968
|
else if (f.type === 'security-file') L.push(`- ⚠️ **sensitive path touched** (path heuristic, not a content scan) — \`${f.file}\``);
|
|
15774
15969
|
else if (f.type === 'secret-detected') L.push(`- 🔑 **secret detected** (${f.secret}) — \`${f.file}\``);
|
|
15775
15970
|
else if (f.type === 'god-node') L.push(`- ⚠️ **god node** — \`${f.file}\` → ${f.count} dependents (high blast radius)`);
|
|
15971
|
+
else if (f.type === 'method-blast') L.push(`- ⚠️ **method blast radius ${f.tier}** — \`${f.file}\` → ${f.functions} function(s) transitively call into this change (score ${f.score}/100)`);
|
|
15776
15972
|
else if (f.type === 'scope-drift') L.push(`- ⚠️ **scope drift** — ${f.count} top-level dirs touched (${f.dirs.join(', ')})`);
|
|
15777
15973
|
}
|
|
15778
15974
|
L.push('');
|
|
@@ -15794,6 +15990,15 @@ __factories["./src/review/pr-evidence"] = function(module, exports) {
|
|
|
15794
15990
|
} else {
|
|
15795
15991
|
L.push('**Blast radius:** _(not in dependency graph — new or leaf file)_');
|
|
15796
15992
|
}
|
|
15993
|
+
if (f.methodBlast && (f.methodBlast.directCallers + f.methodBlast.transitiveCallers) > 0) {
|
|
15994
|
+
const mb = f.methodBlast;
|
|
15995
|
+
const total = mb.directCallers + mb.transitiveCallers;
|
|
15996
|
+
L.push(
|
|
15997
|
+
`**Method blast radius:** ${total} function(s) impacted (score ${mb.score}/100, ${mb.tier}) — ` +
|
|
15998
|
+
mb.impactedFunctions.slice(0, 6).map((id) => '`' + id + '`').join(', ') +
|
|
15999
|
+
(total > 6 ? ` +${total - 6} more` : '')
|
|
16000
|
+
);
|
|
16001
|
+
}
|
|
15797
16002
|
if (f.relatedTests.length) L.push(`Related tests: ${f.relatedTests.slice(0, 8).map((t) => '`' + t + '`').join(', ')}`);
|
|
15798
16003
|
|
|
15799
16004
|
if (f.signatures.length) {
|
|
@@ -15860,7 +16065,7 @@ __factories["./src/review/review-pr"] = function(module, exports) {
|
|
|
15860
16065
|
* @param {object} [opts]
|
|
15861
16066
|
* @param {number} [opts.godNodeThreshold=15]
|
|
15862
16067
|
* @param {number} [opts.scopeThreshold=5]
|
|
15863
|
-
* @returns {{ findings: object[], blast: object[], summary: object }}
|
|
16068
|
+
* @returns {{ findings: object[], blast: object[], methodBlast: object|null, summary: object }}
|
|
15864
16069
|
*/
|
|
15865
16070
|
function reviewPr(changedFiles, cwd, opts = {}) {
|
|
15866
16071
|
const godThreshold = opts.godNodeThreshold != null ? opts.godNodeThreshold : GOD_NODE_THRESHOLD;
|
|
@@ -15921,6 +16126,27 @@ __factories["./src/review/review-pr"] = function(module, exports) {
|
|
|
15921
16126
|
blast.sort((a, b) => b.totalImpact - a.totalImpact);
|
|
15922
16127
|
}
|
|
15923
16128
|
|
|
16129
|
+
// 3b. Method-level blast radius (GR2) — how many FUNCTIONS transitively call
|
|
16130
|
+
// into the change, scored deterministically. Graph optional, like 3.
|
|
16131
|
+
let methodBlast = null;
|
|
16132
|
+
if (srcChanged.length) {
|
|
16133
|
+
try {
|
|
16134
|
+
const { methodBlastRadius } = __require('./src/graph/blast-radius');
|
|
16135
|
+
const mb = methodBlastRadius(srcChanged, cwd, opts.methodBlastOpts || {});
|
|
16136
|
+
if (mb.available) {
|
|
16137
|
+
methodBlast = mb;
|
|
16138
|
+
for (const f of mb.files) {
|
|
16139
|
+
if (f.tier === 'high' || f.tier === 'critical') {
|
|
16140
|
+
findings.push({
|
|
16141
|
+
type: 'method-blast', file: f.file, severity: 'warn',
|
|
16142
|
+
functions: f.directCallers + f.transitiveCallers, score: f.score, tier: f.tier,
|
|
16143
|
+
});
|
|
16144
|
+
}
|
|
16145
|
+
}
|
|
16146
|
+
}
|
|
16147
|
+
} catch (_) { /* call graph optional */ }
|
|
16148
|
+
}
|
|
16149
|
+
|
|
15924
16150
|
// 4. Scope drift: distinct top-level directories touched.
|
|
15925
16151
|
const dirs = [...new Set(paths.map((p) => (p.includes('/') ? p.split('/')[0] : '.')))];
|
|
15926
16152
|
if (dirs.length > scopeThreshold) {
|
|
@@ -15931,6 +16157,7 @@ __factories["./src/review/review-pr"] = function(module, exports) {
|
|
|
15931
16157
|
return {
|
|
15932
16158
|
findings,
|
|
15933
16159
|
blast,
|
|
16160
|
+
methodBlast,
|
|
15934
16161
|
summary: {
|
|
15935
16162
|
filesChanged: files.length,
|
|
15936
16163
|
sourceChanged: srcChanged.length,
|
|
@@ -18625,6 +18852,260 @@ __factories["./src/verify/parsers"] = function(module, exports) {
|
|
|
18625
18852
|
|
|
18626
18853
|
};
|
|
18627
18854
|
|
|
18855
|
+
// ── ./src/wiki/generate ──
|
|
18856
|
+
__factories["./src/wiki/generate"] = function(module, exports) {
|
|
18857
|
+
|
|
18858
|
+
/**
|
|
18859
|
+
* Wiki generation (D9) — `sigmap wiki`.
|
|
18860
|
+
*
|
|
18861
|
+
* Deterministic architecture narrative composed from data SigMap already
|
|
18862
|
+
* computes: the signature index, the dependency graph, conventions, and the
|
|
18863
|
+
* health score. Template prose only — no LLM, no network, no timestamps —
|
|
18864
|
+
* so two runs on an unchanged repo produce byte-identical markdown.
|
|
18865
|
+
*/
|
|
18866
|
+
|
|
18867
|
+
const fs = require('fs');
|
|
18868
|
+
const path = require('path');
|
|
18869
|
+
|
|
18870
|
+
const HUB_LIMIT = 8;
|
|
18871
|
+
const ENTRY_LIMIT = 8;
|
|
18872
|
+
const MODULE_LIMIT = 20;
|
|
18873
|
+
const KEY_FILE_LIMIT = 3;
|
|
18874
|
+
|
|
18875
|
+
// Graph keys come from src/graph/builder's normalizePath (normalized +
|
|
18876
|
+
// lowercased), so relativize against the same normalization of cwd.
|
|
18877
|
+
function _rel(cwd, f) {
|
|
18878
|
+
return path.relative(path.normalize(cwd).toLowerCase(), f).replace(/\\/g, '/');
|
|
18879
|
+
}
|
|
18880
|
+
|
|
18881
|
+
function _pct(fraction) {
|
|
18882
|
+
return Math.round(fraction * 100);
|
|
18883
|
+
}
|
|
18884
|
+
|
|
18885
|
+
/** Project name + version from package.json, falling back to the dir name. */
|
|
18886
|
+
function _identity(cwd) {
|
|
18887
|
+
try {
|
|
18888
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
18889
|
+
if (pkg && pkg.name) return { name: pkg.name, version: pkg.version || null };
|
|
18890
|
+
} catch (_) {}
|
|
18891
|
+
return { name: path.basename(cwd), version: null };
|
|
18892
|
+
}
|
|
18893
|
+
|
|
18894
|
+
/** Module rollup from the signature index (keys are cwd-relative paths). */
|
|
18895
|
+
function _modules(index) {
|
|
18896
|
+
const groups = new Map();
|
|
18897
|
+
let totalTokens = 0;
|
|
18898
|
+
for (const [rel, sigs] of index.entries()) {
|
|
18899
|
+
const parts = String(rel).replace(/\\/g, '/').split('/');
|
|
18900
|
+
const mod = parts.length > 1 ? parts[0] : '.';
|
|
18901
|
+
const tokens = Math.ceil((sigs || []).join('\n').length / 4);
|
|
18902
|
+
totalTokens += tokens;
|
|
18903
|
+
if (!groups.has(mod)) groups.set(mod, { name: mod, files: 0, tokens: 0, fileSigs: [] });
|
|
18904
|
+
const g = groups.get(mod);
|
|
18905
|
+
g.files++;
|
|
18906
|
+
g.tokens += tokens;
|
|
18907
|
+
g.fileSigs.push({ file: rel, sigCount: (sigs || []).length });
|
|
18908
|
+
}
|
|
18909
|
+
const modules = [...groups.values()]
|
|
18910
|
+
.sort((a, b) => b.tokens - a.tokens || a.name.localeCompare(b.name))
|
|
18911
|
+
.slice(0, MODULE_LIMIT)
|
|
18912
|
+
.map((g) => ({
|
|
18913
|
+
name: g.name,
|
|
18914
|
+
files: g.files,
|
|
18915
|
+
tokens: g.tokens,
|
|
18916
|
+
keyFiles: g.fileSigs
|
|
18917
|
+
.sort((a, b) => b.sigCount - a.sigCount || a.file.localeCompare(b.file))
|
|
18918
|
+
.slice(0, KEY_FILE_LIMIT)
|
|
18919
|
+
.map((f) => f.file),
|
|
18920
|
+
}));
|
|
18921
|
+
return { modules, totalTokens };
|
|
18922
|
+
}
|
|
18923
|
+
|
|
18924
|
+
/** Hubs, entry points, and cycle count from the dependency graph. */
|
|
18925
|
+
function _flow(cwd) {
|
|
18926
|
+
try {
|
|
18927
|
+
const { buildFromCwd } = __require('./src/graph/builder');
|
|
18928
|
+
const { detectCycles } = __require('./src/map/import-graph');
|
|
18929
|
+
const graph = buildFromCwd(cwd);
|
|
18930
|
+
if (!graph || !graph.forward || graph.forward.size === 0) return null;
|
|
18931
|
+
|
|
18932
|
+
const importersOf = (f) => (graph.reverse.get(f) || []).length;
|
|
18933
|
+
const hubs = [...graph.reverse.entries()]
|
|
18934
|
+
.map(([f, importers]) => ({ file: _rel(cwd, f), importers: importers.length }))
|
|
18935
|
+
.filter((h) => h.importers > 0)
|
|
18936
|
+
.sort((a, b) => b.importers - a.importers || a.file.localeCompare(b.file))
|
|
18937
|
+
.slice(0, HUB_LIMIT);
|
|
18938
|
+
|
|
18939
|
+
const entryPoints = [...graph.forward.entries()]
|
|
18940
|
+
.filter(([f, deps]) => deps.length > 0 && importersOf(f) === 0)
|
|
18941
|
+
.map(([f, deps]) => ({ file: _rel(cwd, f), imports: deps.length }))
|
|
18942
|
+
.sort((a, b) => b.imports - a.imports || a.file.localeCompare(b.file))
|
|
18943
|
+
.slice(0, ENTRY_LIMIT);
|
|
18944
|
+
|
|
18945
|
+
let cycles = 0;
|
|
18946
|
+
try { cycles = detectCycles(graph.forward).length; } catch (_) {}
|
|
18947
|
+
|
|
18948
|
+
return { hubs, entryPoints, cycles, edges: graph.forward.size };
|
|
18949
|
+
} catch (_) {
|
|
18950
|
+
return null;
|
|
18951
|
+
}
|
|
18952
|
+
}
|
|
18953
|
+
|
|
18954
|
+
/** Conventions summary; index keys are resolved back to absolute paths. */
|
|
18955
|
+
function _conventions(cwd, index) {
|
|
18956
|
+
try {
|
|
18957
|
+
const { extractConventions } = __require('./src/conventions/extract');
|
|
18958
|
+
const files = [...index.keys()].map((rel) => path.join(cwd, rel));
|
|
18959
|
+
const c = extractConventions(cwd, files);
|
|
18960
|
+
return {
|
|
18961
|
+
fileNaming: c.fileNaming
|
|
18962
|
+
? { dominant: c.fileNaming.dominant, pct: _pct(c.fileNaming.dominantPct || 0), tier: c.fileNaming.tier }
|
|
18963
|
+
: null,
|
|
18964
|
+
exportStyle: c.exportStyle
|
|
18965
|
+
? { dominant: c.exportStyle.dominant, pct: _pct(c.exportStyle.dominantPct || 0), tier: c.exportStyle.tier }
|
|
18966
|
+
: null,
|
|
18967
|
+
testFramework: c.testFramework || null,
|
|
18968
|
+
};
|
|
18969
|
+
} catch (_) {
|
|
18970
|
+
return null;
|
|
18971
|
+
}
|
|
18972
|
+
}
|
|
18973
|
+
|
|
18974
|
+
function _health(cwd) {
|
|
18975
|
+
try {
|
|
18976
|
+
const { score } = __require('./src/health/scorer');
|
|
18977
|
+
const h = score(cwd);
|
|
18978
|
+
return { score: h.score, grade: h.grade };
|
|
18979
|
+
} catch (_) {
|
|
18980
|
+
return null;
|
|
18981
|
+
}
|
|
18982
|
+
}
|
|
18983
|
+
|
|
18984
|
+
/**
|
|
18985
|
+
* Build the wiki. Every data source is optional — a repo with no context file
|
|
18986
|
+
* or no resolvable graph still yields a valid document.
|
|
18987
|
+
* @param {string} cwd
|
|
18988
|
+
* @param {object} [opts]
|
|
18989
|
+
* @param {string} [opts.version] SigMap version stamped in the header
|
|
18990
|
+
* @returns {{ data: object, markdown: string }}
|
|
18991
|
+
*/
|
|
18992
|
+
function buildWiki(cwd, opts = {}) {
|
|
18993
|
+
let index = new Map();
|
|
18994
|
+
try {
|
|
18995
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
18996
|
+
index = buildSigIndex(cwd);
|
|
18997
|
+
} catch (_) {}
|
|
18998
|
+
|
|
18999
|
+
const identity = _identity(cwd);
|
|
19000
|
+
const { modules, totalTokens } = _modules(index);
|
|
19001
|
+
const flow = _flow(cwd);
|
|
19002
|
+
const conventions = index.size ? _conventions(cwd, index) : null;
|
|
19003
|
+
const health = _health(cwd);
|
|
19004
|
+
|
|
19005
|
+
const data = {
|
|
19006
|
+
name: identity.name,
|
|
19007
|
+
version: identity.version,
|
|
19008
|
+
files: index.size,
|
|
19009
|
+
modules,
|
|
19010
|
+
totalTokens,
|
|
19011
|
+
flow,
|
|
19012
|
+
conventions,
|
|
19013
|
+
health,
|
|
19014
|
+
};
|
|
19015
|
+
|
|
19016
|
+
return { data, markdown: renderWikiMarkdown(data, opts.version) };
|
|
19017
|
+
}
|
|
19018
|
+
|
|
19019
|
+
/**
|
|
19020
|
+
* Render the narrative markdown. Pure function of `data` — no clocks, no
|
|
19021
|
+
* randomness — so output is byte-stable for a fixed repo state.
|
|
19022
|
+
* @param {object} data
|
|
19023
|
+
* @param {string} [sigmapVersion]
|
|
19024
|
+
* @returns {string}
|
|
19025
|
+
*/
|
|
19026
|
+
function renderWikiMarkdown(data, sigmapVersion) {
|
|
19027
|
+
const L = [];
|
|
19028
|
+
const title = data.version ? `${data.name} v${data.version}` : data.name;
|
|
19029
|
+
L.push(`# ${title} — Architecture Wiki`);
|
|
19030
|
+
L.push('');
|
|
19031
|
+
L.push(`_Deterministically generated from signatures + dependency graph by SigMap${sigmapVersion ? ` v${sigmapVersion}` : ''} — no LLM. Regenerate: \`sigmap wiki\`._`);
|
|
19032
|
+
L.push('');
|
|
19033
|
+
|
|
19034
|
+
L.push('## Overview');
|
|
19035
|
+
if (data.files === 0) {
|
|
19036
|
+
L.push('No signature index found yet — run `sigmap` (or `node gen-context.js`) to generate context, then regenerate this wiki.');
|
|
19037
|
+
} else {
|
|
19038
|
+
const fileWord = data.files === 1 ? 'indexed file' : 'indexed files';
|
|
19039
|
+
const modWord = data.modules.length === 1 ? 'top-level module' : 'top-level modules';
|
|
19040
|
+
L.push(`The codebase spans **${data.files} ${fileWord}** across **${data.modules.length} ${modWord}**, with ~${data.totalTokens} tokens of extracted signatures.`);
|
|
19041
|
+
if (data.health) {
|
|
19042
|
+
L.push(`Context health: **${data.health.score}/100 (${data.health.grade})**.`);
|
|
19043
|
+
}
|
|
19044
|
+
}
|
|
19045
|
+
L.push('');
|
|
19046
|
+
|
|
19047
|
+
if (data.modules.length) {
|
|
19048
|
+
L.push('## Modules');
|
|
19049
|
+
L.push('| Module | Files | Sig tokens | Key files |');
|
|
19050
|
+
L.push('|--------|-------|------------|-----------|');
|
|
19051
|
+
for (const m of data.modules) {
|
|
19052
|
+
L.push(`| \`${m.name}\` | ${m.files} | ~${m.tokens} | ${m.keyFiles.map((f) => `\`${f}\``).join(', ')} |`);
|
|
19053
|
+
}
|
|
19054
|
+
const top = data.modules[0];
|
|
19055
|
+
L.push('');
|
|
19056
|
+
L.push(`The largest module by signature volume is \`${top.name}\` (${top.files} files, ~${top.tokens} tokens) — start there for the core logic.`);
|
|
19057
|
+
L.push('');
|
|
19058
|
+
}
|
|
19059
|
+
|
|
19060
|
+
if (data.flow) {
|
|
19061
|
+
L.push('## Dependency flow');
|
|
19062
|
+
if (data.flow.hubs.length) {
|
|
19063
|
+
L.push('The most depended-on files — changes here have the widest blast radius:');
|
|
19064
|
+
L.push('');
|
|
19065
|
+
L.push('| Hub file | Importers |');
|
|
19066
|
+
L.push('|----------|-----------|');
|
|
19067
|
+
for (const h of data.flow.hubs) L.push(`| \`${h.file}\` | ${h.importers} |`);
|
|
19068
|
+
L.push('');
|
|
19069
|
+
}
|
|
19070
|
+
if (data.flow.entryPoints.length) {
|
|
19071
|
+
L.push('Entry points (imported by nothing, importing the rest):');
|
|
19072
|
+
L.push('');
|
|
19073
|
+
for (const e of data.flow.entryPoints) L.push(`- \`${e.file}\` → ${e.imports} imports`);
|
|
19074
|
+
L.push('');
|
|
19075
|
+
}
|
|
19076
|
+
L.push(data.flow.cycles
|
|
19077
|
+
? `**Dependency cycles:** ${data.flow.cycles} — untangle these first when refactoring.`
|
|
19078
|
+
: '**Dependency cycles:** none detected.');
|
|
19079
|
+
L.push('');
|
|
19080
|
+
}
|
|
19081
|
+
|
|
19082
|
+
if (data.conventions) {
|
|
19083
|
+
L.push('## Conventions');
|
|
19084
|
+
const c = data.conventions;
|
|
19085
|
+
const bits = [];
|
|
19086
|
+
if (c.fileNaming && c.fileNaming.dominant) bits.push(`file naming is predominantly **${c.fileNaming.dominant}** (${c.fileNaming.pct}%, ${c.fileNaming.tier})`);
|
|
19087
|
+
if (c.exportStyle && c.exportStyle.dominant) bits.push(`exports use the **${c.exportStyle.dominant}** style (${c.exportStyle.pct}%, ${c.exportStyle.tier})`);
|
|
19088
|
+
if (c.testFramework) bits.push(`tests run on **${c.testFramework}**`);
|
|
19089
|
+
L.push(bits.length
|
|
19090
|
+
? `In this repo, ${bits.join('; ')}. New code should match.`
|
|
19091
|
+
: 'No dominant conventions detected (repo too small or styles mixed).');
|
|
19092
|
+
L.push('');
|
|
19093
|
+
}
|
|
19094
|
+
|
|
19095
|
+
L.push('## Navigating');
|
|
19096
|
+
L.push('- `sigmap ask "<question>"` — ranked, budgeted mini-context for any task');
|
|
19097
|
+
L.push('- `sigmap --impact <file>` / `--callers <symbol>` — blast radius before you change something');
|
|
19098
|
+
L.push('- `sigmap evidence "<query>"` — machine-consumable Evidence Pack (JSON) for agents/CI');
|
|
19099
|
+
L.push('- MCP: `get_architecture_overview`, `get_map`, `get_callee_signatures` for live agent access');
|
|
19100
|
+
L.push('');
|
|
19101
|
+
|
|
19102
|
+
return L.join('\n');
|
|
19103
|
+
}
|
|
19104
|
+
|
|
19105
|
+
module.exports = { buildWiki, renderWikiMarkdown };
|
|
19106
|
+
|
|
19107
|
+
};
|
|
19108
|
+
|
|
18628
19109
|
// ── ./src/workspace/detector ──
|
|
18629
19110
|
__factories["./src/workspace/detector"] = function(module, exports) {
|
|
18630
19111
|
|
|
@@ -18733,7 +19214,7 @@ function __tryGit(args, opts = {}) {
|
|
|
18733
19214
|
catch (_) { return ''; }
|
|
18734
19215
|
}
|
|
18735
19216
|
|
|
18736
|
-
const VERSION = '8.
|
|
19217
|
+
const VERSION = '8.13.0';
|
|
18737
19218
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
18738
19219
|
|
|
18739
19220
|
function requireSourceOrBundled(key) {
|
|
@@ -20607,6 +21088,7 @@ Usage:
|
|
|
20607
21088
|
${cmd} review-pr Audit a diff — scope drift, god-node edits, missing tests, security files (--staged, --base, --json, --markdown)
|
|
20608
21089
|
${cmd} review-pr --markdown PR Evidence Report — branded Markdown (signatures + blast radius + tests) to post as a PR comment
|
|
20609
21090
|
${cmd} create "<task>" Grounded-creation pipeline: scaffold → verify-plan → verify-ai-output → review-pr (--staged)
|
|
21091
|
+
${cmd} wiki Deterministic architecture wiki from signatures + graph — no LLM (--json, --out <path>)
|
|
20610
21092
|
${cmd} squeeze <file|-> Minimize a pasted stacktrace/CI-log/JSON blob (--json for stats)
|
|
20611
21093
|
${cmd} squeeze --response <file|-> Minimize an agent/tool response (same engine; also exposed as the squeeze_output MCP tool)
|
|
20612
21094
|
${cmd} ask "<query>" --squeeze Auto-accept input minimization (no prompt; for scripts/CI)
|
|
@@ -22227,6 +22709,30 @@ function main() {
|
|
|
22227
22709
|
process.exit(result.ok ? 0 : 1);
|
|
22228
22710
|
}
|
|
22229
22711
|
|
|
22712
|
+
// D9: `sigmap wiki` — deterministic architecture narrative (no LLM).
|
|
22713
|
+
if (args[0] === 'wiki') {
|
|
22714
|
+
const { buildWiki } = requireSourceOrBundled('./src/wiki/generate');
|
|
22715
|
+
const result = buildWiki(cwd, { version: VERSION });
|
|
22716
|
+
if (args.includes('--json')) {
|
|
22717
|
+
process.stdout.write(JSON.stringify(result.data, null, 2) + '\n');
|
|
22718
|
+
process.exit(0);
|
|
22719
|
+
}
|
|
22720
|
+
const outIdx = args.indexOf('--out');
|
|
22721
|
+
const outRel = outIdx >= 0 && args[outIdx + 1] && !args[outIdx + 1].startsWith('--')
|
|
22722
|
+
? args[outIdx + 1]
|
|
22723
|
+
: path.join('.context', 'WIKI.md');
|
|
22724
|
+
const outPath = path.resolve(cwd, outRel);
|
|
22725
|
+
try {
|
|
22726
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
22727
|
+
fs.writeFileSync(outPath, result.markdown, 'utf8');
|
|
22728
|
+
} catch (e) {
|
|
22729
|
+
console.error(`[sigmap] cannot write ${outPath}: ${e.message}`);
|
|
22730
|
+
process.exit(1);
|
|
22731
|
+
}
|
|
22732
|
+
console.log(`[sigmap] wiki → ${path.relative(cwd, outPath)} (${result.data.files} files · ${result.data.modules.length} modules)`);
|
|
22733
|
+
process.exit(0);
|
|
22734
|
+
}
|
|
22735
|
+
|
|
22230
22736
|
// Layer 3: `sigmap conventions` — extract & report repo coding conventions
|
|
22231
22737
|
// (file naming, export style, test framework) for TS/JS/Python so generated
|
|
22232
22738
|
// code matches the house style. Writes .context/conventions.json.
|