sigmap 7.7.0 → 7.8.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.8.0] — 2026-06-17
14
+
15
+ Minor release — `sigmap conventions --conflicts` (grounded codegen, Layer 3).
16
+
17
+ ### Added
18
+ - **`sigmap conventions --conflicts` — per-convention breakdown + rename suggestions (#301):** the next slice of Layer 3. Where `conventions` reports the dominant pattern and a consistency tier, `--conflicts` surfaces *why* a convention is mixed — every variant pattern with its file count, share, a visual bar, and example files, plus rename suggestions that move minority file-naming files toward the dominant style. New zero-dependency, bundle-safe `src/conventions/conflicts.js` (`analyzeConflicts`, `toNamingStyle`, `renameSuggestion`); export-style conflicts list variants but no renames (that's a code change, not a rename). `scoreConvention(labels, refs?)` now attaches up to 3 example files per variant (backward compatible). `--json` emits the structured conflict report; a consistent repo prints "no conflicts". `--report`, `--fix`, `--update`, `--ci`, and CLAUDE.md injection remain follow-ups.
19
+
20
+ ---
21
+
13
22
  ## [7.7.0] — 2026-06-17
14
23
 
15
24
  Minor release — `sigmap conventions` (grounded codegen, Layer 3).
package/gen-context.js CHANGED
@@ -22,6 +22,121 @@ function __require(key) {
22
22
 
23
23
  // ── ./src/cache/freshen ──
24
24
  // ── ./src/conventions/extract ──
25
+ // ── ./src/conventions/conflicts ──
26
+ __factories["./src/conventions/conflicts"] = function(module, exports) {
27
+
28
+ /**
29
+ * Convention conflict analysis (IMPL.md §4 / §5.3 — grounded codegen, Layer 3).
30
+ *
31
+ * Given an `extractConventions` result, surface *why* a convention is mixed:
32
+ * every variant pattern with its file count, share, and example files, plus
33
+ * rename suggestions that move minority file-naming files toward the dominant
34
+ * style. Pure, zero-dependency, bundle-safe.
35
+ */
36
+
37
+ /** Split a file name into its stem (before the first dot) and the rest. */
38
+ function _splitName(filename) {
39
+ const s = String(filename || '');
40
+ const dot = s.indexOf('.');
41
+ if (dot <= 0) return { stem: s, ext: '' };
42
+ return { stem: s.slice(0, dot), ext: s.slice(dot) };
43
+ }
44
+
45
+ /** Break a stem into lowercase word parts regardless of its current style. */
46
+ function _words(stem) {
47
+ return String(stem || '')
48
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camel/Pascal boundaries
49
+ .replace(/[-_]+/g, ' ') // kebab / snake separators
50
+ .trim()
51
+ .split(/\s+/)
52
+ .filter(Boolean)
53
+ .map((w) => w.toLowerCase());
54
+ }
55
+
56
+ const _cap = (w) => w.charAt(0).toUpperCase() + w.slice(1);
57
+
58
+ /**
59
+ * Convert a file stem to a target naming style.
60
+ * @param {string} stem name without extension
61
+ * @param {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'} style
62
+ * @returns {string}
63
+ */
64
+ function toNamingStyle(stem, style) {
65
+ const w = _words(stem);
66
+ if (w.length === 0) return String(stem || '');
67
+ switch (style) {
68
+ case 'PascalCase': return w.map(_cap).join('');
69
+ case 'camelCase': return w[0] + w.slice(1).map(_cap).join('');
70
+ case 'kebab-case': return w.join('-');
71
+ case 'snake_case': return w.join('_');
72
+ default: return String(stem || '');
73
+ }
74
+ }
75
+
76
+ /** Rename suggestion to bring a file to the dominant naming style. */
77
+ function renameSuggestion(filename, dominantStyle) {
78
+ const { stem, ext } = _splitName(filename);
79
+ const to = toNamingStyle(stem, dominantStyle) + ext;
80
+ return { from: filename, to };
81
+ }
82
+
83
+ const LABELS = {
84
+ fileNaming: 'file naming',
85
+ exportStyle: 'export style',
86
+ };
87
+
88
+ /**
89
+ * Analyze an `extractConventions` result for conflicts.
90
+ * @param {object} result the object returned by `extractConventions`
91
+ * @returns {{ hasConflicts: boolean, conventions: Array<{
92
+ * key:string, name:string, dominant:string|null, dominantPct:number,
93
+ * tier:string, total:number,
94
+ * variants:Array<{pattern:string,count:number,pct:number,examples:string[]}>,
95
+ * renames:Array<{from:string,to:string}> }> }}
96
+ */
97
+ function analyzeConflicts(result) {
98
+ const out = [];
99
+ for (const key of ['fileNaming', 'exportStyle']) {
100
+ const conv = result && result[key];
101
+ // A conflict is any convention with more than one observed pattern.
102
+ if (!conv || conv.total === 0 || conv.variants.length < 2) continue;
103
+
104
+ const variants = conv.variants.map((v) => ({
105
+ pattern: v.label,
106
+ count: v.count,
107
+ pct: v.pct,
108
+ examples: v.examples || [],
109
+ }));
110
+
111
+ // Rename suggestions only for file naming (export style is a code change, not a rename).
112
+ const renames = [];
113
+ if (key === 'fileNaming' && conv.dominant) {
114
+ for (const v of conv.variants) {
115
+ if (v.label === conv.dominant) continue;
116
+ for (const ex of (v.examples || [])) {
117
+ renames.push(renameSuggestion(ex, conv.dominant));
118
+ }
119
+ }
120
+ }
121
+
122
+ out.push({
123
+ key,
124
+ name: LABELS[key] || key,
125
+ dominant: conv.dominant,
126
+ dominantPct: conv.dominantPct,
127
+ tier: conv.tier,
128
+ total: conv.total,
129
+ variants,
130
+ renames,
131
+ });
132
+ }
133
+ return { hasConflicts: out.length > 0, conventions: out };
134
+ }
135
+
136
+ module.exports = { analyzeConflicts, toNamingStyle, renameSuggestion };
137
+
138
+ };
139
+
25
140
  __factories["./src/conventions/extract"] = function(module, exports) {
26
141
 
27
142
  /**
@@ -6786,7 +6901,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
6786
6901
 
6787
6902
  const SERVER_INFO = {
6788
6903
  name: 'sigmap',
6789
- version: '7.7.0',
6904
+ version: '7.8.0',
6790
6905
  description: 'SigMap MCP server — code signatures on demand',
6791
6906
  };
6792
6907
 
@@ -12464,7 +12579,7 @@ function __tryGit(args, opts = {}) {
12464
12579
  catch (_) { return ''; }
12465
12580
  }
12466
12581
 
12467
- const VERSION = '7.7.0';
12582
+ const VERSION = '7.8.0';
12468
12583
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
12469
12584
 
12470
12585
  function requireSourceOrBundled(key) {
@@ -15642,6 +15757,38 @@ function main() {
15642
15757
  const files = buildFileList(cwd, config);
15643
15758
  const result = extractConventions(cwd, files);
15644
15759
 
15760
+ // `--conflicts`: surface why a convention is mixed (breakdown + rename suggestions).
15761
+ if (args.includes('--conflicts')) {
15762
+ const { analyzeConflicts } = requireSourceOrBundled('./src/conventions/conflicts');
15763
+ const report = analyzeConflicts(result);
15764
+ if (jsonOut) {
15765
+ process.stdout.write(JSON.stringify(report) + '\n');
15766
+ process.exit(0);
15767
+ }
15768
+ const pctC = (n) => `${(n * 100).toFixed(0)}%`;
15769
+ if (!report.hasConflicts) {
15770
+ console.log('[sigmap] conventions --conflicts (TS/JS/Python)');
15771
+ console.log(' no conflicts — conventions are consistent ✓');
15772
+ process.exit(0);
15773
+ }
15774
+ console.log('[sigmap] conventions --conflicts (TS/JS/Python)');
15775
+ for (const c of report.conventions) {
15776
+ console.log(`\n ${c.name} — dominant: ${c.dominant} ${pctC(c.dominantPct)} [${c.tier}]`);
15777
+ for (const v of c.variants) {
15778
+ const barLen = Math.max(1, Math.round(v.pct * 20));
15779
+ const bar = '█'.repeat(barLen);
15780
+ const tag = v.pattern === c.dominant ? ' (dominant)' : '';
15781
+ const ex = v.examples.length ? ` e.g. ${v.examples.join(', ')}` : '';
15782
+ console.log(` ${v.pattern.padEnd(12)} ${String(v.count).padStart(4)} ${pctC(v.pct).padStart(4)} ${bar}${tag}${ex}`);
15783
+ }
15784
+ if (c.renames.length) {
15785
+ console.log(` rename to match ${c.dominant}:`);
15786
+ for (const r of c.renames) console.log(` ${r.from} → ${r.to}`);
15787
+ }
15788
+ }
15789
+ process.exit(0);
15790
+ }
15791
+
15645
15792
  const outDir = path.join(cwd, '.context');
15646
15793
  const outPath = path.join(outDir, 'conventions.json');
15647
15794
  try {
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.7.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.8.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.7.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.8.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.7.0",
3
+ "version": "7.8.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.7.0",
3
+ "version": "7.8.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.7.0",
3
+ "version": "7.8.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,111 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Convention conflict analysis (IMPL.md §4 / §5.3 — grounded codegen, Layer 3).
5
+ *
6
+ * Given an `extractConventions` result, surface *why* a convention is mixed:
7
+ * every variant pattern with its file count, share, and example files, plus
8
+ * rename suggestions that move minority file-naming files toward the dominant
9
+ * style. Pure, zero-dependency, bundle-safe.
10
+ */
11
+
12
+ /** Split a file name into its stem (before the first dot) and the rest. */
13
+ function _splitName(filename) {
14
+ const s = String(filename || '');
15
+ const dot = s.indexOf('.');
16
+ if (dot <= 0) return { stem: s, ext: '' };
17
+ return { stem: s.slice(0, dot), ext: s.slice(dot) };
18
+ }
19
+
20
+ /** Break a stem into lowercase word parts regardless of its current style. */
21
+ function _words(stem) {
22
+ return String(stem || '')
23
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camel/Pascal boundaries
24
+ .replace(/[-_]+/g, ' ') // kebab / snake separators
25
+ .trim()
26
+ .split(/\s+/)
27
+ .filter(Boolean)
28
+ .map((w) => w.toLowerCase());
29
+ }
30
+
31
+ const _cap = (w) => w.charAt(0).toUpperCase() + w.slice(1);
32
+
33
+ /**
34
+ * Convert a file stem to a target naming style.
35
+ * @param {string} stem name without extension
36
+ * @param {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'} style
37
+ * @returns {string}
38
+ */
39
+ function toNamingStyle(stem, style) {
40
+ const w = _words(stem);
41
+ if (w.length === 0) return String(stem || '');
42
+ switch (style) {
43
+ case 'PascalCase': return w.map(_cap).join('');
44
+ case 'camelCase': return w[0] + w.slice(1).map(_cap).join('');
45
+ case 'kebab-case': return w.join('-');
46
+ case 'snake_case': return w.join('_');
47
+ default: return String(stem || '');
48
+ }
49
+ }
50
+
51
+ /** Rename suggestion to bring a file to the dominant naming style. */
52
+ function renameSuggestion(filename, dominantStyle) {
53
+ const { stem, ext } = _splitName(filename);
54
+ const to = toNamingStyle(stem, dominantStyle) + ext;
55
+ return { from: filename, to };
56
+ }
57
+
58
+ const LABELS = {
59
+ fileNaming: 'file naming',
60
+ exportStyle: 'export style',
61
+ };
62
+
63
+ /**
64
+ * Analyze an `extractConventions` result for conflicts.
65
+ * @param {object} result the object returned by `extractConventions`
66
+ * @returns {{ hasConflicts: boolean, conventions: Array<{
67
+ * key:string, name:string, dominant:string|null, dominantPct:number,
68
+ * tier:string, total:number,
69
+ * variants:Array<{pattern:string,count:number,pct:number,examples:string[]}>,
70
+ * renames:Array<{from:string,to:string}> }> }}
71
+ */
72
+ function analyzeConflicts(result) {
73
+ const out = [];
74
+ for (const key of ['fileNaming', 'exportStyle']) {
75
+ const conv = result && result[key];
76
+ // A conflict is any convention with more than one observed pattern.
77
+ if (!conv || conv.total === 0 || conv.variants.length < 2) continue;
78
+
79
+ const variants = conv.variants.map((v) => ({
80
+ pattern: v.label,
81
+ count: v.count,
82
+ pct: v.pct,
83
+ examples: v.examples || [],
84
+ }));
85
+
86
+ // Rename suggestions only for file naming (export style is a code change, not a rename).
87
+ const renames = [];
88
+ if (key === 'fileNaming' && conv.dominant) {
89
+ for (const v of conv.variants) {
90
+ if (v.label === conv.dominant) continue;
91
+ for (const ex of (v.examples || [])) {
92
+ renames.push(renameSuggestion(ex, conv.dominant));
93
+ }
94
+ }
95
+ }
96
+
97
+ out.push({
98
+ key,
99
+ name: LABELS[key] || key,
100
+ dominant: conv.dominant,
101
+ dominantPct: conv.dominantPct,
102
+ tier: conv.tier,
103
+ total: conv.total,
104
+ variants,
105
+ renames,
106
+ });
107
+ }
108
+ return { hasConflicts: out.length > 0, conventions: out };
109
+ }
110
+
111
+ module.exports = { analyzeConflicts, toNamingStyle, renameSuggestion };
@@ -42,24 +42,42 @@ function classifyNaming(basename) {
42
42
  return 'other';
43
43
  }
44
44
 
45
+ const MAX_EXAMPLES = 3;
46
+
45
47
  /**
46
48
  * Score a set of categorical observations into a dominant convention plus its
47
49
  * consistency tier. The reusable primitive (IMPL.md §5.2).
48
50
  * @param {string[]} labels observed category for each sample (e.g. naming styles)
51
+ * @param {string[]} [refs] optional identifier (e.g. file name) parallel to
52
+ * `labels`; when given, each variant carries up to 3 `examples`.
49
53
  * @returns {{ dominant: string|null, dominantPct: number, total: number,
50
- * variants: Array<{label:string, count:number, pct:number}>,
54
+ * variants: Array<{label:string, count:number, pct:number, examples?:string[]}>,
51
55
  * tier: 'consistent'|'mostly'|'inconsistent'|'unknown' }}
52
56
  */
53
- function scoreConvention(labels) {
54
- const list = (labels || []).filter((l) => l != null && l !== 'other');
55
- const total = list.length;
57
+ function scoreConvention(labels, refs) {
58
+ const all = labels || [];
59
+ const counts = new Map();
60
+ const examples = new Map();
61
+ let total = 0;
62
+ for (let i = 0; i < all.length; i++) {
63
+ const l = all[i];
64
+ if (l == null || l === 'other') continue;
65
+ total++;
66
+ counts.set(l, (counts.get(l) || 0) + 1);
67
+ if (refs && refs[i] != null) {
68
+ const ex = examples.get(l) || [];
69
+ if (ex.length < MAX_EXAMPLES) { ex.push(refs[i]); examples.set(l, ex); }
70
+ }
71
+ }
56
72
  if (total === 0) {
57
73
  return { dominant: null, dominantPct: 0, total: 0, variants: [], tier: 'unknown' };
58
74
  }
59
- const counts = new Map();
60
- for (const l of list) counts.set(l, (counts.get(l) || 0) + 1);
61
75
  const variants = [...counts.entries()]
62
- .map(([label, count]) => ({ label, count, pct: count / total }))
76
+ .map(([label, count]) => {
77
+ const v = { label, count, pct: count / total };
78
+ if (refs) v.examples = examples.get(label) || [];
79
+ return v;
80
+ })
63
81
  .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
64
82
  const top = variants[0];
65
83
  let tier = 'inconsistent';
@@ -131,22 +149,26 @@ function _detectTestFramework(cwd, files) {
131
149
  function extractConventions(cwd, files) {
132
150
  const scoped = (files || []).filter((f) => SCOPED_EXTS.has(path.extname(f).toLowerCase()));
133
151
  const namingLabels = [];
152
+ const namingRefs = [];
134
153
  const exportLabels = [];
154
+ const exportRefs = [];
135
155
  for (const f of scoped) {
136
156
  const base = path.basename(f);
137
157
  // Skip test files for the naming convention (they have their own naming).
138
158
  if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) {
139
159
  namingLabels.push(classifyNaming(base));
160
+ namingRefs.push(base);
140
161
  }
141
162
  if (JS_TS_EXTS.has(path.extname(f).toLowerCase())) {
142
163
  let src = '';
143
164
  try { src = fs.readFileSync(f, 'utf8'); } catch (_) {}
144
165
  exportLabels.push(_jsExportStyle(src));
166
+ exportRefs.push(base);
145
167
  }
146
168
  }
147
169
  return {
148
- fileNaming: scoreConvention(namingLabels),
149
- exportStyle: scoreConvention(exportLabels),
170
+ fileNaming: scoreConvention(namingLabels, namingRefs),
171
+ exportStyle: scoreConvention(exportLabels, exportRefs),
150
172
  testFramework: _detectTestFramework(cwd, scoped),
151
173
  scope: ['typescript', 'javascript', 'python'],
152
174
  scannedFiles: scoped.length,
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.7.0',
21
+ version: '7.8.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24