diffguard 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,184 @@
1
+ """TypeScript/JavaScript language support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tree_sitter
6
+ import tree_sitter_javascript
7
+ import tree_sitter_typescript
8
+
9
+ from diffguard.engine._types import Symbol, compute_body_hash
10
+ from diffguard.languages._utils import node_text
11
+
12
+ # Default to TypeScript grammar (superset of JS)
13
+ _ts_lang = tree_sitter.Language(tree_sitter_typescript.language_typescript())
14
+ _js_lang = tree_sitter.Language(tree_sitter_javascript.language())
15
+
16
+
17
+ def get_language() -> tree_sitter.Language:
18
+ """Return the tree-sitter Language object for TypeScript."""
19
+ return _ts_lang
20
+
21
+
22
+ def get_js_language() -> tree_sitter.Language:
23
+ """Return the tree-sitter Language object for JavaScript."""
24
+ return _js_lang
25
+
26
+
27
+ def extract_symbols(tree: tree_sitter.Tree, source: bytes) -> list[Symbol]:
28
+ """Extract symbols from a parsed JavaScript/TypeScript tree."""
29
+ symbols: list[Symbol] = []
30
+ _walk_node(tree.root_node, source, symbols, parent_class=None)
31
+ return symbols
32
+
33
+
34
+ def _walk_node(
35
+ node: tree_sitter.Node,
36
+ source: bytes,
37
+ symbols: list[Symbol],
38
+ parent_class: str | None,
39
+ ) -> None:
40
+ """Recursively walk the tree and extract symbols."""
41
+ for child in node.children:
42
+ if child.type == "function_declaration":
43
+ _extract_function(child, symbols)
44
+ elif child.type == "class_declaration":
45
+ _extract_class(child, source, symbols)
46
+ elif child.type in ("lexical_declaration", "variable_declaration"):
47
+ _extract_arrow_functions(child, symbols)
48
+ elif child.type == "export_statement":
49
+ _walk_node(child, source, symbols, parent_class)
50
+
51
+
52
+ def _extract_function(
53
+ node: tree_sitter.Node,
54
+ symbols: list[Symbol],
55
+ ) -> None:
56
+ """Extract a function declaration."""
57
+ name_node = node.child_by_field_name("name")
58
+ if name_node is None:
59
+ return
60
+ name = node_text(name_node)
61
+ params_node = node.child_by_field_name("parameters")
62
+ params = node_text(params_node) if params_node else "()"
63
+ signature = f"function {name}{params}"
64
+ body_node = node.child_by_field_name("body")
65
+ body_text = node_text(body_node) if body_node else ""
66
+
67
+ symbols.append(
68
+ Symbol(
69
+ name=name,
70
+ kind="function",
71
+ signature=signature,
72
+ start_line=node.start_point.row + 1,
73
+ end_line=node.end_point.row + 1,
74
+ body_hash=compute_body_hash(body_text),
75
+ )
76
+ )
77
+
78
+
79
+ def _extract_class(
80
+ node: tree_sitter.Node,
81
+ source: bytes,
82
+ symbols: list[Symbol],
83
+ ) -> None:
84
+ """Extract a class declaration and its methods."""
85
+ name_node = node.child_by_field_name("name")
86
+ if name_node is None:
87
+ return
88
+ class_name = node_text(name_node)
89
+ body_node = node.child_by_field_name("body")
90
+ body_text = node_text(body_node) if body_node else ""
91
+
92
+ heritage = ""
93
+ for child in node.children:
94
+ if child.type == "class_heritage":
95
+ heritage = f" {node_text(child)}"
96
+ break
97
+
98
+ symbols.append(
99
+ Symbol(
100
+ name=class_name,
101
+ kind="class",
102
+ signature=f"class {class_name}{heritage}",
103
+ start_line=node.start_point.row + 1,
104
+ end_line=node.end_point.row + 1,
105
+ body_hash=compute_body_hash(body_text),
106
+ )
107
+ )
108
+
109
+ if body_node:
110
+ for child in body_node.children:
111
+ if child.type == "method_definition":
112
+ _extract_method(child, symbols, class_name)
113
+
114
+
115
+ def _extract_method(
116
+ node: tree_sitter.Node,
117
+ symbols: list[Symbol],
118
+ class_name: str,
119
+ ) -> None:
120
+ """Extract a method definition."""
121
+ name_node = node.child_by_field_name("name")
122
+ if name_node is None:
123
+ return
124
+ name = node_text(name_node)
125
+ params_node = node.child_by_field_name("parameters")
126
+ params = node_text(params_node) if params_node else "()"
127
+ signature = f"{name}{params}"
128
+ body_node = node.child_by_field_name("body")
129
+ body_text = node_text(body_node) if body_node else ""
130
+
131
+ symbols.append(
132
+ Symbol(
133
+ name=name,
134
+ kind="method",
135
+ signature=signature,
136
+ start_line=node.start_point.row + 1,
137
+ end_line=node.end_point.row + 1,
138
+ body_hash=compute_body_hash(body_text),
139
+ parent=class_name,
140
+ )
141
+ )
142
+
143
+
144
+ def _extract_arrow_functions(
145
+ node: tree_sitter.Node,
146
+ symbols: list[Symbol],
147
+ ) -> None:
148
+ """Extract arrow functions assigned to variables."""
149
+ # Detect keyword (const/let/var)
150
+ keyword = "const"
151
+ for sib in node.children:
152
+ if not sib.is_named:
153
+ t = node_text(sib)
154
+ if t in ("const", "let", "var"):
155
+ keyword = t
156
+ break
157
+
158
+ for child in node.children:
159
+ if child.type == "variable_declarator":
160
+ name_node = child.child_by_field_name("name")
161
+ value_node = child.child_by_field_name("value")
162
+ if name_node and value_node and value_node.type == "arrow_function":
163
+ name = node_text(name_node)
164
+ params_node = value_node.child_by_field_name("parameters")
165
+ if params_node:
166
+ params = node_text(params_node)
167
+ else:
168
+ param_node = value_node.child_by_field_name("parameter")
169
+ params = f"({node_text(param_node)})" if param_node else "()"
170
+
171
+ signature = f"{keyword} {name} = {params} =>"
172
+ body_node = value_node.child_by_field_name("body")
173
+ body_text = node_text(body_node) if body_node else ""
174
+
175
+ symbols.append(
176
+ Symbol(
177
+ name=name,
178
+ kind="function",
179
+ signature=signature,
180
+ start_line=node.start_point.row + 1,
181
+ end_line=node.end_point.row + 1,
182
+ body_hash=compute_body_hash(body_text),
183
+ )
184
+ )
@@ -0,0 +1 @@
1
+ ; TypeScript tree-sitter queries — placeholder
diffguard/schema.py ADDED
@@ -0,0 +1,97 @@
1
+ """DiffGuard output schema — Pydantic v2 models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Literal
7
+
8
+ from pydantic import BaseModel
9
+
10
+
11
+ class DiffStats(BaseModel):
12
+ """Diff statistics."""
13
+
14
+ files: int
15
+ additions: int
16
+ deletions: int
17
+
18
+
19
+ class Meta(BaseModel):
20
+ """Run metadata."""
21
+
22
+ ref_range: str
23
+ stats: DiffStats
24
+ warnings: list[str] = []
25
+ timing_ms: float | None = None
26
+
27
+
28
+ class SymbolChange(BaseModel):
29
+ """A single symbol-level change."""
30
+
31
+ kind: Literal[
32
+ "function_added",
33
+ "function_removed",
34
+ "function_modified",
35
+ "class_added",
36
+ "class_removed",
37
+ "class_modified",
38
+ "signature_changed",
39
+ "moved",
40
+ ]
41
+ name: str
42
+ signature: str | None = None
43
+ before_signature: str | None = None
44
+ after_signature: str | None = None
45
+ file_from: str | None = None
46
+ line: int | None = None
47
+ breaking: bool = False
48
+ detail: dict[str, Any] | None = None
49
+
50
+
51
+ class FileChange(BaseModel):
52
+ """A changed file with its symbol-level changes."""
53
+
54
+ path: str
55
+ language: str | None = None
56
+ change_type: Literal["added", "removed", "modified", "renamed"]
57
+ generated: bool = False
58
+ binary: bool = False
59
+ parse_error: bool = False
60
+ unsupported_language: bool = False
61
+ changes: list[SymbolChange] = []
62
+
63
+
64
+ class Summary(BaseModel):
65
+ """Aggregate summary of changes.
66
+
67
+ Migration note (v1.0 → v1.1): Added ``focus`` field — a short list of
68
+ the most important items for reviewer agents. Existing consumers that
69
+ ignore unknown fields are unaffected.
70
+ """
71
+
72
+ change_types: dict[str, int] = {}
73
+ breaking_changes: list[SymbolChange] = []
74
+ focus: list[str] = []
75
+
76
+
77
+ class TieredSummary(BaseModel):
78
+ """Multi-tier human-readable summary."""
79
+
80
+ oneliner: str = ""
81
+ short: str = ""
82
+ detailed: str = ""
83
+
84
+
85
+ class DiffGuardOutput(BaseModel):
86
+ """Top-level DiffGuard output."""
87
+
88
+ schema_version: str = "1.1"
89
+ meta: Meta
90
+ files: list[FileChange] = []
91
+ summary: Summary = Summary()
92
+ tiered: TieredSummary = TieredSummary()
93
+
94
+
95
+ def export_json_schema() -> str:
96
+ """Export the JSON schema as a string."""
97
+ return json.dumps(DiffGuardOutput.model_json_schema(), indent=2)
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffguard
3
+ Version: 0.1.3
4
+ Summary: Catches the structural breaks that pass code review
5
+ Project-URL: Homepage, https://github.com/ostehost/diffguard
6
+ Project-URL: Repository, https://github.com/ostehost/diffguard
7
+ Project-URL: Issues, https://github.com/ostehost/diffguard/issues
8
+ Author: ostehost
9
+ License: BSL-1.1
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,code-intelligence,code-review,git,tree-sitter
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: Other/Proprietary License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Classifier: Topic :: Software Development :: Version Control :: Git
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: click>=8.0
24
+ Requires-Dist: pydantic>=2.0
25
+ Requires-Dist: rich>=13.0
26
+ Requires-Dist: tree-sitter-go
27
+ Requires-Dist: tree-sitter-javascript
28
+ Requires-Dist: tree-sitter-python
29
+ Requires-Dist: tree-sitter-typescript>=0.23.2
30
+ Requires-Dist: tree-sitter>=0.24
31
+ Description-Content-Type: text/markdown
32
+
33
+ [![PyPI](https://img.shields.io/pypi/v/diffguard)](https://pypi.org/project/diffguard/)
34
+ [![License](https://img.shields.io/badge/license-BSL%201.1-blue)](LICENSE)
35
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://pypi.org/project/diffguard/)
36
+
37
+ # DiffGuard
38
+
39
+ **Catches the structural breaks that pass code review.**
40
+
41
+ ## A real bug, in one line
42
+
43
+ This diff shipped in Flask ([PR #5898](https://github.com/pallets/flask/pull/5898)):
44
+
45
+ ```diff
46
+ -def redirect(location, code=302, ...):
47
+ +def redirect(location, code=303, ...):
48
+ ```
49
+
50
+ One line. Looks fine. A reviewer approves it.
51
+
52
+ **The real impact:** 7 endpoints silently change HTTP behavior. POST-to-POST redirects become POST-to-GET. No errors. No warnings. Just broken APIs in production.
53
+
54
+ **DiffGuard catches it:**
55
+
56
+ ```
57
+ $ diffguard review eca5fd1d~1..eca5fd1d
58
+
59
+ ⚠ DiffGuard: 2 changes need review
60
+
61
+ DEFAULT VALUE CHANGED: redirect(location, code=302) → redirect(location, code=303)
62
+ src/flask/helpers.py:241 — 7 callers rely on the default
63
+
64
+ DEFAULT VALUE CHANGED: App.redirect(self, location, code=302) → App.redirect(self, location, code=303)
65
+ src/flask/sansio/app.py:935 — 7 callers rely on the default
66
+ ```
67
+
68
+ Tree-sitter AST analysis. No LLM. No network calls. Runs in seconds.
69
+
70
+ ## What it catches
71
+
72
+ Function signature changes, removed/renamed symbols, default value changes — and shows you every caller affected.
73
+
74
+ ## What it doesn't catch
75
+
76
+ Logic bugs, behavioral changes beyond signatures, performance issues, security vulnerabilities. DiffGuard detects **structural breaks**, not all bugs.
77
+
78
+ When there's nothing structural to report, it stays silent (exit code 0, no output).
79
+
80
+ ## Quick Start
81
+
82
+ ```bash
83
+ pip install diffguard
84
+ diffguard review main..feature
85
+ ```
86
+
87
+ Exit codes: `0` = nothing noteworthy, `1` = findings, `2` = error.
88
+
89
+ ## How It Works
90
+
91
+ 1. **Parses the diff** using tree-sitter AST analysis (not regex)
92
+ 2. **Extracts symbols** — functions, classes, signatures
93
+ 3. **Detects high-signal changes** — signature changes, removed symbols, default value changes
94
+ 4. **Scans for callers** — finds every file that references changed symbols
95
+ 5. **Outputs actionable context** — or stays silent if nothing matters
96
+
97
+ ## Agent Integration
98
+
99
+ Works with **Claude Code**, **Cursor**, **GitHub Actions**, or any agent that can run a CLI command.
100
+
101
+ Add one line to your agent config — DiffGuard is silent when nothing matters.
102
+
103
+ See the full [Agent Integration Guide](docs/agent-integration.md) for hooks, CI patterns, and snippets for [Claude Code](docs/claude-md-snippet.md) and [Cursor](docs/cursor-rule-snippet.md).
104
+
105
+ ## GitHub Action
106
+
107
+ ```yaml
108
+ # .github/workflows/diffguard.yml
109
+ name: DiffGuard PR Review
110
+ on:
111
+ pull_request:
112
+ types: [opened, synchronize, reopened]
113
+ permissions:
114
+ contents: read
115
+ pull-requests: write
116
+ jobs:
117
+ diffguard:
118
+ runs-on: ubuntu-latest
119
+ steps:
120
+ - uses: actions/checkout@v4
121
+ with:
122
+ fetch-depth: 0
123
+ - uses: ostehost/diffguard@main
124
+ ```
125
+
126
+ ## Languages
127
+
128
+ - **Python** (most mature — extensive real-world validation)
129
+ - TypeScript / JavaScript
130
+ - Go
131
+ - More planned (Rust, Java, C#)
132
+
133
+ ## Philosophy
134
+
135
+ 1. **Silence is a feature.** No findings? No output. Most diffs don't need structural analysis.
136
+ 2. **Local-first.** Your code never leaves your machine. No SaaS, no API keys, no accounts.
137
+ 3. **Agent-native.** CLI + JSON output. `pip install` and go.
138
+ 4. **Precision over recall.** We'd rather miss a minor issue than cry wolf on every PR.
139
+
140
+ ## License
141
+
142
+ BSL 1.1 — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,26 @@
1
+ diffguard/__init__.py,sha256=_vw6s8kAEZEWKaqdJTNGBUPL6gW1ywfRcUIcq-hbtYs,87
2
+ diffguard/cli.py,sha256=GqmApvmyNXnR1lxuVH4CoXnkg7U7P7MdMPJWzDCQbYk,23389
3
+ diffguard/git.py,sha256=b8SvW0jDh91NHslqbIVsGaGeM7jW3Ew9jjqVQ2laBP4,9786
4
+ diffguard/schema.py,sha256=aQPtehSqeA0UjKnuBhiNbSOZ8Pjv67QI9lyZ-xx8NYs,2258
5
+ diffguard/engine/__init__.py,sha256=8KkxYzAC3ZMTBiP5ysv1LlPXW4XMNf5GJ6K7aTyXmlc,33
6
+ diffguard/engine/_types.py,sha256=LgzAH20Z7a4mRezb_DodPBcD5ldnS4wDSRrd4NiGwo4,990
7
+ diffguard/engine/classifier.py,sha256=OXjvXnn3x5O99DPAg9nXFQ2LPkR4hQNP0gHl0_HoBOY,2637
8
+ diffguard/engine/deps.py,sha256=6rZiwm2MdicefKbveD2-2fIq2A7DtkU2r5M9dvuTjOY,6214
9
+ diffguard/engine/matcher.py,sha256=iYhLGLRGEt0qTejuWQB-AWKU5Y_2Fg9J7kFjZHmKbJ4,4220
10
+ diffguard/engine/parser.py,sha256=hzSNAI1c4rCboPLLszGF_Dil1EygFAXdE5PpDcB_B4c,1338
11
+ diffguard/engine/pipeline.py,sha256=-hhOyP7YyUoxfuCZWhoOc-zkoxJLSnKtRONvpv540gM,6508
12
+ diffguard/engine/signatures.py,sha256=cybi_p1pmWW_UOergRmRLM32xKOBc3GPLdOjHJcHcAU,8545
13
+ diffguard/engine/summarizer.py,sha256=ffkotlB_XwLP_YcREL5CI88L9j4etvXRT9_WwyQcZHs,13387
14
+ diffguard/languages/__init__.py,sha256=1vjO-694V0j6rBI5erxr5G2ctMl03_WpAC-xSsjTLy0,1485
15
+ diffguard/languages/_utils.py,sha256=XVStFVZpdcSVLgOxvpti9dT548SSNF8M_DralHO9Bts,294
16
+ diffguard/languages/go/__init__.py,sha256=kJOi4WWgrVSejQHegPBkHzTRrPuhjItfeIzO0u-r4B4,3466
17
+ diffguard/languages/go/queries.scm,sha256=5mEellIGMjVR7qA4Xt9LlpNEok7G1nfpxPg0vhcW2iQ,41
18
+ diffguard/languages/python/__init__.py,sha256=93M37cZwFjmGLdeXozVVtzBysRTuYTT77LrsZd3jSEo,6480
19
+ diffguard/languages/python/queries.scm,sha256=bv8GYn2bpL6lXW3OcVOEow-nZE3bdX1s3N2QYmyn21s,45
20
+ diffguard/languages/typescript/__init__.py,sha256=bkYxwkcxCCgRsdrUQ5zFjCbU_y4Lm6GFRZN74gHd1bc,6001
21
+ diffguard/languages/typescript/queries.scm,sha256=dJ6ZUwxpwDfDlj7e6irC5ivILomaocttuvAEcf5Nqz8,49
22
+ diffguard-0.1.3.dist-info/METADATA,sha256=OY8KSTYzKCB4MbPrqsDJqtgXNiyEqnYgw3DZ4teYtU4,4783
23
+ diffguard-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
+ diffguard-0.1.3.dist-info/entry_points.txt,sha256=iWYD-Yd6-jOpBE08rQv8I1SQsfCJ3BNWp4jh4sSPy84,49
25
+ diffguard-0.1.3.dist-info/licenses/LICENSE,sha256=lOYMmmcw26-zTcgA3ptu-f653iWyb9qO2LRPgu0R9gg,2508
26
+ diffguard-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ diffguard = diffguard.cli:main
@@ -0,0 +1,53 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: ostehost
6
+ Licensed Work: DiffGuard v0.1.1 (and all subsequent versions)
7
+ Additional Use Grant: You may use the Licensed Work for any purpose other
8
+ than offering the functionality of the Licensed Work
9
+ to third parties as a managed or hosted service.
10
+ Change Date: 2029-02-11
11
+ Change License: Apache License 2.0
12
+
13
+ Terms
14
+
15
+ The Licensor hereby grants you the right to copy, modify, create derivative
16
+ works, redistribute, and make non-production use of the Licensed Work. The
17
+ Licensor may make an Additional Use Grant, above, permitting limited
18
+ production use.
19
+
20
+ Effective on the Change Date, or the fourth anniversary of the first publicly
21
+ available distribution of a specific version of the Licensed Work under this
22
+ License, whichever comes first, the Licensor hereby grants you rights under
23
+ the terms of the Change License, and the rights granted in the paragraph
24
+ above terminate.
25
+
26
+ If your use of the Licensed Work does not comply with the requirements
27
+ currently in effect as described in this License, you must purchase a
28
+ commercial license from the Licensor, its affiliated entities, or authorized
29
+ resellers, or you must refrain from using the Licensed Work.
30
+
31
+ All copies of the original and modified Licensed Work, and derivative works
32
+ of the Licensed Work, are subject to this License. This License applies
33
+ separately for each version of the Licensed Work and the Change Date may vary
34
+ for each version of the Licensed Work released by Licensor.
35
+
36
+ You must conspicuously display this License on each original or modified copy
37
+ of the Licensed Work. If you receive the Licensed Work in original or
38
+ modified form from a third party, the terms and conditions set forth in this
39
+ License apply to your use of that work.
40
+
41
+ Any use of the Licensed Work in violation of this License will automatically
42
+ terminate your rights under this License for the current and all other
43
+ versions of the Licensed Work.
44
+
45
+ This License does not grant you any right in any trademark or logo of
46
+ Licensor or its affiliates (provided that you may use a trademark or logo of
47
+ Licensor as expressly required by this License).
48
+
49
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
50
+ AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
51
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
52
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
53
+ TITLE.