sigmap 6.13.0 → 6.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +11 -11
- package/gen-context.js +2032 -795
- package/package.json +3 -2
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- 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/session/notes.js +99 -0
- package/src/verify/closest-match.js +145 -0
- package/src/verify/hallucination-guard.js +310 -0
- package/src/verify/parsers.js +200 -0
package/gen-context.js
CHANGED
|
@@ -5506,476 +5506,559 @@ __factories["./src/graph/impact"] = function(module, exports) {
|
|
|
5506
5506
|
|
|
5507
5507
|
// ── ./src/mcp/handlers ──
|
|
5508
5508
|
__factories["./src/mcp/handlers"] = function(module, exports) {
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5509
|
+
'use strict';
|
|
5510
|
+
|
|
5511
|
+
const fs = require('fs');
|
|
5512
|
+
const path = require('path');
|
|
5513
|
+
const { execSync } = require('child_process');
|
|
5514
|
+
|
|
5515
|
+
const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
|
|
5516
|
+
const CONTEXT_COLD_FILE = path.join('.github', 'context-cold.md');
|
|
5517
|
+
|
|
5518
|
+
function _readContextFiles(cwd) {
|
|
5519
|
+
const paths = [path.join(cwd, CONTEXT_FILE), path.join(cwd, CONTEXT_COLD_FILE)];
|
|
5520
|
+
const chunks = [];
|
|
5521
|
+
for (const p of paths) {
|
|
5522
|
+
if (fs.existsSync(p)) chunks.push(fs.readFileSync(p, 'utf8'));
|
|
5523
|
+
}
|
|
5524
|
+
return chunks.join('\n');
|
|
5525
|
+
}
|
|
5526
|
+
|
|
5527
|
+
// Section header keywords in PROJECT_MAP.md
|
|
5528
|
+
const MAP_SECTIONS = {
|
|
5529
|
+
imports: '### Import graph',
|
|
5530
|
+
classes: '### Class hierarchy',
|
|
5531
|
+
routes: '### Route table',
|
|
5532
|
+
};
|
|
5533
|
+
|
|
5534
|
+
/**
|
|
5535
|
+
* read_context({ module? }) → string
|
|
5536
|
+
*
|
|
5537
|
+
* Returns the full context file, or just the sections whose file paths
|
|
5538
|
+
* contain the given module substring.
|
|
5539
|
+
*/
|
|
5540
|
+
function readContext(args, cwd) {
|
|
5541
|
+
const content = _readContextFiles(cwd);
|
|
5542
|
+
if (!content) {
|
|
5543
|
+
return 'No context file found. Run: node gen-context.js';
|
|
5544
|
+
}
|
|
5545
|
+
|
|
5546
|
+
if (!args || !args.module) return content;
|
|
5547
|
+
|
|
5548
|
+
const mod = args.module.replace(/\\/g, '/').replace(/\/$/, '');
|
|
5549
|
+
const lines = content.split('\n');
|
|
5550
|
+
const result = [];
|
|
5551
|
+
let capturing = false;
|
|
5552
|
+
|
|
5553
|
+
for (const line of lines) {
|
|
5554
|
+
if (line.startsWith('### ')) {
|
|
5555
|
+
const filePath = line.slice(4).trim().replace(/\\/g, '/');
|
|
5556
|
+
// Match if file path starts with mod or contains /mod/ or /mod
|
|
5557
|
+
capturing =
|
|
5558
|
+
filePath === mod ||
|
|
5559
|
+
filePath.startsWith(mod + '/') ||
|
|
5560
|
+
filePath.includes('/' + mod + '/') ||
|
|
5561
|
+
filePath.includes('/' + mod);
|
|
5562
|
+
if (capturing) result.push(line);
|
|
5563
|
+
continue;
|
|
5564
|
+
}
|
|
5565
|
+
if (capturing) result.push(line);
|
|
5566
|
+
}
|
|
5567
|
+
|
|
5568
|
+
if (result.length === 0) return `No signatures found for module: ${mod}`;
|
|
5569
|
+
return result.join('\n');
|
|
5570
|
+
}
|
|
5571
|
+
|
|
5572
|
+
/**
|
|
5573
|
+
* search_signatures({ query }) → string
|
|
5574
|
+
*
|
|
5575
|
+
* Case-insensitive search through all signature lines.
|
|
5576
|
+
* Returns matching lines grouped by file path.
|
|
5577
|
+
*/
|
|
5578
|
+
function searchSignatures(args, cwd) {
|
|
5579
|
+
if (!args || !args.query) return 'Missing required argument: query';
|
|
5580
|
+
|
|
5581
|
+
const query = args.query.toLowerCase();
|
|
5582
|
+
try {
|
|
5583
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
5584
|
+
const index = buildSigIndex(cwd);
|
|
5585
|
+
if (index.size === 0) {
|
|
5532
5586
|
return 'No context file found. Run: node gen-context.js';
|
|
5533
5587
|
}
|
|
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');
|
|
5588
|
+
|
|
5541
5589
|
const result = [];
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
if (
|
|
5546
|
-
|
|
5547
|
-
|
|
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);
|
|
5590
|
+
for (const [file, sigs] of index.entries()) {
|
|
5591
|
+
const hits = sigs.filter((s) => s.toLowerCase().includes(query));
|
|
5592
|
+
if (hits.length === 0) continue;
|
|
5593
|
+
if (result.length > 0) result.push('');
|
|
5594
|
+
result.push(`### ${file}`);
|
|
5595
|
+
result.push(...hits);
|
|
5557
5596
|
}
|
|
5558
|
-
|
|
5559
|
-
if (result.length === 0) return `No signatures found
|
|
5597
|
+
|
|
5598
|
+
if (result.length === 0) return `No signatures found matching: ${args.query}`;
|
|
5560
5599
|
return result.join('\n');
|
|
5600
|
+
} catch (err) {
|
|
5601
|
+
return `_search_signatures failed: ${err.message}_`;
|
|
5561
5602
|
}
|
|
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();
|
|
5603
|
+
}
|
|
5572
5604
|
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5605
|
+
/**
|
|
5606
|
+
* get_map({ type }) → string
|
|
5607
|
+
*
|
|
5608
|
+
* Returns a section from PROJECT_MAP.md.
|
|
5609
|
+
* type: 'imports' | 'classes' | 'routes'
|
|
5610
|
+
*/
|
|
5611
|
+
function getMap(args, cwd) {
|
|
5612
|
+
if (!args || !args.type) return 'Missing required argument: type';
|
|
5579
5613
|
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
if (result.length > 0) result.push('');
|
|
5585
|
-
result.push(`### ${file}`);
|
|
5586
|
-
result.push(...hits);
|
|
5587
|
-
}
|
|
5614
|
+
const header = MAP_SECTIONS[args.type];
|
|
5615
|
+
if (!header) {
|
|
5616
|
+
return `Unknown map type: "${args.type}". Use: imports, classes, routes`;
|
|
5617
|
+
}
|
|
5588
5618
|
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
return `_search_signatures failed: ${err.message}_`;
|
|
5593
|
-
}
|
|
5619
|
+
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
|
5620
|
+
if (!fs.existsSync(mapPath)) {
|
|
5621
|
+
return 'PROJECT_MAP.md not found. Run: node gen-project-map.js';
|
|
5594
5622
|
}
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5623
|
+
|
|
5624
|
+
const content = fs.readFileSync(mapPath, 'utf8');
|
|
5625
|
+
const idx = content.indexOf(header);
|
|
5626
|
+
if (idx === -1) {
|
|
5627
|
+
return `Section "${header}" not found in PROJECT_MAP.md`;
|
|
5628
|
+
}
|
|
5629
|
+
|
|
5630
|
+
// Extract from this header to the next ### header
|
|
5631
|
+
const after = content.slice(idx);
|
|
5632
|
+
const nextMatch = after.slice(header.length).search(/\n###\s/);
|
|
5633
|
+
return nextMatch === -1 ? after : after.slice(0, header.length + nextMatch);
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
/**
|
|
5637
|
+
* create_checkpoint({ note? }) → string
|
|
5638
|
+
*
|
|
5639
|
+
* Returns a markdown checkpoint summarising current project state:
|
|
5640
|
+
* - Timestamp and optional user note
|
|
5641
|
+
* - Active git branch + last 5 commit messages
|
|
5642
|
+
* - Token count of current context file
|
|
5643
|
+
* - List of modules present in the context
|
|
5644
|
+
* - Route count (if PROJECT_MAP.md exists)
|
|
5645
|
+
*/
|
|
5646
|
+
function createCheckpoint(args, cwd) {
|
|
5647
|
+
const note = (args && args.note) ? args.note.trim() : '';
|
|
5648
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
|
5649
|
+
const lines = [
|
|
5650
|
+
'# SigMap Checkpoint',
|
|
5651
|
+
`**Created:** ${now}`,
|
|
5652
|
+
];
|
|
5653
|
+
|
|
5654
|
+
if (note) lines.push(`**Note:** ${note}`);
|
|
5655
|
+
lines.push('');
|
|
5656
|
+
|
|
5657
|
+
// ── Git info ────────────────────────────────────────────────────────────
|
|
5658
|
+
lines.push('## Git state');
|
|
5659
|
+
try {
|
|
5660
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
5661
|
+
cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
5662
|
+
}).trim();
|
|
5663
|
+
lines.push(`**Branch:** ${branch}`);
|
|
5664
|
+
} catch (_) {
|
|
5665
|
+
lines.push('**Branch:** (not a git repo)');
|
|
5666
|
+
}
|
|
5667
|
+
|
|
5668
|
+
try {
|
|
5669
|
+
const log = execSync(
|
|
5670
|
+
'git log --oneline -5 --no-decorate 2>/dev/null',
|
|
5671
|
+
{ cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
5672
|
+
).trim();
|
|
5673
|
+
if (log) {
|
|
5674
|
+
lines.push('');
|
|
5675
|
+
lines.push('**Recent commits:**');
|
|
5676
|
+
for (const l of log.split('\n')) lines.push(`- ${l}`);
|
|
5608
5677
|
}
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5678
|
+
} catch (_) {} // ignore — not every project uses git
|
|
5679
|
+
lines.push('');
|
|
5680
|
+
|
|
5681
|
+
// ── Context stats ────────────────────────────────────────────────────────
|
|
5682
|
+
lines.push('## Context snapshot');
|
|
5683
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
5684
|
+
if (fs.existsSync(contextPath)) {
|
|
5685
|
+
const content = fs.readFileSync(contextPath, 'utf8');
|
|
5686
|
+
const tokens = Math.ceil(content.length / 4);
|
|
5687
|
+
|
|
5688
|
+
// Count modules (### headers are file paths)
|
|
5689
|
+
const modules = content.split('\n').filter((l) => l.startsWith('### ')).map((l) => l.slice(4).trim());
|
|
5690
|
+
lines.push(`**Token count:** ~${tokens}`);
|
|
5691
|
+
lines.push(`**Modules in context:** ${modules.length}`);
|
|
5692
|
+
|
|
5693
|
+
if (modules.length > 0) {
|
|
5694
|
+
lines.push('');
|
|
5695
|
+
lines.push('**Modules:**');
|
|
5696
|
+
for (const m of modules.slice(0, 20)) lines.push(`- ${m}`);
|
|
5697
|
+
if (modules.length > 20) lines.push(`- … and ${modules.length - 20} more`);
|
|
5613
5698
|
}
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5699
|
+
} else {
|
|
5700
|
+
lines.push('_No context file found. Run: node gen-context.js_');
|
|
5701
|
+
}
|
|
5702
|
+
lines.push('');
|
|
5703
|
+
|
|
5704
|
+
// ── Route summary ────────────────────────────────────────────────────────
|
|
5705
|
+
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
|
5706
|
+
if (fs.existsSync(mapPath)) {
|
|
5707
|
+
const mapContent = fs.readFileSync(mapPath, 'utf8');
|
|
5708
|
+
const routeLines = mapContent.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---'));
|
|
5709
|
+
if (routeLines.length > 0) {
|
|
5710
|
+
lines.push('## Routes');
|
|
5711
|
+
lines.push(`**Total routes detected:** ${routeLines.length}`);
|
|
5712
|
+
lines.push('');
|
|
5713
|
+
for (const r of routeLines.slice(0, 10)) lines.push(r);
|
|
5714
|
+
if (routeLines.length > 10) lines.push(`| … | +${routeLines.length - 10} more | |`);
|
|
5715
|
+
lines.push('');
|
|
5619
5716
|
}
|
|
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);
|
|
5625
5717
|
}
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5718
|
+
|
|
5719
|
+
lines.push('---');
|
|
5720
|
+
lines.push('_Generated by SigMap `create_checkpoint`_');
|
|
5721
|
+
|
|
5722
|
+
return lines.join('\n');
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5725
|
+
/**
|
|
5726
|
+
* get_routing({}) → string
|
|
5727
|
+
*
|
|
5728
|
+
* Reads the current context file, classifies all indexed files by complexity,
|
|
5729
|
+
* and returns a formatted markdown routing guide showing which files belong
|
|
5730
|
+
* to the fast/balanced/powerful model tier.
|
|
5731
|
+
*/
|
|
5732
|
+
function getRouting(args, cwd) {
|
|
5733
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
5734
|
+
if (!fs.existsSync(contextPath)) {
|
|
5735
|
+
return (
|
|
5736
|
+
'_No context file found. Run `node gen-context.js --routing` first._\n\n' +
|
|
5737
|
+
'This generates routing hints that map each file to a model tier:\n' +
|
|
5738
|
+
'- **fast** (haiku/gpt-4o-mini) — config, markup, trivial utilities\n' +
|
|
5739
|
+
'- **balanced** (sonnet/gpt-4o) — standard application code\n' +
|
|
5740
|
+
'- **powerful** (opus/gpt-4-turbo) — complex, security-critical, or large modules'
|
|
5741
|
+
);
|
|
5742
|
+
}
|
|
5743
|
+
|
|
5744
|
+
// Parse file list from context (### headings are file paths)
|
|
5745
|
+
const content = fs.readFileSync(contextPath, 'utf8');
|
|
5746
|
+
const fileRels = content.split('\n')
|
|
5747
|
+
.filter((l) => l.startsWith('### '))
|
|
5748
|
+
.map((l) => l.slice(4).trim());
|
|
5749
|
+
|
|
5750
|
+
// Build synthetic fileEntries for the classifier
|
|
5751
|
+
// We don't have live sig arrays here, so rebuild from the context blocks
|
|
5752
|
+
const entries = [];
|
|
5753
|
+
const blocks = content.split(/^### /m).slice(1); // slice past the header
|
|
5754
|
+
for (const block of blocks) {
|
|
5755
|
+
const firstLine = block.split('\n')[0].trim();
|
|
5756
|
+
const codeBlock = block.match(/```\n([\s\S]*?)```/);
|
|
5757
|
+
const sigs = codeBlock ? codeBlock[1].trim().split('\n').filter(Boolean) : [];
|
|
5758
|
+
entries.push({ filePath: path.join(cwd, firstLine), sigs });
|
|
5759
|
+
}
|
|
5760
|
+
|
|
5761
|
+
try {
|
|
5762
|
+
const { classifyAll } = __require('./src/routing/classifier');
|
|
5763
|
+
const { formatRoutingSection } = __require('./src/routing/hints');
|
|
5764
|
+
const groups = classifyAll(entries, cwd);
|
|
5765
|
+
return formatRoutingSection(groups);
|
|
5766
|
+
} catch (err) {
|
|
5767
|
+
return `_Routing classification failed: ${err.message}_`;
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
|
|
5771
|
+
/**
|
|
5772
|
+
* explain_file({ path }) → string
|
|
5773
|
+
*
|
|
5774
|
+
* Returns a file's signatures, its direct imports, and files that import it.
|
|
5775
|
+
* path: relative path from project root (e.g. 'src/services/auth.ts')
|
|
5776
|
+
*/
|
|
5777
|
+
function explainFile(args, cwd) {
|
|
5778
|
+
if (!args || !args.path) return 'Missing required argument: path';
|
|
5779
|
+
|
|
5780
|
+
const targetRel = args.path.replace(/\\/g, '/').replace(/^\//, '');
|
|
5781
|
+
const targetAbs = path.resolve(cwd, targetRel);
|
|
5782
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
5783
|
+
|
|
5784
|
+
const lines = ['# explain_file: ' + targetRel, ''];
|
|
5785
|
+
|
|
5786
|
+
// ── Signatures (hot + cold + cache via buildSigIndex) ───────────────────
|
|
5787
|
+
lines.push('## Signatures');
|
|
5788
|
+
let indexedFiles = [];
|
|
5789
|
+
|
|
5790
|
+
try {
|
|
5791
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
5792
|
+
const index = buildSigIndex(cwd);
|
|
5793
|
+
let sigs = index.get(targetRel);
|
|
5794
|
+
if (!sigs) {
|
|
5795
|
+
for (const [file, fileSigs] of index.entries()) {
|
|
5796
|
+
if (file === targetRel || file.endsWith('/' + targetRel) || targetRel.endsWith('/' + file)) {
|
|
5797
|
+
sigs = fileSigs;
|
|
5798
|
+
break;
|
|
5799
|
+
}
|
|
5689
5800
|
}
|
|
5801
|
+
}
|
|
5802
|
+
if (sigs && sigs.length > 0) {
|
|
5803
|
+
lines.push(...sigs);
|
|
5690
5804
|
} else {
|
|
5691
|
-
lines.push('_No
|
|
5805
|
+
lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
|
|
5692
5806
|
}
|
|
5807
|
+
indexedFiles = [...index.keys()].map((rel) => path.resolve(cwd, rel));
|
|
5808
|
+
} catch (_) {
|
|
5809
|
+
lines.push('_No context file found. Run: node gen-context.js_');
|
|
5810
|
+
}
|
|
5811
|
+
|
|
5812
|
+
if (!fs.existsSync(targetAbs)) {
|
|
5693
5813
|
lines.push('');
|
|
5694
|
-
|
|
5695
|
-
// ── Route summary ────────────────────────────────────────────────────────
|
|
5696
|
-
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
|
5697
|
-
if (fs.existsSync(mapPath)) {
|
|
5698
|
-
const mapContent = fs.readFileSync(mapPath, 'utf8');
|
|
5699
|
-
const routeLines = mapContent.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---'));
|
|
5700
|
-
if (routeLines.length > 0) {
|
|
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
|
-
}
|
|
5708
|
-
}
|
|
5709
|
-
|
|
5710
|
-
lines.push('---');
|
|
5711
|
-
lines.push('_Generated by SigMap `create_checkpoint`_');
|
|
5712
|
-
|
|
5814
|
+
lines.push('> File not found on disk: ' + targetRel);
|
|
5713
5815
|
return lines.join('\n');
|
|
5714
5816
|
}
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
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}_`;
|
|
5817
|
+
|
|
5818
|
+
lines.push('');
|
|
5819
|
+
|
|
5820
|
+
// ── Direct imports ────────────────────────────────────────────────────────
|
|
5821
|
+
lines.push('## Imports (direct dependencies)');
|
|
5822
|
+
try {
|
|
5823
|
+
const { extractImports } = __require('./src/map/import-graph');
|
|
5824
|
+
const fileContent = fs.readFileSync(targetAbs, 'utf8');
|
|
5825
|
+
const fileSet = new Set(indexedFiles);
|
|
5826
|
+
fileSet.add(targetAbs);
|
|
5827
|
+
const imports = extractImports(targetAbs, fileContent, fileSet);
|
|
5828
|
+
if (imports.length > 0) {
|
|
5829
|
+
for (const imp of imports) lines.push('- ' + path.relative(cwd, imp).replace(/\\/g, '/'));
|
|
5830
|
+
} else {
|
|
5831
|
+
lines.push('_No resolvable relative imports found._');
|
|
5759
5832
|
}
|
|
5833
|
+
} catch (err) {
|
|
5834
|
+
lines.push('_Could not analyze imports: ' + err.message + '_');
|
|
5760
5835
|
}
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
const
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
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_');
|
|
5836
|
+
|
|
5837
|
+
lines.push('');
|
|
5838
|
+
|
|
5839
|
+
// ── Callers (reverse-import lookup) ──────────────────────────────────────
|
|
5840
|
+
lines.push('## Callers (files that import this file)');
|
|
5841
|
+
try {
|
|
5842
|
+
const { extractImports } = __require('./src/map/import-graph');
|
|
5843
|
+
const fileSet = new Set(indexedFiles);
|
|
5844
|
+
fileSet.add(targetAbs);
|
|
5845
|
+
const callers = [];
|
|
5846
|
+
for (const f of indexedFiles) {
|
|
5847
|
+
if (f === targetAbs || !fs.existsSync(f)) continue;
|
|
5848
|
+
try {
|
|
5849
|
+
const fc = fs.readFileSync(f, 'utf8');
|
|
5850
|
+
const imps = extractImports(f, fc, fileSet);
|
|
5851
|
+
if (imps.includes(targetAbs)) callers.push(path.relative(cwd, f).replace(/\\/g, '/'));
|
|
5852
|
+
} catch (_) {}
|
|
5804
5853
|
}
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
lines.push('
|
|
5809
|
-
return lines.join('\n');
|
|
5854
|
+
if (callers.length > 0) {
|
|
5855
|
+
for (const c of callers) lines.push('- ' + c);
|
|
5856
|
+
} else {
|
|
5857
|
+
lines.push('_No indexed files import this file._');
|
|
5810
5858
|
}
|
|
5811
|
-
|
|
5812
|
-
lines.push('');
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
}
|
|
5827
|
-
|
|
5859
|
+
} catch (err) {
|
|
5860
|
+
lines.push('_Could not analyze callers: ' + err.message + '_');
|
|
5861
|
+
}
|
|
5862
|
+
|
|
5863
|
+
return lines.join('\n');
|
|
5864
|
+
}
|
|
5865
|
+
|
|
5866
|
+
/**
|
|
5867
|
+
* list_modules({}) → string
|
|
5868
|
+
*
|
|
5869
|
+
* Lists all srcDir modules present in the context file, sorted by token count
|
|
5870
|
+
* descending. Helps agents decide which module to query with read_context.
|
|
5871
|
+
*/
|
|
5872
|
+
function listModules(args, cwd) {
|
|
5873
|
+
try {
|
|
5874
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
5875
|
+
const index = buildSigIndex(cwd);
|
|
5876
|
+
if (index.size === 0) {
|
|
5877
|
+
return 'No context file found. Run: node gen-context.js';
|
|
5828
5878
|
}
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
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 + '_');
|
|
5879
|
+
|
|
5880
|
+
const groups = {};
|
|
5881
|
+
for (const [rel, sigs] of index.entries()) {
|
|
5882
|
+
const parts = rel.replace(/\\/g, '/').split('/');
|
|
5883
|
+
const mod = parts.length > 1 ? parts[0] : '.';
|
|
5884
|
+
if (!groups[mod]) groups[mod] = { fileCount: 0, tokenCount: 0 };
|
|
5885
|
+
groups[mod].fileCount++;
|
|
5886
|
+
groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
|
|
5853
5887
|
}
|
|
5854
|
-
|
|
5855
|
-
return lines.join('\n');
|
|
5856
|
-
}
|
|
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
5888
|
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
const mod = parts.length > 1 ? parts[0] : '.';
|
|
5870
|
-
if (!groups[mod]) groups[mod] = { fileCount: 0, tokenCount: 0 };
|
|
5871
|
-
groups[mod].fileCount++;
|
|
5872
|
-
groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
|
|
5873
|
-
}
|
|
5889
|
+
const sorted = Object.entries(groups)
|
|
5890
|
+
.map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
|
|
5891
|
+
.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
5874
5892
|
|
|
5875
|
-
|
|
5876
|
-
.map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
|
|
5877
|
-
.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
5893
|
+
if (sorted.length === 0) return 'No modules found in context file.';
|
|
5878
5894
|
|
|
5879
|
-
|
|
5895
|
+
const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
|
|
5880
5896
|
|
|
5881
|
-
|
|
5897
|
+
return [
|
|
5898
|
+
'# Modules',
|
|
5899
|
+
'',
|
|
5900
|
+
'| Module | Files | Tokens |',
|
|
5901
|
+
'|--------|-------|--------|',
|
|
5902
|
+
...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
|
|
5903
|
+
'',
|
|
5904
|
+
`**Total context tokens: ~${total}**`,
|
|
5905
|
+
'',
|
|
5906
|
+
'_Use `read_context({ module: "name" })` to get signatures for a specific module._',
|
|
5907
|
+
].join('\n');
|
|
5908
|
+
} catch (err) {
|
|
5909
|
+
return `_list_modules failed: ${err.message}_`;
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5882
5912
|
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
}
|
|
5895
|
-
|
|
5896
|
-
|
|
5913
|
+
/**
|
|
5914
|
+
* query_context({ query, topK? }) → string
|
|
5915
|
+
*
|
|
5916
|
+
* Ranks context-file entries by relevance to the query and returns the
|
|
5917
|
+
* top-K most relevant files with their signatures and scores.
|
|
5918
|
+
*/
|
|
5919
|
+
function queryContext(args, cwd) {
|
|
5920
|
+
if (!args || !args.query) return 'Missing required argument: query';
|
|
5921
|
+
|
|
5922
|
+
try {
|
|
5923
|
+
const { rank, buildSigIndex, formatRankTable } = __require('./src/retrieval/ranker');
|
|
5924
|
+
const { buildFromCwd } = __require('./src/graph/builder');
|
|
5925
|
+
const index = buildSigIndex(cwd);
|
|
5926
|
+
if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';
|
|
5927
|
+
|
|
5928
|
+
const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
|
|
5929
|
+
// Build dependency graph for neighbor boost — non-fatal if it fails
|
|
5930
|
+
let graph = null;
|
|
5931
|
+
try { graph = buildFromCwd(cwd); } catch (_) {}
|
|
5932
|
+
const results = rank(args.query, index, { topK, cwd, graph });
|
|
5933
|
+
return formatRankTable(results, args.query);
|
|
5934
|
+
} catch (err) {
|
|
5935
|
+
return `_query_context failed: ${err.message}_`;
|
|
5897
5936
|
}
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
}
|
|
5911
|
-
|
|
5912
|
-
}
|
|
5937
|
+
}
|
|
5938
|
+
|
|
5939
|
+
/**
|
|
5940
|
+
* get_impact({ file, depth? }) → string
|
|
5941
|
+
*
|
|
5942
|
+
* Returns a formatted markdown impact report for the given file:
|
|
5943
|
+
* direct importers, transitive importers, affected tests, affected routes.
|
|
5944
|
+
*/
|
|
5945
|
+
function getImpact(args, cwd) {
|
|
5946
|
+
if (!args || !args.file) return 'Missing required argument: file';
|
|
5947
|
+
|
|
5948
|
+
try {
|
|
5949
|
+
const { analyzeImpact, formatImpact } = __require('./src/graph/impact');
|
|
5950
|
+
const depth = Math.max(0, parseInt(args.depth, 10) || 3);
|
|
5951
|
+
const results = analyzeImpact(args.file, cwd, { depth });
|
|
5952
|
+
if (results.length === 0) return `No impact data for: ${args.file}`;
|
|
5953
|
+
return results.map((r) => formatImpact(r.impact)).join('\n\n---\n\n');
|
|
5954
|
+
} catch (err) {
|
|
5955
|
+
return `_get_impact failed: ${err.message}_`;
|
|
5913
5956
|
}
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5957
|
+
}
|
|
5958
|
+
|
|
5959
|
+
/**
|
|
5960
|
+
* get_lines({ file, start, end }) → string
|
|
5961
|
+
*
|
|
5962
|
+
* Surgical Context demand-driven fetch: returns an exact, clamped line range from a
|
|
5963
|
+
* source file. The path is resolved inside the project root (no traversal escape) and
|
|
5964
|
+
* the returned lines are secret-scanned via the same redactor used for signatures.
|
|
5965
|
+
*/
|
|
5966
|
+
function getLines(args, cwd) {
|
|
5967
|
+
if (!args || !args.file) return 'Missing required argument: file';
|
|
5968
|
+
|
|
5969
|
+
const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
|
|
5970
|
+
const abs = path.resolve(cwd, rel);
|
|
5971
|
+
|
|
5972
|
+
// Sandbox: refuse paths that resolve outside the project root.
|
|
5973
|
+
const root = path.resolve(cwd);
|
|
5974
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
5975
|
+
return `Refused: ${rel} resolves outside the project root`;
|
|
5976
|
+
}
|
|
5977
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
5978
|
+
return `File not found: ${rel}`;
|
|
5925
5979
|
}
|
|
5926
5980
|
|
|
5927
|
-
|
|
5928
|
-
|
|
5981
|
+
const start = parseInt(args.start, 10);
|
|
5982
|
+
const end = parseInt(args.end, 10);
|
|
5983
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
5984
|
+
return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
|
|
5985
|
+
}
|
|
5929
5986
|
|
|
5930
|
-
|
|
5931
|
-
|
|
5987
|
+
let lines;
|
|
5988
|
+
try {
|
|
5989
|
+
lines = fs.readFileSync(abs, 'utf8').split('\n');
|
|
5990
|
+
} catch (err) {
|
|
5991
|
+
return `Could not read ${rel}: ${err.message}`;
|
|
5992
|
+
}
|
|
5932
5993
|
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
}
|
|
5938
|
-
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
5939
|
-
return `File not found: ${rel}`;
|
|
5940
|
-
}
|
|
5994
|
+
const total = lines.length;
|
|
5995
|
+
const from = Math.max(1, Math.min(start, end));
|
|
5996
|
+
const to = Math.min(total, Math.max(start, end));
|
|
5997
|
+
if (from > total) return `${rel} has only ${total} lines; requested ${start}-${end}`;
|
|
5941
5998
|
|
|
5942
|
-
|
|
5943
|
-
const end = parseInt(args.end, 10);
|
|
5944
|
-
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
5945
|
-
return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
|
|
5946
|
-
}
|
|
5999
|
+
const slice = lines.slice(from - 1, to);
|
|
5947
6000
|
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
}
|
|
5952
|
-
|
|
5953
|
-
|
|
6001
|
+
// Redaction scan: reuse the signature secret scanner line-by-line.
|
|
6002
|
+
let safeLines = slice;
|
|
6003
|
+
try {
|
|
6004
|
+
const { scan } = __require('./src/security/scanner');
|
|
6005
|
+
safeLines = scan(slice, rel).safe;
|
|
6006
|
+
} catch (_) {} // non-fatal: fall back to raw slice
|
|
5954
6007
|
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
6008
|
+
return [
|
|
6009
|
+
`# ${rel}:${from}-${to}`,
|
|
6010
|
+
'```',
|
|
6011
|
+
...safeLines,
|
|
6012
|
+
'```',
|
|
6013
|
+
].join('\n');
|
|
6014
|
+
}
|
|
5959
6015
|
|
|
5960
|
-
|
|
6016
|
+
/**
|
|
6017
|
+
* read_memory({ limit? }) → string
|
|
6018
|
+
*
|
|
6019
|
+
* Recall the cross-session decision log (notes) plus the last ranking-session
|
|
6020
|
+
* focus, formatted for an agent to consume at the start of a task.
|
|
6021
|
+
*/
|
|
6022
|
+
function readMemory(args, cwd) {
|
|
6023
|
+
let limit = parseInt(args && args.limit, 10);
|
|
6024
|
+
if (!Number.isFinite(limit) || limit <= 0) limit = 10;
|
|
6025
|
+
limit = Math.min(limit, 50);
|
|
5961
6026
|
|
|
5962
|
-
|
|
5963
|
-
try {
|
|
5964
|
-
const { scan } = __require('./src/security/scanner');
|
|
5965
|
-
safeLines = scan(slice, rel).safe;
|
|
5966
|
-
} catch (_) {}
|
|
6027
|
+
const out = ['# SigMap memory'];
|
|
5967
6028
|
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
6029
|
+
let notes = [];
|
|
6030
|
+
try {
|
|
6031
|
+
const { readNotes, formatNotes } = __require('./src/session/notes');
|
|
6032
|
+
notes = readNotes(cwd, limit);
|
|
6033
|
+
out.push('');
|
|
6034
|
+
out.push(`## Recent notes (${notes.length})`);
|
|
6035
|
+
// Most recent first for quick scanning.
|
|
6036
|
+
out.push(formatNotes(notes.slice().reverse()));
|
|
6037
|
+
} catch (_) {
|
|
6038
|
+
out.push('');
|
|
6039
|
+
out.push('_No notes available._');
|
|
5974
6040
|
}
|
|
5975
6041
|
|
|
5976
|
-
|
|
5977
|
-
|
|
6042
|
+
// Last ranking-session focus (if any) — extends src/session/memory.js.
|
|
6043
|
+
try {
|
|
6044
|
+
const { loadSession } = __require('./src/session/memory');
|
|
6045
|
+
const s = loadSession(cwd);
|
|
6046
|
+
if (s && (s.lastQuery || (s.topFiles && s.topFiles.length))) {
|
|
6047
|
+
out.push('');
|
|
6048
|
+
out.push('## Last session');
|
|
6049
|
+
if (s.lastQuery) out.push(`**Last query:** ${s.lastQuery}`);
|
|
6050
|
+
if (s.topFiles && s.topFiles.length) {
|
|
6051
|
+
out.push(`**Focus files:** ${s.topFiles.map((f) => f.file).slice(0, 5).join(', ')}`);
|
|
6052
|
+
}
|
|
6053
|
+
}
|
|
6054
|
+
} catch (_) { /* session optional */ }
|
|
6055
|
+
|
|
6056
|
+
return out.join('\n');
|
|
6057
|
+
}
|
|
5978
6058
|
|
|
6059
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory };
|
|
6060
|
+
|
|
6061
|
+
};
|
|
5979
6062
|
// ── ./src/learning/weights ──
|
|
5980
6063
|
__factories["./src/learning/weights"] = function(module, exports) {
|
|
5981
6064
|
'use strict';
|
|
@@ -6135,344 +6218,364 @@ __factories["./src/learning/weights"] = function(module, exports) {
|
|
|
6135
6218
|
|
|
6136
6219
|
// ── ./src/mcp/server ──
|
|
6137
6220
|
__factories["./src/mcp/server"] = function(module, exports) {
|
|
6138
|
-
|
|
6139
|
-
/**
|
|
6140
|
-
* SigMap MCP server — zero npm dependencies.
|
|
6141
|
-
*
|
|
6142
|
-
* Wire protocol: JSON-RPC 2.0 over stdio.
|
|
6143
|
-
* One JSON object per line on both stdin and stdout.
|
|
6144
|
-
*
|
|
6145
|
-
* Supported methods:
|
|
6146
|
-
* initialize → serverInfo + capabilities
|
|
6147
|
-
* tools/list → 10 tool definitions
|
|
6148
|
-
* tools/call → dispatch to handler, return result
|
|
6149
|
-
*/
|
|
6221
|
+
'use strict';
|
|
6150
6222
|
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6223
|
+
/**
|
|
6224
|
+
* SigMap MCP server — zero npm dependencies.
|
|
6225
|
+
*
|
|
6226
|
+
* Wire protocol: JSON-RPC 2.0 over stdio.
|
|
6227
|
+
* One JSON object per line on both stdin and stdout.
|
|
6228
|
+
*
|
|
6229
|
+
* Supported methods:
|
|
6230
|
+
* initialize → serverInfo + capabilities
|
|
6231
|
+
* tools/list → 11 tool definitions
|
|
6232
|
+
* tools/call → dispatch to handler, return result
|
|
6233
|
+
*/
|
|
6154
6234
|
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6235
|
+
const readline = require('readline');
|
|
6236
|
+
const { TOOLS } = __require('./src/mcp/tools');
|
|
6237
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory } = __require('./src/mcp/handlers');
|
|
6238
|
+
|
|
6239
|
+
const SERVER_INFO = {
|
|
6240
|
+
name: 'sigmap',
|
|
6241
|
+
version: '6.15.0',
|
|
6242
|
+
description: 'SigMap MCP server — code signatures on demand',
|
|
6243
|
+
};
|
|
6244
|
+
|
|
6245
|
+
// ---------------------------------------------------------------------------
|
|
6246
|
+
// JSON-RPC helpers
|
|
6247
|
+
// ---------------------------------------------------------------------------
|
|
6248
|
+
function respond(id, result) {
|
|
6249
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
|
|
6250
|
+
}
|
|
6251
|
+
|
|
6252
|
+
function respondError(id, code, message) {
|
|
6253
|
+
process.stdout.write(
|
|
6254
|
+
JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n'
|
|
6255
|
+
);
|
|
6256
|
+
}
|
|
6257
|
+
|
|
6258
|
+
// ---------------------------------------------------------------------------
|
|
6259
|
+
// Method dispatcher
|
|
6260
|
+
// ---------------------------------------------------------------------------
|
|
6261
|
+
function dispatch(msg, cwd) {
|
|
6262
|
+
const { method, id, params } = msg;
|
|
6263
|
+
|
|
6264
|
+
// Notifications (no id) need no response
|
|
6265
|
+
if (method === 'notifications/initialized' || method === 'notifications/cancelled') {
|
|
6266
|
+
return;
|
|
6166
6267
|
}
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6268
|
+
|
|
6269
|
+
if (method === 'initialize') {
|
|
6270
|
+
respond(id, {
|
|
6271
|
+
protocolVersion: (params && params.protocolVersion) || '2024-11-05',
|
|
6272
|
+
serverInfo: SERVER_INFO,
|
|
6273
|
+
capabilities: { tools: {} },
|
|
6274
|
+
});
|
|
6275
|
+
return;
|
|
6172
6276
|
}
|
|
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}`);
|
|
6277
|
+
|
|
6278
|
+
if (method === 'tools/list') {
|
|
6279
|
+
respond(id, { tools: TOOLS });
|
|
6280
|
+
return;
|
|
6281
|
+
}
|
|
6282
|
+
|
|
6283
|
+
if (method === 'tools/call') {
|
|
6284
|
+
const name = params && params.name;
|
|
6285
|
+
const args = (params && params.arguments) || {};
|
|
6286
|
+
|
|
6287
|
+
let text;
|
|
6288
|
+
try {
|
|
6289
|
+
if (name === 'read_context') text = readContext(args, cwd);
|
|
6290
|
+
else if (name === 'search_signatures') text = searchSignatures(args, cwd);
|
|
6291
|
+
else if (name === 'get_map') text = getMap(args, cwd);
|
|
6292
|
+
else if (name === 'create_checkpoint') text = createCheckpoint(args, cwd);
|
|
6293
|
+
else if (name === 'get_routing') text = getRouting(args, cwd);
|
|
6294
|
+
else if (name === 'explain_file') text = explainFile(args, cwd);
|
|
6295
|
+
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
6296
|
+
else if (name === 'query_context') text = queryContext(args, cwd);
|
|
6297
|
+
else if (name === 'get_impact') text = getImpact(args, cwd);
|
|
6298
|
+
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
6299
|
+
else if (name === 'read_memory') text = readMemory(args, cwd);
|
|
6300
|
+
else {
|
|
6301
|
+
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
6221
6302
|
return;
|
|
6222
6303
|
}
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
content: [{ type: 'text', text: String(text) }],
|
|
6226
|
-
});
|
|
6304
|
+
} catch (err) {
|
|
6305
|
+
respondError(id, -32603, `Tool error: ${err.message}`);
|
|
6227
6306
|
return;
|
|
6228
6307
|
}
|
|
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);
|
|
6308
|
+
|
|
6309
|
+
respond(id, {
|
|
6310
|
+
content: [{ type: 'text', text: String(text) }],
|
|
6264
6311
|
});
|
|
6312
|
+
return;
|
|
6265
6313
|
}
|
|
6266
|
-
|
|
6267
|
-
module.exports = { start };
|
|
6268
|
-
|
|
6269
|
-
};
|
|
6270
6314
|
|
|
6315
|
+
// Unknown method
|
|
6316
|
+
if (id !== undefined && id !== null) {
|
|
6317
|
+
respondError(id, -32601, `Method not found: ${method}`);
|
|
6318
|
+
}
|
|
6319
|
+
}
|
|
6320
|
+
|
|
6321
|
+
// ---------------------------------------------------------------------------
|
|
6322
|
+
// Server entry point
|
|
6323
|
+
// ---------------------------------------------------------------------------
|
|
6324
|
+
function start(cwd) {
|
|
6325
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
6326
|
+
|
|
6327
|
+
rl.on('line', (line) => {
|
|
6328
|
+
const trimmed = line.trim();
|
|
6329
|
+
if (!trimmed) return;
|
|
6330
|
+
|
|
6331
|
+
let msg;
|
|
6332
|
+
try {
|
|
6333
|
+
msg = JSON.parse(trimmed);
|
|
6334
|
+
} catch (_) {
|
|
6335
|
+
// Cannot respond without a valid id — ignore malformed input
|
|
6336
|
+
return;
|
|
6337
|
+
}
|
|
6338
|
+
|
|
6339
|
+
try {
|
|
6340
|
+
dispatch(msg, cwd);
|
|
6341
|
+
} catch (err) {
|
|
6342
|
+
const id = (msg && msg.id) != null ? msg.id : null;
|
|
6343
|
+
respondError(id, -32603, `Internal error: ${err.message}`);
|
|
6344
|
+
}
|
|
6345
|
+
});
|
|
6346
|
+
|
|
6347
|
+
rl.on('close', () => {
|
|
6348
|
+
process.exit(0);
|
|
6349
|
+
});
|
|
6350
|
+
}
|
|
6351
|
+
|
|
6352
|
+
module.exports = { start };
|
|
6353
|
+
|
|
6354
|
+
};
|
|
6271
6355
|
// ── ./src/mcp/tools ──
|
|
6272
6356
|
__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
|
-
|
|
6357
|
+
'use strict';
|
|
6358
|
+
|
|
6359
|
+
/**
|
|
6360
|
+
* MCP tool definitions for SigMap (11 tools).
|
|
6361
|
+
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
6362
|
+
* explain_file, list_modules, query_context, get_impact, get_lines, read_memory.
|
|
6363
|
+
*/
|
|
6364
|
+
|
|
6365
|
+
const TOOLS = [
|
|
6366
|
+
{
|
|
6367
|
+
name: 'read_context',
|
|
6368
|
+
description:
|
|
6369
|
+
'Read extracted code signatures for the project or a specific module path. ' +
|
|
6370
|
+
'Returns the full copilot-instructions.md content (~500–4K tokens) or a ' +
|
|
6371
|
+
'filtered subset when a module path is provided (~50–500 tokens).',
|
|
6372
|
+
inputSchema: {
|
|
6373
|
+
type: 'object',
|
|
6374
|
+
properties: {
|
|
6375
|
+
module: {
|
|
6376
|
+
type: 'string',
|
|
6377
|
+
description:
|
|
6378
|
+
'Optional subdirectory path to scope results (e.g. "src/services"). ' +
|
|
6379
|
+
'Omit to get the full codebase context.',
|
|
6295
6380
|
},
|
|
6296
|
-
required: [],
|
|
6297
6381
|
},
|
|
6382
|
+
required: [],
|
|
6298
6383
|
},
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6384
|
+
},
|
|
6385
|
+
{
|
|
6386
|
+
name: 'search_signatures',
|
|
6387
|
+
description:
|
|
6388
|
+
'Search extracted code signatures for a keyword, function name, or class name. ' +
|
|
6389
|
+
'Returns matching signature lines with their file paths.',
|
|
6390
|
+
inputSchema: {
|
|
6391
|
+
type: 'object',
|
|
6392
|
+
properties: {
|
|
6393
|
+
query: {
|
|
6394
|
+
type: 'string',
|
|
6395
|
+
description: 'Keyword to search for in signatures (case-insensitive).',
|
|
6311
6396
|
},
|
|
6312
|
-
required: ['query'],
|
|
6313
6397
|
},
|
|
6398
|
+
required: ['query'],
|
|
6314
6399
|
},
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6400
|
+
},
|
|
6401
|
+
{
|
|
6402
|
+
name: 'get_map',
|
|
6403
|
+
description:
|
|
6404
|
+
'Read a section from PROJECT_MAP.md — import graph, class hierarchy, or route table. ' +
|
|
6405
|
+
'Requires gen-project-map.js to have been run first.',
|
|
6406
|
+
inputSchema: {
|
|
6407
|
+
type: 'object',
|
|
6408
|
+
properties: {
|
|
6409
|
+
type: {
|
|
6410
|
+
type: 'string',
|
|
6411
|
+
enum: ['imports', 'classes', 'routes'],
|
|
6412
|
+
description: 'Which section to retrieve: imports, classes, or routes.',
|
|
6328
6413
|
},
|
|
6329
|
-
required: ['type'],
|
|
6330
6414
|
},
|
|
6415
|
+
required: ['type'],
|
|
6331
6416
|
},
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6417
|
+
},
|
|
6418
|
+
{
|
|
6419
|
+
name: 'create_checkpoint',
|
|
6420
|
+
description:
|
|
6421
|
+
'Create a session checkpoint summarising current project state. ' +
|
|
6422
|
+
'Returns recent git commits, active branch, token count, and a ' +
|
|
6423
|
+
'compact snapshot of the codebase context — ideal for session handoffs ' +
|
|
6424
|
+
'or periodic saves during long coding sessions.',
|
|
6425
|
+
inputSchema: {
|
|
6426
|
+
type: 'object',
|
|
6427
|
+
properties: {
|
|
6428
|
+
note: {
|
|
6429
|
+
type: 'string',
|
|
6430
|
+
description: 'Optional free-text note to include in the checkpoint (e.g. what you were working on).',
|
|
6346
6431
|
},
|
|
6347
|
-
required: [],
|
|
6348
6432
|
},
|
|
6433
|
+
required: [],
|
|
6349
6434
|
},
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6435
|
+
},
|
|
6436
|
+
{
|
|
6437
|
+
name: 'get_routing',
|
|
6438
|
+
description:
|
|
6439
|
+
'Get model routing hints for this project — which files belong to which complexity ' +
|
|
6440
|
+
'tier (fast/balanced/powerful) and which AI model to use for each type of task. ' +
|
|
6441
|
+
'Helps reduce API costs by 40–80% by routing simple tasks to cheaper models.',
|
|
6442
|
+
inputSchema: {
|
|
6443
|
+
type: 'object',
|
|
6444
|
+
properties: {},
|
|
6445
|
+
required: [],
|
|
6361
6446
|
},
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6447
|
+
},
|
|
6448
|
+
{
|
|
6449
|
+
name: 'explain_file',
|
|
6450
|
+
description:
|
|
6451
|
+
'Explain a specific file: returns its extracted signatures, direct imports ' +
|
|
6452
|
+
'(files it depends on), and callers (files that import it). ' +
|
|
6453
|
+
'Ideal for understanding a file in isolation without reading raw source. ' +
|
|
6454
|
+
'Requires the context file to have been generated first.',
|
|
6455
|
+
inputSchema: {
|
|
6456
|
+
type: 'object',
|
|
6457
|
+
properties: {
|
|
6458
|
+
path: {
|
|
6459
|
+
type: 'string',
|
|
6460
|
+
description:
|
|
6461
|
+
'Relative path from the project root (e.g. "src/services/auth.ts"). ' +
|
|
6462
|
+
'Use the paths shown in read_context output.',
|
|
6378
6463
|
},
|
|
6379
|
-
required: ['path'],
|
|
6380
6464
|
},
|
|
6465
|
+
required: ['path'],
|
|
6381
6466
|
},
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6467
|
+
},
|
|
6468
|
+
{
|
|
6469
|
+
name: 'list_modules',
|
|
6470
|
+
description:
|
|
6471
|
+
'List all top-level modules (srcDirs) present in the context file, ' +
|
|
6472
|
+
'sorted by token count descending. Use this to decide which module to ' +
|
|
6473
|
+
'pass to read_context before querying a specific area of the codebase.',
|
|
6474
|
+
inputSchema: {
|
|
6475
|
+
type: 'object',
|
|
6476
|
+
properties: {},
|
|
6477
|
+
required: [],
|
|
6478
|
+
},
|
|
6479
|
+
},
|
|
6480
|
+
{
|
|
6481
|
+
name: 'query_context',
|
|
6482
|
+
description:
|
|
6483
|
+
'Rank and return the most relevant files for a specific task or question. ' +
|
|
6484
|
+
'Uses keyword + symbol + path scoring to surface only the top-K files relevant ' +
|
|
6485
|
+
'to the query — much cheaper than reading all context. ' +
|
|
6486
|
+
'Returns ranked file list with signatures and relevance scores.',
|
|
6487
|
+
inputSchema: {
|
|
6488
|
+
type: 'object',
|
|
6489
|
+
properties: {
|
|
6490
|
+
query: {
|
|
6491
|
+
type: 'string',
|
|
6492
|
+
description:
|
|
6493
|
+
'Natural language task description or keyword(s) to rank files against. ' +
|
|
6494
|
+
'E.g. "add a new language extractor", "fix secret scanning", "auth module".',
|
|
6495
|
+
},
|
|
6496
|
+
topK: {
|
|
6497
|
+
type: 'number',
|
|
6498
|
+
description: 'Maximum number of files to return (default: 10, max: 25).',
|
|
6499
|
+
},
|
|
6392
6500
|
},
|
|
6501
|
+
required: ['query'],
|
|
6393
6502
|
},
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6503
|
+
},
|
|
6504
|
+
{
|
|
6505
|
+
name: 'get_impact',
|
|
6506
|
+
description:
|
|
6507
|
+
'Show every file that is impacted when a given file changes — direct importers, ' +
|
|
6508
|
+
'transitive importers, affected tests, and affected routes/controllers. ' +
|
|
6509
|
+
'Gives agents instant blast-radius awareness before making a change. ' +
|
|
6510
|
+
'Handles circular dependencies safely (no infinite loops).',
|
|
6511
|
+
inputSchema: {
|
|
6512
|
+
type: 'object',
|
|
6513
|
+
properties: {
|
|
6514
|
+
file: {
|
|
6515
|
+
type: 'string',
|
|
6516
|
+
description:
|
|
6517
|
+
'Relative path from the project root of the file that changed ' +
|
|
6518
|
+
'(e.g. "src/extractors/python.js"). Use forward slashes.',
|
|
6519
|
+
},
|
|
6520
|
+
depth: {
|
|
6521
|
+
type: 'number',
|
|
6522
|
+
description: 'BFS traversal depth limit (default: 3). Use 0 for unlimited.',
|
|
6414
6523
|
},
|
|
6415
|
-
required: ['query'],
|
|
6416
6524
|
},
|
|
6525
|
+
required: ['file'],
|
|
6417
6526
|
},
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6527
|
+
},
|
|
6528
|
+
{
|
|
6529
|
+
name: 'get_lines',
|
|
6530
|
+
description:
|
|
6531
|
+
'Fetch an exact line range from a source file on demand — the Surgical Context ' +
|
|
6532
|
+
'workhorse. Signatures carry `path:start-end` anchors; call this to read just those ' +
|
|
6533
|
+
'lines instead of re-opening the whole file. Lines are clamped to the file bounds and ' +
|
|
6534
|
+
'secret-scanned (redacted) before return. Path is sandboxed to the project root.',
|
|
6535
|
+
inputSchema: {
|
|
6536
|
+
type: 'object',
|
|
6537
|
+
properties: {
|
|
6538
|
+
file: {
|
|
6539
|
+
type: 'string',
|
|
6540
|
+
description:
|
|
6541
|
+
'Relative path from the project root (e.g. "src/config/loader.js"). ' +
|
|
6542
|
+
'Use the path shown in a signature anchor. Use forward slashes.',
|
|
6543
|
+
},
|
|
6544
|
+
start: {
|
|
6545
|
+
type: 'number',
|
|
6546
|
+
description: '1-based start line (inclusive). Clamped to the file bounds.',
|
|
6547
|
+
},
|
|
6548
|
+
end: {
|
|
6549
|
+
type: 'number',
|
|
6550
|
+
description: '1-based end line (inclusive). Clamped to the file bounds.',
|
|
6438
6551
|
},
|
|
6439
|
-
required: ['file'],
|
|
6440
6552
|
},
|
|
6553
|
+
required: ['file', 'start', 'end'],
|
|
6441
6554
|
},
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
'Use the path shown in a signature anchor. Use forward slashes.',
|
|
6457
|
-
},
|
|
6458
|
-
start: {
|
|
6459
|
-
type: 'number',
|
|
6460
|
-
description: '1-based start line (inclusive). Clamped to the file bounds.',
|
|
6461
|
-
},
|
|
6462
|
-
end: {
|
|
6463
|
-
type: 'number',
|
|
6464
|
-
description: '1-based end line (inclusive). Clamped to the file bounds.',
|
|
6465
|
-
},
|
|
6555
|
+
},
|
|
6556
|
+
{
|
|
6557
|
+
name: 'read_memory',
|
|
6558
|
+
description:
|
|
6559
|
+
'Recall the project decision log — recent notes left by humans or agents ' +
|
|
6560
|
+
'across sessions (via `sigmap note`), plus the last ranking-session focus. ' +
|
|
6561
|
+
'Call this at the start of a task to kill cold-start: it answers ' +
|
|
6562
|
+
'"what were we doing and why" without re-reading the whole codebase.',
|
|
6563
|
+
inputSchema: {
|
|
6564
|
+
type: 'object',
|
|
6565
|
+
properties: {
|
|
6566
|
+
limit: {
|
|
6567
|
+
type: 'number',
|
|
6568
|
+
description: 'How many of the most recent notes to return (default: 10, max: 50).',
|
|
6466
6569
|
},
|
|
6467
|
-
required: ['file', 'start', 'end'],
|
|
6468
6570
|
},
|
|
6571
|
+
required: [],
|
|
6469
6572
|
},
|
|
6470
|
-
|
|
6573
|
+
},
|
|
6574
|
+
];
|
|
6471
6575
|
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
};
|
|
6576
|
+
module.exports = { TOOLS };
|
|
6475
6577
|
|
|
6578
|
+
};
|
|
6476
6579
|
// ── ./src/routing/classifier ──
|
|
6477
6580
|
__factories["./src/routing/classifier"] = function(module, exports) {
|
|
6478
6581
|
|
|
@@ -7007,6 +7110,110 @@ __factories["./src/tracking/logger"] = function(module, exports) {
|
|
|
7007
7110
|
|
|
7008
7111
|
};
|
|
7009
7112
|
|
|
7113
|
+
// ── ./src/session/notes ──
|
|
7114
|
+
__factories["./src/session/notes"] = function(module, exports) {
|
|
7115
|
+
'use strict';
|
|
7116
|
+
|
|
7117
|
+
/**
|
|
7118
|
+
* Decision-log notes (Memory, v6.15.0).
|
|
7119
|
+
*
|
|
7120
|
+
* A tiny append-only log of human/agent notes that survives across sessions,
|
|
7121
|
+
* so an agent starting cold can recall "what were we doing / why". Stored as
|
|
7122
|
+
* NDJSON under `.context/` to match the project's other state logs
|
|
7123
|
+
* (usage.ndjson, benchmark-history.ndjson). Zero dependencies.
|
|
7124
|
+
*
|
|
7125
|
+
* Consumed by the `read_memory` MCP tool and the `sigmap note` / `sigmap status`
|
|
7126
|
+
* commands. Complements `src/session/memory.js` (ranking session) rather than
|
|
7127
|
+
* duplicating it.
|
|
7128
|
+
*/
|
|
7129
|
+
|
|
7130
|
+
const fs = require('fs');
|
|
7131
|
+
const path = require('path');
|
|
7132
|
+
const { execSync } = require('child_process');
|
|
7133
|
+
|
|
7134
|
+
const NOTES_FILE = path.join('.context', 'notes.ndjson');
|
|
7135
|
+
const MAX_TEXT = 2000;
|
|
7136
|
+
|
|
7137
|
+
function notesPath(cwd) {
|
|
7138
|
+
return path.join(cwd, NOTES_FILE);
|
|
7139
|
+
}
|
|
7140
|
+
|
|
7141
|
+
function _currentBranch(cwd) {
|
|
7142
|
+
try {
|
|
7143
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
7144
|
+
cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
7145
|
+
}).trim() || null;
|
|
7146
|
+
} catch (_) {
|
|
7147
|
+
return null;
|
|
7148
|
+
}
|
|
7149
|
+
}
|
|
7150
|
+
|
|
7151
|
+
/**
|
|
7152
|
+
* Append a note. Returns the stored entry.
|
|
7153
|
+
* @param {string} cwd
|
|
7154
|
+
* @param {string} text
|
|
7155
|
+
* @param {object} [opts]
|
|
7156
|
+
* @param {string|null} [opts.branch] override branch (default: current git branch)
|
|
7157
|
+
* @param {string} [opts.tag] optional category tag
|
|
7158
|
+
*/
|
|
7159
|
+
function addNote(cwd, text, opts = {}) {
|
|
7160
|
+
const clean = String(text == null ? '' : text).trim().slice(0, MAX_TEXT);
|
|
7161
|
+
if (!clean) throw new Error('note text is empty');
|
|
7162
|
+
const entry = {
|
|
7163
|
+
ts: new Date().toISOString(),
|
|
7164
|
+
text: clean,
|
|
7165
|
+
branch: opts.branch !== undefined ? opts.branch : _currentBranch(cwd),
|
|
7166
|
+
};
|
|
7167
|
+
if (opts.tag) entry.tag = String(opts.tag).trim().slice(0, 40);
|
|
7168
|
+
const p = notesPath(cwd);
|
|
7169
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
7170
|
+
fs.appendFileSync(p, JSON.stringify(entry) + '\n');
|
|
7171
|
+
return entry;
|
|
7172
|
+
}
|
|
7173
|
+
|
|
7174
|
+
/**
|
|
7175
|
+
* Read notes in chronological order (oldest first).
|
|
7176
|
+
* @param {string} cwd
|
|
7177
|
+
* @param {number} [limit=0] keep only the most recent N (0 = all)
|
|
7178
|
+
* @returns {object[]}
|
|
7179
|
+
*/
|
|
7180
|
+
function readNotes(cwd, limit = 0) {
|
|
7181
|
+
let raw;
|
|
7182
|
+
try { raw = fs.readFileSync(notesPath(cwd), 'utf8'); }
|
|
7183
|
+
catch (_) { return []; }
|
|
7184
|
+
const out = [];
|
|
7185
|
+
for (const line of raw.split('\n')) {
|
|
7186
|
+
const t = line.trim();
|
|
7187
|
+
if (!t) continue;
|
|
7188
|
+
try { out.push(JSON.parse(t)); } catch (_) { /* skip corrupt line */ }
|
|
7189
|
+
}
|
|
7190
|
+
if (limit > 0 && out.length > limit) return out.slice(out.length - limit);
|
|
7191
|
+
return out;
|
|
7192
|
+
}
|
|
7193
|
+
|
|
7194
|
+
/** Format notes as a Markdown list (pass already-ordered notes). */
|
|
7195
|
+
function formatNotes(notes) {
|
|
7196
|
+
if (!notes || !notes.length) {
|
|
7197
|
+
return 'No notes recorded yet. Add one with: sigmap note "<text>"';
|
|
7198
|
+
}
|
|
7199
|
+
return notes.map((n) => {
|
|
7200
|
+
const when = String(n.ts || '').replace('T', ' ').slice(0, 16);
|
|
7201
|
+
const br = n.branch ? ` (${n.branch})` : '';
|
|
7202
|
+
const tag = n.tag ? ` #${n.tag}` : '';
|
|
7203
|
+
return `- [${when}${br}]${tag} ${n.text}`;
|
|
7204
|
+
}).join('\n');
|
|
7205
|
+
}
|
|
7206
|
+
|
|
7207
|
+
/** Delete the notes log. Returns true if a file was removed. */
|
|
7208
|
+
function clearNotes(cwd) {
|
|
7209
|
+
try { fs.unlinkSync(notesPath(cwd)); return true; }
|
|
7210
|
+
catch (_) { return false; }
|
|
7211
|
+
}
|
|
7212
|
+
|
|
7213
|
+
module.exports = { notesPath, addNote, readNotes, formatNotes, clearNotes };
|
|
7214
|
+
|
|
7215
|
+
};
|
|
7216
|
+
|
|
7010
7217
|
// ── ./src/session/memory ──
|
|
7011
7218
|
__factories["./src/session/memory"] = function(module, exports) {
|
|
7012
7219
|
'use strict';
|
|
@@ -8873,112 +9080,951 @@ __factories["./src/discovery/r-manifest"] = function(module, exports) {
|
|
|
8873
9080
|
if (!defs.has(m[1])) defs.set(m[1], filePath);
|
|
8874
9081
|
}
|
|
8875
9082
|
}
|
|
8876
|
-
return defs;
|
|
9083
|
+
return defs;
|
|
9084
|
+
}
|
|
9085
|
+
};
|
|
9086
|
+
|
|
9087
|
+
// ── ./src/workspace/detector ──
|
|
9088
|
+
__factories["./src/workspace/detector"] = function(module, exports) {
|
|
9089
|
+
'use strict';
|
|
9090
|
+
const fs = require('fs');
|
|
9091
|
+
const path = require('path');
|
|
9092
|
+
module.exports = { detectWorkspaces, inferPackage, scopeToPackage };
|
|
9093
|
+
|
|
9094
|
+
function detectWorkspaces(cwd) {
|
|
9095
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
9096
|
+
if (!fs.existsSync(pkgPath)) return [];
|
|
9097
|
+
|
|
9098
|
+
let pkg;
|
|
9099
|
+
try {
|
|
9100
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
9101
|
+
} catch {
|
|
9102
|
+
return [];
|
|
9103
|
+
}
|
|
9104
|
+
|
|
9105
|
+
const patterns = pkg.workspaces || [];
|
|
9106
|
+
const dirs = [];
|
|
9107
|
+
|
|
9108
|
+
// Handle both flat array and object with packages field (Yarn v2 format)
|
|
9109
|
+
const patternArray = Array.isArray(patterns) ? patterns : (patterns.packages || []);
|
|
9110
|
+
|
|
9111
|
+
for (const p of patternArray) {
|
|
9112
|
+
const base = p.replace(/\/\*\*?$/, '');
|
|
9113
|
+
const resolved = path.join(cwd, base);
|
|
9114
|
+
if (fs.existsSync(resolved)) {
|
|
9115
|
+
try {
|
|
9116
|
+
for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
|
|
9117
|
+
if (entry.isDirectory()) dirs.push(path.join(resolved, entry.name));
|
|
9118
|
+
}
|
|
9119
|
+
} catch (_) {}
|
|
9120
|
+
}
|
|
9121
|
+
}
|
|
9122
|
+
|
|
9123
|
+
return dirs;
|
|
9124
|
+
}
|
|
9125
|
+
|
|
9126
|
+
// Infer package from query tokens: "add rate limiting to payments" → "packages/payments"
|
|
9127
|
+
function inferPackage(query, workspaceDirs, cwd) {
|
|
9128
|
+
const tokens = query.toLowerCase().split(/\W+/).filter(t => t.length > 2);
|
|
9129
|
+
|
|
9130
|
+
// Find longest matching package name
|
|
9131
|
+
let bestMatch = null;
|
|
9132
|
+
let bestLen = 0;
|
|
9133
|
+
let bestMatchLen = 0;
|
|
9134
|
+
|
|
9135
|
+
for (const dir of workspaceDirs) {
|
|
9136
|
+
const name = path.basename(dir).toLowerCase();
|
|
9137
|
+
for (const token of tokens) {
|
|
9138
|
+
const matchLen = _getMatchLength(name, token);
|
|
9139
|
+
// Only consider matches; use longest match, and break ties by longest package name
|
|
9140
|
+
if (matchLen > 0 && (matchLen > bestLen || (matchLen === bestLen && name.length > bestMatchLen))) {
|
|
9141
|
+
bestMatch = dir;
|
|
9142
|
+
bestLen = matchLen;
|
|
9143
|
+
bestMatchLen = name.length;
|
|
9144
|
+
}
|
|
9145
|
+
}
|
|
9146
|
+
}
|
|
9147
|
+
|
|
9148
|
+
return bestMatch;
|
|
9149
|
+
}
|
|
9150
|
+
|
|
9151
|
+
function _getMatchLength(name, token) {
|
|
9152
|
+
if (name === token) return 1000 + name.length; // Exact match is best
|
|
9153
|
+
if (name.startsWith(token) && token.length >= 3) return 100 + token.length;
|
|
9154
|
+
if (token.startsWith(name) && name.length >= 3) return name.length;
|
|
9155
|
+
return 0;
|
|
9156
|
+
}
|
|
9157
|
+
|
|
9158
|
+
// Return boost multiplier for files inside the inferred package
|
|
9159
|
+
function scopeToPackage(filePath, packageDir) {
|
|
9160
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
9161
|
+
const normalizedPkg = packageDir.replace(/\\/g, '/');
|
|
9162
|
+
|
|
9163
|
+
// Ensure we match the directory boundary, not just a prefix
|
|
9164
|
+
// e.g., packages/payment should not match packages/payment-old
|
|
9165
|
+
if (normalized.startsWith(normalizedPkg)) {
|
|
9166
|
+
const afterPrefix = normalized.slice(normalizedPkg.length);
|
|
9167
|
+
// Check if next char is / or if it's the exact match
|
|
9168
|
+
if (afterPrefix === '' || afterPrefix[0] === '/') {
|
|
9169
|
+
return 0.30;
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9172
|
+
return 0;
|
|
9173
|
+
}
|
|
9174
|
+
};
|
|
9175
|
+
|
|
9176
|
+
/**
|
|
9177
|
+
* SigMap — gen-context.js v1.2.0
|
|
9178
|
+
* Zero-dependency AI context engine.
|
|
9179
|
+
* Runs with: node gen-context.js
|
|
9180
|
+
* No npm install required. Node 18+ built-ins only.
|
|
9181
|
+
*/
|
|
9182
|
+
|
|
9183
|
+
// ── ./src/verify/parsers ──
|
|
9184
|
+
__factories["./src/verify/parsers"] = function(module, exports) {
|
|
9185
|
+
'use strict';
|
|
9186
|
+
|
|
9187
|
+
/**
|
|
9188
|
+
* Parsers for the Hallucination Guard (verify-ai-output).
|
|
9189
|
+
*
|
|
9190
|
+
* Extract the verifiable claims an AI answer makes about a codebase:
|
|
9191
|
+
* - file paths it references
|
|
9192
|
+
* - import / require statements it shows
|
|
9193
|
+
* - function / class symbols it calls
|
|
9194
|
+
* - fenced code blocks (so callers can scope checks to code vs prose)
|
|
9195
|
+
*
|
|
9196
|
+
* Everything here is deterministic and offline — pure string analysis.
|
|
9197
|
+
*/
|
|
9198
|
+
|
|
9199
|
+
// Extensions we are confident name a source/code/config file (no slash required).
|
|
9200
|
+
const KNOWN_CODE_EXT = new Set([
|
|
9201
|
+
'js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'py', 'pyw', 'rb', 'go', 'rs',
|
|
9202
|
+
'java', 'kt', 'swift', 'c', 'h', 'cpp', 'hpp', 'cs', 'php', 'r',
|
|
9203
|
+
'vue', 'svelte', 'css', 'scss', 'less', 'html', 'json', 'yml', 'yaml',
|
|
9204
|
+
'toml', 'xml', 'sql', 'graphql', 'gql', 'proto', 'tf', 'md', 'sh',
|
|
9205
|
+
'gd', 'gdscript',
|
|
9206
|
+
]);
|
|
9207
|
+
|
|
9208
|
+
/**
|
|
9209
|
+
* Extract fenced code blocks.
|
|
9210
|
+
* @param {string} text
|
|
9211
|
+
* @returns {{ lang: string, content: string, line: number }[]}
|
|
9212
|
+
*/
|
|
9213
|
+
function extractCodeBlocks(text) {
|
|
9214
|
+
const blocks = [];
|
|
9215
|
+
const lines = text.split('\n');
|
|
9216
|
+
let inBlock = false;
|
|
9217
|
+
let lang = '';
|
|
9218
|
+
let buf = [];
|
|
9219
|
+
let startLine = 0;
|
|
9220
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9221
|
+
const m = lines[i].match(/^```(\w*)/);
|
|
9222
|
+
if (m) {
|
|
9223
|
+
if (!inBlock) {
|
|
9224
|
+
inBlock = true;
|
|
9225
|
+
lang = m[1] || '';
|
|
9226
|
+
buf = [];
|
|
9227
|
+
startLine = i + 2; // first content line (1-based)
|
|
9228
|
+
} else {
|
|
9229
|
+
blocks.push({ lang, content: buf.join('\n'), line: startLine });
|
|
9230
|
+
inBlock = false;
|
|
9231
|
+
}
|
|
9232
|
+
continue;
|
|
9233
|
+
}
|
|
9234
|
+
if (inBlock) buf.push(lines[i]);
|
|
9235
|
+
}
|
|
9236
|
+
return blocks;
|
|
9237
|
+
}
|
|
9238
|
+
|
|
9239
|
+
/**
|
|
9240
|
+
* Extract file-path references (deduped, first-seen line kept).
|
|
9241
|
+
* A token counts as a path when it has a `.<letter…>` extension AND
|
|
9242
|
+
* either contains a `/` or carries a known code/config extension.
|
|
9243
|
+
* @param {string} text
|
|
9244
|
+
* @returns {{ path: string, line: number }[]}
|
|
9245
|
+
*/
|
|
9246
|
+
function extractFilePaths(text) {
|
|
9247
|
+
const lines = text.split('\n');
|
|
9248
|
+
const seen = new Map();
|
|
9249
|
+
const re = /(?:^|[\s`"'(\[<])([A-Za-z0-9_][\w./-]*\.[A-Za-z][A-Za-z0-9]*)/g;
|
|
9250
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9251
|
+
const line = lines[i];
|
|
9252
|
+
let m;
|
|
9253
|
+
re.lastIndex = 0;
|
|
9254
|
+
while ((m = re.exec(line)) !== null) {
|
|
9255
|
+
const p = m[1];
|
|
9256
|
+
if (/^https?:/i.test(p)) continue;
|
|
9257
|
+
const ext = (p.split('.').pop() || '').toLowerCase();
|
|
9258
|
+
const hasSlash = p.includes('/');
|
|
9259
|
+
if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
|
|
9260
|
+
if (!seen.has(p)) seen.set(p, i + 1);
|
|
9261
|
+
}
|
|
9262
|
+
}
|
|
9263
|
+
return [...seen.entries()].map(([p, line]) => ({ path: p, line }));
|
|
9264
|
+
}
|
|
9265
|
+
|
|
9266
|
+
/**
|
|
9267
|
+
* Extract import / require statements.
|
|
9268
|
+
* @param {string} text
|
|
9269
|
+
* @returns {{ module: string, kind: 'js'|'py', relative: boolean, line: number, raw: string }[]}
|
|
9270
|
+
*/
|
|
9271
|
+
function extractImports(text) {
|
|
9272
|
+
const lines = text.split('\n');
|
|
9273
|
+
const out = [];
|
|
9274
|
+
const push = (module, kind, line, raw) => {
|
|
9275
|
+
if (!module) return;
|
|
9276
|
+
out.push({ module, kind, relative: /^[./]/.test(module), line, raw: raw.trim() });
|
|
9277
|
+
};
|
|
9278
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9279
|
+
const line = lines[i];
|
|
9280
|
+
let m;
|
|
9281
|
+
// JS/TS: import ... from 'x' | export ... from 'x'
|
|
9282
|
+
if ((m = line.match(/\b(?:import|export)\b[^'"]*\bfrom\s*['"]([^'"]+)['"]/))) {
|
|
9283
|
+
push(m[1], 'js', i + 1, line);
|
|
9284
|
+
} else if ((m = line.match(/\bimport\s*['"]([^'"]+)['"]/))) {
|
|
9285
|
+
// side-effect import 'x'
|
|
9286
|
+
push(m[1], 'js', i + 1, line);
|
|
9287
|
+
}
|
|
9288
|
+
// require('x') / dynamic import('x') — may co-occur, scan separately
|
|
9289
|
+
const reqRe = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
9290
|
+
let r;
|
|
9291
|
+
while ((r = reqRe.exec(line)) !== null) push(r[1], 'js', i + 1, line);
|
|
9292
|
+
|
|
9293
|
+
// TS: import X = require('mod')
|
|
9294
|
+
if ((m = line.match(/\bimport\s+[A-Za-z_$][\w$]*\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/))) {
|
|
9295
|
+
push(m[1], 'js', i + 1, line);
|
|
9296
|
+
}
|
|
9297
|
+
|
|
9298
|
+
// Python: from x import y | import x
|
|
9299
|
+
if ((m = line.match(/^\s*from\s+([.\w]+)\s+import\b/))) {
|
|
9300
|
+
push(m[1], 'py', i + 1, line);
|
|
9301
|
+
} else if ((m = line.match(/^\s*import\s+([A-Za-z_][\w.]*)/))) {
|
|
9302
|
+
push(m[1], 'py', i + 1, line);
|
|
9303
|
+
}
|
|
9304
|
+
}
|
|
9305
|
+
|
|
9306
|
+
// Multi-line JS/TS imports, e.g.
|
|
9307
|
+
// import {
|
|
9308
|
+
// A as B,
|
|
9309
|
+
// } from './mod';
|
|
9310
|
+
// The per-line pass above misses these because `from '…'` sits on a later
|
|
9311
|
+
// line. Trigger only when the opening line has no quote and no `from` yet,
|
|
9312
|
+
// then gather forward until the source string appears.
|
|
9313
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9314
|
+
const start = lines[i];
|
|
9315
|
+
if (!/^\s*(?:import|export)\b/.test(start)) continue;
|
|
9316
|
+
if (/['"]/.test(start) || /\bfrom\b/.test(start)) continue; // single-line, already handled
|
|
9317
|
+
let joined = start;
|
|
9318
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 12); j++) {
|
|
9319
|
+
joined += ' ' + lines[j];
|
|
9320
|
+
const fm = joined.match(/\bfrom\s*['"]([^'"]+)['"]/);
|
|
9321
|
+
if (fm) { push(fm[1], 'js', i + 1, start.trim()); break; }
|
|
9322
|
+
if (/['"]/.test(lines[j]) && !/\bfrom\b/.test(joined)) break; // a string that isn't a source — bail
|
|
9323
|
+
}
|
|
9324
|
+
}
|
|
9325
|
+
return out;
|
|
9326
|
+
}
|
|
9327
|
+
|
|
9328
|
+
/**
|
|
9329
|
+
* Extract npm/pnpm/yarn script invocations (`npm run <name>`).
|
|
9330
|
+
* Only the explicit `run` form is matched, to avoid confusing package-manager
|
|
9331
|
+
* subcommands (`yarn add`, `pnpm install`) with script names.
|
|
9332
|
+
* @param {string} text
|
|
9333
|
+
* @returns {{ name: string, line: number }[]}
|
|
9334
|
+
*/
|
|
9335
|
+
function extractNpmScripts(text) {
|
|
9336
|
+
const lines = text.split('\n');
|
|
9337
|
+
const out = [];
|
|
9338
|
+
const seen = new Set();
|
|
9339
|
+
const re = /\b(?:npm|pnpm|yarn)\s+run(?:-script)?\s+([A-Za-z0-9:_-]+)/g;
|
|
9340
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9341
|
+
let m;
|
|
9342
|
+
re.lastIndex = 0;
|
|
9343
|
+
while ((m = re.exec(lines[i])) !== null) {
|
|
9344
|
+
const name = m[1];
|
|
9345
|
+
if (seen.has(name)) continue;
|
|
9346
|
+
seen.add(name);
|
|
9347
|
+
out.push({ name, line: i + 1 });
|
|
9348
|
+
}
|
|
9349
|
+
}
|
|
9350
|
+
return out;
|
|
9351
|
+
}
|
|
9352
|
+
|
|
9353
|
+
/**
|
|
9354
|
+
* Extract function/class symbol references that look like calls.
|
|
9355
|
+
* Restricted to backtick-wrapped calls (`foo(...)`) for high precision.
|
|
9356
|
+
* @param {string} text
|
|
9357
|
+
* @returns {{ name: string, line: number }[]}
|
|
9358
|
+
*/
|
|
9359
|
+
function extractSymbols(text) {
|
|
9360
|
+
const lines = text.split('\n');
|
|
9361
|
+
const out = [];
|
|
9362
|
+
const seen = new Set();
|
|
9363
|
+
const re = /`([A-Za-z_$][\w$]*)\s*\([^`]*\)`/g;
|
|
9364
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9365
|
+
let m;
|
|
9366
|
+
re.lastIndex = 0;
|
|
9367
|
+
while ((m = re.exec(lines[i])) !== null) {
|
|
9368
|
+
const name = m[1];
|
|
9369
|
+
const key = name + '@' + (i + 1);
|
|
9370
|
+
if (seen.has(key)) continue;
|
|
9371
|
+
seen.add(key);
|
|
9372
|
+
out.push({ name, line: i + 1 });
|
|
9373
|
+
}
|
|
9374
|
+
}
|
|
9375
|
+
return out;
|
|
9376
|
+
}
|
|
9377
|
+
|
|
9378
|
+
module.exports = {
|
|
9379
|
+
extractCodeBlocks,
|
|
9380
|
+
extractFilePaths,
|
|
9381
|
+
extractImports,
|
|
9382
|
+
extractSymbols,
|
|
9383
|
+
extractNpmScripts,
|
|
9384
|
+
};
|
|
9385
|
+
|
|
9386
|
+
};
|
|
9387
|
+
|
|
9388
|
+
// ── ./src/verify/closest-match ──
|
|
9389
|
+
__factories["./src/verify/closest-match"] = function(module, exports) {
|
|
9390
|
+
'use strict';
|
|
9391
|
+
|
|
9392
|
+
/**
|
|
9393
|
+
* Closest-match suggestions for the Hallucination Guard (v6.15.0).
|
|
9394
|
+
*
|
|
9395
|
+
* Heuristic layer (plan §5 labels this "Medium" confidence): given a name the
|
|
9396
|
+
* detectors flagged as fake, find the nearest *real* candidate by Levenshtein
|
|
9397
|
+
* distance so the report can say "Did you mean `loadConfig()` in
|
|
9398
|
+
* src/config/loader.js:42?". Pure, deterministic, offline — no network, no LLM.
|
|
9399
|
+
*
|
|
9400
|
+
* All inputs are passed in (symbol/file/script candidate lists) so this module
|
|
9401
|
+
* stays unit-testable without touching the filesystem or the SigMap index.
|
|
9402
|
+
*/
|
|
9403
|
+
|
|
9404
|
+
/**
|
|
9405
|
+
* Levenshtein edit distance with an early-exit ceiling.
|
|
9406
|
+
* Returns `max + 1` as soon as the best achievable distance exceeds `max`,
|
|
9407
|
+
* so callers can cheaply reject far-apart strings.
|
|
9408
|
+
*/
|
|
9409
|
+
function levenshtein(a, b, max = Infinity) {
|
|
9410
|
+
if (a === b) return 0;
|
|
9411
|
+
const al = a.length;
|
|
9412
|
+
const bl = b.length;
|
|
9413
|
+
if (al === 0) return bl;
|
|
9414
|
+
if (bl === 0) return al;
|
|
9415
|
+
if (Math.abs(al - bl) > max) return max + 1;
|
|
9416
|
+
|
|
9417
|
+
let prev = new Array(bl + 1);
|
|
9418
|
+
let curr = new Array(bl + 1);
|
|
9419
|
+
for (let j = 0; j <= bl; j++) prev[j] = j;
|
|
9420
|
+
|
|
9421
|
+
for (let i = 1; i <= al; i++) {
|
|
9422
|
+
curr[0] = i;
|
|
9423
|
+
let rowMin = curr[0];
|
|
9424
|
+
const ca = a.charCodeAt(i - 1);
|
|
9425
|
+
for (let j = 1; j <= bl; j++) {
|
|
9426
|
+
const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
|
|
9427
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
9428
|
+
if (curr[j] < rowMin) rowMin = curr[j];
|
|
9429
|
+
}
|
|
9430
|
+
if (rowMin > max) return max + 1;
|
|
9431
|
+
const tmp = prev;
|
|
9432
|
+
prev = curr;
|
|
9433
|
+
curr = tmp;
|
|
9434
|
+
}
|
|
9435
|
+
return prev[bl];
|
|
9436
|
+
}
|
|
9437
|
+
|
|
9438
|
+
/** Bucket a normalized edit distance into a confidence label (plan §5). */
|
|
9439
|
+
function suggestionConfidence(distance, targetLen) {
|
|
9440
|
+
const ratio = distance / Math.max(targetLen, 1);
|
|
9441
|
+
if (distance === 0 || ratio <= 0.2) return 'high';
|
|
9442
|
+
if (ratio <= 0.4) return 'medium';
|
|
9443
|
+
return 'low';
|
|
9444
|
+
}
|
|
9445
|
+
|
|
9446
|
+
/**
|
|
9447
|
+
* Find the nearest candidate name to `target`.
|
|
9448
|
+
*
|
|
9449
|
+
* @param {string} target
|
|
9450
|
+
* @param {Array<string | { name: string, file?: string, line?: number }>} candidates
|
|
9451
|
+
* @param {object} [opts]
|
|
9452
|
+
* @param {number} [opts.maxRatio=0.5] reject matches farther than ratio·len edits
|
|
9453
|
+
* @param {number} [opts.minLen=3] skip very short targets (too noisy)
|
|
9454
|
+
* @returns {{ name, file, line, distance, confidence } | null}
|
|
9455
|
+
*/
|
|
9456
|
+
function closestMatch(target, candidates, opts = {}) {
|
|
9457
|
+
const maxRatio = opts.maxRatio != null ? opts.maxRatio : 0.5;
|
|
9458
|
+
const minLen = opts.minLen != null ? opts.minLen : 3;
|
|
9459
|
+
if (!target || target.length < minLen) return null;
|
|
9460
|
+
if (!candidates || candidates.length === 0) return null;
|
|
9461
|
+
|
|
9462
|
+
const lower = target.toLowerCase();
|
|
9463
|
+
const cap = Math.max(1, Math.ceil(target.length * maxRatio));
|
|
9464
|
+
let best = null;
|
|
9465
|
+
|
|
9466
|
+
for (const c of candidates) {
|
|
9467
|
+
const name = typeof c === 'string' ? c : c && c.name;
|
|
9468
|
+
if (!name || name === target) continue;
|
|
9469
|
+
if (Math.abs(name.length - target.length) > cap) continue;
|
|
9470
|
+
const d = levenshtein(lower, name.toLowerCase(), cap);
|
|
9471
|
+
if (d > cap) continue;
|
|
9472
|
+
if (!best || d < best.distance ||
|
|
9473
|
+
(d === best.distance && name.length < best.name.length)) {
|
|
9474
|
+
best = {
|
|
9475
|
+
name,
|
|
9476
|
+
file: typeof c === 'object' ? c.file : undefined,
|
|
9477
|
+
line: typeof c === 'object' ? c.line : undefined,
|
|
9478
|
+
distance: d,
|
|
9479
|
+
};
|
|
9480
|
+
if (d === 0) break; // case-only difference — can't beat it
|
|
9481
|
+
}
|
|
9482
|
+
}
|
|
9483
|
+
|
|
9484
|
+
if (!best) return null;
|
|
9485
|
+
best.confidence = suggestionConfidence(best.distance, target.length);
|
|
9486
|
+
return best;
|
|
9487
|
+
}
|
|
9488
|
+
|
|
9489
|
+
/**
|
|
9490
|
+
* Build `[{ name, file, line }]` symbol candidates from a SigMap signature
|
|
9491
|
+
* index (`Map<file, string[]>` whose entries may carry a `:start-end` anchor).
|
|
9492
|
+
*/
|
|
9493
|
+
function buildSymbolCandidates(sigIndex) {
|
|
9494
|
+
const out = [];
|
|
9495
|
+
const seen = new Set();
|
|
9496
|
+
if (!sigIndex) return out;
|
|
9497
|
+
for (const [file, sigs] of sigIndex) {
|
|
9498
|
+
for (const sig of sigs) {
|
|
9499
|
+
const s = String(sig);
|
|
9500
|
+
const lineM = s.match(/:(\d+)(?:-\d+)?\s*$/);
|
|
9501
|
+
const line = lineM ? parseInt(lineM[1], 10) : null;
|
|
9502
|
+
const cleaned = s.replace(/\s*:\d+(?:-\d+)?\s*$/, '');
|
|
9503
|
+
const m = cleaned.match(/\b(?:async\s+function|function|class|def|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/)
|
|
9504
|
+
|| cleaned.match(/([A-Za-z_$][\w$]*)\s*\(/)
|
|
9505
|
+
|| cleaned.match(/([A-Za-z_$][\w$]*)/);
|
|
9506
|
+
if (!m) continue;
|
|
9507
|
+
const name = m[1];
|
|
9508
|
+
const key = name + '@' + file;
|
|
9509
|
+
if (seen.has(key)) continue;
|
|
9510
|
+
seen.add(key);
|
|
9511
|
+
out.push({ name, file, line });
|
|
9512
|
+
}
|
|
9513
|
+
}
|
|
9514
|
+
return out;
|
|
9515
|
+
}
|
|
9516
|
+
|
|
9517
|
+
/** Format a suggestion object into a human one-liner for reports/CLI. */
|
|
9518
|
+
function formatSuggestion(match, asCall) {
|
|
9519
|
+
if (!match) return null;
|
|
9520
|
+
const sym = asCall ? `${match.name}()` : match.name;
|
|
9521
|
+
let where = '';
|
|
9522
|
+
if (match.file) {
|
|
9523
|
+
where = match.line ? ` in ${match.file}:${match.line}` : ` in ${match.file}`;
|
|
8877
9524
|
}
|
|
9525
|
+
return `Did you mean \`${sym}\`${where}?`;
|
|
9526
|
+
}
|
|
9527
|
+
|
|
9528
|
+
module.exports = {
|
|
9529
|
+
levenshtein,
|
|
9530
|
+
closestMatch,
|
|
9531
|
+
buildSymbolCandidates,
|
|
9532
|
+
suggestionConfidence,
|
|
9533
|
+
formatSuggestion,
|
|
8878
9534
|
};
|
|
8879
9535
|
|
|
8880
|
-
|
|
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 };
|
|
9536
|
+
};
|
|
8886
9537
|
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
9538
|
+
// ── ./src/format/verify-report ──
|
|
9539
|
+
__factories["./src/format/verify-report"] = function(module, exports) {
|
|
9540
|
+
'use strict';
|
|
8890
9541
|
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
9542
|
+
/**
|
|
9543
|
+
* Hallucination Guard report view (Surface A, v6.15.0).
|
|
9544
|
+
*
|
|
9545
|
+
* Turns the `verify-ai-output --json` result into a standalone, self-contained
|
|
9546
|
+
* HTML report — red/amber/green per issue, with closest-match suggestions
|
|
9547
|
+
* inline. The visual language deliberately mirrors the planned PR-comment
|
|
9548
|
+
* styling so a single screenshot is reusable across docs and CI (plan proof #5).
|
|
9549
|
+
*
|
|
9550
|
+
* Zero dependencies, inline CSS/SVG, no external assets. Also exports a compact
|
|
9551
|
+
* Markdown renderer for CI / PR comments that shares the same structure.
|
|
9552
|
+
*/
|
|
8897
9553
|
|
|
8898
|
-
|
|
8899
|
-
|
|
9554
|
+
const TYPE_META = {
|
|
9555
|
+
'fake-file': { label: 'Fake file', tone: 'red', icon: '✕' },
|
|
9556
|
+
'fake-test-file': { label: 'Fake test file', tone: 'red', icon: '✕' },
|
|
9557
|
+
'fake-import': { label: 'Fake import', tone: 'red', icon: '✕' },
|
|
9558
|
+
'fake-npm-script': { label: 'Fake npm script', tone: 'red', icon: '✕' },
|
|
9559
|
+
'fake-symbol': { label: 'Fake symbol', tone: 'amber', icon: '!' },
|
|
9560
|
+
};
|
|
8900
9561
|
|
|
8901
|
-
|
|
8902
|
-
|
|
9562
|
+
function escapeHtml(value) {
|
|
9563
|
+
return String(value == null ? '' : value)
|
|
9564
|
+
.replace(/&/g, '&')
|
|
9565
|
+
.replace(/</g, '<')
|
|
9566
|
+
.replace(/>/g, '>')
|
|
9567
|
+
.replace(/"/g, '"');
|
|
9568
|
+
}
|
|
8903
9569
|
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
|
|
8910
|
-
if (entry.isDirectory()) dirs.push(path.join(resolved, entry.name));
|
|
8911
|
-
}
|
|
8912
|
-
} catch (_) {}
|
|
8913
|
-
}
|
|
8914
|
-
}
|
|
9570
|
+
function toneFor(issue) {
|
|
9571
|
+
const meta = TYPE_META[issue.type];
|
|
9572
|
+
if (meta) return meta.tone;
|
|
9573
|
+
return issue.confidence === 'high' ? 'red' : 'amber';
|
|
9574
|
+
}
|
|
8915
9575
|
|
|
8916
|
-
|
|
9576
|
+
function labelFor(issue) {
|
|
9577
|
+
return (TYPE_META[issue.type] && TYPE_META[issue.type].label) || issue.type;
|
|
9578
|
+
}
|
|
9579
|
+
|
|
9580
|
+
/**
|
|
9581
|
+
* Render the verify result to a full HTML document.
|
|
9582
|
+
* @param {{ file?: string, issues: object[], summary: object }} result
|
|
9583
|
+
* @param {object} [opts]
|
|
9584
|
+
* @param {string} [opts.title]
|
|
9585
|
+
* @returns {string} HTML
|
|
9586
|
+
*/
|
|
9587
|
+
function renderReportHtml(result, opts = {}) {
|
|
9588
|
+
const issues = Array.isArray(result.issues) ? result.issues : [];
|
|
9589
|
+
const summary = result.summary || { total: issues.length, byType: {}, clean: issues.length === 0 };
|
|
9590
|
+
const file = result.file || opts.file || 'AI answer';
|
|
9591
|
+
const title = opts.title || 'SigMap — Hallucination Guard report';
|
|
9592
|
+
const clean = summary.clean || issues.length === 0;
|
|
9593
|
+
const byType = summary.byType || {};
|
|
9594
|
+
|
|
9595
|
+
const chips = Object.keys(TYPE_META)
|
|
9596
|
+
.filter((t) => byType[t])
|
|
9597
|
+
.map((t) => `<span class="chip chip-${TYPE_META[t].tone}">${escapeHtml(TYPE_META[t].label)}: ${byType[t]}</span>`)
|
|
9598
|
+
.join('');
|
|
9599
|
+
|
|
9600
|
+
const banner = clean
|
|
9601
|
+
? `<div class="banner banner-green"><span class="dot"></span> No hallucinations detected — ${escapeHtml(String(summary.symbolsIndexed || 0))} symbols indexed</div>`
|
|
9602
|
+
: `<div class="banner banner-red"><span class="dot"></span> ${issues.length} issue${issues.length === 1 ? '' : 's'} found in <code>${escapeHtml(file)}</code></div>`;
|
|
9603
|
+
|
|
9604
|
+
const rows = issues.map((issue) => {
|
|
9605
|
+
const tone = toneFor(issue);
|
|
9606
|
+
const sugg = issue.suggestion
|
|
9607
|
+
? `<div class="suggestion">↳ ${escapeHtml(issue.suggestion)} <span class="conf">heuristic</span></div>`
|
|
9608
|
+
: '';
|
|
9609
|
+
return [
|
|
9610
|
+
`<li class="issue issue-${tone}">`,
|
|
9611
|
+
` <div class="issue-head">`,
|
|
9612
|
+
` <span class="badge badge-${tone}">${escapeHtml(labelFor(issue))}</span>`,
|
|
9613
|
+
` <span class="loc">${escapeHtml(issue.location || ('L' + issue.line))}</span>`,
|
|
9614
|
+
` <span class="conf conf-${escapeHtml(issue.confidence || 'high')}">${escapeHtml(issue.confidence || 'high')} confidence</span>`,
|
|
9615
|
+
` </div>`,
|
|
9616
|
+
` <div class="msg">${escapeHtml(issue.message || issue.value)}</div>`,
|
|
9617
|
+
` ${sugg}`,
|
|
9618
|
+
`</li>`,
|
|
9619
|
+
].join('\n');
|
|
9620
|
+
}).join('\n');
|
|
9621
|
+
|
|
9622
|
+
const list = clean
|
|
9623
|
+
? '<p class="empty">Nothing to report — every file, import, symbol, and script in the answer resolves against the repository.</p>'
|
|
9624
|
+
: `<ul class="issues">${rows}</ul>`;
|
|
9625
|
+
|
|
9626
|
+
return `<!DOCTYPE html>
|
|
9627
|
+
<html lang="en">
|
|
9628
|
+
<head>
|
|
9629
|
+
<meta charset="utf-8">
|
|
9630
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
9631
|
+
<title>${escapeHtml(title)}</title>
|
|
9632
|
+
<style>
|
|
9633
|
+
:root { color-scheme: light dark; }
|
|
9634
|
+
* { box-sizing: border-box; }
|
|
9635
|
+
body { font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #0d1117; color: #e6edf3; }
|
|
9636
|
+
.wrap { max-width: 880px; margin: 0 auto; padding: 32px 20px 64px; }
|
|
9637
|
+
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
9638
|
+
.sub { color: #8b949e; margin: 0 0 20px; font-size: 13px; }
|
|
9639
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #161b22; padding: 1px 5px; border-radius: 4px; font-size: 12.5px; }
|
|
9640
|
+
.banner { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-radius: 8px; font-weight: 600; margin-bottom: 16px; }
|
|
9641
|
+
.banner-green { background: rgba(46,160,67,.15); border: 1px solid rgba(46,160,67,.4); color: #3fb950; }
|
|
9642
|
+
.banner-red { background: rgba(248,81,73,.12); border: 1px solid rgba(248,81,73,.4); color: #f85149; }
|
|
9643
|
+
.dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; }
|
|
9644
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 20px; }
|
|
9645
|
+
.chip { font-size: 12px; padding: 3px 9px; border-radius: 999px; font-weight: 600; }
|
|
9646
|
+
.chip-red { background: rgba(248,81,73,.15); color: #f85149; }
|
|
9647
|
+
.chip-amber { background: rgba(210,153,34,.18); color: #d29922; }
|
|
9648
|
+
ul.issues { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
|
|
9649
|
+
.issue { border: 1px solid #30363d; border-left-width: 4px; border-radius: 8px; padding: 12px 14px; background: #0f141a; }
|
|
9650
|
+
.issue-red { border-left-color: #f85149; }
|
|
9651
|
+
.issue-amber { border-left-color: #d29922; }
|
|
9652
|
+
.issue-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
9653
|
+
.badge { font-size: 11.5px; font-weight: 700; padding: 2px 8px; border-radius: 5px; text-transform: uppercase; letter-spacing: .03em; }
|
|
9654
|
+
.badge-red { background: rgba(248,81,73,.18); color: #f85149; }
|
|
9655
|
+
.badge-amber { background: rgba(210,153,34,.2); color: #d29922; }
|
|
9656
|
+
.loc { font-family: ui-monospace, monospace; color: #8b949e; font-size: 12px; }
|
|
9657
|
+
.conf { font-size: 11px; color: #8b949e; }
|
|
9658
|
+
.conf-high { color: #f85149; }
|
|
9659
|
+
.conf-medium { color: #d29922; }
|
|
9660
|
+
.msg { margin-top: 7px; font-family: ui-monospace, monospace; font-size: 12.5px; color: #e6edf3; }
|
|
9661
|
+
.suggestion { margin-top: 6px; font-size: 12.5px; color: #3fb950; }
|
|
9662
|
+
.suggestion .conf { margin-left: 6px; }
|
|
9663
|
+
.empty { color: #8b949e; }
|
|
9664
|
+
footer { margin-top: 28px; color: #6e7681; font-size: 12px; }
|
|
9665
|
+
a { color: #58a6ff; }
|
|
9666
|
+
</style>
|
|
9667
|
+
</head>
|
|
9668
|
+
<body>
|
|
9669
|
+
<div class="wrap">
|
|
9670
|
+
<h1>Hallucination Guard report</h1>
|
|
9671
|
+
<p class="sub">Deterministic verification of an AI answer against the real repository — <code>sigmap verify-ai-output</code></p>
|
|
9672
|
+
${banner}
|
|
9673
|
+
${chips ? `<div class="chips">${chips}</div>` : ''}
|
|
9674
|
+
${list}
|
|
9675
|
+
<footer>Generated by <a href="https://github.com/manojmallick/sigmap">SigMap</a> · offline, no LLM · suggestions are heuristic (closest-match)</footer>
|
|
9676
|
+
</div>
|
|
9677
|
+
</body>
|
|
9678
|
+
</html>
|
|
9679
|
+
`;
|
|
9680
|
+
}
|
|
9681
|
+
|
|
9682
|
+
/** Compact Markdown rendering of the same result (CI / PR comments). */
|
|
9683
|
+
function renderReportMarkdown(result) {
|
|
9684
|
+
const issues = Array.isArray(result.issues) ? result.issues : [];
|
|
9685
|
+
const summary = result.summary || {};
|
|
9686
|
+
const file = result.file || 'AI answer';
|
|
9687
|
+
if (summary.clean || issues.length === 0) {
|
|
9688
|
+
return `### ✅ Hallucination Guard — clean\n\nNo fabricated files, imports, symbols, or scripts in \`${file}\`.`;
|
|
9689
|
+
}
|
|
9690
|
+
const lines = [
|
|
9691
|
+
`### ❌ Hallucination Guard — ${issues.length} issue${issues.length === 1 ? '' : 's'} in \`${file}\``,
|
|
9692
|
+
'',
|
|
9693
|
+
'| Type | Location | Detail | Suggestion |',
|
|
9694
|
+
'| --- | --- | --- | --- |',
|
|
9695
|
+
];
|
|
9696
|
+
for (const i of issues) {
|
|
9697
|
+
const sugg = i.suggestion ? i.suggestion.replace(/\|/g, '\\|') : '—';
|
|
9698
|
+
lines.push(`| ${labelFor(i)} | ${i.location || ('L' + i.line)} | \`${String(i.value).replace(/\|/g, '\\|')}\` | ${sugg} |`);
|
|
8917
9699
|
}
|
|
9700
|
+
return lines.join('\n');
|
|
9701
|
+
}
|
|
8918
9702
|
|
|
8919
|
-
|
|
8920
|
-
function inferPackage(query, workspaceDirs, cwd) {
|
|
8921
|
-
const tokens = query.toLowerCase().split(/\W+/).filter(t => t.length > 2);
|
|
9703
|
+
module.exports = { renderReportHtml, renderReportMarkdown, escapeHtml };
|
|
8922
9704
|
|
|
8923
|
-
|
|
8924
|
-
let bestMatch = null;
|
|
8925
|
-
let bestLen = 0;
|
|
8926
|
-
let bestMatchLen = 0;
|
|
9705
|
+
};
|
|
8927
9706
|
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
9707
|
+
// ── ./src/verify/hallucination-guard ──
|
|
9708
|
+
__factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
9709
|
+
'use strict';
|
|
9710
|
+
|
|
9711
|
+
/**
|
|
9712
|
+
* Hallucination Guard — deterministic core (Reliable MVP, v6.15.0).
|
|
9713
|
+
*
|
|
9714
|
+
* Given the text of an AI answer, flag claims that do not match the repo:
|
|
9715
|
+
* - fake-file : a referenced path is not on disk
|
|
9716
|
+
* - fake-test-file : a referenced *test* path is not on disk (sub-type)
|
|
9717
|
+
* - fake-import : a relative import does not resolve; a bare import is
|
|
9718
|
+
* absent from package.json deps (builtins allow-listed)
|
|
9719
|
+
* - fake-symbol : a called function/class is absent from the symbol index
|
|
9720
|
+
* - fake-npm-script: `npm run X` where X is not a package.json script
|
|
9721
|
+
*
|
|
9722
|
+
* Each issue carries a `confidence` (detection certainty) and, where a near
|
|
9723
|
+
* match exists, a heuristic `suggestion` ("Did you mean …?"). No network, no
|
|
9724
|
+
* LLM. Reuses SigMap primitives (buildSigIndex) but every external dependency
|
|
9725
|
+
* is injectable via `opts` so the core stays unit-testable.
|
|
9726
|
+
*/
|
|
9727
|
+
|
|
9728
|
+
const fs = require('fs');
|
|
9729
|
+
const path = require('path');
|
|
9730
|
+
const parsers = __require('./src/verify/parsers');
|
|
9731
|
+
const { closestMatch, buildSymbolCandidates, formatSuggestion } = __require('./src/verify/closest-match');
|
|
9732
|
+
|
|
9733
|
+
// A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
|
|
9734
|
+
// a tests/__tests__ directory). Used to flag fake-test-file separately.
|
|
9735
|
+
const TEST_PATH_RE = /(?:\.(?:test|spec)\.[mc]?[jt]sx?$)|(?:(?:^|\/)__tests__\/)|(?:(?:^|\/)test_[^/]+\.py$)|(?:_test\.py$)|(?:(?:^|\/)tests?\/)/i;
|
|
9736
|
+
function isTestPath(p) { return TEST_PATH_RE.test(p); }
|
|
9737
|
+
|
|
9738
|
+
const NODE_BUILTINS = new Set([
|
|
9739
|
+
'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https', 'crypto',
|
|
9740
|
+
'child_process', 'url', 'querystring', 'assert', 'zlib', 'readline', 'net',
|
|
9741
|
+
'tls', 'dns', 'buffer', 'process', 'vm', 'module', 'console', 'timers',
|
|
9742
|
+
'string_decoder', 'perf_hooks', 'worker_threads', 'cluster', 'dgram', 'v8',
|
|
9743
|
+
'tty', 'repl', 'async_hooks', 'inspector', 'fs/promises', 'path/posix',
|
|
9744
|
+
]);
|
|
9745
|
+
|
|
9746
|
+
const PY_BUILTINS = new Set([
|
|
9747
|
+
'os', 'sys', 're', 'json', 'math', 'typing', 'collections', 'itertools',
|
|
9748
|
+
'functools', 'datetime', 'pathlib', 'subprocess', 'abc', 'dataclasses',
|
|
9749
|
+
'enum', 'io', 'time', 'random', 'logging', 'argparse', 'unittest', 'asyncio',
|
|
9750
|
+
'copy', 'hashlib', 'threading', 'string', 'csv', 'glob', 'shutil', 'tempfile',
|
|
9751
|
+
]);
|
|
9752
|
+
|
|
9753
|
+
const LANG_GLOBALS = new Set([
|
|
9754
|
+
// JS
|
|
9755
|
+
'console', 'require', 'module', 'exports', 'process', 'Object', 'Array',
|
|
9756
|
+
'String', 'Number', 'Boolean', 'Math', 'JSON', 'Date', 'Promise', 'Map',
|
|
9757
|
+
'Set', 'WeakMap', 'WeakSet', 'RegExp', 'Error', 'Symbol', 'parseInt',
|
|
9758
|
+
'parseFloat', 'isNaN', 'setTimeout', 'setInterval', 'clearTimeout', 'fetch',
|
|
9759
|
+
'Buffer', 'Function', 'eval', 'encodeURIComponent', 'decodeURIComponent',
|
|
9760
|
+
// Python
|
|
9761
|
+
'print', 'len', 'range', 'str', 'int', 'float', 'dict', 'list', 'tuple',
|
|
9762
|
+
'set', 'bool', 'open', 'enumerate', 'zip', 'map', 'filter', 'sorted',
|
|
9763
|
+
'sum', 'min', 'max', 'abs', 'isinstance', 'super', 'type', 'getattr',
|
|
9764
|
+
'setattr', 'hasattr',
|
|
9765
|
+
]);
|
|
9766
|
+
|
|
9767
|
+
const REL_EXTS = ['', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.json', '.py', '.r', '.R', '.vue'];
|
|
9768
|
+
const REL_INDEX = ['index.js', 'index.ts', 'index.tsx', 'index.jsx', '__init__.py'];
|
|
9769
|
+
|
|
9770
|
+
/**
|
|
9771
|
+
* Build the set of known symbol identifiers from the SigMap signature index,
|
|
9772
|
+
* plus `{ name, file, line }` candidates (for closest-match suggestions).
|
|
9773
|
+
*/
|
|
9774
|
+
function buildSymbolSet(cwd) {
|
|
9775
|
+
const set = new Set();
|
|
9776
|
+
let fileKeys = [];
|
|
9777
|
+
let symbolCandidates = [];
|
|
9778
|
+
try {
|
|
9779
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
9780
|
+
const idx = buildSigIndex(cwd);
|
|
9781
|
+
fileKeys = [...idx.keys()];
|
|
9782
|
+
for (const sigs of idx.values()) {
|
|
9783
|
+
for (const sig of sigs) {
|
|
9784
|
+
const cleaned = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '');
|
|
9785
|
+
const ids = cleaned.match(/[A-Za-z_$][\w$]*/g) || [];
|
|
9786
|
+
for (const id of ids) set.add(id);
|
|
9787
|
+
}
|
|
9788
|
+
}
|
|
9789
|
+
symbolCandidates = buildSymbolCandidates(idx);
|
|
9790
|
+
} catch (_) {}
|
|
9791
|
+
return { set, fileKeys, symbolCandidates };
|
|
9792
|
+
}
|
|
9793
|
+
|
|
9794
|
+
/** Load declared dependency names from package.json. */
|
|
9795
|
+
function loadDeps(cwd) {
|
|
9796
|
+
const deps = new Set();
|
|
9797
|
+
let hasPkg = false;
|
|
9798
|
+
try {
|
|
9799
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
9800
|
+
hasPkg = true;
|
|
9801
|
+
for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
9802
|
+
if (pkg[k] && typeof pkg[k] === 'object') {
|
|
9803
|
+
for (const name of Object.keys(pkg[k])) deps.add(name);
|
|
8938
9804
|
}
|
|
8939
9805
|
}
|
|
9806
|
+
} catch (_) {}
|
|
9807
|
+
return { deps, hasPkg };
|
|
9808
|
+
}
|
|
8940
9809
|
|
|
8941
|
-
|
|
9810
|
+
/** Load the set of npm script names declared in package.json. */
|
|
9811
|
+
function loadScripts(cwd) {
|
|
9812
|
+
const scripts = new Set();
|
|
9813
|
+
try {
|
|
9814
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
9815
|
+
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
9816
|
+
for (const name of Object.keys(pkg.scripts)) scripts.add(name);
|
|
9817
|
+
}
|
|
9818
|
+
} catch (_) {}
|
|
9819
|
+
return scripts;
|
|
9820
|
+
}
|
|
9821
|
+
|
|
9822
|
+
/** Default file-existence check: resolve a referenced path against cwd. */
|
|
9823
|
+
function defaultFileExists(cwd, ref) {
|
|
9824
|
+
const clean = ref.replace(/^\.\//, '');
|
|
9825
|
+
for (const c of [path.resolve(cwd, clean), path.resolve(cwd, ref)]) {
|
|
9826
|
+
try {
|
|
9827
|
+
if (fs.existsSync(c)) return true;
|
|
9828
|
+
} catch (_) {}
|
|
8942
9829
|
}
|
|
9830
|
+
return false;
|
|
9831
|
+
}
|
|
8943
9832
|
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
9833
|
+
/** Default relative-import resolver: fs candidates + basename match in index. */
|
|
9834
|
+
function defaultRelativeResolvable(cwd, mod, fileBasenames) {
|
|
9835
|
+
const base = path.resolve(cwd, mod);
|
|
9836
|
+
for (const e of REL_EXTS) {
|
|
9837
|
+
try {
|
|
9838
|
+
if (fs.existsSync(base + e)) return true;
|
|
9839
|
+
} catch (_) {}
|
|
9840
|
+
}
|
|
9841
|
+
for (const idx of REL_INDEX) {
|
|
9842
|
+
try {
|
|
9843
|
+
if (fs.existsSync(path.join(base, idx))) return true;
|
|
9844
|
+
} catch (_) {}
|
|
8949
9845
|
}
|
|
9846
|
+
// Fall back to basename match against the indexed file set (the answer's
|
|
9847
|
+
// import is relative to a file we cannot know, so a name match is enough
|
|
9848
|
+
// to avoid false positives).
|
|
9849
|
+
const wantBase = path.basename(mod).replace(/\.[^.]+$/, '').toLowerCase();
|
|
9850
|
+
return fileBasenames.has(wantBase);
|
|
9851
|
+
}
|
|
8950
9852
|
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
9853
|
+
/**
|
|
9854
|
+
* Verify an AI answer against the repository.
|
|
9855
|
+
*
|
|
9856
|
+
* Each issue has the shape:
|
|
9857
|
+
* { type, value, line, location, message, confidence, suggestion }
|
|
9858
|
+
* where `confidence` is the *detection* certainty ('high' for path/dep/script
|
|
9859
|
+
* checks, 'medium' for symbol checks) and `suggestion` is a heuristic
|
|
9860
|
+
* closest-match hint (or null).
|
|
9861
|
+
*
|
|
9862
|
+
* @param {string} answerText
|
|
9863
|
+
* @param {string} cwd
|
|
9864
|
+
* @param {object} [opts]
|
|
9865
|
+
* @param {Set<string>} [opts.symbolSet] override known symbols
|
|
9866
|
+
* @param {Array} [opts.symbolCandidates] override { name, file, line } list
|
|
9867
|
+
* @param {Array<string>} [opts.fileCandidates] override repo file paths (suggestions)
|
|
9868
|
+
* @param {Set<string>} [opts.deps] override package deps
|
|
9869
|
+
* @param {Set<string>} [opts.scripts] override package.json script names
|
|
9870
|
+
* @param {boolean} [opts.hasPkg] whether a package.json exists
|
|
9871
|
+
* @param {(ref: string) => boolean} [opts.fileExists] override file check
|
|
9872
|
+
* @param {(mod: string) => boolean} [opts.relativeResolvable] override rel-import check
|
|
9873
|
+
* @returns {{ issues: object[], summary: object }}
|
|
9874
|
+
*/
|
|
9875
|
+
function verify(answerText, cwd, opts = {}) {
|
|
9876
|
+
let symbolSet = opts.symbolSet;
|
|
9877
|
+
let fileBasenames = opts.fileBasenames;
|
|
9878
|
+
let symbolCandidates = opts.symbolCandidates || [];
|
|
9879
|
+
let fileCandidates = opts.fileCandidates || [];
|
|
9880
|
+
if (!symbolSet) {
|
|
9881
|
+
const built = buildSymbolSet(cwd);
|
|
9882
|
+
symbolSet = built.set;
|
|
9883
|
+
fileBasenames = new Set(built.fileKeys.map(
|
|
9884
|
+
(k) => path.basename(k).replace(/\.[^.]+$/, '').toLowerCase()
|
|
9885
|
+
));
|
|
9886
|
+
symbolCandidates = built.symbolCandidates;
|
|
9887
|
+
fileCandidates = built.fileKeys;
|
|
9888
|
+
}
|
|
9889
|
+
if (!fileBasenames) fileBasenames = new Set();
|
|
9890
|
+
|
|
9891
|
+
let deps = opts.deps;
|
|
9892
|
+
let hasPkg = opts.hasPkg;
|
|
9893
|
+
if (!deps) {
|
|
9894
|
+
const loaded = loadDeps(cwd);
|
|
9895
|
+
deps = loaded.deps;
|
|
9896
|
+
if (hasPkg === undefined) hasPkg = loaded.hasPkg;
|
|
9897
|
+
}
|
|
9898
|
+
const scripts = opts.scripts || (hasPkg ? loadScripts(cwd) : new Set());
|
|
9899
|
+
|
|
9900
|
+
const fileExists = opts.fileExists || ((ref) => defaultFileExists(cwd, ref));
|
|
9901
|
+
const relativeResolvable = opts.relativeResolvable
|
|
9902
|
+
|| ((mod) => defaultRelativeResolvable(cwd, mod, fileBasenames));
|
|
9903
|
+
|
|
9904
|
+
// Pre-derive basename candidates for file suggestions (compare on basename so
|
|
9905
|
+
// a wrong directory still surfaces the right file).
|
|
9906
|
+
const fileBasenameCandidates = fileCandidates.map((f) => ({ name: path.basename(f), file: f }));
|
|
9907
|
+
|
|
9908
|
+
const issues = [];
|
|
9909
|
+
const dedupe = new Set();
|
|
9910
|
+
const add = (issue) => {
|
|
9911
|
+
const key = `${issue.type}::${issue.value}`;
|
|
9912
|
+
if (dedupe.has(key)) return;
|
|
9913
|
+
dedupe.add(key);
|
|
9914
|
+
if (!('suggestion' in issue)) issue.suggestion = null;
|
|
9915
|
+
issue.location = `L${issue.line}`;
|
|
9916
|
+
issues.push(issue);
|
|
9917
|
+
};
|
|
8955
9918
|
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
if (
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
9919
|
+
// 1. fake-file / fake-test-file
|
|
9920
|
+
for (const { path: p, line } of parsers.extractFilePaths(answerText)) {
|
|
9921
|
+
if (fileExists(p)) continue;
|
|
9922
|
+
const isTest = isTestPath(p);
|
|
9923
|
+
const match = closestMatch(path.basename(p), fileBasenameCandidates, { minLen: 4 });
|
|
9924
|
+
add({
|
|
9925
|
+
type: isTest ? 'fake-test-file' : 'fake-file',
|
|
9926
|
+
value: p,
|
|
9927
|
+
line,
|
|
9928
|
+
message: `${isTest ? 'Test file' : 'File'} not found on disk: ${p}`,
|
|
9929
|
+
confidence: 'high',
|
|
9930
|
+
suggestion: match ? formatSuggestion(match, false) : null,
|
|
9931
|
+
});
|
|
9932
|
+
}
|
|
9933
|
+
|
|
9934
|
+
// 2. fake-import
|
|
9935
|
+
for (const imp of parsers.extractImports(answerText)) {
|
|
9936
|
+
if (imp.relative) {
|
|
9937
|
+
if (!relativeResolvable(imp.module)) {
|
|
9938
|
+
add({ type: 'fake-import', value: imp.module, line: imp.line, message: `Import does not resolve: ${imp.module}`, confidence: 'high' });
|
|
9939
|
+
}
|
|
9940
|
+
continue;
|
|
9941
|
+
}
|
|
9942
|
+
// Bare module — only verifiable for JS when a package.json exists.
|
|
9943
|
+
const top = imp.module.split('/')[0];
|
|
9944
|
+
if (imp.kind === 'js') {
|
|
9945
|
+
if (!hasPkg) continue;
|
|
9946
|
+
if (NODE_BUILTINS.has(imp.module) || NODE_BUILTINS.has(top)) continue;
|
|
9947
|
+
if (top.startsWith('@')) {
|
|
9948
|
+
const scoped = imp.module.split('/').slice(0, 2).join('/');
|
|
9949
|
+
if (deps.has(scoped) || deps.has(imp.module)) continue;
|
|
9950
|
+
} else if (deps.has(top) || deps.has(imp.module)) {
|
|
9951
|
+
continue;
|
|
8963
9952
|
}
|
|
9953
|
+
const match = closestMatch(top, [...deps], { minLen: 3 });
|
|
9954
|
+
add({
|
|
9955
|
+
type: 'fake-import',
|
|
9956
|
+
value: imp.module,
|
|
9957
|
+
line: imp.line,
|
|
9958
|
+
message: `Package not in dependencies: ${imp.module}`,
|
|
9959
|
+
confidence: 'high',
|
|
9960
|
+
suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
|
|
9961
|
+
});
|
|
9962
|
+
}
|
|
9963
|
+
// Python bare imports: stdlib is unbounded offline — skip to keep precision.
|
|
9964
|
+
}
|
|
9965
|
+
|
|
9966
|
+
// 3. fake-symbol
|
|
9967
|
+
if (symbolSet.size > 0) {
|
|
9968
|
+
for (const { name, line } of parsers.extractSymbols(answerText)) {
|
|
9969
|
+
if (symbolSet.has(name)) continue;
|
|
9970
|
+
if (LANG_GLOBALS.has(name) || NODE_BUILTINS.has(name) || PY_BUILTINS.has(name)) continue;
|
|
9971
|
+
const match = closestMatch(name, symbolCandidates, { minLen: 4 });
|
|
9972
|
+
add({
|
|
9973
|
+
type: 'fake-symbol',
|
|
9974
|
+
value: name,
|
|
9975
|
+
line,
|
|
9976
|
+
message: `Symbol not found in repo index: ${name}()`,
|
|
9977
|
+
confidence: 'medium',
|
|
9978
|
+
suggestion: match ? formatSuggestion(match, true) : null,
|
|
9979
|
+
});
|
|
8964
9980
|
}
|
|
8965
|
-
return 0;
|
|
8966
9981
|
}
|
|
8967
|
-
};
|
|
8968
9982
|
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
9983
|
+
// 4. fake-npm-script
|
|
9984
|
+
if (hasPkg && scripts.size > 0) {
|
|
9985
|
+
for (const { name, line } of parsers.extractNpmScripts(answerText)) {
|
|
9986
|
+
if (scripts.has(name)) continue;
|
|
9987
|
+
const match = closestMatch(name, [...scripts], { minLen: 2 });
|
|
9988
|
+
add({
|
|
9989
|
+
type: 'fake-npm-script',
|
|
9990
|
+
value: name,
|
|
9991
|
+
line,
|
|
9992
|
+
message: `npm script not in package.json: ${name}`,
|
|
9993
|
+
confidence: 'high',
|
|
9994
|
+
suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
|
|
9995
|
+
});
|
|
9996
|
+
}
|
|
9997
|
+
}
|
|
9998
|
+
|
|
9999
|
+
issues.sort((a, b) => a.line - b.line);
|
|
10000
|
+
|
|
10001
|
+
const byType = {
|
|
10002
|
+
'fake-file': 0, 'fake-test-file': 0, 'fake-import': 0,
|
|
10003
|
+
'fake-symbol': 0, 'fake-npm-script': 0,
|
|
10004
|
+
};
|
|
10005
|
+
for (const i of issues) byType[i.type] = (byType[i.type] || 0) + 1;
|
|
10006
|
+
|
|
10007
|
+
const summary = {
|
|
10008
|
+
total: issues.length,
|
|
10009
|
+
byType,
|
|
10010
|
+
clean: issues.length === 0,
|
|
10011
|
+
symbolsIndexed: symbolSet.size,
|
|
10012
|
+
withSuggestion: issues.filter((i) => i.suggestion).length,
|
|
10013
|
+
};
|
|
10014
|
+
|
|
10015
|
+
return { issues, summary };
|
|
10016
|
+
}
|
|
10017
|
+
|
|
10018
|
+
module.exports = { verify, buildSymbolSet, loadDeps, loadScripts, isTestPath };
|
|
10019
|
+
|
|
10020
|
+
};
|
|
8975
10021
|
|
|
8976
10022
|
const fs = require('fs');
|
|
8977
10023
|
const path = require('path');
|
|
8978
10024
|
const os = require('os');
|
|
8979
10025
|
const { execSync } = require('child_process');
|
|
8980
10026
|
|
|
8981
|
-
const VERSION = '6.
|
|
10027
|
+
const VERSION = '6.15.0';
|
|
8982
10028
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
8983
10029
|
|
|
8984
10030
|
function requireSourceOrBundled(key) {
|
|
@@ -10737,6 +11783,12 @@ Usage:
|
|
|
10737
11783
|
${cmd} --impact <file> Show every file impacted by changing <file>
|
|
10738
11784
|
${cmd} --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
|
|
10739
11785
|
${cmd} --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
|
|
11786
|
+
${cmd} verify-ai-output <answer.md> Flag fake files/tests/imports/symbols/npm-scripts in an AI answer
|
|
11787
|
+
${cmd} verify-ai-output <answer.md> --json Hallucination report as JSON (exits 1 if issues)
|
|
11788
|
+
${cmd} verify-ai-output <answer.md> --report Write a standalone HTML report (red/amber/green)
|
|
11789
|
+
${cmd} note "<text>" Append a note to the cross-session decision log
|
|
11790
|
+
${cmd} note List recent notes (also: note --list <N>)
|
|
11791
|
+
${cmd} status Show repo state — branch, dirty files, index freshness, notes
|
|
10740
11792
|
${cmd} --init Write example config + .contextignore scaffold
|
|
10741
11793
|
${cmd} --help Show this message
|
|
10742
11794
|
${cmd} --version Show version
|
|
@@ -11858,6 +12910,191 @@ function main() {
|
|
|
11858
12910
|
process.exit(0);
|
|
11859
12911
|
}
|
|
11860
12912
|
|
|
12913
|
+
// `sigmap note "<text>"` — append to the cross-session decision log.
|
|
12914
|
+
// With no text, lists recent notes (also `note --list [N]`).
|
|
12915
|
+
if (args[0] === 'note') {
|
|
12916
|
+
const jsonOut = args.includes('--json');
|
|
12917
|
+
const { addNote, readNotes, formatNotes } = requireSourceOrBundled('./src/session/notes');
|
|
12918
|
+
const listIdx = args.indexOf('--list');
|
|
12919
|
+
// Build positionals, skipping value-taking flags and their values
|
|
12920
|
+
// (e.g. `--cwd <dir>`, `--list <N>`) so they never leak into the note text.
|
|
12921
|
+
const VALUE_FLAGS = new Set(['--cwd', '--list']);
|
|
12922
|
+
const positional = [];
|
|
12923
|
+
for (let i = 1; i < args.length; i++) {
|
|
12924
|
+
const a = args[i];
|
|
12925
|
+
if (a.startsWith('--')) { if (VALUE_FLAGS.has(a)) i++; continue; }
|
|
12926
|
+
positional.push(a);
|
|
12927
|
+
}
|
|
12928
|
+
const wantsList = listIdx !== -1 || positional.length === 0;
|
|
12929
|
+
|
|
12930
|
+
if (wantsList) {
|
|
12931
|
+
const limArg = listIdx !== -1 ? parseInt(args[listIdx + 1], 10) : parseInt(positional[0], 10);
|
|
12932
|
+
const limit = Number.isFinite(limArg) && limArg > 0 ? limArg : 10;
|
|
12933
|
+
const notes = readNotes(cwd, limit);
|
|
12934
|
+
if (jsonOut) {
|
|
12935
|
+
process.stdout.write(JSON.stringify({ notes }) + '\n');
|
|
12936
|
+
} else {
|
|
12937
|
+
console.log(`[sigmap] ${notes.length} recent note${notes.length === 1 ? '' : 's'}`);
|
|
12938
|
+
console.log(formatNotes(notes.slice().reverse()));
|
|
12939
|
+
}
|
|
12940
|
+
process.exit(0);
|
|
12941
|
+
}
|
|
12942
|
+
|
|
12943
|
+
const text = positional.join(' ');
|
|
12944
|
+
let entry;
|
|
12945
|
+
try {
|
|
12946
|
+
entry = addNote(cwd, text);
|
|
12947
|
+
} catch (err) {
|
|
12948
|
+
console.error(`[sigmap] ${err.message}`);
|
|
12949
|
+
process.exit(1);
|
|
12950
|
+
}
|
|
12951
|
+
if (jsonOut) {
|
|
12952
|
+
process.stdout.write(JSON.stringify({ added: entry }) + '\n');
|
|
12953
|
+
} else {
|
|
12954
|
+
console.log(`[sigmap] noted${entry.branch ? ` (${entry.branch})` : ''}: ${entry.text}`);
|
|
12955
|
+
}
|
|
12956
|
+
process.exit(0);
|
|
12957
|
+
}
|
|
12958
|
+
|
|
12959
|
+
// `sigmap status` — environment / repo state at a glance.
|
|
12960
|
+
if (args[0] === 'status') {
|
|
12961
|
+
const jsonOut = args.includes('--json');
|
|
12962
|
+
const gitOpts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
12963
|
+
const st = { branch: null, dirty: 0, lastIndex: null, indexVersion: null, indexFiles: null, changedSinceIndex: null, notes: 0, lastNote: null };
|
|
12964
|
+
|
|
12965
|
+
try { st.branch = execSync('git rev-parse --abbrev-ref HEAD', gitOpts).trim() || null; } catch (_) {}
|
|
12966
|
+
// Fallback for an unborn branch (fresh repo, no commits yet).
|
|
12967
|
+
if (!st.branch || st.branch === 'HEAD') {
|
|
12968
|
+
try { st.branch = execSync('git symbolic-ref --short HEAD', gitOpts).trim() || st.branch; } catch (_) {}
|
|
12969
|
+
}
|
|
12970
|
+
try {
|
|
12971
|
+
const porcelain = execSync('git status --porcelain', gitOpts).trim();
|
|
12972
|
+
st.dirty = porcelain ? porcelain.split('\n').filter(Boolean).length : 0;
|
|
12973
|
+
} catch (_) {}
|
|
12974
|
+
|
|
12975
|
+
try {
|
|
12976
|
+
const { readLog } = requireSourceOrBundled('./src/tracking/logger');
|
|
12977
|
+
const log = readLog(cwd);
|
|
12978
|
+
if (log.length) {
|
|
12979
|
+
const last = log[log.length - 1];
|
|
12980
|
+
st.lastIndex = last.ts || null;
|
|
12981
|
+
st.indexVersion = last.version || null;
|
|
12982
|
+
st.indexFiles = last.fileCount != null ? last.fileCount : null;
|
|
12983
|
+
}
|
|
12984
|
+
} catch (_) {}
|
|
12985
|
+
|
|
12986
|
+
// Index freshness: count tracked files modified after the last index run.
|
|
12987
|
+
if (st.lastIndex) {
|
|
12988
|
+
try {
|
|
12989
|
+
const since = Date.parse(st.lastIndex);
|
|
12990
|
+
const tracked = execSync('git ls-files', gitOpts).split('\n').filter(Boolean);
|
|
12991
|
+
let changed = 0;
|
|
12992
|
+
for (const f of tracked.slice(0, 5000)) {
|
|
12993
|
+
try { if (fs.statSync(path.join(cwd, f)).mtimeMs > since) changed++; } catch (_) {}
|
|
12994
|
+
}
|
|
12995
|
+
st.changedSinceIndex = changed;
|
|
12996
|
+
} catch (_) {}
|
|
12997
|
+
}
|
|
12998
|
+
|
|
12999
|
+
try {
|
|
13000
|
+
const { readNotes } = requireSourceOrBundled('./src/session/notes');
|
|
13001
|
+
const notes = readNotes(cwd);
|
|
13002
|
+
st.notes = notes.length;
|
|
13003
|
+
if (notes.length) st.lastNote = notes[notes.length - 1];
|
|
13004
|
+
} catch (_) {}
|
|
13005
|
+
|
|
13006
|
+
if (jsonOut) {
|
|
13007
|
+
process.stdout.write(JSON.stringify(st) + '\n');
|
|
13008
|
+
process.exit(0);
|
|
13009
|
+
}
|
|
13010
|
+
|
|
13011
|
+
const fmtAgo = (iso) => {
|
|
13012
|
+
if (!iso) return 'never';
|
|
13013
|
+
const ms = Date.now() - Date.parse(iso);
|
|
13014
|
+
if (!Number.isFinite(ms)) return iso;
|
|
13015
|
+
const m = Math.floor(ms / 60000), h = Math.floor(m / 60), d = Math.floor(h / 24);
|
|
13016
|
+
if (d > 0) return `${d}d ago`;
|
|
13017
|
+
if (h > 0) return `${h}h ago`;
|
|
13018
|
+
if (m > 0) return `${m}m ago`;
|
|
13019
|
+
return 'just now';
|
|
13020
|
+
};
|
|
13021
|
+
|
|
13022
|
+
console.log('[sigmap] status');
|
|
13023
|
+
console.log(` Branch: ${st.branch || '(not a git repo)'}`);
|
|
13024
|
+
console.log(` Working tree: ${st.dirty === 0 ? 'clean' : `${st.dirty} file${st.dirty === 1 ? '' : 's'} changed`}`);
|
|
13025
|
+
if (st.lastIndex) {
|
|
13026
|
+
let fresh = `${fmtAgo(st.lastIndex)} (v${st.indexVersion || '?'}, ${st.indexFiles != null ? st.indexFiles + ' files' : 'n/a'})`;
|
|
13027
|
+
if (st.changedSinceIndex != null && st.changedSinceIndex > 0) fresh += ` — STALE: ${st.changedSinceIndex} file${st.changedSinceIndex === 1 ? '' : 's'} changed since`;
|
|
13028
|
+
console.log(` Last index: ${fresh}`);
|
|
13029
|
+
} else {
|
|
13030
|
+
console.log(' Last index: never — run: sigmap');
|
|
13031
|
+
}
|
|
13032
|
+
if (st.notes > 0) {
|
|
13033
|
+
console.log(` Notes: ${st.notes} (latest: ${st.lastNote.text.slice(0, 60)}${st.lastNote.text.length > 60 ? '…' : ''})`);
|
|
13034
|
+
} else {
|
|
13035
|
+
console.log(' Notes: none — add with: sigmap note "<text>"');
|
|
13036
|
+
}
|
|
13037
|
+
process.exit(0);
|
|
13038
|
+
}
|
|
13039
|
+
|
|
13040
|
+
// `sigmap verify-ai-output <answer.md>` — Hallucination Guard (deterministic core)
|
|
13041
|
+
if (args[0] === 'verify-ai-output') {
|
|
13042
|
+
const target = args[1];
|
|
13043
|
+
const jsonOut = args.includes('--json');
|
|
13044
|
+
const reportIdx = args.indexOf('--report');
|
|
13045
|
+
const reportOut = reportIdx !== -1
|
|
13046
|
+
? (args[reportIdx + 1] && !args[reportIdx + 1].startsWith('--') ? args[reportIdx + 1] : 'sigmap-verify-report.html')
|
|
13047
|
+
: null;
|
|
13048
|
+
if (!target || target.startsWith('--')) {
|
|
13049
|
+
console.error('[sigmap] Usage: sigmap verify-ai-output <answer.md> [--json] [--report [out.html]]');
|
|
13050
|
+
process.exit(1);
|
|
13051
|
+
}
|
|
13052
|
+
const absTarget = path.resolve(cwd, target);
|
|
13053
|
+
if (!fs.existsSync(absTarget)) {
|
|
13054
|
+
console.error(`[sigmap] file not found: ${target}`);
|
|
13055
|
+
process.exit(1);
|
|
13056
|
+
}
|
|
13057
|
+
|
|
13058
|
+
const answerText = fs.readFileSync(absTarget, 'utf8');
|
|
13059
|
+
const { verify } = requireSourceOrBundled('./src/verify/hallucination-guard');
|
|
13060
|
+
const { issues, summary } = verify(answerText, cwd);
|
|
13061
|
+
const rel = path.relative(cwd, absTarget) || target;
|
|
13062
|
+
const result = { file: rel, issues, summary };
|
|
13063
|
+
|
|
13064
|
+
// Optional HTML report (Surface A) — written alongside any other output.
|
|
13065
|
+
if (reportOut) {
|
|
13066
|
+
const { renderReportHtml } = requireSourceOrBundled('./src/format/verify-report');
|
|
13067
|
+
const outAbs = path.resolve(cwd, reportOut);
|
|
13068
|
+
fs.writeFileSync(outAbs, renderReportHtml(result));
|
|
13069
|
+
if (!jsonOut) console.log(`[sigmap] report written: ${path.relative(cwd, outAbs) || reportOut}`);
|
|
13070
|
+
}
|
|
13071
|
+
|
|
13072
|
+
if (jsonOut) {
|
|
13073
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
13074
|
+
process.exit(summary.total > 0 ? 1 : 0);
|
|
13075
|
+
}
|
|
13076
|
+
|
|
13077
|
+
if (summary.total === 0) {
|
|
13078
|
+
console.log(`[sigmap] ✓ ${rel} — no hallucinations detected (${summary.symbolsIndexed} symbols indexed)`);
|
|
13079
|
+
process.exit(0);
|
|
13080
|
+
}
|
|
13081
|
+
|
|
13082
|
+
const labels = {
|
|
13083
|
+
'fake-file': 'Fake file', 'fake-test-file': 'Fake test file',
|
|
13084
|
+
'fake-import': 'Fake import', 'fake-symbol': 'Fake symbol',
|
|
13085
|
+
'fake-npm-script': 'Fake npm script',
|
|
13086
|
+
};
|
|
13087
|
+
const bt = summary.byType;
|
|
13088
|
+
console.log(`[sigmap] ✗ ${rel} — ${summary.total} issue${summary.total === 1 ? '' : 's'} found`);
|
|
13089
|
+
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']}`);
|
|
13090
|
+
console.log('');
|
|
13091
|
+
for (const issue of issues) {
|
|
13092
|
+
console.log(` L${issue.line} [${labels[issue.type] || issue.type}] ${issue.message}`);
|
|
13093
|
+
if (issue.suggestion) console.log(` ↳ ${issue.suggestion}`);
|
|
13094
|
+
}
|
|
13095
|
+
process.exit(1);
|
|
13096
|
+
}
|
|
13097
|
+
|
|
11861
13098
|
// Feature 1: `sigmap explain <file>` — why a file is included or excluded
|
|
11862
13099
|
if (args[0] === 'explain' || args.includes('--explain')) {
|
|
11863
13100
|
const target = args[0] === 'explain'
|