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/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """Forge -- 3-state quality gate for code review."""
4
+
5
+ __version__ = "2.0.0a1"
6
+
7
+ # Exit code constants re-exported from exit_codes module (H5 + R3-L3).
8
+ from .exit_codes import (
9
+ EXIT_BUSY,
10
+ EXIT_CLI_ERROR,
11
+ EXIT_ESCALATED,
12
+ EXIT_FAIL,
13
+ EXIT_PASS,
14
+ )
code_forge/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """Enable ``python3 -m forge`` invocation (R5-M1)."""
4
+
5
+ from code_forge.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
code_forge/autofix.py ADDED
@@ -0,0 +1,78 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """AutoFixer abstract interface + Stub implementation.
4
+
5
+ Parallel to forge.falsify -- Phase 4 will plug in real AI auto-fix; 02-02
6
+ ships stub for DISPO-04/05/06 behavior tests.
7
+
8
+ Design per R1 B2: AutoFixer is pure (returns FixOutcome only, no I/O,
9
+ no revert). StateMachine.revert_fn handles the revert side-effect.
10
+ """
11
+ from abc import ABC, abstractmethod
12
+ from enum import Enum
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ import json
17
+
18
+ from .state import StateFinding
19
+
20
+
21
+ class FixOutcome(str, Enum):
22
+ """Auto-fix result.
23
+
24
+ State machine consumes to drive DISPO-04/05/06 transitions:
25
+ SUCCESS - fix applied, no parse errors; finding -> FIXED
26
+ PARSE_FAIL - fix produced syntax error; revert + fix_attempts++
27
+ NO_CHANGE - autofixer refused; fix_attempts++ (no revert needed)
28
+ EXCEPTION - autofixer raised; infra_errors + fix_attempts++
29
+ """
30
+ SUCCESS = "SUCCESS"
31
+ PARSE_FAIL = "PARSE_FAIL"
32
+ NO_CHANGE = "NO_CHANGE"
33
+ EXCEPTION = "EXCEPTION"
34
+
35
+
36
+ class AutoFixer(ABC):
37
+ """Abstract base for auto-fix engines."""
38
+
39
+ @abstractmethod
40
+ def fix(self, finding: StateFinding, mode_hint: str) -> FixOutcome:
41
+ """Attempt to fix a CONFIRMED finding.
42
+
43
+ mode_hint: "git" | "non-git" -- passed through; consumed by the
44
+ state machine's revert_fn (NOT by AutoFixer) per R1 B2 design:
45
+ - AutoFixer is pure (returns FixOutcome, no I/O, no revert).
46
+ - StateMachine.revert_fn handles the revert side-effect:
47
+ * mode_hint=="git" -> git restore
48
+ * mode_hint=="non-git" -> snapshot restore via 02-03 Snapshot
49
+
50
+ MUST NOT mutate finding. Outcome only; caller drives transitions.
51
+ """
52
+ ...
53
+
54
+
55
+ class StubAutoFixer(AutoFixer):
56
+ """Returns configured FixOutcomes for tests.
57
+
58
+ Config format (JSON):
59
+ {
60
+ "default": "SUCCESS",
61
+ "outcomes": {"fp-xxx": "PARSE_FAIL", "fp-yyy": "SUCCESS", ...}
62
+ }
63
+ """
64
+
65
+ def __init__(self, fixture_path: Optional[Path] = None):
66
+ self._outcomes: dict[str, FixOutcome] = {}
67
+ self._default = FixOutcome.SUCCESS
68
+ if fixture_path:
69
+ data = json.loads(fixture_path.read_text())
70
+ self._default = FixOutcome(data.get("default", "SUCCESS"))
71
+ self._outcomes = {
72
+ fp: FixOutcome(o)
73
+ for fp, o in data.get("outcomes", {}).items()
74
+ }
75
+
76
+ def fix(self, finding: StateFinding, mode_hint: str) -> FixOutcome:
77
+ """Return configured outcome for this finding's fingerprint."""
78
+ return self._outcomes.get(finding.fingerprint, self._default)
code_forge/baseline.py ADDED
@@ -0,0 +1,216 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
3
+ """BaselineSpec discriminated union + resolver.
4
+
5
+ Owned by 02-03. 02-05 CLI parser emits BaselineSpec from string args;
6
+ this module dispatches to the right resolution path.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import asdict, dataclass
11
+ from pathlib import Path
12
+ from typing import Optional, Union
13
+
14
+ from .errors import BaselineResolutionError
15
+ from .git import (
16
+ INDEX,
17
+ WORKING,
18
+ cached_diff,
19
+ git_diff,
20
+ is_git_repo,
21
+ is_pseudo_ref,
22
+ resolve_git_ref,
23
+ working_tree_diff,
24
+ )
25
+ from .snapshot import load_snapshot
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class GitRefBaseline:
30
+ """Baseline = git ref (HEAD, branch, commit sha, WORKING, INDEX)."""
31
+
32
+ ref: str
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class SnapshotBaseline:
37
+ """Baseline = stored .code-forge/snapshots/<source-hash>.json."""
38
+
39
+ path: Path
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class EmptyBaseline:
44
+ """Baseline = nothing. All current source content is new."""
45
+
46
+ pass
47
+
48
+
49
+ BaselineSpec = Union[GitRefBaseline, SnapshotBaseline, EmptyBaseline]
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class ResolvedReview:
54
+ """Concrete review subject + baseline content after resolution.
55
+
56
+ Consumed by state machine (02-02) and source_hash (STATE-07).
57
+ """
58
+
59
+ source_files: list[Path]
60
+ baseline_content: Optional[dict]
61
+ git_diff: Optional[str]
62
+ mode_hint: str # "git" | "non-git"
63
+
64
+
65
+ def resolve_baseline(
66
+ baseline_spec: BaselineSpec,
67
+ head_spec: Optional["GitRefBaseline"],
68
+ paths: list[Path],
69
+ cwd: Path,
70
+ ) -> ResolvedReview:
71
+ """Resolve (baseline, head, paths) to concrete review subject.
72
+
73
+ Raises:
74
+ BaselineResolutionError: invalid combination.
75
+ """
76
+ if isinstance(baseline_spec, GitRefBaseline):
77
+ return _resolve_git(baseline_spec, head_spec, paths, cwd)
78
+ if isinstance(baseline_spec, SnapshotBaseline):
79
+ if head_spec is not None:
80
+ raise BaselineResolutionError(
81
+ "SnapshotBaseline does not accept head_spec "
82
+ "(got: %r); snapshot is its own implicit head"
83
+ % (head_spec,)
84
+ )
85
+ return _resolve_snapshot(baseline_spec, paths, cwd)
86
+ if isinstance(baseline_spec, EmptyBaseline):
87
+ if head_spec is not None and not is_git_repo(cwd):
88
+ raise BaselineResolutionError(
89
+ "EmptyBaseline + head_spec is only valid in a git repo; "
90
+ "cwd=%s" % cwd
91
+ )
92
+ return _resolve_empty(head_spec, paths, cwd)
93
+ raise BaselineResolutionError(
94
+ "unknown baseline spec type: %s" % type(baseline_spec)
95
+ )
96
+
97
+
98
+ def _resolve_git(
99
+ baseline_spec: GitRefBaseline,
100
+ head_spec: Optional[GitRefBaseline],
101
+ paths: list[Path],
102
+ cwd: Path,
103
+ ) -> ResolvedReview:
104
+ """B3 resolution -- git mode.
105
+
106
+ 1. Reject if cwd is not in a git repo.
107
+ 2. Reject pseudo-ref as baseline (WORKING/INDEX are head-only).
108
+ 3. Validate baseline ref via resolve_git_ref.
109
+ 4. Default head = GitRefBaseline(WORKING) if head_spec is None.
110
+ 5. Dispatch by head ref kind.
111
+ 6. Return ResolvedReview with diff, mode_hint="git".
112
+ """
113
+ if not is_git_repo(cwd):
114
+ raise BaselineResolutionError(
115
+ "GitRefBaseline used outside git repo (cwd=%s)" % cwd
116
+ )
117
+ if is_pseudo_ref(baseline_spec.ref):
118
+ raise BaselineResolutionError(
119
+ "baseline cannot be a pseudo-ref (%s); "
120
+ "pseudo-refs are head-only" % baseline_spec.ref
121
+ )
122
+ resolve_git_ref(baseline_spec.ref, cwd) # raises if ref unknown
123
+
124
+ head = head_spec if head_spec is not None else GitRefBaseline(WORKING)
125
+ if head.ref == WORKING:
126
+ diff = working_tree_diff(baseline_spec.ref, paths, cwd)
127
+ elif head.ref == INDEX:
128
+ diff = cached_diff(baseline_spec.ref, paths, cwd)
129
+ else:
130
+ resolve_git_ref(head.ref, cwd)
131
+ diff = git_diff(baseline_spec.ref, head.ref, paths, cwd)
132
+
133
+ return ResolvedReview(
134
+ source_files=paths,
135
+ baseline_content=None,
136
+ git_diff=diff,
137
+ mode_hint="git",
138
+ )
139
+
140
+
141
+ def _resolve_snapshot(
142
+ baseline_spec: SnapshotBaseline,
143
+ paths: list[Path],
144
+ cwd: Path,
145
+ ) -> ResolvedReview:
146
+ """B3 resolution -- stored snapshot.
147
+
148
+ 1. load_snapshot(path); returns None on missing file (BASELINE-03).
149
+ 2. If None: fall back to EmptyBaseline resolution silently.
150
+ 3. Else: pack snapshot dict into baseline_content.
151
+ """
152
+ snap = load_snapshot(baseline_spec.path)
153
+ if snap is None:
154
+ return _resolve_empty(None, paths, cwd)
155
+ return ResolvedReview(
156
+ source_files=paths,
157
+ baseline_content={"snapshot": asdict(snap)},
158
+ git_diff=None,
159
+ mode_hint="non-git",
160
+ )
161
+
162
+
163
+ def _resolve_empty(
164
+ head_spec: Optional[GitRefBaseline],
165
+ paths: list[Path],
166
+ cwd: Path,
167
+ ) -> ResolvedReview:
168
+ """B3 resolution -- empty baseline (all source is new).
169
+
170
+ 1. mode_hint = "git" if cwd is in a git repo else "non-git".
171
+ 2. If git repo AND head_spec provided: diff from empty tree to head
172
+ (git empty tree sha 4b825dc642cb6eb9a060e54bf8d69288fbee4904).
173
+ 3. Otherwise: no diff.
174
+ """
175
+ if is_git_repo(cwd) and head_spec is not None:
176
+ # Git's universal empty-tree sha (constant, not computed)
177
+ empty_tree_sha = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
178
+ if head_spec.ref == WORKING:
179
+ diff = working_tree_diff(empty_tree_sha, paths, cwd)
180
+ elif head_spec.ref == INDEX:
181
+ diff = cached_diff(empty_tree_sha, paths, cwd)
182
+ else:
183
+ resolve_git_ref(head_spec.ref, cwd)
184
+ diff = git_diff(empty_tree_sha, head_spec.ref, paths, cwd)
185
+ return ResolvedReview(
186
+ source_files=paths,
187
+ baseline_content=None,
188
+ git_diff=diff,
189
+ mode_hint="git",
190
+ )
191
+ mode = "git" if is_git_repo(cwd) else "non-git"
192
+ return ResolvedReview(
193
+ source_files=paths,
194
+ baseline_content=None,
195
+ git_diff=None,
196
+ mode_hint=mode,
197
+ )
198
+
199
+
200
+ def serialize_baseline_spec(spec: BaselineSpec) -> str:
201
+ """OQ1 fix: produce single-line repr for state.json.
202
+
203
+ Formats:
204
+ GitRefBaseline(ref="HEAD") -> "git:HEAD"
205
+ SnapshotBaseline(path=...) -> "snapshot:<posix-path>"
206
+ EmptyBaseline() -> "empty"
207
+ """
208
+ if isinstance(spec, GitRefBaseline):
209
+ return "git:%s" % spec.ref
210
+ if isinstance(spec, SnapshotBaseline):
211
+ return "snapshot:%s" % spec.path.as_posix()
212
+ if isinstance(spec, EmptyBaseline):
213
+ return "empty"
214
+ raise BaselineResolutionError(
215
+ "cannot serialize unknown spec type: %s" % type(spec)
216
+ )