cto-ai-cli 6.1.0 → 8.0.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.
- package/README.md +264 -63
- package/dist/cli/index.js +7732 -1729
- package/dist/engine/index.d.ts +1373 -14
- package/dist/engine/index.js +6731 -2110
- package/dist/mcp/index.js +3750 -430
- package/package.json +1 -1
package/dist/engine/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ interface ProjectGraph {
|
|
|
43
43
|
interface GraphEdge {
|
|
44
44
|
from: string;
|
|
45
45
|
to: string;
|
|
46
|
-
type: 'import' | 'export' | 're-export';
|
|
46
|
+
type: 'import' | 'export' | 're-export' | 'call';
|
|
47
47
|
}
|
|
48
48
|
interface HubNode {
|
|
49
49
|
relativePath: string;
|
|
@@ -247,17 +247,6 @@ interface SelectionInput {
|
|
|
247
247
|
}
|
|
248
248
|
declare function selectContext(input: SelectionInput): Promise<ContextSelection>;
|
|
249
249
|
|
|
250
|
-
declare function scoreAllFiles(files: AnalyzedFile[], graph: ProjectGraph, weights?: RiskWeights): void;
|
|
251
|
-
declare function scoreFile(file: AnalyzedFile, graph: ProjectGraph, weights?: RiskWeights): number;
|
|
252
|
-
|
|
253
|
-
declare function calculateCoverage(targetPaths: string[], includedPaths: string[], allFiles: AnalyzedFile[], graph: ProjectGraph, depth?: number): CoverageResult;
|
|
254
|
-
|
|
255
|
-
declare function getPruneLevelForRisk(riskScore: number): PruneLevel;
|
|
256
|
-
declare function optimizeBudget(files: AnalyzedFile[], budget: number): Promise<BudgetPlan>;
|
|
257
|
-
|
|
258
|
-
declare function pruneFile(file: AnalyzedFile, level: PruneLevel): Promise<PrunedContent>;
|
|
259
|
-
declare function pruneFiles(files: AnalyzedFile[], levelFn: (file: AnalyzedFile) => PruneLevel): Promise<PrunedContent[]>;
|
|
260
|
-
|
|
261
250
|
/**
|
|
262
251
|
* TF-IDF Semantic Matching Engine
|
|
263
252
|
*
|
|
@@ -283,6 +272,7 @@ declare function pruneFiles(files: AnalyzedFile[], levelFn: (file: AnalyzedFile)
|
|
|
283
272
|
interface TfIdfIndex {
|
|
284
273
|
documents: Map<string, DocumentVector>;
|
|
285
274
|
idf: Map<string, number>;
|
|
275
|
+
docFreq: Map<string, number>;
|
|
286
276
|
avgDocLength: number;
|
|
287
277
|
totalDocs: number;
|
|
288
278
|
}
|
|
@@ -306,8 +296,10 @@ declare function buildIndex(files: {
|
|
|
306
296
|
/**
|
|
307
297
|
* Query the index with a task description.
|
|
308
298
|
* Returns files ranked by semantic relevance (BM25 scoring).
|
|
299
|
+
*
|
|
300
|
+
* @param expandSynonyms - If true, expands query terms with synonyms for better recall
|
|
309
301
|
*/
|
|
310
|
-
declare function query(index: TfIdfIndex, taskDescription: string, maxResults?: number): SemanticMatch[];
|
|
302
|
+
declare function query(index: TfIdfIndex, taskDescription: string, maxResults?: number, expandSynonyms?: boolean): SemanticMatch[];
|
|
311
303
|
/**
|
|
312
304
|
* Compute pairwise similarity between two documents in the index.
|
|
313
305
|
* Useful for finding related files (e.g., "what other files are similar to auth.ts?")
|
|
@@ -323,8 +315,177 @@ declare function tokenize(text: string): string[];
|
|
|
323
315
|
* Boost TF-IDF scores based on file path relevance to the task.
|
|
324
316
|
* This catches cases where the file content doesn't mention the task terms
|
|
325
317
|
* but the file path does (e.g., task "fix auth" → src/auth/middleware.ts).
|
|
318
|
+
*
|
|
319
|
+
* Scoring:
|
|
320
|
+
* - Directory segment match: 0.4 per term (structural relevance)
|
|
321
|
+
* - Filename match: 0.25 per term (weaker — many files share names)
|
|
322
|
+
* - Multiple directory matches multiply: "cache" + "seller" in path = 0.8
|
|
326
323
|
*/
|
|
327
324
|
declare function boostByPath(matches: SemanticMatch[], allFiles: string[], taskDescription: string): SemanticMatch[];
|
|
325
|
+
declare function boostByLayer(matches: SemanticMatch[], allFiles: string[], taskDescription: string): SemanticMatch[];
|
|
326
|
+
/**
|
|
327
|
+
* Boost BM25 results by import chain proximity.
|
|
328
|
+
*
|
|
329
|
+
* Problem: In Java/TS projects, interfaces and type files have minimal content
|
|
330
|
+
* (8-20 lines). BM25 can't rank them because there aren't enough tokens.
|
|
331
|
+
* But they are IMPORTED by files that DO rank well.
|
|
332
|
+
*
|
|
333
|
+
* Solution: For each top-K BM25 result, find files it imports and files that
|
|
334
|
+
* import it. Give those a score boost proportional to the parent's score.
|
|
335
|
+
* Files imported by MULTIPLE top matches get proportionally more boost.
|
|
336
|
+
*
|
|
337
|
+
* @param matches - BM25 results (already scored)
|
|
338
|
+
* @param dependencies - Map of filePath → files it imports
|
|
339
|
+
* @param topK - How many top matches to expand (default: 10)
|
|
340
|
+
* @param boostFactor - How much of the parent's score to transfer (default: 0.4)
|
|
341
|
+
*/
|
|
342
|
+
declare function boostByImports(matches: SemanticMatch[], dependencies: Map<string, string[]>, topK?: number, boostFactor?: number): SemanticMatch[];
|
|
343
|
+
/**
|
|
344
|
+
* Reciprocal Rank Fusion — state-of-the-art multi-signal ranking.
|
|
345
|
+
* Used by Elasticsearch 8, Pinecone, Cohere.
|
|
346
|
+
*
|
|
347
|
+
* Problem: Additive boosting (BM25 + path + import) saturates at 1.0.
|
|
348
|
+
* When multiple boosts stack, all top files get score=1.0 and ranking
|
|
349
|
+
* becomes arbitrary. This is why irrelevant files appear in top-K.
|
|
350
|
+
*
|
|
351
|
+
* Solution: Rank files independently by each signal, then fuse rankings.
|
|
352
|
+
* RRF_score(d) = Σ weight_i / (k + rank_i(d))
|
|
353
|
+
* where k=60 (smoothing constant, standard in literature).
|
|
354
|
+
*
|
|
355
|
+
* Files that rank well across MULTIPLE signals naturally float to the top.
|
|
356
|
+
* No saturation, no arbitrary caps.
|
|
357
|
+
*
|
|
358
|
+
* @param bm25Matches - Raw BM25 results
|
|
359
|
+
* @param allFiles - All file paths in the project
|
|
360
|
+
* @param taskDescription - The query/task
|
|
361
|
+
* @param dependencies - Import graph
|
|
362
|
+
* @param k - RRF smoothing constant (default: 60, standard)
|
|
363
|
+
*/
|
|
364
|
+
declare function reciprocalRankFusion$1(bm25Matches: SemanticMatch[], allFiles: string[], taskDescription: string, dependencies: Map<string, string[]>, k?: number): SemanticMatch[];
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Persistent TF-IDF Index Cache
|
|
368
|
+
*
|
|
369
|
+
* Problem: Building a TF-IDF index reads every source file and tokenizes it.
|
|
370
|
+
* For a 50K-file repo, that's 5-10 seconds per query. With 20K devs running
|
|
371
|
+
* queries concurrently, re-indexing on every call is unacceptable.
|
|
372
|
+
*
|
|
373
|
+
* Solution: Persist the index to disk with per-file mtime tracking.
|
|
374
|
+
* On subsequent queries, only re-index files that changed since last build.
|
|
375
|
+
*
|
|
376
|
+
* Storage: .cto/index-cache.json
|
|
377
|
+
* {
|
|
378
|
+
* version: 2,
|
|
379
|
+
* builtAt: ISO timestamp,
|
|
380
|
+
* files: { [relativePath]: { mtime: number, terms: { [term]: count }, length: number } },
|
|
381
|
+
* idf: { [term]: number },
|
|
382
|
+
* avgDocLength: number,
|
|
383
|
+
* totalDocs: number,
|
|
384
|
+
* }
|
|
385
|
+
*
|
|
386
|
+
* Invalidation:
|
|
387
|
+
* - Per-file: mtime changed → re-tokenize that file
|
|
388
|
+
* - New files: not in cache → tokenize and add
|
|
389
|
+
* - Deleted files: in cache but not on disk → remove
|
|
390
|
+
* - Version bump: cache format changed → full rebuild
|
|
391
|
+
*
|
|
392
|
+
* The IDF values are recomputed after any incremental update because
|
|
393
|
+
* document frequency changes affect all terms globally.
|
|
394
|
+
*/
|
|
395
|
+
|
|
396
|
+
interface IndexCacheStats {
|
|
397
|
+
/** Total files in the index */
|
|
398
|
+
totalFiles: number;
|
|
399
|
+
/** Files that were re-indexed (changed or new) */
|
|
400
|
+
updatedFiles: number;
|
|
401
|
+
/** Files removed from cache (deleted from disk) */
|
|
402
|
+
removedFiles: number;
|
|
403
|
+
/** Files reused from cache (unchanged) */
|
|
404
|
+
cachedFiles: number;
|
|
405
|
+
/** Whether the cache existed before this build */
|
|
406
|
+
cacheHit: boolean;
|
|
407
|
+
/** Time to build/update the index (ms) */
|
|
408
|
+
buildTimeMs: number;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Build or update a TF-IDF index with disk caching.
|
|
412
|
+
*
|
|
413
|
+
* First call: builds full index and writes cache to .cto/index-cache.json
|
|
414
|
+
* Subsequent calls: reads cache, updates only changed files, rewrites cache
|
|
415
|
+
*
|
|
416
|
+
* @param projectPath - Root of the project (for .cto/ directory)
|
|
417
|
+
* @param files - All files to index: { relativePath, absolutePath, content? }
|
|
418
|
+
* If content is provided, it's used directly. Otherwise, the file is read from disk.
|
|
419
|
+
* @returns The TF-IDF index + stats about cache hits/misses
|
|
420
|
+
*/
|
|
421
|
+
declare function buildIndexCached(projectPath: string, files: {
|
|
422
|
+
relativePath: string;
|
|
423
|
+
absolutePath: string;
|
|
424
|
+
content?: string;
|
|
425
|
+
}[]): {
|
|
426
|
+
index: TfIdfIndex;
|
|
427
|
+
stats: IndexCacheStats;
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* Invalidate the entire cache (force full rebuild on next call).
|
|
431
|
+
*/
|
|
432
|
+
declare function invalidateCache(projectPath: string): void;
|
|
433
|
+
/**
|
|
434
|
+
* Get cache stats without rebuilding.
|
|
435
|
+
*/
|
|
436
|
+
declare function getCacheInfo(projectPath: string): {
|
|
437
|
+
exists: boolean;
|
|
438
|
+
fileCount: number;
|
|
439
|
+
builtAt: string | null;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Query Intent Parsing
|
|
444
|
+
*
|
|
445
|
+
* Before searching, parse the task description into a structured intent:
|
|
446
|
+
* - action: what the developer wants to do (fix, add, refactor, trace)
|
|
447
|
+
* - entities: domain objects mentioned (cache, user, order, chart)
|
|
448
|
+
* - operations: specific operations (delete, create, update, validate)
|
|
449
|
+
* - layers: architectural layers mentioned (controller, service, repository)
|
|
450
|
+
* - qualifiers: narrowing terms (on KVS, in admin, for seller)
|
|
451
|
+
*
|
|
452
|
+
* This structured intent enables:
|
|
453
|
+
* 1. Weighted search: entities get higher BM25 weight than qualifiers
|
|
454
|
+
* 2. Layer-aware expansion: if service layer mentioned, also search use cases
|
|
455
|
+
* 3. Better multi-hop: expand through specific layers, not randomly
|
|
456
|
+
* 4. Smarter chunk selection: prioritize chunks matching entities + operations
|
|
457
|
+
*/
|
|
458
|
+
interface QueryIntent {
|
|
459
|
+
original: string;
|
|
460
|
+
action: ActionType;
|
|
461
|
+
entities: string[];
|
|
462
|
+
operations: string[];
|
|
463
|
+
layers: ArchLayer[];
|
|
464
|
+
qualifiers: string[];
|
|
465
|
+
confidence: number;
|
|
466
|
+
}
|
|
467
|
+
type ActionType = 'fix' | 'add' | 'refactor' | 'trace' | 'test' | 'docs' | 'remove' | 'optimize' | 'unknown';
|
|
468
|
+
type ArchLayer = 'endpoint' | 'usecase' | 'service' | 'repository' | 'cache' | 'client' | 'model' | 'config' | 'queue' | 'middleware';
|
|
469
|
+
/**
|
|
470
|
+
* Parse a task description into a structured query intent.
|
|
471
|
+
*
|
|
472
|
+
* @param task - Raw task description from the developer
|
|
473
|
+
* @returns Structured QueryIntent with action, entities, operations, layers
|
|
474
|
+
*/
|
|
475
|
+
declare function parseQueryIntent(task: string): QueryIntent;
|
|
476
|
+
/**
|
|
477
|
+
* Build a weighted search query from the parsed intent.
|
|
478
|
+
* Entities get highest weight, operations next, qualifiers lowest.
|
|
479
|
+
*
|
|
480
|
+
* @param intent - Parsed query intent
|
|
481
|
+
* @returns Weighted query string with important terms repeated
|
|
482
|
+
*/
|
|
483
|
+
declare function buildWeightedQuery(intent: QueryIntent): string;
|
|
484
|
+
/**
|
|
485
|
+
* Get suggested architectural layers to search based on the intent.
|
|
486
|
+
* If the developer mentions "service", we should also look at use cases and repositories.
|
|
487
|
+
*/
|
|
488
|
+
declare function expandLayers(layers: ArchLayer[]): ArchLayer[];
|
|
328
489
|
|
|
329
490
|
/**
|
|
330
491
|
* Usage Learner — Gets smarter with every use.
|
|
@@ -403,12 +564,1210 @@ declare function getLearnerStats(model: LearnerModel): {
|
|
|
403
564
|
*/
|
|
404
565
|
declare function extractPattern(filePath: string): string;
|
|
405
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Multi-Repo Context Selection
|
|
569
|
+
*
|
|
570
|
+
* Discovers sibling repositories in a workspace and queries them
|
|
571
|
+
* for relevant files when selecting context for a task.
|
|
572
|
+
*
|
|
573
|
+
* How it works:
|
|
574
|
+
* 1. Discover sibling repos (scan parent dir or use explicit paths)
|
|
575
|
+
* 2. For each sibling: list source files, read contents, build TF-IDF index
|
|
576
|
+
* 3. Query each sibling's index with the task description
|
|
577
|
+
* 4. Return ranked matches with repo attribution
|
|
578
|
+
*
|
|
579
|
+
* This is NOT the cross-repo learning system (cross-repo.ts).
|
|
580
|
+
* This is actual multi-repo file discovery and querying.
|
|
581
|
+
*/
|
|
582
|
+
interface SiblingRepo {
|
|
583
|
+
/** Absolute path to the repo root */
|
|
584
|
+
path: string;
|
|
585
|
+
/** Short name (directory name) */
|
|
586
|
+
name: string;
|
|
587
|
+
/** Detected stack (from package.json, tsconfig, etc.) */
|
|
588
|
+
stack: string[];
|
|
589
|
+
/** Number of source files found */
|
|
590
|
+
fileCount: number;
|
|
591
|
+
}
|
|
592
|
+
interface SiblingMatch {
|
|
593
|
+
/** Which sibling repo this file belongs to */
|
|
594
|
+
repoName: string;
|
|
595
|
+
/** Absolute path to the repo */
|
|
596
|
+
repoPath: string;
|
|
597
|
+
/** Relative path within the sibling repo */
|
|
598
|
+
relativePath: string;
|
|
599
|
+
/** Absolute path to the file */
|
|
600
|
+
absolutePath: string;
|
|
601
|
+
/** Semantic relevance score (0-1) */
|
|
602
|
+
score: number;
|
|
603
|
+
/** File content */
|
|
604
|
+
content: string;
|
|
605
|
+
/** Estimated token count */
|
|
606
|
+
tokens: number;
|
|
607
|
+
}
|
|
608
|
+
interface MultiRepoResult {
|
|
609
|
+
/** Sibling repos that were discovered/used */
|
|
610
|
+
siblings: SiblingRepo[];
|
|
611
|
+
/** Top matches from sibling repos, ranked by score */
|
|
612
|
+
matches: SiblingMatch[];
|
|
613
|
+
/** Total time spent indexing + querying (ms) */
|
|
614
|
+
timeMs: number;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Discover sibling repositories by scanning the parent directory.
|
|
618
|
+
* A directory is a "repo" if it contains a known project marker file.
|
|
619
|
+
*/
|
|
620
|
+
declare function discoverSiblingRepos(projectPath: string): SiblingRepo[];
|
|
621
|
+
/**
|
|
622
|
+
* Query sibling repos for files relevant to a task.
|
|
623
|
+
*
|
|
624
|
+
* For each sibling:
|
|
625
|
+
* 1. List source files
|
|
626
|
+
* 2. Build TF-IDF index from file contents
|
|
627
|
+
* 3. Query with task description
|
|
628
|
+
* 4. Return top matches with content
|
|
629
|
+
*
|
|
630
|
+
* @param siblings - Sibling repos to query (from discoverSiblingRepos or explicit paths)
|
|
631
|
+
* @param task - Task description to match against
|
|
632
|
+
* @param maxPerRepo - Max matches per repo (default 5)
|
|
633
|
+
* @param minScore - Minimum semantic score to include (default 0.3)
|
|
634
|
+
*/
|
|
635
|
+
declare function querySiblingRepos(siblings: SiblingRepo[], task: string, maxPerRepo?: number, minScore?: number): MultiRepoResult;
|
|
636
|
+
/**
|
|
637
|
+
* Parse explicit repo paths from a comma-separated string.
|
|
638
|
+
* Resolves relative paths against the current project's parent directory.
|
|
639
|
+
*/
|
|
640
|
+
declare function parseSiblingPaths(pathsStr: string, projectPath: string): SiblingRepo[];
|
|
641
|
+
/**
|
|
642
|
+
* Render multi-repo results for CLI output.
|
|
643
|
+
*/
|
|
644
|
+
declare function renderMultiRepoSummary(result: MultiRepoResult): string;
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Shared Context Pipeline
|
|
648
|
+
*
|
|
649
|
+
* Single function that runs the full context selection pipeline:
|
|
650
|
+
* read files → build TF-IDF index → query → boost → load learner → selectContext
|
|
651
|
+
*
|
|
652
|
+
* Used by both CLI and MCP server. No duplication.
|
|
653
|
+
*/
|
|
654
|
+
|
|
655
|
+
interface ContextPipelineInput {
|
|
656
|
+
projectPath: string;
|
|
657
|
+
task: string;
|
|
658
|
+
analysis: ProjectAnalysis;
|
|
659
|
+
budget?: number;
|
|
660
|
+
/** Optional sibling repos for cross-repo context */
|
|
661
|
+
siblingRepos?: SiblingRepo[];
|
|
662
|
+
}
|
|
663
|
+
interface ContextPipelineResult {
|
|
664
|
+
selection: ContextSelection;
|
|
665
|
+
taskType: string;
|
|
666
|
+
fileContentMap: Map<string, string>;
|
|
667
|
+
semanticMap: Map<string, SemanticMatch>;
|
|
668
|
+
learnerMap: Map<string, LearnerBoost>;
|
|
669
|
+
/** Parsed query intent for downstream chunk selection */
|
|
670
|
+
queryIntent: QueryIntent;
|
|
671
|
+
/** Cross-repo results (only present if siblingRepos were provided) */
|
|
672
|
+
multiRepo?: MultiRepoResult;
|
|
673
|
+
/** Index cache stats (how many files were cached vs rebuilt) */
|
|
674
|
+
indexCacheStats?: IndexCacheStats;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Run the full context selection pipeline.
|
|
678
|
+
* One function, used everywhere. No copy-paste.
|
|
679
|
+
*/
|
|
680
|
+
declare function runContextPipeline(input: ContextPipelineInput): Promise<ContextPipelineResult>;
|
|
681
|
+
|
|
682
|
+
declare function scoreAllFiles(files: AnalyzedFile[], graph: ProjectGraph, weights?: RiskWeights): void;
|
|
683
|
+
declare function scoreFile(file: AnalyzedFile, graph: ProjectGraph, weights?: RiskWeights): number;
|
|
684
|
+
|
|
685
|
+
declare function calculateCoverage(targetPaths: string[], includedPaths: string[], allFiles: AnalyzedFile[], graph: ProjectGraph, depth?: number): CoverageResult;
|
|
686
|
+
|
|
687
|
+
declare function getPruneLevelForRisk(riskScore: number): PruneLevel;
|
|
688
|
+
declare function optimizeBudget(files: AnalyzedFile[], budget: number): Promise<BudgetPlan>;
|
|
689
|
+
|
|
690
|
+
declare function pruneFile(file: AnalyzedFile, level: PruneLevel): Promise<PrunedContent>;
|
|
691
|
+
declare function pruneFiles(files: AnalyzedFile[], levelFn: (file: AnalyzedFile) => PruneLevel): Promise<PrunedContent[]>;
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Synonym Expansion for Query Enhancement
|
|
695
|
+
*
|
|
696
|
+
* Zero dependencies. Zero ML. Pure lookup tables.
|
|
697
|
+
*
|
|
698
|
+
* Expands query terms with domain-specific synonyms to improve recall
|
|
699
|
+
* without requiring exact textual overlap. This bridges the gap between
|
|
700
|
+
* BM25's lexical matching and full semantic embeddings.
|
|
701
|
+
*
|
|
702
|
+
* Example:
|
|
703
|
+
* query("database") → ["database", "db", "repository", "orm", "sql", "prisma"]
|
|
704
|
+
* query("auth") → ["auth", "authentication", "login", "session", "jwt", "token"]
|
|
705
|
+
*
|
|
706
|
+
* Why this works:
|
|
707
|
+
* - Captures common abbreviations (db, auth, repo)
|
|
708
|
+
* - Captures technology-specific terms (prisma, jwt, redis)
|
|
709
|
+
* - Captures conceptual relationships (cache → redis, memcached)
|
|
710
|
+
* - Deterministic, predictable, maintainable
|
|
711
|
+
*
|
|
712
|
+
* Trade-offs vs embeddings:
|
|
713
|
+
* + Zero dependencies, zero latency, zero cost
|
|
714
|
+
* + Deterministic (same query → same expansion)
|
|
715
|
+
* + Easy to debug and extend
|
|
716
|
+
* - Limited to manually curated vocabulary
|
|
717
|
+
* - Doesn't capture novel relationships
|
|
718
|
+
* - Estimated +5-8% recall vs embeddings' +10-15%
|
|
719
|
+
*/
|
|
720
|
+
interface SynonymExpansion {
|
|
721
|
+
original: string;
|
|
722
|
+
expanded: string[];
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Expand a single term with its synonyms.
|
|
726
|
+
* Returns the original term plus all related terms.
|
|
727
|
+
*/
|
|
728
|
+
declare function expandTerm(term: string): string[];
|
|
729
|
+
/**
|
|
730
|
+
* Expand all terms in a query.
|
|
731
|
+
* Deduplicates the result.
|
|
732
|
+
*/
|
|
733
|
+
declare function expandQuery(query: string): string[];
|
|
734
|
+
/**
|
|
735
|
+
* Get expansion details for debugging/telemetry.
|
|
736
|
+
*/
|
|
737
|
+
declare function getExpansionDetails(query: string): SynonymExpansion[];
|
|
738
|
+
/**
|
|
739
|
+
* Get statistics about the synonym dictionary.
|
|
740
|
+
*/
|
|
741
|
+
declare function getSynonymStats(): {
|
|
742
|
+
canonicalTerms: number;
|
|
743
|
+
totalSynonyms: number;
|
|
744
|
+
avgSynonymsPerTerm: number;
|
|
745
|
+
bidirectionalEntries: number;
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Closed-Loop A/B Testing Engine
|
|
750
|
+
*
|
|
751
|
+
* The missing piece: the feedback system records data but never closes the loop.
|
|
752
|
+
* This module adds real experimentation:
|
|
753
|
+
*
|
|
754
|
+
* 1. Define experiments with control + variant strategies
|
|
755
|
+
* 2. Assign requests to groups (deterministic hashing for consistency)
|
|
756
|
+
* 3. Collect outcomes per group
|
|
757
|
+
* 4. Compute statistical significance (z-test for proportions)
|
|
758
|
+
* 5. Auto-promote winning variants when significance threshold met
|
|
759
|
+
*
|
|
760
|
+
* Example experiment:
|
|
761
|
+
* - Control: default composite scoring (semantic 0.55, risk 0.25, learner 0.20)
|
|
762
|
+
* - Variant: reranker-heavy scoring (reranker 0.70, risk 0.15, learner 0.15)
|
|
763
|
+
* - Metric: acceptance rate
|
|
764
|
+
* - Significance: p < 0.05
|
|
765
|
+
*
|
|
766
|
+
* Storage: .cto/experiments.json
|
|
767
|
+
* Design: Pure functions. No external deps. Deterministic assignment.
|
|
768
|
+
*/
|
|
769
|
+
interface Experiment {
|
|
770
|
+
/** Unique experiment ID */
|
|
771
|
+
id: string;
|
|
772
|
+
/** Human-readable name */
|
|
773
|
+
name: string;
|
|
774
|
+
/** What we're testing */
|
|
775
|
+
description: string;
|
|
776
|
+
/** Current status */
|
|
777
|
+
status: 'running' | 'concluded' | 'paused';
|
|
778
|
+
/** When the experiment started */
|
|
779
|
+
startedAt: string;
|
|
780
|
+
/** When it concluded (if applicable) */
|
|
781
|
+
concludedAt?: string;
|
|
782
|
+
/** Traffic split: 0.5 = 50/50 */
|
|
783
|
+
trafficSplit: number;
|
|
784
|
+
/** Minimum observations per group before significance test */
|
|
785
|
+
minObservations: number;
|
|
786
|
+
/** P-value threshold for significance */
|
|
787
|
+
significanceThreshold: number;
|
|
788
|
+
/** Control group config */
|
|
789
|
+
control: ExperimentGroup;
|
|
790
|
+
/** Variant group config */
|
|
791
|
+
variant: ExperimentGroup;
|
|
792
|
+
/** Conclusion (when experiment ends) */
|
|
793
|
+
conclusion?: ExperimentConclusion;
|
|
794
|
+
}
|
|
795
|
+
interface ExperimentGroup {
|
|
796
|
+
/** Group name */
|
|
797
|
+
name: string;
|
|
798
|
+
/** Strategy parameters (passed to the engine) */
|
|
799
|
+
params: Record<string, unknown>;
|
|
800
|
+
/** Collected metrics */
|
|
801
|
+
metrics: GroupMetrics;
|
|
802
|
+
}
|
|
803
|
+
interface GroupMetrics {
|
|
804
|
+
/** Total observations */
|
|
805
|
+
total: number;
|
|
806
|
+
/** Successful outcomes (accepted) */
|
|
807
|
+
successes: number;
|
|
808
|
+
/** Accept rate = successes / total */
|
|
809
|
+
acceptRate: number;
|
|
810
|
+
/** Average time to accept (ms) */
|
|
811
|
+
avgTimeToAccept: number;
|
|
812
|
+
/** Compilable rate */
|
|
813
|
+
compilableRate: number;
|
|
814
|
+
/** Sum of time values (for running average) */
|
|
815
|
+
timeSum: number;
|
|
816
|
+
/** Count of compilable results */
|
|
817
|
+
compilableCount: number;
|
|
818
|
+
}
|
|
819
|
+
interface ExperimentConclusion {
|
|
820
|
+
/** Which group won */
|
|
821
|
+
winner: 'control' | 'variant' | 'no_difference';
|
|
822
|
+
/** Observed p-value */
|
|
823
|
+
pValue: number;
|
|
824
|
+
/** Effect size (difference in accept rates) */
|
|
825
|
+
effectSize: number;
|
|
826
|
+
/** Confidence interval for effect size */
|
|
827
|
+
confidenceInterval: [number, number];
|
|
828
|
+
/** Human-readable summary */
|
|
829
|
+
summary: string;
|
|
830
|
+
}
|
|
831
|
+
interface AssignmentResult {
|
|
832
|
+
/** Which group the request was assigned to */
|
|
833
|
+
group: 'control' | 'variant';
|
|
834
|
+
/** The strategy params for this group */
|
|
835
|
+
params: Record<string, unknown>;
|
|
836
|
+
/** Experiment ID for tracking */
|
|
837
|
+
experimentId: string;
|
|
838
|
+
}
|
|
839
|
+
declare function loadExperiments(projectPath: string): Experiment[];
|
|
840
|
+
declare function saveExperiments(projectPath: string, experiments: Experiment[]): void;
|
|
841
|
+
declare function createExperiment(id: string, name: string, description: string, controlParams: Record<string, unknown>, variantParams: Record<string, unknown>, options?: {
|
|
842
|
+
trafficSplit?: number;
|
|
843
|
+
minObservations?: number;
|
|
844
|
+
significanceThreshold?: number;
|
|
845
|
+
}): Experiment;
|
|
846
|
+
/**
|
|
847
|
+
* Assign a request to control or variant group.
|
|
848
|
+
* Uses deterministic hashing: same (experiment_id, task) → same group.
|
|
849
|
+
* This ensures consistency (retries get the same group).
|
|
850
|
+
*/
|
|
851
|
+
declare function assignGroup(experiment: Experiment, task: string): AssignmentResult | null;
|
|
852
|
+
/**
|
|
853
|
+
* Record an outcome for an experiment group.
|
|
854
|
+
* Updates running statistics and checks for significance.
|
|
855
|
+
*/
|
|
856
|
+
declare function recordOutcome(experiment: Experiment, group: 'control' | 'variant', outcome: {
|
|
857
|
+
accepted: boolean;
|
|
858
|
+
compilable?: boolean;
|
|
859
|
+
timeToAcceptMs?: number;
|
|
860
|
+
}): Experiment;
|
|
861
|
+
interface SignificanceResult {
|
|
862
|
+
/** Two-sided p-value */
|
|
863
|
+
pValue: number;
|
|
864
|
+
/** Z-score */
|
|
865
|
+
zScore: number;
|
|
866
|
+
/** Effect size: variant rate - control rate */
|
|
867
|
+
effectSize: number;
|
|
868
|
+
/** 95% confidence interval for effect size */
|
|
869
|
+
confidenceInterval: [number, number];
|
|
870
|
+
/** Whether the result is significant at the experiment's threshold */
|
|
871
|
+
significant: boolean;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Two-proportion z-test for A/B testing.
|
|
875
|
+
*
|
|
876
|
+
* H0: p_control = p_variant
|
|
877
|
+
* H1: p_control ≠ p_variant (two-sided)
|
|
878
|
+
*
|
|
879
|
+
* This is the standard test for comparing conversion rates.
|
|
880
|
+
*/
|
|
881
|
+
declare function testSignificance(experiment: Experiment): SignificanceResult;
|
|
882
|
+
/**
|
|
883
|
+
* Get the active experiment for this project (if any).
|
|
884
|
+
*/
|
|
885
|
+
declare function getActiveExperiment(experiments: Experiment[]): Experiment | null;
|
|
886
|
+
/**
|
|
887
|
+
* Get all concluded experiments with their results.
|
|
888
|
+
*/
|
|
889
|
+
declare function getConcludedExperiments(experiments: Experiment[]): Experiment[];
|
|
890
|
+
/**
|
|
891
|
+
* Render experiment summary for CLI/dashboard.
|
|
892
|
+
*/
|
|
893
|
+
declare function renderExperimentSummary(experiment: Experiment): string;
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Polyglot Dependency Graph — Import Parsing for Python, Go, Java, Rust
|
|
897
|
+
*
|
|
898
|
+
* Problem: The existing graph.ts uses ts-morph (AST) which only handles TS/JS.
|
|
899
|
+
* For a 20K-dev org with Java, Python, Go, Rust — the dependency graph is empty.
|
|
900
|
+
* No graph → no hub detection → no risk scoring → useless context selection.
|
|
901
|
+
*
|
|
902
|
+
* Solution: Regex-based import parsers for each language. Not AST-accurate, but
|
|
903
|
+
* good enough for dependency graph construction. We don't need perfect resolution;
|
|
904
|
+
* we need to know "file A probably depends on file B" for hub/risk scoring.
|
|
905
|
+
*
|
|
906
|
+
* Each parser:
|
|
907
|
+
* 1. Extracts import specifiers from file content using regex
|
|
908
|
+
* 2. Resolves specifiers to relative file paths within the project
|
|
909
|
+
* 3. Returns edges: { from: relativePath, to: relativePath }
|
|
910
|
+
*
|
|
911
|
+
* Supported languages:
|
|
912
|
+
* - Python: import x, from x import y, relative imports
|
|
913
|
+
* - Go: import "pkg", import ( "pkg" ... )
|
|
914
|
+
* - Java: import com.example.Foo, package declaration
|
|
915
|
+
* - Rust: use crate::x, mod x, use super::x
|
|
916
|
+
*
|
|
917
|
+
* Design: Pure functions. No external deps. Deterministic.
|
|
918
|
+
*/
|
|
919
|
+
|
|
920
|
+
type SupportedLanguage = 'python' | 'go' | 'java' | 'rust' | 'typescript';
|
|
921
|
+
interface ImportSpec {
|
|
922
|
+
/** The raw import specifier as written in the source */
|
|
923
|
+
raw: string;
|
|
924
|
+
/** Whether this is a relative import */
|
|
925
|
+
isRelative: boolean;
|
|
926
|
+
}
|
|
927
|
+
declare function detectLanguage(filePath: string): SupportedLanguage | null;
|
|
928
|
+
/**
|
|
929
|
+
* Parse imports from a non-TS file and resolve to project-relative paths.
|
|
930
|
+
* Returns dependency edges for the project graph.
|
|
931
|
+
*
|
|
932
|
+
* @param filePath - Absolute path to the source file
|
|
933
|
+
* @param relativePath - Project-relative path (e.g., "src/auth/login.py")
|
|
934
|
+
* @param projectPath - Absolute path to the project root
|
|
935
|
+
* @param allRelativePaths - Set of all file paths in the project (for resolution)
|
|
936
|
+
* @param content - Optional file content (read from disk if not provided)
|
|
937
|
+
*/
|
|
938
|
+
declare function parseImports(filePath: string, relativePath: string, projectPath: string, allRelativePaths: Set<string>, content?: string): GraphEdge[];
|
|
939
|
+
/**
|
|
940
|
+
* Parse imports for ALL non-TS files in a project.
|
|
941
|
+
* Call this alongside ts-morph's buildProjectGraph for TS files.
|
|
942
|
+
*/
|
|
943
|
+
declare function parseAllPolyglotImports(files: {
|
|
944
|
+
relativePath: string;
|
|
945
|
+
absolutePath: string;
|
|
946
|
+
content?: string;
|
|
947
|
+
}[], projectPath: string): GraphEdge[];
|
|
948
|
+
/**
|
|
949
|
+
* Estimate cyclomatic complexity from source code using regex.
|
|
950
|
+
* Not AST-accurate but good enough for risk scoring.
|
|
951
|
+
*/
|
|
952
|
+
declare function estimateComplexity(content: string, lang: SupportedLanguage): number;
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Multi-Stage Reranker
|
|
956
|
+
*
|
|
957
|
+
* The problem: BM25 retrieval gets 54% precision. Adding risk scoring drops it
|
|
958
|
+
* to 33% because high-risk irrelevant files fill the budget.
|
|
959
|
+
*
|
|
960
|
+
* The solution: a 3-stage pipeline that turns BM25 candidates into a precision-
|
|
961
|
+
* optimized selection:
|
|
962
|
+
*
|
|
963
|
+
* Stage 1: RETRIEVE (BM25 top-K) — already done by tfidf.ts
|
|
964
|
+
* Stage 2: RERANK (multi-signal rescoring)
|
|
965
|
+
* - Term coverage: what fraction of UNIQUE query terms does the file match?
|
|
966
|
+
* - Term specificity: are the matched terms rare (high IDF) or generic?
|
|
967
|
+
* - Bigram proximity: do query terms appear near each other in the file?
|
|
968
|
+
* - Dependency signal: is this file in the dependency cone of a top match?
|
|
969
|
+
* - Path relevance: does the file path match query terms?
|
|
970
|
+
* Stage 3: QUALITY GATE (adaptive cutoff)
|
|
971
|
+
* - Hard floor: files below absolute threshold are excluded
|
|
972
|
+
* - Elbow detection: find the natural drop-off point in scores
|
|
973
|
+
* - Don't fill budget with noise — stop when quality degrades
|
|
974
|
+
*
|
|
975
|
+
* This is a cross-encoder-like approach using hand-crafted features instead
|
|
976
|
+
* of a neural model. No ML dependencies. Deterministic.
|
|
977
|
+
*/
|
|
978
|
+
|
|
979
|
+
interface RerankInput {
|
|
980
|
+
/** Task description */
|
|
981
|
+
task: string;
|
|
982
|
+
/** BM25 candidates from tfidf.query() */
|
|
983
|
+
candidates: SemanticMatch[];
|
|
984
|
+
/** The TF-IDF index (for IDF weights) */
|
|
985
|
+
index: TfIdfIndex;
|
|
986
|
+
/** File contents for bigram proximity analysis */
|
|
987
|
+
fileContents: Map<string, string>;
|
|
988
|
+
/** Dependency edges: from → to[] */
|
|
989
|
+
dependencies: Map<string, string[]>;
|
|
990
|
+
/** All file paths in the project */
|
|
991
|
+
allFilePaths: string[];
|
|
992
|
+
}
|
|
993
|
+
interface RerankResult {
|
|
994
|
+
/** Reranked and filtered files — only high-quality matches */
|
|
995
|
+
files: RerankedFile[];
|
|
996
|
+
/** Files that were cut by the quality gate */
|
|
997
|
+
filtered: FilteredFile[];
|
|
998
|
+
/** The quality threshold used */
|
|
999
|
+
qualityThreshold: number;
|
|
1000
|
+
/** Telemetry data for observability and debugging */
|
|
1001
|
+
telemetry: RerankTelemetry;
|
|
1002
|
+
}
|
|
1003
|
+
interface RerankTelemetry {
|
|
1004
|
+
/** Total candidates received from BM25 */
|
|
1005
|
+
candidatesIn: number;
|
|
1006
|
+
/** Files that passed the quality gate */
|
|
1007
|
+
candidatesOut: number;
|
|
1008
|
+
/** Files filtered out */
|
|
1009
|
+
candidatesFiltered: number;
|
|
1010
|
+
/** Timing in milliseconds */
|
|
1011
|
+
durationMs: number;
|
|
1012
|
+
/** Signal weight configuration used */
|
|
1013
|
+
weights: typeof WEIGHTS;
|
|
1014
|
+
/** Quality gate thresholds used */
|
|
1015
|
+
gateConfig: {
|
|
1016
|
+
absoluteFloor: number;
|
|
1017
|
+
elbowDropRatio: number;
|
|
1018
|
+
minTermCoverage: number;
|
|
1019
|
+
};
|
|
1020
|
+
/** Aggregate signal statistics across all candidates (before gate) */
|
|
1021
|
+
signalStats: {
|
|
1022
|
+
termCoverage: {
|
|
1023
|
+
min: number;
|
|
1024
|
+
max: number;
|
|
1025
|
+
mean: number;
|
|
1026
|
+
median: number;
|
|
1027
|
+
};
|
|
1028
|
+
termSpecificity: {
|
|
1029
|
+
min: number;
|
|
1030
|
+
max: number;
|
|
1031
|
+
mean: number;
|
|
1032
|
+
median: number;
|
|
1033
|
+
};
|
|
1034
|
+
bigramProximity: {
|
|
1035
|
+
min: number;
|
|
1036
|
+
max: number;
|
|
1037
|
+
mean: number;
|
|
1038
|
+
median: number;
|
|
1039
|
+
};
|
|
1040
|
+
dependencySignal: {
|
|
1041
|
+
min: number;
|
|
1042
|
+
max: number;
|
|
1043
|
+
mean: number;
|
|
1044
|
+
median: number;
|
|
1045
|
+
};
|
|
1046
|
+
pathRelevance: {
|
|
1047
|
+
min: number;
|
|
1048
|
+
max: number;
|
|
1049
|
+
mean: number;
|
|
1050
|
+
median: number;
|
|
1051
|
+
};
|
|
1052
|
+
};
|
|
1053
|
+
/** Filter reason breakdown: reason → count */
|
|
1054
|
+
filterReasons: Record<string, number>;
|
|
1055
|
+
/** Score distribution: [min, p25, p50, p75, max] across all scored candidates */
|
|
1056
|
+
scoreDistribution: [number, number, number, number, number];
|
|
1057
|
+
/** Number of unique query terms */
|
|
1058
|
+
queryTermCount: number;
|
|
1059
|
+
/** Size of the dependency relevance cone */
|
|
1060
|
+
relevanceConeSize: number;
|
|
1061
|
+
}
|
|
1062
|
+
interface RerankedFile {
|
|
1063
|
+
filePath: string;
|
|
1064
|
+
/** Final reranked score (0-1) */
|
|
1065
|
+
score: number;
|
|
1066
|
+
/** Original BM25 score */
|
|
1067
|
+
bm25Score: number;
|
|
1068
|
+
/** Individual signal scores */
|
|
1069
|
+
signals: {
|
|
1070
|
+
termCoverage: number;
|
|
1071
|
+
termSpecificity: number;
|
|
1072
|
+
bigramProximity: number;
|
|
1073
|
+
dependencySignal: number;
|
|
1074
|
+
pathRelevance: number;
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
interface FilteredFile {
|
|
1078
|
+
filePath: string;
|
|
1079
|
+
score: number;
|
|
1080
|
+
reason: string;
|
|
1081
|
+
}
|
|
1082
|
+
declare const WEIGHTS: {
|
|
1083
|
+
termCoverage: number;
|
|
1084
|
+
termSpecificity: number;
|
|
1085
|
+
bigramProximity: number;
|
|
1086
|
+
dependencySignal: number;
|
|
1087
|
+
pathRelevance: number;
|
|
1088
|
+
};
|
|
1089
|
+
/**
|
|
1090
|
+
* Rerank BM25 candidates using multi-signal scoring + quality gate.
|
|
1091
|
+
* Returns only files that pass the quality threshold.
|
|
1092
|
+
*/
|
|
1093
|
+
declare function rerank(input: RerankInput): RerankResult;
|
|
1094
|
+
|
|
406
1095
|
declare function countTokensTiktoken(text: string): number;
|
|
407
1096
|
declare function countTokensChars4(sizeInBytes: number): number;
|
|
408
1097
|
declare function estimateTokens(content: string, sizeInBytes: number, method?: 'chars4' | 'tiktoken'): number;
|
|
409
1098
|
declare function estimateFileTokens(filePath: string, method?: 'chars4' | 'tiktoken'): Promise<number>;
|
|
410
1099
|
declare function freeEncoder(): void;
|
|
411
1100
|
|
|
1101
|
+
/**
|
|
1102
|
+
* Corpus-Learned Semantic Expansion
|
|
1103
|
+
*
|
|
1104
|
+
* Zero dependencies. Pure math.
|
|
1105
|
+
*
|
|
1106
|
+
* Problem: BM25 is lexical — "fix authentication" won't match a file about
|
|
1107
|
+
* "OAuth2 token validator" because the terms don't overlap.
|
|
1108
|
+
*
|
|
1109
|
+
* Solution: Learn term associations from the codebase itself using
|
|
1110
|
+
* Pointwise Mutual Information (PMI). If "auth" and "oauth" frequently
|
|
1111
|
+
* co-occur in the same files, they're semantically related.
|
|
1112
|
+
*
|
|
1113
|
+
* How it works:
|
|
1114
|
+
* 1. Build a co-occurrence matrix from the TF-IDF index
|
|
1115
|
+
* 2. Compute PMI for each term pair: PMI(a,b) = log(P(a,b) / (P(a)·P(b)))
|
|
1116
|
+
* 3. For each query term, find the top-K associated terms
|
|
1117
|
+
* 4. Expand the query with these terms (weighted by PMI strength)
|
|
1118
|
+
*
|
|
1119
|
+
* This bridges vocabulary gaps like:
|
|
1120
|
+
* - "auth" → "oauth", "token", "jwt", "credential"
|
|
1121
|
+
* - "cache" → "redis", "ttl", "invalidat", "evict"
|
|
1122
|
+
* - "order" → "purchas", "cart", "checkout"
|
|
1123
|
+
*
|
|
1124
|
+
* Performance: O(|query| × |vocab|) per query — fast because we only
|
|
1125
|
+
* compute PMI for query terms, not the full matrix.
|
|
1126
|
+
*
|
|
1127
|
+
* Also provides dense document embeddings via Random Indexing (Kanerva 2000):
|
|
1128
|
+
* - Each term gets a random sparse vector
|
|
1129
|
+
* - Document = sum of TF-IDF-weighted term vectors
|
|
1130
|
+
* - Cosine similarity captures latent semantic relationships
|
|
1131
|
+
* - Proven to approximate SVD/LSA without matrix decomposition
|
|
1132
|
+
*/
|
|
1133
|
+
|
|
1134
|
+
interface SemanticExpansion {
|
|
1135
|
+
/** Original query terms */
|
|
1136
|
+
original: string[];
|
|
1137
|
+
/** Expanded terms with weights (includes originals at weight 1.0) */
|
|
1138
|
+
expanded: Map<string, number>;
|
|
1139
|
+
/** Which expansions were added and why */
|
|
1140
|
+
expansions: {
|
|
1141
|
+
term: string;
|
|
1142
|
+
source: string;
|
|
1143
|
+
pmi: number;
|
|
1144
|
+
weight: number;
|
|
1145
|
+
}[];
|
|
1146
|
+
}
|
|
1147
|
+
interface CorpusEmbeddings {
|
|
1148
|
+
/** Document embeddings: filePath → dense vector */
|
|
1149
|
+
documents: Map<string, Float64Array>;
|
|
1150
|
+
/** Embedding dimension */
|
|
1151
|
+
dimension: number;
|
|
1152
|
+
/** Term → random index vector (sparse representation) */
|
|
1153
|
+
termVectors: Map<string, {
|
|
1154
|
+
indices: number[];
|
|
1155
|
+
signs: number[];
|
|
1156
|
+
}>;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Expand query terms using corpus-learned PMI associations.
|
|
1160
|
+
*
|
|
1161
|
+
* For each query term, finds terms that co-occur with it significantly
|
|
1162
|
+
* more than chance would predict. These are added to the query with
|
|
1163
|
+
* reduced weight.
|
|
1164
|
+
*
|
|
1165
|
+
* @param index - TF-IDF index (provides term frequencies and doc frequencies)
|
|
1166
|
+
* @param queryTerms - Tokenized query terms
|
|
1167
|
+
* @param topK - Max expansions per query term (default: 3)
|
|
1168
|
+
* @param minPmi - Minimum PMI score to be considered associated (default: 1.0)
|
|
1169
|
+
* @param expansionWeight - Weight multiplier for expanded terms (default: 0.5)
|
|
1170
|
+
*/
|
|
1171
|
+
declare function expandQueryWithPMI(index: TfIdfIndex, queryTerms: string[], topK?: number, minPmi?: number, expansionWeight?: number): SemanticExpansion;
|
|
1172
|
+
/**
|
|
1173
|
+
* Build dense document embeddings using Random Indexing.
|
|
1174
|
+
*
|
|
1175
|
+
* Random Indexing (Kanerva 2000, Sahlgren 2005) is a lightweight alternative
|
|
1176
|
+
* to LSA/SVD that builds document embeddings incrementally:
|
|
1177
|
+
*
|
|
1178
|
+
* 1. Assign each vocabulary term a random sparse vector (index vector)
|
|
1179
|
+
* with mostly zeros and a few +1/-1 entries
|
|
1180
|
+
* 2. A document's embedding = sum of TF-IDF-weighted index vectors of its terms
|
|
1181
|
+
* 3. Query embedding = sum of index vectors of query terms
|
|
1182
|
+
* 4. Similarity = cosine(query_embedding, doc_embedding)
|
|
1183
|
+
*
|
|
1184
|
+
* Proven to approximate SVD/LSA (Achlioptas 2003, Johnson-Lindenstrauss lemma).
|
|
1185
|
+
* O(docs × terms × nnz) where nnz is the sparsity of index vectors (~6).
|
|
1186
|
+
*
|
|
1187
|
+
* @param index - TF-IDF index
|
|
1188
|
+
* @param dimension - Embedding dimension (default: 128)
|
|
1189
|
+
* @param nnz - Non-zero entries per index vector (default: 6)
|
|
1190
|
+
* @param seed - Random seed for reproducibility (default: 42)
|
|
1191
|
+
*/
|
|
1192
|
+
declare function buildCorpusEmbeddings(index: TfIdfIndex, dimension?: number, nnz?: number, seed?: number): CorpusEmbeddings;
|
|
1193
|
+
/**
|
|
1194
|
+
* Compute embedding for a query string.
|
|
1195
|
+
*/
|
|
1196
|
+
declare function embedQuery(query: string, embeddings: CorpusEmbeddings): Float64Array;
|
|
1197
|
+
/**
|
|
1198
|
+
* Rank all documents by embedding similarity to a query.
|
|
1199
|
+
* Returns sorted list of (filePath, similarity) pairs.
|
|
1200
|
+
*/
|
|
1201
|
+
declare function queryByEmbedding(queryVec: Float64Array, embeddings: CorpusEmbeddings, maxResults?: number): {
|
|
1202
|
+
filePath: string;
|
|
1203
|
+
similarity: number;
|
|
1204
|
+
}[];
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* AST-Aware Tokenizer for Java, Python, Go
|
|
1208
|
+
*
|
|
1209
|
+
* Zero dependencies. Regex-based structural extraction.
|
|
1210
|
+
*
|
|
1211
|
+
* Problem: The generic tokenizer splits on whitespace and camelCase but doesn't
|
|
1212
|
+
* understand language constructs. A Java file with `@Repository` annotation and
|
|
1213
|
+
* `implements CacheService` has critical structural information that BM25 misses.
|
|
1214
|
+
*
|
|
1215
|
+
* Solution: Extract high-value structural tokens from source code:
|
|
1216
|
+
* - Class/interface/enum names (weighted 3×)
|
|
1217
|
+
* - Method/function names (weighted 2×)
|
|
1218
|
+
* - Annotations (@Repository, @Service, @Controller) as layer indicators
|
|
1219
|
+
* - Inheritance (extends/implements) as relationship signals
|
|
1220
|
+
* - Package/module declarations as structural context
|
|
1221
|
+
*
|
|
1222
|
+
* These tokens are prepended to the regular content, boosting their TF-IDF weight.
|
|
1223
|
+
* The result: files with structurally relevant names rank higher even if their
|
|
1224
|
+
* content is minimal (e.g., Java interfaces).
|
|
1225
|
+
*/
|
|
1226
|
+
interface StructuralTokens {
|
|
1227
|
+
/** Class/interface/enum names found */
|
|
1228
|
+
classNames: string[];
|
|
1229
|
+
/** Method/function names found */
|
|
1230
|
+
methodNames: string[];
|
|
1231
|
+
/** Annotations found (without @) */
|
|
1232
|
+
annotations: string[];
|
|
1233
|
+
/** Parent classes/interfaces (extends/implements) */
|
|
1234
|
+
parents: string[];
|
|
1235
|
+
/** Package/module name */
|
|
1236
|
+
packageName: string | null;
|
|
1237
|
+
/** Detected language */
|
|
1238
|
+
language: 'java' | 'python' | 'go' | 'typescript' | 'unknown';
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Extract structural tokens from source code.
|
|
1242
|
+
* Language is detected from file extension or content patterns.
|
|
1243
|
+
*/
|
|
1244
|
+
declare function extractStructuralTokens(content: string, filePath: string): StructuralTokens;
|
|
1245
|
+
/**
|
|
1246
|
+
* Augment file content with structural tokens for better BM25 indexing.
|
|
1247
|
+
*
|
|
1248
|
+
* Prepends high-value structural information to the content:
|
|
1249
|
+
* - Class names repeated 3× (boosted weight)
|
|
1250
|
+
* - Method names repeated 2×
|
|
1251
|
+
* - Annotation-derived layer terms
|
|
1252
|
+
* - Parent class/interface names
|
|
1253
|
+
*
|
|
1254
|
+
* This causes BM25 to rank files higher when the query matches their
|
|
1255
|
+
* structural identity, not just their content.
|
|
1256
|
+
*/
|
|
1257
|
+
declare function augmentContentWithStructure(content: string, filePath: string): string;
|
|
1258
|
+
/**
|
|
1259
|
+
* Get structural summary for a file (for debugging/telemetry).
|
|
1260
|
+
*/
|
|
1261
|
+
declare function getStructuralSummary(content: string, filePath: string): string;
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Feedback-Driven Weight Tuner
|
|
1265
|
+
*
|
|
1266
|
+
* Zero dependencies. Bayesian optimization of signal weights.
|
|
1267
|
+
*
|
|
1268
|
+
* Problem: RRF and boost weights are static (BM25=0.40, path=0.25, import=0.20,
|
|
1269
|
+
* className=0.15). Different codebases have different characteristics — a Java
|
|
1270
|
+
* monolith benefits more from import boost, a microservice from path boost.
|
|
1271
|
+
*
|
|
1272
|
+
* Solution: Record which files the developer actually uses after context selection.
|
|
1273
|
+
* Use this feedback to optimize weights per project using Thompson Sampling:
|
|
1274
|
+
*
|
|
1275
|
+
* 1. Each weight has a Beta distribution prior: Beta(α, β)
|
|
1276
|
+
* 2. When a file ranked high by signal X is accepted → α_X++
|
|
1277
|
+
* 3. When a file ranked high by signal X is rejected → β_X++
|
|
1278
|
+
* 4. Sample from each Beta to get optimistic weights
|
|
1279
|
+
* 5. Normalize to sum to 1.0
|
|
1280
|
+
*
|
|
1281
|
+
* This is the same algorithm used by recommendation systems at Netflix, Spotify.
|
|
1282
|
+
* Converges to optimal weights in ~20-50 selections.
|
|
1283
|
+
*
|
|
1284
|
+
* Storage: .cto/weight-tuner.json — <2KB
|
|
1285
|
+
* Integrates with existing A/B testing infrastructure.
|
|
1286
|
+
*/
|
|
1287
|
+
interface SignalWeight {
|
|
1288
|
+
/** Signal name */
|
|
1289
|
+
name: string;
|
|
1290
|
+
/** Default weight (fallback) */
|
|
1291
|
+
defaultWeight: number;
|
|
1292
|
+
/** Bayesian prior: alpha (success count) */
|
|
1293
|
+
alpha: number;
|
|
1294
|
+
/** Bayesian prior: beta (failure count) */
|
|
1295
|
+
beta: number;
|
|
1296
|
+
}
|
|
1297
|
+
interface WeightTunerModel {
|
|
1298
|
+
version: 1;
|
|
1299
|
+
updatedAt: string;
|
|
1300
|
+
signals: SignalWeight[];
|
|
1301
|
+
totalFeedback: number;
|
|
1302
|
+
/** History of recent weight snapshots for trend analysis */
|
|
1303
|
+
history: {
|
|
1304
|
+
timestamp: string;
|
|
1305
|
+
weights: Record<string, number>;
|
|
1306
|
+
}[];
|
|
1307
|
+
}
|
|
1308
|
+
interface TunedWeights {
|
|
1309
|
+
/** Optimized weights (sum to 1.0) */
|
|
1310
|
+
weights: Record<string, number>;
|
|
1311
|
+
/** Confidence: 0 = pure default, 1 = well-tuned (>50 observations) */
|
|
1312
|
+
confidence: number;
|
|
1313
|
+
/** Whether we're using learned weights or defaults */
|
|
1314
|
+
source: 'learned' | 'default';
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Load the weight tuner model from disk.
|
|
1318
|
+
* Returns fresh model with default priors if none exists.
|
|
1319
|
+
*/
|
|
1320
|
+
declare function loadWeightTuner(projectPath: string): WeightTunerModel;
|
|
1321
|
+
/**
|
|
1322
|
+
* Save the weight tuner model to disk.
|
|
1323
|
+
*/
|
|
1324
|
+
declare function saveWeightTuner(projectPath: string, model: WeightTunerModel): void;
|
|
1325
|
+
/**
|
|
1326
|
+
* Create a fresh model with uniform priors.
|
|
1327
|
+
* Alpha=1, Beta=1 is the non-informative prior (uniform distribution).
|
|
1328
|
+
*/
|
|
1329
|
+
declare function createFreshModel(): WeightTunerModel;
|
|
1330
|
+
/**
|
|
1331
|
+
* Record feedback: which signal contributed to each accepted/rejected file.
|
|
1332
|
+
*
|
|
1333
|
+
* @param model - Current tuner model
|
|
1334
|
+
* @param feedback - Array of (signalName, accepted) pairs
|
|
1335
|
+
*/
|
|
1336
|
+
declare function recordFeedback(model: WeightTunerModel, feedback: {
|
|
1337
|
+
signal: string;
|
|
1338
|
+
accepted: boolean;
|
|
1339
|
+
}[]): WeightTunerModel;
|
|
1340
|
+
/**
|
|
1341
|
+
* Get optimized weights using Thompson Sampling.
|
|
1342
|
+
*
|
|
1343
|
+
* For each signal, sample from its Beta(α, β) distribution.
|
|
1344
|
+
* The mean of Beta(α, β) is α/(α+β), which converges to the
|
|
1345
|
+
* true acceptance rate as we collect more feedback.
|
|
1346
|
+
*
|
|
1347
|
+
* With few observations, falls back to default weights.
|
|
1348
|
+
*/
|
|
1349
|
+
declare function getOptimizedWeights(model: WeightTunerModel): TunedWeights;
|
|
1350
|
+
/**
|
|
1351
|
+
* Determine which signal contributed most to a file's ranking.
|
|
1352
|
+
* Used to attribute feedback to the right signal.
|
|
1353
|
+
*
|
|
1354
|
+
* @param filePath - The file being evaluated
|
|
1355
|
+
* @param signalRanks - Map of signal name → rank of this file in that signal's ranking
|
|
1356
|
+
* @returns The signal name that ranked this file highest
|
|
1357
|
+
*/
|
|
1358
|
+
declare function attributeToSignal(signalRanks: Record<string, number>): string;
|
|
1359
|
+
/**
|
|
1360
|
+
* Render weight tuner status for CLI output.
|
|
1361
|
+
*/
|
|
1362
|
+
declare function renderWeightStatus(model: WeightTunerModel): string;
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Cross-File Call Graph Analysis
|
|
1366
|
+
*
|
|
1367
|
+
* Traces method/function calls across files to build execution paths.
|
|
1368
|
+
* Goes beyond import edges: if endpoint A calls service B.method() which
|
|
1369
|
+
* calls repository C.query(), we produce edges A→B and B→C with type 'call'.
|
|
1370
|
+
*
|
|
1371
|
+
* This is the signal that turns "fix cache retrieval" from matching random
|
|
1372
|
+
* files that mention "cache" into tracing the actual execution path:
|
|
1373
|
+
* controller → use case → cache repository → cache implementation.
|
|
1374
|
+
*
|
|
1375
|
+
* Approach: lightweight regex-based static analysis per language.
|
|
1376
|
+
* No AST parser dependency — works on raw file content.
|
|
1377
|
+
*
|
|
1378
|
+
* Supported: Java, TypeScript/JavaScript, Python, Go
|
|
1379
|
+
*/
|
|
1380
|
+
|
|
1381
|
+
interface MethodDefinition {
|
|
1382
|
+
name: string;
|
|
1383
|
+
className?: string;
|
|
1384
|
+
filePath: string;
|
|
1385
|
+
isExported: boolean;
|
|
1386
|
+
}
|
|
1387
|
+
interface MethodCall {
|
|
1388
|
+
callerFile: string;
|
|
1389
|
+
receiverName: string;
|
|
1390
|
+
methodName: string;
|
|
1391
|
+
}
|
|
1392
|
+
interface CallGraphResult {
|
|
1393
|
+
definitions: MethodDefinition[];
|
|
1394
|
+
calls: MethodCall[];
|
|
1395
|
+
edges: GraphEdge[];
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Build a cross-file call graph from file contents.
|
|
1399
|
+
*
|
|
1400
|
+
* Returns method definitions, method calls, and resolved call edges.
|
|
1401
|
+
* Call edges connect the file making the call to the file defining the method.
|
|
1402
|
+
*
|
|
1403
|
+
* @param files - Array of {relativePath, content} pairs
|
|
1404
|
+
* @returns CallGraphResult with definitions, calls, and resolved edges
|
|
1405
|
+
*/
|
|
1406
|
+
declare function buildCallGraph(files: {
|
|
1407
|
+
relativePath: string;
|
|
1408
|
+
content: string;
|
|
1409
|
+
}[]): CallGraphResult;
|
|
1410
|
+
/**
|
|
1411
|
+
* Boost BM25 results using call graph edges.
|
|
1412
|
+
*
|
|
1413
|
+
* When a file ranks well in BM25, files it calls or is called by
|
|
1414
|
+
* are likely relevant to the same task. This traces execution paths
|
|
1415
|
+
* that import-only analysis misses.
|
|
1416
|
+
*
|
|
1417
|
+
* @param matches - Current ranked matches
|
|
1418
|
+
* @param callEdges - Call graph edges (type 'call')
|
|
1419
|
+
* @param topK - How many top matches to expand (default: 10)
|
|
1420
|
+
* @param boostFactor - Score boost for called/calling files (default: 0.3)
|
|
1421
|
+
*/
|
|
1422
|
+
declare function boostByCallGraph(matches: {
|
|
1423
|
+
filePath: string;
|
|
1424
|
+
score: number;
|
|
1425
|
+
matchedTerms: string[];
|
|
1426
|
+
}[], callEdges: GraphEdge[], topK?: number, boostFactor?: number): {
|
|
1427
|
+
filePath: string;
|
|
1428
|
+
score: number;
|
|
1429
|
+
matchedTerms: string[];
|
|
1430
|
+
}[];
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* Git-Aware Relevance
|
|
1434
|
+
*
|
|
1435
|
+
* Files that are frequently modified together are likely related.
|
|
1436
|
+
* This is the signal that no competitor has — it captures implicit
|
|
1437
|
+
* coupling that import analysis and call graphs miss.
|
|
1438
|
+
*
|
|
1439
|
+
* Examples:
|
|
1440
|
+
* - Controller + its DTO changed together 90% of the time
|
|
1441
|
+
* - Service + its test changed together 80% of the time
|
|
1442
|
+
* - Config + migration changed together in 3 out of 4 commits
|
|
1443
|
+
*
|
|
1444
|
+
* Approach:
|
|
1445
|
+
* 1. Run `git log --name-only` to extract co-change history
|
|
1446
|
+
* 2. Build a co-change matrix: file pairs → co-commit count
|
|
1447
|
+
* 3. Normalize to Jaccard similarity: co(A,B) / (commits(A) + commits(B) - co(A,B))
|
|
1448
|
+
* 4. When file A is selected, boost files with high co-change similarity
|
|
1449
|
+
*
|
|
1450
|
+
* Performance: O(C × F²) where C = commits, F = avg files per commit.
|
|
1451
|
+
* Capped at 500 recent commits to keep it fast.
|
|
1452
|
+
*/
|
|
1453
|
+
interface CoChangeEntry {
|
|
1454
|
+
fileA: string;
|
|
1455
|
+
fileB: string;
|
|
1456
|
+
coCommits: number;
|
|
1457
|
+
similarity: number;
|
|
1458
|
+
}
|
|
1459
|
+
interface CoChangeMatrix {
|
|
1460
|
+
entries: Map<string, CoChangeEntry[]>;
|
|
1461
|
+
fileCommitCounts: Map<string, number>;
|
|
1462
|
+
totalCommits: number;
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Build a co-change matrix from git history.
|
|
1466
|
+
*
|
|
1467
|
+
* @param projectPath - Absolute path to the git repository
|
|
1468
|
+
* @param maxCommits - Max commits to analyze (default: 500)
|
|
1469
|
+
* @param minCoChanges - Minimum co-changes to include a pair (default: 2)
|
|
1470
|
+
* @returns CoChangeMatrix with file pair similarities
|
|
1471
|
+
*/
|
|
1472
|
+
declare function buildCoChangeMatrix(projectPath: string, maxCommits?: number, minCoChanges?: number): CoChangeMatrix;
|
|
1473
|
+
/**
|
|
1474
|
+
* Boost BM25 results using git co-change history.
|
|
1475
|
+
*
|
|
1476
|
+
* When file A ranks well and file B was frequently co-changed with A,
|
|
1477
|
+
* B gets a score boost proportional to A's score × co-change similarity.
|
|
1478
|
+
*
|
|
1479
|
+
* @param matches - Current ranked matches
|
|
1480
|
+
* @param coChangeMatrix - Pre-built co-change matrix
|
|
1481
|
+
* @param topK - How many top matches to expand (default: 10)
|
|
1482
|
+
* @param boostFactor - Max boost multiplier (default: 0.25)
|
|
1483
|
+
* @param minSimilarity - Min Jaccard similarity to apply boost (default: 0.15)
|
|
1484
|
+
*/
|
|
1485
|
+
declare function boostByGitCoChange(matches: {
|
|
1486
|
+
filePath: string;
|
|
1487
|
+
score: number;
|
|
1488
|
+
matchedTerms: string[];
|
|
1489
|
+
}[], coChangeMatrix: CoChangeMatrix, topK?: number, boostFactor?: number, minSimilarity?: number): {
|
|
1490
|
+
filePath: string;
|
|
1491
|
+
score: number;
|
|
1492
|
+
matchedTerms: string[];
|
|
1493
|
+
}[];
|
|
1494
|
+
/**
|
|
1495
|
+
* Get recently modified files from git log.
|
|
1496
|
+
* Files modified more recently are more likely relevant to active work.
|
|
1497
|
+
*
|
|
1498
|
+
* @param projectPath - Absolute path to the git repository
|
|
1499
|
+
* @param days - How many days back to look (default: 30)
|
|
1500
|
+
* @returns Map of filePath → recency score (1.0 = today, decays with age)
|
|
1501
|
+
*/
|
|
1502
|
+
declare function getGitRecency(projectPath: string, days?: number): Map<string, number>;
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* Multi-Hop Reasoning for Enterprise Queries
|
|
1506
|
+
*
|
|
1507
|
+
* Problem: "fix the seller info cache invalidation on KVS delete"
|
|
1508
|
+
* Required chain:
|
|
1509
|
+
* 1. Find delete KVS endpoint (BM25 matches "delete", "KVS")
|
|
1510
|
+
* 2. Find what it calls (use case → dependency graph)
|
|
1511
|
+
* 3. Find what the use case invalidates (cache repo → call graph)
|
|
1512
|
+
* 4. Find the cache implementation
|
|
1513
|
+
*
|
|
1514
|
+
* Current system finds steps 1 and 3 independently.
|
|
1515
|
+
* Multi-hop traces the chain and finds ALL 4.
|
|
1516
|
+
*
|
|
1517
|
+
* Algorithm: Iterative BM25 with dependency/call expansion
|
|
1518
|
+
* Hop 0: BM25(original query) → top-K files
|
|
1519
|
+
* Hop 1: For each top file, find deps + callees → re-query with expanded terms
|
|
1520
|
+
* Hop 2: Repeat with score decay
|
|
1521
|
+
* Aggregate: Combine scores across hops with exponential decay
|
|
1522
|
+
*
|
|
1523
|
+
* Max 3 hops. Each hop expands through both import deps AND call graph edges.
|
|
1524
|
+
*/
|
|
1525
|
+
|
|
1526
|
+
interface MultiHopConfig {
|
|
1527
|
+
maxHops: number;
|
|
1528
|
+
topKPerHop: number;
|
|
1529
|
+
decayFactor: number;
|
|
1530
|
+
minScoreThreshold: number;
|
|
1531
|
+
}
|
|
1532
|
+
interface MultiHopResult {
|
|
1533
|
+
matches: SemanticMatch[];
|
|
1534
|
+
hops: HopDetail[];
|
|
1535
|
+
totalFilesExplored: number;
|
|
1536
|
+
}
|
|
1537
|
+
interface HopDetail {
|
|
1538
|
+
hop: number;
|
|
1539
|
+
seedFiles: string[];
|
|
1540
|
+
newFiles: string[];
|
|
1541
|
+
expandedTerms: string[];
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Execute a multi-hop reasoning query.
|
|
1545
|
+
*
|
|
1546
|
+
* Starting from BM25 results, iteratively expands through the dependency
|
|
1547
|
+
* and call graphs, extracting terms from discovered files to broaden
|
|
1548
|
+
* the search while maintaining relevance through score decay.
|
|
1549
|
+
*
|
|
1550
|
+
* @param index - TF-IDF index for BM25 queries
|
|
1551
|
+
* @param task - Original task description
|
|
1552
|
+
* @param deps - Import dependency map (file → files it imports)
|
|
1553
|
+
* @param callEdges - Call graph edges (from → to with type 'call')
|
|
1554
|
+
* @param fileContents - Map of filePath → file content (for term extraction)
|
|
1555
|
+
* @param config - Multi-hop configuration
|
|
1556
|
+
*/
|
|
1557
|
+
declare function multiHopQuery(index: TfIdfIndex, task: string, deps: Map<string, string[]>, callEdges: {
|
|
1558
|
+
from: string;
|
|
1559
|
+
to: string;
|
|
1560
|
+
}[], fileContents: Map<string, string>, config?: Partial<MultiHopConfig>): MultiHopResult;
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* IDE Telemetry — Incremental Learning from File Opens
|
|
1564
|
+
*
|
|
1565
|
+
* Tracks which files the developer actually opens after receiving context.
|
|
1566
|
+
* If CTO suggests 15 files and the developer only uses 5, those 5 should
|
|
1567
|
+
* be weighted higher next time for similar tasks.
|
|
1568
|
+
*
|
|
1569
|
+
* Storage: .cto/telemetry.json — lightweight, per-project.
|
|
1570
|
+
*
|
|
1571
|
+
* Integration points:
|
|
1572
|
+
* - VS Code extension: file-open events → recordFileOpen()
|
|
1573
|
+
* - LSP bridge: cto/telemetry method
|
|
1574
|
+
* - Context pipeline: getTelemetryBoosts() → selector
|
|
1575
|
+
*
|
|
1576
|
+
* Privacy: Only stores relative file paths and timestamps.
|
|
1577
|
+
* No file contents, no user data.
|
|
1578
|
+
*/
|
|
1579
|
+
interface FileOpenEvent {
|
|
1580
|
+
filePath: string;
|
|
1581
|
+
timestamp: number;
|
|
1582
|
+
taskContext?: string;
|
|
1583
|
+
}
|
|
1584
|
+
interface TelemetrySession {
|
|
1585
|
+
taskDescription: string;
|
|
1586
|
+
suggestedFiles: string[];
|
|
1587
|
+
openedFiles: string[];
|
|
1588
|
+
timestamp: number;
|
|
1589
|
+
}
|
|
1590
|
+
interface TelemetryModel {
|
|
1591
|
+
version: number;
|
|
1592
|
+
sessions: TelemetrySession[];
|
|
1593
|
+
fileOpenCounts: Record<string, number>;
|
|
1594
|
+
fileTaskCounts: Record<string, Record<string, number>>;
|
|
1595
|
+
lastUpdated: number;
|
|
1596
|
+
}
|
|
1597
|
+
declare function loadTelemetry(projectPath: string): TelemetryModel;
|
|
1598
|
+
declare function saveTelemetry(projectPath: string, model: TelemetryModel): void;
|
|
1599
|
+
/**
|
|
1600
|
+
* Record that the user opened a file after receiving context suggestions.
|
|
1601
|
+
*/
|
|
1602
|
+
declare function recordFileOpen(model: TelemetryModel, filePath: string, taskContext?: string): TelemetryModel;
|
|
1603
|
+
/**
|
|
1604
|
+
* Record a complete session: what CTO suggested vs what the user used.
|
|
1605
|
+
*/
|
|
1606
|
+
declare function recordSession(model: TelemetryModel, taskDescription: string, suggestedFiles: string[], openedFiles: string[]): TelemetryModel;
|
|
1607
|
+
/**
|
|
1608
|
+
* Get telemetry-based boosts for file ranking.
|
|
1609
|
+
*
|
|
1610
|
+
* Files the user frequently opens for similar tasks get a positive boost.
|
|
1611
|
+
* Files that CTO suggests but the user never opens get a negative signal.
|
|
1612
|
+
*
|
|
1613
|
+
* @param model - Telemetry model
|
|
1614
|
+
* @param taskType - Current task type (debug, feature, refactor, etc.)
|
|
1615
|
+
* @param candidateFiles - Files to compute boosts for
|
|
1616
|
+
* @returns Map of filePath → boost (-1.0 to +1.0)
|
|
1617
|
+
*/
|
|
1618
|
+
declare function getTelemetryBoosts(model: TelemetryModel, taskType: string, candidateFiles: string[]): Map<string, number>;
|
|
1619
|
+
/**
|
|
1620
|
+
* Render a summary of telemetry data for debugging/display.
|
|
1621
|
+
*/
|
|
1622
|
+
declare function renderTelemetrySummary(model: TelemetryModel): string;
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Embedding-Based Retrieval
|
|
1626
|
+
*
|
|
1627
|
+
* Dense vector search to complement BM25 lexical matching.
|
|
1628
|
+
* Catches semantic similarity that BM25 misses:
|
|
1629
|
+
* - "authentication" matches "login" (no lexical overlap)
|
|
1630
|
+
* - "cache invalidation" matches "clear stored data"
|
|
1631
|
+
*
|
|
1632
|
+
* Two backends:
|
|
1633
|
+
* 1. TF-IDF Cosine (built-in, zero deps, always available)
|
|
1634
|
+
* — builds document vectors from TF-IDF weights, queries via cosine similarity
|
|
1635
|
+
* — accuracy: ~85% of neural embeddings for code search
|
|
1636
|
+
*
|
|
1637
|
+
* 2. ONNX Neural (optional, requires onnxruntime-node + model file)
|
|
1638
|
+
* — all-MiniLM-L6-v2 (23MB), 384-dim embeddings
|
|
1639
|
+
* — accuracy: best-in-class for semantic code search
|
|
1640
|
+
*
|
|
1641
|
+
* Integration: produces a ranked list of (filePath, score) that gets
|
|
1642
|
+
* merged with BM25 results via RRF in the context pipeline.
|
|
1643
|
+
*/
|
|
1644
|
+
|
|
1645
|
+
interface EmbeddingResult {
|
|
1646
|
+
filePath: string;
|
|
1647
|
+
score: number;
|
|
1648
|
+
}
|
|
1649
|
+
interface EmbeddingIndex {
|
|
1650
|
+
backend: 'tfidf-cosine' | 'onnx-minilm';
|
|
1651
|
+
dimensions: number;
|
|
1652
|
+
documentCount: number;
|
|
1653
|
+
query: (text: string, topK: number) => EmbeddingResult[];
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Build a dense embedding index from TF-IDF vectors.
|
|
1657
|
+
*
|
|
1658
|
+
* Each document becomes a vector in IDF-weighted term space.
|
|
1659
|
+
* Queries are vectorized the same way and matched via cosine similarity.
|
|
1660
|
+
*
|
|
1661
|
+
* This is surprisingly effective for code search because:
|
|
1662
|
+
* - Code has strong term distributions (class names, method names)
|
|
1663
|
+
* - IDF weighting naturally emphasizes discriminative terms
|
|
1664
|
+
* - Cosine similarity handles different document lengths well
|
|
1665
|
+
*
|
|
1666
|
+
* Performance: O(V) per query where V = vocabulary size × document count.
|
|
1667
|
+
* For 1000 files × 5000 unique terms = 5M ops. Fast enough for CLI.
|
|
1668
|
+
*/
|
|
1669
|
+
declare function buildTfIdfEmbeddingIndex(index: TfIdfIndex): EmbeddingIndex;
|
|
1670
|
+
/**
|
|
1671
|
+
* Merge BM25 results with embedding results using Reciprocal Rank Fusion.
|
|
1672
|
+
*
|
|
1673
|
+
* RRF(d) = Σ 1/(k + rank_i(d)) for each ranking i
|
|
1674
|
+
*
|
|
1675
|
+
* This is the standard way to combine lexical and semantic search.
|
|
1676
|
+
* k=60 is the standard constant from the RRF paper (Cormack et al., 2009).
|
|
1677
|
+
*
|
|
1678
|
+
* @param bm25Results - BM25 ranked results (filePath, score)
|
|
1679
|
+
* @param embeddingResults - Embedding ranked results (filePath, score)
|
|
1680
|
+
* @param k - RRF constant (default: 60)
|
|
1681
|
+
* @param bm25Weight - Weight for BM25 signal (default: 0.6)
|
|
1682
|
+
* @param embeddingWeight - Weight for embedding signal (default: 0.4)
|
|
1683
|
+
*/
|
|
1684
|
+
declare function reciprocalRankFusion(bm25Results: {
|
|
1685
|
+
filePath: string;
|
|
1686
|
+
score: number;
|
|
1687
|
+
}[], embeddingResults: EmbeddingResult[], k?: number, bm25Weight?: number, embeddingWeight?: number): {
|
|
1688
|
+
filePath: string;
|
|
1689
|
+
score: number;
|
|
1690
|
+
}[];
|
|
1691
|
+
/**
|
|
1692
|
+
* Check if ONNX Runtime is available for neural embeddings.
|
|
1693
|
+
*/
|
|
1694
|
+
declare function isOnnxAvailable(): Promise<boolean>;
|
|
1695
|
+
/**
|
|
1696
|
+
* Build a neural embedding index using ONNX Runtime.
|
|
1697
|
+
* Requires: npm install onnxruntime-node
|
|
1698
|
+
* Model: all-MiniLM-L6-v2 (download separately to .cto/models/)
|
|
1699
|
+
*
|
|
1700
|
+
* Falls back to TF-IDF cosine if ONNX is not available.
|
|
1701
|
+
*/
|
|
1702
|
+
declare function buildNeuralEmbeddingIndex(_files: {
|
|
1703
|
+
relativePath: string;
|
|
1704
|
+
content: string;
|
|
1705
|
+
}[], modelPath?: string): Promise<EmbeddingIndex | null>;
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* Chunk-Level Retrieval
|
|
1709
|
+
*
|
|
1710
|
+
* Instead of including entire files, extract semantic chunks
|
|
1711
|
+
* (functions, methods, classes) and score each chunk independently.
|
|
1712
|
+
*
|
|
1713
|
+
* This is the single biggest efficiency win for context selection:
|
|
1714
|
+
* - A 2000-line file with 1 relevant method → include 50 lines, not 2000
|
|
1715
|
+
* - Token budget goes 10-40x further
|
|
1716
|
+
* - More files can have their relevant parts included
|
|
1717
|
+
*
|
|
1718
|
+
* Chunk types:
|
|
1719
|
+
* - Function/method definition (with body)
|
|
1720
|
+
* - Class/interface declaration (with key members)
|
|
1721
|
+
* - Import block
|
|
1722
|
+
* - Top-level constant/variable block
|
|
1723
|
+
*
|
|
1724
|
+
* Scoring: BM25 term overlap + structural bonus (method name matches query)
|
|
1725
|
+
*/
|
|
1726
|
+
interface CodeChunk {
|
|
1727
|
+
filePath: string;
|
|
1728
|
+
startLine: number;
|
|
1729
|
+
endLine: number;
|
|
1730
|
+
content: string;
|
|
1731
|
+
kind: ChunkKind;
|
|
1732
|
+
name: string;
|
|
1733
|
+
className?: string;
|
|
1734
|
+
score: number;
|
|
1735
|
+
tokens: number;
|
|
1736
|
+
}
|
|
1737
|
+
type ChunkKind = 'function' | 'method' | 'class' | 'interface' | 'import' | 'constant' | 'type' | 'block';
|
|
1738
|
+
interface ChunkRetrievalResult {
|
|
1739
|
+
chunks: CodeChunk[];
|
|
1740
|
+
fileChunks: Map<string, CodeChunk[]>;
|
|
1741
|
+
totalChunks: number;
|
|
1742
|
+
totalTokensUsed: number;
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Extract semantic chunks from a file.
|
|
1746
|
+
*/
|
|
1747
|
+
declare function chunkFile(content: string, filePath: string): CodeChunk[];
|
|
1748
|
+
/**
|
|
1749
|
+
* Score chunks against a query.
|
|
1750
|
+
* Uses BM25 term overlap + structural bonuses.
|
|
1751
|
+
*/
|
|
1752
|
+
declare function scoreChunks(chunks: CodeChunk[], task: string): CodeChunk[];
|
|
1753
|
+
/**
|
|
1754
|
+
* Retrieve the most relevant chunks across multiple files.
|
|
1755
|
+
*
|
|
1756
|
+
* @param files - Array of {relativePath, content} pairs
|
|
1757
|
+
* @param task - Task description to match against
|
|
1758
|
+
* @param tokenBudget - Max tokens to include (default: 30000)
|
|
1759
|
+
* @param minScore - Minimum chunk score to include (default: 0.1)
|
|
1760
|
+
*/
|
|
1761
|
+
declare function retrieveChunks(files: {
|
|
1762
|
+
relativePath: string;
|
|
1763
|
+
content: string;
|
|
1764
|
+
}[], task: string, tokenBudget?: number, minScore?: number): ChunkRetrievalResult;
|
|
1765
|
+
/**
|
|
1766
|
+
* Render chunks for a single file as markdown.
|
|
1767
|
+
* Shows relevant chunks with line numbers, connected by "..." for gaps.
|
|
1768
|
+
*/
|
|
1769
|
+
declare function renderFileChunks(filePath: string, chunks: CodeChunk[], ext: string): string;
|
|
1770
|
+
|
|
412
1771
|
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
413
1772
|
interface LogEntry {
|
|
414
1773
|
level: LogLevel;
|
|
@@ -470,4 +1829,4 @@ interface AuditOptions {
|
|
|
470
1829
|
}
|
|
471
1830
|
declare function auditProject(projectPath: string, filePaths: string[], options?: AuditOptions): Promise<AuditResult>;
|
|
472
1831
|
|
|
473
|
-
export { CtoError, type CtoErrorCode, type DocumentVector, type LearnerBoost, type LearnerBoostInput, type LearnerModel, type LogEntry, type LogLevel, type Logger, type PatternStats, type SecretFinding, type SecretType, type SelectionInput, type SemanticMatch, type SemanticScore, type TfIdfIndex, analyzeProject, auditProject, bfsBidirectional, boostByPath, buildAdjacencyList, buildIndex, buildProjectGraph, calculateCoverage, classifyFileKind, countTokensChars4, countTokensTiktoken, createLogger, createProject, detectStack, estimateFileTokens, estimateTokens, extractPattern, freeEncoder, getLearnerBoosts, getLearnerStats, getPruneLevelForRisk, isCtoError, loadLearner, optimizeBudget, pruneFile, pruneFiles, query, recordSelection, sanitizeContent, saveLearner, scanContentForSecrets, scanFileForSecrets, scanProjectForSecrets, scoreAllFiles, scoreFile, selectContext, setJsonLogging, setLogLevel, similarity, tokenize, walkProject, wrapError };
|
|
1832
|
+
export { type ActionType, type ArchLayer, type AssignmentResult, type CallGraphResult, type ChunkKind, type ChunkRetrievalResult, type CoChangeEntry, type CoChangeMatrix, type CodeChunk, type ContextPipelineInput, type ContextPipelineResult, type CorpusEmbeddings, CtoError, type CtoErrorCode, type DocumentVector, type EmbeddingIndex, type EmbeddingResult, type Experiment, type ExperimentConclusion, type ExperimentGroup, type FileOpenEvent, type FilteredFile, type GroupMetrics, type HopDetail, type ImportSpec, type IndexCacheStats, type LearnerBoost, type LearnerBoostInput, type LearnerModel, type LogEntry, type LogLevel, type Logger, type MethodCall, type MethodDefinition, type MultiHopConfig, type MultiHopResult, type MultiRepoResult, type PatternStats, type QueryIntent, type RerankInput, type RerankResult, type RerankedFile, type SecretFinding, type SecretType, type SelectionInput, type SemanticExpansion, type SemanticMatch, type SemanticScore, type SiblingMatch, type SiblingRepo, type SignalWeight, type SignificanceResult, type StructuralTokens, type SupportedLanguage, type SynonymExpansion, type TelemetryModel, type TelemetrySession, type TfIdfIndex, type TunedWeights, type WeightTunerModel, analyzeProject, assignGroup, attributeToSignal, auditProject, augmentContentWithStructure, bfsBidirectional, boostByCallGraph, boostByGitCoChange, boostByImports, boostByLayer, boostByPath, buildAdjacencyList, buildCallGraph, buildCoChangeMatrix, buildCorpusEmbeddings, buildIndex, buildIndexCached, buildNeuralEmbeddingIndex, buildProjectGraph, buildTfIdfEmbeddingIndex, buildWeightedQuery, calculateCoverage, chunkFile, classifyFileKind, countTokensChars4, countTokensTiktoken, createExperiment, createFreshModel, createLogger, createProject, detectLanguage, detectStack, discoverSiblingRepos, embedQuery, reciprocalRankFusion as embeddingRRF, estimateComplexity, estimateFileTokens, estimateTokens, expandLayers, expandQuery, expandQueryWithPMI, expandTerm, extractPattern, extractStructuralTokens, freeEncoder, getActiveExperiment, getCacheInfo, getConcludedExperiments, getExpansionDetails, getGitRecency, getLearnerBoosts, getLearnerStats, getOptimizedWeights, getPruneLevelForRisk, getStructuralSummary, getSynonymStats, getTelemetryBoosts, invalidateCache, isCtoError, isOnnxAvailable, loadExperiments, loadLearner, loadTelemetry, loadWeightTuner, multiHopQuery, optimizeBudget, parseAllPolyglotImports, parseImports, parseQueryIntent, parseSiblingPaths, pruneFile, pruneFiles, query, queryByEmbedding, querySiblingRepos, reciprocalRankFusion$1 as reciprocalRankFusion, recordFeedback, recordFileOpen, recordOutcome, recordSelection, recordSession, renderExperimentSummary, renderFileChunks, renderMultiRepoSummary, renderTelemetrySummary, renderWeightStatus, rerank, retrieveChunks, runContextPipeline, sanitizeContent, saveExperiments, saveLearner, saveTelemetry, saveWeightTuner, scanContentForSecrets, scanFileForSecrets, scanProjectForSecrets, scoreAllFiles, scoreChunks, scoreFile, selectContext, setJsonLogging, setLogLevel, similarity, testSignificance, tokenize, walkProject, wrapError };
|