karajan-code 4.8.0 → 4.9.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.
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Logical clocks & happened-before (Lamport, 1978)
|
|
2
|
+
|
|
3
|
+
## Problem signature
|
|
4
|
+
|
|
5
|
+
Events across processes/services need a consistent order, and wall clocks
|
|
6
|
+
disagree: "last write wins" flip-flops between replicas, audit logs interleave
|
|
7
|
+
impossibly, a consumer sees an update before the create it depends on.
|
|
8
|
+
|
|
9
|
+
## Reach for it when
|
|
10
|
+
|
|
11
|
+
- Two or more writers/emitters produce events whose CAUSAL order matters
|
|
12
|
+
(A caused B must never be observed as B before A).
|
|
13
|
+
- You are tempted to "fix it with NTP" or timestamps — clock skew makes
|
|
14
|
+
timestamp ordering a race, not an order.
|
|
15
|
+
- You need to detect concurrent (conflicting) updates, not just order them:
|
|
16
|
+
that is the vector-clock extension (one counter per node; compare
|
|
17
|
+
component-wise; incomparable = concurrent — see Dynamo's usage).
|
|
18
|
+
|
|
19
|
+
## Do NOT reach for it when
|
|
20
|
+
|
|
21
|
+
- A single writer (or a single database) already totally orders the events —
|
|
22
|
+
its sequence/autoincrement/WAL IS the clock. Most CRUD apps live here.
|
|
23
|
+
- You only need "roughly recent first" for humans (feeds, logs for reading):
|
|
24
|
+
wall-clock timestamps are fine and far simpler.
|
|
25
|
+
- You need a TOTAL order agreed by all nodes: logical clocks give a partial
|
|
26
|
+
causal order; total order needs consensus (Raft/Paxos) or a sequencer.
|
|
27
|
+
|
|
28
|
+
## Trade-offs
|
|
29
|
+
|
|
30
|
+
- Lamport clocks are one counter: tiny, but they cannot tell concurrent from
|
|
31
|
+
ordered. Vector clocks can, at O(nodes) metadata per event that must be
|
|
32
|
+
carried, stored and pruned.
|
|
33
|
+
- Causality tracking pushes conflict RESOLUTION to the reader (semantic
|
|
34
|
+
merge, CRDTs, or ask-the-user) — ordering was the easy half.
|
|
35
|
+
|
|
36
|
+
## Canonical source
|
|
37
|
+
|
|
38
|
+
Leslie Lamport, "Time, Clocks, and the Ordering of Events in a Distributed
|
|
39
|
+
System", Communications of the ACM, 1978. The happened-before relation and
|
|
40
|
+
the clock condition come verbatim from it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.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",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"files": [
|
|
49
49
|
"src/",
|
|
50
50
|
"bin/",
|
|
51
|
+
"library/",
|
|
51
52
|
"templates/",
|
|
52
53
|
"scripts/",
|
|
53
54
|
"vendor/tree-sitter-grammars/",
|
|
@@ -108,7 +109,7 @@
|
|
|
108
109
|
"express-rate-limit": "^8.5.0",
|
|
109
110
|
"helmet": "^8.1.0",
|
|
110
111
|
"js-yaml": "^4.2.0",
|
|
111
|
-
"karajan-core": "^1.
|
|
112
|
+
"karajan-core": "^1.4.0",
|
|
112
113
|
"karajan-rag": "^1.2.0",
|
|
113
114
|
"knip": "^6.15.0",
|
|
114
115
|
"madge": "^8.0.0",
|
package/src/cli/register-meta.js
CHANGED
|
@@ -294,6 +294,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
294
294
|
.option("--rag-mode <mode>", "hybrid (default) | semantic (cosine only) | keyword (BM25 only)", "hybrid")
|
|
295
295
|
.option("--alpha <n>", "Hybrid weight for semantic component (0..1). Default 0.6", "0.6")
|
|
296
296
|
.option("--where <clause>", "Metadata filter, e.g. 'symbol=loadConfig' or 'hu_id=HU-003 AND kind=plan'")
|
|
297
|
+
.option("--library", "Query the distilled engineering canon (pattern cards shipped with kj + ~/.karajan/library + project .karajan/library) instead of the project corpus. Cards carry problem signatures, when-NOT-to-apply and canonical citations (KJC-TSK-0697).")
|
|
297
298
|
.option("--rerank", "Apply cross-encoder rerank to top-K (needs @huggingface/transformers, slower but more precise)")
|
|
298
299
|
.option("--rerank-model <name>", "Cross-encoder model id. Default: Xenova/ms-marco-MiniLM-L-6-v2")
|
|
299
300
|
.option("--json", "Output the hits as JSON")
|
package/src/commands/rag.js
CHANGED
|
@@ -7,6 +7,7 @@ import { makeGovernedEmbedder } from "../rag/governed-embedder.js";
|
|
|
7
7
|
import { indexProject, indexProjectDelta } from "../rag/indexer.js";
|
|
8
8
|
import { query } from "../rag/retriever.js";
|
|
9
9
|
import { installPostMergeHook, maybeAutoUpdate } from "../rag/auto-update.js";
|
|
10
|
+
import { indexLibrary, LIBRARY_PROJECT } from "../rag/library.js";
|
|
10
11
|
import { loadGoldenQueries, runEval } from "../rag/eval.js";
|
|
11
12
|
import { getKarajanHome } from "../utils/paths.js";
|
|
12
13
|
|
|
@@ -47,6 +48,13 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
|
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
51
|
+
// KJC-TSK-0697 — the library corpus refreshes on every index run.
|
|
52
|
+
// Best-effort and cheap: a handful of cards, content-hash dedup.
|
|
53
|
+
try {
|
|
54
|
+
await indexLibrary({ db, embedder, logger, projectDir });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
logger.warn?.(`[rag] library index failed: ${err.message}`);
|
|
57
|
+
}
|
|
50
58
|
if (flags.json) {
|
|
51
59
|
process.stdout.write(`${JSON.stringify(totals)}\n`);
|
|
52
60
|
} else {
|
|
@@ -87,7 +95,10 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
87
95
|
// <slug>` overrides. Pre-v2.27 chunks with NULL slug are only visible
|
|
88
96
|
// when no filter is in effect.
|
|
89
97
|
const detected = projectSlug(config?.projectDir || process.cwd());
|
|
90
|
-
|
|
98
|
+
// KJC-TSK-0697 — `--library` targets the distilled-canon collection:
|
|
99
|
+
// its own project namespace plus the kind filter.
|
|
100
|
+
const library = Boolean(flags.library);
|
|
101
|
+
const project = library ? LIBRARY_PROJECT : (flags.project === "all" ? null : (flags.project || detected || null));
|
|
91
102
|
// KJC-BUG-0061 follow-up: align the CLI `--json` shape with the MCP
|
|
92
103
|
// handler. The MCP tool responds `{ hits: [], empty: true, topK, scope }`
|
|
93
104
|
// so agents (and the `/kj-rag-query` skill from Camino B) have a
|
|
@@ -101,7 +112,12 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
101
112
|
}
|
|
102
113
|
const mode = flags.mode || "hybrid";
|
|
103
114
|
const alpha = Math.max(0, Math.min(1, Number(flags.alpha) || 0.6));
|
|
104
|
-
|
|
115
|
+
// Safe by grammar: parseWhere (core where-parser.js) accepts ONLY
|
|
116
|
+
// AND-joined key=value pairs — an OR or parens never parses, so the
|
|
117
|
+
// composed clause cannot leak past the kind filter; it fails loudly.
|
|
118
|
+
const where = library
|
|
119
|
+
? (flags.where ? `kind=library AND ${flags.where}` : "kind=library")
|
|
120
|
+
: (flags.where || null);
|
|
105
121
|
const rerankOpts = flags.rerank ? { model: flags.rerankModel } : null;
|
|
106
122
|
const hits = await query(db, makeGovernedEmbedder(config), text, { topK, scope, project, mode, alpha, where, rerankOpts });
|
|
107
123
|
if (flags.json) {
|
|
@@ -36,6 +36,7 @@ const B = {
|
|
|
36
36
|
"Behavior changes carry their test in the same step.",
|
|
37
37
|
"Data-model or API changes never share a step with anything else.",
|
|
38
38
|
"No PR beyond ~150 net lines — partition upfront, not at the gate.",
|
|
39
|
+
"Non-trivial task? Name at least two approaches — one as if the codebase didn't exist — and say why the winner won.",
|
|
39
40
|
],
|
|
40
41
|
"numbered-free plan: steps {description, files}, risks, out-of-scope."),
|
|
41
42
|
},
|
|
@@ -56,6 +57,8 @@ const B = {
|
|
|
56
57
|
[
|
|
57
58
|
"Check the ADRs before deciding — never against an accepted one.",
|
|
58
59
|
"Prefer the design that deletes code over the one that adds layers.",
|
|
60
|
+
"One alternative answers: what would you build as if the codebase didn't exist? Picking the legacy-shaped path is fine — picking it by inertia is not.",
|
|
61
|
+
"Ground that alternative in the canon: `kj rag query --library \"<problem signature>\"` serves distilled pattern cards with when-NOT-to-apply and citations.",
|
|
59
62
|
],
|
|
60
63
|
"chosen design, discarded alternatives with reasons, affected boundaries."),
|
|
61
64
|
},
|
|
@@ -72,6 +72,9 @@ Invariants (the git gates enforce these — they are not suggestions):
|
|
|
72
72
|
it as a proposed ADR (\`kj adr add\`) and ask — never bury it in a PR
|
|
73
73
|
bullet. An oversized-diff warning is not an opinion either: partition,
|
|
74
74
|
or get an explicit OK.
|
|
75
|
+
- A non-trivial plan names at least two approaches — one as if the
|
|
76
|
+
codebase didn't exist — and says why the winner won. Following the
|
|
77
|
+
legacy line is a choice, never a default.
|
|
75
78
|
- Branch first: never commit on the base branch — every change reaches it
|
|
76
79
|
through an atomic PR (~150 net lines, Conventional Commits). More than one
|
|
77
80
|
task, or the base tree must stay untouched? \`kj worktree start <slug>\`
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* library — the distilled engineering canon as its own RAG collection
|
|
3
|
+
* (KJC-TSK-0697). Markdown cards from `<pkg>/library/`, `~/.karajan/library/`
|
|
4
|
+
* and `<project>/.karajan/library/` index into the SAME global rag.db,
|
|
5
|
+
* isolated by project="library" + kind="library", so `kj rag query --library`
|
|
6
|
+
* reaches the canon and normal project queries never see it. The architect
|
|
7
|
+
* consults it to ground the greenfield alternative (KJC-TSK-0696).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { extname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
|
|
16
|
+
import { chunkMarkdown } from "./chunker.js";
|
|
17
|
+
import { insertChunk, deleteChunksBySource, findChunkByHash } from "./vec-store.js";
|
|
18
|
+
|
|
19
|
+
export const LIBRARY_PROJECT = "library";
|
|
20
|
+
|
|
21
|
+
const SHIPPED_LIBRARY_DIR = fileURLToPath(new URL("../../library", import.meta.url));
|
|
22
|
+
|
|
23
|
+
/** Existing library dirs, shipped canon first. */
|
|
24
|
+
export function libraryDirs({ pkgLibraryDir = SHIPPED_LIBRARY_DIR, home = os.homedir(), projectDir = process.cwd() } = {}) {
|
|
25
|
+
const candidates = [pkgLibraryDir, join(home, ".karajan", "library"), join(projectDir, ".karajan", "library")];
|
|
26
|
+
return candidates.filter((d) => existsSync(d));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Index every markdown card found in the library dirs. Idempotent: chunks
|
|
31
|
+
* re-index per source and identical bodies skip the embedder (content-hash
|
|
32
|
+
* dedup within the library project).
|
|
33
|
+
*/
|
|
34
|
+
export async function indexLibrary({ db, embedder, logger = console, pkgLibraryDir, home, projectDir } = {}) {
|
|
35
|
+
let indexed = 0, failed = 0, files = 0;
|
|
36
|
+
for (const dir of libraryDirs({ pkgLibraryDir, home, projectDir })) {
|
|
37
|
+
for (const name of readdirSync(dir)) {
|
|
38
|
+
if (extname(name).toLowerCase() !== ".md") continue;
|
|
39
|
+
const path = join(dir, name);
|
|
40
|
+
files += 1;
|
|
41
|
+
const chunks = chunkMarkdown(readFileSync(path, "utf8"), { path, kind: "library" });
|
|
42
|
+
const hashed = chunks.map((ch) => ({ ch, contentHash: createHash("sha256").update(ch.text).digest("hex") }));
|
|
43
|
+
// Unchanged card (every chunk body already known) → skip before the
|
|
44
|
+
// delete, so idempotent re-runs never touch the embedder.
|
|
45
|
+
if (hashed.length && hashed.every(({ contentHash }) => findChunkByHash(db, contentHash, LIBRARY_PROJECT))) continue;
|
|
46
|
+
deleteChunksBySource(db, path);
|
|
47
|
+
for (const { ch, contentHash } of hashed) {
|
|
48
|
+
try {
|
|
49
|
+
if (findChunkByHash(db, contentHash, LIBRARY_PROJECT)) continue;
|
|
50
|
+
const embedding = await embedder.embed(ch.text);
|
|
51
|
+
insertChunk(db, { source: path, kind: "library", text: ch.text, metadata: ch.metadata, embedding, project: LIBRARY_PROJECT, contentHash });
|
|
52
|
+
indexed += 1;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
failed += 1;
|
|
55
|
+
logger.warn?.(`[rag-library] embed failed for ${path}: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (files) logger.info?.(`[rag-library] ${files} card(s) → ${indexed} chunk(s) (${failed} failed)`);
|
|
61
|
+
return { files, indexed, failed };
|
|
62
|
+
}
|