sigmap 6.14.0 → 7.0.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 +798 -401
- package/CHANGELOG.md +52 -0
- package/README.md +11 -11
- package/gen-context.js +3432 -1197
- package/llms-full.txt +295 -0
- package/llms.txt +56 -0
- package/package.json +31 -13
- package/packages/adapters/claude.js +0 -6
- package/packages/adapters/codex.js +1 -61
- package/packages/adapters/copilot.js +0 -8
- package/packages/adapters/cursor.js +0 -4
- package/packages/adapters/gemini.js +0 -2
- package/packages/adapters/openai.js +0 -2
- package/packages/adapters/windsurf.js +0 -4
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/extractors/prdiff.js +28 -3
- package/src/format/usage-guidance.js +28 -0
- package/src/format/verify-report.js +164 -0
- package/src/mcp/handlers.js +44 -1
- package/src/mcp/server.js +4 -3
- package/src/mcp/tools.js +20 -2
- package/src/nudge.js +97 -0
- package/src/session/notes.js +99 -0
- package/src/squeeze/cilog.js +71 -0
- package/src/squeeze/classify.js +115 -0
- package/src/squeeze/index.js +69 -0
- package/src/squeeze/jsonpayload.js +54 -0
- package/src/squeeze/stacktrace.js +135 -0
- package/src/verify/closest-match.js +145 -0
- package/src/verify/hallucination-guard.js +109 -18
- package/src/verify/parsers.js +51 -0
package/gen-context.js
CHANGED
|
@@ -3164,55 +3164,78 @@ __factories["./src/extractors/coverage"] = function(module, exports) {
|
|
|
3164
3164
|
|
|
3165
3165
|
// ── ./src/extractors/prdiff ──
|
|
3166
3166
|
__factories["./src/extractors/prdiff"] = function(module, exports) {
|
|
3167
|
+
'use strict';
|
|
3167
3168
|
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
const
|
|
3184
|
-
const
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
return m;
|
|
3192
|
-
};
|
|
3193
|
-
|
|
3194
|
-
const aBy = byName(added);
|
|
3195
|
-
const rBy = byName(removed);
|
|
3196
|
-
const modified = [];
|
|
3169
|
+
/**
|
|
3170
|
+
* Compare signature arrays and produce compact diff markers.
|
|
3171
|
+
* @param {string[]} baseSigs
|
|
3172
|
+
* @param {string[]} currentSigs
|
|
3173
|
+
* @returns {{added:string[], removed:string[], modified:string[]}}
|
|
3174
|
+
*/
|
|
3175
|
+
function diffSignatures(baseSigs, currentSigs) {
|
|
3176
|
+
const base = new Set(baseSigs || []);
|
|
3177
|
+
const curr = new Set(currentSigs || []);
|
|
3178
|
+
|
|
3179
|
+
const added = [...curr].filter((s) => !base.has(s));
|
|
3180
|
+
const removed = [...base].filter((s) => !curr.has(s));
|
|
3181
|
+
|
|
3182
|
+
const byName = (arr) => {
|
|
3183
|
+
const m = new Map();
|
|
3184
|
+
for (const s of arr) {
|
|
3185
|
+
const n = extractName(s);
|
|
3186
|
+
if (!n) continue;
|
|
3187
|
+
if (!m.has(n)) m.set(n, []);
|
|
3188
|
+
m.get(n).push(s);
|
|
3189
|
+
}
|
|
3190
|
+
return m;
|
|
3191
|
+
};
|
|
3197
3192
|
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3193
|
+
const aBy = byName(added);
|
|
3194
|
+
const rBy = byName(removed);
|
|
3195
|
+
const modified = [];
|
|
3201
3196
|
|
|
3202
|
-
|
|
3197
|
+
for (const [name] of aBy) {
|
|
3198
|
+
if (rBy.has(name)) modified.push(name);
|
|
3203
3199
|
}
|
|
3204
3200
|
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3201
|
+
return { added, removed, modified };
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
/**
|
|
3205
|
+
* Extract the declared symbol name from a signature line.
|
|
3206
|
+
*
|
|
3207
|
+
* Robust to the real forms SigMap emits — `export class X`, `const x = () =>`,
|
|
3208
|
+
* `async function x`, members, a trailing `:start-end` anchor, and `→ return`
|
|
3209
|
+
* suffixes. Anchored so it never returns a mid-string fragment (the old regex
|
|
3210
|
+
* could turn a signature into a 2-char name like `is`), and returns '' for
|
|
3211
|
+
* non-symbol lines (`module.exports = {…}`, markdown headers) instead of guessing.
|
|
3212
|
+
*/
|
|
3213
|
+
function extractName(sig) {
|
|
3214
|
+
if (!sig) return '';
|
|
3215
|
+
// Drop a trailing `:start-end` (or `:line`) line anchor.
|
|
3216
|
+
let t = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '').trim();
|
|
3217
|
+
// Re-export / barrel lines carry no single declared name.
|
|
3218
|
+
if (/^(?:module\.)?exports\b/.test(t) || /^export\s*\{/.test(t) || /^export\s+\*/.test(t)) return '';
|
|
3219
|
+
// Strip leading modifiers so the keyword/name is at the start.
|
|
3220
|
+
t = t.replace(/^export\s+/, '')
|
|
3221
|
+
.replace(/^default\s+/, '')
|
|
3222
|
+
.replace(/^(?:public|private|protected|static|abstract|final|override|readonly)\s+/g, '')
|
|
3223
|
+
.replace(/^async\s+/, '');
|
|
3224
|
+
let m;
|
|
3225
|
+
// Declared forms: function/def/func/fn/class/interface/trait/struct/enum/record/type <name>
|
|
3226
|
+
if ((m = t.match(/^(?:def|function|func|fn|class|interface|trait|struct|enum|record|type)\s+([A-Za-z_$][\w$]*)/))) return m[1];
|
|
3227
|
+
// const/let/var/val <name> = … (arrow functions, assigned values)
|
|
3228
|
+
if ((m = t.match(/^(?:const|let|var|val)\s+([A-Za-z_$][\w$]*)/))) return m[1];
|
|
3229
|
+
// Call / method form: <name>(…)
|
|
3230
|
+
if ((m = t.match(/^([A-Za-z_$][\w$]*)\s*\(/))) return m[1];
|
|
3231
|
+
// Lone identifier (e.g. a collapsed `symbol` after the anchor was stripped).
|
|
3232
|
+
if ((m = t.match(/^([A-Za-z_$][\w$]*)$/))) return m[1];
|
|
3233
|
+
return '';
|
|
3234
|
+
}
|
|
3211
3235
|
|
|
3212
|
-
|
|
3236
|
+
module.exports = { diffSignatures, extractName };
|
|
3213
3237
|
|
|
3214
3238
|
};
|
|
3215
|
-
|
|
3216
3239
|
// ── ./src/extractors/r ──
|
|
3217
3240
|
__factories["./src/extractors/r"] = function(module, exports) {
|
|
3218
3241
|
|
|
@@ -5506,487 +5529,570 @@ __factories["./src/graph/impact"] = function(module, exports) {
|
|
|
5506
5529
|
|
|
5507
5530
|
// ── ./src/mcp/handlers ──
|
|
5508
5531
|
__factories["./src/mcp/handlers"] = function(module, exports) {
|
|
5509
|
-
|
|
5510
|
-
const fs = require('fs');
|
|
5511
|
-
const path = require('path');
|
|
5512
|
-
const { execSync } = require('child_process');
|
|
5513
|
-
|
|
5514
|
-
const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
|
|
5515
|
-
|
|
5516
|
-
// Section header keywords in PROJECT_MAP.md
|
|
5517
|
-
const MAP_SECTIONS = {
|
|
5518
|
-
imports: '### Import graph',
|
|
5519
|
-
classes: '### Class hierarchy',
|
|
5520
|
-
routes: '### Route table',
|
|
5521
|
-
};
|
|
5522
|
-
|
|
5523
|
-
/**
|
|
5524
|
-
* read_context({ module? }) → string
|
|
5525
|
-
*
|
|
5526
|
-
* Returns the full context file, or just the sections whose file paths
|
|
5527
|
-
* contain the given module substring.
|
|
5528
|
-
*/
|
|
5529
|
-
function readContext(args, cwd) {
|
|
5530
|
-
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
5531
|
-
if (!fs.existsSync(contextPath)) {
|
|
5532
|
-
return 'No context file found. Run: node gen-context.js';
|
|
5533
|
-
}
|
|
5534
|
-
|
|
5535
|
-
const content = fs.readFileSync(contextPath, 'utf8');
|
|
5536
|
-
|
|
5537
|
-
if (!args || !args.module) return content;
|
|
5538
|
-
|
|
5539
|
-
const mod = args.module.replace(/\\/g, '/').replace(/\/$/, '');
|
|
5540
|
-
const lines = content.split('\n');
|
|
5541
|
-
const result = [];
|
|
5542
|
-
let capturing = false;
|
|
5543
|
-
|
|
5544
|
-
for (const line of lines) {
|
|
5545
|
-
if (line.startsWith('### ')) {
|
|
5546
|
-
const filePath = line.slice(4).trim().replace(/\\/g, '/');
|
|
5547
|
-
// Match if file path starts with mod or contains /mod/ or /mod
|
|
5548
|
-
capturing =
|
|
5549
|
-
filePath === mod ||
|
|
5550
|
-
filePath.startsWith(mod + '/') ||
|
|
5551
|
-
filePath.includes('/' + mod + '/') ||
|
|
5552
|
-
filePath.includes('/' + mod);
|
|
5553
|
-
if (capturing) result.push(line);
|
|
5554
|
-
continue;
|
|
5555
|
-
}
|
|
5556
|
-
if (capturing) result.push(line);
|
|
5557
|
-
}
|
|
5558
|
-
|
|
5559
|
-
if (result.length === 0) return `No signatures found for module: ${mod}`;
|
|
5560
|
-
return result.join('\n');
|
|
5561
|
-
}
|
|
5562
|
-
|
|
5563
|
-
/**
|
|
5564
|
-
* search_signatures({ query }) → string
|
|
5565
|
-
*
|
|
5566
|
-
* Case-insensitive search through all signature lines.
|
|
5567
|
-
* Returns matching lines grouped by file path.
|
|
5568
|
-
*/
|
|
5569
|
-
function searchSignatures(args, cwd) {
|
|
5570
|
-
if (!args || !args.query) return 'Missing required argument: query';
|
|
5571
|
-
const query = args.query.toLowerCase();
|
|
5532
|
+
'use strict';
|
|
5572
5533
|
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
if (index.size === 0) {
|
|
5577
|
-
return 'No context file found. Run: node gen-context.js';
|
|
5578
|
-
}
|
|
5534
|
+
const fs = require('fs');
|
|
5535
|
+
const path = require('path');
|
|
5536
|
+
const { execSync } = require('child_process');
|
|
5579
5537
|
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
const hits = sigs.filter((s) => s.toLowerCase().includes(query));
|
|
5583
|
-
if (hits.length === 0) continue;
|
|
5584
|
-
if (result.length > 0) result.push('');
|
|
5585
|
-
result.push(`### ${file}`);
|
|
5586
|
-
result.push(...hits);
|
|
5587
|
-
}
|
|
5538
|
+
const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
|
|
5539
|
+
const CONTEXT_COLD_FILE = path.join('.github', 'context-cold.md');
|
|
5588
5540
|
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5541
|
+
function _readContextFiles(cwd) {
|
|
5542
|
+
const paths = [path.join(cwd, CONTEXT_FILE), path.join(cwd, CONTEXT_COLD_FILE)];
|
|
5543
|
+
const chunks = [];
|
|
5544
|
+
for (const p of paths) {
|
|
5545
|
+
if (fs.existsSync(p)) chunks.push(fs.readFileSync(p, 'utf8'));
|
|
5594
5546
|
}
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5547
|
+
return chunks.join('\n');
|
|
5548
|
+
}
|
|
5549
|
+
|
|
5550
|
+
// Section header keywords in PROJECT_MAP.md
|
|
5551
|
+
const MAP_SECTIONS = {
|
|
5552
|
+
imports: '### Import graph',
|
|
5553
|
+
classes: '### Class hierarchy',
|
|
5554
|
+
routes: '### Route table',
|
|
5555
|
+
};
|
|
5556
|
+
|
|
5557
|
+
/**
|
|
5558
|
+
* read_context({ module? }) → string
|
|
5559
|
+
*
|
|
5560
|
+
* Returns the full context file, or just the sections whose file paths
|
|
5561
|
+
* contain the given module substring.
|
|
5562
|
+
*/
|
|
5563
|
+
function readContext(args, cwd) {
|
|
5564
|
+
const content = _readContextFiles(cwd);
|
|
5565
|
+
if (!content) {
|
|
5566
|
+
return 'No context file found. Run: node gen-context.js';
|
|
5567
|
+
}
|
|
5568
|
+
|
|
5569
|
+
if (!args || !args.module) return content;
|
|
5570
|
+
|
|
5571
|
+
const mod = args.module.replace(/\\/g, '/').replace(/\/$/, '');
|
|
5572
|
+
const lines = content.split('\n');
|
|
5573
|
+
const result = [];
|
|
5574
|
+
let capturing = false;
|
|
5575
|
+
|
|
5576
|
+
for (const line of lines) {
|
|
5577
|
+
if (line.startsWith('### ')) {
|
|
5578
|
+
const filePath = line.slice(4).trim().replace(/\\/g, '/');
|
|
5579
|
+
// Match if file path starts with mod or contains /mod/ or /mod
|
|
5580
|
+
capturing =
|
|
5581
|
+
filePath === mod ||
|
|
5582
|
+
filePath.startsWith(mod + '/') ||
|
|
5583
|
+
filePath.includes('/' + mod + '/') ||
|
|
5584
|
+
filePath.includes('/' + mod);
|
|
5585
|
+
if (capturing) result.push(line);
|
|
5586
|
+
continue;
|
|
5619
5587
|
}
|
|
5620
|
-
|
|
5621
|
-
// Extract from this header to the next ### header
|
|
5622
|
-
const after = content.slice(idx);
|
|
5623
|
-
const nextMatch = after.slice(header.length).search(/\n###\s/);
|
|
5624
|
-
return nextMatch === -1 ? after : after.slice(0, header.length + nextMatch);
|
|
5588
|
+
if (capturing) result.push(line);
|
|
5625
5589
|
}
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
lines.push('');
|
|
5647
|
-
|
|
5648
|
-
// ── Git info ────────────────────────────────────────────────────────────
|
|
5649
|
-
lines.push('## Git state');
|
|
5650
|
-
try {
|
|
5651
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
5652
|
-
cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
5653
|
-
}).trim();
|
|
5654
|
-
lines.push(`**Branch:** ${branch}`);
|
|
5655
|
-
} catch (_) {
|
|
5656
|
-
lines.push('**Branch:** (not a git repo)');
|
|
5657
|
-
}
|
|
5658
|
-
|
|
5659
|
-
try {
|
|
5660
|
-
const log = execSync(
|
|
5661
|
-
'git log --oneline -5 --no-decorate 2>/dev/null',
|
|
5662
|
-
{ cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
5663
|
-
).trim();
|
|
5664
|
-
if (log) {
|
|
5665
|
-
lines.push('');
|
|
5666
|
-
lines.push('**Recent commits:**');
|
|
5667
|
-
for (const l of log.split('\n')) lines.push(`- ${l}`);
|
|
5668
|
-
}
|
|
5669
|
-
} catch (_) {} // ignore — not every project uses git
|
|
5670
|
-
lines.push('');
|
|
5671
|
-
|
|
5672
|
-
// ── Context stats ────────────────────────────────────────────────────────
|
|
5673
|
-
lines.push('## Context snapshot');
|
|
5674
|
-
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
5675
|
-
if (fs.existsSync(contextPath)) {
|
|
5676
|
-
const content = fs.readFileSync(contextPath, 'utf8');
|
|
5677
|
-
const tokens = Math.ceil(content.length / 4);
|
|
5678
|
-
|
|
5679
|
-
// Count modules (### headers are file paths)
|
|
5680
|
-
const modules = content.split('\n').filter((l) => l.startsWith('### ')).map((l) => l.slice(4).trim());
|
|
5681
|
-
lines.push(`**Token count:** ~${tokens}`);
|
|
5682
|
-
lines.push(`**Modules in context:** ${modules.length}`);
|
|
5683
|
-
|
|
5684
|
-
if (modules.length > 0) {
|
|
5685
|
-
lines.push('');
|
|
5686
|
-
lines.push('**Modules:**');
|
|
5687
|
-
for (const m of modules.slice(0, 20)) lines.push(`- ${m}`);
|
|
5688
|
-
if (modules.length > 20) lines.push(`- … and ${modules.length - 20} more`);
|
|
5689
|
-
}
|
|
5690
|
-
} else {
|
|
5691
|
-
lines.push('_No context file found. Run: node gen-context.js_');
|
|
5590
|
+
|
|
5591
|
+
if (result.length === 0) return `No signatures found for module: ${mod}`;
|
|
5592
|
+
return result.join('\n');
|
|
5593
|
+
}
|
|
5594
|
+
|
|
5595
|
+
/**
|
|
5596
|
+
* search_signatures({ query }) → string
|
|
5597
|
+
*
|
|
5598
|
+
* Case-insensitive search through all signature lines.
|
|
5599
|
+
* Returns matching lines grouped by file path.
|
|
5600
|
+
*/
|
|
5601
|
+
function searchSignatures(args, cwd) {
|
|
5602
|
+
if (!args || !args.query) return 'Missing required argument: query';
|
|
5603
|
+
|
|
5604
|
+
const query = args.query.toLowerCase();
|
|
5605
|
+
try {
|
|
5606
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
5607
|
+
const index = buildSigIndex(cwd);
|
|
5608
|
+
if (index.size === 0) {
|
|
5609
|
+
return 'No context file found. Run: node gen-context.js';
|
|
5692
5610
|
}
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
lines.push('## Routes');
|
|
5702
|
-
lines.push(`**Total routes detected:** ${routeLines.length}`);
|
|
5703
|
-
lines.push('');
|
|
5704
|
-
for (const r of routeLines.slice(0, 10)) lines.push(r);
|
|
5705
|
-
if (routeLines.length > 10) lines.push(`| … | +${routeLines.length - 10} more | |`);
|
|
5706
|
-
lines.push('');
|
|
5707
|
-
}
|
|
5611
|
+
|
|
5612
|
+
const result = [];
|
|
5613
|
+
for (const [file, sigs] of index.entries()) {
|
|
5614
|
+
const hits = sigs.filter((s) => s.toLowerCase().includes(query));
|
|
5615
|
+
if (hits.length === 0) continue;
|
|
5616
|
+
if (result.length > 0) result.push('');
|
|
5617
|
+
result.push(`### ${file}`);
|
|
5618
|
+
result.push(...hits);
|
|
5708
5619
|
}
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
return
|
|
5620
|
+
|
|
5621
|
+
if (result.length === 0) return `No signatures found matching: ${args.query}`;
|
|
5622
|
+
return result.join('\n');
|
|
5623
|
+
} catch (err) {
|
|
5624
|
+
return `_search_signatures failed: ${err.message}_`;
|
|
5714
5625
|
}
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
'- **fast** (haiku/gpt-4o-mini) — config, markup, trivial utilities\n' +
|
|
5730
|
-
'- **balanced** (sonnet/gpt-4o) — standard application code\n' +
|
|
5731
|
-
'- **powerful** (opus/gpt-4-turbo) — complex, security-critical, or large modules'
|
|
5732
|
-
);
|
|
5733
|
-
}
|
|
5734
|
-
|
|
5735
|
-
// Parse file list from context (### headings are file paths)
|
|
5736
|
-
const content = fs.readFileSync(contextPath, 'utf8');
|
|
5737
|
-
const fileRels = content.split('\n')
|
|
5738
|
-
.filter((l) => l.startsWith('### '))
|
|
5739
|
-
.map((l) => l.slice(4).trim());
|
|
5740
|
-
|
|
5741
|
-
// Build synthetic fileEntries for the classifier
|
|
5742
|
-
// We don't have live sig arrays here, so rebuild from the context blocks
|
|
5743
|
-
const entries = [];
|
|
5744
|
-
const blocks = content.split(/^### /m).slice(1); // slice past the header
|
|
5745
|
-
for (const block of blocks) {
|
|
5746
|
-
const firstLine = block.split('\n')[0].trim();
|
|
5747
|
-
const codeBlock = block.match(/```\n([\s\S]*?)```/);
|
|
5748
|
-
const sigs = codeBlock ? codeBlock[1].trim().split('\n').filter(Boolean) : [];
|
|
5749
|
-
entries.push({ filePath: path.join(cwd, firstLine), sigs });
|
|
5750
|
-
}
|
|
5751
|
-
|
|
5752
|
-
try {
|
|
5753
|
-
const { classifyAll } = __require('./src/routing/classifier');
|
|
5754
|
-
const { formatRoutingSection } = __require('./src/routing/hints');
|
|
5755
|
-
const groups = classifyAll(entries, cwd);
|
|
5756
|
-
return formatRoutingSection(groups);
|
|
5757
|
-
} catch (err) {
|
|
5758
|
-
return `_Routing classification failed: ${err.message}_`;
|
|
5759
|
-
}
|
|
5626
|
+
}
|
|
5627
|
+
|
|
5628
|
+
/**
|
|
5629
|
+
* get_map({ type }) → string
|
|
5630
|
+
*
|
|
5631
|
+
* Returns a section from PROJECT_MAP.md.
|
|
5632
|
+
* type: 'imports' | 'classes' | 'routes'
|
|
5633
|
+
*/
|
|
5634
|
+
function getMap(args, cwd) {
|
|
5635
|
+
if (!args || !args.type) return 'Missing required argument: type';
|
|
5636
|
+
|
|
5637
|
+
const header = MAP_SECTIONS[args.type];
|
|
5638
|
+
if (!header) {
|
|
5639
|
+
return `Unknown map type: "${args.type}". Use: imports, classes, routes`;
|
|
5760
5640
|
}
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
const targetRel = args.path.replace(/\\/g, '/').replace(/^\//, '');
|
|
5766
|
-
const targetAbs = path.resolve(cwd, targetRel);
|
|
5767
|
-
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
5768
|
-
|
|
5769
|
-
const lines = ['# explain_file: ' + targetRel, ''];
|
|
5770
|
-
|
|
5771
|
-
lines.push('## Signatures');
|
|
5772
|
-
let indexedFiles = [];
|
|
5773
|
-
|
|
5774
|
-
if (fs.existsSync(contextPath)) {
|
|
5775
|
-
const ctxContent = fs.readFileSync(contextPath, 'utf8');
|
|
5776
|
-
const ctxLines = ctxContent.split('\n');
|
|
5777
|
-
let capturing = false;
|
|
5778
|
-
const sigLines = [];
|
|
5779
|
-
|
|
5780
|
-
for (const line of ctxLines) {
|
|
5781
|
-
if (line.startsWith('### ')) {
|
|
5782
|
-
if (capturing) break;
|
|
5783
|
-
const rel = line.slice(4).trim().replace(/\\/g, '/');
|
|
5784
|
-
capturing = rel === targetRel || rel.endsWith('/' + targetRel) || targetRel.endsWith('/' + rel);
|
|
5785
|
-
if (capturing) continue;
|
|
5786
|
-
} else if (capturing) {
|
|
5787
|
-
sigLines.push(line);
|
|
5788
|
-
}
|
|
5789
|
-
}
|
|
5790
|
-
|
|
5791
|
-
const sigs = sigLines.filter((l) => l !== '```' && l.trim() !== '');
|
|
5792
|
-
if (sigs.length > 0) {
|
|
5793
|
-
lines.push(...sigs);
|
|
5794
|
-
} else {
|
|
5795
|
-
lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
|
|
5796
|
-
}
|
|
5797
|
-
|
|
5798
|
-
indexedFiles = ctxContent
|
|
5799
|
-
.split('\n')
|
|
5800
|
-
.filter((l) => l.startsWith('### '))
|
|
5801
|
-
.map((l) => path.resolve(cwd, l.slice(4).trim()));
|
|
5802
|
-
} else {
|
|
5803
|
-
lines.push('_No context file found. Run: node gen-context.js_');
|
|
5804
|
-
}
|
|
5805
|
-
|
|
5806
|
-
if (!fs.existsSync(targetAbs)) {
|
|
5807
|
-
lines.push('');
|
|
5808
|
-
lines.push('> File not found on disk: ' + targetRel);
|
|
5809
|
-
return lines.join('\n');
|
|
5810
|
-
}
|
|
5811
|
-
|
|
5812
|
-
lines.push('');
|
|
5813
|
-
|
|
5814
|
-
lines.push('## Imports (direct dependencies)');
|
|
5815
|
-
try {
|
|
5816
|
-
const { extractImports } = __require('./src/map/import-graph');
|
|
5817
|
-
const fileContent = fs.readFileSync(targetAbs, 'utf8');
|
|
5818
|
-
const fileSet = new Set(indexedFiles);
|
|
5819
|
-
fileSet.add(targetAbs);
|
|
5820
|
-
const imports = extractImports(targetAbs, fileContent, fileSet);
|
|
5821
|
-
if (imports.length > 0) {
|
|
5822
|
-
for (const imp of imports) lines.push('- ' + path.relative(cwd, imp).replace(/\\/g, '/'));
|
|
5823
|
-
} else {
|
|
5824
|
-
lines.push('_No resolvable relative imports found._');
|
|
5825
|
-
}
|
|
5826
|
-
} catch (err) {
|
|
5827
|
-
lines.push('_Could not analyze imports: ' + err.message + '_');
|
|
5828
|
-
}
|
|
5829
|
-
|
|
5830
|
-
lines.push('');
|
|
5831
|
-
|
|
5832
|
-
lines.push('## Callers (files that import this file)');
|
|
5833
|
-
try {
|
|
5834
|
-
const { extractImports } = __require('./src/map/import-graph');
|
|
5835
|
-
const fileSet = new Set(indexedFiles);
|
|
5836
|
-
fileSet.add(targetAbs);
|
|
5837
|
-
const callers = [];
|
|
5838
|
-
for (const f of indexedFiles) {
|
|
5839
|
-
if (f === targetAbs || !fs.existsSync(f)) continue;
|
|
5840
|
-
try {
|
|
5841
|
-
const fc = fs.readFileSync(f, 'utf8');
|
|
5842
|
-
const imps = extractImports(f, fc, fileSet);
|
|
5843
|
-
if (imps.includes(targetAbs)) callers.push(path.relative(cwd, f).replace(/\\/g, '/'));
|
|
5844
|
-
} catch (_) {}
|
|
5845
|
-
}
|
|
5846
|
-
if (callers.length > 0) {
|
|
5847
|
-
for (const c of callers) lines.push('- ' + c);
|
|
5848
|
-
} else {
|
|
5849
|
-
lines.push('_No indexed files import this file._');
|
|
5850
|
-
}
|
|
5851
|
-
} catch (err) {
|
|
5852
|
-
lines.push('_Could not analyze callers: ' + err.message + '_');
|
|
5853
|
-
}
|
|
5854
|
-
|
|
5855
|
-
return lines.join('\n');
|
|
5641
|
+
|
|
5642
|
+
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
|
5643
|
+
if (!fs.existsSync(mapPath)) {
|
|
5644
|
+
return 'PROJECT_MAP.md not found. Run: node gen-project-map.js';
|
|
5856
5645
|
}
|
|
5857
|
-
|
|
5858
|
-
function listModules(args, cwd) {
|
|
5859
|
-
try {
|
|
5860
|
-
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
5861
|
-
const index = buildSigIndex(cwd);
|
|
5862
|
-
if (index.size === 0) {
|
|
5863
|
-
return 'No context file found. Run: node gen-context.js';
|
|
5864
|
-
}
|
|
5865
5646
|
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
groups[mod].fileCount++;
|
|
5872
|
-
groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
|
|
5873
|
-
}
|
|
5647
|
+
const content = fs.readFileSync(mapPath, 'utf8');
|
|
5648
|
+
const idx = content.indexOf(header);
|
|
5649
|
+
if (idx === -1) {
|
|
5650
|
+
return `Section "${header}" not found in PROJECT_MAP.md`;
|
|
5651
|
+
}
|
|
5874
5652
|
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5653
|
+
// Extract from this header to the next ### header
|
|
5654
|
+
const after = content.slice(idx);
|
|
5655
|
+
const nextMatch = after.slice(header.length).search(/\n###\s/);
|
|
5656
|
+
return nextMatch === -1 ? after : after.slice(0, header.length + nextMatch);
|
|
5657
|
+
}
|
|
5878
5658
|
|
|
5879
|
-
|
|
5659
|
+
/**
|
|
5660
|
+
* create_checkpoint({ note? }) → string
|
|
5661
|
+
*
|
|
5662
|
+
* Returns a markdown checkpoint summarising current project state:
|
|
5663
|
+
* - Timestamp and optional user note
|
|
5664
|
+
* - Active git branch + last 5 commit messages
|
|
5665
|
+
* - Token count of current context file
|
|
5666
|
+
* - List of modules present in the context
|
|
5667
|
+
* - Route count (if PROJECT_MAP.md exists)
|
|
5668
|
+
*/
|
|
5669
|
+
function createCheckpoint(args, cwd) {
|
|
5670
|
+
const note = (args && args.note) ? args.note.trim() : '';
|
|
5671
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
|
5672
|
+
const lines = [
|
|
5673
|
+
'# SigMap Checkpoint',
|
|
5674
|
+
`**Created:** ${now}`,
|
|
5675
|
+
];
|
|
5880
5676
|
|
|
5881
|
-
|
|
5677
|
+
if (note) lines.push(`**Note:** ${note}`);
|
|
5678
|
+
lines.push('');
|
|
5882
5679
|
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
'_Use `read_context({ module: "name" })` to get signatures for a specific module._',
|
|
5893
|
-
].join('\n');
|
|
5894
|
-
} catch (err) {
|
|
5895
|
-
return `_list_modules failed: ${err.message}_`;
|
|
5896
|
-
}
|
|
5897
|
-
}
|
|
5898
|
-
|
|
5899
|
-
function queryContext(args, cwd) {
|
|
5900
|
-
if (!args || !args.query) return 'Missing required argument: query';
|
|
5901
|
-
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
5902
|
-
if (!fs.existsSync(contextPath)) return 'No context file found. Run: node gen-context.js';
|
|
5903
|
-
try {
|
|
5904
|
-
const { rank, buildSigIndex, formatRankTable } = __require('./src/retrieval/ranker');
|
|
5905
|
-
const index = buildSigIndex(cwd);
|
|
5906
|
-
if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';
|
|
5907
|
-
const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
|
|
5908
|
-
const results = rank(args.query, index, { topK, cwd });
|
|
5909
|
-
return formatRankTable(results, args.query);
|
|
5910
|
-
} catch (err) {
|
|
5911
|
-
return `_query_context failed: ${err.message}_`;
|
|
5912
|
-
}
|
|
5680
|
+
// ── Git info ────────────────────────────────────────────────────────────
|
|
5681
|
+
lines.push('## Git state');
|
|
5682
|
+
try {
|
|
5683
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
5684
|
+
cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
5685
|
+
}).trim();
|
|
5686
|
+
lines.push(`**Branch:** ${branch}`);
|
|
5687
|
+
} catch (_) {
|
|
5688
|
+
lines.push('**Branch:** (not a git repo)');
|
|
5913
5689
|
}
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5690
|
+
|
|
5691
|
+
try {
|
|
5692
|
+
const log = execSync(
|
|
5693
|
+
'git log --oneline -5 --no-decorate 2>/dev/null',
|
|
5694
|
+
{ cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
5695
|
+
).trim();
|
|
5696
|
+
if (log) {
|
|
5697
|
+
lines.push('');
|
|
5698
|
+
lines.push('**Recent commits:**');
|
|
5699
|
+
for (const l of log.split('\n')) lines.push(`- ${l}`);
|
|
5924
5700
|
}
|
|
5925
|
-
}
|
|
5701
|
+
} catch (_) {} // ignore — not every project uses git
|
|
5702
|
+
lines.push('');
|
|
5926
5703
|
|
|
5927
|
-
|
|
5928
|
-
|
|
5704
|
+
// ── Context stats ────────────────────────────────────────────────────────
|
|
5705
|
+
lines.push('## Context snapshot');
|
|
5706
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
5707
|
+
if (fs.existsSync(contextPath)) {
|
|
5708
|
+
const content = fs.readFileSync(contextPath, 'utf8');
|
|
5709
|
+
const tokens = Math.ceil(content.length / 4);
|
|
5929
5710
|
|
|
5930
|
-
|
|
5931
|
-
const
|
|
5711
|
+
// Count modules (### headers are file paths)
|
|
5712
|
+
const modules = content.split('\n').filter((l) => l.startsWith('### ')).map((l) => l.slice(4).trim());
|
|
5713
|
+
lines.push(`**Token count:** ~${tokens}`);
|
|
5714
|
+
lines.push(`**Modules in context:** ${modules.length}`);
|
|
5932
5715
|
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5716
|
+
if (modules.length > 0) {
|
|
5717
|
+
lines.push('');
|
|
5718
|
+
lines.push('**Modules:**');
|
|
5719
|
+
for (const m of modules.slice(0, 20)) lines.push(`- ${m}`);
|
|
5720
|
+
if (modules.length > 20) lines.push(`- … and ${modules.length - 20} more`);
|
|
5937
5721
|
}
|
|
5938
|
-
|
|
5939
|
-
|
|
5722
|
+
} else {
|
|
5723
|
+
lines.push('_No context file found. Run: node gen-context.js_');
|
|
5724
|
+
}
|
|
5725
|
+
lines.push('');
|
|
5726
|
+
|
|
5727
|
+
// ── Route summary ────────────────────────────────────────────────────────
|
|
5728
|
+
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
|
5729
|
+
if (fs.existsSync(mapPath)) {
|
|
5730
|
+
const mapContent = fs.readFileSync(mapPath, 'utf8');
|
|
5731
|
+
const routeLines = mapContent.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---'));
|
|
5732
|
+
if (routeLines.length > 0) {
|
|
5733
|
+
lines.push('## Routes');
|
|
5734
|
+
lines.push(`**Total routes detected:** ${routeLines.length}`);
|
|
5735
|
+
lines.push('');
|
|
5736
|
+
for (const r of routeLines.slice(0, 10)) lines.push(r);
|
|
5737
|
+
if (routeLines.length > 10) lines.push(`| … | +${routeLines.length - 10} more | |`);
|
|
5738
|
+
lines.push('');
|
|
5940
5739
|
}
|
|
5740
|
+
}
|
|
5941
5741
|
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
5945
|
-
return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
|
|
5946
|
-
}
|
|
5742
|
+
lines.push('---');
|
|
5743
|
+
lines.push('_Generated by SigMap `create_checkpoint`_');
|
|
5947
5744
|
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
lines = fs.readFileSync(abs, 'utf8').split('\n');
|
|
5951
|
-
} catch (err) {
|
|
5952
|
-
return `Could not read ${rel}: ${err.message}`;
|
|
5953
|
-
}
|
|
5745
|
+
return lines.join('\n');
|
|
5746
|
+
}
|
|
5954
5747
|
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5748
|
+
/**
|
|
5749
|
+
* get_routing({}) → string
|
|
5750
|
+
*
|
|
5751
|
+
* Reads the current context file, classifies all indexed files by complexity,
|
|
5752
|
+
* and returns a formatted markdown routing guide showing which files belong
|
|
5753
|
+
* to the fast/balanced/powerful model tier.
|
|
5754
|
+
*/
|
|
5755
|
+
function getRouting(args, cwd) {
|
|
5756
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
5757
|
+
if (!fs.existsSync(contextPath)) {
|
|
5758
|
+
return (
|
|
5759
|
+
'_No context file found. Run `node gen-context.js --routing` first._\n\n' +
|
|
5760
|
+
'This generates routing hints that map each file to a model tier:\n' +
|
|
5761
|
+
'- **fast** (haiku/gpt-4o-mini) — config, markup, trivial utilities\n' +
|
|
5762
|
+
'- **balanced** (sonnet/gpt-4o) — standard application code\n' +
|
|
5763
|
+
'- **powerful** (opus/gpt-4-turbo) — complex, security-critical, or large modules'
|
|
5764
|
+
);
|
|
5765
|
+
}
|
|
5959
5766
|
|
|
5960
|
-
|
|
5767
|
+
// Parse file list from context (### headings are file paths)
|
|
5768
|
+
const content = fs.readFileSync(contextPath, 'utf8');
|
|
5769
|
+
const fileRels = content.split('\n')
|
|
5770
|
+
.filter((l) => l.startsWith('### '))
|
|
5771
|
+
.map((l) => l.slice(4).trim());
|
|
5961
5772
|
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5773
|
+
// Build synthetic fileEntries for the classifier
|
|
5774
|
+
// We don't have live sig arrays here, so rebuild from the context blocks
|
|
5775
|
+
const entries = [];
|
|
5776
|
+
const blocks = content.split(/^### /m).slice(1); // slice past the header
|
|
5777
|
+
for (const block of blocks) {
|
|
5778
|
+
const firstLine = block.split('\n')[0].trim();
|
|
5779
|
+
const codeBlock = block.match(/```\n([\s\S]*?)```/);
|
|
5780
|
+
const sigs = codeBlock ? codeBlock[1].trim().split('\n').filter(Boolean) : [];
|
|
5781
|
+
entries.push({ filePath: path.join(cwd, firstLine), sigs });
|
|
5782
|
+
}
|
|
5967
5783
|
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5784
|
+
try {
|
|
5785
|
+
const { classifyAll } = __require('./src/routing/classifier');
|
|
5786
|
+
const { formatRoutingSection } = __require('./src/routing/hints');
|
|
5787
|
+
const groups = classifyAll(entries, cwd);
|
|
5788
|
+
return formatRoutingSection(groups);
|
|
5789
|
+
} catch (err) {
|
|
5790
|
+
return `_Routing classification failed: ${err.message}_`;
|
|
5974
5791
|
}
|
|
5792
|
+
}
|
|
5975
5793
|
|
|
5976
|
-
|
|
5977
|
-
}
|
|
5794
|
+
/**
|
|
5795
|
+
* explain_file({ path }) → string
|
|
5796
|
+
*
|
|
5797
|
+
* Returns a file's signatures, its direct imports, and files that import it.
|
|
5798
|
+
* path: relative path from project root (e.g. 'src/services/auth.ts')
|
|
5799
|
+
*/
|
|
5800
|
+
function explainFile(args, cwd) {
|
|
5801
|
+
if (!args || !args.path) return 'Missing required argument: path';
|
|
5978
5802
|
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5803
|
+
const targetRel = args.path.replace(/\\/g, '/').replace(/^\//, '');
|
|
5804
|
+
const targetAbs = path.resolve(cwd, targetRel);
|
|
5805
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
5982
5806
|
|
|
5983
|
-
const
|
|
5984
|
-
const path = require('path');
|
|
5807
|
+
const lines = ['# explain_file: ' + targetRel, ''];
|
|
5985
5808
|
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5809
|
+
// ── Signatures (hot + cold + cache via buildSigIndex) ───────────────────
|
|
5810
|
+
lines.push('## Signatures');
|
|
5811
|
+
let indexedFiles = [];
|
|
5812
|
+
|
|
5813
|
+
try {
|
|
5814
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
5815
|
+
const index = buildSigIndex(cwd);
|
|
5816
|
+
let sigs = index.get(targetRel);
|
|
5817
|
+
if (!sigs) {
|
|
5818
|
+
for (const [file, fileSigs] of index.entries()) {
|
|
5819
|
+
if (file === targetRel || file.endsWith('/' + targetRel) || targetRel.endsWith('/' + file)) {
|
|
5820
|
+
sigs = fileSigs;
|
|
5821
|
+
break;
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
if (sigs && sigs.length > 0) {
|
|
5826
|
+
lines.push(...sigs);
|
|
5827
|
+
} else {
|
|
5828
|
+
lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
|
|
5829
|
+
}
|
|
5830
|
+
indexedFiles = [...index.keys()].map((rel) => path.resolve(cwd, rel));
|
|
5831
|
+
} catch (_) {
|
|
5832
|
+
lines.push('_No context file found. Run: node gen-context.js_');
|
|
5833
|
+
}
|
|
5834
|
+
|
|
5835
|
+
if (!fs.existsSync(targetAbs)) {
|
|
5836
|
+
lines.push('');
|
|
5837
|
+
lines.push('> File not found on disk: ' + targetRel);
|
|
5838
|
+
return lines.join('\n');
|
|
5839
|
+
}
|
|
5840
|
+
|
|
5841
|
+
lines.push('');
|
|
5842
|
+
|
|
5843
|
+
// ── Direct imports ────────────────────────────────────────────────────────
|
|
5844
|
+
lines.push('## Imports (direct dependencies)');
|
|
5845
|
+
try {
|
|
5846
|
+
const { extractImports } = __require('./src/map/import-graph');
|
|
5847
|
+
const fileContent = fs.readFileSync(targetAbs, 'utf8');
|
|
5848
|
+
const fileSet = new Set(indexedFiles);
|
|
5849
|
+
fileSet.add(targetAbs);
|
|
5850
|
+
const imports = extractImports(targetAbs, fileContent, fileSet);
|
|
5851
|
+
if (imports.length > 0) {
|
|
5852
|
+
for (const imp of imports) lines.push('- ' + path.relative(cwd, imp).replace(/\\/g, '/'));
|
|
5853
|
+
} else {
|
|
5854
|
+
lines.push('_No resolvable relative imports found._');
|
|
5855
|
+
}
|
|
5856
|
+
} catch (err) {
|
|
5857
|
+
lines.push('_Could not analyze imports: ' + err.message + '_');
|
|
5858
|
+
}
|
|
5859
|
+
|
|
5860
|
+
lines.push('');
|
|
5861
|
+
|
|
5862
|
+
// ── Callers (reverse-import lookup) ──────────────────────────────────────
|
|
5863
|
+
lines.push('## Callers (files that import this file)');
|
|
5864
|
+
try {
|
|
5865
|
+
const { extractImports } = __require('./src/map/import-graph');
|
|
5866
|
+
const fileSet = new Set(indexedFiles);
|
|
5867
|
+
fileSet.add(targetAbs);
|
|
5868
|
+
const callers = [];
|
|
5869
|
+
for (const f of indexedFiles) {
|
|
5870
|
+
if (f === targetAbs || !fs.existsSync(f)) continue;
|
|
5871
|
+
try {
|
|
5872
|
+
const fc = fs.readFileSync(f, 'utf8');
|
|
5873
|
+
const imps = extractImports(f, fc, fileSet);
|
|
5874
|
+
if (imps.includes(targetAbs)) callers.push(path.relative(cwd, f).replace(/\\/g, '/'));
|
|
5875
|
+
} catch (_) {}
|
|
5876
|
+
}
|
|
5877
|
+
if (callers.length > 0) {
|
|
5878
|
+
for (const c of callers) lines.push('- ' + c);
|
|
5879
|
+
} else {
|
|
5880
|
+
lines.push('_No indexed files import this file._');
|
|
5881
|
+
}
|
|
5882
|
+
} catch (err) {
|
|
5883
|
+
lines.push('_Could not analyze callers: ' + err.message + '_');
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5886
|
+
return lines.join('\n');
|
|
5887
|
+
}
|
|
5888
|
+
|
|
5889
|
+
/**
|
|
5890
|
+
* list_modules({}) → string
|
|
5891
|
+
*
|
|
5892
|
+
* Lists all srcDir modules present in the context file, sorted by token count
|
|
5893
|
+
* descending. Helps agents decide which module to query with read_context.
|
|
5894
|
+
*/
|
|
5895
|
+
function listModules(args, cwd) {
|
|
5896
|
+
try {
|
|
5897
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
5898
|
+
const index = buildSigIndex(cwd);
|
|
5899
|
+
if (index.size === 0) {
|
|
5900
|
+
return 'No context file found. Run: node gen-context.js';
|
|
5901
|
+
}
|
|
5902
|
+
|
|
5903
|
+
const groups = {};
|
|
5904
|
+
for (const [rel, sigs] of index.entries()) {
|
|
5905
|
+
const parts = rel.replace(/\\/g, '/').split('/');
|
|
5906
|
+
const mod = parts.length > 1 ? parts[0] : '.';
|
|
5907
|
+
if (!groups[mod]) groups[mod] = { fileCount: 0, tokenCount: 0 };
|
|
5908
|
+
groups[mod].fileCount++;
|
|
5909
|
+
groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
|
|
5910
|
+
}
|
|
5911
|
+
|
|
5912
|
+
const sorted = Object.entries(groups)
|
|
5913
|
+
.map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
|
|
5914
|
+
.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
5915
|
+
|
|
5916
|
+
if (sorted.length === 0) return 'No modules found in context file.';
|
|
5917
|
+
|
|
5918
|
+
const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
|
|
5919
|
+
|
|
5920
|
+
return [
|
|
5921
|
+
'# Modules',
|
|
5922
|
+
'',
|
|
5923
|
+
'| Module | Files | Tokens |',
|
|
5924
|
+
'|--------|-------|--------|',
|
|
5925
|
+
...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
|
|
5926
|
+
'',
|
|
5927
|
+
`**Total context tokens: ~${total}**`,
|
|
5928
|
+
'',
|
|
5929
|
+
'_Use `read_context({ module: "name" })` to get signatures for a specific module._',
|
|
5930
|
+
].join('\n');
|
|
5931
|
+
} catch (err) {
|
|
5932
|
+
return `_list_modules failed: ${err.message}_`;
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
|
|
5936
|
+
/**
|
|
5937
|
+
* query_context({ query, topK? }) → string
|
|
5938
|
+
*
|
|
5939
|
+
* Ranks context-file entries by relevance to the query and returns the
|
|
5940
|
+
* top-K most relevant files with their signatures and scores.
|
|
5941
|
+
*/
|
|
5942
|
+
function queryContext(args, cwd) {
|
|
5943
|
+
if (!args || !args.query) return 'Missing required argument: query';
|
|
5944
|
+
|
|
5945
|
+
try {
|
|
5946
|
+
const { rank, buildSigIndex, formatRankTable } = __require('./src/retrieval/ranker');
|
|
5947
|
+
const { buildFromCwd } = __require('./src/graph/builder');
|
|
5948
|
+
const index = buildSigIndex(cwd);
|
|
5949
|
+
if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';
|
|
5950
|
+
|
|
5951
|
+
const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
|
|
5952
|
+
// Build dependency graph for neighbor boost — non-fatal if it fails
|
|
5953
|
+
let graph = null;
|
|
5954
|
+
try { graph = buildFromCwd(cwd); } catch (_) {}
|
|
5955
|
+
const results = rank(args.query, index, { topK, cwd, graph });
|
|
5956
|
+
return formatRankTable(results, args.query);
|
|
5957
|
+
} catch (err) {
|
|
5958
|
+
return `_query_context failed: ${err.message}_`;
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
|
|
5962
|
+
/**
|
|
5963
|
+
* get_impact({ file, depth? }) → string
|
|
5964
|
+
*
|
|
5965
|
+
* Returns a formatted markdown impact report for the given file:
|
|
5966
|
+
* direct importers, transitive importers, affected tests, affected routes.
|
|
5967
|
+
*/
|
|
5968
|
+
function getImpact(args, cwd) {
|
|
5969
|
+
if (!args || !args.file) return 'Missing required argument: file';
|
|
5970
|
+
|
|
5971
|
+
try {
|
|
5972
|
+
const { analyzeImpact, formatImpact } = __require('./src/graph/impact');
|
|
5973
|
+
const depth = Math.max(0, parseInt(args.depth, 10) || 3);
|
|
5974
|
+
const results = analyzeImpact(args.file, cwd, { depth });
|
|
5975
|
+
if (results.length === 0) return `No impact data for: ${args.file}`;
|
|
5976
|
+
return results.map((r) => formatImpact(r.impact)).join('\n\n---\n\n');
|
|
5977
|
+
} catch (err) {
|
|
5978
|
+
return `_get_impact failed: ${err.message}_`;
|
|
5979
|
+
}
|
|
5980
|
+
}
|
|
5981
|
+
|
|
5982
|
+
/**
|
|
5983
|
+
* get_lines({ file, start, end }) → string
|
|
5984
|
+
*
|
|
5985
|
+
* Surgical Context demand-driven fetch: returns an exact, clamped line range from a
|
|
5986
|
+
* source file. The path is resolved inside the project root (no traversal escape) and
|
|
5987
|
+
* the returned lines are secret-scanned via the same redactor used for signatures.
|
|
5988
|
+
*/
|
|
5989
|
+
function getLines(args, cwd) {
|
|
5990
|
+
if (!args || !args.file) return 'Missing required argument: file';
|
|
5991
|
+
|
|
5992
|
+
const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
|
|
5993
|
+
const abs = path.resolve(cwd, rel);
|
|
5994
|
+
|
|
5995
|
+
// Sandbox: refuse paths that resolve outside the project root.
|
|
5996
|
+
const root = path.resolve(cwd);
|
|
5997
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
5998
|
+
return `Refused: ${rel} resolves outside the project root`;
|
|
5999
|
+
}
|
|
6000
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
6001
|
+
return `File not found: ${rel}`;
|
|
6002
|
+
}
|
|
6003
|
+
|
|
6004
|
+
const start = parseInt(args.start, 10);
|
|
6005
|
+
const end = parseInt(args.end, 10);
|
|
6006
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
6007
|
+
return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
|
|
6008
|
+
}
|
|
6009
|
+
|
|
6010
|
+
let lines;
|
|
6011
|
+
try {
|
|
6012
|
+
lines = fs.readFileSync(abs, 'utf8').split('\n');
|
|
6013
|
+
} catch (err) {
|
|
6014
|
+
return `Could not read ${rel}: ${err.message}`;
|
|
6015
|
+
}
|
|
6016
|
+
|
|
6017
|
+
const total = lines.length;
|
|
6018
|
+
const from = Math.max(1, Math.min(start, end));
|
|
6019
|
+
const to = Math.min(total, Math.max(start, end));
|
|
6020
|
+
if (from > total) return `${rel} has only ${total} lines; requested ${start}-${end}`;
|
|
6021
|
+
|
|
6022
|
+
const slice = lines.slice(from - 1, to);
|
|
6023
|
+
|
|
6024
|
+
// Redaction scan: reuse the signature secret scanner line-by-line.
|
|
6025
|
+
let safeLines = slice;
|
|
6026
|
+
try {
|
|
6027
|
+
const { scan } = __require('./src/security/scanner');
|
|
6028
|
+
safeLines = scan(slice, rel).safe;
|
|
6029
|
+
} catch (_) {} // non-fatal: fall back to raw slice
|
|
6030
|
+
|
|
6031
|
+
return [
|
|
6032
|
+
`# ${rel}:${from}-${to}`,
|
|
6033
|
+
'```',
|
|
6034
|
+
...safeLines,
|
|
6035
|
+
'```',
|
|
6036
|
+
].join('\n');
|
|
6037
|
+
}
|
|
6038
|
+
|
|
6039
|
+
/**
|
|
6040
|
+
* read_memory({ limit? }) → string
|
|
6041
|
+
*
|
|
6042
|
+
* Recall the cross-session decision log (notes) plus the last ranking-session
|
|
6043
|
+
* focus, formatted for an agent to consume at the start of a task.
|
|
6044
|
+
*/
|
|
6045
|
+
function readMemory(args, cwd) {
|
|
6046
|
+
let limit = parseInt(args && args.limit, 10);
|
|
6047
|
+
if (!Number.isFinite(limit) || limit <= 0) limit = 10;
|
|
6048
|
+
limit = Math.min(limit, 50);
|
|
6049
|
+
|
|
6050
|
+
const out = ['# SigMap memory'];
|
|
6051
|
+
|
|
6052
|
+
let notes = [];
|
|
6053
|
+
try {
|
|
6054
|
+
const { readNotes, formatNotes } = __require('./src/session/notes');
|
|
6055
|
+
notes = readNotes(cwd, limit);
|
|
6056
|
+
out.push('');
|
|
6057
|
+
out.push(`## Recent notes (${notes.length})`);
|
|
6058
|
+
// Most recent first for quick scanning.
|
|
6059
|
+
out.push(formatNotes(notes.slice().reverse()));
|
|
6060
|
+
} catch (_) {
|
|
6061
|
+
out.push('');
|
|
6062
|
+
out.push('_No notes available._');
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
// Last ranking-session focus (if any) — extends src/session/memory.js.
|
|
6066
|
+
try {
|
|
6067
|
+
const { loadSession } = __require('./src/session/memory');
|
|
6068
|
+
const s = loadSession(cwd);
|
|
6069
|
+
if (s && (s.lastQuery || (s.topFiles && s.topFiles.length))) {
|
|
6070
|
+
out.push('');
|
|
6071
|
+
out.push('## Last session');
|
|
6072
|
+
if (s.lastQuery) out.push(`**Last query:** ${s.lastQuery}`);
|
|
6073
|
+
if (s.topFiles && s.topFiles.length) {
|
|
6074
|
+
out.push(`**Focus files:** ${s.topFiles.map((f) => f.file).slice(0, 5).join(', ')}`);
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
6077
|
+
} catch (_) { /* session optional */ }
|
|
6078
|
+
|
|
6079
|
+
return out.join('\n');
|
|
6080
|
+
}
|
|
6081
|
+
|
|
6082
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory };
|
|
6083
|
+
|
|
6084
|
+
};
|
|
6085
|
+
// ── ./src/learning/weights ──
|
|
6086
|
+
__factories["./src/learning/weights"] = function(module, exports) {
|
|
6087
|
+
'use strict';
|
|
6088
|
+
|
|
6089
|
+
const fs = require('fs');
|
|
6090
|
+
const path = require('path');
|
|
6091
|
+
|
|
6092
|
+
const DECAY = 0.95;
|
|
6093
|
+
const MAX_MULT = 3.0;
|
|
6094
|
+
const MIN_MULT = 0.30;
|
|
6095
|
+
const BASELINE = 1.0;
|
|
5990
6096
|
|
|
5991
6097
|
function weightsPath(cwd) {
|
|
5992
6098
|
return path.join(cwd, '.context', 'weights.json');
|
|
@@ -6135,344 +6241,364 @@ __factories["./src/learning/weights"] = function(module, exports) {
|
|
|
6135
6241
|
|
|
6136
6242
|
// ── ./src/mcp/server ──
|
|
6137
6243
|
__factories["./src/mcp/server"] = function(module, exports) {
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6244
|
+
'use strict';
|
|
6245
|
+
|
|
6246
|
+
/**
|
|
6247
|
+
* SigMap MCP server — zero npm dependencies.
|
|
6248
|
+
*
|
|
6249
|
+
* Wire protocol: JSON-RPC 2.0 over stdio.
|
|
6250
|
+
* One JSON object per line on both stdin and stdout.
|
|
6251
|
+
*
|
|
6252
|
+
* Supported methods:
|
|
6253
|
+
* initialize → serverInfo + capabilities
|
|
6254
|
+
* tools/list → 11 tool definitions
|
|
6255
|
+
* tools/call → dispatch to handler, return result
|
|
6256
|
+
*/
|
|
6150
6257
|
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6258
|
+
const readline = require('readline');
|
|
6259
|
+
const { TOOLS } = __require('./src/mcp/tools');
|
|
6260
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory } = __require('./src/mcp/handlers');
|
|
6154
6261
|
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
version: '
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6262
|
+
const SERVER_INFO = {
|
|
6263
|
+
name: 'sigmap',
|
|
6264
|
+
version: '7.0.0',
|
|
6265
|
+
description: 'SigMap MCP server — code signatures on demand',
|
|
6266
|
+
};
|
|
6267
|
+
|
|
6268
|
+
// ---------------------------------------------------------------------------
|
|
6269
|
+
// JSON-RPC helpers
|
|
6270
|
+
// ---------------------------------------------------------------------------
|
|
6271
|
+
function respond(id, result) {
|
|
6272
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
|
|
6273
|
+
}
|
|
6274
|
+
|
|
6275
|
+
function respondError(id, code, message) {
|
|
6276
|
+
process.stdout.write(
|
|
6277
|
+
JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n'
|
|
6278
|
+
);
|
|
6279
|
+
}
|
|
6280
|
+
|
|
6281
|
+
// ---------------------------------------------------------------------------
|
|
6282
|
+
// Method dispatcher
|
|
6283
|
+
// ---------------------------------------------------------------------------
|
|
6284
|
+
function dispatch(msg, cwd) {
|
|
6285
|
+
const { method, id, params } = msg;
|
|
6286
|
+
|
|
6287
|
+
// Notifications (no id) need no response
|
|
6288
|
+
if (method === 'notifications/initialized' || method === 'notifications/cancelled') {
|
|
6289
|
+
return;
|
|
6166
6290
|
}
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6291
|
+
|
|
6292
|
+
if (method === 'initialize') {
|
|
6293
|
+
respond(id, {
|
|
6294
|
+
protocolVersion: (params && params.protocolVersion) || '2024-11-05',
|
|
6295
|
+
serverInfo: SERVER_INFO,
|
|
6296
|
+
capabilities: { tools: {} },
|
|
6297
|
+
});
|
|
6298
|
+
return;
|
|
6172
6299
|
}
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
if (method === 'tools/call') {
|
|
6200
|
-
const name = params && params.name;
|
|
6201
|
-
const args = (params && params.arguments) || {};
|
|
6202
|
-
|
|
6203
|
-
let text;
|
|
6204
|
-
try {
|
|
6205
|
-
if (name === 'read_context') text = readContext(args, cwd);
|
|
6206
|
-
else if (name === 'search_signatures') text = searchSignatures(args, cwd);
|
|
6207
|
-
else if (name === 'get_map') text = getMap(args, cwd);
|
|
6208
|
-
else if (name === 'create_checkpoint') text = createCheckpoint(args, cwd);
|
|
6209
|
-
else if (name === 'get_routing') text = getRouting(args, cwd);
|
|
6210
|
-
else if (name === 'explain_file') text = explainFile(args, cwd);
|
|
6211
|
-
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
6212
|
-
else if (name === 'query_context') text = queryContext(args, cwd);
|
|
6213
|
-
else if (name === 'get_impact') text = getImpact(args, cwd);
|
|
6214
|
-
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
6215
|
-
else {
|
|
6216
|
-
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
6217
|
-
return;
|
|
6218
|
-
}
|
|
6219
|
-
} catch (err) {
|
|
6220
|
-
respondError(id, -32603, `Tool error: ${err.message}`);
|
|
6300
|
+
|
|
6301
|
+
if (method === 'tools/list') {
|
|
6302
|
+
respond(id, { tools: TOOLS });
|
|
6303
|
+
return;
|
|
6304
|
+
}
|
|
6305
|
+
|
|
6306
|
+
if (method === 'tools/call') {
|
|
6307
|
+
const name = params && params.name;
|
|
6308
|
+
const args = (params && params.arguments) || {};
|
|
6309
|
+
|
|
6310
|
+
let text;
|
|
6311
|
+
try {
|
|
6312
|
+
if (name === 'read_context') text = readContext(args, cwd);
|
|
6313
|
+
else if (name === 'search_signatures') text = searchSignatures(args, cwd);
|
|
6314
|
+
else if (name === 'get_map') text = getMap(args, cwd);
|
|
6315
|
+
else if (name === 'create_checkpoint') text = createCheckpoint(args, cwd);
|
|
6316
|
+
else if (name === 'get_routing') text = getRouting(args, cwd);
|
|
6317
|
+
else if (name === 'explain_file') text = explainFile(args, cwd);
|
|
6318
|
+
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
6319
|
+
else if (name === 'query_context') text = queryContext(args, cwd);
|
|
6320
|
+
else if (name === 'get_impact') text = getImpact(args, cwd);
|
|
6321
|
+
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
6322
|
+
else if (name === 'read_memory') text = readMemory(args, cwd);
|
|
6323
|
+
else {
|
|
6324
|
+
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
6221
6325
|
return;
|
|
6222
6326
|
}
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
content: [{ type: 'text', text: String(text) }],
|
|
6226
|
-
});
|
|
6327
|
+
} catch (err) {
|
|
6328
|
+
respondError(id, -32603, `Tool error: ${err.message}`);
|
|
6227
6329
|
return;
|
|
6228
6330
|
}
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
respondError(id, -32601, `Method not found: ${method}`);
|
|
6233
|
-
}
|
|
6234
|
-
}
|
|
6235
|
-
|
|
6236
|
-
// ---------------------------------------------------------------------------
|
|
6237
|
-
// Server entry point
|
|
6238
|
-
// ---------------------------------------------------------------------------
|
|
6239
|
-
function start(cwd) {
|
|
6240
|
-
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
6241
|
-
|
|
6242
|
-
rl.on('line', (line) => {
|
|
6243
|
-
const trimmed = line.trim();
|
|
6244
|
-
if (!trimmed) return;
|
|
6245
|
-
|
|
6246
|
-
let msg;
|
|
6247
|
-
try {
|
|
6248
|
-
msg = JSON.parse(trimmed);
|
|
6249
|
-
} catch (_) {
|
|
6250
|
-
// Cannot respond without a valid id — ignore malformed input
|
|
6251
|
-
return;
|
|
6252
|
-
}
|
|
6253
|
-
|
|
6254
|
-
try {
|
|
6255
|
-
dispatch(msg, cwd);
|
|
6256
|
-
} catch (err) {
|
|
6257
|
-
const id = (msg && msg.id) != null ? msg.id : null;
|
|
6258
|
-
respondError(id, -32603, `Internal error: ${err.message}`);
|
|
6259
|
-
}
|
|
6260
|
-
});
|
|
6261
|
-
|
|
6262
|
-
rl.on('close', () => {
|
|
6263
|
-
process.exit(0);
|
|
6331
|
+
|
|
6332
|
+
respond(id, {
|
|
6333
|
+
content: [{ type: 'text', text: String(text) }],
|
|
6264
6334
|
});
|
|
6335
|
+
return;
|
|
6265
6336
|
}
|
|
6266
|
-
|
|
6267
|
-
module.exports = { start };
|
|
6268
|
-
|
|
6269
|
-
};
|
|
6270
6337
|
|
|
6338
|
+
// Unknown method
|
|
6339
|
+
if (id !== undefined && id !== null) {
|
|
6340
|
+
respondError(id, -32601, `Method not found: ${method}`);
|
|
6341
|
+
}
|
|
6342
|
+
}
|
|
6343
|
+
|
|
6344
|
+
// ---------------------------------------------------------------------------
|
|
6345
|
+
// Server entry point
|
|
6346
|
+
// ---------------------------------------------------------------------------
|
|
6347
|
+
function start(cwd) {
|
|
6348
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
6349
|
+
|
|
6350
|
+
rl.on('line', (line) => {
|
|
6351
|
+
const trimmed = line.trim();
|
|
6352
|
+
if (!trimmed) return;
|
|
6353
|
+
|
|
6354
|
+
let msg;
|
|
6355
|
+
try {
|
|
6356
|
+
msg = JSON.parse(trimmed);
|
|
6357
|
+
} catch (_) {
|
|
6358
|
+
// Cannot respond without a valid id — ignore malformed input
|
|
6359
|
+
return;
|
|
6360
|
+
}
|
|
6361
|
+
|
|
6362
|
+
try {
|
|
6363
|
+
dispatch(msg, cwd);
|
|
6364
|
+
} catch (err) {
|
|
6365
|
+
const id = (msg && msg.id) != null ? msg.id : null;
|
|
6366
|
+
respondError(id, -32603, `Internal error: ${err.message}`);
|
|
6367
|
+
}
|
|
6368
|
+
});
|
|
6369
|
+
|
|
6370
|
+
rl.on('close', () => {
|
|
6371
|
+
process.exit(0);
|
|
6372
|
+
});
|
|
6373
|
+
}
|
|
6374
|
+
|
|
6375
|
+
module.exports = { start };
|
|
6376
|
+
|
|
6377
|
+
};
|
|
6271
6378
|
// ── ./src/mcp/tools ──
|
|
6272
6379
|
__factories["./src/mcp/tools"] = function(module, exports) {
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6380
|
+
'use strict';
|
|
6381
|
+
|
|
6382
|
+
/**
|
|
6383
|
+
* MCP tool definitions for SigMap (11 tools).
|
|
6384
|
+
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
6385
|
+
* explain_file, list_modules, query_context, get_impact, get_lines, read_memory.
|
|
6386
|
+
*/
|
|
6387
|
+
|
|
6388
|
+
const TOOLS = [
|
|
6389
|
+
{
|
|
6390
|
+
name: 'read_context',
|
|
6391
|
+
description:
|
|
6392
|
+
'Read extracted code signatures for the project or a specific module path. ' +
|
|
6393
|
+
'Returns the full copilot-instructions.md content (~500–4K tokens) or a ' +
|
|
6394
|
+
'filtered subset when a module path is provided (~50–500 tokens).',
|
|
6395
|
+
inputSchema: {
|
|
6396
|
+
type: 'object',
|
|
6397
|
+
properties: {
|
|
6398
|
+
module: {
|
|
6399
|
+
type: 'string',
|
|
6400
|
+
description:
|
|
6401
|
+
'Optional subdirectory path to scope results (e.g. "src/services"). ' +
|
|
6402
|
+
'Omit to get the full codebase context.',
|
|
6295
6403
|
},
|
|
6296
|
-
required: [],
|
|
6297
6404
|
},
|
|
6405
|
+
required: [],
|
|
6298
6406
|
},
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6407
|
+
},
|
|
6408
|
+
{
|
|
6409
|
+
name: 'search_signatures',
|
|
6410
|
+
description:
|
|
6411
|
+
'Search extracted code signatures for a keyword, function name, or class name. ' +
|
|
6412
|
+
'Returns matching signature lines with their file paths.',
|
|
6413
|
+
inputSchema: {
|
|
6414
|
+
type: 'object',
|
|
6415
|
+
properties: {
|
|
6416
|
+
query: {
|
|
6417
|
+
type: 'string',
|
|
6418
|
+
description: 'Keyword to search for in signatures (case-insensitive).',
|
|
6311
6419
|
},
|
|
6312
|
-
required: ['query'],
|
|
6313
6420
|
},
|
|
6421
|
+
required: ['query'],
|
|
6314
6422
|
},
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6423
|
+
},
|
|
6424
|
+
{
|
|
6425
|
+
name: 'get_map',
|
|
6426
|
+
description:
|
|
6427
|
+
'Read a section from PROJECT_MAP.md — import graph, class hierarchy, or route table. ' +
|
|
6428
|
+
'Requires gen-project-map.js to have been run first.',
|
|
6429
|
+
inputSchema: {
|
|
6430
|
+
type: 'object',
|
|
6431
|
+
properties: {
|
|
6432
|
+
type: {
|
|
6433
|
+
type: 'string',
|
|
6434
|
+
enum: ['imports', 'classes', 'routes'],
|
|
6435
|
+
description: 'Which section to retrieve: imports, classes, or routes.',
|
|
6328
6436
|
},
|
|
6329
|
-
required: ['type'],
|
|
6330
6437
|
},
|
|
6438
|
+
required: ['type'],
|
|
6331
6439
|
},
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6440
|
+
},
|
|
6441
|
+
{
|
|
6442
|
+
name: 'create_checkpoint',
|
|
6443
|
+
description:
|
|
6444
|
+
'Create a session checkpoint summarising current project state. ' +
|
|
6445
|
+
'Returns recent git commits, active branch, token count, and a ' +
|
|
6446
|
+
'compact snapshot of the codebase context — ideal for session handoffs ' +
|
|
6447
|
+
'or periodic saves during long coding sessions.',
|
|
6448
|
+
inputSchema: {
|
|
6449
|
+
type: 'object',
|
|
6450
|
+
properties: {
|
|
6451
|
+
note: {
|
|
6452
|
+
type: 'string',
|
|
6453
|
+
description: 'Optional free-text note to include in the checkpoint (e.g. what you were working on).',
|
|
6346
6454
|
},
|
|
6347
|
-
required: [],
|
|
6348
6455
|
},
|
|
6456
|
+
required: [],
|
|
6349
6457
|
},
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6458
|
+
},
|
|
6459
|
+
{
|
|
6460
|
+
name: 'get_routing',
|
|
6461
|
+
description:
|
|
6462
|
+
'Get model routing hints for this project — which files belong to which complexity ' +
|
|
6463
|
+
'tier (fast/balanced/powerful) and which AI model to use for each type of task. ' +
|
|
6464
|
+
'Helps reduce API costs by 40–80% by routing simple tasks to cheaper models.',
|
|
6465
|
+
inputSchema: {
|
|
6466
|
+
type: 'object',
|
|
6467
|
+
properties: {},
|
|
6468
|
+
required: [],
|
|
6361
6469
|
},
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6470
|
+
},
|
|
6471
|
+
{
|
|
6472
|
+
name: 'explain_file',
|
|
6473
|
+
description:
|
|
6474
|
+
'Explain a specific file: returns its extracted signatures, direct imports ' +
|
|
6475
|
+
'(files it depends on), and callers (files that import it). ' +
|
|
6476
|
+
'Ideal for understanding a file in isolation without reading raw source. ' +
|
|
6477
|
+
'Requires the context file to have been generated first.',
|
|
6478
|
+
inputSchema: {
|
|
6479
|
+
type: 'object',
|
|
6480
|
+
properties: {
|
|
6481
|
+
path: {
|
|
6482
|
+
type: 'string',
|
|
6483
|
+
description:
|
|
6484
|
+
'Relative path from the project root (e.g. "src/services/auth.ts"). ' +
|
|
6485
|
+
'Use the paths shown in read_context output.',
|
|
6378
6486
|
},
|
|
6379
|
-
required: ['path'],
|
|
6380
6487
|
},
|
|
6488
|
+
required: ['path'],
|
|
6381
6489
|
},
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6490
|
+
},
|
|
6491
|
+
{
|
|
6492
|
+
name: 'list_modules',
|
|
6493
|
+
description:
|
|
6494
|
+
'List all top-level modules (srcDirs) present in the context file, ' +
|
|
6495
|
+
'sorted by token count descending. Use this to decide which module to ' +
|
|
6496
|
+
'pass to read_context before querying a specific area of the codebase.',
|
|
6497
|
+
inputSchema: {
|
|
6498
|
+
type: 'object',
|
|
6499
|
+
properties: {},
|
|
6500
|
+
required: [],
|
|
6393
6501
|
},
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6502
|
+
},
|
|
6503
|
+
{
|
|
6504
|
+
name: 'query_context',
|
|
6505
|
+
description:
|
|
6506
|
+
'Rank and return the most relevant files for a specific task or question. ' +
|
|
6507
|
+
'Uses keyword + symbol + path scoring to surface only the top-K files relevant ' +
|
|
6508
|
+
'to the query — much cheaper than reading all context. ' +
|
|
6509
|
+
'Returns ranked file list with signatures and relevance scores.',
|
|
6510
|
+
inputSchema: {
|
|
6511
|
+
type: 'object',
|
|
6512
|
+
properties: {
|
|
6513
|
+
query: {
|
|
6514
|
+
type: 'string',
|
|
6515
|
+
description:
|
|
6516
|
+
'Natural language task description or keyword(s) to rank files against. ' +
|
|
6517
|
+
'E.g. "add a new language extractor", "fix secret scanning", "auth module".',
|
|
6518
|
+
},
|
|
6519
|
+
topK: {
|
|
6520
|
+
type: 'number',
|
|
6521
|
+
description: 'Maximum number of files to return (default: 10, max: 25).',
|
|
6414
6522
|
},
|
|
6415
|
-
required: ['query'],
|
|
6416
6523
|
},
|
|
6524
|
+
required: ['query'],
|
|
6417
6525
|
},
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6526
|
+
},
|
|
6527
|
+
{
|
|
6528
|
+
name: 'get_impact',
|
|
6529
|
+
description:
|
|
6530
|
+
'Show every file that is impacted when a given file changes — direct importers, ' +
|
|
6531
|
+
'transitive importers, affected tests, and affected routes/controllers. ' +
|
|
6532
|
+
'Gives agents instant blast-radius awareness before making a change. ' +
|
|
6533
|
+
'Handles circular dependencies safely (no infinite loops).',
|
|
6534
|
+
inputSchema: {
|
|
6535
|
+
type: 'object',
|
|
6536
|
+
properties: {
|
|
6537
|
+
file: {
|
|
6538
|
+
type: 'string',
|
|
6539
|
+
description:
|
|
6540
|
+
'Relative path from the project root of the file that changed ' +
|
|
6541
|
+
'(e.g. "src/extractors/python.js"). Use forward slashes.',
|
|
6542
|
+
},
|
|
6543
|
+
depth: {
|
|
6544
|
+
type: 'number',
|
|
6545
|
+
description: 'BFS traversal depth limit (default: 3). Use 0 for unlimited.',
|
|
6438
6546
|
},
|
|
6439
|
-
required: ['file'],
|
|
6440
6547
|
},
|
|
6548
|
+
required: ['file'],
|
|
6441
6549
|
},
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6550
|
+
},
|
|
6551
|
+
{
|
|
6552
|
+
name: 'get_lines',
|
|
6553
|
+
description:
|
|
6554
|
+
'Fetch an exact line range from a source file on demand — the Surgical Context ' +
|
|
6555
|
+
'workhorse. Signatures carry `path:start-end` anchors; call this to read just those ' +
|
|
6556
|
+
'lines instead of re-opening the whole file. Lines are clamped to the file bounds and ' +
|
|
6557
|
+
'secret-scanned (redacted) before return. Path is sandboxed to the project root.',
|
|
6558
|
+
inputSchema: {
|
|
6559
|
+
type: 'object',
|
|
6560
|
+
properties: {
|
|
6561
|
+
file: {
|
|
6562
|
+
type: 'string',
|
|
6563
|
+
description:
|
|
6564
|
+
'Relative path from the project root (e.g. "src/config/loader.js"). ' +
|
|
6565
|
+
'Use the path shown in a signature anchor. Use forward slashes.',
|
|
6566
|
+
},
|
|
6567
|
+
start: {
|
|
6568
|
+
type: 'number',
|
|
6569
|
+
description: '1-based start line (inclusive). Clamped to the file bounds.',
|
|
6570
|
+
},
|
|
6571
|
+
end: {
|
|
6572
|
+
type: 'number',
|
|
6573
|
+
description: '1-based end line (inclusive). Clamped to the file bounds.',
|
|
6466
6574
|
},
|
|
6467
|
-
required: ['file', 'start', 'end'],
|
|
6468
6575
|
},
|
|
6576
|
+
required: ['file', 'start', 'end'],
|
|
6469
6577
|
},
|
|
6470
|
-
|
|
6578
|
+
},
|
|
6579
|
+
{
|
|
6580
|
+
name: 'read_memory',
|
|
6581
|
+
description:
|
|
6582
|
+
'Recall the project decision log — recent notes left by humans or agents ' +
|
|
6583
|
+
'across sessions (via `sigmap note`), plus the last ranking-session focus. ' +
|
|
6584
|
+
'Call this at the start of a task to kill cold-start: it answers ' +
|
|
6585
|
+
'"what were we doing and why" without re-reading the whole codebase.',
|
|
6586
|
+
inputSchema: {
|
|
6587
|
+
type: 'object',
|
|
6588
|
+
properties: {
|
|
6589
|
+
limit: {
|
|
6590
|
+
type: 'number',
|
|
6591
|
+
description: 'How many of the most recent notes to return (default: 10, max: 50).',
|
|
6592
|
+
},
|
|
6593
|
+
},
|
|
6594
|
+
required: [],
|
|
6595
|
+
},
|
|
6596
|
+
},
|
|
6597
|
+
];
|
|
6471
6598
|
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
};
|
|
6599
|
+
module.exports = { TOOLS };
|
|
6475
6600
|
|
|
6601
|
+
};
|
|
6476
6602
|
// ── ./src/routing/classifier ──
|
|
6477
6603
|
__factories["./src/routing/classifier"] = function(module, exports) {
|
|
6478
6604
|
|
|
@@ -7007,6 +7133,110 @@ __factories["./src/tracking/logger"] = function(module, exports) {
|
|
|
7007
7133
|
|
|
7008
7134
|
};
|
|
7009
7135
|
|
|
7136
|
+
// ── ./src/session/notes ──
|
|
7137
|
+
__factories["./src/session/notes"] = function(module, exports) {
|
|
7138
|
+
'use strict';
|
|
7139
|
+
|
|
7140
|
+
/**
|
|
7141
|
+
* Decision-log notes (Memory, v6.15.0).
|
|
7142
|
+
*
|
|
7143
|
+
* A tiny append-only log of human/agent notes that survives across sessions,
|
|
7144
|
+
* so an agent starting cold can recall "what were we doing / why". Stored as
|
|
7145
|
+
* NDJSON under `.context/` to match the project's other state logs
|
|
7146
|
+
* (usage.ndjson, benchmark-history.ndjson). Zero dependencies.
|
|
7147
|
+
*
|
|
7148
|
+
* Consumed by the `read_memory` MCP tool and the `sigmap note` / `sigmap status`
|
|
7149
|
+
* commands. Complements `src/session/memory.js` (ranking session) rather than
|
|
7150
|
+
* duplicating it.
|
|
7151
|
+
*/
|
|
7152
|
+
|
|
7153
|
+
const fs = require('fs');
|
|
7154
|
+
const path = require('path');
|
|
7155
|
+
const { execSync } = require('child_process');
|
|
7156
|
+
|
|
7157
|
+
const NOTES_FILE = path.join('.context', 'notes.ndjson');
|
|
7158
|
+
const MAX_TEXT = 2000;
|
|
7159
|
+
|
|
7160
|
+
function notesPath(cwd) {
|
|
7161
|
+
return path.join(cwd, NOTES_FILE);
|
|
7162
|
+
}
|
|
7163
|
+
|
|
7164
|
+
function _currentBranch(cwd) {
|
|
7165
|
+
try {
|
|
7166
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
7167
|
+
cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
7168
|
+
}).trim() || null;
|
|
7169
|
+
} catch (_) {
|
|
7170
|
+
return null;
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
|
|
7174
|
+
/**
|
|
7175
|
+
* Append a note. Returns the stored entry.
|
|
7176
|
+
* @param {string} cwd
|
|
7177
|
+
* @param {string} text
|
|
7178
|
+
* @param {object} [opts]
|
|
7179
|
+
* @param {string|null} [opts.branch] override branch (default: current git branch)
|
|
7180
|
+
* @param {string} [opts.tag] optional category tag
|
|
7181
|
+
*/
|
|
7182
|
+
function addNote(cwd, text, opts = {}) {
|
|
7183
|
+
const clean = String(text == null ? '' : text).trim().slice(0, MAX_TEXT);
|
|
7184
|
+
if (!clean) throw new Error('note text is empty');
|
|
7185
|
+
const entry = {
|
|
7186
|
+
ts: new Date().toISOString(),
|
|
7187
|
+
text: clean,
|
|
7188
|
+
branch: opts.branch !== undefined ? opts.branch : _currentBranch(cwd),
|
|
7189
|
+
};
|
|
7190
|
+
if (opts.tag) entry.tag = String(opts.tag).trim().slice(0, 40);
|
|
7191
|
+
const p = notesPath(cwd);
|
|
7192
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
7193
|
+
fs.appendFileSync(p, JSON.stringify(entry) + '\n');
|
|
7194
|
+
return entry;
|
|
7195
|
+
}
|
|
7196
|
+
|
|
7197
|
+
/**
|
|
7198
|
+
* Read notes in chronological order (oldest first).
|
|
7199
|
+
* @param {string} cwd
|
|
7200
|
+
* @param {number} [limit=0] keep only the most recent N (0 = all)
|
|
7201
|
+
* @returns {object[]}
|
|
7202
|
+
*/
|
|
7203
|
+
function readNotes(cwd, limit = 0) {
|
|
7204
|
+
let raw;
|
|
7205
|
+
try { raw = fs.readFileSync(notesPath(cwd), 'utf8'); }
|
|
7206
|
+
catch (_) { return []; }
|
|
7207
|
+
const out = [];
|
|
7208
|
+
for (const line of raw.split('\n')) {
|
|
7209
|
+
const t = line.trim();
|
|
7210
|
+
if (!t) continue;
|
|
7211
|
+
try { out.push(JSON.parse(t)); } catch (_) { /* skip corrupt line */ }
|
|
7212
|
+
}
|
|
7213
|
+
if (limit > 0 && out.length > limit) return out.slice(out.length - limit);
|
|
7214
|
+
return out;
|
|
7215
|
+
}
|
|
7216
|
+
|
|
7217
|
+
/** Format notes as a Markdown list (pass already-ordered notes). */
|
|
7218
|
+
function formatNotes(notes) {
|
|
7219
|
+
if (!notes || !notes.length) {
|
|
7220
|
+
return 'No notes recorded yet. Add one with: sigmap note "<text>"';
|
|
7221
|
+
}
|
|
7222
|
+
return notes.map((n) => {
|
|
7223
|
+
const when = String(n.ts || '').replace('T', ' ').slice(0, 16);
|
|
7224
|
+
const br = n.branch ? ` (${n.branch})` : '';
|
|
7225
|
+
const tag = n.tag ? ` #${n.tag}` : '';
|
|
7226
|
+
return `- [${when}${br}]${tag} ${n.text}`;
|
|
7227
|
+
}).join('\n');
|
|
7228
|
+
}
|
|
7229
|
+
|
|
7230
|
+
/** Delete the notes log. Returns true if a file was removed. */
|
|
7231
|
+
function clearNotes(cwd) {
|
|
7232
|
+
try { fs.unlinkSync(notesPath(cwd)); return true; }
|
|
7233
|
+
catch (_) { return false; }
|
|
7234
|
+
}
|
|
7235
|
+
|
|
7236
|
+
module.exports = { notesPath, addNote, readNotes, formatNotes, clearNotes };
|
|
7237
|
+
|
|
7238
|
+
};
|
|
7239
|
+
|
|
7010
7240
|
// ── ./src/session/memory ──
|
|
7011
7241
|
__factories["./src/session/memory"] = function(module, exports) {
|
|
7012
7242
|
'use strict';
|
|
@@ -7781,314 +8011,621 @@ __factories["./src/eval/usefulness-scorer"] = function(module, exports) {
|
|
|
7781
8011
|
|
|
7782
8012
|
// ── ./packages/adapters/copilot (bundled) ──
|
|
7783
8013
|
__factories["./packages/adapters/copilot"] = function(module, exports) {
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
8014
|
+
'use strict';
|
|
8015
|
+
|
|
8016
|
+
/**
|
|
8017
|
+
* Copilot adapter — writes to .github/copilot-instructions.md
|
|
8018
|
+
* GitHub Copilot reads this file automatically in every workspace.
|
|
8019
|
+
*
|
|
8020
|
+
* Contract:
|
|
8021
|
+
* format(context, opts?) → string
|
|
8022
|
+
* outputPath(cwd) → string
|
|
8023
|
+
*/
|
|
8024
|
+
|
|
8025
|
+
const path = require('path');
|
|
8026
|
+
const fs = require('fs');
|
|
8027
|
+
|
|
8028
|
+
const name = 'copilot';
|
|
8029
|
+
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
8030
|
+
|
|
8031
|
+
/**
|
|
8032
|
+
* Format context for GitHub Copilot instructions.
|
|
8033
|
+
* @param {string} context - Raw signature context string
|
|
8034
|
+
* @param {object} [opts]
|
|
8035
|
+
* @param {string} [opts.version] - SigMap version string
|
|
8036
|
+
* @returns {string}
|
|
8037
|
+
*/
|
|
8038
|
+
function format(context, opts = {}) {
|
|
8039
|
+
if (!context || typeof context !== 'string') return '';
|
|
8040
|
+
const version = opts.version || 'unknown';
|
|
8041
|
+
const timestamp = new Date().toISOString();
|
|
8042
|
+
const meta = _confidenceMeta(opts);
|
|
8043
|
+
const header = [
|
|
8044
|
+
`<!-- Generated by SigMap gen-context.js v${version} -->`,
|
|
8045
|
+
`<!-- Updated: ${timestamp} -->`,
|
|
8046
|
+
meta,
|
|
8047
|
+
`<!-- Do not edit below — regenerate with: node gen-context.js -->`,
|
|
8048
|
+
'',
|
|
8049
|
+
'# Code signatures',
|
|
8050
|
+
'',
|
|
8051
|
+
].join('\n');
|
|
8052
|
+
return header + context;
|
|
8053
|
+
}
|
|
8054
|
+
|
|
8055
|
+
function _confidenceMeta(opts) {
|
|
8056
|
+
const parts = [`version=${opts.version || 'unknown'}`];
|
|
8057
|
+
if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
|
|
8058
|
+
if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
|
|
8059
|
+
if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
|
|
8060
|
+
if (opts.commit) parts.push(`commit=${opts.commit}`);
|
|
8061
|
+
return `<!-- sigmap: ${parts.join(' ')} -->`;
|
|
8062
|
+
}
|
|
8063
|
+
|
|
8064
|
+
/**
|
|
8065
|
+
* Return the output file path for this adapter.
|
|
8066
|
+
* @param {string} cwd - Project root
|
|
8067
|
+
* @returns {string}
|
|
8068
|
+
*/
|
|
8069
|
+
function outputPath(cwd) {
|
|
8070
|
+
return path.join(cwd, '.github', 'copilot-instructions.md');
|
|
8071
|
+
}
|
|
8072
|
+
|
|
8073
|
+
/**
|
|
8074
|
+
* Write signatures into copilot-instructions.md using append-under-marker.
|
|
8075
|
+
* If marker exists, content above marker is preserved.
|
|
8076
|
+
* If legacy generated content exists without marker, replace it cleanly.
|
|
8077
|
+
* @param {string} context - Raw signature context string
|
|
8078
|
+
* @param {string} cwd - Project root
|
|
8079
|
+
* @param {object} [opts]
|
|
8080
|
+
*/
|
|
8081
|
+
function write(context, cwd, opts = {}) {
|
|
8082
|
+
const filePath = outputPath(cwd);
|
|
8083
|
+
let existing = '';
|
|
8084
|
+
if (fs.existsSync(filePath)) {
|
|
8085
|
+
existing = fs.readFileSync(filePath, 'utf8');
|
|
7809
8086
|
}
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
const
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
8087
|
+
|
|
8088
|
+
const formatted = format(context, opts);
|
|
8089
|
+
const markerIdx = existing.indexOf('## Auto-generated signatures');
|
|
8090
|
+
|
|
8091
|
+
let newContent;
|
|
8092
|
+
if (markerIdx !== -1) {
|
|
8093
|
+
newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
|
|
8094
|
+
} else {
|
|
8095
|
+
const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
|
|
8096
|
+
|| existing.includes('# Code signatures');
|
|
8097
|
+
newContent = isLegacyGenerated
|
|
8098
|
+
? MARKER.trimStart() + formatted
|
|
8099
|
+
: existing + MARKER + formatted;
|
|
7823
8100
|
}
|
|
7824
|
-
module.exports = { name, format, outputPath, write };
|
|
7825
|
-
};
|
|
7826
8101
|
|
|
8102
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
8103
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
8104
|
+
}
|
|
8105
|
+
|
|
8106
|
+
module.exports = { name, format, outputPath, write };
|
|
8107
|
+
|
|
8108
|
+
};
|
|
7827
8109
|
// ── ./packages/adapters/claude (bundled) ──
|
|
7828
8110
|
__factories["./packages/adapters/claude"] = function(module, exports) {
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
8111
|
+
'use strict';
|
|
8112
|
+
|
|
8113
|
+
/**
|
|
8114
|
+
* Claude adapter — appends to CLAUDE.md under a marker line.
|
|
8115
|
+
* Never overwrites human-written content above the marker.
|
|
8116
|
+
*
|
|
8117
|
+
* Contract:
|
|
8118
|
+
* format(context, opts?) → string
|
|
8119
|
+
* outputPath(cwd) → string
|
|
8120
|
+
* write(context, cwd, opts?) → void (handles append logic)
|
|
8121
|
+
*/
|
|
8122
|
+
|
|
8123
|
+
const path = require('path');
|
|
8124
|
+
const fs = require('fs');
|
|
8125
|
+
|
|
8126
|
+
const name = 'claude';
|
|
8127
|
+
|
|
8128
|
+
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
8129
|
+
const ALLOWLIST_MARKER = '<!-- sigmap-bash-allowlist -->';
|
|
8130
|
+
|
|
8131
|
+
const ALLOWLIST_BLOCK = [
|
|
8132
|
+
'## Bash allowlist',
|
|
8133
|
+
'',
|
|
8134
|
+
ALLOWLIST_MARKER,
|
|
8135
|
+
'',
|
|
8136
|
+
'The following sigmap commands are pre-approved — Claude Code will not prompt for confirmation:',
|
|
8137
|
+
'',
|
|
8138
|
+
'```json',
|
|
8139
|
+
JSON.stringify({
|
|
8140
|
+
permissions: {
|
|
8141
|
+
allow: [
|
|
8142
|
+
'Bash(sigmap ask*)',
|
|
8143
|
+
'Bash(sigmap validate*)',
|
|
8144
|
+
'Bash(sigmap judge*)',
|
|
8145
|
+
'Bash(sigmap weights*)',
|
|
8146
|
+
'Bash(sigmap history*)',
|
|
8147
|
+
'Bash(sigmap --query*)',
|
|
8148
|
+
'Bash(sigmap --diff*)',
|
|
8149
|
+
'Bash(sigmap --health*)',
|
|
8150
|
+
'Bash(sigmap --coverage*)',
|
|
8151
|
+
'Bash(node gen-context.js*)',
|
|
8152
|
+
],
|
|
8153
|
+
},
|
|
8154
|
+
}, null, 2),
|
|
8155
|
+
'```',
|
|
8156
|
+
'',
|
|
8157
|
+
'Add the `permissions.allow` array above to `.claude/settings.json` to activate.',
|
|
8158
|
+
'',
|
|
8159
|
+
].join('\n');
|
|
8160
|
+
|
|
8161
|
+
/**
|
|
8162
|
+
* Format context suited for CLAUDE.md.
|
|
8163
|
+
* @param {string} context - Raw signature context string
|
|
8164
|
+
* @param {object} [opts]
|
|
8165
|
+
* @param {string} [opts.version] - SigMap version string
|
|
8166
|
+
* @returns {string}
|
|
8167
|
+
*/
|
|
8168
|
+
function format(context, opts = {}) {
|
|
8169
|
+
if (!context || typeof context !== 'string') return '';
|
|
8170
|
+
const version = opts.version || 'unknown';
|
|
8171
|
+
const timestamp = new Date().toISOString();
|
|
8172
|
+
const meta = _confidenceMeta(opts);
|
|
8173
|
+
return [
|
|
8174
|
+
`<!-- Generated by SigMap v${version} — ${timestamp} -->`,
|
|
8175
|
+
meta,
|
|
7861
8176
|
'',
|
|
8177
|
+
context,
|
|
7862
8178
|
].join('\n');
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
8179
|
+
}
|
|
8180
|
+
|
|
8181
|
+
function _confidenceMeta(opts) {
|
|
8182
|
+
const parts = [`version=${opts.version || 'unknown'}`];
|
|
8183
|
+
if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
|
|
8184
|
+
if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
|
|
8185
|
+
if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
|
|
8186
|
+
if (opts.commit) parts.push(`commit=${opts.commit}`);
|
|
8187
|
+
return `<!-- sigmap: ${parts.join(' ')} -->`;
|
|
8188
|
+
}
|
|
8189
|
+
|
|
8190
|
+
/**
|
|
8191
|
+
* Return the output file path for this adapter.
|
|
8192
|
+
* @param {string} cwd - Project root
|
|
8193
|
+
* @returns {string}
|
|
8194
|
+
*/
|
|
8195
|
+
function outputPath(cwd) {
|
|
8196
|
+
return path.join(cwd, 'CLAUDE.md');
|
|
8197
|
+
}
|
|
8198
|
+
|
|
8199
|
+
/**
|
|
8200
|
+
* Write signatures into CLAUDE.md using the append-under-marker strategy.
|
|
8201
|
+
* Human content above the marker is never touched.
|
|
8202
|
+
* @param {string} context - Raw signature context string
|
|
8203
|
+
* @param {string} cwd - Project root
|
|
8204
|
+
* @param {object} [opts]
|
|
8205
|
+
*/
|
|
8206
|
+
function write(context, cwd, opts = {}) {
|
|
8207
|
+
const filePath = outputPath(cwd);
|
|
8208
|
+
let existing = '';
|
|
8209
|
+
if (fs.existsSync(filePath)) {
|
|
8210
|
+
existing = fs.readFileSync(filePath, 'utf8');
|
|
7878
8211
|
}
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
}
|
|
8212
|
+
const formatted = format(context, opts);
|
|
8213
|
+
const markerIdx = existing.indexOf('## Auto-generated signatures');
|
|
8214
|
+
let newContent;
|
|
8215
|
+
if (markerIdx !== -1) {
|
|
8216
|
+
newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
|
|
8217
|
+
} else {
|
|
8218
|
+
newContent = existing + MARKER + formatted;
|
|
8219
|
+
}
|
|
8220
|
+
|
|
8221
|
+
// Inject ## Bash allowlist above the sig marker if not already present
|
|
8222
|
+
if (!newContent.includes(ALLOWLIST_MARKER)) {
|
|
8223
|
+
const sigMarkerPos = newContent.indexOf('## Auto-generated signatures');
|
|
8224
|
+
if (sigMarkerPos !== -1) {
|
|
8225
|
+
newContent = newContent.slice(0, sigMarkerPos) + ALLOWLIST_BLOCK + '\n' + newContent.slice(sigMarkerPos);
|
|
8226
|
+
} else {
|
|
8227
|
+
newContent = ALLOWLIST_BLOCK + '\n' + newContent;
|
|
7896
8228
|
}
|
|
7897
|
-
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
7898
8229
|
}
|
|
7899
|
-
module.exports = { name, format, outputPath, write };
|
|
7900
|
-
};
|
|
7901
8230
|
|
|
8231
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
8232
|
+
}
|
|
8233
|
+
|
|
8234
|
+
module.exports = { name, format, outputPath, write };
|
|
8235
|
+
|
|
8236
|
+
};
|
|
7902
8237
|
// ── ./packages/adapters/cursor (bundled) ──
|
|
7903
8238
|
__factories["./packages/adapters/cursor"] = function(module, exports) {
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
}
|
|
8239
|
+
'use strict';
|
|
8240
|
+
|
|
8241
|
+
/**
|
|
8242
|
+
* Cursor adapter — writes to .cursorrules
|
|
8243
|
+
* Cursor reads .cursorrules automatically in every workspace.
|
|
8244
|
+
*
|
|
8245
|
+
* Contract:
|
|
8246
|
+
* format(context, opts?) → string
|
|
8247
|
+
* outputPath(cwd) → string
|
|
8248
|
+
*/
|
|
8249
|
+
|
|
8250
|
+
const path = require('path');
|
|
8251
|
+
|
|
8252
|
+
const name = 'cursor';
|
|
8253
|
+
|
|
8254
|
+
/**
|
|
8255
|
+
* Format context for Cursor rules file.
|
|
8256
|
+
* @param {string} context - Raw signature context string
|
|
8257
|
+
* @param {object} [opts]
|
|
8258
|
+
* @param {string} [opts.version] - SigMap version string
|
|
8259
|
+
* @returns {string}
|
|
8260
|
+
*/
|
|
8261
|
+
function format(context, opts = {}) {
|
|
8262
|
+
if (!context || typeof context !== 'string') return '';
|
|
8263
|
+
const version = opts.version || 'unknown';
|
|
8264
|
+
const timestamp = new Date().toISOString();
|
|
8265
|
+
const meta = _confidenceMeta(opts);
|
|
8266
|
+
const header = [
|
|
8267
|
+
`# Code signatures — generated by SigMap v${version}`,
|
|
8268
|
+
`# Updated: ${timestamp}`,
|
|
8269
|
+
`# ${meta}`,
|
|
8270
|
+
`# Regenerate: node gen-context.js`,
|
|
8271
|
+
'',
|
|
8272
|
+
].join('\n');
|
|
8273
|
+
return header + context;
|
|
8274
|
+
}
|
|
8275
|
+
|
|
8276
|
+
function _confidenceMeta(opts) {
|
|
8277
|
+
const parts = [`version=${opts.version || 'unknown'}`];
|
|
8278
|
+
if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
|
|
8279
|
+
if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
|
|
8280
|
+
if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
|
|
8281
|
+
if (opts.commit) parts.push(`commit=${opts.commit}`);
|
|
8282
|
+
return `sigmap: ${parts.join(' ')}`;
|
|
8283
|
+
}
|
|
8284
|
+
|
|
8285
|
+
/**
|
|
8286
|
+
* Return the output file path for this adapter.
|
|
8287
|
+
* @param {string} cwd - Project root
|
|
8288
|
+
* @returns {string}
|
|
8289
|
+
*/
|
|
8290
|
+
function outputPath(cwd) {
|
|
8291
|
+
return path.join(cwd, '.cursorrules');
|
|
8292
|
+
}
|
|
8293
|
+
|
|
8294
|
+
module.exports = { name, format, outputPath };
|
|
7925
8295
|
|
|
8296
|
+
};
|
|
7926
8297
|
// ── ./packages/adapters/windsurf (bundled) ──
|
|
7927
8298
|
__factories["./packages/adapters/windsurf"] = function(module, exports) {
|
|
7928
|
-
|
|
7929
|
-
const name = 'windsurf';
|
|
7930
|
-
function format(context, opts = {}) {
|
|
7931
|
-
if (!context || typeof context !== 'string') return '';
|
|
7932
|
-
const version = (opts && opts.version) || 'unknown';
|
|
7933
|
-
const timestamp = new Date().toISOString();
|
|
7934
|
-
return [
|
|
7935
|
-
`# Code signatures — generated by SigMap v${version}`,
|
|
7936
|
-
`# Updated: ${timestamp}`,
|
|
7937
|
-
`# Regenerate: node gen-context.js`,
|
|
7938
|
-
'#',
|
|
7939
|
-
'# SigMap: before answering, run: sigmap ask "<query>" — finds relevant files',
|
|
7940
|
-
'# SigMap: after config changes, run: sigmap validate',
|
|
7941
|
-
'# SigMap: to verify answers, run: sigmap judge --response <file>',
|
|
7942
|
-
'',
|
|
7943
|
-
context,
|
|
7944
|
-
].join('\n');
|
|
7945
|
-
}
|
|
7946
|
-
function outputPath(cwd) { return path.join(cwd, '.windsurfrules'); }
|
|
7947
|
-
module.exports = { name, format, outputPath };
|
|
7948
|
-
};
|
|
8299
|
+
'use strict';
|
|
7949
8300
|
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
const projectLine = (opts && opts.projectName) ? `Project: ${opts.projectName}\n` : '';
|
|
7959
|
-
return [
|
|
7960
|
-
`You are a coding assistant with full knowledge of this codebase.`,
|
|
7961
|
-
`Below are the code signatures extracted by SigMap v${version} on ${timestamp}.`,
|
|
7962
|
-
projectLine,
|
|
7963
|
-
`Use these signatures to answer questions about the code accurately.`,
|
|
7964
|
-
`When the user asks about a specific file or function, refer to the signatures below.`,
|
|
7965
|
-
`Before answering questions about specific code areas, prefer running \`sigmap ask "<query>"\` to get the most relevant files. After config changes, \`sigmap validate\` confirms coverage.`,
|
|
7966
|
-
``,
|
|
7967
|
-
`## Code Signatures`,
|
|
7968
|
-
``,
|
|
7969
|
-
context,
|
|
7970
|
-
].join('\n');
|
|
7971
|
-
}
|
|
7972
|
-
function outputPath(cwd) { return path.join(cwd, '.github', 'openai-context.md'); }
|
|
7973
|
-
module.exports = { name, format, outputPath };
|
|
7974
|
-
};
|
|
8301
|
+
/**
|
|
8302
|
+
* Windsurf adapter — writes to .windsurfrules
|
|
8303
|
+
* Windsurf reads .windsurfrules automatically in every workspace.
|
|
8304
|
+
*
|
|
8305
|
+
* Contract:
|
|
8306
|
+
* format(context, opts?) → string
|
|
8307
|
+
* outputPath(cwd) → string
|
|
8308
|
+
*/
|
|
7975
8309
|
|
|
7976
|
-
|
|
7977
|
-
__factories["./packages/adapters/gemini"] = function(module, exports) {
|
|
7978
|
-
const path = require('path');
|
|
7979
|
-
const fs = require('fs');
|
|
7980
|
-
const name = 'gemini';
|
|
7981
|
-
const GEMINI_MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
7982
|
-
function format(context, opts = {}) {
|
|
7983
|
-
if (!context || typeof context !== 'string') return '';
|
|
7984
|
-
const version = (opts && opts.version) || 'unknown';
|
|
7985
|
-
const timestamp = new Date().toISOString();
|
|
7986
|
-
const projectLine = (opts && opts.projectName) ? `Project: ${opts.projectName}\n` : '';
|
|
7987
|
-
return [
|
|
7988
|
-
`You are a coding assistant with complete knowledge of this codebase.`,
|
|
7989
|
-
`The following code signatures were extracted by SigMap v${version} on ${timestamp}.`,
|
|
7990
|
-
projectLine,
|
|
7991
|
-
`These signatures represent every public function, class, and type in the project.`,
|
|
7992
|
-
`Refer to them when answering questions about code structure, APIs, and implementation.`,
|
|
7993
|
-
`Before answering questions about specific code areas, suggest running \`sigmap ask "<query>"\` to get the most relevant files. After config changes, \`sigmap validate\` confirms coverage.`,
|
|
7994
|
-
``,
|
|
7995
|
-
`## Code Signatures`,
|
|
7996
|
-
``,
|
|
7997
|
-
context,
|
|
7998
|
-
].join('\n');
|
|
7999
|
-
}
|
|
8000
|
-
function outputPath(cwd) { return path.join(cwd, '.github', 'gemini-context.md'); }
|
|
8001
|
-
function write(context, cwd, opts = {}) {
|
|
8002
|
-
const filePath = outputPath(cwd);
|
|
8003
|
-
let existing = '';
|
|
8004
|
-
if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, 'utf8');
|
|
8005
|
-
const formatted = format(context, opts);
|
|
8006
|
-
const markerIdx = existing.indexOf('## Auto-generated signatures');
|
|
8007
|
-
const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js') || existing.includes('## Code Signatures');
|
|
8008
|
-
const newContent = markerIdx !== -1
|
|
8009
|
-
? existing.slice(0, markerIdx) + GEMINI_MARKER.trimStart() + formatted
|
|
8010
|
-
: (isLegacyGenerated ? GEMINI_MARKER.trimStart() + formatted : existing + GEMINI_MARKER + formatted);
|
|
8011
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
8012
|
-
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
8013
|
-
}
|
|
8014
|
-
module.exports = { name, format, outputPath, write };
|
|
8015
|
-
};
|
|
8310
|
+
const path = require('path');
|
|
8016
8311
|
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
], null, 2),
|
|
8037
|
-
'```',
|
|
8312
|
+
const name = 'windsurf';
|
|
8313
|
+
|
|
8314
|
+
/**
|
|
8315
|
+
* Format context for Windsurf rules file.
|
|
8316
|
+
* @param {string} context - Raw signature context string
|
|
8317
|
+
* @param {object} [opts]
|
|
8318
|
+
* @param {string} [opts.version] - SigMap version string
|
|
8319
|
+
* @returns {string}
|
|
8320
|
+
*/
|
|
8321
|
+
function format(context, opts = {}) {
|
|
8322
|
+
if (!context || typeof context !== 'string') return '';
|
|
8323
|
+
const version = opts.version || 'unknown';
|
|
8324
|
+
const timestamp = new Date().toISOString();
|
|
8325
|
+
const meta = _confidenceMeta(opts);
|
|
8326
|
+
const header = [
|
|
8327
|
+
`# Code signatures — generated by SigMap v${version}`,
|
|
8328
|
+
`# Updated: ${timestamp}`,
|
|
8329
|
+
`# ${meta}`,
|
|
8330
|
+
`# Regenerate: node gen-context.js`,
|
|
8038
8331
|
'',
|
|
8039
8332
|
].join('\n');
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js') || existing.includes('## Code Signatures') || existing.includes('# Code signatures');
|
|
8064
|
-
let newContent = markerIdx !== -1
|
|
8065
|
-
? existing.slice(0, markerIdx) + CODEX_MARKER.trimStart() + formatted
|
|
8066
|
-
: (isLegacyGenerated ? CODEX_MARKER.trimStart() + formatted : existing + CODEX_MARKER + formatted);
|
|
8067
|
-
if (!newContent.includes(CODEX_TOOLS_MARKER)) {
|
|
8068
|
-
const sigPos = newContent.indexOf('## Auto-generated signatures');
|
|
8069
|
-
if (sigPos !== -1) {
|
|
8070
|
-
newContent = newContent.slice(0, sigPos) + CODEX_TOOLS_BLOCK + '\n' + newContent.slice(sigPos);
|
|
8071
|
-
} else {
|
|
8072
|
-
newContent = CODEX_TOOLS_BLOCK + '\n' + newContent;
|
|
8073
|
-
}
|
|
8074
|
-
}
|
|
8075
|
-
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
8076
|
-
}
|
|
8077
|
-
module.exports = { name, format, outputPath, write };
|
|
8333
|
+
return header + context;
|
|
8334
|
+
}
|
|
8335
|
+
|
|
8336
|
+
function _confidenceMeta(opts) {
|
|
8337
|
+
const parts = [`version=${opts.version || 'unknown'}`];
|
|
8338
|
+
if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
|
|
8339
|
+
if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
|
|
8340
|
+
if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
|
|
8341
|
+
if (opts.commit) parts.push(`commit=${opts.commit}`);
|
|
8342
|
+
return `sigmap: ${parts.join(' ')}`;
|
|
8343
|
+
}
|
|
8344
|
+
|
|
8345
|
+
/**
|
|
8346
|
+
* Return the output file path for this adapter.
|
|
8347
|
+
* @param {string} cwd - Project root
|
|
8348
|
+
* @returns {string}
|
|
8349
|
+
*/
|
|
8350
|
+
function outputPath(cwd) {
|
|
8351
|
+
return path.join(cwd, '.windsurfrules');
|
|
8352
|
+
}
|
|
8353
|
+
|
|
8354
|
+
module.exports = { name, format, outputPath };
|
|
8355
|
+
|
|
8078
8356
|
};
|
|
8357
|
+
// ── ./packages/adapters/openai (bundled) ──
|
|
8358
|
+
__factories["./packages/adapters/openai"] = function(module, exports) {
|
|
8359
|
+
'use strict';
|
|
8079
8360
|
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8361
|
+
/**
|
|
8362
|
+
* OpenAI adapter — formats context as an OpenAI system message.
|
|
8363
|
+
* Use the output as the `content` field of a system role message.
|
|
8364
|
+
*
|
|
8365
|
+
* Example usage in code:
|
|
8366
|
+
* const { format } = require('sigmap/adapters/openai');
|
|
8367
|
+
* const systemPrompt = format(context);
|
|
8368
|
+
* // Pass to: openai.chat.completions.create({ messages: [{ role: 'system', content: systemPrompt }] })
|
|
8369
|
+
*
|
|
8370
|
+
* Contract:
|
|
8371
|
+
* format(context, opts?) → string
|
|
8372
|
+
* outputPath(cwd) → string
|
|
8373
|
+
*/
|
|
8374
|
+
|
|
8375
|
+
const path = require('path');
|
|
8376
|
+
|
|
8377
|
+
const name = 'openai';
|
|
8378
|
+
|
|
8379
|
+
/**
|
|
8380
|
+
* Format context as an OpenAI system prompt.
|
|
8381
|
+
* @param {string} context - Raw signature context string
|
|
8382
|
+
* @param {object} [opts]
|
|
8383
|
+
* @param {string} [opts.version] - SigMap version string
|
|
8384
|
+
* @param {string} [opts.projectName] - Optional project name
|
|
8385
|
+
* @returns {string}
|
|
8386
|
+
*/
|
|
8387
|
+
function format(context, opts = {}) {
|
|
8388
|
+
if (!context || typeof context !== 'string') return '';
|
|
8389
|
+
const version = opts.version || 'unknown';
|
|
8390
|
+
const timestamp = new Date().toISOString();
|
|
8391
|
+
const projectLine = opts.projectName
|
|
8392
|
+
? `Project: ${opts.projectName}\n`
|
|
8393
|
+
: '';
|
|
8394
|
+
|
|
8395
|
+
const meta = _confidenceMeta(opts);
|
|
8396
|
+
return [
|
|
8397
|
+
`You are a coding assistant with full knowledge of this codebase.`,
|
|
8398
|
+
`Below are the code signatures extracted by SigMap v${version} on ${timestamp}.`,
|
|
8399
|
+
`<!-- ${meta} -->`,
|
|
8400
|
+
projectLine,
|
|
8401
|
+
`Use these signatures to answer questions about the code accurately.`,
|
|
8402
|
+
`When the user asks about a specific file or function, refer to the signatures below.`,
|
|
8403
|
+
`## Code Signatures`,
|
|
8404
|
+
``,
|
|
8405
|
+
context,
|
|
8406
|
+
].join('\n');
|
|
8407
|
+
}
|
|
8408
|
+
|
|
8409
|
+
/**
|
|
8410
|
+
* Return the output file path for this adapter.
|
|
8411
|
+
* Writes a .openai-context.md file that can be loaded at runtime.
|
|
8412
|
+
* @param {string} cwd - Project root
|
|
8413
|
+
* @returns {string}
|
|
8414
|
+
*/
|
|
8415
|
+
function outputPath(cwd) {
|
|
8416
|
+
return path.join(cwd, '.github', 'openai-context.md');
|
|
8417
|
+
}
|
|
8418
|
+
|
|
8419
|
+
function _confidenceMeta(opts) {
|
|
8420
|
+
const parts = [`version=${opts.version || 'unknown'}`];
|
|
8421
|
+
if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
|
|
8422
|
+
if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
|
|
8423
|
+
if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
|
|
8424
|
+
if (opts.commit) parts.push(`commit=${opts.commit}`);
|
|
8425
|
+
return `sigmap: ${parts.join(' ')}`;
|
|
8426
|
+
}
|
|
8427
|
+
|
|
8428
|
+
module.exports = { name, format, outputPath };
|
|
8429
|
+
|
|
8430
|
+
};
|
|
8431
|
+
// ── ./packages/adapters/gemini (bundled) ──
|
|
8432
|
+
__factories["./packages/adapters/gemini"] = function(module, exports) {
|
|
8433
|
+
'use strict';
|
|
8434
|
+
|
|
8435
|
+
/**
|
|
8436
|
+
* Gemini adapter — formats context as a Gemini system instruction.
|
|
8437
|
+
* Use the output as the `system_instruction` field in a Gemini API request.
|
|
8438
|
+
*
|
|
8439
|
+
* Example usage:
|
|
8440
|
+
* const { format } = require('sigmap/adapters/gemini');
|
|
8441
|
+
* const instruction = format(context);
|
|
8442
|
+
* // Pass to: genAI.getGenerativeModel({ model: 'gemini-pro', systemInstruction: instruction })
|
|
8443
|
+
*
|
|
8444
|
+
* Contract:
|
|
8445
|
+
* format(context, opts?) → string
|
|
8446
|
+
* outputPath(cwd) → string
|
|
8447
|
+
*/
|
|
8448
|
+
|
|
8449
|
+
const path = require('path');
|
|
8450
|
+
const fs = require('fs');
|
|
8451
|
+
|
|
8452
|
+
const name = 'gemini';
|
|
8453
|
+
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
8454
|
+
|
|
8455
|
+
/**
|
|
8456
|
+
* Format context as a Gemini system instruction.
|
|
8457
|
+
* @param {string} context - Raw signature context string
|
|
8458
|
+
* @param {object} [opts]
|
|
8459
|
+
* @param {string} [opts.version] - SigMap version string
|
|
8460
|
+
* @param {string} [opts.projectName] - Optional project name
|
|
8461
|
+
* @returns {string}
|
|
8462
|
+
*/
|
|
8463
|
+
function format(context, opts = {}) {
|
|
8464
|
+
if (!context || typeof context !== 'string') return '';
|
|
8465
|
+
const version = opts.version || 'unknown';
|
|
8466
|
+
const timestamp = new Date().toISOString();
|
|
8467
|
+
const projectLine = opts.projectName
|
|
8468
|
+
? `Project: ${opts.projectName}\n`
|
|
8469
|
+
: '';
|
|
8470
|
+
|
|
8471
|
+
const meta = _confidenceMeta(opts);
|
|
8472
|
+
return [
|
|
8473
|
+
`You are a coding assistant with complete knowledge of this codebase.`,
|
|
8474
|
+
`The following code signatures were extracted by SigMap v${version} on ${timestamp}.`,
|
|
8475
|
+
`<!-- ${meta} -->`,
|
|
8476
|
+
projectLine,
|
|
8477
|
+
`These signatures represent every public function, class, and type in the project.`,
|
|
8478
|
+
`Refer to them when answering questions about code structure, APIs, and implementation.`,
|
|
8479
|
+
`## Code Signatures`,
|
|
8480
|
+
``,
|
|
8481
|
+
context,
|
|
8482
|
+
].join('\n');
|
|
8483
|
+
}
|
|
8484
|
+
|
|
8485
|
+
/**
|
|
8486
|
+
* Return the output file path for this adapter.
|
|
8487
|
+
* @param {string} cwd - Project root
|
|
8488
|
+
* @returns {string}
|
|
8489
|
+
*/
|
|
8490
|
+
function outputPath(cwd) {
|
|
8491
|
+
return path.join(cwd, '.github', 'gemini-context.md');
|
|
8492
|
+
}
|
|
8493
|
+
|
|
8494
|
+
/**
|
|
8495
|
+
* Write signatures into gemini-context.md using append-under-marker.
|
|
8496
|
+
* If marker exists, content above marker is preserved.
|
|
8497
|
+
* If legacy generated content exists without marker, replace it cleanly.
|
|
8498
|
+
* @param {string} context - Raw signature context string
|
|
8499
|
+
* @param {string} cwd - Project root
|
|
8500
|
+
* @param {object} [opts]
|
|
8501
|
+
*/
|
|
8502
|
+
function write(context, cwd, opts = {}) {
|
|
8503
|
+
const filePath = outputPath(cwd);
|
|
8504
|
+
let existing = '';
|
|
8505
|
+
if (fs.existsSync(filePath)) {
|
|
8506
|
+
existing = fs.readFileSync(filePath, 'utf8');
|
|
8507
|
+
}
|
|
8508
|
+
|
|
8509
|
+
const formatted = format(context, opts);
|
|
8510
|
+
const markerIdx = existing.indexOf('## Auto-generated signatures');
|
|
8511
|
+
|
|
8512
|
+
let newContent;
|
|
8513
|
+
if (markerIdx !== -1) {
|
|
8514
|
+
newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
|
|
8515
|
+
} else {
|
|
8516
|
+
const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
|
|
8517
|
+
|| existing.includes('## Code Signatures');
|
|
8518
|
+
newContent = isLegacyGenerated
|
|
8519
|
+
? MARKER.trimStart() + formatted
|
|
8520
|
+
: existing + MARKER + formatted;
|
|
8521
|
+
}
|
|
8522
|
+
|
|
8523
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
8524
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
8525
|
+
}
|
|
8526
|
+
|
|
8527
|
+
function _confidenceMeta(opts) {
|
|
8528
|
+
const parts = [`version=${opts.version || 'unknown'}`];
|
|
8529
|
+
if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
|
|
8530
|
+
if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
|
|
8531
|
+
if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
|
|
8532
|
+
if (opts.commit) parts.push(`commit=${opts.commit}`);
|
|
8533
|
+
return `sigmap: ${parts.join(' ')}`;
|
|
8534
|
+
}
|
|
8535
|
+
|
|
8536
|
+
module.exports = { name, format, outputPath, write };
|
|
8537
|
+
|
|
8538
|
+
};
|
|
8539
|
+
// ── ./packages/adapters/codex (bundled) ──
|
|
8540
|
+
__factories["./packages/adapters/codex"] = function(module, exports) {
|
|
8541
|
+
'use strict';
|
|
8542
|
+
|
|
8543
|
+
/**
|
|
8544
|
+
* Codex adapter — writes OpenAI-style context to AGENTS.md.
|
|
8545
|
+
*
|
|
8546
|
+
* This adapter reuses the same prompt format as the OpenAI adapter,
|
|
8547
|
+
* but targets AGENTS.md so Codex-style agents can read repository guidance.
|
|
8548
|
+
*
|
|
8549
|
+
* Contract:
|
|
8550
|
+
* format(context, opts?) → string
|
|
8551
|
+
* outputPath(cwd) → string
|
|
8552
|
+
* write(context, cwd, opts?) → void
|
|
8553
|
+
*/
|
|
8554
|
+
|
|
8555
|
+
const path = require('path');
|
|
8556
|
+
const fs = require('fs');
|
|
8557
|
+
|
|
8558
|
+
const name = 'codex';
|
|
8559
|
+
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
8560
|
+
|
|
8561
|
+
/**
|
|
8562
|
+
* Format context for AGENTS.md — clean markdown, no LLM preamble.
|
|
8563
|
+
* @param {string} context - Raw signature context string
|
|
8564
|
+
* @param {object} [opts]
|
|
8565
|
+
* @returns {string}
|
|
8566
|
+
*/
|
|
8567
|
+
function format(context, opts = {}) {
|
|
8568
|
+
if (!context || typeof context !== 'string' || !context.trim()) return '';
|
|
8569
|
+
return `# Code signatures\n\n${context}`;
|
|
8570
|
+
}
|
|
8571
|
+
|
|
8572
|
+
/**
|
|
8573
|
+
* Return the output file path for this adapter.
|
|
8574
|
+
* @param {string} cwd - Project root
|
|
8575
|
+
* @returns {string}
|
|
8576
|
+
*/
|
|
8577
|
+
function outputPath(cwd) {
|
|
8578
|
+
return path.join(cwd, 'AGENTS.md');
|
|
8579
|
+
}
|
|
8580
|
+
|
|
8581
|
+
/**
|
|
8582
|
+
* Write signatures into AGENTS.md using append-under-marker.
|
|
8583
|
+
* If marker exists, content above marker is preserved.
|
|
8584
|
+
* If legacy generated content exists without marker, replace it cleanly.
|
|
8585
|
+
* @param {string} context - Raw signature context string
|
|
8586
|
+
* @param {string} cwd - Project root
|
|
8587
|
+
* @param {object} [opts]
|
|
8588
|
+
*/
|
|
8589
|
+
function write(context, cwd, opts = {}) {
|
|
8590
|
+
const filePath = outputPath(cwd);
|
|
8591
|
+
let existing = '';
|
|
8592
|
+
if (fs.existsSync(filePath)) {
|
|
8593
|
+
existing = fs.readFileSync(filePath, 'utf8');
|
|
8594
|
+
}
|
|
8595
|
+
|
|
8596
|
+
const formatted = format(context, opts);
|
|
8597
|
+
const markerIdx = existing.indexOf('## Auto-generated signatures');
|
|
8598
|
+
|
|
8599
|
+
let newContent;
|
|
8600
|
+
if (markerIdx !== -1) {
|
|
8601
|
+
newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
|
|
8602
|
+
} else {
|
|
8603
|
+
const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
|
|
8604
|
+
|| existing.includes('## Code Signatures')
|
|
8605
|
+
|| existing.includes('# Code signatures');
|
|
8606
|
+
newContent = isLegacyGenerated
|
|
8607
|
+
? MARKER.trimStart() + formatted
|
|
8608
|
+
: existing + MARKER + formatted;
|
|
8609
|
+
}
|
|
8610
|
+
|
|
8611
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
8612
|
+
}
|
|
8613
|
+
|
|
8614
|
+
module.exports = { name, format, outputPath, write };
|
|
8615
|
+
|
|
8616
|
+
};
|
|
8617
|
+
// ── ./packages/adapters/index (bundled) ──
|
|
8618
|
+
__factories["./packages/adapters/index"] = function(module, exports) {
|
|
8619
|
+
const ADAPTER_NAMES = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
|
|
8620
|
+
const _adapterCache = {};
|
|
8621
|
+
function getAdapter(name) {
|
|
8622
|
+
if (!name || typeof name !== 'string') return null;
|
|
8623
|
+
const key = name.toLowerCase();
|
|
8624
|
+
if (!ADAPTER_NAMES.includes(key)) return null;
|
|
8625
|
+
if (!_adapterCache[key]) {
|
|
8626
|
+
try { _adapterCache[key] = __require('./packages/adapters/' + key); } catch (_) { return null; }
|
|
8627
|
+
}
|
|
8628
|
+
return _adapterCache[key];
|
|
8092
8629
|
}
|
|
8093
8630
|
function listAdapters() { return ADAPTER_NAMES.slice(); }
|
|
8094
8631
|
function adapt(context, adapterName, opts = {}) {
|
|
@@ -8877,108 +9414,1551 @@ __factories["./src/discovery/r-manifest"] = function(module, exports) {
|
|
|
8877
9414
|
}
|
|
8878
9415
|
};
|
|
8879
9416
|
|
|
8880
|
-
// ── ./src/workspace/detector ──
|
|
8881
|
-
__factories["./src/workspace/detector"] = function(module, exports) {
|
|
8882
|
-
'use strict';
|
|
8883
|
-
const fs = require('fs');
|
|
8884
|
-
const path = require('path');
|
|
8885
|
-
module.exports = { detectWorkspaces, inferPackage, scopeToPackage };
|
|
9417
|
+
// ── ./src/workspace/detector ──
|
|
9418
|
+
__factories["./src/workspace/detector"] = function(module, exports) {
|
|
9419
|
+
'use strict';
|
|
9420
|
+
const fs = require('fs');
|
|
9421
|
+
const path = require('path');
|
|
9422
|
+
module.exports = { detectWorkspaces, inferPackage, scopeToPackage };
|
|
9423
|
+
|
|
9424
|
+
function detectWorkspaces(cwd) {
|
|
9425
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
9426
|
+
if (!fs.existsSync(pkgPath)) return [];
|
|
9427
|
+
|
|
9428
|
+
let pkg;
|
|
9429
|
+
try {
|
|
9430
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
9431
|
+
} catch {
|
|
9432
|
+
return [];
|
|
9433
|
+
}
|
|
9434
|
+
|
|
9435
|
+
const patterns = pkg.workspaces || [];
|
|
9436
|
+
const dirs = [];
|
|
9437
|
+
|
|
9438
|
+
// Handle both flat array and object with packages field (Yarn v2 format)
|
|
9439
|
+
const patternArray = Array.isArray(patterns) ? patterns : (patterns.packages || []);
|
|
9440
|
+
|
|
9441
|
+
for (const p of patternArray) {
|
|
9442
|
+
const base = p.replace(/\/\*\*?$/, '');
|
|
9443
|
+
const resolved = path.join(cwd, base);
|
|
9444
|
+
if (fs.existsSync(resolved)) {
|
|
9445
|
+
try {
|
|
9446
|
+
for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
|
|
9447
|
+
if (entry.isDirectory()) dirs.push(path.join(resolved, entry.name));
|
|
9448
|
+
}
|
|
9449
|
+
} catch (_) {}
|
|
9450
|
+
}
|
|
9451
|
+
}
|
|
9452
|
+
|
|
9453
|
+
return dirs;
|
|
9454
|
+
}
|
|
9455
|
+
|
|
9456
|
+
// Infer package from query tokens: "add rate limiting to payments" → "packages/payments"
|
|
9457
|
+
function inferPackage(query, workspaceDirs, cwd) {
|
|
9458
|
+
const tokens = query.toLowerCase().split(/\W+/).filter(t => t.length > 2);
|
|
9459
|
+
|
|
9460
|
+
// Find longest matching package name
|
|
9461
|
+
let bestMatch = null;
|
|
9462
|
+
let bestLen = 0;
|
|
9463
|
+
let bestMatchLen = 0;
|
|
9464
|
+
|
|
9465
|
+
for (const dir of workspaceDirs) {
|
|
9466
|
+
const name = path.basename(dir).toLowerCase();
|
|
9467
|
+
for (const token of tokens) {
|
|
9468
|
+
const matchLen = _getMatchLength(name, token);
|
|
9469
|
+
// Only consider matches; use longest match, and break ties by longest package name
|
|
9470
|
+
if (matchLen > 0 && (matchLen > bestLen || (matchLen === bestLen && name.length > bestMatchLen))) {
|
|
9471
|
+
bestMatch = dir;
|
|
9472
|
+
bestLen = matchLen;
|
|
9473
|
+
bestMatchLen = name.length;
|
|
9474
|
+
}
|
|
9475
|
+
}
|
|
9476
|
+
}
|
|
9477
|
+
|
|
9478
|
+
return bestMatch;
|
|
9479
|
+
}
|
|
9480
|
+
|
|
9481
|
+
function _getMatchLength(name, token) {
|
|
9482
|
+
if (name === token) return 1000 + name.length; // Exact match is best
|
|
9483
|
+
if (name.startsWith(token) && token.length >= 3) return 100 + token.length;
|
|
9484
|
+
if (token.startsWith(name) && name.length >= 3) return name.length;
|
|
9485
|
+
return 0;
|
|
9486
|
+
}
|
|
9487
|
+
|
|
9488
|
+
// Return boost multiplier for files inside the inferred package
|
|
9489
|
+
function scopeToPackage(filePath, packageDir) {
|
|
9490
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
9491
|
+
const normalizedPkg = packageDir.replace(/\\/g, '/');
|
|
9492
|
+
|
|
9493
|
+
// Ensure we match the directory boundary, not just a prefix
|
|
9494
|
+
// e.g., packages/payment should not match packages/payment-old
|
|
9495
|
+
if (normalized.startsWith(normalizedPkg)) {
|
|
9496
|
+
const afterPrefix = normalized.slice(normalizedPkg.length);
|
|
9497
|
+
// Check if next char is / or if it's the exact match
|
|
9498
|
+
if (afterPrefix === '' || afterPrefix[0] === '/') {
|
|
9499
|
+
return 0.30;
|
|
9500
|
+
}
|
|
9501
|
+
}
|
|
9502
|
+
return 0;
|
|
9503
|
+
}
|
|
9504
|
+
};
|
|
9505
|
+
|
|
9506
|
+
/**
|
|
9507
|
+
* SigMap — gen-context.js v1.2.0
|
|
9508
|
+
* Zero-dependency AI context engine.
|
|
9509
|
+
* Runs with: node gen-context.js
|
|
9510
|
+
* No npm install required. Node 18+ built-ins only.
|
|
9511
|
+
*/
|
|
9512
|
+
|
|
9513
|
+
// ── ./src/format/usage-guidance ──
|
|
9514
|
+
__factories["./src/format/usage-guidance"] = function(module, exports) {
|
|
9515
|
+
'use strict';
|
|
9516
|
+
|
|
9517
|
+
/**
|
|
9518
|
+
* Canonical "how to use SigMap" guidance block (v6.16/v7.0).
|
|
9519
|
+
*
|
|
9520
|
+
* Every adapter emits this one identical block so all generated context files
|
|
9521
|
+
* (CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, GEMINI.md, .cursorrules,
|
|
9522
|
+
* …) carry the same, single usage section — instead of each adapter inventing
|
|
9523
|
+
* its own wording (and codex emitting a redundant second JSON block).
|
|
9524
|
+
*/
|
|
9525
|
+
|
|
9526
|
+
function usageBlock() {
|
|
9527
|
+
return [
|
|
9528
|
+
'## SigMap commands',
|
|
9529
|
+
'',
|
|
9530
|
+
'| When | Command |',
|
|
9531
|
+
'|------|---------|',
|
|
9532
|
+
'| Before answering a question about code | `sigmap ask "<your question>"` |',
|
|
9533
|
+
'| To rank files by topic | `sigmap --query "<topic>"` |',
|
|
9534
|
+
'| After changing config or source dirs | `sigmap validate` |',
|
|
9535
|
+
'| To verify an AI answer is grounded | `sigmap judge --response <file>` |',
|
|
9536
|
+
'',
|
|
9537
|
+
'Always run `sigmap ask` (or `sigmap --query`) before searching for files relevant to a task.',
|
|
9538
|
+
'',
|
|
9539
|
+
].join('\n');
|
|
9540
|
+
}
|
|
9541
|
+
|
|
9542
|
+
module.exports = { usageBlock };
|
|
9543
|
+
|
|
9544
|
+
};
|
|
9545
|
+
|
|
9546
|
+
// ── ./src/squeeze/classify ──
|
|
9547
|
+
__factories["./src/squeeze/classify"] = function(module, exports) {
|
|
9548
|
+
'use strict';
|
|
9549
|
+
|
|
9550
|
+
/**
|
|
9551
|
+
* Squeeze input classifier (v7.0.0).
|
|
9552
|
+
*
|
|
9553
|
+
* Deterministic, zero-dep detector that labels a pasted blob as a
|
|
9554
|
+
* `stacktrace`, `cilog`, or `json` payload — or `null` (pass through). Pure
|
|
9555
|
+
* regex/heuristics; runs in well under 10ms even on large input.
|
|
9556
|
+
*
|
|
9557
|
+
* Order matters: stack traces are highest value and a CI log often *contains*
|
|
9558
|
+
* a trace, so stacktrace is checked first, then cilog, then json.
|
|
9559
|
+
*/
|
|
9560
|
+
|
|
9561
|
+
const FRAME_RE = [
|
|
9562
|
+
/^\s*at\s+.+\(.+:\d+:\d+\)\s*$/, // JS: at fn (file:line:col)
|
|
9563
|
+
/^\s*at\s+.+:\d+:\d+\s*$/, // JS: at file:line:col
|
|
9564
|
+
/^\s*at\s+[\w$.<>]+\(.+\.\w+:\d+\)/, // Java/Kotlin: at pkg.Cls.m(File.java:42)
|
|
9565
|
+
/^\s*File\s+".+",\s+line\s+\d+/, // Python frame
|
|
9566
|
+
/^\s+\w+.*\([^)]*\.(go|rs):\d+\)/, // Go/Rust frame with file:line
|
|
9567
|
+
/^\s*#\d+\s+0x[0-9a-f]+/, // native/gdb frame
|
|
9568
|
+
];
|
|
9569
|
+
|
|
9570
|
+
const STACK_HEADER_RE = [
|
|
9571
|
+
/Traceback \(most recent call last\)/,
|
|
9572
|
+
/Exception in thread/,
|
|
9573
|
+
/\bat Object\.<anonymous>\b/,
|
|
9574
|
+
/thread '.*' panicked at/,
|
|
9575
|
+
/^panic:/m,
|
|
9576
|
+
/goroutine \d+ \[/,
|
|
9577
|
+
];
|
|
9578
|
+
|
|
9579
|
+
function countFrames(lines) {
|
|
9580
|
+
let n = 0;
|
|
9581
|
+
for (const line of lines) {
|
|
9582
|
+
if (FRAME_RE.some((re) => re.test(line))) n++;
|
|
9583
|
+
}
|
|
9584
|
+
return n;
|
|
9585
|
+
}
|
|
9586
|
+
|
|
9587
|
+
function matchesStackTrace(input, lines) {
|
|
9588
|
+
const frames = countFrames(lines);
|
|
9589
|
+
const header = STACK_HEADER_RE.some((re) => re.test(input));
|
|
9590
|
+
// A single explicit header (Traceback/panic) counts as strong evidence even
|
|
9591
|
+
// with few parsed frames; otherwise require 2+ frame-like lines.
|
|
9592
|
+
if (!header && frames < 2) return { match: false, confidence: 0, frames };
|
|
9593
|
+
// Confidence scales with frame count; a header alone floors it at 0.6.
|
|
9594
|
+
let confidence = Math.min(0.97, 0.15 * frames);
|
|
9595
|
+
if (header) confidence = Math.max(confidence, 0.6 + Math.min(0.3, 0.05 * frames));
|
|
9596
|
+
return { match: true, confidence: Number(confidence.toFixed(2)), frames };
|
|
9597
|
+
}
|
|
9598
|
+
|
|
9599
|
+
const TS_RE = /(\b\d{1,2}:\d{2}:\d{2}\b)|(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2})/;
|
|
9600
|
+
const PROGRESS_RE = /(\d{1,3}%)|(\bDownloading\b)|(\bnpm (WARN|notice|http)\b)|(##\[(group|endgroup|command)\])|(\[\d+\/\d+\])|(▕|█|━|⠿)|(\bETA\b)|(\r$)/;
|
|
9601
|
+
|
|
9602
|
+
function matchesCiLog(input, lines) {
|
|
9603
|
+
if (lines.length < 8) return { match: false, confidence: 0 };
|
|
9604
|
+
let logish = 0;
|
|
9605
|
+
const seen = new Map();
|
|
9606
|
+
let repeats = 0;
|
|
9607
|
+
for (const line of lines) {
|
|
9608
|
+
if (TS_RE.test(line) || PROGRESS_RE.test(line)) logish++;
|
|
9609
|
+
const norm = line.replace(/\d+/g, '#').trim();
|
|
9610
|
+
if (norm) {
|
|
9611
|
+
const c = (seen.get(norm) || 0) + 1;
|
|
9612
|
+
seen.set(norm, c);
|
|
9613
|
+
if (c > 1) repeats++;
|
|
9614
|
+
}
|
|
9615
|
+
}
|
|
9616
|
+
const density = logish / lines.length;
|
|
9617
|
+
const repeatRatio = repeats / lines.length;
|
|
9618
|
+
if (density < 0.4 && repeatRatio < 0.4) return { match: false, confidence: 0 };
|
|
9619
|
+
const confidence = Math.min(0.95, Math.max(density, repeatRatio) + 0.15);
|
|
9620
|
+
return { match: true, confidence: Number(confidence.toFixed(2)) };
|
|
9621
|
+
}
|
|
9622
|
+
|
|
9623
|
+
function matchesJsonPayload(input) {
|
|
9624
|
+
const trimmed = input.trim();
|
|
9625
|
+
if (/^[[{]/.test(trimmed)) {
|
|
9626
|
+
try {
|
|
9627
|
+
JSON.parse(trimmed);
|
|
9628
|
+
return { match: true, confidence: 0.95 };
|
|
9629
|
+
} catch (_) { /* not strict JSON — fall through to heuristic */ }
|
|
9630
|
+
}
|
|
9631
|
+
const lines = trimmed.split('\n');
|
|
9632
|
+
if (lines.length < 4) return { match: false, confidence: 0 };
|
|
9633
|
+
let kv = 0;
|
|
9634
|
+
for (const line of lines) {
|
|
9635
|
+
if (/^\s*"[^"]+"\s*:\s*.+/.test(line) || /^\s*[}\]],?\s*$/.test(line)) kv++;
|
|
9636
|
+
}
|
|
9637
|
+
const ratio = kv / lines.length;
|
|
9638
|
+
if (ratio < 0.6) return { match: false, confidence: 0 };
|
|
9639
|
+
return { match: true, confidence: Number(Math.min(0.9, ratio).toFixed(2)) };
|
|
9640
|
+
}
|
|
9641
|
+
|
|
9642
|
+
/**
|
|
9643
|
+
* @param {string} input
|
|
9644
|
+
* @returns {{ category: 'stacktrace'|'cilog'|'json'|null, confidence: number }}
|
|
9645
|
+
*/
|
|
9646
|
+
function classify(input) {
|
|
9647
|
+
if (typeof input !== 'string' || !input.trim()) return { category: null, confidence: 0 };
|
|
9648
|
+
const lines = input.split('\n');
|
|
9649
|
+
|
|
9650
|
+
const st = matchesStackTrace(input, lines);
|
|
9651
|
+
if (st.match) return { category: 'stacktrace', confidence: st.confidence };
|
|
9652
|
+
|
|
9653
|
+
const ci = matchesCiLog(input, lines);
|
|
9654
|
+
if (ci.match) return { category: 'cilog', confidence: ci.confidence };
|
|
9655
|
+
|
|
9656
|
+
const js = matchesJsonPayload(input);
|
|
9657
|
+
if (js.match) return { category: 'json', confidence: js.confidence };
|
|
9658
|
+
|
|
9659
|
+
return { category: null, confidence: 0 };
|
|
9660
|
+
}
|
|
9661
|
+
|
|
9662
|
+
module.exports = { classify, countFrames };
|
|
9663
|
+
|
|
9664
|
+
};
|
|
9665
|
+
|
|
9666
|
+
// ── ./src/squeeze/cilog ──
|
|
9667
|
+
__factories["./src/squeeze/cilog"] = function(module, exports) {
|
|
9668
|
+
'use strict';
|
|
9669
|
+
|
|
9670
|
+
/**
|
|
9671
|
+
* CI / build-log squeeze (v7.0.0).
|
|
9672
|
+
*
|
|
9673
|
+
* Strips timestamps, progress bars, and repeated noise; keeps every error line
|
|
9674
|
+
* plus a small context window around it. Never returns empty — when there are
|
|
9675
|
+
* no errors it falls back to a head/tail summary. Also reused by the stacktrace
|
|
9676
|
+
* squeezer to clean noise surrounding a trace.
|
|
9677
|
+
*/
|
|
9678
|
+
|
|
9679
|
+
const TS_PREFIX_RE = /^\s*(?:\[?\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?Z?\]?\s*|\[?\d{1,2}:\d{2}:\d{2}(?:[.,]\d+)?\]?\s*)+/;
|
|
9680
|
+
const PROGRESS_LINE_RE = /(?:^|\s)(?:\d{1,3}%|Downloading|Receiving objects|Resolving deltas|Compressing objects|npm (?:WARN|notice|http|sill|verb)|##\[(?:group|endgroup|command|section)\]|\[\d+\/\d+\]|ETA[: ]|█|━|▕|⣿)/;
|
|
9681
|
+
const ERROR_RE = /\b(?:error|err!|fail(?:ed|ure)?|exception|panic|fatal|traceback|ERR_|E[A-Z]{3,})\b|✗|❌/i;
|
|
9682
|
+
|
|
9683
|
+
/** Remove a leading timestamp prefix from a single line. */
|
|
9684
|
+
function stripTimestamp(line) {
|
|
9685
|
+
return line.replace(TS_PREFIX_RE, '');
|
|
9686
|
+
}
|
|
9687
|
+
|
|
9688
|
+
/**
|
|
9689
|
+
* @param {string} input
|
|
9690
|
+
* @param {object} [opts]
|
|
9691
|
+
* @param {number} [opts.context=2] context lines kept around each error
|
|
9692
|
+
* @returns {{ squeezed: string, kept: string[], stripped: string[] }}
|
|
9693
|
+
*/
|
|
9694
|
+
function squeezeCiLog(input, opts = {}) {
|
|
9695
|
+
const ctx = opts.context != null ? opts.context : 2;
|
|
9696
|
+
const lines = input.split('\n');
|
|
9697
|
+
const keep = new Set();
|
|
9698
|
+
const errorIdx = [];
|
|
9699
|
+
|
|
9700
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9701
|
+
if (ERROR_RE.test(lines[i])) {
|
|
9702
|
+
errorIdx.push(i);
|
|
9703
|
+
for (let j = Math.max(0, i - ctx); j <= Math.min(lines.length - 1, i + ctx); j++) keep.add(j);
|
|
9704
|
+
}
|
|
9705
|
+
}
|
|
9706
|
+
|
|
9707
|
+
let body;
|
|
9708
|
+
let keptDesc;
|
|
9709
|
+
if (errorIdx.length === 0) {
|
|
9710
|
+
const head = lines.slice(0, 10).map(stripTimestamp);
|
|
9711
|
+
const tail = lines.length > 20 ? lines.slice(-10).map(stripTimestamp) : [];
|
|
9712
|
+
body = head.slice();
|
|
9713
|
+
if (tail.length) body.push(`… (${lines.length - 20} lines omitted) …`, ...tail);
|
|
9714
|
+
keptDesc = ['head/tail summary (no error lines found)'];
|
|
9715
|
+
} else {
|
|
9716
|
+
const sorted = [...keep].sort((a, b) => a - b);
|
|
9717
|
+
body = [];
|
|
9718
|
+
let prev = -2;
|
|
9719
|
+
for (const idx of sorted) {
|
|
9720
|
+
// Drop pure progress/noise lines unless they are themselves errors.
|
|
9721
|
+
const line = stripTimestamp(lines[idx]);
|
|
9722
|
+
if (PROGRESS_LINE_RE.test(line) && !ERROR_RE.test(line)) continue;
|
|
9723
|
+
if (idx > prev + 1) body.push(`… (${idx - prev - 1} lines) …`);
|
|
9724
|
+
body.push(line);
|
|
9725
|
+
prev = idx;
|
|
9726
|
+
}
|
|
9727
|
+
if (body.length === 0) body = errorIdx.map((i) => stripTimestamp(lines[i])); // safety net
|
|
9728
|
+
keptDesc = [`${errorIdx.length} error line(s) + ${ctx}-line context`];
|
|
9729
|
+
}
|
|
9730
|
+
|
|
9731
|
+
return {
|
|
9732
|
+
squeezed: body.join('\n'),
|
|
9733
|
+
kept: keptDesc,
|
|
9734
|
+
stripped: [`${Math.max(0, lines.length - body.length)} timestamp/progress/noise line(s)`],
|
|
9735
|
+
};
|
|
9736
|
+
}
|
|
9737
|
+
|
|
9738
|
+
module.exports = { squeezeCiLog, stripTimestamp, ERROR_RE };
|
|
9739
|
+
|
|
9740
|
+
};
|
|
9741
|
+
|
|
9742
|
+
// ── ./src/squeeze/jsonpayload ──
|
|
9743
|
+
__factories["./src/squeeze/jsonpayload"] = function(module, exports) {
|
|
9744
|
+
'use strict';
|
|
9745
|
+
|
|
9746
|
+
/**
|
|
9747
|
+
* JSON-payload squeeze (v7.0.0).
|
|
9748
|
+
*
|
|
9749
|
+
* Collapses repeated array elements, truncates long string values, and
|
|
9750
|
+
* preserves the schema shape at every depth — so an LLM still sees the
|
|
9751
|
+
* structure of an API/GraphQL/validation error without the bulk.
|
|
9752
|
+
*/
|
|
9753
|
+
|
|
9754
|
+
const MAX_STR = 500;
|
|
9755
|
+
const ARRAY_KEEP = 2;
|
|
9756
|
+
|
|
9757
|
+
function squeezeValue(v, opts) {
|
|
9758
|
+
const maxStr = opts.maxStr;
|
|
9759
|
+
const keep = opts.arrayKeep;
|
|
9760
|
+
if (Array.isArray(v)) {
|
|
9761
|
+
if (v.length <= keep + 1) return v.map((x) => squeezeValue(x, opts));
|
|
9762
|
+
const head = v.slice(0, keep).map((x) => squeezeValue(x, opts));
|
|
9763
|
+
head.push(`…${v.length - keep} more similar items`);
|
|
9764
|
+
return head;
|
|
9765
|
+
}
|
|
9766
|
+
if (v && typeof v === 'object') {
|
|
9767
|
+
const o = {};
|
|
9768
|
+
for (const k of Object.keys(v)) o[k] = squeezeValue(v[k], opts);
|
|
9769
|
+
return o;
|
|
9770
|
+
}
|
|
9771
|
+
if (typeof v === 'string' && v.length > maxStr) {
|
|
9772
|
+
return v.slice(0, maxStr) + `…(${v.length} chars)`;
|
|
9773
|
+
}
|
|
9774
|
+
return v;
|
|
9775
|
+
}
|
|
9776
|
+
|
|
9777
|
+
/**
|
|
9778
|
+
* @param {string} input
|
|
9779
|
+
* @param {object} [opts]
|
|
9780
|
+
* @param {number} [opts.maxStr=500] truncate strings longer than this
|
|
9781
|
+
* @param {number} [opts.arrayKeep=2] array items kept before collapsing
|
|
9782
|
+
* @returns {{ squeezed, kept, stripped }}
|
|
9783
|
+
*/
|
|
9784
|
+
function squeezeJsonPayload(input, opts = {}) {
|
|
9785
|
+
let parsed;
|
|
9786
|
+
try { parsed = JSON.parse(input); }
|
|
9787
|
+
catch (_) { return { squeezed: input, kept: ['(not valid JSON — unchanged)'], stripped: [] }; }
|
|
9788
|
+
const cfg = { maxStr: opts.maxStr != null ? opts.maxStr : MAX_STR, arrayKeep: opts.arrayKeep != null ? opts.arrayKeep : ARRAY_KEEP };
|
|
9789
|
+
const squeezed = JSON.stringify(squeezeValue(parsed, cfg), null, 2);
|
|
9790
|
+
return {
|
|
9791
|
+
squeezed,
|
|
9792
|
+
kept: ['schema shape preserved at all depths'],
|
|
9793
|
+
stripped: ['collapsed repeated array items; truncated long string values'],
|
|
9794
|
+
};
|
|
9795
|
+
}
|
|
9796
|
+
|
|
9797
|
+
module.exports = { squeezeJsonPayload, squeezeValue };
|
|
9798
|
+
|
|
9799
|
+
};
|
|
9800
|
+
|
|
9801
|
+
// ── ./src/squeeze/stacktrace ──
|
|
9802
|
+
__factories["./src/squeeze/stacktrace"] = function(module, exports) {
|
|
9803
|
+
'use strict';
|
|
9804
|
+
|
|
9805
|
+
/**
|
|
9806
|
+
* Stack-trace squeeze (v7.0.0) — the highest-value squeeze module.
|
|
9807
|
+
*
|
|
9808
|
+
* Dedupes repeated exceptions, strips vendor frames, keeps frames in the user's
|
|
9809
|
+
* own source dirs, and — the differentiator — **enriches the top kept frame**
|
|
9810
|
+
* with its real signature from the SigMap symbol index (`buildSigIndex`).
|
|
9811
|
+
* Generic log summarizers can't do this; SigMap has the repo's symbol map.
|
|
9812
|
+
*
|
|
9813
|
+
* Pure/deterministic. The symbol index is injected via `opts.symbolIndex` so
|
|
9814
|
+
* the module is unit-testable without touching the filesystem.
|
|
9815
|
+
*/
|
|
9816
|
+
|
|
9817
|
+
const path = require('path');
|
|
9818
|
+
|
|
9819
|
+
const VENDOR_RE = /(?:^|[\\/])(?:node_modules|vendor|site-packages|dist|build|\.venv|venv|third_party|external|\.cargo|go\/pkg\/mod)[\\/]/;
|
|
9820
|
+
|
|
9821
|
+
/** Parse a frame line across JS/TS, Python, Java/Kotlin, Go, Rust, native. */
|
|
9822
|
+
function parseFrame(line) {
|
|
9823
|
+
let m;
|
|
9824
|
+
if ((m = line.match(/^\s*at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/))) return { fn: m[1], file: m[2], line: +m[3], raw: line };
|
|
9825
|
+
if ((m = line.match(/^\s*at\s+(.+?):(\d+):(\d+)\s*$/))) return { fn: '', file: m[1], line: +m[2], raw: line };
|
|
9826
|
+
if ((m = line.match(/^\s*at\s+([\w$.<>]+)\((.+?):(\d+)\)/))) return { fn: m[1], file: m[2], line: +m[3], raw: line };
|
|
9827
|
+
if ((m = line.match(/^\s*File\s+"(.+?)",\s+line\s+(\d+)(?:,\s+in\s+(.+))?/))) return { fn: (m[3] || '').trim(), file: m[1], line: +m[2], raw: line };
|
|
9828
|
+
if ((m = line.match(/^\s*(.+\.(?:go|rs)):(\d+)/))) return { fn: '', file: m[1], line: +m[2], raw: line };
|
|
9829
|
+
return null;
|
|
9830
|
+
}
|
|
9831
|
+
|
|
9832
|
+
function isVendor(file) { return VENDOR_RE.test(String(file).replace(/\\/g, '/')); }
|
|
9833
|
+
|
|
9834
|
+
function inSrcDirs(file, srcDirs) {
|
|
9835
|
+
const f = String(file).replace(/\\/g, '/');
|
|
9836
|
+
return srcDirs.some((d) => {
|
|
9837
|
+
const dd = String(d).replace(/^\.\//, '').replace(/\/$/, '');
|
|
9838
|
+
return dd && (f === dd || f.startsWith(dd + '/') || f.includes('/' + dd + '/'));
|
|
9839
|
+
});
|
|
9840
|
+
}
|
|
9841
|
+
|
|
9842
|
+
/** Look up the real signature for a frame in the SigMap symbol index. */
|
|
9843
|
+
function enrichFrame(frame, symbolIndex) {
|
|
9844
|
+
if (!symbolIndex || !frame) return null;
|
|
9845
|
+
const want = String(frame.file).replace(/\\/g, '/');
|
|
9846
|
+
const base = path.basename(want);
|
|
9847
|
+
let key = null;
|
|
9848
|
+
for (const k0 of symbolIndex.keys()) {
|
|
9849
|
+
const k = String(k0).replace(/\\/g, '/');
|
|
9850
|
+
if (k === want || want.endsWith('/' + k) || k.endsWith('/' + want)) { key = k0; break; }
|
|
9851
|
+
if (!key && path.basename(k) === base) key = k0;
|
|
9852
|
+
}
|
|
9853
|
+
if (!key) return null;
|
|
9854
|
+
const sigs = symbolIndex.get(key) || [];
|
|
9855
|
+
const wantFn = frame.fn ? frame.fn.split('.').pop() : '';
|
|
9856
|
+
let byLine = null, byName = null;
|
|
9857
|
+
for (const sig of sigs) {
|
|
9858
|
+
const s = String(sig);
|
|
9859
|
+
const mm = s.match(/:(\d+)(?:-(\d+))?\s*$/);
|
|
9860
|
+
if (mm) {
|
|
9861
|
+
const a = +mm[1], b = mm[2] ? +mm[2] : a;
|
|
9862
|
+
if (frame.line >= a && frame.line <= b) byLine = s;
|
|
9863
|
+
}
|
|
9864
|
+
if (wantFn && new RegExp('\\b' + wantFn.replace(/[^\w$]/g, '') + '\\b').test(s)) byName = byName || s;
|
|
9865
|
+
}
|
|
9866
|
+
const sig = byLine || byName;
|
|
9867
|
+
return sig ? { file: key, sig: sig.replace(/\s*:\d+(?:-\d+)?\s*$/, '').trim() } : null;
|
|
9868
|
+
}
|
|
9869
|
+
|
|
9870
|
+
/**
|
|
9871
|
+
* @param {string} input
|
|
9872
|
+
* @param {object} [opts]
|
|
9873
|
+
* @param {string[]} [opts.srcDirs] user source dirs (default ['src'])
|
|
9874
|
+
* @param {Map} [opts.symbolIndex] SigMap signature index for enrichment
|
|
9875
|
+
* @param {number} [opts.maxFrames=8] cap on kept source frames
|
|
9876
|
+
* @returns {{ squeezed, kept, stripped, enriched }}
|
|
9877
|
+
*/
|
|
9878
|
+
function squeezeStackTrace(input, opts = {}) {
|
|
9879
|
+
const srcDirs = (opts.srcDirs && opts.srcDirs.length) ? opts.srcDirs : ['src'];
|
|
9880
|
+
const maxFrames = opts.maxFrames != null ? opts.maxFrames : 8;
|
|
9881
|
+
const lines = input.split('\n');
|
|
9882
|
+
|
|
9883
|
+
const headerCount = new Map();
|
|
9884
|
+
const headerOrder = [];
|
|
9885
|
+
const frames = [];
|
|
9886
|
+
for (const line of lines) {
|
|
9887
|
+
const f = parseFrame(line);
|
|
9888
|
+
if (f) { frames.push(f); continue; }
|
|
9889
|
+
const t = line.trim();
|
|
9890
|
+
if (!t) continue;
|
|
9891
|
+
if (!headerCount.has(t)) headerOrder.push(t);
|
|
9892
|
+
headerCount.set(t, (headerCount.get(t) || 0) + 1);
|
|
9893
|
+
}
|
|
9894
|
+
|
|
9895
|
+
const seen = new Set();
|
|
9896
|
+
let dupFrames = 0;
|
|
9897
|
+
const nonVendor = [];
|
|
9898
|
+
const sourceFrames = [];
|
|
9899
|
+
let vendorCount = 0;
|
|
9900
|
+
for (const f of frames) {
|
|
9901
|
+
const k = f.file + ':' + f.line;
|
|
9902
|
+
if (seen.has(k)) { dupFrames++; continue; }
|
|
9903
|
+
seen.add(k);
|
|
9904
|
+
if (isVendor(f.file)) { vendorCount++; continue; }
|
|
9905
|
+
nonVendor.push(f);
|
|
9906
|
+
if (inSrcDirs(f.file, srcDirs)) sourceFrames.push(f);
|
|
9907
|
+
}
|
|
9908
|
+
|
|
9909
|
+
// Prefer source frames; never return empty (fall back to top non-vendor, then raw).
|
|
9910
|
+
const shown = sourceFrames.length ? sourceFrames.slice(0, maxFrames)
|
|
9911
|
+
: (nonVendor.length ? nonVendor.slice(0, 3) : frames.slice(0, 3));
|
|
9912
|
+
|
|
9913
|
+
const enrichment = shown.length ? enrichFrame(shown[0], opts.symbolIndex) : null;
|
|
9914
|
+
|
|
9915
|
+
const out = [];
|
|
9916
|
+
for (const h of headerOrder) {
|
|
9917
|
+
const n = headerCount.get(h);
|
|
9918
|
+
out.push(n > 1 ? `${h} (occurred ×${n})` : h);
|
|
9919
|
+
}
|
|
9920
|
+
for (let i = 0; i < shown.length; i++) {
|
|
9921
|
+
out.push(' ' + shown[i].raw.trim());
|
|
9922
|
+
if (i === 0 && enrichment) out.push(` ↳ ${enrichment.sig} [${enrichment.file}]`);
|
|
9923
|
+
}
|
|
9924
|
+
|
|
9925
|
+
return {
|
|
9926
|
+
squeezed: out.join('\n'),
|
|
9927
|
+
kept: [
|
|
9928
|
+
`${headerOrder.length} unique exception(s)`,
|
|
9929
|
+
`top ${shown.length} ${sourceFrames.length ? 'source ' : ''}frame(s)`,
|
|
9930
|
+
...(enrichment ? [`enriched ${path.basename(shown[0].file)}:${shown[0].line}`] : []),
|
|
9931
|
+
],
|
|
9932
|
+
stripped: [`${vendorCount} vendor frame(s)`, `${dupFrames} duplicate frame(s)`],
|
|
9933
|
+
enriched: !!enrichment,
|
|
9934
|
+
};
|
|
9935
|
+
}
|
|
9936
|
+
|
|
9937
|
+
module.exports = { squeezeStackTrace, parseFrame, isVendor, inSrcDirs, enrichFrame };
|
|
9938
|
+
|
|
9939
|
+
};
|
|
9940
|
+
|
|
9941
|
+
// ── ./src/squeeze/index ──
|
|
9942
|
+
__factories["./src/squeeze/index"] = function(module, exports) {
|
|
9943
|
+
'use strict';
|
|
9944
|
+
|
|
9945
|
+
/**
|
|
9946
|
+
* Squeeze orchestrator (v7.0.0): classify → squeeze → reduction → decision.
|
|
9947
|
+
*
|
|
9948
|
+
* Always-on and silent: callers run `squeeze()` on pasted input, then use
|
|
9949
|
+
* `shouldPrompt()` to decide whether the reduction is worth interrupting for.
|
|
9950
|
+
* Everything is deterministic and offline; the symbol index for stack-trace
|
|
9951
|
+
* enrichment is passed through via `opts.symbolIndex`.
|
|
9952
|
+
*/
|
|
9953
|
+
|
|
9954
|
+
const { classify } = __require('./src/squeeze/classify');
|
|
9955
|
+
const { squeezeStackTrace } = __require('./src/squeeze/stacktrace');
|
|
9956
|
+
const { squeezeCiLog } = __require('./src/squeeze/cilog');
|
|
9957
|
+
const { squeezeJsonPayload } = __require('./src/squeeze/jsonpayload');
|
|
9958
|
+
|
|
9959
|
+
function estimateTokens(s) { return Math.ceil(String(s || '').length / 4); }
|
|
9960
|
+
|
|
9961
|
+
/**
|
|
9962
|
+
* @param {string} input
|
|
9963
|
+
* @param {object} [opts] forwarded to the category squeezer (srcDirs, symbolIndex, …)
|
|
9964
|
+
* @returns {{ category, confidence, original, squeezed, rawTokens, squeezedTokens, reduction, kept, stripped, enriched, applies }}
|
|
9965
|
+
*/
|
|
9966
|
+
function squeeze(input, opts = {}) {
|
|
9967
|
+
const { category, confidence } = classify(input);
|
|
9968
|
+
const rawTokens = estimateTokens(input);
|
|
9969
|
+
const base = {
|
|
9970
|
+
category, confidence, original: input, squeezed: input,
|
|
9971
|
+
rawTokens, squeezedTokens: rawTokens, reduction: 0,
|
|
9972
|
+
kept: [], stripped: [], enriched: false, applies: false,
|
|
9973
|
+
};
|
|
9974
|
+
if (!category) return base;
|
|
9975
|
+
|
|
9976
|
+
let r;
|
|
9977
|
+
if (category === 'stacktrace') r = squeezeStackTrace(input, opts);
|
|
9978
|
+
else if (category === 'cilog') r = squeezeCiLog(input, opts);
|
|
9979
|
+
else r = squeezeJsonPayload(input, opts);
|
|
9980
|
+
|
|
9981
|
+
const squeezedTokens = estimateTokens(r.squeezed);
|
|
9982
|
+
const reduction = rawTokens > 0 ? (rawTokens - squeezedTokens) / rawTokens : 0;
|
|
9983
|
+
return {
|
|
9984
|
+
category, confidence,
|
|
9985
|
+
original: input, squeezed: r.squeezed,
|
|
9986
|
+
rawTokens, squeezedTokens,
|
|
9987
|
+
reduction: Math.max(0, Number(reduction.toFixed(4))),
|
|
9988
|
+
kept: r.kept || [], stripped: r.stripped || [], enriched: !!r.enriched,
|
|
9989
|
+
applies: squeezedTokens < rawTokens,
|
|
9990
|
+
};
|
|
9991
|
+
}
|
|
9992
|
+
|
|
9993
|
+
/** True when the reduction clears the threshold (accepts 0–1 or 0–100). */
|
|
9994
|
+
function shouldPrompt(reduction, threshold) {
|
|
9995
|
+
const t = threshold > 1 ? threshold / 100 : threshold;
|
|
9996
|
+
return reduction >= t;
|
|
9997
|
+
}
|
|
9998
|
+
|
|
9999
|
+
/** A compact human summary of what squeeze would do (for the prompt). */
|
|
10000
|
+
function formatSummary(result) {
|
|
10001
|
+
const pct = Math.round(result.reduction * 100);
|
|
10002
|
+
const lines = [
|
|
10003
|
+
`Input: ${result.rawTokens.toLocaleString()} tokens`,
|
|
10004
|
+
`Can reduce to ${result.squeezedTokens.toLocaleString()} tokens (${pct}% smaller):`,
|
|
10005
|
+
];
|
|
10006
|
+
for (const k of result.kept) lines.push(` ✓ Kept: ${k}`);
|
|
10007
|
+
for (const s of result.stripped) lines.push(` ✗ Stripped: ${s}`);
|
|
10008
|
+
return lines.join('\n');
|
|
10009
|
+
}
|
|
10010
|
+
|
|
10011
|
+
module.exports = { squeeze, shouldPrompt, formatSummary, estimateTokens };
|
|
10012
|
+
|
|
10013
|
+
};
|
|
10014
|
+
|
|
10015
|
+
// ── ./src/nudge ──
|
|
10016
|
+
__factories["./src/nudge"] = function(module, exports) {
|
|
10017
|
+
'use strict';
|
|
10018
|
+
|
|
10019
|
+
/**
|
|
10020
|
+
* Star nudge + usage tracking (v7.0.0).
|
|
10021
|
+
*
|
|
10022
|
+
* Records run counts in `.context/usage.json` and shows a one-time GitHub-star
|
|
10023
|
+
* message after the tool has been genuinely useful (≥10 runs, ≥8 successes).
|
|
10024
|
+
* Shown exactly once per machine — even under concurrent runs (an `wx` lock
|
|
10025
|
+
* file makes the show race-safe). Wired into `ask` (and the `squeeze` path).
|
|
10026
|
+
*/
|
|
10027
|
+
|
|
10028
|
+
const fs = require('fs');
|
|
10029
|
+
const path = require('path');
|
|
10030
|
+
const os = require('os');
|
|
10031
|
+
const crypto = require('crypto');
|
|
10032
|
+
|
|
10033
|
+
const RUN_THRESHOLD = 10;
|
|
10034
|
+
const SUCCESS_THRESHOLD = 8;
|
|
10035
|
+
|
|
10036
|
+
function usagePath(cwd) { return path.join(cwd, '.context', 'usage.json'); }
|
|
10037
|
+
|
|
10038
|
+
function defaultUsage() {
|
|
10039
|
+
return {
|
|
10040
|
+
totalRuns: 0, successfulRuns: 0, squeezeOffered: 0, squeezeAccepted: 0,
|
|
10041
|
+
starNudgeShown: false, machineId: '', firstRunDate: null, lastRunDate: null,
|
|
10042
|
+
};
|
|
10043
|
+
}
|
|
10044
|
+
|
|
10045
|
+
function readUsage(cwd) {
|
|
10046
|
+
try { return { ...defaultUsage(), ...JSON.parse(fs.readFileSync(usagePath(cwd), 'utf8')) }; }
|
|
10047
|
+
catch (_) { return defaultUsage(); }
|
|
10048
|
+
}
|
|
10049
|
+
|
|
10050
|
+
function writeUsageAtomic(cwd, usage) {
|
|
10051
|
+
const p = usagePath(cwd);
|
|
10052
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
10053
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
10054
|
+
fs.writeFileSync(tmp, JSON.stringify(usage, null, 2));
|
|
10055
|
+
fs.renameSync(tmp, p); // atomic on POSIX
|
|
10056
|
+
}
|
|
10057
|
+
|
|
10058
|
+
const STAR_MESSAGE = [
|
|
10059
|
+
'─────────────────────────────────────────────────────────',
|
|
10060
|
+
' SigMap has helped you 10 times now.',
|
|
10061
|
+
'',
|
|
10062
|
+
" If it's been useful, a GitHub star takes 5 seconds and",
|
|
10063
|
+
' helps other developers find it:',
|
|
10064
|
+
' → github.com/manojmallick/sigmap',
|
|
10065
|
+
'',
|
|
10066
|
+
" (Won't ask again. Press Enter to continue.)",
|
|
10067
|
+
'─────────────────────────────────────────────────────────',
|
|
10068
|
+
].join('\n');
|
|
10069
|
+
|
|
10070
|
+
function showStarNudge(write) {
|
|
10071
|
+
(write || ((s) => process.stderr.write(s)))('\n' + STAR_MESSAGE + '\n');
|
|
10072
|
+
}
|
|
10073
|
+
|
|
10074
|
+
/**
|
|
10075
|
+
* Record one run and, when the thresholds are first met, show the star nudge.
|
|
10076
|
+
* @param {string} cwd
|
|
10077
|
+
* @param {boolean} runSuccess
|
|
10078
|
+
* @param {object} [opts]
|
|
10079
|
+
* @param {boolean} [opts.silent] record only — never print
|
|
10080
|
+
* @param {function} [opts.write] sink for the message (default stderr)
|
|
10081
|
+
* @param {string} [opts.today] override date (testing)
|
|
10082
|
+
* @param {object} [opts.bump] counter deltas to merge (e.g. { squeezeAccepted: 1 })
|
|
10083
|
+
* @returns {{ usage, nudged }}
|
|
10084
|
+
*/
|
|
10085
|
+
function checkStarNudge(cwd, runSuccess, opts = {}) {
|
|
10086
|
+
const usage = readUsage(cwd);
|
|
10087
|
+
usage.totalRuns += 1;
|
|
10088
|
+
if (runSuccess) usage.successfulRuns += 1;
|
|
10089
|
+
if (opts.bump) for (const k of Object.keys(opts.bump)) usage[k] = (usage[k] || 0) + opts.bump[k];
|
|
10090
|
+
|
|
10091
|
+
const today = opts.today || new Date().toISOString().slice(0, 10);
|
|
10092
|
+
if (!usage.firstRunDate) usage.firstRunDate = today;
|
|
10093
|
+
usage.lastRunDate = today;
|
|
10094
|
+
if (!usage.machineId) {
|
|
10095
|
+
try { usage.machineId = 'sha256-' + crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 16); } catch (_) {}
|
|
10096
|
+
}
|
|
10097
|
+
|
|
10098
|
+
let nudged = false;
|
|
10099
|
+
if (!usage.starNudgeShown && usage.totalRuns >= RUN_THRESHOLD && usage.successfulRuns >= SUCCESS_THRESHOLD) {
|
|
10100
|
+
// Race-safe single-show: only the process that creates the lock prints.
|
|
10101
|
+
let won = false;
|
|
10102
|
+
try { fs.closeSync(fs.openSync(usagePath(cwd) + '.nudge.lock', 'wx')); won = true; }
|
|
10103
|
+
catch (_) { won = false; }
|
|
10104
|
+
if (won && !opts.silent) showStarNudge(opts.write);
|
|
10105
|
+
nudged = won;
|
|
10106
|
+
usage.starNudgeShown = true;
|
|
10107
|
+
}
|
|
10108
|
+
|
|
10109
|
+
writeUsageAtomic(cwd, usage);
|
|
10110
|
+
return { usage, nudged };
|
|
10111
|
+
}
|
|
10112
|
+
|
|
10113
|
+
module.exports = { checkStarNudge, readUsage, usagePath, showStarNudge, RUN_THRESHOLD, SUCCESS_THRESHOLD };
|
|
10114
|
+
|
|
10115
|
+
};
|
|
10116
|
+
|
|
10117
|
+
// ── ./src/verify/parsers ──
|
|
10118
|
+
__factories["./src/verify/parsers"] = function(module, exports) {
|
|
10119
|
+
'use strict';
|
|
10120
|
+
|
|
10121
|
+
/**
|
|
10122
|
+
* Parsers for the Hallucination Guard (verify-ai-output).
|
|
10123
|
+
*
|
|
10124
|
+
* Extract the verifiable claims an AI answer makes about a codebase:
|
|
10125
|
+
* - file paths it references
|
|
10126
|
+
* - import / require statements it shows
|
|
10127
|
+
* - function / class symbols it calls
|
|
10128
|
+
* - fenced code blocks (so callers can scope checks to code vs prose)
|
|
10129
|
+
*
|
|
10130
|
+
* Everything here is deterministic and offline — pure string analysis.
|
|
10131
|
+
*/
|
|
10132
|
+
|
|
10133
|
+
// Extensions we are confident name a source/code/config file (no slash required).
|
|
10134
|
+
const KNOWN_CODE_EXT = new Set([
|
|
10135
|
+
'js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'py', 'pyw', 'rb', 'go', 'rs',
|
|
10136
|
+
'java', 'kt', 'swift', 'c', 'h', 'cpp', 'hpp', 'cs', 'php', 'r',
|
|
10137
|
+
'vue', 'svelte', 'css', 'scss', 'less', 'html', 'json', 'yml', 'yaml',
|
|
10138
|
+
'toml', 'xml', 'sql', 'graphql', 'gql', 'proto', 'tf', 'md', 'sh',
|
|
10139
|
+
'gd', 'gdscript',
|
|
10140
|
+
]);
|
|
10141
|
+
|
|
10142
|
+
/**
|
|
10143
|
+
* Extract fenced code blocks.
|
|
10144
|
+
* @param {string} text
|
|
10145
|
+
* @returns {{ lang: string, content: string, line: number }[]}
|
|
10146
|
+
*/
|
|
10147
|
+
function extractCodeBlocks(text) {
|
|
10148
|
+
const blocks = [];
|
|
10149
|
+
const lines = text.split('\n');
|
|
10150
|
+
let inBlock = false;
|
|
10151
|
+
let lang = '';
|
|
10152
|
+
let buf = [];
|
|
10153
|
+
let startLine = 0;
|
|
10154
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10155
|
+
const m = lines[i].match(/^```(\w*)/);
|
|
10156
|
+
if (m) {
|
|
10157
|
+
if (!inBlock) {
|
|
10158
|
+
inBlock = true;
|
|
10159
|
+
lang = m[1] || '';
|
|
10160
|
+
buf = [];
|
|
10161
|
+
startLine = i + 2; // first content line (1-based)
|
|
10162
|
+
} else {
|
|
10163
|
+
blocks.push({ lang, content: buf.join('\n'), line: startLine });
|
|
10164
|
+
inBlock = false;
|
|
10165
|
+
}
|
|
10166
|
+
continue;
|
|
10167
|
+
}
|
|
10168
|
+
if (inBlock) buf.push(lines[i]);
|
|
10169
|
+
}
|
|
10170
|
+
return blocks;
|
|
10171
|
+
}
|
|
10172
|
+
|
|
10173
|
+
/**
|
|
10174
|
+
* Extract file-path references (deduped, first-seen line kept).
|
|
10175
|
+
* A token counts as a path when it has a `.<letter…>` extension AND
|
|
10176
|
+
* either contains a `/` or carries a known code/config extension.
|
|
10177
|
+
* @param {string} text
|
|
10178
|
+
* @returns {{ path: string, line: number }[]}
|
|
10179
|
+
*/
|
|
10180
|
+
function extractFilePaths(text) {
|
|
10181
|
+
const lines = text.split('\n');
|
|
10182
|
+
const seen = new Map();
|
|
10183
|
+
const re = /(?:^|[\s`"'(\[<])([A-Za-z0-9_][\w./-]*\.[A-Za-z][A-Za-z0-9]*)/g;
|
|
10184
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10185
|
+
const line = lines[i];
|
|
10186
|
+
let m;
|
|
10187
|
+
re.lastIndex = 0;
|
|
10188
|
+
while ((m = re.exec(line)) !== null) {
|
|
10189
|
+
const p = m[1];
|
|
10190
|
+
if (/^https?:/i.test(p)) continue;
|
|
10191
|
+
const ext = (p.split('.').pop() || '').toLowerCase();
|
|
10192
|
+
const hasSlash = p.includes('/');
|
|
10193
|
+
if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
|
|
10194
|
+
if (!seen.has(p)) seen.set(p, i + 1);
|
|
10195
|
+
}
|
|
10196
|
+
}
|
|
10197
|
+
return [...seen.entries()].map(([p, line]) => ({ path: p, line }));
|
|
10198
|
+
}
|
|
10199
|
+
|
|
10200
|
+
/**
|
|
10201
|
+
* Extract import / require statements.
|
|
10202
|
+
* @param {string} text
|
|
10203
|
+
* @returns {{ module: string, kind: 'js'|'py', relative: boolean, line: number, raw: string }[]}
|
|
10204
|
+
*/
|
|
10205
|
+
function extractImports(text) {
|
|
10206
|
+
const lines = text.split('\n');
|
|
10207
|
+
const out = [];
|
|
10208
|
+
const push = (module, kind, line, raw) => {
|
|
10209
|
+
if (!module) return;
|
|
10210
|
+
out.push({ module, kind, relative: /^[./]/.test(module), line, raw: raw.trim() });
|
|
10211
|
+
};
|
|
10212
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10213
|
+
const line = lines[i];
|
|
10214
|
+
let m;
|
|
10215
|
+
// JS/TS: import ... from 'x' | export ... from 'x'
|
|
10216
|
+
if ((m = line.match(/\b(?:import|export)\b[^'"]*\bfrom\s*['"]([^'"]+)['"]/))) {
|
|
10217
|
+
push(m[1], 'js', i + 1, line);
|
|
10218
|
+
} else if ((m = line.match(/\bimport\s*['"]([^'"]+)['"]/))) {
|
|
10219
|
+
// side-effect import 'x'
|
|
10220
|
+
push(m[1], 'js', i + 1, line);
|
|
10221
|
+
}
|
|
10222
|
+
// require('x') / dynamic import('x') — may co-occur, scan separately
|
|
10223
|
+
const reqRe = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
10224
|
+
let r;
|
|
10225
|
+
while ((r = reqRe.exec(line)) !== null) push(r[1], 'js', i + 1, line);
|
|
10226
|
+
|
|
10227
|
+
// TS: import X = require('mod')
|
|
10228
|
+
if ((m = line.match(/\bimport\s+[A-Za-z_$][\w$]*\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/))) {
|
|
10229
|
+
push(m[1], 'js', i + 1, line);
|
|
10230
|
+
}
|
|
10231
|
+
|
|
10232
|
+
// Python: from x import y | import x
|
|
10233
|
+
if ((m = line.match(/^\s*from\s+([.\w]+)\s+import\b/))) {
|
|
10234
|
+
push(m[1], 'py', i + 1, line);
|
|
10235
|
+
} else if ((m = line.match(/^\s*import\s+([A-Za-z_][\w.]*)/))) {
|
|
10236
|
+
push(m[1], 'py', i + 1, line);
|
|
10237
|
+
}
|
|
10238
|
+
}
|
|
10239
|
+
|
|
10240
|
+
// Multi-line JS/TS imports, e.g.
|
|
10241
|
+
// import {
|
|
10242
|
+
// A as B,
|
|
10243
|
+
// } from './mod';
|
|
10244
|
+
// The per-line pass above misses these because `from '…'` sits on a later
|
|
10245
|
+
// line. Trigger only when the opening line has no quote and no `from` yet,
|
|
10246
|
+
// then gather forward until the source string appears.
|
|
10247
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10248
|
+
const start = lines[i];
|
|
10249
|
+
if (!/^\s*(?:import|export)\b/.test(start)) continue;
|
|
10250
|
+
if (/['"]/.test(start) || /\bfrom\b/.test(start)) continue; // single-line, already handled
|
|
10251
|
+
let joined = start;
|
|
10252
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 12); j++) {
|
|
10253
|
+
joined += ' ' + lines[j];
|
|
10254
|
+
const fm = joined.match(/\bfrom\s*['"]([^'"]+)['"]/);
|
|
10255
|
+
if (fm) { push(fm[1], 'js', i + 1, start.trim()); break; }
|
|
10256
|
+
if (/['"]/.test(lines[j]) && !/\bfrom\b/.test(joined)) break; // a string that isn't a source — bail
|
|
10257
|
+
}
|
|
10258
|
+
}
|
|
10259
|
+
return out;
|
|
10260
|
+
}
|
|
10261
|
+
|
|
10262
|
+
/**
|
|
10263
|
+
* Extract npm/pnpm/yarn script invocations (`npm run <name>`).
|
|
10264
|
+
* Only the explicit `run` form is matched, to avoid confusing package-manager
|
|
10265
|
+
* subcommands (`yarn add`, `pnpm install`) with script names.
|
|
10266
|
+
* @param {string} text
|
|
10267
|
+
* @returns {{ name: string, line: number }[]}
|
|
10268
|
+
*/
|
|
10269
|
+
function extractNpmScripts(text) {
|
|
10270
|
+
const lines = text.split('\n');
|
|
10271
|
+
const out = [];
|
|
10272
|
+
const seen = new Set();
|
|
10273
|
+
const re = /\b(?:npm|pnpm|yarn)\s+run(?:-script)?\s+([A-Za-z0-9:_-]+)/g;
|
|
10274
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10275
|
+
let m;
|
|
10276
|
+
re.lastIndex = 0;
|
|
10277
|
+
while ((m = re.exec(lines[i])) !== null) {
|
|
10278
|
+
const name = m[1];
|
|
10279
|
+
if (seen.has(name)) continue;
|
|
10280
|
+
seen.add(name);
|
|
10281
|
+
out.push({ name, line: i + 1 });
|
|
10282
|
+
}
|
|
10283
|
+
}
|
|
10284
|
+
return out;
|
|
10285
|
+
}
|
|
10286
|
+
|
|
10287
|
+
/**
|
|
10288
|
+
* Extract function/class symbol references that look like calls.
|
|
10289
|
+
* Restricted to backtick-wrapped calls (`foo(...)`) for high precision.
|
|
10290
|
+
* @param {string} text
|
|
10291
|
+
* @returns {{ name: string, line: number }[]}
|
|
10292
|
+
*/
|
|
10293
|
+
function extractSymbols(text) {
|
|
10294
|
+
const lines = text.split('\n');
|
|
10295
|
+
const out = [];
|
|
10296
|
+
const seen = new Set();
|
|
10297
|
+
const re = /`([A-Za-z_$][\w$]*)\s*\([^`]*\)`/g;
|
|
10298
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10299
|
+
let m;
|
|
10300
|
+
re.lastIndex = 0;
|
|
10301
|
+
while ((m = re.exec(lines[i])) !== null) {
|
|
10302
|
+
const name = m[1];
|
|
10303
|
+
const key = name + '@' + (i + 1);
|
|
10304
|
+
if (seen.has(key)) continue;
|
|
10305
|
+
seen.add(key);
|
|
10306
|
+
out.push({ name, line: i + 1 });
|
|
10307
|
+
}
|
|
10308
|
+
}
|
|
10309
|
+
return out;
|
|
10310
|
+
}
|
|
10311
|
+
|
|
10312
|
+
module.exports = {
|
|
10313
|
+
extractCodeBlocks,
|
|
10314
|
+
extractFilePaths,
|
|
10315
|
+
extractImports,
|
|
10316
|
+
extractSymbols,
|
|
10317
|
+
extractNpmScripts,
|
|
10318
|
+
};
|
|
10319
|
+
|
|
10320
|
+
};
|
|
10321
|
+
|
|
10322
|
+
// ── ./src/verify/closest-match ──
|
|
10323
|
+
__factories["./src/verify/closest-match"] = function(module, exports) {
|
|
10324
|
+
'use strict';
|
|
10325
|
+
|
|
10326
|
+
/**
|
|
10327
|
+
* Closest-match suggestions for the Hallucination Guard (v6.15.0).
|
|
10328
|
+
*
|
|
10329
|
+
* Heuristic layer (plan §5 labels this "Medium" confidence): given a name the
|
|
10330
|
+
* detectors flagged as fake, find the nearest *real* candidate by Levenshtein
|
|
10331
|
+
* distance so the report can say "Did you mean `loadConfig()` in
|
|
10332
|
+
* src/config/loader.js:42?". Pure, deterministic, offline — no network, no LLM.
|
|
10333
|
+
*
|
|
10334
|
+
* All inputs are passed in (symbol/file/script candidate lists) so this module
|
|
10335
|
+
* stays unit-testable without touching the filesystem or the SigMap index.
|
|
10336
|
+
*/
|
|
10337
|
+
|
|
10338
|
+
/**
|
|
10339
|
+
* Levenshtein edit distance with an early-exit ceiling.
|
|
10340
|
+
* Returns `max + 1` as soon as the best achievable distance exceeds `max`,
|
|
10341
|
+
* so callers can cheaply reject far-apart strings.
|
|
10342
|
+
*/
|
|
10343
|
+
function levenshtein(a, b, max = Infinity) {
|
|
10344
|
+
if (a === b) return 0;
|
|
10345
|
+
const al = a.length;
|
|
10346
|
+
const bl = b.length;
|
|
10347
|
+
if (al === 0) return bl;
|
|
10348
|
+
if (bl === 0) return al;
|
|
10349
|
+
if (Math.abs(al - bl) > max) return max + 1;
|
|
10350
|
+
|
|
10351
|
+
let prev = new Array(bl + 1);
|
|
10352
|
+
let curr = new Array(bl + 1);
|
|
10353
|
+
for (let j = 0; j <= bl; j++) prev[j] = j;
|
|
10354
|
+
|
|
10355
|
+
for (let i = 1; i <= al; i++) {
|
|
10356
|
+
curr[0] = i;
|
|
10357
|
+
let rowMin = curr[0];
|
|
10358
|
+
const ca = a.charCodeAt(i - 1);
|
|
10359
|
+
for (let j = 1; j <= bl; j++) {
|
|
10360
|
+
const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
|
|
10361
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
10362
|
+
if (curr[j] < rowMin) rowMin = curr[j];
|
|
10363
|
+
}
|
|
10364
|
+
if (rowMin > max) return max + 1;
|
|
10365
|
+
const tmp = prev;
|
|
10366
|
+
prev = curr;
|
|
10367
|
+
curr = tmp;
|
|
10368
|
+
}
|
|
10369
|
+
return prev[bl];
|
|
10370
|
+
}
|
|
10371
|
+
|
|
10372
|
+
/** Bucket a normalized edit distance into a confidence label (plan §5). */
|
|
10373
|
+
function suggestionConfidence(distance, targetLen) {
|
|
10374
|
+
const ratio = distance / Math.max(targetLen, 1);
|
|
10375
|
+
if (distance === 0 || ratio <= 0.2) return 'high';
|
|
10376
|
+
if (ratio <= 0.4) return 'medium';
|
|
10377
|
+
return 'low';
|
|
10378
|
+
}
|
|
10379
|
+
|
|
10380
|
+
/**
|
|
10381
|
+
* Find the nearest candidate name to `target`.
|
|
10382
|
+
*
|
|
10383
|
+
* @param {string} target
|
|
10384
|
+
* @param {Array<string | { name: string, file?: string, line?: number }>} candidates
|
|
10385
|
+
* @param {object} [opts]
|
|
10386
|
+
* @param {number} [opts.maxRatio=0.5] reject matches farther than ratio·len edits
|
|
10387
|
+
* @param {number} [opts.minLen=3] skip very short targets (too noisy)
|
|
10388
|
+
* @returns {{ name, file, line, distance, confidence } | null}
|
|
10389
|
+
*/
|
|
10390
|
+
function closestMatch(target, candidates, opts = {}) {
|
|
10391
|
+
const maxRatio = opts.maxRatio != null ? opts.maxRatio : 0.5;
|
|
10392
|
+
const minLen = opts.minLen != null ? opts.minLen : 3;
|
|
10393
|
+
if (!target || target.length < minLen) return null;
|
|
10394
|
+
if (!candidates || candidates.length === 0) return null;
|
|
10395
|
+
|
|
10396
|
+
const lower = target.toLowerCase();
|
|
10397
|
+
const cap = Math.max(1, Math.ceil(target.length * maxRatio));
|
|
10398
|
+
let best = null;
|
|
10399
|
+
|
|
10400
|
+
for (const c of candidates) {
|
|
10401
|
+
const name = typeof c === 'string' ? c : c && c.name;
|
|
10402
|
+
if (!name || name === target) continue;
|
|
10403
|
+
if (Math.abs(name.length - target.length) > cap) continue;
|
|
10404
|
+
const d = levenshtein(lower, name.toLowerCase(), cap);
|
|
10405
|
+
if (d > cap) continue;
|
|
10406
|
+
if (!best || d < best.distance ||
|
|
10407
|
+
(d === best.distance && name.length < best.name.length)) {
|
|
10408
|
+
best = {
|
|
10409
|
+
name,
|
|
10410
|
+
file: typeof c === 'object' ? c.file : undefined,
|
|
10411
|
+
line: typeof c === 'object' ? c.line : undefined,
|
|
10412
|
+
distance: d,
|
|
10413
|
+
};
|
|
10414
|
+
if (d === 0) break; // case-only difference — can't beat it
|
|
10415
|
+
}
|
|
10416
|
+
}
|
|
10417
|
+
|
|
10418
|
+
if (!best) return null;
|
|
10419
|
+
best.confidence = suggestionConfidence(best.distance, target.length);
|
|
10420
|
+
return best;
|
|
10421
|
+
}
|
|
10422
|
+
|
|
10423
|
+
/**
|
|
10424
|
+
* Build `[{ name, file, line }]` symbol candidates from a SigMap signature
|
|
10425
|
+
* index (`Map<file, string[]>` whose entries may carry a `:start-end` anchor).
|
|
10426
|
+
*/
|
|
10427
|
+
function buildSymbolCandidates(sigIndex) {
|
|
10428
|
+
const out = [];
|
|
10429
|
+
const seen = new Set();
|
|
10430
|
+
if (!sigIndex) return out;
|
|
10431
|
+
for (const [file, sigs] of sigIndex) {
|
|
10432
|
+
for (const sig of sigs) {
|
|
10433
|
+
const s = String(sig);
|
|
10434
|
+
const lineM = s.match(/:(\d+)(?:-\d+)?\s*$/);
|
|
10435
|
+
const line = lineM ? parseInt(lineM[1], 10) : null;
|
|
10436
|
+
const cleaned = s.replace(/\s*:\d+(?:-\d+)?\s*$/, '');
|
|
10437
|
+
const m = cleaned.match(/\b(?:async\s+function|function|class|def|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/)
|
|
10438
|
+
|| cleaned.match(/([A-Za-z_$][\w$]*)\s*\(/)
|
|
10439
|
+
|| cleaned.match(/([A-Za-z_$][\w$]*)/);
|
|
10440
|
+
if (!m) continue;
|
|
10441
|
+
const name = m[1];
|
|
10442
|
+
const key = name + '@' + file;
|
|
10443
|
+
if (seen.has(key)) continue;
|
|
10444
|
+
seen.add(key);
|
|
10445
|
+
out.push({ name, file, line });
|
|
10446
|
+
}
|
|
10447
|
+
}
|
|
10448
|
+
return out;
|
|
10449
|
+
}
|
|
10450
|
+
|
|
10451
|
+
/** Format a suggestion object into a human one-liner for reports/CLI. */
|
|
10452
|
+
function formatSuggestion(match, asCall) {
|
|
10453
|
+
if (!match) return null;
|
|
10454
|
+
const sym = asCall ? `${match.name}()` : match.name;
|
|
10455
|
+
let where = '';
|
|
10456
|
+
if (match.file) {
|
|
10457
|
+
where = match.line ? ` in ${match.file}:${match.line}` : ` in ${match.file}`;
|
|
10458
|
+
}
|
|
10459
|
+
return `Did you mean \`${sym}\`${where}?`;
|
|
10460
|
+
}
|
|
10461
|
+
|
|
10462
|
+
module.exports = {
|
|
10463
|
+
levenshtein,
|
|
10464
|
+
closestMatch,
|
|
10465
|
+
buildSymbolCandidates,
|
|
10466
|
+
suggestionConfidence,
|
|
10467
|
+
formatSuggestion,
|
|
10468
|
+
};
|
|
10469
|
+
|
|
10470
|
+
};
|
|
10471
|
+
|
|
10472
|
+
// ── ./src/format/verify-report ──
|
|
10473
|
+
__factories["./src/format/verify-report"] = function(module, exports) {
|
|
10474
|
+
'use strict';
|
|
10475
|
+
|
|
10476
|
+
/**
|
|
10477
|
+
* Hallucination Guard report view (Surface A, v6.15.0).
|
|
10478
|
+
*
|
|
10479
|
+
* Turns the `verify-ai-output --json` result into a standalone, self-contained
|
|
10480
|
+
* HTML report — red/amber/green per issue, with closest-match suggestions
|
|
10481
|
+
* inline. The visual language deliberately mirrors the planned PR-comment
|
|
10482
|
+
* styling so a single screenshot is reusable across docs and CI (plan proof #5).
|
|
10483
|
+
*
|
|
10484
|
+
* Zero dependencies, inline CSS/SVG, no external assets. Also exports a compact
|
|
10485
|
+
* Markdown renderer for CI / PR comments that shares the same structure.
|
|
10486
|
+
*/
|
|
8886
10487
|
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
10488
|
+
const TYPE_META = {
|
|
10489
|
+
'fake-file': { label: 'Fake file', tone: 'red', icon: '✕' },
|
|
10490
|
+
'fake-test-file': { label: 'Fake test file', tone: 'red', icon: '✕' },
|
|
10491
|
+
'fake-import': { label: 'Fake import', tone: 'red', icon: '✕' },
|
|
10492
|
+
'fake-npm-script': { label: 'Fake npm script', tone: 'red', icon: '✕' },
|
|
10493
|
+
'fake-symbol': { label: 'Fake symbol', tone: 'amber', icon: '!' },
|
|
10494
|
+
};
|
|
8890
10495
|
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
10496
|
+
function escapeHtml(value) {
|
|
10497
|
+
return String(value == null ? '' : value)
|
|
10498
|
+
.replace(/&/g, '&')
|
|
10499
|
+
.replace(/</g, '<')
|
|
10500
|
+
.replace(/>/g, '>')
|
|
10501
|
+
.replace(/"/g, '"');
|
|
10502
|
+
}
|
|
8897
10503
|
|
|
8898
|
-
|
|
8899
|
-
|
|
10504
|
+
function toneFor(issue) {
|
|
10505
|
+
const meta = TYPE_META[issue.type];
|
|
10506
|
+
if (meta) return meta.tone;
|
|
10507
|
+
return issue.confidence === 'high' ? 'red' : 'amber';
|
|
10508
|
+
}
|
|
8900
10509
|
|
|
8901
|
-
|
|
8902
|
-
|
|
10510
|
+
function labelFor(issue) {
|
|
10511
|
+
return (TYPE_META[issue.type] && TYPE_META[issue.type].label) || issue.type;
|
|
10512
|
+
}
|
|
8903
10513
|
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
10514
|
+
/**
|
|
10515
|
+
* Render the verify result to a full HTML document.
|
|
10516
|
+
* @param {{ file?: string, issues: object[], summary: object }} result
|
|
10517
|
+
* @param {object} [opts]
|
|
10518
|
+
* @param {string} [opts.title]
|
|
10519
|
+
* @returns {string} HTML
|
|
10520
|
+
*/
|
|
10521
|
+
function renderReportHtml(result, opts = {}) {
|
|
10522
|
+
const issues = Array.isArray(result.issues) ? result.issues : [];
|
|
10523
|
+
const summary = result.summary || { total: issues.length, byType: {}, clean: issues.length === 0 };
|
|
10524
|
+
const file = result.file || opts.file || 'AI answer';
|
|
10525
|
+
const title = opts.title || 'SigMap — Hallucination Guard report';
|
|
10526
|
+
const clean = summary.clean || issues.length === 0;
|
|
10527
|
+
const byType = summary.byType || {};
|
|
10528
|
+
|
|
10529
|
+
const chips = Object.keys(TYPE_META)
|
|
10530
|
+
.filter((t) => byType[t])
|
|
10531
|
+
.map((t) => `<span class="chip chip-${TYPE_META[t].tone}">${escapeHtml(TYPE_META[t].label)}: ${byType[t]}</span>`)
|
|
10532
|
+
.join('');
|
|
10533
|
+
|
|
10534
|
+
const banner = clean
|
|
10535
|
+
? `<div class="banner banner-green"><span class="dot"></span> No hallucinations detected — ${escapeHtml(String(summary.symbolsIndexed || 0))} symbols indexed</div>`
|
|
10536
|
+
: `<div class="banner banner-red"><span class="dot"></span> ${issues.length} issue${issues.length === 1 ? '' : 's'} found in <code>${escapeHtml(file)}</code></div>`;
|
|
10537
|
+
|
|
10538
|
+
const rows = issues.map((issue) => {
|
|
10539
|
+
const tone = toneFor(issue);
|
|
10540
|
+
const sugg = issue.suggestion
|
|
10541
|
+
? `<div class="suggestion">↳ ${escapeHtml(issue.suggestion)} <span class="conf">heuristic</span></div>`
|
|
10542
|
+
: '';
|
|
10543
|
+
return [
|
|
10544
|
+
`<li class="issue issue-${tone}">`,
|
|
10545
|
+
` <div class="issue-head">`,
|
|
10546
|
+
` <span class="badge badge-${tone}">${escapeHtml(labelFor(issue))}</span>`,
|
|
10547
|
+
` <span class="loc">${escapeHtml(issue.location || ('L' + issue.line))}</span>`,
|
|
10548
|
+
` <span class="conf conf-${escapeHtml(issue.confidence || 'high')}">${escapeHtml(issue.confidence || 'high')} confidence</span>`,
|
|
10549
|
+
` </div>`,
|
|
10550
|
+
` <div class="msg">${escapeHtml(issue.message || issue.value)}</div>`,
|
|
10551
|
+
` ${sugg}`,
|
|
10552
|
+
`</li>`,
|
|
10553
|
+
].join('\n');
|
|
10554
|
+
}).join('\n');
|
|
10555
|
+
|
|
10556
|
+
const list = clean
|
|
10557
|
+
? '<p class="empty">Nothing to report — every file, import, symbol, and script in the answer resolves against the repository.</p>'
|
|
10558
|
+
: `<ul class="issues">${rows}</ul>`;
|
|
10559
|
+
|
|
10560
|
+
return `<!DOCTYPE html>
|
|
10561
|
+
<html lang="en">
|
|
10562
|
+
<head>
|
|
10563
|
+
<meta charset="utf-8">
|
|
10564
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10565
|
+
<title>${escapeHtml(title)}</title>
|
|
10566
|
+
<style>
|
|
10567
|
+
:root { color-scheme: light dark; }
|
|
10568
|
+
* { box-sizing: border-box; }
|
|
10569
|
+
body { font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #0d1117; color: #e6edf3; }
|
|
10570
|
+
.wrap { max-width: 880px; margin: 0 auto; padding: 32px 20px 64px; }
|
|
10571
|
+
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
10572
|
+
.sub { color: #8b949e; margin: 0 0 20px; font-size: 13px; }
|
|
10573
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #161b22; padding: 1px 5px; border-radius: 4px; font-size: 12.5px; }
|
|
10574
|
+
.banner { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-radius: 8px; font-weight: 600; margin-bottom: 16px; }
|
|
10575
|
+
.banner-green { background: rgba(46,160,67,.15); border: 1px solid rgba(46,160,67,.4); color: #3fb950; }
|
|
10576
|
+
.banner-red { background: rgba(248,81,73,.12); border: 1px solid rgba(248,81,73,.4); color: #f85149; }
|
|
10577
|
+
.dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; }
|
|
10578
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 20px; }
|
|
10579
|
+
.chip { font-size: 12px; padding: 3px 9px; border-radius: 999px; font-weight: 600; }
|
|
10580
|
+
.chip-red { background: rgba(248,81,73,.15); color: #f85149; }
|
|
10581
|
+
.chip-amber { background: rgba(210,153,34,.18); color: #d29922; }
|
|
10582
|
+
ul.issues { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
|
|
10583
|
+
.issue { border: 1px solid #30363d; border-left-width: 4px; border-radius: 8px; padding: 12px 14px; background: #0f141a; }
|
|
10584
|
+
.issue-red { border-left-color: #f85149; }
|
|
10585
|
+
.issue-amber { border-left-color: #d29922; }
|
|
10586
|
+
.issue-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
10587
|
+
.badge { font-size: 11.5px; font-weight: 700; padding: 2px 8px; border-radius: 5px; text-transform: uppercase; letter-spacing: .03em; }
|
|
10588
|
+
.badge-red { background: rgba(248,81,73,.18); color: #f85149; }
|
|
10589
|
+
.badge-amber { background: rgba(210,153,34,.2); color: #d29922; }
|
|
10590
|
+
.loc { font-family: ui-monospace, monospace; color: #8b949e; font-size: 12px; }
|
|
10591
|
+
.conf { font-size: 11px; color: #8b949e; }
|
|
10592
|
+
.conf-high { color: #f85149; }
|
|
10593
|
+
.conf-medium { color: #d29922; }
|
|
10594
|
+
.msg { margin-top: 7px; font-family: ui-monospace, monospace; font-size: 12.5px; color: #e6edf3; }
|
|
10595
|
+
.suggestion { margin-top: 6px; font-size: 12.5px; color: #3fb950; }
|
|
10596
|
+
.suggestion .conf { margin-left: 6px; }
|
|
10597
|
+
.empty { color: #8b949e; }
|
|
10598
|
+
footer { margin-top: 28px; color: #6e7681; font-size: 12px; }
|
|
10599
|
+
a { color: #58a6ff; }
|
|
10600
|
+
</style>
|
|
10601
|
+
</head>
|
|
10602
|
+
<body>
|
|
10603
|
+
<div class="wrap">
|
|
10604
|
+
<h1>Hallucination Guard report</h1>
|
|
10605
|
+
<p class="sub">Deterministic verification of an AI answer against the real repository — <code>sigmap verify-ai-output</code></p>
|
|
10606
|
+
${banner}
|
|
10607
|
+
${chips ? `<div class="chips">${chips}</div>` : ''}
|
|
10608
|
+
${list}
|
|
10609
|
+
<footer>Generated by <a href="https://github.com/manojmallick/sigmap">SigMap</a> · offline, no LLM · suggestions are heuristic (closest-match)</footer>
|
|
10610
|
+
</div>
|
|
10611
|
+
</body>
|
|
10612
|
+
</html>
|
|
10613
|
+
`;
|
|
10614
|
+
}
|
|
8915
10615
|
|
|
8916
|
-
|
|
10616
|
+
/** Compact Markdown rendering of the same result (CI / PR comments). */
|
|
10617
|
+
function renderReportMarkdown(result) {
|
|
10618
|
+
const issues = Array.isArray(result.issues) ? result.issues : [];
|
|
10619
|
+
const summary = result.summary || {};
|
|
10620
|
+
const file = result.file || 'AI answer';
|
|
10621
|
+
if (summary.clean || issues.length === 0) {
|
|
10622
|
+
return `### ✅ Hallucination Guard — clean\n\nNo fabricated files, imports, symbols, or scripts in \`${file}\`.`;
|
|
10623
|
+
}
|
|
10624
|
+
const lines = [
|
|
10625
|
+
`### ❌ Hallucination Guard — ${issues.length} issue${issues.length === 1 ? '' : 's'} in \`${file}\``,
|
|
10626
|
+
'',
|
|
10627
|
+
'| Type | Location | Detail | Suggestion |',
|
|
10628
|
+
'| --- | --- | --- | --- |',
|
|
10629
|
+
];
|
|
10630
|
+
for (const i of issues) {
|
|
10631
|
+
const sugg = i.suggestion ? i.suggestion.replace(/\|/g, '\\|') : '—';
|
|
10632
|
+
lines.push(`| ${labelFor(i)} | ${i.location || ('L' + i.line)} | \`${String(i.value).replace(/\|/g, '\\|')}\` | ${sugg} |`);
|
|
8917
10633
|
}
|
|
10634
|
+
return lines.join('\n');
|
|
10635
|
+
}
|
|
8918
10636
|
|
|
8919
|
-
|
|
8920
|
-
function inferPackage(query, workspaceDirs, cwd) {
|
|
8921
|
-
const tokens = query.toLowerCase().split(/\W+/).filter(t => t.length > 2);
|
|
10637
|
+
module.exports = { renderReportHtml, renderReportMarkdown, escapeHtml };
|
|
8922
10638
|
|
|
8923
|
-
|
|
8924
|
-
let bestMatch = null;
|
|
8925
|
-
let bestLen = 0;
|
|
8926
|
-
let bestMatchLen = 0;
|
|
10639
|
+
};
|
|
8927
10640
|
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
10641
|
+
// ── ./src/verify/hallucination-guard ──
|
|
10642
|
+
__factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
10643
|
+
'use strict';
|
|
10644
|
+
|
|
10645
|
+
/**
|
|
10646
|
+
* Hallucination Guard — deterministic core (Reliable MVP, v6.15.0).
|
|
10647
|
+
*
|
|
10648
|
+
* Given the text of an AI answer, flag claims that do not match the repo:
|
|
10649
|
+
* - fake-file : a referenced path is not on disk
|
|
10650
|
+
* - fake-test-file : a referenced *test* path is not on disk (sub-type)
|
|
10651
|
+
* - fake-import : a relative import does not resolve; a bare import is
|
|
10652
|
+
* absent from package.json deps (builtins allow-listed)
|
|
10653
|
+
* - fake-symbol : a called function/class is absent from the symbol index
|
|
10654
|
+
* - fake-npm-script: `npm run X` where X is not a package.json script
|
|
10655
|
+
*
|
|
10656
|
+
* Each issue carries a `confidence` (detection certainty) and, where a near
|
|
10657
|
+
* match exists, a heuristic `suggestion` ("Did you mean …?"). No network, no
|
|
10658
|
+
* LLM. Reuses SigMap primitives (buildSigIndex) but every external dependency
|
|
10659
|
+
* is injectable via `opts` so the core stays unit-testable.
|
|
10660
|
+
*/
|
|
10661
|
+
|
|
10662
|
+
const fs = require('fs');
|
|
10663
|
+
const path = require('path');
|
|
10664
|
+
const parsers = __require('./src/verify/parsers');
|
|
10665
|
+
const { closestMatch, buildSymbolCandidates, formatSuggestion } = __require('./src/verify/closest-match');
|
|
10666
|
+
|
|
10667
|
+
// A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
|
|
10668
|
+
// a tests/__tests__ directory). Used to flag fake-test-file separately.
|
|
10669
|
+
const TEST_PATH_RE = /(?:\.(?:test|spec)\.[mc]?[jt]sx?$)|(?:(?:^|\/)__tests__\/)|(?:(?:^|\/)test_[^/]+\.py$)|(?:_test\.py$)|(?:(?:^|\/)tests?\/)/i;
|
|
10670
|
+
function isTestPath(p) { return TEST_PATH_RE.test(p); }
|
|
10671
|
+
|
|
10672
|
+
const NODE_BUILTINS = new Set([
|
|
10673
|
+
'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https', 'crypto',
|
|
10674
|
+
'child_process', 'url', 'querystring', 'assert', 'zlib', 'readline', 'net',
|
|
10675
|
+
'tls', 'dns', 'buffer', 'process', 'vm', 'module', 'console', 'timers',
|
|
10676
|
+
'string_decoder', 'perf_hooks', 'worker_threads', 'cluster', 'dgram', 'v8',
|
|
10677
|
+
'tty', 'repl', 'async_hooks', 'inspector', 'fs/promises', 'path/posix',
|
|
10678
|
+
]);
|
|
10679
|
+
|
|
10680
|
+
const PY_BUILTINS = new Set([
|
|
10681
|
+
'os', 'sys', 're', 'json', 'math', 'typing', 'collections', 'itertools',
|
|
10682
|
+
'functools', 'datetime', 'pathlib', 'subprocess', 'abc', 'dataclasses',
|
|
10683
|
+
'enum', 'io', 'time', 'random', 'logging', 'argparse', 'unittest', 'asyncio',
|
|
10684
|
+
'copy', 'hashlib', 'threading', 'string', 'csv', 'glob', 'shutil', 'tempfile',
|
|
10685
|
+
]);
|
|
10686
|
+
|
|
10687
|
+
const LANG_GLOBALS = new Set([
|
|
10688
|
+
// JS
|
|
10689
|
+
'console', 'require', 'module', 'exports', 'process', 'Object', 'Array',
|
|
10690
|
+
'String', 'Number', 'Boolean', 'Math', 'JSON', 'Date', 'Promise', 'Map',
|
|
10691
|
+
'Set', 'WeakMap', 'WeakSet', 'RegExp', 'Error', 'Symbol', 'parseInt',
|
|
10692
|
+
'parseFloat', 'isNaN', 'setTimeout', 'setInterval', 'clearTimeout', 'fetch',
|
|
10693
|
+
'Buffer', 'Function', 'eval', 'encodeURIComponent', 'decodeURIComponent',
|
|
10694
|
+
// Python
|
|
10695
|
+
'print', 'len', 'range', 'str', 'int', 'float', 'dict', 'list', 'tuple',
|
|
10696
|
+
'set', 'bool', 'open', 'enumerate', 'zip', 'map', 'filter', 'sorted',
|
|
10697
|
+
'sum', 'min', 'max', 'abs', 'isinstance', 'super', 'type', 'getattr',
|
|
10698
|
+
'setattr', 'hasattr',
|
|
10699
|
+
]);
|
|
10700
|
+
|
|
10701
|
+
const REL_EXTS = ['', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.json', '.py', '.r', '.R', '.vue'];
|
|
10702
|
+
const REL_INDEX = ['index.js', 'index.ts', 'index.tsx', 'index.jsx', '__init__.py'];
|
|
10703
|
+
|
|
10704
|
+
/**
|
|
10705
|
+
* Build the set of known symbol identifiers from the SigMap signature index,
|
|
10706
|
+
* plus `{ name, file, line }` candidates (for closest-match suggestions).
|
|
10707
|
+
*/
|
|
10708
|
+
function buildSymbolSet(cwd) {
|
|
10709
|
+
const set = new Set();
|
|
10710
|
+
let fileKeys = [];
|
|
10711
|
+
let symbolCandidates = [];
|
|
10712
|
+
try {
|
|
10713
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
10714
|
+
const idx = buildSigIndex(cwd);
|
|
10715
|
+
fileKeys = [...idx.keys()];
|
|
10716
|
+
for (const sigs of idx.values()) {
|
|
10717
|
+
for (const sig of sigs) {
|
|
10718
|
+
const cleaned = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '');
|
|
10719
|
+
const ids = cleaned.match(/[A-Za-z_$][\w$]*/g) || [];
|
|
10720
|
+
for (const id of ids) set.add(id);
|
|
8938
10721
|
}
|
|
8939
10722
|
}
|
|
10723
|
+
symbolCandidates = buildSymbolCandidates(idx);
|
|
10724
|
+
} catch (_) {}
|
|
10725
|
+
return { set, fileKeys, symbolCandidates };
|
|
10726
|
+
}
|
|
8940
10727
|
|
|
8941
|
-
|
|
10728
|
+
/** Load declared dependency names from package.json. */
|
|
10729
|
+
function loadDeps(cwd) {
|
|
10730
|
+
const deps = new Set();
|
|
10731
|
+
let hasPkg = false;
|
|
10732
|
+
try {
|
|
10733
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
10734
|
+
hasPkg = true;
|
|
10735
|
+
for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
10736
|
+
if (pkg[k] && typeof pkg[k] === 'object') {
|
|
10737
|
+
for (const name of Object.keys(pkg[k])) deps.add(name);
|
|
10738
|
+
}
|
|
10739
|
+
}
|
|
10740
|
+
} catch (_) {}
|
|
10741
|
+
return { deps, hasPkg };
|
|
10742
|
+
}
|
|
10743
|
+
|
|
10744
|
+
/** Load the set of npm script names declared in package.json. */
|
|
10745
|
+
function loadScripts(cwd) {
|
|
10746
|
+
const scripts = new Set();
|
|
10747
|
+
try {
|
|
10748
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
10749
|
+
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
10750
|
+
for (const name of Object.keys(pkg.scripts)) scripts.add(name);
|
|
10751
|
+
}
|
|
10752
|
+
} catch (_) {}
|
|
10753
|
+
return scripts;
|
|
10754
|
+
}
|
|
10755
|
+
|
|
10756
|
+
/** Default file-existence check: resolve a referenced path against cwd. */
|
|
10757
|
+
function defaultFileExists(cwd, ref) {
|
|
10758
|
+
const clean = ref.replace(/^\.\//, '');
|
|
10759
|
+
for (const c of [path.resolve(cwd, clean), path.resolve(cwd, ref)]) {
|
|
10760
|
+
try {
|
|
10761
|
+
if (fs.existsSync(c)) return true;
|
|
10762
|
+
} catch (_) {}
|
|
8942
10763
|
}
|
|
10764
|
+
return false;
|
|
10765
|
+
}
|
|
8943
10766
|
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
10767
|
+
/** Default relative-import resolver: fs candidates + basename match in index. */
|
|
10768
|
+
function defaultRelativeResolvable(cwd, mod, fileBasenames) {
|
|
10769
|
+
const base = path.resolve(cwd, mod);
|
|
10770
|
+
for (const e of REL_EXTS) {
|
|
10771
|
+
try {
|
|
10772
|
+
if (fs.existsSync(base + e)) return true;
|
|
10773
|
+
} catch (_) {}
|
|
8949
10774
|
}
|
|
10775
|
+
for (const idx of REL_INDEX) {
|
|
10776
|
+
try {
|
|
10777
|
+
if (fs.existsSync(path.join(base, idx))) return true;
|
|
10778
|
+
} catch (_) {}
|
|
10779
|
+
}
|
|
10780
|
+
// Fall back to basename match against the indexed file set (the answer's
|
|
10781
|
+
// import is relative to a file we cannot know, so a name match is enough
|
|
10782
|
+
// to avoid false positives).
|
|
10783
|
+
const wantBase = path.basename(mod).replace(/\.[^.]+$/, '').toLowerCase();
|
|
10784
|
+
return fileBasenames.has(wantBase);
|
|
10785
|
+
}
|
|
8950
10786
|
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
10787
|
+
/**
|
|
10788
|
+
* Verify an AI answer against the repository.
|
|
10789
|
+
*
|
|
10790
|
+
* Each issue has the shape:
|
|
10791
|
+
* { type, value, line, location, message, confidence, suggestion }
|
|
10792
|
+
* where `confidence` is the *detection* certainty ('high' for path/dep/script
|
|
10793
|
+
* checks, 'medium' for symbol checks) and `suggestion` is a heuristic
|
|
10794
|
+
* closest-match hint (or null).
|
|
10795
|
+
*
|
|
10796
|
+
* @param {string} answerText
|
|
10797
|
+
* @param {string} cwd
|
|
10798
|
+
* @param {object} [opts]
|
|
10799
|
+
* @param {Set<string>} [opts.symbolSet] override known symbols
|
|
10800
|
+
* @param {Array} [opts.symbolCandidates] override { name, file, line } list
|
|
10801
|
+
* @param {Array<string>} [opts.fileCandidates] override repo file paths (suggestions)
|
|
10802
|
+
* @param {Set<string>} [opts.deps] override package deps
|
|
10803
|
+
* @param {Set<string>} [opts.scripts] override package.json script names
|
|
10804
|
+
* @param {boolean} [opts.hasPkg] whether a package.json exists
|
|
10805
|
+
* @param {(ref: string) => boolean} [opts.fileExists] override file check
|
|
10806
|
+
* @param {(mod: string) => boolean} [opts.relativeResolvable] override rel-import check
|
|
10807
|
+
* @returns {{ issues: object[], summary: object }}
|
|
10808
|
+
*/
|
|
10809
|
+
function verify(answerText, cwd, opts = {}) {
|
|
10810
|
+
let symbolSet = opts.symbolSet;
|
|
10811
|
+
let fileBasenames = opts.fileBasenames;
|
|
10812
|
+
let symbolCandidates = opts.symbolCandidates || [];
|
|
10813
|
+
let fileCandidates = opts.fileCandidates || [];
|
|
10814
|
+
if (!symbolSet) {
|
|
10815
|
+
const built = buildSymbolSet(cwd);
|
|
10816
|
+
symbolSet = built.set;
|
|
10817
|
+
fileBasenames = new Set(built.fileKeys.map(
|
|
10818
|
+
(k) => path.basename(k).replace(/\.[^.]+$/, '').toLowerCase()
|
|
10819
|
+
));
|
|
10820
|
+
symbolCandidates = built.symbolCandidates;
|
|
10821
|
+
fileCandidates = built.fileKeys;
|
|
10822
|
+
}
|
|
10823
|
+
if (!fileBasenames) fileBasenames = new Set();
|
|
10824
|
+
|
|
10825
|
+
let deps = opts.deps;
|
|
10826
|
+
let hasPkg = opts.hasPkg;
|
|
10827
|
+
if (!deps) {
|
|
10828
|
+
const loaded = loadDeps(cwd);
|
|
10829
|
+
deps = loaded.deps;
|
|
10830
|
+
if (hasPkg === undefined) hasPkg = loaded.hasPkg;
|
|
10831
|
+
}
|
|
10832
|
+
const scripts = opts.scripts || (hasPkg ? loadScripts(cwd) : new Set());
|
|
10833
|
+
|
|
10834
|
+
const fileExists = opts.fileExists || ((ref) => defaultFileExists(cwd, ref));
|
|
10835
|
+
const relativeResolvable = opts.relativeResolvable
|
|
10836
|
+
|| ((mod) => defaultRelativeResolvable(cwd, mod, fileBasenames));
|
|
10837
|
+
|
|
10838
|
+
// Pre-derive basename candidates for file suggestions (compare on basename so
|
|
10839
|
+
// a wrong directory still surfaces the right file).
|
|
10840
|
+
const fileBasenameCandidates = fileCandidates.map((f) => ({ name: path.basename(f), file: f }));
|
|
10841
|
+
|
|
10842
|
+
const issues = [];
|
|
10843
|
+
const dedupe = new Set();
|
|
10844
|
+
const add = (issue) => {
|
|
10845
|
+
const key = `${issue.type}::${issue.value}`;
|
|
10846
|
+
if (dedupe.has(key)) return;
|
|
10847
|
+
dedupe.add(key);
|
|
10848
|
+
if (!('suggestion' in issue)) issue.suggestion = null;
|
|
10849
|
+
issue.location = `L${issue.line}`;
|
|
10850
|
+
issues.push(issue);
|
|
10851
|
+
};
|
|
8955
10852
|
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
if (
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
10853
|
+
// 1. fake-file / fake-test-file
|
|
10854
|
+
for (const { path: p, line } of parsers.extractFilePaths(answerText)) {
|
|
10855
|
+
if (fileExists(p)) continue;
|
|
10856
|
+
const isTest = isTestPath(p);
|
|
10857
|
+
const match = closestMatch(path.basename(p), fileBasenameCandidates, { minLen: 4 });
|
|
10858
|
+
add({
|
|
10859
|
+
type: isTest ? 'fake-test-file' : 'fake-file',
|
|
10860
|
+
value: p,
|
|
10861
|
+
line,
|
|
10862
|
+
message: `${isTest ? 'Test file' : 'File'} not found on disk: ${p}`,
|
|
10863
|
+
confidence: 'high',
|
|
10864
|
+
suggestion: match ? formatSuggestion(match, false) : null,
|
|
10865
|
+
});
|
|
10866
|
+
}
|
|
10867
|
+
|
|
10868
|
+
// 2. fake-import
|
|
10869
|
+
for (const imp of parsers.extractImports(answerText)) {
|
|
10870
|
+
if (imp.relative) {
|
|
10871
|
+
if (!relativeResolvable(imp.module)) {
|
|
10872
|
+
add({ type: 'fake-import', value: imp.module, line: imp.line, message: `Import does not resolve: ${imp.module}`, confidence: 'high' });
|
|
8963
10873
|
}
|
|
10874
|
+
continue;
|
|
10875
|
+
}
|
|
10876
|
+
// Bare module — only verifiable for JS when a package.json exists.
|
|
10877
|
+
const top = imp.module.split('/')[0];
|
|
10878
|
+
if (imp.kind === 'js') {
|
|
10879
|
+
if (!hasPkg) continue;
|
|
10880
|
+
if (NODE_BUILTINS.has(imp.module) || NODE_BUILTINS.has(top)) continue;
|
|
10881
|
+
if (top.startsWith('@')) {
|
|
10882
|
+
const scoped = imp.module.split('/').slice(0, 2).join('/');
|
|
10883
|
+
if (deps.has(scoped) || deps.has(imp.module)) continue;
|
|
10884
|
+
} else if (deps.has(top) || deps.has(imp.module)) {
|
|
10885
|
+
continue;
|
|
10886
|
+
}
|
|
10887
|
+
const match = closestMatch(top, [...deps], { minLen: 3 });
|
|
10888
|
+
add({
|
|
10889
|
+
type: 'fake-import',
|
|
10890
|
+
value: imp.module,
|
|
10891
|
+
line: imp.line,
|
|
10892
|
+
message: `Package not in dependencies: ${imp.module}`,
|
|
10893
|
+
confidence: 'high',
|
|
10894
|
+
suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
|
|
10895
|
+
});
|
|
10896
|
+
}
|
|
10897
|
+
// Python bare imports: stdlib is unbounded offline — skip to keep precision.
|
|
10898
|
+
}
|
|
10899
|
+
|
|
10900
|
+
// 3. fake-symbol
|
|
10901
|
+
if (symbolSet.size > 0) {
|
|
10902
|
+
for (const { name, line } of parsers.extractSymbols(answerText)) {
|
|
10903
|
+
if (symbolSet.has(name)) continue;
|
|
10904
|
+
if (LANG_GLOBALS.has(name) || NODE_BUILTINS.has(name) || PY_BUILTINS.has(name)) continue;
|
|
10905
|
+
const match = closestMatch(name, symbolCandidates, { minLen: 4 });
|
|
10906
|
+
add({
|
|
10907
|
+
type: 'fake-symbol',
|
|
10908
|
+
value: name,
|
|
10909
|
+
line,
|
|
10910
|
+
message: `Symbol not found in repo index: ${name}()`,
|
|
10911
|
+
confidence: 'medium',
|
|
10912
|
+
suggestion: match ? formatSuggestion(match, true) : null,
|
|
10913
|
+
});
|
|
8964
10914
|
}
|
|
8965
|
-
return 0;
|
|
8966
10915
|
}
|
|
8967
|
-
};
|
|
8968
10916
|
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
10917
|
+
// 4. fake-npm-script
|
|
10918
|
+
if (hasPkg && scripts.size > 0) {
|
|
10919
|
+
for (const { name, line } of parsers.extractNpmScripts(answerText)) {
|
|
10920
|
+
if (scripts.has(name)) continue;
|
|
10921
|
+
const match = closestMatch(name, [...scripts], { minLen: 2 });
|
|
10922
|
+
add({
|
|
10923
|
+
type: 'fake-npm-script',
|
|
10924
|
+
value: name,
|
|
10925
|
+
line,
|
|
10926
|
+
message: `npm script not in package.json: ${name}`,
|
|
10927
|
+
confidence: 'high',
|
|
10928
|
+
suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
|
|
10929
|
+
});
|
|
10930
|
+
}
|
|
10931
|
+
}
|
|
10932
|
+
|
|
10933
|
+
issues.sort((a, b) => a.line - b.line);
|
|
10934
|
+
|
|
10935
|
+
const byType = {
|
|
10936
|
+
'fake-file': 0, 'fake-test-file': 0, 'fake-import': 0,
|
|
10937
|
+
'fake-symbol': 0, 'fake-npm-script': 0,
|
|
10938
|
+
};
|
|
10939
|
+
for (const i of issues) byType[i.type] = (byType[i.type] || 0) + 1;
|
|
10940
|
+
|
|
10941
|
+
const summary = {
|
|
10942
|
+
total: issues.length,
|
|
10943
|
+
byType,
|
|
10944
|
+
clean: issues.length === 0,
|
|
10945
|
+
symbolsIndexed: symbolSet.size,
|
|
10946
|
+
withSuggestion: issues.filter((i) => i.suggestion).length,
|
|
10947
|
+
};
|
|
10948
|
+
|
|
10949
|
+
return { issues, summary };
|
|
10950
|
+
}
|
|
10951
|
+
|
|
10952
|
+
module.exports = { verify, buildSymbolSet, loadDeps, loadScripts, isTestPath };
|
|
10953
|
+
|
|
10954
|
+
};
|
|
8975
10955
|
|
|
8976
10956
|
const fs = require('fs');
|
|
8977
10957
|
const path = require('path');
|
|
8978
10958
|
const os = require('os');
|
|
8979
10959
|
const { execSync } = require('child_process');
|
|
8980
10960
|
|
|
8981
|
-
const VERSION = '
|
|
10961
|
+
const VERSION = '7.0.0';
|
|
8982
10962
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
8983
10963
|
|
|
8984
10964
|
function requireSourceOrBundled(key) {
|
|
@@ -9278,29 +11258,17 @@ function applyTokenBudget(fileEntries, maxTokens) {
|
|
|
9278
11258
|
let total = renderedTotal(fileEntries);
|
|
9279
11259
|
if (total <= budgetForEntries) return fileEntries;
|
|
9280
11260
|
|
|
9281
|
-
//
|
|
9282
|
-
//
|
|
9283
|
-
//
|
|
9284
|
-
//
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
});
|
|
9293
|
-
const collapsedRendered = renderedTotal(collapsed);
|
|
9294
|
-
if (collapsedRendered < total) {
|
|
9295
|
-
console.warn(`[sigmap] budget: collapsed bodies to anchors, reclaimed ~${total - collapsedRendered} tokens`);
|
|
9296
|
-
working = collapsed;
|
|
9297
|
-
total = collapsedRendered;
|
|
9298
|
-
// Collapsing keeps every file, so the section overhead must fit too — not just sigs.
|
|
9299
|
-
if (total <= budgetForEntries) return working;
|
|
9300
|
-
}
|
|
9301
|
-
|
|
9302
|
-
// Sort by drop priority (drop first = index 0)
|
|
9303
|
-
const withPriority = working.map((e) => {
|
|
11261
|
+
// Over budget — degrade gracefully, best file first:
|
|
11262
|
+
// 1. keep FULL signatures (params + return type) while they fit;
|
|
11263
|
+
// 2. when a file no longer fits with full sigs, collapse just THAT file to
|
|
11264
|
+
// its line-anchor pointers (still discoverable via buildSigIndex / the
|
|
11265
|
+
// ranker — it parses the context file, so an anchored file stays findable);
|
|
11266
|
+
// 3. drop a file only when even the anchor form won't fit.
|
|
11267
|
+
// The important, high-priority files therefore keep their real signatures —
|
|
11268
|
+
// the user-visible value — while only low-priority overflow degrades to
|
|
11269
|
+
// anchors instead of vanishing. (Earlier versions collapsed EVERY file to an
|
|
11270
|
+
// anchor the moment you went 1 token over budget, gutting all signatures.)
|
|
11271
|
+
const withPriority = fileEntries.map((e) => {
|
|
9304
11272
|
let priority = 0;
|
|
9305
11273
|
let dropReason = 'budget: low recency';
|
|
9306
11274
|
if (isGeneratedFile(e.filePath)) { priority = 10; dropReason = 'budget: generated file'; }
|
|
@@ -9308,41 +11276,59 @@ function applyTokenBudget(fileEntries, maxTokens) {
|
|
|
9308
11276
|
else if (isTestFile(e.filePath)) { priority = 8; dropReason = 'budget: test file'; }
|
|
9309
11277
|
else if (isConfigFile(e.filePath)) { priority = 6; dropReason = 'budget: config file'; }
|
|
9310
11278
|
else priority = 4;
|
|
9311
|
-
// v4.0: signal quality = sigs per line-of-code (higher = more informative)
|
|
9312
11279
|
const loc = e.content ? e.content.split('\n').length : 1;
|
|
9313
11280
|
const signalQuality = loc > 0 ? (e.sigs ? e.sigs.length : 0) / loc : 0;
|
|
9314
11281
|
return { ...e, priority, dropReason, signalQuality };
|
|
9315
11282
|
});
|
|
9316
11283
|
|
|
9317
|
-
//
|
|
9318
|
-
withPriority.sort((a, b) => {
|
|
9319
|
-
if (
|
|
9320
|
-
if ((
|
|
9321
|
-
return (
|
|
11284
|
+
// Best-first order: lowest drop-priority, then most-recent, then most-informative.
|
|
11285
|
+
const bestFirst = withPriority.slice().sort((a, b) => {
|
|
11286
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
11287
|
+
if ((b.mtime || 0) !== (a.mtime || 0)) return (b.mtime || 0) - (a.mtime || 0);
|
|
11288
|
+
return (b.signalQuality || 0) - (a.signalQuality || 0);
|
|
9322
11289
|
});
|
|
9323
11290
|
|
|
9324
|
-
const
|
|
11291
|
+
const entryCost = (e) => estimateTokens(e.sigs.join('\n')) + sectionOverhead(e);
|
|
11292
|
+
const collapseEntry = (e) => ({
|
|
11293
|
+
...e,
|
|
11294
|
+
sigs: (e.sigs || []).map((s) => {
|
|
11295
|
+
const line = toIndexLine(s);
|
|
11296
|
+
return line && /:\d+-\d+/.test(line) ? line : s;
|
|
11297
|
+
}),
|
|
11298
|
+
});
|
|
11299
|
+
// Keep FULL signatures for the best files while they fit (signatures are the
|
|
11300
|
+
// value the user reads). When a file's full form no longer fits, fall back to
|
|
11301
|
+
// its anchor form (still discoverable via the ranker, which parses this file);
|
|
11302
|
+
// drop only when even the anchor won't fit. So the important files keep real
|
|
11303
|
+
// signatures and only the lowest-priority overflow degrades — instead of every
|
|
11304
|
+
// signature being gutted to an anchor the moment you go over budget.
|
|
11305
|
+
const finalByPath = new Map();
|
|
9325
11306
|
const verboseDropped = [];
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
kept.push(entry);
|
|
11307
|
+
let collapsedCount = 0;
|
|
11308
|
+
let used = 0;
|
|
11309
|
+
for (const entry of bestFirst) { // best first
|
|
11310
|
+
if (used + entryCost(entry) <= budgetForEntries) {
|
|
11311
|
+
finalByPath.set(entry.filePath, entry); used += entryCost(entry); continue;
|
|
11312
|
+
}
|
|
11313
|
+
const slim = collapseEntry(entry);
|
|
11314
|
+
if (used + entryCost(slim) <= budgetForEntries) {
|
|
11315
|
+
finalByPath.set(entry.filePath, slim); used += entryCost(slim); collapsedCount++; continue;
|
|
9336
11316
|
}
|
|
11317
|
+
verboseDropped.push({ filePath: entry.filePath, reason: entry.dropReason });
|
|
9337
11318
|
}
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
11319
|
+
// Restore the original file order for stable output.
|
|
11320
|
+
const kept = withPriority.filter((e) => finalByPath.has(e.filePath)).map((e) => finalByPath.get(e.filePath));
|
|
11321
|
+
|
|
11322
|
+
if (verboseDropped.length > 0 || collapsedCount > 0) {
|
|
11323
|
+
const parts = [];
|
|
11324
|
+
if (verboseDropped.length) parts.push(`dropped ${verboseDropped.length} file(s)`);
|
|
11325
|
+
if (collapsedCount) parts.push(`collapsed ${collapsedCount} low-priority file(s) to anchors`);
|
|
11326
|
+
console.warn(`[sigmap] budget: ${parts.join(', ')} to stay under ${maxTokens} tokens`);
|
|
9341
11327
|
if (process.argv.includes('--verbose')) {
|
|
9342
11328
|
for (const { filePath, reason } of verboseDropped) {
|
|
9343
11329
|
console.warn(`[sigmap] dropped: ${path.relative(process.cwd(), filePath)} — ${reason}`);
|
|
9344
11330
|
}
|
|
9345
|
-
console.warn(`[sigmap] included: ${kept.length} files, dropped: ${verboseDropped.length}`);
|
|
11331
|
+
console.warn(`[sigmap] included: ${kept.length} files (${collapsedCount} collapsed), dropped: ${verboseDropped.length}`);
|
|
9346
11332
|
}
|
|
9347
11333
|
}
|
|
9348
11334
|
return kept;
|
|
@@ -9564,12 +11550,17 @@ function resolveImpactRadius(fileEntries, cwd, config) {
|
|
|
9564
11550
|
}
|
|
9565
11551
|
|
|
9566
11552
|
function formatOutput(fileEntries, cwd, routingEnabled, config, extras) {
|
|
11553
|
+
// One canonical usage block for every context file (CLAUDE.md, AGENTS.md,
|
|
11554
|
+
// copilot-instructions.md, …). Lives here — the single source all writers
|
|
11555
|
+
// consume — so adapters don't each invent their own (now-removed) variant.
|
|
11556
|
+
const { usageBlock } = requireSourceOrBundled('./src/format/usage-guidance');
|
|
9567
11557
|
const lines = [
|
|
9568
11558
|
'<!-- Generated by SigMap gen-context.js v' + VERSION + ' -->',
|
|
9569
11559
|
'<!-- DO NOT EDIT below the marker line — run gen-context.js to regenerate -->',
|
|
9570
11560
|
'',
|
|
9571
11561
|
'# Code signatures',
|
|
9572
11562
|
'',
|
|
11563
|
+
usageBlock(),
|
|
9573
11564
|
];
|
|
9574
11565
|
|
|
9575
11566
|
// Compact dependency map section (shows import relationships, ~50-100 tokens)
|
|
@@ -10737,8 +12728,16 @@ Usage:
|
|
|
10737
12728
|
${cmd} --impact <file> Show every file impacted by changing <file>
|
|
10738
12729
|
${cmd} --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
|
|
10739
12730
|
${cmd} --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
|
|
10740
|
-
${cmd} verify-ai-output <answer.md> Flag fake files/imports/symbols in an AI answer
|
|
10741
|
-
${cmd} verify-ai-output <answer.md> --json
|
|
12731
|
+
${cmd} verify-ai-output <answer.md> Flag fake files/tests/imports/symbols/npm-scripts in an AI answer
|
|
12732
|
+
${cmd} verify-ai-output <answer.md> --json Hallucination report as JSON (exits 1 if issues)
|
|
12733
|
+
${cmd} verify-ai-output <answer.md> --report Write a standalone HTML report (red/amber/green)
|
|
12734
|
+
${cmd} squeeze <file|-> Minimize a pasted stacktrace/CI-log/JSON blob (--json for stats)
|
|
12735
|
+
${cmd} ask "<query>" --squeeze Auto-accept input minimization (no prompt; for scripts/CI)
|
|
12736
|
+
${cmd} ask "<query>" --no-squeeze Disable input minimization entirely
|
|
12737
|
+
${cmd} ask "<query>" --squeeze-threshold N Min reduction %% to prompt (default 30)
|
|
12738
|
+
${cmd} note "<text>" Append a note to the cross-session decision log
|
|
12739
|
+
${cmd} note List recent notes (also: note --list <N>)
|
|
12740
|
+
${cmd} status Show repo state — branch, dirty files, index freshness, notes
|
|
10742
12741
|
${cmd} --init Write example config + .contextignore scaffold
|
|
10743
12742
|
${cmd} --help Show this message
|
|
10744
12743
|
${cmd} --version Show version
|
|
@@ -11124,8 +13123,8 @@ function main() {
|
|
|
11124
13123
|
const { loadSession, saveSession, mergeSessionContext } = requireSourceOrBundled('./src/session/memory');
|
|
11125
13124
|
const { detectWorkspaces, inferPackage, scopeToPackage } = requireSourceOrBundled('./src/workspace/detector');
|
|
11126
13125
|
|
|
11127
|
-
|
|
11128
|
-
|
|
13126
|
+
let intent = detectIntent(query);
|
|
13127
|
+
let intentWeights = getIntentWeights(intent);
|
|
11129
13128
|
|
|
11130
13129
|
const sigIndex = buildSigIndex(cwd);
|
|
11131
13130
|
if (sigIndex.size === 0) {
|
|
@@ -11133,6 +13132,44 @@ function main() {
|
|
|
11133
13132
|
process.exit(1);
|
|
11134
13133
|
}
|
|
11135
13134
|
|
|
13135
|
+
// v7.0.0: Squeeze — classify and minimize pasted stacktrace/CI-log/JSON
|
|
13136
|
+
// input before ranking. Always runs silently; only prompts (interactive
|
|
13137
|
+
// TTY) when the reduction clears the threshold. Never blocks pipes/CI.
|
|
13138
|
+
let squeezeOffered = false;
|
|
13139
|
+
let squeezeAccepted = false;
|
|
13140
|
+
if (!args.includes('--no-squeeze')) {
|
|
13141
|
+
try {
|
|
13142
|
+
const { squeeze: runSqueeze, shouldPrompt, formatSummary } = requireSourceOrBundled('./src/squeeze/index');
|
|
13143
|
+
const sq = runSqueeze(query, { srcDirs: config.srcDirs, symbolIndex: sigIndex });
|
|
13144
|
+
if (sq.applies && sq.reduction > 0) {
|
|
13145
|
+
squeezeOffered = true;
|
|
13146
|
+
const thrIdx = args.indexOf('--squeeze-threshold');
|
|
13147
|
+
const threshold = thrIdx !== -1 ? parseFloat(args[thrIdx + 1]) : 30;
|
|
13148
|
+
const auto = args.includes('--squeeze');
|
|
13149
|
+
const interactive = !!(process.stdin.isTTY && process.stderr.isTTY) && !args.includes('--json');
|
|
13150
|
+
let accept = false;
|
|
13151
|
+
if (auto) {
|
|
13152
|
+
accept = true;
|
|
13153
|
+
} else if (interactive && shouldPrompt(sq.reduction, threshold)) {
|
|
13154
|
+
process.stderr.write('\n' + formatSummary(sq) + '\n\n');
|
|
13155
|
+
process.stderr.write('Proceed with minimized input? [Y/n] ');
|
|
13156
|
+
const buf = Buffer.alloc(8);
|
|
13157
|
+
try {
|
|
13158
|
+
const n = fs.readSync(0, buf, 0, 8, null);
|
|
13159
|
+
const ans = buf.toString('utf8', 0, n).trim().toLowerCase();
|
|
13160
|
+
accept = ans === '' || ans === 'y' || ans === 'yes';
|
|
13161
|
+
} catch (_) { accept = false; }
|
|
13162
|
+
}
|
|
13163
|
+
if (accept) {
|
|
13164
|
+
query = sq.squeezed;
|
|
13165
|
+
intent = detectIntent(query);
|
|
13166
|
+
intentWeights = getIntentWeights(intent);
|
|
13167
|
+
squeezeAccepted = true;
|
|
13168
|
+
}
|
|
13169
|
+
}
|
|
13170
|
+
} catch (_) { /* squeeze is best-effort — never break ask */ }
|
|
13171
|
+
}
|
|
13172
|
+
|
|
11136
13173
|
let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd });
|
|
11137
13174
|
|
|
11138
13175
|
// v6.10: Workspace scoping — infer package from query and apply boost
|
|
@@ -11232,6 +13269,15 @@ function main() {
|
|
|
11232
13269
|
bar,
|
|
11233
13270
|
].join('\n'));
|
|
11234
13271
|
}
|
|
13272
|
+
// v7.0.0: record the run and show the one-time star nudge (interactive only).
|
|
13273
|
+
try {
|
|
13274
|
+
const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
|
|
13275
|
+
const showNudge = !!process.stderr.isTTY && !args.includes('--json');
|
|
13276
|
+
checkStarNudge(cwd, true, {
|
|
13277
|
+
silent: !showNudge,
|
|
13278
|
+
bump: { squeezeOffered: squeezeOffered ? 1 : 0, squeezeAccepted: squeezeAccepted ? 1 : 0 },
|
|
13279
|
+
});
|
|
13280
|
+
} catch (_) {}
|
|
11235
13281
|
process.exit(0);
|
|
11236
13282
|
}
|
|
11237
13283
|
|
|
@@ -11860,12 +13906,186 @@ function main() {
|
|
|
11860
13906
|
process.exit(0);
|
|
11861
13907
|
}
|
|
11862
13908
|
|
|
13909
|
+
// `sigmap note "<text>"` — append to the cross-session decision log.
|
|
13910
|
+
// With no text, lists recent notes (also `note --list [N]`).
|
|
13911
|
+
if (args[0] === 'note') {
|
|
13912
|
+
const jsonOut = args.includes('--json');
|
|
13913
|
+
const { addNote, readNotes, formatNotes } = requireSourceOrBundled('./src/session/notes');
|
|
13914
|
+
const listIdx = args.indexOf('--list');
|
|
13915
|
+
// Build positionals, skipping value-taking flags and their values
|
|
13916
|
+
// (e.g. `--cwd <dir>`, `--list <N>`) so they never leak into the note text.
|
|
13917
|
+
const VALUE_FLAGS = new Set(['--cwd', '--list']);
|
|
13918
|
+
const positional = [];
|
|
13919
|
+
for (let i = 1; i < args.length; i++) {
|
|
13920
|
+
const a = args[i];
|
|
13921
|
+
if (a.startsWith('--')) { if (VALUE_FLAGS.has(a)) i++; continue; }
|
|
13922
|
+
positional.push(a);
|
|
13923
|
+
}
|
|
13924
|
+
const wantsList = listIdx !== -1 || positional.length === 0;
|
|
13925
|
+
|
|
13926
|
+
if (wantsList) {
|
|
13927
|
+
const limArg = listIdx !== -1 ? parseInt(args[listIdx + 1], 10) : parseInt(positional[0], 10);
|
|
13928
|
+
const limit = Number.isFinite(limArg) && limArg > 0 ? limArg : 10;
|
|
13929
|
+
const notes = readNotes(cwd, limit);
|
|
13930
|
+
if (jsonOut) {
|
|
13931
|
+
process.stdout.write(JSON.stringify({ notes }) + '\n');
|
|
13932
|
+
} else {
|
|
13933
|
+
console.log(`[sigmap] ${notes.length} recent note${notes.length === 1 ? '' : 's'}`);
|
|
13934
|
+
console.log(formatNotes(notes.slice().reverse()));
|
|
13935
|
+
}
|
|
13936
|
+
process.exit(0);
|
|
13937
|
+
}
|
|
13938
|
+
|
|
13939
|
+
const text = positional.join(' ');
|
|
13940
|
+
let entry;
|
|
13941
|
+
try {
|
|
13942
|
+
entry = addNote(cwd, text);
|
|
13943
|
+
} catch (err) {
|
|
13944
|
+
console.error(`[sigmap] ${err.message}`);
|
|
13945
|
+
process.exit(1);
|
|
13946
|
+
}
|
|
13947
|
+
if (jsonOut) {
|
|
13948
|
+
process.stdout.write(JSON.stringify({ added: entry }) + '\n');
|
|
13949
|
+
} else {
|
|
13950
|
+
console.log(`[sigmap] noted${entry.branch ? ` (${entry.branch})` : ''}: ${entry.text}`);
|
|
13951
|
+
}
|
|
13952
|
+
process.exit(0);
|
|
13953
|
+
}
|
|
13954
|
+
|
|
13955
|
+
// `sigmap status` — environment / repo state at a glance.
|
|
13956
|
+
if (args[0] === 'status') {
|
|
13957
|
+
const jsonOut = args.includes('--json');
|
|
13958
|
+
const gitOpts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
13959
|
+
const st = { branch: null, dirty: 0, lastIndex: null, indexVersion: null, indexFiles: null, changedSinceIndex: null, notes: 0, lastNote: null };
|
|
13960
|
+
|
|
13961
|
+
try { st.branch = execSync('git rev-parse --abbrev-ref HEAD', gitOpts).trim() || null; } catch (_) {}
|
|
13962
|
+
// Fallback for an unborn branch (fresh repo, no commits yet).
|
|
13963
|
+
if (!st.branch || st.branch === 'HEAD') {
|
|
13964
|
+
try { st.branch = execSync('git symbolic-ref --short HEAD', gitOpts).trim() || st.branch; } catch (_) {}
|
|
13965
|
+
}
|
|
13966
|
+
try {
|
|
13967
|
+
const porcelain = execSync('git status --porcelain', gitOpts).trim();
|
|
13968
|
+
st.dirty = porcelain ? porcelain.split('\n').filter(Boolean).length : 0;
|
|
13969
|
+
} catch (_) {}
|
|
13970
|
+
|
|
13971
|
+
try {
|
|
13972
|
+
const { readLog } = requireSourceOrBundled('./src/tracking/logger');
|
|
13973
|
+
const log = readLog(cwd);
|
|
13974
|
+
if (log.length) {
|
|
13975
|
+
const last = log[log.length - 1];
|
|
13976
|
+
st.lastIndex = last.ts || null;
|
|
13977
|
+
st.indexVersion = last.version || null;
|
|
13978
|
+
st.indexFiles = last.fileCount != null ? last.fileCount : null;
|
|
13979
|
+
}
|
|
13980
|
+
} catch (_) {}
|
|
13981
|
+
|
|
13982
|
+
// Index freshness: count tracked files modified after the last index run.
|
|
13983
|
+
if (st.lastIndex) {
|
|
13984
|
+
try {
|
|
13985
|
+
const since = Date.parse(st.lastIndex);
|
|
13986
|
+
const tracked = execSync('git ls-files', gitOpts).split('\n').filter(Boolean);
|
|
13987
|
+
let changed = 0;
|
|
13988
|
+
for (const f of tracked.slice(0, 5000)) {
|
|
13989
|
+
try { if (fs.statSync(path.join(cwd, f)).mtimeMs > since) changed++; } catch (_) {}
|
|
13990
|
+
}
|
|
13991
|
+
st.changedSinceIndex = changed;
|
|
13992
|
+
} catch (_) {}
|
|
13993
|
+
}
|
|
13994
|
+
|
|
13995
|
+
try {
|
|
13996
|
+
const { readNotes } = requireSourceOrBundled('./src/session/notes');
|
|
13997
|
+
const notes = readNotes(cwd);
|
|
13998
|
+
st.notes = notes.length;
|
|
13999
|
+
if (notes.length) st.lastNote = notes[notes.length - 1];
|
|
14000
|
+
} catch (_) {}
|
|
14001
|
+
|
|
14002
|
+
if (jsonOut) {
|
|
14003
|
+
process.stdout.write(JSON.stringify(st) + '\n');
|
|
14004
|
+
process.exit(0);
|
|
14005
|
+
}
|
|
14006
|
+
|
|
14007
|
+
const fmtAgo = (iso) => {
|
|
14008
|
+
if (!iso) return 'never';
|
|
14009
|
+
const ms = Date.now() - Date.parse(iso);
|
|
14010
|
+
if (!Number.isFinite(ms)) return iso;
|
|
14011
|
+
const m = Math.floor(ms / 60000), h = Math.floor(m / 60), d = Math.floor(h / 24);
|
|
14012
|
+
if (d > 0) return `${d}d ago`;
|
|
14013
|
+
if (h > 0) return `${h}h ago`;
|
|
14014
|
+
if (m > 0) return `${m}m ago`;
|
|
14015
|
+
return 'just now';
|
|
14016
|
+
};
|
|
14017
|
+
|
|
14018
|
+
console.log('[sigmap] status');
|
|
14019
|
+
console.log(` Branch: ${st.branch || '(not a git repo)'}`);
|
|
14020
|
+
console.log(` Working tree: ${st.dirty === 0 ? 'clean' : `${st.dirty} file${st.dirty === 1 ? '' : 's'} changed`}`);
|
|
14021
|
+
if (st.lastIndex) {
|
|
14022
|
+
let fresh = `${fmtAgo(st.lastIndex)} (v${st.indexVersion || '?'}, ${st.indexFiles != null ? st.indexFiles + ' files' : 'n/a'})`;
|
|
14023
|
+
if (st.changedSinceIndex != null && st.changedSinceIndex > 0) fresh += ` — STALE: ${st.changedSinceIndex} file${st.changedSinceIndex === 1 ? '' : 's'} changed since`;
|
|
14024
|
+
console.log(` Last index: ${fresh}`);
|
|
14025
|
+
} else {
|
|
14026
|
+
console.log(' Last index: never — run: sigmap');
|
|
14027
|
+
}
|
|
14028
|
+
if (st.notes > 0) {
|
|
14029
|
+
console.log(` Notes: ${st.notes} (latest: ${st.lastNote.text.slice(0, 60)}${st.lastNote.text.length > 60 ? '…' : ''})`);
|
|
14030
|
+
} else {
|
|
14031
|
+
console.log(' Notes: none — add with: sigmap note "<text>"');
|
|
14032
|
+
}
|
|
14033
|
+
process.exit(0);
|
|
14034
|
+
}
|
|
14035
|
+
|
|
14036
|
+
// v7.0.0: `sigmap squeeze <file|->` — minimize a pasted stacktrace / CI-log / JSON blob
|
|
14037
|
+
if (args[0] === 'squeeze') {
|
|
14038
|
+
const jsonOut = args.includes('--json');
|
|
14039
|
+
const target = args[1] && !args[1].startsWith('--') ? args[1] : '-';
|
|
14040
|
+
let input = '';
|
|
14041
|
+
try {
|
|
14042
|
+
input = fs.readFileSync(target === '-' ? 0 : path.resolve(cwd, target), 'utf8');
|
|
14043
|
+
} catch (e) {
|
|
14044
|
+
console.error(`[sigmap] cannot read input: ${e.message}`);
|
|
14045
|
+
process.exit(1);
|
|
14046
|
+
}
|
|
14047
|
+
|
|
14048
|
+
const { squeeze: runSqueeze, formatSummary } = requireSourceOrBundled('./src/squeeze/index');
|
|
14049
|
+
let symbolIndex = null;
|
|
14050
|
+
try {
|
|
14051
|
+
const { buildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
14052
|
+
symbolIndex = buildSigIndex(cwd);
|
|
14053
|
+
} catch (_) {}
|
|
14054
|
+
const sq = runSqueeze(input, { srcDirs: config.srcDirs, symbolIndex });
|
|
14055
|
+
|
|
14056
|
+
try {
|
|
14057
|
+
const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
|
|
14058
|
+
checkStarNudge(cwd, true, { silent: !process.stderr.isTTY || jsonOut, bump: { squeezeOffered: sq.applies ? 1 : 0 } });
|
|
14059
|
+
} catch (_) {}
|
|
14060
|
+
|
|
14061
|
+
if (jsonOut) {
|
|
14062
|
+
process.stdout.write(JSON.stringify({
|
|
14063
|
+
category: sq.category, confidence: sq.confidence,
|
|
14064
|
+
rawTokens: sq.rawTokens, squeezedTokens: sq.squeezedTokens,
|
|
14065
|
+
reduction: sq.reduction, enriched: sq.enriched, squeezed: sq.squeezed,
|
|
14066
|
+
}) + '\n');
|
|
14067
|
+
process.exit(0);
|
|
14068
|
+
}
|
|
14069
|
+
if (!sq.category) {
|
|
14070
|
+
process.stderr.write('[sigmap] no squeezable structure detected — input unchanged\n');
|
|
14071
|
+
process.stdout.write(input);
|
|
14072
|
+
process.exit(0);
|
|
14073
|
+
}
|
|
14074
|
+
process.stderr.write(formatSummary(sq) + '\n\n');
|
|
14075
|
+
process.stdout.write(sq.squeezed + '\n');
|
|
14076
|
+
process.exit(0);
|
|
14077
|
+
}
|
|
14078
|
+
|
|
11863
14079
|
// `sigmap verify-ai-output <answer.md>` — Hallucination Guard (deterministic core)
|
|
11864
14080
|
if (args[0] === 'verify-ai-output') {
|
|
11865
14081
|
const target = args[1];
|
|
11866
14082
|
const jsonOut = args.includes('--json');
|
|
14083
|
+
const reportIdx = args.indexOf('--report');
|
|
14084
|
+
const reportOut = reportIdx !== -1
|
|
14085
|
+
? (args[reportIdx + 1] && !args[reportIdx + 1].startsWith('--') ? args[reportIdx + 1] : 'sigmap-verify-report.html')
|
|
14086
|
+
: null;
|
|
11867
14087
|
if (!target || target.startsWith('--')) {
|
|
11868
|
-
console.error('[sigmap] Usage: sigmap verify-ai-output <answer.md> [--json]');
|
|
14088
|
+
console.error('[sigmap] Usage: sigmap verify-ai-output <answer.md> [--json] [--report [out.html]]');
|
|
11869
14089
|
process.exit(1);
|
|
11870
14090
|
}
|
|
11871
14091
|
const absTarget = path.resolve(cwd, target);
|
|
@@ -11877,24 +14097,39 @@ function main() {
|
|
|
11877
14097
|
const answerText = fs.readFileSync(absTarget, 'utf8');
|
|
11878
14098
|
const { verify } = requireSourceOrBundled('./src/verify/hallucination-guard');
|
|
11879
14099
|
const { issues, summary } = verify(answerText, cwd);
|
|
14100
|
+
const rel = path.relative(cwd, absTarget) || target;
|
|
14101
|
+
const result = { file: rel, issues, summary };
|
|
14102
|
+
|
|
14103
|
+
// Optional HTML report (Surface A) — written alongside any other output.
|
|
14104
|
+
if (reportOut) {
|
|
14105
|
+
const { renderReportHtml } = requireSourceOrBundled('./src/format/verify-report');
|
|
14106
|
+
const outAbs = path.resolve(cwd, reportOut);
|
|
14107
|
+
fs.writeFileSync(outAbs, renderReportHtml(result));
|
|
14108
|
+
if (!jsonOut) console.log(`[sigmap] report written: ${path.relative(cwd, outAbs) || reportOut}`);
|
|
14109
|
+
}
|
|
11880
14110
|
|
|
11881
14111
|
if (jsonOut) {
|
|
11882
|
-
process.stdout.write(JSON.stringify(
|
|
14112
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
11883
14113
|
process.exit(summary.total > 0 ? 1 : 0);
|
|
11884
14114
|
}
|
|
11885
14115
|
|
|
11886
|
-
const rel = path.relative(cwd, absTarget) || target;
|
|
11887
14116
|
if (summary.total === 0) {
|
|
11888
14117
|
console.log(`[sigmap] ✓ ${rel} — no hallucinations detected (${summary.symbolsIndexed} symbols indexed)`);
|
|
11889
14118
|
process.exit(0);
|
|
11890
14119
|
}
|
|
11891
14120
|
|
|
11892
|
-
const labels = {
|
|
14121
|
+
const labels = {
|
|
14122
|
+
'fake-file': 'Fake file', 'fake-test-file': 'Fake test file',
|
|
14123
|
+
'fake-import': 'Fake import', 'fake-symbol': 'Fake symbol',
|
|
14124
|
+
'fake-npm-script': 'Fake npm script',
|
|
14125
|
+
};
|
|
14126
|
+
const bt = summary.byType;
|
|
11893
14127
|
console.log(`[sigmap] ✗ ${rel} — ${summary.total} issue${summary.total === 1 ? '' : 's'} found`);
|
|
11894
|
-
console.log(` fake-file: ${
|
|
14128
|
+
console.log(` fake-file: ${bt['fake-file']} fake-test-file: ${bt['fake-test-file']} fake-import: ${bt['fake-import']} fake-symbol: ${bt['fake-symbol']} fake-npm-script: ${bt['fake-npm-script']}`);
|
|
11895
14129
|
console.log('');
|
|
11896
14130
|
for (const issue of issues) {
|
|
11897
14131
|
console.log(` L${issue.line} [${labels[issue.type] || issue.type}] ${issue.message}`);
|
|
14132
|
+
if (issue.suggestion) console.log(` ↳ ${issue.suggestion}`);
|
|
11898
14133
|
}
|
|
11899
14134
|
process.exit(1);
|
|
11900
14135
|
}
|