cto-ai-cli 5.2.0 → 7.1.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.
@@ -1,5 +1,4 @@
1
1
  import { Project } from 'ts-morph';
2
- import { EventEmitter } from 'node:events';
3
2
 
4
3
  interface AnalyzedFile {
5
4
  path: string;
@@ -203,7 +202,6 @@ interface AdjacencyList {
203
202
  }
204
203
  declare function buildAdjacencyList(edges: GraphEdge[]): AdjacencyList;
205
204
  declare function bfsBidirectional(seeds: string[], adj: AdjacencyList, depth: number): Set<string>;
206
- declare function matchGlob(path: string, pattern: string): boolean;
207
205
 
208
206
  interface PolicySet {
209
207
  version: string;
@@ -220,634 +218,733 @@ interface PolicyRule {
220
218
  enabled: boolean;
221
219
  }
222
220
  type PolicyRuleType = 'include-always' | 'exclude-always' | 'budget-limit' | 'coverage-minimum' | 'risk-maximum' | 'secret-block';
221
+ interface SecretFinding {
222
+ type: SecretType;
223
+ file: string;
224
+ line: number;
225
+ match: string;
226
+ redacted: string;
227
+ severity: 'critical' | 'high' | 'medium' | 'low';
228
+ }
229
+ type SecretType = 'api-key' | 'aws-key' | 'private-key' | 'password' | 'token' | 'connection-string' | 'env-variable' | 'pii' | 'high-entropy' | 'custom';
223
230
 
224
- declare function getConfigPath(projectPath: string): string;
225
- declare function getPolicyPath(projectPath: string): string;
226
- declare function getCTODir(projectPath: string): string;
227
- declare function loadConfig(projectPath: string): Promise<CTOConfig>;
228
- declare function saveConfig(projectPath: string, config: CTOConfig): Promise<string>;
229
- declare function initProjectConfig(projectPath: string): Promise<{
230
- configPath: string;
231
- policyPath: string;
232
- created: string[];
233
- }>;
234
- declare function loadPolicyFromYAML(projectPath: string): Promise<PolicySet | null>;
235
-
236
- interface CacheOptions {
237
- maxAgeMs: number;
238
- maxEntries: number;
239
- enabled: boolean;
231
+ interface SemanticScore {
232
+ filePath: string;
233
+ score: number;
240
234
  }
235
+ interface LearnerBoostInput {
236
+ filePath: string;
237
+ boost: number;
238
+ }
239
+ interface SelectionInput {
240
+ task: string;
241
+ analysis: ProjectAnalysis;
242
+ budget: number;
243
+ policies?: PolicySet;
244
+ depth?: number;
245
+ semanticScores?: SemanticScore[];
246
+ learnerBoosts?: LearnerBoostInput[];
247
+ }
248
+ declare function selectContext(input: SelectionInput): Promise<ContextSelection>;
249
+
241
250
  /**
242
- * Get a project analysis, using cache when possible.
251
+ * TF-IDF Semantic Matching Engine
243
252
  *
244
- * Cache hit: ~1-5ms (fingerprint check only)
245
- * Cache miss: full analyzeProject() + cache update
246
- */
247
- declare function getCachedAnalysis(projectPath: string, config?: Partial<CTOConfig>): Promise<ProjectAnalysis>;
248
- /**
249
- * Invalidate cache for a specific project (e.g., after a known file change).
250
- */
251
- declare function invalidateCache(projectPath?: string): void;
252
- /**
253
- * Get cache statistics for debugging/monitoring.
254
- */
255
- declare function getCacheStats(): {
256
- entries: number;
257
- totalHits: number;
258
- projects: {
259
- path: string;
260
- hits: number;
261
- ageMs: number;
262
- }[];
263
- };
264
- /**
265
- * Configure cache behavior.
253
+ * Zero dependencies. Pure math.
254
+ *
255
+ * Computes term frequency–inverse document frequency vectors for all files
256
+ * in a project and matches task descriptions against file content semantically.
257
+ * This replaces naive keyword matching with real information retrieval.
258
+ *
259
+ * How it works:
260
+ * 1. Tokenize each file into terms (identifiers, words)
261
+ * 2. Compute TF (term frequency) per document
262
+ * 3. Compute IDF (inverse document frequency) across corpus
263
+ * 4. Score query against each document using cosine similarity
264
+ *
265
+ * Why this matters:
266
+ * - "fix authentication" matches `auth.ts` even though "authentication" ≠ "auth"
267
+ * because stemming normalizes both to "auth"
268
+ * - Files that mention rare, task-specific terms rank higher than files
269
+ * full of common terms like "import", "export", "function"
270
+ * - BM25 variant avoids over-weighting long files
266
271
  */
267
- declare function configureCache(options: Partial<CacheOptions>): void;
268
-
269
- interface WatcherOptions {
270
- debounceMs?: number;
271
- config?: Partial<CTOConfig>;
272
+ interface TfIdfIndex {
273
+ documents: Map<string, DocumentVector>;
274
+ idf: Map<string, number>;
275
+ avgDocLength: number;
276
+ totalDocs: number;
272
277
  }
273
- interface FileChangeEvent {
274
- type: 'add' | 'change' | 'unlink';
275
- path: string;
276
- timestamp: Date;
278
+ interface DocumentVector {
279
+ terms: Map<string, number>;
280
+ length: number;
277
281
  }
278
- declare class ProjectWatcher extends EventEmitter {
279
- private watcher;
280
- private projectPath;
281
- private debounceMs;
282
- private config;
283
- private debounceTimer;
284
- private pendingChanges;
285
- private running;
286
- constructor(projectPath: string, options?: WatcherOptions);
287
- start(): Promise<void>;
288
- stop(): Promise<void>;
289
- isRunning(): boolean;
290
- getProjectPath(): string;
291
- private handleEvent;
292
- private flush;
282
+ interface SemanticMatch {
283
+ filePath: string;
284
+ score: number;
285
+ matchedTerms: string[];
293
286
  }
294
287
  /**
295
- * Start watching a project. Returns the watcher instance.
296
- * If already watching, returns the existing watcher.
288
+ * Build a TF-IDF index from file contents.
289
+ * Call this once per project analysis, then query multiple times.
297
290
  */
298
- declare function watchProject(projectPath: string, options?: WatcherOptions): Promise<ProjectWatcher>;
291
+ declare function buildIndex(files: {
292
+ relativePath: string;
293
+ content: string;
294
+ }[]): TfIdfIndex;
299
295
  /**
300
- * Stop watching a specific project.
296
+ * Query the index with a task description.
297
+ * Returns files ranked by semantic relevance (BM25 scoring).
301
298
  */
302
- declare function unwatchProject(projectPath: string): Promise<void>;
299
+ declare function query(index: TfIdfIndex, taskDescription: string, maxResults?: number): SemanticMatch[];
303
300
  /**
304
- * Stop all active watchers. Call on process shutdown.
301
+ * Compute pairwise similarity between two documents in the index.
302
+ * Useful for finding related files (e.g., "what other files are similar to auth.ts?")
305
303
  */
306
- declare function unwatchAll(): Promise<void>;
304
+ declare function similarity(index: TfIdfIndex, pathA: string, pathB: string): number;
307
305
  /**
308
- * Get all actively watched projects.
306
+ * Tokenize source code into meaningful terms.
307
+ * Handles camelCase, snake_case, PascalCase splitting.
308
+ * Applies stemming and stop word removal.
309
309
  */
310
- declare function getActiveWatchers(): {
311
- path: string;
312
- running: boolean;
313
- }[];
314
-
315
- type ChangeType = 'added' | 'modified' | 'deleted' | 'renamed';
316
- interface ChangedFile {
317
- relativePath: string;
318
- changeType: ChangeType;
319
- linesAdded: number;
320
- linesRemoved: number;
321
- }
322
- interface PRContextResult {
323
- baseBranch: string;
324
- currentBranch: string;
325
- isGitRepo: boolean;
326
- changedFiles: ChangedFile[];
327
- dependencyFiles: string[];
328
- allRelevantFiles: AnalyzedFile[];
329
- totalChangedTokens: number;
330
- totalContextTokens: number;
331
- riskSummary: {
332
- critical: number;
333
- high: number;
334
- medium: number;
335
- low: number;
336
- maxRiskFile: string;
337
- maxRiskScore: number;
338
- };
339
- renderedSummary: string;
340
- }
341
- interface PRContextOptions {
342
- baseBranch?: string;
343
- depth?: number;
344
- includeTests?: boolean;
345
- }
310
+ declare function tokenize(text: string): string[];
346
311
  /**
347
- * Generate PR-focused context by analyzing git changes and expanding dependencies.
348
- *
349
- * @param analysis - Project analysis (from analyzeProject or getCachedAnalysis)
350
- * @param options - PR context options (baseBranch, depth, includeTests)
351
- * @returns Structured PR context with changed files, dependencies, risk summary
312
+ * Boost TF-IDF scores based on file path relevance to the task.
313
+ * This catches cases where the file content doesn't mention the task terms
314
+ * but the file path does (e.g., task "fix auth" → src/auth/middleware.ts).
352
315
  */
353
- declare function generatePRContext(analysis: ProjectAnalysis, options?: PRContextOptions): Promise<PRContextResult>;
316
+ declare function boostByPath(matches: SemanticMatch[], allFiles: string[], taskDescription: string): SemanticMatch[];
354
317
 
355
- type Grade = 'A+' | 'A' | 'A-' | 'B+' | 'B' | 'B-' | 'C+' | 'C' | 'C-' | 'D' | 'F';
356
- interface ContextScore {
357
- overall: number;
358
- grade: Grade;
359
- dimensions: {
360
- efficiency: DimensionScore;
361
- coverage: DimensionScore;
362
- riskControl: DimensionScore;
363
- structure: DimensionScore;
364
- governance: DimensionScore;
365
- };
366
- insights: ScoreInsight[];
367
- comparison: {
368
- naiveTokens: number;
369
- optimizedTokens: number;
370
- savedTokens: number;
371
- savedPercent: number;
372
- monthlySavingsUSD: number;
373
- };
374
- meta: {
375
- projectName: string;
376
- totalFiles: number;
377
- totalTokens: number;
378
- analyzedAt: Date;
379
- };
380
- }
381
- interface DimensionScore {
382
- score: number;
383
- weight: number;
384
- weighted: number;
385
- detail: string;
386
- }
387
- interface ScoreInsight {
388
- type: 'strength' | 'weakness' | 'opportunity';
389
- title: string;
390
- detail: string;
391
- impact: 'high' | 'medium' | 'low';
392
- }
393
318
  /**
394
- * Compute the Context Score™ for a project.
319
+ * Persistent TF-IDF Index Cache
395
320
  *
396
- * @param analysis - Project analysis (from analyzeProject or getCachedAnalysis)
397
- * @param task - Representative task (default: "general code review")
398
- * @param budget - Token budget (default: 50000)
399
- */
400
- declare function computeContextScore(analysis: ProjectAnalysis, task?: string, budget?: number): Promise<ContextScore>;
401
- /**
402
- * Render the Context Score as a beautiful terminal-friendly string.
321
+ * Problem: Building a TF-IDF index reads every source file and tokenizes it.
322
+ * For a 50K-file repo, that's 5-10 seconds per query. With 20K devs running
323
+ * queries concurrently, re-indexing on every call is unacceptable.
324
+ *
325
+ * Solution: Persist the index to disk with per-file mtime tracking.
326
+ * On subsequent queries, only re-index files that changed since last build.
327
+ *
328
+ * Storage: .cto/index-cache.json
329
+ * {
330
+ * version: 2,
331
+ * builtAt: ISO timestamp,
332
+ * files: { [relativePath]: { mtime: number, terms: { [term]: count }, length: number } },
333
+ * idf: { [term]: number },
334
+ * avgDocLength: number,
335
+ * totalDocs: number,
336
+ * }
337
+ *
338
+ * Invalidation:
339
+ * - Per-file: mtime changed → re-tokenize that file
340
+ * - New files: not in cache → tokenize and add
341
+ * - Deleted files: in cache but not on disk → remove
342
+ * - Version bump: cache format changed → full rebuild
343
+ *
344
+ * The IDF values are recomputed after any incremental update because
345
+ * document frequency changes affect all terms globally.
403
346
  */
404
- declare function renderContextScore(score: ContextScore): string;
405
347
 
406
- interface BenchmarkResult {
407
- project: string;
348
+ interface IndexCacheStats {
349
+ /** Total files in the index */
408
350
  totalFiles: number;
409
- totalTokens: number;
410
- budget: number;
411
- task: string;
412
- strategies: {
413
- cto: StrategyResult;
414
- naive: StrategyResult;
415
- random: StrategyResult;
416
- };
417
- winner: 'cto' | 'naive' | 'random';
418
- ctoAdvantage: {
419
- vsNaiveTokensSaved: number;
420
- vsNaiveTokensSavedPercent: number;
421
- vsRandomCoverageGain: number;
422
- vsNaiveCostSavedMonthlyUSD: number;
423
- };
424
- }
425
- interface StrategyResult {
426
- filesSelected: number;
427
- tokensUsed: number;
428
- coverageScore: number;
429
- criticalFilesCovered: number;
430
- criticalFilesTotal: number;
431
- highRiskCovered: number;
432
- highRiskTotal: number;
433
- costPerInteractionUSD: number;
434
- timeMs: number;
351
+ /** Files that were re-indexed (changed or new) */
352
+ updatedFiles: number;
353
+ /** Files removed from cache (deleted from disk) */
354
+ removedFiles: number;
355
+ /** Files reused from cache (unchanged) */
356
+ cachedFiles: number;
357
+ /** Whether the cache existed before this build */
358
+ cacheHit: boolean;
359
+ /** Time to build/update the index (ms) */
360
+ buildTimeMs: number;
435
361
  }
436
362
  /**
437
- * Run a full benchmark comparing CTO vs naive vs random selection.
363
+ * Build or update a TF-IDF index with disk caching.
364
+ *
365
+ * First call: builds full index and writes cache to .cto/index-cache.json
366
+ * Subsequent calls: reads cache, updates only changed files, rewrites cache
367
+ *
368
+ * @param projectPath - Root of the project (for .cto/ directory)
369
+ * @param files - All files to index: { relativePath, absolutePath, content? }
370
+ * If content is provided, it's used directly. Otherwise, the file is read from disk.
371
+ * @returns The TF-IDF index + stats about cache hits/misses
438
372
  */
439
- declare function runBenchmark(analysis: ProjectAnalysis, task?: string, budget?: number): Promise<BenchmarkResult>;
373
+ declare function buildIndexCached(projectPath: string, files: {
374
+ relativePath: string;
375
+ absolutePath: string;
376
+ content?: string;
377
+ }[]): {
378
+ index: TfIdfIndex;
379
+ stats: IndexCacheStats;
380
+ };
440
381
  /**
441
- * Render benchmark results as a formatted comparison table.
382
+ * Invalidate the entire cache (force full rebuild on next call).
442
383
  */
443
- declare function renderBenchmark(result: BenchmarkResult): string;
444
-
445
- type TaskType = 'debug' | 'review' | 'refactor' | 'test' | 'docs' | 'feature' | 'architecture' | 'simple-edit';
446
-
447
- interface QualityBenchmarkResult {
448
- project: string;
449
- task: string;
450
- taskType: TaskType;
451
- budget: number;
452
- strategies: {
453
- cto: QualityMetrics;
454
- naive: QualityMetrics;
455
- random: QualityMetrics;
456
- };
457
- comparison: {
458
- ctoVsNaiveRelevance: number;
459
- ctoVsRandomRelevance: number;
460
- ctoVsNaiveCompleteness: number;
461
- ctoVsRandomCompleteness: number;
462
- ctoNoiseReduction: number;
463
- };
464
- prompts: {
465
- cto: {
466
- rendered: string;
467
- tokens: number;
468
- };
469
- naive: {
470
- rendered: string;
471
- tokens: number;
472
- };
473
- };
474
- verdict: string;
475
- }
476
- interface QualityMetrics {
477
- filesSelected: number;
478
- tokensUsed: number;
479
- relevanceScore: number;
480
- completenessScore: number;
481
- noiseRatio: number;
482
- typeCoverage: number;
483
- dependencyClosure: number;
484
- relevantFilesIncluded: number;
485
- relevantFilesTotal: number;
486
- typeFilesIncluded: number;
487
- typeFilesNeeded: number;
488
- depsIncluded: number;
489
- depsNeeded: number;
490
- }
384
+ declare function invalidateCache(projectPath: string): void;
491
385
  /**
492
- * Run a quality benchmark comparing CTO vs naive vs random context selection.
493
- * Measures relevance, completeness, noise, and dependency closure.
386
+ * Get cache stats without rebuilding.
494
387
  */
495
- declare function runQualityBenchmark(analysis: ProjectAnalysis, task: string, budget?: number): Promise<QualityBenchmarkResult>;
496
- declare function renderQualityBenchmark(result: QualityBenchmarkResult): string;
497
-
498
- interface PredictorModel {
499
- version: number;
500
- trainedAt: string;
501
- totalObservations: number;
502
- taskTypeFrequency: Record<string, Record<string, number>>;
503
- keywordFrequency: Record<string, Record<string, number>>;
504
- fileStats: Record<string, {
505
- totalSelections: number;
506
- avgRiskScore: number;
507
- avgTokens: number;
508
- lastSelected: string;
509
- }>;
510
- coSelection: Record<string, Record<string, number>>;
511
- }
512
- interface PredictionResult {
513
- filePath: string;
514
- predictedScore: number;
515
- reasons: string[];
516
- }
517
- interface PredictorConfig {
518
- maxCoSelectionPairs: number;
519
- decayFactor: number;
520
- minObservations: number;
521
- }
522
- declare function loadModel(projectPath: string): Promise<PredictorModel>;
523
- declare function recordSelection(projectPath: string, task: string, selectedFiles: {
524
- relativePath: string;
525
- riskScore: number;
526
- tokens: number;
527
- }[]): Promise<PredictorModel>;
528
- declare function predictRelevantFiles(projectPath: string, task: string, analysis: ProjectAnalysis, config?: Partial<PredictorConfig>): Promise<PredictionResult[]>;
529
- declare function getPredictorBoosts(projectPath: string, task: string, analysis: ProjectAnalysis): Promise<Map<string, number>>;
530
- declare function getModelStats(model: PredictorModel): {
531
- observations: number;
532
- taskTypes: number;
533
- keywords: number;
534
- trackedFiles: number;
535
- coSelectionPairs: number;
536
- trainedAt: string;
388
+ declare function getCacheInfo(projectPath: string): {
389
+ exists: boolean;
390
+ fileCount: number;
391
+ builtAt: string | null;
537
392
  };
538
393
 
539
- interface ProjectFingerprint {
540
- stack: string[];
541
- sizeClass: 'tiny' | 'small' | 'medium' | 'large' | 'huge';
542
- hasTypes: boolean;
543
- hasTests: boolean;
544
- isMonorepo: boolean;
545
- entryPointPatterns: string[];
546
- dominantLanguage: string;
547
- }
548
- interface CrossRepoModel {
549
- version: number;
394
+ /**
395
+ * Usage Learner — Gets smarter with every use.
396
+ *
397
+ * Zero dependencies. Bayesian priors updated from real usage.
398
+ *
399
+ * How it works:
400
+ * 1. Every time context is selected, log what was included/excluded
401
+ * 2. When user accepts/rejects files, update file relevance priors
402
+ * 3. Next selection uses priors as boost signals alongside TF-IDF + risk
403
+ *
404
+ * Storage: .cto/learner.json — portable, versioned, <10KB typical
405
+ *
406
+ * The model is intentionally simple (no neural nets, no embeddings model):
407
+ * - Beta distribution per file pattern (alpha=accepted, beta=rejected)
408
+ * - Exponential decay so recent feedback weighs more
409
+ * - Pattern-based: learns "test files matter for debug tasks", not
410
+ * specific file paths (so it works across branches/renames)
411
+ */
412
+ interface LearnerModel {
413
+ version: 2;
550
414
  updatedAt: string;
551
- totalProjects: number;
552
- totalObservations: number;
553
- archetypes: Record<string, ArchetypeProfile>;
554
- universalPatterns: PatternStats[];
555
- }
556
- interface ArchetypeProfile {
557
- name: string;
558
- fingerprint: ProjectFingerprint;
559
- projectCount: number;
560
- observationCount: number;
415
+ patterns: Record<string, PatternStats>;
561
416
  taskPatterns: Record<string, Record<string, PatternStats>>;
562
- criticalKinds: {
563
- kind: string;
564
- importance: number;
565
- }[];
566
- criticalDirs: {
567
- pattern: string;
568
- importance: number;
569
- }[];
417
+ totalSelections: number;
570
418
  }
571
419
  interface PatternStats {
572
- pattern: string;
573
- hitCount: number;
574
- totalSelections: number;
575
- hitRate: number;
576
- avgRelevanceBoost: number;
420
+ alpha: number;
421
+ beta: number;
422
+ lastSeen: string;
577
423
  }
578
- interface CrossRepoPrediction {
424
+ interface LearnerBoost {
579
425
  filePath: string;
580
426
  boost: number;
581
- reason: string;
582
427
  confidence: number;
428
+ reason: string;
583
429
  }
584
- declare function computeFingerprint(analysis: ProjectAnalysis): ProjectFingerprint;
585
- declare function loadGlobalModel(): Promise<CrossRepoModel>;
586
- declare function recordCrossRepoSelection(analysis: ProjectAnalysis, task: string, selectedFiles: {
587
- relativePath: string;
588
- riskScore: number;
589
- tokens: number;
590
- }[]): Promise<CrossRepoModel>;
591
- declare function predictFromCrossRepo(analysis: ProjectAnalysis, task: string): Promise<CrossRepoPrediction[]>;
592
- declare function getCrossRepoStats(model: CrossRepoModel): {
593
- totalProjects: number;
594
- totalObservations: number;
595
- archetypes: {
596
- name: string;
597
- projects: number;
598
- observations: number;
599
- }[];
600
- universalPatterns: number;
601
- };
602
-
603
- interface FeedbackEntry {
604
- id: string;
605
- timestamp: string;
606
- task: string;
607
- taskType: TaskType;
608
- contextHash: string;
609
- filesIncluded: string[];
610
- tokensUsed: number;
611
- budget: number;
612
- outcome: FeedbackOutcome;
613
- model?: string;
614
- promptTokens?: number;
615
- sessionId?: string;
616
- strategy?: string;
617
- }
618
- interface FeedbackOutcome {
619
- accepted: boolean;
620
- compilable?: boolean;
621
- testsPassed?: number;
622
- testsTotal?: number;
623
- linesGenerated?: number;
624
- linesAccepted?: number;
625
- timeToAcceptMs?: number;
626
- userRating?: 1 | 2 | 3 | 4 | 5;
627
- notes?: string;
628
- }
629
- interface FeedbackModel {
630
- version: number;
631
- updatedAt: string;
632
- totalFeedback: number;
633
- acceptRate: number;
634
- fileAcceptance: Record<string, {
635
- includedCount: number;
636
- acceptedCount: number;
637
- acceptRate: number;
638
- ewmaAcceptRate: number;
639
- avgTimeToAccept: number;
640
- lastSeen: string;
641
- bayesianLower: number;
642
- }>;
643
- taskTypeAcceptance: Record<string, {
644
- totalCount: number;
645
- acceptedCount: number;
646
- acceptRate: number;
647
- avgCompilable: number;
648
- ewmaAcceptRate: number;
649
- }>;
650
- pairAcceptance: Record<string, {
651
- count: number;
652
- acceptedCount: number;
653
- acceptRate: number;
654
- }>;
655
- sessions: Record<string, {
656
- strategy: string;
657
- entries: number;
658
- acceptRate: number;
659
- }>;
660
- strategyComparison: Record<string, {
661
- totalCount: number;
662
- acceptedCount: number;
430
+ /**
431
+ * Load the learner model from .cto/learner.json.
432
+ * Returns empty model if none exists.
433
+ */
434
+ declare function loadLearner(projectPath: string): Promise<LearnerModel>;
435
+ /**
436
+ * Save the learner model.
437
+ */
438
+ declare function saveLearner(projectPath: string, model: LearnerModel): Promise<void>;
439
+ /**
440
+ * Record a selection event — which files were included and which were excluded.
441
+ * Updates Bayesian priors with exponential decay.
442
+ */
443
+ declare function recordSelection(model: LearnerModel, taskType: string, selectedFiles: string[], excludedFiles: string[]): LearnerModel;
444
+ /**
445
+ * Get boost signals for files based on learned patterns.
446
+ * Returns boosts that should be applied during context selection.
447
+ */
448
+ declare function getLearnerBoosts(model: LearnerModel, taskType: string, files: string[]): LearnerBoost[];
449
+ /**
450
+ * Get learner stats for display.
451
+ */
452
+ declare function getLearnerStats(model: LearnerModel): {
453
+ totalSelections: number;
454
+ patternsLearned: number;
455
+ taskTypes: string[];
456
+ topPatterns: {
457
+ pattern: string;
663
458
  acceptRate: number;
664
- avgTimeToAccept: number;
665
- }>;
666
- insights: FeedbackInsight[];
667
- }
668
- interface FeedbackInsight {
669
- type: 'positive' | 'negative' | 'opportunity';
670
- title: string;
671
- detail: string;
672
- impact: number;
673
- }
674
- declare function loadFeedbackModel(projectPath: string): Promise<FeedbackModel>;
675
- declare function wilsonLowerBound(successes: number, total: number, z?: number): number;
676
- declare function recordFeedback(projectPath: string, entry: Omit<FeedbackEntry, 'id' | 'timestamp' | 'taskType'>): Promise<FeedbackModel>;
677
- declare function getFeedbackBoosts(projectPath: string, task: string): Promise<Map<string, number>>;
678
- interface TeamFeedbackExport {
679
- version: number;
680
- exportedAt: string;
681
- projectName: string;
682
- model: FeedbackModel;
683
- entrySummary: {
684
- total: number;
685
- accepted: number;
686
- sessions: number;
687
- };
688
- }
689
- declare function exportFeedbackForTeam(projectPath: string, projectName: string): Promise<TeamFeedbackExport>;
690
- declare function importTeamFeedback(projectPath: string, teamExport: TeamFeedbackExport): Promise<FeedbackModel>;
691
- declare function renderFeedbackReport(model: FeedbackModel): string;
692
- declare function renderCrossRepoReport(stats: {
693
- totalProjects: number;
694
- totalObservations: number;
695
- archetypes: {
696
- name: string;
697
- projects: number;
698
459
  observations: number;
699
460
  }[];
700
- universalPatterns: number;
701
- }): string;
461
+ };
462
+ /**
463
+ * Extract a generalizable pattern from a file path.
464
+ * Examples: "src/engine/analyzer.ts" becomes "engine/(star).ts",
465
+ * "tests/unit/auth.test.ts" becomes "tests/(star)(star)/(star).test.ts".
466
+ * This way the learner generalizes across similar files,
467
+ * not just memorize specific paths.
468
+ */
469
+ declare function extractPattern(filePath: string): string;
702
470
 
703
- interface SemanticFingerprint {
704
- filePath: string;
705
- domains: SemanticDomain[];
706
- exports: string[];
707
- concepts: string[];
708
- patterns: CodePattern[];
709
- intentScore: Map<string, number>;
710
- }
711
- interface SemanticDomain {
471
+ /**
472
+ * Multi-Repo Context Selection
473
+ *
474
+ * Discovers sibling repositories in a workspace and queries them
475
+ * for relevant files when selecting context for a task.
476
+ *
477
+ * How it works:
478
+ * 1. Discover sibling repos (scan parent dir or use explicit paths)
479
+ * 2. For each sibling: list source files, read contents, build TF-IDF index
480
+ * 3. Query each sibling's index with the task description
481
+ * 4. Return ranked matches with repo attribution
482
+ *
483
+ * This is NOT the cross-repo learning system (cross-repo.ts).
484
+ * This is actual multi-repo file discovery and querying.
485
+ */
486
+ interface SiblingRepo {
487
+ /** Absolute path to the repo root */
488
+ path: string;
489
+ /** Short name (directory name) */
712
490
  name: string;
713
- confidence: number;
714
- signals: string[];
715
- }
716
- interface CodePattern {
717
- type: 'route' | 'model' | 'middleware' | 'config' | 'test' | 'type' | 'util' | 'entry' | 'event' | 'error';
718
- evidence: string;
719
- }
720
- interface SemanticAnalysis {
721
- files: SemanticFingerprint[];
722
- domainGraph: {
723
- from: string;
724
- to: string;
725
- strength: number;
726
- }[];
727
- domainClusters: {
728
- domain: string;
729
- files: string[];
730
- tokenBudget: number;
731
- }[];
732
- }
733
- declare function analyzeSemantics(analysis: ProjectAnalysis): SemanticAnalysis;
734
- declare function semanticBoosts(semantics: SemanticAnalysis, task: string): Map<string, number>;
735
- declare function renderSemanticAnalysis(semantics: SemanticAnalysis): string;
736
-
737
- interface CompilabilityResult {
738
- project: string;
739
- task: string;
740
- budget: number;
741
- strategies: {
742
- cto: CompilabilityMetrics;
743
- naive: CompilabilityMetrics;
744
- random: CompilabilityMetrics;
745
- };
746
- comparison: {
747
- ctoVsNaiveTypeAvailability: number;
748
- ctoVsRandomTypeAvailability: number;
749
- ctoVsNaivePredictedErrors: number;
750
- naiveMissingTypes: string[];
751
- randomMissingTypes: string[];
752
- };
753
- verdict: string;
491
+ /** Detected stack (from package.json, tsconfig, etc.) */
492
+ stack: string[];
493
+ /** Number of source files found */
494
+ fileCount: number;
495
+ }
496
+ interface SiblingMatch {
497
+ /** Which sibling repo this file belongs to */
498
+ repoName: string;
499
+ /** Absolute path to the repo */
500
+ repoPath: string;
501
+ /** Relative path within the sibling repo */
502
+ relativePath: string;
503
+ /** Absolute path to the file */
504
+ absolutePath: string;
505
+ /** Semantic relevance score (0-1) */
506
+ score: number;
507
+ /** File content */
508
+ content: string;
509
+ /** Estimated token count */
510
+ tokens: number;
754
511
  }
755
- interface CompilabilityMetrics {
756
- typeFilesAvailable: number;
757
- typeFilesTotal: number;
758
- typeAvailabilityPercent: number;
759
- importChainsComplete: number;
760
- importChainsTotal: number;
761
- importCompletenessPercent: number;
762
- missingTypeFiles: string[];
763
- missingDependencies: string[];
764
- predictedTypeErrors: number;
765
- predictedImportErrors: number;
766
- predictedTotalErrors: number;
767
- compilabilityScore: number;
768
- filesSelected: number;
769
- tokensUsed: number;
512
+ interface MultiRepoResult {
513
+ /** Sibling repos that were discovered/used */
514
+ siblings: SiblingRepo[];
515
+ /** Top matches from sibling repos, ranked by score */
516
+ matches: SiblingMatch[];
517
+ /** Total time spent indexing + querying (ms) */
518
+ timeMs: number;
770
519
  }
771
- declare function runCompilabilityBenchmark(analysis: ProjectAnalysis, task: string, budget?: number): Promise<CompilabilityResult>;
772
- declare function renderCompilabilityBenchmark(result: CompilabilityResult): string;
520
+ /**
521
+ * Discover sibling repositories by scanning the parent directory.
522
+ * A directory is a "repo" if it contains a known project marker file.
523
+ */
524
+ declare function discoverSiblingRepos(projectPath: string): SiblingRepo[];
525
+ /**
526
+ * Query sibling repos for files relevant to a task.
527
+ *
528
+ * For each sibling:
529
+ * 1. List source files
530
+ * 2. Build TF-IDF index from file contents
531
+ * 3. Query with task description
532
+ * 4. Return top matches with content
533
+ *
534
+ * @param siblings - Sibling repos to query (from discoverSiblingRepos or explicit paths)
535
+ * @param task - Task description to match against
536
+ * @param maxPerRepo - Max matches per repo (default 5)
537
+ * @param minScore - Minimum semantic score to include (default 0.3)
538
+ */
539
+ declare function querySiblingRepos(siblings: SiblingRepo[], task: string, maxPerRepo?: number, minScore?: number): MultiRepoResult;
540
+ /**
541
+ * Parse explicit repo paths from a comma-separated string.
542
+ * Resolves relative paths against the current project's parent directory.
543
+ */
544
+ declare function parseSiblingPaths(pathsStr: string, projectPath: string): SiblingRepo[];
545
+ /**
546
+ * Render multi-repo results for CLI output.
547
+ */
548
+ declare function renderMultiRepoSummary(result: MultiRepoResult): string;
773
549
 
774
- interface CompileProofResult {
775
- project: string;
776
- task: string;
777
- budget: number;
778
- cto: CompileProofStrategy;
779
- naive: CompileProofStrategy;
780
- random: CompileProofStrategy;
781
- headline: string;
782
- details: string;
783
- }
784
- interface CompileProofStrategy {
785
- name: string;
786
- filesIncluded: number;
787
- tokensUsed: number;
788
- typeFilesIncluded: string[];
789
- typeFilesMissing: string[];
790
- compileErrors: number;
791
- errorMessages: string[];
792
- compiles: boolean;
793
- }
794
- declare function runCompileProof(analysis: ProjectAnalysis, task: string, budget?: number): Promise<CompileProofResult>;
795
- declare function renderCompileProof(result: CompileProofResult): string;
550
+ /**
551
+ * Shared Context Pipeline
552
+ *
553
+ * Single function that runs the full context selection pipeline:
554
+ * read files → build TF-IDF index → query → boost → load learner → selectContext
555
+ *
556
+ * Used by both CLI and MCP server. No duplication.
557
+ */
796
558
 
797
- interface ModelProfile {
798
- id: string;
799
- name: string;
800
- provider: 'openai' | 'anthropic' | 'google' | 'mistral' | 'meta' | 'custom';
801
- contextWindow: number;
802
- maxOutput: number;
803
- costPer1MInput: number;
804
- costPer1MOutput: number;
805
- strengths: ModelStrength[];
806
- recommendedBudgetPercent: number;
807
- }
808
- type ModelStrength = 'code-generation' | 'analysis' | 'refactoring' | 'debugging' | 'documentation' | 'testing' | 'general';
809
- interface MultiModelResult {
559
+ interface ContextPipelineInput {
560
+ projectPath: string;
810
561
  task: string;
811
- models: ModelOptimization[];
812
- recommendation: {
813
- bestValue: string;
814
- bestQuality: string;
815
- bestSpeed: string;
816
- reasoning: string;
817
- };
562
+ analysis: ProjectAnalysis;
563
+ budget?: number;
564
+ /** Optional sibling repos for cross-repo context */
565
+ siblingRepos?: SiblingRepo[];
818
566
  }
819
- interface ModelOptimization {
820
- model: ModelProfile;
821
- budget: number;
567
+ interface ContextPipelineResult {
822
568
  selection: ContextSelection;
823
- estimatedCost: number;
824
- qualityScore: number;
825
- recommendation: string;
569
+ taskType: string;
570
+ fileContentMap: Map<string, string>;
571
+ semanticMap: Map<string, SemanticMatch>;
572
+ learnerMap: Map<string, LearnerBoost>;
573
+ /** Cross-repo results (only present if siblingRepos were provided) */
574
+ multiRepo?: MultiRepoResult;
575
+ /** Index cache stats (how many files were cached vs rebuilt) */
576
+ indexCacheStats?: IndexCacheStats;
826
577
  }
827
- declare const MODEL_REGISTRY: ModelProfile[];
828
- declare function optimizeForModels(analysis: ProjectAnalysis, task: string, models?: string[]): Promise<MultiModelResult>;
829
- declare function renderMultiModelResult(result: MultiModelResult): string;
578
+ /**
579
+ * Run the full context selection pipeline.
580
+ * One function, used everywhere. No copy-paste.
581
+ */
582
+ declare function runContextPipeline(input: ContextPipelineInput): Promise<ContextPipelineResult>;
830
583
 
831
584
  declare function scoreAllFiles(files: AnalyzedFile[], graph: ProjectGraph, weights?: RiskWeights): void;
832
585
  declare function scoreFile(file: AnalyzedFile, graph: ProjectGraph, weights?: RiskWeights): number;
833
586
 
834
587
  declare function calculateCoverage(targetPaths: string[], includedPaths: string[], allFiles: AnalyzedFile[], graph: ProjectGraph, depth?: number): CoverageResult;
835
588
 
836
- interface SelectionInput {
837
- task: string;
838
- analysis: ProjectAnalysis;
839
- budget: number;
840
- policies?: PolicySet;
841
- depth?: number;
842
- }
843
- declare function selectContext(input: SelectionInput): Promise<ContextSelection>;
844
-
845
589
  declare function getPruneLevelForRisk(riskScore: number): PruneLevel;
846
590
  declare function optimizeBudget(files: AnalyzedFile[], budget: number): Promise<BudgetPlan>;
847
591
 
848
592
  declare function pruneFile(file: AnalyzedFile, level: PruneLevel): Promise<PrunedContent>;
849
593
  declare function pruneFiles(files: AnalyzedFile[], levelFn: (file: AnalyzedFile) => PruneLevel): Promise<PrunedContent[]>;
850
594
 
595
+ /**
596
+ * Closed-Loop A/B Testing Engine
597
+ *
598
+ * The missing piece: the feedback system records data but never closes the loop.
599
+ * This module adds real experimentation:
600
+ *
601
+ * 1. Define experiments with control + variant strategies
602
+ * 2. Assign requests to groups (deterministic hashing for consistency)
603
+ * 3. Collect outcomes per group
604
+ * 4. Compute statistical significance (z-test for proportions)
605
+ * 5. Auto-promote winning variants when significance threshold met
606
+ *
607
+ * Example experiment:
608
+ * - Control: default composite scoring (semantic 0.55, risk 0.25, learner 0.20)
609
+ * - Variant: reranker-heavy scoring (reranker 0.70, risk 0.15, learner 0.15)
610
+ * - Metric: acceptance rate
611
+ * - Significance: p < 0.05
612
+ *
613
+ * Storage: .cto/experiments.json
614
+ * Design: Pure functions. No external deps. Deterministic assignment.
615
+ */
616
+ interface Experiment {
617
+ /** Unique experiment ID */
618
+ id: string;
619
+ /** Human-readable name */
620
+ name: string;
621
+ /** What we're testing */
622
+ description: string;
623
+ /** Current status */
624
+ status: 'running' | 'concluded' | 'paused';
625
+ /** When the experiment started */
626
+ startedAt: string;
627
+ /** When it concluded (if applicable) */
628
+ concludedAt?: string;
629
+ /** Traffic split: 0.5 = 50/50 */
630
+ trafficSplit: number;
631
+ /** Minimum observations per group before significance test */
632
+ minObservations: number;
633
+ /** P-value threshold for significance */
634
+ significanceThreshold: number;
635
+ /** Control group config */
636
+ control: ExperimentGroup;
637
+ /** Variant group config */
638
+ variant: ExperimentGroup;
639
+ /** Conclusion (when experiment ends) */
640
+ conclusion?: ExperimentConclusion;
641
+ }
642
+ interface ExperimentGroup {
643
+ /** Group name */
644
+ name: string;
645
+ /** Strategy parameters (passed to the engine) */
646
+ params: Record<string, unknown>;
647
+ /** Collected metrics */
648
+ metrics: GroupMetrics;
649
+ }
650
+ interface GroupMetrics {
651
+ /** Total observations */
652
+ total: number;
653
+ /** Successful outcomes (accepted) */
654
+ successes: number;
655
+ /** Accept rate = successes / total */
656
+ acceptRate: number;
657
+ /** Average time to accept (ms) */
658
+ avgTimeToAccept: number;
659
+ /** Compilable rate */
660
+ compilableRate: number;
661
+ /** Sum of time values (for running average) */
662
+ timeSum: number;
663
+ /** Count of compilable results */
664
+ compilableCount: number;
665
+ }
666
+ interface ExperimentConclusion {
667
+ /** Which group won */
668
+ winner: 'control' | 'variant' | 'no_difference';
669
+ /** Observed p-value */
670
+ pValue: number;
671
+ /** Effect size (difference in accept rates) */
672
+ effectSize: number;
673
+ /** Confidence interval for effect size */
674
+ confidenceInterval: [number, number];
675
+ /** Human-readable summary */
676
+ summary: string;
677
+ }
678
+ interface AssignmentResult {
679
+ /** Which group the request was assigned to */
680
+ group: 'control' | 'variant';
681
+ /** The strategy params for this group */
682
+ params: Record<string, unknown>;
683
+ /** Experiment ID for tracking */
684
+ experimentId: string;
685
+ }
686
+ declare function loadExperiments(projectPath: string): Experiment[];
687
+ declare function saveExperiments(projectPath: string, experiments: Experiment[]): void;
688
+ declare function createExperiment(id: string, name: string, description: string, controlParams: Record<string, unknown>, variantParams: Record<string, unknown>, options?: {
689
+ trafficSplit?: number;
690
+ minObservations?: number;
691
+ significanceThreshold?: number;
692
+ }): Experiment;
693
+ /**
694
+ * Assign a request to control or variant group.
695
+ * Uses deterministic hashing: same (experiment_id, task) → same group.
696
+ * This ensures consistency (retries get the same group).
697
+ */
698
+ declare function assignGroup(experiment: Experiment, task: string): AssignmentResult | null;
699
+ /**
700
+ * Record an outcome for an experiment group.
701
+ * Updates running statistics and checks for significance.
702
+ */
703
+ declare function recordOutcome(experiment: Experiment, group: 'control' | 'variant', outcome: {
704
+ accepted: boolean;
705
+ compilable?: boolean;
706
+ timeToAcceptMs?: number;
707
+ }): Experiment;
708
+ interface SignificanceResult {
709
+ /** Two-sided p-value */
710
+ pValue: number;
711
+ /** Z-score */
712
+ zScore: number;
713
+ /** Effect size: variant rate - control rate */
714
+ effectSize: number;
715
+ /** 95% confidence interval for effect size */
716
+ confidenceInterval: [number, number];
717
+ /** Whether the result is significant at the experiment's threshold */
718
+ significant: boolean;
719
+ }
720
+ /**
721
+ * Two-proportion z-test for A/B testing.
722
+ *
723
+ * H0: p_control = p_variant
724
+ * H1: p_control ≠ p_variant (two-sided)
725
+ *
726
+ * This is the standard test for comparing conversion rates.
727
+ */
728
+ declare function testSignificance(experiment: Experiment): SignificanceResult;
729
+ /**
730
+ * Get the active experiment for this project (if any).
731
+ */
732
+ declare function getActiveExperiment(experiments: Experiment[]): Experiment | null;
733
+ /**
734
+ * Get all concluded experiments with their results.
735
+ */
736
+ declare function getConcludedExperiments(experiments: Experiment[]): Experiment[];
737
+ /**
738
+ * Render experiment summary for CLI/dashboard.
739
+ */
740
+ declare function renderExperimentSummary(experiment: Experiment): string;
741
+
742
+ /**
743
+ * Polyglot Dependency Graph — Import Parsing for Python, Go, Java, Rust
744
+ *
745
+ * Problem: The existing graph.ts uses ts-morph (AST) which only handles TS/JS.
746
+ * For a 20K-dev org with Java, Python, Go, Rust — the dependency graph is empty.
747
+ * No graph → no hub detection → no risk scoring → useless context selection.
748
+ *
749
+ * Solution: Regex-based import parsers for each language. Not AST-accurate, but
750
+ * good enough for dependency graph construction. We don't need perfect resolution;
751
+ * we need to know "file A probably depends on file B" for hub/risk scoring.
752
+ *
753
+ * Each parser:
754
+ * 1. Extracts import specifiers from file content using regex
755
+ * 2. Resolves specifiers to relative file paths within the project
756
+ * 3. Returns edges: { from: relativePath, to: relativePath }
757
+ *
758
+ * Supported languages:
759
+ * - Python: import x, from x import y, relative imports
760
+ * - Go: import "pkg", import ( "pkg" ... )
761
+ * - Java: import com.example.Foo, package declaration
762
+ * - Rust: use crate::x, mod x, use super::x
763
+ *
764
+ * Design: Pure functions. No external deps. Deterministic.
765
+ */
766
+
767
+ type SupportedLanguage = 'python' | 'go' | 'java' | 'rust' | 'typescript';
768
+ interface ImportSpec {
769
+ /** The raw import specifier as written in the source */
770
+ raw: string;
771
+ /** Whether this is a relative import */
772
+ isRelative: boolean;
773
+ }
774
+ declare function detectLanguage(filePath: string): SupportedLanguage | null;
775
+ /**
776
+ * Parse imports from a non-TS file and resolve to project-relative paths.
777
+ * Returns dependency edges for the project graph.
778
+ *
779
+ * @param filePath - Absolute path to the source file
780
+ * @param relativePath - Project-relative path (e.g., "src/auth/login.py")
781
+ * @param projectPath - Absolute path to the project root
782
+ * @param allRelativePaths - Set of all file paths in the project (for resolution)
783
+ * @param content - Optional file content (read from disk if not provided)
784
+ */
785
+ declare function parseImports(filePath: string, relativePath: string, projectPath: string, allRelativePaths: Set<string>, content?: string): GraphEdge[];
786
+ /**
787
+ * Parse imports for ALL non-TS files in a project.
788
+ * Call this alongside ts-morph's buildProjectGraph for TS files.
789
+ */
790
+ declare function parseAllPolyglotImports(files: {
791
+ relativePath: string;
792
+ absolutePath: string;
793
+ content?: string;
794
+ }[], projectPath: string): GraphEdge[];
795
+ /**
796
+ * Estimate cyclomatic complexity from source code using regex.
797
+ * Not AST-accurate but good enough for risk scoring.
798
+ */
799
+ declare function estimateComplexity(content: string, lang: SupportedLanguage): number;
800
+
801
+ /**
802
+ * Multi-Stage Reranker
803
+ *
804
+ * The problem: BM25 retrieval gets 54% precision. Adding risk scoring drops it
805
+ * to 33% because high-risk irrelevant files fill the budget.
806
+ *
807
+ * The solution: a 3-stage pipeline that turns BM25 candidates into a precision-
808
+ * optimized selection:
809
+ *
810
+ * Stage 1: RETRIEVE (BM25 top-K) — already done by tfidf.ts
811
+ * Stage 2: RERANK (multi-signal rescoring)
812
+ * - Term coverage: what fraction of UNIQUE query terms does the file match?
813
+ * - Term specificity: are the matched terms rare (high IDF) or generic?
814
+ * - Bigram proximity: do query terms appear near each other in the file?
815
+ * - Dependency signal: is this file in the dependency cone of a top match?
816
+ * - Path relevance: does the file path match query terms?
817
+ * Stage 3: QUALITY GATE (adaptive cutoff)
818
+ * - Hard floor: files below absolute threshold are excluded
819
+ * - Elbow detection: find the natural drop-off point in scores
820
+ * - Don't fill budget with noise — stop when quality degrades
821
+ *
822
+ * This is a cross-encoder-like approach using hand-crafted features instead
823
+ * of a neural model. No ML dependencies. Deterministic.
824
+ */
825
+
826
+ interface RerankInput {
827
+ /** Task description */
828
+ task: string;
829
+ /** BM25 candidates from tfidf.query() */
830
+ candidates: SemanticMatch[];
831
+ /** The TF-IDF index (for IDF weights) */
832
+ index: TfIdfIndex;
833
+ /** File contents for bigram proximity analysis */
834
+ fileContents: Map<string, string>;
835
+ /** Dependency edges: from → to[] */
836
+ dependencies: Map<string, string[]>;
837
+ /** All file paths in the project */
838
+ allFilePaths: string[];
839
+ }
840
+ interface RerankResult {
841
+ /** Reranked and filtered files — only high-quality matches */
842
+ files: RerankedFile[];
843
+ /** Files that were cut by the quality gate */
844
+ filtered: FilteredFile[];
845
+ /** The quality threshold used */
846
+ qualityThreshold: number;
847
+ /** Telemetry data for observability and debugging */
848
+ telemetry: RerankTelemetry;
849
+ }
850
+ interface RerankTelemetry {
851
+ /** Total candidates received from BM25 */
852
+ candidatesIn: number;
853
+ /** Files that passed the quality gate */
854
+ candidatesOut: number;
855
+ /** Files filtered out */
856
+ candidatesFiltered: number;
857
+ /** Timing in milliseconds */
858
+ durationMs: number;
859
+ /** Signal weight configuration used */
860
+ weights: typeof WEIGHTS;
861
+ /** Quality gate thresholds used */
862
+ gateConfig: {
863
+ absoluteFloor: number;
864
+ elbowDropRatio: number;
865
+ minTermCoverage: number;
866
+ };
867
+ /** Aggregate signal statistics across all candidates (before gate) */
868
+ signalStats: {
869
+ termCoverage: {
870
+ min: number;
871
+ max: number;
872
+ mean: number;
873
+ median: number;
874
+ };
875
+ termSpecificity: {
876
+ min: number;
877
+ max: number;
878
+ mean: number;
879
+ median: number;
880
+ };
881
+ bigramProximity: {
882
+ min: number;
883
+ max: number;
884
+ mean: number;
885
+ median: number;
886
+ };
887
+ dependencySignal: {
888
+ min: number;
889
+ max: number;
890
+ mean: number;
891
+ median: number;
892
+ };
893
+ pathRelevance: {
894
+ min: number;
895
+ max: number;
896
+ mean: number;
897
+ median: number;
898
+ };
899
+ };
900
+ /** Filter reason breakdown: reason → count */
901
+ filterReasons: Record<string, number>;
902
+ /** Score distribution: [min, p25, p50, p75, max] across all scored candidates */
903
+ scoreDistribution: [number, number, number, number, number];
904
+ /** Number of unique query terms */
905
+ queryTermCount: number;
906
+ /** Size of the dependency relevance cone */
907
+ relevanceConeSize: number;
908
+ }
909
+ interface RerankedFile {
910
+ filePath: string;
911
+ /** Final reranked score (0-1) */
912
+ score: number;
913
+ /** Original BM25 score */
914
+ bm25Score: number;
915
+ /** Individual signal scores */
916
+ signals: {
917
+ termCoverage: number;
918
+ termSpecificity: number;
919
+ bigramProximity: number;
920
+ dependencySignal: number;
921
+ pathRelevance: number;
922
+ };
923
+ }
924
+ interface FilteredFile {
925
+ filePath: string;
926
+ score: number;
927
+ reason: string;
928
+ }
929
+ declare const WEIGHTS: {
930
+ termCoverage: number;
931
+ termSpecificity: number;
932
+ bigramProximity: number;
933
+ dependencySignal: number;
934
+ pathRelevance: number;
935
+ };
936
+ /**
937
+ * Rerank BM25 candidates using multi-signal scoring + quality gate.
938
+ * Returns only files that pass the quality threshold.
939
+ */
940
+ declare function rerank(input: RerankInput): RerankResult;
941
+
942
+ declare function countTokensTiktoken(text: string): number;
943
+ declare function countTokensChars4(sizeInBytes: number): number;
944
+ declare function estimateTokens(content: string, sizeInBytes: number, method?: 'chars4' | 'tiktoken'): number;
945
+ declare function estimateFileTokens(filePath: string, method?: 'chars4' | 'tiktoken'): Promise<number>;
946
+ declare function freeEncoder(): void;
947
+
851
948
  type LogLevel = 'debug' | 'info' | 'warn' | 'error';
852
949
  interface LogEntry {
853
950
  level: LogLevel;
@@ -877,173 +974,36 @@ declare class CtoError extends Error {
877
974
  declare function isCtoError(err: unknown): err is CtoError;
878
975
  declare function wrapError(err: unknown, code: CtoErrorCode, module: string, context?: Record<string, unknown>): CtoError;
879
976
 
880
- declare function countTokensTiktoken(text: string): number;
881
- declare function countTokensChars4(sizeInBytes: number): number;
882
- declare function estimateTokens(content: string, sizeInBytes: number, method?: 'chars4' | 'tiktoken'): number;
883
- declare function estimateFileTokens(filePath: string, method?: 'chars4' | 'tiktoken'): Promise<number>;
884
- declare function freeEncoder(): void;
885
-
886
- type MonorepoTool = 'npm-workspaces' | 'yarn-workspaces' | 'pnpm-workspaces' | 'turborepo' | 'nx' | 'lerna' | 'none';
887
- interface PackageInfo {
888
- name: string;
889
- path: string;
890
- relativePath: string;
891
- files: number;
892
- tokens: number;
893
- dependencies: string[];
894
- dependents: string[];
895
- isShared: boolean;
896
- entryPoints: string[];
897
- }
898
- interface CrossPackageEdge {
899
- from: string;
900
- to: string;
901
- files: number;
902
- type: 'dependency' | 'devDependency' | 'peer';
903
- }
904
- interface MonorepoAnalysis {
905
- detected: boolean;
906
- tool: MonorepoTool;
907
- rootPath: string;
908
- packages: PackageInfo[];
909
- sharedPackages: PackageInfo[];
910
- crossPackageEdges: CrossPackageEdge[];
911
- isolationScore: number;
912
- totalTokens: number;
913
- packageTokenMap: Record<string, number>;
914
- }
915
- interface PackageContextResult {
916
- targetPackage: string;
917
- includedPackages: string[];
918
- excludedPackages: string[];
919
- originalTokens: number;
920
- optimizedTokens: number;
921
- savedTokens: number;
922
- savedPercent: number;
923
- }
924
- declare function detectMonorepoTool(rootPath: string): Promise<MonorepoTool>;
925
- declare function analyzeMonorepo(rootPath: string, analysis?: ProjectAnalysis): Promise<MonorepoAnalysis>;
926
- declare function selectPackageContext(monorepo: MonorepoAnalysis, targetPackage: string): PackageContextResult;
927
- declare function renderMonorepoAnalysis(mono: MonorepoAnalysis): string;
928
- declare function renderPackageContext(result: PackageContextResult): string;
929
-
930
- interface QualityGateConfig {
931
- threshold: number;
932
- failOnSecrets: boolean;
933
- failOnRegression: boolean;
934
- regressionLimit: number;
935
- baselinePath: string;
936
- secretSeverities: string[];
937
- }
938
- interface QualityGateCheck {
939
- name: string;
940
- passed: boolean;
941
- detail: string;
942
- severity: 'error' | 'warning' | 'info';
943
- }
944
- interface Baseline {
945
- score: number;
946
- grade: string;
947
- timestamp: string;
948
- commit?: string;
949
- branch?: string;
950
- dimensions: Record<string, number>;
951
- }
952
- interface QualityGateResult {
953
- passed: boolean;
954
- score: number;
955
- grade: string;
956
- previousScore: number | null;
957
- delta: number | null;
958
- checks: QualityGateCheck[];
959
- baseline: Baseline | null;
960
- prComment: string;
961
- summary: string;
962
- }
963
- declare const DEFAULT_GATE_CONFIG: QualityGateConfig;
964
- declare function loadBaseline(projectPath: string, baselinePath?: string): Promise<Baseline | null>;
965
- declare function saveBaseline(projectPath: string, score: ContextScore, commit?: string, branch?: string, baselinePath?: string): Promise<void>;
966
- declare function runQualityGate(score: ContextScore, analysis: ProjectAnalysis, secretFindings: {
967
- severity: string;
968
- }[], config?: Partial<QualityGateConfig>): Promise<QualityGateResult>;
969
-
970
- interface ReviewResult {
971
- branch: string;
972
- baseBranch: string;
973
- isGitRepo: boolean;
974
- changedFiles: ReviewFile[];
975
- totalLinesChanged: number;
976
- breakingChanges: BreakingChange[];
977
- missingFiles: MissingFile[];
978
- impactRadius: ImpactRadius;
979
- reviewQuality: ReviewQuality;
980
- reviewPrompt: string;
981
- renderedSummary: string;
982
- }
983
- interface ReviewFile {
984
- relativePath: string;
985
- changeType: 'added' | 'modified' | 'deleted' | 'renamed';
986
- linesAdded: number;
987
- linesRemoved: number;
988
- riskScore: number;
989
- kind: string;
990
- hunks: DiffHunk[];
991
- hasExportChanges: boolean;
992
- hasTypeChanges: boolean;
993
- }
994
- interface DiffHunk {
995
- startLine: number;
996
- endLine: number;
997
- header: string;
998
- additions: string[];
999
- deletions: string[];
1000
- }
1001
- interface BreakingChange {
1002
- file: string;
1003
- type: 'export-removed' | 'export-renamed' | 'type-changed' | 'interface-changed' | 'function-signature' | 'enum-modified' | 'default-export-changed';
1004
- severity: 'critical' | 'high' | 'medium';
1005
- description: string;
1006
- affectedFiles: string[];
1007
- line?: number;
1008
- }
1009
- interface MissingFile {
1010
- file: string;
1011
- reason: string;
1012
- severity: 'high' | 'medium' | 'low';
1013
- relatedChangedFile: string;
1014
- relationship: 'imports' | 'imported-by' | 'sibling-type' | 'test' | 'co-located';
1015
- }
1016
- interface ImpactRadius {
1017
- directlyAffected: number;
1018
- transitivelyAffected: number;
1019
- totalAffected: number;
1020
- affectedTests: number;
1021
- riskScore: number;
1022
- hotspots: {
1023
- file: string;
1024
- dependents: number;
1025
- riskScore: number;
1026
- }[];
1027
- }
1028
- interface ReviewQuality {
1029
- score: number;
1030
- grade: string;
1031
- factors: ReviewFactor[];
1032
- }
1033
- interface ReviewFactor {
1034
- name: string;
1035
- score: number;
1036
- weight: number;
1037
- detail: string;
977
+ declare function scanContentForSecrets(content: string, filePath: string, customPatterns?: string[], extraPiiSafeDomains?: Set<string>): SecretFinding[];
978
+ declare function scanFileForSecrets(filePath: string, projectPath: string, customPatterns?: string[]): Promise<SecretFinding[]>;
979
+ declare function scanProjectForSecrets(projectPath: string, filePaths: string[], customPatterns?: string[]): Promise<SecretFinding[]>;
980
+ declare function sanitizeContent(content: string, customPatterns?: string[]): string;
981
+ interface AuditResult {
982
+ findings: SecretFinding[];
983
+ summary: {
984
+ totalFiles: number;
985
+ filesScanned: number;
986
+ filesWithSecrets: number;
987
+ totalFindings: number;
988
+ bySeverity: {
989
+ critical: number;
990
+ high: number;
991
+ medium: number;
992
+ low: number;
993
+ };
994
+ byType: Record<string, number>;
995
+ };
996
+ recommendations: string[];
1038
997
  }
1039
- interface ReviewOptions {
1040
- baseBranch?: string;
1041
- depth?: number;
1042
- includeTests?: boolean;
1043
- maxPromptFiles?: number;
1044
- maxPromptTokens?: number;
998
+ interface AuditOptions {
999
+ customPatterns?: string[];
1000
+ entropyThreshold?: number;
1001
+ includePII?: boolean;
1002
+ useAllowlist?: boolean;
1003
+ incrementalScan?: boolean;
1004
+ severityOverrides?: Partial<Record<SecretType, SecretFinding['severity']>>;
1005
+ piiSafeDomains?: string[];
1045
1006
  }
1046
- declare function analyzeForReview(analysis: ProjectAnalysis, options?: ReviewOptions): Promise<ReviewResult>;
1047
- declare function renderReviewSummary(branch: string, baseBranch: string, changedFiles: ReviewFile[], breakingChanges: BreakingChange[], missingFiles: MissingFile[], impactRadius: ImpactRadius, reviewQuality: ReviewQuality): string;
1007
+ declare function auditProject(projectPath: string, filePaths: string[], options?: AuditOptions): Promise<AuditResult>;
1048
1008
 
1049
- export { type Baseline, type BenchmarkResult, type BreakingChange, type ChangeType, type ChangedFile, type CompilabilityMetrics, type CompilabilityResult, type CompileProofResult, type CompileProofStrategy, type ContextScore, type CrossPackageEdge, type CrossRepoModel, type CrossRepoPrediction, CtoError, type CtoErrorCode, DEFAULT_GATE_CONFIG, type DiffHunk, type DimensionScore, type FeedbackEntry, type FeedbackInsight, type FeedbackModel, type FeedbackOutcome, type FileChangeEvent, type Grade, type ImpactRadius, type LogEntry, type LogLevel, type Logger, MODEL_REGISTRY, type MissingFile, type ModelOptimization, type ModelProfile, type MonorepoAnalysis, type MonorepoTool, type MultiModelResult, type PRContextOptions, type PRContextResult, type PackageContextResult, type PackageInfo, type PredictionResult, type PredictorModel, type ProjectFingerprint, ProjectWatcher, type QualityBenchmarkResult, type QualityGateCheck, type QualityGateConfig, type QualityGateResult, type QualityMetrics, type ReviewFactor, type ReviewFile, type ReviewOptions, type ReviewQuality, type ReviewResult, type ScoreInsight, type SelectionInput, type SemanticAnalysis, type SemanticDomain, type SemanticFingerprint, type StrategyResult, type TeamFeedbackExport, type WatcherOptions, analyzeForReview, analyzeMonorepo, analyzeProject, analyzeSemantics, bfsBidirectional, buildAdjacencyList, buildProjectGraph, calculateCoverage, classifyFileKind, computeContextScore, computeFingerprint, configureCache, countTokensChars4, countTokensTiktoken, createLogger, createProject, detectMonorepoTool, detectStack, estimateFileTokens, estimateTokens, exportFeedbackForTeam, freeEncoder, generatePRContext, getActiveWatchers, getCTODir, getCacheStats, getCachedAnalysis, getConfigPath, getCrossRepoStats, getFeedbackBoosts, getModelStats, getPolicyPath, getPredictorBoosts, getPruneLevelForRisk, importTeamFeedback, initProjectConfig, invalidateCache, isCtoError, loadBaseline, loadConfig, loadFeedbackModel, loadGlobalModel, loadModel, loadPolicyFromYAML, matchGlob, optimizeBudget, optimizeForModels, predictFromCrossRepo, predictRelevantFiles, pruneFile, pruneFiles, recordCrossRepoSelection, recordFeedback, recordSelection, renderBenchmark, renderCompilabilityBenchmark, renderCompileProof, renderContextScore, renderCrossRepoReport, renderFeedbackReport, renderMonorepoAnalysis, renderMultiModelResult, renderPackageContext, renderQualityBenchmark, renderReviewSummary, renderSemanticAnalysis, runBenchmark, runCompilabilityBenchmark, runCompileProof, runQualityBenchmark, runQualityGate, saveBaseline, saveConfig, scoreAllFiles, scoreFile, selectContext, selectPackageContext, semanticBoosts, setJsonLogging, setLogLevel, unwatchAll, unwatchProject, walkProject, watchProject, wilsonLowerBound, wrapError };
1009
+ export { type AssignmentResult, type ContextPipelineInput, type ContextPipelineResult, CtoError, type CtoErrorCode, type DocumentVector, type Experiment, type ExperimentConclusion, type ExperimentGroup, type FilteredFile, type GroupMetrics, type ImportSpec, type IndexCacheStats, type LearnerBoost, type LearnerBoostInput, type LearnerModel, type LogEntry, type LogLevel, type Logger, type MultiRepoResult, type PatternStats, type RerankInput, type RerankResult, type RerankedFile, type SecretFinding, type SecretType, type SelectionInput, type SemanticMatch, type SemanticScore, type SiblingMatch, type SiblingRepo, type SignificanceResult, type SupportedLanguage, type TfIdfIndex, analyzeProject, assignGroup, auditProject, bfsBidirectional, boostByPath, buildAdjacencyList, buildIndex, buildIndexCached, buildProjectGraph, calculateCoverage, classifyFileKind, countTokensChars4, countTokensTiktoken, createExperiment, createLogger, createProject, detectLanguage, detectStack, discoverSiblingRepos, estimateComplexity, estimateFileTokens, estimateTokens, extractPattern, freeEncoder, getActiveExperiment, getCacheInfo, getConcludedExperiments, getLearnerBoosts, getLearnerStats, getPruneLevelForRisk, invalidateCache, isCtoError, loadExperiments, loadLearner, optimizeBudget, parseAllPolyglotImports, parseImports, parseSiblingPaths, pruneFile, pruneFiles, query, querySiblingRepos, recordOutcome, recordSelection, renderExperimentSummary, renderMultiRepoSummary, rerank, runContextPipeline, sanitizeContent, saveExperiments, saveLearner, scanContentForSecrets, scanFileForSecrets, scanProjectForSecrets, scoreAllFiles, scoreFile, selectContext, setJsonLogging, setLogLevel, similarity, testSignificance, tokenize, walkProject, wrapError };