sigmap 7.7.0 → 7.9.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 +268 -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 +31 -9
- package/src/conventions/inject.js +98 -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.9.0] — 2026-06-17
|
|
14
|
+
|
|
15
|
+
Minor release — `sigmap conventions --inject` (grounded codegen, Layer 3).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`sigmap conventions --inject` — write the conventions block into CLAUDE.md (#304):** the next slice of Layer 3, completing the "agent sees the house style" link in the grounded-creation loop. Renders the detected conventions (file naming, export style, test framework — each with its dominant pattern and consistency tier) into a marker-delimited block and injects it into `CLAUDE.md`, creating the file if absent. New zero-dependency, bundle-safe `src/conventions/inject.js` (`renderConventionsBlock`, `injectConventions`): the injection is idempotent and marker-scoped (`<!-- sigmap-conventions:start -->` … `:end -->`), preserving all human content and coexisting with the existing `## Auto-generated signatures` block. `--report`, `--fix`, `--update`, `--ci`, and Layer 4 scaffold remain follow-ups.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [7.8.0] — 2026-06-17
|
|
23
|
+
|
|
24
|
+
Minor release — `sigmap conventions --conflicts` (grounded codegen, Layer 3).
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **`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.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
13
31
|
## [7.7.0] — 2026-06-17
|
|
14
32
|
|
|
15
33
|
Minor release — `sigmap conventions` (grounded codegen, Layer 3).
|
package/gen-context.js
CHANGED
|
@@ -22,6 +22,223 @@ function __require(key) {
|
|
|
22
22
|
|
|
23
23
|
// ── ./src/cache/freshen ──
|
|
24
24
|
// ── ./src/conventions/extract ──
|
|
25
|
+
// ── ./src/conventions/conflicts ──
|
|
26
|
+
// ── ./src/conventions/inject ──
|
|
27
|
+
__factories["./src/conventions/inject"] = function(module, exports) {
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* CLAUDE.md convention injection (IMPL.md §7 Phase 2 / §8 step 5).
|
|
31
|
+
*
|
|
32
|
+
* Renders the conventions detected by `extractConventions` into a
|
|
33
|
+
* marker-delimited markdown block and injects it into CLAUDE.md so an agent
|
|
34
|
+
* reading the file plans grounded in the repo's house style. Idempotent and
|
|
35
|
+
* marker-scoped — it never touches human content or the `## Auto-generated
|
|
36
|
+
* signatures` block. Pure string transforms; zero-dependency, bundle-safe.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const START = '<!-- sigmap-conventions:start -->';
|
|
40
|
+
const END = '<!-- sigmap-conventions:end -->';
|
|
41
|
+
|
|
42
|
+
const TIER_NOTE = {
|
|
43
|
+
consistent: 'consistent — match it',
|
|
44
|
+
mostly: 'dominant, with some drift',
|
|
45
|
+
inconsistent: 'no clear convention — check neighboring files',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const NAMES = {
|
|
49
|
+
fileNaming: 'File naming',
|
|
50
|
+
exportStyle: 'Export style',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const _pct = (n) => `${Math.round(n * 100)}%`;
|
|
54
|
+
|
|
55
|
+
function _conventionLine(label, conv) {
|
|
56
|
+
if (!conv || conv.total === 0 || !conv.dominant) return null;
|
|
57
|
+
const note = TIER_NOTE[conv.tier] || conv.tier;
|
|
58
|
+
let line = `- **${label}:** ${conv.dominant} (${_pct(conv.dominantPct)} — ${note}).`;
|
|
59
|
+
const others = conv.variants.slice(1).filter((v) => v.pct > 0);
|
|
60
|
+
if (others.length) {
|
|
61
|
+
line += ` Variants: ${others.map((v) => `${v.label} ${_pct(v.pct)}`).join(', ')}.`;
|
|
62
|
+
}
|
|
63
|
+
return line;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Render the conventions block (including its start/end markers).
|
|
68
|
+
* @param {object} result an `extractConventions` result
|
|
69
|
+
* @param {string} [version] SigMap version for the footer
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function renderConventionsBlock(result, version) {
|
|
73
|
+
const lines = [];
|
|
74
|
+
for (const key of ['fileNaming', 'exportStyle']) {
|
|
75
|
+
const l = _conventionLine(NAMES[key], result && result[key]);
|
|
76
|
+
if (l) lines.push(l);
|
|
77
|
+
}
|
|
78
|
+
if (result && result.testFramework) {
|
|
79
|
+
lines.push(`- **Test framework:** ${result.testFramework}.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const body = lines.length
|
|
83
|
+
? lines
|
|
84
|
+
: ['- No conventions detected yet (run `sigmap conventions` on a TS/JS/Python repo).'];
|
|
85
|
+
|
|
86
|
+
const ver = version ? ` v${version}` : '';
|
|
87
|
+
return [
|
|
88
|
+
START,
|
|
89
|
+
'## Conventions (auto-detected by SigMap)',
|
|
90
|
+
'',
|
|
91
|
+
'Match these when writing or editing code (TS/JS/Python):',
|
|
92
|
+
'',
|
|
93
|
+
...body,
|
|
94
|
+
'',
|
|
95
|
+
`<sub>Generated by SigMap${ver} · run \`sigmap conventions --inject\` to refresh.</sub>`,
|
|
96
|
+
END,
|
|
97
|
+
].join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Inject (or replace) the conventions block in existing CLAUDE.md content.
|
|
102
|
+
* Replaces an existing marked block in place; appends one when absent.
|
|
103
|
+
* Idempotent — never touches content outside the markers.
|
|
104
|
+
* @param {string} existing current file content ('' if the file is new)
|
|
105
|
+
* @param {string} block the block returned by `renderConventionsBlock`
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
function injectConventions(existing, block) {
|
|
109
|
+
const src = String(existing || '');
|
|
110
|
+
const startIdx = src.indexOf(START);
|
|
111
|
+
if (startIdx !== -1) {
|
|
112
|
+
const endIdx = src.indexOf(END, startIdx);
|
|
113
|
+
if (endIdx !== -1) {
|
|
114
|
+
const before = src.slice(0, startIdx);
|
|
115
|
+
const after = src.slice(endIdx + END.length);
|
|
116
|
+
return before + block + after;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (src.trim() === '') return block + '\n';
|
|
120
|
+
const sep = src.endsWith('\n') ? '\n' : '\n\n';
|
|
121
|
+
return src + sep + block + '\n';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { renderConventionsBlock, injectConventions, START, END };
|
|
125
|
+
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
__factories["./src/conventions/conflicts"] = function(module, exports) {
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Convention conflict analysis (IMPL.md §4 / §5.3 — grounded codegen, Layer 3).
|
|
132
|
+
*
|
|
133
|
+
* Given an `extractConventions` result, surface *why* a convention is mixed:
|
|
134
|
+
* every variant pattern with its file count, share, and example files, plus
|
|
135
|
+
* rename suggestions that move minority file-naming files toward the dominant
|
|
136
|
+
* style. Pure, zero-dependency, bundle-safe.
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
/** Split a file name into its stem (before the first dot) and the rest. */
|
|
140
|
+
function _splitName(filename) {
|
|
141
|
+
const s = String(filename || '');
|
|
142
|
+
const dot = s.indexOf('.');
|
|
143
|
+
if (dot <= 0) return { stem: s, ext: '' };
|
|
144
|
+
return { stem: s.slice(0, dot), ext: s.slice(dot) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Break a stem into lowercase word parts regardless of its current style. */
|
|
148
|
+
function _words(stem) {
|
|
149
|
+
return String(stem || '')
|
|
150
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camel/Pascal boundaries
|
|
151
|
+
.replace(/[-_]+/g, ' ') // kebab / snake separators
|
|
152
|
+
.trim()
|
|
153
|
+
.split(/\s+/)
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.map((w) => w.toLowerCase());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const _cap = (w) => w.charAt(0).toUpperCase() + w.slice(1);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Convert a file stem to a target naming style.
|
|
162
|
+
* @param {string} stem name without extension
|
|
163
|
+
* @param {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'} style
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
function toNamingStyle(stem, style) {
|
|
167
|
+
const w = _words(stem);
|
|
168
|
+
if (w.length === 0) return String(stem || '');
|
|
169
|
+
switch (style) {
|
|
170
|
+
case 'PascalCase': return w.map(_cap).join('');
|
|
171
|
+
case 'camelCase': return w[0] + w.slice(1).map(_cap).join('');
|
|
172
|
+
case 'kebab-case': return w.join('-');
|
|
173
|
+
case 'snake_case': return w.join('_');
|
|
174
|
+
default: return String(stem || '');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Rename suggestion to bring a file to the dominant naming style. */
|
|
179
|
+
function renameSuggestion(filename, dominantStyle) {
|
|
180
|
+
const { stem, ext } = _splitName(filename);
|
|
181
|
+
const to = toNamingStyle(stem, dominantStyle) + ext;
|
|
182
|
+
return { from: filename, to };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const LABELS = {
|
|
186
|
+
fileNaming: 'file naming',
|
|
187
|
+
exportStyle: 'export style',
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Analyze an `extractConventions` result for conflicts.
|
|
192
|
+
* @param {object} result the object returned by `extractConventions`
|
|
193
|
+
* @returns {{ hasConflicts: boolean, conventions: Array<{
|
|
194
|
+
* key:string, name:string, dominant:string|null, dominantPct:number,
|
|
195
|
+
* tier:string, total:number,
|
|
196
|
+
* variants:Array<{pattern:string,count:number,pct:number,examples:string[]}>,
|
|
197
|
+
* renames:Array<{from:string,to:string}> }> }}
|
|
198
|
+
*/
|
|
199
|
+
function analyzeConflicts(result) {
|
|
200
|
+
const out = [];
|
|
201
|
+
for (const key of ['fileNaming', 'exportStyle']) {
|
|
202
|
+
const conv = result && result[key];
|
|
203
|
+
// A conflict is any convention with more than one observed pattern.
|
|
204
|
+
if (!conv || conv.total === 0 || conv.variants.length < 2) continue;
|
|
205
|
+
|
|
206
|
+
const variants = conv.variants.map((v) => ({
|
|
207
|
+
pattern: v.label,
|
|
208
|
+
count: v.count,
|
|
209
|
+
pct: v.pct,
|
|
210
|
+
examples: v.examples || [],
|
|
211
|
+
}));
|
|
212
|
+
|
|
213
|
+
// Rename suggestions only for file naming (export style is a code change, not a rename).
|
|
214
|
+
const renames = [];
|
|
215
|
+
if (key === 'fileNaming' && conv.dominant) {
|
|
216
|
+
for (const v of conv.variants) {
|
|
217
|
+
if (v.label === conv.dominant) continue;
|
|
218
|
+
for (const ex of (v.examples || [])) {
|
|
219
|
+
renames.push(renameSuggestion(ex, conv.dominant));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
out.push({
|
|
225
|
+
key,
|
|
226
|
+
name: LABELS[key] || key,
|
|
227
|
+
dominant: conv.dominant,
|
|
228
|
+
dominantPct: conv.dominantPct,
|
|
229
|
+
tier: conv.tier,
|
|
230
|
+
total: conv.total,
|
|
231
|
+
variants,
|
|
232
|
+
renames,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return { hasConflicts: out.length > 0, conventions: out };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { analyzeConflicts, toNamingStyle, renameSuggestion };
|
|
239
|
+
|
|
240
|
+
};
|
|
241
|
+
|
|
25
242
|
__factories["./src/conventions/extract"] = function(module, exports) {
|
|
26
243
|
|
|
27
244
|
/**
|
|
@@ -6786,7 +7003,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
6786
7003
|
|
|
6787
7004
|
const SERVER_INFO = {
|
|
6788
7005
|
name: 'sigmap',
|
|
6789
|
-
version: '7.
|
|
7006
|
+
version: '7.9.0',
|
|
6790
7007
|
description: 'SigMap MCP server — code signatures on demand',
|
|
6791
7008
|
};
|
|
6792
7009
|
|
|
@@ -12464,7 +12681,7 @@ function __tryGit(args, opts = {}) {
|
|
|
12464
12681
|
catch (_) { return ''; }
|
|
12465
12682
|
}
|
|
12466
12683
|
|
|
12467
|
-
const VERSION = '7.
|
|
12684
|
+
const VERSION = '7.9.0';
|
|
12468
12685
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
12469
12686
|
|
|
12470
12687
|
function requireSourceOrBundled(key) {
|
|
@@ -15642,6 +15859,55 @@ function main() {
|
|
|
15642
15859
|
const files = buildFileList(cwd, config);
|
|
15643
15860
|
const result = extractConventions(cwd, files);
|
|
15644
15861
|
|
|
15862
|
+
// `--inject`: write/update the conventions block in CLAUDE.md (idempotent).
|
|
15863
|
+
if (args.includes('--inject')) {
|
|
15864
|
+
const { renderConventionsBlock, injectConventions } = requireSourceOrBundled('./src/conventions/inject');
|
|
15865
|
+
const claudePath = path.join(cwd, 'CLAUDE.md');
|
|
15866
|
+
let existing = '';
|
|
15867
|
+
try { if (fs.existsSync(claudePath)) existing = fs.readFileSync(claudePath, 'utf8'); } catch (_) {}
|
|
15868
|
+
const block = renderConventionsBlock(result, VERSION);
|
|
15869
|
+
try {
|
|
15870
|
+
fs.writeFileSync(claudePath, injectConventions(existing, block));
|
|
15871
|
+
} catch (e) {
|
|
15872
|
+
console.error(`[sigmap] cannot write ${claudePath}: ${e.message}`);
|
|
15873
|
+
process.exit(1);
|
|
15874
|
+
}
|
|
15875
|
+
console.log(`[sigmap] conventions → injected block into ${path.relative(cwd, claudePath) || 'CLAUDE.md'}`);
|
|
15876
|
+
process.exit(0);
|
|
15877
|
+
}
|
|
15878
|
+
|
|
15879
|
+
// `--conflicts`: surface why a convention is mixed (breakdown + rename suggestions).
|
|
15880
|
+
if (args.includes('--conflicts')) {
|
|
15881
|
+
const { analyzeConflicts } = requireSourceOrBundled('./src/conventions/conflicts');
|
|
15882
|
+
const report = analyzeConflicts(result);
|
|
15883
|
+
if (jsonOut) {
|
|
15884
|
+
process.stdout.write(JSON.stringify(report) + '\n');
|
|
15885
|
+
process.exit(0);
|
|
15886
|
+
}
|
|
15887
|
+
const pctC = (n) => `${(n * 100).toFixed(0)}%`;
|
|
15888
|
+
if (!report.hasConflicts) {
|
|
15889
|
+
console.log('[sigmap] conventions --conflicts (TS/JS/Python)');
|
|
15890
|
+
console.log(' no conflicts — conventions are consistent ✓');
|
|
15891
|
+
process.exit(0);
|
|
15892
|
+
}
|
|
15893
|
+
console.log('[sigmap] conventions --conflicts (TS/JS/Python)');
|
|
15894
|
+
for (const c of report.conventions) {
|
|
15895
|
+
console.log(`\n ${c.name} — dominant: ${c.dominant} ${pctC(c.dominantPct)} [${c.tier}]`);
|
|
15896
|
+
for (const v of c.variants) {
|
|
15897
|
+
const barLen = Math.max(1, Math.round(v.pct * 20));
|
|
15898
|
+
const bar = '█'.repeat(barLen);
|
|
15899
|
+
const tag = v.pattern === c.dominant ? ' (dominant)' : '';
|
|
15900
|
+
const ex = v.examples.length ? ` e.g. ${v.examples.join(', ')}` : '';
|
|
15901
|
+
console.log(` ${v.pattern.padEnd(12)} ${String(v.count).padStart(4)} ${pctC(v.pct).padStart(4)} ${bar}${tag}${ex}`);
|
|
15902
|
+
}
|
|
15903
|
+
if (c.renames.length) {
|
|
15904
|
+
console.log(` rename to match ${c.dominant}:`);
|
|
15905
|
+
for (const r of c.renames) console.log(` ${r.from} → ${r.to}`);
|
|
15906
|
+
}
|
|
15907
|
+
}
|
|
15908
|
+
process.exit(0);
|
|
15909
|
+
}
|
|
15910
|
+
|
|
15645
15911
|
const outDir = path.join(cwd, '.context');
|
|
15646
15912
|
const outPath = path.join(outDir, 'conventions.json');
|
|
15647
15913
|
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.
|
|
12
|
+
# Version: 7.9.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.9.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.9.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 };
|
|
@@ -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
|
|
55
|
-
const
|
|
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]) =>
|
|
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,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLAUDE.md convention injection (IMPL.md §7 Phase 2 / §8 step 5).
|
|
5
|
+
*
|
|
6
|
+
* Renders the conventions detected by `extractConventions` into a
|
|
7
|
+
* marker-delimited markdown block and injects it into CLAUDE.md so an agent
|
|
8
|
+
* reading the file plans grounded in the repo's house style. Idempotent and
|
|
9
|
+
* marker-scoped — it never touches human content or the `## Auto-generated
|
|
10
|
+
* signatures` block. Pure string transforms; zero-dependency, bundle-safe.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const START = '<!-- sigmap-conventions:start -->';
|
|
14
|
+
const END = '<!-- sigmap-conventions:end -->';
|
|
15
|
+
|
|
16
|
+
const TIER_NOTE = {
|
|
17
|
+
consistent: 'consistent — match it',
|
|
18
|
+
mostly: 'dominant, with some drift',
|
|
19
|
+
inconsistent: 'no clear convention — check neighboring files',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const NAMES = {
|
|
23
|
+
fileNaming: 'File naming',
|
|
24
|
+
exportStyle: 'Export style',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const _pct = (n) => `${Math.round(n * 100)}%`;
|
|
28
|
+
|
|
29
|
+
function _conventionLine(label, conv) {
|
|
30
|
+
if (!conv || conv.total === 0 || !conv.dominant) return null;
|
|
31
|
+
const note = TIER_NOTE[conv.tier] || conv.tier;
|
|
32
|
+
let line = `- **${label}:** ${conv.dominant} (${_pct(conv.dominantPct)} — ${note}).`;
|
|
33
|
+
const others = conv.variants.slice(1).filter((v) => v.pct > 0);
|
|
34
|
+
if (others.length) {
|
|
35
|
+
line += ` Variants: ${others.map((v) => `${v.label} ${_pct(v.pct)}`).join(', ')}.`;
|
|
36
|
+
}
|
|
37
|
+
return line;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Render the conventions block (including its start/end markers).
|
|
42
|
+
* @param {object} result an `extractConventions` result
|
|
43
|
+
* @param {string} [version] SigMap version for the footer
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function renderConventionsBlock(result, version) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
for (const key of ['fileNaming', 'exportStyle']) {
|
|
49
|
+
const l = _conventionLine(NAMES[key], result && result[key]);
|
|
50
|
+
if (l) lines.push(l);
|
|
51
|
+
}
|
|
52
|
+
if (result && result.testFramework) {
|
|
53
|
+
lines.push(`- **Test framework:** ${result.testFramework}.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const body = lines.length
|
|
57
|
+
? lines
|
|
58
|
+
: ['- No conventions detected yet (run `sigmap conventions` on a TS/JS/Python repo).'];
|
|
59
|
+
|
|
60
|
+
const ver = version ? ` v${version}` : '';
|
|
61
|
+
return [
|
|
62
|
+
START,
|
|
63
|
+
'## Conventions (auto-detected by SigMap)',
|
|
64
|
+
'',
|
|
65
|
+
'Match these when writing or editing code (TS/JS/Python):',
|
|
66
|
+
'',
|
|
67
|
+
...body,
|
|
68
|
+
'',
|
|
69
|
+
`<sub>Generated by SigMap${ver} · run \`sigmap conventions --inject\` to refresh.</sub>`,
|
|
70
|
+
END,
|
|
71
|
+
].join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Inject (or replace) the conventions block in existing CLAUDE.md content.
|
|
76
|
+
* Replaces an existing marked block in place; appends one when absent.
|
|
77
|
+
* Idempotent — never touches content outside the markers.
|
|
78
|
+
* @param {string} existing current file content ('' if the file is new)
|
|
79
|
+
* @param {string} block the block returned by `renderConventionsBlock`
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
function injectConventions(existing, block) {
|
|
83
|
+
const src = String(existing || '');
|
|
84
|
+
const startIdx = src.indexOf(START);
|
|
85
|
+
if (startIdx !== -1) {
|
|
86
|
+
const endIdx = src.indexOf(END, startIdx);
|
|
87
|
+
if (endIdx !== -1) {
|
|
88
|
+
const before = src.slice(0, startIdx);
|
|
89
|
+
const after = src.slice(endIdx + END.length);
|
|
90
|
+
return before + block + after;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (src.trim() === '') return block + '\n';
|
|
94
|
+
const sep = src.endsWith('\n') ? '\n' : '\n\n';
|
|
95
|
+
return src + sep + block + '\n';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { renderConventionsBlock, injectConventions, START, END };
|
package/src/mcp/server.js
CHANGED