codebase-context 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +149 -90
  2. package/dist/analyzers/angular/index.d.ts.map +1 -1
  3. package/dist/analyzers/angular/index.js +85 -39
  4. package/dist/analyzers/angular/index.js.map +1 -1
  5. package/dist/analyzers/generic/index.d.ts.map +1 -1
  6. package/dist/analyzers/generic/index.js +5 -4
  7. package/dist/analyzers/generic/index.js.map +1 -1
  8. package/dist/cli-formatters.d.ts +47 -0
  9. package/dist/cli-formatters.d.ts.map +1 -0
  10. package/dist/cli-formatters.js +803 -0
  11. package/dist/cli-formatters.js.map +1 -0
  12. package/dist/cli-memory.d.ts +5 -0
  13. package/dist/cli-memory.d.ts.map +1 -0
  14. package/dist/cli-memory.js +218 -0
  15. package/dist/cli-memory.js.map +1 -0
  16. package/dist/cli.d.ts +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +168 -178
  19. package/dist/cli.js.map +1 -1
  20. package/dist/core/auto-refresh.d.ts +16 -0
  21. package/dist/core/auto-refresh.d.ts.map +1 -0
  22. package/dist/core/auto-refresh.js +25 -0
  23. package/dist/core/auto-refresh.js.map +1 -0
  24. package/dist/core/file-watcher.d.ts +15 -0
  25. package/dist/core/file-watcher.d.ts.map +1 -0
  26. package/dist/core/file-watcher.js +59 -0
  27. package/dist/core/file-watcher.js.map +1 -0
  28. package/dist/core/index-meta.d.ts +3 -0
  29. package/dist/core/index-meta.d.ts.map +1 -1
  30. package/dist/core/index-meta.js +9 -1
  31. package/dist/core/index-meta.js.map +1 -1
  32. package/dist/core/indexer.d.ts.map +1 -1
  33. package/dist/core/indexer.js +74 -15
  34. package/dist/core/indexer.js.map +1 -1
  35. package/dist/core/reranker.d.ts.map +1 -1
  36. package/dist/core/reranker.js +3 -0
  37. package/dist/core/reranker.js.map +1 -1
  38. package/dist/core/search-quality.js +2 -2
  39. package/dist/core/search-quality.js.map +1 -1
  40. package/dist/core/search.d.ts.map +1 -1
  41. package/dist/core/search.js +20 -7
  42. package/dist/core/search.js.map +1 -1
  43. package/dist/core/symbol-references.d.ts +2 -3
  44. package/dist/core/symbol-references.d.ts.map +1 -1
  45. package/dist/core/symbol-references.js +111 -16
  46. package/dist/core/symbol-references.js.map +1 -1
  47. package/dist/embeddings/index.d.ts +8 -0
  48. package/dist/embeddings/index.d.ts.map +1 -1
  49. package/dist/embeddings/index.js +17 -2
  50. package/dist/embeddings/index.js.map +1 -1
  51. package/dist/embeddings/openai.d.ts +1 -1
  52. package/dist/embeddings/openai.d.ts.map +1 -1
  53. package/dist/embeddings/openai.js +3 -1
  54. package/dist/embeddings/openai.js.map +1 -1
  55. package/dist/embeddings/transformers.d.ts +6 -0
  56. package/dist/embeddings/transformers.d.ts.map +1 -1
  57. package/dist/embeddings/transformers.js +12 -5
  58. package/dist/embeddings/transformers.js.map +1 -1
  59. package/dist/embeddings/types.d.ts +1 -0
  60. package/dist/embeddings/types.d.ts.map +1 -1
  61. package/dist/embeddings/types.js +7 -1
  62. package/dist/embeddings/types.js.map +1 -1
  63. package/dist/grammars/manifest.d.ts.map +1 -1
  64. package/dist/grammars/manifest.js +2 -1
  65. package/dist/grammars/manifest.js.map +1 -1
  66. package/dist/index.d.ts +5 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +46 -3
  69. package/dist/index.js.map +1 -1
  70. package/dist/patterns/semantics.d.ts +2 -1
  71. package/dist/patterns/semantics.d.ts.map +1 -1
  72. package/dist/patterns/semantics.js +0 -2
  73. package/dist/patterns/semantics.js.map +1 -1
  74. package/dist/storage/index.d.ts +4 -1
  75. package/dist/storage/index.d.ts.map +1 -1
  76. package/dist/storage/index.js +2 -2
  77. package/dist/storage/index.js.map +1 -1
  78. package/dist/storage/lancedb.d.ts +2 -0
  79. package/dist/storage/lancedb.d.ts.map +1 -1
  80. package/dist/storage/lancedb.js +20 -4
  81. package/dist/storage/lancedb.js.map +1 -1
  82. package/dist/storage/types.d.ts +4 -1
  83. package/dist/storage/types.d.ts.map +1 -1
  84. package/dist/storage/types.js.map +1 -1
  85. package/dist/tools/detect-circular-dependencies.d.ts.map +1 -1
  86. package/dist/tools/detect-circular-dependencies.js.map +1 -1
  87. package/dist/tools/get-team-patterns.d.ts.map +1 -1
  88. package/dist/tools/get-team-patterns.js +30 -14
  89. package/dist/tools/get-team-patterns.js.map +1 -1
  90. package/dist/tools/search-codebase.d.ts.map +1 -1
  91. package/dist/tools/search-codebase.js +296 -189
  92. package/dist/tools/search-codebase.js.map +1 -1
  93. package/dist/tools/types.d.ts +193 -1
  94. package/dist/tools/types.d.ts.map +1 -1
  95. package/dist/types/index.d.ts +73 -11
  96. package/dist/types/index.d.ts.map +1 -1
  97. package/dist/types/index.js +0 -1
  98. package/dist/types/index.js.map +1 -1
  99. package/dist/utils/language-detection.d.ts.map +1 -1
  100. package/dist/utils/language-detection.js +6 -1
  101. package/dist/utils/language-detection.js.map +1 -1
  102. package/dist/utils/tree-sitter.d.ts +11 -0
  103. package/dist/utils/tree-sitter.d.ts.map +1 -1
  104. package/dist/utils/tree-sitter.js +111 -0
  105. package/dist/utils/tree-sitter.js.map +1 -1
  106. package/dist/utils/usage-tracker.d.ts +30 -40
  107. package/dist/utils/usage-tracker.d.ts.map +1 -1
  108. package/dist/utils/usage-tracker.js +66 -8
  109. package/dist/utils/usage-tracker.js.map +1 -1
  110. package/docs/capabilities.md +22 -8
  111. package/docs/cli.md +196 -0
  112. package/grammars/tree-sitter-kotlin.wasm +0 -0
  113. package/package.json +6 -4
  114. package/dist/tools/get-component-usage.d.ts +0 -5
  115. package/dist/tools/get-component-usage.d.ts.map +0 -1
  116. package/dist/tools/get-component-usage.js +0 -83
  117. package/dist/tools/get-component-usage.js.map +0 -1
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
1
  import { promises as fs } from 'fs';
3
2
  import path from 'path';
4
3
  import { CodebaseSearcher } from '../core/search.js';
@@ -115,7 +114,7 @@ export async function handle(args, ctx) {
115
114
  }
116
115
  const searcher = new CodebaseSearcher(ctx.rootPath);
117
116
  let results;
118
- const searchProfile = intent && ['explore', 'edit', 'refactor', 'migrate'].includes(intent) ? intent : 'explore';
117
+ const searchProfile = (intent && ['explore', 'edit', 'refactor', 'migrate'].includes(intent) ? intent : 'explore');
119
118
  try {
120
119
  results = await searcher.search(queryStr, limit || 5, filters, {
121
120
  profile: searchProfile
@@ -180,7 +179,10 @@ export async function handle(args, ctx) {
180
179
  let intelligence = null;
181
180
  try {
182
181
  const intelligenceContent = await fs.readFile(ctx.paths.intelligence, 'utf-8');
183
- intelligence = JSON.parse(intelligenceContent);
182
+ const parsed = JSON.parse(intelligenceContent);
183
+ if (typeof parsed === 'object' && parsed !== null) {
184
+ intelligence = parsed;
185
+ }
184
186
  }
185
187
  catch {
186
188
  /* graceful degradation — intelligence file may not exist yet */
@@ -190,7 +192,10 @@ export async function handle(args, ctx) {
190
192
  try {
191
193
  const relationshipsPath = path.join(path.dirname(ctx.paths.intelligence), RELATIONSHIPS_FILENAME);
192
194
  const relationshipsContent = await fs.readFile(relationshipsPath, 'utf-8');
193
- relationships = JSON.parse(relationshipsContent);
195
+ const parsed = JSON.parse(relationshipsContent);
196
+ if (typeof parsed === 'object' && parsed !== null) {
197
+ relationships = parsed;
198
+ }
194
199
  }
195
200
  catch {
196
201
  /* graceful degradation — relationships sidecar may not exist yet */
@@ -205,6 +210,29 @@ export async function handle(args, ctx) {
205
210
  }
206
211
  return null;
207
212
  }
213
+ function getImportDetailsGraph() {
214
+ if (relationships?.graph?.importDetails) {
215
+ return relationships.graph.importDetails;
216
+ }
217
+ const internalDetails = intelligence?.internalFileGraph?.importDetails;
218
+ if (internalDetails) {
219
+ return internalDetails;
220
+ }
221
+ return null;
222
+ }
223
+ function normalizeGraphPath(filePath) {
224
+ const normalized = filePath.replace(/\\/g, '/');
225
+ if (path.isAbsolute(filePath)) {
226
+ const rel = path.relative(ctx.rootPath, filePath).replace(/\\/g, '/');
227
+ if (rel && !rel.startsWith('..')) {
228
+ return rel;
229
+ }
230
+ }
231
+ return normalized.replace(/^\.\//, '');
232
+ }
233
+ function pathsMatch(a, b) {
234
+ return a === b || a.endsWith(b) || b.endsWith(a);
235
+ }
208
236
  function computeIndexConfidence() {
209
237
  let confidence = 'stale';
210
238
  if (intelligence?.generatedAt) {
@@ -219,20 +247,78 @@ export async function handle(args, ctx) {
219
247
  }
220
248
  return confidence;
221
249
  }
222
- // Cheap impact breadth estimate from the import graph (used for risk assessment).
250
+ function findImportDetail(details, importer, imported) {
251
+ if (!details)
252
+ return null;
253
+ const edges = details[importer];
254
+ if (!edges)
255
+ return null;
256
+ if (edges[imported])
257
+ return edges[imported];
258
+ let bestKey = null;
259
+ for (const depKey of Object.keys(edges)) {
260
+ if (!pathsMatch(depKey, imported))
261
+ continue;
262
+ if (!bestKey || depKey.length > bestKey.length) {
263
+ bestKey = depKey;
264
+ }
265
+ }
266
+ return bestKey ? edges[bestKey] : null;
267
+ }
268
+ // Impact breadth estimate from the import graph (used for risk assessment).
269
+ // 2-hop: direct importers (hop 1) + importers of importers (hop 2).
223
270
  function computeImpactCandidates(resultPaths) {
224
- const impactCandidates = [];
225
271
  const allImports = getImportsGraph();
226
272
  if (!allImports)
227
- return impactCandidates;
273
+ return [];
274
+ const importDetails = getImportDetailsGraph();
275
+ const reverseImportsLocal = new Map();
228
276
  for (const [file, deps] of Object.entries(allImports)) {
229
- if (deps.some((dep) => resultPaths.some((rp) => dep.endsWith(rp) || rp.endsWith(dep)))) {
230
- if (!resultPaths.some((rp) => file.endsWith(rp) || rp.endsWith(file))) {
231
- impactCandidates.push(file);
277
+ for (const dep of deps) {
278
+ if (!reverseImportsLocal.has(dep))
279
+ reverseImportsLocal.set(dep, []);
280
+ reverseImportsLocal.get(dep).push(file);
281
+ }
282
+ }
283
+ const targets = resultPaths.map((rp) => normalizeGraphPath(rp));
284
+ const candidates = new Map();
285
+ const addCandidate = (file, hop, line) => {
286
+ const existing = candidates.get(file);
287
+ if (existing) {
288
+ if (existing.hop <= hop)
289
+ return;
290
+ }
291
+ candidates.set(file, { file, hop, ...(line ? { line } : {}) });
292
+ };
293
+ const collectImporters = (target) => {
294
+ const matches = [];
295
+ for (const [dep, importers] of reverseImportsLocal) {
296
+ if (!pathsMatch(dep, target))
297
+ continue;
298
+ for (const importer of importers) {
299
+ matches.push({ importer, detail: findImportDetail(importDetails, importer, dep) });
232
300
  }
233
301
  }
302
+ return matches;
303
+ };
304
+ // Hop 1
305
+ const hop1Files = [];
306
+ for (const target of targets) {
307
+ for (const { importer, detail } of collectImporters(target)) {
308
+ addCandidate(importer, 1, detail?.line);
309
+ }
310
+ }
311
+ for (const candidate of candidates.values()) {
312
+ if (candidate.hop === 1)
313
+ hop1Files.push(candidate.file);
314
+ }
315
+ // Hop 2
316
+ for (const mid of hop1Files) {
317
+ for (const { importer, detail } of collectImporters(mid)) {
318
+ addCandidate(importer, 2, detail?.line);
319
+ }
234
320
  }
235
- return impactCandidates;
321
+ return Array.from(candidates.values()).slice(0, 20);
236
322
  }
237
323
  // Build reverse import map from relationships sidecar (preferred) or intelligence graph
238
324
  const reverseImports = new Map();
@@ -304,7 +390,7 @@ export async function handle(args, ctx) {
304
390
  }
305
391
  return output;
306
392
  }
307
- const searchQuality = assessSearchQuality(query, results);
393
+ const searchQuality = assessSearchQuality(queryStr, results);
308
394
  // Always-on edit preflight (lite): do not require intent and keep payload small.
309
395
  let editPreflight = undefined;
310
396
  if (intelligence && (!intent || intent === 'explore')) {
@@ -353,193 +439,206 @@ export async function handle(args, ctx) {
353
439
  // Compose preflight card for edit/refactor/migrate intents
354
440
  let preflight = undefined;
355
441
  const preflightIntents = ['edit', 'refactor', 'migrate'];
356
- if (intent && preflightIntents.includes(intent) && intelligence) {
357
- try {
358
- // --- Avoid / Prefer patterns ---
359
- const avoidPatternsList = [];
360
- const preferredPatternsList = [];
361
- const patterns = intelligence.patterns || {};
362
- for (const [category, data] of Object.entries(patterns)) {
363
- // Primary pattern = preferred if Rising or Stable
364
- if (data.primary) {
365
- const p = data.primary;
366
- if (p.trend === 'Rising' || p.trend === 'Stable') {
367
- preferredPatternsList.push({
368
- pattern: p.name,
369
- category,
370
- adoption: p.frequency,
371
- trend: p.trend,
372
- guidance: p.guidance,
373
- ...(p.canonicalExample && { example: p.canonicalExample.file })
374
- });
375
- }
376
- }
377
- // Also-detected patterns that are Declining = avoid
378
- if (data.alsoDetected) {
379
- for (const alt of data.alsoDetected) {
380
- if (alt.trend === 'Declining') {
381
- avoidPatternsList.push({
382
- pattern: alt.name,
442
+ if (intent && preflightIntents.includes(intent)) {
443
+ if (!intelligence) {
444
+ preflight = {
445
+ ready: false,
446
+ nextAction: 'Run a full index rebuild to generate pattern intelligence before editing.'
447
+ };
448
+ }
449
+ else {
450
+ try {
451
+ // --- Avoid / Prefer patterns ---
452
+ const avoidPatternsList = [];
453
+ const preferredPatternsList = [];
454
+ const patterns = intelligence.patterns || {};
455
+ for (const [category, data] of Object.entries(patterns)) {
456
+ // Primary pattern = preferred if Rising or Stable
457
+ if (data.primary) {
458
+ const p = data.primary;
459
+ if (p.trend === 'Rising' || p.trend === 'Stable') {
460
+ preferredPatternsList.push({
461
+ pattern: p.name,
383
462
  category,
384
- adoption: alt.frequency,
385
- trend: 'Declining',
386
- guidance: alt.guidance
463
+ adoption: p.frequency,
464
+ trend: p.trend,
465
+ guidance: p.guidance,
466
+ ...(p.canonicalExample && { example: p.canonicalExample.file })
387
467
  });
388
468
  }
389
469
  }
390
- }
391
- }
392
- // --- Impact candidates (files importing the result files) ---
393
- const resultPaths = results.map((r) => r.filePath);
394
- const impactCandidates = computeImpactCandidates(resultPaths);
395
- // PREF-02: Compute impact coverage (callers of result files that appear in results)
396
- const callerFiles = resultPaths.flatMap((p) => {
397
- const importers = [];
398
- for (const [dep, importerList] of reverseImports) {
399
- if (dep.endsWith(p) || p.endsWith(dep)) {
400
- importers.push(...importerList);
470
+ // Also-detected patterns that are Declining = avoid
471
+ if (data.alsoDetected) {
472
+ for (const alt of data.alsoDetected) {
473
+ if (alt.trend === 'Declining') {
474
+ avoidPatternsList.push({
475
+ pattern: alt.name,
476
+ category,
477
+ adoption: alt.frequency,
478
+ trend: 'Declining',
479
+ guidance: alt.guidance
480
+ });
481
+ }
482
+ }
401
483
  }
402
484
  }
403
- return importers;
404
- });
405
- const uniqueCallers = new Set(callerFiles);
406
- const callersCovered = Array.from(uniqueCallers).filter((f) => resultPaths.some((rp) => f.endsWith(rp) || rp.endsWith(f))).length;
407
- const callersTotal = uniqueCallers.size;
408
- const impactCoverage = callersTotal > 0 ? { covered: callersCovered, total: callersTotal } : undefined;
409
- // --- Risk level (based on circular deps + impact breadth) ---
410
- //TODO: Review this risk level calculation
411
- let _riskLevel = 'low';
412
- let cycleCount = 0;
413
- const graphDataSource = relationships?.graph || intelligence?.internalFileGraph;
414
- if (graphDataSource) {
415
- try {
416
- const graph = InternalFileGraph.fromJSON(graphDataSource, ctx.rootPath);
417
- // Use directory prefixes as scope (not full file paths)
418
- // findCycles(scope) filters files by startsWith, so a full path would only match itself
419
- const scopes = new Set(resultPaths.map((rp) => {
420
- const lastSlash = rp.lastIndexOf('/');
421
- return lastSlash > 0 ? rp.substring(0, lastSlash + 1) : rp;
422
- }));
423
- for (const scope of scopes) {
424
- const cycles = graph.findCycles(scope);
425
- cycleCount += cycles.length;
485
+ // --- Impact candidates (files importing the result files) ---
486
+ const resultPaths = results.map((r) => r.filePath);
487
+ const impactCandidates = computeImpactCandidates(resultPaths);
488
+ // PREF-02: Compute impact coverage (callers of result files that appear in results)
489
+ const callerFiles = resultPaths.flatMap((p) => {
490
+ const importers = [];
491
+ for (const [dep, importerList] of reverseImports) {
492
+ if (dep.endsWith(p) || p.endsWith(dep)) {
493
+ importers.push(...importerList);
494
+ }
495
+ }
496
+ return importers;
497
+ });
498
+ const uniqueCallers = new Set(callerFiles);
499
+ const callersCovered = Array.from(uniqueCallers).filter((f) => resultPaths.some((rp) => f.endsWith(rp) || rp.endsWith(f))).length;
500
+ const callersTotal = uniqueCallers.size;
501
+ const impactCoverage = callersTotal > 0 ? { covered: callersCovered, total: callersTotal } : undefined;
502
+ // --- Risk level (based on circular deps + impact breadth) ---
503
+ //TODO: Review this risk level calculation
504
+ let _riskLevel = 'low';
505
+ let cycleCount = 0;
506
+ const graphDataSource = relationships?.graph || intelligence?.internalFileGraph;
507
+ if (graphDataSource) {
508
+ try {
509
+ const graph = InternalFileGraph.fromJSON(graphDataSource, ctx.rootPath);
510
+ // Use directory prefixes as scope (not full file paths)
511
+ // findCycles(scope) filters files by startsWith, so a full path would only match itself
512
+ const scopes = new Set(resultPaths.map((rp) => {
513
+ const lastSlash = rp.lastIndexOf('/');
514
+ return lastSlash > 0 ? rp.substring(0, lastSlash + 1) : rp;
515
+ }));
516
+ for (const scope of scopes) {
517
+ const cycles = graph.findCycles(scope);
518
+ cycleCount += cycles.length;
519
+ }
520
+ }
521
+ catch {
522
+ // Graph reconstruction failed — skip cycle check
426
523
  }
427
524
  }
428
- catch {
429
- // Graph reconstruction failed — skip cycle check
525
+ if (cycleCount > 0 || impactCandidates.length > 10) {
526
+ _riskLevel = 'high';
430
527
  }
431
- }
432
- if (cycleCount > 0 || impactCandidates.length > 10) {
433
- _riskLevel = 'high';
434
- }
435
- else if (impactCandidates.length > 3) {
436
- _riskLevel = 'medium';
437
- }
438
- // --- Golden files (exemplar code) ---
439
- const goldenFiles = (intelligence.goldenFiles || []).slice(0, 3).map((g) => ({
440
- file: g.file,
441
- score: g.score
442
- }));
443
- // --- Confidence (index freshness) ---
444
- // TODO: Review this confidence calculation
445
- //const confidence = computeIndexConfidence();
446
- // --- Failure memories (1.5x relevance boost) ---
447
- const failureWarnings = relatedMemories
448
- .filter((m) => m.type === 'failure' && !m.stale)
449
- .map((m) => ({
450
- memory: m.memory,
451
- reason: m.reason,
452
- confidence: m.effectiveConfidence
453
- }))
454
- .slice(0, 3);
455
- const preferredPatternsForOutput = preferredPatternsList.slice(0, 5);
456
- const avoidPatternsForOutput = avoidPatternsList.slice(0, 5);
457
- // --- Pattern conflicts (split decisions within categories) ---
458
- const patternConflicts = [];
459
- const hasUnitTestFramework = Boolean(patterns.unitTestFramework?.primary);
460
- for (const [cat, data] of Object.entries(patterns)) {
461
- if (shouldSkipLegacyTestingFrameworkCategory(cat, patterns))
462
- continue;
463
- if (!shouldIncludePatternConflictCategory(cat, query))
464
- continue;
465
- if (!data.primary || !data.alsoDetected?.length)
466
- continue;
467
- const primaryFreq = parseFloat(data.primary.frequency) || 100;
468
- if (primaryFreq >= 80)
469
- continue;
470
- for (const alt of data.alsoDetected) {
471
- const altFreq = parseFloat(alt.frequency) || 0;
472
- if (altFreq >= 20) {
473
- if (isComplementaryPatternConflict(cat, data.primary.name, alt.name))
474
- continue;
475
- if (hasUnitTestFramework && cat === 'testingFramework')
476
- continue;
477
- patternConflicts.push({
478
- category: cat,
479
- primary: { name: data.primary.name, adoption: data.primary.frequency },
480
- alternative: { name: alt.name, adoption: alt.frequency }
481
- });
528
+ else if (impactCandidates.length > 3) {
529
+ _riskLevel = 'medium';
530
+ }
531
+ // --- Golden files (exemplar code) ---
532
+ const goldenFiles = (intelligence.goldenFiles ?? [])
533
+ .slice(0, 3)
534
+ .map((g) => ({
535
+ file: g.file,
536
+ score: g.score
537
+ }));
538
+ // --- Confidence (index freshness) ---
539
+ // TODO: Review this confidence calculation
540
+ //const confidence = computeIndexConfidence();
541
+ // --- Failure memories (1.5x relevance boost) ---
542
+ const failureWarnings = relatedMemories
543
+ .filter((m) => m.type === 'failure' && !m.stale)
544
+ .map((m) => ({
545
+ memory: m.memory,
546
+ reason: m.reason,
547
+ confidence: m.effectiveConfidence
548
+ }))
549
+ .slice(0, 3);
550
+ const preferredPatternsForOutput = preferredPatternsList.slice(0, 5);
551
+ const avoidPatternsForOutput = avoidPatternsList.slice(0, 5);
552
+ // --- Pattern conflicts (split decisions within categories) ---
553
+ const patternConflicts = [];
554
+ const hasUnitTestFramework = Boolean(patterns.unitTestFramework?.primary);
555
+ for (const [cat, data] of Object.entries(patterns)) {
556
+ if (shouldSkipLegacyTestingFrameworkCategory(cat, patterns))
557
+ continue;
558
+ if (!shouldIncludePatternConflictCategory(cat, queryStr))
559
+ continue;
560
+ if (!data.primary || !data.alsoDetected?.length)
561
+ continue;
562
+ const primaryFreq = parseFloat(data.primary.frequency) || 100;
563
+ if (primaryFreq >= 80)
564
+ continue;
565
+ for (const alt of data.alsoDetected) {
566
+ const altFreq = parseFloat(alt.frequency) || 0;
567
+ if (altFreq >= 20) {
568
+ if (isComplementaryPatternConflict(cat, data.primary.name, alt.name))
569
+ continue;
570
+ if (hasUnitTestFramework && cat === 'testingFramework')
571
+ continue;
572
+ patternConflicts.push({
573
+ category: cat,
574
+ primary: { name: data.primary.name, adoption: data.primary.frequency },
575
+ alternative: { name: alt.name, adoption: alt.frequency }
576
+ });
577
+ }
482
578
  }
483
579
  }
484
- }
485
- const evidenceLock = buildEvidenceLock({
486
- results,
487
- preferredPatterns: preferredPatternsForOutput,
488
- relatedMemories,
489
- failureWarnings,
490
- patternConflicts,
491
- searchQualityStatus: searchQuality.status,
492
- impactCoverage
493
- });
494
- const decisionCard = {
495
- ready: evidenceLock.readyToEdit
496
- };
497
- // Add nextAction if not ready
498
- if (!decisionCard.ready && evidenceLock.nextAction) {
499
- decisionCard.nextAction = evidenceLock.nextAction;
500
- }
501
- // Add warnings from failure memories (capped at 3)
502
- if (failureWarnings.length > 0) {
503
- decisionCard.warnings = failureWarnings.slice(0, 3).map((w) => w.memory);
504
- }
505
- // Add patterns (do/avoid, capped at 3 each, with adoption %)
506
- const doPatterns = preferredPatternsForOutput
507
- .slice(0, 3)
508
- .map((p) => `${p.pattern} — ${p.adoption ? ` ${p.adoption}% adoption` : ''}`);
509
- const avoidPatterns = avoidPatternsForOutput
510
- .slice(0, 3)
511
- .map((p) => `${p.pattern} — ${p.adoption ? ` ${p.adoption}% adoption` : ''} (declining)`);
512
- if (doPatterns.length > 0 || avoidPatterns.length > 0) {
513
- decisionCard.patterns = {
514
- ...(doPatterns.length > 0 && { do: doPatterns }),
515
- ...(avoidPatterns.length > 0 && { avoid: avoidPatterns })
580
+ const evidenceLock = buildEvidenceLock({
581
+ results,
582
+ preferredPatterns: preferredPatternsForOutput,
583
+ relatedMemories,
584
+ failureWarnings,
585
+ patternConflicts,
586
+ searchQualityStatus: searchQuality.status,
587
+ impactCoverage
588
+ });
589
+ // Build clean decision card (PREF-01 to PREF-04)
590
+ const decisionCard = {
591
+ ready: evidenceLock.readyToEdit
516
592
  };
517
- }
518
- // Add bestExample (top 1 golden file)
519
- if (goldenFiles.length > 0) {
520
- decisionCard.bestExample = `${goldenFiles[0].file}`;
521
- }
522
- // Add impact (coverage + top 3 files)
523
- if (impactCoverage || impactCandidates.length > 0) {
524
- const impactObj = {};
525
- if (impactCoverage) {
526
- impactObj.coverage = `${impactCoverage.covered}/${impactCoverage.total} callers in results`;
593
+ // Add nextAction if not ready
594
+ if (!decisionCard.ready && evidenceLock.nextAction) {
595
+ decisionCard.nextAction = evidenceLock.nextAction;
527
596
  }
528
- if (impactCandidates.length > 0) {
529
- impactObj.files = impactCandidates.slice(0, 3);
597
+ // Add warnings from failure memories (capped at 3)
598
+ if (failureWarnings.length > 0) {
599
+ decisionCard.warnings = failureWarnings.slice(0, 3).map((w) => w.memory);
530
600
  }
531
- if (Object.keys(impactObj).length > 0) {
532
- decisionCard.impact = impactObj;
601
+ // Add patterns (do/avoid, capped at 3 each, with adoption %)
602
+ const doPatterns = preferredPatternsForOutput
603
+ .slice(0, 3)
604
+ .map((p) => `${p.pattern} — ${p.adoption ? `${p.adoption} adoption` : ''}`);
605
+ const avoidPatterns = avoidPatternsForOutput
606
+ .slice(0, 3)
607
+ .map((p) => `${p.pattern} — ${p.adoption ? `${p.adoption} adoption` : ''} (declining)`);
608
+ if (doPatterns.length > 0 || avoidPatterns.length > 0) {
609
+ decisionCard.patterns = {
610
+ ...(doPatterns.length > 0 && { do: doPatterns }),
611
+ ...(avoidPatterns.length > 0 && { avoid: avoidPatterns })
612
+ };
613
+ }
614
+ // Add bestExample (top 1 golden file)
615
+ if (goldenFiles.length > 0) {
616
+ decisionCard.bestExample = `${goldenFiles[0].file}`;
617
+ }
618
+ // Add impact (coverage + top 3 files)
619
+ if (impactCoverage || impactCandidates.length > 0) {
620
+ const impactObj = {};
621
+ if (impactCoverage) {
622
+ impactObj.coverage = `${impactCoverage.covered}/${impactCoverage.total} callers in results`;
623
+ }
624
+ if (impactCandidates.length > 0) {
625
+ const top = impactCandidates.slice(0, 3);
626
+ impactObj.files = top.map((candidate) => candidate.file);
627
+ impactObj.details = top;
628
+ }
629
+ if (Object.keys(impactObj).length > 0) {
630
+ decisionCard.impact = impactObj;
631
+ }
632
+ }
633
+ // Add whatWouldHelp from evidenceLock
634
+ if (evidenceLock.whatWouldHelp && evidenceLock.whatWouldHelp.length > 0) {
635
+ decisionCard.whatWouldHelp = evidenceLock.whatWouldHelp;
533
636
  }
637
+ preflight = decisionCard;
534
638
  }
535
- // Add whatWouldHelp from evidenceLock
536
- if (evidenceLock.whatWouldHelp && evidenceLock.whatWouldHelp.length > 0) {
537
- decisionCard.whatWouldHelp = evidenceLock.whatWouldHelp;
639
+ catch {
640
+ // Preflight construction failed skip preflight, don't fail the search
538
641
  }
539
- preflight = decisionCard;
540
- }
541
- catch {
542
- // Preflight construction failed — skip preflight, don't fail the search
543
642
  }
544
643
  }
545
644
  // For edit/refactor/migrate: return clean decision card.
@@ -581,14 +680,20 @@ export async function handle(args, ctx) {
581
680
  }
582
681
  return null;
583
682
  }
584
- function enrichSnippetWithScope(snippet, metadata) {
683
+ function formatSnippetFallbackHeader(filePath, startLine) {
684
+ const rel = path.relative(ctx.rootPath, filePath).replace(/\\/g, '/');
685
+ const displayPath = rel && !rel.startsWith('..') && !path.isAbsolute(rel) ? rel : path.basename(filePath);
686
+ return `${displayPath}:${startLine}`;
687
+ }
688
+ function enrichSnippetWithScope(snippet, metadata, filePath, startLine) {
585
689
  if (!snippet)
586
690
  return undefined;
587
- const scopeHeader = buildScopeHeader(metadata);
588
- if (scopeHeader) {
589
- return `// ${scopeHeader}\n${snippet}`;
691
+ const cleanedSnippet = snippet.replace(/^\r?\n+/, '');
692
+ if (cleanedSnippet.startsWith('//')) {
693
+ return cleanedSnippet;
590
694
  }
591
- return snippet;
695
+ const scopeHeader = buildScopeHeader(metadata) ?? formatSnippetFallbackHeader(filePath, startLine);
696
+ return `// ${scopeHeader}\n${cleanedSnippet}`;
592
697
  }
593
698
  return {
594
699
  content: [
@@ -608,13 +713,15 @@ export async function handle(args, ctx) {
608
713
  results: results.map((r) => {
609
714
  const relationshipsAndHints = buildRelationshipHints(r);
610
715
  const enrichedSnippet = includeSnippets
611
- ? enrichSnippetWithScope(r.snippet, r.metadata)
716
+ ? enrichSnippetWithScope(r.snippet, r.metadata, r.filePath, r.startLine)
612
717
  : undefined;
613
718
  return {
614
719
  file: `${r.filePath}:${r.startLine}-${r.endLine}`,
615
720
  summary: r.summary,
616
721
  score: Math.round(r.score * 100) / 100,
617
- ...(r.componentType && r.layer && { type: `${r.componentType}:${r.layer}` }),
722
+ ...(r.componentType &&
723
+ r.layer &&
724
+ r.layer !== 'unknown' && { type: `${r.componentType}:${r.layer}` }),
618
725
  ...(r.trend && r.trend !== 'Stable' && { trend: r.trend }),
619
726
  ...(r.patternWarning && { patternWarning: r.patternWarning }),
620
727
  ...(relationshipsAndHints.relationships && {