sigmap 7.5.0 → 7.7.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,24 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [7.7.0] — 2026-06-17
14
+
15
+ Minor release — `sigmap conventions` (grounded codegen, Layer 3).
16
+
17
+ ### Added
18
+ - **`sigmap conventions` — extract & report a repo's coding conventions (#298):** the first slice of Layer 3 (grounded code generation). Detects the dominant **file naming** style, **export style**, and **test framework** for TS/JS/Python so generated code matches the house style instead of drifting (Cause 4: naming/convention drift). New zero-dependency, bundle-safe `src/conventions/extract.js` exposes `classifyNaming` (PascalCase / camelCase / kebab-case / snake_case), `scoreConvention` (a reusable consistency scorer returning `{ dominant, dominantPct, variants, tier }` with tiers at 90% / 70% — Gap 1's scaffold-confidence floor will reuse it), and `extractConventions`. The command writes `.context/conventions.json` and prints a readable report; `--json` emits machine output. `--conflicts`, `--fix`, `--ci`, and CLAUDE.md injection are deferred to follow-ups.
19
+
20
+ ---
21
+
22
+ ## [7.6.0] — 2026-06-17
23
+
24
+ Minor release — the grounding benchmark (the offline GATE for grounded codegen).
25
+
26
+ ### Added
27
+ - **Grounding benchmark — `npm run benchmark:grounding` (#294):** a deterministic, offline callee-grounding ablation that measures how much ground truth SigMap actually gives an agent. For each corpus repo: `coverage = grounded / universe`, where universe is every symbol defined in the source and grounded is the subset SigMap surfaces in its index (resolvable by `get_callee_signatures`); baseline is 0 (no SigMap → guess every reference). `scripts/run-hallucination-benchmark.mjs` prints per-repo + aggregate coverage; `--save` writes `benchmarks/reports/hallucination.json`; `--gate <pct>` exits non-zero below a threshold. It's an honest *ground-truth-availability proxy*, not a measured LLM hallucination rate (the LLM A/B ablation is a documented follow-up needing an API key). No LLM, no network — runs in CI.
28
+
29
+ ---
30
+
13
31
  ## [7.5.0] — 2026-06-17
14
32
 
15
33
  Minor release — read-time self-heal completes Layer 1 freshness.
package/gen-context.js CHANGED
@@ -21,6 +21,166 @@ function __require(key) {
21
21
 
22
22
 
23
23
  // ── ./src/cache/freshen ──
24
+ // ── ./src/conventions/extract ──
25
+ __factories["./src/conventions/extract"] = function(module, exports) {
26
+
27
+ /**
28
+ * Convention extraction (IMPL.md Layer 3 — grounded code generation).
29
+ *
30
+ * Detects a repo's dominant coding conventions so generated code matches the
31
+ * house style instead of drifting (Cause 4: naming/convention drift). This
32
+ * first slice covers TS/JS/Python and three conventions: file naming, export
33
+ * style, and test framework. `scoreConvention` is the reusable consistency
34
+ * primitive that Gap 1 (scaffold confidence) will also build on.
35
+ *
36
+ * Zero dependencies, bundle-safe (fs + path only).
37
+ */
38
+
39
+ const fs = require('fs');
40
+ const path = require('path');
41
+
42
+ const JS_TS_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
43
+ const PY_EXTS = new Set(['.py']);
44
+ const SCOPED_EXTS = new Set([...JS_TS_EXTS, ...PY_EXTS]);
45
+
46
+ // Consistency tiers (IMPL.md §5.1): a convention is only safe to enforce when
47
+ // it is actually consistent.
48
+ const TIER_CONSISTENT = 0.9;
49
+ const TIER_MOSTLY = 0.7;
50
+
51
+ /**
52
+ * Classify a file's base name (without extension) into a naming style.
53
+ * @param {string} basename a file basename, e.g. "user-service.ts"
54
+ * @returns {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'|'other'}
55
+ */
56
+ function classifyNaming(basename) {
57
+ let stem = String(basename || '');
58
+ const dot = stem.indexOf('.');
59
+ if (dot > 0) stem = stem.slice(0, dot); // strip ext + compound suffix (.test, .d)
60
+ if (!stem) return 'other';
61
+ if (/[-]/.test(stem) && /^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(stem)) return 'kebab-case';
62
+ if (/[_]/.test(stem) && /^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(stem)) return 'snake_case';
63
+ if (/^[A-Z][A-Za-z0-9]*$/.test(stem) && /[a-z]/.test(stem)) return 'PascalCase';
64
+ if (/^[a-z][A-Za-z0-9]*$/.test(stem) && /[A-Z]/.test(stem)) return 'camelCase';
65
+ if (/^[a-z][a-z0-9]*$/.test(stem)) return 'camelCase'; // single lowercase word
66
+ return 'other';
67
+ }
68
+
69
+ /**
70
+ * Score a set of categorical observations into a dominant convention plus its
71
+ * consistency tier. The reusable primitive (IMPL.md §5.2).
72
+ * @param {string[]} labels observed category for each sample (e.g. naming styles)
73
+ * @returns {{ dominant: string|null, dominantPct: number, total: number,
74
+ * variants: Array<{label:string, count:number, pct:number}>,
75
+ * tier: 'consistent'|'mostly'|'inconsistent'|'unknown' }}
76
+ */
77
+ function scoreConvention(labels) {
78
+ const list = (labels || []).filter((l) => l != null && l !== 'other');
79
+ const total = list.length;
80
+ if (total === 0) {
81
+ return { dominant: null, dominantPct: 0, total: 0, variants: [], tier: 'unknown' };
82
+ }
83
+ const counts = new Map();
84
+ for (const l of list) counts.set(l, (counts.get(l) || 0) + 1);
85
+ const variants = [...counts.entries()]
86
+ .map(([label, count]) => ({ label, count, pct: count / total }))
87
+ .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
88
+ const top = variants[0];
89
+ let tier = 'inconsistent';
90
+ if (top.pct >= TIER_CONSISTENT) tier = 'consistent';
91
+ else if (top.pct >= TIER_MOSTLY) tier = 'mostly';
92
+ return {
93
+ dominant: top.label,
94
+ dominantPct: top.pct,
95
+ total,
96
+ variants,
97
+ tier,
98
+ };
99
+ }
100
+
101
+ /** Detect JS/TS export style for a single file's source. */
102
+ function _jsExportStyle(src) {
103
+ const s = String(src || '');
104
+ if (/\bexport\s+default\b/.test(s) || /\bmodule\.exports\s*=\s*(?:function|class|\{?\s*[A-Za-z_$])/.test(s)) {
105
+ // module.exports = { a, b } reads as named; only treat bare assignment as default.
106
+ if (/\bexport\s+default\b/.test(s)) return 'default';
107
+ if (/\bmodule\.exports\s*=\s*\{/.test(s)) return 'named';
108
+ return 'default';
109
+ }
110
+ if (/\bexport\s+(?:const|let|var|function|class|async\s+function|\{|type|interface|enum)\b/.test(s)
111
+ || /\bmodule\.exports\s*=\s*\{/.test(s) || /\bexports\.[A-Za-z_$]/.test(s)) {
112
+ return 'named';
113
+ }
114
+ return 'other';
115
+ }
116
+
117
+ /** Detect the test framework in use from manifests + source heuristics. */
118
+ function _detectTestFramework(cwd, files) {
119
+ const deps = {};
120
+ try {
121
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
122
+ Object.assign(deps, pkg.dependencies, pkg.devDependencies);
123
+ } catch (_) {}
124
+ for (const fw of ['vitest', 'jest', 'mocha', 'ava', 'jasmine']) {
125
+ if (deps[fw]) return fw;
126
+ }
127
+ // Python: pytest in requirements / pyproject.
128
+ for (const manifest of ['requirements.txt', 'requirements-dev.txt', 'pyproject.toml', 'setup.cfg']) {
129
+ try {
130
+ const raw = fs.readFileSync(path.join(cwd, manifest), 'utf8');
131
+ if (/\bpytest\b/.test(raw)) return 'pytest';
132
+ if (/\bunittest\b/.test(raw)) return 'unittest';
133
+ } catch (_) {}
134
+ }
135
+ // Fallback: infer from test file contents.
136
+ for (const f of files) {
137
+ if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) continue;
138
+ let src = '';
139
+ try { src = fs.readFileSync(f, 'utf8'); } catch (_) { continue; }
140
+ if (/\bvi\.(fn|mock|spyOn)\b|from ['"]vitest['"]/.test(src)) return 'vitest';
141
+ if (/\bjest\.(fn|mock|spyOn)\b/.test(src)) return 'jest';
142
+ if (/\bimport pytest\b|@pytest\./.test(src)) return 'pytest';
143
+ if (/\bdescribe\(|\bit\(/.test(src)) return 'mocha/jest-style';
144
+ }
145
+ return null;
146
+ }
147
+
148
+ /**
149
+ * Extract repo coding conventions for the scoped languages (TS/JS/Python).
150
+ * @param {string} cwd repo root
151
+ * @param {string[]} files absolute paths to source files (e.g. from buildFileList)
152
+ * @returns {{ fileNaming: object, exportStyle: object, testFramework: string|null,
153
+ * scope: string[], scannedFiles: number }}
154
+ */
155
+ function extractConventions(cwd, files) {
156
+ const scoped = (files || []).filter((f) => SCOPED_EXTS.has(path.extname(f).toLowerCase()));
157
+ const namingLabels = [];
158
+ const exportLabels = [];
159
+ for (const f of scoped) {
160
+ const base = path.basename(f);
161
+ // Skip test files for the naming convention (they have their own naming).
162
+ if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) {
163
+ namingLabels.push(classifyNaming(base));
164
+ }
165
+ if (JS_TS_EXTS.has(path.extname(f).toLowerCase())) {
166
+ let src = '';
167
+ try { src = fs.readFileSync(f, 'utf8'); } catch (_) {}
168
+ exportLabels.push(_jsExportStyle(src));
169
+ }
170
+ }
171
+ return {
172
+ fileNaming: scoreConvention(namingLabels),
173
+ exportStyle: scoreConvention(exportLabels),
174
+ testFramework: _detectTestFramework(cwd, scoped),
175
+ scope: ['typescript', 'javascript', 'python'],
176
+ scannedFiles: scoped.length,
177
+ };
178
+ }
179
+
180
+ module.exports = { classifyNaming, scoreConvention, extractConventions };
181
+
182
+ };
183
+
24
184
  __factories["./src/cache/freshen"] = function(module, exports) {
25
185
 
26
186
  /**
@@ -6626,7 +6786,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
6626
6786
 
6627
6787
  const SERVER_INFO = {
6628
6788
  name: 'sigmap',
6629
- version: '7.5.0',
6789
+ version: '7.7.0',
6630
6790
  description: 'SigMap MCP server — code signatures on demand',
6631
6791
  };
6632
6792
 
@@ -12304,7 +12464,7 @@ function __tryGit(args, opts = {}) {
12304
12464
  catch (_) { return ''; }
12305
12465
  }
12306
12466
 
12307
- const VERSION = '7.5.0';
12467
+ const VERSION = '7.7.0';
12308
12468
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
12309
12469
 
12310
12470
  function requireSourceOrBundled(key) {
@@ -15473,6 +15633,48 @@ function main() {
15473
15633
  process.exit(0);
15474
15634
  }
15475
15635
 
15636
+ // Layer 3: `sigmap conventions` — extract & report repo coding conventions
15637
+ // (file naming, export style, test framework) for TS/JS/Python so generated
15638
+ // code matches the house style. Writes .context/conventions.json.
15639
+ if (args[0] === 'conventions') {
15640
+ const jsonOut = args.includes('--json');
15641
+ const { extractConventions } = requireSourceOrBundled('./src/conventions/extract');
15642
+ const files = buildFileList(cwd, config);
15643
+ const result = extractConventions(cwd, files);
15644
+
15645
+ const outDir = path.join(cwd, '.context');
15646
+ const outPath = path.join(outDir, 'conventions.json');
15647
+ try {
15648
+ fs.mkdirSync(outDir, { recursive: true });
15649
+ fs.writeFileSync(outPath, JSON.stringify(result, null, 2) + '\n');
15650
+ } catch (e) {
15651
+ console.error(`[sigmap] cannot write ${outPath}: ${e.message}`);
15652
+ process.exit(1);
15653
+ }
15654
+
15655
+ if (jsonOut) {
15656
+ process.stdout.write(JSON.stringify(result) + '\n');
15657
+ process.exit(0);
15658
+ }
15659
+
15660
+ const pct = (n) => `${(n * 100).toFixed(0)}%`;
15661
+ const tierLabel = { consistent: 'consistent', mostly: 'mostly', inconsistent: 'mixed', unknown: 'n/a' };
15662
+ const line = (name, conv) => {
15663
+ if (!conv || conv.total === 0) return ` ${name.padEnd(14)} n/a (no samples)`;
15664
+ const variants = conv.variants.slice(1, 3).map((v) => `${v.label} ${pct(v.pct)}`).join(', ');
15665
+ const tail = variants ? ` · also: ${variants}` : '';
15666
+ return ` ${name.padEnd(14)} ${conv.dominant} ${pct(conv.dominantPct)} [${tierLabel[conv.tier]}]${tail}`;
15667
+ };
15668
+
15669
+ console.log('[sigmap] conventions (TS/JS/Python)');
15670
+ console.log(` scanned ${result.scannedFiles} file${result.scannedFiles === 1 ? '' : 's'}`);
15671
+ console.log(line('file naming', result.fileNaming));
15672
+ console.log(line('export style', result.exportStyle));
15673
+ console.log(` ${'test framework'.padEnd(14)} ${result.testFramework || 'none detected'}`);
15674
+ console.log(`\n → wrote ${path.relative(cwd, outPath)}`);
15675
+ process.exit(0);
15676
+ }
15677
+
15476
15678
  // v7.0.0: `sigmap squeeze <file|->` — minimize a pasted stacktrace / CI-log / JSON blob
15477
15679
  if (args[0] === 'squeeze') {
15478
15680
  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.5.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.7.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.5.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.7.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.5.0",
3
+ "version": "7.7.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": {
@@ -38,7 +38,8 @@
38
38
  "version:sync": "node scripts/sync-versions.mjs",
39
39
  "generate:llms": "node scripts/generate-llms.mjs",
40
40
  "validate:llms": "node scripts/validate-llms.mjs",
41
- "prepublishOnly": "node scripts/check-bundle.mjs && node scripts/check-version-meta.mjs && node scripts/generate-llms.mjs"
41
+ "prepublishOnly": "node scripts/check-bundle.mjs && node scripts/check-version-meta.mjs && node scripts/generate-llms.mjs",
42
+ "benchmark:grounding": "node scripts/run-hallucination-benchmark.mjs"
42
43
  },
43
44
  "files": [
44
45
  "gen-context.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "7.5.0",
3
+ "version": "7.7.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.5.0",
3
+ "version": "7.7.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Convention extraction (IMPL.md Layer 3 — grounded code generation).
5
+ *
6
+ * Detects a repo's dominant coding conventions so generated code matches the
7
+ * house style instead of drifting (Cause 4: naming/convention drift). This
8
+ * first slice covers TS/JS/Python and three conventions: file naming, export
9
+ * style, and test framework. `scoreConvention` is the reusable consistency
10
+ * primitive that Gap 1 (scaffold confidence) will also build on.
11
+ *
12
+ * Zero dependencies, bundle-safe (fs + path only).
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const JS_TS_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
19
+ const PY_EXTS = new Set(['.py']);
20
+ const SCOPED_EXTS = new Set([...JS_TS_EXTS, ...PY_EXTS]);
21
+
22
+ // Consistency tiers (IMPL.md §5.1): a convention is only safe to enforce when
23
+ // it is actually consistent.
24
+ const TIER_CONSISTENT = 0.9;
25
+ const TIER_MOSTLY = 0.7;
26
+
27
+ /**
28
+ * Classify a file's base name (without extension) into a naming style.
29
+ * @param {string} basename a file basename, e.g. "user-service.ts"
30
+ * @returns {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'|'other'}
31
+ */
32
+ function classifyNaming(basename) {
33
+ let stem = String(basename || '');
34
+ const dot = stem.indexOf('.');
35
+ if (dot > 0) stem = stem.slice(0, dot); // strip ext + compound suffix (.test, .d)
36
+ if (!stem) return 'other';
37
+ if (/[-]/.test(stem) && /^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(stem)) return 'kebab-case';
38
+ if (/[_]/.test(stem) && /^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(stem)) return 'snake_case';
39
+ if (/^[A-Z][A-Za-z0-9]*$/.test(stem) && /[a-z]/.test(stem)) return 'PascalCase';
40
+ if (/^[a-z][A-Za-z0-9]*$/.test(stem) && /[A-Z]/.test(stem)) return 'camelCase';
41
+ if (/^[a-z][a-z0-9]*$/.test(stem)) return 'camelCase'; // single lowercase word
42
+ return 'other';
43
+ }
44
+
45
+ /**
46
+ * Score a set of categorical observations into a dominant convention plus its
47
+ * consistency tier. The reusable primitive (IMPL.md §5.2).
48
+ * @param {string[]} labels observed category for each sample (e.g. naming styles)
49
+ * @returns {{ dominant: string|null, dominantPct: number, total: number,
50
+ * variants: Array<{label:string, count:number, pct:number}>,
51
+ * tier: 'consistent'|'mostly'|'inconsistent'|'unknown' }}
52
+ */
53
+ function scoreConvention(labels) {
54
+ const list = (labels || []).filter((l) => l != null && l !== 'other');
55
+ const total = list.length;
56
+ if (total === 0) {
57
+ return { dominant: null, dominantPct: 0, total: 0, variants: [], tier: 'unknown' };
58
+ }
59
+ const counts = new Map();
60
+ for (const l of list) counts.set(l, (counts.get(l) || 0) + 1);
61
+ const variants = [...counts.entries()]
62
+ .map(([label, count]) => ({ label, count, pct: count / total }))
63
+ .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
64
+ const top = variants[0];
65
+ let tier = 'inconsistent';
66
+ if (top.pct >= TIER_CONSISTENT) tier = 'consistent';
67
+ else if (top.pct >= TIER_MOSTLY) tier = 'mostly';
68
+ return {
69
+ dominant: top.label,
70
+ dominantPct: top.pct,
71
+ total,
72
+ variants,
73
+ tier,
74
+ };
75
+ }
76
+
77
+ /** Detect JS/TS export style for a single file's source. */
78
+ function _jsExportStyle(src) {
79
+ const s = String(src || '');
80
+ if (/\bexport\s+default\b/.test(s) || /\bmodule\.exports\s*=\s*(?:function|class|\{?\s*[A-Za-z_$])/.test(s)) {
81
+ // module.exports = { a, b } reads as named; only treat bare assignment as default.
82
+ if (/\bexport\s+default\b/.test(s)) return 'default';
83
+ if (/\bmodule\.exports\s*=\s*\{/.test(s)) return 'named';
84
+ return 'default';
85
+ }
86
+ if (/\bexport\s+(?:const|let|var|function|class|async\s+function|\{|type|interface|enum)\b/.test(s)
87
+ || /\bmodule\.exports\s*=\s*\{/.test(s) || /\bexports\.[A-Za-z_$]/.test(s)) {
88
+ return 'named';
89
+ }
90
+ return 'other';
91
+ }
92
+
93
+ /** Detect the test framework in use from manifests + source heuristics. */
94
+ function _detectTestFramework(cwd, files) {
95
+ const deps = {};
96
+ try {
97
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
98
+ Object.assign(deps, pkg.dependencies, pkg.devDependencies);
99
+ } catch (_) {}
100
+ for (const fw of ['vitest', 'jest', 'mocha', 'ava', 'jasmine']) {
101
+ if (deps[fw]) return fw;
102
+ }
103
+ // Python: pytest in requirements / pyproject.
104
+ for (const manifest of ['requirements.txt', 'requirements-dev.txt', 'pyproject.toml', 'setup.cfg']) {
105
+ try {
106
+ const raw = fs.readFileSync(path.join(cwd, manifest), 'utf8');
107
+ if (/\bpytest\b/.test(raw)) return 'pytest';
108
+ if (/\bunittest\b/.test(raw)) return 'unittest';
109
+ } catch (_) {}
110
+ }
111
+ // Fallback: infer from test file contents.
112
+ for (const f of files) {
113
+ if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) continue;
114
+ let src = '';
115
+ try { src = fs.readFileSync(f, 'utf8'); } catch (_) { continue; }
116
+ if (/\bvi\.(fn|mock|spyOn)\b|from ['"]vitest['"]/.test(src)) return 'vitest';
117
+ if (/\bjest\.(fn|mock|spyOn)\b/.test(src)) return 'jest';
118
+ if (/\bimport pytest\b|@pytest\./.test(src)) return 'pytest';
119
+ if (/\bdescribe\(|\bit\(/.test(src)) return 'mocha/jest-style';
120
+ }
121
+ return null;
122
+ }
123
+
124
+ /**
125
+ * Extract repo coding conventions for the scoped languages (TS/JS/Python).
126
+ * @param {string} cwd repo root
127
+ * @param {string[]} files absolute paths to source files (e.g. from buildFileList)
128
+ * @returns {{ fileNaming: object, exportStyle: object, testFramework: string|null,
129
+ * scope: string[], scannedFiles: number }}
130
+ */
131
+ function extractConventions(cwd, files) {
132
+ const scoped = (files || []).filter((f) => SCOPED_EXTS.has(path.extname(f).toLowerCase()));
133
+ const namingLabels = [];
134
+ const exportLabels = [];
135
+ for (const f of scoped) {
136
+ const base = path.basename(f);
137
+ // Skip test files for the naming convention (they have their own naming).
138
+ if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) {
139
+ namingLabels.push(classifyNaming(base));
140
+ }
141
+ if (JS_TS_EXTS.has(path.extname(f).toLowerCase())) {
142
+ let src = '';
143
+ try { src = fs.readFileSync(f, 'utf8'); } catch (_) {}
144
+ exportLabels.push(_jsExportStyle(src));
145
+ }
146
+ }
147
+ return {
148
+ fileNaming: scoreConvention(namingLabels),
149
+ exportStyle: scoreConvention(exportLabels),
150
+ testFramework: _detectTestFramework(cwd, scoped),
151
+ scope: ['typescript', 'javascript', 'python'],
152
+ scannedFiles: scoped.length,
153
+ };
154
+ }
155
+
156
+ module.exports = { classifyNaming, scoreConvention, extractConventions };
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.5.0',
21
+ version: '7.7.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24