kiri-mcp-server 0.11.0 → 0.13.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 +47 -12
- package/config/domain-terms.yml +37 -0
- package/config/kiri.yml +25 -0
- package/dist/config/domain-terms.yml +37 -0
- package/dist/config/kiri.yml +25 -0
- package/dist/package.json +6 -1
- package/dist/src/server/boost-profiles.d.ts +6 -5
- package/dist/src/server/boost-profiles.d.ts.map +1 -1
- package/dist/src/server/boost-profiles.js +110 -2
- package/dist/src/server/boost-profiles.js.map +1 -1
- package/dist/src/server/config-loader.d.ts +9 -0
- package/dist/src/server/config-loader.d.ts.map +1 -0
- package/dist/src/server/config-loader.js +121 -0
- package/dist/src/server/config-loader.js.map +1 -0
- package/dist/src/server/config.d.ts +4 -0
- package/dist/src/server/config.d.ts.map +1 -1
- package/dist/src/server/config.js +61 -0
- package/dist/src/server/config.js.map +1 -1
- package/dist/src/server/context.d.ts +4 -0
- package/dist/src/server/context.d.ts.map +1 -1
- package/dist/src/server/context.js +6 -0
- package/dist/src/server/context.js.map +1 -1
- package/dist/src/server/domain-terms.d.ts +28 -0
- package/dist/src/server/domain-terms.d.ts.map +1 -0
- package/dist/src/server/domain-terms.js +203 -0
- package/dist/src/server/domain-terms.js.map +1 -0
- package/dist/src/server/handlers.d.ts +3 -0
- package/dist/src/server/handlers.d.ts.map +1 -1
- package/dist/src/server/handlers.js +386 -19
- package/dist/src/server/handlers.js.map +1 -1
- package/dist/src/server/observability/metrics.d.ts +12 -0
- package/dist/src/server/observability/metrics.d.ts.map +1 -1
- package/dist/src/server/observability/metrics.js +11 -0
- package/dist/src/server/observability/metrics.js.map +1 -1
- package/dist/src/server/rpc.d.ts.map +1 -1
- package/dist/src/server/rpc.js +40 -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 +1 -0
- package/dist/src/server/runtime.js.map +1 -1
- package/dist/src/server/services/index.d.ts +2 -0
- package/dist/src/server/services/index.d.ts.map +1 -1
- package/dist/src/server/services/index.js +12 -0
- package/dist/src/server/services/index.js.map +1 -1
- package/dist/src/shared/adaptive-k.d.ts +12 -0
- package/dist/src/shared/adaptive-k.d.ts.map +1 -0
- package/dist/src/shared/adaptive-k.js +8 -0
- package/dist/src/shared/adaptive-k.js.map +1 -0
- package/dist/src/shared/config-validate-adaptive-k.d.ts +3 -0
- package/dist/src/shared/config-validate-adaptive-k.d.ts.map +1 -0
- package/dist/src/shared/config-validate-adaptive-k.js +44 -0
- package/dist/src/shared/config-validate-adaptive-k.js.map +1 -0
- package/dist/src/shared/tokenizer.d.ts +1 -1
- package/dist/src/shared/tokenizer.d.ts.map +1 -1
- package/dist/src/shared/tokenizer.js +97 -15
- package/dist/src/shared/tokenizer.js.map +1 -1
- package/package.json +33 -26
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { checkFTSSchemaExists } from "../indexer/schema.js";
|
|
4
|
+
import { getAdaptiveK } from "../shared/adaptive-k.js";
|
|
4
5
|
import { generateEmbedding, structuralSimilarity } from "../shared/embedding.js";
|
|
5
6
|
import { encode as encodeGPT, tokenizeText } from "../shared/tokenizer.js";
|
|
6
7
|
import { expandAbbreviations } from "./abbreviations.js";
|
|
7
8
|
import { getBoostProfile, } from "./boost-profiles.js";
|
|
9
|
+
import { loadPathPenalties, mergePathPenaltyEntries } from "./config-loader.js";
|
|
8
10
|
import { loadServerConfig } from "./config.js";
|
|
9
11
|
import { coerceProfileName, loadScoringProfile } from "./scoring.js";
|
|
10
12
|
import { createServerServices } from "./services/index.js";
|
|
@@ -336,6 +338,76 @@ function bucketArtifactHints(hints) {
|
|
|
336
338
|
}
|
|
337
339
|
return buckets;
|
|
338
340
|
}
|
|
341
|
+
function selectDomainPathHints(hints) {
|
|
342
|
+
const selected = [];
|
|
343
|
+
for (const hint of hints) {
|
|
344
|
+
if (!SAFE_PATH_PATTERN.test(hint.path)) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (selected.some((entry) => entry.path === hint.path)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
selected.push(hint);
|
|
351
|
+
if (selected.length >= DOMAIN_PATH_HINT_LIMIT) {
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return selected;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* AdaptiveK用カテゴリ自動検出
|
|
359
|
+
*
|
|
360
|
+
* 優先順位:
|
|
361
|
+
* 1. params.categoryが明示的に指定されている場合はそれを使用
|
|
362
|
+
* 2. profile/artifacts/boost_profileから推論
|
|
363
|
+
*
|
|
364
|
+
* @param params - context_bundleのパラメータ
|
|
365
|
+
* @returns 検出されたカテゴリ(undefined = デフォルトK値を使用)
|
|
366
|
+
*/
|
|
367
|
+
function determineCategory(params) {
|
|
368
|
+
// 明示的に指定されている場合はそれを優先
|
|
369
|
+
if (params.category) {
|
|
370
|
+
return params.category;
|
|
371
|
+
}
|
|
372
|
+
const { profile, boost_profile, artifacts } = params;
|
|
373
|
+
// artifacts.failing_testsがある場合はデバッグ作業
|
|
374
|
+
if (artifacts?.failing_tests && artifacts.failing_tests.length > 0) {
|
|
375
|
+
return "debug";
|
|
376
|
+
}
|
|
377
|
+
// profileからの推論
|
|
378
|
+
if (profile === "testfail") {
|
|
379
|
+
return "testfail";
|
|
380
|
+
}
|
|
381
|
+
if (profile === "bugfix" || profile === "typeerror") {
|
|
382
|
+
return "debug";
|
|
383
|
+
}
|
|
384
|
+
if (profile === "refactor") {
|
|
385
|
+
return "api";
|
|
386
|
+
}
|
|
387
|
+
if (profile === "feature") {
|
|
388
|
+
return "feature";
|
|
389
|
+
}
|
|
390
|
+
// boost_profileからの推論
|
|
391
|
+
if (boost_profile === "docs") {
|
|
392
|
+
return "docs";
|
|
393
|
+
}
|
|
394
|
+
// artifacts.editing_pathからの推論
|
|
395
|
+
if (artifacts?.editing_path) {
|
|
396
|
+
const editingPath = artifacts.editing_path.toLowerCase();
|
|
397
|
+
// テストファイルを編集中 → debug
|
|
398
|
+
if (editingPath.includes(".test.") ||
|
|
399
|
+
editingPath.includes(".spec.") ||
|
|
400
|
+
editingPath.includes("__tests__")) {
|
|
401
|
+
return "debug";
|
|
402
|
+
}
|
|
403
|
+
// ドキュメントを編集中 → docs
|
|
404
|
+
if (editingPath.endsWith(".md") || editingPath.includes("/docs/")) {
|
|
405
|
+
return "docs";
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// 推論できない場合はundefined(kDefaultを使用)
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
339
411
|
function isMissingTableError(error, table) {
|
|
340
412
|
if (!(error instanceof Error)) {
|
|
341
413
|
return false;
|
|
@@ -447,22 +519,34 @@ function createHintExpansionConfig(weights) {
|
|
|
447
519
|
const DEFAULT_SEARCH_LIMIT = 50;
|
|
448
520
|
const DEFAULT_BUNDLE_LIMIT = 7; // Reduced from 12 to optimize token usage
|
|
449
521
|
const MAX_BUNDLE_LIMIT = 20;
|
|
522
|
+
const TRACE_SEARCH = process.env.KIRI_TRACE_SEARCH === "1";
|
|
450
523
|
const MAX_KEYWORDS = 12;
|
|
451
524
|
const MAX_MATCHES_PER_KEYWORD = 40;
|
|
452
525
|
const MAX_DEPENDENCY_SEEDS = 8;
|
|
453
526
|
const MAX_DEPENDENCY_SEEDS_QUERY_LIMIT = 100; // SQL injection防御用の上限
|
|
454
527
|
const NEARBY_LIMIT = 6;
|
|
455
528
|
const serverConfig = loadServerConfig();
|
|
529
|
+
const mergedPathMultiplierCache = new Map();
|
|
456
530
|
const SUPPRESS_NON_CODE_ENABLED = serverConfig.features.suppressNonCode;
|
|
457
531
|
const FINAL_RESULT_SUPPRESSION_ENABLED = serverConfig.features.suppressFinalResults;
|
|
458
532
|
const CLAMP_SNIPPETS_ENABLED = serverConfig.features.clampSnippets;
|
|
459
533
|
const FALLBACK_SNIPPET_WINDOW = serverConfig.features.snippetWindow;
|
|
460
534
|
const MAX_RERANK_LIMIT = 50;
|
|
461
535
|
const MAX_ARTIFACT_HINTS = 8;
|
|
536
|
+
const DOMAIN_PATH_HINT_LIMIT = MAX_ARTIFACT_HINTS;
|
|
462
537
|
const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_.\-/]+$/;
|
|
463
538
|
const HINT_PRIORITY_TEXT_MULTIPLIER = serverConfig.hints.priority.textMultiplier;
|
|
464
539
|
const HINT_PRIORITY_PATH_MULTIPLIER = serverConfig.hints.priority.pathMultiplier;
|
|
465
540
|
const HINT_PRIORITY_BASE_BONUS = serverConfig.hints.priority.baseBonus;
|
|
541
|
+
const PATH_FALLBACK_LIMIT = 40;
|
|
542
|
+
const PATH_FALLBACK_TERMS_LIMIT = 5;
|
|
543
|
+
const PATH_FALLBACK_KEEP = 8;
|
|
544
|
+
const AUTO_PATH_SEGMENT_LIMIT = 4;
|
|
545
|
+
function traceSearch(message, ...args) {
|
|
546
|
+
if (TRACE_SEARCH) {
|
|
547
|
+
console.log(`[TRACE context_bundle] ${message}`, ...args);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
466
550
|
const HINT_DIR_LIMIT = serverConfig.hints.directory.limit;
|
|
467
551
|
const HINT_DIR_MAX_FILES = serverConfig.hints.directory.maxFiles;
|
|
468
552
|
const HINT_DEP_OUT_LIMIT = serverConfig.hints.dependency.outLimit;
|
|
@@ -811,8 +895,30 @@ function extractKeywords(text) {
|
|
|
811
895
|
}
|
|
812
896
|
}
|
|
813
897
|
}
|
|
898
|
+
addKeywordDerivedPathSegments(result);
|
|
814
899
|
return result;
|
|
815
900
|
}
|
|
901
|
+
function addKeywordDerivedPathSegments(result) {
|
|
902
|
+
if (result.pathSegments.length >= AUTO_PATH_SEGMENT_LIMIT) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const additional = [];
|
|
906
|
+
for (const keyword of result.keywords) {
|
|
907
|
+
if (keyword.length < 3 || STOP_WORDS.has(keyword)) {
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (result.pathSegments.includes(keyword) || additional.includes(keyword)) {
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
additional.push(keyword);
|
|
914
|
+
if (result.pathSegments.length + additional.length >= AUTO_PATH_SEGMENT_LIMIT) {
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (additional.length > 0) {
|
|
919
|
+
result.pathSegments.push(...additional);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
816
922
|
function ensureCandidate(map, filePath) {
|
|
817
923
|
let candidate = map.get(filePath);
|
|
818
924
|
if (!candidate) {
|
|
@@ -829,12 +935,35 @@ function ensureCandidate(map, filePath) {
|
|
|
829
935
|
embedding: null,
|
|
830
936
|
semanticSimilarity: null,
|
|
831
937
|
pathMatchHits: 0, // Issue #68: Track path match count
|
|
938
|
+
keywordHits: new Set(),
|
|
939
|
+
phraseHits: 0,
|
|
940
|
+
// pathFallbackReason は optional なので省略(exactOptionalPropertyTypes対応)
|
|
941
|
+
fallbackTextHits: 0,
|
|
832
942
|
penalties: [], // Issue #68: Penalty log for telemetry
|
|
833
943
|
};
|
|
834
944
|
map.set(filePath, candidate);
|
|
835
945
|
}
|
|
836
946
|
return candidate;
|
|
837
947
|
}
|
|
948
|
+
function normalizePathPrefix(rawPrefix) {
|
|
949
|
+
// Normalize, strip leading slashes/dots, and ensure trailing slash for exact prefix match
|
|
950
|
+
const normalized = path.posix.normalize(rawPrefix.replace(/\\/g, "/"));
|
|
951
|
+
const stripped = normalized.replace(/^\.\//, "").replace(/^\/+/, "");
|
|
952
|
+
if (stripped === "" || stripped === ".") {
|
|
953
|
+
return "";
|
|
954
|
+
}
|
|
955
|
+
return stripped.endsWith("/") ? stripped : `${stripped}/`;
|
|
956
|
+
}
|
|
957
|
+
function normalizeFilePath(filePath) {
|
|
958
|
+
return path.posix.normalize(filePath.replace(/\\/g, "/")).replace(/^\/+/, "");
|
|
959
|
+
}
|
|
960
|
+
function pathMatchesPrefix(filePath, normalizedPrefix) {
|
|
961
|
+
if (!normalizedPrefix) {
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
const normalizedPath = normalizeFilePath(filePath);
|
|
965
|
+
return normalizedPath.startsWith(normalizedPrefix);
|
|
966
|
+
}
|
|
838
967
|
async function expandHintCandidatesForHints(params) {
|
|
839
968
|
const { hintPaths, config } = params;
|
|
840
969
|
if (hintPaths.length === 0 || config.perHintLimit <= 0 || config.dbQueryBudget <= 0) {
|
|
@@ -1537,13 +1666,17 @@ async function safeLinkQuery(db, tableAvailability, sql, params) {
|
|
|
1537
1666
|
throw error;
|
|
1538
1667
|
}
|
|
1539
1668
|
}
|
|
1540
|
-
async function fetchMetadataOnlyCandidates(db, tableAvailability, repoId, filters, limit) {
|
|
1669
|
+
async function fetchMetadataOnlyCandidates(db, tableAvailability, repoId, filters, limit, pathPrefix) {
|
|
1541
1670
|
if (!tableAvailability.hasMetadataTables || filters.length === 0 || limit <= 0) {
|
|
1542
1671
|
return [];
|
|
1543
1672
|
}
|
|
1544
1673
|
const filterClauses = buildMetadataFilterConditions(filters);
|
|
1545
1674
|
const whereClauses = ["f.repo_id = ?"];
|
|
1546
1675
|
const params = [repoId];
|
|
1676
|
+
if (pathPrefix) {
|
|
1677
|
+
whereClauses.push("f.path LIKE ?");
|
|
1678
|
+
params.push(`${pathPrefix}%`);
|
|
1679
|
+
}
|
|
1547
1680
|
for (const clause of filterClauses) {
|
|
1548
1681
|
whereClauses.push(clause.sql);
|
|
1549
1682
|
params.push(...clause.params);
|
|
@@ -1568,13 +1701,17 @@ async function fetchMetadataOnlyCandidates(db, tableAvailability, repoId, filter
|
|
|
1568
1701
|
throw error;
|
|
1569
1702
|
}
|
|
1570
1703
|
}
|
|
1571
|
-
async function fetchMetadataKeywordMatches(db, tableAvailability, repoId, keywords, filters, limit, excludePaths) {
|
|
1704
|
+
async function fetchMetadataKeywordMatches(db, tableAvailability, repoId, keywords, filters, limit, excludePaths, pathPrefix) {
|
|
1572
1705
|
if (!tableAvailability.hasMetadataTables || keywords.length === 0 || limit <= 0) {
|
|
1573
1706
|
return [];
|
|
1574
1707
|
}
|
|
1575
1708
|
const keywordClauses = keywords.map(() => "mk.value ILIKE ?").join(" OR ");
|
|
1576
1709
|
const params = [repoId, ...keywords.map((kw) => `%${kw}%`)];
|
|
1577
1710
|
const whereClauses = ["mk.repo_id = ?", `(${keywordClauses})`];
|
|
1711
|
+
if (pathPrefix) {
|
|
1712
|
+
whereClauses.push("f.path LIKE ?");
|
|
1713
|
+
params.push(`${pathPrefix}%`);
|
|
1714
|
+
}
|
|
1578
1715
|
if (excludePaths.size > 0) {
|
|
1579
1716
|
const placeholders = Array.from(excludePaths)
|
|
1580
1717
|
.map(() => "?")
|
|
@@ -1774,6 +1911,50 @@ function applyFileTypeBoost(path, baseScore, profileConfig, weights) {
|
|
|
1774
1911
|
}
|
|
1775
1912
|
return baseScore * multiplier;
|
|
1776
1913
|
}
|
|
1914
|
+
function applyCoverageBoost(candidate, extractedTerms, weights) {
|
|
1915
|
+
// Skip for pure path-fallback candidates without text evidence
|
|
1916
|
+
if (candidate.reasons.has("fallback:path") &&
|
|
1917
|
+
candidate.keywordHits.size === 0 &&
|
|
1918
|
+
candidate.phraseHits === 0) {
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
// Coverage boost is only meaningful for text/phrase evidence; skip if no text evidence at all
|
|
1922
|
+
if (candidate.keywordHits.size === 0 && candidate.phraseHits === 0) {
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
if (extractedTerms.keywords.length > 0 && candidate.keywordHits.size > 0) {
|
|
1926
|
+
const coverage = candidate.keywordHits.size / extractedTerms.keywords.length;
|
|
1927
|
+
const bonus = coverage * weights.textMatch * 0.4;
|
|
1928
|
+
candidate.score += bonus;
|
|
1929
|
+
candidate.reasons.add(`coverage:keywords:${coverage.toFixed(2)}`);
|
|
1930
|
+
}
|
|
1931
|
+
if (extractedTerms.phrases.length > 0 && candidate.phraseHits > 0) {
|
|
1932
|
+
const phraseCoverage = Math.min(1, candidate.phraseHits / extractedTerms.phrases.length);
|
|
1933
|
+
const bonus = phraseCoverage * weights.textMatch * 0.6;
|
|
1934
|
+
candidate.score += bonus;
|
|
1935
|
+
candidate.reasons.add(`coverage:phrases:${phraseCoverage.toFixed(2)}`);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
async function fetchPathFallbackCandidates(db, repoId, terms, limit) {
|
|
1939
|
+
if (terms.length === 0 || limit <= 0) {
|
|
1940
|
+
return [];
|
|
1941
|
+
}
|
|
1942
|
+
const filters = terms.map(() => "f.path ILIKE ?").join(" OR ");
|
|
1943
|
+
const params = [repoId, ...terms.map((term) => `%${term}%`), limit];
|
|
1944
|
+
return await db.all(`
|
|
1945
|
+
SELECT f.path, f.lang, f.ext, f.is_binary, b.content, fe.vector_json, fe.dims AS vector_dims
|
|
1946
|
+
FROM file f
|
|
1947
|
+
JOIN blob b ON b.hash = f.blob_hash
|
|
1948
|
+
LEFT JOIN file_embedding fe
|
|
1949
|
+
ON fe.repo_id = f.repo_id
|
|
1950
|
+
AND fe.path = f.path
|
|
1951
|
+
WHERE f.repo_id = ?
|
|
1952
|
+
AND f.is_binary = FALSE
|
|
1953
|
+
AND (${filters})
|
|
1954
|
+
ORDER BY f.path
|
|
1955
|
+
LIMIT ?
|
|
1956
|
+
`, params);
|
|
1957
|
+
}
|
|
1777
1958
|
/**
|
|
1778
1959
|
* パスベースのスコアリングを適用(加算的ブースト)
|
|
1779
1960
|
* goalのキーワード/フレーズがファイルパスに含まれる場合にスコアを加算
|
|
@@ -1955,6 +2136,18 @@ function applyMultiplicativeFilePenalties(candidate, path, lowerPath, fileName,
|
|
|
1955
2136
|
function applyFileTypeMultipliers(candidate, path, ext, profileConfig, weights) {
|
|
1956
2137
|
const fileName = path.split("/").pop() ?? "";
|
|
1957
2138
|
const lowerPath = path.toLowerCase();
|
|
2139
|
+
// Very low value: schemas, fixtures, testdata, examples, baseline
|
|
2140
|
+
const schemaJson = lowerPath.endsWith(".schema.json") || lowerPath.includes("/schemas/");
|
|
2141
|
+
const isFixture = lowerPath.includes("/fixtures/") ||
|
|
2142
|
+
lowerPath.includes("/fixture/") ||
|
|
2143
|
+
lowerPath.includes("/testdata/");
|
|
2144
|
+
const isExample = lowerPath.includes("/examples/") || lowerPath.includes("/example/");
|
|
2145
|
+
const isBaseline = lowerPath.includes("baseline") || lowerPath.includes("golden");
|
|
2146
|
+
if (schemaJson || isFixture || isExample || isBaseline) {
|
|
2147
|
+
candidate.scoreMultiplier *= weights.configPenaltyMultiplier;
|
|
2148
|
+
candidate.reasons.add("penalty:low-value-file");
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
1958
2151
|
// ✅ Step 1: Low-value files (v1.0.0: syntax/perf/legal/migration)
|
|
1959
2152
|
// Apply configPenaltyMultiplier (strong penalty) to rarely useful file types
|
|
1960
2153
|
const isSyntaxGrammar = path.includes("/syntaxes/") &&
|
|
@@ -2072,6 +2265,7 @@ export async function filesSearch(context, params) {
|
|
|
2072
2265
|
cleanedQuery = cleanedQuery.trim();
|
|
2073
2266
|
hasTextQuery = cleanedQuery.length > 0;
|
|
2074
2267
|
}
|
|
2268
|
+
const pathPrefix = params.path_prefix && params.path_prefix.length > 0 ? params.path_prefix : undefined;
|
|
2075
2269
|
const metadataValueSeed = metadataFilters
|
|
2076
2270
|
.flatMap((filter) => filter.values)
|
|
2077
2271
|
.map((value) => value.trim())
|
|
@@ -2170,7 +2364,7 @@ export async function filesSearch(context, params) {
|
|
|
2170
2364
|
candidateRows.push(...textRows);
|
|
2171
2365
|
}
|
|
2172
2366
|
if (!hasTextQuery && hasAnyMetadataFilters) {
|
|
2173
|
-
const metadataOnlyRows = await fetchMetadataOnlyCandidates(db, context.tableAvailability, repoId, metadataFilters, limit * 2);
|
|
2367
|
+
const metadataOnlyRows = await fetchMetadataOnlyCandidates(db, context.tableAvailability, repoId, metadataFilters, limit * 2, pathPrefix);
|
|
2174
2368
|
for (const row of metadataOnlyRows) {
|
|
2175
2369
|
row.score = 1 + metadataFilters.length * 0.2;
|
|
2176
2370
|
}
|
|
@@ -2180,7 +2374,7 @@ export async function filesSearch(context, params) {
|
|
|
2180
2374
|
const metadataKeywords = splitQueryWords(cleanedQuery.toLowerCase()).map((kw) => kw.toLowerCase());
|
|
2181
2375
|
if (metadataKeywords.length > 0) {
|
|
2182
2376
|
const excludePaths = new Set(candidateRows.map((row) => row.path));
|
|
2183
|
-
const metadataRows = await fetchMetadataKeywordMatches(db, context.tableAvailability, repoId, metadataKeywords, metadataFilters, limit * 2, excludePaths);
|
|
2377
|
+
const metadataRows = await fetchMetadataKeywordMatches(db, context.tableAvailability, repoId, metadataKeywords, metadataFilters, limit * 2, excludePaths, pathPrefix);
|
|
2184
2378
|
candidateRows.push(...metadataRows);
|
|
2185
2379
|
}
|
|
2186
2380
|
}
|
|
@@ -2207,7 +2401,17 @@ export async function filesSearch(context, params) {
|
|
|
2207
2401
|
const filterValueSet = new Set(metadataFilters.flatMap((filter) => filter.values.map((value) => value.toLowerCase())));
|
|
2208
2402
|
const boostProfile = params.boost_profile ??
|
|
2209
2403
|
(hasHintMetadataFilters ? "balanced" : hasStrictMetadataFilters ? "docs" : "default");
|
|
2210
|
-
const
|
|
2404
|
+
const baseProfileConfig = getBoostProfile(boostProfile);
|
|
2405
|
+
const cachedMerged = mergedPathMultiplierCache.get(boostProfile);
|
|
2406
|
+
const mergedPathMultipliers = cachedMerged ??
|
|
2407
|
+
mergePathPenaltyEntries(baseProfileConfig.pathMultipliers, [], serverConfig.pathPenalties);
|
|
2408
|
+
if (!cachedMerged) {
|
|
2409
|
+
mergedPathMultiplierCache.set(boostProfile, mergedPathMultipliers);
|
|
2410
|
+
}
|
|
2411
|
+
const profileConfig = {
|
|
2412
|
+
...baseProfileConfig,
|
|
2413
|
+
pathMultipliers: mergedPathMultipliers,
|
|
2414
|
+
};
|
|
2211
2415
|
const weights = loadScoringProfile(null);
|
|
2212
2416
|
const options = parseOutputOptions(params);
|
|
2213
2417
|
const previewQuery = hasTextQuery
|
|
@@ -2543,7 +2747,22 @@ async function contextBundleImpl(context, params) {
|
|
|
2543
2747
|
mergedFilters: metadataFilters,
|
|
2544
2748
|
}));
|
|
2545
2749
|
}
|
|
2546
|
-
|
|
2750
|
+
// AdaptiveK: カテゴリに基づいてK値を動的に調整
|
|
2751
|
+
// 1. params.categoryが明示的に指定されていればそれを使用
|
|
2752
|
+
// 2. そうでなければprofile/artifacts/boost_profileから自動検出
|
|
2753
|
+
const detectedCategory = determineCategory(params);
|
|
2754
|
+
const adaptiveK = getAdaptiveK(detectedCategory, serverConfig.adaptiveK);
|
|
2755
|
+
const adaptiveLimit = normalizeBundleLimit(adaptiveK);
|
|
2756
|
+
const requestedLimit = normalizeBundleLimit(params.limit);
|
|
2757
|
+
// AdaptiveKが無効な場合はrequestLimitをそのまま使用
|
|
2758
|
+
// AdaptiveKが有効な場合:
|
|
2759
|
+
// - ユーザー指定limitがなければadaptiveKを使用
|
|
2760
|
+
// - ユーザー指定がある場合は小さい方を採用(過剰取得防止)
|
|
2761
|
+
const limit = serverConfig.adaptiveK.enabled
|
|
2762
|
+
? params.limit === undefined
|
|
2763
|
+
? adaptiveLimit
|
|
2764
|
+
: Math.min(requestedLimit, adaptiveLimit)
|
|
2765
|
+
: requestedLimit;
|
|
2547
2766
|
const artifacts = params.artifacts ?? {};
|
|
2548
2767
|
const artifactHints = normalizeArtifactHints(artifacts.hints);
|
|
2549
2768
|
const hintBuckets = bucketArtifactHints(artifactHints);
|
|
@@ -2551,6 +2770,9 @@ async function contextBundleImpl(context, params) {
|
|
|
2551
2770
|
const substringHints = hintBuckets.substringHints;
|
|
2552
2771
|
const includeTokensEstimate = params.includeTokensEstimate === true;
|
|
2553
2772
|
const isCompact = params.compact === true;
|
|
2773
|
+
const pathPrefix = params.path_prefix && params.path_prefix.length > 0
|
|
2774
|
+
? normalizePathPrefix(params.path_prefix)
|
|
2775
|
+
: undefined;
|
|
2554
2776
|
// 項目2: トークンバジェット保護警告
|
|
2555
2777
|
// 大量データ+非コンパクトモード+トークン推定なしの場合に警告
|
|
2556
2778
|
// リクエストごとに警告(warnForRequestを使用)
|
|
@@ -2580,9 +2802,18 @@ async function contextBundleImpl(context, params) {
|
|
|
2580
2802
|
.join(" ");
|
|
2581
2803
|
keywordSources.push(filterSeed);
|
|
2582
2804
|
}
|
|
2805
|
+
const baseSeed = keywordSources.join(" ");
|
|
2806
|
+
const domainExpansion = process.env.KIRI_ENABLE_DOMAIN_TERMS === "1"
|
|
2807
|
+
? context.services.domainTerms.expandFromText(baseSeed)
|
|
2808
|
+
: { matched: [], aliases: [], fileHints: [] };
|
|
2809
|
+
if (domainExpansion.aliases.length > 0) {
|
|
2810
|
+
keywordSources.push(domainExpansion.aliases.join(" "));
|
|
2811
|
+
}
|
|
2583
2812
|
const semanticSeed = keywordSources.join(" ");
|
|
2584
2813
|
const queryEmbedding = generateEmbedding(semanticSeed)?.values ?? null;
|
|
2585
2814
|
const extractedTerms = extractKeywords(semanticSeed);
|
|
2815
|
+
const segmentPreview = extractedTerms.pathSegments.slice(0, AUTO_PATH_SEGMENT_LIMIT).join(",");
|
|
2816
|
+
traceSearch(`terms repo=${repoId} id=${params.requestId ?? "n/a"} keywords=${extractedTerms.keywords.length} phrases=${extractedTerms.phrases.length} pathSegments=${extractedTerms.pathSegments.length} segs=[${segmentPreview}]`);
|
|
2586
2817
|
// フォールバック: editing_pathからキーワードを抽出
|
|
2587
2818
|
if (extractedTerms.phrases.length === 0 &&
|
|
2588
2819
|
extractedTerms.keywords.length === 0 &&
|
|
@@ -2599,14 +2830,24 @@ async function contextBundleImpl(context, params) {
|
|
|
2599
2830
|
// ✅ Cache boost profile config to avoid redundant lookups in hot path
|
|
2600
2831
|
const boostProfile = params.boost_profile ??
|
|
2601
2832
|
(hasHintMetadataFilters ? "balanced" : hasStrictMetadataFilters ? "docs" : "default");
|
|
2602
|
-
const
|
|
2833
|
+
const baseProfileConfig = getBoostProfile(boostProfile);
|
|
2834
|
+
const profileConfig = {
|
|
2835
|
+
...baseProfileConfig,
|
|
2836
|
+
pathMultipliers: loadPathPenalties(baseProfileConfig.pathMultipliers),
|
|
2837
|
+
};
|
|
2603
2838
|
// フレーズマッチング(高い重み: textMatch × 2)- 統合クエリでパフォーマンス改善
|
|
2604
2839
|
if (extractedTerms.phrases.length > 0) {
|
|
2605
2840
|
const phrasePlaceholders = extractedTerms.phrases
|
|
2606
2841
|
.map(() => "b.content ILIKE '%' || ? || '%'")
|
|
2607
2842
|
.join(" OR ");
|
|
2608
2843
|
// DEBUG: Log SQL query parameters for troubleshooting
|
|
2609
|
-
|
|
2844
|
+
traceSearch(`Executing phrase match query with repo_id=${repoId}, phrases=${JSON.stringify(extractedTerms.phrases)}`);
|
|
2845
|
+
const phraseWhereClauses = ["f.repo_id = ?", "f.is_binary = FALSE", `(${phrasePlaceholders})`];
|
|
2846
|
+
const phraseParams = [repoId, ...extractedTerms.phrases];
|
|
2847
|
+
if (pathPrefix) {
|
|
2848
|
+
phraseWhereClauses.push("f.path LIKE ?");
|
|
2849
|
+
phraseParams.push(`${pathPrefix}%`);
|
|
2850
|
+
}
|
|
2610
2851
|
const rows = await db.all(`
|
|
2611
2852
|
SELECT f.path, f.lang, f.ext, f.is_binary, b.content, fe.vector_json, fe.dims AS vector_dims
|
|
2612
2853
|
FROM file f
|
|
@@ -2614,19 +2855,21 @@ async function contextBundleImpl(context, params) {
|
|
|
2614
2855
|
LEFT JOIN file_embedding fe
|
|
2615
2856
|
ON fe.repo_id = f.repo_id
|
|
2616
2857
|
AND fe.path = f.path
|
|
2617
|
-
WHERE
|
|
2618
|
-
AND
|
|
2619
|
-
AND (${phrasePlaceholders})
|
|
2858
|
+
WHERE ${phraseWhereClauses.join(`
|
|
2859
|
+
AND `)}
|
|
2620
2860
|
ORDER BY f.path
|
|
2621
2861
|
LIMIT ?
|
|
2622
|
-
`, [
|
|
2862
|
+
`, [...phraseParams, MAX_MATCHES_PER_KEYWORD * extractedTerms.phrases.length]);
|
|
2623
2863
|
// DEBUG: Log returned paths and verify they match expected repo_id
|
|
2624
2864
|
if (rows.length > 0) {
|
|
2625
|
-
|
|
2865
|
+
traceSearch(`Phrase match returned ${rows.length} rows. Sample paths: ${rows
|
|
2866
|
+
.slice(0, 3)
|
|
2867
|
+
.map((r) => r.path)
|
|
2868
|
+
.join(", ")}`);
|
|
2626
2869
|
// Verify repo_id of returned files
|
|
2627
2870
|
const pathsToCheck = rows.slice(0, 3).map((r) => r.path);
|
|
2628
2871
|
const verification = await db.all(`SELECT path, repo_id FROM file WHERE path IN (${pathsToCheck.map(() => "?").join(", ")}) LIMIT 3`, pathsToCheck);
|
|
2629
|
-
|
|
2872
|
+
traceSearch(`Repo ID verification`, verification);
|
|
2630
2873
|
}
|
|
2631
2874
|
for (const row of rows) {
|
|
2632
2875
|
if (row.content === null) {
|
|
@@ -2639,6 +2882,7 @@ async function contextBundleImpl(context, params) {
|
|
|
2639
2882
|
continue; // Should not happen, but defensive check
|
|
2640
2883
|
}
|
|
2641
2884
|
const candidate = ensureCandidate(candidates, row.path);
|
|
2885
|
+
candidate.phraseHits += matchedPhrases.length;
|
|
2642
2886
|
// 各マッチしたフレーズに対してスコアリング
|
|
2643
2887
|
for (const phrase of matchedPhrases) {
|
|
2644
2888
|
// フレーズマッチは通常の2倍のスコア
|
|
@@ -2669,12 +2913,23 @@ async function contextBundleImpl(context, params) {
|
|
|
2669
2913
|
});
|
|
2670
2914
|
}
|
|
2671
2915
|
}
|
|
2916
|
+
traceSearch(`phrase search produced ${rows.length} rows, candidates=${candidates.size}`);
|
|
2672
2917
|
}
|
|
2673
2918
|
// キーワードマッチング(通常の重み)- 統合クエリでパフォーマンス改善
|
|
2674
2919
|
if (extractedTerms.keywords.length > 0) {
|
|
2675
2920
|
const keywordPlaceholders = extractedTerms.keywords
|
|
2676
2921
|
.map(() => "b.content ILIKE '%' || ? || '%'")
|
|
2677
2922
|
.join(" OR ");
|
|
2923
|
+
const keywordWhereClauses = [
|
|
2924
|
+
"f.repo_id = ?",
|
|
2925
|
+
"f.is_binary = FALSE",
|
|
2926
|
+
`(${keywordPlaceholders})`,
|
|
2927
|
+
];
|
|
2928
|
+
const keywordParams = [repoId, ...extractedTerms.keywords];
|
|
2929
|
+
if (pathPrefix) {
|
|
2930
|
+
keywordWhereClauses.push("f.path LIKE ?");
|
|
2931
|
+
keywordParams.push(`${pathPrefix}%`);
|
|
2932
|
+
}
|
|
2678
2933
|
const rows = await db.all(`
|
|
2679
2934
|
SELECT f.path, f.lang, f.ext, f.is_binary, b.content, fe.vector_json, fe.dims AS vector_dims
|
|
2680
2935
|
FROM file f
|
|
@@ -2682,12 +2937,11 @@ async function contextBundleImpl(context, params) {
|
|
|
2682
2937
|
LEFT JOIN file_embedding fe
|
|
2683
2938
|
ON fe.repo_id = f.repo_id
|
|
2684
2939
|
AND fe.path = f.path
|
|
2685
|
-
WHERE
|
|
2686
|
-
AND
|
|
2687
|
-
AND (${keywordPlaceholders})
|
|
2940
|
+
WHERE ${keywordWhereClauses.join(`
|
|
2941
|
+
AND `)}
|
|
2688
2942
|
ORDER BY f.path
|
|
2689
2943
|
LIMIT ?
|
|
2690
|
-
`, [
|
|
2944
|
+
`, [...keywordParams, MAX_MATCHES_PER_KEYWORD * extractedTerms.keywords.length]);
|
|
2691
2945
|
for (const row of rows) {
|
|
2692
2946
|
if (row.content === null) {
|
|
2693
2947
|
continue;
|
|
@@ -2703,6 +2957,7 @@ async function contextBundleImpl(context, params) {
|
|
|
2703
2957
|
for (const keyword of matchedKeywords) {
|
|
2704
2958
|
candidate.score += weights.textMatch;
|
|
2705
2959
|
candidate.reasons.add(`text:${keyword}`);
|
|
2960
|
+
candidate.keywordHits.add(keyword);
|
|
2706
2961
|
}
|
|
2707
2962
|
// Apply boost profile once per file
|
|
2708
2963
|
if (boostProfile !== "none") {
|
|
@@ -2728,15 +2983,114 @@ async function contextBundleImpl(context, params) {
|
|
|
2728
2983
|
});
|
|
2729
2984
|
}
|
|
2730
2985
|
}
|
|
2986
|
+
traceSearch(`keyword search produced ${rows.length} rows, candidates=${candidates.size}`);
|
|
2987
|
+
}
|
|
2988
|
+
const fallbackTerms = Array.from(new Set([...extractedTerms.phrases, ...extractedTerms.keywords, ...extractedTerms.pathSegments]
|
|
2989
|
+
.map((term) => term.toLowerCase())
|
|
2990
|
+
.filter((term) => term.length >= 3))).slice(0, PATH_FALLBACK_TERMS_LIMIT);
|
|
2991
|
+
if (fallbackTerms.length > 0) {
|
|
2992
|
+
const fallbackRows = await fetchPathFallbackCandidates(db, repoId, fallbackTerms, Math.min(limit * 2, PATH_FALLBACK_LIMIT));
|
|
2993
|
+
const fallbackReason = stringMatchSeeds.size === 0
|
|
2994
|
+
? "no-string-match"
|
|
2995
|
+
: candidates.size < limit
|
|
2996
|
+
? "low-candidates"
|
|
2997
|
+
: "supplemental";
|
|
2998
|
+
traceSearch(`path fallback triggered (${fallbackReason}) terms=${JSON.stringify(fallbackTerms)} rows=${fallbackRows.length}`);
|
|
2999
|
+
const fallbackWeight = stringMatchSeeds.size === 0 ? weights.pathMatch * 0.75 : weights.pathMatch * 0.2;
|
|
3000
|
+
for (const row of fallbackRows) {
|
|
3001
|
+
const candidate = ensureCandidate(candidates, row.path);
|
|
3002
|
+
candidate.pathFallbackReason = fallbackReason;
|
|
3003
|
+
candidate.score += fallbackWeight;
|
|
3004
|
+
candidate.reasons.add("fallback:path");
|
|
3005
|
+
const contentLower = row.content?.toLowerCase() ?? "";
|
|
3006
|
+
if (contentLower.length > 0) {
|
|
3007
|
+
let textHits = 0;
|
|
3008
|
+
for (const term of fallbackTerms) {
|
|
3009
|
+
if (contentLower.includes(term)) {
|
|
3010
|
+
textHits += 1;
|
|
3011
|
+
candidate.keywordHits.add(term);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
candidate.fallbackTextHits += textHits;
|
|
3015
|
+
if (textHits > 0) {
|
|
3016
|
+
const textBoost = textHits * weights.textMatch * 0.15;
|
|
3017
|
+
candidate.score += textBoost;
|
|
3018
|
+
candidate.reasons.add(`fallback:content:${textHits}`);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
candidate.matchLine ??= 1;
|
|
3022
|
+
candidate.lang ??= row.lang;
|
|
3023
|
+
candidate.ext ??= row.ext;
|
|
3024
|
+
candidate.totalLines ??= row.content?.split(/\r?\n/).length ?? null;
|
|
3025
|
+
candidate.content ??= row.content;
|
|
3026
|
+
candidate.embedding ??= parseEmbedding(row.vector_json ?? null, row.vector_dims ?? null);
|
|
3027
|
+
if (boostProfile !== "none") {
|
|
3028
|
+
applyBoostProfile(candidate, row, profileConfig, weights, extractedTerms);
|
|
3029
|
+
}
|
|
3030
|
+
stringMatchSeeds.add(row.path);
|
|
3031
|
+
if (!fileCache.has(row.path) && row.content) {
|
|
3032
|
+
fileCache.set(row.path, {
|
|
3033
|
+
content: row.content,
|
|
3034
|
+
lang: row.lang,
|
|
3035
|
+
ext: row.ext,
|
|
3036
|
+
totalLines: candidate.totalLines ?? 0,
|
|
3037
|
+
embedding: candidate.embedding,
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
// Drop fallback-only candidates with zero text evidence before trimming
|
|
3042
|
+
for (const [path, candidate] of Array.from(candidates.entries())) {
|
|
3043
|
+
const isFallbackOnly = candidate.reasons.has("fallback:path") &&
|
|
3044
|
+
candidate.keywordHits.size === 0 &&
|
|
3045
|
+
candidate.phraseHits === 0;
|
|
3046
|
+
const hasTextEvidence = candidate.fallbackTextHits > 0;
|
|
3047
|
+
if (isFallbackOnly && !hasTextEvidence) {
|
|
3048
|
+
candidates.delete(path);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
// Demote fallback-only hits without text evidence
|
|
3052
|
+
for (const candidate of candidates.values()) {
|
|
3053
|
+
const isFallbackOnly = candidate.reasons.has("fallback:path") &&
|
|
3054
|
+
candidate.keywordHits.size === 0 &&
|
|
3055
|
+
candidate.phraseHits === 0;
|
|
3056
|
+
const hasTextEvidence = candidate.fallbackTextHits > 0;
|
|
3057
|
+
if (isFallbackOnly && !hasTextEvidence) {
|
|
3058
|
+
candidate.scoreMultiplier *= 0.5;
|
|
3059
|
+
candidate.reasons.add("penalty:fallback-no-text");
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
if (fallbackRows.length > PATH_FALLBACK_KEEP) {
|
|
3063
|
+
const fallbackOnly = Array.from(candidates.entries())
|
|
3064
|
+
.filter(([_, candidate]) => candidate.reasons.has("fallback:path") &&
|
|
3065
|
+
candidate.keywordHits.size === 0 &&
|
|
3066
|
+
candidate.phraseHits === 0)
|
|
3067
|
+
.sort((a, b) => b[1].score - a[1].score);
|
|
3068
|
+
const toDrop = fallbackOnly.slice(PATH_FALLBACK_KEEP);
|
|
3069
|
+
for (const [path] of toDrop) {
|
|
3070
|
+
candidates.delete(path);
|
|
3071
|
+
}
|
|
3072
|
+
traceSearch(`path fallback trimmed kept=${PATH_FALLBACK_KEEP} dropped=${toDrop.length} candidates=${candidates.size}`);
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
if (extractedTerms.keywords.length > 0 || extractedTerms.phrases.length > 0) {
|
|
3076
|
+
for (const candidate of candidates.values()) {
|
|
3077
|
+
applyCoverageBoost(candidate, extractedTerms, weights);
|
|
3078
|
+
}
|
|
2731
3079
|
}
|
|
2732
3080
|
const artifactPathTargets = artifactPathHints.map((hintPath) => ({
|
|
2733
3081
|
path: hintPath,
|
|
2734
3082
|
sourceHint: hintPath,
|
|
2735
3083
|
origin: "artifact",
|
|
2736
3084
|
}));
|
|
3085
|
+
const domainPathTargets = selectDomainPathHints(domainExpansion.fileHints).map((hint) => ({
|
|
3086
|
+
path: hint.path,
|
|
3087
|
+
sourceHint: hint.source,
|
|
3088
|
+
origin: "dictionary",
|
|
3089
|
+
}));
|
|
2737
3090
|
const dictionaryPathTargets = await fetchDictionaryPathHints(db, context.tableAvailability, repoId, substringHints, HINT_DICTIONARY_LIMIT);
|
|
2738
3091
|
const { list: resolvedPathHintTargets, meta: hintSeedMeta } = createHintSeedMeta([
|
|
2739
3092
|
...artifactPathTargets,
|
|
3093
|
+
...domainPathTargets,
|
|
2740
3094
|
...dictionaryPathTargets,
|
|
2741
3095
|
]);
|
|
2742
3096
|
if (resolvedPathHintTargets.length > 0) {
|
|
@@ -2831,6 +3185,9 @@ async function contextBundleImpl(context, params) {
|
|
|
2831
3185
|
const materializeCandidates = async () => {
|
|
2832
3186
|
const result = [];
|
|
2833
3187
|
for (const candidate of candidates.values()) {
|
|
3188
|
+
if (!pathMatchesPrefix(candidate.path, pathPrefix)) {
|
|
3189
|
+
continue;
|
|
3190
|
+
}
|
|
2834
3191
|
if (isSuppressedPath(candidate.path)) {
|
|
2835
3192
|
continue;
|
|
2836
3193
|
}
|
|
@@ -2864,7 +3221,7 @@ async function contextBundleImpl(context, params) {
|
|
|
2864
3221
|
if (!hasAnyMetadataFilters) {
|
|
2865
3222
|
return;
|
|
2866
3223
|
}
|
|
2867
|
-
const metadataRows = await fetchMetadataOnlyCandidates(db, context.tableAvailability, repoId, metadataFilters, limit * 2);
|
|
3224
|
+
const metadataRows = await fetchMetadataOnlyCandidates(db, context.tableAvailability, repoId, metadataFilters, limit * 2, pathPrefix);
|
|
2868
3225
|
if (metadataRows.length === 0) {
|
|
2869
3226
|
return;
|
|
2870
3227
|
}
|
|
@@ -2891,9 +3248,11 @@ async function contextBundleImpl(context, params) {
|
|
|
2891
3248
|
await addMetadataFallbackCandidates();
|
|
2892
3249
|
}
|
|
2893
3250
|
let materializedCandidates = await materializeCandidates();
|
|
3251
|
+
traceSearch(`materialized candidates: ${materializedCandidates.length}`);
|
|
2894
3252
|
if (materializedCandidates.length === 0 && hasAnyMetadataFilters) {
|
|
2895
3253
|
await addMetadataFallbackCandidates();
|
|
2896
3254
|
materializedCandidates = await materializeCandidates();
|
|
3255
|
+
traceSearch(`materialized candidates after metadata fallback: ${materializedCandidates.length}`);
|
|
2897
3256
|
}
|
|
2898
3257
|
if (materializedCandidates.length === 0) {
|
|
2899
3258
|
// Get warnings from WarningManager (includes breaking change notification if applicable)
|
|
@@ -3021,6 +3380,14 @@ async function contextBundleImpl(context, params) {
|
|
|
3021
3380
|
}
|
|
3022
3381
|
return b.score - a.score;
|
|
3023
3382
|
});
|
|
3383
|
+
if (TRACE_SEARCH) {
|
|
3384
|
+
const sample = rankedCandidates.slice(0, 5).map((candidate) => ({
|
|
3385
|
+
path: candidate.path,
|
|
3386
|
+
score: Number(candidate.score.toFixed(3)),
|
|
3387
|
+
reasons: Array.from(candidate.reasons).slice(0, 3),
|
|
3388
|
+
}));
|
|
3389
|
+
traceSearch(`ranked candidates=${rankedCandidates.length}`, sample);
|
|
3390
|
+
}
|
|
3024
3391
|
const prioritizedCandidates = prioritizeHintCandidates(rankedCandidates, resolvedPathHintTargets.map((target) => target.path), limit);
|
|
3025
3392
|
if (prioritizedCandidates.length === 0) {
|
|
3026
3393
|
const warnings = [...context.warningManager.responseWarnings];
|