sigmap 7.31.0 → 8.1.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.
@@ -127,9 +127,13 @@ function formatOutput(sections) {
127
127
  ];
128
128
 
129
129
  const parts = [
130
- { key: 'imports', header: '### Import graph', content: sections.imports },
131
- { key: 'classes', header: '### Class hierarchy', content: sections.classes },
132
- { key: 'routes', header: '### Route table', content: sections.routes },
130
+ { key: 'imports', header: '### Import graph', content: sections.imports },
131
+ { key: 'classes', header: '### Class hierarchy', content: sections.classes },
132
+ { key: 'routes', header: '### Route table', content: sections.routes },
133
+ { key: 'env', header: '### Environment variables', content: sections.env },
134
+ { key: 'buildci', header: '### Build & CI', content: sections.buildci },
135
+ { key: 'manifests', header: '### Config & manifests', content: sections.manifests },
136
+ { key: 'migrations', header: '### Database migrations', content: sections.migrations },
133
137
  ];
134
138
 
135
139
  for (const { header, content } of parts) {
@@ -165,9 +169,13 @@ function main() {
165
169
  }
166
170
 
167
171
  const sections = {
168
- imports: runAnalyzer('import-graph', files, cwd),
169
- classes: runAnalyzer('class-hierarchy', files, cwd),
170
- routes: runAnalyzer('route-table', files, cwd),
172
+ imports: runAnalyzer('import-graph', files, cwd),
173
+ classes: runAnalyzer('class-hierarchy', files, cwd),
174
+ routes: runAnalyzer('route-table', files, cwd),
175
+ env: runAnalyzer('env-schema', files, cwd),
176
+ buildci: runAnalyzer('build-ci', files, cwd),
177
+ manifests: runAnalyzer('config-manifest', files, cwd),
178
+ migrations: runAnalyzer('migrations', files, cwd),
171
179
  };
172
180
 
173
181
  const output = formatOutput(sections);
package/llms-full.txt CHANGED
@@ -11,13 +11,13 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 7.31.0 | Benchmark: sigmap-v7.31-main (2026-07-02)
14
+ # Version: 8.1.0 | Benchmark: sigmap-v8.1-main (2026-07-04)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
18
18
  ---
19
19
 
20
- ## Core metrics (benchmark: sigmap-v7.31-main, 2026-07-02)
20
+ ## Core metrics (benchmark: sigmap-v8.1-main, 2026-07-04)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
package/llms.txt CHANGED
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 7.31.0 | Benchmark: sigmap-v7.31-main (2026-07-02)
14
+ # Version: 8.1.0 | Benchmark: sigmap-v8.1-main (2026-07-04)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
@@ -23,7 +23,7 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
23
23
  - No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
24
24
  - Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
25
25
 
26
- ## Core metrics (benchmark: sigmap-v7.31-main, 2026-07-02)
26
+ ## Core metrics (benchmark: sigmap-v8.1-main, 2026-07-04)
27
27
 
28
28
  - hit@5 retrieval: 86.7% vs 13.6% random baseline (6.4× lift)
29
29
  - Token reduction: 97.0% average across benchmark repos
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "7.31.0",
3
+ "version": "8.1.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": {
@@ -27,6 +27,7 @@
27
27
  "benchmark:matrix": "node scripts/run-benchmark-matrix.mjs --save --skip-clone",
28
28
  "benchmark:verify": "node scripts/run-verify-benchmark.mjs",
29
29
  "benchmark:squeeze": "node scripts/run-squeeze-benchmark.mjs --save",
30
+ "benchmark:test-discovery": "node scripts/run-test-discovery-benchmark.mjs --save",
30
31
  "validate:squeeze": "node scripts/run-squeeze-benchmark.mjs --gate",
31
32
  "health": "node gen-context.js --health",
32
33
  "map": "node gen-project-map.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "7.31.0",
3
+ "version": "8.1.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.31.0",
3
+ "version": "8.1.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -33,7 +33,14 @@ const DEFAULT_TOP = 12;
33
33
  const GENERATED_RE = /(^|\/)(dist|build|out|vendor|node_modules)\/|\.(generated|min|bundle)\.|\.(pb|_pb)\.|\.pb\.go$|_pb2\.py$/;
34
34
  const TEST_RE = /(^|\/)(tests?|__tests__|spec|specs)\/|\.(test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(go|py|rb)$/;
35
35
  const CONFIG_RE = /\.(json|ya?ml|toml|ini|conf|config|properties|env)$|(^|\/)(\.?[a-z]+rc)$|\.config\.[a-z]+$/i;
36
- const SECURITY_RE = /(^|\/|[._-])(auth|authn|authz|login|password|passwd|secret|credential|token|session|crypto|cipher|payment|billing|checkout|oauth|jwt|permission|acl|rbac)([._-]|\/|$)/i;
36
+ // DB migrations: framework dirs (Rails/Alembic/Prisma), Flyway `V1__x.sql`,
37
+ // timestamped migration files, and `*_migration.*` naming.
38
+ const MIGRATION_RE = /(^|\/)(migrations?|alembic\/versions|prisma\/migrations)(\/|$)|(^|\/)db\/migrate\/|(^|\/)V\d+(_\d+)*__[^/]+\.(sql|java)$|(^|\/)\d{8,}[_-][^/]+\.(sql|rb|py|js|ts)$|[._-]migration[s]?[._-]/i;
39
+ const PAYMENT_RE = /(^|\/|[._-])(payment|payments|billing|checkout|invoice|invoicing|subscription|stripe|paypal|braintree|charge|refund|payout)([._-]|\/|$)/i;
40
+ const AUTH_RE = /(^|\/|[._-])(auth|authn|authz|login|logout|signin|signup|password|passwd|session|oauth|jwt|permission|permissions|acl|rbac|credential|credentials)([._-]|\/|$)/i;
41
+ const SECURITY_RE = /(^|\/|[._-])(secret|secrets|crypto|cipher|encrypt|decrypt|token|signing|keystore|vault)([._-]|\/|$)/i;
42
+ // Public API surface: `api/` dirs, `public-api`, and module barrel entrypoints.
43
+ const PUBLIC_API_RE = /(^|\/)api(\/|$)|(^|\/)public[-_]?api(\/|$)|(^|\/)index\.(js|ts|mjs|cjs)$/i;
37
44
 
38
45
  /**
39
46
  * Split a signature's ` :start-end` line anchor from its symbol text.
@@ -51,17 +58,25 @@ function parseAnchor(sig) {
51
58
  }
52
59
 
53
60
  /**
54
- * Classify a file into a coarse risk label. Path-based heuristic (v1) — the
55
- * richer label set (C3) lands in v8.5.
61
+ * Classify a file into a risk label (C3, v8.5). Path-based, deterministic.
62
+ * Precedence is strict, most-specific-risk first: a migration touching payments
63
+ * is labeled `migration` (a schema change is the dominant risk), payment/auth
64
+ * outrank the generic `security` bucket, and `config`/`public-api` resolve
65
+ * before the `source` fallback. `test`/`generated` semantics are preserved so
66
+ * existing consumers (findRelatedTests, verifier) keep working.
56
67
  * @param {string} relPath
57
- * @returns {'generated'|'test'|'config'|'security'|'source'}
68
+ * @returns {'generated'|'test'|'migration'|'payment'|'auth'|'security'|'config'|'public-api'|'source'}
58
69
  */
59
70
  function riskLabelFor(relPath) {
60
71
  const p = relPath.replace(/\\/g, '/');
61
72
  if (GENERATED_RE.test(p)) return 'generated';
62
73
  if (TEST_RE.test(p)) return 'test';
74
+ if (MIGRATION_RE.test(p)) return 'migration';
75
+ if (PAYMENT_RE.test(p)) return 'payment';
76
+ if (AUTH_RE.test(p)) return 'auth';
63
77
  if (SECURITY_RE.test(p)) return 'security';
64
78
  if (CONFIG_RE.test(p)) return 'config';
79
+ if (PUBLIC_API_RE.test(p)) return 'public-api';
65
80
  return 'source';
66
81
  }
67
82
 
@@ -72,9 +87,28 @@ function stemOf(relPath) {
72
87
  }
73
88
 
74
89
  /**
75
- * Best-effort impl→test discovery (v1). Matches test files whose stem equals
76
- * the implementation file's stem, by common convention. Deterministic. The
77
- * accuracy-measured discovery (C2) lands in v8.5.
90
+ * Infer the implementation stem a test file targets, by stripping the
91
+ * conventional test affixes across languages (measured in the C2 benchmark):
92
+ * foo.test.js / foo.spec.ts → foo (JS/TS)
93
+ * test_foo.py → foo (Python / pytest)
94
+ * foo_test.go / foo_test.py → foo (Go, unittest)
95
+ * FooTest.java / BarSpec.scala → Foo (JVM, PascalCase)
96
+ * @param {string} relPath
97
+ * @returns {string}
98
+ */
99
+ function testTargetStem(relPath) {
100
+ let s = stemOf(relPath); // strips ext + trailing .test/.spec
101
+ s = s.replace(/^test[_-]/i, ''); // Python: test_foo
102
+ s = s.replace(/[_-]test$/i, ''); // Go / unittest: foo_test
103
+ s = s.replace(/(Tests?|Specs?)$/, ''); // JVM PascalCase: FooTest, BarSpec
104
+ return s;
105
+ }
106
+
107
+ /**
108
+ * Impl→test discovery (C2, v8.5). Matches test files back to their
109
+ * implementation by normalizing conventional test affixes, so JS/TS, Python,
110
+ * Go, and JVM naming conventions all resolve. Deterministic; accuracy is
111
+ * measured by `scripts/run-test-discovery-benchmark.mjs`.
78
112
  * @param {string} relPath
79
113
  * @param {string[]} allFiles - universe of indexed files (relative paths)
80
114
  * @returns {string[]}
@@ -87,7 +121,7 @@ function findRelatedTests(relPath, allFiles) {
87
121
  for (const f of allFiles) {
88
122
  if (f === relPath) continue;
89
123
  if (riskLabelFor(f) !== 'test') continue;
90
- if (stemOf(f).toLowerCase() === stem) out.push(f);
124
+ if (testTargetStem(f).toLowerCase() === stem) out.push(f);
91
125
  }
92
126
  return out.sort();
93
127
  }
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Build & CI extractor (v8.5 C1).
5
+ *
6
+ * Surfaces how the project is built and validated: npm/pnpm/yarn scripts
7
+ * (package.json), GitHub Actions workflows (.github/workflows/*.yml), and
8
+ * Makefile targets. Pure, zero-dependency, deterministic.
9
+ *
10
+ * @param {string[]} files — absolute file paths (unused; roots are read directly)
11
+ * @param {string} cwd — project root
12
+ * @returns {string} formatted markdown table (empty string if none found)
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const MAX_ROWS = 120;
19
+
20
+ function readJson(p) {
21
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
22
+ }
23
+
24
+ function npmScripts(cwd, rows) {
25
+ const pkg = readJson(path.join(cwd, 'package.json'));
26
+ if (!pkg || !pkg.scripts || typeof pkg.scripts !== 'object') return;
27
+ for (const name of Object.keys(pkg.scripts).sort()) {
28
+ rows.push({ kind: 'script', name, detail: 'npm run ' + name });
29
+ }
30
+ }
31
+
32
+ function ciWorkflows(cwd, rows) {
33
+ const dir = path.join(cwd, '.github', 'workflows');
34
+ let entries;
35
+ try { entries = fs.readdirSync(dir); } catch (_) { return; }
36
+ for (const file of entries.sort()) {
37
+ if (!/\.ya?ml$/i.test(file)) continue;
38
+ let content;
39
+ try { content = fs.readFileSync(path.join(dir, file), 'utf8'); } catch (_) { continue; }
40
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
41
+ const name = nameMatch ? nameMatch[1].trim().replace(/^['"]|['"]$/g, '') : file;
42
+ // Trigger events from an `on:` mapping or inline form.
43
+ const onMatch = content.match(/^on:\s*(.*)$/m);
44
+ let triggers = '';
45
+ if (onMatch) {
46
+ if (onMatch[1].trim()) {
47
+ triggers = onMatch[1].replace(/[[\]{}'"]/g, '').trim();
48
+ } else {
49
+ const block = content.slice(onMatch.index);
50
+ const events = [...block.matchAll(/^\s{2,}([a-z_]+):/gm)].map((m) => m[1]);
51
+ triggers = [...new Set(events)].slice(0, 6).join(', ');
52
+ }
53
+ }
54
+ rows.push({ kind: 'ci', name, detail: `${file}${triggers ? ' — ' + triggers : ''}` });
55
+ }
56
+ }
57
+
58
+ function makeTargets(cwd, rows) {
59
+ let content;
60
+ try { content = fs.readFileSync(path.join(cwd, 'Makefile'), 'utf8'); } catch (_) { return; }
61
+ const targets = [];
62
+ for (const line of content.split('\n')) {
63
+ const m = line.match(/^([a-zA-Z0-9_][a-zA-Z0-9_.-]*)\s*:(?!=)/);
64
+ if (m && m[1] !== '.PHONY') targets.push(m[1]);
65
+ }
66
+ for (const t of [...new Set(targets)].sort()) {
67
+ rows.push({ kind: 'make', name: t, detail: 'make ' + t });
68
+ }
69
+ }
70
+
71
+ function analyze(files, cwd) {
72
+ const rows = [];
73
+ npmScripts(cwd, rows);
74
+ ciWorkflows(cwd, rows);
75
+ makeTargets(cwd, rows);
76
+ if (rows.length === 0) return '';
77
+
78
+ const lines = [
79
+ '| Kind | Name | Detail |',
80
+ '|------|------|--------|',
81
+ ];
82
+ for (const r of rows.slice(0, MAX_ROWS)) {
83
+ lines.push(`| ${r.kind} | ${r.name} | ${r.detail} |`);
84
+ }
85
+ if (rows.length > MAX_ROWS) {
86
+ lines.push(`| … | | +${rows.length - MAX_ROWS} more |`);
87
+ }
88
+ return lines.join('\n');
89
+ }
90
+
91
+ module.exports = { analyze };
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Config & package-manifest extractor (v8.5 C1).
5
+ *
6
+ * Surfaces the project's package manifests (name / version / dependency counts)
7
+ * across ecosystems and the notable root config files present. Pure,
8
+ * zero-dependency, deterministic.
9
+ *
10
+ * @param {string[]} files — absolute file paths (unused; roots are read directly)
11
+ * @param {string} cwd — project root
12
+ * @returns {string} formatted markdown table (empty string if none found)
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const CONFIG_FILES = [
19
+ 'tsconfig.json', 'jsconfig.json', '.eslintrc', '.eslintrc.json', '.eslintrc.js',
20
+ '.prettierrc', 'babel.config.js', 'jest.config.js', 'vitest.config.ts',
21
+ 'webpack.config.js', 'vite.config.ts', 'rollup.config.js', 'tailwind.config.js',
22
+ 'docker-compose.yml', 'docker-compose.yaml', 'Dockerfile', '.editorconfig',
23
+ ];
24
+
25
+ function readText(p) { try { return fs.readFileSync(p, 'utf8'); } catch (_) { return null; } }
26
+ function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; } }
27
+ function count(obj) { return obj && typeof obj === 'object' ? Object.keys(obj).length : 0; }
28
+
29
+ function manifests(cwd, rows) {
30
+ const pkg = readJson(path.join(cwd, 'package.json'));
31
+ if (pkg) {
32
+ const deps = count(pkg.dependencies);
33
+ const dev = count(pkg.devDependencies);
34
+ const id = [pkg.name, pkg.version].filter(Boolean).join('@') || 'package.json';
35
+ rows.push({ manifest: 'package.json (npm)', detail: `${id} · ${deps} deps, ${dev} devDeps` });
36
+ }
37
+
38
+ const pyproject = readText(path.join(cwd, 'pyproject.toml'));
39
+ if (pyproject) {
40
+ const name = (pyproject.match(/^\s*name\s*=\s*["']([^"']+)["']/m) || [])[1];
41
+ const ver = (pyproject.match(/^\s*version\s*=\s*["']([^"']+)["']/m) || [])[1];
42
+ rows.push({ manifest: 'pyproject.toml (python)', detail: [name, ver].filter(Boolean).join('@') || 'present' });
43
+ } else if (readText(path.join(cwd, 'setup.py'))) {
44
+ rows.push({ manifest: 'setup.py (python)', detail: 'present' });
45
+ }
46
+ if (readText(path.join(cwd, 'requirements.txt'))) {
47
+ rows.push({ manifest: 'requirements.txt (python)', detail: 'present' });
48
+ }
49
+
50
+ const cargo = readText(path.join(cwd, 'Cargo.toml'));
51
+ if (cargo) {
52
+ const name = (cargo.match(/^\s*name\s*=\s*["']([^"']+)["']/m) || [])[1];
53
+ const ver = (cargo.match(/^\s*version\s*=\s*["']([^"']+)["']/m) || [])[1];
54
+ rows.push({ manifest: 'Cargo.toml (rust)', detail: [name, ver].filter(Boolean).join('@') || 'present' });
55
+ }
56
+
57
+ const gomod = readText(path.join(cwd, 'go.mod'));
58
+ if (gomod) {
59
+ const mod = (gomod.match(/^module\s+(\S+)/m) || [])[1];
60
+ const go = (gomod.match(/^go\s+(\S+)/m) || [])[1];
61
+ rows.push({ manifest: 'go.mod (go)', detail: [mod, go && 'go ' + go].filter(Boolean).join(' · ') || 'present' });
62
+ }
63
+
64
+ if (readText(path.join(cwd, 'pom.xml'))) rows.push({ manifest: 'pom.xml (maven)', detail: 'present' });
65
+ if (readText(path.join(cwd, 'build.gradle')) || readText(path.join(cwd, 'build.gradle.kts'))) {
66
+ rows.push({ manifest: 'build.gradle (gradle)', detail: 'present' });
67
+ }
68
+ if (readText(path.join(cwd, 'Gemfile'))) rows.push({ manifest: 'Gemfile (ruby)', detail: 'present' });
69
+ const composer = readJson(path.join(cwd, 'composer.json'));
70
+ if (composer) {
71
+ rows.push({ manifest: 'composer.json (php)', detail: `${composer.name || 'present'} · ${count(composer.require)} deps` });
72
+ }
73
+ }
74
+
75
+ function configFiles(cwd) {
76
+ const present = [];
77
+ for (const f of CONFIG_FILES) {
78
+ if (fs.existsSync(path.join(cwd, f))) present.push(f);
79
+ }
80
+ return present;
81
+ }
82
+
83
+ function analyze(files, cwd) {
84
+ const rows = [];
85
+ manifests(cwd, rows);
86
+ const configs = configFiles(cwd);
87
+ if (rows.length === 0 && configs.length === 0) return '';
88
+
89
+ const lines = [];
90
+ if (rows.length) {
91
+ lines.push('| Manifest | Detail |', '|----------|--------|');
92
+ for (const r of rows) lines.push(`| ${r.manifest} | ${r.detail} |`);
93
+ }
94
+ if (configs.length) {
95
+ if (lines.length) lines.push('');
96
+ lines.push(`**Config files:** ${configs.map((c) => '`' + c + '`').join(', ')}`);
97
+ }
98
+ return lines.join('\n');
99
+ }
100
+
101
+ module.exports = { analyze };
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Environment-variable schema extractor (v8.5 C1).
5
+ *
6
+ * Surfaces the environment the project actually reads — from source across
7
+ * JS/TS, Python, Ruby, and Go, plus keys declared in a committed `.env.example`
8
+ * / `.env.sample` / `.env.template`. Pure, zero-dependency, deterministic.
9
+ *
10
+ * @param {string[]} files — absolute file paths to analyze (srcDirs-scoped)
11
+ * @param {string} cwd — project root
12
+ * @returns {string} formatted markdown table (empty string if none found)
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const SCAN_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.go']);
19
+ const EXAMPLE_FILES = ['.env.example', '.env.sample', '.env.template', '.env.dist'];
20
+
21
+ // process.env.X / process.env['X'] / import.meta.env.X / Deno.env.get('X')
22
+ const JS_RE = /(?:process\.env|import\.meta\.env)(?:\.([A-Z_][A-Z0-9_]*)|\[\s*['"]([A-Z_][A-Z0-9_]*)['"]\s*\])|Deno\.env\.get\(\s*['"]([A-Z_][A-Z0-9_]*)['"]/g;
23
+ // os.environ['X'] / os.environ.get('X') / os.getenv('X') / getenv('X')
24
+ const PY_RE = /(?:os\.)?(?:environ(?:\.get)?\[?\s*['"]([A-Z_][A-Z0-9_]*)['"]|getenv\(\s*['"]([A-Z_][A-Z0-9_]*)['"])/g;
25
+ const RB_RE = /ENV\[\s*['"]([A-Z_][A-Z0-9_]*)['"]\s*\]/g;
26
+ const GO_RE = /os\.(?:Getenv|LookupEnv)\(\s*["`']([A-Z_][A-Z0-9_]*)["`']/g;
27
+
28
+ const MAX_ROWS = 200;
29
+
30
+ function collectMatches(re, content, into) {
31
+ let m;
32
+ re.lastIndex = 0;
33
+ while ((m = re.exec(content)) !== null) {
34
+ const name = m[1] || m[2] || m[3];
35
+ if (name) into.add(name);
36
+ }
37
+ }
38
+
39
+ function readExampleKeys(cwd) {
40
+ const keys = new Set();
41
+ for (const name of EXAMPLE_FILES) {
42
+ let content;
43
+ try { content = fs.readFileSync(path.join(cwd, name), 'utf8'); } catch (_) { continue; }
44
+ for (const line of content.split('\n')) {
45
+ const t = line.trim();
46
+ if (!t || t.startsWith('#')) continue;
47
+ const eq = t.match(/^(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=/);
48
+ if (eq) keys.add(eq[1]);
49
+ }
50
+ }
51
+ return keys;
52
+ }
53
+
54
+ function analyze(files, cwd) {
55
+ const fromCode = new Set();
56
+
57
+ for (const filePath of files) {
58
+ const ext = path.extname(filePath).toLowerCase();
59
+ if (!SCAN_EXTS.has(ext)) continue;
60
+ let content;
61
+ try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }
62
+
63
+ if (ext === '.py') collectMatches(PY_RE, content, fromCode);
64
+ else if (ext === '.rb') collectMatches(RB_RE, content, fromCode);
65
+ else if (ext === '.go') collectMatches(GO_RE, content, fromCode);
66
+ else collectMatches(JS_RE, content, fromCode);
67
+ }
68
+
69
+ const fromExample = readExampleKeys(cwd);
70
+ const all = new Set([...fromCode, ...fromExample]);
71
+ if (all.size === 0) return '';
72
+
73
+ const names = [...all].sort();
74
+ const lines = [
75
+ '| Variable | Source |',
76
+ '|----------|--------|',
77
+ ];
78
+ for (const name of names.slice(0, MAX_ROWS)) {
79
+ const src = [];
80
+ if (fromCode.has(name)) src.push('code');
81
+ if (fromExample.has(name)) src.push('.env.example');
82
+ lines.push(`| ${name} | ${src.join(', ')} |`);
83
+ }
84
+ if (names.length > MAX_ROWS) {
85
+ lines.push(`| … | +${names.length - MAX_ROWS} more |`);
86
+ }
87
+ return lines.join('\n');
88
+ }
89
+
90
+ module.exports = { analyze };
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Database-migration extractor (v8.5 C1).
5
+ *
6
+ * Detects schema-migration files across the common frameworks — Rails
7
+ * (db/migrate), Django/Alembic, Prisma, Flyway (`V1__name.sql`), knex/Sequelize,
8
+ * and timestamped SQL — and surfaces them with a parsed version + name. Pure,
9
+ * zero-dependency, deterministic.
10
+ *
11
+ * @param {string[]} files — absolute file paths (unused; the tree is walked)
12
+ * @param {string} cwd — project root
13
+ * @returns {string} formatted markdown table (empty string if none found)
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const MAX_DEPTH = 6;
20
+ const MAX_ROWS = 200;
21
+ const SKIP_DIR = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'target', '.venv', 'venv', '__pycache__']);
22
+ const MIG_EXT = new Set(['.sql', '.rb', '.py', '.js', '.ts']);
23
+
24
+ // A directory whose path marks its children as migrations.
25
+ const MIG_DIR_RE = /(^|\/)(db\/migrate|migrations?|alembic\/versions|prisma\/migrations)$/i;
26
+ // A filename that is itself a migration regardless of directory.
27
+ const FLYWAY_RE = /^V\d+(?:[._]\d+)*__(.+)\.(sql|java)$/;
28
+ const TIMESTAMP_RE = /^(\d{8,})[_-](.+)\.(sql|rb|py|js|ts)$/;
29
+ const NAMED_RE = /[._-]migrations?[._-]/i;
30
+
31
+ function walk(dir, cwd, depth, out) {
32
+ if (depth > MAX_DEPTH) return;
33
+ let entries;
34
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
35
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
36
+
37
+ const relDir = path.relative(cwd, dir).replace(/\\/g, '/');
38
+ const dirIsMigration = MIG_DIR_RE.test(relDir);
39
+
40
+ for (const e of entries) {
41
+ if (e.isDirectory()) {
42
+ if (SKIP_DIR.has(e.name)) continue;
43
+ walk(path.join(dir, e.name), cwd, depth + 1, out);
44
+ continue;
45
+ }
46
+ const ext = path.extname(e.name).toLowerCase();
47
+ if (!MIG_EXT.has(ext)) continue;
48
+
49
+ const rel = path.relative(cwd, path.join(dir, e.name)).replace(/\\/g, '/');
50
+ let version = null;
51
+ let name = null;
52
+
53
+ let m;
54
+ if ((m = e.name.match(FLYWAY_RE))) { version = e.name.split('__')[0]; name = m[1].replace(/_/g, ' '); }
55
+ else if ((m = e.name.match(TIMESTAMP_RE))) { version = m[1]; name = m[2].replace(/[_-]/g, ' '); }
56
+ else if (dirIsMigration) { version = '—'; name = e.name.replace(ext, ''); }
57
+ else if (NAMED_RE.test(e.name)) { version = '—'; name = e.name.replace(ext, ''); }
58
+ else continue;
59
+
60
+ out.push({ version, name, file: rel });
61
+ }
62
+ }
63
+
64
+ function analyze(files, cwd) {
65
+ const found = [];
66
+ walk(cwd, cwd, 0, found);
67
+ if (found.length === 0) return '';
68
+
69
+ found.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
70
+
71
+ const lines = [
72
+ '| Version | Migration | File |',
73
+ '|---------|-----------|------|',
74
+ ];
75
+ for (const r of found.slice(0, MAX_ROWS)) {
76
+ lines.push(`| ${r.version} | ${r.name} | ${r.file} |`);
77
+ }
78
+ if (found.length > MAX_ROWS) {
79
+ lines.push(`| … | +${found.length - MAX_ROWS} more | |`);
80
+ }
81
+ return lines.join('\n');
82
+ }
83
+
84
+ module.exports = { analyze };
@@ -21,6 +21,10 @@ const MAP_SECTIONS = {
21
21
  imports: '### Import graph',
22
22
  classes: '### Class hierarchy',
23
23
  routes: '### Route table',
24
+ env: '### Environment variables',
25
+ buildci: '### Build & CI',
26
+ manifests: '### Config & manifests',
27
+ migrations: '### Database migrations',
24
28
  };
25
29
 
26
30
  /**
@@ -106,7 +110,7 @@ function getMap(args, cwd) {
106
110
 
107
111
  const header = MAP_SECTIONS[args.type];
108
112
  if (!header) {
109
- return `Unknown map type: "${args.type}". Use: imports, classes, routes`;
113
+ return `Unknown map type: "${args.type}". Use: ${Object.keys(MAP_SECTIONS).join(', ')}`;
110
114
  }
111
115
 
112
116
  const mapPath = path.join(cwd, 'PROJECT_MAP.md');
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.31.0',
21
+ version: '8.1.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -21,6 +21,7 @@ const fs = require('fs');
21
21
  const path = require('path');
22
22
  const parsers = require('./parsers');
23
23
  const { closestMatch, buildSymbolCandidates, formatSuggestion } = require('./closest-match');
24
+ const { buildLibraryIndex } = require('./lib-index');
24
25
 
25
26
  // A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
26
27
  // a tests/__tests__ directory). Used to flag fake-test-file separately.
@@ -189,6 +190,28 @@ function verify(answerText, cwd, opts = {}) {
189
190
  }
190
191
  if (!fileBasenames) fileBasenames = new Set();
191
192
 
193
+ // Installed-library grounding (G5/D5, the moat): union the exported symbols of
194
+ // the libraries actually installed in node_modules, so genuine library calls
195
+ // stop false-flagging as fake-symbol and the summary can pin the versions the
196
+ // answer was verified against. Auto-runs only when the caller did not override
197
+ // the symbol set (keeps hermetic callers unchanged); disable with libIndex:false.
198
+ let libraries = opts.libraries || [];
199
+ {
200
+ let libSyms = opts.libSymbols;
201
+ if (!libSyms && opts.libIndex !== false && !opts.symbolSet) {
202
+ try {
203
+ const li = buildLibraryIndex(cwd, { version: opts.version });
204
+ libSyms = li.symbols;
205
+ if (!opts.libraries) libraries = li.libraries;
206
+ } catch (_) { libSyms = null; }
207
+ }
208
+ if (libSyms && libSyms.size) {
209
+ const merged = new Set(symbolSet);
210
+ for (const s of libSyms) merged.add(s);
211
+ symbolSet = merged;
212
+ }
213
+ }
214
+
192
215
  let deps = opts.deps;
193
216
  let hasPkg = opts.hasPkg;
194
217
  if (!deps) {
@@ -312,6 +335,8 @@ function verify(answerText, cwd, opts = {}) {
312
335
  clean: issues.length === 0,
313
336
  symbolsIndexed: symbolSet.size,
314
337
  withSuggestion: issues.filter((i) => i.suggestion).length,
338
+ librariesIndexed: libraries.length,
339
+ libraries: libraries.map((l) => ({ name: l.name, version: l.version, symbols: l.symbols, typed: l.typed })),
315
340
  };
316
341
 
317
342
  return { issues, summary };