karajan-code 3.7.1 → 3.8.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,74 @@
1
+ // KJC-TSK-0569 (Onboard B) — deterministic synthesis of the read-only sweep.
2
+ //
3
+ // Turns the sweep bundle (Onboard A, 0568) into one coherent digest: a
4
+ // human-readable report plus a machine summary the decider (StartDecidorRole)
5
+ // consumes to pick an intent. No LLM here — the costly call is the haiku
6
+ // decider, not a redundant strong-model narration. Reuses the harden advisory
7
+ // formatter so the improvements section reads exactly like `kj harden --report`.
8
+ import { formatAdvisoryReport } from "../harden/advisory.js";
9
+
10
+ const EMPTY_TALLY = { install: 0, update: 0, review: 0, keep: 0 };
11
+
12
+ /** Tally harden advisory artifacts by recommendation. */
13
+ function tallyImprovements(improvements) {
14
+ const tally = { ...EMPTY_TALLY };
15
+ for (const a of improvements?.artifacts ?? []) {
16
+ if (a?.recommendation in tally) tally[a.recommendation] += 1;
17
+ }
18
+ return tally;
19
+ }
20
+
21
+ /** Quality/maintenance gaps as short phrases, derived from signals + drift. */
22
+ function collectGaps(signals = {}, drift) {
23
+ const gaps = [];
24
+ if (!signals.hasTests) gaps.push("no tests");
25
+ if (!signals.hasCI) gaps.push("no CI");
26
+ if (typeof signals.staleDays === "number" && signals.staleDays > 365) {
27
+ gaps.push(`last commit ${signals.staleDays}d old`);
28
+ }
29
+ if (drift && drift.ok === false) {
30
+ const failed = (drift.checks ?? []).filter((c) => c?.ok === false).length;
31
+ if (failed > 0) gaps.push(`${failed} harden check(s) drifting`);
32
+ }
33
+ return gaps;
34
+ }
35
+
36
+ /**
37
+ * @param {object} bundle output of runReadOnlySweep (0568)
38
+ * @returns {{text: string, summary: object}}
39
+ */
40
+ export function buildAssessment(bundle) {
41
+ if (!bundle || typeof bundle !== "object") {
42
+ throw new TypeError("buildAssessment: bundle must be an object");
43
+ }
44
+ const { maturity = {}, signals = {}, drift, improvements, rag, qmd } = bundle;
45
+ const gaps = collectGaps(signals, drift);
46
+ const improvementCounts = tallyImprovements(improvements);
47
+
48
+ const summary = {
49
+ maturity: maturity.maturity ?? "unknown",
50
+ declared: Boolean(maturity.declared),
51
+ deepHealthRecommended: Boolean(maturity.deepHealthRecommended),
52
+ gaps,
53
+ driftOk: drift ? drift.ok !== false : null,
54
+ improvementCounts,
55
+ indexed: rag ? Boolean(rag.indexed) : null,
56
+ qmdAvailable: qmd ? Boolean(qmd.available) : null,
57
+ };
58
+
59
+ const lines = [
60
+ `Project maturity: ${summary.maturity}${summary.declared ? " (declared)" : " (inferred)"}`,
61
+ ...(maturity.reasons ?? []).map((r) => ` - ${r}`),
62
+ "",
63
+ `Tests: ${signals.hasTests ? "yes" : "no"} · CI: ${signals.hasCI ? "yes" : "no"} · commits: ${signals.commitCount ?? 0}`,
64
+ `Quality gaps: ${gaps.length ? gaps.join(", ") : "none detected"}`,
65
+ `Knowledge index: ${summary.indexed === null ? "n/a" : summary.indexed ? "present" : "missing"}` +
66
+ ` · qmd: ${summary.qmdAvailable === null ? "n/a" : summary.qmdAvailable ? "available" : "absent"}`,
67
+ ];
68
+
69
+ if (improvements) {
70
+ lines.push("", ...formatAdvisoryReport(improvements));
71
+ }
72
+
73
+ return { text: lines.join("\n"), summary };
74
+ }
@@ -0,0 +1,73 @@
1
+ // KJC-TSK-0568 (Onboard A) — maturity classifier for the Brain (`kj start`).
2
+ //
3
+ // Pure and deterministic: given a bundle of read-only signals it labels the
4
+ // project new | existing | legacy. No LLM, no I/O — the sweep (sweep.js) does
5
+ // the reading; this only decides. The label drives which read-only subset the
6
+ // orchestrator runs and, downstream (0570), whether a deep health pass is worth
7
+ // the cost. A user-declared maturity always wins over inference.
8
+ //
9
+ // Acceptance criteria (KJC-TSK-0568):
10
+ // - empty repo / scaffolding only -> new
11
+ // - code + config + tests/CI -> existing
12
+ // - code but missing tests/CI, or very stale -> legacy
13
+
14
+ /** Last-commit age (days) beyond which a repo counts as stalled/neglected. */
15
+ export const STALE_DAYS_THRESHOLD = 365;
16
+
17
+ const VALID = new Set(["new", "existing", "legacy"]);
18
+
19
+ /**
20
+ * @param {object} signals
21
+ * @param {("new"|"existing"|"legacy"|null)} [signals.declared] user override
22
+ * @param {boolean} signals.hasSourceCode real source files present
23
+ * @param {boolean} signals.scaffoldingOnly only generator boilerplate, no real code
24
+ * @param {boolean} signals.hasTests a test suite is present
25
+ * @param {boolean} signals.hasCI CI workflows are present
26
+ * @param {number} [signals.commitCount] number of commits (capped upstream)
27
+ * @param {number|null} [signals.staleDays] age of the last commit in days
28
+ * @returns {{maturity: "new"|"existing"|"legacy", reasons: string[], deepHealthRecommended: boolean, declared: boolean}}
29
+ */
30
+ export function classifyMaturity(signals) {
31
+ if (!signals || typeof signals !== "object") {
32
+ throw new TypeError("classifyMaturity: signals must be an object");
33
+ }
34
+
35
+ if (VALID.has(signals.declared)) {
36
+ const maturity = signals.declared;
37
+ return {
38
+ maturity,
39
+ reasons: [`maturity declared by user as "${maturity}"`],
40
+ deepHealthRecommended: maturity === "legacy",
41
+ declared: true,
42
+ };
43
+ }
44
+
45
+ const reasons = [];
46
+ let maturity;
47
+
48
+ if (!signals.hasSourceCode || signals.scaffoldingOnly) {
49
+ maturity = "new";
50
+ reasons.push(signals.scaffoldingOnly ? "only scaffolding present, no real code yet" : "no source code found");
51
+ } else {
52
+ const stalled = typeof signals.staleDays === "number" && signals.staleDays > STALE_DAYS_THRESHOLD;
53
+ const gaps = [];
54
+ if (!signals.hasTests) gaps.push("no tests");
55
+ if (!signals.hasCI) gaps.push("no CI");
56
+ if (stalled) gaps.push(`last commit ${signals.staleDays}d old (stalled)`);
57
+
58
+ if (gaps.length > 0) {
59
+ maturity = "legacy";
60
+ reasons.push(`code present but neglected: ${gaps.join(", ")}`);
61
+ } else {
62
+ maturity = "existing";
63
+ reasons.push("code with tests and CI, actively maintained");
64
+ }
65
+ }
66
+
67
+ return {
68
+ maturity,
69
+ reasons,
70
+ deepHealthRecommended: maturity === "legacy",
71
+ declared: false,
72
+ };
73
+ }
@@ -0,0 +1,79 @@
1
+ // KJC-TSK-0569 (Onboard B) — the StartDecidor: the only LLM in the Brain's
2
+ // onboarding. Reads the deterministic assessment + user goal, picks ONE intent
3
+ // from a closed set, and never executes (0570 maps the intent to a safe command).
4
+ // Provider falls back to the coder, model defaults to haiku. Unparseable/unknown
5
+ // output degrades to ASK_USER so `kj start` always has a sane next step.
6
+ import { AgentRole } from "../roles/agent-role.js";
7
+ import { extractFirstJson } from "../utils/json-extract.js";
8
+ import { buildStartDecisionPrompt, INTENTS } from "../prompts/start-decision.js";
9
+
10
+ const FALLBACK_QUESTION = "What would you like to do with this project?";
11
+
12
+ export class StartDecidorRole extends AgentRole {
13
+ constructor(opts) {
14
+ super({ ...opts, name: "start" });
15
+ }
16
+
17
+ extractInput(input) {
18
+ const src = typeof input === "string" ? { userMessage: input } : input || {};
19
+ return {
20
+ userMessage: src.userMessage || "",
21
+ assessment: src.assessment || "",
22
+ onOutput: src.onOutput || null,
23
+ ...src,
24
+ };
25
+ }
26
+
27
+ async buildPrompt({ userMessage, assessment }) {
28
+ return {
29
+ prompt: buildStartDecisionPrompt({ userMessage, assessment, instructions: this.instructions }),
30
+ };
31
+ }
32
+
33
+ parseOutput(raw) {
34
+ return extractFirstJson(raw);
35
+ }
36
+
37
+ buildSuccessResult(parsed, provider) {
38
+ const intent = INTENTS.has(parsed.intent) ? parsed.intent : "ASK_USER";
39
+ const rationale = String(parsed.rationale || "").trim();
40
+ const questionToAsk = String(parsed.questionToAsk || "").trim();
41
+ const degraded = intent !== parsed.intent;
42
+ return {
43
+ intent,
44
+ rationale: rationale || (degraded ? "Unrecognized intent, asking the user." : "No rationale provided."),
45
+ questionToAsk: intent === "ASK_USER" ? questionToAsk || FALLBACK_QUESTION : "",
46
+ provider,
47
+ };
48
+ }
49
+
50
+ buildSummary(parsed) {
51
+ const intent = INTENTS.has(parsed.intent) ? parsed.intent : "ASK_USER";
52
+ return `Start decision: ${intent}`;
53
+ }
54
+
55
+ handleParseNull(agentResult, provider) {
56
+ return this.degradedDecision({ agentResult, provider, reason: "no JSON found" });
57
+ }
58
+
59
+ handleParseError(err, agentResult, provider) {
60
+ return this.degradedDecision({ agentResult, provider, reason: err?.message || "parse error" });
61
+ }
62
+
63
+ // Never block onboarding: garbage in → ask the user one open question.
64
+ degradedDecision({ agentResult, provider, reason }) {
65
+ this.logger?.warn?.(`[start] decider output unusable (${reason}) — asking the user.`);
66
+ return {
67
+ ok: true,
68
+ result: {
69
+ intent: "ASK_USER",
70
+ rationale: `Could not read a decision (${reason}).`,
71
+ questionToAsk: FALLBACK_QUESTION,
72
+ provider,
73
+ degraded: true,
74
+ },
75
+ summary: "Start decision: ASK_USER (fallback)",
76
+ usage: agentResult.usage,
77
+ };
78
+ }
79
+ }
@@ -0,0 +1,112 @@
1
+ // KJC-TSK-0568 (Onboard A) — read-only sweep orchestrator for the Brain.
2
+ //
3
+ // Runs the READ-ONLY signals fitting the project's maturity and aggregates them
4
+ // into one structured bundle for synthesis (Onboard B, 0569). Writes nothing.
5
+ // No LLM: maturity is deterministic and the costly audit is deferred — legacy
6
+ // only flags `deepHealthRecommended` (0570 decides). Subset by maturity: new =
7
+ // brief+tests; existing/legacy = + drift, harden advisory, rag + qmd status.
8
+ // Collaborators are injected (opts.deps) so it stays trivially testable.
9
+ import { existsSync } from "node:fs";
10
+ import { execFileSync } from "node:child_process";
11
+
12
+ import { collectAll } from "../onboarder/collectors/index.js";
13
+ import { checkHarden } from "../harden/check.js";
14
+ import { compareHarden } from "../harden/advisory.js";
15
+ import { detectTestFramework } from "../utils/project-detect.js";
16
+ import { detectQmd } from "../utils/qmd-detect.js";
17
+ import {
18
+ dbPath, openVecStore, projectSlug, countChunks, getLastIndexedCommit,
19
+ } from "../rag/vec-store.js";
20
+ import { classifyMaturity } from "./maturity.js";
21
+
22
+ const CODE_EXT = new Set([
23
+ ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".py", ".go",
24
+ ".rs", ".java", ".php", ".rb", ".c", ".cc", ".cpp", ".cs",
25
+ ]);
26
+ const SCAFFOLD_MAX_FILES = 3;
27
+ const CI_MARKERS = [".github/workflows", ".gitlab-ci.yml"];
28
+
29
+ const safe = async (fn) => { try { return await fn(); } catch { return null; } };
30
+
31
+ /** Count real source files in a tree bundle, skipping tests. */
32
+ export function countCodeFiles(tree = []) {
33
+ let n = 0;
34
+ for (const node of tree) {
35
+ if (node.kind === "dir") n += countCodeFiles(node.children);
36
+ else if (node.kind === "file") {
37
+ const ext = node.path.slice(node.path.lastIndexOf("."));
38
+ const isTest = /\.(test|spec)\./.test(node.path) || /(^|\/)(test|tests|__tests__)\//.test(node.path);
39
+ if (CODE_EXT.has(ext) && !isTest) n += 1;
40
+ }
41
+ }
42
+ return n;
43
+ }
44
+
45
+ /** Age of the last commit in days, or null on non-git / no commits. */
46
+ function lastCommitAgeDays(projectDir) {
47
+ try {
48
+ const out = execFileSync("git", ["log", "-1", "--format=%ct"], {
49
+ cwd: projectDir, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
50
+ }).trim();
51
+ if (!out) return null;
52
+ return Math.floor((Date.now() - Number(out) * 1000) / 86_400_000);
53
+ } catch { return null; }
54
+ }
55
+
56
+ /** Read-only RAG index status; null when no index file exists yet. */
57
+ function readRagStatus(projectDir) {
58
+ try {
59
+ if (!existsSync(dbPath())) return { indexed: false, chunks: 0, lastIndexedCommit: null };
60
+ const db = openVecStore();
61
+ return {
62
+ indexed: true,
63
+ chunks: countChunks(db, { kind: "code" }),
64
+ lastIndexedCommit: getLastIndexedCommit(db, projectSlug(projectDir)),
65
+ };
66
+ } catch { return null; }
67
+ }
68
+
69
+ /**
70
+ * @param {string} projectDir
71
+ * @param {{declared?: ("new"|"existing"|"legacy"|null), profile?: string, deps?: object}} [opts]
72
+ * declared = user-declared maturity; deps = injected collaborators (defaults = real collectors).
73
+ */
74
+ export async function runReadOnlySweep(projectDir, { declared = null, profile = "standard", deps = {} } = {}) {
75
+ const d = {
76
+ collectAll, detectTestFramework, checkHarden, compareHarden,
77
+ detectQmd, ragStatus: readRagStatus, gitAgeDays: lastCommitAgeDays, ...deps,
78
+ };
79
+
80
+ const brief = await safe(() => d.collectAll(projectDir));
81
+ const tests = await safe(() => d.detectTestFramework(projectDir));
82
+
83
+ const tree = brief?.tree ?? [];
84
+ const present = brief?.configs?.present ?? [];
85
+ const codeFiles = countCodeFiles(tree);
86
+ const commitCount = brief?.git?.commitCount ?? 0;
87
+
88
+ const signals = {
89
+ hasSourceCode: codeFiles > 0,
90
+ scaffoldingOnly: codeFiles > 0 && codeFiles <= SCAFFOLD_MAX_FILES && commitCount <= 1,
91
+ hasTests: Boolean(tests?.hasTests),
92
+ hasCI: CI_MARKERS.some((m) => present.includes(m)),
93
+ commitCount,
94
+ staleDays: d.gitAgeDays(projectDir),
95
+ };
96
+
97
+ const maturity = classifyMaturity({ declared, ...signals });
98
+ const deep = maturity.maturity !== "new";
99
+
100
+ return {
101
+ projectDir,
102
+ maturity,
103
+ signals,
104
+ brief,
105
+ tests,
106
+ drift: deep ? await safe(() => d.checkHarden({ projectDir, profile })) : null,
107
+ improvements: deep ? await safe(() => d.compareHarden({ projectDir, profile })) : null,
108
+ rag: deep ? await safe(() => d.ragStatus(projectDir)) : null,
109
+ qmd: deep ? await safe(() => d.detectQmd()) : null,
110
+ collectedAt: new Date().toISOString(),
111
+ };
112
+ }