karajan-code 2.33.0 → 2.34.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/README.md CHANGED
@@ -345,6 +345,24 @@ Opt out: set `telemetry: false` in `~/.karajan/kj.config.yml`
345
345
 
346
346
  ## Recent releases
347
347
 
348
+ > **v2.34.0 released** — Minor. Two epics in one window. **KJC-PCS-0052 "Multi-language RAG"** — Python, Rust, Go and Java join JS/TS as first-class citizens of the local RAG index (AST chunkers via `web-tree-sitter` + multi-lang watcher + audit/onboarder multi-stack manifest sniffer), so a polyglot repo gets the same semantic retrieval surface as a single-language one. **KJC-PCS-0053 "RAG Quality & Observability"** — `kj rag eval` ships a frozen golden-query harness with `recall@k + MRR` (CI-gated via `--min-recall`), content-hash dedup skips redundant embeddings, optional MMR diversification returns *different* chunks instead of five near-duplicates, and `docs/RAG.md` gains a six-questions deep-dive documenting the system end-to-end. `kj rag index --since <ref>` + a post-merge git hook keep the index in sync with `git pull` / merge without manual intervention.
349
+ >
350
+ > - **`vec_store_meta` + `kj rag index --since`** (PR #882, KJC-TSK-0455 PR1): per-project last-indexed-commit stamp; `--since auto` resolves it; explicit refs honoured; missing baseline falls back to a full index with a friendly warning. Idempotent.
351
+ > - **Post-merge hook + pre-run drift check** (PR #883, KJC-TSK-0455 PR2): new `kj rag install-hooks` writes `.git/hooks/post-merge` so `git pull` auto-triggers a delta re-index. Pre-run drift check emits a one-line hint when `HEAD` is ahead of the last indexed SHA by more than N files.
352
+ > - **Language registry + Python canary** (PR #884, KJC-TSK-0474): new `src/lang/registry.js` with `adapterForPath(file)`; the indexer + watcher both route through it. Python lands as the first non-JS adapter to exercise the registry shape from day one.
353
+ > - **Python AST chunker** (PR #886, KJC-TSK-0478) and **Rust AST chunker** (PR #888, KJC-TSK-0479): web-tree-sitter walkers + regex fallbacks, grammars vendored under `vendor/tree-sitter-grammars/*.wasm` so SEA binaries stay self-contained.
354
+ > - **Indexer prepares Python / Rust grammars** (PR #889, KJC-TSK-0480): `prepareAdapters()` awaits the grammar loads before walking so the first file doesn't pay the cold-load tax mid-batch.
355
+ > - **Go AST chunker** (PR #890, KJC-TSK-0481): tree-sitter walker + regex fallback. Method receivers rendered explicitly so retrieval surfaces method-vs-function disambiguation without re-parsing.
356
+ > - **Multi-stack manifest sniffer for `basal-cost`** (PR #891, KJC-TSK-0476): `kj audit` now reads Python (`pyproject.toml`, `requirements*.txt`), Rust (`Cargo.toml`), Go (`go.mod`) and Java (`pom.xml`, `build.gradle*`) manifests alongside `package.json`. Cost estimates no longer assume JS/TS.
357
+ > - **Multi-stack frameworks in audit bundle** (PR #892, KJC-TSK-0477): the audit bundle JSON gains a `stacks` field listing detected language stacks + framework hints (Django, Flask, Actix, Tokio, Gin, Echo, Spring, Quarkus...). Onboarder + audit role render polyglot reality.
358
+ > - **Java AST chunker** (PR #893, KJC-TSK-0486): two-level walker (class / interface / enum / record → method / constructor) + regex fallback. Inner classes flattened with `Outer$Inner` symbols.
359
+ > - **`docs/RAG.md` six-questions deep-dive** (PR #894, KJC-TSK-0485): per-language chunker table, six-provider embedder matrix, hybrid + rerank + metadata filter, update strategy table, validation tiers. Excluded from the shrink-budget gate.
360
+ > - **Watcher derived from language registry** (PR #895, KJC-TSK-0482): `kj watch` matcher no longer hard-codes JS/TS. Python / Rust / Go / Java sources trigger live re-index.
361
+ > - **Content-hash skip-on-match** (PR #896, KJC-TSK-0484 PR-A): each chunk gains a `sha256` column; identical bodies skip the Ollama call. Re-indexing identical files now costs zero embeddings.
362
+ > - **MMR diversification in retriever** (PR #898, KJC-TSK-0484 PR-B): optional `rag.search.mmr` over the top-N candidates; lambda (`rag.search.mmrLambda`, default 0.5) trades pure cosine relevance for diversity. Off by default.
363
+ > - **Retrieval-quality harness** (PR #899, KJC-TSK-0483 PR-A): pure `runEval(queries, runQuery, { topK, ks })` scoring recall@k (binary) + MRR. `tests/rag/golden-queries.json` ships 20 entries covering the public surface of `src/rag/`.
364
+ > - **`kj rag eval` CLI + baseline docs** (PR #900, KJC-TSK-0483 PR-B): subcommand wires the harness to the indexed corpus, emits aggregate + per-query report (human or `--json`), and sets `process.exitCode = 1` when `--min-recall <n>` is set and aggregate falls below — drop-in CI gate after `kj rag index`.
365
+
348
366
  > **v2.33.0 released** — Minor. **AI Harness Scorecard golden metric (KJC-PCS-0051, Plan B)** — every `kj audit` now boots a Docker one-shot of `addyosmani/ai-harness-scorecard`, gets a deterministic 0–100 score + A–F grade, persists it to a per-project `audit-history.db`, and on the next run renders the delta vs the previous baseline plus an optional Unicode-bar trend sparkline. One golden number for "how AI-friendly is this repo today vs last week," zero LLM tokens spent.
349
367
  >
350
368
  > - **Harness Docker one-shot** (PR #877, KJC-TSK-0470): `src/audit/harness-runner.js` runs `docker run --rm -v <repo>:/repo addyosmani/ai-harness-scorecard analyze`, parses the verdict, auto-skips on missing Docker. `--no-harness` opts out.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.33.0",
3
+ "version": "2.34.0",
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",
@@ -37,6 +37,7 @@
37
37
  "bin/",
38
38
  "templates/",
39
39
  "scripts/",
40
+ "vendor/tree-sitter-grammars/",
40
41
  "packages/hu-board/src/",
41
42
  "packages/hu-board/public/",
42
43
  "packages/hu-board/package.json",
@@ -85,7 +86,8 @@
85
86
  "knip": "^6.14.1",
86
87
  "madge": "^8.0.0",
87
88
  "sqlite-vec": "^0.1.9",
88
- "valibot": "^1.3.1"
89
+ "valibot": "^1.3.1",
90
+ "web-tree-sitter": "^0.26.9"
89
91
  },
90
92
  "devDependencies": {
91
93
  "@eslint/js": "^10.0.1",
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # KJC-TSK-0455 — Auto-refresh the local RAG index after a merge so retrieval
3
+ # never serves stale code. Installed by `kj rag install-hooks`. The marker
4
+ # in this comment is how the installer tells kj-managed hooks from user hooks.
5
+ set -euo pipefail
6
+ if ! command -v kj >/dev/null 2>&1; then exit 0; fi
7
+ kj rag index --since auto >/dev/null 2>&1 || true
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  import { getKarajanHome } from "../utils/paths.js";
6
+ import { sniffManifests, summariseStack } from "../utils/manifest-sniff.js";
6
7
 
7
8
  const execFileAsync = promisify(execFile);
8
9
 
@@ -59,16 +60,16 @@ async function countLines(filePath) {
59
60
  }
60
61
  }
61
62
 
62
- async function countDependencies(projectDir) {
63
- const pkgPath = path.join(projectDir, "package.json");
64
- try {
65
- const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
66
- const deps = Object.keys(pkg.dependencies || {});
67
- const devDeps = Object.keys(pkg.devDependencies || {});
68
- return { dependencies: deps.length, devDependencies: devDeps.length, total: deps.length + devDeps.length };
69
- } catch {
70
- return { dependencies: 0, devDependencies: 0, total: 0 };
63
+ function countDependenciesFromStack(stack) {
64
+ // Suma deps + devDeps de TODOS los manifests detectados (multi-stack).
65
+ // Si no hay manifests, devuelve ceros — coherente con repos sin paquete.
66
+ let deps = 0;
67
+ let devDeps = 0;
68
+ for (const m of stack.manifests) {
69
+ deps += m.deps;
70
+ devDeps += m.devDeps;
71
71
  }
72
+ return { dependencies: deps, devDependencies: devDeps, total: deps + devDeps };
72
73
  }
73
74
 
74
75
  function runDepcheck(projectDir) {
@@ -231,8 +232,11 @@ export async function measureBasalCost(projectDir) {
231
232
  totalLines += await countLines(f);
232
233
  }
233
234
 
234
- const depInfo = await countDependencies(projectDir);
235
- const depcheck = await runDepcheck(projectDir);
235
+ const stack = summariseStack(await sniffManifests(projectDir));
236
+ const depInfo = countDependenciesFromStack(stack);
237
+ // depcheck solo aplica a Node; en stacks puros no-Node lo saltamos.
238
+ const hasNode = stack.manifests.some((m) => m.stack === "node");
239
+ const depcheck = hasNode ? await runDepcheck(projectDir) : { unused: [], note: "depcheck skipped (no package.json)" };
236
240
  const deadExports = await findDeadExports(sourceFiles);
237
241
 
238
242
  return {
@@ -240,7 +244,8 @@ export async function measureBasalCost(projectDir) {
240
244
  totalFiles: sourceFiles.length,
241
245
  dependencies: depInfo,
242
246
  unusedDependencies: depcheck,
243
- deadExports
247
+ deadExports,
248
+ stack
244
249
  };
245
250
  }
246
251
 
@@ -3,7 +3,7 @@ import { triageCommand } from "../commands/triage.js";
3
3
  import { researcherCommand } from "../commands/researcher.js";
4
4
  import { architectCommand } from "../commands/architect.js";
5
5
  import { onboardCommand } from "../commands/onboard.js";
6
- import { ragIndexCommand, ragQueryCommand } from "../commands/rag.js";
6
+ import { ragIndexCommand, ragQueryCommand, ragInstallHooksCommand, ragEvalCommand } from "../commands/rag.js";
7
7
  import { watchStartCommand, watchStopCommand, watchStatusCommand } from "../commands/watch.js";
8
8
  import { auditCommand } from "../commands/audit.js";
9
9
  import { resumeCommand } from "../commands/resume.js";
@@ -104,12 +104,21 @@ export function registerMeta(program, { pkgVersion }) {
104
104
  rag.command("index")
105
105
  .description("Index plans + onboarding (and optionally project sources) into the local vector store")
106
106
  .option("--with-sources", "Also index the projectDir's JS/TS files")
107
+ .option("--since <ref>", "Only re-index files changed since <ref> ('auto' uses the last indexed commit; falls back to full index if unset). KJC-TSK-0455.")
107
108
  .option("--json", "Output the totals as JSON")
108
109
  .action(async (flags) => {
109
110
  await withConfig(pkgVersion, "rag-index", flags, async ({ config, logger }) => {
110
111
  await ragIndexCommand({ config, logger, flags });
111
112
  });
112
113
  });
114
+ rag.command("install-hooks")
115
+ .description("Install the post-merge git hook so the RAG index auto-refreshes after pulls/merges (KJC-TSK-0455)")
116
+ .option("--json", "Output the result as JSON")
117
+ .action(async (flags) => {
118
+ await withConfig(pkgVersion, "rag-install-hooks", flags, async ({ config, logger }) => {
119
+ await ragInstallHooksCommand({ config, logger, flags });
120
+ });
121
+ });
113
122
  rag.command("query <text>")
114
123
  .description("Run a semantic query against the indexed RAG corpus")
115
124
  .option("--scope <scope>", "plans | code | onboarding | all (default: all)", "all")
@@ -136,6 +145,20 @@ export function registerMeta(program, { pkgVersion }) {
136
145
  await ragQueryCommand({ text, config, logger, flags: { ...flags, mode: ragMode } });
137
146
  });
138
147
  });
148
+ // KJC-TSK-0483 — `kj rag eval` runs the golden-query harness and reports
149
+ // recall@5/recall@10/MRR. `--min-recall` turns it into a CI gate.
150
+ rag.command("eval")
151
+ .description("Score the retriever against a golden query set (recall@k + MRR)")
152
+ .option("--golden <path>", "Path to golden queries JSON (default: tests/rag/golden-queries.json)")
153
+ .option("--top-k <n>", "Number of hits to retrieve per query (default: 10)", "10")
154
+ .option("--min-recall <n>", "Fail (exit 1) if aggregated recall@5 falls below this threshold")
155
+ .option("--project <slug>", "Filter by project slug. Pass 'all' to query across every indexed project.")
156
+ .option("--json", "Emit the full report as JSON")
157
+ .action(async (flags) => {
158
+ await withConfig(pkgVersion, "rag-eval", flags, async ({ config, logger }) => {
159
+ await ragEvalCommand({ config, logger, flags });
160
+ });
161
+ });
139
162
 
140
163
  // KJC-TSK-0441 — `kj watch [start|stop|status]` for live RAG re-index.
141
164
  const watch = program.command("watch").description("Live RAG re-index daemon");
@@ -110,6 +110,7 @@ export function registerPipeline(program, { pkgVersion }) {
110
110
  .option("--skip-role <role...>", "Force one or more roles OFF regardless of triage (e.g. --skip-role tester security)")
111
111
  .option("--force-role <role...>", "Force one or more roles ON regardless of triage")
112
112
  .option("--skip-spec-review", "Bypass the spec-reviewer pre-pipeline audit")
113
+ .option("--no-rag-update", "Skip the pre-run RAG drift check (KJC-TSK-0455). Same as config.rag.autoUpdate.onRun=false.")
113
114
  .option("--dry-run", "Show what would be executed without running anything")
114
115
  .option("--json", "Output JSON only (no styled display)")
115
116
  .option("-q, --quiet", "Show only stage status lines, suppress raw agent output (default)")
@@ -2,10 +2,12 @@
2
2
  // kj rag index [--project <slug>] [--with-sources]
3
3
  // kj rag query <text> [--scope plans|code|onboarding|all] [--top-k N] [--json]
4
4
  // Closes the v2.22.0 RAG MVP end-to-end from the terminal.
5
- import { openVecStore, countChunks, projectSlug } from "../rag/vec-store.js";
5
+ import { openVecStore, countChunks, projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "../rag/vec-store.js";
6
6
  import { makeEmbedder } from "../rag/embedders/factory.js";
7
- import { indexProject } from "../rag/indexer.js";
7
+ import { indexProject, indexProjectDelta } from "../rag/indexer.js";
8
8
  import { query } from "../rag/retriever.js";
9
+ import { installPostMergeHook } from "../rag/auto-update.js";
10
+ import { loadGoldenQueries, runEval } from "../rag/eval.js";
9
11
  import { getKarajanHome } from "../utils/paths.js";
10
12
 
11
13
  function openDb(config) {
@@ -16,15 +18,39 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
16
18
  const projectDir = config?.projectDir || process.cwd();
17
19
  const db = openDb(config);
18
20
  try {
19
- const totals = await indexProject(projectDir, {
20
- db, embedder: makeEmbedder(config),
21
- karajanHome: getKarajanHome(), logger,
22
- withSources: Boolean(flags.withSources),
23
- });
21
+ const slug = projectSlug(projectDir);
22
+ const embedder = makeEmbedder(config);
23
+ const sinceFlag = flags.since;
24
+ // KJC-TSK-0455 — `--since auto` resolves to the last commit we indexed;
25
+ // an explicit ref is honoured as-is. Without a baseline (first-time
26
+ // index) we fall back to a full reindex with a friendly warning so the
27
+ // hook in PR2 stays a no-op-friendly entrypoint.
28
+ let since = null;
29
+ if (sinceFlag) {
30
+ since = sinceFlag === "auto" ? getLastIndexedCommit(db, slug) : sinceFlag;
31
+ if (!since) logger.warn?.("[rag] --since auto: no previous index recorded; running full index");
32
+ }
33
+ let totals;
34
+ if (since) {
35
+ try {
36
+ totals = await indexProjectDelta(projectDir, { db, embedder, since, logger });
37
+ } catch (err) {
38
+ logger.warn?.(`[rag] delta index failed (${err.message}); falling back to full index`);
39
+ totals = null;
40
+ }
41
+ }
42
+ if (!totals) {
43
+ totals = await indexProject(projectDir, {
44
+ db, embedder,
45
+ karajanHome: getKarajanHome(), logger,
46
+ withSources: Boolean(flags.withSources),
47
+ });
48
+ }
49
+ if (totals.head) setLastIndexedCommit(db, slug, totals.head);
24
50
  if (flags.json) {
25
51
  process.stdout.write(`${JSON.stringify(totals)}\n`);
26
52
  } else {
27
- logger.info(`[rag] indexed ${totals.indexed} chunk(s) across ${totals.files} file(s) (${totals.failed} failed)`);
53
+ logger.info(`[rag] indexed ${totals.indexed} chunk(s) across ${totals.files} file(s) (${totals.failed} failed${totals.deleted ? `, ${totals.deleted} chunks deleted` : ""})`);
28
54
  }
29
55
  return totals;
30
56
  } finally {
@@ -32,6 +58,18 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
32
58
  }
33
59
  }
34
60
 
61
+ // KJC-TSK-0455 — `kj rag install-hooks` wires the post-merge hook so the
62
+ // local index keeps up after `git pull` / merges. Opt-in to keep us from
63
+ // silently writing under `.git/hooks/`; pre-run drift check covers users
64
+ // who never run it.
65
+ export async function ragInstallHooksCommand({ config, logger, flags = {} }) {
66
+ const projectDir = config?.projectDir || process.cwd();
67
+ const res = installPostMergeHook({ projectDir, logger });
68
+ if (flags.json) process.stdout.write(`${JSON.stringify(res)}\n`);
69
+ else if (res.installed) logger.info(`[rag] installed post-merge hook at ${res.target}`);
70
+ return res;
71
+ }
72
+
35
73
  export async function ragQueryCommand({ text, config, logger, flags = {} }) {
36
74
  if (!text) throw new Error("kj rag query: text argument required");
37
75
  const db = openDb(config);
@@ -75,3 +113,39 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
75
113
  db.close();
76
114
  }
77
115
  }
116
+
117
+ // KJC-TSK-0483 — `kj rag eval`. Runs the retriever against a golden query
118
+ // set and reports recall@5, recall@10 and MRR per query and aggregated.
119
+ // Exit code != 0 when `--min-recall` is set and the aggregated recall@5
120
+ // falls below the threshold — lets CI fail noisily on retrieval regressions.
121
+ const DEFAULT_GOLDEN = "tests/rag/golden-queries.json";
122
+ export async function ragEvalCommand({ config, logger, flags = {} }) {
123
+ const db = openDb(config);
124
+ try {
125
+ const goldenPath = flags.golden || DEFAULT_GOLDEN;
126
+ const queries = loadGoldenQueries(goldenPath);
127
+ const topK = Math.max(1, Number(flags.topK) || 10);
128
+ const detected = projectSlug(config?.projectDir || process.cwd());
129
+ const project = flags.project === "all" ? null : (flags.project || detected || null);
130
+ const embedder = makeEmbedder(config);
131
+ const runQuery = async (text, k) => query(db, embedder, text, { topK: k, scope: "all", project });
132
+ const report = await runEval(queries, runQuery, { topK, ks: [5, 10] });
133
+ if (flags.json) {
134
+ process.stdout.write(`${JSON.stringify(report)}\n`);
135
+ } else {
136
+ logger.info(`[rag-eval] ${report.aggregate.n} queries · recall@5=${report.aggregate.recall["@5"].toFixed(3)} · recall@10=${report.aggregate.recall["@10"].toFixed(3)} · MRR=${report.aggregate.mrr.toFixed(3)}`);
137
+ for (const r of report.results) {
138
+ const rank = r.firstRank == null ? "—" : `#${r.firstRank}`;
139
+ logger.info(` [${rank}] ${r.query}`);
140
+ }
141
+ }
142
+ const minRecall = Number(flags.minRecall);
143
+ if (Number.isFinite(minRecall) && report.aggregate.recall["@5"] < minRecall) {
144
+ logger.error?.(`[rag-eval] recall@5=${report.aggregate.recall["@5"].toFixed(3)} below --min-recall=${minRecall}`);
145
+ process.exitCode = 1;
146
+ }
147
+ return report;
148
+ } finally {
149
+ db.close();
150
+ }
151
+ }
@@ -12,6 +12,7 @@ import { resolveRole } from "../config.js";
12
12
  import { parseCardId } from "../planning-game/adapter.js";
13
13
  import { confirmCwd } from "../utils/cwd-confirm.js";
14
14
  import { runSpecReview } from "../spec-review/run-spec-review.js";
15
+ import { maybeAutoUpdate } from "../rag/auto-update.js";
15
16
 
16
17
  function createCliAskQuestion(opts = {}) {
17
18
  const { sessionId = null } = opts;
@@ -84,6 +85,13 @@ export async function runCommandHandler({ task, config, logger, flags }) {
84
85
  }
85
86
  }
86
87
 
88
+ // KJC-TSK-0455 — Pre-run drift check. Default ON; opt out with
89
+ // --no-rag-update (flag) or config.rag.autoUpdate.onRun=false. If the
90
+ // local index is behind HEAD we re-embed the delta before the agents
91
+ // start reading from the RAG store; on any failure we log and move on
92
+ // so a broken vector store never blocks a run.
93
+ await maybeAutoUpdate({ projectDir, config, logger, flags });
94
+
87
95
  // Best-effort session cleanup before starting
88
96
  try {
89
97
  const { cleanupExpiredSessions } = await import("../session-cleanup.js");
@@ -189,6 +189,13 @@ const DEFAULTS = {
189
189
  devtools_mcp: false,
190
190
  thresholds: { lcp: 2500, cls: 0.1, inp: 200 }
191
191
  },
192
+ rag: {
193
+ // KJC-TSK-0455 — Auto-refresh the local RAG index. `onRun` runs a
194
+ // delta re-index before every `kj run`; `onCommit` is consulted by
195
+ // `kj rag install-hooks` to decide whether to install the post-merge
196
+ // hook. Both default ON; flip to false to opt out without uninstalling.
197
+ autoUpdate: { onCommit: true, onRun: true }
198
+ },
192
199
  telemetry: true,
193
200
  guards: {
194
201
  output: {
@@ -0,0 +1,94 @@
1
+ // KJC-PCS-0052 PR-B.2.3 — Go source chunker (tree-sitter + regex fallback).
2
+ // Mismo contrato síncrono que chunk-rust: AST si el caller awaited
3
+ // `prepareGo()` antes, regex si no. Top-level: func / method / type.
4
+ //
5
+ // `method_declaration` (con receiver) y `function_declaration` se tratan
6
+ // igual — symbol es el nombre, sin componer "Recv.Method" (coherente con
7
+ // Python: del scope se ocupa la búsqueda semántica, no el chunk).
8
+ // `type_declaration` contiene type_spec(s); usamos el primero como symbol.
9
+
10
+ import { getLoaded, loadLanguage } from "./tree-sitter-loader.js";
11
+
12
+ const DEFAULT_LIMIT = 800;
13
+ const DEFAULT_OVERLAP = 100;
14
+ const NAMED_KINDS = new Set([
15
+ "function_declaration",
16
+ "method_declaration",
17
+ ]);
18
+ const TOP_RE = /^(?:func(?:\s+\([^)]+\))?|type)\s+([A-Za-z_]\w*)/gm;
19
+
20
+ export async function prepareGo() {
21
+ return loadLanguage("go");
22
+ }
23
+
24
+ function windowText(text, limit, overlap) {
25
+ if (text.length <= limit) return [text];
26
+ const step = Math.max(1, limit - overlap);
27
+ const out = [];
28
+ for (let i = 0; i < text.length; i += step) {
29
+ out.push(text.slice(i, i + limit));
30
+ if (i + limit >= text.length) break;
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function symbolForType(node) {
36
+ for (const spec of node.namedChildren) {
37
+ if (spec.type === "type_spec" || spec.type === "type_alias") {
38
+ const nameNode = spec.childForFieldName("name");
39
+ if (nameNode) return nameNode.text;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function marksViaAST(text) {
46
+ const entry = getLoaded("go");
47
+ if (!entry) return null;
48
+ let tree;
49
+ try {
50
+ tree = entry.parser.parse(text);
51
+ } catch {
52
+ return null;
53
+ }
54
+ const marks = [];
55
+ for (const child of tree.rootNode.namedChildren) {
56
+ let symbol = null;
57
+ if (NAMED_KINDS.has(child.type)) {
58
+ const nameNode = child.childForFieldName("name");
59
+ if (nameNode) symbol = nameNode.text;
60
+ } else if (child.type === "type_declaration") {
61
+ symbol = symbolForType(child);
62
+ }
63
+ if (symbol) marks.push({ idx: child.startIndex, symbol });
64
+ }
65
+ return marks;
66
+ }
67
+
68
+ function marksViaRegex(text) {
69
+ const marks = [];
70
+ TOP_RE.lastIndex = 0;
71
+ let m;
72
+ while ((m = TOP_RE.exec(text)) !== null) marks.push({ idx: m.index, symbol: m[1] });
73
+ return marks;
74
+ }
75
+
76
+ export function chunkGo(text, { path = "<src>", limit = DEFAULT_LIMIT, overlap = DEFAULT_OVERLAP } = {}) {
77
+ const marks = marksViaAST(text) ?? marksViaRegex(text);
78
+ if (marks.length === 0) {
79
+ return windowText(text, limit, overlap).map((piece) => ({
80
+ text: piece,
81
+ metadata: { source: path, kind: "code", symbol: null, language: "go" },
82
+ }));
83
+ }
84
+ const out = [];
85
+ for (let i = 0; i < marks.length; i += 1) {
86
+ const start = marks[i].idx;
87
+ const end = i + 1 < marks.length ? marks[i + 1].idx : text.length;
88
+ const body = text.slice(start, end);
89
+ for (const piece of windowText(body, limit, overlap)) {
90
+ out.push({ text: piece, metadata: { source: path, kind: "code", symbol: marks[i].symbol, language: "go" } });
91
+ }
92
+ }
93
+ return out;
94
+ }
@@ -0,0 +1,90 @@
1
+ // KJC-PCS-0052 PR-B.2.5 — Java chunker (tree-sitter + regex fallback).
2
+ // Walker 2 niveles: top-level emite class/interface/enum/record; segundo pase
3
+ // desciende al body por method_declaration / constructor_declaration. Symbol
4
+ // sin prefijar (coherente con chunk-go), el RAG ya lleva contexto del fichero.
5
+
6
+ import { getLoaded, loadLanguage } from "./tree-sitter-loader.js";
7
+
8
+ const DEFAULT_LIMIT = 800;
9
+ const DEFAULT_OVERLAP = 100;
10
+ const CONTAINER_KINDS = new Set([
11
+ "class_declaration",
12
+ "interface_declaration",
13
+ "enum_declaration",
14
+ "record_declaration",
15
+ ]);
16
+ const MEMBER_KINDS = new Set([
17
+ "method_declaration",
18
+ "constructor_declaration",
19
+ ]);
20
+ const TOP_RE = /^[ \t]*(?:public\s+|private\s+|protected\s+|static\s+|final\s+|abstract\s+|sealed\s+|non-sealed\s+)*(class|interface|enum|record)\s+([A-Za-z_]\w*)/gm;
21
+
22
+ export async function prepareJava() {
23
+ return loadLanguage("java");
24
+ }
25
+
26
+ function windowText(text, limit, overlap) {
27
+ if (text.length <= limit) return [text];
28
+ const step = Math.max(1, limit - overlap);
29
+ const out = [];
30
+ for (let i = 0; i < text.length; i += step) {
31
+ out.push(text.slice(i, i + limit));
32
+ if (i + limit >= text.length) break;
33
+ }
34
+ return out;
35
+ }
36
+
37
+ function nameOf(node) {
38
+ const n = node.childForFieldName("name");
39
+ return n ? n.text : null;
40
+ }
41
+
42
+ function marksViaAST(text) {
43
+ const entry = getLoaded("java");
44
+ if (!entry) return null;
45
+ let tree;
46
+ try { tree = entry.parser.parse(text); } catch { return null; }
47
+ const marks = [];
48
+ for (const child of tree.rootNode.namedChildren) {
49
+ if (!CONTAINER_KINDS.has(child.type)) continue;
50
+ const s = nameOf(child);
51
+ if (s) marks.push({ idx: child.startIndex, symbol: s });
52
+ const body = child.childForFieldName("body");
53
+ if (!body) continue;
54
+ for (const m of body.namedChildren) {
55
+ if (!MEMBER_KINDS.has(m.type)) continue;
56
+ const ms = nameOf(m);
57
+ if (ms) marks.push({ idx: m.startIndex, symbol: ms });
58
+ }
59
+ }
60
+ marks.sort((a, b) => a.idx - b.idx);
61
+ return marks;
62
+ }
63
+
64
+ function marksViaRegex(text) {
65
+ const marks = [];
66
+ TOP_RE.lastIndex = 0;
67
+ let m;
68
+ while ((m = TOP_RE.exec(text)) !== null) marks.push({ idx: m.index, symbol: m[2] });
69
+ return marks;
70
+ }
71
+
72
+ export function chunkJava(text, { path = "<src>", limit = DEFAULT_LIMIT, overlap = DEFAULT_OVERLAP } = {}) {
73
+ const marks = marksViaAST(text) ?? marksViaRegex(text);
74
+ if (marks.length === 0) {
75
+ return windowText(text, limit, overlap).map((piece) => ({
76
+ text: piece,
77
+ metadata: { source: path, kind: "code", symbol: null, language: "java" },
78
+ }));
79
+ }
80
+ const out = [];
81
+ for (let i = 0; i < marks.length; i += 1) {
82
+ const start = marks[i].idx;
83
+ const end = i + 1 < marks.length ? marks[i + 1].idx : text.length;
84
+ const body = text.slice(start, end);
85
+ for (const piece of windowText(body, limit, overlap)) {
86
+ out.push({ text: piece, metadata: { source: path, kind: "code", symbol: marks[i].symbol, language: "java" } });
87
+ }
88
+ }
89
+ return out;
90
+ }
@@ -0,0 +1,88 @@
1
+ // KJC-PCS-0052 PR-B.2.1 — Python source chunker (tree-sitter + regex fallback).
2
+ // Top-level `def`/`async def`/`class` siguen siendo la frontera de chunk, pero
3
+ // ahora con dos caminos:
4
+ // 1. AST (web-tree-sitter): si el caller hizo `await preparePython()` antes
5
+ // del indexado, leemos el árbol y capturamos solo nodos cuyo `parent.type`
6
+ // es `module` — eso excluye métodos anidados sin depender de la
7
+ // indentación visual y captura decoradores (`decorated_definition`)
8
+ // apuntando al def/class real subyacente.
9
+ // 2. Regex: fallback síncrono para entornos sin tree-sitter listo (tests
10
+ // legacy, primera invocación antes de `preparePython`). Misma semántica
11
+ // que PR-B.
12
+ //
13
+ // El contrato externo no cambia: `chunkPython(text, opts)` es síncrono y
14
+ // devuelve `[{ text, metadata: { source, kind, symbol, language } }]`.
15
+
16
+ import { getLoaded, loadLanguage } from "./tree-sitter-loader.js";
17
+
18
+ const DEFAULT_LIMIT = 800;
19
+ const DEFAULT_OVERLAP = 100;
20
+ const TOP_RE = /^(?:async\s+)?(?:def|class)\s+([A-Za-z_]\w*)/gm;
21
+
22
+ export async function preparePython() {
23
+ return loadLanguage("python");
24
+ }
25
+
26
+ function windowText(text, limit, overlap) {
27
+ if (text.length <= limit) return [text];
28
+ const step = Math.max(1, limit - overlap);
29
+ const out = [];
30
+ for (let i = 0; i < text.length; i += step) {
31
+ out.push(text.slice(i, i + limit));
32
+ if (i + limit >= text.length) break;
33
+ }
34
+ return out;
35
+ }
36
+
37
+ function marksViaAST(text) {
38
+ const entry = getLoaded("python");
39
+ if (!entry) return null;
40
+ let tree;
41
+ try {
42
+ tree = entry.parser.parse(text);
43
+ } catch {
44
+ return null;
45
+ }
46
+ const marks = [];
47
+ for (const child of tree.rootNode.namedChildren) {
48
+ let defNode = null;
49
+ if (child.type === "function_definition" || child.type === "class_definition") {
50
+ defNode = child;
51
+ } else if (child.type === "decorated_definition") {
52
+ defNode = child.childForFieldName("definition");
53
+ }
54
+ if (!defNode) continue;
55
+ const nameNode = defNode.childForFieldName("name");
56
+ if (!nameNode) continue;
57
+ marks.push({ idx: child.startIndex, symbol: nameNode.text });
58
+ }
59
+ return marks;
60
+ }
61
+
62
+ function marksViaRegex(text) {
63
+ const marks = [];
64
+ TOP_RE.lastIndex = 0;
65
+ let m;
66
+ while ((m = TOP_RE.exec(text)) !== null) marks.push({ idx: m.index, symbol: m[1] });
67
+ return marks;
68
+ }
69
+
70
+ export function chunkPython(text, { path = "<src>", limit = DEFAULT_LIMIT, overlap = DEFAULT_OVERLAP } = {}) {
71
+ const marks = marksViaAST(text) ?? marksViaRegex(text);
72
+ if (marks.length === 0) {
73
+ return windowText(text, limit, overlap).map((piece) => ({
74
+ text: piece,
75
+ metadata: { source: path, kind: "code", symbol: null, language: "python" },
76
+ }));
77
+ }
78
+ const out = [];
79
+ for (let i = 0; i < marks.length; i += 1) {
80
+ const start = marks[i].idx;
81
+ const end = i + 1 < marks.length ? marks[i + 1].idx : text.length;
82
+ const body = text.slice(start, end);
83
+ for (const piece of windowText(body, limit, overlap)) {
84
+ out.push({ text: piece, metadata: { source: path, kind: "code", symbol: marks[i].symbol, language: "python" } });
85
+ }
86
+ }
87
+ return out;
88
+ }