sigmap 7.11.0 → 7.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,15 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [7.12.0] — 2026-06-17
14
+
15
+ Minor release — `sigmap review-pr` (grounded codegen, Gap 2 — last guard stage).
16
+
17
+ ### Added
18
+ - **`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.
19
+
20
+ ---
21
+
13
22
  ## [7.11.0] — 2026-06-17
14
23
 
15
24
  Minor release — `sigmap verify-plan` (grounded codegen, Gap 2).
package/gen-context.js CHANGED
@@ -27,6 +27,116 @@ 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
+ __factories["./src/review/review-pr"] = function(module, exports) {
32
+
33
+ /**
34
+ * review-pr (IMPL.md §6 step 4 — last guard stage of the `create` pipeline).
35
+ *
36
+ * Audits a diff for drift and side effects after a PR is opened: scope drift,
37
+ * edits to high-fan-in "god-node" files, source changes without matching tests,
38
+ * and changes to security-sensitive files. Pure (takes a changed-file list),
39
+ * zero-dependency, bundle-safe; reuses the impact graph for blast radius.
40
+ */
41
+
42
+ const path = require('path');
43
+ const { analyzeImpact } = __require('./src/graph/impact');
44
+
45
+ const SECURITY_PATTERNS = [
46
+ /(^|\/)\.env(\.|$)/i,
47
+ /(^|\/)(secrets?|credentials?)(\/|\.|$)/i,
48
+ /(^|\/)auth(\/|\.|-)/i,
49
+ /(^|\/)package(-lock)?\.json$/,
50
+ /(^|\/)(yarn\.lock|pnpm-lock\.yaml)$/,
51
+ /(^|\/)\.github\/workflows\//,
52
+ /(^|\/)Dockerfile/i,
53
+ /\.(pem|key|crt|p12)$/i,
54
+ ];
55
+
56
+ const SRC_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.rb', '.php']);
57
+ const GOD_NODE_THRESHOLD = 15; // transitive dependents → high-fan-in "god node"
58
+ const SCOPE_DIR_THRESHOLD = 5; // distinct top-level dirs → scope drift
59
+
60
+ function isTestFile(p) {
61
+ return /\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.(py|go)$|(^|\/)(tests?|__tests__|spec)\//.test(p);
62
+ }
63
+ function isSource(p) {
64
+ return SRC_EXTS.has(path.extname(p).toLowerCase()) && !isTestFile(p);
65
+ }
66
+
67
+ /**
68
+ * Audit a changed-file list.
69
+ * @param {Array<{path:string,status:string}>|string[]} changedFiles
70
+ * @param {string} cwd
71
+ * @param {object} [opts]
72
+ * @param {number} [opts.godNodeThreshold=15]
73
+ * @param {number} [opts.scopeThreshold=5]
74
+ * @returns {{ findings: object[], blast: object[], summary: object }}
75
+ */
76
+ function reviewPr(changedFiles, cwd, opts = {}) {
77
+ const godThreshold = opts.godNodeThreshold != null ? opts.godNodeThreshold : GOD_NODE_THRESHOLD;
78
+ const scopeThreshold = opts.scopeThreshold != null ? opts.scopeThreshold : SCOPE_DIR_THRESHOLD;
79
+ const files = (changedFiles || []).map((f) => (typeof f === 'string' ? { path: f, status: 'M' } : f));
80
+
81
+ const findings = [];
82
+ const paths = files.map((f) => f.path);
83
+ const live = files.filter((f) => f.status !== 'D');
84
+ const srcChanged = live.filter((f) => isSource(f.path)).map((f) => f.path);
85
+ const testChanged = paths.filter(isTestFile);
86
+
87
+ // 1. Missing tests: a changed source file with no matching changed test file.
88
+ for (const s of srcChanged) {
89
+ const stem = path.basename(s).replace(/\.[^.]+$/, '');
90
+ const covered = testChanged.some((t) => path.basename(t).includes(stem));
91
+ if (!covered) findings.push({ type: 'missing-tests', file: s, severity: 'warn' });
92
+ }
93
+
94
+ // 2. Security-sensitive files.
95
+ for (const f of live) {
96
+ if (SECURITY_PATTERNS.some((re) => re.test(f.path))) {
97
+ findings.push({ type: 'security-file', file: f.path, severity: 'warn' });
98
+ }
99
+ }
100
+
101
+ // 3. God-node edits (high blast radius).
102
+ const blast = [];
103
+ if (srcChanged.length) {
104
+ let impacts = [];
105
+ try { impacts = analyzeImpact(srcChanged, cwd, {}); } catch (_) { impacts = []; }
106
+ for (const { file, impact } of impacts) {
107
+ blast.push({ file, totalImpact: impact.totalImpact });
108
+ if (impact.totalImpact > godThreshold) {
109
+ findings.push({ type: 'god-node', file, count: impact.totalImpact, severity: 'warn' });
110
+ }
111
+ }
112
+ blast.sort((a, b) => b.totalImpact - a.totalImpact);
113
+ }
114
+
115
+ // 4. Scope drift: distinct top-level directories touched.
116
+ const dirs = [...new Set(paths.map((p) => (p.includes('/') ? p.split('/')[0] : '.')))];
117
+ if (dirs.length > scopeThreshold) {
118
+ findings.push({ type: 'scope-drift', dirs, count: dirs.length, threshold: scopeThreshold, severity: 'warn' });
119
+ }
120
+
121
+ const byType = findings.reduce((a, f) => { a[f.type] = (a[f.type] || 0) + 1; return a; }, {});
122
+ return {
123
+ findings,
124
+ blast,
125
+ summary: {
126
+ filesChanged: files.length,
127
+ sourceChanged: srcChanged.length,
128
+ testsChanged: testChanged.length,
129
+ findings: findings.length,
130
+ byType,
131
+ ok: findings.length === 0,
132
+ },
133
+ };
134
+ }
135
+
136
+ module.exports = { reviewPr, SECURITY_PATTERNS, GOD_NODE_THRESHOLD, SCOPE_DIR_THRESHOLD };
137
+
138
+ };
139
+
30
140
  __factories["./src/cache/freshen"] = function(module, exports) {
31
141
 
32
142
  /**
@@ -7259,7 +7369,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
7259
7369
 
7260
7370
  const SERVER_INFO = {
7261
7371
  name: 'sigmap',
7262
- version: '7.11.0',
7372
+ version: '7.12.0',
7263
7373
  description: 'SigMap MCP server — code signatures on demand',
7264
7374
  };
7265
7375
 
@@ -12937,7 +13047,7 @@ function __tryGit(args, opts = {}) {
12937
13047
  catch (_) { return ''; }
12938
13048
  }
12939
13049
 
12940
- const VERSION = '7.11.0';
13050
+ const VERSION = '7.12.0';
12941
13051
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
12942
13052
 
12943
13053
  function requireSourceOrBundled(key) {
@@ -16389,6 +16499,60 @@ function main() {
16389
16499
  process.exit(1);
16390
16500
  }
16391
16501
 
16502
+ // Gap 2 (step 4): `sigmap review-pr` — diff audit (scope drift, god-nodes,
16503
+ // missing tests, security-sensitive files). Last guard stage of `create`.
16504
+ if (args[0] === 'review-pr') {
16505
+ const jsonOut = args.includes('--json');
16506
+ const staged = args.includes('--staged');
16507
+ const baseIdx = args.indexOf('--base');
16508
+ const baseArg = baseIdx !== -1 && args[baseIdx + 1] && !args[baseIdx + 1].startsWith('--') ? args[baseIdx + 1] : null;
16509
+
16510
+ let nameStatus = '';
16511
+ if (staged) {
16512
+ nameStatus = __tryGit(['diff', '--cached', '--name-status'], { cwd });
16513
+ } else {
16514
+ let base = baseArg;
16515
+ if (!base) {
16516
+ for (const ref of ['main', 'develop']) {
16517
+ if (__tryGit(['rev-parse', '--verify', '--quiet', ref], { cwd })) { base = ref; break; }
16518
+ }
16519
+ }
16520
+ const mb = base ? (__tryGit(['merge-base', 'HEAD', base], { cwd }) || base) : 'HEAD~1';
16521
+ nameStatus = __tryGit(['diff', '--name-status', `${mb}..HEAD`], { cwd });
16522
+ }
16523
+
16524
+ const changedFiles = nameStatus.split('\n').filter(Boolean).map((line) => {
16525
+ const parts = line.split('\t');
16526
+ const status = parts[0][0]; // M/A/D/R…
16527
+ const file = parts[parts.length - 1]; // rename → new path
16528
+ return { path: file, status };
16529
+ });
16530
+
16531
+ const { reviewPr } = requireSourceOrBundled('./src/review/review-pr');
16532
+ const result = reviewPr(changedFiles, cwd, {});
16533
+
16534
+ if (jsonOut) {
16535
+ process.stdout.write(JSON.stringify(result) + '\n');
16536
+ process.exit(result.summary.ok ? 0 : 1);
16537
+ }
16538
+
16539
+ const s = result.summary;
16540
+ console.log(`[sigmap] review-pr — ${s.filesChanged} file(s) changed (${s.sourceChanged} source, ${s.testsChanged} test)`);
16541
+ if (s.findings === 0) {
16542
+ console.log(' ✓ no findings — scope, tests, blast radius, and sensitive files all clear');
16543
+ process.exit(0);
16544
+ }
16545
+ const label = { 'missing-tests': 'missing tests', 'security-file': 'security file', 'god-node': 'god node', 'scope-drift': 'scope drift' };
16546
+ for (const f of result.findings) {
16547
+ if (f.type === 'missing-tests') console.log(` ⚠ ${label[f.type]}: ${f.file} changed with no matching test`);
16548
+ else if (f.type === 'security-file') console.log(` ⚠ ${label[f.type]}: ${f.file}`);
16549
+ else if (f.type === 'god-node') console.log(` ⚠ ${label[f.type]}: ${f.file} → ${f.count} dependents`);
16550
+ else if (f.type === 'scope-drift') console.log(` ⚠ ${label[f.type]}: ${f.count} top-level dirs (${f.dirs.join(', ')})`);
16551
+ }
16552
+ console.log(`\n ${s.findings} finding(s)`);
16553
+ process.exit(1);
16554
+ }
16555
+
16392
16556
  // Feature 1: `sigmap explain <file>` — why a file is included or excluded
16393
16557
  if (args[0] === 'explain' || args.includes('--explain')) {
16394
16558
  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.11.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.12.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.11.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.12.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.11.0",
3
+ "version": "7.12.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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "7.11.0",
3
+ "version": "7.12.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "7.11.0",
3
+ "version": "7.12.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '7.11.0',
21
+ version: '7.12.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -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 };