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
@@ -0,0 +1,91 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """CLI-03 env override resolution (cli_value > env > default).
4
+
5
+ FORGE_MODE is intentionally NOT here -- 02-04 resolve_mode owns it.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Mapping, Optional
10
+
11
+ from .disposition import MAX_FIX_ATTEMPTS_PER_FINGERPRINT
12
+ from .errors import CliError
13
+
14
+
15
+ DEFAULT_MAX_TOTAL_ROUNDS = 20
16
+ MAX_REASONABLE_FIX_ATTEMPTS = 100 # sanity bound
17
+ MAX_REASONABLE_TOTAL_ROUNDS = 1000 # sanity bound
18
+
19
+
20
+ def resolve_max_total_rounds(
21
+ cli_value: Optional[int], env: Mapping[str, str]
22
+ ) -> int:
23
+ """Resolve max total rounds: cli > env > default (20)."""
24
+ if cli_value is not None:
25
+ return _validate_int(
26
+ cli_value, "--max-total-rounds", MAX_REASONABLE_TOTAL_ROUNDS
27
+ )
28
+ raw = env.get("FORGE_MAX_TOTAL_ROUNDS")
29
+ if raw is None or raw == "":
30
+ return DEFAULT_MAX_TOTAL_ROUNDS
31
+ return _parse_env_int(
32
+ raw, "FORGE_MAX_TOTAL_ROUNDS", MAX_REASONABLE_TOTAL_ROUNDS
33
+ )
34
+
35
+
36
+ def resolve_max_fix_attempts(
37
+ cli_value: Optional[int], env: Mapping[str, str]
38
+ ) -> int:
39
+ """Resolve max fix attempts: cli > env > default (3)."""
40
+ if cli_value is not None:
41
+ return _validate_int(
42
+ cli_value, "--max-fix-attempts", MAX_REASONABLE_FIX_ATTEMPTS
43
+ )
44
+ raw = env.get("FORGE_MAX_FIX_ATTEMPTS_PER_FINGERPRINT")
45
+ if raw is None or raw == "":
46
+ return MAX_FIX_ATTEMPTS_PER_FINGERPRINT
47
+ return _parse_env_int(
48
+ raw, "FORGE_MAX_FIX_ATTEMPTS_PER_FINGERPRINT",
49
+ MAX_REASONABLE_FIX_ATTEMPTS,
50
+ )
51
+
52
+
53
+ def resolve_falsification_engine(
54
+ cli_value: Optional[str], env: Mapping[str, str]
55
+ ) -> str:
56
+ """Resolve falsification engine: cli > env > default (auto)."""
57
+ if cli_value is not None:
58
+ return cli_value
59
+ raw = env.get("FORGE_FALSIFICATION_ENGINE")
60
+ if raw is None or raw == "":
61
+ return "auto"
62
+ key = raw.strip().lower()
63
+ if key not in {"auto", "stub", "real"}:
64
+ raise CliError(
65
+ "invalid FORGE_FALSIFICATION_ENGINE: %r "
66
+ "(expected auto|stub|real)" % raw
67
+ )
68
+ return key
69
+
70
+
71
+ def _parse_env_int(raw: str, name: str, sanity_cap: int) -> int:
72
+ """Parse string env value to int with validation."""
73
+ try:
74
+ value = int(raw.strip())
75
+ except ValueError:
76
+ raise CliError("invalid %s: %r (expected int)" % (name, raw))
77
+ return _validate_int(value, name, sanity_cap)
78
+
79
+
80
+ def _validate_int(value: int, name: str, sanity_cap: int) -> int:
81
+ """Validate int >= 1 and <= sanity_cap."""
82
+ if value < 1:
83
+ raise CliError(
84
+ "invalid %s: %d (must be >= 1)" % (name, value)
85
+ )
86
+ if value > sanity_cap:
87
+ raise CliError(
88
+ "invalid %s: %d (exceeds sanity cap %d)"
89
+ % (name, value, sanity_cap)
90
+ )
91
+ return value
code_forge/errors.py ADDED
@@ -0,0 +1,34 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """Forge-specific error types for state management."""
4
+
5
+
6
+ class SchemaVersionMismatchError(Exception):
7
+ """Raised when state.json schema_version does not match expected."""
8
+
9
+
10
+ class CorruptedStateError(Exception):
11
+ """Raised when state.json is corrupt or internally inconsistent."""
12
+
13
+
14
+ class BaselineResolutionError(Exception):
15
+ """Raised when baseline resolution fails (invalid ref, bad combination)."""
16
+
17
+
18
+ class SnapshotSchemaMismatchError(Exception):
19
+ """Raised when snapshot schema_version does not match expected."""
20
+
21
+
22
+ class CorruptedSnapshotError(Exception):
23
+ """Raised when snapshot file is corrupt or unparseable."""
24
+
25
+
26
+ class CliError(Exception):
27
+ """Raised on invalid CLI args or env values.
28
+
29
+ main() catches and maps to EXIT_CLI_ERROR (exit 2).
30
+ """
31
+
32
+
33
+ class ComponentsConfigError(Exception):
34
+ """Raised when .code-forge/components.yaml fails schema validation."""
@@ -0,0 +1,37 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """CLI-02 exit code constants + Verdict -> exit mapping.
4
+
5
+ Phase 1 cli.py had EXIT_PASS / EXIT_FAIL inline. 02-05 promotes them
6
+ to a dedicated module and adds CLI_ERROR / BUSY / ESCALATED.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from .state import Verdict
11
+
12
+
13
+ EXIT_PASS = 0
14
+ EXIT_FAIL = 1
15
+ EXIT_CLI_ERROR = 2
16
+ EXIT_BUSY = 3
17
+ EXIT_ESCALATED = 4
18
+
19
+
20
+ def verdict_to_exit(verdict: Verdict) -> int:
21
+ """Map terminal Verdict to CLI-02 exit code.
22
+
23
+ Raises ValueError on Verdict.PENDING (caller bug: HOLD should have
24
+ been consumed by HOLD-resume loop before reaching this mapping).
25
+ """
26
+ if verdict == Verdict.PASS:
27
+ return EXIT_PASS
28
+ if verdict == Verdict.FAIL:
29
+ return EXIT_FAIL
30
+ if verdict == Verdict.ESCALATED:
31
+ return EXIT_ESCALATED
32
+ if verdict == Verdict.PENDING:
33
+ raise ValueError(
34
+ "verdict_to_exit called with PENDING; HOLD-resume loop "
35
+ "must consume PENDING before terminal mapping"
36
+ )
37
+ raise ValueError("unknown verdict: %r" % verdict)
@@ -0,0 +1,191 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """STATE-10 falsifier + autofixer + revert_fn factories.
4
+
5
+ Centralizes "which impl do we instantiate" decisions so cli.py stays
6
+ declarative and Phase 4 can swap impls without touching the CLI.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import shutil
11
+ import subprocess
12
+ from pathlib import Path
13
+ from typing import Callable
14
+
15
+ from .autofix import AutoFixer, FixOutcome, StubAutoFixer
16
+ from .baseline import ResolvedReview
17
+ from .disposition import Disposition
18
+ from .falsify import Falsifier, StubFalsifier
19
+ from .e2e_check import run_e2e_check
20
+ from .mutation import run_mutation
21
+ from .state import StateFinding
22
+
23
+
24
+ def build_falsifier(engine: str) -> Falsifier:
25
+ """STATE-10 engine factory.
26
+
27
+ engine = "auto": try Phase 4 import; fall back to stub if absent.
28
+ engine = "stub": always StubFalsifier.
29
+ engine = "real": Phase 4 falsifier (NOT shipped v2.0).
30
+ """
31
+ if engine == "stub":
32
+ return StubFalsifier()
33
+ if engine == "auto":
34
+ try:
35
+ from .falsify_real import RealFalsifier # noqa: F401
36
+ return RealFalsifier()
37
+ except ImportError:
38
+ return StubFalsifier()
39
+ if engine == "real":
40
+ try:
41
+ from .falsify_real import RealFalsifier
42
+ return RealFalsifier()
43
+ except ImportError:
44
+ raise NotImplementedError(
45
+ "--falsification-engine=real requires Phase 4 "
46
+ "(not shipped in v2.0). Use "
47
+ "--falsification-engine=auto or =stub."
48
+ )
49
+ raise ValueError(
50
+ "unknown engine: %r (expected auto|stub|real)" % engine
51
+ )
52
+
53
+
54
+ def build_autofixer(resolved: ResolvedReview) -> AutoFixer:
55
+ """v2.0 returns StubAutoFixer; non-git wraps to prevent PARSE_FAIL.
56
+
57
+ R2-M1: non-git mode wraps StubAutoFixer in _NonGitSafeAutoFixer
58
+ to convert PARSE_FAIL -> NO_CHANGE, preventing revert_fn from
59
+ being invoked (which would raise NotImplementedError per B1).
60
+ """
61
+ base = StubAutoFixer()
62
+ if resolved.mode_hint == "non-git":
63
+ return _NonGitSafeAutoFixer(base)
64
+ return base
65
+
66
+
67
+ class _NonGitSafeAutoFixer(AutoFixer):
68
+ """R2-M1 wrapper: convert PARSE_FAIL -> NO_CHANGE in non-git mode.
69
+
70
+ Prevents StateMachine from calling revert_fn (which raises
71
+ NotImplementedError per B1 in non-git mode). Behavior:
72
+ - PARSE_FAIL -> NO_CHANGE (consumes fix budget; no revert call)
73
+ - SUCCESS / NO_CHANGE / EXCEPTION pass through unchanged
74
+
75
+ R3-7: fix signature byte-equal to AutoFixer ABC.
76
+ """
77
+
78
+ def __init__(self, inner: AutoFixer):
79
+ super().__init__()
80
+ self._inner = inner
81
+
82
+ def fix(self, finding: StateFinding, mode_hint: str) -> FixOutcome:
83
+ """Wrap inner fix; convert PARSE_FAIL to NO_CHANGE."""
84
+ outcome = self._inner.fix(finding, mode_hint)
85
+ if outcome == FixOutcome.PARSE_FAIL:
86
+ return FixOutcome.NO_CHANGE
87
+ return outcome
88
+
89
+
90
+ def build_revert_fn(
91
+ resolved: ResolvedReview, cwd: Path
92
+ ) -> Callable[[StateFinding], None]:
93
+ """Build revert_fn for StateMachine constructor.
94
+
95
+ Dispatches on resolved.mode_hint:
96
+ "git" -> git restore <file>
97
+ "non-git" -> NotImplementedError (B1: v2.0 limitation)
98
+ """
99
+ if resolved.mode_hint == "git":
100
+ return _make_git_restore(cwd)
101
+ if resolved.mode_hint == "non-git":
102
+ return _make_snapshot_restore(cwd, resolved)
103
+ raise ValueError("unknown mode_hint: %r" % resolved.mode_hint)
104
+
105
+
106
+ def _make_git_restore(
107
+ cwd: Path,
108
+ ) -> Callable[[StateFinding], None]:
109
+ """Git mode revert: restore file to index version."""
110
+ def _revert(finding: StateFinding) -> None:
111
+ subprocess.run(
112
+ ["git", "restore", "--", finding.file],
113
+ cwd=str(cwd), check=True,
114
+ )
115
+ return _revert
116
+
117
+
118
+ def _make_snapshot_restore(
119
+ cwd: Path, resolved: ResolvedReview
120
+ ) -> Callable[[StateFinding], None]:
121
+ """B1: non-git revert NOT supported in v2.0.
122
+
123
+ 02-03 Snapshot stores content_hash only, not raw content.
124
+ revert_fn raises NotImplementedError unconditionally.
125
+ """
126
+ def _revert(finding: StateFinding) -> None:
127
+ raise NotImplementedError(
128
+ "non-git autofix revert is not supported in v2.0 "
129
+ "(02-03 Snapshot stores content_hash, not raw content). "
130
+ "Use git mode for autofix, or configure autofixer to "
131
+ "avoid PARSE_FAIL outcomes in non-git mode. "
132
+ "Tracked as v2.x candidate."
133
+ )
134
+ return _revert
135
+
136
+
137
+ def build_l2_runner() -> Callable:
138
+ """Build l2_runner (mutation testing) callable.
139
+
140
+ Returns a callable with signature:
141
+ (diff_files: list[str], baseline_cmd: list[str])
142
+ -> tuple[list[StateFinding], list[str]]
143
+
144
+ If mutmut is not on PATH, returns a no-op callable that produces
145
+ a single MUTATION_SKIPPED finding (soft dependency).
146
+
147
+ The returned callable delegates to run_mutation from the mutation
148
+ module when mutmut is available.
149
+ """
150
+ if shutil.which("mutmut") is None:
151
+ # mutmut not available, return no-op with MUTATION_SKIPPED
152
+ def _no_mutation(
153
+ diff_files: list[str],
154
+ baseline_cmd: list[str],
155
+ ) -> tuple[list[StateFinding], list[str]]:
156
+ findings = [
157
+ StateFinding(
158
+ id="MUTATION_SKIPPED",
159
+ fingerprint="mutation-unavailable",
160
+ source="MUTANT",
161
+ disposition=Disposition.DISMISSED,
162
+ file="",
163
+ line_range=[],
164
+ description="mutmut not installed (soft dependency)",
165
+ )
166
+ ]
167
+ infra_errors = ["mutmut not found on PATH"]
168
+ return (findings, infra_errors)
169
+ return _no_mutation
170
+
171
+ # mutmut is available, delegate to run_mutation
172
+ return run_mutation
173
+
174
+
175
+ def build_e2e_checker() -> Callable:
176
+ """Build e2e_checker callable for R3 coverage heuristic.
177
+
178
+ Returns a callable with signature:
179
+ (diff_text: str, repo_root: Path)
180
+ -> tuple[list[StateFinding], list[str]]
181
+
182
+ Unlike build_l2_runner, there is no external-binary availability check:
183
+ e2e_check has no soft dependency (unidiff is a hard dep already used by
184
+ diff.py). The factory returns run_e2e_check directly.
185
+
186
+ The factory exists for symmetry with build_l2_runner so plan 03-03 can
187
+ inject it into the state machine the same way as l2_runner, and a future
188
+ variant (e.g. with config loading) can be swapped without touching
189
+ machine.py.
190
+ """
191
+ return run_e2e_check
code_forge/falsify.py ADDED
@@ -0,0 +1,85 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """Abstract Falsifier interface + Stub implementation.
4
+
5
+ Phase 4 will provide the real Falsifier. 02-01 stub allows state machine
6
+ to be built and tested without Phase 4.
7
+
8
+ This module imports StateFinding from code_forge.state (NOT the Phase 1
9
+ forge.parsers.base.Finding). StateFinding is the persisted-in-state.json
10
+ representation; parsers.base.Finding is the parser-emitted record.
11
+ """
12
+
13
+ import json
14
+ from abc import ABC, abstractmethod
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ from .disposition import Disposition
19
+ from .state import StateFinding
20
+
21
+
22
+ class Falsifier(ABC):
23
+ """Abstract base for falsification engines.
24
+
25
+ falsify() must NOT return Disposition.FIXED (FIXED is a state machine
26
+ transition after auto-fix, not a falsifier output).
27
+ """
28
+
29
+ @abstractmethod
30
+ def falsify(self, finding: StateFinding) -> Disposition:
31
+ """Classify a finding as CONFIRMED, DISMISSED, or UNCERTAIN.
32
+
33
+ Returning FIXED raises ValueError (state machine concern).
34
+ """
35
+ ...
36
+
37
+
38
+ class StubFalsifier(Falsifier):
39
+ """Returns configurable Dispositions for tests.
40
+
41
+ Config format (JSON):
42
+ {
43
+ "default": "CONFIRMED",
44
+ "dispositions": {"fp-xxx": "DISMISSED", ...},
45
+ "errors": {"fp-yyy": "timeout", ...}
46
+ }
47
+
48
+ Precedence: errors checked before dispositions/default.
49
+ FIXED in any disposition position is rejected at constructor AND
50
+ at falsify() time (defense in depth).
51
+ """
52
+
53
+ def __init__(self, fixture_path: Optional[Path] = None):
54
+ self._dispositions: dict[str, Disposition] = {}
55
+ self._errors: dict[str, str] = {}
56
+ self._default = Disposition.CONFIRMED
57
+ if fixture_path:
58
+ data = json.loads(fixture_path.read_text())
59
+ self._default = Disposition(data.get("default", "CONFIRMED"))
60
+ if self._default == Disposition.FIXED:
61
+ raise ValueError(
62
+ "FIXED is not a valid falsifier output (default)"
63
+ )
64
+ self._dispositions = {}
65
+ for fp, d in data.get("dispositions", {}).items():
66
+ disp = Disposition(d)
67
+ if disp == Disposition.FIXED:
68
+ raise ValueError(
69
+ "FIXED is not a valid falsifier output "
70
+ "(fingerprint %s)" % fp
71
+ )
72
+ self._dispositions[fp] = disp
73
+ self._errors = dict(data.get("errors", {}))
74
+
75
+ def falsify(self, finding: StateFinding) -> Disposition:
76
+ """Return configured disposition or raise on error key."""
77
+ if finding.fingerprint in self._errors:
78
+ raise RuntimeError(
79
+ "stub-simulated falsification error: %s"
80
+ % self._errors[finding.fingerprint]
81
+ )
82
+ disp = self._dispositions.get(finding.fingerprint, self._default)
83
+ if disp == Disposition.FIXED:
84
+ raise ValueError("FIXED is not a valid falsifier output")
85
+ return disp