This is an automated email from the ASF dual-hosted git repository.

terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git


The following commit(s) were added to refs/heads/master by this push:
     new 319a369d4a5 Simplify Python prechecks and policy summary reporting 
(#40228)
319a369d4a5 is described below

commit 319a369d4a5488e30ba74bc04fccaceea4aedf12
Author: Liang Zhang <[email protected]>
AuthorDate: Sun Oct 18 23:38:36 2026 +0800

    Simplify Python prechecks and policy summary reporting (#40228)
---
 .codex/harness/agents/run.py                       |  69 +++++------
 .codex/harness/agents/test_run.py                  |  73 ++++++++++++
 .codex/skills/gen-ut/scripts/scan_quality_rules.py |  57 +++++----
 .../skills/gen-ut/tests/test_scan_quality_rules.py | 131 +++++++++++++++++++++
 .../review-pr/tests/test_build_review_inventory.py |  20 ----
 .../skills/review-pr/tests/test_review_common.py   |  21 ++++
 .../skills/review-pr/tests/test_review_ledger.py   |   4 -
 7 files changed, 289 insertions(+), 86 deletions(-)

diff --git a/.codex/harness/agents/run.py b/.codex/harness/agents/run.py
index 27cb0fe99f7..c55bc25fcef 100644
--- a/.codex/harness/agents/run.py
+++ b/.codex/harness/agents/run.py
@@ -2515,31 +2515,39 @@ def main() -> int:
     trace_results = []
     if args.mode in {"trace", "all"}:
         trace_results = run_local_read_traces(bundle, selected_profiles, 
args.trace_cwd or ["root", "nested"])
+    policy_summary = {
+        "policy_manifest": bundle["manifest_file"],
+        "policy_manifest_sha256": bundle["manifest_sha256"],
+        "policy_bundle_sha256": bundle["bundle_sha256"],
+        "policy_inventory_sha256": bundle["inventory_sha256"],
+        "policy_sources": bundle["inventory"],
+        "policy_source_changes": inventory_changes,
+        "authorized_policy_source_changes": 
sorted(args.authorized_policy_source_change),
+        "policy_source_baseline_available": baseline is not None and 
"policy_sources" in baseline,
+        "policy_binding_sha256": digest_json(policy_bindings),
+        "policy_bindings": policy_bindings,
+        "policy_binding_changes": binding_changes,
+        "authorized_policy_binding_changes": 
sorted(args.authorized_policy_binding_change),
+        "policy_binding_baseline_available": baseline is not None and 
"policy_bindings" in baseline,
+        "profiles": (
+            {each: bundle["profiles"][each] for each in selected_profiles}
+            if args.mode in {"validate", "trace"} else bundle["profiles"]
+        ),
+        "profile_case_counts": profile_case_counts,
+        "root_max_bytes": bundle["root_max_bytes"],
+        "default_project_doc_max_bytes": DEFAULT_PROJECT_DOC_MAX_BYTES,
+        "semantic_evaluation": semantic_evaluation_metadata(
+            semantic_policy_assertions, executed=args.mode not in {"validate", 
"trace"}
+        ),
+        "routing_trace": routing_trace_metadata(trace_results),
+        "impact": impact,
+    }
     if args.mode in {"validate", "trace"}:
         deterministic_regressions = sorted(set(contract_regressions + 
inventory_regressions + binding_regressions))
         summary = {
             "label": args.label,
             "mode": args.mode,
-            "policy_manifest": bundle["manifest_file"],
-            "policy_manifest_sha256": bundle["manifest_sha256"],
-            "policy_bundle_sha256": bundle["bundle_sha256"],
-            "policy_inventory_sha256": bundle["inventory_sha256"],
-            "policy_sources": bundle["inventory"],
-            "policy_source_changes": inventory_changes,
-            "authorized_policy_source_changes": 
sorted(args.authorized_policy_source_change),
-            "policy_source_baseline_available": baseline is not None and 
"policy_sources" in baseline,
-            "policy_binding_sha256": digest_json(policy_bindings),
-            "policy_bindings": policy_bindings,
-            "policy_binding_changes": binding_changes,
-            "authorized_policy_binding_changes": 
sorted(args.authorized_policy_binding_change),
-            "policy_binding_baseline_available": baseline is not None and 
"policy_bindings" in baseline,
-            "profiles": {each: bundle["profiles"][each] for each in 
selected_profiles},
-            "profile_case_counts": profile_case_counts,
-            "root_max_bytes": bundle["root_max_bytes"],
-            "default_project_doc_max_bytes": DEFAULT_PROJECT_DOC_MAX_BYTES,
-            "semantic_evaluation": 
semantic_evaluation_metadata(semantic_policy_assertions, executed=False),
-            "routing_trace": routing_trace_metadata(trace_results),
-            "impact": impact,
+            **policy_summary,
             "trace_results": trace_results,
             "critical_regressions": deterministic_regressions,
             "output_dir": str(output_dir),
@@ -2644,26 +2652,7 @@ def main() -> int:
         "mode": args.mode,
         "policy_file": "AGENTS.md",
         "policy_sha256": policy_sha256,
-        "policy_manifest": bundle["manifest_file"],
-        "policy_manifest_sha256": bundle["manifest_sha256"],
-        "policy_bundle_sha256": bundle["bundle_sha256"],
-        "policy_inventory_sha256": bundle["inventory_sha256"],
-        "policy_sources": bundle["inventory"],
-        "policy_source_changes": inventory_changes,
-        "authorized_policy_source_changes": 
sorted(args.authorized_policy_source_change),
-        "policy_source_baseline_available": baseline is not None and 
"policy_sources" in baseline,
-        "policy_binding_sha256": digest_json(policy_bindings),
-        "policy_bindings": policy_bindings,
-        "policy_binding_changes": binding_changes,
-        "authorized_policy_binding_changes": 
sorted(args.authorized_policy_binding_change),
-        "policy_binding_baseline_available": baseline is not None and 
"policy_bindings" in baseline,
-        "profiles": bundle["profiles"],
-        "profile_case_counts": profile_case_counts,
-        "root_max_bytes": bundle["root_max_bytes"],
-        "default_project_doc_max_bytes": DEFAULT_PROJECT_DOC_MAX_BYTES,
-        "semantic_evaluation": 
semantic_evaluation_metadata(semantic_policy_assertions),
-        "routing_trace": routing_trace_metadata(trace_results),
-        "impact": impact,
+        **policy_summary,
         "evaluator": {"model": EVALUATOR_MODEL, "reasoning_effort": 
EVALUATOR_REASONING_EFFORT},
         "runner_sha256": 
hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
         "cases_sha256": 
hashlib.sha256(Path(__file__).with_name("cases.toml").read_bytes()).hexdigest(),
diff --git a/.codex/harness/agents/test_run.py 
b/.codex/harness/agents/test_run.py
index 3a4325d5ca1..92910c2f4e6 100644
--- a/.codex/harness/agents/test_run.py
+++ b/.codex/harness/agents/test_run.py
@@ -64,6 +64,79 @@ class RunTest(unittest.TestCase):
             self.assertEqual(0, run.main())
         run_cases.assert_not_called()
 
+    def test_main_summary_contract_for_each_mode(self) -> None:
+        case = run.load_cases(["read_only_review"])[0]
+        bundle = 
run.load_policy_manifest(Path(run.__file__).resolve().parents[3])
+        policy_keys = [
+            "policy_manifest", "policy_manifest_sha256", 
"policy_bundle_sha256", "policy_inventory_sha256",
+            "policy_sources", "policy_source_changes", 
"authorized_policy_source_changes",
+            "policy_source_baseline_available", "policy_binding_sha256", 
"policy_bindings", "policy_binding_changes",
+            "authorized_policy_binding_changes", 
"policy_binding_baseline_available", "profiles", "profile_case_counts",
+            "root_max_bytes", "default_project_doc_max_bytes", 
"semantic_evaluation", "routing_trace", "impact",
+        ]
+        actual = {"results": [self._create_case_result(case, "Review 
FooService without editing it.")]}
+        evaluations = [self._create_evaluation([case["id"]], [])]
+        trace_results = [{"passed": True}]
+        for mode in ("validate", "trace", "semantic", "all"):
+            with self.subTest(mode=mode), tempfile.TemporaryDirectory() as 
directory:
+                output_dir = Path(directory) / "output"
+                with patch("sys.argv", [
+                        "run.py", "--mode", mode, "--case", case["id"], 
"--profile", "root",
+                        "--output-dir", str(output_dir)
+                ]), patch.object(run, "resolve_codex_home", 
return_value=Path(directory)), patch.object(
+                        run, "run_cases", return_value=(0, 1.0, actual, 
evaluations)
+                ) as run_cases, patch.object(
+                        run, "semantic_surface_digest", 
return_value="semantic-surface"
+                ), patch.object(
+                        run, "run_local_read_traces", 
return_value=trace_results
+                ), patch.object(
+                        run, "compare_policy_inventory", 
return_value=(["inventory-regression"], [])
+                ), patch.object(
+                        run, "compare_with_baseline", 
return_value=(["semantic-regression"], [])
+                ), patch.object(
+                        run, "read_usage", return_value={
+                            "input_tokens": 1, "cached_input_tokens": 0, 
"uncached_input_tokens": 1, "output_tokens": 1,
+                        }
+                ), patch("builtins.print") as print_summary:
+                    self.assertEqual(1, run.main())
+                summary = json.loads((output_dir / 
"summary.json").read_text(encoding="utf-8"))
+                self.assertEqual(summary, 
json.loads(print_summary.call_args.args[0]))
+                deterministic = mode in {"validate", "trace"}
+                policy_start = 2 if deterministic else 4
+                self.assertEqual(policy_keys, 
list(summary)[policy_start:policy_start + len(policy_keys)])
+                self.assertEqual(["label", "mode"], list(summary)[:2])
+                self.assertEqual(mode, summary["mode"])
+                self.assertEqual(bundle["manifest_file"], 
summary["policy_manifest"])
+                self.assertEqual(bundle["manifest_sha256"], 
summary["policy_manifest_sha256"])
+                self.assertEqual(bundle["bundle_sha256"], 
summary["policy_bundle_sha256"])
+                self.assertEqual(bundle["inventory_sha256"], 
summary["policy_inventory_sha256"])
+                self.assertEqual(bundle["inventory"], 
summary["policy_sources"])
+                self.assertEqual([], summary["policy_source_changes"])
+                self.assertEqual([], 
summary["authorized_policy_source_changes"])
+                self.assertFalse(summary["policy_source_baseline_available"])
+                self.assertFalse(summary["policy_binding_baseline_available"])
+                self.assertEqual(bundle["root_max_bytes"], 
summary["root_max_bytes"])
+                self.assertEqual(run.DEFAULT_PROJECT_DOC_MAX_BYTES, 
summary["default_project_doc_max_bytes"])
+                self.assertIsNone(summary["impact"])
+                expected_profiles = {"root": bundle["profiles"]["root"]} if 
deterministic else bundle["profiles"]
+                self.assertEqual(expected_profiles, summary["profiles"])
+                self.assertEqual(not deterministic, 
summary["semantic_evaluation"]["executed"])
+                expected_traces = trace_results if mode in {"trace", "all"} 
else []
+                self.assertEqual(expected_traces, summary["trace_results"])
+                self.assertEqual(bool(expected_traces), 
summary["routing_trace"]["executed"])
+                expected_regressions = ["inventory-regression"] if 
deterministic else ["inventory-regression", "semantic-regression"]
+                self.assertEqual(expected_regressions, 
summary["critical_regressions"])
+                if deterministic:
+                    run_cases.assert_not_called()
+                    self.assertEqual([
+                        "trace_results", "critical_regressions", "output_dir", 
"policy_snapshot_dir"
+                    ], list(summary)[policy_start + len(policy_keys):])
+                else:
+                    run_cases.assert_called_once()
+                    self.assertEqual(["policy_file", "policy_sha256"], 
list(summary)[2:4])
+                    self.assertEqual("AGENTS.md", summary["policy_file"])
+                    
self.assertEqual(hashlib.sha256(Path(run.__file__).read_bytes()).hexdigest(), 
summary["runner_sha256"])
+
     @staticmethod
     def _create_case_result(case: dict[str, Any], summary: str) -> dict[str, 
Any]:
         return {
diff --git a/.codex/skills/gen-ut/scripts/scan_quality_rules.py 
b/.codex/skills/gen-ut/scripts/scan_quality_rules.py
index fa348550ba4..6778929658b 100644
--- a/.codex/skills/gen-ut/scripts/scan_quality_rules.py
+++ b/.codex/skills/gen-ut/scripts/scan_quality_rules.py
@@ -784,11 +784,11 @@ def checkstyle_preview_final_parameters(path: Path, 
source: str) -> list[str]:
     return violations
 
 
-def collect_precheck_violations(context: FileScanContext) -> dict[str, 
list[str]]:
+def collect_precheck_violations(path: Path, source: str, method_bodies: 
dict[str, str]) -> dict[str, list[str]]:
     return {
-        "checkstyle-final-parameters": 
checkstyle_preview_final_parameters(context.path, context.source),
-        "parameterized-methodsource": check_r15_d(context.path, 
context.source, context.method_bodies),
-        "parameterized-name-parameter": check_r15_e(context.path, 
context.source),
+        "checkstyle-final-parameters": 
checkstyle_preview_final_parameters(path, source),
+        "parameterized-methodsource": check_r15_d(path, source, method_bodies),
+        "parameterized-name-parameter": check_r15_e(path, source),
     }
 
 
@@ -808,12 +808,12 @@ def create_file_scan_context(path: Path, scope_baseline: 
dict | None = None) ->
     )
 
 
-def file_rule_violations(context: FileScanContext) -> dict[str, list[str]]:
+def file_rule_violations(context: FileScanContext, precheck_violations: 
dict[str, list[str]]) -> dict[str, list[str]]:
     violations = defaultdict(list)
     violations["R8"].extend(check_parameterized_name(context.path, 
context.source))
     violations["R14"].extend(check_r14(context.path, context.source))
-    violations["R15-D"].extend(check_r15_d(context.path, context.source, 
context.method_bodies))
-    violations["R15-E"].extend(check_r15_e(context.path, context.source))
+    
violations["R15-D"].extend(precheck_violations["parameterized-methodsource"])
+    
violations["R15-E"].extend(precheck_violations["parameterized-name-parameter"])
     violations["R15-F"].extend(check_r15_f(context.path, context.source))
     violations["R15-H"].extend(check_r15_h(context.path, context.source))
     violations["R15-I"].extend(check_r15_i(context.path, context.source, 
context.method_bodies))
@@ -832,17 +832,36 @@ def build_rule_result(violations_by_rule: dict[str, 
list[str]]) -> dict[str, dic
     }
 
 
-def collect_scan_result(paths: list[Path], scope_baseline_path: Path | None) 
-> dict:
+def collect_scan_result(paths: list[Path], scope_baseline_path: Path | None, 
precheck_only: bool = False) -> dict:
     violations_by_rule = defaultdict(list)
     precheck_violations = defaultdict(list)
     java_paths = [each for each in paths if each.suffix == ".java"]
-    scope_baseline = load_scope_baseline(scope_baseline_path) if 
scope_baseline_path and scope_baseline_path.exists() else None
-    contexts = [create_file_scan_context(each, scope_baseline) for each in 
java_paths]
-    for context in contexts:
-        for rule, entries in file_rule_violations(context).items():
-            violations_by_rule[rule].extend(entries)
-        for name, entries in collect_precheck_violations(context).items():
+    scope_baseline = load_scope_baseline(scope_baseline_path) if not 
precheck_only and scope_baseline_path and scope_baseline_path.exists() else None
+    contexts = []
+    for path in java_paths:
+        if precheck_only:
+            source = path.read_text(encoding="utf-8")
+            method_bodies = {name: body for name, (_, body) in 
parse_method_blocks(source).items()}
+        else:
+            context = create_file_scan_context(path, scope_baseline)
+            contexts.append(context)
+            source = context.source
+            method_bodies = context.method_bodies
+        file_prechecks = collect_precheck_violations(path, source, 
method_bodies)
+        for name, entries in file_prechecks.items():
             precheck_violations[name].extend(entries)
+        if not precheck_only:
+            for rule, entries in file_rule_violations(context, 
file_prechecks).items():
+                violations_by_rule[rule].extend(entries)
+    prechecks = {
+        name: {
+            "message": PRECHECK_MESSAGES[name],
+            "violations": precheck_violations[name],
+        }
+        for name in PRECHECK_ORDER
+    }
+    if precheck_only:
+        return {"prechecks": prechecks}
     violations_by_rule["R15-C"].extend(
         check_scope_baseline(scope_baseline_path, paths) if 
scope_baseline_path else ["missing scope baseline"]
     )
@@ -853,13 +872,7 @@ def collect_scan_result(paths: list[Path], 
scope_baseline_path: Path | None) ->
     ]
     return {
         "rules": build_rule_result(violations_by_rule),
-        "prechecks": {
-            name: {
-                "message": PRECHECK_MESSAGES[name],
-                "violations": precheck_violations[name],
-            }
-            for name in PRECHECK_ORDER
-        },
+        "prechecks": prechecks,
         "candidates": [asdict(each) for context in contexts for each in 
context.candidates],
         "reviews": {
             "R15-A": [str(context.path) for context in contexts if not 
context.target_public_methods],
@@ -954,7 +967,7 @@ def main() -> int:
             print(f"[scope-baseline] path={output_path.resolve()} 
allowedPathCount={len(paths)}")
             return 0
         baseline_path = Path(args.scope_baseline) if args.scope_baseline else 
None
-        result = collect_scan_result(paths, baseline_path)
+        result = collect_scan_result(paths, baseline_path, 
precheck_only=args.precheck_only and not args.json)
         if args.json:
             print(json.dumps(result, indent=2, sort_keys=True))
             return 1 if failed_rule_names(result) else 0
diff --git a/.codex/skills/gen-ut/tests/test_scan_quality_rules.py 
b/.codex/skills/gen-ut/tests/test_scan_quality_rules.py
index 0443cd68864..e7d3d61060b 100644
--- a/.codex/skills/gen-ut/tests/test_scan_quality_rules.py
+++ b/.codex/skills/gen-ut/tests/test_scan_quality_rules.py
@@ -15,6 +15,7 @@
 # limitations under the License.
 #
 
+import io
 import json
 import subprocess
 import sys
@@ -167,6 +168,136 @@ class ScanQualityRulesTest(unittest.TestCase):
         actual = 
rules.resolve_target_source_path(Path("src/test/java/sample/TargetTest.java"), 
source)
         self.assertEqual(Path("src/main/java/sample/Target.java"), actual)
 
+    def test_scan_reuses_parameterized_prechecks(self):
+        source = 'class TargetTest {\n@ParameterizedTest(name = "{0}")\nvoid 
assertRun(int value) {}\n}'
+        with tempfile.TemporaryDirectory() as temp_dir:
+            paths = [Path(temp_dir) / "TargetTest.java", Path(temp_dir) / 
"OtherTest.java"]
+            for path in paths:
+                path.write_text(source, encoding="utf-8")
+            with patch.object(rules, "check_r15_d", wraps=rules.check_r15_d) 
as check_d, patch.object(rules, "check_r15_e", wraps=rules.check_r15_e) as 
check_e:
+                actual = rules.collect_scan_result([*paths, Path(temp_dir) / 
"sample.bin"], None)
+        self.assertEqual(2, check_d.call_count)
+        self.assertEqual(2, check_e.call_count)
+        expected_d = [f"{path}:2 method=assertRun missing @MethodSource" for 
path in paths]
+        expected_e = [f"{path}:2 method=assertRun firstParam=int value" for 
path in paths]
+        expected_violations = {"R15-C": ["missing scope baseline"], "R15-D": 
expected_d, "R15-E": expected_e}
+        self.assertEqual({
+            "rules": {
+                each.name: {"message": each.message, "mode": each.mode, 
"violations": expected_violations.get(each.name, [])}
+                for each in rules.RULE_SPECS
+            },
+            "prechecks": {
+                "checkstyle-final-parameters": {
+                    "message": 
rules.PRECHECK_MESSAGES["checkstyle-final-parameters"],
+                    "violations": [f"{path}:2 method=assertRun param=int 
value" for path in paths],
+                },
+                "parameterized-methodsource": {"message": 
rules.PRECHECK_MESSAGES["parameterized-methodsource"], "violations": 
expected_d},
+                "parameterized-name-parameter": {"message": 
rules.PRECHECK_MESSAGES["parameterized-name-parameter"], "violations": 
expected_e},
+            },
+            "candidates": [],
+            "reviews": {"R15-A": [str(path) for path in paths], "R15-B": [], 
"R15-D": [], "R15-G": [], "R15-I": [], "R15-J": [str(path) for path in paths]},
+            "java_file_count": 2,
+        }, actual)
+
+    def test_precheck_only_skips_full_scan_work(self):
+        with tempfile.TemporaryDirectory() as temp_dir:
+            path = Path(temp_dir) / "src/test/java/sample/TargetTest.java"
+            target = Path(temp_dir) / "src/main/java/sample/Target.java"
+            baseline = Path(temp_dir) / "scope.json"
+            path.parent.mkdir(parents=True)
+            target.parent.mkdir(parents=True)
+            path.write_text('class TargetTest { @ParameterizedTest(name = 
"{0}") @MethodSource("Samples#cases") void assertRun(final String name) {} }', 
encoding="utf-8")
+            target.write_bytes(b"\xff")
+            baseline.write_text("invalid baseline", encoding="utf-8")
+            with (
+                patch.object(sys, "argv", ["scan", "--precheck-only", 
"--scope-baseline", str(baseline), str(path)]),
+                patch.object(sys, "stdout", new_callable=io.StringIO) as 
stdout,
+                patch.object(Path, "read_text", autospec=True, 
side_effect=Path.read_text) as read_text,
+                patch.object(rules, "create_file_scan_context", 
side_effect=AssertionError("full context requested")),
+                patch.object(rules.subprocess, "run", 
side_effect=AssertionError("Git requested")),
+            ):
+                actual = rules.main()
+            read_text.assert_called_once_with(path, encoding="utf-8")
+        self.assertEqual(0, actual)
+        self.assertEqual("[precheck:checkstyle-final-parameters] 
ok\n[precheck:parameterized-methodsource] 
ok\n[precheck:parameterized-name-parameter] ok\n", stdout.getvalue())
+
+    def test_precheck_only_diagnostics(self):
+        with tempfile.TemporaryDirectory() as temp_dir:
+            path = Path(temp_dir) / "TargetTest.java"
+            valid_source = (
+                'class TargetTest { @ParameterizedTest(name = "{0}") 
@MethodSource("samples") void assertRun(final String name) {} '
+                'static Stream<Arguments> samples() { return 
Stream.of(Arguments.of("foo"), Arguments.of("bar"), Arguments.of("baz")); } }'
+            )
+            invalid_source = 'class TargetTest { @ParameterizedTest(name = 
"{0}") void assertRun(int value) {} }'
+            expected_violations = (
+                "[precheck:checkstyle-final-parameters] test method parameters 
should be final to avoid Checkstyle FinalParameters failures\n"
+                f"{path}:1 method=assertRun param=int value\n"
+                "[precheck:parameterized-methodsource] parameterized tests 
should use @MethodSource providers with at least 3 Arguments rows\n"
+                f"{path}:1 method=assertRun missing @MethodSource\n"
+                "[precheck:parameterized-name-parameter] parameterized tests 
should declare the first parameter exactly as `final String name`\n"
+                f"{path}:1 method=assertRun firstParam=int value\n"
+            )
+            expected_ok = "[precheck:checkstyle-final-parameters] 
ok\n[precheck:parameterized-methodsource] 
ok\n[precheck:parameterized-name-parameter] ok\n"
+            expected_short_provider = (
+                "[precheck:checkstyle-final-parameters] ok\n"
+                "[precheck:parameterized-methodsource] parameterized tests 
should use @MethodSource providers with at least 3 Arguments rows\n"
+                f"{path}:1 method=assertRun argumentsRows=2\n"
+                "[precheck:parameterized-name-parameter] ok\n"
+            )
+            fixtures = (
+                ("valid", path, valid_source, 0, expected_ok, ""),
+                ("violations", path, invalid_source, 1, expected_violations, 
""),
+                ("short-provider", path, valid_source.replace(', 
Arguments.of("baz")', ""), 1, expected_short_provider, ""),
+                ("missing-java", Path(temp_dir) / "MissingTest.java", None, 2, 
"", f"[Errno 2] No such file or directory: '{Path(temp_dir) / 
'MissingTest.java'}'\n"),
+                ("non-java", Path(temp_dir) / "sample.bin", None, 0, 
expected_ok, ""),
+            )
+            for name, input_path, source, expected_exit, expected_stdout, 
expected_stderr in fixtures:
+                with self.subTest(name=name):
+                    if source is not None:
+                        input_path.write_text(source, encoding="utf-8")
+                    with (
+                        patch.object(sys, "argv", ["scan", "--precheck-only", 
"--summary-only", str(input_path)]),
+                        patch.object(sys, "stdout", new_callable=io.StringIO) 
as stdout,
+                        patch.object(sys, "stderr", new_callable=io.StringIO) 
as stderr,
+                    ):
+                        actual = rules.main()
+                    self.assertEqual(expected_exit, actual)
+                    self.assertEqual(expected_stdout, stdout.getvalue())
+                    self.assertEqual(expected_stderr, stderr.getvalue())
+
+    def test_json_precedes_precheck_and_summary(self):
+        with tempfile.TemporaryDirectory() as temp_dir:
+            path = Path(temp_dir) / "TargetTest.java"
+            path.write_text("class TargetTest {}", encoding="utf-8")
+            with (
+                patch.object(sys, "argv", ["scan", "--json", 
"--precheck-only", "--summary-only", str(path)]),
+                patch.object(sys, "stdout", new_callable=io.StringIO) as 
stdout,
+            ):
+                actual = rules.main()
+        result = json.loads(stdout.getvalue())
+        self.assertEqual(1, actual)
+        self.assertEqual(["candidates", "java_file_count", "prechecks", 
"reviews", "rules"], list(result))
+        self.assertEqual(["missing scope baseline"], 
result["rules"]["R15-C"]["violations"])
+        self.assertEqual(1, result["java_file_count"])
+        self.assertEqual(json.dumps(result, indent=2, sort_keys=True) + "\n", 
stdout.getvalue())
+
+    def test_capture_precedes_report_flags(self):
+        with tempfile.TemporaryDirectory() as temp_dir:
+            repo_root = Path(temp_dir)
+            baseline = repo_root / "scope.json"
+            path = repo_root / "TargetTest.java"
+            with (
+                patch.object(sys, "argv", ["scan", "--capture-scope-baseline", 
str(baseline), "--json", "--precheck-only", "--summary-only", str(path)]),
+                patch.object(sys, "stdout", new_callable=io.StringIO) as 
stdout,
+                patch.object(rules, "get_repo_root", return_value=repo_root),
+                patch.object(rules, "write_scope_baseline") as write_baseline,
+                patch.object(rules, "collect_scan_result", 
side_effect=AssertionError("scan requested")),
+            ):
+                actual = rules.main()
+            write_baseline.assert_called_once_with(baseline, repo_root, [path])
+        self.assertEqual(0, actual)
+        self.assertEqual(f"[scope-baseline] path={baseline.resolve()} 
allowedPathCount=1\n", stdout.getvalue())
+
     def test_scope_baseline_detects_change_to_preexisting_dirty_file(self):
         with tempfile.TemporaryDirectory() as temp_dir:
             repo_root = Path(temp_dir)
diff --git a/.codex/skills/review-pr/tests/test_build_review_inventory.py 
b/.codex/skills/review-pr/tests/test_build_review_inventory.py
index 7786eb3fc9c..0e8ebf44565 100644
--- a/.codex/skills/review-pr/tests/test_build_review_inventory.py
+++ b/.codex/skills/review-pr/tests/test_build_review_inventory.py
@@ -16,7 +16,6 @@
 #
 
 import sys
-import tempfile
 import unittest
 from argparse import Namespace
 from pathlib import Path
@@ -64,25 +63,6 @@ class BuildReviewInventoryTest(unittest.TestCase):
         self.assertEqual("AGENTS.md", 
actual["changed_files_by_category"]["docs"][0]["path"])
         resolve_candidate_changes.assert_called_once_with(Path("/repo"), 
"merge-base", "candidate.txt")
 
-    def test_resolve_candidate_changes_includes_untracked_files(self):
-        with tempfile.TemporaryDirectory() as temp_dir:
-            candidate_file = Path(temp_dir) / "candidate.txt"
-            candidate_file.write_text("AGENTS.md\n.codex/new.toml\n", 
encoding="utf-8")
-            with patch.object(common, "run_git", 
side_effect=["M\tAGENTS.md\n", ".codex/new.toml\n"]) as run_git:
-                actual = common.resolve_candidate_changes(Path("/repo"), 
"baseline", str(candidate_file))
-        self.assertEqual([common.ChangedFile(status="M", path="AGENTS.md"),
-                          common.ChangedFile(status="A", 
path=".codex/new.toml")], actual)
-        literal_paths = [":(literal)AGENTS.md", ":(literal).codex/new.toml"]
-        run_git.assert_any_call(["diff", "--name-status", "baseline", "--", 
*literal_paths], Path("/repo"))
-        run_git.assert_any_call(["ls-files", "--others", "--exclude-standard", 
"--", *literal_paths], Path("/repo"), allow_empty=True)
-
-    def test_resolve_candidate_changes_rejects_unchanged_paths(self):
-        with tempfile.TemporaryDirectory() as temp_dir:
-            candidate_file = Path(temp_dir) / "candidate.txt"
-            candidate_file.write_text("AGENTS.md\n", encoding="utf-8")
-            with patch.object(common, "run_git", side_effect=["", ""]), 
self.assertRaisesRegex(RuntimeError, "not changed"):
-                common.resolve_candidate_changes(Path("/repo"), "baseline", 
str(candidate_file))
-
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/.codex/skills/review-pr/tests/test_review_common.py 
b/.codex/skills/review-pr/tests/test_review_common.py
index ce399b9a915..0493a0f772a 100644
--- a/.codex/skills/review-pr/tests/test_review_common.py
+++ b/.codex/skills/review-pr/tests/test_review_common.py
@@ -19,12 +19,14 @@ import sys
 import tempfile
 import unittest
 from pathlib import Path
+from unittest.mock import patch
 
 
 SCRIPTS_DIR = Path(__file__).resolve().parents[1] / "scripts"
 if str(SCRIPTS_DIR) not in sys.path:
     sys.path.insert(0, str(SCRIPTS_DIR))
 
+import review_common as common
 from review_common import ChangedFile, categorize, compare_github_files, 
parse_name_status
 
 
@@ -46,6 +48,25 @@ class ReviewCommonTest(unittest.TestCase):
         self.assertEqual("build-config", categorize(".github/README.md"))
         self.assertEqual("docs", categorize("docs/community/index.md"))
 
+    def test_resolve_candidate_changes_includes_untracked_files(self):
+        with tempfile.TemporaryDirectory() as temp_dir:
+            candidate_file = Path(temp_dir) / "candidate.txt"
+            candidate_file.write_text("AGENTS.md\n.codex/new.toml\n", 
encoding="utf-8")
+            with patch.object(common, "run_git", 
side_effect=["M\tAGENTS.md\n", ".codex/new.toml\n"]) as run_git:
+                actual = common.resolve_candidate_changes(Path("/repo"), 
"baseline", str(candidate_file))
+        self.assertEqual([common.ChangedFile(status="M", path="AGENTS.md"),
+                          common.ChangedFile(status="A", 
path=".codex/new.toml")], actual)
+        literal_paths = [":(literal)AGENTS.md", ":(literal).codex/new.toml"]
+        run_git.assert_any_call(["diff", "--name-status", "baseline", "--", 
*literal_paths], Path("/repo"))
+        run_git.assert_any_call(["ls-files", "--others", "--exclude-standard", 
"--", *literal_paths], Path("/repo"), allow_empty=True)
+
+    def test_resolve_candidate_changes_rejects_unchanged_paths(self):
+        with tempfile.TemporaryDirectory() as temp_dir:
+            candidate_file = Path(temp_dir) / "candidate.txt"
+            candidate_file.write_text("AGENTS.md\n", encoding="utf-8")
+            with patch.object(common, "run_git", side_effect=["", ""]), 
self.assertRaisesRegex(RuntimeError, "not changed"):
+                common.resolve_candidate_changes(Path("/repo"), "baseline", 
str(candidate_file))
+
     def test_compare_github_files_reports_bounded_differences(self):
         with tempfile.TemporaryDirectory() as temp_dir:
             github_files = Path(temp_dir) / "github-files.txt"
diff --git a/.codex/skills/review-pr/tests/test_review_ledger.py 
b/.codex/skills/review-pr/tests/test_review_ledger.py
index c18bce95765..f1c43fd1a0a 100644
--- a/.codex/skills/review-pr/tests/test_review_ledger.py
+++ b/.codex/skills/review-pr/tests/test_review_ledger.py
@@ -712,10 +712,6 @@ class ReviewLedgerTest(unittest.TestCase):
         self.save_session_report(actual, len(actual["reviews"]) - 1, report)
         self.assertTrue(ledger.validate_session(actual))
 
-    def test_session_allows_second_pair_after_disagreement(self):
-        actual = self.create_session([[("F1", "confirmed")], [], [("F1", 
"confirmed")], [("F1", "confirmed")]])
-        self.assertEqual([], ledger.validate_session(actual))
-
     def test_session_preserves_finding_discovered_during_result_review(self):
         original = self.create_session([[], [], [("F1", "confirmed")], [("F1", 
"confirmed")]])
         original["rounds"][0]["findings"].append({

Reply via email to