mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 12:09:27 +00:00
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:
co-authored by
Claude Opus 5
parent
c93669c34f
commit
29cef32bac
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user