@mrciphersmith/keryx 0.3.116 → 0.3.118
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/README.md +1 -1
- package/dist/cli.js +5098 -2892
- package/dist/core.js +718 -9
- package/package.json +1 -1
- package/src/gdskills/bundled/skills/review/review-architecture/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-backend/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-clean-code/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-core-boundaries/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-flow-graph/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-frontend/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-frontend-conventions/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-highload/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-jev-comments/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-jev-contract/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-jev-docs/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-jev-risk/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-jev-rules/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-jev-scenarios/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-layout/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-logic/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +95 -10
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +37 -37
- package/src/gdskills/bundled/skills/review/review-performance/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-pr-contract/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-pr-feedback/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-regression/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-security-code/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-style/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-testing-practices/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-verifier/SKILL.md +1 -1
package/dist/core.js
CHANGED
|
@@ -11945,6 +11945,9 @@ function renderMetaprojectGitignoreBlock() {
|
|
|
11945
11945
|
# Recommendation journal (flow 392): what the human chose and why they deviated.
|
|
11946
11946
|
# Personal runtime state, written 0o600.
|
|
11947
11947
|
.metaproject/data/decisions/
|
|
11948
|
+
# \`keryx review mutate\` runs the project's tests in a scratch copy of the tree under here; it is
|
|
11949
|
+
# removed when the run ends, but a killed run can leave it behind.
|
|
11950
|
+
.metaproject/data/review/mutate-scratch/
|
|
11948
11951
|
`;
|
|
11949
11952
|
}
|
|
11950
11953
|
function escapeRegExp2(value) {
|
|
@@ -26582,6 +26585,24 @@ function globToRegExp(glob) {
|
|
|
26582
26585
|
}
|
|
26583
26586
|
|
|
26584
26587
|
// src/review/scope.ts
|
|
26588
|
+
var SCOPE_DROP_REASONS = [
|
|
26589
|
+
"lockfile",
|
|
26590
|
+
"generated",
|
|
26591
|
+
"vendored",
|
|
26592
|
+
"snapshot",
|
|
26593
|
+
"minified",
|
|
26594
|
+
"binary",
|
|
26595
|
+
"whitespace-only",
|
|
26596
|
+
"comment-only"
|
|
26597
|
+
];
|
|
26598
|
+
var DEFAULT_CONTEXT_LINES = 20;
|
|
26599
|
+
var DEFAULT_REVIEW_SCOPE_CONFIG = {
|
|
26600
|
+
contextLines: DEFAULT_CONTEXT_LINES,
|
|
26601
|
+
detectWhitespaceOnly: true,
|
|
26602
|
+
detectCommentOnly: true,
|
|
26603
|
+
retainCommentBlocks: false,
|
|
26604
|
+
additionalGeneratedDirectories: []
|
|
26605
|
+
};
|
|
26585
26606
|
var LOCKFILE_BASENAMES = new Set([
|
|
26586
26607
|
"package-lock.json",
|
|
26587
26608
|
"npm-shrinkwrap.json",
|
|
@@ -26628,7 +26649,182 @@ var SOURCE_ROOT_SEGMENTS = new Set([
|
|
|
26628
26649
|
"specs",
|
|
26629
26650
|
"__tests__"
|
|
26630
26651
|
]);
|
|
26652
|
+
var GENERATED_SUFFIXES = [
|
|
26653
|
+
".js.map",
|
|
26654
|
+
".mjs.map",
|
|
26655
|
+
".cjs.map",
|
|
26656
|
+
".css.map",
|
|
26657
|
+
".pb.go",
|
|
26658
|
+
".pb.cc",
|
|
26659
|
+
".pb.h",
|
|
26660
|
+
"_pb2.py",
|
|
26661
|
+
"_pb2_grpc.py",
|
|
26662
|
+
".generated.ts",
|
|
26663
|
+
".g.dart",
|
|
26664
|
+
".freezed.dart"
|
|
26665
|
+
];
|
|
26666
|
+
var MINIFIED_SUFFIXES = [".min.js", ".min.mjs", ".min.cjs", ".min.css"];
|
|
26631
26667
|
var SNAPSHOT_DIRECTORIES = new Set(["__snapshots__"]);
|
|
26668
|
+
var SNAPSHOT_SUFFIXES = [".snap"];
|
|
26669
|
+
function classifyPath(path, config = DEFAULT_REVIEW_SCOPE_CONFIG) {
|
|
26670
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
26671
|
+
const segments = normalized.split("/").filter((segment) => segment.length > 0);
|
|
26672
|
+
const basename = segments.at(-1) ?? normalized;
|
|
26673
|
+
const directories = segments.slice(0, -1);
|
|
26674
|
+
for (const segment of directories) {
|
|
26675
|
+
if (VENDOR_DIRECTORIES.has(segment)) {
|
|
26676
|
+
return { reason: "vendored", detail: `vendored: path segment "${segment}/"` };
|
|
26677
|
+
}
|
|
26678
|
+
}
|
|
26679
|
+
const extraGenerated = new Set(config.additionalGeneratedDirectories);
|
|
26680
|
+
let sawSourceRoot = false;
|
|
26681
|
+
for (const segment of directories) {
|
|
26682
|
+
if (GENERATED_DIRECTORIES.has(segment)) {
|
|
26683
|
+
return { reason: "generated", detail: `generated: build-output directory "${segment}/"` };
|
|
26684
|
+
}
|
|
26685
|
+
if (extraGenerated.has(segment)) {
|
|
26686
|
+
return { reason: "generated", detail: `generated: project-configured directory "${segment}/"` };
|
|
26687
|
+
}
|
|
26688
|
+
if (AMBIGUOUS_GENERATED_DIRECTORIES.has(segment) && !sawSourceRoot) {
|
|
26689
|
+
return { reason: "generated", detail: `generated: build-output directory "${segment}/" with no source root above it` };
|
|
26690
|
+
}
|
|
26691
|
+
if (SOURCE_ROOT_SEGMENTS.has(segment)) {
|
|
26692
|
+
sawSourceRoot = true;
|
|
26693
|
+
}
|
|
26694
|
+
}
|
|
26695
|
+
for (const segment of directories) {
|
|
26696
|
+
if (SNAPSHOT_DIRECTORIES.has(segment)) {
|
|
26697
|
+
return { reason: "snapshot", detail: `snapshot: path segment "${segment}/"` };
|
|
26698
|
+
}
|
|
26699
|
+
}
|
|
26700
|
+
if (SNAPSHOT_SUFFIXES.some((suffix) => basename.endsWith(suffix))) {
|
|
26701
|
+
return { reason: "snapshot", detail: `snapshot: filename ends with "${SNAPSHOT_SUFFIXES.find((s) => basename.endsWith(s)) ?? ""}"` };
|
|
26702
|
+
}
|
|
26703
|
+
if (LOCKFILE_BASENAMES.has(basename)) {
|
|
26704
|
+
return { reason: "lockfile", detail: `lockfile: dependency-manager output "${basename}"` };
|
|
26705
|
+
}
|
|
26706
|
+
const minified = MINIFIED_SUFFIXES.find((suffix) => basename.endsWith(suffix));
|
|
26707
|
+
if (minified !== undefined) {
|
|
26708
|
+
return { reason: "minified", detail: `minified: filename ends with "${minified}"` };
|
|
26709
|
+
}
|
|
26710
|
+
const generatedSuffix = GENERATED_SUFFIXES.find((suffix) => basename.endsWith(suffix));
|
|
26711
|
+
if (generatedSuffix !== undefined) {
|
|
26712
|
+
return { reason: "generated", detail: `generated: filename ends with "${generatedSuffix}"` };
|
|
26713
|
+
}
|
|
26714
|
+
return null;
|
|
26715
|
+
}
|
|
26716
|
+
var HUNK_HEADER = /^@@+ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
26717
|
+
function stripDiffPathPrefix(raw) {
|
|
26718
|
+
const withoutTimestamp = raw.split("\t")[0] ?? raw;
|
|
26719
|
+
const trimmed = withoutTimestamp.trim();
|
|
26720
|
+
if (trimmed === "/dev/null") {
|
|
26721
|
+
return trimmed;
|
|
26722
|
+
}
|
|
26723
|
+
return /^[ab]\//.test(trimmed) ? trimmed.slice(2) : trimmed;
|
|
26724
|
+
}
|
|
26725
|
+
function parseUnifiedDiff(diff) {
|
|
26726
|
+
const files = [];
|
|
26727
|
+
let current;
|
|
26728
|
+
let hunk;
|
|
26729
|
+
let newCursor = 0;
|
|
26730
|
+
let pendingOldPath;
|
|
26731
|
+
const closeHunk = () => {
|
|
26732
|
+
if (hunk !== undefined && current !== undefined) {
|
|
26733
|
+
current.hunks.push(hunk);
|
|
26734
|
+
}
|
|
26735
|
+
hunk = undefined;
|
|
26736
|
+
};
|
|
26737
|
+
const closeFile = () => {
|
|
26738
|
+
closeHunk();
|
|
26739
|
+
if (current !== undefined) {
|
|
26740
|
+
files.push(current);
|
|
26741
|
+
}
|
|
26742
|
+
current = undefined;
|
|
26743
|
+
pendingOldPath = undefined;
|
|
26744
|
+
};
|
|
26745
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
26746
|
+
if (line.startsWith("diff --git ")) {
|
|
26747
|
+
closeFile();
|
|
26748
|
+
const match = /^diff --git (.+?) (\S+)$/.exec(line);
|
|
26749
|
+
const path = match ? stripDiffPathPrefix(match[2]) : "";
|
|
26750
|
+
current = { path, binary: false, hunks: [] };
|
|
26751
|
+
continue;
|
|
26752
|
+
}
|
|
26753
|
+
if (line.startsWith("--- ")) {
|
|
26754
|
+
closeHunk();
|
|
26755
|
+
pendingOldPath = stripDiffPathPrefix(line.slice(4));
|
|
26756
|
+
if (current === undefined) {
|
|
26757
|
+
current = { path: pendingOldPath, binary: false, hunks: [] };
|
|
26758
|
+
}
|
|
26759
|
+
continue;
|
|
26760
|
+
}
|
|
26761
|
+
if (line.startsWith("+++ ")) {
|
|
26762
|
+
closeHunk();
|
|
26763
|
+
const newPath = stripDiffPathPrefix(line.slice(4));
|
|
26764
|
+
if (current === undefined) {
|
|
26765
|
+
current = { path: newPath, binary: false, hunks: [] };
|
|
26766
|
+
}
|
|
26767
|
+
const resolved = newPath === "/dev/null" ? pendingOldPath ?? current.path : newPath;
|
|
26768
|
+
if (resolved.length > 0 && resolved !== "/dev/null") {
|
|
26769
|
+
current.path = resolved;
|
|
26770
|
+
}
|
|
26771
|
+
continue;
|
|
26772
|
+
}
|
|
26773
|
+
if (current !== undefined && (line.startsWith("Binary files ") || line.startsWith("GIT binary patch"))) {
|
|
26774
|
+
closeHunk();
|
|
26775
|
+
current.binary = true;
|
|
26776
|
+
continue;
|
|
26777
|
+
}
|
|
26778
|
+
const header = HUNK_HEADER.exec(line);
|
|
26779
|
+
if (header !== null && current !== undefined) {
|
|
26780
|
+
closeHunk();
|
|
26781
|
+
newCursor = Number.parseInt(header[3], 10);
|
|
26782
|
+
hunk = { header: line, lines: [] };
|
|
26783
|
+
continue;
|
|
26784
|
+
}
|
|
26785
|
+
if (hunk === undefined) {
|
|
26786
|
+
continue;
|
|
26787
|
+
}
|
|
26788
|
+
if (line.startsWith("\\")) {
|
|
26789
|
+
continue;
|
|
26790
|
+
}
|
|
26791
|
+
const marker = line.charAt(0);
|
|
26792
|
+
const anchor = Math.max(1, newCursor);
|
|
26793
|
+
if (marker === "+") {
|
|
26794
|
+
hunk.lines.push({ kind: "add", raw: line, text: line.slice(1), anchor });
|
|
26795
|
+
newCursor += 1;
|
|
26796
|
+
continue;
|
|
26797
|
+
}
|
|
26798
|
+
if (marker === "-") {
|
|
26799
|
+
hunk.lines.push({ kind: "del", raw: line, text: line.slice(1), anchor });
|
|
26800
|
+
continue;
|
|
26801
|
+
}
|
|
26802
|
+
if (marker === " " || line.length === 0) {
|
|
26803
|
+
hunk.lines.push({ kind: "context", raw: line, text: line.slice(1), anchor });
|
|
26804
|
+
newCursor += 1;
|
|
26805
|
+
continue;
|
|
26806
|
+
}
|
|
26807
|
+
closeHunk();
|
|
26808
|
+
}
|
|
26809
|
+
closeFile();
|
|
26810
|
+
return files.filter((file) => file.path.length > 0 && file.path !== "/dev/null");
|
|
26811
|
+
}
|
|
26812
|
+
function changeBlocks(hunk) {
|
|
26813
|
+
const blocks = [];
|
|
26814
|
+
let start;
|
|
26815
|
+
for (let index = 0;index <= hunk.lines.length; index += 1) {
|
|
26816
|
+
const line = hunk.lines[index];
|
|
26817
|
+
const changed = line !== undefined && line.kind !== "context";
|
|
26818
|
+
if (changed && start === undefined) {
|
|
26819
|
+
start = index;
|
|
26820
|
+
}
|
|
26821
|
+
if (!changed && start !== undefined) {
|
|
26822
|
+
blocks.push({ from: start, to: index, lines: hunk.lines.slice(start, index) });
|
|
26823
|
+
start = undefined;
|
|
26824
|
+
}
|
|
26825
|
+
}
|
|
26826
|
+
return blocks;
|
|
26827
|
+
}
|
|
26632
26828
|
var INDENT_SIGNIFICANT_EXTENSIONS = new Set([
|
|
26633
26829
|
".py",
|
|
26634
26830
|
".pyi",
|
|
@@ -26648,6 +26844,47 @@ var INDENT_SIGNIFICANT_EXTENSIONS = new Set([
|
|
|
26648
26844
|
".coffee",
|
|
26649
26845
|
".nim"
|
|
26650
26846
|
]);
|
|
26847
|
+
function extensionOf(path) {
|
|
26848
|
+
const basename = path.split("/").at(-1) ?? path;
|
|
26849
|
+
const dot = basename.lastIndexOf(".");
|
|
26850
|
+
return dot <= 0 ? "" : basename.slice(dot).toLowerCase();
|
|
26851
|
+
}
|
|
26852
|
+
var STRING_LITERAL = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g;
|
|
26853
|
+
function hasUnterminatedQuote(text) {
|
|
26854
|
+
return /["'`]/.test(text.replace(STRING_LITERAL, ""));
|
|
26855
|
+
}
|
|
26856
|
+
function normalizeForWhitespace(text, indentSignificant) {
|
|
26857
|
+
if (indentSignificant) {
|
|
26858
|
+
return text.replace(/\s+$/, "");
|
|
26859
|
+
}
|
|
26860
|
+
let normalized = "";
|
|
26861
|
+
let cursor = 0;
|
|
26862
|
+
for (const match of text.matchAll(STRING_LITERAL)) {
|
|
26863
|
+
const index = match.index ?? 0;
|
|
26864
|
+
normalized += text.slice(cursor, index).replace(/\s+/g, " ") + match[0];
|
|
26865
|
+
cursor = index + match[0].length;
|
|
26866
|
+
}
|
|
26867
|
+
normalized += text.slice(cursor).replace(/\s+/g, " ");
|
|
26868
|
+
return normalized.trim();
|
|
26869
|
+
}
|
|
26870
|
+
function isWhitespaceOnly(hunk, block, indentSignificant) {
|
|
26871
|
+
if (!indentSignificant && hunk.lines.some((line) => hasUnterminatedQuote(line.text))) {
|
|
26872
|
+
return false;
|
|
26873
|
+
}
|
|
26874
|
+
const removed = [];
|
|
26875
|
+
const added = [];
|
|
26876
|
+
for (const line of block.lines) {
|
|
26877
|
+
const normalized = normalizeForWhitespace(line.text, indentSignificant);
|
|
26878
|
+
if (normalized.length === 0) {
|
|
26879
|
+
continue;
|
|
26880
|
+
}
|
|
26881
|
+
(line.kind === "del" ? removed : added).push(normalized);
|
|
26882
|
+
}
|
|
26883
|
+
if (removed.length !== added.length) {
|
|
26884
|
+
return false;
|
|
26885
|
+
}
|
|
26886
|
+
return removed.every((value, index) => value === added[index]);
|
|
26887
|
+
}
|
|
26651
26888
|
var C_FAMILY = { line: ["//"], block: true, multilineStringMarkers: ["`"] };
|
|
26652
26889
|
var HASH_FAMILY = { line: ["#"], block: false, multilineStringMarkers: ['"""', "'''", "<<"] };
|
|
26653
26890
|
var DASH_FAMILY = { line: ["--"], block: false, multilineStringMarkers: ["[["] };
|
|
@@ -26683,6 +26920,332 @@ var COMMENT_STYLES = new Map([
|
|
|
26683
26920
|
...[".py", ".rb", ".sh", ".bash", ".zsh", ".yml", ".yaml", ".toml", ".tf", ".tfvars"].map((extension) => [extension, HASH_FAMILY]),
|
|
26684
26921
|
...[".sql", ".lua"].map((extension) => [extension, DASH_FAMILY])
|
|
26685
26922
|
]);
|
|
26923
|
+
var DIRECTIVE_MARKERS = [
|
|
26924
|
+
"eslint",
|
|
26925
|
+
"ts-ignore",
|
|
26926
|
+
"ts-expect-error",
|
|
26927
|
+
"ts-nocheck",
|
|
26928
|
+
"@ts-",
|
|
26929
|
+
"prettier",
|
|
26930
|
+
"biome-ignore",
|
|
26931
|
+
"rome-ignore",
|
|
26932
|
+
"oxlint",
|
|
26933
|
+
"stylelint",
|
|
26934
|
+
"istanbul ignore",
|
|
26935
|
+
"c8 ignore",
|
|
26936
|
+
"v8 ignore",
|
|
26937
|
+
"coverage:",
|
|
26938
|
+
"nolint",
|
|
26939
|
+
"golangci",
|
|
26940
|
+
"go:build",
|
|
26941
|
+
"go:generate",
|
|
26942
|
+
"go:embed",
|
|
26943
|
+
"deno-lint",
|
|
26944
|
+
"noinspection",
|
|
26945
|
+
"pylint",
|
|
26946
|
+
"mypy",
|
|
26947
|
+
"type: ignore",
|
|
26948
|
+
"noqa",
|
|
26949
|
+
"flake8",
|
|
26950
|
+
"pragma",
|
|
26951
|
+
"nosec",
|
|
26952
|
+
"bandit",
|
|
26953
|
+
"shellcheck",
|
|
26954
|
+
"swiftlint",
|
|
26955
|
+
"codeql",
|
|
26956
|
+
"sonar",
|
|
26957
|
+
"keryx:",
|
|
26958
|
+
"-*- coding",
|
|
26959
|
+
"use strict",
|
|
26960
|
+
"@flow",
|
|
26961
|
+
"@jsx",
|
|
26962
|
+
"clang-format",
|
|
26963
|
+
"editorconfig",
|
|
26964
|
+
"webpackchunkname",
|
|
26965
|
+
"@vite-ignore",
|
|
26966
|
+
"sourcemappingurl"
|
|
26967
|
+
];
|
|
26968
|
+
function isDirectiveComment(text) {
|
|
26969
|
+
const lowered = text.toLowerCase();
|
|
26970
|
+
if (lowered.trimStart().startsWith("#!")) {
|
|
26971
|
+
return true;
|
|
26972
|
+
}
|
|
26973
|
+
return DIRECTIVE_MARKERS.some((marker) => lowered.includes(marker));
|
|
26974
|
+
}
|
|
26975
|
+
function commentFlags(lines, style) {
|
|
26976
|
+
let inBlock = false;
|
|
26977
|
+
if (style.block) {
|
|
26978
|
+
const openIndex = lines.findIndex((line) => line.text.includes("/*"));
|
|
26979
|
+
const closeIndex = lines.findIndex((line) => line.text.includes("*/"));
|
|
26980
|
+
inBlock = closeIndex >= 0 && (openIndex < 0 || closeIndex < openIndex);
|
|
26981
|
+
}
|
|
26982
|
+
return lines.map((line) => {
|
|
26983
|
+
const trimmed = line.text.trim();
|
|
26984
|
+
if (inBlock) {
|
|
26985
|
+
const close = trimmed.lastIndexOf("*/");
|
|
26986
|
+
if (close < 0) {
|
|
26987
|
+
return true;
|
|
26988
|
+
}
|
|
26989
|
+
inBlock = false;
|
|
26990
|
+
return trimmed.slice(close + 2).trim().length === 0;
|
|
26991
|
+
}
|
|
26992
|
+
if (trimmed.length === 0) {
|
|
26993
|
+
return true;
|
|
26994
|
+
}
|
|
26995
|
+
if (style.line.some((marker) => trimmed.startsWith(marker))) {
|
|
26996
|
+
return true;
|
|
26997
|
+
}
|
|
26998
|
+
if (style.block && trimmed.startsWith("/*")) {
|
|
26999
|
+
const close = trimmed.lastIndexOf("*/");
|
|
27000
|
+
if (close < 0) {
|
|
27001
|
+
inBlock = true;
|
|
27002
|
+
return true;
|
|
27003
|
+
}
|
|
27004
|
+
return trimmed.slice(close + 2).trim().length === 0;
|
|
27005
|
+
}
|
|
27006
|
+
return false;
|
|
27007
|
+
});
|
|
27008
|
+
}
|
|
27009
|
+
var BLOCK_COMMENT_DELIMITER = /\/\*|\*\//;
|
|
27010
|
+
var QUOTED_BLOCK_DELIMITER = /["'`][^"'`]*(?:\/\*|\*\/)/;
|
|
27011
|
+
function isCommentOnly(hunk, block, path) {
|
|
27012
|
+
const style = COMMENT_STYLES.get(extensionOf(path));
|
|
27013
|
+
if (style === undefined) {
|
|
27014
|
+
return false;
|
|
27015
|
+
}
|
|
27016
|
+
if (style.multilineStringMarkers.some((marker) => hunk.lines.some((line) => line.text.includes(marker)))) {
|
|
27017
|
+
return false;
|
|
27018
|
+
}
|
|
27019
|
+
if (style.block) {
|
|
27020
|
+
const changedDelimiter = hunk.lines.some((line) => line.kind !== "context" && BLOCK_COMMENT_DELIMITER.test(line.text));
|
|
27021
|
+
if (changedDelimiter || hunk.lines.some((line) => QUOTED_BLOCK_DELIMITER.test(line.text))) {
|
|
27022
|
+
return false;
|
|
27023
|
+
}
|
|
27024
|
+
}
|
|
27025
|
+
const flags = commentFlags(hunk.lines, style);
|
|
27026
|
+
let sawComment = false;
|
|
27027
|
+
for (let index = block.from;index < block.to; index += 1) {
|
|
27028
|
+
const line = hunk.lines[index];
|
|
27029
|
+
if (line === undefined) {
|
|
27030
|
+
return false;
|
|
27031
|
+
}
|
|
27032
|
+
if (flags[index] !== true) {
|
|
27033
|
+
return false;
|
|
27034
|
+
}
|
|
27035
|
+
if (line.text.trim().length === 0) {
|
|
27036
|
+
continue;
|
|
27037
|
+
}
|
|
27038
|
+
if (isDirectiveComment(line.text)) {
|
|
27039
|
+
return false;
|
|
27040
|
+
}
|
|
27041
|
+
sawComment = true;
|
|
27042
|
+
}
|
|
27043
|
+
return sawComment;
|
|
27044
|
+
}
|
|
27045
|
+
function emptyCounts() {
|
|
27046
|
+
const droppedByReason = Object.fromEntries(SCOPE_DROP_REASONS.map((reason) => [reason, 0]));
|
|
27047
|
+
return {
|
|
27048
|
+
filesSeen: 0,
|
|
27049
|
+
filesRetained: 0,
|
|
27050
|
+
filesDropped: 0,
|
|
27051
|
+
blocksSeen: 0,
|
|
27052
|
+
blocksRetained: 0,
|
|
27053
|
+
blocksDropped: 0,
|
|
27054
|
+
changedLinesRetained: 0,
|
|
27055
|
+
changedLinesDropped: 0,
|
|
27056
|
+
droppedByReason
|
|
27057
|
+
};
|
|
27058
|
+
}
|
|
27059
|
+
function resolveConfig(config) {
|
|
27060
|
+
const contextLines = config?.contextLines ?? DEFAULT_REVIEW_SCOPE_CONFIG.contextLines;
|
|
27061
|
+
return {
|
|
27062
|
+
contextLines: Number.isFinite(contextLines) ? Math.max(0, Math.trunc(contextLines)) : DEFAULT_CONTEXT_LINES,
|
|
27063
|
+
detectWhitespaceOnly: config?.detectWhitespaceOnly ?? DEFAULT_REVIEW_SCOPE_CONFIG.detectWhitespaceOnly,
|
|
27064
|
+
detectCommentOnly: config?.detectCommentOnly ?? DEFAULT_REVIEW_SCOPE_CONFIG.detectCommentOnly,
|
|
27065
|
+
retainCommentBlocks: config?.retainCommentBlocks ?? DEFAULT_REVIEW_SCOPE_CONFIG.retainCommentBlocks,
|
|
27066
|
+
additionalGeneratedDirectories: config?.additionalGeneratedDirectories ?? DEFAULT_REVIEW_SCOPE_CONFIG.additionalGeneratedDirectories
|
|
27067
|
+
};
|
|
27068
|
+
}
|
|
27069
|
+
function windowsFor(blocks, contextLines) {
|
|
27070
|
+
const windows = [];
|
|
27071
|
+
for (const block of blocks) {
|
|
27072
|
+
const anchors = block.lines.map((line) => line.anchor);
|
|
27073
|
+
const first = Math.min(...anchors);
|
|
27074
|
+
const last = Math.max(...anchors);
|
|
27075
|
+
const start = Math.max(1, first - contextLines);
|
|
27076
|
+
const end = last + contextLines;
|
|
27077
|
+
const previous = windows.at(-1);
|
|
27078
|
+
if (previous !== undefined && start <= previous.end) {
|
|
27079
|
+
previous.end = Math.max(previous.end, end);
|
|
27080
|
+
previous.blocks.push(block);
|
|
27081
|
+
continue;
|
|
27082
|
+
}
|
|
27083
|
+
windows.push({ start, end, blocks: [block] });
|
|
27084
|
+
}
|
|
27085
|
+
return windows;
|
|
27086
|
+
}
|
|
27087
|
+
function keepsCommentBlock(setting, hunk, block) {
|
|
27088
|
+
if (setting === false) {
|
|
27089
|
+
return false;
|
|
27090
|
+
}
|
|
27091
|
+
const added = [];
|
|
27092
|
+
for (let index = block.from;index < block.to; index += 1) {
|
|
27093
|
+
const line = hunk.lines[index];
|
|
27094
|
+
if (line !== undefined && line.kind === "add" && line.text.trim().length > 0) {
|
|
27095
|
+
added.push(line.text);
|
|
27096
|
+
}
|
|
27097
|
+
}
|
|
27098
|
+
if (added.length === 0) {
|
|
27099
|
+
return false;
|
|
27100
|
+
}
|
|
27101
|
+
if (setting === true) {
|
|
27102
|
+
return true;
|
|
27103
|
+
}
|
|
27104
|
+
let pattern;
|
|
27105
|
+
try {
|
|
27106
|
+
pattern = new RegExp(setting, "i");
|
|
27107
|
+
} catch {
|
|
27108
|
+
return true;
|
|
27109
|
+
}
|
|
27110
|
+
return added.some((text) => pattern.test(text));
|
|
27111
|
+
}
|
|
27112
|
+
function buildReviewScope(diff, config) {
|
|
27113
|
+
const resolved = resolveConfig(config);
|
|
27114
|
+
const counts = emptyCounts();
|
|
27115
|
+
const drops = [];
|
|
27116
|
+
const kept = [];
|
|
27117
|
+
const regions = [];
|
|
27118
|
+
const files = [];
|
|
27119
|
+
for (const file of parseUnifiedDiff(diff)) {
|
|
27120
|
+
counts.filesSeen += 1;
|
|
27121
|
+
const fileChangedLines = file.hunks.reduce((total, current) => total + current.lines.filter((line) => line.kind !== "context").length, 0);
|
|
27122
|
+
const pathDrop = classifyPath(file.path, resolved);
|
|
27123
|
+
if (pathDrop !== null) {
|
|
27124
|
+
counts.filesDropped += 1;
|
|
27125
|
+
counts.changedLinesDropped += fileChangedLines;
|
|
27126
|
+
counts.droppedByReason[pathDrop.reason] += 1;
|
|
27127
|
+
drops.push({
|
|
27128
|
+
path: file.path,
|
|
27129
|
+
reason: pathDrop.reason,
|
|
27130
|
+
detail: pathDrop.detail,
|
|
27131
|
+
granularity: "file",
|
|
27132
|
+
changedLines: fileChangedLines
|
|
27133
|
+
});
|
|
27134
|
+
continue;
|
|
27135
|
+
}
|
|
27136
|
+
if (file.binary) {
|
|
27137
|
+
counts.filesDropped += 1;
|
|
27138
|
+
counts.droppedByReason.binary += 1;
|
|
27139
|
+
drops.push({
|
|
27140
|
+
path: file.path,
|
|
27141
|
+
reason: "binary",
|
|
27142
|
+
detail: "binary: the diff carries no reviewable text for this file",
|
|
27143
|
+
granularity: "file",
|
|
27144
|
+
changedLines: 0
|
|
27145
|
+
});
|
|
27146
|
+
continue;
|
|
27147
|
+
}
|
|
27148
|
+
const indentSignificant = INDENT_SIGNIFICANT_EXTENSIONS.has(extensionOf(file.path));
|
|
27149
|
+
let retainedInFile = 0;
|
|
27150
|
+
for (const hunk of file.hunks) {
|
|
27151
|
+
const retained = [];
|
|
27152
|
+
for (const block of changeBlocks(hunk)) {
|
|
27153
|
+
counts.blocksSeen += 1;
|
|
27154
|
+
const changedLines = block.lines.length;
|
|
27155
|
+
const span = {
|
|
27156
|
+
startLine: Math.min(...block.lines.map((line) => line.anchor)),
|
|
27157
|
+
endLine: Math.max(...block.lines.map((line) => line.anchor))
|
|
27158
|
+
};
|
|
27159
|
+
if (resolved.detectWhitespaceOnly && isWhitespaceOnly(hunk, block, indentSignificant)) {
|
|
27160
|
+
counts.blocksDropped += 1;
|
|
27161
|
+
counts.changedLinesDropped += changedLines;
|
|
27162
|
+
counts.droppedByReason["whitespace-only"] += 1;
|
|
27163
|
+
drops.push({
|
|
27164
|
+
path: file.path,
|
|
27165
|
+
reason: "whitespace-only",
|
|
27166
|
+
detail: indentSignificant ? "whitespace-only: trailing whitespace only (leading whitespace is significant for this file type and was compared)" : "whitespace-only: identical once whitespace is removed, with no change in line count",
|
|
27167
|
+
granularity: "block",
|
|
27168
|
+
startLine: span.startLine,
|
|
27169
|
+
endLine: span.endLine,
|
|
27170
|
+
changedLines
|
|
27171
|
+
});
|
|
27172
|
+
continue;
|
|
27173
|
+
}
|
|
27174
|
+
if (resolved.detectCommentOnly && isCommentOnly(hunk, block, file.path)) {
|
|
27175
|
+
if (keepsCommentBlock(resolved.retainCommentBlocks ?? false, hunk, block)) {
|
|
27176
|
+
kept.push({
|
|
27177
|
+
path: file.path,
|
|
27178
|
+
reason: "kept:comment-rule",
|
|
27179
|
+
detail: `kept:comment-rule: comment-only block in ${extensionOf(file.path)} retained because the repository has a rule about comments`,
|
|
27180
|
+
startLine: span.startLine,
|
|
27181
|
+
endLine: span.endLine,
|
|
27182
|
+
changedLines
|
|
27183
|
+
});
|
|
27184
|
+
counts.keptByReason = { ...counts.keptByReason, "kept:comment-rule": (counts.keptByReason?.["kept:comment-rule"] ?? 0) + 1 };
|
|
27185
|
+
counts.blocksRetained += 1;
|
|
27186
|
+
counts.changedLinesRetained += changedLines;
|
|
27187
|
+
retained.push(block);
|
|
27188
|
+
continue;
|
|
27189
|
+
}
|
|
27190
|
+
counts.blocksDropped += 1;
|
|
27191
|
+
counts.changedLinesDropped += changedLines;
|
|
27192
|
+
counts.droppedByReason["comment-only"] += 1;
|
|
27193
|
+
drops.push({
|
|
27194
|
+
path: file.path,
|
|
27195
|
+
reason: "comment-only",
|
|
27196
|
+
detail: `comment-only: every changed line is a comment in ${extensionOf(file.path)} and none is a tool directive`,
|
|
27197
|
+
granularity: "block",
|
|
27198
|
+
startLine: span.startLine,
|
|
27199
|
+
endLine: span.endLine,
|
|
27200
|
+
changedLines
|
|
27201
|
+
});
|
|
27202
|
+
continue;
|
|
27203
|
+
}
|
|
27204
|
+
counts.blocksRetained += 1;
|
|
27205
|
+
counts.changedLinesRetained += changedLines;
|
|
27206
|
+
retained.push(block);
|
|
27207
|
+
}
|
|
27208
|
+
if (retained.length === 0) {
|
|
27209
|
+
continue;
|
|
27210
|
+
}
|
|
27211
|
+
const hunkFirst = hunk.lines[0]?.anchor ?? 1;
|
|
27212
|
+
const hunkLast = hunk.lines.at(-1)?.anchor ?? hunkFirst;
|
|
27213
|
+
for (const window of windowsFor(retained, resolved.contextLines)) {
|
|
27214
|
+
const lines = hunk.lines.filter((line) => line.anchor >= window.start && line.anchor <= window.end);
|
|
27215
|
+
if (lines.length === 0) {
|
|
27216
|
+
continue;
|
|
27217
|
+
}
|
|
27218
|
+
const truncated = window.start < hunkFirst && hunkFirst > 1 || window.end > hunkLast;
|
|
27219
|
+
regions.push({
|
|
27220
|
+
path: file.path,
|
|
27221
|
+
startLine: lines[0].anchor,
|
|
27222
|
+
endLine: lines.at(-1).anchor,
|
|
27223
|
+
changedLines: window.blocks.reduce((total, block) => total + block.lines.length, 0),
|
|
27224
|
+
contextTruncated: truncated,
|
|
27225
|
+
text: lines.map((line) => line.raw).join(`
|
|
27226
|
+
`)
|
|
27227
|
+
});
|
|
27228
|
+
retainedInFile += 1;
|
|
27229
|
+
}
|
|
27230
|
+
}
|
|
27231
|
+
if (retainedInFile > 0) {
|
|
27232
|
+
counts.filesRetained += 1;
|
|
27233
|
+
files.push(file.path);
|
|
27234
|
+
} else {
|
|
27235
|
+
counts.filesDropped += 1;
|
|
27236
|
+
}
|
|
27237
|
+
}
|
|
27238
|
+
return {
|
|
27239
|
+
schemaVersion: 1,
|
|
27240
|
+
mode: "diff",
|
|
27241
|
+
contextLines: resolved.contextLines,
|
|
27242
|
+
files,
|
|
27243
|
+
regions,
|
|
27244
|
+
drops,
|
|
27245
|
+
...kept.length > 0 ? { kept } : {},
|
|
27246
|
+
counts
|
|
27247
|
+
};
|
|
27248
|
+
}
|
|
26686
27249
|
|
|
26687
27250
|
// src/review/types.ts
|
|
26688
27251
|
var FINDING_DISPOSITION_STATES = [
|
|
@@ -28072,8 +28635,11 @@ var operatorDenials = new Set;
|
|
|
28072
28635
|
|
|
28073
28636
|
// src/review/autonomous-answers.ts
|
|
28074
28637
|
var ENV_AUTONOMOUS_ANSWERS_FILE = "KERYX_AUTONOMOUS_ANSWERS_FILE";
|
|
28075
|
-
|
|
28076
28638
|
// src/review/coverage.ts
|
|
28639
|
+
var LIGHT_MAX_FILES = 5;
|
|
28640
|
+
var LIGHT_MAX_CHANGED_LINES = 300;
|
|
28641
|
+
var LIGHT_FIXED_REVIEWERS = ["review-logic", "review-testing-practices"];
|
|
28642
|
+
var LIGHT_CONVENTIONS_PREFERENCE = ["review-frontend-conventions", "review-backend", "review-clean-code"];
|
|
28077
28643
|
var NOT_ROSTER = new Set([
|
|
28078
28644
|
"review-orchestrator",
|
|
28079
28645
|
"review-verifier",
|
|
@@ -28095,6 +28661,85 @@ var BUNDLED_PATH_GATES = {
|
|
|
28095
28661
|
"review-frontend-conventions": (f) => FRONTEND_EXT.test(f) || /\.stories\.tsx$/.test(f) || CODE_FILE.test(f),
|
|
28096
28662
|
"review-layout": (f) => /\.(tsx|jsx|css|scss)$/.test(f)
|
|
28097
28663
|
};
|
|
28664
|
+
var RISK_PATH_GATES = Object.fromEntries(Object.entries(BUNDLED_PATH_GATES).filter(([name]) => name !== "review-testing-practices" && name !== "review-frontend-conventions"));
|
|
28665
|
+
async function decideRoundDepth(root, request, input) {
|
|
28666
|
+
if (request === "full")
|
|
28667
|
+
return { requested: request, depth: "full", reason: "--depth full" };
|
|
28668
|
+
const checked = await autoDepth(root, input);
|
|
28669
|
+
if (request === "auto")
|
|
28670
|
+
return { requested: request, ...checked };
|
|
28671
|
+
return checked.depth === "light" ? { requested: request, depth: "light", reason: `--depth light: ${checked.reason}` } : { requested: request, depth: "full", reason: `--depth light not honoured: ${checked.reason.replace(/^auto: /, "")}` };
|
|
28672
|
+
}
|
|
28673
|
+
async function autoDepth(root, input) {
|
|
28674
|
+
const full = (reason) => ({ depth: "full", reason });
|
|
28675
|
+
if (input.forceFull !== undefined)
|
|
28676
|
+
return full(`auto: ${input.forceFull} forces the full set`);
|
|
28677
|
+
const files = input.files;
|
|
28678
|
+
if (files === undefined)
|
|
28679
|
+
return full("auto: no scope file list, so the diff size and risk class are unknown");
|
|
28680
|
+
if (input.changedLines === undefined)
|
|
28681
|
+
return full("auto: no line count (changed-line count unknown; pass --scope <scope.json> or --diff-lines <n>)");
|
|
28682
|
+
if (files.length > LIGHT_MAX_FILES)
|
|
28683
|
+
return full(`auto: ${files.length} files exceeds ${LIGHT_MAX_FILES}`);
|
|
28684
|
+
if (input.changedLines > LIGHT_MAX_CHANGED_LINES)
|
|
28685
|
+
return full(`auto: ${input.changedLines} changed lines exceeds ${LIGHT_MAX_CHANGED_LINES}`);
|
|
28686
|
+
const hit = Object.entries(RISK_PATH_GATES).find(([, gate]) => files.some(gate));
|
|
28687
|
+
if (hit)
|
|
28688
|
+
return full(`auto: a file hits the ${hit[0]} path gate (risk class)`);
|
|
28689
|
+
if (files.some((f) => DATA_FILE.test(f)))
|
|
28690
|
+
return full("auto: a data/migration file is in the diff (risk class)");
|
|
28691
|
+
const inventory = await collectReviewers(root);
|
|
28692
|
+
for (const reviewer of inventory.project) {
|
|
28693
|
+
if (reviewer.pathsSource === "none" || !reviewer.paths.length)
|
|
28694
|
+
continue;
|
|
28695
|
+
const patterns = reviewer.paths.map(globToRegExp2);
|
|
28696
|
+
if (files.some((f) => patterns.some((p) => p.test(f))))
|
|
28697
|
+
return full(`auto: a file hits the project reviewer ${reviewer.name} path gate (risk class)`);
|
|
28698
|
+
}
|
|
28699
|
+
return { depth: "light", reason: `auto: ${files.length} file(s), ${input.changedLines} changed line(s), no risk-class path gate` };
|
|
28700
|
+
}
|
|
28701
|
+
var ROUND_DEPTH_FILE = "round-depth.json";
|
|
28702
|
+
async function packageDepthBasis(packageDir) {
|
|
28703
|
+
const manifest = await readFile22(path46.join(packageDir, "manifest.json"), "utf8").then((t) => JSON.parse(t)).catch(() => {
|
|
28704
|
+
return;
|
|
28705
|
+
});
|
|
28706
|
+
const head = manifest?.target?.head;
|
|
28707
|
+
const files = await packageScopeFiles(packageDir);
|
|
28708
|
+
return { head: typeof head === "string" ? head : undefined, files: files === undefined ? undefined : [...files].sort(), changedLines: await packageChangedLines(packageDir) };
|
|
28709
|
+
}
|
|
28710
|
+
async function packageChangedLines(packageDir) {
|
|
28711
|
+
try {
|
|
28712
|
+
const parsed = JSON.parse(await readFile22(path46.join(packageDir, "scope-files.json"), "utf8"));
|
|
28713
|
+
return typeof parsed?.changedLines === "number" && Number.isFinite(parsed.changedLines) && parsed.changedLines >= 0 ? parsed.changedLines : undefined;
|
|
28714
|
+
} catch {
|
|
28715
|
+
return;
|
|
28716
|
+
}
|
|
28717
|
+
}
|
|
28718
|
+
async function packageRoundDepth(packageDir) {
|
|
28719
|
+
let parsed;
|
|
28720
|
+
try {
|
|
28721
|
+
parsed = JSON.parse(await readFile22(path46.join(packageDir, ROUND_DEPTH_FILE), "utf8"));
|
|
28722
|
+
} catch {
|
|
28723
|
+
return;
|
|
28724
|
+
}
|
|
28725
|
+
if (!((parsed?.depth === "light" || parsed?.depth === "full") && typeof parsed.reason === "string"))
|
|
28726
|
+
return;
|
|
28727
|
+
const decision = { requested: parsed.requested ?? "auto", depth: parsed.depth, reason: parsed.reason };
|
|
28728
|
+
if (decision.depth === "full")
|
|
28729
|
+
return decision;
|
|
28730
|
+
const now = await packageDepthBasis(packageDir);
|
|
28731
|
+
const recorded = Array.isArray(parsed.files) && parsed.files.every((f) => typeof f === "string") ? [...parsed.files].sort() : undefined;
|
|
28732
|
+
const stale = (why) => ({ requested: decision.requested, depth: "full", reason: `${ROUND_DEPTH_FILE} ignored: ${why}`, stale: why });
|
|
28733
|
+
if (recorded === undefined || now.files === undefined || recorded.join(`
|
|
28734
|
+
`) !== now.files.join(`
|
|
28735
|
+
`))
|
|
28736
|
+
return stale("it was decided for a different scope file list");
|
|
28737
|
+
if (typeof parsed.changedLines !== "number" || parsed.changedLines !== now.changedLines)
|
|
28738
|
+
return stale("it was decided for a different changed-line count");
|
|
28739
|
+
if (parsed.head !== undefined && now.head !== undefined && parsed.head !== now.head)
|
|
28740
|
+
return stale("it was decided for a different head");
|
|
28741
|
+
return decision;
|
|
28742
|
+
}
|
|
28098
28743
|
function globToRegExp2(glob) {
|
|
28099
28744
|
const body = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\/?/g, (match, offset, whole) => offset + match.length === whole.length ? "\uE001" : "\uE000").replace(/\*/g, "[^/]*").replace(/\uE001/g, ".*").replace(/\uE000/g, "(?:.*/)?");
|
|
28100
28745
|
return new RegExp(`^${body}$`);
|
|
@@ -28159,6 +28804,18 @@ async function requiredReviewers(root, mode, files, options = {}) {
|
|
|
28159
28804
|
projectGates.set(reviewer.name, reviewer.paths.map(globToRegExp2));
|
|
28160
28805
|
}
|
|
28161
28806
|
const filesKnown = files !== undefined;
|
|
28807
|
+
if (options.depth === "light") {
|
|
28808
|
+
const conventions = LIGHT_CONVENTIONS_PREFERENCE.find((name) => roster.includes(name));
|
|
28809
|
+
const keep = new Set([...LIGHT_FIXED_REVIEWERS, ...conventions === undefined ? [] : [conventions]]);
|
|
28810
|
+
const light = roster.filter((name) => keep.has(name));
|
|
28811
|
+
for (const name of roster)
|
|
28812
|
+
if (!keep.has(name))
|
|
28813
|
+
excluded.push({ reviewer: name, reason: "light round: only logic, testing and the stack's conventions reviewer run" });
|
|
28814
|
+
for (const name of [REGRESSION, PR_CONTRACT])
|
|
28815
|
+
if (inventory.bundled.some((r) => r.name === name))
|
|
28816
|
+
excluded.push({ reviewer: name, reason: "light round: not part of the light set" });
|
|
28817
|
+
return { mode, depth: "light", required: light.sort(), excluded, filesKnown };
|
|
28818
|
+
}
|
|
28162
28819
|
const gated = (name) => {
|
|
28163
28820
|
if (!filesKnown)
|
|
28164
28821
|
return;
|
|
@@ -28197,13 +28854,58 @@ async function requiredReviewers(root, mode, files, options = {}) {
|
|
|
28197
28854
|
else
|
|
28198
28855
|
excluded.push({ reviewer: PR_CONTRACT, reason: "no pr-context.md in the package: there is no PR body checklist to compare with the diff" });
|
|
28199
28856
|
}
|
|
28200
|
-
return { mode, required: required.sort(), excluded, filesKnown };
|
|
28857
|
+
return { mode, depth: "full", required: required.sort(), excluded, filesKnown };
|
|
28858
|
+
}
|
|
28859
|
+
async function gitDiffStats(root, target) {
|
|
28860
|
+
try {
|
|
28861
|
+
assertGitRevision(target.ref);
|
|
28862
|
+
assertGitRevision(target.head);
|
|
28863
|
+
const proc = Bun.spawn(["git", "diff", "--no-color", "-U20", "--end-of-options", `${target.ref}...${target.head}`], { cwd: root, stdout: "pipe", stderr: "pipe", timeout: 15000, killSignal: "SIGKILL", maxBuffer: 8 * 1024 * 1024 });
|
|
28864
|
+
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
28865
|
+
if (exitCode !== 0 || proc.signalCode !== null)
|
|
28866
|
+
return;
|
|
28867
|
+
const scope = buildReviewScope(stdout);
|
|
28868
|
+
return { files: scope.files, changedLines: scope.counts.changedLinesRetained };
|
|
28869
|
+
} catch {
|
|
28870
|
+
return;
|
|
28871
|
+
}
|
|
28872
|
+
}
|
|
28873
|
+
async function packageDiffStats(options) {
|
|
28874
|
+
if (options.diffStats !== undefined)
|
|
28875
|
+
return options.diffStats();
|
|
28876
|
+
if (options.packageDir === undefined)
|
|
28877
|
+
return;
|
|
28878
|
+
const manifest = await readFile22(path46.join(options.packageDir, "manifest.json"), "utf8").then((t) => JSON.parse(t)).catch(() => {
|
|
28879
|
+
return;
|
|
28880
|
+
});
|
|
28881
|
+
const { ref, head } = manifest?.target ?? {};
|
|
28882
|
+
return typeof ref === "string" && typeof head === "string" ? gitDiffStats(options.root, { ref, head }) : undefined;
|
|
28201
28883
|
}
|
|
28202
28884
|
function isAttendedRun(env = process.env) {
|
|
28203
28885
|
return env[ENV_AUTONOMOUS_ANSWERS_FILE] === undefined;
|
|
28204
28886
|
}
|
|
28205
|
-
function
|
|
28887
|
+
async function effectiveDepth(record, options) {
|
|
28888
|
+
if (record.depth !== "light")
|
|
28889
|
+
return { depth: "full" };
|
|
28890
|
+
if (options.packageDir !== undefined) {
|
|
28891
|
+
const recorded = await packageRoundDepth(options.packageDir);
|
|
28892
|
+
if (recorded?.depth === "full" && recorded.stale === undefined)
|
|
28893
|
+
return { depth: "full", refused: `${ROUND_DEPTH_FILE} records a full round (${recorded.reason}), which binds` };
|
|
28894
|
+
}
|
|
28895
|
+
let input = { files: options.files, changedLines: options.changedLines };
|
|
28896
|
+
if (options.packageDir !== undefined || options.diffStats !== undefined) {
|
|
28897
|
+
const stats = await packageDiffStats(options);
|
|
28898
|
+
if (stats === undefined)
|
|
28899
|
+
return { depth: "full", refused: "the diff could not be resolved from git to confirm the size" };
|
|
28900
|
+
input = { files: stats.files, changedLines: stats.changedLines };
|
|
28901
|
+
}
|
|
28902
|
+
const decision = await decideRoundDepth(options.root, "auto", input);
|
|
28903
|
+
return decision.depth === "light" ? { depth: "light" } : { depth: "full", refused: decision.reason.replace(/^auto: /, "") };
|
|
28904
|
+
}
|
|
28905
|
+
function mutationErrors(record, options, depth) {
|
|
28206
28906
|
const pending = options.pendingMutants ?? 0;
|
|
28907
|
+
if (depth === "light")
|
|
28908
|
+
return [];
|
|
28207
28909
|
if (pending === 0 || (options.attended ?? isAttendedRun()) === false)
|
|
28208
28910
|
return [];
|
|
28209
28911
|
const w = record.mutationWaiver;
|
|
@@ -28228,9 +28930,10 @@ function packageHasPrContext(packageDir, name = "pr-context.md") {
|
|
|
28228
28930
|
}
|
|
28229
28931
|
async function coverageErrors(dispatch, options) {
|
|
28230
28932
|
const record = dispatch && typeof dispatch === "object" && !Array.isArray(dispatch) ? dispatch : undefined;
|
|
28231
|
-
|
|
28933
|
+
const effective = record === undefined ? undefined : await effectiveDepth(record, options);
|
|
28934
|
+
return [...await reviewerSetErrors(dispatch, options, effective), ...record === undefined || effective === undefined ? [] : mutationErrors(record, options, effective.depth)];
|
|
28232
28935
|
}
|
|
28233
|
-
async function reviewerSetErrors(dispatch, options) {
|
|
28936
|
+
async function reviewerSetErrors(dispatch, options, effective) {
|
|
28234
28937
|
if (!dispatch || typeof dispatch !== "object" || Array.isArray(dispatch))
|
|
28235
28938
|
return [];
|
|
28236
28939
|
const record = dispatch;
|
|
@@ -28248,14 +28951,15 @@ async function reviewerSetErrors(dispatch, options) {
|
|
|
28248
28951
|
return ["diff mode needs the scope-A file list: pass `--scope <scope.json>` to `keryx review start|ingest`, or run mode all"];
|
|
28249
28952
|
const need = await requiredReviewers(options.root, record.mode, options.files, {
|
|
28250
28953
|
blastRadius: options.blastRadius,
|
|
28251
|
-
prContext: options.prContext === true && dispatchKnowsRole(record, PR_CONTRACT)
|
|
28954
|
+
prContext: options.prContext === true && dispatchKnowsRole(record, PR_CONTRACT),
|
|
28955
|
+
depth: effective?.depth ?? "full"
|
|
28252
28956
|
});
|
|
28253
28957
|
const selected = new Set(Array.isArray(record.selected) ? record.selected.filter(text) : []);
|
|
28254
28958
|
const missing = need.required.filter((name) => !selected.has(name));
|
|
28255
28959
|
if (!missing.length)
|
|
28256
28960
|
return [];
|
|
28257
28961
|
return [
|
|
28258
|
-
`dispatch covers ${need.required.length - missing.length} of ${need.required.length} reviewers that mode ${record.mode} requires; missing: ${missing.join(", ")}. ` + "Dispatch them, or record an operator override {operatorQuote, reason}; `keryx review required` prints the set"
|
|
28962
|
+
`dispatch covers ${need.required.length - missing.length} of ${need.required.length} reviewers that mode ${record.mode} requires; missing: ${missing.join(", ")}. ` + "Dispatch them, or record an operator override {operatorQuote, reason}; `keryx review required` prints the set" + (effective?.refused === undefined ? "" : `. The dispatch says depth light, but the full set applies: ${effective.refused}`)
|
|
28259
28963
|
];
|
|
28260
28964
|
}
|
|
28261
28965
|
async function packageScopeFiles(packageDir) {
|
|
@@ -28684,6 +29388,8 @@ var MUTANT_KINDS = ["behavior", "presentation"];
|
|
|
28684
29388
|
var MUTANTS_SPEC = "mutants.json";
|
|
28685
29389
|
var EVIDENCE_DIR = "research-evidence";
|
|
28686
29390
|
var SKIP_COPY = new Set([".git", "node_modules"]);
|
|
29391
|
+
var SCRATCH_DIR = path49.join(".metaproject", "data", "review", "mutate-scratch");
|
|
29392
|
+
var OUTPUT_KEEP_CHARS = 256 * 1024;
|
|
28687
29393
|
function parseMutantSpec(raw) {
|
|
28688
29394
|
const list = Array.isArray(raw) ? raw : raw?.mutants;
|
|
28689
29395
|
if (!Array.isArray(list) || list.length === 0)
|
|
@@ -28712,6 +29418,7 @@ function parseMutantSpec(raw) {
|
|
|
28712
29418
|
};
|
|
28713
29419
|
});
|
|
28714
29420
|
}
|
|
29421
|
+
var liveGroups = new Set;
|
|
28715
29422
|
async function doneMutantIds(packageDir) {
|
|
28716
29423
|
const dir = path49.join(packageDir, EVIDENCE_DIR);
|
|
28717
29424
|
const done = new Set;
|
|
@@ -28809,7 +29516,9 @@ async function packageResearchErrors(packageDir, artifacts, findings, options =
|
|
|
28809
29516
|
const dispatch = a.reviewerDispatch === undefined ? [] : dispatchErrors(ledger?.dispatch);
|
|
28810
29517
|
const coverage = a.reviewerDispatch === undefined || options.coverage === false || dispatch.length ? [] : await coverageErrors(ledger?.dispatch, {
|
|
28811
29518
|
root: projectRootOfPackage(packageDir),
|
|
29519
|
+
packageDir,
|
|
28812
29520
|
files: await packageScopeFiles(packageDir),
|
|
29521
|
+
changedLines: await packageChangedLines(packageDir),
|
|
28813
29522
|
blastRadius: await packageHasBlastRadius(packageDir),
|
|
28814
29523
|
prContext: packageHasPrContext(packageDir),
|
|
28815
29524
|
pendingMutants: (await pendingMutants(packageDir)).length
|
|
@@ -31006,7 +31715,7 @@ function parseAcKinds(content) {
|
|
|
31006
31715
|
}
|
|
31007
31716
|
return { criteria, errors };
|
|
31008
31717
|
}
|
|
31009
|
-
function
|
|
31718
|
+
function emptyCounts2() {
|
|
31010
31719
|
return { exec: 0, invariant: 0, judged: 0, none: 0, unclassified: 0 };
|
|
31011
31720
|
}
|
|
31012
31721
|
function buildAcKindReport(flowId, content) {
|
|
@@ -31019,7 +31728,7 @@ function buildAcKindReport(flowId, content) {
|
|
|
31019
31728
|
return { report: reportFromRecords(flowId, criteria), errors: parsed.errors };
|
|
31020
31729
|
}
|
|
31021
31730
|
function reportFromRecords(flowId, criteria) {
|
|
31022
|
-
const counts =
|
|
31731
|
+
const counts = emptyCounts2();
|
|
31023
31732
|
const byValue = { revert: 0, mutation: 0, "failure-scenario": 0 };
|
|
31024
31733
|
let proven = 0;
|
|
31025
31734
|
for (const record of Object.values(criteria)) {
|