karajan-code 4.66.1 → 4.68.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 +1 -1
- package/src/cli/register-meta.js +3 -2
- package/src/commands/harden.js +4 -3
- package/src/git/automation.js +25 -4
- package/src/harden/hook-commands.js +48 -3
- package/src/orchestrator/drivers/init-context.js +2 -2
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +4 -0
- package/src/orchestrator/flow-runner.js +4 -3
- package/src/orchestrator/stages/format-stage.js +61 -0
- package/src/review/pr-requirements.js +33 -4
- package/src/session/mutators.js +5 -0
package/package.json
CHANGED
package/src/cli/register-meta.js
CHANGED
|
@@ -1119,7 +1119,8 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
1119
1119
|
.option("--no-config", "Skip lint/format/commit config files (hooks only)")
|
|
1120
1120
|
.option("--no-ci", "Skip CI quality workflows")
|
|
1121
1121
|
.option("--no-guidelines", "Skip AI-agent guideline files (AGENTS.md/CLAUDE.md)")
|
|
1122
|
-
.option("--
|
|
1122
|
+
.option("--sandbox", "Turn the host Bash sandbox on where it can start (off by default: it blocks gh, push and Sonar)")
|
|
1123
|
+
.option("--no-sandbox", "Leave the host Bash sandbox alone (the default; kept for existing scripts)")
|
|
1123
1124
|
.option("--mutation", "Also seed an opt-in nightly/manual mutation CI job (never a PR gate)")
|
|
1124
1125
|
.option("--only <dirs...>", "Harden only these language roots (e.g. frontend backend)")
|
|
1125
1126
|
.option("--exclude <globs...>", "Skip language roots matching these globs (e.g. wrappers)")
|
|
@@ -1137,7 +1138,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
1137
1138
|
config: flags.config !== false,
|
|
1138
1139
|
ci: flags.ci !== false,
|
|
1139
1140
|
guidelines: flags.guidelines !== false,
|
|
1140
|
-
sandbox: flags.sandbox
|
|
1141
|
+
sandbox: flags.sandbox === true,
|
|
1141
1142
|
mutation: Boolean(flags.mutation),
|
|
1142
1143
|
only: flags.only ?? [],
|
|
1143
1144
|
exclude: flags.exclude ?? [],
|
package/src/commands/harden.js
CHANGED
|
@@ -44,7 +44,7 @@ const JS_TEST_CMD = {
|
|
|
44
44
|
*/
|
|
45
45
|
export async function resolveCmds(projectDir, language) {
|
|
46
46
|
const lang = language ?? detectStackRoots(projectDir)[0]?.language ?? null;
|
|
47
|
-
const cmds =
|
|
47
|
+
const cmds = commandsForLanguage(lang, projectDir);
|
|
48
48
|
if (lang !== "javascript" && lang !== "typescript") return cmds;
|
|
49
49
|
|
|
50
50
|
const pkgPath = join(projectDir, "package.json");
|
|
@@ -92,7 +92,7 @@ export async function hardenCommand({
|
|
|
92
92
|
only = [],
|
|
93
93
|
exclude = [],
|
|
94
94
|
commitSupervisor = false,
|
|
95
|
-
sandbox =
|
|
95
|
+
sandbox = false,
|
|
96
96
|
kjVersion = null,
|
|
97
97
|
// KJC-BUG-0289: the contract files already dirty before anything was generated
|
|
98
98
|
// (kj init captures them before writing its own). What is dirty NOW is the person's.
|
|
@@ -162,7 +162,8 @@ export async function hardenCommand({
|
|
|
162
162
|
// sin sello, que es como una sesión se dejaba sin herramientas.
|
|
163
163
|
const sh = installSentinelHooks({ projectDir, logger, human: commitSupervisor });
|
|
164
164
|
result.sentinelHooks = sh.wired ? "wired" : "script-only";
|
|
165
|
-
// KJC-TSK-1092: the host sandbox,
|
|
165
|
+
// KJC-TSK-1092/1241: the host sandbox, only when asked (kj harden --sandbox):
|
|
166
|
+
// inside it kj run cannot push, open the PR or reach Sonar on localhost.
|
|
166
167
|
if (sandbox) result.sandbox = installSandbox({ projectDir, logger }).state;
|
|
167
168
|
}
|
|
168
169
|
} catch (err) {
|
package/src/git/automation.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { addCheckpoint } from "../session/store.js";
|
|
7
|
+
import { setGitPreexisting } from "../session/mutators.js";
|
|
7
8
|
import { pipelineSonarBlock, stampStagedVerdict } from "../review/verdict-store.js";
|
|
8
9
|
import { branchSize } from "../commands/pr-size.js";
|
|
9
10
|
import { pushRemedy } from "./hu-automation.js";
|
|
@@ -80,7 +81,11 @@ function prefixForTaskType(taskType) {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
export function commitMessageFromTask(task, taskType = null) {
|
|
83
|
-
|
|
84
|
+
// KJC-BUG-0413 (#2565): the header comes from the first paragraph, without
|
|
85
|
+
// its leading Markdown marks; the whole task made "feat: # Roadmap ... Context: ...".
|
|
86
|
+
const firstParagraph = String(task || "").trim().split(/\n[ \t]*\r?\n/)[0];
|
|
87
|
+
const clean = firstParagraph
|
|
88
|
+
.replace(/^[#>*\-\s]+/, "")
|
|
84
89
|
.replaceAll(/\s+/g, " ")
|
|
85
90
|
.trim();
|
|
86
91
|
const prefix = prefixForTaskType(taskType);
|
|
@@ -119,9 +124,22 @@ async function checkoutRunBranch(created) {
|
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
export async function prepareGitAutomation({ config, task, logger, session }) {
|
|
127
|
+
export async function prepareGitAutomation({ config, task, logger, session, preexisting: resumedBaseline = null }) {
|
|
123
128
|
const enabled = config.git.auto_commit || config.git.auto_push || config.git.auto_pr;
|
|
124
|
-
if (!enabled)
|
|
129
|
+
if (!enabled) {
|
|
130
|
+
// KJC-TSK-1237: the format stage needs the baseline also when kj does not
|
|
131
|
+
// commit; outside git there is none, and that stage says so.
|
|
132
|
+
let baseline = resumedBaseline;
|
|
133
|
+
if (!baseline) {
|
|
134
|
+
try {
|
|
135
|
+
baseline = await listPendingPaths();
|
|
136
|
+
} catch {
|
|
137
|
+
baseline = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (session && Array.isArray(baseline)) setGitPreexisting(session, baseline);
|
|
141
|
+
return { enabled: false };
|
|
142
|
+
}
|
|
125
143
|
|
|
126
144
|
if (!(await ensureGitRepo())) {
|
|
127
145
|
throw new Error("Git automation requested but current directory is not a git repository");
|
|
@@ -147,7 +165,10 @@ export async function prepareGitAutomation({ config, task, logger, session }) {
|
|
|
147
165
|
// pipeline commits only what it changes. Both names of a pending rename are
|
|
148
166
|
// kept. Fails closed: with no answer from git the run stops here rather than
|
|
149
167
|
// commit the person's pending changes later.
|
|
150
|
-
|
|
168
|
+
// KJC-BUG-0412 (#2564): a resumed run keeps the baseline of the session it
|
|
169
|
+
// resumes; the tree now also holds that session's own work.
|
|
170
|
+
const preexisting = resumedBaseline ?? await listPendingPaths();
|
|
171
|
+
if (session) setGitPreexisting(session, preexisting);
|
|
151
172
|
const repoHasCommits = await hasCommits();
|
|
152
173
|
|
|
153
174
|
// New repo without commits: create branch directly (no fetch/sync possible)
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* commands depend on what the project defines). Unknown language ⇒ no commands,
|
|
9
9
|
* leaving only the universal POSIX-shell checks.
|
|
10
10
|
*/
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
// KJC-BUG-0411 (#2563): uv and poetry keep pytest and ruff in the project's
|
|
15
|
+
// venv, where a bare command exits 127; their manager runs them.
|
|
16
|
+
const PYTHON_RUNNERS = [["uv.lock", "uv run"], ["poetry.lock", "poetry run"]];
|
|
11
17
|
|
|
12
18
|
export const COMMANDS_BY_LANGUAGE = {
|
|
13
19
|
python: { lint: "ruff check .", format: "ruff format --check .", test: "pytest" },
|
|
@@ -19,7 +25,46 @@ export const COMMANDS_BY_LANGUAGE = {
|
|
|
19
25
|
},
|
|
20
26
|
};
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
// KJC-TSK-1237 (#2565): the formatter in write mode, run on the files of a session.
|
|
29
|
+
const FORMAT_WRITE_BY_LANGUAGE = {
|
|
30
|
+
python: { command: "ruff format", extensions: [".py", ".pyi"] },
|
|
31
|
+
go: { command: "gofmt -w", extensions: [".go"] },
|
|
32
|
+
php: { command: "vendor/bin/php-cs-fixer fix", extensions: [".php"] },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// KJC-TSK-1239: JS/TS takes the formatter the project declares, from its node_modules.
|
|
36
|
+
// Declared both? The one its scripts call wins; else prettier, the more common.
|
|
37
|
+
const JS_TS = [".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx"];
|
|
38
|
+
const JS_FORMATTERS = [
|
|
39
|
+
{ pkg: "prettier", bin: "prettier", inScripts: /\bprettier\b/, command: "node_modules/.bin/prettier --write" },
|
|
40
|
+
{ pkg: "@biomejs/biome", bin: "biome", inScripts: /\bbiome\b/, command: "node_modules/.bin/biome format --write" },
|
|
41
|
+
];
|
|
42
|
+
function jsFormatWrite(projectDir) {
|
|
43
|
+
let pkg;
|
|
44
|
+
try { pkg = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8")); } catch { return null; }
|
|
45
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
46
|
+
const scripts = Object.values(pkg.scripts ?? {}).join("\n");
|
|
47
|
+
const usable = JS_FORMATTERS.filter((f) => deps[f.pkg] && existsSync(join(projectDir, "node_modules", ".bin", f.bin)));
|
|
48
|
+
const chosen = usable.find((f) => f.inScripts.test(scripts)) ?? usable[0];
|
|
49
|
+
return chosen ? { command: chosen.command, extensions: JS_TS } : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const runnerFor = (language, projectDir) => (language === "python" && projectDir
|
|
53
|
+
? PYTHON_RUNNERS.find(([lock]) => existsSync(join(projectDir, lock)))?.[1] ?? null
|
|
54
|
+
: null);
|
|
55
|
+
|
|
56
|
+
/** Native commands for a language, or {} when JS/TS or unknown; python ones through uv/poetry when the project uses them. */
|
|
57
|
+
export function commandsForLanguage(language, projectDir = null) {
|
|
58
|
+
const cmds = { ...COMMANDS_BY_LANGUAGE[language] };
|
|
59
|
+
const runner = runnerFor(language, projectDir);
|
|
60
|
+
if (runner) for (const [step, command] of Object.entries(cmds)) cmds[step] = `${runner} ${command}`;
|
|
61
|
+
return cmds;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The write-mode formatter of a language and the extensions it takes, or null. */
|
|
65
|
+
export function formatWriteFor(language, projectDir = null) {
|
|
66
|
+
if (language === "javascript" || language === "typescript") return projectDir ? jsFormatWrite(projectDir) : null;
|
|
67
|
+
const entry = FORMAT_WRITE_BY_LANGUAGE[language];
|
|
68
|
+
if (!entry) return null;
|
|
69
|
+
return { ...entry, command: [runnerFor(language, projectDir), entry.command].filter(Boolean).join(" ") };
|
|
25
70
|
}
|
|
@@ -47,7 +47,7 @@ export function adoptPreLoopConfig(ctx, updatedConfig) {
|
|
|
47
47
|
ctx.coderRoleInstance.config = updatedConfig;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
export async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
|
|
50
|
+
export async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags, preexisting = null }) {
|
|
51
51
|
// Auto-init .karajan/ if missing (copies coder-rules, review-rules, role templates)
|
|
52
52
|
const initProjectDir = config.projectDir || process.cwd();
|
|
53
53
|
await autoInit(initProjectDir, logger);
|
|
@@ -229,7 +229,7 @@ export async function initFlowContext({ task, config, logger, emitter, askQuesti
|
|
|
229
229
|
ctx.journalDecisions = [];
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session });
|
|
232
|
+
ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session, preexisting });
|
|
233
233
|
const projectDir = ctx.config.projectDir || process.cwd();
|
|
234
234
|
ctx.reviewRules = (await resolveReviewProfile({ mode: ctx.config.review_mode, projectDir })).rules;
|
|
235
235
|
await ctx.coderRoleInstance.init();
|
|
@@ -23,9 +23,13 @@ import { runPerfStage } from "../../stages/perf-stage.js";
|
|
|
23
23
|
import { runTddDisciplineStage } from "../../stages/tdd-discipline-stage.js";
|
|
24
24
|
import { runSuiteGreenStage, suiteGreenRequired } from "../../stages/suite-green-stage.js";
|
|
25
25
|
import { runToolJudgeStage } from "../../stages/tool-judge-stage.js";
|
|
26
|
+
import { runFormatStage } from "../../stages/format-stage.js";
|
|
26
27
|
import { tryCiComment } from "../../ci-integration.js";
|
|
27
28
|
|
|
28
29
|
export async function runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults, coderRole, pipelineFlags, brainCtx }) {
|
|
30
|
+
// KJC-TSK-1237 (#2565): the coder's files reach every gate formatted.
|
|
31
|
+
await runFormatStage({ config, logger, emitter, eventBase, session });
|
|
32
|
+
|
|
29
33
|
const tddResult = await runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration: i, askQuestion, task, brainCtx });
|
|
30
34
|
if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
|
|
31
35
|
if (tddResult.action === "continue") return { action: "continue" };
|
|
@@ -112,7 +112,7 @@ export async function runFlow(opts) {
|
|
|
112
112
|
return withRunContext({}, () => _runFlowInner(opts));
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
async function _runFlowInner({ task, config, logger, flags = {}, emitter = null, askQuestion = null, pgTaskId = null, pgProject = null }) {
|
|
115
|
+
async function _runFlowInner({ task, config, logger, flags = {}, emitter = null, askQuestion = null, pgTaskId = null, pgProject = null, preexisting = null }) {
|
|
116
116
|
// Defensive test-harness resolution for callers that bypass loadConfig()
|
|
117
117
|
// (orchestrator unit tests that build raw config objects). Production
|
|
118
118
|
// callers go through src/config/loader.js → loadConfig which already
|
|
@@ -153,7 +153,7 @@ async function _runFlowInner({ task, config, logger, flags = {}, emitter = null,
|
|
|
153
153
|
|
|
154
154
|
let ctx;
|
|
155
155
|
try {
|
|
156
|
-
ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
|
|
156
|
+
ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags, preexisting });
|
|
157
157
|
} catch (initError) {
|
|
158
158
|
// Pre-loop / preflight failures are CONFIG problems, not pipeline
|
|
159
159
|
// disputes. Pre-v2.7.5 we escalated them to Solomon, which:
|
|
@@ -356,7 +356,8 @@ export async function resumeFlow({ sessionId, answer, config, logger, flags = {}
|
|
|
356
356
|
|
|
357
357
|
// Re-run the flow with the existing session context
|
|
358
358
|
try {
|
|
359
|
-
|
|
359
|
+
// KJC-BUG-0412: what was pending is what was pending when THIS session started.
|
|
360
|
+
return await runFlow({ task, config: sessionConfig, logger, flags, emitter, askQuestion, preexisting: session.git_preexisting ?? null });
|
|
360
361
|
} catch (err) {
|
|
361
362
|
await markSessionStatus(session, "failed");
|
|
362
363
|
throw err;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-1237 (#2565): no stage formatted the coder's files, so a pre-commit
|
|
3
|
+
* format check aborted the final commit after every gate was green, and a
|
|
4
|
+
* one-command fix reached Solomon. This stage runs the project's formatter in
|
|
5
|
+
* write mode (the one kj harden knows) on the files of this session only:
|
|
6
|
+
* pending now and not pending when the session started. It is not a gate: a
|
|
7
|
+
* failing or missing formatter is said and the next stages judge the files.
|
|
8
|
+
* Opt-out: `development.format_after_coder: false`.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
11
|
+
import { extname, join } from "node:path";
|
|
12
|
+
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
13
|
+
import { runCommand } from "../../utils/process.js";
|
|
14
|
+
import { listPendingPaths } from "../../utils/git.js";
|
|
15
|
+
import { detectStackRoots } from "../../harden/stack-roots.js";
|
|
16
|
+
import { formatWriteFor } from "../../harden/hook-commands.js";
|
|
17
|
+
|
|
18
|
+
const TIMEOUT_MS = 2 * 60 * 1000;
|
|
19
|
+
const primaryLanguage = (projectDir) => detectStackRoots(projectDir)[0]?.language ?? null;
|
|
20
|
+
|
|
21
|
+
const expand = (projectDir, p) => (p.endsWith("/")
|
|
22
|
+
? readdirSync(join(projectDir, p), { recursive: true }).map((f) => join(p, f))
|
|
23
|
+
: [p]);
|
|
24
|
+
|
|
25
|
+
/** Files of the session the formatter takes; git names a new directory, so its files are listed. */
|
|
26
|
+
function filesToFormat({ pending, preexisting, extensions, projectDir }) {
|
|
27
|
+
const before = new Set(preexisting);
|
|
28
|
+
return pending.filter((p) => !before.has(p) && existsSync(join(projectDir, p)))
|
|
29
|
+
.flatMap((p) => expand(projectDir, p))
|
|
30
|
+
.filter((p) => extensions.includes(extname(p)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @returns {Promise<{ran: boolean, ok?: boolean, files?: string[]}>} */
|
|
34
|
+
export async function runFormatStage({ config, logger, emitter, eventBase, session, detectLanguageImpl = primaryLanguage, listPendingImpl = (dir) => listPendingPaths(dir), runImpl = async (...args) => runCommand(...args) }) {
|
|
35
|
+
if (config?.development?.format_after_coder === false) return { ran: false };
|
|
36
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
37
|
+
const formatter = formatWriteFor(detectLanguageImpl(projectDir), projectDir);
|
|
38
|
+
if (!formatter) return { ran: false };
|
|
39
|
+
const preexisting = session?.git_preexisting;
|
|
40
|
+
if (!Array.isArray(preexisting)) {
|
|
41
|
+
logger?.warn?.("format: no baseline of what was pending when the session started: nothing formatted");
|
|
42
|
+
return { ran: false };
|
|
43
|
+
}
|
|
44
|
+
let files;
|
|
45
|
+
try {
|
|
46
|
+
files = filesToFormat({ pending: await listPendingImpl(projectDir), preexisting, extensions: formatter.extensions, projectDir });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
logger?.warn?.(`format: the session's files could not be listed (${err.message}): nothing formatted`);
|
|
49
|
+
return { ran: false };
|
|
50
|
+
}
|
|
51
|
+
if (files.length === 0) return { ran: false };
|
|
52
|
+
const [bin, ...args] = formatter.command.split(" ");
|
|
53
|
+
const res = await runImpl(bin, [...args, "--", ...files], { cwd: projectDir, timeout: TIMEOUT_MS }).catch(() => ({ exitCode: null }));
|
|
54
|
+
const ok = res.exitCode === 0;
|
|
55
|
+
const message = ok
|
|
56
|
+
? `format: ${formatter.command} on ${files.length} file(s)`
|
|
57
|
+
: `format: ${formatter.command} failed (exit ${res.exitCode ?? "none"}); the next stages judge the files as they are`;
|
|
58
|
+
logger?.[ok ? "info" : "warn"]?.(message);
|
|
59
|
+
emitProgress(emitter, makeEvent("format:result", { ...eventBase, stage: "format" }, { status: ok ? "ok" : "warn", message, detail: { command: formatter.command, files } }));
|
|
60
|
+
return { ran: true, ok, files };
|
|
61
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* The rules are the own-work ones: the same budget, Conventional Commits and
|
|
5
5
|
* attribution patterns, and tests-with-code.
|
|
6
6
|
*/
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
7
9
|
import { AI_ATTRIBUTION, CONVENTIONAL_TYPES } from "../harden/hook-templates.js";
|
|
8
10
|
import { loadPolicy } from "../policy/engine.js";
|
|
9
11
|
import { runCommand } from "../utils/process.js";
|
|
@@ -59,12 +61,35 @@ export function commitChecks(commits) {
|
|
|
59
61
|
];
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
// KJC-TSK-1238 (#2263): a convention the reviewed repo declares (commitlint, kj's commit-msg hook).
|
|
65
|
+
const CONVENTION_FILES = [
|
|
66
|
+
"commitlint.config.js", "commitlint.config.cjs", "commitlint.config.mjs", "commitlint.config.ts",
|
|
67
|
+
".commitlintrc", ".commitlintrc.json", ".commitlintrc.yml", ".commitlintrc.yaml", ".commitlintrc.js", ".commitlintrc.cjs",
|
|
68
|
+
".karajan/hooks/commit-msg",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
export function declaresCommitConvention(projectDir) {
|
|
72
|
+
if (CONVENTION_FILES.some((f) => existsSync(join(projectDir, f)))) return true;
|
|
73
|
+
try {
|
|
74
|
+
return Boolean(JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8")).commitlint);
|
|
75
|
+
} catch {
|
|
76
|
+
return false; // no package.json, or not JSON: it declares nothing
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Source changed with no test changed (a delete-only change needs none). On
|
|
82
|
+
* someone else's PR test-first is kj's rule, not theirs (KJC-TSK-1238): a
|
|
83
|
+
* change with no business logic needs no test, so it is a question, not a failure.
|
|
84
|
+
*/
|
|
63
85
|
export function testsCheck({ config, numstat }) {
|
|
64
86
|
const rows = rowsOf(numstat);
|
|
65
87
|
const res = checkTestsWithCode({ config, stagedFiles: rows.map((r) => r.file), numstat: rows });
|
|
66
|
-
|
|
67
|
-
return {
|
|
88
|
+
if (res.ok && res.mode !== "warn") return { id: "tests", ok: true, detail: `tests: ${res.mode}` };
|
|
89
|
+
return {
|
|
90
|
+
id: "tests", ok: null, detail: res.reason || "source changed with no test changed",
|
|
91
|
+
question: "No test changed: does the change touch business logic, behaviour a unit, integration or E2E test could observe? A plain index, configuration or infrastructure needs none.",
|
|
92
|
+
};
|
|
68
93
|
}
|
|
69
94
|
|
|
70
95
|
const git = async (projectDir, args) => {
|
|
@@ -83,5 +108,9 @@ export async function prRequirementChecks({ range, projectDir, config, run = git
|
|
|
83
108
|
const [sha, subject = "", body = ""] = r.split("\x1f");
|
|
84
109
|
return { sha, subject, body };
|
|
85
110
|
});
|
|
86
|
-
|
|
111
|
+
const [conventional, attribution] = commitChecks(commits);
|
|
112
|
+
const commitFormat = declaresCommitConvention(projectDir)
|
|
113
|
+
? conventional
|
|
114
|
+
: { id: "conventional-commits", ok: null, skipped: true, detail: "the repo declares no commit convention (no commitlint config, no kj commit-msg hook): not checked" };
|
|
115
|
+
return [sizeCheck({ numstat, limit: prSizeLimit({ projectDir, config }).limit }), commitFormat, attribution, testsCheck({ config, numstat })];
|
|
87
116
|
}
|
package/src/session/mutators.js
CHANGED
|
@@ -227,6 +227,11 @@ export function setPgTaskId(session, id) {
|
|
|
227
227
|
session.pg_task_id = id;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/** KJC-BUG-0412: what was pending in the tree when the session started. */
|
|
231
|
+
export function setGitPreexisting(session, paths) {
|
|
232
|
+
session.git_preexisting = paths;
|
|
233
|
+
}
|
|
234
|
+
|
|
230
235
|
export function appendPgCommits(session, commits) {
|
|
231
236
|
session.pg_commits = [...(session.pg_commits ?? []), ...commits];
|
|
232
237
|
}
|