sigmap 7.10.0 → 7.11.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/CHANGELOG.md +9 -0
- package/gen-context.js +476 -295
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/mcp/server.js +1 -1
- package/src/plan/verify-plan.js +108 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,15 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [7.11.0] — 2026-06-17
|
|
14
|
+
|
|
15
|
+
Minor release — `sigmap verify-plan` (grounded codegen, Gap 2).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`sigmap verify-plan <plan.md>` — check a plan against the live index (#310):** the first piece of the `sigmap create` pipeline (Gap 2, step 2). Before an agent executes a plan, `verify-plan` validates it against the live index — referenced files and symbols exist, blast radius is acceptable, scope is in bounds — catching Cause 1+2 at plan time (cheaper than after the code is written). New zero-dependency, bundle-safe `src/plan/verify-plan.js` (`verifyPlan`): flags missing files and unknown symbols (with closest-match suggestions), computes per-file blast radius via the impact graph (flags high-blast-radius files), and flags broad scope. Plan input schema is **markdown** (consistent with `verify-ai-output`). CLI reads a file or stdin (`-`), supports `--json`, and exits 1 on blocking errors. The `sigmap create` orchestration and `review-pr` remain follow-ups.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
13
22
|
## [7.10.0] — 2026-06-17
|
|
14
23
|
|
|
15
24
|
Minor release — `sigmap scaffold` with a confidence floor (grounded codegen, Layer 4).
|
package/gen-context.js
CHANGED
|
@@ -25,222 +25,135 @@ function __require(key) {
|
|
|
25
25
|
// ── ./src/conventions/conflicts ──
|
|
26
26
|
// ── ./src/conventions/inject ──
|
|
27
27
|
// ── ./src/scaffold/propose ──
|
|
28
|
-
|
|
28
|
+
// ── ./src/plan/verify-plan ──
|
|
29
|
+
// ── ./src/cache/freshen ──
|
|
30
|
+
__factories["./src/cache/freshen"] = function(module, exports) {
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
33
|
+
* Read-time self-heal (IMPL.md Layer 1, "safety net" tier).
|
|
32
34
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* Keeps the sig-cache in line with the current source tree so the index reflects
|
|
36
|
+
* on-disk reality even when no write hook was called. Re-extracts files modified
|
|
37
|
+
* since the context file was generated (bounded to actual session edits, not the
|
|
38
|
+
* whole tree), drops cache entries for deleted files, and persists. buildSigIndex
|
|
39
|
+
* already merges the cache, so the next read is fresh.
|
|
40
|
+
*
|
|
41
|
+
* Throttled per cwd. Skips entirely when there is no generated index to heal
|
|
42
|
+
* (a cold repo should run `generate` or use the notify hooks).
|
|
43
|
+
*
|
|
44
|
+
* Zero-dependency, bundle-safe (fs + dispatch + sig-cache).
|
|
39
45
|
*/
|
|
40
46
|
|
|
41
|
-
const
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const path = require('path');
|
|
49
|
+
const { loadCache, saveCache, getChangedFiles } = __require('./src/cache/sig-cache');
|
|
50
|
+
const { extractFile, langFor } = __require('./src/extractors/dispatch');
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
52
|
+
const DEFAULT_SRC_DIRS = ['src', 'app', 'lib', 'packages', 'services', 'api'];
|
|
53
|
+
const DEFAULT_EXCLUDE = [
|
|
54
|
+
'node_modules', '.git', 'dist', 'build', 'out', '__pycache__',
|
|
55
|
+
'.next', 'coverage', 'target', 'vendor', '.context',
|
|
56
|
+
];
|
|
57
|
+
const CONTEXT_PATHS = [
|
|
58
|
+
['.github', 'copilot-instructions.md'],
|
|
59
|
+
['CLAUDE.md'], ['AGENTS.md'], ['.github', 'context-cold.md'],
|
|
60
|
+
];
|
|
61
|
+
const THROTTLE_MS = 1500;
|
|
62
|
+
const _lastRun = new Map();
|
|
46
63
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return
|
|
64
|
+
function _readConfig(cwd) {
|
|
65
|
+
try {
|
|
66
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
|
|
67
|
+
return cfg && typeof cfg === 'object' ? cfg : {};
|
|
68
|
+
} catch (_) { return {}; }
|
|
52
69
|
}
|
|
53
70
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const slash = s.lastIndexOf('/');
|
|
58
|
-
const base = slash >= 0 ? s.slice(slash + 1) : s;
|
|
59
|
-
const dot = base.indexOf('.');
|
|
60
|
-
return dot > 0 ? base.slice(0, dot) : base;
|
|
71
|
+
function _pkgVersion(cwd) {
|
|
72
|
+
try { return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).version || '0.0.0'; }
|
|
73
|
+
catch (_) { return '0.0.0'; }
|
|
61
74
|
}
|
|
62
75
|
|
|
63
|
-
/**
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
/** Newest mtime among existing generated context files, or 0 if none. */
|
|
77
|
+
function _contextMtime(cwd) {
|
|
78
|
+
let newest = 0;
|
|
79
|
+
for (const parts of CONTEXT_PATHS) {
|
|
80
|
+
try { newest = Math.max(newest, fs.statSync(path.join(cwd, ...parts)).mtimeMs); } catch (_) {}
|
|
67
81
|
}
|
|
68
|
-
return
|
|
82
|
+
return newest;
|
|
69
83
|
}
|
|
70
84
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
* @returns {{ ok:boolean, refused:boolean, name:string, tier:string,
|
|
81
|
-
* confidence:number, threshold:number, hardFloor:number, forced:boolean,
|
|
82
|
-
* warning:string|null, reason:string, proposal:object|null, conflicts:object }}
|
|
83
|
-
*/
|
|
84
|
-
function proposeScaffold(name, conventions, opts = {}) {
|
|
85
|
-
const threshold = Math.max(HARD_FLOOR, opts.threshold != null ? opts.threshold : DEFAULT_THRESHOLD);
|
|
86
|
-
const force = !!opts.force;
|
|
87
|
-
const ext = opts.ext || 'js';
|
|
88
|
-
const fileNaming = (conventions && conventions.fileNaming) || { dominant: null, dominantPct: 0, total: 0 };
|
|
89
|
-
const exportStyle = (conventions && conventions.exportStyle) || { dominant: null };
|
|
90
|
-
const confidence = fileNaming.dominantPct || 0;
|
|
91
|
-
const tier = fileNaming.total > 0 ? _tier(confidence) : 'unknown';
|
|
92
|
-
const conflicts = analyzeConflicts(conventions || {});
|
|
93
|
-
|
|
94
|
-
const base = {
|
|
95
|
-
ok: false, refused: true, name: String(name || ''), tier, confidence,
|
|
96
|
-
threshold, hardFloor: HARD_FLOOR, forced: false, warning: null,
|
|
97
|
-
reason: '', proposal: null, conflicts,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
if (!fileNaming.dominant || fileNaming.total === 0) {
|
|
101
|
-
return { ...base, reason: 'no file-naming convention detected — cannot propose a name' };
|
|
102
|
-
}
|
|
103
|
-
if (confidence < HARD_FLOOR) {
|
|
104
|
-
return {
|
|
105
|
-
...base,
|
|
106
|
-
reason: `file-naming consistency ${(confidence * 100).toFixed(0)}% is below the hard floor ${(HARD_FLOOR * 100).toFixed(0)}% — refusing (not overridable)`,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
if (confidence < threshold && !force) {
|
|
110
|
-
return {
|
|
111
|
-
...base,
|
|
112
|
-
reason: `file-naming consistency ${(confidence * 100).toFixed(0)}% is below the threshold ${(threshold * 100).toFixed(0)}% — refusing (use --force to override above the ${(HARD_FLOOR * 100).toFixed(0)}% floor)`,
|
|
113
|
-
};
|
|
85
|
+
function _walk(dir, exclude, out, depth, maxDepth) {
|
|
86
|
+
if (depth > maxDepth) return;
|
|
87
|
+
let entries;
|
|
88
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
89
|
+
for (const e of entries) {
|
|
90
|
+
if (exclude.has(e.name)) continue;
|
|
91
|
+
const full = path.join(dir, e.name);
|
|
92
|
+
if (e.isDirectory()) _walk(full, exclude, out, depth + 1, maxDepth);
|
|
93
|
+
else if (e.isFile() && langFor(e.name)) out.push(full);
|
|
114
94
|
}
|
|
115
|
-
|
|
116
|
-
const styledStem = toNamingStyle(_stem(name), fileNaming.dominant);
|
|
117
|
-
const forced = confidence < threshold && force;
|
|
118
|
-
const proposal = {
|
|
119
|
-
filename: `${styledStem}.${ext}`,
|
|
120
|
-
namingStyle: fileNaming.dominant,
|
|
121
|
-
exportStyle: exportStyle.dominant || 'named',
|
|
122
|
-
testFile: _testFile(styledStem, conventions.testFramework, ext),
|
|
123
|
-
testFramework: conventions.testFramework || null,
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
...base,
|
|
128
|
-
ok: true,
|
|
129
|
-
refused: false,
|
|
130
|
-
forced,
|
|
131
|
-
warning: forced
|
|
132
|
-
? `proposed below the ${(threshold * 100).toFixed(0)}% threshold (--force); conventions are only ${tier}`
|
|
133
|
-
: null,
|
|
134
|
-
reason: forced ? 'forced proposal above the hard floor' : `conventions are ${tier} — proposing`,
|
|
135
|
-
proposal,
|
|
136
|
-
};
|
|
137
95
|
}
|
|
138
96
|
|
|
139
|
-
module.exports = { proposeScaffold, DEFAULT_THRESHOLD, HARD_FLOOR };
|
|
140
|
-
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
__factories["./src/conventions/inject"] = function(module, exports) {
|
|
144
|
-
|
|
145
97
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* reading the file plans grounded in the repo's house style. Idempotent and
|
|
151
|
-
* marker-scoped — it never touches human content or the `## Auto-generated
|
|
152
|
-
* signatures` block. Pure string transforms; zero-dependency, bundle-safe.
|
|
98
|
+
* Re-extract source files changed since the last generate; drop deleted files.
|
|
99
|
+
* @param {string} cwd
|
|
100
|
+
* @param {{force?:boolean, now?:number}} [opts]
|
|
101
|
+
* @returns {number} cache entries touched
|
|
153
102
|
*/
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const TIER_NOTE = {
|
|
159
|
-
consistent: 'consistent — match it',
|
|
160
|
-
mostly: 'dominant, with some drift',
|
|
161
|
-
inconsistent: 'no clear convention — check neighboring files',
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const NAMES = {
|
|
165
|
-
fileNaming: 'File naming',
|
|
166
|
-
exportStyle: 'Export style',
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const _pct = (n) => `${Math.round(n * 100)}%`;
|
|
170
|
-
|
|
171
|
-
function _conventionLine(label, conv) {
|
|
172
|
-
if (!conv || conv.total === 0 || !conv.dominant) return null;
|
|
173
|
-
const note = TIER_NOTE[conv.tier] || conv.tier;
|
|
174
|
-
let line = `- **${label}:** ${conv.dominant} (${_pct(conv.dominantPct)} — ${note}).`;
|
|
175
|
-
const others = conv.variants.slice(1).filter((v) => v.pct > 0);
|
|
176
|
-
if (others.length) {
|
|
177
|
-
line += ` Variants: ${others.map((v) => `${v.label} ${_pct(v.pct)}`).join(', ')}.`;
|
|
103
|
+
function freshen(cwd, opts = {}) {
|
|
104
|
+
const now = opts.now != null ? opts.now : Date.now();
|
|
105
|
+
if (!opts.force) {
|
|
106
|
+
if (now - (_lastRun.get(cwd) || 0) < THROTTLE_MS) return 0;
|
|
178
107
|
}
|
|
179
|
-
|
|
180
|
-
}
|
|
108
|
+
_lastRun.set(cwd, now);
|
|
181
109
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
function renderConventionsBlock(result, version) {
|
|
189
|
-
const lines = [];
|
|
190
|
-
for (const key of ['fileNaming', 'exportStyle']) {
|
|
191
|
-
const l = _conventionLine(NAMES[key], result && result[key]);
|
|
192
|
-
if (l) lines.push(l);
|
|
193
|
-
}
|
|
194
|
-
if (result && result.testFramework) {
|
|
195
|
-
lines.push(`- **Test framework:** ${result.testFramework}.`);
|
|
196
|
-
}
|
|
110
|
+
try {
|
|
111
|
+
const version = _pkgVersion(cwd);
|
|
112
|
+
const cache = loadCache(cwd, version);
|
|
113
|
+
const ctxMtime = _contextMtime(cwd);
|
|
114
|
+
// Nothing to heal: no generated context AND no live cache overlay.
|
|
115
|
+
if (ctxMtime === 0 && cache.size === 0) return 0;
|
|
197
116
|
|
|
198
|
-
|
|
199
|
-
?
|
|
200
|
-
|
|
117
|
+
const cfg = _readConfig(cwd);
|
|
118
|
+
const srcDirs = Array.isArray(cfg.srcDirs) && cfg.srcDirs.length ? cfg.srcDirs : DEFAULT_SRC_DIRS;
|
|
119
|
+
const exclude = new Set([...DEFAULT_EXCLUDE, ...(Array.isArray(cfg.exclude) ? cfg.exclude : [])]);
|
|
120
|
+
const maxDepth = Number.isFinite(cfg.maxDepth) ? cfg.maxDepth : 8;
|
|
201
121
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
'Match these when writing or editing code (TS/JS/Python):',
|
|
208
|
-
'',
|
|
209
|
-
...body,
|
|
210
|
-
'',
|
|
211
|
-
`<sub>Generated by SigMap${ver} · run \`sigmap conventions --inject\` to refresh.</sub>`,
|
|
212
|
-
END,
|
|
213
|
-
].join('\n');
|
|
214
|
-
}
|
|
122
|
+
const files = [];
|
|
123
|
+
for (const d of srcDirs) {
|
|
124
|
+
const abs = path.isAbsolute(d) ? d : path.join(cwd, d);
|
|
125
|
+
if (fs.existsSync(abs)) _walk(abs, exclude, files, 0, maxDepth);
|
|
126
|
+
}
|
|
215
127
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (endIdx !== -1) {
|
|
230
|
-
const before = src.slice(0, startIdx);
|
|
231
|
-
const after = src.slice(endIdx + END.length);
|
|
232
|
-
return before + block + after;
|
|
128
|
+
// Candidates = files modified since the context was generated, or not yet cached.
|
|
129
|
+
const candidates = files.filter((f) => {
|
|
130
|
+
try { return fs.statSync(f).mtimeMs > ctxMtime || !cache.has(f); } catch (_) { return false; }
|
|
131
|
+
});
|
|
132
|
+
const { changed } = getChangedFiles(candidates, cache);
|
|
133
|
+
|
|
134
|
+
let touched = 0;
|
|
135
|
+
for (const f of changed) {
|
|
136
|
+
try {
|
|
137
|
+
const sigs = extractFile(f, fs.readFileSync(f, 'utf8'));
|
|
138
|
+
cache.set(f, { mtime: fs.statSync(f).mtimeMs, sigs });
|
|
139
|
+
touched++;
|
|
140
|
+
} catch (_) {}
|
|
233
141
|
}
|
|
142
|
+
// Note: deletions are NOT swept here — a cache entry may be a `notify`
|
|
143
|
+
// overlay for a file not yet on disk. Explicit removal is `notify_file_deleted`.
|
|
144
|
+
|
|
145
|
+
if (touched > 0) saveCache(cwd, version, cache);
|
|
146
|
+
return touched;
|
|
147
|
+
} catch (_) {
|
|
148
|
+
return 0;
|
|
234
149
|
}
|
|
235
|
-
if (src.trim() === '') return block + '\n';
|
|
236
|
-
const sep = src.endsWith('\n') ? '\n' : '\n\n';
|
|
237
|
-
return src + sep + block + '\n';
|
|
238
150
|
}
|
|
239
151
|
|
|
240
|
-
module.exports = {
|
|
152
|
+
module.exports = { freshen };
|
|
241
153
|
|
|
242
154
|
};
|
|
243
155
|
|
|
156
|
+
// ── ./src/conventions/conflicts ──
|
|
244
157
|
__factories["./src/conventions/conflicts"] = function(module, exports) {
|
|
245
158
|
|
|
246
159
|
/**
|
|
@@ -355,6 +268,7 @@ __factories["./src/conventions/conflicts"] = function(module, exports) {
|
|
|
355
268
|
|
|
356
269
|
};
|
|
357
270
|
|
|
271
|
+
// ── ./src/conventions/extract ──
|
|
358
272
|
__factories["./src/conventions/extract"] = function(module, exports) {
|
|
359
273
|
|
|
360
274
|
/**
|
|
@@ -399,24 +313,42 @@ __factories["./src/conventions/extract"] = function(module, exports) {
|
|
|
399
313
|
return 'other';
|
|
400
314
|
}
|
|
401
315
|
|
|
316
|
+
const MAX_EXAMPLES = 3;
|
|
317
|
+
|
|
402
318
|
/**
|
|
403
319
|
* Score a set of categorical observations into a dominant convention plus its
|
|
404
320
|
* consistency tier. The reusable primitive (IMPL.md §5.2).
|
|
405
321
|
* @param {string[]} labels observed category for each sample (e.g. naming styles)
|
|
322
|
+
* @param {string[]} [refs] optional identifier (e.g. file name) parallel to
|
|
323
|
+
* `labels`; when given, each variant carries up to 3 `examples`.
|
|
406
324
|
* @returns {{ dominant: string|null, dominantPct: number, total: number,
|
|
407
|
-
* variants: Array<{label:string, count:number, pct:number}>,
|
|
325
|
+
* variants: Array<{label:string, count:number, pct:number, examples?:string[]}>,
|
|
408
326
|
* tier: 'consistent'|'mostly'|'inconsistent'|'unknown' }}
|
|
409
327
|
*/
|
|
410
|
-
function scoreConvention(labels) {
|
|
411
|
-
const
|
|
412
|
-
const
|
|
328
|
+
function scoreConvention(labels, refs) {
|
|
329
|
+
const all = labels || [];
|
|
330
|
+
const counts = new Map();
|
|
331
|
+
const examples = new Map();
|
|
332
|
+
let total = 0;
|
|
333
|
+
for (let i = 0; i < all.length; i++) {
|
|
334
|
+
const l = all[i];
|
|
335
|
+
if (l == null || l === 'other') continue;
|
|
336
|
+
total++;
|
|
337
|
+
counts.set(l, (counts.get(l) || 0) + 1);
|
|
338
|
+
if (refs && refs[i] != null) {
|
|
339
|
+
const ex = examples.get(l) || [];
|
|
340
|
+
if (ex.length < MAX_EXAMPLES) { ex.push(refs[i]); examples.set(l, ex); }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
413
343
|
if (total === 0) {
|
|
414
344
|
return { dominant: null, dominantPct: 0, total: 0, variants: [], tier: 'unknown' };
|
|
415
345
|
}
|
|
416
|
-
const counts = new Map();
|
|
417
|
-
for (const l of list) counts.set(l, (counts.get(l) || 0) + 1);
|
|
418
346
|
const variants = [...counts.entries()]
|
|
419
|
-
.map(([label, count]) =>
|
|
347
|
+
.map(([label, count]) => {
|
|
348
|
+
const v = { label, count, pct: count / total };
|
|
349
|
+
if (refs) v.examples = examples.get(label) || [];
|
|
350
|
+
return v;
|
|
351
|
+
})
|
|
420
352
|
.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
421
353
|
const top = variants[0];
|
|
422
354
|
let tier = 'inconsistent';
|
|
@@ -488,157 +420,365 @@ __factories["./src/conventions/extract"] = function(module, exports) {
|
|
|
488
420
|
function extractConventions(cwd, files) {
|
|
489
421
|
const scoped = (files || []).filter((f) => SCOPED_EXTS.has(path.extname(f).toLowerCase()));
|
|
490
422
|
const namingLabels = [];
|
|
423
|
+
const namingRefs = [];
|
|
491
424
|
const exportLabels = [];
|
|
425
|
+
const exportRefs = [];
|
|
492
426
|
for (const f of scoped) {
|
|
493
427
|
const base = path.basename(f);
|
|
494
428
|
// Skip test files for the naming convention (they have their own naming).
|
|
495
429
|
if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) {
|
|
496
430
|
namingLabels.push(classifyNaming(base));
|
|
431
|
+
namingRefs.push(base);
|
|
497
432
|
}
|
|
498
433
|
if (JS_TS_EXTS.has(path.extname(f).toLowerCase())) {
|
|
499
434
|
let src = '';
|
|
500
435
|
try { src = fs.readFileSync(f, 'utf8'); } catch (_) {}
|
|
501
436
|
exportLabels.push(_jsExportStyle(src));
|
|
437
|
+
exportRefs.push(base);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
fileNaming: scoreConvention(namingLabels, namingRefs),
|
|
442
|
+
exportStyle: scoreConvention(exportLabels, exportRefs),
|
|
443
|
+
testFramework: _detectTestFramework(cwd, scoped),
|
|
444
|
+
scope: ['typescript', 'javascript', 'python'],
|
|
445
|
+
scannedFiles: scoped.length,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
module.exports = { classifyNaming, scoreConvention, extractConventions };
|
|
450
|
+
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// ── ./src/conventions/inject ──
|
|
454
|
+
__factories["./src/conventions/inject"] = function(module, exports) {
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* CLAUDE.md convention injection (IMPL.md §7 Phase 2 / §8 step 5).
|
|
458
|
+
*
|
|
459
|
+
* Renders the conventions detected by `extractConventions` into a
|
|
460
|
+
* marker-delimited markdown block and injects it into CLAUDE.md so an agent
|
|
461
|
+
* reading the file plans grounded in the repo's house style. Idempotent and
|
|
462
|
+
* marker-scoped — it never touches human content or the `## Auto-generated
|
|
463
|
+
* signatures` block. Pure string transforms; zero-dependency, bundle-safe.
|
|
464
|
+
*/
|
|
465
|
+
|
|
466
|
+
const START = '<!-- sigmap-conventions:start -->';
|
|
467
|
+
const END = '<!-- sigmap-conventions:end -->';
|
|
468
|
+
|
|
469
|
+
const TIER_NOTE = {
|
|
470
|
+
consistent: 'consistent — match it',
|
|
471
|
+
mostly: 'dominant, with some drift',
|
|
472
|
+
inconsistent: 'no clear convention — check neighboring files',
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const NAMES = {
|
|
476
|
+
fileNaming: 'File naming',
|
|
477
|
+
exportStyle: 'Export style',
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const _pct = (n) => `${Math.round(n * 100)}%`;
|
|
481
|
+
|
|
482
|
+
function _conventionLine(label, conv) {
|
|
483
|
+
if (!conv || conv.total === 0 || !conv.dominant) return null;
|
|
484
|
+
const note = TIER_NOTE[conv.tier] || conv.tier;
|
|
485
|
+
let line = `- **${label}:** ${conv.dominant} (${_pct(conv.dominantPct)} — ${note}).`;
|
|
486
|
+
const others = conv.variants.slice(1).filter((v) => v.pct > 0);
|
|
487
|
+
if (others.length) {
|
|
488
|
+
line += ` Variants: ${others.map((v) => `${v.label} ${_pct(v.pct)}`).join(', ')}.`;
|
|
489
|
+
}
|
|
490
|
+
return line;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Render the conventions block (including its start/end markers).
|
|
495
|
+
* @param {object} result an `extractConventions` result
|
|
496
|
+
* @param {string} [version] SigMap version for the footer
|
|
497
|
+
* @returns {string}
|
|
498
|
+
*/
|
|
499
|
+
function renderConventionsBlock(result, version) {
|
|
500
|
+
const lines = [];
|
|
501
|
+
for (const key of ['fileNaming', 'exportStyle']) {
|
|
502
|
+
const l = _conventionLine(NAMES[key], result && result[key]);
|
|
503
|
+
if (l) lines.push(l);
|
|
504
|
+
}
|
|
505
|
+
if (result && result.testFramework) {
|
|
506
|
+
lines.push(`- **Test framework:** ${result.testFramework}.`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const body = lines.length
|
|
510
|
+
? lines
|
|
511
|
+
: ['- No conventions detected yet (run `sigmap conventions` on a TS/JS/Python repo).'];
|
|
512
|
+
|
|
513
|
+
const ver = version ? ` v${version}` : '';
|
|
514
|
+
return [
|
|
515
|
+
START,
|
|
516
|
+
'## Conventions (auto-detected by SigMap)',
|
|
517
|
+
'',
|
|
518
|
+
'Match these when writing or editing code (TS/JS/Python):',
|
|
519
|
+
'',
|
|
520
|
+
...body,
|
|
521
|
+
'',
|
|
522
|
+
`<sub>Generated by SigMap${ver} · run \`sigmap conventions --inject\` to refresh.</sub>`,
|
|
523
|
+
END,
|
|
524
|
+
].join('\n');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Inject (or replace) the conventions block in existing CLAUDE.md content.
|
|
529
|
+
* Replaces an existing marked block in place; appends one when absent.
|
|
530
|
+
* Idempotent — never touches content outside the markers.
|
|
531
|
+
* @param {string} existing current file content ('' if the file is new)
|
|
532
|
+
* @param {string} block the block returned by `renderConventionsBlock`
|
|
533
|
+
* @returns {string}
|
|
534
|
+
*/
|
|
535
|
+
function injectConventions(existing, block) {
|
|
536
|
+
const src = String(existing || '');
|
|
537
|
+
const startIdx = src.indexOf(START);
|
|
538
|
+
if (startIdx !== -1) {
|
|
539
|
+
const endIdx = src.indexOf(END, startIdx);
|
|
540
|
+
if (endIdx !== -1) {
|
|
541
|
+
const before = src.slice(0, startIdx);
|
|
542
|
+
const after = src.slice(endIdx + END.length);
|
|
543
|
+
return before + block + after;
|
|
502
544
|
}
|
|
503
545
|
}
|
|
504
|
-
return
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
testFramework: _detectTestFramework(cwd, scoped),
|
|
508
|
-
scope: ['typescript', 'javascript', 'python'],
|
|
509
|
-
scannedFiles: scoped.length,
|
|
510
|
-
};
|
|
546
|
+
if (src.trim() === '') return block + '\n';
|
|
547
|
+
const sep = src.endsWith('\n') ? '\n' : '\n\n';
|
|
548
|
+
return src + sep + block + '\n';
|
|
511
549
|
}
|
|
512
550
|
|
|
513
|
-
module.exports = {
|
|
551
|
+
module.exports = { renderConventionsBlock, injectConventions, START, END };
|
|
514
552
|
|
|
515
553
|
};
|
|
516
554
|
|
|
517
|
-
|
|
555
|
+
// ── ./src/scaffold/propose ──
|
|
556
|
+
__factories["./src/scaffold/propose"] = function(module, exports) {
|
|
518
557
|
|
|
519
558
|
/**
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
* Keeps the sig-cache in line with the current source tree so the index reflects
|
|
523
|
-
* on-disk reality even when no write hook was called. Re-extracts files modified
|
|
524
|
-
* since the context file was generated (bounded to actual session edits, not the
|
|
525
|
-
* whole tree), drops cache entries for deleted files, and persists. buildSigIndex
|
|
526
|
-
* already merges the cache, so the next read is fresh.
|
|
527
|
-
*
|
|
528
|
-
* Throttled per cwd. Skips entirely when there is no generated index to heal
|
|
529
|
-
* (a cold repo should run `generate` or use the notify hooks).
|
|
559
|
+
* Scaffold proposal with a confidence floor (IMPL.md §5 — Cause 3).
|
|
530
560
|
*
|
|
531
|
-
*
|
|
561
|
+
* Proposes a convention-matched structure for a new module — filename in the
|
|
562
|
+
* repo's dominant naming style, the export style to use, and a matching test
|
|
563
|
+
* file — but only when the conventions are consistent enough. Below a hard
|
|
564
|
+
* floor it refuses and surfaces the conflict, because a wrong proposal
|
|
565
|
+
* systematizes bad code. Pure, zero-dependency, bundle-safe; reuses the
|
|
566
|
+
* conventions primitives.
|
|
532
567
|
*/
|
|
533
568
|
|
|
534
|
-
const
|
|
535
|
-
const path = require('path');
|
|
536
|
-
const { loadCache, saveCache, getChangedFiles } = __require('./src/cache/sig-cache');
|
|
537
|
-
const { extractFile, langFor } = __require('./src/extractors/dispatch');
|
|
569
|
+
const { toNamingStyle, analyzeConflicts } = __require('./src/conventions/conflicts');
|
|
538
570
|
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
'.next', 'coverage', 'target', 'vendor', '.context',
|
|
543
|
-
];
|
|
544
|
-
const CONTEXT_PATHS = [
|
|
545
|
-
['.github', 'copilot-instructions.md'],
|
|
546
|
-
['CLAUDE.md'], ['AGENTS.md'], ['.github', 'context-cold.md'],
|
|
547
|
-
];
|
|
548
|
-
const THROTTLE_MS = 1500;
|
|
549
|
-
const _lastRun = new Map();
|
|
571
|
+
// Soft threshold is configurable; the hard floor is not (IMPL.md §5.1).
|
|
572
|
+
const DEFAULT_THRESHOLD = 0.7;
|
|
573
|
+
const HARD_FLOOR = 0.5;
|
|
550
574
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
575
|
+
/** Tier for a consistency score (matches the conventions tiers). */
|
|
576
|
+
function _tier(pct) {
|
|
577
|
+
if (pct >= 0.9) return 'consistent';
|
|
578
|
+
if (pct >= 0.7) return 'mostly';
|
|
579
|
+
return 'inconsistent';
|
|
556
580
|
}
|
|
557
581
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
582
|
+
/** Strip any extension/compound suffix from a requested name → bare stem. */
|
|
583
|
+
function _stem(name) {
|
|
584
|
+
const s = String(name || '').trim();
|
|
585
|
+
const slash = s.lastIndexOf('/');
|
|
586
|
+
const base = slash >= 0 ? s.slice(slash + 1) : s;
|
|
587
|
+
const dot = base.indexOf('.');
|
|
588
|
+
return dot > 0 ? base.slice(0, dot) : base;
|
|
561
589
|
}
|
|
562
590
|
|
|
563
|
-
/**
|
|
564
|
-
function
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
try { newest = Math.max(newest, fs.statSync(path.join(cwd, ...parts)).mtimeMs); } catch (_) {}
|
|
591
|
+
/** Test file path for a styled stem given the detected framework + ext. */
|
|
592
|
+
function _testFile(styledStem, framework, ext) {
|
|
593
|
+
if (framework === 'pytest' || framework === 'unittest') {
|
|
594
|
+
return `test_${toNamingStyle(styledStem, 'snake_case')}.py`;
|
|
568
595
|
}
|
|
569
|
-
return
|
|
596
|
+
return `${styledStem}.test.${ext}`;
|
|
570
597
|
}
|
|
571
598
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
599
|
+
/**
|
|
600
|
+
* Propose a convention-matched scaffold, gated by a confidence floor.
|
|
601
|
+
* @param {string} name desired module name (any casing; extension ignored)
|
|
602
|
+
* @param {object} conventions an `extractConventions` result
|
|
603
|
+
* @param {object} [opts]
|
|
604
|
+
* @param {number} [opts.threshold=0.7] soft threshold (clamped to ≥ hard floor)
|
|
605
|
+
* @param {boolean} [opts.force=false] allow proposing below the soft threshold
|
|
606
|
+
* (never below the hard floor)
|
|
607
|
+
* @param {string} [opts.ext='js'] file extension for the proposed files
|
|
608
|
+
* @returns {{ ok:boolean, refused:boolean, name:string, tier:string,
|
|
609
|
+
* confidence:number, threshold:number, hardFloor:number, forced:boolean,
|
|
610
|
+
* warning:string|null, reason:string, proposal:object|null, conflicts:object }}
|
|
611
|
+
*/
|
|
612
|
+
function proposeScaffold(name, conventions, opts = {}) {
|
|
613
|
+
const threshold = Math.max(HARD_FLOOR, opts.threshold != null ? opts.threshold : DEFAULT_THRESHOLD);
|
|
614
|
+
const force = !!opts.force;
|
|
615
|
+
const ext = opts.ext || 'js';
|
|
616
|
+
const fileNaming = (conventions && conventions.fileNaming) || { dominant: null, dominantPct: 0, total: 0 };
|
|
617
|
+
const exportStyle = (conventions && conventions.exportStyle) || { dominant: null };
|
|
618
|
+
const confidence = fileNaming.dominantPct || 0;
|
|
619
|
+
const tier = fileNaming.total > 0 ? _tier(confidence) : 'unknown';
|
|
620
|
+
const conflicts = analyzeConflicts(conventions || {});
|
|
621
|
+
|
|
622
|
+
const base = {
|
|
623
|
+
ok: false, refused: true, name: String(name || ''), tier, confidence,
|
|
624
|
+
threshold, hardFloor: HARD_FLOOR, forced: false, warning: null,
|
|
625
|
+
reason: '', proposal: null, conflicts,
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
if (!fileNaming.dominant || fileNaming.total === 0) {
|
|
629
|
+
return { ...base, reason: 'no file-naming convention detected — cannot propose a name' };
|
|
630
|
+
}
|
|
631
|
+
if (confidence < HARD_FLOOR) {
|
|
632
|
+
return {
|
|
633
|
+
...base,
|
|
634
|
+
reason: `file-naming consistency ${(confidence * 100).toFixed(0)}% is below the hard floor ${(HARD_FLOOR * 100).toFixed(0)}% — refusing (not overridable)`,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (confidence < threshold && !force) {
|
|
638
|
+
return {
|
|
639
|
+
...base,
|
|
640
|
+
reason: `file-naming consistency ${(confidence * 100).toFixed(0)}% is below the threshold ${(threshold * 100).toFixed(0)}% — refusing (use --force to override above the ${(HARD_FLOOR * 100).toFixed(0)}% floor)`,
|
|
641
|
+
};
|
|
581
642
|
}
|
|
643
|
+
|
|
644
|
+
const styledStem = toNamingStyle(_stem(name), fileNaming.dominant);
|
|
645
|
+
const forced = confidence < threshold && force;
|
|
646
|
+
const proposal = {
|
|
647
|
+
filename: `${styledStem}.${ext}`,
|
|
648
|
+
namingStyle: fileNaming.dominant,
|
|
649
|
+
exportStyle: exportStyle.dominant || 'named',
|
|
650
|
+
testFile: _testFile(styledStem, conventions.testFramework, ext),
|
|
651
|
+
testFramework: conventions.testFramework || null,
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
...base,
|
|
656
|
+
ok: true,
|
|
657
|
+
refused: false,
|
|
658
|
+
forced,
|
|
659
|
+
warning: forced
|
|
660
|
+
? `proposed below the ${(threshold * 100).toFixed(0)}% threshold (--force); conventions are only ${tier}`
|
|
661
|
+
: null,
|
|
662
|
+
reason: forced ? 'forced proposal above the hard floor' : `conventions are ${tier} — proposing`,
|
|
663
|
+
proposal,
|
|
664
|
+
};
|
|
582
665
|
}
|
|
583
666
|
|
|
667
|
+
module.exports = { proposeScaffold, DEFAULT_THRESHOLD, HARD_FLOOR };
|
|
668
|
+
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
__factories["./src/plan/verify-plan"] = function(module, exports) {
|
|
672
|
+
|
|
584
673
|
/**
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
674
|
+
* verify-plan (IMPL.md §6.1 — Gap 2, step 2 of the `create` pipeline).
|
|
675
|
+
*
|
|
676
|
+
* Checks a plan (markdown) against the LIVE index before execution: do the
|
|
677
|
+
* referenced files and symbols exist, is the blast radius acceptable, is the
|
|
678
|
+
* scope in bounds? Catches Cause 1+2 at plan time — cheaper than after the
|
|
679
|
+
* code is written. Reuses the verify primitives + the impact graph.
|
|
680
|
+
* Zero-dependency, bundle-safe.
|
|
589
681
|
*/
|
|
590
|
-
function freshen(cwd, opts = {}) {
|
|
591
|
-
const now = opts.now != null ? opts.now : Date.now();
|
|
592
|
-
if (!opts.force) {
|
|
593
|
-
if (now - (_lastRun.get(cwd) || 0) < THROTTLE_MS) return 0;
|
|
594
|
-
}
|
|
595
|
-
_lastRun.set(cwd, now);
|
|
596
682
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
683
|
+
const fs = require('fs');
|
|
684
|
+
const path = require('path');
|
|
685
|
+
const { extractFilePaths, extractSymbols } = __require('./src/verify/parsers');
|
|
686
|
+
const { buildSymbolSet } = __require('./src/verify/hallucination-guard');
|
|
687
|
+
const { closestMatch } = __require('./src/verify/closest-match');
|
|
688
|
+
const { analyzeImpact } = __require('./src/graph/impact');
|
|
603
689
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const exclude = new Set([...DEFAULT_EXCLUDE, ...(Array.isArray(cfg.exclude) ? cfg.exclude : [])]);
|
|
607
|
-
const maxDepth = Number.isFinite(cfg.maxDepth) ? cfg.maxDepth : 8;
|
|
690
|
+
const DEFAULT_BLAST_THRESHOLD = 20; // transitive+direct dependents → "high blast radius"
|
|
691
|
+
const DEFAULT_SCOPE_THRESHOLD = 10; // distinct referenced files → "broad scope"
|
|
608
692
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}
|
|
693
|
+
/** Resolve a referenced path against cwd (handles a leading "./"). */
|
|
694
|
+
function _fileExists(cwd, ref) {
|
|
695
|
+
const clean = ref.replace(/^\.\//, '');
|
|
696
|
+
for (const c of [path.resolve(cwd, clean), path.resolve(cwd, ref)]) {
|
|
697
|
+
try { if (fs.existsSync(c)) return true; } catch (_) {}
|
|
698
|
+
}
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
614
701
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
702
|
+
/**
|
|
703
|
+
* Verify a plan against the live index.
|
|
704
|
+
* @param {string} planText the plan as markdown
|
|
705
|
+
* @param {string} cwd repo root
|
|
706
|
+
* @param {object} [opts]
|
|
707
|
+
* @param {number} [opts.blastThreshold=20]
|
|
708
|
+
* @param {number} [opts.scopeThreshold=10]
|
|
709
|
+
* @param {(ref:string)=>boolean} [opts.fileExists] override for testing
|
|
710
|
+
* @returns {{ issues: object[], blast: object[], scope: object, summary: object }}
|
|
711
|
+
*/
|
|
712
|
+
function verifyPlan(planText, cwd, opts = {}) {
|
|
713
|
+
const blastThreshold = opts.blastThreshold != null ? opts.blastThreshold : DEFAULT_BLAST_THRESHOLD;
|
|
714
|
+
const scopeThreshold = opts.scopeThreshold != null ? opts.scopeThreshold : DEFAULT_SCOPE_THRESHOLD;
|
|
715
|
+
const fileExists = opts.fileExists || ((ref) => _fileExists(cwd, ref));
|
|
716
|
+
|
|
717
|
+
const text = String(planText || '');
|
|
718
|
+
const filesRef = extractFilePaths(text); // [{ path, line }]
|
|
719
|
+
const symbolsRef = extractSymbols(text); // [{ name, line }]
|
|
720
|
+
const { set: symbolSet, symbolCandidates } = buildSymbolSet(cwd);
|
|
721
|
+
|
|
722
|
+
const issues = [];
|
|
723
|
+
|
|
724
|
+
// 1. Referenced files must exist.
|
|
725
|
+
const existingFiles = [];
|
|
726
|
+
for (const f of filesRef) {
|
|
727
|
+
if (fileExists(f.path)) existingFiles.push(f.path);
|
|
728
|
+
else issues.push({ type: 'missing-file', ref: f.path, line: f.line, severity: 'error' });
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// 2. Referenced symbols must exist in the live index (suggest a near match).
|
|
732
|
+
for (const s of symbolsRef) {
|
|
733
|
+
if (symbolSet.has(s.name)) continue;
|
|
734
|
+
const match = closestMatch(s.name, symbolCandidates);
|
|
735
|
+
issues.push({
|
|
736
|
+
type: 'unknown-symbol', ref: s.name, line: s.line, severity: 'error',
|
|
737
|
+
suggestion: match ? match.name : null,
|
|
618
738
|
});
|
|
619
|
-
|
|
739
|
+
}
|
|
620
740
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
741
|
+
// 3. Blast radius for each existing referenced file (one graph build).
|
|
742
|
+
const blast = [];
|
|
743
|
+
if (existingFiles.length) {
|
|
744
|
+
let impacts = [];
|
|
745
|
+
try { impacts = analyzeImpact(existingFiles, cwd, {}); } catch (_) { impacts = []; }
|
|
746
|
+
for (const { file, impact } of impacts) {
|
|
747
|
+
const entry = { file, totalImpact: impact.totalImpact, tests: impact.tests.length };
|
|
748
|
+
blast.push(entry);
|
|
749
|
+
if (impact.totalImpact > blastThreshold) {
|
|
750
|
+
issues.push({ type: 'high-blast-radius', ref: file, count: impact.totalImpact, severity: 'warn' });
|
|
751
|
+
}
|
|
628
752
|
}
|
|
629
|
-
|
|
630
|
-
|
|
753
|
+
blast.sort((a, b) => b.totalImpact - a.totalImpact);
|
|
754
|
+
}
|
|
631
755
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
756
|
+
// 4. Scope.
|
|
757
|
+
const scope = { files: filesRef.length, symbols: symbolsRef.length, threshold: scopeThreshold };
|
|
758
|
+
if (filesRef.length > scopeThreshold) {
|
|
759
|
+
issues.push({ type: 'broad-scope', count: filesRef.length, threshold: scopeThreshold, severity: 'warn' });
|
|
636
760
|
}
|
|
761
|
+
|
|
762
|
+
const errors = issues.filter((i) => i.severity === 'error').length;
|
|
763
|
+
const warnings = issues.filter((i) => i.severity === 'warn').length;
|
|
764
|
+
return {
|
|
765
|
+
issues,
|
|
766
|
+
blast,
|
|
767
|
+
scope,
|
|
768
|
+
summary: {
|
|
769
|
+
filesReferenced: filesRef.length,
|
|
770
|
+
symbolsReferenced: symbolsRef.length,
|
|
771
|
+
errors,
|
|
772
|
+
warnings,
|
|
773
|
+
ok: errors === 0,
|
|
774
|
+
},
|
|
775
|
+
};
|
|
637
776
|
}
|
|
638
777
|
|
|
639
|
-
module.exports = {
|
|
778
|
+
module.exports = { verifyPlan, DEFAULT_BLAST_THRESHOLD, DEFAULT_SCOPE_THRESHOLD };
|
|
640
779
|
|
|
641
780
|
};
|
|
781
|
+
|
|
642
782
|
// ── ./src/extractors/dispatch ──
|
|
643
783
|
__factories["./src/extractors/dispatch"] = function(module, exports) {
|
|
644
784
|
|
|
@@ -7119,7 +7259,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
7119
7259
|
|
|
7120
7260
|
const SERVER_INFO = {
|
|
7121
7261
|
name: 'sigmap',
|
|
7122
|
-
version: '7.
|
|
7262
|
+
version: '7.11.0',
|
|
7123
7263
|
description: 'SigMap MCP server — code signatures on demand',
|
|
7124
7264
|
};
|
|
7125
7265
|
|
|
@@ -12797,7 +12937,7 @@ function __tryGit(args, opts = {}) {
|
|
|
12797
12937
|
catch (_) { return ''; }
|
|
12798
12938
|
}
|
|
12799
12939
|
|
|
12800
|
-
const VERSION = '7.
|
|
12940
|
+
const VERSION = '7.11.0';
|
|
12801
12941
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
12802
12942
|
|
|
12803
12943
|
function requireSourceOrBundled(key) {
|
|
@@ -16151,6 +16291,47 @@ function main() {
|
|
|
16151
16291
|
}
|
|
16152
16292
|
|
|
16153
16293
|
// `sigmap verify-ai-output <answer.md>` — Hallucination Guard (deterministic core)
|
|
16294
|
+
// Gap 2 (step 2): `sigmap verify-plan <plan.md>` — check a plan against the
|
|
16295
|
+
// live index before execution (files/symbols exist, blast radius, scope).
|
|
16296
|
+
if (args[0] === 'verify-plan') {
|
|
16297
|
+
const target = args[1] && !args[1].startsWith('--') ? args[1] : null;
|
|
16298
|
+
const jsonOut = args.includes('--json');
|
|
16299
|
+
if (!target) {
|
|
16300
|
+
console.error('[sigmap] Usage: sigmap verify-plan <plan.md|-> [--json]');
|
|
16301
|
+
process.exit(1);
|
|
16302
|
+
}
|
|
16303
|
+
let planText = '';
|
|
16304
|
+
try {
|
|
16305
|
+
planText = fs.readFileSync(target === '-' ? 0 : path.resolve(cwd, target), 'utf8');
|
|
16306
|
+
} catch (e) {
|
|
16307
|
+
console.error(`[sigmap] cannot read plan: ${e.message}`);
|
|
16308
|
+
process.exit(1);
|
|
16309
|
+
}
|
|
16310
|
+
const { verifyPlan } = requireSourceOrBundled('./src/plan/verify-plan');
|
|
16311
|
+
const result = verifyPlan(planText, cwd);
|
|
16312
|
+
|
|
16313
|
+
if (jsonOut) {
|
|
16314
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
16315
|
+
process.exit(result.summary.ok ? 0 : 1);
|
|
16316
|
+
}
|
|
16317
|
+
|
|
16318
|
+
const s = result.summary;
|
|
16319
|
+
console.log(`[sigmap] verify-plan — ${s.filesReferenced} file(s), ${s.symbolsReferenced} symbol(s) referenced`);
|
|
16320
|
+
if (result.issues.length === 0) {
|
|
16321
|
+
console.log(' ✓ plan checks out — all references exist, blast radius and scope in bounds');
|
|
16322
|
+
process.exit(0);
|
|
16323
|
+
}
|
|
16324
|
+
for (const i of result.issues) {
|
|
16325
|
+
const mark = i.severity === 'error' ? '✗' : '⚠';
|
|
16326
|
+
if (i.type === 'missing-file') console.log(` ${mark} missing file: ${i.ref} (line ${i.line})`);
|
|
16327
|
+
else if (i.type === 'unknown-symbol') console.log(` ${mark} unknown symbol: ${i.ref}()${i.suggestion ? ` — did you mean ${i.suggestion}()?` : ''} (line ${i.line})`);
|
|
16328
|
+
else if (i.type === 'high-blast-radius') console.log(` ${mark} high blast radius: ${i.ref} → ${i.count} dependents`);
|
|
16329
|
+
else if (i.type === 'broad-scope') console.log(` ${mark} broad scope: ${i.count} files (threshold ${i.threshold})`);
|
|
16330
|
+
}
|
|
16331
|
+
console.log(`\n ${s.errors} error(s), ${s.warnings} warning(s)`);
|
|
16332
|
+
process.exit(s.ok ? 0 : 1);
|
|
16333
|
+
}
|
|
16334
|
+
|
|
16154
16335
|
if (args[0] === 'verify-ai-output') {
|
|
16155
16336
|
const target = args[1];
|
|
16156
16337
|
const jsonOut = args.includes('--json');
|
package/llms-full.txt
CHANGED
|
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
|
|
|
9
9
|
grounded. Deterministic, offline, no embeddings or vector database. Works with
|
|
10
10
|
Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
11
11
|
|
|
12
|
-
# Version: 7.
|
|
12
|
+
# Version: 7.11.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
|
|
13
13
|
# Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
|
|
14
14
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
15
15
|
|
package/llms.txt
CHANGED
|
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
|
|
|
9
9
|
grounded. Deterministic, offline, no embeddings or vector database. Works with
|
|
10
10
|
Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
11
11
|
|
|
12
|
-
# Version: 7.
|
|
12
|
+
# Version: 7.11.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
|
|
13
13
|
# Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
|
|
14
14
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
15
15
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.11.0",
|
|
4
4
|
"description": "97% token reduction for AI coding. Extracts function & class signatures with TF-IDF ranking to feed only the right files to Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP. Zero dependencies, runs offline via npx.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
package/src/mcp/server.js
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* verify-plan (IMPL.md §6.1 — Gap 2, step 2 of the `create` pipeline).
|
|
5
|
+
*
|
|
6
|
+
* Checks a plan (markdown) against the LIVE index before execution: do the
|
|
7
|
+
* referenced files and symbols exist, is the blast radius acceptable, is the
|
|
8
|
+
* scope in bounds? Catches Cause 1+2 at plan time — cheaper than after the
|
|
9
|
+
* code is written. Reuses the verify primitives + the impact graph.
|
|
10
|
+
* Zero-dependency, bundle-safe.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { extractFilePaths, extractSymbols } = require('../verify/parsers');
|
|
16
|
+
const { buildSymbolSet } = require('../verify/hallucination-guard');
|
|
17
|
+
const { closestMatch } = require('../verify/closest-match');
|
|
18
|
+
const { analyzeImpact } = require('../graph/impact');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_BLAST_THRESHOLD = 20; // transitive+direct dependents → "high blast radius"
|
|
21
|
+
const DEFAULT_SCOPE_THRESHOLD = 10; // distinct referenced files → "broad scope"
|
|
22
|
+
|
|
23
|
+
/** Resolve a referenced path against cwd (handles a leading "./"). */
|
|
24
|
+
function _fileExists(cwd, ref) {
|
|
25
|
+
const clean = ref.replace(/^\.\//, '');
|
|
26
|
+
for (const c of [path.resolve(cwd, clean), path.resolve(cwd, ref)]) {
|
|
27
|
+
try { if (fs.existsSync(c)) return true; } catch (_) {}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Verify a plan against the live index.
|
|
34
|
+
* @param {string} planText the plan as markdown
|
|
35
|
+
* @param {string} cwd repo root
|
|
36
|
+
* @param {object} [opts]
|
|
37
|
+
* @param {number} [opts.blastThreshold=20]
|
|
38
|
+
* @param {number} [opts.scopeThreshold=10]
|
|
39
|
+
* @param {(ref:string)=>boolean} [opts.fileExists] override for testing
|
|
40
|
+
* @returns {{ issues: object[], blast: object[], scope: object, summary: object }}
|
|
41
|
+
*/
|
|
42
|
+
function verifyPlan(planText, cwd, opts = {}) {
|
|
43
|
+
const blastThreshold = opts.blastThreshold != null ? opts.blastThreshold : DEFAULT_BLAST_THRESHOLD;
|
|
44
|
+
const scopeThreshold = opts.scopeThreshold != null ? opts.scopeThreshold : DEFAULT_SCOPE_THRESHOLD;
|
|
45
|
+
const fileExists = opts.fileExists || ((ref) => _fileExists(cwd, ref));
|
|
46
|
+
|
|
47
|
+
const text = String(planText || '');
|
|
48
|
+
const filesRef = extractFilePaths(text); // [{ path, line }]
|
|
49
|
+
const symbolsRef = extractSymbols(text); // [{ name, line }]
|
|
50
|
+
const { set: symbolSet, symbolCandidates } = buildSymbolSet(cwd);
|
|
51
|
+
|
|
52
|
+
const issues = [];
|
|
53
|
+
|
|
54
|
+
// 1. Referenced files must exist.
|
|
55
|
+
const existingFiles = [];
|
|
56
|
+
for (const f of filesRef) {
|
|
57
|
+
if (fileExists(f.path)) existingFiles.push(f.path);
|
|
58
|
+
else issues.push({ type: 'missing-file', ref: f.path, line: f.line, severity: 'error' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Referenced symbols must exist in the live index (suggest a near match).
|
|
62
|
+
for (const s of symbolsRef) {
|
|
63
|
+
if (symbolSet.has(s.name)) continue;
|
|
64
|
+
const match = closestMatch(s.name, symbolCandidates);
|
|
65
|
+
issues.push({
|
|
66
|
+
type: 'unknown-symbol', ref: s.name, line: s.line, severity: 'error',
|
|
67
|
+
suggestion: match ? match.name : null,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Blast radius for each existing referenced file (one graph build).
|
|
72
|
+
const blast = [];
|
|
73
|
+
if (existingFiles.length) {
|
|
74
|
+
let impacts = [];
|
|
75
|
+
try { impacts = analyzeImpact(existingFiles, cwd, {}); } catch (_) { impacts = []; }
|
|
76
|
+
for (const { file, impact } of impacts) {
|
|
77
|
+
const entry = { file, totalImpact: impact.totalImpact, tests: impact.tests.length };
|
|
78
|
+
blast.push(entry);
|
|
79
|
+
if (impact.totalImpact > blastThreshold) {
|
|
80
|
+
issues.push({ type: 'high-blast-radius', ref: file, count: impact.totalImpact, severity: 'warn' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
blast.sort((a, b) => b.totalImpact - a.totalImpact);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. Scope.
|
|
87
|
+
const scope = { files: filesRef.length, symbols: symbolsRef.length, threshold: scopeThreshold };
|
|
88
|
+
if (filesRef.length > scopeThreshold) {
|
|
89
|
+
issues.push({ type: 'broad-scope', count: filesRef.length, threshold: scopeThreshold, severity: 'warn' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const errors = issues.filter((i) => i.severity === 'error').length;
|
|
93
|
+
const warnings = issues.filter((i) => i.severity === 'warn').length;
|
|
94
|
+
return {
|
|
95
|
+
issues,
|
|
96
|
+
blast,
|
|
97
|
+
scope,
|
|
98
|
+
summary: {
|
|
99
|
+
filesReferenced: filesRef.length,
|
|
100
|
+
symbolsReferenced: symbolsRef.length,
|
|
101
|
+
errors,
|
|
102
|
+
warnings,
|
|
103
|
+
ok: errors === 0,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { verifyPlan, DEFAULT_BLAST_THRESHOLD, DEFAULT_SCOPE_THRESHOLD };
|