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.
- package/README.md +25 -0
- package/package.json +4 -2
- package/scripts/esbuild-sea.config.mjs +10 -1
- package/scripts/git-hooks/post-merge +7 -0
- package/src/audit/audit-history-display.js +61 -0
- package/src/audit/audit-history.js +102 -0
- package/src/audit/basal-cost.js +17 -12
- package/src/audit/harness-scorecard.js +51 -0
- package/src/audit/harness-section.js +77 -0
- package/src/checks/harness-scorecard.js +41 -0
- package/src/cli/register-meta.js +28 -1
- package/src/cli/register-pipeline.js +1 -0
- package/src/commands/audit.js +73 -16
- package/src/commands/doctor.js +2 -0
- package/src/commands/rag.js +82 -8
- package/src/commands/run.js +8 -0
- package/src/config/defaults.js +7 -0
- package/src/lang/chunk-go.js +94 -0
- package/src/lang/chunk-java.js +90 -0
- package/src/lang/chunk-python.js +88 -0
- package/src/lang/chunk-rust.js +102 -0
- package/src/lang/go.js +14 -0
- package/src/lang/java.js +16 -0
- package/src/lang/js.js +8 -0
- package/src/lang/python.js +14 -0
- package/src/lang/registry.js +79 -0
- package/src/lang/rust.js +14 -0
- package/src/lang/tree-sitter-loader.js +49 -0
- package/src/onboarder/collectors/framework-multilang.js +64 -0
- package/src/onboarder/collectors/index.js +18 -1
- package/src/rag/auto-update.js +50 -0
- package/src/rag/chunker.js +7 -3
- package/src/rag/eval.js +61 -0
- package/src/rag/indexer.js +81 -10
- package/src/rag/retriever.js +57 -5
- package/src/rag/vec-store.js +69 -3
- package/src/rag/watcher.js +6 -4
- package/src/utils/manifest-sniff.js +82 -0
- package/templates/roles/onboarder.md +10 -1
- package/vendor/tree-sitter-grammars/README.md +21 -0
- package/vendor/tree-sitter-grammars/go.wasm +0 -0
- package/vendor/tree-sitter-grammars/java.wasm +0 -0
- package/vendor/tree-sitter-grammars/python.wasm +0 -0
- package/vendor/tree-sitter-grammars/rust.wasm +0 -0
|
@@ -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
|
+
};
|
package/src/lang/java.js
ADDED
|
@@ -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
|
+
}
|
package/src/lang/rust.js
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// KJC-TSK-0477 — Detectores mínimos de framework por stack (Python/Rust/Go).
|
|
2
|
+
// Consume el sniff de manifests (PR-C) para conocer deps + presencia de
|
|
3
|
+
// archivos canónicos. Cero deps externas, fail-soft (devuelve null o lista
|
|
4
|
+
// vacía). El brief markdown se sintetiza río abajo desde estos campos.
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
const PY_FW = [
|
|
9
|
+
{ name: "django", deps: ["django"], file: "manage.py" },
|
|
10
|
+
{ name: "flask", deps: ["flask"], file: "app.py" },
|
|
11
|
+
{ name: "fastapi", deps: ["fastapi"], file: "main.py" },
|
|
12
|
+
];
|
|
13
|
+
const RUST_FW = [
|
|
14
|
+
{ name: "axum", deps: ["axum"] },
|
|
15
|
+
{ name: "actix-web", deps: ["actix-web"] },
|
|
16
|
+
{ name: "rocket", deps: ["rocket"] },
|
|
17
|
+
];
|
|
18
|
+
const GO_FW = [
|
|
19
|
+
{ name: "gin", deps: ["github.com/gin-gonic/gin"] },
|
|
20
|
+
{ name: "echo", deps: ["github.com/labstack/echo"] },
|
|
21
|
+
{ name: "fiber", deps: ["github.com/gofiber/fiber"] },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function matchFw(manifestRaw, projectDir, table) {
|
|
25
|
+
const found = [];
|
|
26
|
+
for (const fw of table) {
|
|
27
|
+
const hasDep = fw.deps.some((d) => manifestRaw?.includes(d));
|
|
28
|
+
const hasFile = fw.file ? existsSync(path.join(projectDir, fw.file)) : false;
|
|
29
|
+
if (hasDep || hasFile) found.push({ name: fw.name, viaDep: hasDep, viaFile: hasFile });
|
|
30
|
+
}
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function detectLayout(projectDir, expected) {
|
|
35
|
+
return expected.filter((d) => existsSync(path.join(projectDir, d)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Detect frameworks + canonical layout per stack. Returns one entry per
|
|
39
|
+
// detected manifest stack (skips Node — the existing JS path already covers
|
|
40
|
+
// that with detectProjectStack).
|
|
41
|
+
export function detectFrameworksMultiLang(projectDir, manifests = [], { readManifest } = {}) {
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const m of manifests) {
|
|
44
|
+
if (m.stack === "node") continue;
|
|
45
|
+
const raw = readManifest ? readManifest(m) : "";
|
|
46
|
+
if (m.stack === "python") {
|
|
47
|
+
out.push({ stack: "python", manifest: m.manifest,
|
|
48
|
+
frameworks: matchFw(raw, projectDir, PY_FW),
|
|
49
|
+
layout: detectLayout(projectDir, ["apps", "src", "tests"]) });
|
|
50
|
+
} else if (m.stack === "rust") {
|
|
51
|
+
const isBin = existsSync(path.join(projectDir, "src", "main.rs"));
|
|
52
|
+
const isLib = existsSync(path.join(projectDir, "src", "lib.rs"));
|
|
53
|
+
out.push({ stack: "rust", manifest: m.manifest,
|
|
54
|
+
crateType: isBin && isLib ? "mixed" : isBin ? "binary" : isLib ? "library" : null,
|
|
55
|
+
frameworks: matchFw(raw, projectDir, RUST_FW),
|
|
56
|
+
layout: detectLayout(projectDir, ["src", "tests", "examples", "benches"]) });
|
|
57
|
+
} else if (m.stack === "go") {
|
|
58
|
+
out.push({ stack: "go", manifest: m.manifest, modulePath: m.name || null,
|
|
59
|
+
frameworks: matchFw(raw, projectDir, GO_FW),
|
|
60
|
+
layout: detectLayout(projectDir, ["cmd", "internal", "pkg"]) });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
@@ -7,6 +7,8 @@ import { readdir } from "node:fs/promises";
|
|
|
7
7
|
import { join, relative } from "node:path";
|
|
8
8
|
|
|
9
9
|
import { detectProjectStack } from "../../utils/stack-detect.js";
|
|
10
|
+
import { sniffManifests } from "../../utils/manifest-sniff.js";
|
|
11
|
+
import { detectFrameworksMultiLang } from "./framework-multilang.js";
|
|
10
12
|
|
|
11
13
|
const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".karajan", ".next", "__pycache__"]);
|
|
12
14
|
|
|
@@ -100,16 +102,31 @@ export async function collectAdrs(projectDir) {
|
|
|
100
102
|
return found;
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
// Read a manifest file as raw text (best-effort) for framework dep matching.
|
|
106
|
+
function readManifestRaw(projectDir) {
|
|
107
|
+
return (m) => {
|
|
108
|
+
try { return readFileSync(join(projectDir, m.manifest), "utf8"); }
|
|
109
|
+
catch { return ""; }
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
// One-shot bundle: every slot is independent; missing slots → null/[].
|
|
114
|
+
// KJC-TSK-0477: manifests (multi-stack) + frameworksMultiLang (Python/Rust/Go)
|
|
115
|
+
// se añaden de forma aditiva para que el brief pueda incluir secciones
|
|
116
|
+
// condicionales sin romper consumidores existentes.
|
|
104
117
|
export async function collectAll(projectDir) {
|
|
105
|
-
const [stack, tree, adrs] = await Promise.all([
|
|
118
|
+
const [stack, tree, adrs, manifests] = await Promise.all([
|
|
106
119
|
detectProjectStack(projectDir).catch(() => null),
|
|
107
120
|
collectTree(projectDir).catch(() => []),
|
|
108
121
|
collectAdrs(projectDir).catch(() => []),
|
|
122
|
+
sniffManifests(projectDir).catch(() => []),
|
|
109
123
|
]);
|
|
124
|
+
const frameworksMultiLang = detectFrameworksMultiLang(projectDir, manifests, { readManifest: readManifestRaw(projectDir) });
|
|
110
125
|
return {
|
|
111
126
|
projectDir,
|
|
112
127
|
stack,
|
|
128
|
+
manifests,
|
|
129
|
+
frameworksMultiLang,
|
|
113
130
|
tree,
|
|
114
131
|
git: collectGitHistory(projectDir),
|
|
115
132
|
configs: collectConfigs(projectDir),
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// KJC-TSK-0455 PR2 — Pre-run drift check + post-merge hook installer.
|
|
2
|
+
// Keeps the local RAG index aligned with HEAD so retrieval never serves
|
|
3
|
+
// stale code without the user having to run `kj rag index` by hand.
|
|
4
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { execa } from "execa";
|
|
8
|
+
|
|
9
|
+
import { openVecStore, projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "./vec-store.js";
|
|
10
|
+
import { indexProjectDelta } from "./indexer.js";
|
|
11
|
+
import { makeEmbedder } from "./embedders/factory.js";
|
|
12
|
+
|
|
13
|
+
const HOOK_SRC = resolve(fileURLToPath(import.meta.url), "../../../scripts/git-hooks/post-merge");
|
|
14
|
+
|
|
15
|
+
export async function maybeAutoUpdate({ projectDir, config, logger = console, flags = {} } = {}) {
|
|
16
|
+
// commander turns `--no-rag-update` into flags.ragUpdate === false.
|
|
17
|
+
if (flags?.ragUpdate === false || config?.rag?.autoUpdate?.onRun === false) return { skipped: true };
|
|
18
|
+
if (!projectDir || !existsSync(join(projectDir, ".git"))) return { skipped: true };
|
|
19
|
+
let head;
|
|
20
|
+
try { const r = await execa("git", ["-C", projectDir, "rev-parse", "HEAD"]); head = r.stdout.trim(); }
|
|
21
|
+
catch { return { skipped: true }; }
|
|
22
|
+
const slug = projectSlug(projectDir);
|
|
23
|
+
const db = openVecStore({ dim: config?.rag?.embedder?.dim || 768 });
|
|
24
|
+
try {
|
|
25
|
+
const since = getLastIndexedCommit(db, slug);
|
|
26
|
+
if (!since || since === head) return { skipped: true, head };
|
|
27
|
+
logger.info?.(`[rag] drift detected (${since.slice(0, 7)} → ${head.slice(0, 7)}); running delta update`);
|
|
28
|
+
const totals = await indexProjectDelta(projectDir, { db, embedder: makeEmbedder(config), since, logger });
|
|
29
|
+
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
30
|
+
return { ran: true, totals };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
logger.warn?.(`[rag] auto-update failed (${err.message}); continuing without refresh`);
|
|
33
|
+
return { failed: true, error: err.message };
|
|
34
|
+
} finally { db.close(); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function installPostMergeHook({ projectDir, logger = console } = {}) {
|
|
38
|
+
const hooksDir = join(projectDir, ".git", "hooks");
|
|
39
|
+
if (!existsSync(join(projectDir, ".git"))) throw new Error(`Not a git repository: ${projectDir}`);
|
|
40
|
+
if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
|
|
41
|
+
const target = join(hooksDir, "post-merge");
|
|
42
|
+
const src = readFileSync(HOOK_SRC, "utf8");
|
|
43
|
+
if (existsSync(target) && !readFileSync(target, "utf8").includes("KJC-TSK-0455")) {
|
|
44
|
+
logger.warn?.(`[rag] ${target} already exists and was not installed by kj; leaving untouched`);
|
|
45
|
+
return { skipped: true, target };
|
|
46
|
+
}
|
|
47
|
+
writeFileSync(target, src);
|
|
48
|
+
chmodSync(target, 0o755);
|
|
49
|
+
return { installed: true, target };
|
|
50
|
+
}
|
package/src/rag/chunker.js
CHANGED
|
@@ -92,13 +92,17 @@ export function chunkPlan(plan, { path = "<plan>", limit = DEFAULT_CHUNK_LIMIT }
|
|
|
92
92
|
// ---------- source chunker (export-symbol granularity) ------------
|
|
93
93
|
|
|
94
94
|
import { chunkSourceAST } from "./chunker-ast.js";
|
|
95
|
+
import { adapterForPath } from "../lang/registry.js";
|
|
95
96
|
|
|
96
97
|
const EXPORT_RE = /^export\s+(?:async\s+)?(?:function|class|const|let)\s+(\w+)/gm;
|
|
97
98
|
|
|
98
|
-
// KJC-TSK-0444
|
|
99
|
-
//
|
|
100
|
-
//
|
|
99
|
+
// KJC-TSK-0444 + KJC-PCS-0052 PR-B — dispatch por adapter cuando el path
|
|
100
|
+
// coincide con un lenguaje registrado que aporta su propio `chunkSource`
|
|
101
|
+
// (p.ej. Python regex top-level). Si no, mantiene el camino JS histórico:
|
|
102
|
+
// AST de @babel/parser primero, fallback a export-regex.
|
|
101
103
|
export function chunkSource(text, opts = {}) {
|
|
104
|
+
const ad = adapterForPath(opts.path);
|
|
105
|
+
if (ad?.chunkSource) return ad.chunkSource(text, opts);
|
|
102
106
|
const ast = chunkSourceAST(text, opts);
|
|
103
107
|
if (ast) return ast;
|
|
104
108
|
return chunkSourceRegex(text, opts);
|
package/src/rag/eval.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// KJC-TSK-0483 — Retrieval-quality harness. Loads a golden set of queries
|
|
2
|
+
// (each with expected source paths and/or symbol names), runs the retriever
|
|
3
|
+
// against each, and reports recall@k + MRR per query and aggregated. Pure:
|
|
4
|
+
// the runner takes `runQuery` as a function so the CLI plugs in `query()`
|
|
5
|
+
// while tests can use a stub.
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_KS = [5, 10];
|
|
9
|
+
|
|
10
|
+
export function loadGoldenQueries(path) {
|
|
11
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
12
|
+
if (!Array.isArray(data)) throw new Error(`golden queries: expected an array in ${path}`);
|
|
13
|
+
return data;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hitMatchesExpected(hit, expected) {
|
|
17
|
+
if (!hit) return false;
|
|
18
|
+
const src = hit.source || "";
|
|
19
|
+
if (expected.sources?.some((s) => src.endsWith(s))) return true;
|
|
20
|
+
const sym = hit.metadata?.symbol;
|
|
21
|
+
if (sym && expected.symbols?.includes(sym)) return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Per-query metrics. `recall@k` is binary — 1 if any expected match lands in
|
|
26
|
+
// the top-k, else 0. `rr` is the reciprocal of the first relevant rank (1 if
|
|
27
|
+
// the very first hit matches, 0 if none do). `firstRank` is null on miss so
|
|
28
|
+
// callers can render "—" without re-checking.
|
|
29
|
+
export function scoreQuery(hits, expected, ks = DEFAULT_KS) {
|
|
30
|
+
const recall = {};
|
|
31
|
+
for (const k of ks) {
|
|
32
|
+
recall[`@${k}`] = hits.slice(0, k).some((h) => hitMatchesExpected(h, expected)) ? 1 : 0;
|
|
33
|
+
}
|
|
34
|
+
let firstRank = null;
|
|
35
|
+
let rr = 0;
|
|
36
|
+
for (let i = 0; i < hits.length; i += 1) {
|
|
37
|
+
if (hitMatchesExpected(hits[i], expected)) { firstRank = i + 1; rr = 1 / firstRank; break; }
|
|
38
|
+
}
|
|
39
|
+
return { recall, rr, firstRank };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function aggregate(results, ks = DEFAULT_KS) {
|
|
43
|
+
const n = results.length || 1;
|
|
44
|
+
const recall = {};
|
|
45
|
+
for (const k of ks) {
|
|
46
|
+
recall[`@${k}`] = results.reduce((acc, r) => acc + (r.recall[`@${k}`] || 0), 0) / n;
|
|
47
|
+
}
|
|
48
|
+
const mrr = results.reduce((acc, r) => acc + (r.rr || 0), 0) / n;
|
|
49
|
+
return { n: results.length, recall, mrr };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function runEval(queries, runQuery, { topK = 10, ks = DEFAULT_KS } = {}) {
|
|
53
|
+
const results = [];
|
|
54
|
+
for (const q of queries) {
|
|
55
|
+
const hits = (await runQuery(q.query, topK)) || [];
|
|
56
|
+
const expected = { sources: q.expected_sources || [], symbols: q.expected_symbols || [] };
|
|
57
|
+
const scored = scoreQuery(hits, expected, ks);
|
|
58
|
+
results.push({ query: q.query, hits, ...scored });
|
|
59
|
+
}
|
|
60
|
+
return { results, aggregate: aggregate(results, ks) };
|
|
61
|
+
}
|