karajan-code 2.33.0 → 3.0.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 (53) hide show
  1. package/README.md +136 -36
  2. package/bin/kj-rag-mcp.js +75 -0
  3. package/docs/README.es.md +4 -4
  4. package/package.json +22 -16
  5. package/packages/hu-board/package.json +5 -2
  6. package/scripts/git-hooks/post-merge +7 -0
  7. package/src/audit/basal-cost.js +17 -12
  8. package/src/audit/deterministic-summary.js +20 -0
  9. package/src/audit/injection-findings.js +105 -0
  10. package/src/checks/hardware.js +150 -0
  11. package/src/checks/node.js +9 -10
  12. package/src/cli/register-meta.js +57 -1
  13. package/src/cli/register-pipeline.js +1 -0
  14. package/src/commands/doctor.js +2 -0
  15. package/src/commands/init.js +41 -1
  16. package/src/commands/rag-mcp.js +18 -0
  17. package/src/commands/rag.js +82 -8
  18. package/src/commands/run.js +8 -0
  19. package/src/commands/telemetry.js +70 -0
  20. package/src/config/defaults.js +11 -1
  21. package/src/lang/chunk-go.js +94 -0
  22. package/src/lang/chunk-java.js +90 -0
  23. package/src/lang/chunk-python.js +88 -0
  24. package/src/lang/chunk-rust.js +102 -0
  25. package/src/lang/go.js +14 -0
  26. package/src/lang/java.js +16 -0
  27. package/src/lang/js.js +8 -0
  28. package/src/lang/python.js +14 -0
  29. package/src/lang/registry.js +79 -0
  30. package/src/lang/rust.js +14 -0
  31. package/src/lang/tree-sitter-loader.js +49 -0
  32. package/src/onboarder/collectors/framework-multilang.js +64 -0
  33. package/src/onboarder/collectors/index.js +18 -1
  34. package/src/orchestrator/stages/coder-stage.js +3 -0
  35. package/src/orchestrator/stages/reviewer-stage.js +11 -0
  36. package/src/rag/auto-update.js +50 -0
  37. package/src/rag/chunker.js +7 -3
  38. package/src/rag/eval.js +61 -0
  39. package/src/rag/indexer.js +81 -10
  40. package/src/rag/retriever.js +57 -5
  41. package/src/rag/vec-store.js +69 -3
  42. package/src/rag/watcher.js +6 -4
  43. package/src/roles/audit-role.js +9 -1
  44. package/src/sonar/scanner.js +51 -16
  45. package/src/utils/manifest-sniff.js +82 -0
  46. package/src/utils/quota-simulator.js +56 -0
  47. package/src/utils/telemetry.js +34 -12
  48. package/templates/roles/onboarder.md +10 -1
  49. package/vendor/tree-sitter-grammars/README.md +21 -0
  50. package/vendor/tree-sitter-grammars/go.wasm +0 -0
  51. package/vendor/tree-sitter-grammars/java.wasm +0 -0
  52. package/vendor/tree-sitter-grammars/python.wasm +0 -0
  53. package/vendor/tree-sitter-grammars/rust.wasm +0 -0
@@ -0,0 +1,70 @@
1
+ import { TELEMETRY_ENDPOINT, buildTelemetryPayload, isTelemetryEnabled } from "../utils/telemetry.js";
2
+
3
+ /**
4
+ * Render the exact payload that would be POSTed for every event type.
5
+ *
6
+ * Read-only by design: this never opens a socket. It exists so users
7
+ * can inspect what telemetry sends before (or after) opting in,
8
+ * without grepping the source. The contract here MUST stay in sync
9
+ * with the producers in src/cli/_shared.js and the pipeline emitter
10
+ * in src/orchestrator/flow-runner.js — if a new field appears there,
11
+ * mirror it in the example below.
12
+ */
13
+ function buildPreview(version) {
14
+ const v = version || "x.y.z";
15
+ return {
16
+ endpoint: TELEMETRY_ENDPOINT,
17
+ events: [
18
+ buildTelemetryPayload("install", { version: v }),
19
+ buildTelemetryPayload("cli_command", { version: v, command: "run" }),
20
+ buildTelemetryPayload("pipeline_complete", {
21
+ version: v,
22
+ mode: "standard",
23
+ agent: "claude",
24
+ duration_s: 42,
25
+ success: true,
26
+ taskType: "feature"
27
+ })
28
+ ]
29
+ };
30
+ }
31
+
32
+ export async function telemetryPreviewCommand({ config, logger, version, json }) {
33
+ const preview = buildPreview(version);
34
+ if (json) {
35
+ logger.info(JSON.stringify(preview, null, 2));
36
+ return preview;
37
+ }
38
+ const enabled = isTelemetryEnabled(config);
39
+ logger.info(`Telemetry endpoint: ${preview.endpoint}`);
40
+ logger.info(`Telemetry enabled: ${enabled ? "yes" : "no"}`);
41
+ logger.info("");
42
+ logger.info("Exact payloads that would be POSTed (one per event type):");
43
+ logger.info("");
44
+ for (const payload of preview.events) {
45
+ logger.info(` [${payload.event}]`);
46
+ logger.info(` ${JSON.stringify(payload)}`);
47
+ logger.info("");
48
+ }
49
+ logger.info("No data was sent — `kj telemetry preview` never opens a socket.");
50
+ return preview;
51
+ }
52
+
53
+ export async function telemetryStatusCommand({ config, logger, json }) {
54
+ const enabled = isTelemetryEnabled(config);
55
+ const report = {
56
+ enabled,
57
+ endpoint: TELEMETRY_ENDPOINT,
58
+ flipHint: enabled
59
+ ? "Set `telemetry: false` in ~/.karajan/kj.config.yml (or re-run `kj init`) to turn it off."
60
+ : "Set `telemetry: true` in ~/.karajan/kj.config.yml (or re-run `kj init`) to turn it on."
61
+ };
62
+ if (json) {
63
+ logger.info(JSON.stringify(report, null, 2));
64
+ return report;
65
+ }
66
+ logger.info(`Telemetry: ${enabled ? "enabled" : "disabled"}`);
67
+ logger.info(`Endpoint: ${report.endpoint}`);
68
+ logger.info(report.flipHint);
69
+ return report;
70
+ }
@@ -189,7 +189,17 @@ const DEFAULTS = {
189
189
  devtools_mcp: false,
190
190
  thresholds: { lcp: 2500, cls: 0.1, inp: 200 }
191
191
  },
192
- telemetry: true,
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
+ },
199
+ // Opt-in. Set by the `kj init` wizard (explicit consent prompt in the OS
200
+ // locale). When the key is absent or false, no events are sent. See
201
+ // src/utils/telemetry.js and the Privacy & Telemetry section of README.md.
202
+ telemetry: false,
193
203
  guards: {
194
204
  output: {
195
205
  enabled: true,
@@ -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
+ }
@@ -0,0 +1,102 @@
1
+ // KJC-PCS-0052 PR-B.2.2 — Rust source chunker (tree-sitter + regex fallback).
2
+ // Mismo contrato síncrono que chunk-python: AST si el caller awaited
3
+ // `prepareRust()` antes, regex si no. Top-level items (fn / struct / enum /
4
+ // trait / impl / mod) son la frontera de chunk.
5
+ //
6
+ // El nodo `impl_item` no tiene field `name`; usamos el campo `type` para
7
+ // que metadata.symbol apunte al tipo implementado (útil para grep en RAG).
8
+
9
+ import { getLoaded, loadLanguage } from "./tree-sitter-loader.js";
10
+
11
+ const DEFAULT_LIMIT = 800;
12
+ const DEFAULT_OVERLAP = 100;
13
+ const NAMED_KINDS = new Set([
14
+ "function_item",
15
+ "struct_item",
16
+ "enum_item",
17
+ "trait_item",
18
+ "mod_item",
19
+ "type_item",
20
+ "union_item",
21
+ ]);
22
+ const TOP_RE = /^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+|unsafe\s+|extern\s+(?:"[^"]+"\s+)?)*(?:fn|struct|enum|trait|mod|type|union|impl)(?:\s+(?:<[^>]+>\s+)?)?([A-Za-z_][\w]*)/gm;
23
+
24
+ export async function prepareRust() {
25
+ return loadLanguage("rust");
26
+ }
27
+
28
+ function windowText(text, limit, overlap) {
29
+ if (text.length <= limit) return [text];
30
+ const step = Math.max(1, limit - overlap);
31
+ const out = [];
32
+ for (let i = 0; i < text.length; i += step) {
33
+ out.push(text.slice(i, i + limit));
34
+ if (i + limit >= text.length) break;
35
+ }
36
+ return out;
37
+ }
38
+
39
+ function symbolForImpl(node) {
40
+ const typeNode = node.childForFieldName("type");
41
+ return typeNode ? typeNode.text : null;
42
+ }
43
+
44
+ function marksViaAST(text) {
45
+ const entry = getLoaded("rust");
46
+ if (!entry) return null;
47
+ let tree;
48
+ try {
49
+ tree = entry.parser.parse(text);
50
+ } catch {
51
+ return null;
52
+ }
53
+ const marks = [];
54
+ let pendingAttrStart = null;
55
+ for (const child of tree.rootNode.namedChildren) {
56
+ if (child.type === "attribute_item" || child.type === "inner_attribute_item") {
57
+ if (pendingAttrStart === null) pendingAttrStart = child.startIndex;
58
+ continue;
59
+ }
60
+ let symbol = null;
61
+ if (NAMED_KINDS.has(child.type)) {
62
+ const nameNode = child.childForFieldName("name");
63
+ if (nameNode) symbol = nameNode.text;
64
+ } else if (child.type === "impl_item") {
65
+ symbol = symbolForImpl(child);
66
+ }
67
+ if (symbol) {
68
+ const idx = pendingAttrStart ?? child.startIndex;
69
+ marks.push({ idx, symbol });
70
+ }
71
+ pendingAttrStart = null;
72
+ }
73
+ return marks;
74
+ }
75
+
76
+ function marksViaRegex(text) {
77
+ const marks = [];
78
+ TOP_RE.lastIndex = 0;
79
+ let m;
80
+ while ((m = TOP_RE.exec(text)) !== null) marks.push({ idx: m.index, symbol: m[1] });
81
+ return marks;
82
+ }
83
+
84
+ export function chunkRust(text, { path = "<src>", limit = DEFAULT_LIMIT, overlap = DEFAULT_OVERLAP } = {}) {
85
+ const marks = marksViaAST(text) ?? marksViaRegex(text);
86
+ if (marks.length === 0) {
87
+ return windowText(text, limit, overlap).map((piece) => ({
88
+ text: piece,
89
+ metadata: { source: path, kind: "code", symbol: null, language: "rust" },
90
+ }));
91
+ }
92
+ const out = [];
93
+ for (let i = 0; i < marks.length; i += 1) {
94
+ const start = marks[i].idx;
95
+ const end = i + 1 < marks.length ? marks[i + 1].idx : text.length;
96
+ const body = text.slice(start, end);
97
+ for (const piece of windowText(body, limit, overlap)) {
98
+ out.push({ text: piece, metadata: { source: path, kind: "code", symbol: marks[i].symbol, language: "rust" } });
99
+ }
100
+ }
101
+ return out;
102
+ }
package/src/lang/go.js ADDED
@@ -0,0 +1,14 @@
1
+ // KJC-PCS-0052 PR-B.2.3 — Go adapter. Detección por go.mod/go.sum; vendor/
2
+ // y bin/ no se recorren (vendor lo usa `go mod vendor`, bin son builds).
3
+ // chunk-go usa AST (tree-sitter) cuando el caller hizo `await prepareGo()`
4
+ // y cae a regex en otro caso (mismo patrón que PYTHON_ADAPTER / RUST_ADAPTER).
5
+ import { chunkGo, prepareGo } from "./chunk-go.js";
6
+
7
+ export const GO_ADAPTER = {
8
+ id: "go",
9
+ extensions: [".go"],
10
+ skipSegments: ["vendor", "bin"],
11
+ manifests: ["go.mod", "go.sum"],
12
+ chunkSource: chunkGo,
13
+ prepare: prepareGo,
14
+ };
@@ -0,0 +1,16 @@
1
+ // KJC-PCS-0052 PR-B.2.5 — Java adapter. Detección por pom.xml / build.gradle
2
+ // (Maven, Gradle Groovy y Kotlin DSL). target/ y build/ son outputs estándar
3
+ // de ambos sistemas; .gradle/ es el cache del wrapper; out/ lo usan IDEs
4
+ // (IntelliJ/Eclipse) para classes. chunk-java usa AST (tree-sitter) cuando
5
+ // el caller hizo `await prepareJava()` y cae a regex en otro caso (mismo
6
+ // patrón que GO_ADAPTER / RUST_ADAPTER / PYTHON_ADAPTER).
7
+ import { chunkJava, prepareJava } from "./chunk-java.js";
8
+
9
+ export const JAVA_ADAPTER = {
10
+ id: "java",
11
+ extensions: [".java"],
12
+ skipSegments: ["target", "build", ".gradle", "out"],
13
+ manifests: ["pom.xml", "build.gradle", "build.gradle.kts"],
14
+ chunkSource: chunkJava,
15
+ prepare: prepareJava,
16
+ };
package/src/lang/js.js ADDED
@@ -0,0 +1,8 @@
1
+ // KJC-PCS-0052 PR-A — JS/TS adapter. Codifica las constantes que vivían
2
+ // hard-coded en src/rag/indexer.js antes del registry.
3
+ export const JS_ADAPTER = {
4
+ id: "js",
5
+ extensions: [".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"],
6
+ skipSegments: ["node_modules", ".next", ".nuxt", ".svelte-kit"],
7
+ manifests: ["package.json"],
8
+ };
@@ -0,0 +1,14 @@
1
+ // KJC-PCS-0052 PR-A/PR-B — Python adapter. Detección por manifest activa
2
+ // el adapter; virtualenvs y caches se excluyen del walk. PR-B añade
3
+ // `chunkSource` regex top-level (def/async def/class) — tree-sitter real
4
+ // queda para PR-B.2 (decisión WASM vs native).
5
+ import { chunkPython, preparePython } from "./chunk-python.js";
6
+
7
+ export const PYTHON_ADAPTER = {
8
+ id: "python",
9
+ extensions: [".py"],
10
+ skipSegments: ["__pycache__", ".venv", "venv", ".tox", ".pytest_cache", ".mypy_cache", ".ruff_cache"],
11
+ manifests: ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"],
12
+ chunkSource: chunkPython,
13
+ prepare: preparePython,
14
+ };
@@ -0,0 +1,79 @@
1
+ // KJC-PCS-0052 PR-A — Language adapter registry. Antes vivían CODE_EXT_RE
2
+ // y SKIP_SEGMENTS hardcoded en indexer.js (solo JS/TS); ahora cada adapter
3
+ // declara extensiones, skipSegments y manifests. PR-B añadirá AST chunkers
4
+ // (tree-sitter); PR-C/D usan el mismo registry para audit y onboarder.
5
+ import { existsSync } from "node:fs";
6
+ import { extname, join } from "node:path";
7
+
8
+ import { GO_ADAPTER } from "./go.js";
9
+ import { JAVA_ADAPTER } from "./java.js";
10
+ import { JS_ADAPTER } from "./js.js";
11
+ import { PYTHON_ADAPTER } from "./python.js";
12
+ import { RUST_ADAPTER } from "./rust.js";
13
+
14
+ // Segmentos siempre excluidos del walk, independientes de stack: VCS,
15
+ // outputs de build, scratch de Karajan, sandbox del audit. Lo que viva
16
+ // aquí jamás se indexa.
17
+ const COMMON_SKIP_SEGMENTS = [".git", "dist", "build", "coverage", ".karajan", ".kj", "_diet"];
18
+
19
+ const REGISTRY = [JS_ADAPTER, PYTHON_ADAPTER, RUST_ADAPTER, GO_ADAPTER, JAVA_ADAPTER];
20
+
21
+ export function getAdapters() {
22
+ return REGISTRY.slice();
23
+ }
24
+
25
+ /**
26
+ * Detecta qué adapters aplican a un projectDir mirando los manifests
27
+ * declarados. Si ningún adapter coincide (proyecto sin manifest), cae
28
+ * a JS para no romper repos legacy o sandboxes de tests.
29
+ */
30
+ export function detectAdaptersForProject(projectDir) {
31
+ if (!projectDir) return [JS_ADAPTER];
32
+ const matched = REGISTRY.filter((a) => a.manifests.some((m) => existsSync(join(projectDir, m))));
33
+ return matched.length ? matched : [JS_ADAPTER];
34
+ }
35
+
36
+ /**
37
+ * Construye los matchers que usa el indexer al recorrer el árbol:
38
+ * `isCodeFile(path)` decide si la extensión cuadra con algún adapter
39
+ * activo; `shouldSkip(path)` aplica la unión de skipSegments comunes +
40
+ * los del adapter. Devuelve también `extensions` por si el caller quiere
41
+ * inspeccionarlas (p.ej. tests).
42
+ */
43
+ export function buildMatchers(adapters) {
44
+ const extSet = new Set();
45
+ const skipSet = new Set(COMMON_SKIP_SEGMENTS);
46
+ for (const a of adapters) {
47
+ for (const e of a.extensions) extSet.add(e.toLowerCase());
48
+ for (const s of a.skipSegments || []) skipSet.add(s);
49
+ }
50
+ return {
51
+ isCodeFile: (p) => extSet.has(extname(p).toLowerCase()),
52
+ shouldSkip: (p) => p.split("/").some((seg) => skipSet.has(seg)),
53
+ extensions: [...extSet],
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Unión de extensiones de TODOS los adapters registrados. Útil para
59
+ * `detectKind` en el indexer, que recibe un path suelto sin saber el
60
+ * projectDir y necesita reconocer una extensión de código aunque venga
61
+ * de un adapter no detectado en el proyecto actual.
62
+ */
63
+ export function getAllCodeExtensions() {
64
+ const out = new Set();
65
+ for (const a of REGISTRY) for (const e of a.extensions) out.add(e.toLowerCase());
66
+ return [...out];
67
+ }
68
+
69
+ /**
70
+ * KJC-PCS-0052 PR-B — devuelve el adapter cuya `extensions` cubre el
71
+ * path, o `null` si ninguno matchea. Lo consume el chunker para delegar
72
+ * a un chunker específico por lenguaje (p.ej. Python).
73
+ */
74
+ export function adapterForPath(path) {
75
+ if (!path) return null;
76
+ const ext = extname(path).toLowerCase();
77
+ for (const a of REGISTRY) if (a.extensions.includes(ext)) return a;
78
+ return null;
79
+ }
@@ -0,0 +1,14 @@
1
+ // KJC-PCS-0052 PR-B.2.2 — Rust adapter. Detección por Cargo.toml; target/
2
+ // y skip segments evitan que el indexer recorra builds. chunk-rust usa
3
+ // AST (tree-sitter) cuando el caller hizo `await prepareRust()` y cae a
4
+ // regex en otro caso (mismo patrón que PYTHON_ADAPTER).
5
+ import { chunkRust, prepareRust } from "./chunk-rust.js";
6
+
7
+ export const RUST_ADAPTER = {
8
+ id: "rust",
9
+ extensions: [".rs"],
10
+ skipSegments: ["target", ".cargo"],
11
+ manifests: ["Cargo.toml", "Cargo.lock"],
12
+ chunkSource: chunkRust,
13
+ prepare: prepareRust,
14
+ };
@@ -0,0 +1,49 @@
1
+ // KJC-PCS-0052 PR-B.2.1 — Lazy loader for web-tree-sitter parsers.
2
+ // Async init (Parser.init + Language.load) is paid once per language id
3
+ // and cached. Callers that want AST chunking must `await loadLanguage(...)`
4
+ // during pre-loop; the per-file chunker then reads the cached entry
5
+ // synchronously via `getLoaded(id)` and falls back to regex if not ready.
6
+ //
7
+ // Pure WASM (web-tree-sitter) over native bindings: no node-gyp build, no
8
+ // per-platform .node files — works inside the SEA binary because the
9
+ // grammar `.wasm` files travel with the npm package under
10
+ // `vendor/tree-sitter-grammars/` (see ../../vendor/.../README.md).
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, resolve } from "node:path";
13
+
14
+ import { Language, Parser } from "web-tree-sitter";
15
+
16
+ const HERE = dirname(fileURLToPath(import.meta.url));
17
+ const GRAMMAR_DIR = resolve(HERE, "../../vendor/tree-sitter-grammars");
18
+
19
+ const cache = new Map();
20
+ let runtimePromise = null;
21
+
22
+ async function initRuntime() {
23
+ if (!runtimePromise) runtimePromise = Parser.init();
24
+ return runtimePromise;
25
+ }
26
+
27
+ export function grammarPath(id) {
28
+ return resolve(GRAMMAR_DIR, `${id}.wasm`);
29
+ }
30
+
31
+ export async function loadLanguage(id, wasmPath = grammarPath(id)) {
32
+ if (cache.has(id)) return cache.get(id);
33
+ await initRuntime();
34
+ const language = await Language.load(wasmPath);
35
+ const parser = new Parser();
36
+ parser.setLanguage(language);
37
+ const entry = { parser, language };
38
+ cache.set(id, entry);
39
+ return entry;
40
+ }
41
+
42
+ export function getLoaded(id) {
43
+ return cache.get(id) || null;
44
+ }
45
+
46
+ export function resetForTests() {
47
+ cache.clear();
48
+ runtimePromise = null;
49
+ }