kiri-mcp-server 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -17
- package/config/scoring-profiles.yml +15 -0
- package/dist/config/scoring-profiles.yml +15 -0
- package/dist/package.json +1 -1
- package/dist/src/server/context.d.ts +2 -0
- package/dist/src/server/context.d.ts.map +1 -1
- package/dist/src/server/handlers.d.ts +1 -0
- package/dist/src/server/handlers.d.ts.map +1 -1
- package/dist/src/server/handlers.js +327 -93
- package/dist/src/server/handlers.js.map +1 -1
- package/dist/src/server/rpc.d.ts +34 -0
- package/dist/src/server/rpc.d.ts.map +1 -1
- package/dist/src/server/rpc.js +76 -4
- package/dist/src/server/rpc.js.map +1 -1
- package/dist/src/server/runtime.d.ts.map +1 -1
- package/dist/src/server/runtime.js +2 -0
- package/dist/src/server/runtime.js.map +1 -1
- package/dist/src/server/scoring.d.ts +6 -0
- package/dist/src/server/scoring.d.ts.map +1 -1
- package/dist/src/server/scoring.js +22 -4
- package/dist/src/server/scoring.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,155 @@ import path from "node:path";
|
|
|
2
2
|
import { generateEmbedding, structuralSimilarity } from "../shared/embedding.js";
|
|
3
3
|
import { encode as encodeGPT, tokenizeText } from "../shared/tokenizer.js";
|
|
4
4
|
import { coerceProfileName, loadScoringProfile } from "./scoring.js";
|
|
5
|
+
// Configuration file patterns (v0.8.0+: consolidated to avoid duplication)
|
|
6
|
+
// Comprehensive list covering multiple languages and tools
|
|
7
|
+
const CONFIG_FILES = [
|
|
8
|
+
// JavaScript/TypeScript/Node.js
|
|
9
|
+
"package.json",
|
|
10
|
+
"package-lock.json",
|
|
11
|
+
"npm-shrinkwrap.json",
|
|
12
|
+
"tsconfig.json",
|
|
13
|
+
"jsconfig.json",
|
|
14
|
+
"pnpm-lock.yaml",
|
|
15
|
+
"yarn.lock",
|
|
16
|
+
"bun.lockb",
|
|
17
|
+
// Python
|
|
18
|
+
"requirements.txt",
|
|
19
|
+
"pyproject.toml",
|
|
20
|
+
"setup.py",
|
|
21
|
+
"setup.cfg",
|
|
22
|
+
"Pipfile",
|
|
23
|
+
"Pipfile.lock",
|
|
24
|
+
"poetry.lock",
|
|
25
|
+
"pytest.ini",
|
|
26
|
+
"tox.ini",
|
|
27
|
+
".python-version",
|
|
28
|
+
// Ruby
|
|
29
|
+
"Gemfile",
|
|
30
|
+
"Gemfile.lock",
|
|
31
|
+
".ruby-version",
|
|
32
|
+
"Rakefile",
|
|
33
|
+
// Go
|
|
34
|
+
"go.mod",
|
|
35
|
+
"go.sum",
|
|
36
|
+
"Makefile",
|
|
37
|
+
// PHP
|
|
38
|
+
"composer.json",
|
|
39
|
+
"composer.lock",
|
|
40
|
+
"phpunit.xml",
|
|
41
|
+
// Java/Kotlin/Gradle/Maven
|
|
42
|
+
"pom.xml",
|
|
43
|
+
"build.gradle",
|
|
44
|
+
"build.gradle.kts",
|
|
45
|
+
"settings.gradle",
|
|
46
|
+
"settings.gradle.kts",
|
|
47
|
+
"gradle.properties",
|
|
48
|
+
// Rust
|
|
49
|
+
"Cargo.toml",
|
|
50
|
+
"Cargo.lock",
|
|
51
|
+
// Swift
|
|
52
|
+
"Package.swift",
|
|
53
|
+
"Package.resolved",
|
|
54
|
+
// .NET
|
|
55
|
+
"packages.lock.json",
|
|
56
|
+
"global.json",
|
|
57
|
+
// C/C++
|
|
58
|
+
"CMakeLists.txt",
|
|
59
|
+
"Makefile.am",
|
|
60
|
+
"configure.ac",
|
|
61
|
+
// Docker
|
|
62
|
+
"Dockerfile",
|
|
63
|
+
"docker-compose.yml",
|
|
64
|
+
"docker-compose.yaml",
|
|
65
|
+
".dockerignore",
|
|
66
|
+
// CI/CD
|
|
67
|
+
".travis.yml",
|
|
68
|
+
".gitlab-ci.yml",
|
|
69
|
+
"Jenkinsfile",
|
|
70
|
+
"azure-pipelines.yml",
|
|
71
|
+
// Git
|
|
72
|
+
".gitignore",
|
|
73
|
+
".gitattributes",
|
|
74
|
+
".gitmodules",
|
|
75
|
+
// Editor config
|
|
76
|
+
".editorconfig",
|
|
77
|
+
// Webserver config
|
|
78
|
+
"Caddyfile",
|
|
79
|
+
"nginx.conf",
|
|
80
|
+
".htaccess",
|
|
81
|
+
"httpd.conf",
|
|
82
|
+
"apache2.conf",
|
|
83
|
+
"lighttpd.conf",
|
|
84
|
+
];
|
|
85
|
+
// Configuration directories (files inside these directories are treated as config)
|
|
86
|
+
// Note: No trailing slashes - exact segment matching prevents false positives
|
|
87
|
+
const CONFIG_DIRECTORIES = [
|
|
88
|
+
"bootstrap", // Laravel/Symfony framework bootstrap
|
|
89
|
+
"config", // Generic config directory (all frameworks)
|
|
90
|
+
"migrations", // Database migrations
|
|
91
|
+
"db/migrate", // Ruby on Rails migrations
|
|
92
|
+
"alembic/versions", // Python Alembic migrations
|
|
93
|
+
"seeds", // Database seeds
|
|
94
|
+
"fixtures", // Test fixtures
|
|
95
|
+
"test-data", // Test data
|
|
96
|
+
"locales", // i18n translations
|
|
97
|
+
"i18n", // i18n translations
|
|
98
|
+
"translations", // i18n translations
|
|
99
|
+
"lang", // i18n translations
|
|
100
|
+
".terraform", // Terraform state
|
|
101
|
+
"terraform", // Terraform configs
|
|
102
|
+
"k8s", // Kubernetes manifests
|
|
103
|
+
"kubernetes", // Kubernetes manifests
|
|
104
|
+
"ansible", // Ansible playbooks
|
|
105
|
+
"cloudformation", // CloudFormation templates
|
|
106
|
+
"pulumi", // Pulumi infrastructure
|
|
107
|
+
];
|
|
108
|
+
const CONFIG_EXTENSIONS = [".lock", ".env", ".conf"];
|
|
109
|
+
const CONFIG_PATTERNS = [
|
|
110
|
+
// Generic config patterns (any language)
|
|
111
|
+
".config.js",
|
|
112
|
+
".config.ts",
|
|
113
|
+
".config.mjs",
|
|
114
|
+
".config.cjs",
|
|
115
|
+
".config.json",
|
|
116
|
+
".config.yaml",
|
|
117
|
+
".config.yml",
|
|
118
|
+
".config.toml",
|
|
119
|
+
// Linter/Formatter configs
|
|
120
|
+
".eslintrc",
|
|
121
|
+
".prettierrc",
|
|
122
|
+
".stylelintrc",
|
|
123
|
+
".pylintrc",
|
|
124
|
+
".flake8",
|
|
125
|
+
".rubocop.yml",
|
|
126
|
+
// CI config files
|
|
127
|
+
".circleci/config.yml",
|
|
128
|
+
".github/workflows",
|
|
129
|
+
];
|
|
130
|
+
/**
|
|
131
|
+
* Check if a file path represents a configuration file
|
|
132
|
+
* Supports multiple languages: JS/TS, Python, Ruby, Go, PHP, Java, Rust, C/C++, Docker, CI/CD
|
|
133
|
+
* Also checks if file is in a config directory (bootstrap/, config/, migrations/, etc.)
|
|
134
|
+
* Uses exact path segment matching to prevent false positives (e.g., "myconfig/" won't match "config/")
|
|
135
|
+
* @param path - Full file path
|
|
136
|
+
* @param fileName - File name only (extracted from path)
|
|
137
|
+
* @returns true if the file is a configuration file
|
|
138
|
+
*/
|
|
139
|
+
function isConfigFile(path, fileName) {
|
|
140
|
+
// Normalize path separators (Windows compatibility)
|
|
141
|
+
const normalizedPath = path.replace(/\\/g, "/");
|
|
142
|
+
// Check if file is in a config directory using exact path segment matching
|
|
143
|
+
// Split path into segments and check for exact matches to prevent false positives
|
|
144
|
+
// e.g., "bootstrap" won't match "my-bootstrap-theme" in path segments
|
|
145
|
+
// Filter empty strings to handle absolute paths (e.g., "/src/app" → ["", "src", "app"] → ["src", "app"])
|
|
146
|
+
const pathSegments = new Set(normalizedPath.split("/").filter(Boolean));
|
|
147
|
+
const isInConfigDirectory = CONFIG_DIRECTORIES.some((dir) => pathSegments.has(dir));
|
|
148
|
+
return (CONFIG_FILES.includes(fileName) ||
|
|
149
|
+
CONFIG_EXTENSIONS.some((ce) => path.endsWith(ce)) ||
|
|
150
|
+
CONFIG_PATTERNS.some((pattern) => path.includes(pattern)) ||
|
|
151
|
+
fileName.startsWith(".env") ||
|
|
152
|
+
isInConfigDirectory);
|
|
153
|
+
}
|
|
5
154
|
const DEFAULT_SEARCH_LIMIT = 50;
|
|
6
155
|
const DEFAULT_SNIPPET_WINDOW = 150;
|
|
7
156
|
const DEFAULT_BUNDLE_LIMIT = 7; // Reduced from 12 to optimize token usage
|
|
@@ -93,7 +242,6 @@ function extractQuotedPhrases(text) {
|
|
|
93
242
|
const quotePattern = /"([^"]+)"|'([^']+)'/g;
|
|
94
243
|
let match;
|
|
95
244
|
let remaining = text;
|
|
96
|
-
// eslint-disable-next-line no-cond-assign
|
|
97
245
|
while ((match = quotePattern.exec(text)) !== null) {
|
|
98
246
|
const phrase = (match[1] || match[2] || "").trim().toLowerCase();
|
|
99
247
|
if (phrase.length >= 3) {
|
|
@@ -200,6 +348,7 @@ function ensureCandidate(map, filePath) {
|
|
|
200
348
|
candidate = {
|
|
201
349
|
path: filePath,
|
|
202
350
|
score: 0,
|
|
351
|
+
scoreMultiplier: 1.0, // Default: no boost or penalty
|
|
203
352
|
reasons: new Set(),
|
|
204
353
|
matchLine: null,
|
|
205
354
|
content: null,
|
|
@@ -361,14 +510,15 @@ function splitQueryWords(query) {
|
|
|
361
510
|
return words.length > 0 ? words : [query]; // 全て除外された場合は元のクエリを使用
|
|
362
511
|
}
|
|
363
512
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
513
|
+
* files_search専用のファイルタイプブースト適用(v0.7.0+: 設定可能な乗算的ペナルティ)
|
|
514
|
+
* context_bundleと同じ乗算的ペナルティロジックを使用
|
|
366
515
|
* @param path - ファイルパス
|
|
367
|
-
* @param baseScore -
|
|
368
|
-
* @param profile - ブーストプロファイル
|
|
516
|
+
* @param baseScore - 基本スコア(FTS BM25スコアまたは1.0)
|
|
517
|
+
* @param profile - ブーストプロファイル
|
|
518
|
+
* @param weights - スコアリングウェイト設定(乗算的ペナルティに使用)
|
|
369
519
|
* @returns ブースト適用後のスコア
|
|
370
520
|
*/
|
|
371
|
-
function applyFileTypeBoost(path, baseScore, profile = "default") {
|
|
521
|
+
function applyFileTypeBoost(path, baseScore, profile = "default", weights) {
|
|
372
522
|
// Blacklisted directories that are almost always irrelevant for code context
|
|
373
523
|
const blacklistedDirs = [
|
|
374
524
|
".cursor/",
|
|
@@ -385,82 +535,97 @@ function applyFileTypeBoost(path, baseScore, profile = "default") {
|
|
|
385
535
|
if (profile === "none") {
|
|
386
536
|
return baseScore;
|
|
387
537
|
}
|
|
538
|
+
// Extract file extension for type detection
|
|
539
|
+
const ext = path.includes(".") ? path.substring(path.lastIndexOf(".")) : null;
|
|
540
|
+
// ✅ UNIFIED LOGIC: Use same multiplicative penalties as context_bundle
|
|
388
541
|
if (profile === "docs") {
|
|
542
|
+
// Boost documentation files
|
|
389
543
|
if (path.endsWith(".md") || path.endsWith(".yaml") || path.endsWith(".yml")) {
|
|
390
|
-
return baseScore * 1.
|
|
544
|
+
return baseScore * 1.5; // 50% boost (same as context_bundle)
|
|
391
545
|
}
|
|
546
|
+
// Penalty for implementation files in docs mode
|
|
392
547
|
if (path.startsWith("src/") &&
|
|
393
548
|
(path.endsWith(".ts") || path.endsWith(".js") || path.endsWith(".tsx"))) {
|
|
394
|
-
return baseScore * 0.5; //
|
|
549
|
+
return baseScore * 0.5; // 50% penalty
|
|
395
550
|
}
|
|
396
551
|
return baseScore;
|
|
397
552
|
}
|
|
398
|
-
// Default profile:
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
553
|
+
// Default profile: Use configurable multiplicative penalties
|
|
554
|
+
let multiplier = 1.0;
|
|
555
|
+
const fileName = path.split("/").pop() ?? "";
|
|
556
|
+
// ✅ Step 1: Config files get strongest penalty (95% reduction)
|
|
557
|
+
if (isConfigFile(path, fileName)) {
|
|
558
|
+
multiplier *= weights.configPenaltyMultiplier; // 0.05 = 95% reduction
|
|
559
|
+
return baseScore * multiplier;
|
|
560
|
+
}
|
|
561
|
+
// ✅ Step 2: Documentation files get moderate penalty (50% reduction)
|
|
562
|
+
const docExtensions = [".md", ".yaml", ".yml", ".mdc"];
|
|
563
|
+
if (docExtensions.some((docExt) => path.endsWith(docExt))) {
|
|
564
|
+
multiplier *= weights.docPenaltyMultiplier; // 0.5 = 50% reduction
|
|
565
|
+
return baseScore * multiplier;
|
|
566
|
+
}
|
|
567
|
+
// ✅ Step 3: Implementation file boosts
|
|
403
568
|
if (path.startsWith("src/app/")) {
|
|
404
|
-
|
|
569
|
+
multiplier *= weights.implBoostMultiplier * 1.4; // Extra boost for app files
|
|
405
570
|
}
|
|
406
|
-
if (path.startsWith("src/components/")) {
|
|
407
|
-
|
|
571
|
+
else if (path.startsWith("src/components/")) {
|
|
572
|
+
multiplier *= weights.implBoostMultiplier * 1.3;
|
|
408
573
|
}
|
|
409
|
-
if (path.startsWith("src/lib/")) {
|
|
410
|
-
|
|
574
|
+
else if (path.startsWith("src/lib/")) {
|
|
575
|
+
multiplier *= weights.implBoostMultiplier * 1.2;
|
|
411
576
|
}
|
|
412
|
-
if (path.startsWith("src/")
|
|
413
|
-
(
|
|
414
|
-
|
|
577
|
+
else if (path.startsWith("src/")) {
|
|
578
|
+
if (ext === ".ts" || ext === ".tsx" || ext === ".js") {
|
|
579
|
+
multiplier *= weights.implBoostMultiplier; // Base impl boost
|
|
580
|
+
}
|
|
415
581
|
}
|
|
582
|
+
// Test files: additive penalty (keep strong for files_search)
|
|
416
583
|
if (path.startsWith("tests/") || path.startsWith("test/")) {
|
|
417
|
-
return baseScore * 0.2; //
|
|
584
|
+
return baseScore * 0.2; // Strong penalty for tests
|
|
418
585
|
}
|
|
419
|
-
return baseScore;
|
|
586
|
+
return baseScore * multiplier;
|
|
420
587
|
}
|
|
421
588
|
/**
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* @param candidate - スコアリング対象の候補
|
|
425
|
-
* @param row - ファイル情報(path, ext)
|
|
426
|
-
* @param profile - ブーストプロファイル
|
|
589
|
+
* パスベースのスコアリングを適用(加算的ブースト)
|
|
590
|
+
* goalのキーワード/フレーズがファイルパスに含まれる場合にスコアを加算
|
|
427
591
|
*/
|
|
428
|
-
function
|
|
429
|
-
if (
|
|
592
|
+
function applyPathBasedScoring(candidate, lowerPath, weights, extractedTerms) {
|
|
593
|
+
if (!extractedTerms || weights.pathMatch <= 0) {
|
|
430
594
|
return;
|
|
431
595
|
}
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
for (const phrase of extractedTerms.phrases) {
|
|
439
|
-
if (lowerPath.includes(phrase)) {
|
|
440
|
-
candidate.score += pathMatchWeight * 1.5; // 1.5倍のブースト
|
|
441
|
-
candidate.reasons.add(`path-phrase:${phrase}`);
|
|
442
|
-
break; // 最初のマッチのみ適用
|
|
443
|
-
}
|
|
596
|
+
// フレーズがパスに完全一致する場合(最高の重み)
|
|
597
|
+
for (const phrase of extractedTerms.phrases) {
|
|
598
|
+
if (lowerPath.includes(phrase)) {
|
|
599
|
+
candidate.score += weights.pathMatch * 1.5; // 1.5倍のブースト
|
|
600
|
+
candidate.reasons.add(`path-phrase:${phrase}`);
|
|
601
|
+
return; // 最初のマッチのみ適用
|
|
444
602
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
603
|
+
}
|
|
604
|
+
// パスセグメントがマッチする場合(中程度の重み)
|
|
605
|
+
const pathParts = lowerPath.split("/");
|
|
606
|
+
for (const segment of extractedTerms.pathSegments) {
|
|
607
|
+
if (pathParts.includes(segment)) {
|
|
608
|
+
candidate.score += weights.pathMatch;
|
|
609
|
+
candidate.reasons.add(`path-segment:${segment}`);
|
|
610
|
+
return; // 最初のマッチのみ適用
|
|
453
611
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
612
|
+
}
|
|
613
|
+
// 通常のキーワードがパスに含まれる場合(低い重み)
|
|
614
|
+
for (const keyword of extractedTerms.keywords) {
|
|
615
|
+
if (lowerPath.includes(keyword)) {
|
|
616
|
+
candidate.score += weights.pathMatch * 0.5; // 0.5倍のブースト
|
|
617
|
+
candidate.reasons.add(`path-keyword:${keyword}`);
|
|
618
|
+
return; // 最初のマッチのみ適用
|
|
461
619
|
}
|
|
462
620
|
}
|
|
463
|
-
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* 加算的ファイルペナルティを適用
|
|
624
|
+
* ブラックリストディレクトリ、テストファイル、lockファイル、設定ファイル、マイグレーションファイルに強いペナルティ
|
|
625
|
+
* @returns true if penalty was applied and processing should stop
|
|
626
|
+
*/
|
|
627
|
+
function applyAdditiveFilePenalties(candidate, path, lowerPath, fileName) {
|
|
628
|
+
// Blacklisted directories - effectively remove
|
|
464
629
|
const blacklistedDirs = [
|
|
465
630
|
".cursor/",
|
|
466
631
|
".devcontainer/",
|
|
@@ -484,18 +649,18 @@ function applyBoostProfile(candidate, row, profile, extractedTerms, pathMatchWei
|
|
|
484
649
|
"temp/",
|
|
485
650
|
];
|
|
486
651
|
if (blacklistedDirs.some((dir) => path.startsWith(dir))) {
|
|
487
|
-
candidate.score = -100;
|
|
652
|
+
candidate.score = -100;
|
|
488
653
|
candidate.reasons.add("penalty:blacklisted-dir");
|
|
489
|
-
return;
|
|
654
|
+
return true;
|
|
490
655
|
}
|
|
491
|
-
//
|
|
656
|
+
// Test files - strong penalty
|
|
492
657
|
const testPatterns = [".spec.ts", ".spec.js", ".test.ts", ".test.js", ".spec.tsx", ".test.tsx"];
|
|
493
658
|
if (testPatterns.some((pattern) => lowerPath.endsWith(pattern))) {
|
|
494
|
-
candidate.score -= 2.0;
|
|
659
|
+
candidate.score -= 2.0;
|
|
495
660
|
candidate.reasons.add("penalty:test-file");
|
|
496
|
-
return;
|
|
661
|
+
return true;
|
|
497
662
|
}
|
|
498
|
-
//
|
|
663
|
+
// Lock files - very strong penalty
|
|
499
664
|
const lockFiles = [
|
|
500
665
|
"package-lock.json",
|
|
501
666
|
"pnpm-lock.yaml",
|
|
@@ -506,11 +671,11 @@ function applyBoostProfile(candidate, row, profile, extractedTerms, pathMatchWei
|
|
|
506
671
|
"poetry.lock",
|
|
507
672
|
];
|
|
508
673
|
if (lockFiles.some((lockFile) => fileName === lockFile)) {
|
|
509
|
-
candidate.score -= 3.0;
|
|
674
|
+
candidate.score -= 3.0;
|
|
510
675
|
candidate.reasons.add("penalty:lock-file");
|
|
511
|
-
return;
|
|
676
|
+
return true;
|
|
512
677
|
}
|
|
513
|
-
//
|
|
678
|
+
// Configuration files - strong penalty
|
|
514
679
|
const configPatterns = [
|
|
515
680
|
".config.js",
|
|
516
681
|
".config.ts",
|
|
@@ -531,56 +696,103 @@ function applyBoostProfile(candidate, row, profile, extractedTerms, pathMatchWei
|
|
|
531
696
|
fileName === "Dockerfile" ||
|
|
532
697
|
fileName === "docker-compose.yml" ||
|
|
533
698
|
fileName === "docker-compose.yaml") {
|
|
534
|
-
candidate.score -= 1.5;
|
|
699
|
+
candidate.score -= 1.5;
|
|
535
700
|
candidate.reasons.add("penalty:config-file");
|
|
536
|
-
return;
|
|
701
|
+
return true;
|
|
537
702
|
}
|
|
538
|
-
//
|
|
703
|
+
// Migration files - strong penalty
|
|
539
704
|
if (lowerPath.includes("migrate") || lowerPath.includes("migration")) {
|
|
540
|
-
candidate.score -= 2.0;
|
|
705
|
+
candidate.score -= 2.0;
|
|
541
706
|
candidate.reasons.add("penalty:migration-file");
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
return false; // No penalty applied, continue processing
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* ファイルタイプ別の乗算的ペナルティ/ブーストを適用(v0.7.0+)
|
|
713
|
+
* profile="docs": ドキュメントファイルをブースト
|
|
714
|
+
* profile="default": ドキュメントファイルにペナルティ、実装ファイルをブースト
|
|
715
|
+
*/
|
|
716
|
+
function applyFileTypeMultipliers(candidate, path, ext, profile, weights) {
|
|
717
|
+
if (profile === "none") {
|
|
542
718
|
return;
|
|
543
719
|
}
|
|
720
|
+
// ✅ CRITICAL SAFETY: profile="docs" mode boosts docs, skips penalties
|
|
544
721
|
if (profile === "docs") {
|
|
545
|
-
|
|
546
|
-
if (
|
|
547
|
-
candidate.
|
|
722
|
+
const docExtensions = [".md", ".yaml", ".yml", ".mdc"];
|
|
723
|
+
if (docExtensions.some((docExt) => path.endsWith(docExt))) {
|
|
724
|
+
candidate.scoreMultiplier *= 1.5; // 50% boost for docs
|
|
548
725
|
candidate.reasons.add("boost:doc-file");
|
|
549
726
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
candidate.reasons.add("penalty:impl-file");
|
|
553
|
-
}
|
|
727
|
+
// No penalty for implementation files in "docs" mode
|
|
728
|
+
return;
|
|
554
729
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
730
|
+
// DEFAULT PROFILE: Use MULTIPLICATIVE penalties for config/docs, MULTIPLICATIVE boosts for impl files
|
|
731
|
+
if (profile === "default") {
|
|
732
|
+
const fileName = path.split("/").pop() ?? "";
|
|
733
|
+
// ✅ Step 1: Config files get strongest penalty (95% reduction)
|
|
734
|
+
if (isConfigFile(path, fileName)) {
|
|
735
|
+
candidate.scoreMultiplier *= weights.configPenaltyMultiplier; // 0.05 = 95% reduction
|
|
736
|
+
candidate.reasons.add("penalty:config-file");
|
|
737
|
+
return; // Don't apply impl boosts to config files
|
|
738
|
+
}
|
|
739
|
+
// ✅ Step 2: Documentation files get moderate penalty (50% reduction)
|
|
740
|
+
const docExtensions = [".md", ".yaml", ".yml", ".mdc"];
|
|
559
741
|
if (docExtensions.some((docExt) => path.endsWith(docExt))) {
|
|
560
|
-
candidate.
|
|
742
|
+
candidate.scoreMultiplier *= weights.docPenaltyMultiplier; // 0.5 = 50% reduction
|
|
561
743
|
candidate.reasons.add("penalty:doc-file");
|
|
744
|
+
return; // Don't apply impl boosts to docs
|
|
562
745
|
}
|
|
563
|
-
//
|
|
746
|
+
// ✅ Step 3: Implementation files get multiplicative boost
|
|
564
747
|
if (path.startsWith("src/app/")) {
|
|
565
|
-
candidate.
|
|
748
|
+
candidate.scoreMultiplier *= weights.implBoostMultiplier * 1.4; // Extra boost for app files
|
|
566
749
|
candidate.reasons.add("boost:app-file");
|
|
567
750
|
}
|
|
568
751
|
else if (path.startsWith("src/components/")) {
|
|
569
|
-
candidate.
|
|
752
|
+
candidate.scoreMultiplier *= weights.implBoostMultiplier * 1.3;
|
|
570
753
|
candidate.reasons.add("boost:component-file");
|
|
571
754
|
}
|
|
572
755
|
else if (path.startsWith("src/lib/")) {
|
|
573
|
-
candidate.
|
|
756
|
+
candidate.scoreMultiplier *= weights.implBoostMultiplier * 1.2;
|
|
574
757
|
candidate.reasons.add("boost:lib-file");
|
|
575
758
|
}
|
|
576
759
|
else if (path.startsWith("src/")) {
|
|
577
760
|
if (ext === ".ts" || ext === ".tsx" || ext === ".js") {
|
|
578
|
-
candidate.
|
|
761
|
+
candidate.scoreMultiplier *= weights.implBoostMultiplier;
|
|
579
762
|
candidate.reasons.add("boost:impl-file");
|
|
580
763
|
}
|
|
581
764
|
}
|
|
582
765
|
}
|
|
583
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* contextBundle専用のブーストプロファイル適用(v0.7.0+: リファクタリング版)
|
|
769
|
+
* 複雑度を削減するために3つのヘルパー関数に分割:
|
|
770
|
+
* 1. applyPathBasedScoring: パスベースの加算的スコアリング
|
|
771
|
+
* 2. applyAdditiveFilePenalties: 強力な加算的ペナルティ
|
|
772
|
+
* 3. applyFileTypeMultipliers: 乗算的ペナルティ/ブースト
|
|
773
|
+
*
|
|
774
|
+
* CRITICAL SAFETY RULES:
|
|
775
|
+
* 1. Multipliers are stored in candidate.scoreMultiplier, applied AFTER all additive scoring
|
|
776
|
+
* 2. profile="docs" skips documentation penalties (allows doc-focused queries)
|
|
777
|
+
* 3. Blacklist/test/lock/config files keep additive penalties (already very strong)
|
|
778
|
+
*/
|
|
779
|
+
function applyBoostProfile(candidate, row, profile, weights, extractedTerms) {
|
|
780
|
+
if (profile === "none") {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const { path, ext } = row;
|
|
784
|
+
const lowerPath = path.toLowerCase();
|
|
785
|
+
const fileName = path.split("/").pop() ?? "";
|
|
786
|
+
// Step 1: パスベースのスコアリング(加算的ブースト)
|
|
787
|
+
applyPathBasedScoring(candidate, lowerPath, weights, extractedTerms);
|
|
788
|
+
// Step 2: 加算的ペナルティ(ブラックリスト、テスト、lock、設定、マイグレーション)
|
|
789
|
+
const shouldStop = applyAdditiveFilePenalties(candidate, path, lowerPath, fileName);
|
|
790
|
+
if (shouldStop) {
|
|
791
|
+
return; // ペナルティが適用された場合は処理終了
|
|
792
|
+
}
|
|
793
|
+
// Step 3: ファイルタイプ別の乗算的ペナルティ/ブースト
|
|
794
|
+
applyFileTypeMultipliers(candidate, path, ext, profile, weights);
|
|
795
|
+
}
|
|
584
796
|
export async function filesSearch(context, params) {
|
|
585
797
|
const { db, repoId } = context;
|
|
586
798
|
const { query } = params;
|
|
@@ -663,11 +875,14 @@ export async function filesSearch(context, params) {
|
|
|
663
875
|
}
|
|
664
876
|
const rows = await db.all(sql, values);
|
|
665
877
|
const boostProfile = params.boost_profile ?? "default";
|
|
878
|
+
// ✅ v0.7.0+: Load configurable scoring weights for unified boosting logic
|
|
879
|
+
// Note: filesSearch doesn't have a separate profile parameter, uses default weights
|
|
880
|
+
const weights = loadScoringProfile(null);
|
|
666
881
|
return rows
|
|
667
882
|
.map((row) => {
|
|
668
883
|
const { preview, line } = buildPreview(row.content ?? "", query);
|
|
669
884
|
const baseScore = row.score ?? 1.0; // FTS時はBM25スコア、ILIKE時は1.0
|
|
670
|
-
const boostedScore = applyFileTypeBoost(row.path, baseScore, boostProfile);
|
|
885
|
+
const boostedScore = applyFileTypeBoost(row.path, baseScore, boostProfile, weights);
|
|
671
886
|
return {
|
|
672
887
|
path: row.path,
|
|
673
888
|
preview,
|
|
@@ -832,7 +1047,7 @@ export async function contextBundle(context, params) {
|
|
|
832
1047
|
candidate.reasons.add(`phrase:${phrase}`);
|
|
833
1048
|
}
|
|
834
1049
|
// Apply boost profile once per file
|
|
835
|
-
applyBoostProfile(candidate, row, boostProfile,
|
|
1050
|
+
applyBoostProfile(candidate, row, boostProfile, weights, extractedTerms);
|
|
836
1051
|
// Use first matched phrase for preview (guaranteed to exist due to length check above)
|
|
837
1052
|
const { line } = buildPreview(row.content, matchedPhrases[0]);
|
|
838
1053
|
candidate.matchLine =
|
|
@@ -890,7 +1105,7 @@ export async function contextBundle(context, params) {
|
|
|
890
1105
|
candidate.reasons.add(`text:${keyword}`);
|
|
891
1106
|
}
|
|
892
1107
|
// Apply boost profile once per file
|
|
893
|
-
applyBoostProfile(candidate, row, boostProfile,
|
|
1108
|
+
applyBoostProfile(candidate, row, boostProfile, weights, extractedTerms);
|
|
894
1109
|
// Use first matched keyword for preview (guaranteed to exist due to length check above)
|
|
895
1110
|
const { line } = buildPreview(row.content, matchedKeywords[0]);
|
|
896
1111
|
candidate.matchLine =
|
|
@@ -1011,9 +1226,22 @@ export async function contextBundle(context, params) {
|
|
|
1011
1226
|
materializedCandidates.push(candidate);
|
|
1012
1227
|
}
|
|
1013
1228
|
if (materializedCandidates.length === 0) {
|
|
1014
|
-
|
|
1229
|
+
// Get warnings from WarningManager (includes breaking change notification if applicable)
|
|
1230
|
+
const warnings = [...context.warningManager.responseWarnings];
|
|
1231
|
+
return {
|
|
1232
|
+
context: [],
|
|
1233
|
+
tokens_estimate: 0,
|
|
1234
|
+
...(warnings.length > 0 && { warnings }),
|
|
1235
|
+
};
|
|
1015
1236
|
}
|
|
1016
1237
|
applyStructuralScores(materializedCandidates, queryEmbedding, weights.structural);
|
|
1238
|
+
// ✅ CRITICAL SAFETY: Apply multipliers AFTER all additive scoring (v0.7.0)
|
|
1239
|
+
// Only apply to positive scores to prevent negative score inversion
|
|
1240
|
+
for (const candidate of materializedCandidates) {
|
|
1241
|
+
if (candidate.scoreMultiplier !== 1.0 && candidate.score > 0) {
|
|
1242
|
+
candidate.score *= candidate.scoreMultiplier;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1017
1245
|
const sortedCandidates = materializedCandidates
|
|
1018
1246
|
.filter((candidate) => candidate.score > 0) // Filter out candidates with negative or zero scores
|
|
1019
1247
|
.sort((a, b) => {
|
|
@@ -1083,7 +1311,13 @@ export async function contextBundle(context, params) {
|
|
|
1083
1311
|
const lineCount = Math.max(1, item.range[1] - item.range[0] + 1);
|
|
1084
1312
|
return acc + lineCount * 4;
|
|
1085
1313
|
}, 0);
|
|
1086
|
-
|
|
1314
|
+
// Get warnings from WarningManager (includes breaking change notification if applicable)
|
|
1315
|
+
const warnings = [...context.warningManager.responseWarnings];
|
|
1316
|
+
return {
|
|
1317
|
+
context: results,
|
|
1318
|
+
tokens_estimate: tokensEstimate,
|
|
1319
|
+
...(warnings.length > 0 && { warnings }),
|
|
1320
|
+
};
|
|
1087
1321
|
}
|
|
1088
1322
|
export async function semanticRerank(context, params) {
|
|
1089
1323
|
const text = params.text?.trim() ?? "";
|