sigmap 7.15.0 → 7.17.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 +203 -2
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +3 -2
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/conventions/fix.js +58 -0
- package/src/eval/llm-ablation.js +113 -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.17.0] — 2026-06-18
|
|
14
|
+
|
|
15
|
+
Minor release — `sigmap conventions --fix` (grounded codegen, Layer 3 — completes the conventions flags).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`sigmap conventions --fix` — exhaustive rename/move checklist (#328):** the complete, actionable list of every source file whose name doesn't match the dominant convention, with full from→to paths, ready to paste into a task or PR. Distinct from `--conflicts` (a diagnostic summary with up to 3 example basenames) — `--fix` lists *every* offending file with its real path. New zero-dependency, bundle-safe `src/conventions/fix.js` (`buildFixList`) reuses `classifyNaming` + `toNamingStyle`; the command prints a checkbox checklist + count (or "no fixes needed") and is read-only (it never performs renames). `--json` for machine output. This completes the `conventions` flag set (`--conflicts`, `--inject`, `--report`, `--ci`, `--fix`).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [7.16.0] — 2026-06-18
|
|
23
|
+
|
|
24
|
+
Minor release — LLM A/B hallucination ablation harness (grounded codegen, IMPL §9).
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **LLM A/B hallucination ablation harness (#325):** the honest measurement behind the grounded-codegen plan (IMPL §9). Runs a model twice per task — (A) no SigMap context, (B) with SigMap grounding — pipes both outputs through the hallucination guard, and reports the measured delta in flagged codebase-fact errors. New zero-dependency, bundle-safe `src/eval/llm-ablation.js` (`buildGrounding`, `scoreAnswer`, `runAblation`) keeps the model call **injected**, so the harness is fully offline-testable; the live runner `scripts/run-llm-ablation.mjs` wires Anthropic via `ANTHROPIC_API_KEY` and prints the A/B table + delta (`npm run benchmark:llm-ablation`), degrading to a graceful skip (exit 0) when no key is set. The network fetch is confined to `scripts/`, never the published library surface. Starter corpus in `benchmarks/llm-ablation-tasks.json`. This turns §9 from an offline coverage proxy into a ready-to-run real A/B — the moment a key is present, it produces the measured hallucination delta.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
13
31
|
## [7.15.0] — 2026-06-18
|
|
14
32
|
|
|
15
33
|
Minor release — `sigmap conventions --ci` (grounded codegen, Layer 3 polish).
|
package/gen-context.js
CHANGED
|
@@ -31,6 +31,185 @@ function __require(key) {
|
|
|
31
31
|
// ── ./src/create/orchestrate ──
|
|
32
32
|
// ── ./src/conventions/report ──
|
|
33
33
|
// ── ./src/conventions/ci ──
|
|
34
|
+
// ── ./src/eval/llm-ablation ──
|
|
35
|
+
// ── ./src/conventions/fix ──
|
|
36
|
+
__factories["./src/conventions/fix"] = function(module, exports) {
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convention fix list (IMPL.md §4 — `conventions --fix`).
|
|
40
|
+
*
|
|
41
|
+
* The complete, actionable rename checklist: every scoped source file whose
|
|
42
|
+
* name doesn't match the dominant file-naming convention, with full from→to
|
|
43
|
+
* paths. Distinct from `--conflicts` (a diagnostic summary with up to 3 example
|
|
44
|
+
* basenames) — `--fix` lists *every* offending file with its real path, ready
|
|
45
|
+
* to paste into a task or PR. Pure, zero-dependency, bundle-safe.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
const path = require('path');
|
|
49
|
+
const { classifyNaming } = __require('./src/conventions/extract');
|
|
50
|
+
const { toNamingStyle } = __require('./src/conventions/conflicts');
|
|
51
|
+
|
|
52
|
+
const JS_TS_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
53
|
+
const PY_EXTS = new Set(['.py']);
|
|
54
|
+
const SCOPED_EXTS = new Set([...JS_TS_EXTS, ...PY_EXTS]);
|
|
55
|
+
|
|
56
|
+
const TEST_RE = /\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/;
|
|
57
|
+
|
|
58
|
+
/** Rename a file path's basename to the target naming style (keep dir + ext). */
|
|
59
|
+
function _renamePath(relPath, style) {
|
|
60
|
+
const dir = relPath.includes('/') ? relPath.slice(0, relPath.lastIndexOf('/') + 1) : '';
|
|
61
|
+
const base = relPath.includes('/') ? relPath.slice(relPath.lastIndexOf('/') + 1) : relPath;
|
|
62
|
+
const dot = base.indexOf('.');
|
|
63
|
+
const stem = dot > 0 ? base.slice(0, dot) : base;
|
|
64
|
+
const ext = dot > 0 ? base.slice(dot) : '';
|
|
65
|
+
return `${dir}${toNamingStyle(stem, style)}${ext}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build the exhaustive rename checklist for the dominant file-naming convention.
|
|
70
|
+
* @param {string} cwd repo root (for relative paths)
|
|
71
|
+
* @param {string[]} files absolute source paths (e.g. from buildFileList)
|
|
72
|
+
* @param {object} conventions an `extractConventions` result (for the dominant style)
|
|
73
|
+
* @returns {{ dominant: string|null, renames: Array<{from:string,to:string,fromStyle:string}>, count: number }}
|
|
74
|
+
*/
|
|
75
|
+
function buildFixList(cwd, files, conventions) {
|
|
76
|
+
const dominant = conventions && conventions.fileNaming && conventions.fileNaming.dominant;
|
|
77
|
+
if (!dominant) return { dominant: null, renames: [], count: 0 };
|
|
78
|
+
|
|
79
|
+
const renames = [];
|
|
80
|
+
for (const f of files || []) {
|
|
81
|
+
if (!SCOPED_EXTS.has(path.extname(f).toLowerCase())) continue;
|
|
82
|
+
if (TEST_RE.test(f)) continue;
|
|
83
|
+
const base = path.basename(f);
|
|
84
|
+
const style = classifyNaming(base);
|
|
85
|
+
if (style === 'other' || style === dominant) continue;
|
|
86
|
+
const rel = path.relative(cwd, f).replace(/\\/g, '/');
|
|
87
|
+
renames.push({ from: rel, to: _renamePath(rel, dominant), fromStyle: style });
|
|
88
|
+
}
|
|
89
|
+
renames.sort((a, b) => a.from.localeCompare(b.from));
|
|
90
|
+
return { dominant, renames, count: renames.length };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { buildFixList };
|
|
94
|
+
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
__factories["./src/eval/llm-ablation"] = function(module, exports) {
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* LLM A/B hallucination ablation (IMPL.md §9) — the honest measurement.
|
|
101
|
+
*
|
|
102
|
+
* Runs a model twice per task — (A) no SigMap context, (B) with SigMap
|
|
103
|
+
* grounding — pipes both outputs through the hallucination guard, and reports
|
|
104
|
+
* the measured delta in flagged codebase-fact errors. The model call is
|
|
105
|
+
* INJECTED (`complete(prompt) → text`), so the harness itself is pure and
|
|
106
|
+
* offline-testable; the live model adapter lives in `scripts/run-llm-ablation.mjs`.
|
|
107
|
+
* Zero-dependency, bundle-safe (no network here).
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
const { verify } = __require('./src/verify/hallucination-guard');
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build the SigMap grounding block for a repo — what we prepend to a task
|
|
114
|
+
* prompt in arm B. Conventions (the house style) + the known-symbol list
|
|
115
|
+
* (so the model can reference real names instead of guessing).
|
|
116
|
+
* @param {string} cwd
|
|
117
|
+
* @param {object} [opts]
|
|
118
|
+
* @param {number} [opts.maxSymbols=80]
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
function buildGrounding(cwd, opts = {}) {
|
|
122
|
+
const maxSymbols = opts.maxSymbols != null ? opts.maxSymbols : 80;
|
|
123
|
+
const parts = [];
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const { extractConventions } = __require('./src/conventions/extract');
|
|
127
|
+
const { renderConventionsBlock } = __require('./src/conventions/inject');
|
|
128
|
+
const { loadConfig } = __require('./src/config/loader');
|
|
129
|
+
let files = [];
|
|
130
|
+
try {
|
|
131
|
+
const cfg = loadConfig(cwd);
|
|
132
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
133
|
+
files = [...buildSigIndex(cwd).keys()];
|
|
134
|
+
void cfg;
|
|
135
|
+
} catch (_) {}
|
|
136
|
+
const conv = extractConventions(cwd, files);
|
|
137
|
+
parts.push(renderConventionsBlock(conv));
|
|
138
|
+
} catch (_) {}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const { buildSymbolSet } = __require('./src/verify/hallucination-guard');
|
|
142
|
+
const { set } = buildSymbolSet(cwd);
|
|
143
|
+
const names = [...set].slice(0, maxSymbols);
|
|
144
|
+
if (names.length) parts.push(`## Known symbols (reference these exactly)\n${names.join(', ')}`);
|
|
145
|
+
} catch (_) {}
|
|
146
|
+
|
|
147
|
+
return parts.join('\n\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Count flagged codebase-fact errors in an answer (the §9 metric).
|
|
152
|
+
* @param {string} answerText
|
|
153
|
+
* @param {string} cwd
|
|
154
|
+
* @returns {number}
|
|
155
|
+
*/
|
|
156
|
+
function scoreAnswer(answerText, cwd) {
|
|
157
|
+
try {
|
|
158
|
+
const { summary } = verify(String(answerText || ''), cwd);
|
|
159
|
+
return summary.total || 0;
|
|
160
|
+
} catch (_) {
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Run the A/B ablation over a task corpus.
|
|
167
|
+
* @param {Array<{id:string, prompt:string}>} tasks
|
|
168
|
+
* @param {string} cwd
|
|
169
|
+
* @param {(prompt:string, meta:object)=>string} complete injected model call
|
|
170
|
+
* @param {object} [opts]
|
|
171
|
+
* @param {string} [opts.grounding] precomputed grounding (else built from cwd)
|
|
172
|
+
* @returns {{ tasks: object[], aggregate: object }}
|
|
173
|
+
*/
|
|
174
|
+
function runAblation(tasks, cwd, complete, opts = {}) {
|
|
175
|
+
const grounding = opts.grounding != null ? opts.grounding : buildGrounding(cwd);
|
|
176
|
+
const rows = [];
|
|
177
|
+
let sumA = 0;
|
|
178
|
+
let sumB = 0;
|
|
179
|
+
|
|
180
|
+
for (const task of tasks || []) {
|
|
181
|
+
const basePrompt = task.prompt || '';
|
|
182
|
+
const groundedPrompt = grounding ? `${grounding}\n\n---\n\n${basePrompt}` : basePrompt;
|
|
183
|
+
|
|
184
|
+
const outA = String(complete(basePrompt, { id: task.id, grounded: false }) || '');
|
|
185
|
+
const outB = String(complete(groundedPrompt, { id: task.id, grounded: true }) || '');
|
|
186
|
+
|
|
187
|
+
const aFlagged = scoreAnswer(outA, cwd);
|
|
188
|
+
const bFlagged = scoreAnswer(outB, cwd);
|
|
189
|
+
sumA += aFlagged;
|
|
190
|
+
sumB += bFlagged;
|
|
191
|
+
rows.push({ id: task.id, aFlagged, bFlagged });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const n = rows.length;
|
|
195
|
+
const per100 = (sum) => (n > 0 ? (sum / n) * 100 : 0);
|
|
196
|
+
return {
|
|
197
|
+
tasks: rows,
|
|
198
|
+
aggregate: {
|
|
199
|
+
n,
|
|
200
|
+
withoutFlagged: sumA,
|
|
201
|
+
withFlagged: sumB,
|
|
202
|
+
delta: sumA - sumB,
|
|
203
|
+
withoutPer100: per100(sumA),
|
|
204
|
+
withPer100: per100(sumB),
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = { buildGrounding, scoreAnswer, runAblation };
|
|
210
|
+
|
|
211
|
+
};
|
|
212
|
+
|
|
34
213
|
__factories["./src/conventions/ci"] = function(module, exports) {
|
|
35
214
|
|
|
36
215
|
/**
|
|
@@ -7590,7 +7769,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
7590
7769
|
|
|
7591
7770
|
const SERVER_INFO = {
|
|
7592
7771
|
name: 'sigmap',
|
|
7593
|
-
version: '7.
|
|
7772
|
+
version: '7.17.0',
|
|
7594
7773
|
description: 'SigMap MCP server — code signatures on demand',
|
|
7595
7774
|
};
|
|
7596
7775
|
|
|
@@ -13268,7 +13447,7 @@ function __tryGit(args, opts = {}) {
|
|
|
13268
13447
|
catch (_) { return ''; }
|
|
13269
13448
|
}
|
|
13270
13449
|
|
|
13271
|
-
const VERSION = '7.
|
|
13450
|
+
const VERSION = '7.17.0';
|
|
13272
13451
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
13273
13452
|
|
|
13274
13453
|
function requireSourceOrBundled(key) {
|
|
@@ -16463,6 +16642,28 @@ function main() {
|
|
|
16463
16642
|
process.exit(0);
|
|
16464
16643
|
}
|
|
16465
16644
|
|
|
16645
|
+
// `--fix`: exhaustive rename checklist — every file not matching the dominant style.
|
|
16646
|
+
if (args.includes('--fix')) {
|
|
16647
|
+
const { buildFixList } = requireSourceOrBundled('./src/conventions/fix');
|
|
16648
|
+
const fixList = buildFixList(cwd, files, result);
|
|
16649
|
+
if (jsonOut) {
|
|
16650
|
+
process.stdout.write(JSON.stringify(fixList) + '\n');
|
|
16651
|
+
process.exit(0);
|
|
16652
|
+
}
|
|
16653
|
+
console.log('[sigmap] conventions --fix (TS/JS/Python)');
|
|
16654
|
+
if (!fixList.dominant) {
|
|
16655
|
+
console.log(' no dominant file-naming convention — nothing to fix');
|
|
16656
|
+
process.exit(0);
|
|
16657
|
+
}
|
|
16658
|
+
if (fixList.count === 0) {
|
|
16659
|
+
console.log(` ✓ no fixes needed — every file matches ${fixList.dominant}`);
|
|
16660
|
+
process.exit(0);
|
|
16661
|
+
}
|
|
16662
|
+
console.log(` ${fixList.count} file${fixList.count === 1 ? '' : 's'} to rename to ${fixList.dominant}:`);
|
|
16663
|
+
for (const r of fixList.renames) console.log(` - [ ] ${r.from} → ${r.to}`);
|
|
16664
|
+
process.exit(0);
|
|
16665
|
+
}
|
|
16666
|
+
|
|
16466
16667
|
// `--ci`: gate — fail when overall consistency is below a threshold (or regresses).
|
|
16467
16668
|
if (args.includes('--ci')) {
|
|
16468
16669
|
const { ciGate } = requireSourceOrBundled('./src/conventions/ci');
|
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.17.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.17.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.17.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": {
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"generate:llms": "node scripts/generate-llms.mjs",
|
|
40
40
|
"validate:llms": "node scripts/validate-llms.mjs",
|
|
41
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
|
+
"benchmark:grounding": "node scripts/run-hallucination-benchmark.mjs",
|
|
43
|
+
"benchmark:llm-ablation": "node scripts/run-llm-ablation.mjs"
|
|
43
44
|
},
|
|
44
45
|
"files": [
|
|
45
46
|
"gen-context.js",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convention fix list (IMPL.md §4 — `conventions --fix`).
|
|
5
|
+
*
|
|
6
|
+
* The complete, actionable rename checklist: every scoped source file whose
|
|
7
|
+
* name doesn't match the dominant file-naming convention, with full from→to
|
|
8
|
+
* paths. Distinct from `--conflicts` (a diagnostic summary with up to 3 example
|
|
9
|
+
* basenames) — `--fix` lists *every* offending file with its real path, ready
|
|
10
|
+
* to paste into a task or PR. Pure, zero-dependency, bundle-safe.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { classifyNaming } = require('./extract');
|
|
15
|
+
const { toNamingStyle } = require('./conflicts');
|
|
16
|
+
|
|
17
|
+
const JS_TS_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
18
|
+
const PY_EXTS = new Set(['.py']);
|
|
19
|
+
const SCOPED_EXTS = new Set([...JS_TS_EXTS, ...PY_EXTS]);
|
|
20
|
+
|
|
21
|
+
const TEST_RE = /\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/;
|
|
22
|
+
|
|
23
|
+
/** Rename a file path's basename to the target naming style (keep dir + ext). */
|
|
24
|
+
function _renamePath(relPath, style) {
|
|
25
|
+
const dir = relPath.includes('/') ? relPath.slice(0, relPath.lastIndexOf('/') + 1) : '';
|
|
26
|
+
const base = relPath.includes('/') ? relPath.slice(relPath.lastIndexOf('/') + 1) : relPath;
|
|
27
|
+
const dot = base.indexOf('.');
|
|
28
|
+
const stem = dot > 0 ? base.slice(0, dot) : base;
|
|
29
|
+
const ext = dot > 0 ? base.slice(dot) : '';
|
|
30
|
+
return `${dir}${toNamingStyle(stem, style)}${ext}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the exhaustive rename checklist for the dominant file-naming convention.
|
|
35
|
+
* @param {string} cwd repo root (for relative paths)
|
|
36
|
+
* @param {string[]} files absolute source paths (e.g. from buildFileList)
|
|
37
|
+
* @param {object} conventions an `extractConventions` result (for the dominant style)
|
|
38
|
+
* @returns {{ dominant: string|null, renames: Array<{from:string,to:string,fromStyle:string}>, count: number }}
|
|
39
|
+
*/
|
|
40
|
+
function buildFixList(cwd, files, conventions) {
|
|
41
|
+
const dominant = conventions && conventions.fileNaming && conventions.fileNaming.dominant;
|
|
42
|
+
if (!dominant) return { dominant: null, renames: [], count: 0 };
|
|
43
|
+
|
|
44
|
+
const renames = [];
|
|
45
|
+
for (const f of files || []) {
|
|
46
|
+
if (!SCOPED_EXTS.has(path.extname(f).toLowerCase())) continue;
|
|
47
|
+
if (TEST_RE.test(f)) continue;
|
|
48
|
+
const base = path.basename(f);
|
|
49
|
+
const style = classifyNaming(base);
|
|
50
|
+
if (style === 'other' || style === dominant) continue;
|
|
51
|
+
const rel = path.relative(cwd, f).replace(/\\/g, '/');
|
|
52
|
+
renames.push({ from: rel, to: _renamePath(rel, dominant), fromStyle: style });
|
|
53
|
+
}
|
|
54
|
+
renames.sort((a, b) => a.from.localeCompare(b.from));
|
|
55
|
+
return { dominant, renames, count: renames.length };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { buildFixList };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LLM A/B hallucination ablation (IMPL.md §9) — the honest measurement.
|
|
5
|
+
*
|
|
6
|
+
* Runs a model twice per task — (A) no SigMap context, (B) with SigMap
|
|
7
|
+
* grounding — pipes both outputs through the hallucination guard, and reports
|
|
8
|
+
* the measured delta in flagged codebase-fact errors. The model call is
|
|
9
|
+
* INJECTED (`complete(prompt) → text`), so the harness itself is pure and
|
|
10
|
+
* offline-testable; the live model adapter lives in `scripts/run-llm-ablation.mjs`.
|
|
11
|
+
* Zero-dependency, bundle-safe (no network here).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { verify } = require('../verify/hallucination-guard');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the SigMap grounding block for a repo — what we prepend to a task
|
|
18
|
+
* prompt in arm B. Conventions (the house style) + the known-symbol list
|
|
19
|
+
* (so the model can reference real names instead of guessing).
|
|
20
|
+
* @param {string} cwd
|
|
21
|
+
* @param {object} [opts]
|
|
22
|
+
* @param {number} [opts.maxSymbols=80]
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function buildGrounding(cwd, opts = {}) {
|
|
26
|
+
const maxSymbols = opts.maxSymbols != null ? opts.maxSymbols : 80;
|
|
27
|
+
const parts = [];
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const { extractConventions } = require('../conventions/extract');
|
|
31
|
+
const { renderConventionsBlock } = require('../conventions/inject');
|
|
32
|
+
const { loadConfig } = require('../config/loader');
|
|
33
|
+
let files = [];
|
|
34
|
+
try {
|
|
35
|
+
const cfg = loadConfig(cwd);
|
|
36
|
+
const { buildSigIndex } = require('../retrieval/ranker');
|
|
37
|
+
files = [...buildSigIndex(cwd).keys()];
|
|
38
|
+
void cfg;
|
|
39
|
+
} catch (_) {}
|
|
40
|
+
const conv = extractConventions(cwd, files);
|
|
41
|
+
parts.push(renderConventionsBlock(conv));
|
|
42
|
+
} catch (_) {}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const { buildSymbolSet } = require('../verify/hallucination-guard');
|
|
46
|
+
const { set } = buildSymbolSet(cwd);
|
|
47
|
+
const names = [...set].slice(0, maxSymbols);
|
|
48
|
+
if (names.length) parts.push(`## Known symbols (reference these exactly)\n${names.join(', ')}`);
|
|
49
|
+
} catch (_) {}
|
|
50
|
+
|
|
51
|
+
return parts.join('\n\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Count flagged codebase-fact errors in an answer (the §9 metric).
|
|
56
|
+
* @param {string} answerText
|
|
57
|
+
* @param {string} cwd
|
|
58
|
+
* @returns {number}
|
|
59
|
+
*/
|
|
60
|
+
function scoreAnswer(answerText, cwd) {
|
|
61
|
+
try {
|
|
62
|
+
const { summary } = verify(String(answerText || ''), cwd);
|
|
63
|
+
return summary.total || 0;
|
|
64
|
+
} catch (_) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Run the A/B ablation over a task corpus.
|
|
71
|
+
* @param {Array<{id:string, prompt:string}>} tasks
|
|
72
|
+
* @param {string} cwd
|
|
73
|
+
* @param {(prompt:string, meta:object)=>string} complete injected model call
|
|
74
|
+
* @param {object} [opts]
|
|
75
|
+
* @param {string} [opts.grounding] precomputed grounding (else built from cwd)
|
|
76
|
+
* @returns {{ tasks: object[], aggregate: object }}
|
|
77
|
+
*/
|
|
78
|
+
function runAblation(tasks, cwd, complete, opts = {}) {
|
|
79
|
+
const grounding = opts.grounding != null ? opts.grounding : buildGrounding(cwd);
|
|
80
|
+
const rows = [];
|
|
81
|
+
let sumA = 0;
|
|
82
|
+
let sumB = 0;
|
|
83
|
+
|
|
84
|
+
for (const task of tasks || []) {
|
|
85
|
+
const basePrompt = task.prompt || '';
|
|
86
|
+
const groundedPrompt = grounding ? `${grounding}\n\n---\n\n${basePrompt}` : basePrompt;
|
|
87
|
+
|
|
88
|
+
const outA = String(complete(basePrompt, { id: task.id, grounded: false }) || '');
|
|
89
|
+
const outB = String(complete(groundedPrompt, { id: task.id, grounded: true }) || '');
|
|
90
|
+
|
|
91
|
+
const aFlagged = scoreAnswer(outA, cwd);
|
|
92
|
+
const bFlagged = scoreAnswer(outB, cwd);
|
|
93
|
+
sumA += aFlagged;
|
|
94
|
+
sumB += bFlagged;
|
|
95
|
+
rows.push({ id: task.id, aFlagged, bFlagged });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const n = rows.length;
|
|
99
|
+
const per100 = (sum) => (n > 0 ? (sum / n) * 100 : 0);
|
|
100
|
+
return {
|
|
101
|
+
tasks: rows,
|
|
102
|
+
aggregate: {
|
|
103
|
+
n,
|
|
104
|
+
withoutFlagged: sumA,
|
|
105
|
+
withFlagged: sumB,
|
|
106
|
+
delta: sumA - sumB,
|
|
107
|
+
withoutPer100: per100(sumA),
|
|
108
|
+
withPer100: per100(sumB),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { buildGrounding, scoreAnswer, runAblation };
|
package/src/mcp/server.js
CHANGED