sigmap 7.9.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 +18 -0
- package/gen-context.js +526 -179
- 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/src/scaffold/propose.js +112 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@ 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
|
+
|
|
22
|
+
## [7.10.0] — 2026-06-17
|
|
23
|
+
|
|
24
|
+
Minor release — `sigmap scaffold` with a confidence floor (grounded codegen, Layer 4).
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **`sigmap scaffold <name>` — convention-matched proposal with a confidence floor (#307):** the first slice of Layer 4 (Cause 3 — guessing structure for new code). Proposes a convention-matched structure for a new module — filename in the repo's dominant naming style, the export style to use, and a matching test file — but **only when the conventions are consistent enough**. New zero-dependency, bundle-safe `src/scaffold/propose.js` (`proposeScaffold`): the governing confidence is file-naming consistency, with a soft threshold (default 0.70, overridable via `--threshold`) and a **non-overridable hard floor of 0.50**. Below the threshold it refuses and surfaces the conflict (reusing `analyzeConflicts`); `--force` allows a proposal between the floor and the threshold (flagged with a warning), but never below the floor — a wrong proposal systematizes bad code. CLI supports `--ext`, `--threshold`, `--force`, and `--json` (refusal exits 1). Scaffold persistence (`.sigmap/scaffold/latest.md`), `--naming-pattern` override, and the `verify-plan` → `create` → `review-pr` pipeline remain follow-ups.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
13
31
|
## [7.9.0] — 2026-06-17
|
|
14
32
|
|
|
15
33
|
Minor release — `sigmap conventions --inject` (grounded codegen, Layer 3).
|
package/gen-context.js
CHANGED
|
@@ -24,107 +24,136 @@ function __require(key) {
|
|
|
24
24
|
// ── ./src/conventions/extract ──
|
|
25
25
|
// ── ./src/conventions/conflicts ──
|
|
26
26
|
// ── ./src/conventions/inject ──
|
|
27
|
-
|
|
27
|
+
// ── ./src/scaffold/propose ──
|
|
28
|
+
// ── ./src/plan/verify-plan ──
|
|
29
|
+
// ── ./src/cache/freshen ──
|
|
30
|
+
__factories["./src/cache/freshen"] = function(module, exports) {
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
|
-
*
|
|
33
|
+
* Read-time self-heal (IMPL.md Layer 1, "safety net" tier).
|
|
31
34
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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).
|
|
37
45
|
*/
|
|
38
46
|
|
|
39
|
-
const
|
|
40
|
-
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');
|
|
41
51
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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();
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 {}; }
|
|
69
|
+
}
|
|
52
70
|
|
|
53
|
-
|
|
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'; }
|
|
74
|
+
}
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 (_) {}
|
|
81
|
+
}
|
|
82
|
+
return newest;
|
|
83
|
+
}
|
|
84
|
+
|
|
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);
|
|
62
94
|
}
|
|
63
|
-
return line;
|
|
64
95
|
}
|
|
65
96
|
|
|
66
97
|
/**
|
|
67
|
-
*
|
|
68
|
-
* @param {
|
|
69
|
-
* @param {
|
|
70
|
-
* @returns {
|
|
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
|
|
71
102
|
*/
|
|
72
|
-
function
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (l) lines.push(l);
|
|
77
|
-
}
|
|
78
|
-
if (result && result.testFramework) {
|
|
79
|
-
lines.push(`- **Test framework:** ${result.testFramework}.`);
|
|
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;
|
|
80
107
|
}
|
|
108
|
+
_lastRun.set(cwd, now);
|
|
81
109
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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;
|
|
85
116
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
'',
|
|
91
|
-
'Match these when writing or editing code (TS/JS/Python):',
|
|
92
|
-
'',
|
|
93
|
-
...body,
|
|
94
|
-
'',
|
|
95
|
-
`<sub>Generated by SigMap${ver} · run \`sigmap conventions --inject\` to refresh.</sub>`,
|
|
96
|
-
END,
|
|
97
|
-
].join('\n');
|
|
98
|
-
}
|
|
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;
|
|
99
121
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
127
|
+
|
|
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 (_) {}
|
|
117
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;
|
|
118
149
|
}
|
|
119
|
-
if (src.trim() === '') return block + '\n';
|
|
120
|
-
const sep = src.endsWith('\n') ? '\n' : '\n\n';
|
|
121
|
-
return src + sep + block + '\n';
|
|
122
150
|
}
|
|
123
151
|
|
|
124
|
-
module.exports = {
|
|
152
|
+
module.exports = { freshen };
|
|
125
153
|
|
|
126
154
|
};
|
|
127
155
|
|
|
156
|
+
// ── ./src/conventions/conflicts ──
|
|
128
157
|
__factories["./src/conventions/conflicts"] = function(module, exports) {
|
|
129
158
|
|
|
130
159
|
/**
|
|
@@ -239,6 +268,7 @@ __factories["./src/conventions/conflicts"] = function(module, exports) {
|
|
|
239
268
|
|
|
240
269
|
};
|
|
241
270
|
|
|
271
|
+
// ── ./src/conventions/extract ──
|
|
242
272
|
__factories["./src/conventions/extract"] = function(module, exports) {
|
|
243
273
|
|
|
244
274
|
/**
|
|
@@ -283,24 +313,42 @@ __factories["./src/conventions/extract"] = function(module, exports) {
|
|
|
283
313
|
return 'other';
|
|
284
314
|
}
|
|
285
315
|
|
|
316
|
+
const MAX_EXAMPLES = 3;
|
|
317
|
+
|
|
286
318
|
/**
|
|
287
319
|
* Score a set of categorical observations into a dominant convention plus its
|
|
288
320
|
* consistency tier. The reusable primitive (IMPL.md §5.2).
|
|
289
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`.
|
|
290
324
|
* @returns {{ dominant: string|null, dominantPct: number, total: number,
|
|
291
|
-
* variants: Array<{label:string, count:number, pct:number}>,
|
|
325
|
+
* variants: Array<{label:string, count:number, pct:number, examples?:string[]}>,
|
|
292
326
|
* tier: 'consistent'|'mostly'|'inconsistent'|'unknown' }}
|
|
293
327
|
*/
|
|
294
|
-
function scoreConvention(labels) {
|
|
295
|
-
const
|
|
296
|
-
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
|
+
}
|
|
297
343
|
if (total === 0) {
|
|
298
344
|
return { dominant: null, dominantPct: 0, total: 0, variants: [], tier: 'unknown' };
|
|
299
345
|
}
|
|
300
|
-
const counts = new Map();
|
|
301
|
-
for (const l of list) counts.set(l, (counts.get(l) || 0) + 1);
|
|
302
346
|
const variants = [...counts.entries()]
|
|
303
|
-
.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
|
+
})
|
|
304
352
|
.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
305
353
|
const top = variants[0];
|
|
306
354
|
let tier = 'inconsistent';
|
|
@@ -372,22 +420,26 @@ __factories["./src/conventions/extract"] = function(module, exports) {
|
|
|
372
420
|
function extractConventions(cwd, files) {
|
|
373
421
|
const scoped = (files || []).filter((f) => SCOPED_EXTS.has(path.extname(f).toLowerCase()));
|
|
374
422
|
const namingLabels = [];
|
|
423
|
+
const namingRefs = [];
|
|
375
424
|
const exportLabels = [];
|
|
425
|
+
const exportRefs = [];
|
|
376
426
|
for (const f of scoped) {
|
|
377
427
|
const base = path.basename(f);
|
|
378
428
|
// Skip test files for the naming convention (they have their own naming).
|
|
379
429
|
if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) {
|
|
380
430
|
namingLabels.push(classifyNaming(base));
|
|
431
|
+
namingRefs.push(base);
|
|
381
432
|
}
|
|
382
433
|
if (JS_TS_EXTS.has(path.extname(f).toLowerCase())) {
|
|
383
434
|
let src = '';
|
|
384
435
|
try { src = fs.readFileSync(f, 'utf8'); } catch (_) {}
|
|
385
436
|
exportLabels.push(_jsExportStyle(src));
|
|
437
|
+
exportRefs.push(base);
|
|
386
438
|
}
|
|
387
439
|
}
|
|
388
440
|
return {
|
|
389
|
-
fileNaming: scoreConvention(namingLabels),
|
|
390
|
-
exportStyle: scoreConvention(exportLabels),
|
|
441
|
+
fileNaming: scoreConvention(namingLabels, namingRefs),
|
|
442
|
+
exportStyle: scoreConvention(exportLabels, exportRefs),
|
|
391
443
|
testFramework: _detectTestFramework(cwd, scoped),
|
|
392
444
|
scope: ['typescript', 'javascript', 'python'],
|
|
393
445
|
scannedFiles: scoped.length,
|
|
@@ -398,131 +450,335 @@ __factories["./src/conventions/extract"] = function(module, exports) {
|
|
|
398
450
|
|
|
399
451
|
};
|
|
400
452
|
|
|
401
|
-
|
|
453
|
+
// ── ./src/conventions/inject ──
|
|
454
|
+
__factories["./src/conventions/inject"] = function(module, exports) {
|
|
402
455
|
|
|
403
456
|
/**
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* Keeps the sig-cache in line with the current source tree so the index reflects
|
|
407
|
-
* on-disk reality even when no write hook was called. Re-extracts files modified
|
|
408
|
-
* since the context file was generated (bounded to actual session edits, not the
|
|
409
|
-
* whole tree), drops cache entries for deleted files, and persists. buildSigIndex
|
|
410
|
-
* already merges the cache, so the next read is fresh.
|
|
411
|
-
*
|
|
412
|
-
* Throttled per cwd. Skips entirely when there is no generated index to heal
|
|
413
|
-
* (a cold repo should run `generate` or use the notify hooks).
|
|
457
|
+
* CLAUDE.md convention injection (IMPL.md §7 Phase 2 / §8 step 5).
|
|
414
458
|
*
|
|
415
|
-
*
|
|
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.
|
|
416
464
|
*/
|
|
417
465
|
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
const { loadCache, saveCache, getChangedFiles } = __require('./src/cache/sig-cache');
|
|
421
|
-
const { extractFile, langFor } = __require('./src/extractors/dispatch');
|
|
466
|
+
const START = '<!-- sigmap-conventions:start -->';
|
|
467
|
+
const END = '<!-- sigmap-conventions:end -->';
|
|
422
468
|
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const CONTEXT_PATHS = [
|
|
429
|
-
['.github', 'copilot-instructions.md'],
|
|
430
|
-
['CLAUDE.md'], ['AGENTS.md'], ['.github', 'context-cold.md'],
|
|
431
|
-
];
|
|
432
|
-
const THROTTLE_MS = 1500;
|
|
433
|
-
const _lastRun = new Map();
|
|
469
|
+
const TIER_NOTE = {
|
|
470
|
+
consistent: 'consistent — match it',
|
|
471
|
+
mostly: 'dominant, with some drift',
|
|
472
|
+
inconsistent: 'no clear convention — check neighboring files',
|
|
473
|
+
};
|
|
434
474
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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;
|
|
440
491
|
}
|
|
441
492
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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');
|
|
445
525
|
}
|
|
446
526
|
|
|
447
|
-
/**
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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;
|
|
544
|
+
}
|
|
452
545
|
}
|
|
453
|
-
return
|
|
546
|
+
if (src.trim() === '') return block + '\n';
|
|
547
|
+
const sep = src.endsWith('\n') ? '\n' : '\n\n';
|
|
548
|
+
return src + sep + block + '\n';
|
|
454
549
|
}
|
|
455
550
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
551
|
+
module.exports = { renderConventionsBlock, injectConventions, START, END };
|
|
552
|
+
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// ── ./src/scaffold/propose ──
|
|
556
|
+
__factories["./src/scaffold/propose"] = function(module, exports) {
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Scaffold proposal with a confidence floor (IMPL.md §5 — Cause 3).
|
|
560
|
+
*
|
|
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.
|
|
567
|
+
*/
|
|
568
|
+
|
|
569
|
+
const { toNamingStyle, analyzeConflicts } = __require('./src/conventions/conflicts');
|
|
570
|
+
|
|
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;
|
|
574
|
+
|
|
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';
|
|
580
|
+
}
|
|
581
|
+
|
|
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;
|
|
589
|
+
}
|
|
590
|
+
|
|
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`;
|
|
465
595
|
}
|
|
596
|
+
return `${styledStem}.test.${ext}`;
|
|
466
597
|
}
|
|
467
598
|
|
|
468
599
|
/**
|
|
469
|
-
*
|
|
470
|
-
* @param {string}
|
|
471
|
-
* @param {
|
|
472
|
-
* @
|
|
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 }}
|
|
473
611
|
*/
|
|
474
|
-
function
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
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
|
+
};
|
|
478
642
|
}
|
|
479
|
-
_lastRun.set(cwd, now);
|
|
480
643
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
+
};
|
|
487
653
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
+
};
|
|
665
|
+
}
|
|
492
666
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (fs.existsSync(abs)) _walk(abs, exclude, files, 0, maxDepth);
|
|
497
|
-
}
|
|
667
|
+
module.exports = { proposeScaffold, DEFAULT_THRESHOLD, HARD_FLOOR };
|
|
668
|
+
|
|
669
|
+
};
|
|
498
670
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
671
|
+
__factories["./src/plan/verify-plan"] = function(module, exports) {
|
|
672
|
+
|
|
673
|
+
/**
|
|
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.
|
|
681
|
+
*/
|
|
682
|
+
|
|
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');
|
|
689
|
+
|
|
690
|
+
const DEFAULT_BLAST_THRESHOLD = 20; // transitive+direct dependents → "high blast radius"
|
|
691
|
+
const DEFAULT_SCOPE_THRESHOLD = 10; // distinct referenced files → "broad scope"
|
|
692
|
+
|
|
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
|
+
}
|
|
701
|
+
|
|
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,
|
|
502
738
|
});
|
|
503
|
-
|
|
739
|
+
}
|
|
504
740
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
+
}
|
|
512
752
|
}
|
|
513
|
-
|
|
514
|
-
|
|
753
|
+
blast.sort((a, b) => b.totalImpact - a.totalImpact);
|
|
754
|
+
}
|
|
515
755
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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' });
|
|
520
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
|
+
};
|
|
521
776
|
}
|
|
522
777
|
|
|
523
|
-
module.exports = {
|
|
778
|
+
module.exports = { verifyPlan, DEFAULT_BLAST_THRESHOLD, DEFAULT_SCOPE_THRESHOLD };
|
|
524
779
|
|
|
525
780
|
};
|
|
781
|
+
|
|
526
782
|
// ── ./src/extractors/dispatch ──
|
|
527
783
|
__factories["./src/extractors/dispatch"] = function(module, exports) {
|
|
528
784
|
|
|
@@ -7003,7 +7259,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
7003
7259
|
|
|
7004
7260
|
const SERVER_INFO = {
|
|
7005
7261
|
name: 'sigmap',
|
|
7006
|
-
version: '7.
|
|
7262
|
+
version: '7.11.0',
|
|
7007
7263
|
description: 'SigMap MCP server — code signatures on demand',
|
|
7008
7264
|
};
|
|
7009
7265
|
|
|
@@ -12681,7 +12937,7 @@ function __tryGit(args, opts = {}) {
|
|
|
12681
12937
|
catch (_) { return ''; }
|
|
12682
12938
|
}
|
|
12683
12939
|
|
|
12684
|
-
const VERSION = '7.
|
|
12940
|
+
const VERSION = '7.11.0';
|
|
12685
12941
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
12686
12942
|
|
|
12687
12943
|
function requireSourceOrBundled(key) {
|
|
@@ -15941,6 +16197,56 @@ function main() {
|
|
|
15941
16197
|
process.exit(0);
|
|
15942
16198
|
}
|
|
15943
16199
|
|
|
16200
|
+
// Layer 4: `sigmap scaffold <name>` — propose a convention-matched structure
|
|
16201
|
+
// for a new module, gated by a confidence floor (refuses if conventions are
|
|
16202
|
+
// too inconsistent; a wrong proposal systematizes bad code).
|
|
16203
|
+
if (args[0] === 'scaffold') {
|
|
16204
|
+
const jsonOut = args.includes('--json');
|
|
16205
|
+
const name = args[1] && !args[1].startsWith('--') ? args[1] : null;
|
|
16206
|
+
if (!name) {
|
|
16207
|
+
console.error('[sigmap] usage: sigmap scaffold <name> [--ext <e>] [--threshold <n>] [--force] [--json]');
|
|
16208
|
+
process.exit(1);
|
|
16209
|
+
}
|
|
16210
|
+
const thIdx = args.indexOf('--threshold');
|
|
16211
|
+
const threshold = thIdx !== -1 && args[thIdx + 1] ? parseFloat(args[thIdx + 1]) : undefined;
|
|
16212
|
+
const extIdx = args.indexOf('--ext');
|
|
16213
|
+
const ext = extIdx !== -1 && args[extIdx + 1] ? args[extIdx + 1].replace(/^\./, '') : undefined;
|
|
16214
|
+
const force = args.includes('--force');
|
|
16215
|
+
|
|
16216
|
+
const { extractConventions } = requireSourceOrBundled('./src/conventions/extract');
|
|
16217
|
+
const { proposeScaffold } = requireSourceOrBundled('./src/scaffold/propose');
|
|
16218
|
+
const conventions = extractConventions(cwd, buildFileList(cwd, config));
|
|
16219
|
+
const decision = proposeScaffold(name, conventions, { threshold, ext, force });
|
|
16220
|
+
|
|
16221
|
+
if (jsonOut) {
|
|
16222
|
+
process.stdout.write(JSON.stringify(decision) + '\n');
|
|
16223
|
+
process.exit(decision.ok ? 0 : 1);
|
|
16224
|
+
}
|
|
16225
|
+
|
|
16226
|
+
const pctS = (n) => `${(n * 100).toFixed(0)}%`;
|
|
16227
|
+
if (decision.ok) {
|
|
16228
|
+
console.log(`[sigmap] scaffold "${decision.name}" — conventions ${decision.tier} (${pctS(decision.confidence)})`);
|
|
16229
|
+
if (decision.warning) console.log(` ⚠ ${decision.warning}`);
|
|
16230
|
+
const p = decision.proposal;
|
|
16231
|
+
console.log(` file: ${p.filename} (${p.namingStyle})`);
|
|
16232
|
+
console.log(` export style: ${p.exportStyle}`);
|
|
16233
|
+
console.log(` test file: ${p.testFile}${p.testFramework ? ` (${p.testFramework})` : ''}`);
|
|
16234
|
+
process.exit(0);
|
|
16235
|
+
}
|
|
16236
|
+
|
|
16237
|
+
console.log(`[sigmap] scaffold "${decision.name}" — REFUSED`);
|
|
16238
|
+
console.log(` ${decision.reason}`);
|
|
16239
|
+
if (decision.conflicts && decision.conflicts.hasConflicts) {
|
|
16240
|
+
for (const c of decision.conflicts.conventions) {
|
|
16241
|
+
console.log(`\n ${c.name} — dominant: ${c.dominant} ${pctS(c.dominantPct)} [${c.tier}]`);
|
|
16242
|
+
for (const v of c.variants) {
|
|
16243
|
+
console.log(` ${v.pattern.padEnd(12)} ${pctS(v.pct).padStart(4)}${v.examples.length ? ` e.g. ${v.examples.join(', ')}` : ''}`);
|
|
16244
|
+
}
|
|
16245
|
+
}
|
|
16246
|
+
}
|
|
16247
|
+
process.exit(1);
|
|
16248
|
+
}
|
|
16249
|
+
|
|
15944
16250
|
// v7.0.0: `sigmap squeeze <file|->` — minimize a pasted stacktrace / CI-log / JSON blob
|
|
15945
16251
|
if (args[0] === 'squeeze') {
|
|
15946
16252
|
const jsonOut = args.includes('--json');
|
|
@@ -15985,6 +16291,47 @@ function main() {
|
|
|
15985
16291
|
}
|
|
15986
16292
|
|
|
15987
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
|
+
|
|
15988
16335
|
if (args[0] === 'verify-ai-output') {
|
|
15989
16336
|
const target = args[1];
|
|
15990
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 };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scaffold proposal with a confidence floor (IMPL.md §5 — Cause 3).
|
|
5
|
+
*
|
|
6
|
+
* Proposes a convention-matched structure for a new module — filename in the
|
|
7
|
+
* repo's dominant naming style, the export style to use, and a matching test
|
|
8
|
+
* file — but only when the conventions are consistent enough. Below a hard
|
|
9
|
+
* floor it refuses and surfaces the conflict, because a wrong proposal
|
|
10
|
+
* systematizes bad code. Pure, zero-dependency, bundle-safe; reuses the
|
|
11
|
+
* conventions primitives.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { toNamingStyle, analyzeConflicts } = require('../conventions/conflicts');
|
|
15
|
+
|
|
16
|
+
// Soft threshold is configurable; the hard floor is not (IMPL.md §5.1).
|
|
17
|
+
const DEFAULT_THRESHOLD = 0.7;
|
|
18
|
+
const HARD_FLOOR = 0.5;
|
|
19
|
+
|
|
20
|
+
/** Tier for a consistency score (matches the conventions tiers). */
|
|
21
|
+
function _tier(pct) {
|
|
22
|
+
if (pct >= 0.9) return 'consistent';
|
|
23
|
+
if (pct >= 0.7) return 'mostly';
|
|
24
|
+
return 'inconsistent';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Strip any extension/compound suffix from a requested name → bare stem. */
|
|
28
|
+
function _stem(name) {
|
|
29
|
+
const s = String(name || '').trim();
|
|
30
|
+
const slash = s.lastIndexOf('/');
|
|
31
|
+
const base = slash >= 0 ? s.slice(slash + 1) : s;
|
|
32
|
+
const dot = base.indexOf('.');
|
|
33
|
+
return dot > 0 ? base.slice(0, dot) : base;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Test file path for a styled stem given the detected framework + ext. */
|
|
37
|
+
function _testFile(styledStem, framework, ext) {
|
|
38
|
+
if (framework === 'pytest' || framework === 'unittest') {
|
|
39
|
+
return `test_${toNamingStyle(styledStem, 'snake_case')}.py`;
|
|
40
|
+
}
|
|
41
|
+
return `${styledStem}.test.${ext}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Propose a convention-matched scaffold, gated by a confidence floor.
|
|
46
|
+
* @param {string} name desired module name (any casing; extension ignored)
|
|
47
|
+
* @param {object} conventions an `extractConventions` result
|
|
48
|
+
* @param {object} [opts]
|
|
49
|
+
* @param {number} [opts.threshold=0.7] soft threshold (clamped to ≥ hard floor)
|
|
50
|
+
* @param {boolean} [opts.force=false] allow proposing below the soft threshold
|
|
51
|
+
* (never below the hard floor)
|
|
52
|
+
* @param {string} [opts.ext='js'] file extension for the proposed files
|
|
53
|
+
* @returns {{ ok:boolean, refused:boolean, name:string, tier:string,
|
|
54
|
+
* confidence:number, threshold:number, hardFloor:number, forced:boolean,
|
|
55
|
+
* warning:string|null, reason:string, proposal:object|null, conflicts:object }}
|
|
56
|
+
*/
|
|
57
|
+
function proposeScaffold(name, conventions, opts = {}) {
|
|
58
|
+
const threshold = Math.max(HARD_FLOOR, opts.threshold != null ? opts.threshold : DEFAULT_THRESHOLD);
|
|
59
|
+
const force = !!opts.force;
|
|
60
|
+
const ext = opts.ext || 'js';
|
|
61
|
+
const fileNaming = (conventions && conventions.fileNaming) || { dominant: null, dominantPct: 0, total: 0 };
|
|
62
|
+
const exportStyle = (conventions && conventions.exportStyle) || { dominant: null };
|
|
63
|
+
const confidence = fileNaming.dominantPct || 0;
|
|
64
|
+
const tier = fileNaming.total > 0 ? _tier(confidence) : 'unknown';
|
|
65
|
+
const conflicts = analyzeConflicts(conventions || {});
|
|
66
|
+
|
|
67
|
+
const base = {
|
|
68
|
+
ok: false, refused: true, name: String(name || ''), tier, confidence,
|
|
69
|
+
threshold, hardFloor: HARD_FLOOR, forced: false, warning: null,
|
|
70
|
+
reason: '', proposal: null, conflicts,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (!fileNaming.dominant || fileNaming.total === 0) {
|
|
74
|
+
return { ...base, reason: 'no file-naming convention detected — cannot propose a name' };
|
|
75
|
+
}
|
|
76
|
+
if (confidence < HARD_FLOOR) {
|
|
77
|
+
return {
|
|
78
|
+
...base,
|
|
79
|
+
reason: `file-naming consistency ${(confidence * 100).toFixed(0)}% is below the hard floor ${(HARD_FLOOR * 100).toFixed(0)}% — refusing (not overridable)`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (confidence < threshold && !force) {
|
|
83
|
+
return {
|
|
84
|
+
...base,
|
|
85
|
+
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)`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const styledStem = toNamingStyle(_stem(name), fileNaming.dominant);
|
|
90
|
+
const forced = confidence < threshold && force;
|
|
91
|
+
const proposal = {
|
|
92
|
+
filename: `${styledStem}.${ext}`,
|
|
93
|
+
namingStyle: fileNaming.dominant,
|
|
94
|
+
exportStyle: exportStyle.dominant || 'named',
|
|
95
|
+
testFile: _testFile(styledStem, conventions.testFramework, ext),
|
|
96
|
+
testFramework: conventions.testFramework || null,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
...base,
|
|
101
|
+
ok: true,
|
|
102
|
+
refused: false,
|
|
103
|
+
forced,
|
|
104
|
+
warning: forced
|
|
105
|
+
? `proposed below the ${(threshold * 100).toFixed(0)}% threshold (--force); conventions are only ${tier}`
|
|
106
|
+
: null,
|
|
107
|
+
reason: forced ? 'forced proposal above the hard floor' : `conventions are ${tier} — proposing`,
|
|
108
|
+
proposal,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { proposeScaffold, DEFAULT_THRESHOLD, HARD_FLOOR };
|