From a487d1dfb52031d0a6177a6ff49b0eeedabbfdda Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:50:38 +0200 Subject: [PATCH] fix(push): a gap from another language partition no longer blocks admission _blocking_degradations already exempted declared exclusions. It now also ignores a gap another language partition raised, because a JavaScript file missing from the JavaScript graph cannot make a PHP answer wrong. Treating it as though it could left a WordPress plugin -- PHP plus its admin JavaScript, which is every plugin -- unable to admit a finding it had already established. A gap that names no partition still reaches everything, and so does every gap when the claim own language is unknown, because unknown scope is not evidence of independence. The control asserts all four directions. With this and the scan fixes, task 13.4 is verified on the subject that motivated it: PMPro completes 120 of 127 questions where it completed none, its 19 findings unchanged, and the seven still unresolved are the JavaScript partition own questions, which is the correct answer. 1,420 tests pass, eighteen skipped. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo --- .kiro/specs/pre-push-safety-net/tasks.md | 4 ++- src/openultrasast/push/policy.py | 31 +++++++++++++++++++----- tests/test_push_policy.py | 23 ++++++++++++++++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/.kiro/specs/pre-push-safety-net/tasks.md b/.kiro/specs/pre-push-safety-net/tasks.md index c3dbe1d..b13a9c6 100644 --- a/.kiro/specs/pre-push-safety-net/tasks.md +++ b/.kiro/specs/pre-push-safety-net/tasks.md @@ -569,7 +569,9 @@ Preserve all completed task evidence below. - Do not reclassify the reason as a declared exclusion. That would claim cross-language flows are out of scope when they are unresolved, which is the opposite of what this project's degradation vocabulary exists to say. - Done when a question whose scope lies inside one partition completes, a question whose scope could cross a partition boundary stays unresolved with the boundary named, and no question completes by widening its scope. Evidence: `benchmarks/measurements/2026-09-21-two-languages-defeat-completion.json`. - _Both halves implemented 2026-09-21._ A language boundary is now a declared limit: it can hide a finding and never invent one, so it stays published and stops demoting. And an integrity gap raised by a partition census now carries the partition it came from and reaches only that partition's questions, because a missing JavaScript file cannot make a PHP answer wrong. Two controls exercise the real boundaries rather than injecting a reason, and both were confirmed to FAIL with the classification restored. The rule behind both is now written in the design as an invariant, with all five instances listed, because this is the fifth time it has been found and fixed one instance at a time. - - _Not yet verified on a subject._ PMPro is the measurement that motivated this and it must be re-run to show a PHP question completing. That run is what closes this task. + - _Verified on the subject 2026-09-21:_ PMPro completes **120 of 127** questions where it completed none, with its 19 findings unchanged. The seven still unresolved are the JavaScript partition's own questions, which is correct: the four missing files are its files, so its answers are the ones that could be wrong. 1,419 tests pass, eighteen skipped; Ruff/format/mypy pass. + - _Three causes, not one, and the third was hidden by the record rather than by the code._ The same census gap is raised in TWO passes, arbitration and evidence, and carrying the partition in one but not the other left an unscoped copy that kept demoting everything, so the fix looked applied and was not. It was found by making the census print each outcome's reason instead of only its status, after which three separate causes stopped looking identical. + - _Admission is scoped too._ `_blocking_degradations` already exempted declared exclusions; it now also ignores a gap another language partition raised, so a JavaScript build artefact cannot block a PHP finding. A gap naming no partition still reaches everything, and so does every gap when the claim's own language is unknown, because unknown scope is not evidence of independence. Evidence: `benchmarks/measurements/2026-09-21-partition-gap-blast-radius.json`. - _Boundary: contributor-scan query/context evidence, Ranker Scope Contract_ - _Depends: 13.3_ - _Requirements: 2.1, 2.2, 6.3, 8.1_ diff --git a/src/openultrasast/push/policy.py b/src/openultrasast/push/policy.py index 44892f7..13b2bd9 100644 --- a/src/openultrasast/push/policy.py +++ b/src/openultrasast/push/policy.py @@ -360,9 +360,28 @@ def _blocking_gaps(scan: ModelScanResult, *questions: QuestionIdentity) -> tuple ) -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 _blocking_degradations(scan: ModelScanResult, *, language: str = "") -> tuple[str, ...]: + """Degradations that undermine an answer. A declared exclusion is not one of them, and neither is a gap + another language partition raised. + + A partitioned scan records a gap per graph. A JavaScript file missing from the JavaScript graph cannot + make a PHP answer wrong, and treating it as though it could is what left a WordPress plugin -- PHP plus + its admin JavaScript, which is every plugin -- unable to admit a finding it had already established. + Measured 2026-09-21: four build artefacts against 4,425 PHP regions. + + Scope is read from the degradation's own partition. A gap that names none reaches everything, and so does + every gap when the claim's own language is unknown, because unknown scope is not evidence of independence. + """ + kept: list[str] = [] + for item in scan.degradations: + reason = str(item.get("reason")) + if reason in DECLARED_EXCLUSION_GAPS: + continue + partition = str(item.get("census_language") or "") + if partition and language and partition != language: + continue + kept.append(reason) + return tuple(kept) # A qualified callee: an identifier, optionally reached through `.`, `->` or `::`. Receiver @@ -425,7 +444,7 @@ def dependency_gaps( if scan is None: continue gaps.extend(gap for gap in (scan.scope.unresolved_boundaries if scan.scope else ()) if pertains(gap)) - gaps.extend(_blocking_degradations(scan)) + gaps.extend(_blocking_degradations(scan, language=op.question.language)) return tuple(dict.fromkeys(gaps)) @@ -611,7 +630,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 _blocking_degradations(base) + and not _blocking_degradations(base, language=op.question.language) and required <= selected and required <= answered and {q.identity for q in base.question_outcomes} <= selected @@ -632,7 +651,7 @@ def _compare_evidence( # 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 _blocking_gaps(head, op.question) if not gap.startswith(REACHABILITY_BOUNDED)] - or _blocking_degradations(head) + or _blocking_degradations(head, language=op.question.language) or not _valid_answer(head, op.question, head_ops) or op.question not in {q.identity for q in head.scope.selected} ): diff --git a/tests/test_push_policy.py b/tests/test_push_policy.py index 80b504f..ee93d13 100644 --- a/tests/test_push_policy.py +++ b/tests/test_push_policy.py @@ -600,3 +600,26 @@ def test_a_candidate_without_an_operation_reports_no_dependency_gaps(): candidate = replace(delta(scan(), scan(rows=inventory_rows(), findings=False), changed_line_context()), head_operation=None) assert dependency_gaps(candidate, context=changed_line_context(), head=scan()) == () + + +def test_a_gap_from_another_language_partition_does_not_block_admission(): + """A JavaScript file missing from the JavaScript graph cannot make a PHP answer wrong. + + A partitioned scan records a gap per graph, and treating one partition's gap as evidence against every + claim left a WordPress plugin -- PHP plus its admin JavaScript, which is every plugin -- unable to admit a + finding it had already established. Measured 2026-09-21: four build artefacts against 4,425 PHP regions. + """ + from openultrasast.push.policy import _blocking_degradations + + elsewhere = {"stage": "model", "reason": "partition_file_census_incomplete", "census_language": "javascript"} + here = {"stage": "model", "reason": "partition_file_census_incomplete", "census_language": "php"} + unplaced = {"stage": "model", "reason": "partition_file_census_incomplete"} + + def scan(*degradations): + return ModelScanResult(findings=(), degradations=tuple(degradations)) + + assert _blocking_degradations(scan(elsewhere), language="php") == () + assert _blocking_degradations(scan(here), language="php") == ("partition_file_census_incomplete",) + # Unknown scope is not evidence of independence, in either direction. + assert _blocking_degradations(scan(unplaced), language="php") == ("partition_file_census_incomplete",) + assert _blocking_degradations(scan(elsewhere), language="") == ("partition_file_census_incomplete",)