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.
@@ -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 - ブーストプロファイル ("default" | "docs" | "none")
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.8; // Stronger boost for docs
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; // Stronger penalty for implementation files
549
+ return baseScore * 0.5; // 50% penalty
395
550
  }
396
551
  return baseScore;
397
552
  }
398
- // Default profile: prioritize implementation files, heavily penalize docs
399
- const docExtensions = [".md", ".yaml", ".yml", ".mdc", ".json"];
400
- if (docExtensions.some((ext) => path.endsWith(ext))) {
401
- return baseScore * 0.1; // Heavy penalty for docs
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
- return baseScore * 1.8;
569
+ multiplier *= weights.implBoostMultiplier * 1.4; // Extra boost for app files
405
570
  }
406
- if (path.startsWith("src/components/")) {
407
- return baseScore * 1.7;
571
+ else if (path.startsWith("src/components/")) {
572
+ multiplier *= weights.implBoostMultiplier * 1.3;
408
573
  }
409
- if (path.startsWith("src/lib/")) {
410
- return baseScore * 1.6;
574
+ else if (path.startsWith("src/lib/")) {
575
+ multiplier *= weights.implBoostMultiplier * 1.2;
411
576
  }
412
- if (path.startsWith("src/") &&
413
- (path.endsWith(".ts") || path.endsWith(".js") || path.endsWith(".tsx"))) {
414
- return baseScore * 1.5;
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; // Also penalize tests in default mode
584
+ return baseScore * 0.2; // Strong penalty for tests
418
585
  }
419
- return baseScore;
586
+ return baseScore * multiplier;
420
587
  }
421
588
  /**
422
- * contextBundle専用のブーストプロファイル適用
423
- * candidateのスコアと理由を直接変更する
424
- * @param candidate - スコアリング対象の候補
425
- * @param row - ファイル情報(path, ext)
426
- * @param profile - ブーストプロファイル
589
+ * パスベースのスコアリングを適用(加算的ブースト)
590
+ * goalのキーワード/フレーズがファイルパスに含まれる場合にスコアを加算
427
591
  */
428
- function applyBoostProfile(candidate, row, profile, extractedTerms, pathMatchWeight) {
429
- if (profile === "none") {
592
+ function applyPathBasedScoring(candidate, lowerPath, weights, extractedTerms) {
593
+ if (!extractedTerms || weights.pathMatch <= 0) {
430
594
  return;
431
595
  }
432
- const { path, ext } = row;
433
- const lowerPath = path.toLowerCase();
434
- const fileName = path.split("/").pop() ?? "";
435
- // パスベースのスコアリング: goalのキーワード/フレーズがファイルパスに含まれる場合にブースト
436
- if (extractedTerms && pathMatchWeight && pathMatchWeight > 0) {
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
- const pathParts = lowerPath.split("/");
447
- for (const segment of extractedTerms.pathSegments) {
448
- if (pathParts.includes(segment)) {
449
- candidate.score += pathMatchWeight;
450
- candidate.reasons.add(`path-segment:${segment}`);
451
- break; // 最初のマッチのみ適用
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
- for (const keyword of extractedTerms.keywords) {
456
- if (lowerPath.includes(keyword)) {
457
- candidate.score += pathMatchWeight * 0.5; // 0.5倍のブースト
458
- candidate.reasons.add(`path-keyword:${keyword}`);
459
- break; // 最初のマッチのみ適用
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
- // Blacklisted directories that are almost always irrelevant for code context
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; // Effectively remove it
652
+ candidate.score = -100;
488
653
  candidate.reasons.add("penalty:blacklisted-dir");
489
- return;
654
+ return true;
490
655
  }
491
- // Penalize test files explicitly (even if outside test directories)
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; // Strong penalty for test files
659
+ candidate.score -= 2.0;
495
660
  candidate.reasons.add("penalty:test-file");
496
- return;
661
+ return true;
497
662
  }
498
- // Penalize lock files and package manifests
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; // Very strong penalty for lock files
674
+ candidate.score -= 3.0;
510
675
  candidate.reasons.add("penalty:lock-file");
511
- return;
676
+ return true;
512
677
  }
513
- // Penalize configuration files
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; // Strong penalty for config files
699
+ candidate.score -= 1.5;
535
700
  candidate.reasons.add("penalty:config-file");
536
- return;
701
+ return true;
537
702
  }
538
- // Penalize migration files (by path content)
703
+ // Migration files - strong penalty
539
704
  if (lowerPath.includes("migrate") || lowerPath.includes("migration")) {
540
- candidate.score -= 2.0; // Strong penalty for migrations
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
- // DOCS PROFILE: Boost docs, penalize code
546
- if (path.endsWith(".md") || path.endsWith(".yaml") || path.endsWith(".yml")) {
547
- candidate.score += 0.8;
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
- else if (path.startsWith("src/") && (ext === ".ts" || ext === ".tsx" || ext === ".js")) {
551
- candidate.score -= 0.5;
552
- candidate.reasons.add("penalty:impl-file");
553
- }
727
+ // No penalty for implementation files in "docs" mode
728
+ return;
554
729
  }
555
- else if (profile === "default") {
556
- // DEFAULT PROFILE: Penalize docs heavily, boost implementation files.
557
- // Penalize documentation and other non-code files
558
- const docExtensions = [".md", ".yaml", ".yml", ".mdc", ".json"];
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.score -= 2.0; // Strong penalty to overcome doc-heavy keyword matches
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
- // Boost implementation files, with more specific paths getting higher scores
746
+ // Step 3: Implementation files get multiplicative boost
564
747
  if (path.startsWith("src/app/")) {
565
- candidate.score += 0.8;
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.score += 0.7;
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.score += 0.6;
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.score += 0.5;
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, extractedTerms, weights.pathMatch);
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, extractedTerms, weights.pathMatch);
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
- return { context: [], tokens_estimate: 0 };
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
- return { context: results, tokens_estimate: tokensEstimate };
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() ?? "";