fix(scan): scope a partition census gap to its own partition, and write the rule down

A missing JavaScript file cannot make a PHP answer wrong. A census gap now carries the partition it
was raised for, and reaches only that partition questions. On the measured plugin four JavaScript
files -- a webpack build output, a minified select2, a build config and one source file -- had taken
the admissibility of 4,425 PHP regions with them.

Two controls exercise the real boundaries rather than injecting a reason, and both were confirmed to
FAIL with the old classification restored, so they guard what they claim to.

And the rule is now in the design, because this was the fifth instance found and fixed one at a time.
Two invariants, eleven instances between them:

  A degradation is owned by the narrowest scope that caused it. Vendor exclusion as a global veto,
  line ambiguity inherited by every question, an empty scope blocking a sibling comparison, a language
  boundary demoting every answer, a census gap demoting every answer. The fourth and fifth cost the
  declared v0.1 target its entire admissibility, and neither passing subject could reveal it because
  both hold one language.

  A token matches the thing it names, not text that contains it. gets inside fgets, resolve inside
  resolveUrl, user inside users, a sink word inside a comment, a dotted setting on its last segment,
  a flow query reading the receiver.

Each carries a review test: a new degradation must state the scope it reaches and justify demoting
that far, and a text match must be bounded AND confined to the part of the node that names the callee.

1,419 tests pass, eighteen skipped.

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-21 13:21:27 +02:00
co-authored by Claude Opus 5
parent c93669c34f
commit 29cef32bac
5 changed files with 123 additions and 6 deletions
+40 -1
View File
@@ -233,7 +233,7 @@ def test_an_unresolved_context_projection_still_demotes_the_question(tmp_path):
assert result.question_outcomes[0].reason == "change_context_incomplete"
def scan_with_layout(tmp_path, *, vendor=False, symlink=False):
def scan_with_layout(tmp_path, *, vendor=False, symlink=False, second_language=False):
"""Exercise the real partition boundaries rather than injecting a reason."""
(tmp_path / "source.php").write_text("readable\n")
if vendor:
@@ -241,6 +241,8 @@ def scan_with_layout(tmp_path, *, vendor=False, symlink=False):
(tmp_path / "vendor" / "lib.php").write_text("third party\n")
if symlink:
(tmp_path / "linked.php").symlink_to(tmp_path / "source.php")
if second_language:
(tmp_path / "admin.js").write_text("const x = 1;\n")
rows = vector() + [row("source.php"), {"kind": "context_summary"}]
return scan_repository(
tmp_path,
@@ -347,3 +349,40 @@ def test_a_question_naming_a_function_the_file_does_not_define_says_so(tmp_path)
# The gap is owned by the question that asked it, so it cannot block another question.
assert gaps[0].endswith(outcome.identity.question_id)
assert not any("context_scope_empty" in g for g in result.scope.unresolved_boundaries)
def test_a_second_language_does_not_invalidate_the_first_language_answer(tmp_path):
"""A repository holding two languages must still be able to complete a question.
Measured 2026-09-21 on a WordPress plugin: PHP with 36 regions of admin JavaScript completed NONE of 520
questions while establishing 43 findings, because a partition boundary was classified as a graph-integrity
failure and any such gap demotes every answer in the repository. Admission requires a completed outcome,
so every multi-language repository was unadmittable by construction -- which is every plugin this product
was built for.
A flow this tool cannot follow out of one language can hide a finding and never invent one, so the
boundary is a declared limit. It stays reported; it no longer demotes.
"""
result = scan_with_layout(tmp_path, second_language=True)
assert "cross_partition_semantics_unresolved" in result.scope.unresolved_boundaries, result.scope.unresolved_boundaries
assert result.question_outcomes[0].status == "completed", result.question_outcomes[0]
def test_a_gap_in_another_partition_does_not_demote_this_one(tmp_path):
"""An integrity gap reaches the questions it can be wrong about, which is its own partition's.
On that same plugin four JavaScript files -- a webpack build output, a minified select2, a build config and
one source file -- were absent from the JavaScript graph, and a repository-wide demotion took the
admissibility of 4,425 PHP regions with them. A missing JavaScript file cannot make a PHP answer wrong.
"""
from openultrasast.model.scan import GRAPH_INTEGRITY_GAPS, _scan_repository_impl # noqa: F401
result = scan_with_layout(tmp_path, second_language=True)
scoped = [d for d in result.degradations if d.get("census_language")]
for gap in scoped:
assert gap["reason"] in GRAPH_INTEGRITY_GAPS, gap
# Whatever the fake graph reports, a PHP question may only be demoted by a gap from the PHP partition.
php = [o for o in result.question_outcomes if o.identity.language == "php"]
assert php, result.question_outcomes
if all(str(d.get("census_language") or "") != "php" for d in result.degradations):
assert all(o.status == "completed" for o in php), [(o.status, o.reason) for o in php]