karajan-code 4.37.0 → 4.39.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.
@@ -117,6 +117,16 @@ export async function commitSupervisorRegeneration({
117
117
  `harden --commit es un acto humano y este proceso desciende de un agente (${anc.match}) — ni con pty falso ni con el entorno limpio (ADR 0009)`,
118
118
  );
119
119
  }
120
+ const run = gitFn || ((args) => execFileSync("git", args, { cwd: projectDir, encoding: "utf8" }));
121
+ // KJC-BUG-0244 (grebla #958): el commit sellado solo lleva el supervisor, pero
122
+ // un stage ajeno se quedaba en esta rama y el siguiente commit caía aquí con la
123
+ // card equivocada. Se para ANTES de pedir nada al humano y se nombra.
124
+ const foreign = run(["diff", "--cached", "--name-only"]).split("\n").filter((f) => f && !f.startsWith(HOOKS_PREFIX) && f !== PROVENANCE_FILE);
125
+ if (foreign.length > 0) {
126
+ throw new Error(
127
+ `harden --commit: hay cambios en el stage que no son del supervisor (${foreign.slice(0, 5).join(", ")}${foreign.length > 5 ? ", …" : ""}): commitéalos en su rama o sácalos del stage (git restore --staged <ficheros>) antes de sellar`,
128
+ );
129
+ }
120
130
  // Capa 4 (test adversarial 6-sep: un huérfano a init con pty falso y
121
131
  // prompts a ciegas llegó hasta aquí): nonce aleatorio tecleado de vuelta.
122
132
  // Un alimentador ciego no conoce el código; automatizar su lectura exige
@@ -126,7 +136,6 @@ export async function commitSupervisorRegeneration({
126
136
  if (answer !== nonce) {
127
137
  throw new Error(`harden --commit: confirmación humana fallida (esperaba "${nonce}") — ADR 0009`);
128
138
  }
129
- const run = gitFn || ((args) => execFileSync("git", args, { cwd: projectDir, encoding: "utf8" }));
130
139
  const drift = supervisorDrift({ projectDir, gitFn: run });
131
140
  // La provenance describe SIEMPRE el estado COMPLETO del supervisor (cazado
132
141
  // en el primer estreno real: un sello parcial pisaba al anterior y dejaba
@@ -24,7 +24,7 @@ const sha256 = (buf) => createHash("sha256").update(buf).digest("hex");
24
24
  * @returns {{files: Set<string>, reason?: string}} rutas repo-relativas de
25
25
  * supervisor cuyo estado actual está respaldado por la provenance sellada.
26
26
  */
27
- export function verifiedSupervisorFiles({ projectDir }) {
27
+ export function verifiedSupervisorFiles({ projectDir, trackedOnly = false }) {
28
28
  let prov;
29
29
  try {
30
30
  prov = JSON.parse(readFileSync(resolve(projectDir, PROVENANCE_FILE), "utf8"));
@@ -37,7 +37,11 @@ export function verifiedSupervisorFiles({ projectDir }) {
37
37
  }
38
38
  const ok = new Set();
39
39
  const unjudgeable = new Set();
40
- const entries = Array.isArray(prov?.files) ? prov.files : [];
40
+ // KJC-BUG-0259 (user's decision, option A): history is judged with the kj of
41
+ // when it was committed. The harness guards are machine-local and never in the
42
+ // repo; the review and each PR's CI verify them when the seal is made. A check
43
+ // over the repo's history (release check) judges only what is versioned.
44
+ const entries = (Array.isArray(prov?.files) ? prov.files : []).filter((e) => !trackedOnly || !String(e?.file).startsWith(HARNESS_PREFIX));
41
45
  // KJC-BUG-0211: desde KJC-BUG-0197 el sello cubre tambien los guardias del
42
46
  // supervisor, y aqui solo se sabian verificar los hooks. Como `complete`
43
47
  // exige que TODAS las entradas verifiquen, el sello no se podia ni commitear.
@@ -93,10 +97,10 @@ export function verifiedSupervisorFiles({ projectDir }) {
93
97
  * `note` dice lo que este sitio NO ha podido juzgar, para que un levantamiento
94
98
  * mas laxo de lo que parece no pase por silencio (KJC-BUG-0212).
95
99
  */
96
- export function liftSealedSupervisorViolations({ projectDir, violations }) {
100
+ export function liftSealedSupervisorViolations({ projectDir, violations, trackedOnly = false }) {
97
101
  const RULE = "defaults.supervisor.write";
98
102
  if (!violations.some((v) => v.rule_id === RULE && v.file)) return { violations, lifted: 0 };
99
- const sealed = verifiedSupervisorFiles({ projectDir });
103
+ const sealed = verifiedSupervisorFiles({ projectDir, trackedOnly });
100
104
  // La violación sobre la PROPIA provenance solo se alza si la provenance
101
105
  // entera verificó — a medias no describe la verdad y sigue denegada.
102
106
  const liftable = (v) =>
@@ -7,11 +7,22 @@
7
7
  * toca: borrarla es decision del usuario, no efecto de una migracion.
8
8
  */
9
9
  import { existsSync } from "node:fs";
10
+ import { sep } from "node:path";
10
11
 
11
12
  import { openVecStore, insertChunk, getLastIndexedCommit, setLastIndexedCommit } from "./vec-store.js";
12
13
 
13
14
  export const countProjectChunks = (db, slug) => db.prepare("SELECT COUNT(*) AS n FROM chunks WHERE project_slug = ?").get(slug).n;
14
15
 
16
+ /**
17
+ * KJC-BUG-0258: chunks of the project's OWN files (sources under projectDir), as
18
+ * opposed to its plans and briefs. A prefix compare, not LIKE: paths may hold % or _.
19
+ */
20
+ export const countProjectSources = (db, slug, projectDir) => {
21
+ // path.sep: sources are stored as native paths (backslashes on Windows).
22
+ const prefix = projectDir.replace(/[\\/]+$/, "") + sep;
23
+ return db.prepare("SELECT COUNT(*) AS n FROM chunks WHERE project_slug = ? AND substr(source, 1, ?) = ?").get(slug, prefix.length, prefix).n;
24
+ };
25
+
15
26
  /**
16
27
  * KJC-TSK-0882: que decir cuando el indice del proyecto no tiene nada suyo.
17
28
  * Si la base global tiene sus chunks, migrar (segundos); si no, indexar.
@@ -29,9 +40,12 @@ export function emptyIndexRemedy({ slug, legacyPath, dim = 768 }) {
29
40
  }
30
41
 
31
42
  /**
32
- * @returns {{state: "migrated"|"already"|"nothing", migrated: number, reason: string}}
43
+ * KJC-BUG-0255: `keep(source)` is the indexer's criterion today (generated,
44
+ * excluded, .ragignore, sensitive, gone). The global index was filled before
45
+ * those rules, so copying it blind brought vendor/*.min.js along as noise.
46
+ * @returns {{state: "migrated"|"already"|"nothing", migrated: number, skipped?: number, reason: string}}
33
47
  */
34
- export function migrateProjectIndex({ slug, legacyPath, targetPath, dim = 768 }) {
48
+ export function migrateProjectIndex({ slug, legacyPath, targetPath, dim = 768, keep = () => true }) {
35
49
  const nothing = { state: "nothing", migrated: 0, reason: `the global index holds nothing for ${slug}: kj rag index --with-sources` };
36
50
  if (!existsSync(legacyPath)) return nothing;
37
51
 
@@ -49,8 +63,10 @@ export function migrateProjectIndex({ slug, legacyPath, targetPath, dim = 768 })
49
63
  const rows = legacy.prepare("SELECT id, source, kind, text, metadata, content_hash FROM chunks WHERE project_slug = ?").all(slug);
50
64
  if (rows.length === 0) return nothing;
51
65
  const vecOf = legacy.prepare("SELECT embedding FROM vec_chunks WHERE rowid = ?");
66
+ let skipped = 0;
52
67
  const copy = target.transaction(() => {
53
68
  for (const r of rows) {
69
+ if (!keep(r.source)) { skipped++; continue; }
54
70
  const raw = vecOf.get(BigInt(r.id))?.embedding;
55
71
  if (!raw) continue; // un chunk sin vector no se puede buscar: no se inventa
56
72
  const embedding = new Float32Array(raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength));
@@ -63,7 +79,8 @@ export function migrateProjectIndex({ slug, legacyPath, targetPath, dim = 768 })
63
79
  const migrated = countProjectChunks(target, slug);
64
80
  const parts = [`${migrated} chunks of ${slug} copied with their embeddings`];
65
81
  if (stamp) parts.push(`indexed at ${stamp.slice(0, 9)}`);
66
- return { state: "migrated", migrated, reason: parts.join(", ") };
82
+ if (skipped) parts.push(`${skipped} left out (the indexer would not take them today: generated, excluded or gone)`);
83
+ return { state: "migrated", migrated, skipped, reason: parts.join(", ") };
67
84
  } finally {
68
85
  legacy.close();
69
86
  }
@@ -4,7 +4,8 @@
4
4
  * hu-board → the branch must reference a LIVE card (block by default);
5
5
  * planning-game/external → card-shaped reference required (warn default,
6
6
  * block via method_gates.card_first). Exemptions explicit and visible:
7
- * base branch, configured prefixes (releases), KJ_ALLOW_NO_CARD=1.
7
+ * base branch and configured prefixes (releases). No escape (ADR 0015): a
8
+ * gate an agent can switch off is a suggestion.
8
9
  */
9
10
  import { listPlans, loadPlan } from "../plan/plan-store.js";
10
11
  import { escapeRegExp } from "../utils/escape-regexp.js";
@@ -48,10 +49,7 @@ async function findLiveCardInBranch(projectDir, branch) {
48
49
  return closedHit;
49
50
  }
50
51
 
51
- export async function checkCardFirst({ config = {}, projectDir = process.cwd(), branch, env = process.env, deps = {} }) {
52
- if (env.KJ_ALLOW_NO_CARD === "1") {
53
- return { ok: true, mode: "exempt", reason: "KJ_ALLOW_NO_CARD=1 (explicit escape hatch)" };
54
- }
52
+ export async function checkCardFirst({ config = {}, projectDir = process.cwd(), branch, deps = {} }) {
55
53
  const exempt = config.method_gates?.card_first_exempt_branches || DEFAULT_EXEMPT_PREFIXES;
56
54
  if (exempt.some((p) => branch.startsWith(p))) {
57
55
  return { ok: true, mode: "exempt", reason: `branch prefix exempt (${branch.split("/")[0]}/…)` };
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Adaptador del gate de policy en el flujo de review de karajan-code
3
3
  * (GOV-A, KJC-TSK-0745). La decisión warn/deny/inexcepcionable/excepción
4
- * vive en el kernel (@karajan-family/governance); aquí solo el dominio: la
5
- * excepción se pide con KJ_ALLOW_POLICY=1 + KJ_POLICY_REASON, y el
6
- * artefacto de este dominio es el diff staged — por eso el alcance
7
- * registrado dice "este diff exacto".
4
+ * vive en el kernel (@karajan-family/governance); aquí solo el dominio: el
5
+ * artefacto es el diff staged. Sin excepción por diff (ADR 0015,
6
+ * KJC-TSK-0932): una regla que falla se corrige por PR.
8
7
  */
9
8
  import { evaluateGate } from "@karajan-family/governance";
10
9
 
@@ -13,13 +12,12 @@ import { evaluateGate } from "@karajan-family/governance";
13
12
  */
14
13
  export function evaluatePolicyGate({
15
14
  policy, errors = [], role = "coder", files = [], netLinesAdded = null,
16
- diffHashValue = null, env = process.env, recordException = () => {},
15
+ diffHashValue = null, recordException = () => {},
17
16
  standingExceptions = [], now = new Date(),
18
17
  }) {
19
18
  return evaluateGate({
20
19
  policy, errors, role, files, netLinesAdded,
21
20
  artifactHash: diffHashValue,
22
- exemption: { requested: env.KJ_ALLOW_POLICY === "1", justification: env.KJ_POLICY_REASON, hint: 'KJ_POLICY_REASON="por qué"' },
23
21
  recordException: (entry) => recordException({ ...entry, scope: "este diff exacto" }),
24
22
  standingExceptions, now,
25
23
  });
@@ -110,9 +110,9 @@ export async function runSonarPregate({ config, stagedFiles = [], touchedLines =
110
110
  }
111
111
  for (const f of own) indexed.add(`${prefix}${f}`);
112
112
  keys.push(scan.projectKey);
113
- // The scan above ALWAYS runs before issues are read (single-flight): the
114
- // verdict is about the code as it is now, never a stale server analysis
115
- // (KJC-TSK-0795 AC2 — that failure mode has no route here, by design).
113
+ // The scan above ALWAYS runs before issues are read (single-flight), and
114
+ // it returns once the server has processed it (KJC-BUG-0263: before, the
115
+ // upload returned first and the issues read were the previous analysis).
116
116
  const res = await getOpenIssues(config, scan.projectKey);
117
117
  totalProject += res.total ?? (res.issues || []).length;
118
118
  // Issue components are package-relative: made repo-relative here.
@@ -5,7 +5,7 @@
5
5
  * `method_gates.tests_with_code`. Test-FIRST remains the reviewer's
6
6
  * judgment; test-PRESENT belongs to the gate. Patterns come from the
7
7
  * project's own `development.*` config (same ones headless enforces).
8
- * KJ_ALLOW_NO_TESTS=1 is the explicit escape hatch.
8
+ * No escape (ADR 0015): deletions and comment-only cleanups already pass.
9
9
  */
10
10
 
11
11
  const DEFAULT_TEST_PATTERNS = ["/tests/", "/__tests__/", ".test.", ".spec."];
@@ -28,10 +28,7 @@ export function sourceFilesOf(config = {}, files = []) {
28
28
  };
29
29
  }
30
30
 
31
- export function checkTestsWithCode({ config = {}, stagedFiles = [], numstat = null, env = process.env }) {
32
- if (env.KJ_ALLOW_NO_TESTS === "1") {
33
- return { ok: true, mode: "exempt", reason: "KJ_ALLOW_NO_TESTS=1 (explicit escape hatch)" };
34
- }
31
+ export function checkTestsWithCode({ config = {}, stagedFiles = [], numstat = null }) {
35
32
  const { sources, hasTests } = sourceFilesOf(config, stagedFiles);
36
33
 
37
34
  if (sources.length === 0 || hasTests) return { ok: true, mode: "pass" };
@@ -51,7 +48,7 @@ export function checkTestsWithCode({ config = {}, stagedFiles = [], numstat = nu
51
48
  }
52
49
 
53
50
  const policy = config.method_gates?.tests_with_code || "warn";
54
- const reason = `source changes without any test change (${sources.slice(0, 5).join(", ")}${sources.length > 5 ? "…" : ""}) — the failing test comes first; add one or KJ_ALLOW_NO_TESTS=1 for a deliberate exception`;
51
+ const reason = `source changes without any test change (${sources.slice(0, 5).join(", ")}${sources.length > 5 ? "…" : ""}) — the failing test comes first: add one`;
55
52
  return policy === "block"
56
53
  ? { ok: false, mode: "block", sources, reason }
57
54
  : { ok: true, mode: "warn", sources, reason };
package/src/sonar/api.js CHANGED
@@ -82,6 +82,27 @@ async function sonarFetch(config, urlPath) {
82
82
  );
83
83
  }
84
84
 
85
+ const CE_FINAL = new Set(["SUCCESS", "FAILED", "CANCELED"]);
86
+ const sleepMs = (ms) => new Promise((done) => setTimeout(done, ms));
87
+
88
+ /**
89
+ * KJC-BUG-0263 — the scanner uploads the analysis and returns; the server
90
+ * processes it in the background, and issues read before that task ends are
91
+ * the PREVIOUS analysis. Polls /api/ce/task until it reaches a final status.
92
+ * @returns {Promise<{ok: boolean, status: string, timedOut?: boolean}>}
93
+ */
94
+ export async function waitForAnalysis(config, taskId, { timeoutMs = 120_000, intervalMs = 1000, sleep = sleepMs } = {}) {
95
+ const deadline = Date.now() + timeoutMs;
96
+ let status = "UNKNOWN";
97
+ while (Date.now() < deadline) {
98
+ const body = await sonarFetch(config, `/api/ce/task?id=${encodeURIComponent(taskId)}`);
99
+ try { status = JSON.parse(body).task?.status || "UNKNOWN"; } catch { status = "UNKNOWN"; }
100
+ if (CE_FINAL.has(status)) return { ok: status === "SUCCESS", status };
101
+ await sleep(intervalMs);
102
+ }
103
+ return { ok: false, status, timedOut: true };
104
+ }
105
+
85
106
  /**
86
107
  * KJC-BUG-0226 (issue #1838) — de donde sale la clave que se CONSULTA. El
87
108
  * scanner sube el analisis con la que el repo declara en su
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import os from "node:os";
5
5
  import { runCommand } from "../utils/process.js";
6
6
  import { sonarUp } from "./manager.js";
7
+ import { waitForAnalysis } from "./api.js";
7
8
  import { resolveSonarProjectKey } from "./project-key.js";
8
9
  import {
9
10
  resolveSonarHost,
@@ -380,14 +381,32 @@ export async function runSonarScan(config, projectKey = null, { verbose = false,
380
381
  }
381
382
 
382
383
  const result = await runCommand(cmd, args, { timeout: scannerTimeout, env, cwd: scanCwd });
384
+ // KJC-BUG-0263: the scan is done when the server has processed it; whoever reads
385
+ // the issues next would otherwise get the previous analysis. Said out loud when
386
+ // the log names no task to wait for.
387
+ let ok = result.exitCode === 0;
388
+ let analysisNote = null;
389
+ if (ok) {
390
+ const taskId = /api\/ce\/task\?id=([\w-]+)/.exec(String(result.stdout || ""))?.[1];
391
+ if (taskId) {
392
+ const done = await waitForAnalysis({ ...config, sonarqube: { ...sonarConfig, host: apiHost } }, taskId);
393
+ if (!done.ok) {
394
+ ok = false;
395
+ analysisNote = `sonar: the server did not finish analysis ${taskId} (${done.status}${done.timedOut ? ", timed out" : ""}) — its issues would be the previous analysis`;
396
+ }
397
+ } else {
398
+ analysisNote = "sonar: the scanner log names no analysis task, so kj could not wait for the server — issues may belong to the previous analysis";
399
+ }
400
+ }
383
401
  return {
384
- ok: result.exitCode === 0,
402
+ ok,
385
403
  projectKey: effectiveProjectKey,
386
404
  cwd: scanCwd,
387
405
  scanner: pick.type,
388
- note,
406
+ note: [note, analysisNote].filter(Boolean).join("; ") || null,
389
407
  stdout: result.stdout,
390
- stderr: result.stderr,
408
+ // A scan that failed on the server's side says so, not with the scanner's own log.
409
+ stderr: !ok && result.exitCode === 0 ? analysisNote : result.stderr,
391
410
  exitCode: result.exitCode
392
411
  };
393
412
  }