mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
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:
co-authored by
Claude Opus 5
parent
c1cc332bc1
commit
bc68000e93
@@ -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.
|
||||
|
||||
@@ -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}
|
||||
):
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user