code-review-forge 2.0.0a1__py3-none-any.whl

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 (62) hide show
  1. code_forge/__init__.py +14 -0
  2. code_forge/__main__.py +8 -0
  3. code_forge/autofix.py +78 -0
  4. code_forge/baseline.py +216 -0
  5. code_forge/cli.py +983 -0
  6. code_forge/delta.py +65 -0
  7. code_forge/diagnose.py +109 -0
  8. code_forge/diff.py +82 -0
  9. code_forge/disposition.py +32 -0
  10. code_forge/e2e_check.py +641 -0
  11. code_forge/env_resolver.py +91 -0
  12. code_forge/errors.py +34 -0
  13. code_forge/exit_codes.py +37 -0
  14. code_forge/factories.py +191 -0
  15. code_forge/falsify.py +85 -0
  16. code_forge/gate_check.py +466 -0
  17. code_forge/git.py +351 -0
  18. code_forge/hold.py +126 -0
  19. code_forge/install_hooks.py +331 -0
  20. code_forge/lock.py +162 -0
  21. code_forge/machine.py +792 -0
  22. code_forge/mode_resolver.py +60 -0
  23. code_forge/mutation.py +380 -0
  24. code_forge/parsers/__init__.py +56 -0
  25. code_forge/parsers/_sarif.py +77 -0
  26. code_forge/parsers/base.py +65 -0
  27. code_forge/parsers/checkpatch.py +66 -0
  28. code_forge/parsers/clippy.py +85 -0
  29. code_forge/parsers/non_ascii.py +47 -0
  30. code_forge/parsers/ruff.py +18 -0
  31. code_forge/parsers/semgrep.py +18 -0
  32. code_forge/parsers/shellcheck.py +56 -0
  33. code_forge/registry.py +153 -0
  34. code_forge/reporter.py +133 -0
  35. code_forge/runner.py +205 -0
  36. code_forge/sarif.py +226 -0
  37. code_forge/skills/adversarial-qe/SKILL.md +272 -0
  38. code_forge/skills/code-forge/SKILL.md +1193 -0
  39. code_forge/skills/code-review-expert/SKILL.md +162 -0
  40. code_forge/skills/code-review-expert/references/code-quality-checklist.md +130 -0
  41. code_forge/skills/code-review-expert/references/removal-plan.md +52 -0
  42. code_forge/skills/code-review-expert/references/security-checklist.md +118 -0
  43. code_forge/skills/code-review-expert/references/solid-checklist.md +65 -0
  44. code_forge/skills/kernel-fp-verify/SKILL.md +101 -0
  45. code_forge/skills/qodo-review/SKILL.md +135 -0
  46. code_forge/skills/smoke-test/SKILL.md +253 -0
  47. code_forge/skills/smoke-test/references/boundary-cases.md +114 -0
  48. code_forge/skills/smoke-test/references/concurrency-patterns.md +306 -0
  49. code_forge/skills/smoke-test/references/injection-payloads.md +124 -0
  50. code_forge/skills/smoke-test/test-library/shell/README.md +271 -0
  51. code_forge/skills/smoke-test/test-library/shell/primitives.sh +352 -0
  52. code_forge/skills/smoke-test/test-library/shell/primitives_test.sh +324 -0
  53. code_forge/snapshot.py +196 -0
  54. code_forge/source.py +64 -0
  55. code_forge/state.py +246 -0
  56. code_forge/verdict.py +43 -0
  57. code_review_forge-2.0.0a1.dist-info/METADATA +237 -0
  58. code_review_forge-2.0.0a1.dist-info/RECORD +62 -0
  59. code_review_forge-2.0.0a1.dist-info/WHEEL +5 -0
  60. code_review_forge-2.0.0a1.dist-info/entry_points.txt +2 -0
  61. code_review_forge-2.0.0a1.dist-info/licenses/LICENSE +179 -0
  62. code_review_forge-2.0.0a1.dist-info/top_level.txt +1 -0
code_forge/git.py ADDED
@@ -0,0 +1,351 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """Git subprocess wrapper with diff-spec validation.
4
+
5
+ This module is THE single owner of git diff subprocess calls in the
6
+ codebase. All other modules call these functions -- they do not call
7
+ git directly. Addresses Consensus #1 (git diff execution unowned).
8
+
9
+ 02-03 additions: repo detection, ref validation, pseudo-ref resolution.
10
+ Existing Phase 1 API unchanged. New surface (B1 + H2 fixes):
11
+ - is_git_repo(cwd) -> bool (B1)
12
+ - resolve_git_ref(ref, cwd) -> str (resolved sha) (B1)
13
+ - is_pseudo_ref(name) -> bool
14
+ - working_tree_diff(...) -> str (H2)
15
+ - cached_diff(...) -> str
16
+ - git_diff(baseline, head, ...) -> str
17
+ """
18
+
19
+ import re
20
+ import shutil
21
+ import subprocess
22
+ import warnings
23
+ from pathlib import Path
24
+
25
+ # Safe known flags that are allowed despite starting with --
26
+ _SAFE_FLAGS = frozenset({"--staged", "--cached"})
27
+
28
+ # Allowlist regex for diff-spec values.
29
+ # Permits: branch names (feature/foo), tags (v1.2.3), commit hashes
30
+ # (abc123), HEAD references (HEAD, HEAD~1, HEAD^), remote refs
31
+ # (origin/main), commit ranges (abc..def, HEAD~3..HEAD), and the @
32
+ # character (for refs like HEAD@).
33
+ #
34
+ # Round 7 R7-L5 (DeepSeek): ^ and - placement inside the character
35
+ # class is fragile. Both are now explicitly escaped (\^, \-) so
36
+ # reordering the class will not silently change semantics.
37
+ #
38
+ # Curly braces ({}) are NOT permitted -- users should use explicit
39
+ # ref names instead of @{u} / @{upstream} syntax.
40
+ _DIFF_SPEC_RE = re.compile(
41
+ r"^[A-Za-z0-9_./~@\^\-]+(?:\.\.[A-Za-z0-9_./~@\^\-]+)?$"
42
+ )
43
+
44
+
45
+ def validate_diff_spec(diff_spec: str) -> str:
46
+ """Validate diff_spec against flag injection.
47
+
48
+ Returns sanitized spec unchanged.
49
+
50
+ Raises:
51
+ ValueError: on empty string, unsafe flags, or characters
52
+ outside the allowlist.
53
+ """
54
+ if not diff_spec:
55
+ raise ValueError("diff_spec must not be empty")
56
+
57
+ # Allow safe known flags
58
+ if diff_spec in _SAFE_FLAGS:
59
+ return diff_spec
60
+
61
+ # Reject other leading dashes (flag injection)
62
+ if diff_spec.startswith("-"):
63
+ raise ValueError(
64
+ "Invalid diff_spec: '%s' looks like a flag" % diff_spec
65
+ )
66
+
67
+ # Allowlist check -- reject everything not matching
68
+ if not _DIFF_SPEC_RE.match(diff_spec):
69
+ raise ValueError(
70
+ "Invalid diff_spec: '%s' contains disallowed characters"
71
+ % diff_spec
72
+ )
73
+
74
+ return diff_spec
75
+
76
+
77
+ def run_git_diff(
78
+ diff_spec: str = "HEAD",
79
+ extra_args: list[str] | None = None,
80
+ ) -> str:
81
+ """Execute git diff and return raw diff text.
82
+
83
+ Validates diff_spec before use. Default: git diff -U0 HEAD
84
+ (working tree vs HEAD).
85
+
86
+ Supports: HEAD, --staged, commit..commit, commit ranges.
87
+
88
+ Note (Round 3 item 12): extra_args exists for future extensibility
89
+ (e.g. --name-only). Currently unused by any caller in Phase 1.
90
+
91
+ Git diff exit code semantics (CRITICAL):
92
+ Exit 0: no differences (return stdout, typically empty)
93
+ Exit 1: differences found (NORMAL -- return stdout with diff)
94
+ Exit 128+: fatal git error (raise RuntimeError with stderr)
95
+
96
+ This addresses Mimo F-03: git diff returns 1 when differences
97
+ exist, NOT an error condition.
98
+
99
+ Raises:
100
+ RuntimeError: if git is not found or git returns exit 128+
101
+ ValueError: if diff_spec is invalid (from validate_diff_spec)
102
+ """
103
+ diff_spec = validate_diff_spec(diff_spec)
104
+
105
+ if shutil.which("git") is None:
106
+ raise RuntimeError("git not found")
107
+
108
+ cmd = ["git", "diff", "-U0", diff_spec] + (extra_args or [])
109
+
110
+ result = subprocess.run(
111
+ cmd,
112
+ capture_output=True,
113
+ text=True,
114
+ check=False,
115
+ )
116
+
117
+ if result.returncode not in (0, 1):
118
+ raise RuntimeError(
119
+ result.stderr or f"git diff failed (exit {result.returncode})"
120
+ )
121
+
122
+ return result.stdout
123
+
124
+
125
+ # --- 02-03 additions: pseudo-refs, repo detection, ref validation ---
126
+
127
+ WORKING = "WORKING"
128
+ INDEX = "INDEX"
129
+ PSEUDO_REFS = {WORKING, INDEX}
130
+
131
+
132
+ def is_pseudo_ref(name: str) -> bool:
133
+ """Check whether name is a forge pseudo-ref (WORKING or INDEX)."""
134
+ return name in PSEUDO_REFS
135
+
136
+
137
+ def is_git_repo(cwd: Path) -> bool:
138
+ """B1 fix: check whether cwd is inside a git repo.
139
+
140
+ Uses `git rev-parse --git-dir` (non-zero outside a repo).
141
+ """
142
+ try:
143
+ result = subprocess.run(
144
+ ["git", "rev-parse", "--git-dir"],
145
+ cwd=cwd,
146
+ capture_output=True,
147
+ text=True,
148
+ check=False,
149
+ )
150
+ return result.returncode == 0
151
+ except (FileNotFoundError, OSError):
152
+ return False
153
+
154
+
155
+ def resolve_git_ref(ref: str, cwd: Path) -> str:
156
+ """B1 fix: validate that a git ref exists; return its resolved sha.
157
+
158
+ Raises:
159
+ BaselineResolutionError: ref does not exist.
160
+ """
161
+ from .errors import BaselineResolutionError
162
+
163
+ result = subprocess.run(
164
+ ["git", "rev-parse", "--verify", ref + "^{commit}"],
165
+ cwd=cwd,
166
+ capture_output=True,
167
+ text=True,
168
+ check=False,
169
+ )
170
+ if result.returncode != 0:
171
+ raise BaselineResolutionError(
172
+ "git ref %r does not resolve in %s: %s"
173
+ % (ref, cwd, result.stderr.strip())
174
+ )
175
+ return result.stdout.strip()
176
+
177
+
178
+ def _is_likely_binary(path: Path) -> bool:
179
+ """H2 fix: heuristic binary detection via null-byte in first 8KB.
180
+
181
+ Matches git's own diff-detection behavior (loosely). Used to skip
182
+ binary untracked files in working_tree_diff.
183
+ """
184
+ try:
185
+ with open(path, "rb") as f:
186
+ chunk = f.read(8192)
187
+ return b"\0" in chunk
188
+ except OSError:
189
+ return False
190
+
191
+
192
+ def git_diff(
193
+ baseline_ref: str,
194
+ head_ref: str,
195
+ paths: list[Path],
196
+ repo_root: Path,
197
+ ) -> str:
198
+ """Standard `git diff <baseline_ref> <head_ref> -- <paths>`.
199
+
200
+ Exit code semantics (R3-1 fix; matches Phase 1 run_git_diff per
201
+ Mimo F-03 in src/forge/git.py:80-86):
202
+ 0 = no differences (return empty stdout)
203
+ 1 = differences found (NORMAL -- return stdout with diff)
204
+ 2+ = real git error (raise BaselineResolutionError)
205
+ """
206
+ from .errors import BaselineResolutionError
207
+
208
+ cmd = (
209
+ ["git", "diff", baseline_ref, head_ref, "--"]
210
+ + [str(p) for p in paths]
211
+ )
212
+ result = subprocess.run(
213
+ cmd, cwd=repo_root, capture_output=True, text=True, check=False
214
+ )
215
+ if result.returncode not in (0, 1):
216
+ raise BaselineResolutionError(
217
+ "git diff %s..%s failed (exit %d): %s"
218
+ % (
219
+ baseline_ref,
220
+ head_ref,
221
+ result.returncode,
222
+ result.stderr.strip(),
223
+ )
224
+ )
225
+ return result.stdout
226
+
227
+
228
+ def cached_diff(
229
+ baseline_ref: str,
230
+ paths: list[Path],
231
+ repo_root: Path,
232
+ ) -> str:
233
+ """`git diff --cached <baseline_ref> -- <paths>` (staged vs baseline).
234
+
235
+ Exit code semantics (R3-1 fix): accept 0/1, raise on 2+.
236
+ """
237
+ from .errors import BaselineResolutionError
238
+
239
+ cmd = (
240
+ ["git", "diff", "--cached", baseline_ref, "--"]
241
+ + [str(p) for p in paths]
242
+ )
243
+ result = subprocess.run(
244
+ cmd, cwd=repo_root, capture_output=True, text=True, check=False
245
+ )
246
+ if result.returncode not in (0, 1):
247
+ raise BaselineResolutionError(
248
+ "git diff --cached %s failed (exit %d): %s"
249
+ % (baseline_ref, result.returncode, result.stderr.strip())
250
+ )
251
+ return result.stdout
252
+
253
+
254
+ def working_tree_diff(
255
+ baseline_ref: str,
256
+ paths: list[Path],
257
+ repo_root: Path,
258
+ ) -> str:
259
+ """Diff baseline..working_tree, including non-binary untracked files.
260
+
261
+ Tracked diff: `git diff <baseline_ref> -- <paths>`.
262
+ Untracked: enumerate via `git ls-files --others --exclude-standard`,
263
+ synthesize as full-add via `git diff --no-index /dev/null <file>`.
264
+ H2 fix: binary untracked files are SKIPPED with warnings.warn.
265
+
266
+ Exit code handling (R2-1 + R3-1): all git diff calls accept exit 0/1,
267
+ raise BaselineResolutionError on exit 2+. Follows Phase 1 run_git_diff
268
+ convention (Mimo F-03).
269
+ """
270
+ from .errors import BaselineResolutionError
271
+
272
+ # Tracked diff (R3-1: must NOT use check=True)
273
+ tracked_cmd = (
274
+ ["git", "diff", baseline_ref, "--"]
275
+ + [str(p) for p in paths]
276
+ )
277
+ tracked_result = subprocess.run(
278
+ tracked_cmd,
279
+ cwd=repo_root,
280
+ capture_output=True,
281
+ text=True,
282
+ check=False,
283
+ )
284
+ if tracked_result.returncode not in (0, 1):
285
+ raise BaselineResolutionError(
286
+ "git diff %s (tracked, working_tree_diff) failed (exit %d): %s"
287
+ % (
288
+ baseline_ref,
289
+ tracked_result.returncode,
290
+ tracked_result.stderr.strip(),
291
+ )
292
+ )
293
+ tracked = tracked_result.stdout
294
+
295
+ # Untracked files (ls-files has no exit-1-normal semantics)
296
+ ls_cmd = (
297
+ ["git", "ls-files", "--others", "--exclude-standard", "--"]
298
+ + [str(p) for p in paths]
299
+ )
300
+ untracked_paths = [
301
+ line
302
+ for line in subprocess.run(
303
+ ls_cmd,
304
+ cwd=repo_root,
305
+ capture_output=True,
306
+ text=True,
307
+ check=True,
308
+ ).stdout.splitlines()
309
+ if line.strip()
310
+ ]
311
+
312
+ untracked_diffs: list[str] = []
313
+ skipped_binary: list[str] = []
314
+ for rel_path in sorted(untracked_paths):
315
+ full = repo_root / rel_path
316
+ if _is_likely_binary(full):
317
+ skipped_binary.append(rel_path)
318
+ continue
319
+ # R2-1: git diff --no-index exit codes:
320
+ # 0 = files identical (impossible vs /dev/null with content)
321
+ # 1 = files differ (THE expected case)
322
+ # 2+ = real error
323
+ cmd = ["git", "diff", "--no-index", "/dev/null", str(full)]
324
+ result = subprocess.run(
325
+ cmd,
326
+ cwd=repo_root,
327
+ capture_output=True,
328
+ text=True,
329
+ check=False,
330
+ )
331
+ if result.returncode not in (0, 1):
332
+ raise BaselineResolutionError(
333
+ "git diff --no-index failed for untracked file %s "
334
+ "(exit %d): %s"
335
+ % (rel_path, result.returncode, result.stderr.strip())
336
+ )
337
+ untracked_diffs.append(result.stdout)
338
+
339
+ if skipped_binary:
340
+ warnings.warn(
341
+ "forge: skipped %d binary untracked file(s) from "
342
+ "working-tree diff: %s%s"
343
+ % (
344
+ len(skipped_binary),
345
+ skipped_binary[:3],
346
+ "..." if len(skipped_binary) > 3 else "",
347
+ ),
348
+ stacklevel=2,
349
+ )
350
+
351
+ return tracked + "\n".join(untracked_diffs)
code_forge/hold.py ADDED
@@ -0,0 +1,126 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """HOLD UX + ESCALATED-frozen predicate.
4
+
5
+ run_hold_ui prompts human for UNCERTAIN dispositions. check_escalated_frozen
6
+ implements DISPO-05(c) deferred from 02-02.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Callable
12
+
13
+ from .disposition import Disposition, MAX_FIX_ATTEMPTS_PER_FINGERPRINT
14
+ from .state import State, StateFinding, save_state
15
+
16
+
17
+ VALID_INPUTS = {"c": Disposition.CONFIRMED, "d": Disposition.DISMISSED}
18
+ QUIT_INPUTS = {"q"}
19
+
20
+
21
+ class HoldAborted(Exception):
22
+ """Raised when human aborts HOLD UX (Ctrl+D / EOF / "q" input).
23
+
24
+ Message is generic ("HOLD UX aborted by user"), not stdin-specific.
25
+ """
26
+
27
+
28
+ def run_hold_ui(
29
+ state: State,
30
+ state_path: Path,
31
+ input_fn: Callable[[str], str] = input,
32
+ output_fn: Callable[[str], None] = print,
33
+ ) -> None:
34
+ """Prompt human for each UNCERTAIN finding.
35
+
36
+ For each finding with disposition == UNCERTAIN:
37
+ - Print summary (id, file:line, description).
38
+ - Prompt: "[c]onfirm / [d]ismiss / [s]kip / [q]uit: "
39
+ - "c" -> set disposition CONFIRMED
40
+ - "d" -> set disposition DISMISSED
41
+ - "s" -> leave UNCERTAIN, move on
42
+ - "q" -> raise HoldAborted
43
+ - invalid -> reprompt
44
+ - EOF -> raise HoldAborted
45
+
46
+ After loop: clear hold_reason, rebuild dispositions cache, persist.
47
+
48
+ Idempotent: if zero UNCERTAIN findings, returns immediately with
49
+ no I/O (caller may invoke unconditionally after PENDING return).
50
+ """
51
+ uncertain = [
52
+ f for f in state.findings if f.disposition == Disposition.UNCERTAIN
53
+ ]
54
+ if not uncertain:
55
+ state.hold_reason = None
56
+ save_state(state, state_path)
57
+ return
58
+
59
+ output_fn(
60
+ "HOLD: %d UNCERTAIN finding(s) need human disposition."
61
+ % len(uncertain)
62
+ )
63
+ for finding in uncertain:
64
+ _prompt_one(finding, input_fn, output_fn)
65
+
66
+ state.hold_reason = None
67
+ state.dispositions = {f.id: f.disposition for f in state.findings}
68
+ save_state(state, state_path)
69
+
70
+
71
+ def _prompt_one(
72
+ finding: StateFinding,
73
+ input_fn: Callable[[str], str],
74
+ output_fn: Callable[[str], None],
75
+ ) -> None:
76
+ """Inner per-finding prompt loop (reprompts on invalid input)."""
77
+ output_fn(
78
+ " [%s] %s:%d-%d %s"
79
+ % (
80
+ finding.id,
81
+ finding.file,
82
+ finding.line_range[0],
83
+ finding.line_range[1],
84
+ finding.description,
85
+ )
86
+ )
87
+ while True:
88
+ try:
89
+ choice = input_fn(
90
+ " [c]onfirm / [d]ismiss / [s]kip / [q]uit: "
91
+ ).strip().lower()
92
+ except EOFError:
93
+ raise HoldAborted("HOLD UX aborted by user")
94
+ if choice in QUIT_INPUTS:
95
+ raise HoldAborted("HOLD UX aborted by user")
96
+ if choice == "s":
97
+ return
98
+ if choice in VALID_INPUTS:
99
+ finding.disposition = VALID_INPUTS[choice]
100
+ return
101
+ output_fn(" (invalid input %r; expected c/d/s/q)" % choice)
102
+
103
+
104
+ def check_escalated_frozen(state: State) -> bool:
105
+ """DISPO-05(c) predicate: re-CONFIRM of promoted finding -> ESCALATED.
106
+
107
+ Returns True iff ALL of:
108
+ - state.hold_reason is None (not currently in HOLD entry)
109
+ - state.promoted_fingerprints is non-empty
110
+ - at least one finding has: disposition == CONFIRMED AND
111
+ fingerprint in promoted_fingerprints AND
112
+ fix_attempts[fp] >= MAX_FIX_ATTEMPTS_PER_FINGERPRINT
113
+ """
114
+ if state.hold_reason is not None:
115
+ return False
116
+ if not state.promoted_fingerprints:
117
+ return False
118
+ for finding in state.findings:
119
+ if (
120
+ finding.disposition == Disposition.CONFIRMED
121
+ and finding.fingerprint in state.promoted_fingerprints
122
+ and state.fix_attempts.get(finding.fingerprint, 0)
123
+ >= MAX_FIX_ATTEMPTS_PER_FINGERPRINT
124
+ ):
125
+ return True
126
+ return False