sigmap 7.6.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 +18 -0
- package/gen-context.js +351 -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/conventions/conflicts.js +111 -0
- package/src/conventions/extract.js +178 -0
- package/src/mcp/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@ 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
|
+
|
|
22
|
+
## [7.7.0] — 2026-06-17
|
|
23
|
+
|
|
24
|
+
Minor release — `sigmap conventions` (grounded codegen, Layer 3).
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **`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.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
13
31
|
## [7.6.0] — 2026-06-17
|
|
14
32
|
|
|
15
33
|
Minor release — the grounding benchmark (the offline GATE for grounded codegen).
|
package/gen-context.js
CHANGED
|
@@ -21,6 +21,281 @@ function __require(key) {
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
// ── ./src/cache/freshen ──
|
|
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
|
+
|
|
140
|
+
__factories["./src/conventions/extract"] = function(module, exports) {
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Convention extraction (IMPL.md Layer 3 — grounded code generation).
|
|
144
|
+
*
|
|
145
|
+
* Detects a repo's dominant coding conventions so generated code matches the
|
|
146
|
+
* house style instead of drifting (Cause 4: naming/convention drift). This
|
|
147
|
+
* first slice covers TS/JS/Python and three conventions: file naming, export
|
|
148
|
+
* style, and test framework. `scoreConvention` is the reusable consistency
|
|
149
|
+
* primitive that Gap 1 (scaffold confidence) will also build on.
|
|
150
|
+
*
|
|
151
|
+
* Zero dependencies, bundle-safe (fs + path only).
|
|
152
|
+
*/
|
|
153
|
+
|
|
154
|
+
const fs = require('fs');
|
|
155
|
+
const path = require('path');
|
|
156
|
+
|
|
157
|
+
const JS_TS_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
158
|
+
const PY_EXTS = new Set(['.py']);
|
|
159
|
+
const SCOPED_EXTS = new Set([...JS_TS_EXTS, ...PY_EXTS]);
|
|
160
|
+
|
|
161
|
+
// Consistency tiers (IMPL.md §5.1): a convention is only safe to enforce when
|
|
162
|
+
// it is actually consistent.
|
|
163
|
+
const TIER_CONSISTENT = 0.9;
|
|
164
|
+
const TIER_MOSTLY = 0.7;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Classify a file's base name (without extension) into a naming style.
|
|
168
|
+
* @param {string} basename a file basename, e.g. "user-service.ts"
|
|
169
|
+
* @returns {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'|'other'}
|
|
170
|
+
*/
|
|
171
|
+
function classifyNaming(basename) {
|
|
172
|
+
let stem = String(basename || '');
|
|
173
|
+
const dot = stem.indexOf('.');
|
|
174
|
+
if (dot > 0) stem = stem.slice(0, dot); // strip ext + compound suffix (.test, .d)
|
|
175
|
+
if (!stem) return 'other';
|
|
176
|
+
if (/[-]/.test(stem) && /^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(stem)) return 'kebab-case';
|
|
177
|
+
if (/[_]/.test(stem) && /^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(stem)) return 'snake_case';
|
|
178
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(stem) && /[a-z]/.test(stem)) return 'PascalCase';
|
|
179
|
+
if (/^[a-z][A-Za-z0-9]*$/.test(stem) && /[A-Z]/.test(stem)) return 'camelCase';
|
|
180
|
+
if (/^[a-z][a-z0-9]*$/.test(stem)) return 'camelCase'; // single lowercase word
|
|
181
|
+
return 'other';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Score a set of categorical observations into a dominant convention plus its
|
|
186
|
+
* consistency tier. The reusable primitive (IMPL.md §5.2).
|
|
187
|
+
* @param {string[]} labels observed category for each sample (e.g. naming styles)
|
|
188
|
+
* @returns {{ dominant: string|null, dominantPct: number, total: number,
|
|
189
|
+
* variants: Array<{label:string, count:number, pct:number}>,
|
|
190
|
+
* tier: 'consistent'|'mostly'|'inconsistent'|'unknown' }}
|
|
191
|
+
*/
|
|
192
|
+
function scoreConvention(labels) {
|
|
193
|
+
const list = (labels || []).filter((l) => l != null && l !== 'other');
|
|
194
|
+
const total = list.length;
|
|
195
|
+
if (total === 0) {
|
|
196
|
+
return { dominant: null, dominantPct: 0, total: 0, variants: [], tier: 'unknown' };
|
|
197
|
+
}
|
|
198
|
+
const counts = new Map();
|
|
199
|
+
for (const l of list) counts.set(l, (counts.get(l) || 0) + 1);
|
|
200
|
+
const variants = [...counts.entries()]
|
|
201
|
+
.map(([label, count]) => ({ label, count, pct: count / total }))
|
|
202
|
+
.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
203
|
+
const top = variants[0];
|
|
204
|
+
let tier = 'inconsistent';
|
|
205
|
+
if (top.pct >= TIER_CONSISTENT) tier = 'consistent';
|
|
206
|
+
else if (top.pct >= TIER_MOSTLY) tier = 'mostly';
|
|
207
|
+
return {
|
|
208
|
+
dominant: top.label,
|
|
209
|
+
dominantPct: top.pct,
|
|
210
|
+
total,
|
|
211
|
+
variants,
|
|
212
|
+
tier,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Detect JS/TS export style for a single file's source. */
|
|
217
|
+
function _jsExportStyle(src) {
|
|
218
|
+
const s = String(src || '');
|
|
219
|
+
if (/\bexport\s+default\b/.test(s) || /\bmodule\.exports\s*=\s*(?:function|class|\{?\s*[A-Za-z_$])/.test(s)) {
|
|
220
|
+
// module.exports = { a, b } reads as named; only treat bare assignment as default.
|
|
221
|
+
if (/\bexport\s+default\b/.test(s)) return 'default';
|
|
222
|
+
if (/\bmodule\.exports\s*=\s*\{/.test(s)) return 'named';
|
|
223
|
+
return 'default';
|
|
224
|
+
}
|
|
225
|
+
if (/\bexport\s+(?:const|let|var|function|class|async\s+function|\{|type|interface|enum)\b/.test(s)
|
|
226
|
+
|| /\bmodule\.exports\s*=\s*\{/.test(s) || /\bexports\.[A-Za-z_$]/.test(s)) {
|
|
227
|
+
return 'named';
|
|
228
|
+
}
|
|
229
|
+
return 'other';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Detect the test framework in use from manifests + source heuristics. */
|
|
233
|
+
function _detectTestFramework(cwd, files) {
|
|
234
|
+
const deps = {};
|
|
235
|
+
try {
|
|
236
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
237
|
+
Object.assign(deps, pkg.dependencies, pkg.devDependencies);
|
|
238
|
+
} catch (_) {}
|
|
239
|
+
for (const fw of ['vitest', 'jest', 'mocha', 'ava', 'jasmine']) {
|
|
240
|
+
if (deps[fw]) return fw;
|
|
241
|
+
}
|
|
242
|
+
// Python: pytest in requirements / pyproject.
|
|
243
|
+
for (const manifest of ['requirements.txt', 'requirements-dev.txt', 'pyproject.toml', 'setup.cfg']) {
|
|
244
|
+
try {
|
|
245
|
+
const raw = fs.readFileSync(path.join(cwd, manifest), 'utf8');
|
|
246
|
+
if (/\bpytest\b/.test(raw)) return 'pytest';
|
|
247
|
+
if (/\bunittest\b/.test(raw)) return 'unittest';
|
|
248
|
+
} catch (_) {}
|
|
249
|
+
}
|
|
250
|
+
// Fallback: infer from test file contents.
|
|
251
|
+
for (const f of files) {
|
|
252
|
+
if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) continue;
|
|
253
|
+
let src = '';
|
|
254
|
+
try { src = fs.readFileSync(f, 'utf8'); } catch (_) { continue; }
|
|
255
|
+
if (/\bvi\.(fn|mock|spyOn)\b|from ['"]vitest['"]/.test(src)) return 'vitest';
|
|
256
|
+
if (/\bjest\.(fn|mock|spyOn)\b/.test(src)) return 'jest';
|
|
257
|
+
if (/\bimport pytest\b|@pytest\./.test(src)) return 'pytest';
|
|
258
|
+
if (/\bdescribe\(|\bit\(/.test(src)) return 'mocha/jest-style';
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Extract repo coding conventions for the scoped languages (TS/JS/Python).
|
|
265
|
+
* @param {string} cwd repo root
|
|
266
|
+
* @param {string[]} files absolute paths to source files (e.g. from buildFileList)
|
|
267
|
+
* @returns {{ fileNaming: object, exportStyle: object, testFramework: string|null,
|
|
268
|
+
* scope: string[], scannedFiles: number }}
|
|
269
|
+
*/
|
|
270
|
+
function extractConventions(cwd, files) {
|
|
271
|
+
const scoped = (files || []).filter((f) => SCOPED_EXTS.has(path.extname(f).toLowerCase()));
|
|
272
|
+
const namingLabels = [];
|
|
273
|
+
const exportLabels = [];
|
|
274
|
+
for (const f of scoped) {
|
|
275
|
+
const base = path.basename(f);
|
|
276
|
+
// Skip test files for the naming convention (they have their own naming).
|
|
277
|
+
if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) {
|
|
278
|
+
namingLabels.push(classifyNaming(base));
|
|
279
|
+
}
|
|
280
|
+
if (JS_TS_EXTS.has(path.extname(f).toLowerCase())) {
|
|
281
|
+
let src = '';
|
|
282
|
+
try { src = fs.readFileSync(f, 'utf8'); } catch (_) {}
|
|
283
|
+
exportLabels.push(_jsExportStyle(src));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
fileNaming: scoreConvention(namingLabels),
|
|
288
|
+
exportStyle: scoreConvention(exportLabels),
|
|
289
|
+
testFramework: _detectTestFramework(cwd, scoped),
|
|
290
|
+
scope: ['typescript', 'javascript', 'python'],
|
|
291
|
+
scannedFiles: scoped.length,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = { classifyNaming, scoreConvention, extractConventions };
|
|
296
|
+
|
|
297
|
+
};
|
|
298
|
+
|
|
24
299
|
__factories["./src/cache/freshen"] = function(module, exports) {
|
|
25
300
|
|
|
26
301
|
/**
|
|
@@ -6626,7 +6901,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
6626
6901
|
|
|
6627
6902
|
const SERVER_INFO = {
|
|
6628
6903
|
name: 'sigmap',
|
|
6629
|
-
version: '7.
|
|
6904
|
+
version: '7.8.0',
|
|
6630
6905
|
description: 'SigMap MCP server — code signatures on demand',
|
|
6631
6906
|
};
|
|
6632
6907
|
|
|
@@ -12304,7 +12579,7 @@ function __tryGit(args, opts = {}) {
|
|
|
12304
12579
|
catch (_) { return ''; }
|
|
12305
12580
|
}
|
|
12306
12581
|
|
|
12307
|
-
const VERSION = '7.
|
|
12582
|
+
const VERSION = '7.8.0';
|
|
12308
12583
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
12309
12584
|
|
|
12310
12585
|
function requireSourceOrBundled(key) {
|
|
@@ -15473,6 +15748,80 @@ function main() {
|
|
|
15473
15748
|
process.exit(0);
|
|
15474
15749
|
}
|
|
15475
15750
|
|
|
15751
|
+
// Layer 3: `sigmap conventions` — extract & report repo coding conventions
|
|
15752
|
+
// (file naming, export style, test framework) for TS/JS/Python so generated
|
|
15753
|
+
// code matches the house style. Writes .context/conventions.json.
|
|
15754
|
+
if (args[0] === 'conventions') {
|
|
15755
|
+
const jsonOut = args.includes('--json');
|
|
15756
|
+
const { extractConventions } = requireSourceOrBundled('./src/conventions/extract');
|
|
15757
|
+
const files = buildFileList(cwd, config);
|
|
15758
|
+
const result = extractConventions(cwd, files);
|
|
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
|
+
|
|
15792
|
+
const outDir = path.join(cwd, '.context');
|
|
15793
|
+
const outPath = path.join(outDir, 'conventions.json');
|
|
15794
|
+
try {
|
|
15795
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
15796
|
+
fs.writeFileSync(outPath, JSON.stringify(result, null, 2) + '\n');
|
|
15797
|
+
} catch (e) {
|
|
15798
|
+
console.error(`[sigmap] cannot write ${outPath}: ${e.message}`);
|
|
15799
|
+
process.exit(1);
|
|
15800
|
+
}
|
|
15801
|
+
|
|
15802
|
+
if (jsonOut) {
|
|
15803
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
15804
|
+
process.exit(0);
|
|
15805
|
+
}
|
|
15806
|
+
|
|
15807
|
+
const pct = (n) => `${(n * 100).toFixed(0)}%`;
|
|
15808
|
+
const tierLabel = { consistent: 'consistent', mostly: 'mostly', inconsistent: 'mixed', unknown: 'n/a' };
|
|
15809
|
+
const line = (name, conv) => {
|
|
15810
|
+
if (!conv || conv.total === 0) return ` ${name.padEnd(14)} n/a (no samples)`;
|
|
15811
|
+
const variants = conv.variants.slice(1, 3).map((v) => `${v.label} ${pct(v.pct)}`).join(', ');
|
|
15812
|
+
const tail = variants ? ` · also: ${variants}` : '';
|
|
15813
|
+
return ` ${name.padEnd(14)} ${conv.dominant} ${pct(conv.dominantPct)} [${tierLabel[conv.tier]}]${tail}`;
|
|
15814
|
+
};
|
|
15815
|
+
|
|
15816
|
+
console.log('[sigmap] conventions (TS/JS/Python)');
|
|
15817
|
+
console.log(` scanned ${result.scannedFiles} file${result.scannedFiles === 1 ? '' : 's'}`);
|
|
15818
|
+
console.log(line('file naming', result.fileNaming));
|
|
15819
|
+
console.log(line('export style', result.exportStyle));
|
|
15820
|
+
console.log(` ${'test framework'.padEnd(14)} ${result.testFramework || 'none detected'}`);
|
|
15821
|
+
console.log(`\n → wrote ${path.relative(cwd, outPath)}`);
|
|
15822
|
+
process.exit(0);
|
|
15823
|
+
}
|
|
15824
|
+
|
|
15476
15825
|
// v7.0.0: `sigmap squeeze <file|->` — minimize a pasted stacktrace / CI-log / JSON blob
|
|
15477
15826
|
if (args[0] === 'squeeze') {
|
|
15478
15827
|
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.
|
|
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.
|
|
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.
|
|
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": {
|
|
@@ -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 };
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
const MAX_EXAMPLES = 3;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Score a set of categorical observations into a dominant convention plus its
|
|
49
|
+
* consistency tier. The reusable primitive (IMPL.md §5.2).
|
|
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`.
|
|
53
|
+
* @returns {{ dominant: string|null, dominantPct: number, total: number,
|
|
54
|
+
* variants: Array<{label:string, count:number, pct:number, examples?:string[]}>,
|
|
55
|
+
* tier: 'consistent'|'mostly'|'inconsistent'|'unknown' }}
|
|
56
|
+
*/
|
|
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
|
+
}
|
|
72
|
+
if (total === 0) {
|
|
73
|
+
return { dominant: null, dominantPct: 0, total: 0, variants: [], tier: 'unknown' };
|
|
74
|
+
}
|
|
75
|
+
const variants = [...counts.entries()]
|
|
76
|
+
.map(([label, count]) => {
|
|
77
|
+
const v = { label, count, pct: count / total };
|
|
78
|
+
if (refs) v.examples = examples.get(label) || [];
|
|
79
|
+
return v;
|
|
80
|
+
})
|
|
81
|
+
.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
82
|
+
const top = variants[0];
|
|
83
|
+
let tier = 'inconsistent';
|
|
84
|
+
if (top.pct >= TIER_CONSISTENT) tier = 'consistent';
|
|
85
|
+
else if (top.pct >= TIER_MOSTLY) tier = 'mostly';
|
|
86
|
+
return {
|
|
87
|
+
dominant: top.label,
|
|
88
|
+
dominantPct: top.pct,
|
|
89
|
+
total,
|
|
90
|
+
variants,
|
|
91
|
+
tier,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Detect JS/TS export style for a single file's source. */
|
|
96
|
+
function _jsExportStyle(src) {
|
|
97
|
+
const s = String(src || '');
|
|
98
|
+
if (/\bexport\s+default\b/.test(s) || /\bmodule\.exports\s*=\s*(?:function|class|\{?\s*[A-Za-z_$])/.test(s)) {
|
|
99
|
+
// module.exports = { a, b } reads as named; only treat bare assignment as default.
|
|
100
|
+
if (/\bexport\s+default\b/.test(s)) return 'default';
|
|
101
|
+
if (/\bmodule\.exports\s*=\s*\{/.test(s)) return 'named';
|
|
102
|
+
return 'default';
|
|
103
|
+
}
|
|
104
|
+
if (/\bexport\s+(?:const|let|var|function|class|async\s+function|\{|type|interface|enum)\b/.test(s)
|
|
105
|
+
|| /\bmodule\.exports\s*=\s*\{/.test(s) || /\bexports\.[A-Za-z_$]/.test(s)) {
|
|
106
|
+
return 'named';
|
|
107
|
+
}
|
|
108
|
+
return 'other';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Detect the test framework in use from manifests + source heuristics. */
|
|
112
|
+
function _detectTestFramework(cwd, files) {
|
|
113
|
+
const deps = {};
|
|
114
|
+
try {
|
|
115
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
116
|
+
Object.assign(deps, pkg.dependencies, pkg.devDependencies);
|
|
117
|
+
} catch (_) {}
|
|
118
|
+
for (const fw of ['vitest', 'jest', 'mocha', 'ava', 'jasmine']) {
|
|
119
|
+
if (deps[fw]) return fw;
|
|
120
|
+
}
|
|
121
|
+
// Python: pytest in requirements / pyproject.
|
|
122
|
+
for (const manifest of ['requirements.txt', 'requirements-dev.txt', 'pyproject.toml', 'setup.cfg']) {
|
|
123
|
+
try {
|
|
124
|
+
const raw = fs.readFileSync(path.join(cwd, manifest), 'utf8');
|
|
125
|
+
if (/\bpytest\b/.test(raw)) return 'pytest';
|
|
126
|
+
if (/\bunittest\b/.test(raw)) return 'unittest';
|
|
127
|
+
} catch (_) {}
|
|
128
|
+
}
|
|
129
|
+
// Fallback: infer from test file contents.
|
|
130
|
+
for (const f of files) {
|
|
131
|
+
if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) continue;
|
|
132
|
+
let src = '';
|
|
133
|
+
try { src = fs.readFileSync(f, 'utf8'); } catch (_) { continue; }
|
|
134
|
+
if (/\bvi\.(fn|mock|spyOn)\b|from ['"]vitest['"]/.test(src)) return 'vitest';
|
|
135
|
+
if (/\bjest\.(fn|mock|spyOn)\b/.test(src)) return 'jest';
|
|
136
|
+
if (/\bimport pytest\b|@pytest\./.test(src)) return 'pytest';
|
|
137
|
+
if (/\bdescribe\(|\bit\(/.test(src)) return 'mocha/jest-style';
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Extract repo coding conventions for the scoped languages (TS/JS/Python).
|
|
144
|
+
* @param {string} cwd repo root
|
|
145
|
+
* @param {string[]} files absolute paths to source files (e.g. from buildFileList)
|
|
146
|
+
* @returns {{ fileNaming: object, exportStyle: object, testFramework: string|null,
|
|
147
|
+
* scope: string[], scannedFiles: number }}
|
|
148
|
+
*/
|
|
149
|
+
function extractConventions(cwd, files) {
|
|
150
|
+
const scoped = (files || []).filter((f) => SCOPED_EXTS.has(path.extname(f).toLowerCase()));
|
|
151
|
+
const namingLabels = [];
|
|
152
|
+
const namingRefs = [];
|
|
153
|
+
const exportLabels = [];
|
|
154
|
+
const exportRefs = [];
|
|
155
|
+
for (const f of scoped) {
|
|
156
|
+
const base = path.basename(f);
|
|
157
|
+
// Skip test files for the naming convention (they have their own naming).
|
|
158
|
+
if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) {
|
|
159
|
+
namingLabels.push(classifyNaming(base));
|
|
160
|
+
namingRefs.push(base);
|
|
161
|
+
}
|
|
162
|
+
if (JS_TS_EXTS.has(path.extname(f).toLowerCase())) {
|
|
163
|
+
let src = '';
|
|
164
|
+
try { src = fs.readFileSync(f, 'utf8'); } catch (_) {}
|
|
165
|
+
exportLabels.push(_jsExportStyle(src));
|
|
166
|
+
exportRefs.push(base);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
fileNaming: scoreConvention(namingLabels, namingRefs),
|
|
171
|
+
exportStyle: scoreConvention(exportLabels, exportRefs),
|
|
172
|
+
testFramework: _detectTestFramework(cwd, scoped),
|
|
173
|
+
scope: ['typescript', 'javascript', 'python'],
|
|
174
|
+
scannedFiles: scoped.length,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = { classifyNaming, scoreConvention, extractConventions };
|
package/src/mcp/server.js
CHANGED