karajan-code 4.35.0 → 4.35.1
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 +1 -1
- package/src/audit/harness-scorecard.js +8 -1
- package/src/audit/harness-section.js +13 -2
- package/src/checks/rag-coverage.js +4 -1
- package/src/checks/runner.js +27 -5
- package/src/checks/skills.js +10 -0
- package/src/checks/types.js +5 -1
- package/src/commands/review-gate.js +2 -2
- package/src/harden/config-templates.js +11 -0
- package/src/harden/harness-hooks.js +92 -3
- package/src/harden/sentinel-hooks.js +12 -1
- package/src/orchestrator/drivers/pre-loop.js +1 -1
- package/src/rag/coverage.js +24 -2
- package/src/review/gate-gitignore.js +23 -1
- package/src/review/kj-provenance.js +42 -0
- package/src/review/rag-ledger.js +12 -4
- package/src/review/rag-requirement.js +17 -3
- package/src/sonar/api.js +11 -2
- package/src/sonar/project-key.js +33 -0
- package/src/sonar/scanner.js +37 -2
package/package.json
CHANGED
|
@@ -40,9 +40,16 @@ export function hintForReason(reason, image) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* KJC-BUG-0228 (issue #1840) — OPT-IN. La imagen por defecto es de un tercero y
|
|
45
|
+
* dejo de existir en el registro publico, asi que el paso venia activo, fallaba
|
|
46
|
+
* con "pull access denied" y se saltaba en cada `kj audit`. kj no puede depender
|
|
47
|
+
* por defecto de algo que otro puede borrar: quien quiera el scorecard lo pide,
|
|
48
|
+
* y quien tenga su propia imagen la declara en `audit.harness.image`.
|
|
49
|
+
*/
|
|
43
50
|
export function normalizeHarnessConfig(h = {}) {
|
|
44
51
|
return {
|
|
45
|
-
enabled: h
|
|
52
|
+
enabled: h?.enabled === true,
|
|
46
53
|
image: h.image || D.image,
|
|
47
54
|
pullTimeoutMs: pos(h.pull_timeout_ms, D.pullTimeoutMs),
|
|
48
55
|
assessTimeoutMs: pos(h.assess_timeout_ms, D.assessTimeoutMs),
|
|
@@ -20,7 +20,12 @@ const ts = () => new Date().toISOString().replaceAll(/[:.]/g, "-");
|
|
|
20
20
|
|
|
21
21
|
export async function runHarnessSection({ projectDir, harnessConfig = null, runAssess = runHarnessAssess } = {}) {
|
|
22
22
|
const cfg = normalizeHarnessConfig(harnessConfig || {});
|
|
23
|
-
|
|
23
|
+
// KJC-BUG-0228: desactivado por defecto, y DICHO. Un paso que desaparece en
|
|
24
|
+
// silencio del informe se lee como un paso aprobado.
|
|
25
|
+
if (!cfg.enabled) {
|
|
26
|
+
const asked = harnessConfig?.enabled === false;
|
|
27
|
+
return { ok: true, skipped: true, reason: asked ? "disabled" : "opt-in" };
|
|
28
|
+
}
|
|
24
29
|
const res = await runAssess(projectDir, cfg);
|
|
25
30
|
if (!res.ok) return { ok: false, error: res.error, reason: res.reason, hint: res.hint, image: cfg.image };
|
|
26
31
|
const outDir = path.join(projectDir, ".karajan", "audit-runs", ts());
|
|
@@ -38,7 +43,13 @@ export async function runHarnessSection({ projectDir, harnessConfig = null, runA
|
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export function formatHarnessSection(summary) {
|
|
41
|
-
if (!summary
|
|
46
|
+
if (!summary) return "";
|
|
47
|
+
// KJC-BUG-0228: cuando no corre porque nadie lo pidio, se dice como pedirlo.
|
|
48
|
+
// Callarlo dejaba un informe que parecia completo sin serlo.
|
|
49
|
+
if (summary.reason === "opt-in") {
|
|
50
|
+
return "## Harness Scorecard\n\n_not run: it is opt-in (`audit.harness.enabled: true`), and it needs a scorecard image you can pull — declare yours in `audit.harness.image`._\n";
|
|
51
|
+
}
|
|
52
|
+
if (summary.skipped) return "";
|
|
42
53
|
if (!summary.ok) {
|
|
43
54
|
const reason = summary.reason ? ` (${summary.reason})` : "";
|
|
44
55
|
const hint = summary.hint ? `\n\n${summary.hint}\n` : "";
|
|
@@ -41,7 +41,10 @@ export function createRagCoverageCheck({ openStore = (dim) => openVecStore({ dim
|
|
|
41
41
|
const sample = gaps.slice(0, 3).join(", ") + (gaps.length > 3 ? ", …" : "");
|
|
42
42
|
return { ok: false, severity: "fail", detail: `${c.missing.length} source(s) outside the RAG index, ${c.stale.length} stale (${sample}) — ${fix}` };
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
// KJC-BUG-0221: lo que kj genera no se exige indexado, pero se dice —
|
|
45
|
+
// una exencion que no se ve es indistinguible de un gate que dejo de mirar.
|
|
46
|
+
const generated = c.generated?.length ? `, ${c.generated.length} generated by kj (not required)` : "";
|
|
47
|
+
return { ok: true, severity: "info", detail: `${c.indexed}/${c.total} sources indexed at ${c.lastIndexedCommit.slice(0, 9)}${generated}` };
|
|
45
48
|
} finally {
|
|
46
49
|
db.close();
|
|
47
50
|
}
|
package/src/checks/runner.js
CHANGED
|
@@ -92,8 +92,14 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
92
92
|
entries.map(async (entry) => {
|
|
93
93
|
if (entry.status === STATUS.SKIPPED) return;
|
|
94
94
|
const start = Date.now();
|
|
95
|
+
// Per-check override: checks that spawn slow tools (e.g. `npx <pkg>` cold
|
|
96
|
+
// start) declare detectTimeoutMs so the blanket pipeline timeout doesn't
|
|
97
|
+
// kill them. Falls back to the pipeline-wide timeoutMs.
|
|
98
|
+
const detectTimeoutMs = Number.isFinite(entry.check.detectTimeoutMs)
|
|
99
|
+
? entry.check.detectTimeoutMs
|
|
100
|
+
: timeoutMs;
|
|
95
101
|
try {
|
|
96
|
-
const result = await withTimeout(entry.check.detect({ config, signal }),
|
|
102
|
+
const result = await withTimeout(entry.check.detect({ config, signal }), detectTimeoutMs, entry.check.name);
|
|
97
103
|
entry.detectResult = result;
|
|
98
104
|
entry.runMs = Date.now() - start;
|
|
99
105
|
entry.status = resolveStatusFromDetect(entry.check, result);
|
|
@@ -112,9 +118,21 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
112
118
|
} catch (err) {
|
|
113
119
|
entry.runMs = Date.now() - start;
|
|
114
120
|
if (err instanceof TimeoutError) {
|
|
115
|
-
entry.detectResult = { ok: false, severity: "warn", detail: `Timeout after ${
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
entry.detectResult = { ok: false, severity: "warn", detail: `Timeout after ${detectTimeoutMs}ms` };
|
|
122
|
+
// A timeout on a degradable (optional) check must not block the
|
|
123
|
+
// pipeline: degrade to WARN and disable its feature flags, same as
|
|
124
|
+
// a regular failure (KJC-BUG-0049 semantics).
|
|
125
|
+
if (entry.check.degradable) {
|
|
126
|
+
for (const dotPath of entry.check.degradable.disables || []) {
|
|
127
|
+
setDotPath(overrides, dotPath, false);
|
|
128
|
+
}
|
|
129
|
+
entry.status = STATUS.WARN;
|
|
130
|
+
entry.detail = `${entry.detectResult.detail} → ${entry.check.degradable.warn}`;
|
|
131
|
+
entry.degraded = true;
|
|
132
|
+
} else {
|
|
133
|
+
entry.status = STATUS.TIMEOUT;
|
|
134
|
+
entry.detail = entry.detectResult.detail;
|
|
135
|
+
}
|
|
118
136
|
} else {
|
|
119
137
|
entry.detectResult = { ok: false, severity: "fail", detail: `Detection error: ${err.message}` };
|
|
120
138
|
entry.status = STATUS.FAIL;
|
|
@@ -172,7 +190,11 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
172
190
|
entries.filter((e) => e.remediated).map(async (entry) => {
|
|
173
191
|
const start = Date.now();
|
|
174
192
|
try {
|
|
175
|
-
const result = await withTimeout(
|
|
193
|
+
const result = await withTimeout(
|
|
194
|
+
entry.check.detect({ config: verifyConfig, signal }),
|
|
195
|
+
Number.isFinite(entry.check.detectTimeoutMs) ? entry.check.detectTimeoutMs : timeoutMs,
|
|
196
|
+
`${entry.check.name}:reverify`
|
|
197
|
+
);
|
|
176
198
|
entry.runMs += Date.now() - start;
|
|
177
199
|
if (!result.ok) {
|
|
178
200
|
entry.status = STATUS.FAIL;
|
package/src/checks/skills.js
CHANGED
|
@@ -23,6 +23,16 @@ export function createOpenSkillsCheck() {
|
|
|
23
23
|
strategy: STRATEGY.PROMPT,
|
|
24
24
|
applies: skillsEnabled,
|
|
25
25
|
describe: "Install OpenSkills globally: npm install -g openskills",
|
|
26
|
+
// Detection shells out to `npx openskills --version`; a cold npx cache
|
|
27
|
+
// downloads the package first (~8s observed), well above the pipeline's
|
|
28
|
+
// 3s blanket timeout. Must exceed openskills-client's internal 15s timeout.
|
|
29
|
+
detectTimeoutMs: 20_000,
|
|
30
|
+
// Optional feature: even if detection times out, degrade instead of
|
|
31
|
+
// blocking the whole preflight (skill injection is skipped for the run).
|
|
32
|
+
degradable: {
|
|
33
|
+
disables: ["skills.enabled"],
|
|
34
|
+
warn: "skill auto-install disabled for this run",
|
|
35
|
+
},
|
|
26
36
|
async detect() {
|
|
27
37
|
const available = await isOpenSkillsAvailable();
|
|
28
38
|
if (available) {
|
package/src/checks/types.js
CHANGED
|
@@ -59,7 +59,11 @@
|
|
|
59
59
|
* @property {DegradableSpec} [degradable] - KJC-BUG-0049: si está definido y el check falla, en lugar de abortar
|
|
60
60
|
* el preflight, se desactivan los flags listados en `disables` y se emite
|
|
61
61
|
* WARN con `warn`. Útil para features opcionales (auto_pr, auto_push,
|
|
62
|
-
* sonar advisory, etc.) que no son blockers absolutos.
|
|
62
|
+
* sonar advisory, etc.) que no son blockers absolutos. También aplica
|
|
63
|
+
* cuando el detect agota su timeout (timeout → WARN, no TIMEOUT).
|
|
64
|
+
* @property {number} [detectTimeoutMs] - per-check detect timeout override. Use for checks that spawn slow
|
|
65
|
+
* tools (e.g. `npx <pkg>` cold start) where the pipeline-wide
|
|
66
|
+
* timeoutMs would kill an otherwise healthy detect.
|
|
63
67
|
*/
|
|
64
68
|
|
|
65
69
|
/**
|
|
@@ -386,8 +386,8 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
386
386
|
console.log(`⚠ rag [${RAG_RULE_ID}]: the pipeline verdict carries no session ledger — the headless pipeline does not consult the RAG yet; sealed as a warn`);
|
|
387
387
|
if (!warnIds.includes(RAG_RULE_ID)) warnIds.push(RAG_RULE_ID);
|
|
388
388
|
} else if (res.ok) {
|
|
389
|
-
const { harness, verified, mismatched } = readRagLedger(projectDir);
|
|
390
|
-
const rreq = checkRagVerdict({ config, stagedFiles: changedFiles, rag: res.verdict.rag, harness, verified, mismatched, standingExceptions: std.standing });
|
|
389
|
+
const { harness, verified, mismatched, drift } = readRagLedger(projectDir);
|
|
390
|
+
const rreq = checkRagVerdict({ config, stagedFiles: changedFiles, rag: res.verdict.rag, harness, verified, mismatched, drift, standingExceptions: std.standing });
|
|
391
391
|
if (!rreq.ok) res = { ok: false, verdict: res.verdict, reason: rreq.reason };
|
|
392
392
|
else if (rreq.mode === "granted" && rreq.grant) console.log(formatRagGrant(rreq.grant));
|
|
393
393
|
}
|
|
@@ -118,6 +118,17 @@ export const GO_CONFIGS = [{ file: ".golangci.yml", blockId: "golangci", style:
|
|
|
118
118
|
export const PHPSTAN_BODY = ["parameters:", " level: 6", " paths:", " - src", " - tests"].join("\n");
|
|
119
119
|
export const PHP_CONFIGS = [{ file: "phpstan.neon", blockId: "phpstan", style: "hash", body: PHPSTAN_BODY }];
|
|
120
120
|
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* KJC-BUG-0221 — los nombres que kj ESCRIBE en el proyecto. Lo consume la
|
|
124
|
+
* cobertura del RAG para no denunciar como drift un fichero que kj acaba de
|
|
125
|
+
* generar. Se deriva de las tablas de arriba: anadir un config aqui no obliga
|
|
126
|
+
* a acordarse de una segunda lista en otro modulo.
|
|
127
|
+
*/
|
|
128
|
+
export const GENERATED_CONFIG_FILES = new Set(
|
|
129
|
+
[...UNIVERSAL_CONFIGS, ...JS_CONFIGS, ...PY_CONFIGS, ...GO_CONFIGS, ...PHP_CONFIGS].map((c) => c.file),
|
|
130
|
+
);
|
|
131
|
+
|
|
121
132
|
/** Lint/format configs by detected language. Unknown ⇒ universal only. */
|
|
122
133
|
export const CONFIGS_BY_LANGUAGE = {
|
|
123
134
|
javascript: JS_CONFIGS,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* supports tool hooks.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
12
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
14
|
import { join } from "node:path";
|
|
14
15
|
|
|
@@ -18,7 +19,11 @@ const SCRIPT_BODY = `#!/usr/bin/env node
|
|
|
18
19
|
// so a gate bug never bricks the session.
|
|
19
20
|
import console from "node:console";
|
|
20
21
|
import process from "node:process";
|
|
21
|
-
import { existsSync } from "node:fs";
|
|
22
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
23
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
// El guard vive en <repo>/.karajan/harness/, asi que el arbol es dos arriba.
|
|
26
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
22
27
|
let raw = "";
|
|
23
28
|
process.stdin.on("data", (d) => { raw += d; });
|
|
24
29
|
process.stdin.on("end", () => {
|
|
@@ -30,10 +35,75 @@ process.stdin.on("end", () => {
|
|
|
30
35
|
process.exit(2);
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
|
-
if (tool === "Bash"
|
|
38
|
+
if (tool === "Bash") {
|
|
34
39
|
const cmd = String(input.command || "");
|
|
40
|
+
// KJC-BUG-0227: el escape tambien viaja en el PROPIO comando, como en el
|
|
41
|
+
// resto de guardias. Mirando solo process.env el agente no tenia salida:
|
|
42
|
+
// un export dentro del comando jamas llega a este proceso.
|
|
43
|
+
// Solo cuentan las asignaciones DEL PRINCIPIO, que son las que el shell
|
|
44
|
+
// aplica al comando. Buscarla en cualquier parte del texto dejaba pasar
|
|
45
|
+
// un "echo KJ_ALLOW_REWRITE=1; python3 ..." (catch de la review).
|
|
46
|
+
const toks = cmd.trim().split(/\\s+/);
|
|
47
|
+
let i = 0;
|
|
48
|
+
let named = false;
|
|
49
|
+
while (i < toks.length && /^[A-Z][A-Z0-9_]*=/.test(toks[i])) {
|
|
50
|
+
if (toks[i] === "KJ_ALLOW_REWRITE=1") named = true;
|
|
51
|
+
i += 1;
|
|
52
|
+
}
|
|
53
|
+
// Una asignacion delante solo alcanza a SU comando, asi que el escape del
|
|
54
|
+
// comando vale unicamente en un comando SIMPLE: en "VAR=1 true && python"
|
|
55
|
+
// o "VAR=1 && python" el python nunca la recibe (catch de la review). Lo
|
|
56
|
+
// que va entre comillas no encadena nada, asi que no cuenta.
|
|
57
|
+
// Tampoco es simple si hay sustitucion: el proceso de dentro de $( ) no
|
|
58
|
+
// hereda la asignacion (catch de la review). Mismo criterio que el resto
|
|
59
|
+
// de guardias del Sentinel.
|
|
60
|
+
// Dentro de comillas SIMPLES todo es literal; dentro de dobles, un ; no
|
|
61
|
+
// encadena pero $( ) si ejecuta. Por eso se miran dos cosas distintas.
|
|
62
|
+
const noSingle = cmd.replaceAll(/'[^']*'/g, "");
|
|
63
|
+
const substitutes = noSingle.includes("$(") || noSingle.includes("\\u0060");
|
|
64
|
+
const bare = noSingle.replaceAll(/"[^"]*"/g, "");
|
|
65
|
+
const simple = !substitutes && !/[;&|]/.test(bare) && !bare.includes("\\n");
|
|
66
|
+
const cmdEscape = named && simple && i < toks.length;
|
|
67
|
+
const escaped = process.env.KJ_ALLOW_REWRITE === "1" || cmdEscape;
|
|
68
|
+
// Un escape ignorado EN SILENCIO era el bug: si esta puesto y no vale, se dice.
|
|
69
|
+
if (named && !cmdEscape) console.error("kj tool gate: KJ_ALLOW_REWRITE=1 presente pero IGNORADO — solo vale delante del comando y en uno simple (sin ; | & fuera de comillas).");
|
|
35
70
|
const writes = /open\\s*\\([^)]*["'][wa]["']|>\\s*\\S+\\.json\\b/.test(cmd);
|
|
36
|
-
|
|
71
|
+
// KJC-BUG-0227: y solo se vigila lo que esta DENTRO del arbol. Un fichero
|
|
72
|
+
// temporal del scratchpad no es asunto de este guard, y bloquearlo
|
|
73
|
+
// ensenaba a rodearlo. Sin ruta reconocible se vigila: el lado seguro.
|
|
74
|
+
const targets = cmd.match(/[\\w./-]+\\.json\\b/g) || [];
|
|
75
|
+
// Solo se exime lo que se puede establecer CON CERTEZA: una ruta absoluta
|
|
76
|
+
// fuera del arbol. Una relativa depende del cwd del shell, que este hook
|
|
77
|
+
// no conoce — "cd packages && ... open(\\"../package.json\\")" escribe
|
|
78
|
+
// dentro del repo (catch de la review), asi que se sigue vigilando.
|
|
79
|
+
// Un enlace puede apuntar dentro: /tmp/link.json escribiria en el repo
|
|
80
|
+
// (catch de la review). Se resuelve el padre real antes de clasificar, y
|
|
81
|
+
// si no se puede resolver, se vigila.
|
|
82
|
+
// Fuera del arbol es SALIR de el, no que el nombre empiece por dos puntos:
|
|
83
|
+
// <repo>/..config.json esta dentro (catch de la review).
|
|
84
|
+
const escapesRoot = (rel) => rel === ".." || rel.startsWith("../");
|
|
85
|
+
const outsideForSure = (t) => {
|
|
86
|
+
if (!t.startsWith("/")) return false;
|
|
87
|
+
const abs = resolve(t);
|
|
88
|
+
try {
|
|
89
|
+
const root = realpathSync(ROOT);
|
|
90
|
+
// El propio fichero puede SER el enlace (/tmp/link.json -> repo/cfg.json),
|
|
91
|
+
// asi que se resuelve entero cuando existe; si aun no existe, manda su
|
|
92
|
+
// directorio real.
|
|
93
|
+
if (existsSync(abs)) return escapesRoot(relative(root, realpathSync(abs)));
|
|
94
|
+
const parts = abs.split("/");
|
|
95
|
+
const name = parts.pop();
|
|
96
|
+
return escapesRoot(relative(root, realpathSync(parts.join("/") || "/") + "/" + name));
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
// Con expansion del shell no se sabe a donde apunta nada: "$PWD/cfg.json"
|
|
102
|
+
// deja el trozo "/cfg.json", que parece absoluto y ajeno sin serlo (catch
|
|
103
|
+
// de la review). Ante la duda se vigila.
|
|
104
|
+
const expands = /[$~]/.test(cmd) || cmd.includes("\\u0060");
|
|
105
|
+
const mine = expands || targets.length === 0 || !targets.every(outsideForSure);
|
|
106
|
+
if (!escaped && mine && /json\\.dumps?\\s*\\(/.test(cmd) && writes) {
|
|
37
107
|
console.error("kj tool gate: reserializing a whole JSON file makes the diff unreviewable — make targeted edits instead (KJ_ALLOW_REWRITE=1 to override consciously).");
|
|
38
108
|
process.exit(2);
|
|
39
109
|
}
|
|
@@ -100,8 +170,27 @@ export function mergeClaudeHooks({ projectDir, logger = console, entries }) {
|
|
|
100
170
|
return { wired: true };
|
|
101
171
|
}
|
|
102
172
|
|
|
173
|
+
/**
|
|
174
|
+
* La raiz del proyecto: el toplevel de git, o el directorio dado si no hay git.
|
|
175
|
+
* Vive aqui y no se importa de sentinel-hooks porque ese modulo importa de
|
|
176
|
+
* este: el ciclo rompe la carga.
|
|
177
|
+
*/
|
|
178
|
+
function resolveProjectRoot(dir) {
|
|
179
|
+
try {
|
|
180
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || dir;
|
|
181
|
+
} catch {
|
|
182
|
+
return dir;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
103
186
|
/** Write the script and merge the PreToolUse entries into .claude/settings.json. */
|
|
104
187
|
export function installHarnessHooks({ projectDir = process.cwd(), logger = console } = {}) {
|
|
188
|
+
// KJC-BUG-0223: el harness es del proyecto y vive en su raiz. Instalado desde
|
|
189
|
+
// un subdirectorio sembraba ahi un segundo `.karajan/harness` que no gobierna
|
|
190
|
+
// nada, y el gate seguia mirando otro sitio.
|
|
191
|
+
const root = resolveProjectRoot(projectDir);
|
|
192
|
+
if (root !== projectDir) logger?.info?.(`kj harden: el harness es del proyecto, se instala en su raíz (${root})`);
|
|
193
|
+
projectDir = root;
|
|
105
194
|
const scriptAbs = writeHarnessScript(projectDir, "pretooluse.mjs", SCRIPT_BODY);
|
|
106
195
|
const { wired } = mergeClaudeHooks({
|
|
107
196
|
projectDir,
|
|
@@ -1198,7 +1198,11 @@ export function verifySentinelScripts({ projectDir, readFileFn = readFileSync, g
|
|
|
1198
1198
|
ok: true,
|
|
1199
1199
|
mismatched: [],
|
|
1200
1200
|
drift,
|
|
1201
|
-
|
|
1201
|
+
// KJC-BUG-0222: `kj harden` NO pone al dia un guard sellado, lo deja en paz
|
|
1202
|
+
// a proposito (KJC-BUG-0193), asi que mandarlo era un bucle: ejecutas,
|
|
1203
|
+
// sale "unchanged" y el gate repite lo mismo. El que resuelve ESTE estado
|
|
1204
|
+
// es `kj harden --commit`, y es acto del humano.
|
|
1205
|
+
reason: "el harness instalado está sellado y las plantillas de este árbol van por delante — no es manipulación, y `kj harden` lo deja como está a propósito: quien lo avanza es el humano con `kj harden --commit`",
|
|
1202
1206
|
};
|
|
1203
1207
|
}
|
|
1204
1208
|
|
|
@@ -1214,6 +1218,13 @@ export function verifySentinelScripts({ projectDir, readFileFn = readFileSync, g
|
|
|
1214
1218
|
* guard is still the right move.
|
|
1215
1219
|
*/
|
|
1216
1220
|
export function installSentinelHooks({ projectDir = process.cwd(), logger = console, human = false, gitShowFn = null } = {}) {
|
|
1221
|
+
// KJC-BUG-0223: instalado desde un subdirectorio, esto sembraba un segundo
|
|
1222
|
+
// `.karajan/harness` ahi, y el gate (que mira otro sitio) pasaba a verlo. El
|
|
1223
|
+
// ritual quedaba "arreglado" y el arbol se llenaba de arneses huerfanos que
|
|
1224
|
+
// no gobiernan nada. El harness es del proyecto: va a su raiz.
|
|
1225
|
+
const root = resolveSentinelRoot(projectDir);
|
|
1226
|
+
if (root !== projectDir) logger?.info?.(`kj harden: el harness es del proyecto, se instala en su raíz (${root})`);
|
|
1227
|
+
projectDir = root;
|
|
1217
1228
|
const sealed = human ? new Map() : sealedByPath(projectDir, gitShowFn);
|
|
1218
1229
|
const deferred = [];
|
|
1219
1230
|
const keepsItsSeal = (name) => {
|
|
@@ -322,7 +322,7 @@ export async function runPreLoopStages({ config, logger, emitter, eventBase, ses
|
|
|
322
322
|
// Hard fail if blocking checks failed (SonarQube enabled but not available)
|
|
323
323
|
if (!preflightResult.ok) {
|
|
324
324
|
const errorLines = (preflightResult.errors || [])
|
|
325
|
-
.map(e => ` - ${e.message}
|
|
325
|
+
.map(e => ` - ${e.message}${e.fix ? `\n Fix: ${e.fix}` : ""}`)
|
|
326
326
|
.join("\n");
|
|
327
327
|
throw new Error(
|
|
328
328
|
`Preflight FAILED — environment changed during session. Fix the issues and retry:\n${errorLines}`
|
package/src/rag/coverage.js
CHANGED
|
@@ -10,8 +10,27 @@
|
|
|
10
10
|
import { execa } from "execa";
|
|
11
11
|
import { extname, isAbsolute, normalize, relative } from "node:path";
|
|
12
12
|
import { detectAdaptersForProject, buildMatchers } from "../lang/registry.js";
|
|
13
|
+
import { GENERATED_CONFIG_FILES } from "../harden/config-templates.js";
|
|
13
14
|
import { getLastIndexedCommit, projectSlug } from "./vec-store.js";
|
|
14
15
|
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* KJC-BUG-0221 — `kj harden` instala commitlint.config.js y eslint.config.js, y
|
|
19
|
+
* la cobertura los exigia indexados, asi que `kj check` denunciaba como drift
|
|
20
|
+
* dos ficheros que kj mismo acababa de escribir: la herramienta creando el
|
|
21
|
+
* fichero y quejandose de el. El criterio es el que ya usa el presupuesto de
|
|
22
|
+
* LOC (src/review/loc-budget.js): lo generado no cuenta, porque nadie lo
|
|
23
|
+
* escribio, y el RAG existe para responder sobre el codigo del equipo.
|
|
24
|
+
*
|
|
25
|
+
* La lista sale de la propia tabla de plantillas de harden, para que anadir un
|
|
26
|
+
* config no obligue a acordarse de esta otra lista.
|
|
27
|
+
*/
|
|
28
|
+
export function isGeneratedByHarden(rel) {
|
|
29
|
+
if (!rel) return false;
|
|
30
|
+
const base = rel.split("/").pop();
|
|
31
|
+
return GENERATED_CONFIG_FILES.has(base);
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
35
|
* @param {string} projectDir
|
|
17
36
|
* @param {{db: object}} deps - an open vec store
|
|
@@ -21,7 +40,10 @@ export async function ragIndexCoverage(projectDir, { db }) {
|
|
|
21
40
|
const project = projectSlug(projectDir);
|
|
22
41
|
const matchers = buildMatchers(detectAdaptersForProject(projectDir));
|
|
23
42
|
const { stdout } = await execa("git", ["-C", projectDir, "ls-files", "-z"]);
|
|
24
|
-
const
|
|
43
|
+
const all = stdout.split("\0").filter((p) => p && matchers.isCodeFile(p) && !matchers.shouldSkip(p));
|
|
44
|
+
// Lo que kj genera se informa aparte: ni se exige indexado ni se esconde.
|
|
45
|
+
const generated = all.filter(isGeneratedByHarden);
|
|
46
|
+
const sources = all.filter((p) => !isGeneratedByHarden(p));
|
|
25
47
|
|
|
26
48
|
const rows = db.prepare("SELECT DISTINCT source FROM chunks WHERE kind = 'code' AND project_slug = ?").all(project);
|
|
27
49
|
const indexed = new Set(rows.map((r) => (isAbsolute(r.source) ? relative(projectDir, r.source) : r.source)));
|
|
@@ -44,7 +66,7 @@ export async function ragIndexCoverage(projectDir, { db }) {
|
|
|
44
66
|
stale = sources.filter((p) => indexed.has(p) && changed.has(p));
|
|
45
67
|
}
|
|
46
68
|
|
|
47
|
-
return { project, total: sources.length, indexed: sources.length - missing.length, missing, stale, lastIndexedCommit, absent: indexed.size === 0 };
|
|
69
|
+
return { project, total: sources.length, indexed: sources.length - missing.length, missing, stale, generated, lastIndexedCommit, absent: indexed.size === 0 };
|
|
48
70
|
}
|
|
49
71
|
|
|
50
72
|
/**
|
|
@@ -57,7 +57,29 @@ export async function ensureContractBlockPresent(projectDir) {
|
|
|
57
57
|
* La ruta se pregunta a git para que funcione tambien en un worktree, donde
|
|
58
58
|
* `.git` es un fichero y no un directorio.
|
|
59
59
|
*/
|
|
60
|
-
const LOCAL_ARTIFACTS = [
|
|
60
|
+
const LOCAL_ARTIFACTS = [
|
|
61
|
+
".reviews/", ".kj/", ".kj-ready.json",
|
|
62
|
+
// KJC-BUG-0220: lo que kj escribe DENTRO de .karajan/ y NO viaja. Sin esto
|
|
63
|
+
// se quedaba en tierra de nadie (ni versionado ni ignorado) y salia como `??`
|
|
64
|
+
// en cada git status del equipo. Se enumera en vez de ignorar .karajan/
|
|
65
|
+
// entero a proposito: kj.config.yml y las reglas del agente son del equipo, y
|
|
66
|
+
// esconderselas seria decidir por ellos. Los ficheros del supervisor viajan
|
|
67
|
+
// por su cauce sellado (ADR 0009) y tampoco entran aqui.
|
|
68
|
+
".karajan/harness/", // el arnes del anfitrion: cada maquina lo regenera
|
|
69
|
+
".karajan/policy-decisions.jsonl",
|
|
70
|
+
".karajan/policy-exceptions.jsonl",
|
|
71
|
+
".karajan/identity.local.yml", // identidad por clon (ADR 0005)
|
|
72
|
+
".karajan/sentinel-state.json",
|
|
73
|
+
".karajan/rag.db",
|
|
74
|
+
".karajan/audit-history.db",
|
|
75
|
+
".karajan/update-check.json",
|
|
76
|
+
".karajan/karajan.env",
|
|
77
|
+
".karajan/agent-skills/",
|
|
78
|
+
".karajan/audits/",
|
|
79
|
+
".karajan/reviews/",
|
|
80
|
+
".karajan/steward/",
|
|
81
|
+
".karajan/plans/",
|
|
82
|
+
];
|
|
61
83
|
|
|
62
84
|
export async function excludeLocalArtifacts(projectDir, run = runCommand) {
|
|
63
85
|
let file = path.join(projectDir, ".git", "info", "exclude");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj-provenance — KJC-BUG-0222. Un gate que dice QUE falla sin decir CONTRA QUE
|
|
3
|
+
* compara ensena a saltarselo. El aviso del harness decia "does not match the
|
|
4
|
+
* installed kj" y ahi acababa, asi que en campo se leyo como "kj se
|
|
5
|
+
* desincroniza solo": una sesion se monto el ritual de ejecutar `kj harden`
|
|
6
|
+
* antes de cada review y de cada commit, veinte veces en un dia, sin
|
|
7
|
+
* preguntarse por que. La causa estaba entera en el dato que faltaba: ese kj
|
|
8
|
+
* estaba npm-linkado a un arbol de desarrollo cuyas plantillas cambiaron seis
|
|
9
|
+
* veces en dos dias.
|
|
10
|
+
*
|
|
11
|
+
* Saltarse un gate ritualmente es peor que no tenerlo, porque parece que se
|
|
12
|
+
* cumple.
|
|
13
|
+
*/
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { dirname, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Que kj esta juzgando: version, de donde sale y si es un paquete instalado o
|
|
22
|
+
* un arbol de desarrollo enlazado. Nunca lanza: esto SOLO adorna un mensaje, y
|
|
23
|
+
* un fallo aqui jamas puede cambiar el veredicto de un gate.
|
|
24
|
+
*
|
|
25
|
+
* @returns {{version: string|null, root: string, linked: boolean, label: string}}
|
|
26
|
+
*/
|
|
27
|
+
export function kjProvenance(moduleDir = here) {
|
|
28
|
+
const root = resolve(moduleDir, "..", "..");
|
|
29
|
+
let version = null;
|
|
30
|
+
try {
|
|
31
|
+
version = createRequire(import.meta.url)(resolve(root, "package.json")).version || null;
|
|
32
|
+
} catch { /* sin manifest legible: se dice lo que se sabe */ }
|
|
33
|
+
// Un kj instalado vive bajo node_modules. Cualquier otra ruta es un arbol de
|
|
34
|
+
// trabajo enlazado (npm link), y entonces sus plantillas se mueven con cada
|
|
35
|
+
// edicion de quien lo desarrolla, en TODOS los proyectos de la maquina.
|
|
36
|
+
const linked = !root.includes(`${"node_modules"}/karajan-code`) && !root.endsWith("node_modules");
|
|
37
|
+
const v = version ? `kj ${version}` : "an unknown kj";
|
|
38
|
+
const label = linked
|
|
39
|
+
? `${v} LINKED from ${root} (a development tree: its templates move with every edit there, in every project on this machine)`
|
|
40
|
+
: `${v} (${root})`;
|
|
41
|
+
return { version, root, linked, label };
|
|
42
|
+
}
|
package/src/review/rag-ledger.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
-
import { verifySentinelScripts } from "../harden/sentinel-hooks.js";
|
|
12
|
+
import { resolveSentinelRoot, verifySentinelScripts } from "../harden/sentinel-hooks.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @param {string} projectDir
|
|
@@ -22,10 +22,18 @@ import { verifySentinelScripts } from "../harden/sentinel-hooks.js";
|
|
|
22
22
|
* the requirement fails closed until the human re-runs `kj harden`.
|
|
23
23
|
*/
|
|
24
24
|
export function readRagLedger(projectDir) {
|
|
25
|
-
|
|
25
|
+
// KJC-BUG-0223: el harness vive en la RAIZ del proyecto. Este modulo miraba
|
|
26
|
+
// el directorio recibido (en la practica, process.cwd()) mientras el
|
|
27
|
+
// verificador resolvia el toplevel de git, asi que desde un subdirectorio los
|
|
28
|
+
// dos juzgaban sobre arboles distintos y el gate bloqueaba siempre.
|
|
29
|
+
const root = resolveSentinelRoot(projectDir);
|
|
30
|
+
const harnessDir = path.join(root, ".karajan", "harness");
|
|
26
31
|
const harness = existsSync(harnessDir);
|
|
27
|
-
const check = harness ? verifySentinelScripts({ projectDir }) : { ok: false, mismatched: [] };
|
|
28
|
-
|
|
32
|
+
const check = harness ? verifySentinelScripts({ projectDir: root }) : { ok: false, mismatched: [] };
|
|
33
|
+
// KJC-BUG-0222: `drift` viaja hasta el mensaje. Un guard sellado que se ha
|
|
34
|
+
// quedado atras NO lo arregla `kj harden` (lo deja en paz a proposito), asi
|
|
35
|
+
// que mandar ese comando para ese fichero es un bucle.
|
|
36
|
+
const base = { root, harness, verified: harness && check.ok === true, mismatched: check.mismatched || [], drift: check.drift || [], queries: [], hits: [] };
|
|
29
37
|
let state;
|
|
30
38
|
try {
|
|
31
39
|
state = JSON.parse(readFileSync(path.join(harnessDir, "sentinel-state.json"), "utf8"));
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* concepts that are not in the diff. That list travels to the reviewer,
|
|
13
13
|
* because the bugs of 2026-09-16 were exactly the twin nobody touched.
|
|
14
14
|
*/
|
|
15
|
+
import { kjProvenance } from "./kj-provenance.js";
|
|
15
16
|
import { sourceFilesOf } from "./tests-with-code.js";
|
|
16
17
|
|
|
17
18
|
export const RAG_RULE_ID = "method.rag.code";
|
|
@@ -30,6 +31,17 @@ function liveGrant(standingExceptions, now) {
|
|
|
30
31
|
* @param {{available: boolean, reason?: string, queries: object[], hits: string[]}} [args.ledger]
|
|
31
32
|
* @returns {{ok: boolean, mode: "docs-only"|"pass"|"granted"|"block", reason?: string, grant?: object, sources?: string[], queries?: number, covered?: string[], uncovered?: string[], twinsUntouched?: string[]}}
|
|
32
33
|
*/
|
|
34
|
+
/**
|
|
35
|
+
* KJC-BUG-0222 — el comando que resuelve ESTE estado. `kj harden` repone lo que
|
|
36
|
+
* nadie sello; un guard sellado que se ha quedado atras lo deja en paz a
|
|
37
|
+
* proposito (KJC-BUG-0193), y avanzarlo es acto humano.
|
|
38
|
+
*/
|
|
39
|
+
function harnessRemedy(drift = []) {
|
|
40
|
+
return drift.length > 0
|
|
41
|
+
? `the human runs \`kj harden\` to restore them, and \`kj harden --commit\` for the sealed ones (${drift.join(", ")}), which \`kj harden\` leaves untouched on purpose`
|
|
42
|
+
: "the human runs `kj harden` to regenerate it";
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
export function checkRagRequirement({ config = {}, stagedFiles = [], newFiles = [], ledger, standingExceptions = [], now = new Date(), env = {} }) {
|
|
34
46
|
const { sources } = sourceFilesOf(config, stagedFiles);
|
|
35
47
|
if (sources.length === 0) return { ok: true, mode: "docs-only" };
|
|
@@ -55,7 +67,9 @@ export function checkRagRequirement({ config = {}, stagedFiles = [], newFiles =
|
|
|
55
67
|
// than the ledger) cannot vouch for anything: fail closed, like the tamper
|
|
56
68
|
// check does, until the human regenerates it.
|
|
57
69
|
if (ledger?.harness === true && ledger?.verified === false) {
|
|
58
|
-
|
|
70
|
+
// KJC-BUG-0222: decir CONTRA QUE no coincide. Sin ese dato el aviso se lee
|
|
71
|
+
// como "kj se desincroniza solo" y se automatiza el sintoma.
|
|
72
|
+
return { ok: false, mode: "block", sources, reason: `${head} — the installed Sentinel harness does not match the templates of ${kjProvenance().label} (${(ledger.mismatched || []).join(", ") || "scripts missing"}): ${harnessRemedy(ledger.drift)} before code is reviewed. ${GRANT_HINT}` };
|
|
59
73
|
}
|
|
60
74
|
if (!ledger?.available) {
|
|
61
75
|
return { ok: false, mode: "block", sources, reason: `${head} — ${ledger?.reason || "no session ledger"}: consult the RAG about the change (kj_rag_query / kj rag query) and run \`kj review --staged\` again. ${GRANT_HINT}` };
|
|
@@ -82,7 +96,7 @@ export function checkRagRequirement({ config = {}, stagedFiles = [], newFiles =
|
|
|
82
96
|
* block — the diff hash pins the file set, so a source missing from
|
|
83
97
|
* `covered` was never answered about, whatever the block claims.
|
|
84
98
|
*/
|
|
85
|
-
export function checkRagVerdict({ config = {}, stagedFiles = [], rag, harness = true, verified = true, mismatched = [], standingExceptions = [], now = new Date() }) {
|
|
99
|
+
export function checkRagVerdict({ config = {}, stagedFiles = [], rag, harness = true, verified = true, mismatched = [], drift = [], standingExceptions = [], now = new Date() }) {
|
|
86
100
|
const { sources } = sourceFilesOf(config, stagedFiles);
|
|
87
101
|
if (sources.length === 0) return { ok: true, mode: "docs-only" };
|
|
88
102
|
const grant = liveGrant(standingExceptions, now);
|
|
@@ -94,7 +108,7 @@ export function checkRagVerdict({ config = {}, stagedFiles = [], rag, harness =
|
|
|
94
108
|
return { ok: false, mode: "block", sources, reason: `${head} — no Sentinel harness in this tree, so no session ledger can exist: install it with \`kj harden\`. ${GRANT_HINT}` };
|
|
95
109
|
}
|
|
96
110
|
if (!verified) {
|
|
97
|
-
return { ok: false, mode: "block", sources, reason: `${head} — the Sentinel harness does not match the
|
|
111
|
+
return { ok: false, mode: "block", sources, reason: `${head} — the installed Sentinel harness does not match the templates of ${kjProvenance().label} (${mismatched.join(", ") || "scripts missing"}): ${harnessRemedy(drift)}. ${GRANT_HINT}` };
|
|
98
112
|
}
|
|
99
113
|
if (!rag) {
|
|
100
114
|
return { ok: false, mode: "block", sources, reason: `${head} — the verdict carries no rag block; run \`kj review --staged\` again. ${GRANT_HINT}` };
|
package/src/sonar/api.js
CHANGED
|
@@ -82,8 +82,17 @@ async function sonarFetch(config, urlPath) {
|
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* KJC-BUG-0226 (issue #1838) — de donde sale la clave que se CONSULTA. El
|
|
87
|
+
* scanner sube el analisis con la que el repo declara en su
|
|
88
|
+
* sonar-project.properties; preguntando por otra (la derivada del remote), el
|
|
89
|
+
* audit acababa listando issues de repos ajenos del mismo SonarQube. Pasando
|
|
90
|
+
* el directorio, la resolucion lee esa misma clave declarada.
|
|
91
|
+
*/
|
|
92
|
+
const queryCwd = (config) => config?.projectDir || process.cwd();
|
|
93
|
+
|
|
85
94
|
export async function getQualityGateStatus(config, projectKey = null) {
|
|
86
|
-
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey });
|
|
95
|
+
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey, cwd: queryCwd(config) });
|
|
87
96
|
const body = await sonarFetch(config, `/api/qualitygates/project_status?projectKey=${effectiveProjectKey}`);
|
|
88
97
|
|
|
89
98
|
try {
|
|
@@ -95,7 +104,7 @@ export async function getQualityGateStatus(config, projectKey = null) {
|
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
export async function getOpenIssues(config, projectKey = null) {
|
|
98
|
-
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey });
|
|
107
|
+
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey, cwd: queryCwd(config) });
|
|
99
108
|
const body = await sonarFetch(config, `/api/issues/search?projectKeys=${effectiveProjectKey}&statuses=OPEN`);
|
|
100
109
|
|
|
101
110
|
let parsedIssues = [];
|
package/src/sonar/project-key.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
2
5
|
import { runCommand } from "../utils/process.js";
|
|
3
6
|
|
|
4
7
|
function slug(value) {
|
|
@@ -55,6 +58,27 @@ function canonicalRepoId(remoteUrl) {
|
|
|
55
58
|
return `${host}/${segments.join("/")}`;
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
/**
|
|
62
|
+
* KJC-BUG-0226 (issue #1838) — la clave que el REPO declara en su
|
|
63
|
+
* sonar-project.properties es la que el servidor conoce, porque es con la que
|
|
64
|
+
* el scanner sube el analisis. Sin leerla aqui, kj escaneaba un proyecto y
|
|
65
|
+
* preguntaba por otro (una clave derivada del remote), y el audit acababa
|
|
66
|
+
* listando issues de repos ajenos del mismo SonarQube.
|
|
67
|
+
*
|
|
68
|
+
* Sincrono y tolerante: sin fichero, ilegible o sin la clave, se sigue por
|
|
69
|
+
* donde se iba.
|
|
70
|
+
*/
|
|
71
|
+
function declaredProjectKey(cwd) {
|
|
72
|
+
try {
|
|
73
|
+
const raw = fs.readFileSync(path.join(cwd, "sonar-project.properties"), "utf8");
|
|
74
|
+
for (const line of raw.split("\n")) {
|
|
75
|
+
const m = /^\s*sonar\.projectKey\s*=\s*(.+?)\s*$/.exec(line);
|
|
76
|
+
if (m) return m[1];
|
|
77
|
+
}
|
|
78
|
+
} catch { /* sin properties legible: se deriva como siempre */ }
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
export async function resolveSonarProjectKey(config, options = {}) {
|
|
59
83
|
const explicit = String(
|
|
60
84
|
options.projectKey || process.env.KJ_SONAR_PROJECT_KEY || config?.sonarqube?.project_key || ""
|
|
@@ -63,6 +87,15 @@ export async function resolveSonarProjectKey(config, options = {}) {
|
|
|
63
87
|
return normalizeProjectKey(explicit);
|
|
64
88
|
}
|
|
65
89
|
|
|
90
|
+
// KJC-BUG-0226: lo que el repo declara manda sobre lo que kj derivaria, que
|
|
91
|
+
// es justo lo que el scanner ya hace al subir el analisis. Solo con un `cwd`
|
|
92
|
+
// explicito: quien no dice que directorio le importa se queda como estaba, y
|
|
93
|
+
// asi esto no cambia el comportamiento de nadie mas que el de la consulta.
|
|
94
|
+
if (options.cwd) {
|
|
95
|
+
const declared = declaredProjectKey(options.cwd);
|
|
96
|
+
if (declared) return normalizeProjectKey(declared);
|
|
97
|
+
}
|
|
98
|
+
|
|
66
99
|
const remote = await runCommand("git", ["config", "--get", "remote.origin.url"]);
|
|
67
100
|
const remoteUrl = String(remote.stdout || "").trim();
|
|
68
101
|
if (remote.exitCode !== 0 || !remoteUrl) {
|
package/src/sonar/scanner.js
CHANGED
|
@@ -50,8 +50,38 @@ export function indexedFilesFrom(scanOutput) {
|
|
|
50
50
|
return files;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* KJC-BUG-0225 — `.git` es un fichero (no un directorio) en un worktree
|
|
55
|
+
* enlazado y en un submodulo: exactamente los dos casos que JGit no sabe abrir.
|
|
56
|
+
* Sin proceso y sin depender de que git responda.
|
|
57
|
+
*/
|
|
58
|
+
export function isLinkedWorktree(dir) {
|
|
59
|
+
try {
|
|
60
|
+
return fs.statSync(path.join(dir, ".git")).isFile();
|
|
61
|
+
} catch {
|
|
62
|
+
return false; // sin .git no hay SCM que desactivar
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* KJC-BUG-0225 (issue #1839) — dentro de un carril de `kj worktree start`, el
|
|
68
|
+
* plugin SCM del scanner (JGit) no sabe abrir un worktree enlazado (`.git` es
|
|
69
|
+
* un fichero que apunta a `.git/worktrees/<carril>`) y revienta el analisis
|
|
70
|
+
* entero. El pre-gate lo reportaba como "UNAVAILABLE" y la ejecucion seguia:
|
|
71
|
+
* todo cambio hecho como el metodo manda, un carril por tarea, se quedaba sin
|
|
72
|
+
* quality gate y sin un rojo que lo dijera.
|
|
73
|
+
*
|
|
74
|
+
* Se desactiva SOLO el SCM, que en un carril no aporta nada (alimenta blame y
|
|
75
|
+
* asignacion de autoria); las reglas, la cobertura y el resto del analisis se
|
|
76
|
+
* quedan exactamente igual.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} projectKey
|
|
79
|
+
* @param {object} scanner config del scanner (scm_disabled lo fuerza siempre)
|
|
80
|
+
* @param {{linkedWorktree?: boolean}} [where]
|
|
81
|
+
*/
|
|
82
|
+
export function buildScannerOpts(projectKey, scanner = {}, where = {}) {
|
|
54
83
|
const opts = [`-Dsonar.projectKey=${projectKey}`];
|
|
84
|
+
if (scanner.scm_disabled || where.linkedWorktree) opts.push("-Dsonar.scm.disabled=true");
|
|
55
85
|
// KJC-TSK-0838: verbose is what makes the scanner name every indexed file.
|
|
56
86
|
if (scanner.verbose) opts.push("-Dsonar.verbose=true");
|
|
57
87
|
if (scanner.sources) opts.push(`-Dsonar.sources=${scanner.sources}`);
|
|
@@ -317,11 +347,16 @@ export async function runSonarScan(config, projectKey = null, { verbose = false,
|
|
|
317
347
|
: null;
|
|
318
348
|
|
|
319
349
|
const pick = await pickSonarScanner(sonarConfig.scanner);
|
|
350
|
+
// KJC-BUG-0225: en un carril, el plugin SCM del scanner (JGit) no sabe abrir
|
|
351
|
+
// el worktree enlazado y tumba el analisis entero. La senal es la causa
|
|
352
|
+
// literal del error y no necesita proceso: en un worktree enlazado (y en un
|
|
353
|
+
// submodulo, que JGit trata igual) `.git` es un FICHERO, no un directorio.
|
|
354
|
+
const linkedWorktree = isLinkedWorktree(scanCwd);
|
|
320
355
|
const env = {
|
|
321
356
|
...process.env,
|
|
322
357
|
SONAR_HOST_URL: pick.type === "native" ? rawHost : host,
|
|
323
358
|
SONAR_TOKEN: token || "",
|
|
324
|
-
SONAR_SCANNER_OPTS: buildScannerOpts(effectiveProjectKey, scannerConfig)
|
|
359
|
+
SONAR_SCANNER_OPTS: buildScannerOpts(effectiveProjectKey, scannerConfig, { linkedWorktree })
|
|
325
360
|
};
|
|
326
361
|
|
|
327
362
|
let cmd, args;
|