@karajan-family/code 5.0.3 → 5.1.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/audit/deterministic-summary.js +26 -0
- package/src/cli/register-pipeline.js +1 -0
- package/src/commands/review-gate.js +5 -2
- package/src/environment/briefs.js +1 -0
- package/src/environment/playbook.js +4 -4
- package/src/prompts/reviewer.js +6 -2
- package/src/review/diff-facts.js +110 -0
- package/src/review/one-shot-review.js +7 -0
- package/src/review/solomon-arbitration.js +21 -8
package/package.json
CHANGED
|
@@ -33,6 +33,7 @@ export function formatDeterministicSummary(ctx) {
|
|
|
33
33
|
const lines = ["## Deterministic Findings", ""];
|
|
34
34
|
lines.push("These are the rule-based findings (no LLM). They cost zero tokens and are always factually accurate. The interactive prompt below asks if you want to extend this with an LLM analysis (which will cost tokens).");
|
|
35
35
|
lines.push("");
|
|
36
|
+
lines.push(...formatCoverageBlock(auditCoverage(ctx)));
|
|
36
37
|
|
|
37
38
|
if (ctx.stack) lines.push(...formatStackBlock(ctx.stack));
|
|
38
39
|
if (ctx.basalCost) lines.push(...formatBasalCostBlock(ctx.basalCost, ctx.growthDelta));
|
|
@@ -51,6 +52,31 @@ export function formatDeterministicSummary(ctx) {
|
|
|
51
52
|
return lines.join("\n");
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
// KJC-TSK-1381 (#3208): a green audit that skipped half its checks must not read as complete.
|
|
56
|
+
const CHECK_NAMES = {
|
|
57
|
+
sonarFindings: "Sonar", osvFindings: "osv-scanner", semgrepFindings: "semgrep",
|
|
58
|
+
injectionFindings: "prompt-injection", infraFindings: "checkov", npmSignatures: "npm signatures",
|
|
59
|
+
circularDeps: "madge", deadExports: "knip", aiSlop: "ai-slop", envKeys: "env keys", webperf: "webperf",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Checks attempted (a section skipped by a flag is null and does not count), and those that did not run. */
|
|
63
|
+
export function auditCoverage(ctx) {
|
|
64
|
+
const attempted = Object.entries(CHECK_NAMES).filter(([key]) => ctx[key]);
|
|
65
|
+
const notRun = attempted.filter(([key]) => ctx[key].available === false)
|
|
66
|
+
.map(([key, name]) => ({ name, reason: ctx[key].reason || "did not run" }));
|
|
67
|
+
return { ran: attempted.length - notRun.length, total: attempted.length, notRun };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatCoverageBlock({ ran, total, notRun }) {
|
|
71
|
+
const lines = [`**Coverage:** ${ran} of ${total} checks ran.`];
|
|
72
|
+
for (const n of notRun) lines.push(`- not run, ${n.name}: ${n.reason}`);
|
|
73
|
+
if (notRun.some((n) => /not installed|not found/i.test(n.reason))) {
|
|
74
|
+
lines.push("- Install the missing tools with `kj install-tools`: until then this audit covers less than it can.");
|
|
75
|
+
}
|
|
76
|
+
lines.push("");
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
const MAX_SAMPLE_ENV_KEYS = 8;
|
|
55
81
|
|
|
56
82
|
// KJC-TSK-0845: the same key resolved with two rules in two modules, and a
|
|
@@ -87,6 +87,7 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
87
87
|
.option("--position <text>", "Why the brain disagrees with the reviewer")
|
|
88
88
|
.option("--position-file <path>", "Read the position from a file (no quoting to get right)")
|
|
89
89
|
.option("--range <range>", "Arbitrate a git range instead of the staged diff")
|
|
90
|
+
.option("--finding <id>", "Arbitrate only this finding of the verdict; the others keep blocking")
|
|
90
91
|
.action(async (flags) => {
|
|
91
92
|
await withConfig(pkgVersion, "solomon", flags, async ({ config, logger }) => {
|
|
92
93
|
await solomonCommand({ config, logger, flags });
|
|
@@ -176,8 +176,11 @@ export async function solomonCommand({ config, logger = null, flags = {} }) {
|
|
|
176
176
|
process.exitCode = 1;
|
|
177
177
|
return { ruling: "reject", reasoning: "security-class policy denial — not arbitrable" };
|
|
178
178
|
}
|
|
179
|
-
const res = await runSolomonArbitration({ diff, position, config, logger, projectDir });
|
|
180
|
-
if (res.ruling === "approve") {
|
|
179
|
+
const res = await runSolomonArbitration({ diff, position, config, logger, projectDir, findingId: flags.finding || null });
|
|
180
|
+
if (res.ruling === "approve" && res.remaining?.length > 0) {
|
|
181
|
+
// KJC-TSK-1379: one finding arbitrated; the rest still block the gate.
|
|
182
|
+
console.log(`⚖ Solomon (${res.solomon}) rules for the brain on ${flags.finding}; still blocking: ${res.remaining.join(", ")}.`);
|
|
183
|
+
} else if (res.ruling === "approve") {
|
|
181
184
|
console.log(`⚖ Solomon (${res.solomon}) rules for the brain — verdict recorded, the gate is open.`);
|
|
182
185
|
} else {
|
|
183
186
|
const who = res.solomon ? ` (${res.solomon})` : "";
|
|
@@ -74,6 +74,7 @@ const B = {
|
|
|
74
74
|
...(config?.method_gates?.mutation
|
|
75
75
|
? ["Green is not proof: run `kj mutate --since <base>` after the suite passes and BEFORE review — kill surviving mutants (weak asserts) first."]
|
|
76
76
|
: []),
|
|
77
|
+
"Once green, simplify your change before `kj review --staged`: same behavior, no test weakened, within the line budget. The verdict binds to the exact diff, so a cleanup after it costs a second review.",
|
|
77
78
|
],
|
|
78
79
|
"green suite + the list of new/changed tests and what each one pins.");
|
|
79
80
|
},
|
|
@@ -64,10 +64,10 @@ Invariants (the git gates enforce these — they are not suggestions):
|
|
|
64
64
|
\`kj_rag_query\` tool: reach for it before grepping by hand. Questions about
|
|
65
65
|
the project's docs, notes, ADRs or plans: ask qmd (its MCP) first.
|
|
66
66
|
- ${trackingLine(stateBackend, boardName)}
|
|
67
|
-
- Tests prove behavior:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
- Tests prove behavior: failing test first (TDD), suite never left red.
|
|
68
|
+
Green is not proof: with \`method_gates.mutation\` declared, run
|
|
69
|
+
\`kj mutate --since <base>\` and kill surviving mutants (weak asserts); then
|
|
70
|
+
simplify (same behavior, no test weakened). Both BEFORE the review.
|
|
71
71
|
- Every diff is reviewed by a DIFFERENT AI before it is committed
|
|
72
72
|
(\`kj review --staged\`): verdicts bind to the exact diff — change the code
|
|
73
73
|
and it must be reviewed again. Disagree with a rejection? \`kj solomon\`.
|
package/src/prompts/reviewer.js
CHANGED
|
@@ -35,7 +35,7 @@ const SERENA_INSTRUCTIONS = [
|
|
|
35
35
|
* on it; the per-review content (task context + git diff, up to 12 KB
|
|
36
36
|
* and always different) goes last.
|
|
37
37
|
*/
|
|
38
|
-
export async function buildReviewerPromptLayout({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, domainContext = null, projectDir = null, language = "en", provider = null, roleSkillBlock = null }) {
|
|
38
|
+
export async function buildReviewerPromptLayout({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, domainContext = null, projectDir = null, language = "en", provider = null, roleSkillBlock = null, factsBlock = null }) {
|
|
39
39
|
const { body: clippedDiff, note: clipNote } = clipDiff(diff);
|
|
40
40
|
// KJC-BUG-0138: full diff on purpose — the moved-to side survives clipping.
|
|
41
41
|
const splitSignal = buildSplitSignal(diff);
|
|
@@ -61,7 +61,9 @@ export async function buildReviewerPromptLayout({ task, diff, reviewRules, mode,
|
|
|
61
61
|
section("A claim that a tool, platform or API does not support something the diff uses is not a blocking issue on its own: your training data may predate the feature. Block only when the diff or the task context shows it fails; otherwise put it in non_blocking_suggestions.", STABLE),
|
|
62
62
|
section("Return only one valid JSON object and nothing else.", STABLE),
|
|
63
63
|
section("JSON schema:", STABLE),
|
|
64
|
-
section('{"approved":boolean,"blocking_issues":[{"id":string,"severity":"critical|high|medium|low","category":"security|correctness|performance|style|other","file":string,"line":number,"description":string,"suggested_fix":string}],"non_blocking_suggestions":[string],"summary":string,"confidence":number}', STABLE),
|
|
64
|
+
section('{"approved":boolean,"blocking_issues":[{"id":string,"severity":"critical|high|medium|low","category":"security|correctness|performance|style|other","file":string|null,"line":number|null,"description":string,"suggested_fix":string}],"non_blocking_suggestions":[string],"summary":string,"confidence":number}', STABLE),
|
|
65
|
+
// KJC-TSK-1379 (#3207): a finding that names no line is hard to apply and hard to arbitrate.
|
|
66
|
+
section("Every blocking issue names the file and the line of the diff it is about; use null only for a finding about the whole diff.", STABLE),
|
|
65
67
|
section(serenaEnabled ? SERENA_INSTRUCTIONS : null, STABLE),
|
|
66
68
|
section(rtkSnippet, STABLE),
|
|
67
69
|
section(productContext ? `## Product Context\n${productContext}` : null, STABLE),
|
|
@@ -69,6 +71,8 @@ export async function buildReviewerPromptLayout({ task, diff, reviewRules, mode,
|
|
|
69
71
|
section(`Review rules:\n${reviewRules}`, STABLE),
|
|
70
72
|
section(skillSection, STABLE),
|
|
71
73
|
section(`Task context:\n${task}`, VOLATILE),
|
|
74
|
+
// KJC-TSK-1380 (#3207): facts computed without a model, before the diff they describe.
|
|
75
|
+
section(factsBlock || null, VOLATILE),
|
|
72
76
|
section(`Git diff:\n${clippedDiff}`, VOLATILE),
|
|
73
77
|
section(clipNote, VOLATILE),
|
|
74
78
|
section(splitSignal, VOLATILE),
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-1380 (#3207): facts about a diff computed without a model, each with its
|
|
3
|
+
* file:line where it has one. The reviewer starts from them and keeps the judgement;
|
|
4
|
+
* they inform, they never block on their own.
|
|
5
|
+
*/
|
|
6
|
+
import { scanText, loadPrivacyList } from "../privacy/scan.js";
|
|
7
|
+
|
|
8
|
+
const MAX_SIZE_FACTS = 10;
|
|
9
|
+
const TEST_FILE = /(^|\/)(tests?|__tests__|spec)\/|\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
10
|
+
const CODE_FILE = /\.([cm]?[jt]sx?|py|go|rb|java|rs|php|cs|kt|swift)$/;
|
|
11
|
+
const DYNAMIC_IMPORT = /await\s+import\(\s*["'`]([^"'`]+)["'`]\s*\)/;
|
|
12
|
+
const STATIC_IMPORT = /^\s*import\s[^;]*?from\s+["']([^"']+)["']/gm;
|
|
13
|
+
const DEP_LINE = /^\s*"([^"]+)":\s*"([^"]*)"\s*,?\s*$/;
|
|
14
|
+
const DEP_SECTIONS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
15
|
+
const NOT_DEPS = new Set(["name", "version", "description", "main", "module", "types", "license", "type", "node", "npm"]);
|
|
16
|
+
|
|
17
|
+
/** Added lines (with their line in the new file) and removed counts, per file. */
|
|
18
|
+
function parseDiff(diff) {
|
|
19
|
+
const files = new Map();
|
|
20
|
+
let current = null;
|
|
21
|
+
let line = 0;
|
|
22
|
+
let name;
|
|
23
|
+
for (const raw of String(diff).split("\n")) {
|
|
24
|
+
if (raw.startsWith("diff --git ")) {
|
|
25
|
+
name = / b\/(.+)$/.exec(raw)?.[1] ?? null;
|
|
26
|
+
current = name ? { added: [], removed: 0 } : null;
|
|
27
|
+
if (current) files.set(name, current);
|
|
28
|
+
line = 1;
|
|
29
|
+
} else if (raw.startsWith("+++ ")) {
|
|
30
|
+
continue; // file header, not an added line (the file was named by "diff --git")
|
|
31
|
+
} else if (raw.startsWith("@@")) {
|
|
32
|
+
line = Number(/\+(\d+)/.exec(raw)?.[1] ?? 1);
|
|
33
|
+
} else if (current && raw.startsWith("+")) {
|
|
34
|
+
current.added.push({ line, text: raw.slice(1) });
|
|
35
|
+
line += 1;
|
|
36
|
+
} else if (current && raw.startsWith("-") && !raw.startsWith("---")) {
|
|
37
|
+
current.removed += 1;
|
|
38
|
+
} else if (current && raw.startsWith(" ")) {
|
|
39
|
+
line += 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return files;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function declaredDeps(readFile, name) {
|
|
46
|
+
try {
|
|
47
|
+
const pkg = JSON.parse(readFile(name));
|
|
48
|
+
return new Set(DEP_SECTIONS.flatMap((s) => Object.keys(pkg[s] || {})));
|
|
49
|
+
} catch { return null; } // unreadable: fall back to the shape of the line
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function dependencyFacts(name, added, readFile) {
|
|
53
|
+
if (!/(^|\/)package\.json$/.test(name)) return [];
|
|
54
|
+
const deps = readFile ? declaredDeps(readFile, name) : null;
|
|
55
|
+
return added.flatMap(({ line, text }) => {
|
|
56
|
+
const m = DEP_LINE.exec(text);
|
|
57
|
+
if (!m) return [];
|
|
58
|
+
const isDep = deps ? deps.has(m[1]) : !NOT_DEPS.has(m[1]) && /^[\^~<>=*\d]|^(npm|workspace|file|git)/.test(m[2]);
|
|
59
|
+
return isDep ? [{ kind: "new-dependency", file: name, line, text: `adds dependency ${m[1]}@${m[2]}` }] : [];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function dynamicImportFacts(name, added, readFile) {
|
|
64
|
+
if (!readFile) return [];
|
|
65
|
+
let content;
|
|
66
|
+
try { content = readFile(name); } catch { return []; }
|
|
67
|
+
const staticSpecs = new Set([...content.matchAll(STATIC_IMPORT)].map((m) => m[1]));
|
|
68
|
+
return added.flatMap(({ line, text }) => {
|
|
69
|
+
const spec = DYNAMIC_IMPORT.exec(text)?.[1];
|
|
70
|
+
return spec && staticSpecs.has(spec) ? [{ kind: "redundant-dynamic-import", file: name, line, text: `await import("${spec}") of a module this file already imports statically` }] : [];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function secretFacts(name, added, list) {
|
|
75
|
+
return added.flatMap(({ line, text }) => scanText(text, { list, source: name })
|
|
76
|
+
.filter((f) => f.severity === "block" || f.type === "secret-assignment")
|
|
77
|
+
.map((f) => ({ kind: "secret-shape", file: name, line, text: `${f.type}: ${f.masked}` })));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} diff unified diff
|
|
82
|
+
* @param {{ readFile?: (path: string) => string, list?: object }} [opts] readFile reads a file as it is after the diff
|
|
83
|
+
*/
|
|
84
|
+
export function diffFacts(diff, { readFile = null, list } = {}) {
|
|
85
|
+
const files = parseDiff(diff);
|
|
86
|
+
const names = [...files.keys()];
|
|
87
|
+
const privacy = list ?? loadPrivacyList(); // read once, not per added line
|
|
88
|
+
const facts = [...files]
|
|
89
|
+
.toSorted(([, a], [, b]) => (b.added.length + b.removed) - (a.added.length + a.removed))
|
|
90
|
+
.slice(0, MAX_SIZE_FACTS)
|
|
91
|
+
.map(([name, f]) => ({ kind: "size", file: name, text: `+${f.added.length} / -${f.removed}` }));
|
|
92
|
+
const code = names.filter((n) => CODE_FILE.test(n) && !TEST_FILE.test(n));
|
|
93
|
+
if (code.length > 0 && !names.some((n) => TEST_FILE.test(n))) {
|
|
94
|
+
facts.push({ kind: "no-tests", text: `code changes in ${code.length} file(s) and no test file is touched` });
|
|
95
|
+
}
|
|
96
|
+
for (const [name, f] of files) {
|
|
97
|
+
facts.push(...dependencyFacts(name, f.added, readFile), ...dynamicImportFacts(name, f.added, readFile),
|
|
98
|
+
...secretFacts(name, f.added, privacy));
|
|
99
|
+
}
|
|
100
|
+
return facts;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The facts as a prompt block; empty when there are none. */
|
|
104
|
+
export function formatDiffFacts(facts) {
|
|
105
|
+
if (!facts?.length) return "";
|
|
106
|
+
const where = (f) => (f.file ? `${f.file}${f.line ? `:${f.line}` : ""} ` : "");
|
|
107
|
+
return ["## Verified facts (computed without a model)",
|
|
108
|
+
"Checked mechanically on this diff. Start from them; judging what they mean is yours.",
|
|
109
|
+
...facts.map((f) => `- ${where(f)}${f.text}`)].join("\n");
|
|
110
|
+
}
|
|
@@ -17,6 +17,9 @@ import { detectAvailableAgents, detectHostAgent } from "../utils/agent-detect.js
|
|
|
17
17
|
import { accountLabel } from "../utils/agent-home.js";
|
|
18
18
|
import { saveVerdict, diffHash } from "./verdict-store.js";
|
|
19
19
|
import { summarizeGeneratedFiles } from "./prompt-diff.js";
|
|
20
|
+
import { diffFacts, formatDiffFacts } from "./diff-facts.js";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
20
23
|
import { reportUnparseableVerdict } from "./unparseable-verdict.js";
|
|
21
24
|
import { detectWorkspace } from "./workspace.js";
|
|
22
25
|
import { branchOf } from "./correction-rounds.js";
|
|
@@ -107,11 +110,14 @@ export async function runOneShotReview({
|
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
const { rules } = await resolveReviewProfile({ mode: "standard", projectDir });
|
|
113
|
+
// KJC-TSK-1380 (#3207): facts computed without a model; the reviewer starts from them.
|
|
114
|
+
const facts = diffFacts(diff, { readFile: (p) => readFileSync(join(projectDir || process.cwd(), p), "utf8") });
|
|
110
115
|
const attempt = async (who, cfg = config) => {
|
|
111
116
|
const prompt = await buildReviewerPrompt({
|
|
112
117
|
task: task || "Review the following diff for correctness, security and maintainability.",
|
|
113
118
|
// KJC-BUG-0347: generated files summarized; the verdict still binds to the full diff.
|
|
114
119
|
diff: summarizeGeneratedFiles(diff), reviewRules: rules, mode: "standard", provider: who, projectDir, roleSkillBlock: skills,
|
|
120
|
+
factsBlock: formatDiffFacts(facts),
|
|
115
121
|
});
|
|
116
122
|
// KJC-TSK-1344: with two subscriptions per CLI, say which account reviews.
|
|
117
123
|
const account = accountLabel(who, { env });
|
|
@@ -139,6 +145,7 @@ export async function runOneShotReview({
|
|
|
139
145
|
}
|
|
140
146
|
|
|
141
147
|
const record = await verdictRecord(parsed, { reviewer: activeReviewer, hostAgent, projectDir, sonar, rag, ui });
|
|
148
|
+
if (facts.length > 0) record.facts = facts;
|
|
142
149
|
return persist ? saveVerdict(projectDir, diff, record) : record;
|
|
143
150
|
}
|
|
144
151
|
|
|
@@ -43,7 +43,7 @@ export async function pickThirdParty({ config, hostAgent, reviewerAgent, detectA
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export async function runSolomonArbitration({
|
|
46
|
-
diff, position, config, logger, projectDir,
|
|
46
|
+
diff, position, config, logger, projectDir, findingId = null,
|
|
47
47
|
hostAgent = detectHostAgent(),
|
|
48
48
|
createAgentFn = createAgent,
|
|
49
49
|
detectAgents = detectAvailableAgents,
|
|
@@ -56,8 +56,15 @@ export async function runSolomonArbitration({
|
|
|
56
56
|
throw new Error("nothing to arbitrate — there is no rejected verdict for the current diff (run `kj review --staged` first)");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// KJC-TSK-1379 (#3207): with findingId only that finding is disputed; the rest keep blocking.
|
|
60
|
+
const allIssues = verdict.issues || [];
|
|
61
|
+
const disputed = findingId ? allIssues.filter((issue) => issue.id === findingId) : allIssues;
|
|
62
|
+
if (findingId && disputed.length === 0) {
|
|
63
|
+
throw new Error(`no finding "${findingId}" in the rejected verdict; its findings are: ${allIssues.map((i) => i.id).join(", ") || "none"}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
59
66
|
// ABSOLUTE RULE: security findings are never overridable.
|
|
60
|
-
const securityIssues =
|
|
67
|
+
const securityIssues = disputed.filter((issue) => securityReason(issue));
|
|
61
68
|
if (securityIssues.length > 0) {
|
|
62
69
|
const named = securityIssues.map(describeSecurityIssue).join("; ");
|
|
63
70
|
const record = await saveVerdict(projectDir, diff, {
|
|
@@ -79,8 +86,9 @@ export async function runSolomonArbitration({
|
|
|
79
86
|
"The reviewer REJECTED a diff; the orchestrating brain DISAGREES. Decide who is right.",
|
|
80
87
|
"Never approve to save time or effort — only if the reviewer's objections are genuinely wrong or immaterial for this diff.",
|
|
81
88
|
'Return only one JSON object: {"ruling":"approve"|"reject","reasoning":string}. "approve" = the brain is right, the diff may ship as-is. "reject" = the reviewer is right, the brain must fix the issues.',
|
|
89
|
+
...(findingId ? ["Rule on this one finding only: the reviewer's other findings are not in dispute."] : []),
|
|
82
90
|
"", "## Reviewer's blocking issues",
|
|
83
|
-
JSON.stringify(
|
|
91
|
+
JSON.stringify(disputed, null, 2),
|
|
84
92
|
"", "## Brain's position", position,
|
|
85
93
|
// KJC-BUG-0347: generated files summarized; the ruling still binds to the full diff.
|
|
86
94
|
"", "## The diff under dispute", summarizeGeneratedFiles(diff),
|
|
@@ -115,12 +123,17 @@ export async function runSolomonArbitration({
|
|
|
115
123
|
// for this exact hash and travels with the overriding verdict — without it
|
|
116
124
|
// `kj review --check` refuses every arbitrated diff.
|
|
117
125
|
const evidence = { ...(verdict.sonar ? { sonar: verdict.sonar } : {}), ...(verdict.rag ? { rag: verdict.rag } : {}) };
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
if (findingId) arbitration.findingId = findingId;
|
|
127
|
+
const remaining = parsed.ruling === "approve" ? allIssues.filter((issue) => !disputed.includes(issue)) : allIssues;
|
|
128
|
+
let record;
|
|
129
|
+
if (parsed.ruling !== "approve") record = await saveVerdict(projectDir, diff, { ...verdict, arbitration });
|
|
130
|
+
else if (remaining.length > 0) record = await saveVerdict(projectDir, diff, { ...verdict, issues: remaining, arbitration });
|
|
131
|
+
else {
|
|
132
|
+
record = await saveVerdict(projectDir, diff, {
|
|
120
133
|
verdict: "approved", reviewer: `solomon:${solomon}`, host: hostAgent || null,
|
|
121
134
|
issues: [], summary: `Arbitration overrode ${verdict.reviewer}'s rejection: ${parsed.reasoning || ""}`.trim(), arbitration, ...evidence,
|
|
122
|
-
})
|
|
123
|
-
|
|
135
|
+
});
|
|
136
|
+
}
|
|
124
137
|
|
|
125
|
-
return { ruling: parsed.ruling, reasoning: parsed.reasoning || "", solomon, record };
|
|
138
|
+
return { ruling: parsed.ruling, reasoning: parsed.reasoning || "", solomon, record, remaining: remaining.map((issue) => issue.id) };
|
|
126
139
|
}
|