sigmap 8.10.0 → 8.12.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.
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Terse signature encoder (D7).
5
+ *
6
+ * Deterministic compaction of signature lines for the generated context —
7
+ * opt-in via `--terse` / `terse: true`. Every transform is a fixed string
8
+ * rewrite (no heuristics, no LLM), so terse output stays byte-stable.
9
+ *
10
+ * The line anchor (` :start-end`) and everything after it (Python/R doc
11
+ * hints) are preserved byte-exactly: `parseAnchor`, `get_lines`, and the
12
+ * evidence pack keep working on terse output. Symbol extraction is safe too —
13
+ * `extractName` in src/extractors/prdiff.js already recognizes `fn <name>`.
14
+ */
15
+
16
+ /** First ` :start[-end]` anchor token (two-space prefix, as emitted by line-anchor.js). */
17
+ const ANCHOR_RE = /\s{2}:\d+(?:-\d+)?(?=\s|$)/;
18
+
19
+ /**
20
+ * Split a signature into the compactable text and the byte-preserved suffix
21
+ * (anchor + any trailing doc hint).
22
+ * @param {string} sig
23
+ * @returns {{ text: string, suffix: string }}
24
+ */
25
+ function splitAnchor(sig) {
26
+ const s = String(sig);
27
+ const m = ANCHOR_RE.exec(s);
28
+ if (!m) return { text: s, suffix: '' };
29
+ return { text: s.slice(0, m.index), suffix: s.slice(m.index) };
30
+ }
31
+
32
+ /**
33
+ * Compact one signature line. Leading whitespace (member indentation) is kept.
34
+ * @param {string} sig
35
+ * @returns {string}
36
+ */
37
+ function encodeTerseSig(sig) {
38
+ const { text, suffix } = splitAnchor(sig);
39
+ let t = text
40
+ .replace(/\basync function\b/g, 'async fn')
41
+ .replace(/\bfunction\b/g, 'fn')
42
+ .replace(/\s+→\s+/g, '→')
43
+ .replace(/,\s+/g, ',')
44
+ .replace(/\s+=\s+/g, '=')
45
+ .replace(/\{\s+/g, '{')
46
+ .replace(/\s+\}/g, '}')
47
+ .replace(/(\S)\s{2,}(?=\S)/g, '$1 ')
48
+ .replace(/^module\.exports=/, 'exports=');
49
+ return t + suffix;
50
+ }
51
+
52
+ /**
53
+ * Compact an array of signature lines.
54
+ * @param {string[]} sigs
55
+ * @returns {string[]}
56
+ */
57
+ function encodeTerseSigs(sigs) {
58
+ return (sigs || []).map(encodeTerseSig);
59
+ }
60
+
61
+ /** Estimated tokens of joined signature lines (same chars/4 rule as elsewhere). */
62
+ function _tokens(sigs) {
63
+ return Math.ceil(sigs.join('\n').length / 4);
64
+ }
65
+
66
+ /**
67
+ * Measure the real reduction terse encoding buys over a set of signature
68
+ * lists — the D7 "measure first" gate. Never quote a number this didn't produce.
69
+ * @param {string[][]} sigsList one string[] per file
70
+ * @returns {{ beforeTokens: number, afterTokens: number, reductionPct: number }}
71
+ */
72
+ function measureTerse(sigsList) {
73
+ let beforeTokens = 0;
74
+ let afterTokens = 0;
75
+ for (const sigs of sigsList || []) {
76
+ if (!sigs || !sigs.length) continue;
77
+ beforeTokens += _tokens(sigs);
78
+ afterTokens += _tokens(encodeTerseSigs(sigs));
79
+ }
80
+ const reductionPct = beforeTokens > 0
81
+ ? Math.round(((beforeTokens - afterTokens) / beforeTokens) * 1000) / 10
82
+ : 0;
83
+ return { beforeTokens, afterTokens, reductionPct };
84
+ }
85
+
86
+ module.exports = { encodeTerseSig, encodeTerseSigs, measureTerse, splitAnchor };
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '8.10.0',
21
+ version: '8.12.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -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 };