sigmap 8.9.1 → 8.11.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/AGENTS.md +589 -182
- package/CHANGELOG.md +38 -0
- package/README.md +7 -3
- package/gen-context.js +759 -212
- package/llms-full.txt +4 -2
- package/llms.txt +2 -2
- package/package.json +10 -3
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/defaults.js +3 -0
- package/src/conventions/extract.js +13 -3
- package/src/conventions/fix.js +4 -1
- package/src/extractors/javascript.js +4 -4
- package/src/extractors/typescript.js +5 -5
- package/src/format/terse.js +86 -0
- package/src/graph/builder.js +124 -16
- package/src/graph/impact.js +2 -2
- package/src/health/scorer.js +172 -97
- package/src/judge/judge-engine.js +62 -2
- package/src/mcp/server.js +1 -1
- package/src/plan/planner.js +44 -15
- package/src/review/pr-evidence.js +2 -1
- package/src/review/review-pr.js +24 -2
- package/src/util/truncate.js +42 -0
package/gen-context.js
CHANGED
|
@@ -1501,6 +1501,9 @@ __factories["./src/config/defaults"] = function(module, exports) {
|
|
|
1501
1501
|
// Include a compact `name@version` list of installed direct deps (D8)
|
|
1502
1502
|
versionPins: true,
|
|
1503
1503
|
|
|
1504
|
+
// Terse signature encoding — deterministic compaction of sig lines (D7, opt-in)
|
|
1505
|
+
terse: false,
|
|
1506
|
+
|
|
1504
1507
|
// Include TODO/FIXME/HACK/XXX comments as compact section
|
|
1505
1508
|
todos: true,
|
|
1506
1509
|
|
|
@@ -2056,8 +2059,16 @@ __factories["./src/conventions/extract"] = function(module, exports) {
|
|
|
2056
2059
|
|
|
2057
2060
|
/**
|
|
2058
2061
|
* Classify a file's base name (without extension) into a naming style.
|
|
2062
|
+
*
|
|
2063
|
+
* A single lowercase word (`user`, `index`, `loader`) is classified
|
|
2064
|
+
* `single-word`, NOT `camelCase`: it has no case boundary or separator, so it is
|
|
2065
|
+
* compatible with camelCase, kebab-case AND snake_case at once and expresses no
|
|
2066
|
+
* distinguishable convention. `scoreConvention` treats it as style-neutral
|
|
2067
|
+
* (excluded), so a repo of single-word files reports "unknown" rather than a
|
|
2068
|
+
* spurious "100% camelCase".
|
|
2069
|
+
*
|
|
2059
2070
|
* @param {string} basename a file basename, e.g. "user-service.ts"
|
|
2060
|
-
* @returns {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'|'other'}
|
|
2071
|
+
* @returns {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'|'single-word'|'other'}
|
|
2061
2072
|
*/
|
|
2062
2073
|
function classifyNaming(basename) {
|
|
2063
2074
|
let stem = String(basename || '');
|
|
@@ -2068,7 +2079,7 @@ __factories["./src/conventions/extract"] = function(module, exports) {
|
|
|
2068
2079
|
if (/[_]/.test(stem) && /^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(stem)) return 'snake_case';
|
|
2069
2080
|
if (/^[A-Z][A-Za-z0-9]*$/.test(stem) && /[a-z]/.test(stem)) return 'PascalCase';
|
|
2070
2081
|
if (/^[a-z][A-Za-z0-9]*$/.test(stem) && /[A-Z]/.test(stem)) return 'camelCase';
|
|
2071
|
-
if (/^[a-z][a-z0-9]*$/.test(stem)) return '
|
|
2082
|
+
if (/^[a-z][a-z0-9]*$/.test(stem)) return 'single-word'; // style-neutral (no case boundary)
|
|
2072
2083
|
return 'other';
|
|
2073
2084
|
}
|
|
2074
2085
|
|
|
@@ -2091,7 +2102,9 @@ __factories["./src/conventions/extract"] = function(module, exports) {
|
|
|
2091
2102
|
let total = 0;
|
|
2092
2103
|
for (let i = 0; i < all.length; i++) {
|
|
2093
2104
|
const l = all[i];
|
|
2094
|
-
|
|
2105
|
+
// 'other' = unclassifiable; 'single-word' = style-neutral. Both express no
|
|
2106
|
+
// distinguishable convention and are excluded from the score.
|
|
2107
|
+
if (l == null || l === 'other' || l === 'single-word') continue;
|
|
2095
2108
|
total++;
|
|
2096
2109
|
counts.set(l, (counts.get(l) || 0) + 1);
|
|
2097
2110
|
if (refs && refs[i] != null) {
|
|
@@ -2259,7 +2272,10 @@ __factories["./src/conventions/fix"] = function(module, exports) {
|
|
|
2259
2272
|
if (TEST_RE.test(f)) continue;
|
|
2260
2273
|
const base = path.basename(f);
|
|
2261
2274
|
const style = classifyNaming(base);
|
|
2262
|
-
|
|
2275
|
+
// Skip unclassifiable ('other') and style-neutral single-word names — a
|
|
2276
|
+
// single lowercase word already satisfies any convention, so renaming it
|
|
2277
|
+
// (e.g. user.js → user.js) is a no-op at best and noise at worst.
|
|
2278
|
+
if (style === 'other' || style === 'single-word' || style === dominant) continue;
|
|
2263
2279
|
const rel = path.relative(cwd, f).replace(/\\/g, '/');
|
|
2264
2280
|
renames.push({ from: rel, to: _renamePath(rel, dominant), fromStyle: style });
|
|
2265
2281
|
}
|
|
@@ -6071,6 +6087,7 @@ __factories["./src/extractors/java"] = function(module, exports) {
|
|
|
6071
6087
|
__factories["./src/extractors/javascript"] = function(module, exports) {
|
|
6072
6088
|
|
|
6073
6089
|
const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
|
|
6090
|
+
const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
|
|
6074
6091
|
|
|
6075
6092
|
/**
|
|
6076
6093
|
* Extract signatures from JavaScript source code.
|
|
@@ -6150,9 +6167,8 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
|
|
|
6150
6167
|
anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
|
|
6151
6168
|
}
|
|
6152
6169
|
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
.slice(0, 25);
|
|
6170
|
+
const withAnchors = sigs.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s));
|
|
6171
|
+
return capWithNotice(withAnchors, 25, 'signatures');
|
|
6156
6172
|
}
|
|
6157
6173
|
|
|
6158
6174
|
function extractBlock(src, startIndex) {
|
|
@@ -6183,7 +6199,7 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
|
|
|
6183
6199
|
const retStr = formatReturnHint(returnHints.get(m[1]));
|
|
6184
6200
|
members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
|
|
6185
6201
|
}
|
|
6186
|
-
return members
|
|
6202
|
+
return capMembersWithNotice(members, 8, 'methods');
|
|
6187
6203
|
}
|
|
6188
6204
|
|
|
6189
6205
|
function buildReturnHints(src) {
|
|
@@ -8063,6 +8079,7 @@ __factories["./src/extractors/toml"] = function(module, exports) {
|
|
|
8063
8079
|
__factories["./src/extractors/typescript"] = function(module, exports) {
|
|
8064
8080
|
|
|
8065
8081
|
const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
|
|
8082
|
+
const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
|
|
8066
8083
|
|
|
8067
8084
|
/**
|
|
8068
8085
|
* Extract signatures from TypeScript source code.
|
|
@@ -8214,9 +8231,8 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
|
|
|
8214
8231
|
}
|
|
8215
8232
|
}
|
|
8216
8233
|
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
.slice(0, 35);
|
|
8234
|
+
const withAnchors = sigs.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s));
|
|
8235
|
+
return capWithNotice(withAnchors, 35, 'signatures');
|
|
8220
8236
|
}
|
|
8221
8237
|
|
|
8222
8238
|
function extractBlock(src, startIndex) {
|
|
@@ -8246,7 +8262,7 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
|
|
|
8246
8262
|
const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
|
|
8247
8263
|
members.push({ text: `${m[1]}(${normalizeParams(m[2])})`, start, end: m.index + m[0].length });
|
|
8248
8264
|
}
|
|
8249
|
-
return members
|
|
8265
|
+
return capMembersWithNotice(members, 8, 'members');
|
|
8250
8266
|
}
|
|
8251
8267
|
|
|
8252
8268
|
const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);
|
|
@@ -8272,7 +8288,7 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
|
|
|
8272
8288
|
const retStr = retType ? ` → ${retType}` : '';
|
|
8273
8289
|
members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
|
|
8274
8290
|
}
|
|
8275
|
-
return members
|
|
8291
|
+
return capMembersWithNotice(members, 8, 'methods');
|
|
8276
8292
|
}
|
|
8277
8293
|
|
|
8278
8294
|
function normalizeParams(params) {
|
|
@@ -9998,6 +10014,96 @@ __factories["./src/format/llms-txt"] = function(module, exports) {
|
|
|
9998
10014
|
|
|
9999
10015
|
};
|
|
10000
10016
|
|
|
10017
|
+
// ── ./src/format/terse ──
|
|
10018
|
+
__factories["./src/format/terse"] = function(module, exports) {
|
|
10019
|
+
|
|
10020
|
+
/**
|
|
10021
|
+
* Terse signature encoder (D7).
|
|
10022
|
+
*
|
|
10023
|
+
* Deterministic compaction of signature lines for the generated context —
|
|
10024
|
+
* opt-in via `--terse` / `terse: true`. Every transform is a fixed string
|
|
10025
|
+
* rewrite (no heuristics, no LLM), so terse output stays byte-stable.
|
|
10026
|
+
*
|
|
10027
|
+
* The line anchor (` :start-end`) and everything after it (Python/R doc
|
|
10028
|
+
* hints) are preserved byte-exactly: `parseAnchor`, `get_lines`, and the
|
|
10029
|
+
* evidence pack keep working on terse output. Symbol extraction is safe too —
|
|
10030
|
+
* `extractName` in src/extractors/prdiff.js already recognizes `fn <name>`.
|
|
10031
|
+
*/
|
|
10032
|
+
|
|
10033
|
+
/** First ` :start[-end]` anchor token (two-space prefix, as emitted by line-anchor.js). */
|
|
10034
|
+
const ANCHOR_RE = /\s{2}:\d+(?:-\d+)?(?=\s|$)/;
|
|
10035
|
+
|
|
10036
|
+
/**
|
|
10037
|
+
* Split a signature into the compactable text and the byte-preserved suffix
|
|
10038
|
+
* (anchor + any trailing doc hint).
|
|
10039
|
+
* @param {string} sig
|
|
10040
|
+
* @returns {{ text: string, suffix: string }}
|
|
10041
|
+
*/
|
|
10042
|
+
function splitAnchor(sig) {
|
|
10043
|
+
const s = String(sig);
|
|
10044
|
+
const m = ANCHOR_RE.exec(s);
|
|
10045
|
+
if (!m) return { text: s, suffix: '' };
|
|
10046
|
+
return { text: s.slice(0, m.index), suffix: s.slice(m.index) };
|
|
10047
|
+
}
|
|
10048
|
+
|
|
10049
|
+
/**
|
|
10050
|
+
* Compact one signature line. Leading whitespace (member indentation) is kept.
|
|
10051
|
+
* @param {string} sig
|
|
10052
|
+
* @returns {string}
|
|
10053
|
+
*/
|
|
10054
|
+
function encodeTerseSig(sig) {
|
|
10055
|
+
const { text, suffix } = splitAnchor(sig);
|
|
10056
|
+
let t = text
|
|
10057
|
+
.replace(/\basync function\b/g, 'async fn')
|
|
10058
|
+
.replace(/\bfunction\b/g, 'fn')
|
|
10059
|
+
.replace(/\s+→\s+/g, '→')
|
|
10060
|
+
.replace(/,\s+/g, ',')
|
|
10061
|
+
.replace(/\s+=\s+/g, '=')
|
|
10062
|
+
.replace(/\{\s+/g, '{')
|
|
10063
|
+
.replace(/\s+\}/g, '}')
|
|
10064
|
+
.replace(/(\S)\s{2,}(?=\S)/g, '$1 ')
|
|
10065
|
+
.replace(/^module\.exports=/, 'exports=');
|
|
10066
|
+
return t + suffix;
|
|
10067
|
+
}
|
|
10068
|
+
|
|
10069
|
+
/**
|
|
10070
|
+
* Compact an array of signature lines.
|
|
10071
|
+
* @param {string[]} sigs
|
|
10072
|
+
* @returns {string[]}
|
|
10073
|
+
*/
|
|
10074
|
+
function encodeTerseSigs(sigs) {
|
|
10075
|
+
return (sigs || []).map(encodeTerseSig);
|
|
10076
|
+
}
|
|
10077
|
+
|
|
10078
|
+
/** Estimated tokens of joined signature lines (same chars/4 rule as elsewhere). */
|
|
10079
|
+
function _tokens(sigs) {
|
|
10080
|
+
return Math.ceil(sigs.join('\n').length / 4);
|
|
10081
|
+
}
|
|
10082
|
+
|
|
10083
|
+
/**
|
|
10084
|
+
* Measure the real reduction terse encoding buys over a set of signature
|
|
10085
|
+
* lists — the D7 "measure first" gate. Never quote a number this didn't produce.
|
|
10086
|
+
* @param {string[][]} sigsList one string[] per file
|
|
10087
|
+
* @returns {{ beforeTokens: number, afterTokens: number, reductionPct: number }}
|
|
10088
|
+
*/
|
|
10089
|
+
function measureTerse(sigsList) {
|
|
10090
|
+
let beforeTokens = 0;
|
|
10091
|
+
let afterTokens = 0;
|
|
10092
|
+
for (const sigs of sigsList || []) {
|
|
10093
|
+
if (!sigs || !sigs.length) continue;
|
|
10094
|
+
beforeTokens += _tokens(sigs);
|
|
10095
|
+
afterTokens += _tokens(encodeTerseSigs(sigs));
|
|
10096
|
+
}
|
|
10097
|
+
const reductionPct = beforeTokens > 0
|
|
10098
|
+
? Math.round(((beforeTokens - afterTokens) / beforeTokens) * 1000) / 10
|
|
10099
|
+
: 0;
|
|
10100
|
+
return { beforeTokens, afterTokens, reductionPct };
|
|
10101
|
+
}
|
|
10102
|
+
|
|
10103
|
+
module.exports = { encodeTerseSig, encodeTerseSigs, measureTerse, splitAnchor };
|
|
10104
|
+
|
|
10105
|
+
};
|
|
10106
|
+
|
|
10001
10107
|
// ── ./src/format/usage-guidance ──
|
|
10002
10108
|
__factories["./src/format/usage-guidance"] = function(module, exports) {
|
|
10003
10109
|
|
|
@@ -10232,20 +10338,19 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
10232
10338
|
const R_EXTS = new Set(['.r', '.R']);
|
|
10233
10339
|
|
|
10234
10340
|
/**
|
|
10235
|
-
*
|
|
10236
|
-
*
|
|
10237
|
-
* @param {string}
|
|
10341
|
+
* Probe an absolute base path for a JS/TS module file in fileSet, trying the
|
|
10342
|
+
* usual extension and index-file candidates.
|
|
10343
|
+
* @param {string} base - absolute path (no extension) to probe
|
|
10238
10344
|
* @param {Set<string>} fileSet
|
|
10239
10345
|
* @returns {string|null}
|
|
10240
10346
|
*/
|
|
10241
|
-
function
|
|
10242
|
-
const base = path.resolve(dir, importStr);
|
|
10347
|
+
function probeJs(base, fileSet) {
|
|
10243
10348
|
const candidates = [
|
|
10244
10349
|
base,
|
|
10245
10350
|
base + '.ts', base + '.tsx',
|
|
10246
10351
|
base + '.js', base + '.jsx', base + '.mjs', base + '.cjs',
|
|
10247
|
-
path.join(base, 'index.ts'),
|
|
10248
|
-
path.join(base, 'index.js'),
|
|
10352
|
+
path.join(base, 'index.ts'), path.join(base, 'index.tsx'),
|
|
10353
|
+
path.join(base, 'index.js'), path.join(base, 'index.jsx'),
|
|
10249
10354
|
];
|
|
10250
10355
|
for (const c of candidates) {
|
|
10251
10356
|
const normC = normalizePath(c);
|
|
@@ -10254,6 +10359,103 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
10254
10359
|
return null;
|
|
10255
10360
|
}
|
|
10256
10361
|
|
|
10362
|
+
/**
|
|
10363
|
+
* Resolve a JS/TS relative import string to an absolute path in fileSet.
|
|
10364
|
+
* @param {string} dir - directory of the importing file
|
|
10365
|
+
* @param {string} importStr - raw import string (e.g. './utils', '../store')
|
|
10366
|
+
* @param {Set<string>} fileSet
|
|
10367
|
+
* @returns {string|null}
|
|
10368
|
+
*/
|
|
10369
|
+
function resolveJsPath(dir, importStr, fileSet) {
|
|
10370
|
+
return probeJs(path.resolve(dir, importStr), fileSet);
|
|
10371
|
+
}
|
|
10372
|
+
|
|
10373
|
+
/**
|
|
10374
|
+
* Strip comments and trailing commas so a tsconfig/jsconfig (JSONC) parses.
|
|
10375
|
+
* Deliberately conservative — leaves string contents alone.
|
|
10376
|
+
*/
|
|
10377
|
+
function stripJsonc(src) {
|
|
10378
|
+
let out = '';
|
|
10379
|
+
let inStr = false, quote = '', inLine = false, inBlock = false;
|
|
10380
|
+
for (let i = 0; i < src.length; i++) {
|
|
10381
|
+
const c = src[i], n = src[i + 1];
|
|
10382
|
+
if (inLine) { if (c === '\n') { inLine = false; out += c; } continue; }
|
|
10383
|
+
if (inBlock) { if (c === '*' && n === '/') { inBlock = false; i++; } continue; }
|
|
10384
|
+
if (inStr) { out += c; if (c === '\\') { out += (n || ''); i++; } else if (c === quote) inStr = false; continue; }
|
|
10385
|
+
if (c === '"' || c === "'") { inStr = true; quote = c; out += c; continue; }
|
|
10386
|
+
if (c === '/' && n === '/') { inLine = true; i++; continue; }
|
|
10387
|
+
if (c === '/' && n === '*') { inBlock = true; i++; continue; }
|
|
10388
|
+
out += c;
|
|
10389
|
+
}
|
|
10390
|
+
// remove trailing commas before } or ]
|
|
10391
|
+
return out.replace(/,(\s*[}\]])/g, '$1');
|
|
10392
|
+
}
|
|
10393
|
+
|
|
10394
|
+
/**
|
|
10395
|
+
* Load the JS/TS path-alias map from tsconfig.json / jsconfig.json.
|
|
10396
|
+
* Resolves `compilerOptions.paths` and `baseUrl` into absolute target bases so
|
|
10397
|
+
* bare/aliased imports (e.g. `@/utils`, `components/Button`) can be resolved to
|
|
10398
|
+
* on-disk files. Returns null when no config or no aliasing is configured.
|
|
10399
|
+
*
|
|
10400
|
+
* @param {string} cwd
|
|
10401
|
+
* @returns {{ baseUrl: string|null, entries: Array<{prefix:string,wildcard:boolean,targets:string[]}> }|null}
|
|
10402
|
+
*/
|
|
10403
|
+
function loadAliasMap(cwd) {
|
|
10404
|
+
if (!cwd) return null;
|
|
10405
|
+
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
|
10406
|
+
let json;
|
|
10407
|
+
try { json = JSON.parse(stripJsonc(fs.readFileSync(path.join(cwd, name), 'utf8'))); }
|
|
10408
|
+
catch (_) { continue; }
|
|
10409
|
+
const co = (json && json.compilerOptions) || {};
|
|
10410
|
+
const baseUrl = co.baseUrl ? path.resolve(cwd, co.baseUrl) : null;
|
|
10411
|
+
const base = baseUrl || cwd;
|
|
10412
|
+
const entries = [];
|
|
10413
|
+
for (const [pattern, targets] of Object.entries(co.paths || {})) {
|
|
10414
|
+
const wildcard = pattern.includes('*');
|
|
10415
|
+
const prefix = pattern.replace(/\*.*$/, '');
|
|
10416
|
+
const tgs = (Array.isArray(targets) ? targets : [])
|
|
10417
|
+
.map((t) => path.resolve(base, String(t).replace(/\*.*$/, '')));
|
|
10418
|
+
if (tgs.length) entries.push({ prefix, wildcard, targets: tgs });
|
|
10419
|
+
}
|
|
10420
|
+
if (baseUrl || entries.length) return { baseUrl, entries };
|
|
10421
|
+
return null;
|
|
10422
|
+
}
|
|
10423
|
+
return null;
|
|
10424
|
+
}
|
|
10425
|
+
|
|
10426
|
+
/**
|
|
10427
|
+
* Resolve a non-relative JS/TS import specifier through the alias map.
|
|
10428
|
+
* @param {string} spec - e.g. '@/utils', '@app/Button', 'components/Nav'
|
|
10429
|
+
* @param {object|null} aliasMap - from loadAliasMap
|
|
10430
|
+
* @param {Set<string>} fileSet
|
|
10431
|
+
* @returns {string|null}
|
|
10432
|
+
*/
|
|
10433
|
+
function resolveAlias(spec, aliasMap, fileSet) {
|
|
10434
|
+
if (!aliasMap) return null;
|
|
10435
|
+
for (const e of aliasMap.entries) {
|
|
10436
|
+
if (e.wildcard) {
|
|
10437
|
+
if (spec.startsWith(e.prefix)) {
|
|
10438
|
+
const rest = spec.slice(e.prefix.length);
|
|
10439
|
+
for (const t of e.targets) {
|
|
10440
|
+
const r = probeJs(rest ? path.join(t, rest) : t, fileSet);
|
|
10441
|
+
if (r) return r;
|
|
10442
|
+
}
|
|
10443
|
+
}
|
|
10444
|
+
} else if (spec === e.prefix) {
|
|
10445
|
+
for (const t of e.targets) {
|
|
10446
|
+
const r = probeJs(t, fileSet);
|
|
10447
|
+
if (r) return r;
|
|
10448
|
+
}
|
|
10449
|
+
}
|
|
10450
|
+
}
|
|
10451
|
+
// Bare import resolved from baseUrl (tsconfig baseUrl without an explicit alias).
|
|
10452
|
+
if (aliasMap.baseUrl) {
|
|
10453
|
+
const r = probeJs(path.join(aliasMap.baseUrl, spec), fileSet);
|
|
10454
|
+
if (r) return r;
|
|
10455
|
+
}
|
|
10456
|
+
return null;
|
|
10457
|
+
}
|
|
10458
|
+
|
|
10257
10459
|
/**
|
|
10258
10460
|
* Resolve an R `source(...)` argument to an absolute path in fileSet.
|
|
10259
10461
|
* Tries the dir-relative path first, then a cwd-relative path so that
|
|
@@ -10299,21 +10501,29 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
10299
10501
|
|
|
10300
10502
|
// ── JS / TS ───────────────────────────────────────────────────────────────
|
|
10301
10503
|
if (JS_EXTS.has(ext)) {
|
|
10504
|
+
const aliasMap = ctx && ctx.aliasMap;
|
|
10505
|
+
// Resolve any specifier: relative → dir-relative; otherwise via tsconfig/
|
|
10506
|
+
// jsconfig path aliases + baseUrl. Bare npm packages (react, lodash) fall
|
|
10507
|
+
// through to null because they are not in fileSet, so no false edges.
|
|
10508
|
+
const resolveSpec = (spec) => spec.startsWith('.')
|
|
10509
|
+
? resolveJsPath(dir, spec, fileSet)
|
|
10510
|
+
: resolveAlias(spec, aliasMap, fileSet);
|
|
10511
|
+
|
|
10302
10512
|
const stripped = content
|
|
10303
10513
|
.replace(/\/\/.*$/gm, '')
|
|
10304
10514
|
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
10305
10515
|
|
|
10306
|
-
// ES imports: import ... from './foo' or import './side-effect'
|
|
10307
|
-
const reEs = /(?:^|[\r\n])\s*import\s+(?:[^'";\r\n]*?\s+from\s+)?['"](\.[^'"]+)['"]/g;
|
|
10308
10516
|
let m;
|
|
10517
|
+
// ES imports: import ... from 'x' | import 'x' | export ... from 'x'
|
|
10518
|
+
const reEs = /(?:^|[\r\n])\s*(?:import|export)\s+(?:[^'";\r\n]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
10309
10519
|
while ((m = reEs.exec(stripped)) !== null) {
|
|
10310
|
-
const r =
|
|
10520
|
+
const r = resolveSpec(m[1]);
|
|
10311
10521
|
if (r) found.push(r);
|
|
10312
10522
|
}
|
|
10313
|
-
// CommonJS
|
|
10314
|
-
const
|
|
10315
|
-
while ((m =
|
|
10316
|
-
const r =
|
|
10523
|
+
// CommonJS require('x') and dynamic import('x').
|
|
10524
|
+
const reCall = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
10525
|
+
while ((m = reCall.exec(stripped)) !== null) {
|
|
10526
|
+
const r = resolveSpec(m[1]);
|
|
10317
10527
|
if (r) found.push(r);
|
|
10318
10528
|
}
|
|
10319
10529
|
}
|
|
@@ -10485,6 +10695,10 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
10485
10695
|
const fileSet = new Set(files.map((f) => path.resolve(f)));
|
|
10486
10696
|
// Create a normalized version for cross-platform case-insensitive lookups
|
|
10487
10697
|
const fileSetNormalized = new Set([...fileSet].map(normalizePath));
|
|
10698
|
+
// Resolve the JS/TS path-alias map once (tsconfig/jsconfig paths + baseUrl),
|
|
10699
|
+
// unless a caller supplied one explicitly via ctx.
|
|
10700
|
+
const aliasMap = (ctx && 'aliasMap' in ctx) ? ctx.aliasMap : loadAliasMap(cwd);
|
|
10701
|
+
const effectiveCtx = Object.assign({}, ctx, { aliasMap });
|
|
10488
10702
|
const forward = new Map();
|
|
10489
10703
|
const reverse = new Map();
|
|
10490
10704
|
|
|
@@ -10505,7 +10719,7 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
10505
10719
|
}
|
|
10506
10720
|
|
|
10507
10721
|
const normFilePath = normalizePath(filePath);
|
|
10508
|
-
const deps = extractFileDeps(filePath, content, fileSetNormalized, cwd,
|
|
10722
|
+
const deps = extractFileDeps(filePath, content, fileSetNormalized, cwd, effectiveCtx);
|
|
10509
10723
|
if (deps.length > 0) {
|
|
10510
10724
|
forward.set(normFilePath, deps);
|
|
10511
10725
|
for (const dep of deps) {
|
|
@@ -10584,7 +10798,7 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
10584
10798
|
return build(files, cwd, ctx);
|
|
10585
10799
|
}
|
|
10586
10800
|
|
|
10587
|
-
module.exports = { build, buildFromCwd, extractFileDeps, normalizePath };
|
|
10801
|
+
module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias };
|
|
10588
10802
|
|
|
10589
10803
|
};
|
|
10590
10804
|
|
|
@@ -11182,11 +11396,11 @@ __factories["./src/graph/impact"] = function(module, exports) {
|
|
|
11182
11396
|
lines.push('');
|
|
11183
11397
|
|
|
11184
11398
|
if (result.direct.length === 0 && result.transitive.length === 0) {
|
|
11185
|
-
lines.push('_No
|
|
11399
|
+
lines.push('_No importers found via relative + aliased imports (lower bound — dynamic/computed imports are not tracked)._');
|
|
11186
11400
|
return lines.join('\n');
|
|
11187
11401
|
}
|
|
11188
11402
|
|
|
11189
|
-
lines.push(`**Total impacted files:** ${result.totalImpact}`);
|
|
11403
|
+
lines.push(`**Total impacted files:** ${result.totalImpact} _(lower bound — resolves relative + tsconfig/jsconfig-aliased imports)_`);
|
|
11190
11404
|
lines.push('');
|
|
11191
11405
|
|
|
11192
11406
|
if (result.direct.length > 0) {
|
|
@@ -11241,155 +11455,230 @@ __factories["./src/graph/impact"] = function(module, exports) {
|
|
|
11241
11455
|
__factories["./src/health/scorer"] = function(module, exports) {
|
|
11242
11456
|
|
|
11243
11457
|
/**
|
|
11244
|
-
* SigMap health scorer.
|
|
11458
|
+
* SigMap health scorer (v8.11 — auditable composite).
|
|
11245
11459
|
*
|
|
11246
|
-
* Computes a
|
|
11247
|
-
*
|
|
11248
|
-
*
|
|
11249
|
-
*
|
|
11460
|
+
* Computes a 0-100 health score for a project. Every deduction is recorded in a
|
|
11461
|
+
* `components[]` breakdown so the number is auditable (which signal cost what),
|
|
11462
|
+
* and purely-informational metrics live under `diagnostics` rather than being
|
|
11463
|
+
* dressed up as if they affect the grade.
|
|
11250
11464
|
*
|
|
11251
|
-
*
|
|
11252
|
-
*
|
|
11465
|
+
* Scored signals (each appears in `components` only when it fires):
|
|
11466
|
+
* 1. context never generated — no adapter output exists though source does (45 pts)
|
|
11467
|
+
* 2. staleness — freshest adapter output older than 7 days (≤30 pts)
|
|
11468
|
+
* 3. low token reduction — avg reduction under threshold (full strategy) (20 pts)
|
|
11469
|
+
* 4. cold-context staleness — hot-cold context-cold.md older than 1 day (≤10 pts)
|
|
11470
|
+
* 5. over-budget rate — >20% of runs exceeded the token budget (20 pts)
|
|
11471
|
+
* 6. sustained over-budget — ≥3 consecutive over-budget runs (5 pts)
|
|
11253
11472
|
*
|
|
11254
|
-
*
|
|
11473
|
+
* Diagnostics (informational, NOT scored): p50/p95 token count, and
|
|
11474
|
+
* languageCoverage (share of SigMap's supported languages present in the repo —
|
|
11475
|
+
* this is language diversity, not extractor quality, and was previously
|
|
11476
|
+
* mislabeled "extractorCoverage").
|
|
11255
11477
|
*
|
|
11256
|
-
*
|
|
11478
|
+
* Freshness looks at the freshest of ANY adapter output (not just Copilot), so a
|
|
11479
|
+
* Claude/Codex/Cursor user is not falsely flagged as "never generated".
|
|
11257
11480
|
*
|
|
11258
|
-
*
|
|
11259
|
-
*
|
|
11260
|
-
*
|
|
11261
|
-
*
|
|
11262
|
-
*
|
|
11263
|
-
*
|
|
11264
|
-
* daysSinceRegen: number|null,
|
|
11265
|
-
* strategyFreshnessDays: number|null,
|
|
11266
|
-
* totalRuns: number,
|
|
11267
|
-
* overBudgetRuns: number,
|
|
11268
|
-
* }}
|
|
11481
|
+
* Grade scale: A ≥ 90 | B ≥ 75 | C ≥ 60 | D < 60. Never throws.
|
|
11482
|
+
*
|
|
11483
|
+
* @param {string} cwd
|
|
11484
|
+
* @returns {object} { score, grade, components, strategy, tokenReductionPct,
|
|
11485
|
+
* daysSinceRegen, strategyFreshnessDays, totalRuns, overBudgetRuns,
|
|
11486
|
+
* overBudgetStreak, languageCoverage, extractorCoverage, diagnostics }
|
|
11269
11487
|
*/
|
|
11270
|
-
function score(cwd) {
|
|
11271
|
-
const fs = require('fs');
|
|
11272
|
-
const path = require('path');
|
|
11273
11488
|
|
|
11274
|
-
|
|
11275
|
-
|
|
11276
|
-
|
|
11277
|
-
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11489
|
+
const fs = require('fs');
|
|
11490
|
+
const path = require('path');
|
|
11491
|
+
|
|
11492
|
+
// Every path a SigMap adapter may write context to (freshness looks at the
|
|
11493
|
+
// freshest existing one). Mirrors the ranker's adapter-output probe order.
|
|
11494
|
+
const CONTEXT_FILES = [
|
|
11495
|
+
['.github', 'copilot-instructions.md'],
|
|
11496
|
+
['CLAUDE.md'],
|
|
11497
|
+
['AGENTS.md'],
|
|
11498
|
+
['.cursorrules'],
|
|
11499
|
+
['.windsurfrules'],
|
|
11500
|
+
['.github', 'openai-context.md'],
|
|
11501
|
+
['.github', 'gemini-context.md'],
|
|
11502
|
+
['llm-full.txt'],
|
|
11503
|
+
['llm.txt'],
|
|
11504
|
+
];
|
|
11505
|
+
|
|
11506
|
+
function gradeFor(points) {
|
|
11507
|
+
if (points >= 90) return 'A';
|
|
11508
|
+
if (points >= 75) return 'B';
|
|
11509
|
+
if (points >= 60) return 'C';
|
|
11510
|
+
return 'D';
|
|
11511
|
+
}
|
|
11512
|
+
|
|
11513
|
+
/**
|
|
11514
|
+
* Pure scoring core. Given gathered signals, return the score, grade, and the
|
|
11515
|
+
* labeled list of deductions. No IO — unit-testable in isolation.
|
|
11516
|
+
*
|
|
11517
|
+
* @param {object} s gathered signals
|
|
11518
|
+
* @returns {{ score:number, grade:'A'|'B'|'C'|'D', components:Array }}
|
|
11519
|
+
*/
|
|
11520
|
+
function composeHealth(s) {
|
|
11521
|
+
const components = [];
|
|
11522
|
+
const add = (id, label, penalty, detail) => {
|
|
11523
|
+
const p = Math.round(penalty);
|
|
11524
|
+
if (p > 0) components.push({ id, label, penalty: p, detail });
|
|
11525
|
+
};
|
|
11526
|
+
|
|
11527
|
+
// 1. Context never generated — a project with source files but no context of
|
|
11528
|
+
// any kind is not "healthy"; it hasn't been set up. Gated on hasSource so
|
|
11529
|
+
// an empty/new directory is not penalised for having nothing to index.
|
|
11530
|
+
if (s.daysSinceRegen === null && s.hasSource) {
|
|
11531
|
+
add('not-generated', 'context never generated', 45,
|
|
11532
|
+
'no adapter output found — run `sigmap` to generate context');
|
|
11533
|
+
}
|
|
11534
|
+
|
|
11535
|
+
// 2. Staleness — freshest adapter output older than the 7-day window.
|
|
11536
|
+
if (s.daysSinceRegen !== null && s.daysSinceRegen > 7) {
|
|
11537
|
+
add('staleness', 'context stale', Math.min(30, Math.floor((s.daysSinceRegen - 7) * 4)),
|
|
11538
|
+
`${s.daysSinceRegen}d since last regen (>7d)`);
|
|
11539
|
+
}
|
|
11540
|
+
|
|
11541
|
+
// 3. Low token reduction — only meaningful for the 'full' strategy; hot-cold
|
|
11542
|
+
// and per-module intentionally produce small/partial outputs.
|
|
11543
|
+
const reductionThreshold = s.strategy === 'full' ? 60 : 0;
|
|
11544
|
+
if (s.tokenReductionPct !== null && s.tokenReductionPct < reductionThreshold) {
|
|
11545
|
+
add('low-reduction', 'low token reduction', 20,
|
|
11546
|
+
`${s.tokenReductionPct}% avg reduction (<${reductionThreshold}%)`);
|
|
11547
|
+
}
|
|
11548
|
+
|
|
11549
|
+
// 4. Cold-context staleness (hot-cold only).
|
|
11550
|
+
if (s.strategy === 'hot-cold' && s.strategyFreshnessDays !== null && s.strategyFreshnessDays > 1) {
|
|
11551
|
+
add('cold-freshness', 'cold context stale', Math.min(10, Math.floor(s.strategyFreshnessDays - 1) * 3),
|
|
11552
|
+
`context-cold.md ${s.strategyFreshnessDays}d old`);
|
|
11553
|
+
}
|
|
11554
|
+
|
|
11555
|
+
// 5. Over-budget rate.
|
|
11556
|
+
if (s.overBudgetRuns > 0 && s.totalRuns > 0) {
|
|
11557
|
+
const rate = (s.overBudgetRuns / s.totalRuns) * 100;
|
|
11558
|
+
if (rate > 20) add('over-budget', 'runs over budget', 20,
|
|
11559
|
+
`${Math.round(rate)}% of runs exceeded budget (>20%)`);
|
|
11560
|
+
}
|
|
11283
11561
|
|
|
11284
|
-
//
|
|
11562
|
+
// 6. Sustained over-budget streak — previously computed but never scored.
|
|
11563
|
+
if (s.overBudgetStreak >= 3) {
|
|
11564
|
+
add('over-budget-streak', 'sustained over-budget', 5,
|
|
11565
|
+
`${s.overBudgetStreak} consecutive over-budget runs`);
|
|
11566
|
+
}
|
|
11567
|
+
|
|
11568
|
+
const penalty = components.reduce((sum, c) => sum + c.penalty, 0);
|
|
11569
|
+
const score = Math.max(0, Math.min(100, 100 - penalty));
|
|
11570
|
+
return { score, grade: gradeFor(score), components };
|
|
11571
|
+
}
|
|
11572
|
+
|
|
11573
|
+
/**
|
|
11574
|
+
* Gather health signals from disk and score them. Never throws.
|
|
11575
|
+
* @param {string} cwd
|
|
11576
|
+
*/
|
|
11577
|
+
function score(cwd) {
|
|
11285
11578
|
let strategy = 'full';
|
|
11286
11579
|
try {
|
|
11287
11580
|
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
11288
11581
|
if (fs.existsSync(cfgPath)) {
|
|
11289
|
-
|
|
11290
|
-
strategy = cfg.strategy || 'full';
|
|
11582
|
+
strategy = JSON.parse(fs.readFileSync(cfgPath, 'utf8')).strategy || 'full';
|
|
11291
11583
|
}
|
|
11292
11584
|
} catch (_) {}
|
|
11293
11585
|
|
|
11294
|
-
// ──
|
|
11586
|
+
// ── Usage-log signals (only present when tracking has recorded runs) ────────
|
|
11587
|
+
let tokenReductionPct = null;
|
|
11588
|
+
let overBudgetRuns = 0;
|
|
11589
|
+
let totalRuns = 0;
|
|
11590
|
+
let p50TokenCount = 0;
|
|
11591
|
+
let p95TokenCount = 0;
|
|
11592
|
+
let overBudgetStreak = 0;
|
|
11295
11593
|
try {
|
|
11296
11594
|
const { readLog, summarize } = __require('./src/tracking/logger');
|
|
11297
|
-
const { percentile, overBudgetStreak:
|
|
11595
|
+
const { percentile, overBudgetStreak: calcStreak } = __require('./src/format/dashboard');
|
|
11298
11596
|
const entries = readLog(cwd);
|
|
11299
|
-
const
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
overBudgetRuns = s.overBudgetRuns;
|
|
11304
|
-
totalRuns = s.totalRuns;
|
|
11597
|
+
const sum = summarize(entries);
|
|
11598
|
+
if (sum.totalRuns > 0) tokenReductionPct = sum.avgReductionPct;
|
|
11599
|
+
overBudgetRuns = sum.overBudgetRuns;
|
|
11600
|
+
totalRuns = sum.totalRuns;
|
|
11305
11601
|
const finals = entries.map((e) => Number(e.finalTokens)).filter(Number.isFinite);
|
|
11306
11602
|
p50TokenCount = Math.round(percentile(finals, 50));
|
|
11307
11603
|
p95TokenCount = Math.round(percentile(finals, 95));
|
|
11308
|
-
overBudgetStreak =
|
|
11309
|
-
} catch (_) {
|
|
11310
|
-
// No usage log yet — proceed with nulls
|
|
11311
|
-
}
|
|
11604
|
+
overBudgetStreak = calcStreak(entries);
|
|
11605
|
+
} catch (_) {}
|
|
11312
11606
|
|
|
11607
|
+
// ── Language coverage (DIAGNOSTIC — share of supported languages present,
|
|
11608
|
+
// i.e. diversity, not extractor quality). Also yields hasSource. ──────────
|
|
11609
|
+
let languageCoverage = null;
|
|
11610
|
+
let hasSource = false;
|
|
11313
11611
|
try {
|
|
11314
11612
|
const { computeExtractorCoverage } = __require('./src/format/dashboard');
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
}
|
|
11613
|
+
const cov = computeExtractorCoverage(cwd);
|
|
11614
|
+
languageCoverage = { covered: cov.covered, supported: cov.supported, pct: cov.pct };
|
|
11615
|
+
hasSource = Object.values(cov.perLanguage || {}).some((n) => n > 0);
|
|
11616
|
+
} catch (_) {}
|
|
11319
11617
|
|
|
11320
|
-
// ──
|
|
11618
|
+
// ── Freshness across ALL adapter outputs (freshest wins) ───────────────────
|
|
11619
|
+
let daysSinceRegen = null;
|
|
11321
11620
|
try {
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
const
|
|
11325
|
-
|
|
11621
|
+
let newest = null;
|
|
11622
|
+
for (const parts of CONTEXT_FILES) {
|
|
11623
|
+
const p = path.join(cwd, ...parts);
|
|
11624
|
+
try {
|
|
11625
|
+
if (fs.existsSync(p)) {
|
|
11626
|
+
const m = fs.statSync(p).mtimeMs;
|
|
11627
|
+
if (newest === null || m > newest) newest = m;
|
|
11628
|
+
}
|
|
11629
|
+
} catch (_) {}
|
|
11630
|
+
}
|
|
11631
|
+
if (newest !== null) {
|
|
11632
|
+
daysSinceRegen = parseFloat(((Date.now() - newest) / (1000 * 60 * 60 * 24)).toFixed(1));
|
|
11326
11633
|
}
|
|
11327
11634
|
} catch (_) {}
|
|
11328
11635
|
|
|
11329
|
-
// ──
|
|
11636
|
+
// ── Cold-context freshness (hot-cold strategy only) ────────────────────────
|
|
11637
|
+
let strategyFreshnessDays = null;
|
|
11330
11638
|
if (strategy === 'hot-cold') {
|
|
11331
11639
|
try {
|
|
11332
11640
|
const coldFile = path.join(cwd, '.github', 'context-cold.md');
|
|
11333
11641
|
if (fs.existsSync(coldFile)) {
|
|
11334
|
-
const
|
|
11335
|
-
strategyFreshnessDays = parseFloat(((Date.now() -
|
|
11642
|
+
const m = fs.statSync(coldFile).mtimeMs;
|
|
11643
|
+
strategyFreshnessDays = parseFloat(((Date.now() - m) / (1000 * 60 * 60 * 24)).toFixed(1));
|
|
11336
11644
|
}
|
|
11337
11645
|
} catch (_) {}
|
|
11338
11646
|
}
|
|
11339
11647
|
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
// - per-module: per-file budgets; global reduction < 60% expected, no penalty
|
|
11351
|
-
// - full: standard 60% threshold
|
|
11352
|
-
const reductionThreshold = (strategy === 'full') ? 60 : 0; // disable for hot-cold/per-module
|
|
11353
|
-
if (tokenReductionPct !== null && tokenReductionPct < reductionThreshold) {
|
|
11354
|
-
points -= 20;
|
|
11355
|
-
}
|
|
11356
|
-
|
|
11357
|
-
// hot-cold strategy freshness penalty: context-cold.md older than 1 day (-10 pts)
|
|
11358
|
-
if (strategy === 'hot-cold' && strategyFreshnessDays !== null && strategyFreshnessDays > 1) {
|
|
11359
|
-
points -= Math.min(10, Math.floor(strategyFreshnessDays - 1) * 3);
|
|
11360
|
-
}
|
|
11361
|
-
|
|
11362
|
-
// Over-budget penalty: more than 20% of runs exceeded the token budget (-20)
|
|
11363
|
-
if (overBudgetRuns > 0 && totalRuns > 0) {
|
|
11364
|
-
const overBudgetRate = (overBudgetRuns / totalRuns) * 100;
|
|
11365
|
-
if (overBudgetRate > 20) points -= 20;
|
|
11366
|
-
}
|
|
11367
|
-
|
|
11368
|
-
points = Math.max(0, Math.min(100, Math.round(points)));
|
|
11369
|
-
|
|
11370
|
-
let grade;
|
|
11371
|
-
if (points >= 90) grade = 'A';
|
|
11372
|
-
else if (points >= 75) grade = 'B';
|
|
11373
|
-
else if (points >= 60) grade = 'C';
|
|
11374
|
-
else grade = 'D';
|
|
11648
|
+
const { score: points, grade, components } = composeHealth({
|
|
11649
|
+
strategy,
|
|
11650
|
+
daysSinceRegen,
|
|
11651
|
+
strategyFreshnessDays,
|
|
11652
|
+
tokenReductionPct,
|
|
11653
|
+
overBudgetRuns,
|
|
11654
|
+
totalRuns,
|
|
11655
|
+
overBudgetStreak,
|
|
11656
|
+
hasSource,
|
|
11657
|
+
});
|
|
11375
11658
|
|
|
11376
11659
|
return {
|
|
11377
11660
|
score: points,
|
|
11378
11661
|
grade,
|
|
11662
|
+
components,
|
|
11379
11663
|
strategy,
|
|
11380
11664
|
tokenReductionPct,
|
|
11381
11665
|
daysSinceRegen,
|
|
11382
11666
|
strategyFreshnessDays,
|
|
11383
11667
|
totalRuns,
|
|
11384
11668
|
overBudgetRuns,
|
|
11669
|
+
overBudgetStreak,
|
|
11670
|
+
languageCoverage,
|
|
11671
|
+
// Back-compat top-level fields (also surfaced, honestly grouped, under
|
|
11672
|
+
// `diagnostics`). `extractorCoverage` keeps its old name but its value is
|
|
11673
|
+
// language-diversity pct (never extractor quality) — prefer `languageCoverage`.
|
|
11385
11674
|
p50TokenCount,
|
|
11386
11675
|
p95TokenCount,
|
|
11387
|
-
|
|
11388
|
-
|
|
11676
|
+
extractorCoverage: languageCoverage ? languageCoverage.pct : 0,
|
|
11677
|
+
diagnostics: { p50TokenCount, p95TokenCount, languageCoverage },
|
|
11389
11678
|
};
|
|
11390
11679
|
}
|
|
11391
11680
|
|
|
11392
|
-
module.exports = { score };
|
|
11681
|
+
module.exports = { score, composeHealth };
|
|
11393
11682
|
|
|
11394
11683
|
};
|
|
11395
11684
|
|
|
@@ -11462,6 +11751,7 @@ __factories["./src/judge/judge-engine"] = function(module, exports) {
|
|
|
11462
11751
|
const fs = require('fs');
|
|
11463
11752
|
const path = require('path');
|
|
11464
11753
|
const { boostFiles, normalizeFile, penalizeFiles } = __require('./src/learning/weights');
|
|
11754
|
+
const parsers = __require('./src/verify/parsers');
|
|
11465
11755
|
|
|
11466
11756
|
const STOP = new Set([
|
|
11467
11757
|
'the','a','an','in','on','at','to','of','for','and','or','but',
|
|
@@ -11484,6 +11774,57 @@ __factories["./src/judge/judge-engine"] = function(module, exports) {
|
|
|
11484
11774
|
return parseFloat((matched.length / respTokens.length).toFixed(3));
|
|
11485
11775
|
}
|
|
11486
11776
|
|
|
11777
|
+
/**
|
|
11778
|
+
* Claim-level grounding (v8.10) — the structural half of the judge.
|
|
11779
|
+
*
|
|
11780
|
+
* `groundedness` above measures lexical *word* overlap: "does the answer reuse
|
|
11781
|
+
* context vocabulary?" That is a weak proxy — an answer can echo context words
|
|
11782
|
+
* while asserting a symbol, file, or import the context never mentions (a
|
|
11783
|
+
* hallucination), and still score high. This function extracts the answer's
|
|
11784
|
+
* *concrete, checkable claims* — the same high-precision claims the hallucination
|
|
11785
|
+
* guard checks (backtick-wrapped `foo()` calls, `path/to/file.ext` references,
|
|
11786
|
+
* and `import … from 'mod'` statements) — and verifies each one appears in the
|
|
11787
|
+
* provided context. A claim the context never grounds is a hallucination signal
|
|
11788
|
+
* that pure word-overlap cannot see.
|
|
11789
|
+
*
|
|
11790
|
+
* Deterministic, offline, zero-dependency. Reuses `src/verify/parsers`.
|
|
11791
|
+
*
|
|
11792
|
+
* @param {string} response
|
|
11793
|
+
* @param {string} context
|
|
11794
|
+
* @returns {{ total: number, grounded: number, ungrounded: Array<{kind:string, value:string}> }}
|
|
11795
|
+
*/
|
|
11796
|
+
function claimGrounding(response, context) {
|
|
11797
|
+
if (!response || !context) return { total: 0, grounded: 0, ungrounded: [] };
|
|
11798
|
+
const ctxLower = context.toLowerCase();
|
|
11799
|
+
|
|
11800
|
+
const raw = [];
|
|
11801
|
+
for (const s of parsers.extractSymbols(response)) raw.push({ kind: 'symbol', value: s.name });
|
|
11802
|
+
for (const f of parsers.extractFilePaths(response)) raw.push({ kind: 'file', value: f.path });
|
|
11803
|
+
for (const i of parsers.extractImports(response)) raw.push({ kind: 'import', value: i.module });
|
|
11804
|
+
|
|
11805
|
+
const seen = new Set();
|
|
11806
|
+
const claims = raw.filter((c) => {
|
|
11807
|
+
const key = `${c.kind}::${c.value}`;
|
|
11808
|
+
if (seen.has(key)) return false;
|
|
11809
|
+
seen.add(key);
|
|
11810
|
+
return true;
|
|
11811
|
+
});
|
|
11812
|
+
|
|
11813
|
+
const ungrounded = [];
|
|
11814
|
+
let grounded = 0;
|
|
11815
|
+
for (const c of claims) {
|
|
11816
|
+
// A file claim is grounded if its basename appears in context (the answer
|
|
11817
|
+
// may cite a different directory than the map records). Symbols and modules
|
|
11818
|
+
// are matched on the token itself.
|
|
11819
|
+
const needle = c.value.toLowerCase();
|
|
11820
|
+
const base = c.kind === 'file' ? (c.value.split('/').pop() || c.value).toLowerCase() : needle;
|
|
11821
|
+
if (ctxLower.includes(base) || ctxLower.includes(needle)) grounded++;
|
|
11822
|
+
else ungrounded.push({ kind: c.kind, value: c.value });
|
|
11823
|
+
}
|
|
11824
|
+
|
|
11825
|
+
return { total: claims.length, grounded, ungrounded };
|
|
11826
|
+
}
|
|
11827
|
+
|
|
11487
11828
|
const GENERIC_MARKERS = [
|
|
11488
11829
|
'however, based on my knowledge',
|
|
11489
11830
|
'generally speaking',
|
|
@@ -11535,8 +11876,16 @@ __factories["./src/judge/judge-engine"] = function(module, exports) {
|
|
|
11535
11876
|
}
|
|
11536
11877
|
}
|
|
11537
11878
|
|
|
11879
|
+
// Structural claim grounding: any concrete symbol/file/import the answer
|
|
11880
|
+
// states that the context never mentions is a hallucination the lexical
|
|
11881
|
+
// score above cannot detect. Each ungrounded claim fails the verdict.
|
|
11882
|
+
const claims = claimGrounding(response, context);
|
|
11883
|
+
for (const c of claims.ungrounded) {
|
|
11884
|
+
reasons.push(`${c.kind} claim not grounded in context: ${c.value}${c.kind === 'symbol' ? '()' : ''}`);
|
|
11885
|
+
}
|
|
11886
|
+
|
|
11538
11887
|
const verdict = score >= threshold && reasons.length === 0 ? 'pass' : 'fail';
|
|
11539
|
-
const result = { score, verdict, reasons };
|
|
11888
|
+
const result = { score, verdict, reasons, claims };
|
|
11540
11889
|
|
|
11541
11890
|
if (opts.learn) {
|
|
11542
11891
|
const learning = {
|
|
@@ -11578,7 +11927,7 @@ __factories["./src/judge/judge-engine"] = function(module, exports) {
|
|
|
11578
11927
|
return result;
|
|
11579
11928
|
}
|
|
11580
11929
|
|
|
11581
|
-
module.exports = { groundedness, judge };
|
|
11930
|
+
module.exports = { groundedness, claimGrounding, judge };
|
|
11582
11931
|
|
|
11583
11932
|
};
|
|
11584
11933
|
|
|
@@ -13694,7 +14043,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
13694
14043
|
|
|
13695
14044
|
const SERVER_INFO = {
|
|
13696
14045
|
name: 'sigmap',
|
|
13697
|
-
version: '8.
|
|
14046
|
+
version: '8.11.0',
|
|
13698
14047
|
description: 'SigMap MCP server — code signatures on demand',
|
|
13699
14048
|
};
|
|
13700
14049
|
|
|
@@ -14292,7 +14641,7 @@ __factories["./src/plan/planner"] = function(module, exports) {
|
|
|
14292
14641
|
|
|
14293
14642
|
module.exports = { createPlan };
|
|
14294
14643
|
|
|
14295
|
-
function createPlan(goal, cwd, config) {
|
|
14644
|
+
function createPlan(goal, cwd, config = {}) {
|
|
14296
14645
|
// Step 1: Detect intent and rank files for the goal
|
|
14297
14646
|
const intent = detectIntent(goal);
|
|
14298
14647
|
const sigIndex = buildSigIndex(cwd);
|
|
@@ -14306,30 +14655,59 @@ __factories["./src/plan/planner"] = function(module, exports) {
|
|
|
14306
14655
|
const highConf = ranked.filter(r => r.confidence === 'high').slice(0, 5);
|
|
14307
14656
|
const medConf = ranked.filter(r => r.confidence === 'medium').slice(0, 5);
|
|
14308
14657
|
|
|
14309
|
-
// Step 3:
|
|
14658
|
+
// Step 3: Impact radius — union the reverse-dependency blast radius of EVERY
|
|
14659
|
+
// high-confidence file (not just the top one), bounded to 3 hops. Note the
|
|
14660
|
+
// dependency graph resolves relative imports only, so this is a *lower bound*
|
|
14661
|
+
// on real coupling (aliased/bare/dynamic imports are invisible). Previously
|
|
14662
|
+
// this passed `{ maxDepth: 3 }`, which getImpact ignores — it reads `depth`,
|
|
14663
|
+
// so the traversal silently ran unbounded (depth 0). Fixed to `depth: 3`.
|
|
14310
14664
|
let impact = null;
|
|
14311
14665
|
if (highConf.length > 0) {
|
|
14312
|
-
const entryFile = highConf[0].file;
|
|
14313
14666
|
try {
|
|
14314
14667
|
const graph = buildFromCwd(cwd);
|
|
14315
|
-
|
|
14668
|
+
// getImpact normalizes graph paths to lowercase, so on a case-varying
|
|
14669
|
+
// filesystem (e.g. macOS `/Users`) its returned paths climb out of cwd.
|
|
14670
|
+
// Re-anchor every impacted path to a clean, case-insensitive repo-relative
|
|
14671
|
+
// form so dedup against the entry set works and output is readable.
|
|
14672
|
+
const clean = (f) => {
|
|
14673
|
+
const abs = path.resolve(cwd, f);
|
|
14674
|
+
return abs.toLowerCase().startsWith(cwd.toLowerCase())
|
|
14675
|
+
? abs.slice(cwd.length).replace(/^[/\\]/, '')
|
|
14676
|
+
: path.relative(cwd, abs);
|
|
14677
|
+
};
|
|
14678
|
+
const entrySet = new Set(highConf.map(r => r.file));
|
|
14679
|
+
const direct = new Set();
|
|
14680
|
+
const transitive = new Set();
|
|
14681
|
+
for (const r of highConf) {
|
|
14682
|
+
const imp = getImpact(r.file, graph, { depth: 3, cwd });
|
|
14683
|
+
for (const f of (imp.direct || [])) direct.add(clean(f));
|
|
14684
|
+
for (const f of (imp.transitive || [])) transitive.add(clean(f));
|
|
14685
|
+
}
|
|
14686
|
+
// The files we plan to change are not their own blast radius; and a file
|
|
14687
|
+
// reached directly from one entry outranks a transitive reach from another.
|
|
14688
|
+
for (const e of entrySet) { direct.delete(e); transitive.delete(e); }
|
|
14689
|
+
for (const f of direct) transitive.delete(f);
|
|
14690
|
+
impact = { direct: [...direct], transitive: [...transitive] };
|
|
14316
14691
|
} catch (_) {
|
|
14317
14692
|
// Graph build failed, continue without impact
|
|
14318
14693
|
}
|
|
14319
14694
|
}
|
|
14320
14695
|
|
|
14321
|
-
// Step 4:
|
|
14322
|
-
|
|
14696
|
+
// Step 4: Flag which files-to-inspect have detectable test coverage. The test
|
|
14697
|
+
// index maps test-*name tokens*, not test files, so `isTested` can only tell
|
|
14698
|
+
// us a source file is covered — it cannot name the test file. We therefore
|
|
14699
|
+
// report the covered SOURCE files honestly rather than pretending to list the
|
|
14700
|
+
// tests to run.
|
|
14701
|
+
let coveredFiles = [];
|
|
14323
14702
|
try {
|
|
14324
14703
|
const testIndex = buildTestIndex(cwd, config.testDirs || ['test', 'tests', '__tests__', 'spec']);
|
|
14325
|
-
|
|
14326
|
-
const
|
|
14327
|
-
const fnNames = sigs.map(s => {
|
|
14704
|
+
coveredFiles = highConf.filter(r => {
|
|
14705
|
+
const fnNames = (r.sigs || []).map(s => {
|
|
14328
14706
|
const m = s.match(/(?:function|def|fn)\s+(\w+)/);
|
|
14329
14707
|
return m ? m[1] : null;
|
|
14330
14708
|
}).filter(Boolean);
|
|
14331
14709
|
return fnNames.some(fn => isTested(fn, testIndex));
|
|
14332
|
-
});
|
|
14710
|
+
}).map(r => r.file);
|
|
14333
14711
|
} catch (_) {
|
|
14334
14712
|
// Coverage index failed, continue without test info
|
|
14335
14713
|
}
|
|
@@ -14339,11 +14717,11 @@ __factories["./src/plan/planner"] = function(module, exports) {
|
|
|
14339
14717
|
intent,
|
|
14340
14718
|
inspectFirst: highConf.map(r => r.file),
|
|
14341
14719
|
likelyToChange: medConf.map(r => r.file),
|
|
14342
|
-
impactRadius: impact
|
|
14343
|
-
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
testsAffected:
|
|
14720
|
+
impactRadius: impact,
|
|
14721
|
+
coveredFiles,
|
|
14722
|
+
// `testsAffected` retained for backward compatibility; it is the set of
|
|
14723
|
+
// covered source files, NOT the test files (which the index cannot name).
|
|
14724
|
+
testsAffected: coveredFiles,
|
|
14347
14725
|
};
|
|
14348
14726
|
}
|
|
14349
14727
|
|
|
@@ -15392,7 +15770,8 @@ __factories["./src/review/pr-evidence"] = function(module, exports) {
|
|
|
15392
15770
|
L.push('### Review findings');
|
|
15393
15771
|
for (const f of evidence.review.findings) {
|
|
15394
15772
|
if (f.type === 'missing-tests') L.push(`- ⚠️ **missing tests** — \`${f.file}\` changed with no matching test`);
|
|
15395
|
-
else if (f.type === 'security-file') L.push(`- ⚠️ **
|
|
15773
|
+
else if (f.type === 'security-file') L.push(`- ⚠️ **sensitive path touched** (path heuristic, not a content scan) — \`${f.file}\``);
|
|
15774
|
+
else if (f.type === 'secret-detected') L.push(`- 🔑 **secret detected** (${f.secret}) — \`${f.file}\``);
|
|
15396
15775
|
else if (f.type === 'god-node') L.push(`- ⚠️ **god node** — \`${f.file}\` → ${f.count} dependents (high blast radius)`);
|
|
15397
15776
|
else if (f.type === 'scope-drift') L.push(`- ⚠️ **scope drift** — ${f.count} top-level dirs touched (${f.dirs.join(', ')})`);
|
|
15398
15777
|
}
|
|
@@ -15447,8 +15826,10 @@ __factories["./src/review/review-pr"] = function(module, exports) {
|
|
|
15447
15826
|
* zero-dependency, bundle-safe; reuses the impact graph for blast radius.
|
|
15448
15827
|
*/
|
|
15449
15828
|
|
|
15829
|
+
const fs = require('fs');
|
|
15450
15830
|
const path = require('path');
|
|
15451
15831
|
const { analyzeImpact } = __require('./src/graph/impact');
|
|
15832
|
+
const { PATTERNS } = __require('./src/security/patterns');
|
|
15452
15833
|
|
|
15453
15834
|
const SECURITY_PATTERNS = [
|
|
15454
15835
|
/(^|\/)\.env(\.|$)/i,
|
|
@@ -15499,10 +15880,30 @@ __factories["./src/review/review-pr"] = function(module, exports) {
|
|
|
15499
15880
|
if (!covered) findings.push({ type: 'missing-tests', file: s, severity: 'warn' });
|
|
15500
15881
|
}
|
|
15501
15882
|
|
|
15502
|
-
//
|
|
15883
|
+
// 2a. Sensitive-path heuristic — flags files whose PATH looks security-relevant
|
|
15884
|
+
// (.env, auth/, lockfiles, workflows, key material). This is a path heuristic,
|
|
15885
|
+
// NOT a content scan: it flags touching the path regardless of what changed,
|
|
15886
|
+
// and cannot see a secret hidden in an innocently-named file. `basis` records
|
|
15887
|
+
// that honestly so consumers don't mistake it for a content check.
|
|
15503
15888
|
for (const f of live) {
|
|
15504
15889
|
if (SECURITY_PATTERNS.some((re) => re.test(f.path))) {
|
|
15505
|
-
findings.push({ type: 'security-file', file: f.path, severity: 'warn' });
|
|
15890
|
+
findings.push({ type: 'security-file', file: f.path, severity: 'warn', basis: 'path-heuristic' });
|
|
15891
|
+
}
|
|
15892
|
+
}
|
|
15893
|
+
|
|
15894
|
+
// 2b. Real secret scan — read each changed file's CONTENT and match known
|
|
15895
|
+
// secret patterns. This is the actual security check (content, not filename):
|
|
15896
|
+
// it catches a hardcoded key in a file the path heuristic would never flag.
|
|
15897
|
+
const readFile = opts.readFile || ((p) => fs.readFileSync(path.resolve(cwd, p), 'utf8'));
|
|
15898
|
+
for (const f of live) {
|
|
15899
|
+
let content;
|
|
15900
|
+
try { content = readFile(f.path); } catch (_) { continue; } // absent/unreadable → skip
|
|
15901
|
+
if (typeof content !== 'string' || content.length > 2_000_000) continue; // skip huge/binary
|
|
15902
|
+
for (const pat of PATTERNS) {
|
|
15903
|
+
if (pat.regex.test(content)) {
|
|
15904
|
+
findings.push({ type: 'secret-detected', file: f.path, secret: pat.name, severity: 'high', basis: 'content-scan' });
|
|
15905
|
+
break; // one hit is enough to flag the file
|
|
15906
|
+
}
|
|
15506
15907
|
}
|
|
15507
15908
|
}
|
|
15508
15909
|
|
|
@@ -17122,6 +17523,52 @@ __factories["./src/util/git"] = function(module, exports) {
|
|
|
17122
17523
|
|
|
17123
17524
|
};
|
|
17124
17525
|
|
|
17526
|
+
// ── ./src/util/truncate ──
|
|
17527
|
+
__factories["./src/util/truncate"] = function(module, exports) {
|
|
17528
|
+
|
|
17529
|
+
/**
|
|
17530
|
+
* Visible truncation for extractor caps (v8.11).
|
|
17531
|
+
*
|
|
17532
|
+
* Extractors cap per-file signatures and per-class members to protect the token
|
|
17533
|
+
* budget. Historically that truncation was SILENT — the tail of a large file
|
|
17534
|
+
* simply vanished with no trace, so "5 of 40 methods extracted" looked identical
|
|
17535
|
+
* to "fully extracted". These helpers keep the cap but append a visible marker
|
|
17536
|
+
* so the loss is always disclosed.
|
|
17537
|
+
*
|
|
17538
|
+
* Zero-dependency, bundle-safe.
|
|
17539
|
+
*/
|
|
17540
|
+
|
|
17541
|
+
/**
|
|
17542
|
+
* Cap a string array, appending a `… +N more <label>` marker when items drop.
|
|
17543
|
+
* @param {string[]} items
|
|
17544
|
+
* @param {number} limit
|
|
17545
|
+
* @param {string} label e.g. 'signatures'
|
|
17546
|
+
* @returns {string[]}
|
|
17547
|
+
*/
|
|
17548
|
+
function capWithNotice(items, limit, label) {
|
|
17549
|
+
if (!Array.isArray(items) || items.length <= limit) return items;
|
|
17550
|
+
const dropped = items.length - limit;
|
|
17551
|
+
return items.slice(0, limit).concat(`… +${dropped} more ${label}`);
|
|
17552
|
+
}
|
|
17553
|
+
|
|
17554
|
+
/**
|
|
17555
|
+
* Cap an array of member objects ({ text, ... }), appending a marker member
|
|
17556
|
+
* when items drop so the class block discloses the omission.
|
|
17557
|
+
* @param {Array<{text:string}>} members
|
|
17558
|
+
* @param {number} limit
|
|
17559
|
+
* @param {string} [label='methods']
|
|
17560
|
+
* @returns {Array<{text:string}>}
|
|
17561
|
+
*/
|
|
17562
|
+
function capMembersWithNotice(members, limit, label = 'methods') {
|
|
17563
|
+
if (!Array.isArray(members) || members.length <= limit) return members;
|
|
17564
|
+
const dropped = members.length - limit;
|
|
17565
|
+
return members.slice(0, limit).concat({ text: `… +${dropped} more ${label}`, start: 0, end: 0 });
|
|
17566
|
+
}
|
|
17567
|
+
|
|
17568
|
+
module.exports = { capWithNotice, capMembersWithNotice };
|
|
17569
|
+
|
|
17570
|
+
};
|
|
17571
|
+
|
|
17125
17572
|
// ── ./src/verify/closest-match ──
|
|
17126
17573
|
__factories["./src/verify/closest-match"] = function(module, exports) {
|
|
17127
17574
|
|
|
@@ -18286,7 +18733,7 @@ function __tryGit(args, opts = {}) {
|
|
|
18286
18733
|
catch (_) { return ''; }
|
|
18287
18734
|
}
|
|
18288
18735
|
|
|
18289
|
-
const VERSION = '8.
|
|
18736
|
+
const VERSION = '8.11.0';
|
|
18290
18737
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
18291
18738
|
|
|
18292
18739
|
function requireSourceOrBundled(key) {
|
|
@@ -18963,6 +19410,21 @@ function formatOutput(fileEntries, cwd, routingEnabled, config, extras) {
|
|
|
18963
19410
|
groups[group].push({ rel, sigs: entry.sigs });
|
|
18964
19411
|
}
|
|
18965
19412
|
|
|
19413
|
+
// D7: terse signature encoding — anchors preserved byte-exactly
|
|
19414
|
+
const terseOn = !!(config && config.terse);
|
|
19415
|
+
let encodeTerseSigs = null;
|
|
19416
|
+
if (terseOn) {
|
|
19417
|
+
try {
|
|
19418
|
+
const terseMod = requireSourceOrBundled('./src/format/terse');
|
|
19419
|
+
encodeTerseSigs = terseMod.encodeTerseSigs;
|
|
19420
|
+
const m = terseMod.measureTerse(fileEntries.map((e) => e.sigs));
|
|
19421
|
+
console.warn(`[sigmap] terse: sig block ${m.beforeTokens} → ${m.afterTokens} tokens (-${m.reductionPct}%)`);
|
|
19422
|
+
} catch (err) {
|
|
19423
|
+
encodeTerseSigs = null;
|
|
19424
|
+
console.warn(`[sigmap] terse encoding skipped: ${err.message}`);
|
|
19425
|
+
}
|
|
19426
|
+
}
|
|
19427
|
+
|
|
18966
19428
|
for (const [group, entries] of Object.entries(groups).sort()) {
|
|
18967
19429
|
lines.push(`## ${group}`);
|
|
18968
19430
|
lines.push('');
|
|
@@ -18972,7 +19434,7 @@ function formatOutput(fileEntries, cwd, routingEnabled, config, extras) {
|
|
|
18972
19434
|
const usedByStr = usedBy && usedBy.length ? ` (used by: ${usedBy.join(', ')})` : '';
|
|
18973
19435
|
lines.push(`### ${rel}${usedByStr}`);
|
|
18974
19436
|
lines.push('```');
|
|
18975
|
-
lines.push(...sigs);
|
|
19437
|
+
lines.push(...(encodeTerseSigs ? encodeTerseSigs(sigs) : sigs));
|
|
18976
19438
|
lines.push('```');
|
|
18977
19439
|
lines.push('');
|
|
18978
19440
|
}
|
|
@@ -20086,6 +20548,7 @@ Usage:
|
|
|
20086
20548
|
${cmd} --monorepo Generate per-package context (monorepo)
|
|
20087
20549
|
${cmd} --each Run for every repo in the current directory
|
|
20088
20550
|
${cmd} --routing Include model routing hints in output
|
|
20551
|
+
${cmd} --terse Compact signature encoding (deterministic; line anchors preserved)
|
|
20089
20552
|
${cmd} --format cache Also write Anthropic prompt-cache JSON
|
|
20090
20553
|
${cmd} --track Append run metrics to .context/usage.ndjson
|
|
20091
20554
|
${cmd} --watch Generate + watch for file changes
|
|
@@ -20292,15 +20755,9 @@ function registerMcp(cwd, scriptPath) {
|
|
|
20292
20755
|
// ---------------------------------------------------------------------------
|
|
20293
20756
|
// v4.2 helpers
|
|
20294
20757
|
// ---------------------------------------------------------------------------
|
|
20295
|
-
|
|
20296
|
-
|
|
20297
|
-
|
|
20298
|
-
'gpt-4o-mini': 0.000150,
|
|
20299
|
-
'claude-3-5-sonnet': 0.003,
|
|
20300
|
-
'claude-3-haiku': 0.00025,
|
|
20301
|
-
'claude-opus-4': 0.015,
|
|
20302
|
-
'gemini-1.5-pro': 0.00125,
|
|
20303
|
-
};
|
|
20758
|
+
// Pricing lives in src/tracking/pricing.js (the verified, single-source table
|
|
20759
|
+
// shared with the `gain` dashboard). The old inline MODEL_COSTS table was
|
|
20760
|
+
// removed — it disagreed with pricing.js (e.g. gpt-4o at $5 vs $2.50 /Mtok).
|
|
20304
20761
|
|
|
20305
20762
|
function buildMiniContext(ranked, cwd) {
|
|
20306
20763
|
const lines = ['# SigMap Query Context', `Generated: ${new Date().toISOString()}`, ''];
|
|
@@ -20478,6 +20935,11 @@ function main() {
|
|
|
20478
20935
|
config.testCoverage = true;
|
|
20479
20936
|
}
|
|
20480
20937
|
|
|
20938
|
+
// --terse: compact signature encoding without editing config (D7)
|
|
20939
|
+
if (args.includes('--terse')) {
|
|
20940
|
+
config.terse = true;
|
|
20941
|
+
}
|
|
20942
|
+
|
|
20481
20943
|
// ── --output <file> — parse early so every subsequent block can use it ─────
|
|
20482
20944
|
// Resolves the custom output path and merges it into config.customOutput.
|
|
20483
20945
|
// Also persists the resolved relative path to gen-context.config.json so
|
|
@@ -20671,8 +21133,14 @@ function main() {
|
|
|
20671
21133
|
}
|
|
20672
21134
|
if (rawTok === 0) rawTok = getRawTokenCount(cwd, config);
|
|
20673
21135
|
const savings = rawTok > 0 ? Math.round((1 - ctxTok / rawTok) * 100) : 0;
|
|
20674
|
-
const
|
|
20675
|
-
const
|
|
21136
|
+
const __mIdx = args.indexOf('--model');
|
|
21137
|
+
const model = (__mIdx !== -1 && args[__mIdx + 1] && !args[__mIdx + 1].startsWith('--')) ? args[__mIdx + 1] : 'gpt-4o';
|
|
21138
|
+
// Single source of truth for pricing — the verified pricing.js table, shared
|
|
21139
|
+
// with the `gain` dashboard (previously an inline MODEL_COSTS table disagreed
|
|
21140
|
+
// with it, e.g. gpt-4o priced at $5/Mtok here vs $2.50/Mtok there).
|
|
21141
|
+
const { resolvePrice: __resolvePrice } = requireSourceOrBundled('./src/tracking/pricing');
|
|
21142
|
+
const __price = __resolvePrice(model);
|
|
21143
|
+
const rateK = __price.perMtok / 1000; // USD per 1K tokens
|
|
20676
21144
|
const costRaw = ((rawTok / 1000) * rateK).toFixed(4);
|
|
20677
21145
|
const costCtx = ((ctxTok / 1000) * rateK).toFixed(4);
|
|
20678
21146
|
|
|
@@ -20682,6 +21150,8 @@ function main() {
|
|
|
20682
21150
|
process.stdout.write(JSON.stringify({
|
|
20683
21151
|
intent, coverage: coveragePct, contextTokens: ctxTok,
|
|
20684
21152
|
costBefore: costRaw, costAfter: costCtx, savingsPct: savings,
|
|
21153
|
+
pricedModel: __price.model,
|
|
21154
|
+
costBasis: 'estimate — counterfactual = full content of ranked files; input tokens only',
|
|
20685
21155
|
riskLevel, contextPath: path.relative(cwd, outPath),
|
|
20686
21156
|
}) + '\n');
|
|
20687
21157
|
} else {
|
|
@@ -20697,6 +21167,7 @@ function main() {
|
|
|
20697
21167
|
` Coverage : ${coveragePct}%`,
|
|
20698
21168
|
` Risk : ${riskLevel}`,
|
|
20699
21169
|
` Cost : $${costCtx}/query (was $${costRaw} · saved ${savings}%)`,
|
|
21170
|
+
` ${' '.repeat(9)} est. @ ${__price.model} $${__price.perMtok}/Mtok input; "was" = full ranked files`,
|
|
20700
21171
|
bar,
|
|
20701
21172
|
].join('\n'));
|
|
20702
21173
|
}
|
|
@@ -20825,21 +21296,57 @@ function main() {
|
|
|
20825
21296
|
process.exit(0);
|
|
20826
21297
|
}
|
|
20827
21298
|
|
|
20828
|
-
// v4.2: `sigmap suggest-profile` —
|
|
21299
|
+
// v4.2: `sigmap suggest-profile` — infer the task profile from the actual
|
|
21300
|
+
// staged CHANGES (test ratio, breadth, doc/config mix), not just one
|
|
21301
|
+
// commit-message keyword. The commit message is only a secondary disambiguator
|
|
21302
|
+
// for source-focused edits, and the sole (weak) signal when nothing is staged.
|
|
20829
21303
|
if (args[0] === 'suggest-profile') {
|
|
20830
21304
|
const short = args.includes('--short');
|
|
20831
|
-
let msg = '',
|
|
21305
|
+
let msg = '', diffRaw = '';
|
|
20832
21306
|
try {
|
|
20833
|
-
msg
|
|
20834
|
-
|
|
21307
|
+
msg = __git(['log', '-1', '--format=%s'], { cwd, timeout: 3000 }).trim();
|
|
21308
|
+
diffRaw = __git(['diff', '--cached', '--name-only'], { cwd, timeout: 3000 });
|
|
20835
21309
|
} catch (_) {}
|
|
20836
21310
|
|
|
21311
|
+
const files = diffRaw.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
21312
|
+
const isTest = (f) => /\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.(py|go)$|(^|\/)(tests?|__tests__|spec)\//i.test(f);
|
|
21313
|
+
const isSrc = (f) => /\.(js|jsx|ts|tsx|mjs|cjs|py|go|rs|java|rb|php)$/i.test(f) && !isTest(f);
|
|
21314
|
+
const isDocCfg = (f) => /\.(md|json|ya?ml|toml|cfg|ini)$/i.test(f) || /(^|\/)\.github\//i.test(f);
|
|
21315
|
+
const fixMsg = /fix|bug|error|crash|exception/i.test(msg);
|
|
21316
|
+
const archMsg = /refactor|architect|redesign|module/i.test(msg);
|
|
21317
|
+
|
|
21318
|
+
const testN = files.filter(isTest).length;
|
|
21319
|
+
const srcN = files.filter(isSrc).length;
|
|
21320
|
+
const docCfgN = files.filter(isDocCfg).length;
|
|
21321
|
+
const dirs = new Set(files.map((f) => (f.includes('/') ? f.split('/')[0] : '.')));
|
|
21322
|
+
|
|
20837
21323
|
let profile = 'default';
|
|
20838
|
-
let reason
|
|
20839
|
-
if
|
|
20840
|
-
|
|
20841
|
-
|
|
20842
|
-
|
|
21324
|
+
let reason;
|
|
21325
|
+
if (files.length === 0) {
|
|
21326
|
+
if (fixMsg) { profile = 'debug'; reason = `no staged files; commit hint: "${msg.slice(0, 50)}"`; }
|
|
21327
|
+
else if (archMsg) { profile = 'architecture'; reason = `no staged files; commit hint: "${msg.slice(0, 50)}"`; }
|
|
21328
|
+
else if (/review|pr|pull.request|check/i.test(msg)) { profile = 'review'; reason = `no staged files; commit hint: "${msg.slice(0, 50)}"`; }
|
|
21329
|
+
else { reason = 'no staged files and no strong commit-message signal'; }
|
|
21330
|
+
} else if (dirs.size >= 4) {
|
|
21331
|
+
profile = 'architecture';
|
|
21332
|
+
reason = `changes span ${dirs.size} top-level dirs (${[...dirs].slice(0, 4).join(', ')}…)`;
|
|
21333
|
+
} else if (testN > 0 && testN >= srcN) {
|
|
21334
|
+
profile = 'debug';
|
|
21335
|
+
reason = `${testN} test file(s) staged (≥ ${srcN} source) — test-driven change`;
|
|
21336
|
+
} else if (srcN === 0 && docCfgN > 0) {
|
|
21337
|
+
profile = 'review';
|
|
21338
|
+
reason = `only docs/config staged (${docCfgN} file(s)) — meta change`;
|
|
21339
|
+
} else if (srcN > 0 && archMsg) {
|
|
21340
|
+
profile = 'architecture';
|
|
21341
|
+
reason = `${srcN} source file(s) + refactor-style commit`;
|
|
21342
|
+
} else if (srcN > 0 && fixMsg) {
|
|
21343
|
+
profile = 'debug';
|
|
21344
|
+
reason = `${srcN} source file(s) + fix-style commit`;
|
|
21345
|
+
} else if (srcN > 0) {
|
|
21346
|
+
reason = `${srcN} source file(s) staged, no strong task signal`;
|
|
21347
|
+
} else {
|
|
21348
|
+
reason = 'staged changes do not match a known task profile';
|
|
21349
|
+
}
|
|
20843
21350
|
|
|
20844
21351
|
if (short) {
|
|
20845
21352
|
console.log(profile);
|
|
@@ -20939,61 +21446,57 @@ function main() {
|
|
|
20939
21446
|
process.exit(1);
|
|
20940
21447
|
}
|
|
20941
21448
|
|
|
20942
|
-
const {
|
|
20943
|
-
|
|
20944
|
-
const intent = detectIntent(goal);
|
|
20945
|
-
const intentWeights = getIntentWeights(intent);
|
|
21449
|
+
const { createPlan } = requireSourceOrBundled('./src/plan/planner');
|
|
21450
|
+
const plan = createPlan(goal, cwd, config);
|
|
20946
21451
|
|
|
20947
|
-
|
|
20948
|
-
if (sigIndex.size === 0) {
|
|
21452
|
+
if (plan.error) {
|
|
20949
21453
|
console.error('[sigmap] no context file found. Run: sigmap (to generate first)');
|
|
20950
21454
|
process.exit(1);
|
|
20951
21455
|
}
|
|
20952
21456
|
|
|
20953
|
-
const
|
|
20954
|
-
|
|
20955
|
-
// Separate into confidence levels
|
|
20956
|
-
const highConf = ranked.filter(r => r.confidence === 'high').slice(0, 5);
|
|
20957
|
-
const medConf = ranked.filter(r => r.confidence === 'medium').slice(0, 5);
|
|
20958
|
-
|
|
20959
|
-
// Compute impact radius (simplified — no graph for now)
|
|
20960
|
-
let impact = null;
|
|
20961
|
-
|
|
20962
|
-
// Identify likely-affected tests (simplified — checks for .test. or .spec. in filename)
|
|
20963
|
-
const testedFiles = highConf.filter(r => /\.(test|spec)\.(js|ts|py)$|_test\.(js|ts|py)$/.test(r.file));
|
|
21457
|
+
const relOf = (f) => path.relative(cwd, path.isAbsolute(f) ? f : path.join(cwd, f));
|
|
21458
|
+
const impact = plan.impactRadius;
|
|
20964
21459
|
|
|
20965
21460
|
if (args.includes('--json')) {
|
|
20966
21461
|
process.stdout.write(JSON.stringify({
|
|
20967
|
-
goal
|
|
20968
|
-
|
|
20969
|
-
|
|
21462
|
+
goal: plan.goal,
|
|
21463
|
+
intent: plan.intent,
|
|
21464
|
+
inspectFirst: plan.inspectFirst,
|
|
21465
|
+
likelyToChange: plan.likelyToChange,
|
|
20970
21466
|
impactRadius: impact,
|
|
20971
|
-
|
|
21467
|
+
coveredFiles: plan.coveredFiles,
|
|
21468
|
+
testsAffected: plan.testsAffected,
|
|
20972
21469
|
}, null, 2) + '\n');
|
|
20973
21470
|
} else {
|
|
20974
21471
|
const bar = '─'.repeat(50);
|
|
20975
21472
|
console.log(bar);
|
|
20976
21473
|
console.log(` sigmap plan "${goal}"`);
|
|
20977
|
-
console.log(` Intent : ${intent}`);
|
|
21474
|
+
console.log(` Intent : ${plan.intent}`);
|
|
20978
21475
|
console.log(bar);
|
|
20979
21476
|
console.log('');
|
|
20980
21477
|
console.log(' Inspect first (highest relevance):');
|
|
20981
|
-
if (
|
|
21478
|
+
if (plan.inspectFirst.length === 0) {
|
|
20982
21479
|
console.log(' (no files found)');
|
|
20983
21480
|
} else {
|
|
20984
|
-
|
|
21481
|
+
plan.inspectFirst.forEach((f, i) => console.log(` ${i + 1}. ${relOf(f)}`));
|
|
20985
21482
|
}
|
|
20986
21483
|
console.log('');
|
|
20987
21484
|
console.log(' Likely to change:');
|
|
20988
|
-
if (
|
|
21485
|
+
if (plan.likelyToChange.length === 0) {
|
|
20989
21486
|
console.log(' (no files found)');
|
|
20990
21487
|
} else {
|
|
20991
|
-
|
|
21488
|
+
plan.likelyToChange.forEach((f, i) => console.log(` ${i + 1}. ${relOf(f)}`));
|
|
20992
21489
|
}
|
|
20993
|
-
if (
|
|
21490
|
+
if (impact && (impact.direct.length || impact.transitive.length)) {
|
|
20994
21491
|
console.log('');
|
|
20995
|
-
console.log('
|
|
20996
|
-
|
|
21492
|
+
console.log(' Impact radius (relative-import dependents, ≤3 hops — lower bound):');
|
|
21493
|
+
impact.direct.forEach(f => console.log(` • ${relOf(f)} (direct)`));
|
|
21494
|
+
impact.transitive.forEach(f => console.log(` • ${relOf(f)} (transitive)`));
|
|
21495
|
+
}
|
|
21496
|
+
if (plan.coveredFiles.length > 0) {
|
|
21497
|
+
console.log('');
|
|
21498
|
+
console.log(' Files with test coverage (re-run their suites after changing):');
|
|
21499
|
+
plan.coveredFiles.forEach(f => console.log(` • ${relOf(f)}`));
|
|
20997
21500
|
}
|
|
20998
21501
|
console.log('');
|
|
20999
21502
|
console.log(bar);
|
|
@@ -21151,18 +21654,18 @@ function main() {
|
|
|
21151
21654
|
}
|
|
21152
21655
|
|
|
21153
21656
|
if (entries.length === 0) {
|
|
21154
|
-
console.log('[sigmap] No
|
|
21657
|
+
console.log('[sigmap] No manual weights set. Run: sigmap learn --good <file> to boost a file.');
|
|
21155
21658
|
process.exit(0);
|
|
21156
21659
|
}
|
|
21157
21660
|
|
|
21158
|
-
console.log('[sigmap]
|
|
21661
|
+
console.log('[sigmap] Manual file weights — boost/penalty multipliers vs baseline:');
|
|
21159
21662
|
for (const [file, mult] of entries) {
|
|
21160
21663
|
const bar = mult >= 1
|
|
21161
21664
|
? `+${'█'.repeat(Math.max(1, Math.round((mult - 1) * 10)))}`
|
|
21162
21665
|
: `-${'░'.repeat(Math.max(1, Math.round((1 - mult) * 10)))}`;
|
|
21163
21666
|
console.log(` ${file.padEnd(50)} x${mult.toFixed(2)} ${bar}`);
|
|
21164
21667
|
}
|
|
21165
|
-
console.log(`\n
|
|
21668
|
+
console.log(`\n ${entries.length} file(s) manually boosted/penalized (set via \`sigmap learn\`; decays toward 1.0 over time — not automatic learning).`);
|
|
21166
21669
|
console.log(' To reset: sigmap learn --reset');
|
|
21167
21670
|
process.exit(0);
|
|
21168
21671
|
}
|
|
@@ -21192,7 +21695,17 @@ function main() {
|
|
|
21192
21695
|
if (coveragePct < 70)
|
|
21193
21696
|
warnings.push(`coverage ${coveragePct}% is below recommended 70% — increase maxTokens or expand srcDirs`);
|
|
21194
21697
|
|
|
21195
|
-
// Optional query
|
|
21698
|
+
// Optional query check. Two complementary signals:
|
|
21699
|
+
// (a) cased-symbol coverage — if the query literally names a camelCase /
|
|
21700
|
+
// PascalCase symbol (loginUser, AuthMiddleware), confirm it lands in
|
|
21701
|
+
// the top-5. This is a no-op for lowercase natural-language queries.
|
|
21702
|
+
// (b) retrieval-confidence report — works for ANY query, including plain
|
|
21703
|
+
// NL like "login rate limit". Built from the ranker's own score
|
|
21704
|
+
// distribution: a zero top score means the current context has no
|
|
21705
|
+
// lexical match at all; a near-tie between rank-1 and rank-2 means the
|
|
21706
|
+
// ranking is flat and coverage is ambiguous. This replaces the old
|
|
21707
|
+
// behaviour where an NL query silently produced no output whatsoever.
|
|
21708
|
+
let queryReport = null;
|
|
21196
21709
|
const valQueryIdx = args.indexOf('--query');
|
|
21197
21710
|
if (valQueryIdx !== -1) {
|
|
21198
21711
|
const q = (args[valQueryIdx + 1] || '').trim();
|
|
@@ -21200,6 +21713,8 @@ function main() {
|
|
|
21200
21713
|
try {
|
|
21201
21714
|
const { rank, buildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
21202
21715
|
const ranked = rank(q, buildSigIndex(cwd), { topK: 5, cwd });
|
|
21716
|
+
|
|
21717
|
+
// (a) cased-symbol coverage
|
|
21203
21718
|
const symbols = extractQuerySymbols(q);
|
|
21204
21719
|
const missing = symbols.filter((sym) =>
|
|
21205
21720
|
!ranked.some((r) => r.sigs && r.sigs.some((s) => s.toLowerCase().includes(sym.toLowerCase())))
|
|
@@ -21208,12 +21723,38 @@ function main() {
|
|
|
21208
21723
|
warnings.push(`query "${q}" references symbols not in top-5 context: ${missing.join(', ')}`);
|
|
21209
21724
|
else if (symbols.length > 0)
|
|
21210
21725
|
console.log(`[sigmap] ✓ query coverage OK — all ${symbols.length} symbols found`);
|
|
21726
|
+
|
|
21727
|
+
// (b) retrieval-confidence report
|
|
21728
|
+
const top = ranked[0] || null;
|
|
21729
|
+
const topScore = top ? (top.score || 0) : 0;
|
|
21730
|
+
const secondScore = ranked[1] ? (ranked[1].score || 0) : 0;
|
|
21731
|
+
const gapRatio = topScore > 0 ? (topScore - secondScore) / topScore : 0;
|
|
21732
|
+
let queryConfidence;
|
|
21733
|
+
if (topScore <= 0) queryConfidence = 'none';
|
|
21734
|
+
else if (ranked.length > 1 && gapRatio < 0.1) queryConfidence = 'low';
|
|
21735
|
+
else queryConfidence = (top && top.confidence) || 'medium';
|
|
21736
|
+
|
|
21737
|
+
queryReport = {
|
|
21738
|
+
text: q,
|
|
21739
|
+
topFile: top ? top.file : null,
|
|
21740
|
+
topScore: parseFloat(topScore.toFixed(3)),
|
|
21741
|
+
confidence: queryConfidence,
|
|
21742
|
+
};
|
|
21743
|
+
|
|
21744
|
+
if (queryConfidence === 'none')
|
|
21745
|
+
warnings.push(`query "${q}" has no lexical match in the current context — expand srcDirs or raise maxTokens`);
|
|
21746
|
+
else if (queryConfidence === 'low')
|
|
21747
|
+
warnings.push(`query "${q}" ranks flat (top ${top.file} score ${queryReport.topScore}, no dominant match) — context may not cover it well`);
|
|
21748
|
+
else if (!args.includes('--json'))
|
|
21749
|
+
console.log(`[sigmap] ✓ query "${q}" → ${top.file} (score ${queryReport.topScore}, confidence ${queryConfidence})`);
|
|
21211
21750
|
} catch (_) {}
|
|
21212
21751
|
}
|
|
21213
21752
|
}
|
|
21214
21753
|
|
|
21215
21754
|
if (args.includes('--json')) {
|
|
21216
|
-
|
|
21755
|
+
const payload = { valid: issues.length === 0, issues, warnings, coverage: coveragePct };
|
|
21756
|
+
if (queryReport) payload.query = queryReport;
|
|
21757
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
21217
21758
|
} else {
|
|
21218
21759
|
for (const w of warnings) console.warn(`[sigmap] ⚠ ${w}`);
|
|
21219
21760
|
if (issues.length === 0) {
|
|
@@ -22163,10 +22704,11 @@ function main() {
|
|
|
22163
22704
|
console.log(' ✓ no findings — scope, tests, blast radius, and sensitive files all clear');
|
|
22164
22705
|
process.exit(0);
|
|
22165
22706
|
}
|
|
22166
|
-
const label = { 'missing-tests': 'missing tests', 'security-file': '
|
|
22707
|
+
const label = { 'missing-tests': 'missing tests', 'security-file': 'sensitive path (path heuristic)', 'secret-detected': 'secret detected', 'god-node': 'god node', 'scope-drift': 'scope drift' };
|
|
22167
22708
|
for (const f of result.findings) {
|
|
22168
22709
|
if (f.type === 'missing-tests') console.log(` ⚠ ${label[f.type]}: ${f.file} changed with no matching test`);
|
|
22169
22710
|
else if (f.type === 'security-file') console.log(` ⚠ ${label[f.type]}: ${f.file}`);
|
|
22711
|
+
else if (f.type === 'secret-detected') console.log(` ✗ ${label[f.type]}: ${f.secret} in ${f.file}`);
|
|
22170
22712
|
else if (f.type === 'god-node') console.log(` ⚠ ${label[f.type]}: ${f.file} → ${f.count} dependents`);
|
|
22171
22713
|
else if (f.type === 'scope-drift') console.log(` ⚠ ${label[f.type]}: ${f.count} top-level dirs (${f.dirs.join(', ')})`);
|
|
22172
22714
|
}
|
|
@@ -22228,7 +22770,7 @@ function main() {
|
|
|
22228
22770
|
process.exit(result.summary.ok ? 0 : 1);
|
|
22229
22771
|
}
|
|
22230
22772
|
|
|
22231
|
-
console.log(`[sigmap] create${result.task ? ` "${result.task}"` : ''} —
|
|
22773
|
+
console.log(`[sigmap] create${result.task ? ` "${result.task}"` : ''} — running deterministic guards (scaffold · verify-plan · verify-ai-output · review-pr); the LLM does the authoring`);
|
|
22232
22774
|
for (const st of result.steps) {
|
|
22233
22775
|
const mark = st.skipped ? '–' : (st.ok ? '✓' : '✗');
|
|
22234
22776
|
const status = st.skipped ? `skipped (${st.reason})` : (st.ok ? 'ok' : 'FAILED');
|
|
@@ -22874,8 +23416,12 @@ function main() {
|
|
|
22874
23416
|
const rawTok = getRawTokenCount(cwd, config);
|
|
22875
23417
|
runGenerate(cwd, config, false);
|
|
22876
23418
|
|
|
22877
|
-
const
|
|
22878
|
-
const
|
|
23419
|
+
const __mIdxCost = args.indexOf('--model');
|
|
23420
|
+
const model = (__mIdxCost !== -1 && args[__mIdxCost + 1] && !args[__mIdxCost + 1].startsWith('--')) ? args[__mIdxCost + 1] : 'gpt-4o';
|
|
23421
|
+
// Single source of truth for pricing — shared with `gain` (pricing.js).
|
|
23422
|
+
const { resolvePrice: __resolvePriceCost } = requireSourceOrBundled('./src/tracking/pricing');
|
|
23423
|
+
const __priceCost = __resolvePriceCost(model);
|
|
23424
|
+
const rateK = __priceCost.perMtok / 1000; // USD per 1K tokens
|
|
22879
23425
|
|
|
22880
23426
|
const ctxPath = config.customOutput
|
|
22881
23427
|
? path.resolve(cwd, config.customOutput)
|
|
@@ -22888,19 +23434,20 @@ function main() {
|
|
|
22888
23434
|
const costCtx = (outTok / 1000) * rateK;
|
|
22889
23435
|
|
|
22890
23436
|
const out = {
|
|
22891
|
-
model,
|
|
23437
|
+
model: __priceCost.model,
|
|
22892
23438
|
rawTokens: rawTok,
|
|
22893
23439
|
contextTokens: outTok,
|
|
22894
23440
|
costRaw: costRaw.toFixed(4),
|
|
22895
23441
|
costContext: costCtx.toFixed(4),
|
|
22896
23442
|
savingsPct: savings,
|
|
23443
|
+
costBasis: 'estimate — counterfactual = whole-repo tokens; input tokens only',
|
|
22897
23444
|
};
|
|
22898
23445
|
|
|
22899
23446
|
if (args.includes('--json')) {
|
|
22900
23447
|
process.stdout.write(JSON.stringify(out) + '\n');
|
|
22901
23448
|
} else {
|
|
22902
|
-
console.log(`\n Cost estimate (${model}):`);
|
|
22903
|
-
console.log(` Without SigMap : ${rawTok.toLocaleString()} tok $${out.costRaw}/query`);
|
|
23449
|
+
console.log(`\n Cost estimate (${__priceCost.model} @ $${__priceCost.perMtok}/Mtok input, est.):`);
|
|
23450
|
+
console.log(` Without SigMap : ${rawTok.toLocaleString()} tok $${out.costRaw}/query (counterfactual: whole repo)`);
|
|
22904
23451
|
console.log(` With SigMap : ${outTok.toLocaleString()} tok $${out.costContext}/query`);
|
|
22905
23452
|
console.log(` Savings : ${savings}% ($${(costRaw - costCtx).toFixed(4)} saved per query)\n`);
|
|
22906
23453
|
}
|