fix(scan): symlink, unsupported-language and unparsed-file gaps are owned by what they name

This extends dbcb230 to the remaining graph-integrity gaps. In the independent
evaluation these gaps were repository-wide:
- alerta: one symlink, `alerta/app.wsgi`, which is not even source, made 261 of 261
  questions unresolved.
- wger and Signal K: a language no frontend reads did the same.
- YesWiki: unparsed files did the same.

Now:
- the partition build records the paths a symlink boundary skipped (a directory
  ends in `/`) and the languages no frontend reads;
- `files_unparsed` carries its full paths;
- the scan demotes only questions about an owned path or under an owned directory,
  and only questions in an owned language.
Every gap stays in the report as a boundary. A gap that names nothing keeps its
partition-wide or repository-wide reach. Tier-0 pruning stays off, as before.

The tests that pinned repository-wide reach now assert ownership. The one about a
vendor exclusion masking an integrity failure keeps its intent, using a census gap
that names no file.

Retune after protocol-v1; exploratory only.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-25 14:30:36 +02:00
co-authored by Claude Opus 5.5
parent 949977c7a6
commit 65dcb46b5e
4 changed files with 96 additions and 31 deletions
+24 -4
View File
@@ -192,11 +192,18 @@ def test_missing_graph_files_disable_safe_pruning_and_completion(tmp_path):
cpg.unparsed = ("missing.py",)
return cpg
result = scan_repository(source(tmp_path, "a.py"), [region("a.py")], backend=Partial({"a.py": vector(False)}), ranking_mode="evidence")
assert len(result.scope.selected) == 1
result = scan_repository(
source(tmp_path, "a.py", "missing.py"),
[region("a.py"), region("missing.py")],
backend=Partial({"a.py": vector(False), "missing.py": vector(False)}),
ranking_mode="evidence",
)
# Pruning stays off: the unparsed file may hold the very sink a vector did not see.
assert len(result.scope.selected) == 2
assert result.scope.deferred == ()
assert result.question_outcomes[0].status == "unresolved"
assert result.family_coverage[0].completed == 0
# Completion is owned by the file: the unparsed one is unresolved, the one the graph read is answered.
status = {o.identity.path: o.status for o in result.question_outcomes}
assert status == {"a.py": "completed", "missing.py": "unresolved"}, status
def test_cli_sample_uses_deferred_order_not_scanned_count(tmp_path):
@@ -433,3 +440,16 @@ def test_a_census_gap_that_names_no_file_still_reaches_its_partition(tmp_path):
regions = [region("app.py", "python"), region("other.py", "python")]
result = scan_repository(root, regions, backend=_CensusGapBackend(None, evidence=evidence), population_complete=True)
assert all(o.status == "unresolved" for o in result.question_outcomes), [(o.status, o.reason) for o in result.question_outcomes]
def test_a_language_no_frontend_reads_demotes_only_its_own_questions(tmp_path):
"""wger's Python questions all went unresolved because the repository also holds files of a language no
frontend reads. That gap is about THAT language; it is reported, and it names the language."""
root = source(tmp_path, "app.py")
(tmp_path / "tools").mkdir()
(tmp_path / "tools" / "main.go").write_text("package main\n")
result = scan_repository(root, [region("app.py")], backend=Backend({"app.py": vector()}), population_complete=True)
gap = [d for d in result.degradations if d.get("reason") == "frontend_unsupported"]
assert gap and gap[0].get("census_language") == "go", result.degradations
assert "frontend_unsupported" in result.scope.unresolved_boundaries
assert result.question_outcomes[0].status == "completed", result.question_outcomes