codebase-context 1.4.1 → 1.5.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/README.md +193 -45
- package/dist/analyzers/generic/index.d.ts +0 -1
- package/dist/analyzers/generic/index.d.ts.map +1 -1
- package/dist/analyzers/generic/index.js +0 -13
- package/dist/analyzers/generic/index.js.map +1 -1
- package/dist/constants/codebase-context.d.ts +2 -0
- package/dist/constants/codebase-context.d.ts.map +1 -1
- package/dist/constants/codebase-context.js +2 -0
- package/dist/constants/codebase-context.js.map +1 -1
- package/dist/constants/git-patterns.d.ts +12 -0
- package/dist/constants/git-patterns.d.ts.map +1 -0
- package/dist/constants/git-patterns.js +11 -0
- package/dist/constants/git-patterns.js.map +1 -0
- package/dist/core/analyzer-registry.d.ts.map +1 -1
- package/dist/core/analyzer-registry.js +3 -1
- package/dist/core/analyzer-registry.js.map +1 -1
- package/dist/core/indexer.d.ts +2 -0
- package/dist/core/indexer.d.ts.map +1 -1
- package/dist/core/indexer.js +155 -20
- package/dist/core/indexer.js.map +1 -1
- package/dist/core/manifest.d.ts +39 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +86 -0
- package/dist/core/manifest.js.map +1 -0
- package/dist/core/search-quality.d.ts +10 -0
- package/dist/core/search-quality.d.ts.map +1 -0
- package/dist/core/search-quality.js +64 -0
- package/dist/core/search-quality.js.map +1 -0
- package/dist/core/search.d.ts +16 -0
- package/dist/core/search.d.ts.map +1 -1
- package/dist/core/search.js +251 -56
- package/dist/core/search.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +460 -53
- package/dist/index.js.map +1 -1
- package/dist/memory/git-memory.d.ts +9 -0
- package/dist/memory/git-memory.d.ts.map +1 -0
- package/dist/memory/git-memory.js +51 -0
- package/dist/memory/git-memory.js.map +1 -0
- package/dist/memory/store.d.ts +16 -0
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +40 -1
- package/dist/memory/store.js.map +1 -1
- package/dist/patterns/semantics.d.ts +4 -0
- package/dist/patterns/semantics.d.ts.map +1 -0
- package/dist/patterns/semantics.js +24 -0
- package/dist/patterns/semantics.js.map +1 -0
- package/dist/preflight/evidence-lock.d.ts +50 -0
- package/dist/preflight/evidence-lock.d.ts.map +1 -0
- package/dist/preflight/evidence-lock.js +130 -0
- package/dist/preflight/evidence-lock.js.map +1 -0
- package/dist/preflight/query-scope.d.ts +3 -0
- package/dist/preflight/query-scope.d.ts.map +1 -0
- package/dist/preflight/query-scope.js +40 -0
- package/dist/preflight/query-scope.js.map +1 -0
- package/dist/resources/uri.d.ts +5 -0
- package/dist/resources/uri.d.ts.map +1 -0
- package/dist/resources/uri.js +15 -0
- package/dist/resources/uri.js.map +1 -0
- package/dist/storage/lancedb.d.ts +1 -0
- package/dist/storage/lancedb.d.ts.map +1 -1
- package/dist/storage/lancedb.js +24 -3
- package/dist/storage/lancedb.js.map +1 -1
- package/dist/storage/types.d.ts +5 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js.map +1 -1
- package/dist/types/index.d.ts +21 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/git-dates.d.ts +1 -0
- package/dist/utils/git-dates.d.ts.map +1 -1
- package/dist/utils/git-dates.js +20 -0
- package/dist/utils/git-dates.js.map +1 -1
- package/dist/utils/usage-tracker.d.ts.map +1 -1
- package/dist/utils/usage-tracker.js +3 -6
- package/dist/utils/usage-tracker.js.map +1 -1
- package/package.json +16 -8
package/dist/index.js
CHANGED
|
@@ -16,9 +16,16 @@ import { analyzerRegistry } from './core/analyzer-registry.js';
|
|
|
16
16
|
import { AngularAnalyzer } from './analyzers/angular/index.js';
|
|
17
17
|
import { GenericAnalyzer } from './analyzers/generic/index.js';
|
|
18
18
|
import { InternalFileGraph } from './utils/usage-tracker.js';
|
|
19
|
+
import { getFileCommitDates } from './utils/git-dates.js';
|
|
19
20
|
import { IndexCorruptedError } from './errors/index.js';
|
|
20
21
|
import { CODEBASE_CONTEXT_DIRNAME, MEMORY_FILENAME, INTELLIGENCE_FILENAME, KEYWORD_INDEX_FILENAME, VECTOR_DB_DIRNAME } from './constants/codebase-context.js';
|
|
21
|
-
import { appendMemoryFile, readMemoriesFile, filterMemories, applyUnfilteredLimit } from './memory/store.js';
|
|
22
|
+
import { appendMemoryFile, readMemoriesFile, filterMemories, applyUnfilteredLimit, withConfidence } from './memory/store.js';
|
|
23
|
+
import { parseGitLogLineToMemory } from './memory/git-memory.js';
|
|
24
|
+
import { buildEvidenceLock } from './preflight/evidence-lock.js';
|
|
25
|
+
import { shouldIncludePatternConflictCategory } from './preflight/query-scope.js';
|
|
26
|
+
import { isComplementaryPatternCategory, isComplementaryPatternConflict, shouldSkipLegacyTestingFrameworkCategory } from './patterns/semantics.js';
|
|
27
|
+
import { CONTEXT_RESOURCE_URI, isContextResourceUri } from './resources/uri.js';
|
|
28
|
+
import { assessSearchQuality } from './core/search-quality.js';
|
|
22
29
|
analyzerRegistry.register(new AngularAnalyzer());
|
|
23
30
|
analyzerRegistry.register(new GenericAnalyzer());
|
|
24
31
|
// Resolve root path with validation
|
|
@@ -110,12 +117,14 @@ async function migrateToNewStructure() {
|
|
|
110
117
|
return false;
|
|
111
118
|
}
|
|
112
119
|
}
|
|
120
|
+
// Read version from package.json so it never drifts
|
|
121
|
+
const PKG_VERSION = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8')).version;
|
|
113
122
|
const indexState = {
|
|
114
123
|
status: 'idle'
|
|
115
124
|
};
|
|
116
125
|
const server = new Server({
|
|
117
126
|
name: 'codebase-context',
|
|
118
|
-
version:
|
|
127
|
+
version: PKG_VERSION
|
|
119
128
|
}, {
|
|
120
129
|
capabilities: {
|
|
121
130
|
tools: {},
|
|
@@ -127,6 +136,8 @@ const TOOLS = [
|
|
|
127
136
|
name: 'search_codebase',
|
|
128
137
|
description: 'Search the indexed codebase using natural language queries. Returns code summaries with file locations. ' +
|
|
129
138
|
'Supports framework-specific queries and architectural layer filtering. ' +
|
|
139
|
+
'When intent is "edit", "refactor", or "migrate", returns a preflight card with risk level, ' +
|
|
140
|
+
'patterns to use/avoid, impact candidates, related memories, and an evidence lock score — all in one call. ' +
|
|
130
141
|
'Use the returned filePath with other tools to read complete file contents.',
|
|
131
142
|
inputSchema: {
|
|
132
143
|
type: 'object',
|
|
@@ -135,6 +146,13 @@ const TOOLS = [
|
|
|
135
146
|
type: 'string',
|
|
136
147
|
description: 'Natural language search query'
|
|
137
148
|
},
|
|
149
|
+
intent: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
enum: ['explore', 'edit', 'refactor', 'migrate'],
|
|
152
|
+
description: 'Search intent. Use "explore" (default) for read-only browsing. ' +
|
|
153
|
+
'Use "edit", "refactor", or "migrate" to get a preflight card with risk assessment, ' +
|
|
154
|
+
'patterns to prefer/avoid, affected files, relevant team memories, and ready-to-edit evidence checks.'
|
|
155
|
+
},
|
|
138
156
|
limit: {
|
|
139
157
|
type: 'number',
|
|
140
158
|
description: 'Maximum number of results to return (default: 5)',
|
|
@@ -289,8 +307,9 @@ const TOOLS = [
|
|
|
289
307
|
properties: {
|
|
290
308
|
type: {
|
|
291
309
|
type: 'string',
|
|
292
|
-
enum: ['convention', 'decision', 'gotcha'],
|
|
293
|
-
description: 'Type of memory being recorded'
|
|
310
|
+
enum: ['convention', 'decision', 'gotcha', 'failure'],
|
|
311
|
+
description: 'Type of memory being recorded. Use "failure" for things that were tried and failed — ' +
|
|
312
|
+
'prevents repeating the same mistakes.'
|
|
294
313
|
},
|
|
295
314
|
category: {
|
|
296
315
|
type: 'string',
|
|
@@ -325,7 +344,7 @@ const TOOLS = [
|
|
|
325
344
|
type: {
|
|
326
345
|
type: 'string',
|
|
327
346
|
description: 'Filter by memory type',
|
|
328
|
-
enum: ['convention', 'decision', 'gotcha']
|
|
347
|
+
enum: ['convention', 'decision', 'gotcha', 'failure']
|
|
329
348
|
},
|
|
330
349
|
query: {
|
|
331
350
|
type: 'string',
|
|
@@ -341,7 +360,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
341
360
|
// MCP Resources - Proactive context injection
|
|
342
361
|
const RESOURCES = [
|
|
343
362
|
{
|
|
344
|
-
uri:
|
|
363
|
+
uri: CONTEXT_RESOURCE_URI,
|
|
345
364
|
name: 'Codebase Intelligence',
|
|
346
365
|
description: 'Automatic codebase context: libraries used, team patterns, and conventions. ' +
|
|
347
366
|
'Read this BEFORE generating code to follow team standards.',
|
|
@@ -389,16 +408,35 @@ async function generateCodebaseContext() {
|
|
|
389
408
|
}
|
|
390
409
|
// Pattern consensus
|
|
391
410
|
if (intelligence.patterns && Object.keys(intelligence.patterns).length > 0) {
|
|
411
|
+
const patterns = intelligence.patterns;
|
|
392
412
|
lines.push("## YOUR Codebase's Actual Patterns (Not Generic Best Practices)");
|
|
393
413
|
lines.push('');
|
|
394
414
|
lines.push('These patterns were detected by analyzing your actual code.');
|
|
395
415
|
lines.push('This is what YOUR team does in practice, not what tutorials recommend.');
|
|
396
416
|
lines.push('');
|
|
397
|
-
for (const [category, data] of Object.entries(
|
|
417
|
+
for (const [category, data] of Object.entries(patterns)) {
|
|
418
|
+
if (shouldSkipLegacyTestingFrameworkCategory(category, patterns)) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
398
421
|
const patternData = data;
|
|
399
422
|
const primary = patternData.primary;
|
|
423
|
+
const alternatives = patternData.alsoDetected ?? [];
|
|
400
424
|
if (!primary)
|
|
401
425
|
continue;
|
|
426
|
+
if (isComplementaryPatternCategory(category, [primary.name, ...alternatives.map((alt) => alt.name)].filter(Boolean))) {
|
|
427
|
+
const secondary = alternatives[0];
|
|
428
|
+
if (secondary) {
|
|
429
|
+
const categoryName = category
|
|
430
|
+
.replace(/([A-Z])/g, ' $1')
|
|
431
|
+
.trim()
|
|
432
|
+
.replace(/^./, (str) => str.toUpperCase());
|
|
433
|
+
lines.push(`### ${categoryName}: **${primary.name}** (${primary.frequency}) + **${secondary.name}** (${secondary.frequency})`);
|
|
434
|
+
lines.push(' → Computed and effect are complementary Signals primitives and are commonly used together.');
|
|
435
|
+
lines.push(' → Treat this as balanced usage, not a hard split decision.');
|
|
436
|
+
lines.push('');
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
402
440
|
const percentage = parseInt(primary.frequency);
|
|
403
441
|
const categoryName = category
|
|
404
442
|
.replace(/([A-Z])/g, ' $1')
|
|
@@ -411,16 +449,16 @@ async function generateCodebaseContext() {
|
|
|
411
449
|
else if (percentage >= 80) {
|
|
412
450
|
lines.push(`### ${categoryName}: **${primary.name}** (${primary.frequency} - strong consensus)`);
|
|
413
451
|
lines.push(` → Your team strongly prefers ${primary.name}`);
|
|
414
|
-
if (
|
|
415
|
-
const alt =
|
|
452
|
+
if (alternatives.length) {
|
|
453
|
+
const alt = alternatives[0];
|
|
416
454
|
lines.push(` → Minority pattern: ${alt.name} (${alt.frequency}) - avoid for new code`);
|
|
417
455
|
}
|
|
418
456
|
}
|
|
419
457
|
else if (percentage >= 60) {
|
|
420
458
|
lines.push(`### ${categoryName}: **${primary.name}** (${primary.frequency} - majority)`);
|
|
421
459
|
lines.push(` → Most code uses ${primary.name}, but not unanimous`);
|
|
422
|
-
if (
|
|
423
|
-
lines.push(` → Also detected: ${
|
|
460
|
+
if (alternatives.length) {
|
|
461
|
+
lines.push(` → Also detected: ${alternatives[0].name} (${alternatives[0].frequency})`);
|
|
424
462
|
}
|
|
425
463
|
}
|
|
426
464
|
else {
|
|
@@ -428,8 +466,8 @@ async function generateCodebaseContext() {
|
|
|
428
466
|
lines.push(`### ${categoryName}: ⚠️ NO TEAM CONSENSUS`);
|
|
429
467
|
lines.push(` Your codebase is split between multiple approaches:`);
|
|
430
468
|
lines.push(` - ${primary.name} (${primary.frequency})`);
|
|
431
|
-
if (
|
|
432
|
-
for (const alt of
|
|
469
|
+
if (alternatives.length) {
|
|
470
|
+
for (const alt of alternatives.slice(0, 2)) {
|
|
433
471
|
lines.push(` - ${alt.name} (${alt.frequency})`);
|
|
434
472
|
}
|
|
435
473
|
}
|
|
@@ -450,12 +488,12 @@ async function generateCodebaseContext() {
|
|
|
450
488
|
}
|
|
451
489
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
452
490
|
const uri = request.params.uri;
|
|
453
|
-
if (uri
|
|
491
|
+
if (isContextResourceUri(uri)) {
|
|
454
492
|
const content = await generateCodebaseContext();
|
|
455
493
|
return {
|
|
456
494
|
contents: [
|
|
457
495
|
{
|
|
458
|
-
uri,
|
|
496
|
+
uri: CONTEXT_RESOURCE_URI,
|
|
459
497
|
mimeType: 'text/plain',
|
|
460
498
|
text: content
|
|
461
499
|
}
|
|
@@ -464,13 +502,51 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
464
502
|
}
|
|
465
503
|
throw new Error(`Unknown resource: ${uri}`);
|
|
466
504
|
});
|
|
467
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Extract memories from conventional git commits (refactor:, migrate:, fix:, revert:).
|
|
507
|
+
* Scans last 90 days. Deduplicates via content hash. Zero friction alternative to manual memory.
|
|
508
|
+
*/
|
|
509
|
+
async function extractGitMemories() {
|
|
510
|
+
// Quick check: skip if not a git repo
|
|
511
|
+
if (!(await fileExists(path.join(ROOT_PATH, '.git'))))
|
|
512
|
+
return 0;
|
|
513
|
+
const { execSync } = await import('child_process');
|
|
514
|
+
let log;
|
|
515
|
+
try {
|
|
516
|
+
// Format: ISO-date<TAB>hash subject (e.g. "2026-01-15T10:00:00+00:00\tabc1234 fix: race condition")
|
|
517
|
+
log = execSync('git log --format="%aI\t%h %s" --since="90 days ago" --no-merges', {
|
|
518
|
+
cwd: ROOT_PATH,
|
|
519
|
+
encoding: 'utf-8',
|
|
520
|
+
timeout: 5000
|
|
521
|
+
}).trim();
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// Git not available or command failed — silently skip
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
if (!log)
|
|
528
|
+
return 0;
|
|
529
|
+
const lines = log.split('\n').filter(Boolean);
|
|
530
|
+
let added = 0;
|
|
531
|
+
for (const line of lines) {
|
|
532
|
+
const parsedMemory = parseGitLogLineToMemory(line);
|
|
533
|
+
if (!parsedMemory)
|
|
534
|
+
continue;
|
|
535
|
+
const result = await appendMemoryFile(PATHS.memory, parsedMemory);
|
|
536
|
+
if (result.status === 'added')
|
|
537
|
+
added++;
|
|
538
|
+
}
|
|
539
|
+
return added;
|
|
540
|
+
}
|
|
541
|
+
async function performIndexing(incrementalOnly) {
|
|
468
542
|
indexState.status = 'indexing';
|
|
469
|
-
|
|
543
|
+
const mode = incrementalOnly ? 'incremental' : 'full';
|
|
544
|
+
console.error(`Indexing (${mode}): ${ROOT_PATH}`);
|
|
470
545
|
try {
|
|
471
546
|
let lastLoggedProgress = { phase: '', percentage: -1 };
|
|
472
547
|
const indexer = new CodebaseIndexer({
|
|
473
548
|
rootPath: ROOT_PATH,
|
|
549
|
+
incrementalOnly,
|
|
474
550
|
onProgress: (progress) => {
|
|
475
551
|
// Only log when phase or percentage actually changes (prevents duplicate logs)
|
|
476
552
|
const shouldLog = progress.phase !== lastLoggedProgress.phase ||
|
|
@@ -487,6 +563,16 @@ async function performIndexing() {
|
|
|
487
563
|
indexState.lastIndexed = new Date();
|
|
488
564
|
indexState.stats = stats;
|
|
489
565
|
console.error(`Complete: ${stats.indexedFiles} files, ${stats.totalChunks} chunks in ${(stats.duration / 1000).toFixed(2)}s`);
|
|
566
|
+
// Auto-extract memories from git history (non-blocking, best-effort)
|
|
567
|
+
try {
|
|
568
|
+
const gitMemories = await extractGitMemories();
|
|
569
|
+
if (gitMemories > 0) {
|
|
570
|
+
console.error(`[git-memory] Extracted ${gitMemories} new memor${gitMemories === 1 ? 'y' : 'ies'} from git history`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
// Git memory extraction is optional — never fail indexing over it
|
|
575
|
+
}
|
|
490
576
|
}
|
|
491
577
|
catch (error) {
|
|
492
578
|
indexState.status = 'error';
|
|
@@ -509,7 +595,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
509
595
|
try {
|
|
510
596
|
switch (name) {
|
|
511
597
|
case 'search_codebase': {
|
|
512
|
-
const { query, limit, filters } = args;
|
|
598
|
+
const { query, limit, filters, intent } = args;
|
|
513
599
|
if (indexState.status === 'indexing') {
|
|
514
600
|
return {
|
|
515
601
|
content: [
|
|
@@ -539,8 +625,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
539
625
|
}
|
|
540
626
|
const searcher = new CodebaseSearcher(ROOT_PATH);
|
|
541
627
|
let results;
|
|
628
|
+
const searchProfile = intent && ['explore', 'edit', 'refactor', 'migrate'].includes(intent)
|
|
629
|
+
? intent
|
|
630
|
+
: 'explore';
|
|
542
631
|
try {
|
|
543
|
-
results = await searcher.search(query, limit || 5, filters
|
|
632
|
+
results = await searcher.search(query, limit || 5, filters, {
|
|
633
|
+
profile: searchProfile
|
|
634
|
+
});
|
|
544
635
|
}
|
|
545
636
|
catch (error) {
|
|
546
637
|
if (error instanceof IndexCorruptedError) {
|
|
@@ -550,7 +641,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
550
641
|
console.error('[Auto-Heal] Success. Retrying search...');
|
|
551
642
|
const freshSearcher = new CodebaseSearcher(ROOT_PATH);
|
|
552
643
|
try {
|
|
553
|
-
results = await freshSearcher.search(query, limit || 5, filters
|
|
644
|
+
results = await freshSearcher.search(query, limit || 5, filters, {
|
|
645
|
+
profile: searchProfile
|
|
646
|
+
});
|
|
554
647
|
}
|
|
555
648
|
catch (retryError) {
|
|
556
649
|
return {
|
|
@@ -585,36 +678,305 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
585
678
|
throw error; // Propagate unexpected errors
|
|
586
679
|
}
|
|
587
680
|
}
|
|
588
|
-
// Load memories for keyword matching
|
|
681
|
+
// Load memories for keyword matching, enriched with confidence
|
|
589
682
|
const allMemories = await readMemoriesFile(PATHS.memory);
|
|
590
|
-
const
|
|
591
|
-
return allMemories.filter((m) => {
|
|
592
|
-
const searchText = `${m.memory} ${m.reason}`.toLowerCase();
|
|
593
|
-
return queryTerms.some((term) => searchText.includes(term));
|
|
594
|
-
});
|
|
595
|
-
};
|
|
683
|
+
const allMemoriesWithConf = withConfidence(allMemories);
|
|
596
684
|
const queryTerms = query.toLowerCase().split(/\s+/);
|
|
597
|
-
const relatedMemories =
|
|
685
|
+
const relatedMemories = allMemoriesWithConf
|
|
686
|
+
.filter((m) => {
|
|
687
|
+
const searchText = `${m.memory} ${m.reason}`.toLowerCase();
|
|
688
|
+
return queryTerms.some((term) => searchText.includes(term));
|
|
689
|
+
})
|
|
690
|
+
.sort((a, b) => b.effectiveConfidence - a.effectiveConfidence);
|
|
691
|
+
// Load intelligence data for enrichment (all intents, not just preflight)
|
|
692
|
+
let intelligence = null;
|
|
693
|
+
try {
|
|
694
|
+
const intelligenceContent = await fs.readFile(PATHS.intelligence, 'utf-8');
|
|
695
|
+
intelligence = JSON.parse(intelligenceContent);
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
/* graceful degradation — intelligence file may not exist yet */
|
|
699
|
+
}
|
|
700
|
+
// Build reverse import map from intelligence graph
|
|
701
|
+
const reverseImports = new Map();
|
|
702
|
+
if (intelligence?.internalFileGraph?.imports) {
|
|
703
|
+
for (const [file, deps] of Object.entries(intelligence.internalFileGraph.imports)) {
|
|
704
|
+
for (const dep of deps) {
|
|
705
|
+
if (!reverseImports.has(dep))
|
|
706
|
+
reverseImports.set(dep, []);
|
|
707
|
+
reverseImports.get(dep).push(file);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Load git dates for lastModified enrichment
|
|
712
|
+
let gitDates = null;
|
|
713
|
+
try {
|
|
714
|
+
gitDates = await getFileCommitDates(ROOT_PATH);
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
/* not a git repo */
|
|
718
|
+
}
|
|
719
|
+
// Enrich a search result with relationship data
|
|
720
|
+
function enrichResult(r) {
|
|
721
|
+
const rPath = r.filePath;
|
|
722
|
+
// importedBy: files that import this result (reverse lookup)
|
|
723
|
+
const importedBy = [];
|
|
724
|
+
for (const [dep, importers] of reverseImports) {
|
|
725
|
+
if (dep.endsWith(rPath) || rPath.endsWith(dep)) {
|
|
726
|
+
importedBy.push(...importers);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// imports: files this result depends on (forward lookup)
|
|
730
|
+
const imports = [];
|
|
731
|
+
if (intelligence?.internalFileGraph?.imports) {
|
|
732
|
+
for (const [file, deps] of Object.entries(intelligence.internalFileGraph.imports)) {
|
|
733
|
+
if (file.endsWith(rPath) || rPath.endsWith(file)) {
|
|
734
|
+
imports.push(...deps);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// testedIn: heuristic — same basename with .spec/.test extension
|
|
739
|
+
const testedIn = [];
|
|
740
|
+
const baseName = path.basename(rPath).replace(/\.[^.]+$/, '');
|
|
741
|
+
if (intelligence?.internalFileGraph?.imports) {
|
|
742
|
+
for (const file of Object.keys(intelligence.internalFileGraph.imports)) {
|
|
743
|
+
const fileBase = path.basename(file);
|
|
744
|
+
if ((fileBase.includes('.spec.') || fileBase.includes('.test.')) &&
|
|
745
|
+
fileBase.startsWith(baseName)) {
|
|
746
|
+
testedIn.push(file);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// lastModified: from git dates
|
|
751
|
+
let lastModified;
|
|
752
|
+
if (gitDates) {
|
|
753
|
+
// Try matching by relative path (git dates use repo-relative forward-slash paths)
|
|
754
|
+
const relPath = path.relative(ROOT_PATH, rPath).replace(/\\/g, '/');
|
|
755
|
+
const date = gitDates.get(relPath);
|
|
756
|
+
if (date) {
|
|
757
|
+
lastModified = date.toISOString();
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// Only return if we have at least one piece of data
|
|
761
|
+
if (importedBy.length === 0 &&
|
|
762
|
+
imports.length === 0 &&
|
|
763
|
+
testedIn.length === 0 &&
|
|
764
|
+
!lastModified) {
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
...(importedBy.length > 0 && { importedBy }),
|
|
769
|
+
...(imports.length > 0 && { imports }),
|
|
770
|
+
...(testedIn.length > 0 && { testedIn }),
|
|
771
|
+
...(lastModified && { lastModified })
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const searchQuality = assessSearchQuality(query, results);
|
|
775
|
+
// Compose preflight card for edit/refactor/migrate intents
|
|
776
|
+
let preflight = undefined;
|
|
777
|
+
const preflightIntents = ['edit', 'refactor', 'migrate'];
|
|
778
|
+
if (intent && preflightIntents.includes(intent) && intelligence) {
|
|
779
|
+
try {
|
|
780
|
+
// --- Avoid / Prefer patterns ---
|
|
781
|
+
const avoidPatterns = [];
|
|
782
|
+
const preferredPatterns = [];
|
|
783
|
+
const patterns = intelligence.patterns || {};
|
|
784
|
+
for (const [category, data] of Object.entries(patterns)) {
|
|
785
|
+
// Primary pattern = preferred if Rising or Stable
|
|
786
|
+
if (data.primary) {
|
|
787
|
+
const p = data.primary;
|
|
788
|
+
if (p.trend === 'Rising' || p.trend === 'Stable') {
|
|
789
|
+
preferredPatterns.push({
|
|
790
|
+
pattern: p.name,
|
|
791
|
+
category,
|
|
792
|
+
adoption: p.frequency,
|
|
793
|
+
trend: p.trend,
|
|
794
|
+
guidance: p.guidance,
|
|
795
|
+
...(p.canonicalExample && { example: p.canonicalExample.file })
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Also-detected patterns that are Declining = avoid
|
|
800
|
+
if (data.alsoDetected) {
|
|
801
|
+
for (const alt of data.alsoDetected) {
|
|
802
|
+
if (alt.trend === 'Declining') {
|
|
803
|
+
avoidPatterns.push({
|
|
804
|
+
pattern: alt.name,
|
|
805
|
+
category,
|
|
806
|
+
adoption: alt.frequency,
|
|
807
|
+
trend: 'Declining',
|
|
808
|
+
guidance: alt.guidance
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// --- Impact candidates (files importing the result files) ---
|
|
815
|
+
const impactCandidates = [];
|
|
816
|
+
const resultPaths = results.map((r) => r.filePath);
|
|
817
|
+
if (intelligence.internalFileGraph?.imports) {
|
|
818
|
+
const allImports = intelligence.internalFileGraph.imports;
|
|
819
|
+
for (const [file, deps] of Object.entries(allImports)) {
|
|
820
|
+
if (deps.some((dep) => resultPaths.some((rp) => dep.endsWith(rp) || rp.endsWith(dep)))) {
|
|
821
|
+
if (!resultPaths.some((rp) => file.endsWith(rp) || rp.endsWith(file))) {
|
|
822
|
+
impactCandidates.push(file);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// --- Risk level (based on circular deps + impact breadth) ---
|
|
828
|
+
let riskLevel = 'low';
|
|
829
|
+
let cycleCount = 0;
|
|
830
|
+
if (intelligence.internalFileGraph) {
|
|
831
|
+
try {
|
|
832
|
+
const graph = InternalFileGraph.fromJSON(intelligence.internalFileGraph, ROOT_PATH);
|
|
833
|
+
// Use directory prefixes as scope (not full file paths)
|
|
834
|
+
// findCycles(scope) filters files by startsWith, so a full path would only match itself
|
|
835
|
+
const scopes = new Set(resultPaths.map((rp) => {
|
|
836
|
+
const lastSlash = rp.lastIndexOf('/');
|
|
837
|
+
return lastSlash > 0 ? rp.substring(0, lastSlash + 1) : rp;
|
|
838
|
+
}));
|
|
839
|
+
for (const scope of scopes) {
|
|
840
|
+
const cycles = graph.findCycles(scope);
|
|
841
|
+
cycleCount += cycles.length;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
// Graph reconstruction failed — skip cycle check
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (cycleCount > 0 || impactCandidates.length > 10) {
|
|
849
|
+
riskLevel = 'high';
|
|
850
|
+
}
|
|
851
|
+
else if (impactCandidates.length > 3) {
|
|
852
|
+
riskLevel = 'medium';
|
|
853
|
+
}
|
|
854
|
+
// --- Golden files (exemplar code) ---
|
|
855
|
+
const goldenFiles = (intelligence.goldenFiles || []).slice(0, 3).map((g) => ({
|
|
856
|
+
file: g.file,
|
|
857
|
+
score: g.score
|
|
858
|
+
}));
|
|
859
|
+
// --- Confidence (index freshness) ---
|
|
860
|
+
let confidence = 'stale';
|
|
861
|
+
if (intelligence.generatedAt) {
|
|
862
|
+
const indexAge = Date.now() - new Date(intelligence.generatedAt).getTime();
|
|
863
|
+
const hoursOld = indexAge / (1000 * 60 * 60);
|
|
864
|
+
if (hoursOld < 24) {
|
|
865
|
+
confidence = 'fresh';
|
|
866
|
+
}
|
|
867
|
+
else if (hoursOld < 168) {
|
|
868
|
+
confidence = 'aging';
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// --- Failure memories (1.5x relevance boost) ---
|
|
872
|
+
const failureWarnings = relatedMemories
|
|
873
|
+
.filter((m) => m.type === 'failure' && !m.stale)
|
|
874
|
+
.map((m) => ({
|
|
875
|
+
memory: m.memory,
|
|
876
|
+
reason: m.reason,
|
|
877
|
+
confidence: m.effectiveConfidence
|
|
878
|
+
}))
|
|
879
|
+
.slice(0, 3);
|
|
880
|
+
const preferredPatternsForOutput = preferredPatterns.slice(0, 5);
|
|
881
|
+
const avoidPatternsForOutput = avoidPatterns.slice(0, 5);
|
|
882
|
+
// --- Pattern conflicts (split decisions within categories) ---
|
|
883
|
+
const patternConflicts = [];
|
|
884
|
+
const hasUnitTestFramework = Boolean(patterns.unitTestFramework?.primary);
|
|
885
|
+
for (const [cat, data] of Object.entries(patterns)) {
|
|
886
|
+
if (shouldSkipLegacyTestingFrameworkCategory(cat, patterns))
|
|
887
|
+
continue;
|
|
888
|
+
if (!shouldIncludePatternConflictCategory(cat, query))
|
|
889
|
+
continue;
|
|
890
|
+
if (!data.primary || !data.alsoDetected?.length)
|
|
891
|
+
continue;
|
|
892
|
+
const primaryFreq = parseFloat(data.primary.frequency) || 100;
|
|
893
|
+
if (primaryFreq >= 80)
|
|
894
|
+
continue;
|
|
895
|
+
for (const alt of data.alsoDetected) {
|
|
896
|
+
const altFreq = parseFloat(alt.frequency) || 0;
|
|
897
|
+
if (altFreq >= 20) {
|
|
898
|
+
if (isComplementaryPatternConflict(cat, data.primary.name, alt.name))
|
|
899
|
+
continue;
|
|
900
|
+
if (hasUnitTestFramework && cat === 'testingFramework')
|
|
901
|
+
continue;
|
|
902
|
+
patternConflicts.push({
|
|
903
|
+
category: cat,
|
|
904
|
+
primary: { name: data.primary.name, adoption: data.primary.frequency },
|
|
905
|
+
alternative: { name: alt.name, adoption: alt.frequency }
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const evidenceLock = buildEvidenceLock({
|
|
911
|
+
results,
|
|
912
|
+
preferredPatterns: preferredPatternsForOutput,
|
|
913
|
+
relatedMemories,
|
|
914
|
+
failureWarnings,
|
|
915
|
+
patternConflicts
|
|
916
|
+
});
|
|
917
|
+
// Bump risk if there are active failure memories for this area
|
|
918
|
+
if (failureWarnings.length > 0 && riskLevel === 'low') {
|
|
919
|
+
riskLevel = 'medium';
|
|
920
|
+
}
|
|
921
|
+
// If evidence triangulation is weak, avoid claiming low risk
|
|
922
|
+
if (evidenceLock.status === 'block' && riskLevel === 'low') {
|
|
923
|
+
riskLevel = 'medium';
|
|
924
|
+
}
|
|
925
|
+
// If epistemic stress says abstain, bump risk
|
|
926
|
+
if (evidenceLock.epistemicStress?.abstain && riskLevel === 'low') {
|
|
927
|
+
riskLevel = 'medium';
|
|
928
|
+
}
|
|
929
|
+
preflight = {
|
|
930
|
+
intent,
|
|
931
|
+
riskLevel,
|
|
932
|
+
confidence,
|
|
933
|
+
evidenceLock,
|
|
934
|
+
...(preferredPatternsForOutput.length > 0 && {
|
|
935
|
+
preferredPatterns: preferredPatternsForOutput
|
|
936
|
+
}),
|
|
937
|
+
...(avoidPatternsForOutput.length > 0 && {
|
|
938
|
+
avoidPatterns: avoidPatternsForOutput
|
|
939
|
+
}),
|
|
940
|
+
...(goldenFiles.length > 0 && { goldenFiles }),
|
|
941
|
+
...(impactCandidates.length > 0 && {
|
|
942
|
+
impactCandidates: impactCandidates.slice(0, 10)
|
|
943
|
+
}),
|
|
944
|
+
...(cycleCount > 0 && { circularDependencies: cycleCount }),
|
|
945
|
+
...(failureWarnings.length > 0 && { failureWarnings })
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
catch {
|
|
949
|
+
// Preflight construction failed — skip preflight, don't fail the search
|
|
950
|
+
}
|
|
951
|
+
}
|
|
598
952
|
return {
|
|
599
953
|
content: [
|
|
600
954
|
{
|
|
601
955
|
type: 'text',
|
|
602
956
|
text: JSON.stringify({
|
|
603
957
|
status: 'success',
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
958
|
+
...(preflight && { preflight }),
|
|
959
|
+
searchQuality,
|
|
960
|
+
results: results.map((r) => {
|
|
961
|
+
const relationships = enrichResult(r);
|
|
962
|
+
return {
|
|
963
|
+
summary: r.summary,
|
|
964
|
+
snippet: r.snippet,
|
|
965
|
+
filePath: `${r.filePath}:${r.startLine}-${r.endLine}`,
|
|
966
|
+
score: r.score,
|
|
967
|
+
relevanceReason: r.relevanceReason,
|
|
968
|
+
componentType: r.componentType,
|
|
969
|
+
layer: r.layer,
|
|
970
|
+
framework: r.framework,
|
|
971
|
+
trend: r.trend,
|
|
972
|
+
patternWarning: r.patternWarning,
|
|
973
|
+
...(relationships && { relationships })
|
|
974
|
+
};
|
|
975
|
+
}),
|
|
616
976
|
totalResults: results.length,
|
|
617
|
-
...(relatedMemories.length > 0 && {
|
|
977
|
+
...(relatedMemories.length > 0 && {
|
|
978
|
+
relatedMemories: relatedMemories.slice(0, 5)
|
|
979
|
+
})
|
|
618
980
|
}, null, 2)
|
|
619
981
|
}
|
|
620
982
|
]
|
|
@@ -635,7 +997,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
635
997
|
totalFiles: indexState.stats.totalFiles,
|
|
636
998
|
indexedFiles: indexState.stats.indexedFiles,
|
|
637
999
|
totalChunks: indexState.stats.totalChunks,
|
|
638
|
-
duration: `${(indexState.stats.duration / 1000).toFixed(2)}s
|
|
1000
|
+
duration: `${(indexState.stats.duration / 1000).toFixed(2)}s`,
|
|
1001
|
+
incremental: indexState.stats.incremental
|
|
639
1002
|
}
|
|
640
1003
|
: undefined,
|
|
641
1004
|
progress: progress
|
|
@@ -657,10 +1020,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
657
1020
|
const { reason, incrementalOnly } = args;
|
|
658
1021
|
const mode = incrementalOnly ? 'incremental' : 'full';
|
|
659
1022
|
console.error(`Refresh requested (${mode}): ${reason || 'Manual trigger'}`);
|
|
660
|
-
|
|
661
|
-
// use `incrementalOnly` to only re-index changed files.
|
|
662
|
-
// For now, always do full re-index but acknowledge the intention.
|
|
663
|
-
performIndexing();
|
|
1023
|
+
performIndexing(incrementalOnly);
|
|
664
1024
|
return {
|
|
665
1025
|
content: [
|
|
666
1026
|
{
|
|
@@ -669,12 +1029,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
669
1029
|
status: 'started',
|
|
670
1030
|
mode,
|
|
671
1031
|
message: incrementalOnly
|
|
672
|
-
? 'Incremental re-indexing
|
|
1032
|
+
? 'Incremental re-indexing started. Only changed files will be re-embedded.'
|
|
673
1033
|
: 'Full re-indexing started. Check status with get_indexing_status.',
|
|
674
|
-
reason
|
|
675
|
-
note: incrementalOnly
|
|
676
|
-
? 'Incremental mode requested. Full re-index for now; true incremental indexing coming in Phase 2.'
|
|
677
|
-
: undefined
|
|
1034
|
+
reason
|
|
678
1035
|
}, null, 2)
|
|
679
1036
|
}
|
|
680
1037
|
]
|
|
@@ -828,6 +1185,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
828
1185
|
result.stateManagement = intelligence.patterns?.stateManagement;
|
|
829
1186
|
}
|
|
830
1187
|
else if (category === 'testing') {
|
|
1188
|
+
result.unitTestFramework = intelligence.patterns?.unitTestFramework;
|
|
1189
|
+
result.e2eFramework = intelligence.patterns?.e2eFramework;
|
|
831
1190
|
result.testingFramework = intelligence.patterns?.testingFramework;
|
|
832
1191
|
result.testMocking = intelligence.patterns?.testMocking;
|
|
833
1192
|
}
|
|
@@ -857,6 +1216,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
857
1216
|
catch (_error) {
|
|
858
1217
|
// No memory file yet, that's fine - don't fail the whole request
|
|
859
1218
|
}
|
|
1219
|
+
// Detect pattern conflicts: primary < 80% and any alternative > 20%
|
|
1220
|
+
const conflicts = [];
|
|
1221
|
+
const patternsData = intelligence.patterns || {};
|
|
1222
|
+
const hasUnitTestFramework = Boolean(patternsData.unitTestFramework?.primary);
|
|
1223
|
+
for (const [cat, data] of Object.entries(patternsData)) {
|
|
1224
|
+
if (shouldSkipLegacyTestingFrameworkCategory(cat, patternsData))
|
|
1225
|
+
continue;
|
|
1226
|
+
if (category && category !== 'all' && cat !== category)
|
|
1227
|
+
continue;
|
|
1228
|
+
if (!data.primary || !data.alsoDetected?.length)
|
|
1229
|
+
continue;
|
|
1230
|
+
const primaryFreq = parseFloat(data.primary.frequency) || 100;
|
|
1231
|
+
if (primaryFreq >= 80)
|
|
1232
|
+
continue;
|
|
1233
|
+
for (const alt of data.alsoDetected) {
|
|
1234
|
+
const altFreq = parseFloat(alt.frequency) || 0;
|
|
1235
|
+
if (altFreq < 20)
|
|
1236
|
+
continue;
|
|
1237
|
+
if (isComplementaryPatternConflict(cat, data.primary.name, alt.name))
|
|
1238
|
+
continue;
|
|
1239
|
+
if (hasUnitTestFramework && cat === 'testingFramework')
|
|
1240
|
+
continue;
|
|
1241
|
+
conflicts.push({
|
|
1242
|
+
category: cat,
|
|
1243
|
+
primary: {
|
|
1244
|
+
name: data.primary.name,
|
|
1245
|
+
adoption: data.primary.frequency,
|
|
1246
|
+
trend: data.primary.trend
|
|
1247
|
+
},
|
|
1248
|
+
alternative: {
|
|
1249
|
+
name: alt.name,
|
|
1250
|
+
adoption: alt.frequency,
|
|
1251
|
+
trend: alt.trend
|
|
1252
|
+
},
|
|
1253
|
+
note: `Split decision: ${data.primary.frequency} ${data.primary.name} (${data.primary.trend || 'unknown'}) vs ${alt.frequency} ${alt.name} (${alt.trend || 'unknown'})`
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (conflicts.length > 0) {
|
|
1258
|
+
result.conflicts = conflicts;
|
|
1259
|
+
}
|
|
860
1260
|
return {
|
|
861
1261
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
862
1262
|
};
|
|
@@ -1099,19 +1499,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1099
1499
|
}
|
|
1100
1500
|
const filtered = filterMemories(allMemories, { category, type, query });
|
|
1101
1501
|
const limited = applyUnfilteredLimit(filtered, { category, type, query }, 20);
|
|
1502
|
+
// Enrich with confidence decay
|
|
1503
|
+
const enriched = withConfidence(limited.memories);
|
|
1504
|
+
const staleCount = enriched.filter((m) => m.stale).length;
|
|
1102
1505
|
return {
|
|
1103
1506
|
content: [
|
|
1104
1507
|
{
|
|
1105
1508
|
type: 'text',
|
|
1106
1509
|
text: JSON.stringify({
|
|
1107
1510
|
status: 'success',
|
|
1108
|
-
count:
|
|
1511
|
+
count: enriched.length,
|
|
1109
1512
|
totalCount: limited.totalCount,
|
|
1110
1513
|
truncated: limited.truncated,
|
|
1514
|
+
...(staleCount > 0 && {
|
|
1515
|
+
staleCount,
|
|
1516
|
+
staleNote: `${staleCount} memor${staleCount === 1 ? 'y' : 'ies'} below 30% confidence. Consider reviewing or removing.`
|
|
1517
|
+
}),
|
|
1111
1518
|
message: limited.truncated
|
|
1112
1519
|
? 'Showing 20 most recent. Use filters (category/type/query) for targeted results.'
|
|
1113
1520
|
: undefined,
|
|
1114
|
-
memories:
|
|
1521
|
+
memories: enriched
|
|
1115
1522
|
}, null, 2)
|
|
1116
1523
|
}
|
|
1117
1524
|
]
|