diff --git a/diffgraph/schema/diffgraph-v2.schema.json b/diffgraph/schema/diffgraph-v2.schema.json index 7c243ca..1802327 100644 --- a/diffgraph/schema/diffgraph-v2.schema.json +++ b/diffgraph/schema/diffgraph-v2.schema.json @@ -395,6 +395,11 @@ "UNKNOWN", "not_a_git_repository", "git_diff_failed", + "git_untracked_failed", + "invalid_base_ref", + "invalid_head_ref", + "merge_base_failed", + "malformed_merge_base", "malformed_git_output", "undecodable_path", "missing_object_id", @@ -402,7 +407,10 @@ "worktree_read_failed", "hash_object_failed", "malformed_hash_object_output", - "unmerged_index_entry" + "unmerged_index_entry", + "pathspec_outside_repository", + "gitlink_head_failed", + "malformed_gitlink_head" ], "description": "Machine-readable warning code. Consumers can surface these to the user." }, diff --git a/diffgraph/structural.py b/diffgraph/structural.py index b7bdfbb..a07d6a1 100644 --- a/diffgraph/structural.py +++ b/diffgraph/structural.py @@ -36,6 +36,31 @@ QUERY_VERSION = "python-structure-v2" _PARSER_STATE = threading.local() +# Resolver failures are part of the public artifact contract. Keep their +# machine-readable identity instead of collapsing an actionable Git failure +# into UNKNOWN, so clients can offer the right remediation without parsing +# the human-oriented detail string. +RESOLUTION_WARNING_CODES = frozenset({ + "not_a_git_repository", + "git_diff_failed", + "git_untracked_failed", + "invalid_base_ref", + "invalid_head_ref", + "merge_base_failed", + "malformed_merge_base", + "malformed_git_output", + "undecodable_path", + "missing_object_id", + "unsupported_worktree_entry", + "worktree_read_failed", + "hash_object_failed", + "malformed_hash_object_output", + "unmerged_index_entry", + "pathspec_outside_repository", + "gitlink_head_failed", + "malformed_gitlink_head", +}) + class StructuralDependencyError(ImportError): """A required local structural parser dependency is unavailable.""" @@ -370,19 +395,7 @@ def _warning(code: str, path: Optional[str], detail: str) -> Dict[str, str]: def _resolution_warning(item: ResolutionWarning) -> Dict[str, str]: - known_codes = { - "not_a_git_repository", - "git_diff_failed", - "malformed_git_output", - "undecodable_path", - "missing_object_id", - "unsupported_worktree_entry", - "worktree_read_failed", - "hash_object_failed", - "malformed_hash_object_output", - "unmerged_index_entry", - } - code = item.code if item.code in known_codes else "UNKNOWN" + code = item.code if item.code in RESOLUTION_WARNING_CODES else "UNKNOWN" return _warning(code, item.path, "{}: {}".format(item.code, item.message)) diff --git a/tests/test_structural.py b/tests/test_structural.py index 5516f09..d88f4ab 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -778,7 +778,7 @@ def test_cli_structural_json_rejects_four_dot_range(tmp_path, monkeypatch): assert "explicit non-empty BASE..HEAD or BASE...HEAD refs" in result.output -def test_cli_structural_json_preserves_invalid_ref_warning(tmp_path, monkeypatch): +def test_cli_structural_json_preserves_invalid_ref_warning_code(tmp_path, monkeypatch): from click.testing import CliRunner from diffgraph.cli import main @@ -795,10 +795,35 @@ def test_cli_structural_json_preserves_invalid_ref_warning(tmp_path, monkeypatch artifact = json.loads(result.output) assert artifact["files"] == [] warning = artifact["metadata"]["warnings"][0] - assert warning["code"] == "UNKNOWN" + assert warning["code"] == "invalid_base_ref" assert warning["detail"].startswith("invalid_base_ref:") +def test_cli_structural_json_preserves_scoped_pathspec_warning_code(tmp_path, monkeypatch): + """Pathspec failures remain actionable structured artifact warnings.""" + from click.testing import CliRunner + from diffgraph.cli import main + + root = repo(tmp_path) + write(root, "app.py", "def value():\n return 1\n") + commit(root) + monkeypatch.chdir(root) + + outside = tmp_path / "outside.py" + result = CliRunner().invoke( + main, ["--structural-json", "-", "diff", "--", str(outside)] + ) + + assert result.exit_code == 0, result.output + artifact = json.loads(result.output) + assert artifact["files"] == [] + assert artifact["metadata"]["warnings"] == [{ + "code": "pathspec_outside_repository", + "file": str(outside), + "detail": "pathspec_outside_repository: Absolute pathspec is outside the repository and was not resolved", + }] + + def test_cli_structural_json_requires_separator_before_pathspecs(tmp_path, monkeypatch): from click.testing import CliRunner from diffgraph.cli import main