karajan-code 4.49.4 → 4.49.5
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/package.json
CHANGED
package/src/commands/rag.js
CHANGED
|
@@ -10,6 +10,7 @@ import { indexFile, indexProject, indexProjectDelta, sourceKind } from "../rag/i
|
|
|
10
10
|
import { indexableReason, ragExclude } from "../rag/indexable.js";
|
|
11
11
|
import { fileIndexState } from "../rag/coverage.js";
|
|
12
12
|
import { query } from "../rag/retriever.js";
|
|
13
|
+
import { pathNamedHits, withPathNamedHits } from "../rag/path-hits.js";
|
|
13
14
|
import { installPostMergeHook, maybeAutoUpdate } from "../rag/auto-update.js";
|
|
14
15
|
import { indexLibrary, LIBRARY_PROJECT } from "../rag/library.js";
|
|
15
16
|
import { loadGoldenQueries, runEval } from "../rag/eval.js";
|
|
@@ -166,7 +167,8 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
166
167
|
let where = flags.where || null;
|
|
167
168
|
if (library) where = flags.where ? `kind=library AND ${flags.where}` : "kind=library";
|
|
168
169
|
const rerankOpts = flags.rerank ? { model: flags.rerankModel } : null;
|
|
169
|
-
const
|
|
170
|
+
const ranked = await query(db, makeGovernedEmbedder(config), text, { topK, scope, project, mode, alpha, where, rerankOpts });
|
|
171
|
+
const hits = library ? ranked : withPathNamedHits(pathNamedHits(db, text, { project, projectDir: config?.projectDir || process.cwd() }), ranked, topK);
|
|
170
172
|
if (flags.json) {
|
|
171
173
|
process.stdout.write(`${JSON.stringify(hits)}\n`);
|
|
172
174
|
} else {
|
|
@@ -9,6 +9,7 @@ import { makeGovernedEmbedder } from "../../rag/governed-embedder.js";
|
|
|
9
9
|
import { indexProject } from "../../rag/indexer.js";
|
|
10
10
|
import { ragExclude } from "../../rag/indexable.js";
|
|
11
11
|
import { query } from "../../rag/retriever.js";
|
|
12
|
+
import { pathNamedHits, withPathNamedHits } from "../../rag/path-hits.js";
|
|
12
13
|
import { getKarajanHome } from "../../utils/paths.js";
|
|
13
14
|
import { resolveProjectDir, buildConfig, responseText, failPayload } from "../shared-helpers.js";
|
|
14
15
|
|
|
@@ -40,7 +41,9 @@ export async function handleRagQuery(args, server) {
|
|
|
40
41
|
const remedy = emptyIndexRemedy({ slug: project, legacyPath: join(getKarajanHome(), "rag.db"), dim });
|
|
41
42
|
return responseText({ hits: [], empty: true, topK, scope, project, remedy });
|
|
42
43
|
}
|
|
43
|
-
const
|
|
44
|
+
const ranked = await query(db, makeGovernedEmbedder(config), text, { topK, scope, project });
|
|
45
|
+
// KJC-BUG-0348: a file the question names comes first, as in the CLI.
|
|
46
|
+
const hits = withPathNamedHits(pathNamedHits(db, text, { project, projectDir }), ranked, topK);
|
|
44
47
|
return responseText({ hits, empty: false, topK, scope, project });
|
|
45
48
|
} finally { db.close(); }
|
|
46
49
|
} catch (err) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-BUG-0348 (#2115): ranking alone decides what a query returns, and in a
|
|
3
|
+
* large index a freshly indexed file (an .astro page) never made the top hits,
|
|
4
|
+
* neither by its path nor by its text: the rag-first gate kept blocking it.
|
|
5
|
+
* A query that NAMES an indexed file returns that file first, deterministically.
|
|
6
|
+
*/
|
|
7
|
+
import { isAbsolute, relative } from "node:path";
|
|
8
|
+
|
|
9
|
+
// A path-like token: segments joined by "/" ending in a file extension.
|
|
10
|
+
const PATH_TOKEN = /[\w@.-]+(?:\/[\w@.-]+)*\.[A-Za-z0-9]+/g;
|
|
11
|
+
|
|
12
|
+
const parseMetadata = (raw) => {
|
|
13
|
+
try { return raw ? JSON.parse(raw) : {}; } catch { return {}; }
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** @returns {object[]} the first chunk of each indexed file of `project` the text names */
|
|
17
|
+
export function pathNamedHits(db, text, { project, projectDir }) {
|
|
18
|
+
const tokens = [...new Set(String(text || "").match(PATH_TOKEN) ?? [])];
|
|
19
|
+
if (!project || tokens.length === 0) return [];
|
|
20
|
+
const sources = db.prepare("SELECT DISTINCT source FROM chunks WHERE project_slug = ?").all(project).map((r) => r.source);
|
|
21
|
+
const named = sources.filter((source) => {
|
|
22
|
+
const rel = (isAbsolute(source) ? relative(projectDir, source) : source).split("\\").join("/");
|
|
23
|
+
return tokens.some((t) => rel === t || rel.endsWith(`/${t}`));
|
|
24
|
+
});
|
|
25
|
+
const first = db.prepare("SELECT id, source, kind, text, metadata, project_slug FROM chunks WHERE project_slug = ? AND source = ? ORDER BY id LIMIT 1");
|
|
26
|
+
return named.map((source) => {
|
|
27
|
+
const row = first.get(project, source);
|
|
28
|
+
return { ...row, metadata: parseMetadata(row.metadata), distance: 0, score: 0, matchedBy: "path" };
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The named files first, then the ranked hits, never the same chunk twice, at most topK. */
|
|
33
|
+
export function withPathNamedHits(named, ranked, topK) {
|
|
34
|
+
const ids = new Set(named.map((h) => h.id));
|
|
35
|
+
return [...named, ...ranked.filter((h) => !ids.has(h.id))].slice(0, topK);
|
|
36
|
+
}
|