@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.
Files changed (30) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +5098 -2892
  3. package/dist/core.js +718 -9
  4. package/package.json +1 -1
  5. package/src/gdskills/bundled/skills/review/review-architecture/SKILL.md +1 -1
  6. package/src/gdskills/bundled/skills/review/review-backend/SKILL.md +1 -1
  7. package/src/gdskills/bundled/skills/review/review-clean-code/SKILL.md +1 -1
  8. package/src/gdskills/bundled/skills/review/review-core-boundaries/SKILL.md +1 -1
  9. package/src/gdskills/bundled/skills/review/review-flow-graph/SKILL.md +1 -1
  10. package/src/gdskills/bundled/skills/review/review-frontend/SKILL.md +1 -1
  11. package/src/gdskills/bundled/skills/review/review-frontend-conventions/SKILL.md +1 -1
  12. package/src/gdskills/bundled/skills/review/review-highload/SKILL.md +1 -1
  13. package/src/gdskills/bundled/skills/review/review-jev-comments/SKILL.md +1 -1
  14. package/src/gdskills/bundled/skills/review/review-jev-contract/SKILL.md +1 -1
  15. package/src/gdskills/bundled/skills/review/review-jev-docs/SKILL.md +1 -1
  16. package/src/gdskills/bundled/skills/review/review-jev-risk/SKILL.md +1 -1
  17. package/src/gdskills/bundled/skills/review/review-jev-rules/SKILL.md +1 -1
  18. package/src/gdskills/bundled/skills/review/review-jev-scenarios/SKILL.md +1 -1
  19. package/src/gdskills/bundled/skills/review/review-layout/SKILL.md +1 -1
  20. package/src/gdskills/bundled/skills/review/review-logic/SKILL.md +1 -1
  21. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +95 -10
  22. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +37 -37
  23. package/src/gdskills/bundled/skills/review/review-performance/SKILL.md +1 -1
  24. package/src/gdskills/bundled/skills/review/review-pr-contract/SKILL.md +1 -1
  25. package/src/gdskills/bundled/skills/review/review-pr-feedback/SKILL.md +1 -1
  26. package/src/gdskills/bundled/skills/review/review-regression/SKILL.md +1 -1
  27. package/src/gdskills/bundled/skills/review/review-security-code/SKILL.md +1 -1
  28. package/src/gdskills/bundled/skills/review/review-style/SKILL.md +1 -1
  29. package/src/gdskills/bundled/skills/review/review-testing-practices/SKILL.md +1 -1
  30. 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 mutationErrors(record, options) {
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
- return [...await reviewerSetErrors(dispatch, options), ...record === undefined ? [] : mutationErrors(record, options)];
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 emptyCounts() {
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 = emptyCounts();
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)) {