sigmap 7.11.0 → 7.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/gen-context.js +321 -2
- 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/create/orchestrate.js +86 -0
- package/src/mcp/server.js +1 -1
- package/src/review/review-pr.js +106 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [7.13.0] — 2026-06-17
|
|
14
|
+
|
|
15
|
+
Minor release — `sigmap create` (grounded codegen, Gap 2 — the pipeline capstone).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`sigmap create "<task>"` — orchestrate the 4-stage grounded-creation pipeline (#316):** the capstone of the grounded-codegen work. One command sequences the four guard stages — `scaffold` → `verify-plan` → `verify-ai-output` → `review-pr` — with `1/4`…`4/4` numbering and a single pass/fail summary. Each stage runs only when its input is present (`--name` → scaffold, `--plan` → verify-plan, `--answer` → verify-ai-output, the git diff → review-pr); a stage with no input is skipped and does not fail the run. New zero-dependency, bundle-safe `src/create/orchestrate.js` (`orchestrate`) delegates to the real stage modules — no logic duplication. CLI supports `--name`, `--plan`, `--answer`, `--staged`/`--base`, and `--json`, and exits non-zero when a ran stage fails. This completes the grounded-creation loop: every root cause (1–4) is closed and all four guard stages are now sequenced by one command.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [7.12.0] — 2026-06-17
|
|
23
|
+
|
|
24
|
+
Minor release — `sigmap review-pr` (grounded codegen, Gap 2 — last guard stage).
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **`sigmap review-pr` — diff audit for drift + side effects (#313):** the last guard stage of the `sigmap create` pipeline (IMPL.md §6 step 4). After a PR is opened, it audits the diff for **scope drift** (too many distinct top-level dirs), **god-node edits** (changed files with transitive dependents above a threshold, via the impact graph), **missing tests** (a changed source file with no matching changed test), and **security-sensitive files** (`.env*`, auth, secrets, `package.json`/lockfiles, `.github/workflows/**`, Dockerfiles, keys). New zero-dependency, bundle-safe `src/review/review-pr.js` (`reviewPr`); deletions are excluded from the source/security checks. CLI `review-pr [--base <ref>] [--staged] [--json]` collects the diff via shell-free git and exits non-zero when any finding is present (CI-gate). With this, all four create-pipeline guard stages exist (`scaffold` → `verify-plan` → `verify-ai-output` → `review-pr`); the `sigmap create` orchestrator remains the final follow-up.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
13
31
|
## [7.11.0] — 2026-06-17
|
|
14
32
|
|
|
15
33
|
Minor release — `sigmap verify-plan` (grounded codegen, Gap 2).
|
package/gen-context.js
CHANGED
|
@@ -27,6 +27,206 @@ function __require(key) {
|
|
|
27
27
|
// ── ./src/scaffold/propose ──
|
|
28
28
|
// ── ./src/plan/verify-plan ──
|
|
29
29
|
// ── ./src/cache/freshen ──
|
|
30
|
+
// ── ./src/review/review-pr ──
|
|
31
|
+
// ── ./src/create/orchestrate ──
|
|
32
|
+
__factories["./src/create/orchestrate"] = function(module, exports) {
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* create orchestrator (IMPL.md §6.2 — the capstone of the grounded-creation loop).
|
|
36
|
+
*
|
|
37
|
+
* Sequences the four guard stages — scaffold → verify-plan → verify-ai-output →
|
|
38
|
+
* review-pr — in one pass with `n/4` numbering and a single pass/fail summary.
|
|
39
|
+
* The agent does the LLM writing between stages; `create` runs the deterministic
|
|
40
|
+
* guards it owns. A stage runs only when its input is present (else it is
|
|
41
|
+
* skipped, which does not fail the run). Zero-dependency, bundle-safe; delegates
|
|
42
|
+
* to the real stage modules.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const { proposeScaffold } = __require('./src/scaffold/propose');
|
|
46
|
+
const { verifyPlan } = __require('./src/plan/verify-plan');
|
|
47
|
+
const { verify } = __require('./src/verify/hallucination-guard');
|
|
48
|
+
const { reviewPr } = __require('./src/review/review-pr');
|
|
49
|
+
|
|
50
|
+
const TOTAL = 4;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Run the create pipeline over whatever inputs are available.
|
|
54
|
+
* @param {object} ctx
|
|
55
|
+
* @param {string} [ctx.task] free-text task label (echoed back)
|
|
56
|
+
* @param {string} [ctx.name] module name → enables the scaffold stage
|
|
57
|
+
* @param {object} [ctx.conventions] an `extractConventions` result (for scaffold)
|
|
58
|
+
* @param {object} [ctx.scaffoldOpts] options forwarded to `proposeScaffold`
|
|
59
|
+
* @param {string} [ctx.plan] plan markdown → enables verify-plan
|
|
60
|
+
* @param {string} [ctx.answer] AI answer markdown → enables verify-ai-output
|
|
61
|
+
* @param {Array<{path:string,status:string}>} [ctx.changedFiles] → enables review-pr
|
|
62
|
+
* @param {string} cwd repo root
|
|
63
|
+
* @returns {{ task: string|null, steps: object[], summary: object }}
|
|
64
|
+
*/
|
|
65
|
+
function orchestrate(ctx = {}, cwd) {
|
|
66
|
+
const steps = [];
|
|
67
|
+
|
|
68
|
+
// 1/4 — scaffold (needs a name + conventions)
|
|
69
|
+
if (ctx.name && ctx.conventions) {
|
|
70
|
+
const d = proposeScaffold(ctx.name, ctx.conventions, ctx.scaffoldOpts || {});
|
|
71
|
+
steps.push({ n: 1, total: TOTAL, name: 'scaffold', ran: true, ok: !!d.ok, skipped: false, detail: d });
|
|
72
|
+
} else {
|
|
73
|
+
steps.push({ n: 1, total: TOTAL, name: 'scaffold', ran: false, ok: null, skipped: true, reason: 'no --name' });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2/4 — verify-plan (needs a plan)
|
|
77
|
+
if (ctx.plan != null && String(ctx.plan).trim() !== '') {
|
|
78
|
+
const r = verifyPlan(ctx.plan, cwd);
|
|
79
|
+
steps.push({ n: 2, total: TOTAL, name: 'verify-plan', ran: true, ok: !!r.summary.ok, skipped: false, detail: r });
|
|
80
|
+
} else {
|
|
81
|
+
steps.push({ n: 2, total: TOTAL, name: 'verify-plan', ran: false, ok: null, skipped: true, reason: 'no --plan' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3/4 — verify-ai-output (needs an answer)
|
|
85
|
+
if (ctx.answer != null && String(ctx.answer).trim() !== '') {
|
|
86
|
+
const r = verify(ctx.answer, cwd);
|
|
87
|
+
steps.push({ n: 3, total: TOTAL, name: 'verify-ai-output', ran: true, ok: r.summary.total === 0, skipped: false, detail: r });
|
|
88
|
+
} else {
|
|
89
|
+
steps.push({ n: 3, total: TOTAL, name: 'verify-ai-output', ran: false, ok: null, skipped: true, reason: 'no --answer' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 4/4 — review-pr (needs changed files)
|
|
93
|
+
if (Array.isArray(ctx.changedFiles) && ctx.changedFiles.length) {
|
|
94
|
+
const r = reviewPr(ctx.changedFiles, cwd);
|
|
95
|
+
steps.push({ n: 4, total: TOTAL, name: 'review-pr', ran: true, ok: !!r.summary.ok, skipped: false, detail: r });
|
|
96
|
+
} else {
|
|
97
|
+
steps.push({ n: 4, total: TOTAL, name: 'review-pr', ran: false, ok: null, skipped: true, reason: 'no changes' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const ran = steps.filter((s) => s.ran);
|
|
101
|
+
const passed = ran.filter((s) => s.ok).length;
|
|
102
|
+
const failed = ran.length - passed;
|
|
103
|
+
return {
|
|
104
|
+
task: ctx.task || null,
|
|
105
|
+
steps,
|
|
106
|
+
summary: {
|
|
107
|
+
total: TOTAL,
|
|
108
|
+
ran: ran.length,
|
|
109
|
+
skipped: steps.length - ran.length,
|
|
110
|
+
passed,
|
|
111
|
+
failed,
|
|
112
|
+
ok: failed === 0,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { orchestrate, TOTAL };
|
|
118
|
+
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
__factories["./src/review/review-pr"] = function(module, exports) {
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* review-pr (IMPL.md §6 step 4 — last guard stage of the `create` pipeline).
|
|
125
|
+
*
|
|
126
|
+
* Audits a diff for drift and side effects after a PR is opened: scope drift,
|
|
127
|
+
* edits to high-fan-in "god-node" files, source changes without matching tests,
|
|
128
|
+
* and changes to security-sensitive files. Pure (takes a changed-file list),
|
|
129
|
+
* zero-dependency, bundle-safe; reuses the impact graph for blast radius.
|
|
130
|
+
*/
|
|
131
|
+
|
|
132
|
+
const path = require('path');
|
|
133
|
+
const { analyzeImpact } = __require('./src/graph/impact');
|
|
134
|
+
|
|
135
|
+
const SECURITY_PATTERNS = [
|
|
136
|
+
/(^|\/)\.env(\.|$)/i,
|
|
137
|
+
/(^|\/)(secrets?|credentials?)(\/|\.|$)/i,
|
|
138
|
+
/(^|\/)auth(\/|\.|-)/i,
|
|
139
|
+
/(^|\/)package(-lock)?\.json$/,
|
|
140
|
+
/(^|\/)(yarn\.lock|pnpm-lock\.yaml)$/,
|
|
141
|
+
/(^|\/)\.github\/workflows\//,
|
|
142
|
+
/(^|\/)Dockerfile/i,
|
|
143
|
+
/\.(pem|key|crt|p12)$/i,
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
const SRC_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.rb', '.php']);
|
|
147
|
+
const GOD_NODE_THRESHOLD = 15; // transitive dependents → high-fan-in "god node"
|
|
148
|
+
const SCOPE_DIR_THRESHOLD = 5; // distinct top-level dirs → scope drift
|
|
149
|
+
|
|
150
|
+
function isTestFile(p) {
|
|
151
|
+
return /\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.(py|go)$|(^|\/)(tests?|__tests__|spec)\//.test(p);
|
|
152
|
+
}
|
|
153
|
+
function isSource(p) {
|
|
154
|
+
return SRC_EXTS.has(path.extname(p).toLowerCase()) && !isTestFile(p);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Audit a changed-file list.
|
|
159
|
+
* @param {Array<{path:string,status:string}>|string[]} changedFiles
|
|
160
|
+
* @param {string} cwd
|
|
161
|
+
* @param {object} [opts]
|
|
162
|
+
* @param {number} [opts.godNodeThreshold=15]
|
|
163
|
+
* @param {number} [opts.scopeThreshold=5]
|
|
164
|
+
* @returns {{ findings: object[], blast: object[], summary: object }}
|
|
165
|
+
*/
|
|
166
|
+
function reviewPr(changedFiles, cwd, opts = {}) {
|
|
167
|
+
const godThreshold = opts.godNodeThreshold != null ? opts.godNodeThreshold : GOD_NODE_THRESHOLD;
|
|
168
|
+
const scopeThreshold = opts.scopeThreshold != null ? opts.scopeThreshold : SCOPE_DIR_THRESHOLD;
|
|
169
|
+
const files = (changedFiles || []).map((f) => (typeof f === 'string' ? { path: f, status: 'M' } : f));
|
|
170
|
+
|
|
171
|
+
const findings = [];
|
|
172
|
+
const paths = files.map((f) => f.path);
|
|
173
|
+
const live = files.filter((f) => f.status !== 'D');
|
|
174
|
+
const srcChanged = live.filter((f) => isSource(f.path)).map((f) => f.path);
|
|
175
|
+
const testChanged = paths.filter(isTestFile);
|
|
176
|
+
|
|
177
|
+
// 1. Missing tests: a changed source file with no matching changed test file.
|
|
178
|
+
for (const s of srcChanged) {
|
|
179
|
+
const stem = path.basename(s).replace(/\.[^.]+$/, '');
|
|
180
|
+
const covered = testChanged.some((t) => path.basename(t).includes(stem));
|
|
181
|
+
if (!covered) findings.push({ type: 'missing-tests', file: s, severity: 'warn' });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 2. Security-sensitive files.
|
|
185
|
+
for (const f of live) {
|
|
186
|
+
if (SECURITY_PATTERNS.some((re) => re.test(f.path))) {
|
|
187
|
+
findings.push({ type: 'security-file', file: f.path, severity: 'warn' });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 3. God-node edits (high blast radius).
|
|
192
|
+
const blast = [];
|
|
193
|
+
if (srcChanged.length) {
|
|
194
|
+
let impacts = [];
|
|
195
|
+
try { impacts = analyzeImpact(srcChanged, cwd, {}); } catch (_) { impacts = []; }
|
|
196
|
+
for (const { file, impact } of impacts) {
|
|
197
|
+
blast.push({ file, totalImpact: impact.totalImpact });
|
|
198
|
+
if (impact.totalImpact > godThreshold) {
|
|
199
|
+
findings.push({ type: 'god-node', file, count: impact.totalImpact, severity: 'warn' });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
blast.sort((a, b) => b.totalImpact - a.totalImpact);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 4. Scope drift: distinct top-level directories touched.
|
|
206
|
+
const dirs = [...new Set(paths.map((p) => (p.includes('/') ? p.split('/')[0] : '.')))];
|
|
207
|
+
if (dirs.length > scopeThreshold) {
|
|
208
|
+
findings.push({ type: 'scope-drift', dirs, count: dirs.length, threshold: scopeThreshold, severity: 'warn' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const byType = findings.reduce((a, f) => { a[f.type] = (a[f.type] || 0) + 1; return a; }, {});
|
|
212
|
+
return {
|
|
213
|
+
findings,
|
|
214
|
+
blast,
|
|
215
|
+
summary: {
|
|
216
|
+
filesChanged: files.length,
|
|
217
|
+
sourceChanged: srcChanged.length,
|
|
218
|
+
testsChanged: testChanged.length,
|
|
219
|
+
findings: findings.length,
|
|
220
|
+
byType,
|
|
221
|
+
ok: findings.length === 0,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { reviewPr, SECURITY_PATTERNS, GOD_NODE_THRESHOLD, SCOPE_DIR_THRESHOLD };
|
|
227
|
+
|
|
228
|
+
};
|
|
229
|
+
|
|
30
230
|
__factories["./src/cache/freshen"] = function(module, exports) {
|
|
31
231
|
|
|
32
232
|
/**
|
|
@@ -7259,7 +7459,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
7259
7459
|
|
|
7260
7460
|
const SERVER_INFO = {
|
|
7261
7461
|
name: 'sigmap',
|
|
7262
|
-
version: '7.
|
|
7462
|
+
version: '7.13.0',
|
|
7263
7463
|
description: 'SigMap MCP server — code signatures on demand',
|
|
7264
7464
|
};
|
|
7265
7465
|
|
|
@@ -12937,7 +13137,7 @@ function __tryGit(args, opts = {}) {
|
|
|
12937
13137
|
catch (_) { return ''; }
|
|
12938
13138
|
}
|
|
12939
13139
|
|
|
12940
|
-
const VERSION = '7.
|
|
13140
|
+
const VERSION = '7.13.0';
|
|
12941
13141
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
12942
13142
|
|
|
12943
13143
|
function requireSourceOrBundled(key) {
|
|
@@ -16389,6 +16589,125 @@ function main() {
|
|
|
16389
16589
|
process.exit(1);
|
|
16390
16590
|
}
|
|
16391
16591
|
|
|
16592
|
+
// Gap 2 (step 4): `sigmap review-pr` — diff audit (scope drift, god-nodes,
|
|
16593
|
+
// missing tests, security-sensitive files). Last guard stage of `create`.
|
|
16594
|
+
if (args[0] === 'review-pr') {
|
|
16595
|
+
const jsonOut = args.includes('--json');
|
|
16596
|
+
const staged = args.includes('--staged');
|
|
16597
|
+
const baseIdx = args.indexOf('--base');
|
|
16598
|
+
const baseArg = baseIdx !== -1 && args[baseIdx + 1] && !args[baseIdx + 1].startsWith('--') ? args[baseIdx + 1] : null;
|
|
16599
|
+
|
|
16600
|
+
let nameStatus = '';
|
|
16601
|
+
if (staged) {
|
|
16602
|
+
nameStatus = __tryGit(['diff', '--cached', '--name-status'], { cwd });
|
|
16603
|
+
} else {
|
|
16604
|
+
let base = baseArg;
|
|
16605
|
+
if (!base) {
|
|
16606
|
+
for (const ref of ['main', 'develop']) {
|
|
16607
|
+
if (__tryGit(['rev-parse', '--verify', '--quiet', ref], { cwd })) { base = ref; break; }
|
|
16608
|
+
}
|
|
16609
|
+
}
|
|
16610
|
+
const mb = base ? (__tryGit(['merge-base', 'HEAD', base], { cwd }) || base) : 'HEAD~1';
|
|
16611
|
+
nameStatus = __tryGit(['diff', '--name-status', `${mb}..HEAD`], { cwd });
|
|
16612
|
+
}
|
|
16613
|
+
|
|
16614
|
+
const changedFiles = nameStatus.split('\n').filter(Boolean).map((line) => {
|
|
16615
|
+
const parts = line.split('\t');
|
|
16616
|
+
const status = parts[0][0]; // M/A/D/R…
|
|
16617
|
+
const file = parts[parts.length - 1]; // rename → new path
|
|
16618
|
+
return { path: file, status };
|
|
16619
|
+
});
|
|
16620
|
+
|
|
16621
|
+
const { reviewPr } = requireSourceOrBundled('./src/review/review-pr');
|
|
16622
|
+
const result = reviewPr(changedFiles, cwd, {});
|
|
16623
|
+
|
|
16624
|
+
if (jsonOut) {
|
|
16625
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
16626
|
+
process.exit(result.summary.ok ? 0 : 1);
|
|
16627
|
+
}
|
|
16628
|
+
|
|
16629
|
+
const s = result.summary;
|
|
16630
|
+
console.log(`[sigmap] review-pr — ${s.filesChanged} file(s) changed (${s.sourceChanged} source, ${s.testsChanged} test)`);
|
|
16631
|
+
if (s.findings === 0) {
|
|
16632
|
+
console.log(' ✓ no findings — scope, tests, blast radius, and sensitive files all clear');
|
|
16633
|
+
process.exit(0);
|
|
16634
|
+
}
|
|
16635
|
+
const label = { 'missing-tests': 'missing tests', 'security-file': 'security file', 'god-node': 'god node', 'scope-drift': 'scope drift' };
|
|
16636
|
+
for (const f of result.findings) {
|
|
16637
|
+
if (f.type === 'missing-tests') console.log(` ⚠ ${label[f.type]}: ${f.file} changed with no matching test`);
|
|
16638
|
+
else if (f.type === 'security-file') console.log(` ⚠ ${label[f.type]}: ${f.file}`);
|
|
16639
|
+
else if (f.type === 'god-node') console.log(` ⚠ ${label[f.type]}: ${f.file} → ${f.count} dependents`);
|
|
16640
|
+
else if (f.type === 'scope-drift') console.log(` ⚠ ${label[f.type]}: ${f.count} top-level dirs (${f.dirs.join(', ')})`);
|
|
16641
|
+
}
|
|
16642
|
+
console.log(`\n ${s.findings} finding(s)`);
|
|
16643
|
+
process.exit(1);
|
|
16644
|
+
}
|
|
16645
|
+
|
|
16646
|
+
// Gap 2 (capstone): `sigmap create "<task>"` — orchestrate the 4 guard stages
|
|
16647
|
+
// (scaffold → verify-plan → verify-ai-output → review-pr) with n/4 numbering.
|
|
16648
|
+
if (args[0] === 'create') {
|
|
16649
|
+
const jsonOut = args.includes('--json');
|
|
16650
|
+
const task = args[1] && !args[1].startsWith('--') ? args[1] : null;
|
|
16651
|
+
const flagVal = (flag) => { const i = args.indexOf(flag); return i !== -1 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : null; };
|
|
16652
|
+
const nameArg = flagVal('--name');
|
|
16653
|
+
const planFile = flagVal('--plan');
|
|
16654
|
+
const answerFile = flagVal('--answer');
|
|
16655
|
+
const staged = args.includes('--staged');
|
|
16656
|
+
const baseArg = flagVal('--base');
|
|
16657
|
+
|
|
16658
|
+
const ctx = { task };
|
|
16659
|
+
|
|
16660
|
+
if (nameArg) {
|
|
16661
|
+
ctx.name = nameArg;
|
|
16662
|
+
try {
|
|
16663
|
+
const { extractConventions } = requireSourceOrBundled('./src/conventions/extract');
|
|
16664
|
+
ctx.conventions = extractConventions(cwd, buildFileList(cwd, config));
|
|
16665
|
+
} catch (_) {}
|
|
16666
|
+
}
|
|
16667
|
+
for (const [flag, file, key] of [['--plan', planFile, 'plan'], ['--answer', answerFile, 'answer']]) {
|
|
16668
|
+
if (!file) continue;
|
|
16669
|
+
try { ctx[key] = fs.readFileSync(path.resolve(cwd, file), 'utf8'); }
|
|
16670
|
+
catch (e) { console.error(`[sigmap] cannot read ${flag} file: ${e.message}`); process.exit(1); }
|
|
16671
|
+
}
|
|
16672
|
+
|
|
16673
|
+
// review-pr inputs: changed files from git (same collection as review-pr).
|
|
16674
|
+
let nameStatus = '';
|
|
16675
|
+
if (staged) {
|
|
16676
|
+
nameStatus = __tryGit(['diff', '--cached', '--name-status'], { cwd });
|
|
16677
|
+
} else {
|
|
16678
|
+
let base = baseArg;
|
|
16679
|
+
if (!base) {
|
|
16680
|
+
for (const ref of ['main', 'develop']) {
|
|
16681
|
+
if (__tryGit(['rev-parse', '--verify', '--quiet', ref], { cwd })) { base = ref; break; }
|
|
16682
|
+
}
|
|
16683
|
+
}
|
|
16684
|
+
const mb = base ? (__tryGit(['merge-base', 'HEAD', base], { cwd }) || base) : 'HEAD~1';
|
|
16685
|
+
nameStatus = __tryGit(['diff', '--name-status', `${mb}..HEAD`], { cwd });
|
|
16686
|
+
}
|
|
16687
|
+
ctx.changedFiles = nameStatus.split('\n').filter(Boolean).map((line) => {
|
|
16688
|
+
const parts = line.split('\t');
|
|
16689
|
+
return { path: parts[parts.length - 1], status: parts[0][0] };
|
|
16690
|
+
});
|
|
16691
|
+
|
|
16692
|
+
const { orchestrate } = requireSourceOrBundled('./src/create/orchestrate');
|
|
16693
|
+
const result = orchestrate(ctx, cwd);
|
|
16694
|
+
|
|
16695
|
+
if (jsonOut) {
|
|
16696
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
16697
|
+
process.exit(result.summary.ok ? 0 : 1);
|
|
16698
|
+
}
|
|
16699
|
+
|
|
16700
|
+
console.log(`[sigmap] create${result.task ? ` "${result.task}"` : ''} — grounded-creation pipeline`);
|
|
16701
|
+
for (const st of result.steps) {
|
|
16702
|
+
const mark = st.skipped ? '–' : (st.ok ? '✓' : '✗');
|
|
16703
|
+
const status = st.skipped ? `skipped (${st.reason})` : (st.ok ? 'ok' : 'FAILED');
|
|
16704
|
+
console.log(` ${st.n}/${st.total} ${mark} ${st.name.padEnd(16)} ${status}`);
|
|
16705
|
+
}
|
|
16706
|
+
const su = result.summary;
|
|
16707
|
+
console.log(`\n ${su.ran}/${su.total} ran · ${su.passed} passed · ${su.failed} failed · ${su.skipped} skipped`);
|
|
16708
|
+
process.exit(su.ok ? 0 : 1);
|
|
16709
|
+
}
|
|
16710
|
+
|
|
16392
16711
|
// Feature 1: `sigmap explain <file>` — why a file is included or excluded
|
|
16393
16712
|
if (args[0] === 'explain' || args.includes('--explain')) {
|
|
16394
16713
|
const target = args[0] === 'explain'
|
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.13.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.13.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.13.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": {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* create orchestrator (IMPL.md §6.2 — the capstone of the grounded-creation loop).
|
|
5
|
+
*
|
|
6
|
+
* Sequences the four guard stages — scaffold → verify-plan → verify-ai-output →
|
|
7
|
+
* review-pr — in one pass with `n/4` numbering and a single pass/fail summary.
|
|
8
|
+
* The agent does the LLM writing between stages; `create` runs the deterministic
|
|
9
|
+
* guards it owns. A stage runs only when its input is present (else it is
|
|
10
|
+
* skipped, which does not fail the run). Zero-dependency, bundle-safe; delegates
|
|
11
|
+
* to the real stage modules.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { proposeScaffold } = require('../scaffold/propose');
|
|
15
|
+
const { verifyPlan } = require('../plan/verify-plan');
|
|
16
|
+
const { verify } = require('../verify/hallucination-guard');
|
|
17
|
+
const { reviewPr } = require('../review/review-pr');
|
|
18
|
+
|
|
19
|
+
const TOTAL = 4;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Run the create pipeline over whatever inputs are available.
|
|
23
|
+
* @param {object} ctx
|
|
24
|
+
* @param {string} [ctx.task] free-text task label (echoed back)
|
|
25
|
+
* @param {string} [ctx.name] module name → enables the scaffold stage
|
|
26
|
+
* @param {object} [ctx.conventions] an `extractConventions` result (for scaffold)
|
|
27
|
+
* @param {object} [ctx.scaffoldOpts] options forwarded to `proposeScaffold`
|
|
28
|
+
* @param {string} [ctx.plan] plan markdown → enables verify-plan
|
|
29
|
+
* @param {string} [ctx.answer] AI answer markdown → enables verify-ai-output
|
|
30
|
+
* @param {Array<{path:string,status:string}>} [ctx.changedFiles] → enables review-pr
|
|
31
|
+
* @param {string} cwd repo root
|
|
32
|
+
* @returns {{ task: string|null, steps: object[], summary: object }}
|
|
33
|
+
*/
|
|
34
|
+
function orchestrate(ctx = {}, cwd) {
|
|
35
|
+
const steps = [];
|
|
36
|
+
|
|
37
|
+
// 1/4 — scaffold (needs a name + conventions)
|
|
38
|
+
if (ctx.name && ctx.conventions) {
|
|
39
|
+
const d = proposeScaffold(ctx.name, ctx.conventions, ctx.scaffoldOpts || {});
|
|
40
|
+
steps.push({ n: 1, total: TOTAL, name: 'scaffold', ran: true, ok: !!d.ok, skipped: false, detail: d });
|
|
41
|
+
} else {
|
|
42
|
+
steps.push({ n: 1, total: TOTAL, name: 'scaffold', ran: false, ok: null, skipped: true, reason: 'no --name' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2/4 — verify-plan (needs a plan)
|
|
46
|
+
if (ctx.plan != null && String(ctx.plan).trim() !== '') {
|
|
47
|
+
const r = verifyPlan(ctx.plan, cwd);
|
|
48
|
+
steps.push({ n: 2, total: TOTAL, name: 'verify-plan', ran: true, ok: !!r.summary.ok, skipped: false, detail: r });
|
|
49
|
+
} else {
|
|
50
|
+
steps.push({ n: 2, total: TOTAL, name: 'verify-plan', ran: false, ok: null, skipped: true, reason: 'no --plan' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3/4 — verify-ai-output (needs an answer)
|
|
54
|
+
if (ctx.answer != null && String(ctx.answer).trim() !== '') {
|
|
55
|
+
const r = verify(ctx.answer, cwd);
|
|
56
|
+
steps.push({ n: 3, total: TOTAL, name: 'verify-ai-output', ran: true, ok: r.summary.total === 0, skipped: false, detail: r });
|
|
57
|
+
} else {
|
|
58
|
+
steps.push({ n: 3, total: TOTAL, name: 'verify-ai-output', ran: false, ok: null, skipped: true, reason: 'no --answer' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 4/4 — review-pr (needs changed files)
|
|
62
|
+
if (Array.isArray(ctx.changedFiles) && ctx.changedFiles.length) {
|
|
63
|
+
const r = reviewPr(ctx.changedFiles, cwd);
|
|
64
|
+
steps.push({ n: 4, total: TOTAL, name: 'review-pr', ran: true, ok: !!r.summary.ok, skipped: false, detail: r });
|
|
65
|
+
} else {
|
|
66
|
+
steps.push({ n: 4, total: TOTAL, name: 'review-pr', ran: false, ok: null, skipped: true, reason: 'no changes' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ran = steps.filter((s) => s.ran);
|
|
70
|
+
const passed = ran.filter((s) => s.ok).length;
|
|
71
|
+
const failed = ran.length - passed;
|
|
72
|
+
return {
|
|
73
|
+
task: ctx.task || null,
|
|
74
|
+
steps,
|
|
75
|
+
summary: {
|
|
76
|
+
total: TOTAL,
|
|
77
|
+
ran: ran.length,
|
|
78
|
+
skipped: steps.length - ran.length,
|
|
79
|
+
passed,
|
|
80
|
+
failed,
|
|
81
|
+
ok: failed === 0,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { orchestrate, TOTAL };
|
package/src/mcp/server.js
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* review-pr (IMPL.md §6 step 4 — last guard stage of the `create` pipeline).
|
|
5
|
+
*
|
|
6
|
+
* Audits a diff for drift and side effects after a PR is opened: scope drift,
|
|
7
|
+
* edits to high-fan-in "god-node" files, source changes without matching tests,
|
|
8
|
+
* and changes to security-sensitive files. Pure (takes a changed-file list),
|
|
9
|
+
* zero-dependency, bundle-safe; reuses the impact graph for blast radius.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { analyzeImpact } = require('../graph/impact');
|
|
14
|
+
|
|
15
|
+
const SECURITY_PATTERNS = [
|
|
16
|
+
/(^|\/)\.env(\.|$)/i,
|
|
17
|
+
/(^|\/)(secrets?|credentials?)(\/|\.|$)/i,
|
|
18
|
+
/(^|\/)auth(\/|\.|-)/i,
|
|
19
|
+
/(^|\/)package(-lock)?\.json$/,
|
|
20
|
+
/(^|\/)(yarn\.lock|pnpm-lock\.yaml)$/,
|
|
21
|
+
/(^|\/)\.github\/workflows\//,
|
|
22
|
+
/(^|\/)Dockerfile/i,
|
|
23
|
+
/\.(pem|key|crt|p12)$/i,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const SRC_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.rb', '.php']);
|
|
27
|
+
const GOD_NODE_THRESHOLD = 15; // transitive dependents → high-fan-in "god node"
|
|
28
|
+
const SCOPE_DIR_THRESHOLD = 5; // distinct top-level dirs → scope drift
|
|
29
|
+
|
|
30
|
+
function isTestFile(p) {
|
|
31
|
+
return /\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.(py|go)$|(^|\/)(tests?|__tests__|spec)\//.test(p);
|
|
32
|
+
}
|
|
33
|
+
function isSource(p) {
|
|
34
|
+
return SRC_EXTS.has(path.extname(p).toLowerCase()) && !isTestFile(p);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Audit a changed-file list.
|
|
39
|
+
* @param {Array<{path:string,status:string}>|string[]} changedFiles
|
|
40
|
+
* @param {string} cwd
|
|
41
|
+
* @param {object} [opts]
|
|
42
|
+
* @param {number} [opts.godNodeThreshold=15]
|
|
43
|
+
* @param {number} [opts.scopeThreshold=5]
|
|
44
|
+
* @returns {{ findings: object[], blast: object[], summary: object }}
|
|
45
|
+
*/
|
|
46
|
+
function reviewPr(changedFiles, cwd, opts = {}) {
|
|
47
|
+
const godThreshold = opts.godNodeThreshold != null ? opts.godNodeThreshold : GOD_NODE_THRESHOLD;
|
|
48
|
+
const scopeThreshold = opts.scopeThreshold != null ? opts.scopeThreshold : SCOPE_DIR_THRESHOLD;
|
|
49
|
+
const files = (changedFiles || []).map((f) => (typeof f === 'string' ? { path: f, status: 'M' } : f));
|
|
50
|
+
|
|
51
|
+
const findings = [];
|
|
52
|
+
const paths = files.map((f) => f.path);
|
|
53
|
+
const live = files.filter((f) => f.status !== 'D');
|
|
54
|
+
const srcChanged = live.filter((f) => isSource(f.path)).map((f) => f.path);
|
|
55
|
+
const testChanged = paths.filter(isTestFile);
|
|
56
|
+
|
|
57
|
+
// 1. Missing tests: a changed source file with no matching changed test file.
|
|
58
|
+
for (const s of srcChanged) {
|
|
59
|
+
const stem = path.basename(s).replace(/\.[^.]+$/, '');
|
|
60
|
+
const covered = testChanged.some((t) => path.basename(t).includes(stem));
|
|
61
|
+
if (!covered) findings.push({ type: 'missing-tests', file: s, severity: 'warn' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2. Security-sensitive files.
|
|
65
|
+
for (const f of live) {
|
|
66
|
+
if (SECURITY_PATTERNS.some((re) => re.test(f.path))) {
|
|
67
|
+
findings.push({ type: 'security-file', file: f.path, severity: 'warn' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. God-node edits (high blast radius).
|
|
72
|
+
const blast = [];
|
|
73
|
+
if (srcChanged.length) {
|
|
74
|
+
let impacts = [];
|
|
75
|
+
try { impacts = analyzeImpact(srcChanged, cwd, {}); } catch (_) { impacts = []; }
|
|
76
|
+
for (const { file, impact } of impacts) {
|
|
77
|
+
blast.push({ file, totalImpact: impact.totalImpact });
|
|
78
|
+
if (impact.totalImpact > godThreshold) {
|
|
79
|
+
findings.push({ type: 'god-node', file, count: impact.totalImpact, severity: 'warn' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
blast.sort((a, b) => b.totalImpact - a.totalImpact);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 4. Scope drift: distinct top-level directories touched.
|
|
86
|
+
const dirs = [...new Set(paths.map((p) => (p.includes('/') ? p.split('/')[0] : '.')))];
|
|
87
|
+
if (dirs.length > scopeThreshold) {
|
|
88
|
+
findings.push({ type: 'scope-drift', dirs, count: dirs.length, threshold: scopeThreshold, severity: 'warn' });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const byType = findings.reduce((a, f) => { a[f.type] = (a[f.type] || 0) + 1; return a; }, {});
|
|
92
|
+
return {
|
|
93
|
+
findings,
|
|
94
|
+
blast,
|
|
95
|
+
summary: {
|
|
96
|
+
filesChanged: files.length,
|
|
97
|
+
sourceChanged: srcChanged.length,
|
|
98
|
+
testsChanged: testChanged.length,
|
|
99
|
+
findings: findings.length,
|
|
100
|
+
byType,
|
|
101
|
+
ok: findings.length === 0,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { reviewPr, SECURITY_PATTERNS, GOD_NODE_THRESHOLD, SCOPE_DIR_THRESHOLD };
|