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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "4.49.4",
3
+ "version": "4.49.5",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -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 hits = await query(db, makeGovernedEmbedder(config), text, { topK, scope, project, mode, alpha, where, rerankOpts });
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 hits = await query(db, makeGovernedEmbedder(config), text, { topK, scope, project });
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
+ }