karajan-code 2.32.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.
Files changed (44) hide show
  1. package/README.md +25 -0
  2. package/package.json +4 -2
  3. package/scripts/esbuild-sea.config.mjs +10 -1
  4. package/scripts/git-hooks/post-merge +7 -0
  5. package/src/audit/audit-history-display.js +61 -0
  6. package/src/audit/audit-history.js +102 -0
  7. package/src/audit/basal-cost.js +17 -12
  8. package/src/audit/harness-scorecard.js +51 -0
  9. package/src/audit/harness-section.js +77 -0
  10. package/src/checks/harness-scorecard.js +41 -0
  11. package/src/cli/register-meta.js +28 -1
  12. package/src/cli/register-pipeline.js +1 -0
  13. package/src/commands/audit.js +73 -16
  14. package/src/commands/doctor.js +2 -0
  15. package/src/commands/rag.js +82 -8
  16. package/src/commands/run.js +8 -0
  17. package/src/config/defaults.js +7 -0
  18. package/src/lang/chunk-go.js +94 -0
  19. package/src/lang/chunk-java.js +90 -0
  20. package/src/lang/chunk-python.js +88 -0
  21. package/src/lang/chunk-rust.js +102 -0
  22. package/src/lang/go.js +14 -0
  23. package/src/lang/java.js +16 -0
  24. package/src/lang/js.js +8 -0
  25. package/src/lang/python.js +14 -0
  26. package/src/lang/registry.js +79 -0
  27. package/src/lang/rust.js +14 -0
  28. package/src/lang/tree-sitter-loader.js +49 -0
  29. package/src/onboarder/collectors/framework-multilang.js +64 -0
  30. package/src/onboarder/collectors/index.js +18 -1
  31. package/src/rag/auto-update.js +50 -0
  32. package/src/rag/chunker.js +7 -3
  33. package/src/rag/eval.js +61 -0
  34. package/src/rag/indexer.js +81 -10
  35. package/src/rag/retriever.js +57 -5
  36. package/src/rag/vec-store.js +69 -3
  37. package/src/rag/watcher.js +6 -4
  38. package/src/utils/manifest-sniff.js +82 -0
  39. package/templates/roles/onboarder.md +10 -1
  40. package/vendor/tree-sitter-grammars/README.md +21 -0
  41. package/vendor/tree-sitter-grammars/go.wasm +0 -0
  42. package/vendor/tree-sitter-grammars/java.wasm +0 -0
  43. package/vendor/tree-sitter-grammars/python.wasm +0 -0
  44. package/vendor/tree-sitter-grammars/rust.wasm +0 -0
@@ -11,6 +11,9 @@ import { withCliRunLog } from "../utils/cli-run-log.js";
11
11
  import { createCliProgressReporter } from "../utils/cli-progress.js";
12
12
  import { runAgentReadiness, formatAgentReadinessReport } from "../audit/agent-readiness.js";
13
13
  import { formatDeterministicSummary } from "../audit/deterministic-summary.js";
14
+ import { runHarnessSection, formatHarnessSection, harnessSummaryForJson } from "../audit/harness-section.js";
15
+ import { persistAuditRun, openAuditHistoryDb, getLatestPreviousRun, getRecentScores } from "../audit/audit-history.js";
16
+ import { computeHistoryDiff, formatHistoryDiff, formatTrendSparkline } from "../audit/audit-history-display.js";
14
17
 
15
18
  const execFileAsync = promisify(execFile);
16
19
 
@@ -191,20 +194,43 @@ async function buildReportHeader(projectDir, invocation) {
191
194
  return lines.join("\n");
192
195
  }
193
196
 
194
- function describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, json, deterministicOnly, yes }) {
197
+ function composeHistoryBlock({ diffMd, trendMd }) {
198
+ const parts = [];
199
+ if (diffMd) parts.push(diffMd);
200
+ if (trendMd) parts.push(trendMd);
201
+ return parts.length ? `\n\n## Harness Score History\n${parts.join("\n\n")}` : "";
202
+ }
203
+
204
+ // KJC-TSK-0473 — Resolve diff vs previous run + optional trend sparkline.
205
+ // Errors silenced: history is a nice-to-have; a broken db must not break audit.
206
+ function loadAuditHistoryContext(projectDir, currentHarness, { showTrend = false } = {}) {
207
+ let db;
208
+ try {
209
+ db = openAuditHistoryDb(projectDir);
210
+ if (!db) return { diff: null, diffMd: "", trendMd: "" };
211
+ const previous = currentHarness?.score != null ? getLatestPreviousRun(db, new Date().toISOString()) : null;
212
+ const diff = currentHarness?.score != null ? computeHistoryDiff(currentHarness, previous) : null;
213
+ const trendMd = showTrend ? formatTrendSparkline(getRecentScores(db, 10)) : "";
214
+ return { diff, diffMd: formatHistoryDiff(diff), trendMd };
215
+ } catch { return { diff: null, diffMd: "", trendMd: "" }; }
216
+ finally { try { db?.close(); } catch { /* noop */ } }
217
+ }
218
+
219
+ function describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, json, deterministicOnly, yes }) {
195
220
  const parts = ["kj audit"];
196
221
  if (task && task !== "Analyze the full codebase") parts.push(JSON.stringify(task));
197
222
  if (dimensions && dimensions !== "all") parts.push(`--dimensions=${dimensions}`);
198
223
  if (noSonar) parts.push("--no-sonar");
199
224
  if (noOsv) parts.push("--no-osv");
200
225
  if (noSemgrep) parts.push("--no-semgrep");
226
+ if (noHarness) parts.push("--no-harness");
201
227
  if (deterministicOnly) parts.push("--deterministic-only");
202
228
  if (yes) parts.push("--yes");
203
229
  if (json) parts.push("--json");
204
230
  return parts.join(" ");
205
231
  }
206
232
 
207
- export async function auditCommand({ task, config, logger, dimensions, json, agentReadiness, path: pathArg, noSonar = false, noOsv = false, noSemgrep = false, reportFile = null, deterministicOnly = false, yes = false, promptFn = null }) {
233
+ export async function auditCommand({ task, config, logger, dimensions, json, agentReadiness, path: pathArg, noSonar = false, noOsv = false, noSemgrep = false, noHarness = false, reportFile = null, deterministicOnly = false, yes = false, trend = false, promptFn = null }) {
208
234
  // --agent-readiness is a STANDALONE, deterministic, LLM-free audit
209
235
  // dimension. It scores any third-party repo for AI-agent readability
210
236
  // (llms.txt presence, page token budgets, robots allowlist, etc.).
@@ -247,6 +273,27 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
247
273
  const deterministicCtx = await role.collectDeterministic(roleInput);
248
274
  const deterministicMd = formatDeterministicSummary(deterministicCtx);
249
275
 
276
+ // KJC-TSK-0471 — Harness Scorecard (Docker one-shot, golden metric).
277
+ // Runs after the deterministic phase, before the LLM. Auto-skipped on
278
+ // --no-harness or if `runHarnessSection` surfaces an error (graceful
279
+ // degradation: a missing Docker daemon must not break `kj audit`).
280
+ let harnessSummary = null;
281
+ if (!noHarness) {
282
+ try {
283
+ harnessSummary = await runHarnessSection({
284
+ projectDir: config?.projectDir || process.cwd(),
285
+ harnessConfig: config?.audit?.harness || null,
286
+ });
287
+ if (harnessSummary?.ok && !harnessSummary.skipped) {
288
+ runLog.logText(`[audit] harness score=${harnessSummary.score}/100 grade=${harnessSummary.grade}`);
289
+ }
290
+ } catch (e) {
291
+ harnessSummary = { ok: false, error: e?.message || String(e) };
292
+ runLog.logText(`[audit] harness skipped: ${harnessSummary.error}`);
293
+ }
294
+ }
295
+ const harnessMd = formatHarnessSection(harnessSummary);
296
+
250
297
  // --json suppresses the interactive flow regardless: it would mangle
251
298
  // a script's stdout. JSON consumers always get the full audit unless
252
299
  // they explicitly pass deterministicOnly.
@@ -263,23 +310,28 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
263
310
  if (!runLlm) {
264
311
  // Deterministic-only path. Produce the report from what we already
265
312
  // have, persist it (md or json), and exit. Zero tokens spent.
313
+ const harnessJson = harnessSummaryForJson(harnessSummary);
314
+ const currentHarness = harnessSummary?.ok && !harnessSummary.skipped ? harnessSummary : null;
315
+ const history = loadAuditHistoryContext(config?.projectDir || process.cwd(), currentHarness, { showTrend: trend });
316
+ const historyMd = composeHistoryBlock(history);
266
317
  const stdoutContent = json
267
- ? JSON.stringify({ deterministic: deterministicCtx, mode: "deterministic-only" }, null, 2)
268
- : (deterministicOnly ? deterministicMd : ""); // already printed above when interactive
269
- if (json || deterministicOnly) console.log(stdoutContent);
318
+ ? JSON.stringify({ deterministic: deterministicCtx, harnessScore: harnessJson, harnessScoreDiff: history.diff, mode: "deterministic-only" }, null, 2)
319
+ : (deterministicOnly ? `${deterministicMd}\n${harnessMd}${historyMd}` : `${harnessMd}${historyMd}`);
320
+ if (json || deterministicOnly || harnessMd) console.log(stdoutContent);
270
321
 
271
322
  if (reportPath) {
272
323
  let payload;
273
324
  if (reportPath.endsWith(".json")) {
274
- payload = JSON.stringify({ deterministic: deterministicCtx, mode: "deterministic-only" }, null, 2);
325
+ payload = JSON.stringify({ deterministic: deterministicCtx, harnessScore: harnessJson, harnessScoreDiff: history.diff, mode: "deterministic-only" }, null, 2);
275
326
  } else {
276
- const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, json, deterministicOnly, yes }));
277
- payload = header + deterministicMd + "\n";
327
+ const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, json, deterministicOnly, yes }));
328
+ payload = `${header}${deterministicMd}\n${harnessMd}${historyMd}`;
278
329
  }
279
330
  await fs.writeFile(reportPath, payload, "utf8");
280
331
  runLog.logText(`[audit] report written (deterministic-only) → ${reportPath}`);
281
332
  logger.info(`Audit report written: ${reportPath}`);
282
333
  }
334
+ persistAuditRun(config?.projectDir || process.cwd(), { runId: new Date().toISOString(), timestamp: new Date().toISOString(), gitSha: null, harness: harnessSummary?.ok && !harnessSummary.skipped ? harnessSummary : null, rawReportPath: reportPath });
283
335
  logger.info("Audit completed (deterministic-only).");
284
336
  return { ok: true, mode: "deterministic-only", reportPath: reportPath || undefined };
285
337
  }
@@ -306,18 +358,22 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
306
358
  runLog.logText(`[audit] usage tokens=${usage.total_tokens} cost=$${(usage.cost_usd ?? 0).toFixed(4)} duration=${(usage.durationMs / 1000).toFixed(1)}s`);
307
359
  }
308
360
 
361
+ const harnessJson = harnessSummaryForJson(harnessSummary);
362
+ const currentHarness = harnessSummary?.ok && !harnessSummary.skipped ? harnessSummary : null;
363
+ const history = loadAuditHistoryContext(config?.projectDir || process.cwd(), currentHarness, { showTrend: trend });
364
+ const historyMd = composeHistoryBlock(history);
309
365
  let stdoutContent;
310
366
  if (json) {
311
367
  const jsonPayload = parsed
312
- ? { ...parsed, usage: usage || undefined }
313
- : { ...roleResult, usage: usage || undefined };
368
+ ? { ...parsed, harnessScore: harnessJson, harnessScoreDiff: history.diff, usage: usage || undefined }
369
+ : { ...roleResult, harnessScore: harnessJson, harnessScoreDiff: history.diff, usage: usage || undefined };
314
370
  stdoutContent = JSON.stringify(jsonPayload, null, 2);
315
371
  } else if (parsed?.summary?.overallHealth) {
316
- stdoutContent = formatAudit(parsed, usage);
372
+ stdoutContent = `${formatAudit(parsed, usage)}\n${harnessMd}${historyMd}`;
317
373
  } else if (parsed?.raw) {
318
- stdoutContent = parsed.raw + (usage ? `\n\n${formatUsageSummary(usage) || ""}` : "");
374
+ stdoutContent = parsed.raw + (usage ? `\n\n${formatUsageSummary(usage) || ""}` : "") + `\n${harnessMd}${historyMd}`;
319
375
  } else {
320
- stdoutContent = roleResult.summary || "Audit complete.";
376
+ stdoutContent = `${roleResult.summary || "Audit complete."}\n${harnessMd}${historyMd}`;
321
377
  }
322
378
  console.log(stdoutContent);
323
379
 
@@ -325,11 +381,11 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
325
381
  let payload;
326
382
  if (reportPath.endsWith(".json")) {
327
383
  const jsonPayload = parsed
328
- ? { ...parsed, usage: usage || undefined }
329
- : { ...roleResult, usage: usage || undefined };
384
+ ? { ...parsed, harnessScore: harnessJson, harnessScoreDiff: history.diff, usage: usage || undefined }
385
+ : { ...roleResult, harnessScore: harnessJson, harnessScoreDiff: history.diff, usage: usage || undefined };
330
386
  payload = JSON.stringify(jsonPayload, null, 2);
331
387
  } else {
332
- const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, json, deterministicOnly, yes }));
388
+ const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, json, deterministicOnly, yes }));
333
389
  payload = header + stdoutContent + "\n";
334
390
  }
335
391
  await fs.writeFile(reportPath, payload, "utf8");
@@ -337,6 +393,7 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
337
393
  logger.info(`Audit report written: ${reportPath}`);
338
394
  }
339
395
 
396
+ persistAuditRun(config?.projectDir || process.cwd(), { runId: new Date().toISOString(), timestamp: new Date().toISOString(), gitSha: null, harness: harnessSummary?.ok && !harnessSummary.skipped ? harnessSummary : null, rawReportPath: reportPath });
340
397
  logger.info("Audit completed.");
341
398
  return { ok: true, reportPath: reportPath || undefined };
342
399
  });
@@ -19,6 +19,7 @@ import { getConfigFileChecks } from "../checks/config-files.js";
19
19
  import { getBinaryChecks } from "../checks/binaries.js";
20
20
  import { getSonarChecks } from "../checks/sonar.js";
21
21
  import { getOllamaChecks } from "../checks/ollama.js";
22
+ import { getHarnessScorecardChecks } from "../checks/harness-scorecard.js";
22
23
  import { getCiChecks } from "../checks/ci.js";
23
24
  import { getRtkChecks } from "../checks/rtk.js";
24
25
  import { getNodeChecks } from "../checks/node.js";
@@ -54,6 +55,7 @@ function buildChecks(config, { projectOnly = false } = {}) {
54
55
  ...getBinaryChecks(),
55
56
  ...getSonarChecks(),
56
57
  ...getOllamaChecks(),
58
+ ...getHarnessScorecardChecks(),
57
59
  ...getPortChecks(),
58
60
  ...getTokenChecks(config),
59
61
  ...getMcpHealthChecks(),
@@ -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
+ }