sigmap 6.5.2 → 6.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +26 -35
- package/CHANGELOG.md +18 -0
- package/gen-context.js +207 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/defaults.js +4 -0
- package/src/mcp/server.js +1 -1
- package/src/plan/planner.js +65 -0
- package/src/session/memory.js +56 -0
package/AGENTS.md
CHANGED
|
@@ -56,15 +56,11 @@ Use this marker block for all appendable context files:
|
|
|
56
56
|
| To query by topic | `sigmap --query "<topic>"` |
|
|
57
57
|
|
|
58
58
|
Always run `sigmap ask` or `sigmap --query` before searching for files relevant to a task.
|
|
59
|
-
## changes (last 5 commits —
|
|
59
|
+
## changes (last 5 commits — 8 minutes ago)
|
|
60
60
|
```
|
|
61
|
-
src/
|
|
62
|
-
src/
|
|
63
|
-
src/
|
|
64
|
-
src/discovery/source-root-scorer.js +getRecentlyChangedDirs +scoreCandidate +_countSourceFiles
|
|
65
|
-
src/discovery/framework-detector.js +detectFrameworks +_readDeps +_readFile +_existsAnywhere
|
|
66
|
-
src/discovery/sigmapignore.js +loadIgnorePatterns +matchesIgnorePattern
|
|
67
|
-
src/retrieval/ranker.js +_computePenalty ~scoreFile ~rank ~buildSigIndex
|
|
61
|
+
src/plan/planner.js +createPlan
|
|
62
|
+
src/retrieval/ranker.js +_computePenalty +_computeHubs +_isHub ~scoreFile
|
|
63
|
+
src/session/memory.js +sessionPath +loadSession +saveSession +mergeSessionContext
|
|
68
64
|
```
|
|
69
65
|
|
|
70
66
|
## packages
|
|
@@ -185,27 +181,6 @@ function write(context, cwd, opts = {})
|
|
|
185
181
|
|
|
186
182
|
## src
|
|
187
183
|
|
|
188
|
-
### src/extractors/javascript.js
|
|
189
|
-
```
|
|
190
|
-
module.exports = { extract }
|
|
191
|
-
function extract(src) → string[]
|
|
192
|
-
function extractBlock(src, startIndex)
|
|
193
|
-
function extractClassMembers(block, returnHints)
|
|
194
|
-
function buildReturnHints(src)
|
|
195
|
-
function normalizeType(type)
|
|
196
|
-
function formatReturnHint(type)
|
|
197
|
-
function normalizeParams(params)
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### src/extractors/kotlin.js
|
|
201
|
-
```
|
|
202
|
-
module.exports = { extract }
|
|
203
|
-
function extract(src) → string[]
|
|
204
|
-
function extractBlock(src, startIndex)
|
|
205
|
-
function extractMembers(block)
|
|
206
|
-
function normalizeParams(params)
|
|
207
|
-
```
|
|
208
|
-
|
|
209
184
|
### src/extractors/php.js
|
|
210
185
|
```
|
|
211
186
|
module.exports = { extract }
|
|
@@ -668,6 +643,21 @@ function loadIgnorePatterns(cwd)
|
|
|
668
643
|
function matchesIgnorePattern(dirName, patterns)
|
|
669
644
|
```
|
|
670
645
|
|
|
646
|
+
### src/plan/planner.js
|
|
647
|
+
```
|
|
648
|
+
module.exports = { createPlan }
|
|
649
|
+
function createPlan(goal, cwd, config)
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### src/mcp/server.js
|
|
653
|
+
```
|
|
654
|
+
module.exports = { start }
|
|
655
|
+
function respond(id, result)
|
|
656
|
+
function respondError(id, code, message)
|
|
657
|
+
function dispatch(msg, cwd)
|
|
658
|
+
function start(cwd)
|
|
659
|
+
```
|
|
660
|
+
|
|
671
661
|
### src/retrieval/ranker.js
|
|
672
662
|
```
|
|
673
663
|
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent }
|
|
@@ -683,11 +673,12 @@ function formatRankJSON(results, query) → object
|
|
|
683
673
|
function detectIntent(query)
|
|
684
674
|
```
|
|
685
675
|
|
|
686
|
-
### src/
|
|
676
|
+
### src/session/memory.js
|
|
687
677
|
```
|
|
688
|
-
module.exports = {
|
|
689
|
-
function
|
|
690
|
-
function
|
|
691
|
-
function
|
|
692
|
-
function
|
|
678
|
+
module.exports = { loadSession, saveSession, mergeSessionContext, clearSession }
|
|
679
|
+
function sessionPath(cwd)
|
|
680
|
+
function loadSession(cwd)
|
|
681
|
+
function saveSession(cwd, { intent, topFiles, query })
|
|
682
|
+
function mergeSessionContext(scores, session, currentIntent)
|
|
683
|
+
function clearSession(cwd)
|
|
693
684
|
```
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [6.6.1] — 2026-04-27
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **JVM project structure support** — Added auto-detection of Java, Kotlin, and Scala project directories. `srcDirs` now includes `src/main/java`, `src/main/kotlin`, `src/main/scala`, `app/src/main/java`, `app/src/main/kotlin`, `src/test/java`, and `src/test/kotlin` for out-of-the-box support of JVM-based projects.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## [6.6.0] — 2026-04-27
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Session memory** — Carry context across follow-up queries within a coding session. New `src/session/memory.js` module manages session state with 4-hour TTL. Previous session's top-5 files get +0.2 score boost in next query; boost reduced to +0.1 if intent differs (topic-switch guard).
|
|
26
|
+
- **`sigmap ask --followup`** — Reuse previous session's context when making follow-up queries. Session automatically saved after each `ask` command for seamless context carry-forward.
|
|
27
|
+
- **`sigmap plan "<goal>"`** — Analyze change impact and plan modifications. Returns files grouped by confidence (inspect-first vs likely-to-change), impact radius, and affected tests. Supports `--json` output for agent integration.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
13
31
|
## [6.5.2] — 2026-04-27
|
|
14
32
|
|
|
15
33
|
### Added
|
package/gen-context.js
CHANGED
|
@@ -5387,7 +5387,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
5387
5387
|
|
|
5388
5388
|
const SERVER_INFO = {
|
|
5389
5389
|
name: 'sigmap',
|
|
5390
|
-
version: '6.
|
|
5390
|
+
version: '6.6.1',
|
|
5391
5391
|
description: 'SigMap MCP server — code signatures on demand',
|
|
5392
5392
|
};
|
|
5393
5393
|
|
|
@@ -6211,6 +6211,124 @@ __factories["./src/tracking/logger"] = function(module, exports) {
|
|
|
6211
6211
|
|
|
6212
6212
|
};
|
|
6213
6213
|
|
|
6214
|
+
// ── ./src/session/memory ──
|
|
6215
|
+
__factories["./src/session/memory"] = function(module, exports) {
|
|
6216
|
+
'use strict';
|
|
6217
|
+
|
|
6218
|
+
const fs = require('fs');
|
|
6219
|
+
const path = require('path');
|
|
6220
|
+
|
|
6221
|
+
const SESSION_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours — one coding session
|
|
6222
|
+
|
|
6223
|
+
function sessionPath(cwd) {
|
|
6224
|
+
return path.join(cwd, '.context', 'session.json');
|
|
6225
|
+
}
|
|
6226
|
+
|
|
6227
|
+
function loadSession(cwd) {
|
|
6228
|
+
const p = sessionPath(cwd);
|
|
6229
|
+
if (!fs.existsSync(p)) return null;
|
|
6230
|
+
try {
|
|
6231
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
6232
|
+
if (Date.now() - raw.ts > SESSION_TTL_MS) return null; // expired
|
|
6233
|
+
return raw;
|
|
6234
|
+
} catch {
|
|
6235
|
+
return null;
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
|
|
6239
|
+
function saveSession(cwd, { intent, topFiles, query }) {
|
|
6240
|
+
const p = sessionPath(cwd);
|
|
6241
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
6242
|
+
fs.writeFileSync(p, JSON.stringify({
|
|
6243
|
+
ts: Date.now(),
|
|
6244
|
+
intent,
|
|
6245
|
+
topFiles, // [{ file, score }] — top 5 from last ask
|
|
6246
|
+
lastQuery: query,
|
|
6247
|
+
}));
|
|
6248
|
+
}
|
|
6249
|
+
|
|
6250
|
+
function mergeSessionContext(scores, session, currentIntent) {
|
|
6251
|
+
if (!session) return scores;
|
|
6252
|
+
|
|
6253
|
+
const boostAmount = session.intent === currentIntent ? 0.20 : 0.10;
|
|
6254
|
+
const sessionBoost = new Map(session.topFiles.map(f => [f.file, boostAmount]));
|
|
6255
|
+
|
|
6256
|
+
return scores.map(r => ({
|
|
6257
|
+
...r,
|
|
6258
|
+
score: r.score + (sessionBoost.get(r.file) || 0),
|
|
6259
|
+
}));
|
|
6260
|
+
}
|
|
6261
|
+
|
|
6262
|
+
function clearSession(cwd) {
|
|
6263
|
+
const p = sessionPath(cwd);
|
|
6264
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
6265
|
+
}
|
|
6266
|
+
|
|
6267
|
+
module.exports = { loadSession, saveSession, mergeSessionContext, clearSession };
|
|
6268
|
+
};
|
|
6269
|
+
|
|
6270
|
+
// ── ./src/plan/planner ──
|
|
6271
|
+
__factories["./src/plan/planner"] = function(module, exports) {
|
|
6272
|
+
'use strict';
|
|
6273
|
+
|
|
6274
|
+
const { buildFromCwd } = __require('./src/graph/builder');
|
|
6275
|
+
const { getImpact } = __require('./src/graph/impact');
|
|
6276
|
+
const { buildSigIndex, rank, detectIntent } = __require('./src/retrieval/ranker');
|
|
6277
|
+
const { buildTestIndex, isTested } = __require('./src/extractors/coverage');
|
|
6278
|
+
|
|
6279
|
+
function createPlan(goal, cwd, config) {
|
|
6280
|
+
const intent = detectIntent(goal);
|
|
6281
|
+
const sigIndex = buildSigIndex(cwd);
|
|
6282
|
+
if (sigIndex.size === 0) {
|
|
6283
|
+
return { error: 'no context found' };
|
|
6284
|
+
}
|
|
6285
|
+
|
|
6286
|
+
const ranked = rank(goal, sigIndex, { topK: 15, cwd });
|
|
6287
|
+
const highConf = ranked.filter(r => r.confidence === 'high').slice(0, 5);
|
|
6288
|
+
const medConf = ranked.filter(r => r.confidence === 'medium').slice(0, 5);
|
|
6289
|
+
|
|
6290
|
+
let impact = null;
|
|
6291
|
+
if (highConf.length > 0) {
|
|
6292
|
+
const entryFile = highConf[0].file;
|
|
6293
|
+
try {
|
|
6294
|
+
const graph = buildFromCwd(cwd);
|
|
6295
|
+
impact = getImpact(entryFile, graph, { maxDepth: 3, cwd });
|
|
6296
|
+
} catch (_) {
|
|
6297
|
+
// Graph build failed, continue without impact
|
|
6298
|
+
}
|
|
6299
|
+
}
|
|
6300
|
+
|
|
6301
|
+
let testedFiles = [];
|
|
6302
|
+
try {
|
|
6303
|
+
const testIndex = buildTestIndex(cwd, config.testDirs || ['test', 'tests', '__tests__', 'spec']);
|
|
6304
|
+
testedFiles = highConf.filter(r => {
|
|
6305
|
+
const sigs = r.sigs || [];
|
|
6306
|
+
const fnNames = sigs.map(s => {
|
|
6307
|
+
const m = s.match(/(?:function|def|fn)\s+(\w+)/);
|
|
6308
|
+
return m ? m[1] : null;
|
|
6309
|
+
}).filter(Boolean);
|
|
6310
|
+
return fnNames.some(fn => isTested(fn, testIndex));
|
|
6311
|
+
});
|
|
6312
|
+
} catch (_) {
|
|
6313
|
+
// Coverage index failed, continue without test info
|
|
6314
|
+
}
|
|
6315
|
+
|
|
6316
|
+
return {
|
|
6317
|
+
goal,
|
|
6318
|
+
intent,
|
|
6319
|
+
inspectFirst: highConf.map(r => r.file),
|
|
6320
|
+
likelyToChange: medConf.map(r => r.file),
|
|
6321
|
+
impactRadius: impact ? {
|
|
6322
|
+
direct: [...(impact.direct || [])],
|
|
6323
|
+
transitive: [...(impact.transitive || [])],
|
|
6324
|
+
} : null,
|
|
6325
|
+
testsAffected: testedFiles.map(r => r.file),
|
|
6326
|
+
};
|
|
6327
|
+
}
|
|
6328
|
+
|
|
6329
|
+
module.exports = { createPlan };
|
|
6330
|
+
};
|
|
6331
|
+
|
|
6214
6332
|
// ── ./src/retrieval/tokenizer ──
|
|
6215
6333
|
__factories["./src/retrieval/tokenizer"] = function(module, exports) {
|
|
6216
6334
|
'use strict';
|
|
@@ -7737,7 +7855,7 @@ const path = require('path');
|
|
|
7737
7855
|
const os = require('os');
|
|
7738
7856
|
const { execSync } = require('child_process');
|
|
7739
7857
|
|
|
7740
|
-
const VERSION = '6.
|
|
7858
|
+
const VERSION = '6.6.1';
|
|
7741
7859
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
7742
7860
|
|
|
7743
7861
|
function requireSourceOrBundled(key) {
|
|
@@ -9791,6 +9909,7 @@ function main() {
|
|
|
9791
9909
|
|
|
9792
9910
|
const { detectIntent, buildSigIndex, rank } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
9793
9911
|
const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
|
|
9912
|
+
const { loadSession, saveSession, mergeSessionContext } = requireSourceOrBundled('./src/session/memory');
|
|
9794
9913
|
|
|
9795
9914
|
const intent = detectIntent(query);
|
|
9796
9915
|
const intentWeights = getIntentWeights(intent);
|
|
@@ -9801,7 +9920,21 @@ function main() {
|
|
|
9801
9920
|
process.exit(1);
|
|
9802
9921
|
}
|
|
9803
9922
|
|
|
9804
|
-
|
|
9923
|
+
let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd });
|
|
9924
|
+
|
|
9925
|
+
// v6.8: --followup support — carry session context forward
|
|
9926
|
+
const isFollowup = args.includes('--followup');
|
|
9927
|
+
const session = isFollowup ? loadSession(cwd) : null;
|
|
9928
|
+
if (session) {
|
|
9929
|
+
ranked = mergeSessionContext(ranked, session, intent);
|
|
9930
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
9931
|
+
if (!args.includes('--json')) {
|
|
9932
|
+
process.stderr.write(`[sigmap] ♻ followup — carrying context from: "${session.lastQuery.slice(0, 60)}"\n`);
|
|
9933
|
+
}
|
|
9934
|
+
}
|
|
9935
|
+
|
|
9936
|
+
// v6.8: Save session for future --followup calls
|
|
9937
|
+
saveSession(cwd, { intent, topFiles: ranked.slice(0, 5).map(r => ({ file: r.file, score: r.score })), query });
|
|
9805
9938
|
const miniCtx = buildMiniContext(ranked, cwd);
|
|
9806
9939
|
const outPath = path.join(cwd, '.context', 'query-context.md');
|
|
9807
9940
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
@@ -9947,6 +10080,77 @@ function main() {
|
|
|
9947
10080
|
process.exit(0);
|
|
9948
10081
|
}
|
|
9949
10082
|
|
|
10083
|
+
// v6.8: `sigmap plan "<goal>"` — analyze what files need changes and impact radius
|
|
10084
|
+
if (args[0] === 'plan') {
|
|
10085
|
+
const goal = args[1];
|
|
10086
|
+
if (!goal || goal.startsWith('--')) {
|
|
10087
|
+
console.error('[sigmap] Usage: sigmap plan "<goal>"');
|
|
10088
|
+
console.error(' Example: sigmap plan "add rate limiting to the API"');
|
|
10089
|
+
process.exit(1);
|
|
10090
|
+
}
|
|
10091
|
+
|
|
10092
|
+
const { detectIntent, buildSigIndex, rank } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
10093
|
+
|
|
10094
|
+
const intent = detectIntent(goal);
|
|
10095
|
+
const intentWeights = getIntentWeights(intent);
|
|
10096
|
+
|
|
10097
|
+
const sigIndex = buildSigIndex(cwd);
|
|
10098
|
+
if (sigIndex.size === 0) {
|
|
10099
|
+
console.error('[sigmap] no context file found. Run: sigmap (to generate first)');
|
|
10100
|
+
process.exit(1);
|
|
10101
|
+
}
|
|
10102
|
+
|
|
10103
|
+
const ranked = rank(goal, sigIndex, { topK: 15, weights: intentWeights, cwd });
|
|
10104
|
+
|
|
10105
|
+
// Separate into confidence levels
|
|
10106
|
+
const highConf = ranked.filter(r => r.confidence === 'high').slice(0, 5);
|
|
10107
|
+
const medConf = ranked.filter(r => r.confidence === 'medium').slice(0, 5);
|
|
10108
|
+
|
|
10109
|
+
// Compute impact radius (simplified — no graph for now)
|
|
10110
|
+
let impact = null;
|
|
10111
|
+
|
|
10112
|
+
// Identify likely-affected tests (simplified — checks for .test. or .spec. in filename)
|
|
10113
|
+
const testedFiles = highConf.filter(r => /\.(test|spec)\.(js|ts|py)$|_test\.(js|ts|py)$/.test(r.file));
|
|
10114
|
+
|
|
10115
|
+
if (args.includes('--json')) {
|
|
10116
|
+
process.stdout.write(JSON.stringify({
|
|
10117
|
+
goal, intent,
|
|
10118
|
+
inspectFirst: highConf.map(r => r.file),
|
|
10119
|
+
likelyToChange: medConf.map(r => r.file),
|
|
10120
|
+
impactRadius: impact,
|
|
10121
|
+
testsAffected: testedFiles.map(r => r.file),
|
|
10122
|
+
}, null, 2) + '\n');
|
|
10123
|
+
} else {
|
|
10124
|
+
const bar = '─'.repeat(50);
|
|
10125
|
+
console.log(bar);
|
|
10126
|
+
console.log(` sigmap plan "${goal}"`);
|
|
10127
|
+
console.log(` Intent : ${intent}`);
|
|
10128
|
+
console.log(bar);
|
|
10129
|
+
console.log('');
|
|
10130
|
+
console.log(' Inspect first (highest relevance):');
|
|
10131
|
+
if (highConf.length === 0) {
|
|
10132
|
+
console.log(' (no files found)');
|
|
10133
|
+
} else {
|
|
10134
|
+
highConf.forEach((r, i) => console.log(` ${i + 1}. ${path.relative(cwd, r.file)}`));
|
|
10135
|
+
}
|
|
10136
|
+
console.log('');
|
|
10137
|
+
console.log(' Likely to change:');
|
|
10138
|
+
if (medConf.length === 0) {
|
|
10139
|
+
console.log(' (no files found)');
|
|
10140
|
+
} else {
|
|
10141
|
+
medConf.forEach((r, i) => console.log(` ${i + 1}. ${path.relative(cwd, r.file)}`));
|
|
10142
|
+
}
|
|
10143
|
+
if (testedFiles.length > 0) {
|
|
10144
|
+
console.log('');
|
|
10145
|
+
console.log(' Tests to run after change:');
|
|
10146
|
+
testedFiles.forEach(r => console.log(` • ${path.relative(cwd, r.file)}`));
|
|
10147
|
+
}
|
|
10148
|
+
console.log('');
|
|
10149
|
+
console.log(bar);
|
|
10150
|
+
}
|
|
10151
|
+
process.exit(0);
|
|
10152
|
+
}
|
|
10153
|
+
|
|
9950
10154
|
// v5.9: `sigmap bench --submit` — format local benchmark history as a shareable community block
|
|
9951
10155
|
if (args[0] === 'bench' && args.includes('--submit')) {
|
|
9952
10156
|
const versionMeta = (() => {
|
package/package.json
CHANGED
package/src/config/defaults.js
CHANGED
|
@@ -26,6 +26,10 @@ const DEFAULTS = {
|
|
|
26
26
|
'pages', 'components', 'hooks', 'routes', 'controllers',
|
|
27
27
|
'models', 'views', 'resources', 'config', 'db',
|
|
28
28
|
'projects', 'apps', 'libs', 'instance', 'blueprints',
|
|
29
|
+
// JVM project structures (Java, Kotlin, Scala)
|
|
30
|
+
'src/main/java', 'src/main/kotlin', 'src/main/scala',
|
|
31
|
+
'app/src/main/java', 'app/src/main/kotlin',
|
|
32
|
+
'src/test/java', 'src/test/kotlin',
|
|
29
33
|
],
|
|
30
34
|
|
|
31
35
|
// Directory/file names to exclude entirely
|
package/src/mcp/server.js
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { buildFromCwd } = require('../graph/builder');
|
|
6
|
+
const { getImpact } = require('../graph/impact');
|
|
7
|
+
const { buildSigIndex, rank, detectIntent } = require('../retrieval/ranker');
|
|
8
|
+
const { buildTestIndex, isTested } = require('../extractors/coverage');
|
|
9
|
+
|
|
10
|
+
module.exports = { createPlan };
|
|
11
|
+
|
|
12
|
+
function createPlan(goal, cwd, config) {
|
|
13
|
+
// Step 1: Detect intent and rank files for the goal
|
|
14
|
+
const intent = detectIntent(goal);
|
|
15
|
+
const sigIndex = buildSigIndex(cwd);
|
|
16
|
+
if (sigIndex.size === 0) {
|
|
17
|
+
return { error: 'no context found' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ranked = rank(goal, sigIndex, { topK: 15, cwd });
|
|
21
|
+
|
|
22
|
+
// Step 2: Separate into confidence levels
|
|
23
|
+
const highConf = ranked.filter(r => r.confidence === 'high').slice(0, 5);
|
|
24
|
+
const medConf = ranked.filter(r => r.confidence === 'medium').slice(0, 5);
|
|
25
|
+
|
|
26
|
+
// Step 3: Compute impact radius for highest-confidence file
|
|
27
|
+
let impact = null;
|
|
28
|
+
if (highConf.length > 0) {
|
|
29
|
+
const entryFile = highConf[0].file;
|
|
30
|
+
try {
|
|
31
|
+
const graph = buildFromCwd(cwd);
|
|
32
|
+
impact = getImpact(entryFile, graph, { maxDepth: 3, cwd });
|
|
33
|
+
} catch (_) {
|
|
34
|
+
// Graph build failed, continue without impact
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Step 4: Identify likely-affected tests
|
|
39
|
+
let testedFiles = [];
|
|
40
|
+
try {
|
|
41
|
+
const testIndex = buildTestIndex(cwd, config.testDirs || ['test', 'tests', '__tests__', 'spec']);
|
|
42
|
+
testedFiles = highConf.filter(r => {
|
|
43
|
+
const sigs = r.sigs || [];
|
|
44
|
+
const fnNames = sigs.map(s => {
|
|
45
|
+
const m = s.match(/(?:function|def|fn)\s+(\w+)/);
|
|
46
|
+
return m ? m[1] : null;
|
|
47
|
+
}).filter(Boolean);
|
|
48
|
+
return fnNames.some(fn => isTested(fn, testIndex));
|
|
49
|
+
});
|
|
50
|
+
} catch (_) {
|
|
51
|
+
// Coverage index failed, continue without test info
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
goal,
|
|
56
|
+
intent,
|
|
57
|
+
inspectFirst: highConf.map(r => r.file),
|
|
58
|
+
likelyToChange: medConf.map(r => r.file),
|
|
59
|
+
impactRadius: impact ? {
|
|
60
|
+
direct: [...(impact.direct || [])],
|
|
61
|
+
transitive: [...(impact.transitive || [])],
|
|
62
|
+
} : null,
|
|
63
|
+
testsAffected: testedFiles.map(r => r.file),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
module.exports = { loadSession, saveSession, mergeSessionContext, clearSession };
|
|
7
|
+
|
|
8
|
+
const SESSION_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours — one coding session
|
|
9
|
+
|
|
10
|
+
function sessionPath(cwd) {
|
|
11
|
+
return path.join(cwd, '.context', 'session.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function loadSession(cwd) {
|
|
15
|
+
const p = sessionPath(cwd);
|
|
16
|
+
if (!fs.existsSync(p)) return null;
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
19
|
+
if (Date.now() - raw.ts > SESSION_TTL_MS) return null; // expired
|
|
20
|
+
return raw;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveSession(cwd, { intent, topFiles, query }) {
|
|
27
|
+
const p = sessionPath(cwd);
|
|
28
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
29
|
+
fs.writeFileSync(p, JSON.stringify({
|
|
30
|
+
ts: Date.now(),
|
|
31
|
+
intent,
|
|
32
|
+
topFiles, // [{ file, score }] — top 5 from last ask
|
|
33
|
+
lastQuery: query,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Merge session context as additional ranking signal:
|
|
38
|
+
// any file that appeared in the previous session's top-5 gets a +0.2 boost
|
|
39
|
+
// Reduce to +0.1 if the intent has changed (topic switch guard)
|
|
40
|
+
function mergeSessionContext(scores, session, currentIntent) {
|
|
41
|
+
if (!session) return scores;
|
|
42
|
+
|
|
43
|
+
const boostAmount = session.intent === currentIntent ? 0.20 : 0.10;
|
|
44
|
+
const sessionBoost = new Map(session.topFiles.map(f => [f.file, boostAmount]));
|
|
45
|
+
|
|
46
|
+
return scores.map(r => ({
|
|
47
|
+
...r,
|
|
48
|
+
// Additive boost — cannot reduce a score
|
|
49
|
+
score: r.score + (sessionBoost.get(r.file) || 0),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clearSession(cwd) {
|
|
54
|
+
const p = sessionPath(cwd);
|
|
55
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
56
|
+
}
|