karajan-code 3.12.3 → 3.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "3.12.3",
3
+ "version": "3.13.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",
@@ -12,6 +12,7 @@ import { existsSync, readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
 
14
14
  import { findManagedBlock } from "../utils/managed-markers.js";
15
+ import { findAlternative } from "./alternatives.js";
15
16
  import { CONFIGS_BY_LANGUAGE, UNIVERSAL_CONFIGS } from "./config-templates.js";
16
17
  import { detectStackRoots } from "./stack-roots.js";
17
18
 
@@ -98,6 +99,13 @@ function readArtifactContent(searchDir, foundAt) {
98
99
 
99
100
  /** Map a classified state to a recommendation + human rationale. */
100
101
  function recommend(state) {
102
+ if (state.status === "SATISFIED_BY_ALTERNATIVE") {
103
+ return {
104
+ recommendation: "keep",
105
+ rationale: `covered by ${state.foundAt} (${state.alternativeTool}) — kj won't add a second linter/formatter`,
106
+ mergeable: false,
107
+ };
108
+ }
101
109
  if (state.status === "MISSING") return { recommendation: "install", rationale: `kj would add ${state.file}`, mergeable: true };
102
110
  if (state.status === "USER_OWNED") {
103
111
  const n = state.improvements?.length ?? 0;
@@ -109,11 +117,20 @@ function recommend(state) {
109
117
  return { recommendation: "update", rationale: `kj v${state.currentVersion} available (you have v${state.managedVersion})`, mergeable: true };
110
118
  }
111
119
 
112
- /** Classify a single artifact found (or not) under `searchDir`. */
113
- export function classifyArtifact(searchDir, artifact) {
120
+ /**
121
+ * Classify a single artifact found (or not) under `searchDir`. `altDirs`
122
+ * widens the cross-tool alternative lookup (a root-level biome.json covers a
123
+ * monorepo's language roots); the user's OWN config of the same tool still
124
+ * wins over an alternative.
125
+ */
126
+ export function classifyArtifact(searchDir, artifact, altDirs = [searchDir]) {
114
127
  const foundAt = findArtifactFile(searchDir, artifact);
115
128
  const base = { foundAt, managedVersion: null, upToDate: null, improvements: [] };
116
129
  let state = { ...base, status: "MISSING" };
130
+ if (!foundAt) {
131
+ const alt = findAlternative(altDirs, artifact.id);
132
+ if (alt) state = { ...base, status: "SATISFIED_BY_ALTERNATIVE", foundAt: alt.foundAt, alternativeTool: alt.tool };
133
+ }
117
134
  if (foundAt) {
118
135
  const content = readArtifactContent(searchDir, foundAt);
119
136
  if (artifact.blockId && content.includes(`kj:managed:${artifact.blockId}`)) {
@@ -139,8 +156,10 @@ export function compareHarden({ projectDir = process.cwd(), profile = "standard"
139
156
  for (const { dir, language } of detectStackRoots(projectDir, { only, exclude })) {
140
157
  const searchDir = dir === "." ? projectDir : join(projectDir, dir);
141
158
  for (const cfg of CONFIGS_BY_LANGUAGE[language] ?? []) {
142
- const res = classifyArtifact(searchDir, toArtifact(cfg));
143
- artifacts.push({ dir, ...res, foundAt: res.foundAt && dir !== "." ? join(dir, res.foundAt) : res.foundAt });
159
+ const res = classifyArtifact(searchDir, toArtifact(cfg), [searchDir, projectDir]);
160
+ // Alternative matches may live at the repo root — keep their path unprefixed.
161
+ const prefix = res.foundAt && dir !== "." && res.status !== "SATISFIED_BY_ALTERNATIVE";
162
+ artifacts.push({ dir, ...res, foundAt: prefix ? join(dir, res.foundAt) : res.foundAt });
144
163
  }
145
164
  }
146
165
  return { artifacts };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Cross-tool alternatives for `kj harden` (KJC-TSK-0614).
3
+ *
4
+ * A user's Biome config replaces BOTH eslint and prettier — seeding kj's
5
+ * configs next to biome.json installs a second, conflicting linter/formatter
6
+ * that fights the user's own hooks. The advisory's EQUIVALENTS map only
7
+ * covers same-tool filename variants; this map covers whole-tool
8
+ * substitutions, and the three surfaces (advisory, config-engine, check)
9
+ * consult it before treating an artifact as missing.
10
+ *
11
+ * Detection is config-file based on purpose: deterministic, and a false
12
+ * negative just means kj proposes its default (the pre-0614 behaviour),
13
+ * never that it overwrites the user's tool.
14
+ */
15
+
16
+ import { existsSync } from "node:fs";
17
+ import { join } from "node:path";
18
+
19
+ const ALTERNATIVES = {
20
+ eslint: [{ tool: "biome", files: ["biome.json", "biome.jsonc"] }],
21
+ prettier: [{ tool: "biome", files: ["biome.json", "biome.jsonc"] }],
22
+ };
23
+
24
+ /**
25
+ * First alternative tool that satisfies `artifactId`, searching each dir in
26
+ * order (a language root first, then the repo root — a root-level biome.json
27
+ * covers the whole monorepo). Null when none applies.
28
+ *
29
+ * @param {string[]} searchDirs
30
+ * @param {string} artifactId - "eslint" | "prettier" | ...
31
+ * @returns {{ tool: string, foundAt: string }|null}
32
+ */
33
+ export function findAlternative(searchDirs, artifactId) {
34
+ for (const alt of ALTERNATIVES[artifactId] ?? []) {
35
+ for (const dir of searchDirs) {
36
+ for (const rel of alt.files) {
37
+ if (existsSync(join(dir, rel))) return { tool: alt.tool, foundAt: rel };
38
+ }
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * Artifact id for a config-template entry — same convention as the
46
+ * advisory's toArtifact: prettier is the only marker-less JSON config.
47
+ */
48
+ export function artifactIdForConfig(cfg) {
49
+ return cfg.blockId ?? "prettier";
50
+ }
@@ -11,6 +11,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
 
13
13
  import { runCommand } from "../utils/process.js";
14
+ import { artifactIdForConfig, findAlternative } from "./alternatives.js";
14
15
  import { CONFIGS_BY_LANGUAGE, UNIVERSAL_CONFIGS } from "./config-templates.js";
15
16
  import { PROFILE_HOOKS } from "./hook-templates.js";
16
17
  import { detectStackRoots } from "./stack-roots.js";
@@ -53,8 +54,18 @@ export async function checkHarden({ projectDir = process.cwd(), profile = "stand
53
54
  checks.push(presence(projectDir, cfg.file, `config:${cfg.file}`, "missing — run kj harden"));
54
55
  }
55
56
  for (const { dir, language } of roots) {
57
+ const rootDir = dir === "." ? projectDir : join(projectDir, dir);
56
58
  for (const cfg of CONFIGS_BY_LANGUAGE[language] ?? []) {
57
59
  const rel = dir === "." ? cfg.file : join(dir, cfg.file);
60
+ // A config covered by the user's own tool (biome.json ⇒ eslint+prettier)
61
+ // is not drift — kj must not demand a second linter/formatter (KJC-TSK-0614).
62
+ if (!existsSync(join(projectDir, rel))) {
63
+ const alt = findAlternative([rootDir, projectDir], artifactIdForConfig(cfg));
64
+ if (alt) {
65
+ checks.push({ id: `config:${rel}`, ok: true, detail: `covered by ${alt.foundAt} (${alt.tool})` });
66
+ continue;
67
+ }
68
+ }
58
69
  checks.push(presence(projectDir, rel, `config:${rel}`, `missing for ${language} — run kj harden`));
59
70
  }
60
71
  }
@@ -9,6 +9,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
 
11
11
  import { upsertManagedBlock } from "../utils/managed-markers.js";
12
+ import { artifactIdForConfig, findAlternative } from "./alternatives.js";
12
13
  import { CONFIGS_BY_LANGUAGE, UNIVERSAL_CONFIGS } from "./config-templates.js";
13
14
 
14
15
  const BLOCK_VERSION = 1;
@@ -18,13 +19,26 @@ function writeFile(target, content) {
18
19
  writeFileSync(target, content);
19
20
  }
20
21
 
21
- /** Seed a list of config entries into `targetDir`, labelling each by `prefix`. */
22
- function seedInto(targetDir, configs, dryRun, prefix, results) {
22
+ /**
23
+ * Seed a list of config entries into `targetDir`, labelling each by `prefix`.
24
+ * `altDirs` widens the cross-tool alternative lookup (KJC-TSK-0614): a config
25
+ * covered by the user's own tool (biome.json ⇒ eslint+prettier) is never
26
+ * seeded — kj must not install a second, conflicting linter/formatter.
27
+ */
28
+ function seedInto(targetDir, configs, dryRun, prefix, results, altDirs = [targetDir]) {
23
29
  for (const cfg of configs) {
24
30
  const target = join(targetDir, cfg.file);
25
31
  const exists = existsSync(target);
26
32
  const file = prefix === "." ? cfg.file : join(prefix, cfg.file);
27
33
 
34
+ if (!exists) {
35
+ const alt = findAlternative(altDirs, artifactIdForConfig(cfg));
36
+ if (alt) {
37
+ results.push({ file, action: "covered", by: alt.foundAt });
38
+ continue;
39
+ }
40
+ }
41
+
28
42
  if (cfg.json) {
29
43
  const action = exists ? "skipped" : "inserted";
30
44
  if (!dryRun && !exists) writeFile(target, `${cfg.body}\n`);
@@ -69,7 +83,9 @@ export function installConfigsForRoots({ projectDir = process.cwd(), roots = [],
69
83
  const results = [];
70
84
  seedInto(projectDir, UNIVERSAL_CONFIGS, dryRun, ".", results);
71
85
  for (const { dir, language } of roots) {
72
- seedInto(join(projectDir, dir), CONFIGS_BY_LANGUAGE[language] ?? [], dryRun, dir, results);
86
+ const rootDir = join(projectDir, dir);
87
+ // A root-level biome.json covers every language root of the monorepo.
88
+ seedInto(rootDir, CONFIGS_BY_LANGUAGE[language] ?? [], dryRun, dir, results, [rootDir, projectDir]);
73
89
  }
74
90
  return { dryRun, configs: results };
75
91
  }