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.
Files changed (77) hide show
  1. package/README.md +193 -45
  2. package/dist/analyzers/generic/index.d.ts +0 -1
  3. package/dist/analyzers/generic/index.d.ts.map +1 -1
  4. package/dist/analyzers/generic/index.js +0 -13
  5. package/dist/analyzers/generic/index.js.map +1 -1
  6. package/dist/constants/codebase-context.d.ts +2 -0
  7. package/dist/constants/codebase-context.d.ts.map +1 -1
  8. package/dist/constants/codebase-context.js +2 -0
  9. package/dist/constants/codebase-context.js.map +1 -1
  10. package/dist/constants/git-patterns.d.ts +12 -0
  11. package/dist/constants/git-patterns.d.ts.map +1 -0
  12. package/dist/constants/git-patterns.js +11 -0
  13. package/dist/constants/git-patterns.js.map +1 -0
  14. package/dist/core/analyzer-registry.d.ts.map +1 -1
  15. package/dist/core/analyzer-registry.js +3 -1
  16. package/dist/core/analyzer-registry.js.map +1 -1
  17. package/dist/core/indexer.d.ts +2 -0
  18. package/dist/core/indexer.d.ts.map +1 -1
  19. package/dist/core/indexer.js +155 -20
  20. package/dist/core/indexer.js.map +1 -1
  21. package/dist/core/manifest.d.ts +39 -0
  22. package/dist/core/manifest.d.ts.map +1 -0
  23. package/dist/core/manifest.js +86 -0
  24. package/dist/core/manifest.js.map +1 -0
  25. package/dist/core/search-quality.d.ts +10 -0
  26. package/dist/core/search-quality.d.ts.map +1 -0
  27. package/dist/core/search-quality.js +64 -0
  28. package/dist/core/search-quality.js.map +1 -0
  29. package/dist/core/search.d.ts +16 -0
  30. package/dist/core/search.d.ts.map +1 -1
  31. package/dist/core/search.js +251 -56
  32. package/dist/core/search.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +460 -53
  36. package/dist/index.js.map +1 -1
  37. package/dist/memory/git-memory.d.ts +9 -0
  38. package/dist/memory/git-memory.d.ts.map +1 -0
  39. package/dist/memory/git-memory.js +51 -0
  40. package/dist/memory/git-memory.js.map +1 -0
  41. package/dist/memory/store.d.ts +16 -0
  42. package/dist/memory/store.d.ts.map +1 -1
  43. package/dist/memory/store.js +40 -1
  44. package/dist/memory/store.js.map +1 -1
  45. package/dist/patterns/semantics.d.ts +4 -0
  46. package/dist/patterns/semantics.d.ts.map +1 -0
  47. package/dist/patterns/semantics.js +24 -0
  48. package/dist/patterns/semantics.js.map +1 -0
  49. package/dist/preflight/evidence-lock.d.ts +50 -0
  50. package/dist/preflight/evidence-lock.d.ts.map +1 -0
  51. package/dist/preflight/evidence-lock.js +130 -0
  52. package/dist/preflight/evidence-lock.js.map +1 -0
  53. package/dist/preflight/query-scope.d.ts +3 -0
  54. package/dist/preflight/query-scope.d.ts.map +1 -0
  55. package/dist/preflight/query-scope.js +40 -0
  56. package/dist/preflight/query-scope.js.map +1 -0
  57. package/dist/resources/uri.d.ts +5 -0
  58. package/dist/resources/uri.d.ts.map +1 -0
  59. package/dist/resources/uri.js +15 -0
  60. package/dist/resources/uri.js.map +1 -0
  61. package/dist/storage/lancedb.d.ts +1 -0
  62. package/dist/storage/lancedb.d.ts.map +1 -1
  63. package/dist/storage/lancedb.js +24 -3
  64. package/dist/storage/lancedb.js.map +1 -1
  65. package/dist/storage/types.d.ts +5 -0
  66. package/dist/storage/types.d.ts.map +1 -1
  67. package/dist/storage/types.js.map +1 -1
  68. package/dist/types/index.d.ts +21 -1
  69. package/dist/types/index.d.ts.map +1 -1
  70. package/dist/utils/git-dates.d.ts +1 -0
  71. package/dist/utils/git-dates.d.ts.map +1 -1
  72. package/dist/utils/git-dates.js +20 -0
  73. package/dist/utils/git-dates.js.map +1 -1
  74. package/dist/utils/usage-tracker.d.ts.map +1 -1
  75. package/dist/utils/usage-tracker.js +3 -6
  76. package/dist/utils/usage-tracker.js.map +1 -1
  77. 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: '1.4.0'
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: 'codebase://context',
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(intelligence.patterns)) {
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 (patternData.alsoDetected?.length) {
415
- const alt = patternData.alsoDetected[0];
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 (patternData.alsoDetected?.length) {
423
- lines.push(` → Also detected: ${patternData.alsoDetected[0].name} (${patternData.alsoDetected[0].frequency})`);
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 (patternData.alsoDetected?.length) {
432
- for (const alt of patternData.alsoDetected.slice(0, 2)) {
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 === 'codebase://context') {
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
- async function performIndexing() {
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
- console.error(`Indexing: ${ROOT_PATH}`);
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 findRelatedMemories = (queryTerms) => {
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 = findRelatedMemories(queryTerms);
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
- results: results.map((r) => ({
605
- summary: r.summary,
606
- snippet: r.snippet,
607
- filePath: `${r.filePath}:${r.startLine}-${r.endLine}`,
608
- score: r.score,
609
- relevanceReason: r.relevanceReason,
610
- componentType: r.componentType,
611
- layer: r.layer,
612
- framework: r.framework,
613
- trend: r.trend,
614
- patternWarning: r.patternWarning
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 && { relatedMemories })
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
- // TODO: When incremental indexing is implemented (Phase 2),
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 requested. Check status with get_indexing_status.'
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: limited.memories.length,
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: limited.memories
1521
+ memories: enriched
1115
1522
  }, null, 2)
1116
1523
  }
1117
1524
  ]