diff --git a/diffgraph/structural.py b/diffgraph/structural.py index 5c00fa3..63fba74 100644 --- a/diffgraph/structural.py +++ b/diffgraph/structural.py @@ -1,8 +1,8 @@ """Deterministic, local DiffGraph v2 extraction from exact Git snapshots. -This first baseline intentionally supports Python only. Other languages remain in -``files`` and produce scoped ``UNSUPPORTED_LANGUAGE`` warnings; no capability is -inferred from a filename beyond that explicit boundary. +This first baseline intentionally supports Python source and stub files only. +Other languages remain in ``files`` and produce scoped ``UNSUPPORTED_LANGUAGE`` +warnings; no capability is inferred from a filename beyond that explicit boundary. """ from __future__ import annotations @@ -34,6 +34,7 @@ ANALYZER = "diffgraph-python-tree-sitter" QUERY_VERSION = "python-structure-v2" +PYTHON_SUFFIXES = frozenset({".py", ".pyi"}) _PARSER_STATE = threading.local() # Resolver failures are part of the public artifact contract. Keep their @@ -744,7 +745,7 @@ def _unsupported_language_warning(entry: SnapshotEntry, path: str) -> Dict[str, return _warning( "UNSUPPORTED_LANGUAGE", path, - "Deterministic extraction currently supports Python (.py) only; " + "Deterministic extraction currently supports Python (.py and .pyi) only; " "pre-change blob {}; post-change blob {}.".format( entry.old_oid or "absent", entry.new_oid or "absent", ), @@ -947,7 +948,7 @@ def analyze_local_diff( "python" if not binary_sides and not non_regular_modes - and Path(path).suffix.lower() == ".py" + and Path(path).suffix.lower() in PYTHON_SUFFIXES else None ), "change_kind": _change_kind(entry.status, entry.old_oid, entry.new_oid), diff --git a/tests/test_structural.py b/tests/test_structural.py index 3e5490b..8b577d4 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -180,6 +180,26 @@ def test_pathspec_scope_is_not_widened(tmp_path): assert [item["path"] for item in artifact["files"]] == ["inside/a.py"] +def test_python_stub_files_are_analyzed_with_tree_sitter_provenance(tmp_path): + """Typed Python stubs are part of the deterministic Python baseline.""" + root = repo(tmp_path) + write(root, "api.pyi", "def load(value: str) -> int: ...\n") + commit(root) + write(root, "api.pyi", "def load(value: str, retries: int = 0) -> int: ...\n") + + artifact = analyze_local_diff(str(root)) + + assert_valid(artifact) + assert artifact["files"][0]["language"] == "python" + assert artifact["metadata"]["files_analyzed"] == 1 + assert artifact["metadata"]["warnings"] == [] + symbol = artifact["symbols"][0] + assert (symbol["id"], symbol["name"], symbol["change_kind"]) == ( + "sym::api.pyi::load", "load", "modified" + ) + assert "parser=tree-sitter-language-pack@" in symbol["evidence"][0]["detail"] + + def test_unsupported_language_is_explicit_and_not_overclaimed(tmp_path): """Unsupported snapshots keep their explicit capability boundary and blobs.""" root = repo(tmp_path) @@ -194,7 +214,7 @@ def test_unsupported_language_is_explicit_and_not_overclaimed(tmp_path): assert artifact["metadata"]["files_skipped"] == 1 assert artifact["metadata"]["warnings"][0]["code"] == "UNSUPPORTED_LANGUAGE" assert artifact["metadata"]["warnings"][0]["detail"] == ( - "Deterministic extraction currently supports Python (.py) only; " + "Deterministic extraction currently supports Python (.py and .pyi) only; " "pre-change blob {}; post-change blob {}." ).format(old_oid, new_oid) @@ -218,7 +238,7 @@ def test_unsupported_language_one_sided_blob_is_absent(tmp_path, change_kind): artifact = analyze_local_diff(str(root)) assert_valid(artifact) assert artifact["metadata"]["warnings"][0]["detail"] == ( - "Deterministic extraction currently supports Python (.py) only; " + "Deterministic extraction currently supports Python (.py and .pyi) only; " "pre-change blob {}; post-change blob {}." ).format(old_oid, new_oid)