karajan-code 4.49.3 → 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.3",
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",
@@ -76,6 +76,7 @@
76
76
  "postinstall": "node scripts/postinstall.js",
77
77
  "setup": "node scripts/install.js",
78
78
  "test": "vitest run",
79
+ "test:scenarios": "vitest run --config tests/scenarios/vitest.config.js --root .",
79
80
  "test:watch": "vitest",
80
81
  "test:coverage": "vitest run --coverage",
81
82
  "lint": "eslint src/ packages/",
@@ -259,7 +259,12 @@ export function registerPipeline(program, { pkgVersion }) {
259
259
  // ENV-B1 (KJC-TSK-0637): the gate mode records a verdict tied to
260
260
  // the exact diff so the pre-commit hook can enforce cross-AI review.
261
261
  if (flags.staged || flags.check || flags.range || flags.installGate || flags.prune) {
262
- await reviewGateCommand({ config, logger, flags: { ...flags, task } });
262
+ // KJC-BUG-0349: the gate's task is optional, but --task-file carries
263
+ // the evidence the reviewer must weigh; it was never read here.
264
+ const gateTask = task || flags.taskFile
265
+ ? await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger })
266
+ : task;
267
+ await reviewGateCommand({ config, logger, flags: { ...flags, task: gateTask } });
263
268
  return;
264
269
  }
265
270
  const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
@@ -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
+ }
@@ -8,7 +8,7 @@ import { waitForAnalysis } from "./api.js";
8
8
  import { resolveSonarProjectKey } from "./project-key.js";
9
9
  import {
10
10
  resolveSonarHost,
11
- resolveSonarToken as resolveToken,
11
+ resolveSonarTokenAsync,
12
12
  resolveSonarCredentials,
13
13
  } from "./config-resolver.js";
14
14
 
@@ -210,8 +210,9 @@ async function maybeRunCoverage(config) {
210
210
  return checkLcovExists(lcovPath, blockOnFailure, run.stdout || "");
211
211
  }
212
212
 
213
- async function resolveSonarTokenWithFallback(config, apiHost) {
214
- const explicitToken = resolveToken(config);
213
+ export async function resolveSonarTokenWithFallback(config, apiHost) {
214
+ // KJC-BUG-0350: the async resolver also reads the token the bootstrap persisted.
215
+ const explicitToken = await resolveSonarTokenAsync(config);
215
216
  if (explicitToken) return explicitToken;
216
217
 
217
218
  const { user: adminUser, passwords } = await resolveSonarCredentials(config);