feat(prover): delivery D: control flow decides dominance; the trace carries each guard's relation, effect and the operation's control context

Opus agent delivery under spec access-control-prover, requirement 10. `semantic/cfg.py` builds a control-flow
graph per function from the syntax tree (if/else, switch arms, loops, try/catch/finally, return, throw, break,
continue, exit, assert) with dominators and a per-line control context, and leaves a function without a graph
rather than a wrong one. `obligations/cfg_dominance.py::CfgDominance` discharges a statement guard only when it
lies on every path to the operation and the operation is unreachable from its failing arm; a predicate whose
result nothing consumes is `value_only` and never discharges; router, decorator and earlier-hop witnesses behave
as before; without a graph the order rule applies and `cfg_unavailable` is recorded. The trace's guards carry
`relation` and `effect`, the trace carries `operation_context`; Java parameter lists carry names only; a
handler-named operation's arguments are the handler's parameters. The checker's default stays order-based.

Smoke on the 23 fix changes: 15 caught, 55 of 93 locations, precision 15 of 15, no failure, no change over
120 s (unchanged from delivery B). Gate: 3575 tests pass; the reserved-repository guard passes on the export.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-14 19:01:07 +02:00
co-authored by Claude Fable 5.1
parent 6c3d4a7d72
commit 03b114cbeb
12 changed files with 1546 additions and 18 deletions
+69
View File
@@ -295,3 +295,72 @@ def test_remix_missing_kinds_and_public_policy(tmp_path, public, expected_missin
change["files"] = [{"path": "app/routes/items.ts", "head_lines": [[1, 1]]}]
result = prove_change(root, change)
assert [finding["missing"] for finding in result.findings] == expected_missing
@pytest.mark.parametrize(
"guard,findings,relation,effect",
[
(
[" if (shared) {", " Jenkins.get().checkPermission(Jenkins.ADMINISTER);", " }"],
1,
"some_paths",
"throws",
),
(
[" if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {", ' throw new AccessDeniedException("denied");', " }"],
0,
"all_paths",
"throws",
),
([" boolean allowed = Jenkins.get().hasPermission(Jenkins.ADMINISTER);"], 1, "all_paths", "value_only"),
],
)
def test_control_flow_decides_the_verdict_and_the_trace(tmp_path, guard, findings, relation, effect):
body = "\n ".join(guard)
root, change = repository(
tmp_path,
f"""class X extends Descriptor<Thing> {{
public Object doTestConnection(@QueryParameter boolean shared) {{
{body}
return CredentialsProvider.lookupCredentials(Thing.class);
}}
}}""",
)
result = prove_change(root, change)
assert len(result.findings) == findings
assert result.coverage.operations_discharged == 1 - findings
trace = result.traces[0]
assert trace["parameters"] == ["shared"]
assert [(g["relation"], g["effect"]) for g in trace["hops"][0]["guards_before_call"]] == [(relation, effect)]
assert not [item for item in result.degradations if item.get("reason") == "cfg_unavailable"]
if relation == "some_paths":
assert trace["missing"] == "path_guard" and trace["operation_context"]["conditions"] == []
def test_handler_operation_trace_carries_handler_parameters(tmp_path):
root, change = repository(
tmp_path,
"""class X extends Descriptor<Thing> {
public Object doCheckValue(@QueryParameter String value, @AncestorInPath Item item) {
return 1;
}
}""",
)
result = prove_change(root, change)
assert result.traces[0]["arguments"] == [["value", "request_input"], ["item", "request_input"]]
def test_unmodelled_function_is_judged_by_order_and_recorded(tmp_path):
root, change = repository(
tmp_path,
"""class X extends Descriptor<Thing> {
public Object doTestConnection() {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
outer: for (;;) { break outer; }
return CredentialsProvider.lookupCredentials(Thing.class);
}
}""",
)
result = prove_change(root, change)
assert not result.findings and result.coverage.operations_discharged == 1
assert any(item.get("reason") == "cfg_unavailable" and item.get("function") == "doTestConnection" for item in result.degradations)