feat(pre-push-safety-net): bind the reachability boundary to what it limits (M1b 10.5)

dynamic_external_or_depth_context_unresolved was applied as though it invalidated
everything about a question. It does not. Bounded reachability can hide further
flows; it cannot invalidate a flow that was traced, and it cannot remove a call
from a structurally scoped enumeration.

At scan level the boundary is now recorded against its owning question without
demoting that question's answer. An unavailable context projection, an empty
context scope and an unreadable method extent are unknown ownership and still
demote.

In comparison it no longer blocks the head's own claim, since the finding rests
on a path that was actually traced, and no longer blocks an absence read off the
enumerated inventory. It still blocks every claim that rests on a flow not having
been traced, including the anchored source-connection branch, which now reports
base_reachability_bounded rather than silently passing.

The query declares the basis of its enumeration. A file- or nesting-scoped
question walks calls structurally, so an unresolved destination cannot remove one
from scope. Following the call graph makes the reachable set the scope, so the
same unresolved destination can shrink what was enumerated; that inventory cannot
establish absence while reachability is bounded, and says so.

One prior control asserted a question's own reachability gap blocks its own
claim. It is updated with its reason and split, so an own projection gap is still
proven to block while an own reachability gap no longer does.

1,362 tests passed, nine skipped. Rollout stays NO-GO.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-16 17:26:16 +02:00
co-authored by Claude Opus 5
parent c73efd2bde
commit b202aabd8b
6 changed files with 161 additions and 23 deletions
+34
View File
@@ -197,3 +197,37 @@ def test_any_other_inherited_transaction_gap_still_marks_questions_incomplete(tm
)
assert result.question_outcomes[0].status == "unresolved"
assert result.question_outcomes[0].reason == "change_context_incomplete"
def boundary(reason):
return {"kind": "context_boundary", "reason": reason}
def test_bounded_reachability_is_recorded_without_demoting_an_answered_question(tmp_path):
"""M1b 10.5: the boundary limits which further flows could be seen, not this answer."""
(tmp_path / "source.php").write_text("readable\n")
bounded = boundary("dynamic_external_or_depth_context_unresolved:contributor-scan")
rows = vector() + [row("source.php"), {"kind": "context_summary"}, bounded]
result = scan_repository(
tmp_path,
[region("source.php", "php")],
backend=Backend({"source.php": rows}),
change_context=context(),
)
assert result.question_outcomes[0].status == "completed"
# Still reported, bound to the question that owns it, so comparison can consult it.
owned = [g for g in result.scope.unresolved_boundaries if g.startswith("dynamic_external_or_depth_context_unresolved")]
assert owned and owned[0].endswith(result.question_outcomes[0].identity.question_id)
def test_an_unresolved_context_projection_still_demotes_the_question(tmp_path):
(tmp_path / "source.php").write_text("readable\n")
rows = vector() + [row("source.php"), {"kind": "context_summary"}, boundary("context_location_unavailable")]
result = scan_repository(
tmp_path,
[region("source.php", "php")],
backend=Backend({"source.php": rows}),
change_context=context(),
)
assert result.question_outcomes[0].status == "unresolved"
assert result.question_outcomes[0].reason == "change_context_incomplete"
+56 -4
View File
@@ -228,9 +228,18 @@ def test_unrelated_question_context_gap_does_not_poison_completed_comparison():
question_outcomes=(*head.question_outcomes, QuestionOutcome(other, "unresolved", "change_context_incomplete", "[]")),
)
assert delta(head, scan(rows=[], findings=False)).novelty == "new"
own_gap = gap.rsplit(":", 1)[0] + ":" + head.scope.selected[0].identity.question_id
mine = head.scope.selected[0].identity.question_id
# M1b 10.5: the question's own bounded reachability can hide further flows, but it cannot
# invalidate the flow this finding was traced from, so it no longer blocks its own claim.
own_reachability = gap.rsplit(":", 1)[0] + ":" + mine
assert (
delta(replace(head, scope=replace(head.scope, unresolved_boundaries=(own_gap,))), scan(rows=[], findings=False)).novelty
delta(replace(head, scope=replace(head.scope, unresolved_boundaries=(own_reachability,))), scan(rows=[], findings=False)).novelty
== "new"
)
# An unavailable context projection is unknown ownership, not bounded reachability, and still blocks.
own_projection = "context_projection_unavailable:contributor-scan:" + mine
assert (
delta(replace(head, scope=replace(head.scope, unresolved_boundaries=(own_projection,))), scan(rows=[], findings=False)).novelty
== "unknown"
)
assert (
@@ -426,12 +435,13 @@ def test_any_other_transaction_context_gap_still_blocks():
assert (candidate.novelty, candidate.reason) == ("unknown", "change_context_unresolved")
def inventory_rows(*operations, complete=True, path="a.js", line=4):
def inventory_rows(*operations, complete=True, path="a.js", line=4, scope="structural"):
rows = [
{"kind": "operation_inventory", "inventory": op, "inventoryFile": path, "inventoryLine": line, "inventoryMethod": "handler"}
for op in operations
]
return rows + ([{"kind": "operation_inventory_complete", "operations": len(rows)}] if complete else [])
marker = {"kind": "operation_inventory_complete", "operations": len(rows), "scope": scope}
return rows + ([marker] if complete else [])
def test_an_operation_present_in_the_base_inventory_is_not_new():
@@ -473,3 +483,45 @@ def test_inventory_rows_do_not_count_as_unparsed_answer_body():
rows = json.loads(head.question_outcomes[0].raw_rows_json) + inventory_rows("exec(cmd)")
head = replace(head, question_outcomes=(replace(head.question_outcomes[0], raw_rows_json=json.dumps(rows)),))
assert delta(head, scan(rows=inventory_rows("query(sql, params)"), findings=False), changed_line_context()).novelty == "new"
def bounded_base(*operations, own_question=True, scope="structural"):
"""A base whose only gap is its own bounded reachability."""
base = scan(rows=inventory_rows(*operations, scope=scope), findings=False)
owner = base.scope.selected[0].identity if own_question else replace(base.scope.selected[0].identity, family="path")
gap = "dynamic_external_or_depth_context_unresolved:contributor-scan:" + owner.question_id
return replace(base, scope=replace(base.scope, unresolved_boundaries=(gap,)))
def test_absence_from_a_structural_enumeration_survives_bounded_reachability():
"""M1b 10.5: enumeration is structural, so an unresolved destination cannot remove a call."""
candidate = delta(scan(), bounded_base(), changed_line_context())
assert (candidate.novelty, candidate.reason) == ("new", "operation_absent_from_comparable_base")
def test_a_traced_flow_absence_still_requires_complete_reachability():
"""The anchored branch rests on no flow having been traced, so the bound still blocks."""
candidate = delta(scan(), bounded_base(), context())
assert candidate.novelty == "unknown" and candidate.reason == "base_reachability_bounded"
def test_a_reachability_derived_enumeration_cannot_establish_absence_while_bounded():
candidate = delta(scan(), bounded_base(scope="reachability"), changed_line_context())
assert candidate.novelty == "unknown"
assert candidate.reason == "base_enumeration_scope_reachability_derived"
def test_a_reachability_derived_enumeration_is_usable_when_reachability_is_complete():
base = scan(rows=inventory_rows(scope="reachability"), findings=False)
assert delta(scan(), base, changed_line_context()).novelty == "new"
def test_a_non_reachability_base_gap_still_blocks_an_enumerated_absence():
base = scan(rows=inventory_rows(), findings=False)
base = replace(base, scope=replace(base.scope, unresolved_boundaries=("graph_incomplete",)))
assert delta(scan(), base, changed_line_context()).novelty == "unknown"
def test_a_present_operation_still_refuses_novelty_under_bounded_reachability():
candidate = delta(scan(), bounded_base("exec(cmd)"), changed_line_context())
assert candidate.reason == "operation_present_in_base_without_traced_flow"