karajan-code 4.35.0 → 4.36.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/package.json +1 -1
- package/packages/hu-board/public/utils/board-view.js +2 -1
- package/packages/hu-board/public/utils/config-editor.js +5 -2
- package/packages/hu-board/public/utils/terminal-panel.js +2 -1
- package/packages/hu-board/src/config-yaml.js +11 -9
- package/packages/hu-board/src/project-dir.js +42 -0
- package/packages/hu-board/src/routes/api.js +11 -3
- package/packages/hu-board/src/terminal-wire.js +5 -1
- package/packages/hu-board/src/terminal.js +8 -3
- package/src/audit/harness-scorecard.js +8 -1
- package/src/audit/harness-section.js +13 -2
- package/src/checks/rag-coverage.js +9 -5
- package/src/checks/runner.js +27 -5
- package/src/checks/skills.js +10 -0
- package/src/checks/types.js +5 -1
- package/src/cli/register-meta.js +15 -4
- package/src/commands/board.js +20 -8
- package/src/commands/bootstrap.js +2 -0
- package/src/commands/env.js +8 -4
- package/src/commands/harden.js +26 -4
- package/src/commands/init.js +2 -1
- package/src/commands/plan/generate.js +4 -6
- package/src/commands/rag.js +69 -10
- package/src/commands/review-gate.js +15 -6
- package/src/commands/watch.js +13 -10
- package/src/config/defaults.js +3 -2
- package/src/environment/playbook.js +10 -3
- package/src/harden/config-templates.js +11 -0
- package/src/harden/guidelines-engine.js +24 -7
- package/src/harden/harness-hooks.js +92 -3
- package/src/harden/sentinel-hooks.js +80 -8
- package/src/lang/registry.js +2 -1
- package/src/mcp/handlers/rag-handler.js +14 -4
- package/src/orchestrator/drivers/post-loop.js +13 -11
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +3 -2
- package/src/orchestrator/drivers/pre-loop.js +1 -1
- package/src/orchestrator/stages/rag-context-stage.js +5 -2
- package/src/rag/auto-update.js +5 -3
- package/src/rag/coverage.js +36 -13
- package/src/rag/indexable.js +97 -0
- package/src/rag/indexer.js +31 -13
- package/src/rag/library.js +19 -13
- package/src/rag/migrate.js +73 -0
- package/src/rag/onnx-fallback.js +5 -3
- package/src/rag/project-store.js +48 -0
- package/src/rag/watcher.js +25 -14
- package/src/review/gate-gitignore.js +23 -1
- package/src/review/kj-provenance.js +68 -0
- package/src/review/rag-ledger.js +12 -4
- package/src/review/rag-requirement.js +17 -3
- package/src/review/verdict-store.js +6 -1
- package/src/sonar/api.js +11 -2
- package/src/sonar/project-key.js +33 -0
- package/src/sonar/scanner.js +37 -2
- package/src/start/sweep.js +4 -3
- package/src/utils/rulesync.js +61 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* hooks, which is why the guaranteed level requires Claude as host (ADR).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
11
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
14
|
import { join } from "node:path";
|
|
@@ -131,6 +131,7 @@ const POST_BODY = `#!/usr/bin/env node
|
|
|
131
131
|
// kj sentinel state writer (KJC-TSK-0713) — managed by \`kj harden\`.
|
|
132
132
|
// Records deterministic method facts per session; never blocks, never fails
|
|
133
133
|
// a tool call (PostToolUse, always exit 0).
|
|
134
|
+
import console from "node:console";
|
|
134
135
|
import process from "node:process";
|
|
135
136
|
import { relative } from "node:path";
|
|
136
137
|
import { spawnSync } from "node:child_process";
|
|
@@ -256,7 +257,16 @@ process.stdin.on("end", () => {
|
|
|
256
257
|
const state = load();
|
|
257
258
|
const s = session(state, sid);
|
|
258
259
|
const ref = CARD.exec(headRef || branchOf() || "");
|
|
259
|
-
|
|
260
|
+
// KJC-BUG-0230: una EPICA (…-PCS-…) no tiene estado que mover: el PG
|
|
261
|
+
// la deja en To Do por mas status que reciba y kj hu move no la
|
|
262
|
+
// conoce. Registrarla como pendiente era un bloqueo sin cauce. Se dice
|
|
263
|
+
// en el momento, para que no parezca que el board dejo de mirar.
|
|
264
|
+
const epic = ref && /-PCS-/i.test(ref[0]);
|
|
265
|
+
if (epic) {
|
|
266
|
+
console.error("karajan sentinel: board-sync — la PR #" + mergedPr + " pertenece a la epica " + ref[0].toUpperCase() + ", que no tiene estado que mover: no queda pendiente (su cierre va en la descripcion de la epica)");
|
|
267
|
+
} else {
|
|
268
|
+
(s.pending_moves ||= []).push({ card: ref ? ref[0].toUpperCase() : null, pr: mergedPr, at: Date.now(), ...(headRef ? {} : { head: "unknown" }) });
|
|
269
|
+
}
|
|
260
270
|
save(state);
|
|
261
271
|
}
|
|
262
272
|
const moved = /kj\\s+hu\\s+move\\s+([A-Za-z0-9-]+)\\s+([a-z&-]+)/i.exec(String(input.command || ""));
|
|
@@ -980,9 +990,19 @@ process.stdin.on("end", () => {
|
|
|
980
990
|
// cualquier proyecto sin indexar. Nada legible = se bloquea.
|
|
981
991
|
const cov = spawnSync("kj", ["rag", "covers", rel, "--json"], { cwd: ROOT, encoding: "utf8" });
|
|
982
992
|
let blind = null;
|
|
983
|
-
|
|
993
|
+
let absent = null;
|
|
994
|
+
try {
|
|
995
|
+
const c = JSON.parse(String(cov.stdout || ""));
|
|
996
|
+
if (c.state === "not-indexable") blind = c.reason;
|
|
997
|
+
// KJC-TSK-0891: si el indice no tiene el fichero, consultar no puede
|
|
998
|
+
// devolverlo; el remedio es indexarlo, y se nombra.
|
|
999
|
+
if (c.state === "stale" || c.state === "index-empty") absent = c.reason;
|
|
1000
|
+
} catch { blind = null; }
|
|
984
1001
|
if (blind) {
|
|
985
1002
|
console.error("karajan sentinel: rag-first — " + blind + " — no se te exige consultar lo que el indice no puede tener; el gate lo anota y sigue" + doc("rag-first"));
|
|
1003
|
+
} else if (absent) {
|
|
1004
|
+
console.error("karajan sentinel: rag-first — el indice aun no tiene " + rel + " (" + absent + "); indexalo y consulta despues: kj_rag_query / kj rag query <que hace " + rel + ">" + doc("rag-first"));
|
|
1005
|
+
process.exit(2);
|
|
986
1006
|
} else {
|
|
987
1007
|
console.error("karajan sentinel: rag-first — el RAG no ha respondido sobre " + rel + " en esta sesion; consulta antes de tocarlo: kj_rag_query / kj rag query <que hace " + rel + " y donde mas vive ese concepto>. (KJ_ALLOW_NO_RAG=1 = excepcion consciente, queda registrada)" + doc("rag-first"));
|
|
988
1008
|
process.exit(2);
|
|
@@ -1159,7 +1179,33 @@ function sealedByPath(projectDir, gitShowFn) {
|
|
|
1159
1179
|
}
|
|
1160
1180
|
|
|
1161
1181
|
/**
|
|
1162
|
-
*
|
|
1182
|
+
* KJC-BUG-0224: la procedencia PROPIA del harness, lo que `kj harden` escribio
|
|
1183
|
+
* (sha256 por guardia). Distinto del sello humano: no concede nada, solo dice
|
|
1184
|
+
* "esto lo puso kj", y lo unico que habilita es regenerar desde el kj
|
|
1185
|
+
* instalado. Forjarlo, por tanto, solo consigue que el guardia se restaure.
|
|
1186
|
+
*/
|
|
1187
|
+
const INSTALLED_RECORD = "installed.json";
|
|
1188
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
1189
|
+
|
|
1190
|
+
function readInstalledRecord(dir) {
|
|
1191
|
+
try {
|
|
1192
|
+
const files = JSON.parse(readFileSync(join(dir, INSTALLED_RECORD), "utf8"))?.files;
|
|
1193
|
+
return files && typeof files === "object" ? files : {};
|
|
1194
|
+
} catch {
|
|
1195
|
+
return {};
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function writeInstalledRecord(dir) {
|
|
1200
|
+
const files = {};
|
|
1201
|
+
for (const name of Object.keys(SCRIPT_BODIES)) {
|
|
1202
|
+
try { files[name] = sha256(readFileSync(join(dir, name), "utf8")); } catch { /* sin fichero no hay nada que registrar */ }
|
|
1203
|
+
}
|
|
1204
|
+
writeFileSync(join(dir, INSTALLED_RECORD), `${JSON.stringify({ files }, null, 2)}\n`);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* @returns {{ok: boolean, mismatched: string[], drift?: string[], regenerated?: string[], reason?: string}}
|
|
1163
1209
|
* `mismatched` is TAMPERING: a guard holds content nobody sealed, and that is
|
|
1164
1210
|
* what the hooks block on. `drift` is a sealed guard whose templates have
|
|
1165
1211
|
* moved on, which `kj harden` applies and which is not a defect.
|
|
@@ -1185,20 +1231,38 @@ export function verifySentinelScripts({ projectDir, readFileFn = readFileSync, g
|
|
|
1185
1231
|
// cannot be borrowed by also editing a template: that was the hole in the
|
|
1186
1232
|
// first attempt at this fix, and the review was right to reject it.
|
|
1187
1233
|
const sealed = sealedByPath(root, gitShowFn);
|
|
1234
|
+
const ownRecord = readInstalledRecord(dir);
|
|
1188
1235
|
const drift = [];
|
|
1189
1236
|
const tampered = [];
|
|
1237
|
+
const regenerated = [];
|
|
1190
1238
|
for (const name of mismatched) {
|
|
1191
1239
|
const text = installed.get(name);
|
|
1192
|
-
const
|
|
1193
|
-
if (
|
|
1240
|
+
const hash = text === undefined ? null : sha256(text);
|
|
1241
|
+
if (hash && sealed.get(`.karajan/harness/${name}`) === hash) drift.push(name);
|
|
1242
|
+
else if (hash && ownRecord[name] === hash) regenerated.push(name);
|
|
1194
1243
|
else tampered.push(name);
|
|
1195
1244
|
}
|
|
1196
|
-
|
|
1245
|
+
// KJC-BUG-0224 caso 1: lo que kj escribio y nadie sello ni toco se pone al
|
|
1246
|
+
// dia solo. Es lo que hacia el ritual de `kj harden`, hecho por la maquina.
|
|
1247
|
+
if (regenerated.length > 0) {
|
|
1248
|
+
for (const name of regenerated) writeHarnessScript(root, name, SCRIPT_BODIES[name]);
|
|
1249
|
+
writeInstalledRecord(dir);
|
|
1250
|
+
}
|
|
1251
|
+
const done = regenerated.length ? { regenerated } : {};
|
|
1252
|
+
if (tampered.length > 0) return { ok: false, mismatched: tampered, ...(drift.length ? { drift } : {}), ...done };
|
|
1253
|
+
if (drift.length === 0) {
|
|
1254
|
+
return { ok: true, mismatched: [], ...done, reason: `kj avanzó y estos guardias eran los que kj escribió: regenerados (${regenerated.join(", ")})` };
|
|
1255
|
+
}
|
|
1197
1256
|
return {
|
|
1257
|
+
...done,
|
|
1198
1258
|
ok: true,
|
|
1199
1259
|
mismatched: [],
|
|
1200
1260
|
drift,
|
|
1201
|
-
|
|
1261
|
+
// KJC-BUG-0222: `kj harden` NO pone al dia un guard sellado, lo deja en paz
|
|
1262
|
+
// a proposito (KJC-BUG-0193), asi que mandarlo era un bucle: ejecutas,
|
|
1263
|
+
// sale "unchanged" y el gate repite lo mismo. El que resuelve ESTE estado
|
|
1264
|
+
// es `kj harden --commit`, y es acto del humano.
|
|
1265
|
+
reason: "el harness instalado está sellado y las plantillas de este árbol van por delante — no es manipulación, y `kj harden` lo deja como está a propósito: quien lo avanza es el humano con `kj harden --commit`",
|
|
1202
1266
|
};
|
|
1203
1267
|
}
|
|
1204
1268
|
|
|
@@ -1214,6 +1278,13 @@ export function verifySentinelScripts({ projectDir, readFileFn = readFileSync, g
|
|
|
1214
1278
|
* guard is still the right move.
|
|
1215
1279
|
*/
|
|
1216
1280
|
export function installSentinelHooks({ projectDir = process.cwd(), logger = console, human = false, gitShowFn = null } = {}) {
|
|
1281
|
+
// KJC-BUG-0223: instalado desde un subdirectorio, esto sembraba un segundo
|
|
1282
|
+
// `.karajan/harness` ahi, y el gate (que mira otro sitio) pasaba a verlo. El
|
|
1283
|
+
// ritual quedaba "arreglado" y el arbol se llenaba de arneses huerfanos que
|
|
1284
|
+
// no gobiernan nada. El harness es del proyecto: va a su raiz.
|
|
1285
|
+
const root = resolveSentinelRoot(projectDir);
|
|
1286
|
+
if (root !== projectDir) logger?.info?.(`kj harden: el harness es del proyecto, se instala en su raíz (${root})`);
|
|
1287
|
+
projectDir = root;
|
|
1217
1288
|
const sealed = human ? new Map() : sealedByPath(projectDir, gitShowFn);
|
|
1218
1289
|
const deferred = [];
|
|
1219
1290
|
const keepsItsSeal = (name) => {
|
|
@@ -1232,6 +1303,7 @@ export function installSentinelHooks({ projectDir = process.cwd(), logger = cons
|
|
|
1232
1303
|
}
|
|
1233
1304
|
return writeHarnessScript(projectDir, name, body);
|
|
1234
1305
|
});
|
|
1306
|
+
writeInstalledRecord(join(projectDir, ".karajan", "harness"));
|
|
1235
1307
|
if (deferred.length > 0) {
|
|
1236
1308
|
logger?.info?.(`kj harden: ${deferred.join(", ")} se queda como está — es lo que un humano selló, y avanzarlo es suyo: kj harden --commit`);
|
|
1237
1309
|
}
|
package/src/lang/registry.js
CHANGED
|
@@ -14,7 +14,8 @@ import { RUST_ADAPTER } from "./rust.js";
|
|
|
14
14
|
// Segmentos siempre excluidos del walk, independientes de stack: VCS,
|
|
15
15
|
// outputs de build, scratch de Karajan, sandbox del audit. Lo que viva
|
|
16
16
|
// aquí jamás se indexa.
|
|
17
|
-
|
|
17
|
+
// KJC-BUG-0231: `.claude` guarda worktrees de subagentes, copias enteras del repo.
|
|
18
|
+
const COMMON_SKIP_SEGMENTS = [".git", "dist", "build", "coverage", ".karajan", ".kj", "_diet", ".claude"];
|
|
18
19
|
|
|
19
20
|
const REGISTRY = [JS_ADAPTER, PYTHON_ADAPTER, RUST_ADAPTER, GO_ADAPTER, JAVA_ADAPTER];
|
|
20
21
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// KJC-PCS-0049 Step 7 — MCP handlers calling src/rag/* directly. Imports
|
|
2
2
|
// from src/commands/* are forbidden by the layer-boundaries test
|
|
3
3
|
// (MCP and CLI are peer layers); we hit the pure rag/* modules instead.
|
|
4
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { countChunks, projectSlug } from "../../rag/vec-store.js";
|
|
6
|
+
import { openProjectStore } from "../../rag/project-store.js";
|
|
7
|
+
import { countProjectChunks, emptyIndexRemedy } from "../../rag/migrate.js";
|
|
5
8
|
import { makeGovernedEmbedder } from "../../rag/governed-embedder.js";
|
|
6
9
|
import { indexProject } from "../../rag/indexer.js";
|
|
10
|
+
import { ragExclude } from "../../rag/indexable.js";
|
|
7
11
|
import { query } from "../../rag/retriever.js";
|
|
8
12
|
import { getKarajanHome } from "../../utils/paths.js";
|
|
9
13
|
import { resolveProjectDir, buildConfig, responseText, failPayload } from "../shared-helpers.js";
|
|
@@ -28,9 +32,14 @@ export async function handleRagQuery(args, server) {
|
|
|
28
32
|
// projectDir, `project: "all"` disables the filter, `project: <slug>`
|
|
29
33
|
// overrides it.
|
|
30
34
|
const project = args?.project === "all" ? null : (args?.project || projectSlug(projectDir) || null);
|
|
31
|
-
const
|
|
35
|
+
const dim = config?.rag?.embedder?.dim || 768;
|
|
36
|
+
// KJC-TSK-0883 (ADR 0011): el indice del proyecto, como la CLI (0882).
|
|
37
|
+
const db = openProjectStore({ projectDir, dim });
|
|
32
38
|
try {
|
|
33
|
-
if (countChunks(db) === 0
|
|
39
|
+
if (countChunks(db) === 0 || (project && countProjectChunks(db, project) === 0)) {
|
|
40
|
+
const remedy = emptyIndexRemedy({ slug: project, legacyPath: join(getKarajanHome(), "rag.db"), dim });
|
|
41
|
+
return responseText({ hits: [], empty: true, topK, scope, project, remedy });
|
|
42
|
+
}
|
|
34
43
|
const hits = await query(db, makeGovernedEmbedder(config), text, { topK, scope, project });
|
|
35
44
|
return responseText({ hits, empty: false, topK, scope, project });
|
|
36
45
|
} finally { db.close(); }
|
|
@@ -43,12 +52,13 @@ export async function handleRagIndex(args, server) {
|
|
|
43
52
|
try {
|
|
44
53
|
const projectDir = await resolveProjectDir(server, args?.projectDir);
|
|
45
54
|
const config = await buildConfig({ ...args, projectDir }, "rag-index");
|
|
46
|
-
const db =
|
|
55
|
+
const db = openProjectStore({ projectDir, dim: config?.rag?.embedder?.dim || 768 });
|
|
47
56
|
try {
|
|
48
57
|
const totals = await indexProject(projectDir, {
|
|
49
58
|
db, embedder: makeGovernedEmbedder(config),
|
|
50
59
|
karajanHome: getKarajanHome(), logger: silentLogger,
|
|
51
60
|
withSources: Boolean(args?.withSources),
|
|
61
|
+
exclude: ragExclude(config), // KJC-TSK-0891
|
|
52
62
|
});
|
|
53
63
|
return responseText(totals);
|
|
54
64
|
} finally { db.close(); }
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { generateDiff } from "../../review/diff-generator.js";
|
|
25
25
|
import { finalizeGitAutomation } from "../../git/automation.js";
|
|
26
26
|
import { listCommitsBetween, listFilesChangedSince } from "../../utils/git.js";
|
|
27
|
-
import { loadPlan
|
|
27
|
+
import { loadPlan } from "../../plan/plan-store.js";
|
|
28
28
|
import { computePlanAdherenceScore } from "../../audit/plan-adherence.js";
|
|
29
29
|
import { saveSession, markSessionStatus } from "../../session/store.js";
|
|
30
30
|
import {
|
|
@@ -334,22 +334,24 @@ export async function handleMaxIterationsReached({ session, budgetSummary, emitt
|
|
|
334
334
|
return { approved: false, sessionId: session.id, reason: "max_iterations" };
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
/**
|
|
338
|
+
* KJC-TSK-0884 (ADR 0011): the board starts by itself, like Sonar. Only a
|
|
339
|
+
* declared false on either switch opts out; nothing declared means on.
|
|
340
|
+
*/
|
|
341
|
+
export const boardAutoStartWanted = (config) => config?.hu_board?.auto_start !== false && config?.hu_board?.enabled !== false;
|
|
342
|
+
|
|
337
343
|
export async function tryAutoStartBoard(config, logger, emitter, eventBase) {
|
|
338
|
-
|
|
339
|
-
// (enabled && auto_start) was confusing because `enabled` was never the
|
|
340
|
-
// user-facing switch — only auto_start was. This matches the AC: "kj run
|
|
341
|
-
// auto-inicia el board server si hu_board.auto_start es true".
|
|
342
|
-
if (!config.hu_board?.auto_start) return;
|
|
344
|
+
if (!boardAutoStartWanted(config)) return;
|
|
343
345
|
// Never auto-start during vitest: races the PID file and starts a detached
|
|
344
346
|
// process that outlives the test run.
|
|
345
347
|
if (process.env.VITEST || process.env.NODE_ENV === "test") return;
|
|
346
348
|
|
|
347
349
|
try {
|
|
348
|
-
const { startBoard, renderBoardBanner } = await import("../../commands/board.js");
|
|
349
|
-
const boardPort = config.hu_board
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
const slug = config.projectDir ?
|
|
350
|
+
const { startBoard, renderBoardBanner, boardSlug } = await import("../../commands/board.js");
|
|
351
|
+
const boardPort = config.hu_board?.port || 4000;
|
|
352
|
+
// KJC-TSK-0884: the project's scoped view (/p/<slug>), the parent repo's
|
|
353
|
+
// even from a worktree lane.
|
|
354
|
+
const slug = config.projectDir ? boardSlug(config.projectDir) : null;
|
|
353
355
|
const boardResult = await startBoard(boardPort, { projectSlug: slug });
|
|
354
356
|
const status = boardResult.alreadyRunning ? "already running" : "started";
|
|
355
357
|
// Highlighted URL box — visible regardless of log level (matches the
|
|
@@ -122,9 +122,10 @@ export async function maybeGenerateAutoHuBatch({
|
|
|
122
122
|
// node process around after the suite (TSK-0273).
|
|
123
123
|
if (process.env.VITEST || process.env.NODE_ENV === "test") return;
|
|
124
124
|
try {
|
|
125
|
-
const { startBoard, renderBoardBanner } = await import("../../../commands/board.js");
|
|
125
|
+
const { startBoard, renderBoardBanner, boardSlug } = await import("../../../commands/board.js");
|
|
126
126
|
const desiredPort = session.config_snapshot?.hu_board?.port ?? 4000;
|
|
127
|
-
|
|
127
|
+
// KJC-TSK-0884: its project's scoped view, not the global dashboard.
|
|
128
|
+
const boardResult = await startBoard(desiredPort, { projectSlug: projectDir ? boardSlug(projectDir) : null });
|
|
128
129
|
const url = boardResult.url;
|
|
129
130
|
const status = boardResult.alreadyRunning ? "already running" : "started";
|
|
130
131
|
const projectName = batch.projectName || "Auto-generated HUs";
|
|
@@ -322,7 +322,7 @@ export async function runPreLoopStages({ config, logger, emitter, eventBase, ses
|
|
|
322
322
|
// Hard fail if blocking checks failed (SonarQube enabled but not available)
|
|
323
323
|
if (!preflightResult.ok) {
|
|
324
324
|
const errorLines = (preflightResult.errors || [])
|
|
325
|
-
.map(e => ` - ${e.message}
|
|
325
|
+
.map(e => ` - ${e.message}${e.fix ? `\n Fix: ${e.fix}` : ""}`)
|
|
326
326
|
.join("\n");
|
|
327
327
|
throw new Error(
|
|
328
328
|
`Preflight FAILED — environment changed during session. Fix the issues and retry:\n${errorLines}`
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
// Failures degrade gracefully (warn + continue); the rest of the pipeline
|
|
6
6
|
// never sees an exception from this stage.
|
|
7
7
|
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
8
|
-
import {
|
|
8
|
+
import { countChunks } from "../../rag/vec-store.js";
|
|
9
9
|
import { makeGovernedEmbedder } from "../../rag/governed-embedder.js";
|
|
10
10
|
import { query } from "../../rag/retriever.js";
|
|
11
|
+
import { openProjectStore } from "../../rag/project-store.js";
|
|
11
12
|
|
|
12
13
|
const DEFAULT_TOP_K = 5;
|
|
13
14
|
const DEFAULT_SCOPE = "all";
|
|
@@ -44,7 +45,9 @@ export async function runRagContextStage({ config, logger, emitter, eventBase, t
|
|
|
44
45
|
if (typeof task !== "string" || task.length === 0) return { skipped: true, reason: "no-task" };
|
|
45
46
|
try {
|
|
46
47
|
const dim = config?.rag?.embedder?.dim || 768;
|
|
47
|
-
|
|
48
|
+
// KJC-TSK-0882: el preload que entra en el prompt del coder solo puede traer
|
|
49
|
+
// codigo de ESTE proyecto: antes abria el indice de la maquina sin filtrar.
|
|
50
|
+
const db = openProjectStore({ projectDir: config?.projectDir || process.cwd(), dim });
|
|
48
51
|
try {
|
|
49
52
|
if (countChunks(db) === 0) {
|
|
50
53
|
logger?.info?.("[rag-preload] corpus is empty, skipping (run `kj rag index` to seed)");
|
package/src/rag/auto-update.js
CHANGED
|
@@ -6,9 +6,11 @@ import { isAbsolute, join, resolve } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { execa } from "execa";
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "./vec-store.js";
|
|
10
10
|
import { indexProjectDelta } from "./indexer.js";
|
|
11
|
+
import { ragExclude } from "./indexable.js";
|
|
11
12
|
import { makeGovernedEmbedder } from "./governed-embedder.js";
|
|
13
|
+
import { openProjectStore } from "./project-store.js";
|
|
12
14
|
|
|
13
15
|
const HOOK_SRC = resolve(fileURLToPath(import.meta.url), "../../../scripts/git-hooks/post-merge");
|
|
14
16
|
|
|
@@ -20,12 +22,12 @@ export async function maybeAutoUpdate({ projectDir, config, logger = console, fl
|
|
|
20
22
|
try { const r = await execa("git", ["-C", projectDir, "rev-parse", "HEAD"]); head = r.stdout.trim(); }
|
|
21
23
|
catch { return { skipped: true }; }
|
|
22
24
|
const slug = projectSlug(projectDir);
|
|
23
|
-
const db =
|
|
25
|
+
const db = openProjectStore({ projectDir, dim: config?.rag?.embedder?.dim || 768 });
|
|
24
26
|
try {
|
|
25
27
|
const since = getLastIndexedCommit(db, slug);
|
|
26
28
|
if (!since || since === head) return { skipped: true, head };
|
|
27
29
|
logger.info?.(`[rag] drift detected (${since.slice(0, 7)} → ${head.slice(0, 7)}); running delta update`);
|
|
28
|
-
const totals = await indexProjectDelta(projectDir, { db, embedder: makeGovernedEmbedder(config), since, logger });
|
|
30
|
+
const totals = await indexProjectDelta(projectDir, { db, embedder: makeGovernedEmbedder(config), since, logger, exclude: ragExclude(config) });
|
|
29
31
|
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
30
32
|
return { ran: true, totals };
|
|
31
33
|
} catch (err) {
|
package/src/rag/coverage.js
CHANGED
|
@@ -8,20 +8,42 @@
|
|
|
8
8
|
* matchers are the single truth, here and in the indexer.
|
|
9
9
|
*/
|
|
10
10
|
import { execa } from "execa";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { isAbsolute, join, normalize, relative } from "node:path";
|
|
12
|
+
import { indexableReason } from "./indexable.js";
|
|
13
|
+
import { GENERATED_CONFIG_FILES } from "../harden/config-templates.js";
|
|
13
14
|
import { getLastIndexedCommit, projectSlug } from "./vec-store.js";
|
|
14
15
|
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* KJC-BUG-0221 — `kj harden` instala commitlint.config.js y eslint.config.js, y
|
|
19
|
+
* la cobertura los exigia indexados, asi que `kj check` denunciaba como drift
|
|
20
|
+
* dos ficheros que kj mismo acababa de escribir: la herramienta creando el
|
|
21
|
+
* fichero y quejandose de el. El criterio es el que ya usa el presupuesto de
|
|
22
|
+
* LOC (src/review/loc-budget.js): lo generado no cuenta, porque nadie lo
|
|
23
|
+
* escribio, y el RAG existe para responder sobre el codigo del equipo.
|
|
24
|
+
*
|
|
25
|
+
* La lista sale de la propia tabla de plantillas de harden, para que anadir un
|
|
26
|
+
* config no obligue a acordarse de esta otra lista.
|
|
27
|
+
*/
|
|
28
|
+
export function isGeneratedByHarden(rel) {
|
|
29
|
+
if (!rel) return false;
|
|
30
|
+
const base = rel.split("/").pop();
|
|
31
|
+
return GENERATED_CONFIG_FILES.has(base);
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
35
|
* @param {string} projectDir
|
|
17
36
|
* @param {{db: object}} deps - an open vec store
|
|
18
37
|
* @returns {Promise<{project: string, total: number, indexed: number, missing: string[], stale: string[], lastIndexedCommit: string|null, absent: boolean}>}
|
|
19
38
|
*/
|
|
20
|
-
export async function ragIndexCoverage(projectDir, { db }) {
|
|
39
|
+
export async function ragIndexCoverage(projectDir, { db, exclude = [] }) {
|
|
21
40
|
const project = projectSlug(projectDir);
|
|
22
|
-
const matchers = buildMatchers(detectAdaptersForProject(projectDir));
|
|
23
41
|
const { stdout } = await execa("git", ["-C", projectDir, "ls-files", "-z"]);
|
|
24
|
-
|
|
42
|
+
// KJC-TSK-0891: the indexer's own criterion, not a list of extensions.
|
|
43
|
+
const all = stdout.split("\0").filter((p) => p && indexableReason(p, join(projectDir, p), { exclude, projectDir }) === null);
|
|
44
|
+
// Lo que kj genera se informa aparte: ni se exige indexado ni se esconde.
|
|
45
|
+
const generated = all.filter(isGeneratedByHarden);
|
|
46
|
+
const sources = all.filter((p) => !isGeneratedByHarden(p));
|
|
25
47
|
|
|
26
48
|
const rows = db.prepare("SELECT DISTINCT source FROM chunks WHERE kind = 'code' AND project_slug = ?").all(project);
|
|
27
49
|
const indexed = new Set(rows.map((r) => (isAbsolute(r.source) ? relative(projectDir, r.source) : r.source)));
|
|
@@ -44,7 +66,7 @@ export async function ragIndexCoverage(projectDir, { db }) {
|
|
|
44
66
|
stale = sources.filter((p) => indexed.has(p) && changed.has(p));
|
|
45
67
|
}
|
|
46
68
|
|
|
47
|
-
return { project, total: sources.length, indexed: sources.length - missing.length, missing, stale, lastIndexedCommit, absent: indexed.size === 0 };
|
|
69
|
+
return { project, total: sources.length, indexed: sources.length - missing.length, missing, stale, generated, lastIndexedCommit, absent: indexed.size === 0 };
|
|
48
70
|
}
|
|
49
71
|
|
|
50
72
|
/**
|
|
@@ -60,20 +82,21 @@ export async function ragIndexCoverage(projectDir, { db }) {
|
|
|
60
82
|
*
|
|
61
83
|
* @returns {{state: "indexed"|"stale"|"not-indexable"|"index-empty", rel: string, canAnswer: boolean, reason: string}}
|
|
62
84
|
*/
|
|
63
|
-
export function fileIndexState(projectDir, file, { db }) {
|
|
85
|
+
export function fileIndexState(projectDir, file, { db, exclude = [] }) {
|
|
64
86
|
const project = projectSlug(projectDir);
|
|
65
87
|
// `./src/a.js` y `src/a.js` son el mismo fichero: sin normalizar, el segundo
|
|
66
88
|
// salia "stale" estando indexado (catch de la review).
|
|
67
89
|
const rel = normalize(isAbsolute(file) ? relative(projectDir, file) : file);
|
|
68
|
-
const matchers = buildMatchers(detectAdaptersForProject(projectDir));
|
|
69
90
|
const rows = db.prepare("SELECT DISTINCT source FROM chunks WHERE project_slug = ?").all(project);
|
|
70
91
|
const indexed = new Set(rows.map((r) => normalize(isAbsolute(r.source) ? relative(projectDir, r.source) : r.source)));
|
|
71
92
|
|
|
72
93
|
if (indexed.has(rel)) return { state: "indexed", rel, canAnswer: true, reason: `the index holds chunks for ${rel}` };
|
|
73
94
|
if (indexed.size === 0) return { state: "index-empty", rel, canAnswer: false, reason: `nothing of this project is indexed yet: kj rag index` };
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
95
|
+
// KJC-TSK-0891: not-indexable only for the file's NATURE (binary, generated,
|
|
96
|
+
// huge, excluded, skipped path), never for its language.
|
|
97
|
+
const out = indexableReason(rel, join(projectDir, rel), { exclude, projectDir });
|
|
98
|
+
if (out) return { state: "not-indexable", rel, canAnswer: false, reason: out };
|
|
99
|
+
// KJC-TSK-0891: `--since auto` only reaches what changed, so it cannot fix an
|
|
100
|
+
// unchanged file the index never held; indexing THIS file does.
|
|
101
|
+
return { state: "stale", rel, canAnswer: false, reason: `${rel} is indexable but absent from the index: kj rag index --file ${rel}` };
|
|
79
102
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-0891 (#1807 point 4) — what the index takes, decided without a list
|
|
3
|
+
* of extensions: there are countless languages, and a list always falls short.
|
|
4
|
+
* Any text file the project versions is indexable. What stays out stays out for
|
|
5
|
+
* its NATURE, never its language: binary, generated, too large, excluded by the
|
|
6
|
+
* project, or under a path the indexer always skips. Where a language adapter
|
|
7
|
+
* exists the chunker splits by symbol; elsewhere it falls back to overlapping
|
|
8
|
+
* text windows (chunkSource already does).
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { closeSync, lstatSync, openSync, readSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
import { matchesAny } from "@karajan-family/governance";
|
|
14
|
+
|
|
15
|
+
import { buildMatchers, detectAdaptersForProject } from "../lang/registry.js";
|
|
16
|
+
|
|
17
|
+
const MAX_BYTES = 512 * 1024;
|
|
18
|
+
const SNIFF_BYTES = 8192;
|
|
19
|
+
// Generated by tools, not written by the team: noise in any language.
|
|
20
|
+
const GENERATED = [
|
|
21
|
+
/(^|\/)(package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|Cargo\.lock|poetry\.lock|Pipfile\.lock|uv\.lock|composer\.lock|Gemfile\.lock|go\.sum)$/,
|
|
22
|
+
/\.min\.(js|css)$/,
|
|
23
|
+
/\.map$/,
|
|
24
|
+
/\.snap$/,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// May hold secrets: never sent to an embedder, which can be a remote service.
|
|
28
|
+
const SENSITIVE = [
|
|
29
|
+
/(^|\/)\.env(\.[^/]*)?$/,
|
|
30
|
+
/\.(pem|key|p12|pfx|jks|keystore)$/i,
|
|
31
|
+
/(^|\/)id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// The skipped paths are the ones of THIS project's languages: Java's `bin/`
|
|
35
|
+
// is build output, a JS project's `bin/` is source. Cached per project.
|
|
36
|
+
const skipCache = new Map();
|
|
37
|
+
const skipsFor = (projectDir) => {
|
|
38
|
+
const key = projectDir ?? "";
|
|
39
|
+
if (!skipCache.has(key)) skipCache.set(key, buildMatchers(detectAdaptersForProject(projectDir ?? process.cwd())).shouldSkip);
|
|
40
|
+
return skipCache.get(key);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** What the project keeps out of its index: `rag.exclude`, repo-relative globs. */
|
|
44
|
+
export const ragExclude = (config) => (Array.isArray(config?.rag?.exclude) ? config.rag.exclude : []);
|
|
45
|
+
|
|
46
|
+
function looksBinary(abs) {
|
|
47
|
+
const fd = openSync(abs, "r");
|
|
48
|
+
try {
|
|
49
|
+
const buf = Buffer.alloc(SNIFF_BYTES);
|
|
50
|
+
const n = readSync(fd, buf, 0, SNIFF_BYTES, 0);
|
|
51
|
+
return buf.subarray(0, n).includes(0);
|
|
52
|
+
} finally {
|
|
53
|
+
closeSync(fd);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} rel path relative to the project root
|
|
59
|
+
* @param {string} abs absolute path
|
|
60
|
+
* @param {{exclude?: string[], skip?: (rel: string) => boolean}} [opts]
|
|
61
|
+
* @returns {string|null} why it stays out of the index, or null if it goes in
|
|
62
|
+
*/
|
|
63
|
+
export function indexableReason(rel, abs, { exclude = [], projectDir = null, skip = skipsFor(projectDir) } = {}) {
|
|
64
|
+
if (skip(rel)) return `${rel} lives under a path the indexer always skips`;
|
|
65
|
+
if (matchesAny(rel, exclude)) return `${rel} is excluded by the project's rag.exclude`;
|
|
66
|
+
if (SENSITIVE.some((re) => re.test(rel))) return `${rel} may hold secrets, and the embedder can be a remote service`;
|
|
67
|
+
if (GENERATED.some((re) => re.test(rel))) return `${rel} is a generated file (lockfile, minified, sourcemap or snapshot)`;
|
|
68
|
+
let st;
|
|
69
|
+
try { st = lstatSync(abs); } catch { return `${rel} does not exist`; }
|
|
70
|
+
// Never followed: a versioned symlink could point the embedder at any
|
|
71
|
+
// readable file outside the project, secrets included.
|
|
72
|
+
if (st.isSymbolicLink()) return `${rel} is a symbolic link`;
|
|
73
|
+
if (!st.isFile()) return `${rel} is not a regular file`;
|
|
74
|
+
if (st.size > MAX_BYTES) return `${rel} is larger than ${MAX_BYTES / 1024} KB`;
|
|
75
|
+
try {
|
|
76
|
+
if (looksBinary(abs)) return `${rel} is binary`;
|
|
77
|
+
} catch {
|
|
78
|
+
return `${rel} could not be read`;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The project's files as git sees them: versioned, plus new ones git does not
|
|
85
|
+
* ignore. Null outside a git repo, so the caller can fall back to a walk.
|
|
86
|
+
* @returns {string[]|null} paths relative to projectDir
|
|
87
|
+
*/
|
|
88
|
+
export function listProjectFiles(projectDir) {
|
|
89
|
+
try {
|
|
90
|
+
const out = execFileSync("git", ["-C", projectDir, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
91
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024,
|
|
92
|
+
});
|
|
93
|
+
return [...new Set(out.split("\0").filter(Boolean))];
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/rag/indexer.js
CHANGED
|
@@ -12,7 +12,8 @@ import { execa } from "execa";
|
|
|
12
12
|
|
|
13
13
|
import { chunkMarkdown, chunkPlan, chunkSource } from "./chunker.js";
|
|
14
14
|
import { insertChunk, deleteChunksBySource, findChunkByHash } from "./vec-store.js";
|
|
15
|
-
import { detectAdaptersForProject,
|
|
15
|
+
import { detectAdaptersForProject, getAllCodeExtensions } from "../lang/registry.js";
|
|
16
|
+
import { indexableReason, listProjectFiles } from "./indexable.js";
|
|
16
17
|
|
|
17
18
|
// KJC-PCS-0052 PR-A — antes vivían dos consts hard-coded para JS aquí
|
|
18
19
|
// (CODE_EXT_RE + SKIP_SEGMENTS). Ahora vienen del registry de adapters,
|
|
@@ -39,19 +40,28 @@ function chunksFor(path, kind) {
|
|
|
39
40
|
if (kind === "plan" || kind === "onboarding") {
|
|
40
41
|
return chunkMarkdown(text, { path, kind });
|
|
41
42
|
}
|
|
43
|
+
// KJC-TSK-0891: a project's own prose is still a project source ("code":
|
|
44
|
+
// the store's schema allows no other kind), split by headings.
|
|
45
|
+
if (kind === "code" && /\.mdx?$/i.test(path)) return chunkMarkdown(text, { path, kind });
|
|
42
46
|
if (kind === "code") return chunkSource(text, { path });
|
|
43
47
|
return [];
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
/**
|
|
51
|
+
* KJC-TSK-0891: every project source is indexed as "code". Where an adapter
|
|
52
|
+
* exists it splits by symbol; any other text falls back to overlapping windows.
|
|
53
|
+
*/
|
|
54
|
+
export const sourceKind = () => "code";
|
|
55
|
+
|
|
46
56
|
/**
|
|
47
57
|
* Index a single file. Returns `{ indexed, failed }`. Idempotent —
|
|
48
58
|
* pre-existing chunks for this `source` path are deleted before the
|
|
49
59
|
* fresh batch is written, so a second call against the same file
|
|
50
60
|
* leaves the store with exactly the latest chunks (no duplicates).
|
|
51
61
|
*/
|
|
52
|
-
export async function indexFile(path, { db, embedder, logger = console, project = null } = {}) {
|
|
62
|
+
export async function indexFile(path, { db, embedder, logger = console, project = null, kind: declaredKind = null } = {}) {
|
|
53
63
|
if (!existsSync(path)) { logger.warn?.(`[rag-indexer] file not found: ${path}`); return { indexed: 0, failed: 0 }; }
|
|
54
|
-
const kind = detectKind(path);
|
|
64
|
+
const kind = declaredKind ?? detectKind(path);
|
|
55
65
|
if (!kind) { logger.warn?.(`[rag-indexer] unknown kind: ${path}`); return { indexed: 0, failed: 0 }; }
|
|
56
66
|
const chunks = chunksFor(path, kind);
|
|
57
67
|
if (!chunks.length) return { indexed: 0, failed: 0, skipped: 0 };
|
|
@@ -118,7 +128,7 @@ async function listFiles(dir, predicate) {
|
|
|
118
128
|
* indexing tens of thousands of files is expensive — the CLI exposes
|
|
119
129
|
* it as `--with-sources`.
|
|
120
130
|
*/
|
|
121
|
-
export async function indexProject(projectDir, { db, embedder, karajanHome, logger = console, withSources = false } = {}) {
|
|
131
|
+
export async function indexProject(projectDir, { db, embedder, karajanHome, logger = console, withSources = false, exclude = [] } = {}) {
|
|
122
132
|
// KJC-TSK-0640: a full index must stamp HEAD too — without it,
|
|
123
133
|
// last_indexed_commit stayed null after the FIRST index, so the drift
|
|
124
134
|
// delta-update (maybeAutoUpdate) never engaged until a manual --since.
|
|
@@ -148,10 +158,20 @@ export async function indexProject(projectDir, { db, embedder, karajanHome, logg
|
|
|
148
158
|
// duplicate every chunk and contaminate retrieval scores. `_diet` is the
|
|
149
159
|
// tests/_diet/ sandbox used by the test-diet audit harness — never user
|
|
150
160
|
// code. Both viven en COMMON_SKIP_SEGMENTS dentro del registry.
|
|
151
|
-
|
|
152
|
-
|
|
161
|
+
// KJC-BUG-0231: las exclusiones se juzgan DENTRO del proyecto (ruta relativa).
|
|
162
|
+
// KJC-TSK-0891: sin lista de extensiones: cualquier texto que el proyecto
|
|
163
|
+
// versiona, fuera lo binario, generado, enorme o excluido (indexable.js).
|
|
164
|
+
const outOf = (abs) => indexableReason(relative(projectDir, abs), abs, { exclude, projectDir });
|
|
165
|
+
const listed = listProjectFiles(projectDir);
|
|
166
|
+
const candidates = listed ? listed.map((rel) => join(projectDir, rel)) : await listFiles(projectDir, () => true);
|
|
167
|
+
const sources = candidates.filter((abs) => outOf(abs) === null);
|
|
168
|
+
// Lo que un indice anterior guardo y hoy ya no entra (excluido o borrado), fuera.
|
|
169
|
+
const prefix = `${projectDir}/`;
|
|
170
|
+
for (const { source } of db.prepare("SELECT DISTINCT source FROM chunks WHERE project_slug = ?").all(slug)) {
|
|
171
|
+
if (source.startsWith(prefix) && outOf(source) !== null) deleteChunksBySource(db, source);
|
|
172
|
+
}
|
|
153
173
|
for (const s of sources) {
|
|
154
|
-
const r = await indexFile(s, { db, embedder, logger, project: slug });
|
|
174
|
+
const r = await indexFile(s, { db, embedder, logger, project: slug, kind: sourceKind(s) });
|
|
155
175
|
totals.indexed += r.indexed; totals.failed += r.failed; totals.files += 1;
|
|
156
176
|
}
|
|
157
177
|
}
|
|
@@ -165,7 +185,7 @@ export async function indexProject(projectDir, { db, embedder, karajanHome, logg
|
|
|
165
185
|
* `head` lets callers persist via setLastIndexedCommit. Throws on diff failure
|
|
166
186
|
* (shallow clone, unknown ref) so the CLI can fall back to a full reindex.
|
|
167
187
|
*/
|
|
168
|
-
export async function indexProjectDelta(projectDir, { db, embedder, since, logger = console } = {}) {
|
|
188
|
+
export async function indexProjectDelta(projectDir, { db, embedder, since, logger = console, exclude = [] } = {}) {
|
|
169
189
|
const totals = { indexed: 0, failed: 0, files: 0, deleted: 0, head: null };
|
|
170
190
|
const slug = projectDir.split("/").pop()?.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase() || "project";
|
|
171
191
|
const { stdout: head } = await execa("git", ["-C", projectDir, "rev-parse", "HEAD"]);
|
|
@@ -182,16 +202,14 @@ export async function indexProjectDelta(projectDir, { db, embedder, since, logge
|
|
|
182
202
|
}
|
|
183
203
|
const adapters = detectAdaptersForProject(projectDir);
|
|
184
204
|
await prepareAdapters(adapters, { logger });
|
|
185
|
-
const matchers = buildMatchers(adapters);
|
|
186
205
|
for (const { kind, p } of ops) {
|
|
187
|
-
if (matchers.shouldSkip(p)) continue;
|
|
188
|
-
if (!matchers.isCodeFile(p)) continue;
|
|
189
206
|
const abs = isAbsolute(p) ? p : join(projectDir, p);
|
|
190
207
|
if (kind === "del") {
|
|
191
208
|
totals.deleted += deleteChunksBySource(db, abs);
|
|
192
209
|
logger.info?.(`[rag-indexer] delta delete ${relative(process.cwd(), abs)}`);
|
|
193
|
-
} else if (
|
|
194
|
-
|
|
210
|
+
} else if (indexableReason(relative(projectDir, abs), abs, { exclude, projectDir }) === null) {
|
|
211
|
+
// KJC-TSK-0891: the same criterion as the full walk, not a list of extensions.
|
|
212
|
+
const r = await indexFile(abs, { db, embedder, logger, project: slug, kind: sourceKind(abs) });
|
|
195
213
|
totals.indexed += r.indexed; totals.failed += r.failed; totals.files += 1;
|
|
196
214
|
}
|
|
197
215
|
}
|