fix(pre-push-safety-net): apply the exclusion distinction inside comparison too

The NodeGoat rerun after 10.4-10.6 showed both target questions completing, which
is what those tasks were for, but the comparison still reported
head_context_incomplete. Two checks inside comparison had not been updated with
the same distinction: any degradation blocked, and any scope boundary blocked.

A declared exclusion and a correspondence ambiguity both reached those checks. The
first is a scope choice rather than a failure to read the graph, and the second is
decided per operation since 10.3, so neither should block a claim. Both remain in
aggregate coverage. Integrity degradations, unknown ownership and every other
boundary still block, and bounded reachability is still filtered separately
because it blocks only claims resting on a flow not having been traced.

1,368 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:51:49 +02:00
co-authored by Claude Opus 5
parent c1cc332bc1
commit bc68000e93
3 changed files with 50 additions and 6 deletions
+1 -1
View File
@@ -315,7 +315,7 @@ Preserve all completed task evidence below.
- _Boundary: contributor-scan graph/scope evidence, Cache and Engine Boundary_
- _Depends: 10.5_
- _Requirements: 2.2, 2.3, 8.2, 8.4_
- _Verified 2026-09-16:_ 1,365 tests passed, nine skipped; Ruff/format/mypy passed. Graph boundaries are split into integrity failures, which still demote a completed answer, and declared exclusions, which do not. Whether a particular answer needed the excluded semantics is carried by that question's own unresolved call destinations from 10.5, not assumed for every answer. Controls exercise the real partition logic rather than an injected reason: a vendored directory leaves the answer completed with the exclusion still reported, an unresolved symlink still demotes, and the two together still demote. The exclusion stays physical and out of targets and graphs.
- _Verified 2026-09-16:_ 1,365 tests passed, nine skipped; Ruff/format/mypy passed. Graph boundaries are split into integrity failures, which still demote a completed answer, and declared exclusions, which do not. Whether a particular answer needed the excluded semantics is carried by that question's own unresolved call destinations from 10.5, not assumed for every answer. Controls exercise the real partition logic rather than an injected reason: a vendored directory leaves the answer completed with the exclusion still reported, an unresolved symlink still demotes, and the two together still demote. The exclusion stays physical and out of targets and graphs. The NodeGoat rerun showed the same distinction was still missing inside comparison, where any degradation and any boundary blocked: comparison now filters declared exclusions and correspondence ambiguity on both sides, keeping integrity degradations and every other boundary blocking.
- [ ] 10.7 Carry query-to-operation provenance into the reported evidence
- The target is selected through a file-fallback question with `function=None` while rows report method `<lambda>2`. M1a showed this did not block the comparison once the question completed, so it is provenance quality rather than the blocker.
- Record verified query-to-operation provenance on the evidence; do not inject benchmark function names into regions or count coincident sites.
+25 -5
View File
@@ -18,7 +18,7 @@ from openultrasast.model.contracts import ChangeContext, ExecutionBudget, Questi
from openultrasast.model.ladder import Rung
from openultrasast.model.pipeline import ModelFinding, _arbitrate_all
from openultrasast.model.regions import AMBIGUOUS_CORRESPONDENCE, ScanRegion
from openultrasast.model.scan import ModelScanResult, ScanBudget, _prefetched, _spec_for, scan_repository
from openultrasast.model.scan import DECLARED_EXCLUSION_GAPS, ModelScanResult, ScanBudget, _prefetched, _spec_for, scan_repository
from openultrasast.model.specs import ConfigSpec, DominanceSpec, TaintSpec
from openultrasast.push.cache import ArtifactCache, SemanticKeys
from openultrasast.push.contracts import ComparisonAnalysis, PushComparison, PushResult
@@ -310,6 +310,26 @@ def _blocking_context(context: ChangeContext) -> tuple[str, ...]:
return tuple(gap for gap in context.unresolved_boundaries if not gap.endswith(":" + AMBIGUOUS_CORRESPONDENCE))
def _blocking_gaps(scan: ModelScanResult, *questions: QuestionIdentity) -> tuple[str, ...]:
"""Scan gaps that can block a comparison claim.
Correspondence ambiguity is decided per operation, and a declared exclusion is a scope
choice rather than a failure to read the graph, so neither belongs here. Both remain in
aggregate coverage. Bounded reachability is kept and filtered by the caller, because it
blocks only the claims that rest on a flow not having been traced.
"""
return tuple(
gap
for gap in _context_boundaries(scan, *questions)
if not gap.endswith(":" + AMBIGUOUS_CORRESPONDENCE) and gap.split(":")[0] not in DECLARED_EXCLUSION_GAPS
)
def _blocking_degradations(scan: ModelScanResult) -> tuple[str, ...]:
"""Degradations that undermine an answer. A declared exclusion is not one of them."""
return tuple(str(d.get("reason")) for d in scan.degradations if str(d.get("reason")) not in DECLARED_EXCLUSION_GAPS)
def _required_scope(question: QuestionIdentity, context: ChangeContext) -> set[QuestionIdentity]:
"""Base questions this comparison requires: the counterpart and its recorded dependencies.
@@ -483,7 +503,7 @@ def _compare_evidence(
required = _required_scope(op.question, context)
selected = {q.identity for q in base.scope.selected} if base is not None and base.scope is not None else set()
answered = {q.identity for q in base.question_outcomes if q.status == "completed"} if base is not None else set()
boundaries = _context_boundaries(base, *required) if base is not None else ("base_unavailable",)
boundaries = _blocking_gaps(base, *required) if base is not None else ("base_unavailable",)
# Everything a base answer needs except its reachability, which limits only claims
# that rest on a flow not having been traced.
established = bool(
@@ -492,7 +512,7 @@ def _compare_evidence(
and base.scope.population_complete
# A truncated base scan cannot establish absence, wherever it was truncated.
and all(question.reason == "tier_zero" for question in base.scope.deferred)
and not base.degradations
and not _blocking_degradations(base)
and required <= selected
and required <= answered
and {q.identity for q in base.question_outcomes} <= selected
@@ -512,8 +532,8 @@ def _compare_evidence(
not head.scope
# A bounded reachability on the head can hide further flows; it cannot invalidate
# the flow this finding was traced from, so it does not block the head's own claim.
or [gap for gap in _context_boundaries(head, op.question) if not gap.startswith(REACHABILITY_BOUNDED)]
or head.degradations
or [gap for gap in _blocking_gaps(head, op.question) if not gap.startswith(REACHABILITY_BOUNDED)]
or _blocking_degradations(head)
or not _valid_answer(head, op.question, head_ops)
or op.question not in {q.identity for q in head.scope.selected}
):
+24
View File
@@ -525,3 +525,27 @@ def test_a_non_reachability_base_gap_still_blocks_an_enumerated_absence():
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"
def degraded(result, *reasons):
return replace(result, degradations=tuple({"stage": "cpg", "reason": r} for r in reasons))
def test_a_declared_exclusion_does_not_block_a_comparison_on_either_side():
"""M1b 10.6 in comparison: the exclusion is a scope choice, not a failed read."""
head = degraded(scan(), "vendor_semantics_unresolved")
base = degraded(scan(rows=inventory_rows(), findings=False), "vendor_semantics_unresolved")
assert delta(head, base, changed_line_context()).novelty == "new"
def test_an_integrity_degradation_still_blocks_a_comparison():
head = degraded(scan(), "vendor_semantics_unresolved", "cpg_empty")
base = scan(rows=inventory_rows(), findings=False)
assert delta(head, base, changed_line_context()).reason == "head_context_incomplete"
assert delta(scan(), degraded(base, "cpg_empty"), changed_line_context()).novelty == "unknown"
def test_a_declared_exclusion_boundary_does_not_block_either():
base = scan(rows=inventory_rows(), findings=False)
base = replace(base, scope=replace(base.scope, unresolved_boundaries=("vendor_semantics_unresolved",)))
assert delta(scan(), base, changed_line_context()).novelty == "new"