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.8.0",
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.0.0",
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",
@@ -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")
@@ -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
- const project = flags.project === "all" ? null : (flags.project || detected || null);
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
- const where = flags.where || null;
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
+ }