karajan-code 4.62.0 → 4.63.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 +2 -2
- package/packages/hu-board/src/plan-mutations.js +21 -0
- package/src/cli/register-meta.js +15 -2
- package/src/commands/hu.js +112 -13
- package/src/commands/rag.js +34 -8
- package/src/hu/github-board.js +27 -5
- package/src/hu/team-board-cache.js +84 -0
- package/src/hu/team-board-mirror.js +26 -0
- package/src/lang/chunk-go.js +2 -27
- package/src/lang/chunk-java.js +7 -32
- package/src/lang/chunk-python.js +2 -27
- package/src/lang/chunk-rust.js +2 -27
- package/src/lang/chunk-span.js +49 -0
- package/src/plan/plan-store.js +4 -1
- package/src/rag/auto-update.js +55 -7
- package/src/rag/bootstrap-index.js +86 -0
- package/src/rag/chunker-ast.js +5 -12
- package/src/rag/chunker.js +2 -14
- package/src/rag/embed-header.js +35 -0
- package/src/rag/embed-schema.js +28 -0
- package/src/rag/eval.js +39 -3
- package/src/rag/indexer.js +36 -14
- package/src/rag/worktree-files.js +26 -0
package/src/rag/indexer.js
CHANGED
|
@@ -14,6 +14,9 @@ import { chunkMarkdown, chunkPlan, chunkSource } from "./chunker.js";
|
|
|
14
14
|
import { insertChunk, deleteChunksBySource, findChunkByHash, getEmbeddingsByIds } from "./vec-store.js";
|
|
15
15
|
import { detectAdaptersForProject, getAllCodeExtensions } from "../lang/registry.js";
|
|
16
16
|
import { indexableReason, listProjectFiles } from "./indexable.js";
|
|
17
|
+
import { EMBED_SCHEMA, embedText } from "./embed-header.js";
|
|
18
|
+
import { writeEmbedSchema } from "./embed-schema.js";
|
|
19
|
+
import { untrackedFiles } from "./worktree-files.js";
|
|
17
20
|
|
|
18
21
|
// KJC-PCS-0052 PR-A — antes vivían dos consts hard-coded para JS aquí
|
|
19
22
|
// (CODE_EXT_RE + SKIP_SEGMENTS). Ahora vienen del registry de adapters,
|
|
@@ -59,7 +62,7 @@ export const sourceKind = () => "code";
|
|
|
59
62
|
* fresh batch is written, so a second call against the same file
|
|
60
63
|
* leaves the store with exactly the latest chunks (no duplicates).
|
|
61
64
|
*/
|
|
62
|
-
export async function indexFile(path, { db, embedder, logger = console, project = null, kind: declaredKind = null } = {}) {
|
|
65
|
+
export async function indexFile(path, { db, embedder, logger = console, project = null, kind: declaredKind = null, root = process.cwd() } = {}) {
|
|
63
66
|
if (!existsSync(path)) { logger.warn?.(`[rag-indexer] file not found: ${path}`); return { indexed: 0, failed: 0 }; }
|
|
64
67
|
const kind = declaredKind ?? detectKind(path);
|
|
65
68
|
if (!kind) { logger.warn?.(`[rag-indexer] unknown kind: ${path}`); return { indexed: 0, failed: 0 }; }
|
|
@@ -75,11 +78,17 @@ export async function indexFile(path, { db, embedder, logger = console, project
|
|
|
75
78
|
// the stored vector: a file whose every chunk duplicates another (CLAUDE.md
|
|
76
79
|
// next to the AGENTS.md kj harden writes) was left with no rows at all, and
|
|
77
80
|
// the coverage check reported it missing after every reindex.
|
|
78
|
-
|
|
81
|
+
// KJC-TSK-1171 (RAG-B): the embedder reads the metadata header before the body, so a
|
|
82
|
+
// query by file, module or symbol name lands; the stored text stays the body alone.
|
|
83
|
+
// The dedup key is what the embedder read, header included, under the current schema: a
|
|
84
|
+
// vector is reused only when the same body sits under the same path, symbol and lines,
|
|
85
|
+
// never one embedded under another header or an older schema.
|
|
86
|
+
const embeddable = embedText(ch, { root });
|
|
87
|
+
const contentHash = createHash("sha256").update(`${EMBED_SCHEMA}\n${embeddable}`).digest("hex");
|
|
79
88
|
try {
|
|
80
89
|
const dup = findChunkByHash(db, contentHash, project);
|
|
81
90
|
const stored = dup ? getEmbeddingsByIds(db, [dup.id]).get(Number(dup.id)) : null;
|
|
82
|
-
const embedding = stored ?? await embedder.embed(
|
|
91
|
+
const embedding = stored ?? await embedder.embed(embeddable);
|
|
83
92
|
insertChunk(db, { source: path, kind: ch.metadata.kind || kind, text: ch.text, metadata: ch.metadata, embedding, project, contentHash });
|
|
84
93
|
if (stored) skipped += 1; else indexed += 1;
|
|
85
94
|
} catch (err) {
|
|
@@ -145,13 +154,13 @@ export async function indexProject(projectDir, { db, embedder, karajanHome, logg
|
|
|
145
154
|
if (existsSync(planRoot)) {
|
|
146
155
|
const plans = await listFiles(planRoot, (p) => /plan-.*\.json$/.test(p));
|
|
147
156
|
for (const p of plans) {
|
|
148
|
-
const r = await indexFile(p, { db, embedder, logger, project: slug });
|
|
157
|
+
const r = await indexFile(p, { db, embedder, logger, project: slug, root: projectDir });
|
|
149
158
|
totals.indexed += r.indexed; totals.failed += r.failed; totals.files += 1;
|
|
150
159
|
}
|
|
151
160
|
}
|
|
152
161
|
const onboarding = join(karajanHome, ONBOARDING_DIR, `${slug}.md`);
|
|
153
162
|
if (existsSync(onboarding)) {
|
|
154
|
-
const r = await indexFile(onboarding, { db, embedder, logger, project: slug });
|
|
163
|
+
const r = await indexFile(onboarding, { db, embedder, logger, project: slug, root: projectDir });
|
|
155
164
|
totals.indexed += r.indexed; totals.failed += r.failed; totals.files += 1;
|
|
156
165
|
}
|
|
157
166
|
if (withSources) {
|
|
@@ -173,9 +182,13 @@ export async function indexProject(projectDir, { db, embedder, karajanHome, logg
|
|
|
173
182
|
if (source.startsWith(prefix) && outOf(source) !== null) deleteChunksBySource(db, source);
|
|
174
183
|
}
|
|
175
184
|
for (const s of sources) {
|
|
176
|
-
const r = await indexFile(s, { db, embedder, logger, project: slug, kind: sourceKind() });
|
|
185
|
+
const r = await indexFile(s, { db, embedder, logger, project: slug, kind: sourceKind(), root: projectDir });
|
|
177
186
|
totals.indexed += r.indexed; totals.failed += r.failed; totals.files += 1;
|
|
178
187
|
}
|
|
188
|
+
// KJC-TSK-1171: only a COMPLETE index of the sources carries the embed schema; a file the
|
|
189
|
+
// embedder failed on leaves the marker as it was, so the rebuild is asked for again.
|
|
190
|
+
if (totals.failed === 0) writeEmbedSchema(projectDir);
|
|
191
|
+
else logger.warn?.(`[rag-indexer] ${totals.failed} chunk(s) failed: the embed schema is not recorded, the index will be rebuilt again`);
|
|
179
192
|
}
|
|
180
193
|
return totals;
|
|
181
194
|
}
|
|
@@ -187,21 +200,30 @@ export async function indexProject(projectDir, { db, embedder, karajanHome, logg
|
|
|
187
200
|
* `head` lets callers persist via setLastIndexedCommit. Throws on diff failure
|
|
188
201
|
* (shallow clone, unknown ref) so the CLI can fall back to a full reindex.
|
|
189
202
|
*/
|
|
190
|
-
export async function indexProjectDelta(projectDir, { db, embedder, since, logger = console, exclude = [] } = {}) {
|
|
203
|
+
export async function indexProjectDelta(projectDir, { db, embedder, since, logger = console, exclude = [], worktree = false, reconcile = [] } = {}) {
|
|
191
204
|
const totals = { indexed: 0, failed: 0, files: 0, deleted: 0, head: null };
|
|
192
205
|
const slug = projectDir.split("/").pop()?.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase() || "project";
|
|
193
206
|
const { stdout: head } = await execa("git", ["-C", projectDir, "rev-parse", "HEAD"]);
|
|
194
207
|
totals.head = head.trim();
|
|
195
|
-
if (totals.head === since) return totals;
|
|
196
|
-
|
|
208
|
+
if (totals.head === since && !worktree) return totals;
|
|
209
|
+
// KJC-TSK-1174 (RAG-A1): with `worktree`, the diff ends at the files on disk instead of HEAD,
|
|
210
|
+
// and untracked files join it, so what is being written is retrievable before it is committed.
|
|
211
|
+
const { stdout } = await execa("git", ["-C", projectDir, "diff", "--name-status", "-z", since, ...(worktree ? [] : ["HEAD"])]);
|
|
197
212
|
const parts = stdout.split("\0").filter(Boolean);
|
|
198
213
|
const ops = []; // { kind: 'del'|'idx', p }
|
|
199
|
-
|
|
214
|
+
let i = 0;
|
|
215
|
+
while (i < parts.length) {
|
|
200
216
|
const s = parts[i];
|
|
201
|
-
if (s.startsWith("R")) { ops.push({ kind: "del", p: parts[
|
|
202
|
-
else if (s
|
|
203
|
-
else { ops.push({ kind: "
|
|
217
|
+
if (s.startsWith("R")) { ops.push({ kind: "del", p: parts[i + 1] }, { kind: "idx", p: parts[i + 2] }); i += 3; }
|
|
218
|
+
else if (s.startsWith("C")) { ops.push({ kind: "idx", p: parts[i + 2] }); i += 3; } // a copy keeps its source
|
|
219
|
+
else if (s === "D") { ops.push({ kind: "del", p: parts[i + 1] }); i += 2; }
|
|
220
|
+
else { ops.push({ kind: "idx", p: parts[i + 1] }); i += 2; }
|
|
204
221
|
}
|
|
222
|
+
if (worktree) for (const p of await untrackedFiles(projectDir)) ops.push({ kind: "idx", p });
|
|
223
|
+
// Paths a previous refresh took from a dirty tree: on disk they are reindexed as they are now
|
|
224
|
+
// (reverted or still dirty), gone they are dropped. Each path is handled once.
|
|
225
|
+
const seen = new Set(ops.map((o) => o.p));
|
|
226
|
+
for (const p of reconcile) if (!seen.has(p)) ops.push({ kind: existsSync(join(projectDir, p)) ? "idx" : "del", p });
|
|
205
227
|
const adapters = detectAdaptersForProject(projectDir);
|
|
206
228
|
await prepareAdapters(adapters, { logger });
|
|
207
229
|
for (const { kind, p } of ops) {
|
|
@@ -211,7 +233,7 @@ export async function indexProjectDelta(projectDir, { db, embedder, since, logge
|
|
|
211
233
|
logger.info?.(`[rag-indexer] delta delete ${relative(process.cwd(), abs)}`);
|
|
212
234
|
} else if (indexableReason(relative(projectDir, abs), abs, { exclude, projectDir }) === null) {
|
|
213
235
|
// KJC-TSK-0891: the same criterion as the full walk, not a list of extensions.
|
|
214
|
-
const r = await indexFile(abs, { db, embedder, logger, project: slug, kind: sourceKind() });
|
|
236
|
+
const r = await indexFile(abs, { db, embedder, logger, project: slug, kind: sourceKind(), root: projectDir });
|
|
215
237
|
totals.indexed += r.indexed; totals.failed += r.failed; totals.files += 1;
|
|
216
238
|
}
|
|
217
239
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// KJC-TSK-1174 (RAG-A1): what the working tree holds beyond HEAD, for the pre-query refresh.
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import { projectDbPath } from "./project-store.js";
|
|
6
|
+
|
|
7
|
+
/** Files git does not track yet and does not ignore: the ones a session has just created. */
|
|
8
|
+
export async function untrackedFiles(projectDir) {
|
|
9
|
+
const { stdout } = await execa("git", ["-C", projectDir, "ls-files", "--others", "--exclude-standard", "-z"]);
|
|
10
|
+
return stdout.split("\0").filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// The dirty paths the last refresh indexed, next to the store. Reverted or deleted since, git no
|
|
14
|
+
// longer lists them, yet the index holds their discarded content: the next refresh reconciles them.
|
|
15
|
+
const notePath = (projectDir) => `${projectDbPath(projectDir)}.worktree-paths`;
|
|
16
|
+
|
|
17
|
+
export function readRefreshedPaths(projectDir) {
|
|
18
|
+
const file = notePath(projectDir);
|
|
19
|
+
return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function writeRefreshedPaths(projectDir, paths) {
|
|
23
|
+
const file = notePath(projectDir);
|
|
24
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
25
|
+
writeFileSync(file, JSON.stringify(paths), "utf8");
|
|
26
|
+
}
|