kiri-mcp-server 0.9.9 → 0.10.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 +51 -7
- package/dist/package.json +4 -1
- package/dist/src/client/proxy.js +81 -12
- package/dist/src/client/proxy.js.map +1 -1
- package/dist/src/daemon/daemon.js +91 -14
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/server/abbreviations.d.ts +47 -0
- package/dist/src/server/abbreviations.d.ts.map +1 -0
- package/dist/src/server/abbreviations.js +71 -0
- package/dist/src/server/abbreviations.js.map +1 -0
- package/dist/src/server/boost-profiles.d.ts +63 -0
- package/dist/src/server/boost-profiles.d.ts.map +1 -0
- package/dist/src/server/boost-profiles.js +86 -0
- package/dist/src/server/boost-profiles.js.map +1 -0
- package/dist/src/server/handlers.d.ts +3 -2
- package/dist/src/server/handlers.d.ts.map +1 -1
- package/dist/src/server/handlers.js +457 -94
- package/dist/src/server/handlers.js.map +1 -1
- package/dist/src/server/main.d.ts.map +1 -1
- package/dist/src/server/main.js +112 -30
- package/dist/src/server/main.js.map +1 -1
- package/dist/src/server/rpc.d.ts.map +1 -1
- package/dist/src/server/rpc.js +28 -9
- package/dist/src/server/rpc.js.map +1 -1
- package/dist/src/server/rrf.d.ts +86 -0
- package/dist/src/server/rrf.d.ts.map +1 -0
- package/dist/src/server/rrf.js +108 -0
- package/dist/src/server/rrf.js.map +1 -0
- package/dist/src/server/scoring.d.ts.map +1 -1
- package/dist/src/server/scoring.js +19 -0
- package/dist/src/server/scoring.js.map +1 -1
- package/dist/src/shared/cli/args.d.ts +70 -0
- package/dist/src/shared/cli/args.d.ts.map +1 -0
- package/dist/src/shared/cli/args.js +84 -0
- package/dist/src/shared/cli/args.js.map +1 -0
- package/dist/src/shared/embedding/engine.d.ts +38 -0
- package/dist/src/shared/embedding/engine.d.ts.map +1 -0
- package/dist/src/shared/embedding/engine.js +6 -0
- package/dist/src/shared/embedding/engine.js.map +1 -0
- package/dist/src/shared/embedding/lsh-engine.d.ts +11 -0
- package/dist/src/shared/embedding/lsh-engine.d.ts.map +1 -0
- package/dist/src/shared/embedding/lsh-engine.js +14 -0
- package/dist/src/shared/embedding/lsh-engine.js.map +1 -0
- package/dist/src/shared/embedding/registry.d.ts +25 -0
- package/dist/src/shared/embedding/registry.d.ts.map +1 -0
- package/dist/src/shared/embedding/registry.js +50 -0
- package/dist/src/shared/embedding/registry.js.map +1 -0
- package/dist/src/shared/embedding/semantic-engine.d.ts +14 -0
- package/dist/src/shared/embedding/semantic-engine.d.ts.map +1 -0
- package/dist/src/shared/embedding/semantic-engine.js +50 -0
- package/dist/src/shared/embedding/semantic-engine.js.map +1 -0
- package/dist/src/shared/models/model-manager.d.ts +38 -0
- package/dist/src/shared/models/model-manager.d.ts.map +1 -0
- package/dist/src/shared/models/model-manager.js +116 -0
- package/dist/src/shared/models/model-manager.js.map +1 -0
- package/dist/src/shared/models/model-manifest.d.ts +22 -0
- package/dist/src/shared/models/model-manifest.d.ts.map +1 -0
- package/dist/src/shared/models/model-manifest.js +24 -0
- package/dist/src/shared/models/model-manifest.js.map +1 -0
- package/dist/src/shared/utils/validation.d.ts +14 -0
- package/dist/src/shared/utils/validation.d.ts.map +1 -0
- package/dist/src/shared/utils/validation.js +22 -0
- package/dist/src/shared/utils/validation.js.map +1 -0
- package/package.json +4 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { checkFTSSchemaExists } from "../indexer/schema.js";
|
|
3
4
|
import { generateEmbedding, structuralSimilarity } from "../shared/embedding.js";
|
|
4
5
|
import { encode as encodeGPT, tokenizeText } from "../shared/tokenizer.js";
|
|
5
6
|
import { getRepoPathCandidates, normalizeRepoPath } from "../shared/utils/path.js";
|
|
7
|
+
import { expandAbbreviations } from "./abbreviations.js";
|
|
8
|
+
import { getBoostProfile, } from "./boost-profiles.js";
|
|
6
9
|
import { coerceProfileName, loadScoringProfile } from "./scoring.js";
|
|
7
10
|
// Configuration file patterns (v0.8.0+: consolidated to avoid duplication)
|
|
8
11
|
// Comprehensive list covering multiple languages and tools
|
|
@@ -225,6 +228,9 @@ const MAX_DEPENDENCY_SEEDS_QUERY_LIMIT = 100; // SQL injection防御用の上限
|
|
|
225
228
|
const NEARBY_LIMIT = 6;
|
|
226
229
|
const FALLBACK_SNIPPET_WINDOW = 40; // Reduced from 120 to optimize token usage
|
|
227
230
|
const MAX_RERANK_LIMIT = 50;
|
|
231
|
+
// Issue #68: Path/Large File Penalty configuration (環境変数で上書き可能)
|
|
232
|
+
const PATH_MISS_DELTA = parseFloat(process.env.KIRI_PATH_MISS_DELTA || "-0.5");
|
|
233
|
+
const LARGE_FILE_DELTA = parseFloat(process.env.KIRI_LARGE_FILE_DELTA || "-0.8");
|
|
228
234
|
const MAX_WHY_TAGS = 10;
|
|
229
235
|
// 項目3: whyタグの優先度マップ(低い数値ほど高優先度)
|
|
230
236
|
// All actual tag prefixes used in the codebase
|
|
@@ -513,6 +519,8 @@ function ensureCandidate(map, filePath) {
|
|
|
513
519
|
ext: null,
|
|
514
520
|
embedding: null,
|
|
515
521
|
semanticSimilarity: null,
|
|
522
|
+
pathMatchHits: 0, // Issue #68: Track path match count
|
|
523
|
+
penalties: [], // Issue #68: Penalty log for telemetry
|
|
516
524
|
};
|
|
517
525
|
map.set(filePath, candidate);
|
|
518
526
|
}
|
|
@@ -677,6 +685,22 @@ function splitQueryWords(query) {
|
|
|
677
685
|
const words = query.split(/[\s/\-_]+/).filter((w) => w.length > 2);
|
|
678
686
|
return words.length > 0 ? words : [query]; // 全て除外された場合は元のクエリを使用
|
|
679
687
|
}
|
|
688
|
+
/**
|
|
689
|
+
* パス固有のマルチプライヤーを取得(最長プレフィックスマッチ)
|
|
690
|
+
* 配列の順序に依存せず、常に最長一致のプレフィックスを選択
|
|
691
|
+
* @param filePath - ファイルパス
|
|
692
|
+
* @param profileConfig - ブーストプロファイル設定
|
|
693
|
+
* @returns パス固有のマルチプライヤー(マッチなしの場合は1.0)
|
|
694
|
+
*/
|
|
695
|
+
function getPathMultiplier(filePath, profileConfig) {
|
|
696
|
+
let bestMatch = { prefix: "", multiplier: 1.0 };
|
|
697
|
+
for (const { prefix, multiplier } of profileConfig.pathMultipliers) {
|
|
698
|
+
if (filePath.startsWith(prefix) && prefix.length > bestMatch.prefix.length) {
|
|
699
|
+
bestMatch = { prefix, multiplier };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return bestMatch.multiplier;
|
|
703
|
+
}
|
|
680
704
|
/**
|
|
681
705
|
* files_search専用のファイルタイプブースト適用(v0.7.0+: 設定可能な乗算的ペナルティ)
|
|
682
706
|
* context_bundleと同じ乗算的ペナルティロジックを使用
|
|
@@ -686,7 +710,7 @@ function splitQueryWords(query) {
|
|
|
686
710
|
* @param weights - スコアリングウェイト設定(乗算的ペナルティに使用)
|
|
687
711
|
* @returns ブースト適用後のスコア
|
|
688
712
|
*/
|
|
689
|
-
function applyFileTypeBoost(path, baseScore,
|
|
713
|
+
function applyFileTypeBoost(path, baseScore, profileConfig, _weights) {
|
|
690
714
|
// Blacklisted directories that are almost always irrelevant for code context
|
|
691
715
|
const blacklistedDirs = [
|
|
692
716
|
".cursor/",
|
|
@@ -699,58 +723,39 @@ function applyFileTypeBoost(path, baseScore, profile = "default", weights) {
|
|
|
699
723
|
];
|
|
700
724
|
for (const dir of blacklistedDirs) {
|
|
701
725
|
if (path.startsWith(dir)) {
|
|
702
|
-
//
|
|
703
|
-
if (
|
|
726
|
+
// ✅ Decoupled: Check denylist overrides from profile config
|
|
727
|
+
if (profileConfig.denylistOverrides.includes(dir)) {
|
|
704
728
|
continue;
|
|
705
729
|
}
|
|
706
730
|
return -100; // Effectively remove it
|
|
707
731
|
}
|
|
708
732
|
}
|
|
709
|
-
|
|
710
|
-
return baseScore;
|
|
711
|
-
}
|
|
712
|
-
// Extract file extension for type detection
|
|
733
|
+
const fileName = path.split("/").pop() ?? "";
|
|
713
734
|
const ext = path.includes(".") ? path.substring(path.lastIndexOf(".")) : null;
|
|
714
|
-
// ✅ UNIFIED LOGIC: Use same multiplicative penalties as context_bundle
|
|
715
|
-
if (profile === "docs") {
|
|
716
|
-
// Boost documentation files
|
|
717
|
-
if (path.endsWith(".md") || path.endsWith(".yaml") || path.endsWith(".yml")) {
|
|
718
|
-
return baseScore * 1.5; // 50% boost (same as context_bundle)
|
|
719
|
-
}
|
|
720
|
-
// Penalty for implementation files in docs mode
|
|
721
|
-
if (path.startsWith("src/") &&
|
|
722
|
-
(path.endsWith(".ts") || path.endsWith(".js") || path.endsWith(".tsx"))) {
|
|
723
|
-
return baseScore * 0.5; // 50% penalty
|
|
724
|
-
}
|
|
725
|
-
return baseScore;
|
|
726
|
-
}
|
|
727
|
-
// Default profile: Use configurable multiplicative penalties
|
|
728
735
|
let multiplier = 1.0;
|
|
729
|
-
|
|
730
|
-
// ✅ Step 1: Config files get strongest penalty (95% reduction)
|
|
736
|
+
// ✅ Step 1: Config files
|
|
731
737
|
if (isConfigFile(path, fileName)) {
|
|
732
|
-
multiplier *=
|
|
738
|
+
multiplier *= profileConfig.fileTypeMultipliers.config;
|
|
733
739
|
return baseScore * multiplier;
|
|
734
740
|
}
|
|
735
|
-
// ✅ Step 2: Documentation files
|
|
741
|
+
// ✅ Step 2: Documentation files
|
|
736
742
|
const docExtensions = [".md", ".yaml", ".yml", ".mdc"];
|
|
737
743
|
if (docExtensions.some((docExt) => path.endsWith(docExt))) {
|
|
738
|
-
multiplier *=
|
|
744
|
+
multiplier *= profileConfig.fileTypeMultipliers.doc;
|
|
739
745
|
return baseScore * multiplier;
|
|
740
746
|
}
|
|
741
|
-
// ✅ Step 3: Implementation
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
multiplier *=
|
|
747
|
-
|
|
748
|
-
else if (path.startsWith("src/lib/")) {
|
|
749
|
-
multiplier *= weights.implBoostMultiplier * 1.2;
|
|
747
|
+
// ✅ Step 3: Implementation files with path-specific boosts
|
|
748
|
+
const implMultiplier = profileConfig.fileTypeMultipliers.impl;
|
|
749
|
+
// ✅ Use longest-prefix-match logic (order-independent)
|
|
750
|
+
const pathBoost = getPathMultiplier(path, profileConfig);
|
|
751
|
+
if (pathBoost !== 1.0) {
|
|
752
|
+
multiplier *= implMultiplier * pathBoost;
|
|
753
|
+
return baseScore * multiplier;
|
|
750
754
|
}
|
|
751
|
-
|
|
755
|
+
// Fallback for other src/ files
|
|
756
|
+
if (path.startsWith("src/")) {
|
|
752
757
|
if (ext === ".ts" || ext === ".tsx" || ext === ".js") {
|
|
753
|
-
multiplier *=
|
|
758
|
+
multiplier *= implMultiplier;
|
|
754
759
|
}
|
|
755
760
|
}
|
|
756
761
|
// Test files: additive penalty (keep strong for files_search)
|
|
@@ -767,29 +772,92 @@ function applyPathBasedScoring(candidate, lowerPath, weights, extractedTerms) {
|
|
|
767
772
|
if (!extractedTerms || weights.pathMatch <= 0) {
|
|
768
773
|
return;
|
|
769
774
|
}
|
|
775
|
+
// hasAddedScore gates additive boosts; pathMatchHits/reasons still track every hit for penalties/debugging.
|
|
776
|
+
let hasAddedScore = false;
|
|
770
777
|
// フレーズがパスに完全一致する場合(最高の重み)
|
|
771
778
|
for (const phrase of extractedTerms.phrases) {
|
|
772
779
|
if (lowerPath.includes(phrase)) {
|
|
773
|
-
|
|
780
|
+
if (!hasAddedScore) {
|
|
781
|
+
candidate.score += weights.pathMatch * 1.5; // 1.5倍のブースト
|
|
782
|
+
hasAddedScore = true;
|
|
783
|
+
}
|
|
774
784
|
candidate.reasons.add(`path-phrase:${phrase}`);
|
|
775
|
-
|
|
785
|
+
candidate.pathMatchHits++; // Issue #68: Track path match for penalty calculation
|
|
776
786
|
}
|
|
777
787
|
}
|
|
778
788
|
// パスセグメントがマッチする場合(中程度の重み)
|
|
779
789
|
const pathParts = lowerPath.split("/");
|
|
780
790
|
for (const segment of extractedTerms.pathSegments) {
|
|
781
791
|
if (pathParts.includes(segment)) {
|
|
782
|
-
|
|
792
|
+
if (!hasAddedScore) {
|
|
793
|
+
candidate.score += weights.pathMatch;
|
|
794
|
+
hasAddedScore = true;
|
|
795
|
+
}
|
|
783
796
|
candidate.reasons.add(`path-segment:${segment}`);
|
|
784
|
-
|
|
797
|
+
candidate.pathMatchHits++; // Issue #68: Track path match for penalty calculation
|
|
785
798
|
}
|
|
786
799
|
}
|
|
787
800
|
// 通常のキーワードがパスに含まれる場合(低い重み)
|
|
801
|
+
const matchedKeywords = new Set();
|
|
788
802
|
for (const keyword of extractedTerms.keywords) {
|
|
789
803
|
if (lowerPath.includes(keyword)) {
|
|
790
|
-
|
|
804
|
+
if (!hasAddedScore) {
|
|
805
|
+
candidate.score += weights.pathMatch * 0.5; // 0.5倍のブースト
|
|
806
|
+
hasAddedScore = true;
|
|
807
|
+
}
|
|
791
808
|
candidate.reasons.add(`path-keyword:${keyword}`);
|
|
792
|
-
|
|
809
|
+
candidate.pathMatchHits++; // Issue #68: Track path match for penalty calculation
|
|
810
|
+
matchedKeywords.add(keyword); // Track for abbreviation expansion
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// ADR 003: Abbreviation expansion for keywords with zero exact matches
|
|
814
|
+
// Avoid double-counting by only expanding keywords that didn't match exactly
|
|
815
|
+
// Skip abbreviation expansion for files that will be heavily penalized (test/config/lock files)
|
|
816
|
+
const fileName = lowerPath.split("/").pop() ?? "";
|
|
817
|
+
const testPatterns = [".spec.ts", ".spec.js", ".test.ts", ".test.js", ".spec.tsx", ".test.tsx"];
|
|
818
|
+
const lockFiles = [
|
|
819
|
+
"package-lock.json",
|
|
820
|
+
"pnpm-lock.yaml",
|
|
821
|
+
"yarn.lock",
|
|
822
|
+
"bun.lockb",
|
|
823
|
+
"gemfile.lock",
|
|
824
|
+
"cargo.lock",
|
|
825
|
+
"poetry.lock",
|
|
826
|
+
];
|
|
827
|
+
const configPatterns = [
|
|
828
|
+
"tsconfig.json",
|
|
829
|
+
"vite.config",
|
|
830
|
+
"vitest.config",
|
|
831
|
+
"eslint.config",
|
|
832
|
+
"prettier.config",
|
|
833
|
+
"package.json",
|
|
834
|
+
".env",
|
|
835
|
+
"dockerfile",
|
|
836
|
+
];
|
|
837
|
+
const shouldSkipAbbreviation = testPatterns.some((pattern) => lowerPath.endsWith(pattern)) ||
|
|
838
|
+
lockFiles.some((lock) => fileName === lock) ||
|
|
839
|
+
configPatterns.some((cfg) => fileName.includes(cfg));
|
|
840
|
+
if (!shouldSkipAbbreviation) {
|
|
841
|
+
for (const keyword of extractedTerms.keywords) {
|
|
842
|
+
if (matchedKeywords.has(keyword)) {
|
|
843
|
+
continue; // Skip keywords that already matched exactly
|
|
844
|
+
}
|
|
845
|
+
const expandedTerms = expandAbbreviations(keyword);
|
|
846
|
+
// Try each expanded variant (except the original keyword itself)
|
|
847
|
+
for (const term of expandedTerms) {
|
|
848
|
+
if (term === keyword)
|
|
849
|
+
continue; // Skip original to avoid duplicate check
|
|
850
|
+
if (lowerPath.includes(term)) {
|
|
851
|
+
// Lower weight (0.4x) for abbreviation-expanded matches
|
|
852
|
+
if (!hasAddedScore) {
|
|
853
|
+
candidate.score += weights.pathMatch * 0.4;
|
|
854
|
+
hasAddedScore = true;
|
|
855
|
+
}
|
|
856
|
+
candidate.reasons.add(`abbr-path:${keyword}→${term}`);
|
|
857
|
+
candidate.pathMatchHits++; // Count for penalty calculation
|
|
858
|
+
break; // Only count first match per keyword to avoid over-boosting
|
|
859
|
+
}
|
|
860
|
+
}
|
|
793
861
|
}
|
|
794
862
|
}
|
|
795
863
|
}
|
|
@@ -799,7 +867,7 @@ function applyPathBasedScoring(candidate, lowerPath, weights, extractedTerms) {
|
|
|
799
867
|
* @param profile - boost_profile設定("docs"の場合はdocs/ディレクトリのブラックリストをスキップ)
|
|
800
868
|
* @returns true if penalty was applied and processing should stop
|
|
801
869
|
*/
|
|
802
|
-
function applyAdditiveFilePenalties(candidate, path, lowerPath, fileName,
|
|
870
|
+
function applyAdditiveFilePenalties(candidate, path, lowerPath, fileName, profileConfig) {
|
|
803
871
|
// Blacklisted directories - effectively remove
|
|
804
872
|
const blacklistedDirs = [
|
|
805
873
|
".cursor/",
|
|
@@ -825,10 +893,9 @@ function applyAdditiveFilePenalties(candidate, path, lowerPath, fileName, profil
|
|
|
825
893
|
];
|
|
826
894
|
for (const dir of blacklistedDirs) {
|
|
827
895
|
if (path.startsWith(dir)) {
|
|
828
|
-
// ✅
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
continue; // このブラックリストエントリをスキップ
|
|
896
|
+
// ✅ Decoupled: Check denylist overrides from profile config
|
|
897
|
+
if (profileConfig.denylistOverrides.includes(dir)) {
|
|
898
|
+
continue; // Skip this blacklisted directory
|
|
832
899
|
}
|
|
833
900
|
candidate.score = -100;
|
|
834
901
|
candidate.reasons.add("penalty:blacklisted-dir");
|
|
@@ -857,7 +924,7 @@ function applyAdditiveFilePenalties(candidate, path, lowerPath, fileName, profil
|
|
|
857
924
|
candidate.reasons.add("penalty:lock-file");
|
|
858
925
|
return true;
|
|
859
926
|
}
|
|
860
|
-
// Configuration files -
|
|
927
|
+
// Configuration files - penalty handling depends on profile
|
|
861
928
|
const configPatterns = [
|
|
862
929
|
".config.js",
|
|
863
930
|
".config.ts",
|
|
@@ -878,6 +945,12 @@ function applyAdditiveFilePenalties(candidate, path, lowerPath, fileName, profil
|
|
|
878
945
|
fileName === "Dockerfile" ||
|
|
879
946
|
fileName === "docker-compose.yml" ||
|
|
880
947
|
fileName === "docker-compose.yaml") {
|
|
948
|
+
// ✅ Use explicit flag instead of magic number (0.3) to determine behavior
|
|
949
|
+
// This decouples profile detection from multiplier values
|
|
950
|
+
if (profileConfig.skipConfigAdditivePenalty) {
|
|
951
|
+
return false; // Continue to multiplicative penalty only
|
|
952
|
+
}
|
|
953
|
+
// For other profiles, apply strong additive penalty
|
|
881
954
|
candidate.score -= 1.5;
|
|
882
955
|
candidate.reasons.add("penalty:config-file");
|
|
883
956
|
return true;
|
|
@@ -895,54 +968,50 @@ function applyAdditiveFilePenalties(candidate, path, lowerPath, fileName, profil
|
|
|
895
968
|
* profile="docs": ドキュメントファイルをブースト
|
|
896
969
|
* profile="default": ドキュメントファイルにペナルティ、実装ファイルをブースト
|
|
897
970
|
*/
|
|
898
|
-
function applyFileTypeMultipliers(candidate, path, ext,
|
|
899
|
-
|
|
900
|
-
|
|
971
|
+
function applyFileTypeMultipliers(candidate, path, ext, profileConfig, _weights) {
|
|
972
|
+
const fileName = path.split("/").pop() ?? "";
|
|
973
|
+
// ✅ Step 1: Config files
|
|
974
|
+
if (isConfigFile(path, fileName)) {
|
|
975
|
+
candidate.scoreMultiplier *= profileConfig.fileTypeMultipliers.config;
|
|
976
|
+
candidate.reasons.add("penalty:config-file");
|
|
977
|
+
return; // Don't apply impl boosts to config files
|
|
901
978
|
}
|
|
902
|
-
// ✅
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
979
|
+
// ✅ Step 2: Documentation files
|
|
980
|
+
const docExtensions = [".md", ".yaml", ".yml", ".mdc"];
|
|
981
|
+
if (docExtensions.some((docExt) => path.endsWith(docExt))) {
|
|
982
|
+
const docMultiplier = profileConfig.fileTypeMultipliers.doc;
|
|
983
|
+
candidate.scoreMultiplier *= docMultiplier;
|
|
984
|
+
if (docMultiplier > 1.0) {
|
|
907
985
|
candidate.reasons.add("boost:doc-file");
|
|
908
986
|
}
|
|
909
|
-
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
// DEFAULT PROFILE: Use MULTIPLICATIVE penalties for config/docs, MULTIPLICATIVE boosts for impl files
|
|
913
|
-
if (profile === "default") {
|
|
914
|
-
const fileName = path.split("/").pop() ?? "";
|
|
915
|
-
// ✅ Step 1: Config files get strongest penalty (95% reduction)
|
|
916
|
-
if (isConfigFile(path, fileName)) {
|
|
917
|
-
candidate.scoreMultiplier *= weights.configPenaltyMultiplier; // 0.05 = 95% reduction
|
|
918
|
-
candidate.reasons.add("penalty:config-file");
|
|
919
|
-
return; // Don't apply impl boosts to config files
|
|
920
|
-
}
|
|
921
|
-
// ✅ Step 2: Documentation files get moderate penalty (50% reduction)
|
|
922
|
-
const docExtensions = [".md", ".yaml", ".yml", ".mdc"];
|
|
923
|
-
if (docExtensions.some((docExt) => path.endsWith(docExt))) {
|
|
924
|
-
candidate.scoreMultiplier *= weights.docPenaltyMultiplier; // 0.5 = 50% reduction
|
|
987
|
+
else if (docMultiplier < 1.0) {
|
|
925
988
|
candidate.reasons.add("penalty:doc-file");
|
|
926
|
-
return; // Don't apply impl boosts to docs
|
|
927
989
|
}
|
|
928
|
-
//
|
|
990
|
+
return; // Don't apply impl boosts to docs
|
|
991
|
+
}
|
|
992
|
+
// ✅ Step 3: Implementation files with path-specific boosts
|
|
993
|
+
const implMultiplier = profileConfig.fileTypeMultipliers.impl;
|
|
994
|
+
// ✅ Use longest-prefix-match logic (order-independent)
|
|
995
|
+
const pathBoost = getPathMultiplier(path, profileConfig);
|
|
996
|
+
if (pathBoost !== 1.0) {
|
|
997
|
+
candidate.scoreMultiplier *= implMultiplier * pathBoost;
|
|
998
|
+
// Add specific reason based on matched path
|
|
929
999
|
if (path.startsWith("src/app/")) {
|
|
930
|
-
candidate.scoreMultiplier *= weights.implBoostMultiplier * 1.4; // Extra boost for app files
|
|
931
1000
|
candidate.reasons.add("boost:app-file");
|
|
932
1001
|
}
|
|
933
1002
|
else if (path.startsWith("src/components/")) {
|
|
934
|
-
candidate.scoreMultiplier *= weights.implBoostMultiplier * 1.3;
|
|
935
1003
|
candidate.reasons.add("boost:component-file");
|
|
936
1004
|
}
|
|
937
1005
|
else if (path.startsWith("src/lib/")) {
|
|
938
|
-
candidate.scoreMultiplier *= weights.implBoostMultiplier * 1.2;
|
|
939
1006
|
candidate.reasons.add("boost:lib-file");
|
|
940
1007
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
// Fallback for other src/ files
|
|
1011
|
+
if (path.startsWith("src/")) {
|
|
1012
|
+
if (ext === ".ts" || ext === ".tsx" || ext === ".js") {
|
|
1013
|
+
candidate.scoreMultiplier *= implMultiplier;
|
|
1014
|
+
candidate.reasons.add("boost:impl-file");
|
|
946
1015
|
}
|
|
947
1016
|
}
|
|
948
1017
|
}
|
|
@@ -958,22 +1027,19 @@ function applyFileTypeMultipliers(candidate, path, ext, profile, weights) {
|
|
|
958
1027
|
* 2. profile="docs" skips documentation penalties (allows doc-focused queries)
|
|
959
1028
|
* 3. Blacklist/test/lock/config files keep additive penalties (already very strong)
|
|
960
1029
|
*/
|
|
961
|
-
function applyBoostProfile(candidate, row,
|
|
962
|
-
if (profile === "none") {
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
1030
|
+
function applyBoostProfile(candidate, row, profileConfig, weights, extractedTerms) {
|
|
965
1031
|
const { path, ext } = row;
|
|
966
1032
|
const lowerPath = path.toLowerCase();
|
|
967
1033
|
const fileName = path.split("/").pop() ?? "";
|
|
968
1034
|
// Step 1: パスベースのスコアリング(加算的ブースト)
|
|
969
1035
|
applyPathBasedScoring(candidate, lowerPath, weights, extractedTerms);
|
|
970
1036
|
// Step 2: 加算的ペナルティ(ブラックリスト、テスト、lock、設定、マイグレーション)
|
|
971
|
-
const shouldStop = applyAdditiveFilePenalties(candidate, path, lowerPath, fileName,
|
|
1037
|
+
const shouldStop = applyAdditiveFilePenalties(candidate, path, lowerPath, fileName, profileConfig);
|
|
972
1038
|
if (shouldStop) {
|
|
973
1039
|
return; // ペナルティが適用された場合は処理終了
|
|
974
1040
|
}
|
|
975
1041
|
// Step 3: ファイルタイプ別の乗算的ペナルティ/ブースト
|
|
976
|
-
applyFileTypeMultipliers(candidate, path, ext,
|
|
1042
|
+
applyFileTypeMultipliers(candidate, path, ext, profileConfig, weights);
|
|
977
1043
|
}
|
|
978
1044
|
export async function filesSearch(context, params) {
|
|
979
1045
|
const { db, repoId } = context;
|
|
@@ -1058,6 +1124,7 @@ export async function filesSearch(context, params) {
|
|
|
1058
1124
|
}
|
|
1059
1125
|
const rows = await db.all(sql, values);
|
|
1060
1126
|
const boostProfile = params.boost_profile ?? "default";
|
|
1127
|
+
const profileConfig = getBoostProfile(boostProfile);
|
|
1061
1128
|
// ✅ v0.7.0+: Load configurable scoring weights for unified boosting logic
|
|
1062
1129
|
// Note: filesSearch doesn't have a separate profile parameter, uses default weights
|
|
1063
1130
|
const weights = loadScoringProfile(null);
|
|
@@ -1077,7 +1144,9 @@ export async function filesSearch(context, params) {
|
|
|
1077
1144
|
matchLine = findFirstMatchLine(row.content ?? "", query);
|
|
1078
1145
|
}
|
|
1079
1146
|
const baseScore = row.score ?? 1.0; // FTS時はBM25スコア、ILIKE時は1.0
|
|
1080
|
-
const boostedScore =
|
|
1147
|
+
const boostedScore = boostProfile === "none"
|
|
1148
|
+
? baseScore
|
|
1149
|
+
: applyFileTypeBoost(row.path, baseScore, profileConfig, weights);
|
|
1081
1150
|
const result = {
|
|
1082
1151
|
path: row.path,
|
|
1083
1152
|
matchLine,
|
|
@@ -1177,6 +1246,265 @@ export async function snippetsGet(context, params) {
|
|
|
1177
1246
|
symbolKind,
|
|
1178
1247
|
};
|
|
1179
1248
|
}
|
|
1249
|
+
// ============================================================================
|
|
1250
|
+
// Issue #68: Path/Large File Penalty Helper Functions
|
|
1251
|
+
// ============================================================================
|
|
1252
|
+
/**
|
|
1253
|
+
* 環境変数からペナルティ機能フラグを読み取る
|
|
1254
|
+
*/
|
|
1255
|
+
function readPenaltyFlags() {
|
|
1256
|
+
return {
|
|
1257
|
+
pathPenalty: process.env.KIRI_PATH_PENALTY === "1",
|
|
1258
|
+
largeFilePenalty: process.env.KIRI_LARGE_FILE_PENALTY === "1",
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* クエリ統計を計算(単語数と平均単語長)
|
|
1263
|
+
*/
|
|
1264
|
+
function computeQueryStats(goal) {
|
|
1265
|
+
const words = goal
|
|
1266
|
+
.trim()
|
|
1267
|
+
.split(/\s+/)
|
|
1268
|
+
.filter((w) => w.length > 0);
|
|
1269
|
+
const totalLength = words.reduce((sum, w) => sum + w.length, 0);
|
|
1270
|
+
return {
|
|
1271
|
+
wordCount: words.length,
|
|
1272
|
+
avgWordLength: words.length > 0 ? totalLength / words.length : 0,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Path Miss Penaltyをcandidateに適用(レガシー: Binary penalty)
|
|
1277
|
+
* 条件: wordCount >= 2 AND avgWordLength >= 4 AND pathMatchHits === 0
|
|
1278
|
+
*
|
|
1279
|
+
* @deprecated Use applyGraduatedPenalty() instead (ADR 002)
|
|
1280
|
+
*/
|
|
1281
|
+
function applyPathMissPenalty(candidate, queryStats) {
|
|
1282
|
+
if (queryStats.wordCount >= 2 && queryStats.avgWordLength >= 4 && candidate.pathMatchHits === 0) {
|
|
1283
|
+
candidate.score += PATH_MISS_DELTA; // -0.5
|
|
1284
|
+
recordPenaltyEvent(candidate, "path-miss", PATH_MISS_DELTA, {
|
|
1285
|
+
wordCount: queryStats.wordCount,
|
|
1286
|
+
avgWordLength: queryStats.avgWordLength,
|
|
1287
|
+
pathMatchHits: candidate.pathMatchHits,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* 段階的ペナルティをcandidateに適用(Issue #68: Graduated Penalty)
|
|
1293
|
+
* ADR 002: Graduated Penalty System
|
|
1294
|
+
*
|
|
1295
|
+
* @param candidate Candidate to apply penalty to
|
|
1296
|
+
* @param queryStats Query statistics for eligibility check
|
|
1297
|
+
* @param config Graduated penalty configuration
|
|
1298
|
+
*/
|
|
1299
|
+
function applyGraduatedPenalty(candidate, queryStats, config) {
|
|
1300
|
+
const penalty = computeGraduatedPenalty(candidate.pathMatchHits, queryStats, config);
|
|
1301
|
+
if (penalty !== 0) {
|
|
1302
|
+
candidate.score += penalty;
|
|
1303
|
+
recordPenaltyEvent(candidate, "path-miss", penalty, {
|
|
1304
|
+
wordCount: queryStats.wordCount,
|
|
1305
|
+
avgWordLength: queryStats.avgWordLength,
|
|
1306
|
+
pathMatchHits: candidate.pathMatchHits,
|
|
1307
|
+
tier: candidate.pathMatchHits === 0
|
|
1308
|
+
? "tier0"
|
|
1309
|
+
: candidate.pathMatchHits === 1
|
|
1310
|
+
? "tier1"
|
|
1311
|
+
: candidate.pathMatchHits === 2
|
|
1312
|
+
? "tier2"
|
|
1313
|
+
: "no-penalty",
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Large File Penaltyをcandidateに適用
|
|
1319
|
+
* 条件: totalLines > 500 AND matchLine > 120
|
|
1320
|
+
* TODO(Issue #68): Add "no symbol at match location" check after selectSnippet integration
|
|
1321
|
+
*/
|
|
1322
|
+
function applyLargeFilePenalty(candidate) {
|
|
1323
|
+
const { totalLines, matchLine } = candidate;
|
|
1324
|
+
if (totalLines !== null && totalLines > 500 && matchLine !== null && matchLine > 120) {
|
|
1325
|
+
candidate.score += LARGE_FILE_DELTA; // -0.8
|
|
1326
|
+
recordPenaltyEvent(candidate, "large-file", LARGE_FILE_DELTA, {
|
|
1327
|
+
totalLines,
|
|
1328
|
+
matchLine,
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* ペナルティイベントを記録(テレメトリ用)
|
|
1334
|
+
*/
|
|
1335
|
+
function recordPenaltyEvent(candidate, kind, delta, details) {
|
|
1336
|
+
candidate.penalties.push({ kind, delta, details });
|
|
1337
|
+
candidate.reasons.add(`penalty:${kind}`);
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* pathMatchHits分布を計算(Issue #68: Telemetry)
|
|
1341
|
+
* LDE: 純粋関数として実装(副作用なし、イミュータブル)
|
|
1342
|
+
*/
|
|
1343
|
+
function computePathMatchDistribution(candidates) {
|
|
1344
|
+
let zero = 0;
|
|
1345
|
+
let one = 0;
|
|
1346
|
+
let two = 0;
|
|
1347
|
+
let three = 0;
|
|
1348
|
+
let fourPlus = 0;
|
|
1349
|
+
for (const candidate of candidates) {
|
|
1350
|
+
const hits = candidate.pathMatchHits;
|
|
1351
|
+
if (hits === 0)
|
|
1352
|
+
zero++;
|
|
1353
|
+
else if (hits === 1)
|
|
1354
|
+
one++;
|
|
1355
|
+
else if (hits === 2)
|
|
1356
|
+
two++;
|
|
1357
|
+
else if (hits === 3)
|
|
1358
|
+
three++;
|
|
1359
|
+
else
|
|
1360
|
+
fourPlus++;
|
|
1361
|
+
}
|
|
1362
|
+
return {
|
|
1363
|
+
zero,
|
|
1364
|
+
one,
|
|
1365
|
+
two,
|
|
1366
|
+
three,
|
|
1367
|
+
fourPlus,
|
|
1368
|
+
total: candidates.length,
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* スコア統計を計算(Issue #68: Telemetry)
|
|
1373
|
+
* LDE: 純粋関数として実装(副作用なし、イミュータブル)
|
|
1374
|
+
*/
|
|
1375
|
+
function computeScoreStats(candidates) {
|
|
1376
|
+
if (candidates.length === 0) {
|
|
1377
|
+
return { min: 0, max: 0, mean: 0, median: 0 };
|
|
1378
|
+
}
|
|
1379
|
+
const scores = candidates.map((c) => c.score).sort((a, b) => a - b);
|
|
1380
|
+
const sum = scores.reduce((acc, s) => acc + s, 0);
|
|
1381
|
+
const mean = sum / scores.length;
|
|
1382
|
+
const median = scores[Math.floor(scores.length / 2)] ?? 0;
|
|
1383
|
+
return {
|
|
1384
|
+
min: scores[0] ?? 0,
|
|
1385
|
+
max: scores[scores.length - 1] ?? 0,
|
|
1386
|
+
mean,
|
|
1387
|
+
median,
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* ペナルティ適用状況を計算(Issue #68: Telemetry)
|
|
1392
|
+
* LDE: 純粋関数として実装(副作用なし、イミュータブル)
|
|
1393
|
+
*/
|
|
1394
|
+
function computePenaltyTelemetry(candidates) {
|
|
1395
|
+
let pathMissPenalties = 0;
|
|
1396
|
+
let largeFilePenalties = 0;
|
|
1397
|
+
for (const candidate of candidates) {
|
|
1398
|
+
for (const penalty of candidate.penalties) {
|
|
1399
|
+
if (penalty.kind === "path-miss")
|
|
1400
|
+
pathMissPenalties++;
|
|
1401
|
+
if (penalty.kind === "large-file")
|
|
1402
|
+
largeFilePenalties++;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return {
|
|
1406
|
+
pathMissPenalties,
|
|
1407
|
+
largeFilePenalties,
|
|
1408
|
+
totalCandidates: candidates.length,
|
|
1409
|
+
pathMatchDistribution: computePathMatchDistribution(candidates),
|
|
1410
|
+
scoreStats: computeScoreStats(candidates),
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* テレメトリーをファイル出力(Issue #68: Debug)
|
|
1415
|
+
* LDE: 副作用を分離(I/O操作)
|
|
1416
|
+
*
|
|
1417
|
+
* JSON Lines形式で /tmp/kiri-penalty-telemetry.jsonl に追記
|
|
1418
|
+
*/
|
|
1419
|
+
function logPenaltyTelemetry(telemetry, queryStats) {
|
|
1420
|
+
const dist = telemetry.pathMatchDistribution;
|
|
1421
|
+
const scores = telemetry.scoreStats;
|
|
1422
|
+
// JSON Lines形式でテレメトリーデータを記録
|
|
1423
|
+
const telemetryRecord = {
|
|
1424
|
+
timestamp: new Date().toISOString(),
|
|
1425
|
+
query: {
|
|
1426
|
+
wordCount: queryStats.wordCount,
|
|
1427
|
+
avgWordLength: queryStats.avgWordLength,
|
|
1428
|
+
},
|
|
1429
|
+
totalCandidates: telemetry.totalCandidates,
|
|
1430
|
+
pathMissPenalties: telemetry.pathMissPenalties,
|
|
1431
|
+
largeFilePenalties: telemetry.largeFilePenalties,
|
|
1432
|
+
pathMatchDistribution: {
|
|
1433
|
+
zero: dist.zero,
|
|
1434
|
+
one: dist.one,
|
|
1435
|
+
two: dist.two,
|
|
1436
|
+
three: dist.three,
|
|
1437
|
+
fourPlus: dist.fourPlus,
|
|
1438
|
+
total: dist.total,
|
|
1439
|
+
percentages: {
|
|
1440
|
+
zero: ((dist.zero / dist.total) * 100).toFixed(1),
|
|
1441
|
+
one: ((dist.one / dist.total) * 100).toFixed(1),
|
|
1442
|
+
two: ((dist.two / dist.total) * 100).toFixed(1),
|
|
1443
|
+
three: ((dist.three / dist.total) * 100).toFixed(1),
|
|
1444
|
+
fourPlus: ((dist.fourPlus / dist.total) * 100).toFixed(1),
|
|
1445
|
+
},
|
|
1446
|
+
},
|
|
1447
|
+
scoreStats: {
|
|
1448
|
+
min: scores.min.toFixed(2),
|
|
1449
|
+
max: scores.max.toFixed(2),
|
|
1450
|
+
mean: scores.mean.toFixed(2),
|
|
1451
|
+
median: scores.median.toFixed(2),
|
|
1452
|
+
// 最大ペナルティ(-0.8)との比率
|
|
1453
|
+
penaltyRatio: ((0.8 / scores.mean) * 100).toFixed(1) + "%",
|
|
1454
|
+
},
|
|
1455
|
+
};
|
|
1456
|
+
const telemetryFile = "/tmp/kiri-penalty-telemetry.jsonl";
|
|
1457
|
+
fs.appendFileSync(telemetryFile, JSON.stringify(telemetryRecord) + "\n");
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* 環境変数から段階的ペナルティ設定を読み込む(Issue #68: Graduated Penalty)
|
|
1461
|
+
* LDE: 純粋関数(I/O分離、テスト可能)
|
|
1462
|
+
*/
|
|
1463
|
+
function readGraduatedPenaltyConfig() {
|
|
1464
|
+
return {
|
|
1465
|
+
enabled: process.env.KIRI_GRADUATED_PENALTY === "1",
|
|
1466
|
+
minWordCount: parseFloat(process.env.KIRI_PENALTY_MIN_WORD_COUNT || "2"),
|
|
1467
|
+
minAvgWordLength: parseFloat(process.env.KIRI_PENALTY_MIN_AVG_WORD_LENGTH || "4.0"),
|
|
1468
|
+
tier0Delta: parseFloat(process.env.KIRI_PENALTY_TIER_0 || "-0.8"),
|
|
1469
|
+
tier1Delta: parseFloat(process.env.KIRI_PENALTY_TIER_1 || "-0.4"),
|
|
1470
|
+
tier2Delta: parseFloat(process.env.KIRI_PENALTY_TIER_2 || "-0.2"),
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* 段階的ペナルティ値を計算(Issue #68: Graduated Penalty)
|
|
1475
|
+
* LDE: 純粋関数(副作用なし、参照透明性)
|
|
1476
|
+
*
|
|
1477
|
+
* ADR 002: Graduated Penalty System
|
|
1478
|
+
* - Tier 0 (pathMatchHits === 0): Strong penalty (no path evidence)
|
|
1479
|
+
* - Tier 1 (pathMatchHits === 1): Medium penalty (weak path evidence)
|
|
1480
|
+
* - Tier 2 (pathMatchHits === 2): Light penalty (moderate path evidence)
|
|
1481
|
+
* - Tier 3+ (pathMatchHits >= 3): No penalty (strong path evidence)
|
|
1482
|
+
*
|
|
1483
|
+
* Invariants:
|
|
1484
|
+
* - Result is always <= 0 (non-positive)
|
|
1485
|
+
* - More path hits → less penalty (monotonicity)
|
|
1486
|
+
* - Query must meet eligibility criteria
|
|
1487
|
+
*
|
|
1488
|
+
* @param pathMatchHits Number of path-based scoring matches
|
|
1489
|
+
* @param queryStats Query word count and average word length
|
|
1490
|
+
* @param config Graduated penalty configuration
|
|
1491
|
+
* @returns Penalty delta (always <= 0)
|
|
1492
|
+
*/
|
|
1493
|
+
function computeGraduatedPenalty(pathMatchHits, queryStats, config) {
|
|
1494
|
+
// Early return if query doesn't meet criteria
|
|
1495
|
+
if (queryStats.wordCount < config.minWordCount ||
|
|
1496
|
+
queryStats.avgWordLength < config.minAvgWordLength) {
|
|
1497
|
+
return 0;
|
|
1498
|
+
}
|
|
1499
|
+
// Graduated penalty tiers
|
|
1500
|
+
if (pathMatchHits === 0)
|
|
1501
|
+
return config.tier0Delta;
|
|
1502
|
+
if (pathMatchHits === 1)
|
|
1503
|
+
return config.tier1Delta;
|
|
1504
|
+
if (pathMatchHits === 2)
|
|
1505
|
+
return config.tier2Delta;
|
|
1506
|
+
return 0; // pathMatchHits >= 3: no penalty
|
|
1507
|
+
}
|
|
1180
1508
|
export async function contextBundle(context, params) {
|
|
1181
1509
|
context.warningManager.startRequest();
|
|
1182
1510
|
const { db, repoId } = context;
|
|
@@ -1224,6 +1552,9 @@ export async function contextBundle(context, params) {
|
|
|
1224
1552
|
const candidates = new Map();
|
|
1225
1553
|
const stringMatchSeeds = new Set();
|
|
1226
1554
|
const fileCache = new Map();
|
|
1555
|
+
// ✅ Cache boost profile config to avoid redundant lookups in hot path
|
|
1556
|
+
const boostProfile = params.boost_profile ?? "default";
|
|
1557
|
+
const profileConfig = getBoostProfile(boostProfile);
|
|
1227
1558
|
// フレーズマッチング(高い重み: textMatch × 2)- 統合クエリでパフォーマンス改善
|
|
1228
1559
|
if (extractedTerms.phrases.length > 0) {
|
|
1229
1560
|
const phrasePlaceholders = extractedTerms.phrases
|
|
@@ -1242,7 +1573,6 @@ export async function contextBundle(context, params) {
|
|
|
1242
1573
|
ORDER BY f.path
|
|
1243
1574
|
LIMIT ?
|
|
1244
1575
|
`, [repoId, ...extractedTerms.phrases, MAX_MATCHES_PER_KEYWORD * extractedTerms.phrases.length]);
|
|
1245
|
-
const boostProfile = params.boost_profile ?? "default";
|
|
1246
1576
|
for (const row of rows) {
|
|
1247
1577
|
if (row.content === null) {
|
|
1248
1578
|
continue;
|
|
@@ -1261,7 +1591,9 @@ export async function contextBundle(context, params) {
|
|
|
1261
1591
|
candidate.reasons.add(`phrase:${phrase}`);
|
|
1262
1592
|
}
|
|
1263
1593
|
// Apply boost profile once per file
|
|
1264
|
-
|
|
1594
|
+
if (boostProfile !== "none") {
|
|
1595
|
+
applyBoostProfile(candidate, row, profileConfig, weights, extractedTerms);
|
|
1596
|
+
}
|
|
1265
1597
|
// Use first matched phrase for preview (guaranteed to exist due to length check above)
|
|
1266
1598
|
const { line } = buildPreview(row.content, matchedPhrases[0]);
|
|
1267
1599
|
candidate.matchLine =
|
|
@@ -1301,7 +1633,6 @@ export async function contextBundle(context, params) {
|
|
|
1301
1633
|
ORDER BY f.path
|
|
1302
1634
|
LIMIT ?
|
|
1303
1635
|
`, [repoId, ...extractedTerms.keywords, MAX_MATCHES_PER_KEYWORD * extractedTerms.keywords.length]);
|
|
1304
|
-
const boostProfile = params.boost_profile ?? "default";
|
|
1305
1636
|
for (const row of rows) {
|
|
1306
1637
|
if (row.content === null) {
|
|
1307
1638
|
continue;
|
|
@@ -1319,7 +1650,9 @@ export async function contextBundle(context, params) {
|
|
|
1319
1650
|
candidate.reasons.add(`text:${keyword}`);
|
|
1320
1651
|
}
|
|
1321
1652
|
// Apply boost profile once per file
|
|
1322
|
-
|
|
1653
|
+
if (boostProfile !== "none") {
|
|
1654
|
+
applyBoostProfile(candidate, row, profileConfig, weights, extractedTerms);
|
|
1655
|
+
}
|
|
1323
1656
|
// Use first matched keyword for preview (guaranteed to exist due to length check above)
|
|
1324
1657
|
const { line } = buildPreview(row.content, matchedKeywords[0]);
|
|
1325
1658
|
candidate.matchLine =
|
|
@@ -1456,6 +1789,36 @@ export async function contextBundle(context, params) {
|
|
|
1456
1789
|
candidate.score *= candidate.scoreMultiplier;
|
|
1457
1790
|
}
|
|
1458
1791
|
}
|
|
1792
|
+
// Issue #68: Apply Path-Based Penalties (after multipliers, before sorting)
|
|
1793
|
+
const penaltyFlags = readPenaltyFlags();
|
|
1794
|
+
const queryStats = computeQueryStats(goal); // Always compute for telemetry
|
|
1795
|
+
const graduatedConfig = readGraduatedPenaltyConfig();
|
|
1796
|
+
// ADR 002: Use graduated penalty system if enabled, otherwise use legacy binary penalty
|
|
1797
|
+
if (graduatedConfig.enabled && penaltyFlags.pathPenalty) {
|
|
1798
|
+
for (const candidate of materializedCandidates) {
|
|
1799
|
+
applyGraduatedPenalty(candidate, queryStats, graduatedConfig);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
else if (penaltyFlags.pathPenalty) {
|
|
1803
|
+
// Legacy mode: Binary penalty (pathMatchHits === 0 only)
|
|
1804
|
+
for (const candidate of materializedCandidates) {
|
|
1805
|
+
applyPathMissPenalty(candidate, queryStats);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
// Issue #68: Apply Large File Penalty (after multipliers, before sorting)
|
|
1809
|
+
if (penaltyFlags.largeFilePenalty) {
|
|
1810
|
+
for (const candidate of materializedCandidates) {
|
|
1811
|
+
applyLargeFilePenalty(candidate);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
// Issue #68: Telemetry(デバッグ用、環境変数で制御)
|
|
1815
|
+
// LDE: 純粋関数(計算)と副作用(I/O)を分離
|
|
1816
|
+
const enableTelemetry = process.env.KIRI_PENALTY_TELEMETRY === "1";
|
|
1817
|
+
if (enableTelemetry) {
|
|
1818
|
+
console.error(`[DEBUG] Telemetry enabled. Flags: pathPenalty=${penaltyFlags.pathPenalty}, largeFilePenalty=${penaltyFlags.largeFilePenalty}`);
|
|
1819
|
+
const telemetry = computePenaltyTelemetry(materializedCandidates);
|
|
1820
|
+
logPenaltyTelemetry(telemetry, queryStats);
|
|
1821
|
+
}
|
|
1459
1822
|
const sortedCandidates = materializedCandidates
|
|
1460
1823
|
.filter((candidate) => candidate.score > 0) // Filter out candidates with negative or zero scores
|
|
1461
1824
|
.sort((a, b) => {
|