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
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wiki generation (D9) — `sigmap wiki`.
|
|
5
|
+
*
|
|
6
|
+
* Deterministic architecture narrative composed from data SigMap already
|
|
7
|
+
* computes: the signature index, the dependency graph, conventions, and the
|
|
8
|
+
* health score. Template prose only — no LLM, no network, no timestamps —
|
|
9
|
+
* so two runs on an unchanged repo produce byte-identical markdown.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const HUB_LIMIT = 8;
|
|
16
|
+
const ENTRY_LIMIT = 8;
|
|
17
|
+
const MODULE_LIMIT = 20;
|
|
18
|
+
const KEY_FILE_LIMIT = 3;
|
|
19
|
+
|
|
20
|
+
// Graph keys come from src/graph/builder's normalizePath (normalized +
|
|
21
|
+
// lowercased), so relativize against the same normalization of cwd.
|
|
22
|
+
function _rel(cwd, f) {
|
|
23
|
+
return path.relative(path.normalize(cwd).toLowerCase(), f).replace(/\\/g, '/');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function _pct(fraction) {
|
|
27
|
+
return Math.round(fraction * 100);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Project name + version from package.json, falling back to the dir name. */
|
|
31
|
+
function _identity(cwd) {
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
34
|
+
if (pkg && pkg.name) return { name: pkg.name, version: pkg.version || null };
|
|
35
|
+
} catch (_) {}
|
|
36
|
+
return { name: path.basename(cwd), version: null };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Module rollup from the signature index (keys are cwd-relative paths). */
|
|
40
|
+
function _modules(index) {
|
|
41
|
+
const groups = new Map();
|
|
42
|
+
let totalTokens = 0;
|
|
43
|
+
for (const [rel, sigs] of index.entries()) {
|
|
44
|
+
const parts = String(rel).replace(/\\/g, '/').split('/');
|
|
45
|
+
const mod = parts.length > 1 ? parts[0] : '.';
|
|
46
|
+
const tokens = Math.ceil((sigs || []).join('\n').length / 4);
|
|
47
|
+
totalTokens += tokens;
|
|
48
|
+
if (!groups.has(mod)) groups.set(mod, { name: mod, files: 0, tokens: 0, fileSigs: [] });
|
|
49
|
+
const g = groups.get(mod);
|
|
50
|
+
g.files++;
|
|
51
|
+
g.tokens += tokens;
|
|
52
|
+
g.fileSigs.push({ file: rel, sigCount: (sigs || []).length });
|
|
53
|
+
}
|
|
54
|
+
const modules = [...groups.values()]
|
|
55
|
+
.sort((a, b) => b.tokens - a.tokens || a.name.localeCompare(b.name))
|
|
56
|
+
.slice(0, MODULE_LIMIT)
|
|
57
|
+
.map((g) => ({
|
|
58
|
+
name: g.name,
|
|
59
|
+
files: g.files,
|
|
60
|
+
tokens: g.tokens,
|
|
61
|
+
keyFiles: g.fileSigs
|
|
62
|
+
.sort((a, b) => b.sigCount - a.sigCount || a.file.localeCompare(b.file))
|
|
63
|
+
.slice(0, KEY_FILE_LIMIT)
|
|
64
|
+
.map((f) => f.file),
|
|
65
|
+
}));
|
|
66
|
+
return { modules, totalTokens };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Hubs, entry points, and cycle count from the dependency graph. */
|
|
70
|
+
function _flow(cwd) {
|
|
71
|
+
try {
|
|
72
|
+
const { buildFromCwd } = require('../graph/builder');
|
|
73
|
+
const { detectCycles } = require('../map/import-graph');
|
|
74
|
+
const graph = buildFromCwd(cwd);
|
|
75
|
+
if (!graph || !graph.forward || graph.forward.size === 0) return null;
|
|
76
|
+
|
|
77
|
+
const importersOf = (f) => (graph.reverse.get(f) || []).length;
|
|
78
|
+
const hubs = [...graph.reverse.entries()]
|
|
79
|
+
.map(([f, importers]) => ({ file: _rel(cwd, f), importers: importers.length }))
|
|
80
|
+
.filter((h) => h.importers > 0)
|
|
81
|
+
.sort((a, b) => b.importers - a.importers || a.file.localeCompare(b.file))
|
|
82
|
+
.slice(0, HUB_LIMIT);
|
|
83
|
+
|
|
84
|
+
const entryPoints = [...graph.forward.entries()]
|
|
85
|
+
.filter(([f, deps]) => deps.length > 0 && importersOf(f) === 0)
|
|
86
|
+
.map(([f, deps]) => ({ file: _rel(cwd, f), imports: deps.length }))
|
|
87
|
+
.sort((a, b) => b.imports - a.imports || a.file.localeCompare(b.file))
|
|
88
|
+
.slice(0, ENTRY_LIMIT);
|
|
89
|
+
|
|
90
|
+
let cycles = 0;
|
|
91
|
+
try { cycles = detectCycles(graph.forward).length; } catch (_) {}
|
|
92
|
+
|
|
93
|
+
return { hubs, entryPoints, cycles, edges: graph.forward.size };
|
|
94
|
+
} catch (_) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Conventions summary; index keys are resolved back to absolute paths. */
|
|
100
|
+
function _conventions(cwd, index) {
|
|
101
|
+
try {
|
|
102
|
+
const { extractConventions } = require('../conventions/extract');
|
|
103
|
+
const files = [...index.keys()].map((rel) => path.join(cwd, rel));
|
|
104
|
+
const c = extractConventions(cwd, files);
|
|
105
|
+
return {
|
|
106
|
+
fileNaming: c.fileNaming
|
|
107
|
+
? { dominant: c.fileNaming.dominant, pct: _pct(c.fileNaming.dominantPct || 0), tier: c.fileNaming.tier }
|
|
108
|
+
: null,
|
|
109
|
+
exportStyle: c.exportStyle
|
|
110
|
+
? { dominant: c.exportStyle.dominant, pct: _pct(c.exportStyle.dominantPct || 0), tier: c.exportStyle.tier }
|
|
111
|
+
: null,
|
|
112
|
+
testFramework: c.testFramework || null,
|
|
113
|
+
};
|
|
114
|
+
} catch (_) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _health(cwd) {
|
|
120
|
+
try {
|
|
121
|
+
const { score } = require('../health/scorer');
|
|
122
|
+
const h = score(cwd);
|
|
123
|
+
return { score: h.score, grade: h.grade };
|
|
124
|
+
} catch (_) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build the wiki. Every data source is optional — a repo with no context file
|
|
131
|
+
* or no resolvable graph still yields a valid document.
|
|
132
|
+
* @param {string} cwd
|
|
133
|
+
* @param {object} [opts]
|
|
134
|
+
* @param {string} [opts.version] SigMap version stamped in the header
|
|
135
|
+
* @returns {{ data: object, markdown: string }}
|
|
136
|
+
*/
|
|
137
|
+
function buildWiki(cwd, opts = {}) {
|
|
138
|
+
let index = new Map();
|
|
139
|
+
try {
|
|
140
|
+
const { buildSigIndex } = require('../retrieval/ranker');
|
|
141
|
+
index = buildSigIndex(cwd);
|
|
142
|
+
} catch (_) {}
|
|
143
|
+
|
|
144
|
+
const identity = _identity(cwd);
|
|
145
|
+
const { modules, totalTokens } = _modules(index);
|
|
146
|
+
const flow = _flow(cwd);
|
|
147
|
+
const conventions = index.size ? _conventions(cwd, index) : null;
|
|
148
|
+
const health = _health(cwd);
|
|
149
|
+
|
|
150
|
+
const data = {
|
|
151
|
+
name: identity.name,
|
|
152
|
+
version: identity.version,
|
|
153
|
+
files: index.size,
|
|
154
|
+
modules,
|
|
155
|
+
totalTokens,
|
|
156
|
+
flow,
|
|
157
|
+
conventions,
|
|
158
|
+
health,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return { data, markdown: renderWikiMarkdown(data, opts.version) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Render the narrative markdown. Pure function of `data` — no clocks, no
|
|
166
|
+
* randomness — so output is byte-stable for a fixed repo state.
|
|
167
|
+
* @param {object} data
|
|
168
|
+
* @param {string} [sigmapVersion]
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
function renderWikiMarkdown(data, sigmapVersion) {
|
|
172
|
+
const L = [];
|
|
173
|
+
const title = data.version ? `${data.name} v${data.version}` : data.name;
|
|
174
|
+
L.push(`# ${title} — Architecture Wiki`);
|
|
175
|
+
L.push('');
|
|
176
|
+
L.push(`_Deterministically generated from signatures + dependency graph by SigMap${sigmapVersion ? ` v${sigmapVersion}` : ''} — no LLM. Regenerate: \`sigmap wiki\`._`);
|
|
177
|
+
L.push('');
|
|
178
|
+
|
|
179
|
+
L.push('## Overview');
|
|
180
|
+
if (data.files === 0) {
|
|
181
|
+
L.push('No signature index found yet — run `sigmap` (or `node gen-context.js`) to generate context, then regenerate this wiki.');
|
|
182
|
+
} else {
|
|
183
|
+
const fileWord = data.files === 1 ? 'indexed file' : 'indexed files';
|
|
184
|
+
const modWord = data.modules.length === 1 ? 'top-level module' : 'top-level modules';
|
|
185
|
+
L.push(`The codebase spans **${data.files} ${fileWord}** across **${data.modules.length} ${modWord}**, with ~${data.totalTokens} tokens of extracted signatures.`);
|
|
186
|
+
if (data.health) {
|
|
187
|
+
L.push(`Context health: **${data.health.score}/100 (${data.health.grade})**.`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
L.push('');
|
|
191
|
+
|
|
192
|
+
if (data.modules.length) {
|
|
193
|
+
L.push('## Modules');
|
|
194
|
+
L.push('| Module | Files | Sig tokens | Key files |');
|
|
195
|
+
L.push('|--------|-------|------------|-----------|');
|
|
196
|
+
for (const m of data.modules) {
|
|
197
|
+
L.push(`| \`${m.name}\` | ${m.files} | ~${m.tokens} | ${m.keyFiles.map((f) => `\`${f}\``).join(', ')} |`);
|
|
198
|
+
}
|
|
199
|
+
const top = data.modules[0];
|
|
200
|
+
L.push('');
|
|
201
|
+
L.push(`The largest module by signature volume is \`${top.name}\` (${top.files} files, ~${top.tokens} tokens) — start there for the core logic.`);
|
|
202
|
+
L.push('');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (data.flow) {
|
|
206
|
+
L.push('## Dependency flow');
|
|
207
|
+
if (data.flow.hubs.length) {
|
|
208
|
+
L.push('The most depended-on files — changes here have the widest blast radius:');
|
|
209
|
+
L.push('');
|
|
210
|
+
L.push('| Hub file | Importers |');
|
|
211
|
+
L.push('|----------|-----------|');
|
|
212
|
+
for (const h of data.flow.hubs) L.push(`| \`${h.file}\` | ${h.importers} |`);
|
|
213
|
+
L.push('');
|
|
214
|
+
}
|
|
215
|
+
if (data.flow.entryPoints.length) {
|
|
216
|
+
L.push('Entry points (imported by nothing, importing the rest):');
|
|
217
|
+
L.push('');
|
|
218
|
+
for (const e of data.flow.entryPoints) L.push(`- \`${e.file}\` → ${e.imports} imports`);
|
|
219
|
+
L.push('');
|
|
220
|
+
}
|
|
221
|
+
L.push(data.flow.cycles
|
|
222
|
+
? `**Dependency cycles:** ${data.flow.cycles} — untangle these first when refactoring.`
|
|
223
|
+
: '**Dependency cycles:** none detected.');
|
|
224
|
+
L.push('');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (data.conventions) {
|
|
228
|
+
L.push('## Conventions');
|
|
229
|
+
const c = data.conventions;
|
|
230
|
+
const bits = [];
|
|
231
|
+
if (c.fileNaming && c.fileNaming.dominant) bits.push(`file naming is predominantly **${c.fileNaming.dominant}** (${c.fileNaming.pct}%, ${c.fileNaming.tier})`);
|
|
232
|
+
if (c.exportStyle && c.exportStyle.dominant) bits.push(`exports use the **${c.exportStyle.dominant}** style (${c.exportStyle.pct}%, ${c.exportStyle.tier})`);
|
|
233
|
+
if (c.testFramework) bits.push(`tests run on **${c.testFramework}**`);
|
|
234
|
+
L.push(bits.length
|
|
235
|
+
? `In this repo, ${bits.join('; ')}. New code should match.`
|
|
236
|
+
: 'No dominant conventions detected (repo too small or styles mixed).');
|
|
237
|
+
L.push('');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
L.push('## Navigating');
|
|
241
|
+
L.push('- `sigmap ask "<question>"` — ranked, budgeted mini-context for any task');
|
|
242
|
+
L.push('- `sigmap --impact <file>` / `--callers <symbol>` — blast radius before you change something');
|
|
243
|
+
L.push('- `sigmap evidence "<query>"` — machine-consumable Evidence Pack (JSON) for agents/CI');
|
|
244
|
+
L.push('- MCP: `get_architecture_overview`, `get_map`, `get_callee_signatures` for live agent access');
|
|
245
|
+
L.push('');
|
|
246
|
+
|
|
247
|
+
return L.join('\n');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = { buildWiki, renderWikiMarkdown };
|