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.
- code_forge/__init__.py +14 -0
- code_forge/__main__.py +8 -0
- code_forge/autofix.py +78 -0
- code_forge/baseline.py +216 -0
- code_forge/cli.py +983 -0
- code_forge/delta.py +65 -0
- code_forge/diagnose.py +109 -0
- code_forge/diff.py +82 -0
- code_forge/disposition.py +32 -0
- code_forge/e2e_check.py +641 -0
- code_forge/env_resolver.py +91 -0
- code_forge/errors.py +34 -0
- code_forge/exit_codes.py +37 -0
- code_forge/factories.py +191 -0
- code_forge/falsify.py +85 -0
- code_forge/gate_check.py +466 -0
- code_forge/git.py +351 -0
- code_forge/hold.py +126 -0
- code_forge/install_hooks.py +331 -0
- code_forge/lock.py +162 -0
- code_forge/machine.py +792 -0
- code_forge/mode_resolver.py +60 -0
- code_forge/mutation.py +380 -0
- code_forge/parsers/__init__.py +56 -0
- code_forge/parsers/_sarif.py +77 -0
- code_forge/parsers/base.py +65 -0
- code_forge/parsers/checkpatch.py +66 -0
- code_forge/parsers/clippy.py +85 -0
- code_forge/parsers/non_ascii.py +47 -0
- code_forge/parsers/ruff.py +18 -0
- code_forge/parsers/semgrep.py +18 -0
- code_forge/parsers/shellcheck.py +56 -0
- code_forge/registry.py +153 -0
- code_forge/reporter.py +133 -0
- code_forge/runner.py +205 -0
- code_forge/sarif.py +226 -0
- code_forge/skills/adversarial-qe/SKILL.md +272 -0
- code_forge/skills/code-forge/SKILL.md +1193 -0
- code_forge/skills/code-review-expert/SKILL.md +162 -0
- code_forge/skills/code-review-expert/references/code-quality-checklist.md +130 -0
- code_forge/skills/code-review-expert/references/removal-plan.md +52 -0
- code_forge/skills/code-review-expert/references/security-checklist.md +118 -0
- code_forge/skills/code-review-expert/references/solid-checklist.md +65 -0
- code_forge/skills/kernel-fp-verify/SKILL.md +101 -0
- code_forge/skills/qodo-review/SKILL.md +135 -0
- code_forge/skills/smoke-test/SKILL.md +253 -0
- code_forge/skills/smoke-test/references/boundary-cases.md +114 -0
- code_forge/skills/smoke-test/references/concurrency-patterns.md +306 -0
- code_forge/skills/smoke-test/references/injection-payloads.md +124 -0
- code_forge/skills/smoke-test/test-library/shell/README.md +271 -0
- code_forge/skills/smoke-test/test-library/shell/primitives.sh +352 -0
- code_forge/skills/smoke-test/test-library/shell/primitives_test.sh +324 -0
- code_forge/snapshot.py +196 -0
- code_forge/source.py +64 -0
- code_forge/state.py +246 -0
- code_forge/verdict.py +43 -0
- code_review_forge-2.0.0a1.dist-info/METADATA +237 -0
- code_review_forge-2.0.0a1.dist-info/RECORD +62 -0
- code_review_forge-2.0.0a1.dist-info/WHEEL +5 -0
- code_review_forge-2.0.0a1.dist-info/entry_points.txt +2 -0
- code_review_forge-2.0.0a1.dist-info/licenses/LICENSE +179 -0
- code_review_forge-2.0.0a1.dist-info/top_level.txt +1 -0
code_forge/delta.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
|
|
3
|
+
"""Delta computation -- filter findings to changed lines only.
|
|
4
|
+
|
|
5
|
+
This is the core of baseline mode (LAYER0-02). The design doc says
|
|
6
|
+
"Layer 0 flags only NEW violations introduced by the diff." The
|
|
7
|
+
delta filter determines "NEW" by checking if the finding's line
|
|
8
|
+
range intersects lines that were added or modified in the diff.
|
|
9
|
+
|
|
10
|
+
This module is a pure function with no I/O -- same inputs always
|
|
11
|
+
produce same outputs, satisfying GATE-02 determinism.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from code_forge.parsers.base import Finding, ToolError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def filter_delta(
|
|
18
|
+
findings: list[Finding | ToolError],
|
|
19
|
+
changed_lines: dict[str, set[int]],
|
|
20
|
+
) -> tuple[list[Finding | ToolError], list[Finding | ToolError]]:
|
|
21
|
+
"""Filter findings to only those on changed lines.
|
|
22
|
+
|
|
23
|
+
Returns a 2-tuple:
|
|
24
|
+
delta_findings: findings whose line range intersects changed
|
|
25
|
+
lines, plus all ToolError items (not filtered by line).
|
|
26
|
+
all_findings: a copy of the input list (preserved for the
|
|
27
|
+
reporter to show "N pre-existing violation(s) in
|
|
28
|
+
unchanged code").
|
|
29
|
+
|
|
30
|
+
ToolError items are always included in delta_findings because
|
|
31
|
+
tool errors represent tool-level failures that must be reported
|
|
32
|
+
regardless of which lines changed.
|
|
33
|
+
|
|
34
|
+
For each Finding, a multi-line finding (line != end_line) is
|
|
35
|
+
kept if ANY line in range(finding.line, finding.end_line + 1)
|
|
36
|
+
intersects the changed lines set (RESEARCH.md Pitfall 2).
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
findings: list of Finding and/or ToolError items
|
|
40
|
+
changed_lines: {file_path: set_of_changed_line_numbers}
|
|
41
|
+
from extract_changed_lines()
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
(delta_findings, all_findings)
|
|
45
|
+
"""
|
|
46
|
+
all_findings = list(findings)
|
|
47
|
+
delta_findings: list[Finding | ToolError] = []
|
|
48
|
+
|
|
49
|
+
for item in findings:
|
|
50
|
+
# ToolError items always pass through
|
|
51
|
+
if isinstance(item, ToolError):
|
|
52
|
+
delta_findings.append(item)
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
# Finding: check if file is in changed_lines
|
|
56
|
+
file_lines = changed_lines.get(item.file)
|
|
57
|
+
if file_lines is None:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Check if any line in the finding's range intersects
|
|
61
|
+
finding_range = range(item.line, item.end_line + 1)
|
|
62
|
+
if any(ln in file_lines for ln in finding_range):
|
|
63
|
+
delta_findings.append(item)
|
|
64
|
+
|
|
65
|
+
return (delta_findings, all_findings)
|
code_forge/diagnose.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
|
|
3
|
+
"""STATE-05 A/B/C/D non-convergence classifier.
|
|
4
|
+
|
|
5
|
+
Pure function over round_history + infra_errors. Called when state machine
|
|
6
|
+
exhausts MAX_TOTAL_ROUNDS without reaching fixpoint or HOLD.
|
|
7
|
+
|
|
8
|
+
Categories:
|
|
9
|
+
A = FIXED -> CONFIRMED oscillation
|
|
10
|
+
(same fingerprint toggles disposition across consecutive rounds)
|
|
11
|
+
B = net CONFIRMED count not decreasing across rounds
|
|
12
|
+
(R1 H4 fix: expanded to cover both genuinely new fingerprints each
|
|
13
|
+
round AND stuck CONFIRMED count that auto-fix never reduces.
|
|
14
|
+
Threshold: net CONFIRMED non-decreasing over last 3 rounds.)
|
|
15
|
+
C = UNCERTAIN accumulation
|
|
16
|
+
(falsifier indecisive; UNCERTAIN count grows monotonically >= 3 rounds)
|
|
17
|
+
D = infrastructure failure
|
|
18
|
+
(ANY infra_errors entry: binary trigger per R3 MED2)
|
|
19
|
+
|
|
20
|
+
Tie-breaker priority: D > A > B > C
|
|
21
|
+
"""
|
|
22
|
+
from typing import Literal
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def diagnose_non_convergence(
|
|
26
|
+
round_history: list[dict],
|
|
27
|
+
infra_errors: list[str],
|
|
28
|
+
) -> Literal["A", "B", "C", "D"]:
|
|
29
|
+
"""Classify why state machine failed to converge.
|
|
30
|
+
|
|
31
|
+
round_history: list of per-round dicts with keys:
|
|
32
|
+
- round: int
|
|
33
|
+
- l0_fingerprints: list[str]
|
|
34
|
+
- l1_fingerprints: list[str]
|
|
35
|
+
- dispositions: dict[str, str] (fingerprint -> Disposition value)
|
|
36
|
+
- fixed_fingerprints: list[str] (this round)
|
|
37
|
+
|
|
38
|
+
infra_errors: list of error message strings.
|
|
39
|
+
|
|
40
|
+
Returns: "A" | "B" | "C" | "D"
|
|
41
|
+
"""
|
|
42
|
+
# R3 MED2: ANY infra_errors entry -> D (binary trigger).
|
|
43
|
+
if infra_errors:
|
|
44
|
+
return "D"
|
|
45
|
+
if _has_fixed_to_confirmed_toggle(round_history):
|
|
46
|
+
return "A"
|
|
47
|
+
if _has_monotonic_new_confirmed(round_history):
|
|
48
|
+
return "B"
|
|
49
|
+
if _has_uncertain_growth(round_history):
|
|
50
|
+
return "C"
|
|
51
|
+
# Default fallback: oscillation-most-likely if no other signal.
|
|
52
|
+
return "A"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _has_fixed_to_confirmed_toggle(history: list[dict]) -> bool:
|
|
56
|
+
"""Category A: same fingerprint FIXED in round N, CONFIRMED in N+1.
|
|
57
|
+
|
|
58
|
+
Detects fix-loop instability where auto-fix appears to succeed but
|
|
59
|
+
finding re-detects in next round.
|
|
60
|
+
"""
|
|
61
|
+
for i in range(len(history) - 1):
|
|
62
|
+
current_disps = history[i].get("dispositions", {})
|
|
63
|
+
next_disps = history[i + 1].get("dispositions", {})
|
|
64
|
+
for fp, disp in current_disps.items():
|
|
65
|
+
if disp == "FIXED" and next_disps.get(fp) == "CONFIRMED":
|
|
66
|
+
return True
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _has_monotonic_new_confirmed(history: list[dict]) -> bool:
|
|
71
|
+
"""Category B (R1 H4 expanded): net CONFIRMED count not decreasing.
|
|
72
|
+
|
|
73
|
+
Returns True iff over the last 3 rounds, the count of CONFIRMED
|
|
74
|
+
fingerprints did NOT strictly decrease across any consecutive pair.
|
|
75
|
+
Requires >= 3 rounds of history; returns False otherwise.
|
|
76
|
+
"""
|
|
77
|
+
if len(history) < 3:
|
|
78
|
+
return False
|
|
79
|
+
window = history[-3:]
|
|
80
|
+
counts = []
|
|
81
|
+
for entry in window:
|
|
82
|
+
disps = entry.get("dispositions", {})
|
|
83
|
+
n = sum(1 for v in disps.values() if v == "CONFIRMED")
|
|
84
|
+
counts.append(n)
|
|
85
|
+
# Must have at least one CONFIRMED in the window to qualify as B
|
|
86
|
+
if max(counts) == 0:
|
|
87
|
+
return False
|
|
88
|
+
# Non-decreasing: every pair c[i] <= c[i+1]
|
|
89
|
+
for i in range(len(counts) - 1):
|
|
90
|
+
if counts[i] > counts[i + 1]:
|
|
91
|
+
return False
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _has_uncertain_growth(history: list[dict]) -> bool:
|
|
96
|
+
"""Category C: UNCERTAIN count grows monotonically over >= 3 rounds."""
|
|
97
|
+
if len(history) < 3:
|
|
98
|
+
return False
|
|
99
|
+
window = history[-3:]
|
|
100
|
+
counts = []
|
|
101
|
+
for entry in window:
|
|
102
|
+
disps = entry.get("dispositions", {})
|
|
103
|
+
n = sum(1 for v in disps.values() if v == "UNCERTAIN")
|
|
104
|
+
counts.append(n)
|
|
105
|
+
# Strictly increasing: every pair c[i] < c[i+1]
|
|
106
|
+
for i in range(len(counts) - 1):
|
|
107
|
+
if counts[i] >= counts[i + 1]:
|
|
108
|
+
return False
|
|
109
|
+
return True
|
code_forge/diff.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
|
|
3
|
+
"""Git diff parser with changed-line extraction.
|
|
4
|
+
|
|
5
|
+
Uses unidiff library (RESEARCH.md Pattern 3) to parse unified diff
|
|
6
|
+
text and extract changed line numbers per file.
|
|
7
|
+
|
|
8
|
+
Line-number drift between tool output and diff is N/A: both reference
|
|
9
|
+
the same working tree file state. Tools run on the actual files
|
|
10
|
+
(post-patch), and git diff reports target-side line numbers for those
|
|
11
|
+
same files. Addresses LAYER0-03 and review Consensus #2.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
import unidiff
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def extract_changed_lines(diff_text: str) -> dict[str, set[int]]:
|
|
22
|
+
"""Parse unified diff, return {file: set_of_changed_line_numbers}.
|
|
23
|
+
|
|
24
|
+
Only added/modified lines. Deleted files excluded.
|
|
25
|
+
|
|
26
|
+
Line-number drift is N/A because both tools and diff reference
|
|
27
|
+
the working tree file state. Tools run on the actual files
|
|
28
|
+
(post-patch), and git diff -U0 HEAD reports target-side line
|
|
29
|
+
numbers for those same files. Addresses LAYER0-03 and review
|
|
30
|
+
Consensus #2.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
diff_text: raw unified diff text (from git diff output)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Dict mapping file paths to sets of added line numbers.
|
|
37
|
+
Empty dict for empty or unparseable diff.
|
|
38
|
+
"""
|
|
39
|
+
if not diff_text or not diff_text.strip():
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
patchset = unidiff.PatchSet(diff_text)
|
|
44
|
+
except unidiff.errors.UnidiffParseError:
|
|
45
|
+
logger.warning("Failed to parse diff text, returning empty dict")
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
result = {}
|
|
49
|
+
for patched_file in patchset:
|
|
50
|
+
# Skip deleted files
|
|
51
|
+
if patched_file.is_removed_file:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
# Use target path (handles renames correctly)
|
|
55
|
+
filepath = patched_file.path
|
|
56
|
+
|
|
57
|
+
changed_lines = set()
|
|
58
|
+
for hunk in patched_file:
|
|
59
|
+
for line in hunk:
|
|
60
|
+
if line.is_added and line.target_line_no is not None:
|
|
61
|
+
changed_lines.add(line.target_line_no)
|
|
62
|
+
|
|
63
|
+
if changed_lines:
|
|
64
|
+
result[filepath] = changed_lines
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_changed_files(diff_text: str) -> list[str]:
|
|
70
|
+
"""Return sorted list of files with additions/modifications.
|
|
71
|
+
|
|
72
|
+
Uses extract_changed_lines internally. Only files with at least
|
|
73
|
+
one added line are included.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
diff_text: raw unified diff text
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Sorted list of file paths with additions.
|
|
80
|
+
"""
|
|
81
|
+
changed = extract_changed_lines(diff_text)
|
|
82
|
+
return sorted(changed.keys())
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026, Minxi Hou <houminxi@gmail.com>
|
|
3
|
+
"""Disposition protocol for forge state machine.
|
|
4
|
+
|
|
5
|
+
Owned by Phase 2 sub-plan 02-01. All other sub-plans + Phase 4 must conform.
|
|
6
|
+
DO NOT add fields without bumping DISPOSITION_PROTOCOL_VERSION.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
DISPOSITION_PROTOCOL_VERSION: Final[int] = 1
|
|
13
|
+
MAX_FIX_ATTEMPTS_PER_FINGERPRINT: Final[int] = 3
|
|
14
|
+
FEEDBACK_SCHEMA_VERSION: Final[int] = 1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Disposition(str, Enum):
|
|
18
|
+
"""Finding disposition states.
|
|
19
|
+
|
|
20
|
+
State transitions (enforced by state machine, not by enum):
|
|
21
|
+
- (new) -> CONFIRMED | DISMISSED | UNCERTAIN (set by falsify())
|
|
22
|
+
- CONFIRMED -> FIXED (after successful auto-fix)
|
|
23
|
+
- CONFIRMED -> UNCERTAIN (DISPO-05 promotion)
|
|
24
|
+
- FIXED -> (remove from active) (next-round gone)
|
|
25
|
+
- FIXED -> CONFIRMED (next-round persists)
|
|
26
|
+
- UNCERTAIN -> CONFIRMED (human re-CONFIRM in HOLD)
|
|
27
|
+
- UNCERTAIN -> DISMISSED (human dismiss in HOLD)
|
|
28
|
+
"""
|
|
29
|
+
CONFIRMED = "CONFIRMED"
|
|
30
|
+
DISMISSED = "DISMISSED"
|
|
31
|
+
UNCERTAIN = "UNCERTAIN"
|
|
32
|
+
FIXED = "FIXED"
|