diffsniff-gatekeeper 0.1.0__tar.gz
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.
- diffsniff_gatekeeper-0.1.0/PKG-INFO +162 -0
- diffsniff_gatekeeper-0.1.0/README.md +148 -0
- diffsniff_gatekeeper-0.1.0/pyproject.toml +28 -0
- diffsniff_gatekeeper-0.1.0/setup.cfg +4 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff/analyzers/__init__.py +0 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff/analyzers/adversarial.py +122 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff/analyzers/base.py +20 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff/analyzers/ml_expert.py +127 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff/cli.py +107 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff/git_engine.py +66 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff_gatekeeper.egg-info/PKG-INFO +162 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff_gatekeeper.egg-info/SOURCES.txt +14 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff_gatekeeper.egg-info/dependency_links.txt +1 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff_gatekeeper.egg-info/entry_points.txt +2 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff_gatekeeper.egg-info/requires.txt +3 -0
- diffsniff_gatekeeper-0.1.0/src/diffsniff_gatekeeper.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diffsniff-gatekeeper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A local git gatekeeper that blocks AI-generated slop in PR descriptions.
|
|
5
|
+
Author-email: Kaushal <tiwarikaushal2012@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: litellm>=1.0.0
|
|
12
|
+
Requires-Dist: scikit-learn>=1.0.0
|
|
13
|
+
Requires-Dist: numpy>=1.20.0
|
|
14
|
+
|
|
15
|
+
# DiffSniff
|
|
16
|
+
|
|
17
|
+
**Stop AI-generated filler from sneaking into your Git history.**
|
|
18
|
+
|
|
19
|
+
DiffSniff is a local, terminal-based gatekeeper that cross-references your staged Git changes against your Pull Request description. If it detects that the description is generic AI-generated filler that doesn't accurately reflect the code you've written, it blocks the commit.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Why DiffSniff?
|
|
24
|
+
|
|
25
|
+
Traditional AI detectors focus on writing style, which makes them easy to bypass. You can simply ask a model to "write like a human."
|
|
26
|
+
|
|
27
|
+
DiffSniff takes a different approach.
|
|
28
|
+
|
|
29
|
+
Instead of analyzing how text is written, it verifies whether the claims in your PR description actually match the code changes in your diff.
|
|
30
|
+
|
|
31
|
+
### How It Works
|
|
32
|
+
|
|
33
|
+
#### Local Heuristics
|
|
34
|
+
|
|
35
|
+
DiffSniff performs lightweight local analysis to identify low-effort copy-paste descriptions by measuring:
|
|
36
|
+
|
|
37
|
+
* Vocabulary overlap
|
|
38
|
+
* Structural variance
|
|
39
|
+
* Content specificity
|
|
40
|
+
|
|
41
|
+
This catches obvious mismatches instantly without requiring an API call.
|
|
42
|
+
|
|
43
|
+
#### Adversarial Q&A
|
|
44
|
+
|
|
45
|
+
DiffSniff then:
|
|
46
|
+
|
|
47
|
+
1. Feeds your Git diff to an LLM.
|
|
48
|
+
2. Generates highly specific technical questions about the code changes.
|
|
49
|
+
3. Checks whether your PR description answers those questions.
|
|
50
|
+
|
|
51
|
+
If your PR claims one thing while the code does another, DiffSniff flags it.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
Install directly from PyPI:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install diffsniff-gatekeeper
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
### Bring Your Own Model
|
|
68
|
+
|
|
69
|
+
DiffSniff uses LiteLLM under the hood, allowing you to use virtually any supported provider, including:
|
|
70
|
+
|
|
71
|
+
* Gemini
|
|
72
|
+
* OpenAI
|
|
73
|
+
* Anthropic
|
|
74
|
+
* Local models
|
|
75
|
+
|
|
76
|
+
### Example: Gemini
|
|
77
|
+
|
|
78
|
+
Export your API key:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
export GEMINI_API_KEY="your-google-ai-key"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Optional Configuration
|
|
85
|
+
|
|
86
|
+
Create a `config.json` file in your working directory to customize behavior:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"model": "gemini/gemma-4-31b-it",
|
|
91
|
+
"slop_threshold": 55,
|
|
92
|
+
"num_questions": 3
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Configuration Options
|
|
97
|
+
|
|
98
|
+
| Option | Description |
|
|
99
|
+
| ---------------- | ------------------------------------------- |
|
|
100
|
+
| `model` | LLM used for adversarial questioning |
|
|
101
|
+
| `slop_threshold` | Minimum score required to pass validation |
|
|
102
|
+
| `num_questions` | Number of generated code-specific questions |
|
|
103
|
+
|
|
104
|
+
### Using OpenAI
|
|
105
|
+
|
|
106
|
+
To switch providers:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"model": "gpt-4o"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Then export your API key:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
export OPENAI_API_KEY="your-openai-key"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Usage
|
|
123
|
+
|
|
124
|
+
1. Stage your code changes:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
git add .
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
2. Write a draft PR description in a Markdown file.
|
|
131
|
+
|
|
132
|
+
3. Run DiffSniff:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
diffsniff pr_draft.md
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Results
|
|
141
|
+
|
|
142
|
+
### ✅ Pass
|
|
143
|
+
|
|
144
|
+
Your PR description accurately reflects the code changes and you're ready to push.
|
|
145
|
+
|
|
146
|
+
### ❌ Fail
|
|
147
|
+
|
|
148
|
+
Your description doesn't sufficiently explain what the code actually does.
|
|
149
|
+
|
|
150
|
+
Rewrite the documentation and try again.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Philosophy
|
|
155
|
+
|
|
156
|
+
DiffSniff doesn't care whether a human or an AI wrote your PR description.
|
|
157
|
+
|
|
158
|
+
It cares whether the description is *true*.
|
|
159
|
+
|
|
160
|
+
If your documentation accurately explains the code, it passes.
|
|
161
|
+
|
|
162
|
+
If it's generic filler disconnected from reality, it fails.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# DiffSniff
|
|
2
|
+
|
|
3
|
+
**Stop AI-generated filler from sneaking into your Git history.**
|
|
4
|
+
|
|
5
|
+
DiffSniff is a local, terminal-based gatekeeper that cross-references your staged Git changes against your Pull Request description. If it detects that the description is generic AI-generated filler that doesn't accurately reflect the code you've written, it blocks the commit.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why DiffSniff?
|
|
10
|
+
|
|
11
|
+
Traditional AI detectors focus on writing style, which makes them easy to bypass. You can simply ask a model to "write like a human."
|
|
12
|
+
|
|
13
|
+
DiffSniff takes a different approach.
|
|
14
|
+
|
|
15
|
+
Instead of analyzing how text is written, it verifies whether the claims in your PR description actually match the code changes in your diff.
|
|
16
|
+
|
|
17
|
+
### How It Works
|
|
18
|
+
|
|
19
|
+
#### Local Heuristics
|
|
20
|
+
|
|
21
|
+
DiffSniff performs lightweight local analysis to identify low-effort copy-paste descriptions by measuring:
|
|
22
|
+
|
|
23
|
+
* Vocabulary overlap
|
|
24
|
+
* Structural variance
|
|
25
|
+
* Content specificity
|
|
26
|
+
|
|
27
|
+
This catches obvious mismatches instantly without requiring an API call.
|
|
28
|
+
|
|
29
|
+
#### Adversarial Q&A
|
|
30
|
+
|
|
31
|
+
DiffSniff then:
|
|
32
|
+
|
|
33
|
+
1. Feeds your Git diff to an LLM.
|
|
34
|
+
2. Generates highly specific technical questions about the code changes.
|
|
35
|
+
3. Checks whether your PR description answers those questions.
|
|
36
|
+
|
|
37
|
+
If your PR claims one thing while the code does another, DiffSniff flags it.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
Install directly from PyPI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install diffsniff-gatekeeper
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
### Bring Your Own Model
|
|
54
|
+
|
|
55
|
+
DiffSniff uses LiteLLM under the hood, allowing you to use virtually any supported provider, including:
|
|
56
|
+
|
|
57
|
+
* Gemini
|
|
58
|
+
* OpenAI
|
|
59
|
+
* Anthropic
|
|
60
|
+
* Local models
|
|
61
|
+
|
|
62
|
+
### Example: Gemini
|
|
63
|
+
|
|
64
|
+
Export your API key:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export GEMINI_API_KEY="your-google-ai-key"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Optional Configuration
|
|
71
|
+
|
|
72
|
+
Create a `config.json` file in your working directory to customize behavior:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"model": "gemini/gemma-4-31b-it",
|
|
77
|
+
"slop_threshold": 55,
|
|
78
|
+
"num_questions": 3
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Configuration Options
|
|
83
|
+
|
|
84
|
+
| Option | Description |
|
|
85
|
+
| ---------------- | ------------------------------------------- |
|
|
86
|
+
| `model` | LLM used for adversarial questioning |
|
|
87
|
+
| `slop_threshold` | Minimum score required to pass validation |
|
|
88
|
+
| `num_questions` | Number of generated code-specific questions |
|
|
89
|
+
|
|
90
|
+
### Using OpenAI
|
|
91
|
+
|
|
92
|
+
To switch providers:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"model": "gpt-4o"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then export your API key:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
export OPENAI_API_KEY="your-openai-key"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Usage
|
|
109
|
+
|
|
110
|
+
1. Stage your code changes:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
git add .
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
2. Write a draft PR description in a Markdown file.
|
|
117
|
+
|
|
118
|
+
3. Run DiffSniff:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
diffsniff pr_draft.md
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Results
|
|
127
|
+
|
|
128
|
+
### ✅ Pass
|
|
129
|
+
|
|
130
|
+
Your PR description accurately reflects the code changes and you're ready to push.
|
|
131
|
+
|
|
132
|
+
### ❌ Fail
|
|
133
|
+
|
|
134
|
+
Your description doesn't sufficiently explain what the code actually does.
|
|
135
|
+
|
|
136
|
+
Rewrite the documentation and try again.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Philosophy
|
|
141
|
+
|
|
142
|
+
DiffSniff doesn't care whether a human or an AI wrote your PR description.
|
|
143
|
+
|
|
144
|
+
It cares whether the description is *true*.
|
|
145
|
+
|
|
146
|
+
If your documentation accurately explains the code, it passes.
|
|
147
|
+
|
|
148
|
+
If it's generic filler disconnected from reality, it fails.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "diffsniff-gatekeeper" # Change this to something unique if PyPI rejects it!
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Kaushal", email="tiwarikaushal2012@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
description = "A local git gatekeeper that blocks AI-generated slop in PR descriptions."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"litellm>=1.0.0",
|
|
21
|
+
"scikit-learn>=1.0.0",
|
|
22
|
+
"numpy>=1.20.0"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
# This line is the CLI magic. It maps the terminal command "diffsniff"
|
|
27
|
+
# to the main() function inside your src/diffsniff/cli.py file.
|
|
28
|
+
diffsniff = "diffsniff.cli:main"
|
|
File without changes
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import Dict, Any, Set
|
|
4
|
+
from litellm import completion
|
|
5
|
+
from diffsniff.analyzers.base import BaseAnalyzer
|
|
6
|
+
|
|
7
|
+
class AdversarialQAExpert(BaseAnalyzer):
|
|
8
|
+
"""
|
|
9
|
+
Layer 2: The LLM Interrogator.
|
|
10
|
+
Generates a configurable number of technical questions based ONLY on the code diff,
|
|
11
|
+
then checks if the developer's written prose actually answers them.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, default_num_questions: int = 3):
|
|
15
|
+
self.default_num_questions = default_num_questions
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return "Adversarial Q&A Engine"
|
|
20
|
+
|
|
21
|
+
def analyze(self, pr_text: str, code_tokens: Set[str], raw_diff: str = "") -> Dict[str, Any]:
|
|
22
|
+
# 1. Graceful Fallback: If no API key is found, skip the LLM check so the CLI works offline.
|
|
23
|
+
if not os.environ.get("GEMINI_API_KEY") and not os.environ.get("OPENROUTER_API_KEY") and not os.environ.get("OPENAI_API_KEY"):
|
|
24
|
+
return {
|
|
25
|
+
"score_penalty": 0,
|
|
26
|
+
"metrics": {"status": "Offline Mode. API Key missing. Skipping Adversarial layer."}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if not raw_diff.strip():
|
|
30
|
+
return {"score_penalty": 0, "metrics": {"status": "No diff provided to interrogate."}}
|
|
31
|
+
|
|
32
|
+
# 2. Load model & question count configurations from config if they exist
|
|
33
|
+
model = "gemini/gemma-4-31b-it" # Default
|
|
34
|
+
num_questions = self.default_num_questions
|
|
35
|
+
|
|
36
|
+
if os.path.exists("config.json"):
|
|
37
|
+
try:
|
|
38
|
+
with open("config.json", "r") as f:
|
|
39
|
+
config_data = json.load(f)
|
|
40
|
+
model = config_data.get("model", model)
|
|
41
|
+
num_questions = config_data.get("num_questions", num_questions)
|
|
42
|
+
except Exception:
|
|
43
|
+
pass # Fallback to default if json is malformed
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# PHASE 1: Code Interrogation (Diff -> Dynamic Questions)
|
|
47
|
+
q_prompt = (
|
|
48
|
+
f"[GIT DIFF]\n{raw_diff}\n\n"
|
|
49
|
+
f"Act as a strict Senior Software Engineer. Read the git diff above and generate EXACTLY {num_questions} "
|
|
50
|
+
f"highly specific technical questions a developer must be able to answer if they actually wrote this code. "
|
|
51
|
+
f"Do not include pleasantries, formatting, or intro text. Just output the {num_questions} questions on separate lines."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
q_response = completion(
|
|
55
|
+
model=model,
|
|
56
|
+
messages=[{"role": "user", "content": q_prompt}],
|
|
57
|
+
temperature=0.1
|
|
58
|
+
)
|
|
59
|
+
questions = q_response.choices[0].message.content
|
|
60
|
+
|
|
61
|
+
# PHASE 2: Cross-Examination (Questions + Prose -> Dynamic JSON Verdict)
|
|
62
|
+
eval_prompt = (
|
|
63
|
+
f"[QUESTIONS TO ANSWER]\n{questions}\n\n"
|
|
64
|
+
f"[PROPOSED PR DESCRIPTION]\n{pr_text}\n\n"
|
|
65
|
+
f"Determine if the PR Description factually answers each of the questions above based ONLY on the provided text. "
|
|
66
|
+
f"Also, identify any major claims in the description that are NOT verified by the questions/diff context. "
|
|
67
|
+
f"Output strictly in JSON format with this exact structure: \n"
|
|
68
|
+
'{\n'
|
|
69
|
+
' "assessments": [\n'
|
|
70
|
+
' { "question_number": 1, "answered": true/false },\n'
|
|
71
|
+
' { "question_number": 2, "answered": true/false }\n'
|
|
72
|
+
' ],\n'
|
|
73
|
+
' "unverified_claims": ["list", "fluff", "here"]\n'
|
|
74
|
+
'}'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
e_response = completion(
|
|
78
|
+
model=model,
|
|
79
|
+
messages=[{"role": "user", "content": eval_prompt}],
|
|
80
|
+
response_format={"type": "json_object"},
|
|
81
|
+
temperature=0.0
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
raw_content = e_response.choices[0].message.content
|
|
85
|
+
|
|
86
|
+
# Defensive parser: Extract strictly between first { and last }
|
|
87
|
+
start_idx = raw_content.find('{')
|
|
88
|
+
end_idx = raw_content.rfind('}')
|
|
89
|
+
|
|
90
|
+
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
|
|
91
|
+
json_str = raw_content[start_idx:end_idx + 1]
|
|
92
|
+
else:
|
|
93
|
+
json_str = raw_content
|
|
94
|
+
|
|
95
|
+
eval_data = json.loads(json_str)
|
|
96
|
+
|
|
97
|
+
# ⚖️ Dynamic Penalty Engine
|
|
98
|
+
assessments = eval_data.get("assessments", [])
|
|
99
|
+
answered_count = sum(1 for item in assessments if item.get("answered") is True)
|
|
100
|
+
|
|
101
|
+
# Avoid division by zero issues
|
|
102
|
+
effective_num_qs = len(assessments) if len(assessments) > 0 else num_questions
|
|
103
|
+
|
|
104
|
+
# Proportional scaling (questions account for up to 60 points of penalty)
|
|
105
|
+
penalty_per_unanswered = 60 / effective_num_qs
|
|
106
|
+
penalty = (effective_num_qs - answered_count) * penalty_per_unanswered
|
|
107
|
+
|
|
108
|
+
# Flat penalty for hallucinated claims (up to 40 points)
|
|
109
|
+
unverified = eval_data.get("unverified_claims", [])
|
|
110
|
+
if len(unverified) > 0:
|
|
111
|
+
penalty += 40
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"score_penalty": min(int(penalty), 100),
|
|
115
|
+
"metrics": {
|
|
116
|
+
"questions_generated": questions,
|
|
117
|
+
"evaluation_matrix": eval_data
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
return {"score_penalty": 0, "metrics": {"error": f"LLM Generation Failure: {str(e)}"}}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# base version for all analyzers. May change in the future.abs
|
|
2
|
+
from abc import ABC,abstractmethod
|
|
3
|
+
from typing import Dict,Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseAnalyzer(ABC):
|
|
7
|
+
|
|
8
|
+
@property
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def name(self)->str:
|
|
11
|
+
"""The name of the analyzer"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def analyze(self,pr_text:str,code_tokens:str,raw_diff:str=""):
|
|
16
|
+
"""
|
|
17
|
+
Takes the written text and the physical code changes,
|
|
18
|
+
and returns a dictionary of metrics.
|
|
19
|
+
"""
|
|
20
|
+
pass
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import re
|
|
3
|
+
from typing import Dict, Any, Set
|
|
4
|
+
from diffsniff.analyzers.base import BaseAnalyzer
|
|
5
|
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
6
|
+
from sklearn.metrics.pairwise import cosine_similarity
|
|
7
|
+
|
|
8
|
+
class SemanticsMLExpert(BaseAnalyzer):
|
|
9
|
+
"""
|
|
10
|
+
Layer 1: The ML & Statistical Expert.
|
|
11
|
+
Isolates, sanitizes, and normalizes code telemetry and description prose
|
|
12
|
+
to compute deterministic context alignment scores.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def name(self) -> str:
|
|
17
|
+
return "ML & Statistical Expert"
|
|
18
|
+
|
|
19
|
+
def clean_and_explode_tokens(self, token_set: Set[str]) -> str:
|
|
20
|
+
"""Splits structural camelCase and snake_case tokens into plain words."""
|
|
21
|
+
exploded_words = []
|
|
22
|
+
for token in token_set:
|
|
23
|
+
camel_split = re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', token)
|
|
24
|
+
clean_string = re.sub(r'[_.-]+', ' ', camel_split)
|
|
25
|
+
words = re.findall(r'\b[a-zA-Z]{3,}\b', clean_string.lower())
|
|
26
|
+
exploded_words.extend(words)
|
|
27
|
+
return " ".join(exploded_words)
|
|
28
|
+
|
|
29
|
+
def extract_diff_features(self, raw_diff: str) -> Dict[str, Any]:
|
|
30
|
+
"""Parses raw patch structural telemetry to track physical code changes."""
|
|
31
|
+
if not raw_diff.strip():
|
|
32
|
+
return {"lines_added": 0, "lines_removed": 0, "entropy_factor": 0.0, "structural_keywords_count": 0}
|
|
33
|
+
|
|
34
|
+
lines = raw_diff.splitlines()
|
|
35
|
+
added_lines = [l for l in lines if l.startswith('+') and not l.startswith('+++')]
|
|
36
|
+
removed_lines = [l for l in lines if l.startswith('-') and not l.startswith('---')]
|
|
37
|
+
|
|
38
|
+
structural_keywords = re.compile(r'\b(def|class|import|return|async|await|try|except|function|const|let)\b')
|
|
39
|
+
keyword_hits = sum(len(structural_keywords.findall(line)) for line in added_lines)
|
|
40
|
+
|
|
41
|
+
files_changed = len([l for l in lines if l.startswith('+++ b/')])
|
|
42
|
+
total_churn = len(added_lines) + len(removed_lines)
|
|
43
|
+
entropy = round(files_changed / total_churn, 4) if total_churn > 0 else 0.0
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"lines_added": len(added_lines),
|
|
47
|
+
"lines_removed": len(removed_lines),
|
|
48
|
+
"entropy_factor": entropy,
|
|
49
|
+
"structural_keywords_count": keyword_hits
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def sanitize_prose(self, text: str) -> str:
|
|
53
|
+
"""
|
|
54
|
+
CRITICAL CLOSING LOOP: Strips out markdown code blocks (```...```)
|
|
55
|
+
and raw inline backticks to prevent developers from spoofing token alignment.
|
|
56
|
+
"""
|
|
57
|
+
# Remove multiline code blocks completely
|
|
58
|
+
no_code_blocks = re.sub(r'```[\s\S]*?```', ' ', text)
|
|
59
|
+
# Remove inline backticks, symbols, and formatting structures
|
|
60
|
+
return re.sub(r'[*#`_\[\]\-]+', ' ', no_code_blocks.lower())
|
|
61
|
+
|
|
62
|
+
def compute_lexical_diversity(self, words: list) -> float:
|
|
63
|
+
"""Computes Type-Token Ratio (TTR) to measure vocabulary variation."""
|
|
64
|
+
if not words:
|
|
65
|
+
return 0.0
|
|
66
|
+
return round(len(set(words)) / len(words), 4)
|
|
67
|
+
|
|
68
|
+
def calculate_burstiness_variance(self, text: str) -> float:
|
|
69
|
+
"""Calculates standard deviation (sigma) of sentence lengths to trace sentence uniformity."""
|
|
70
|
+
sentences = [s.strip() for s in re.split(r'[.!?]+', text) if s.strip()]
|
|
71
|
+
if len(sentences) <= 1:
|
|
72
|
+
return 0.0
|
|
73
|
+
|
|
74
|
+
lengths = [len(re.findall(r'\b[a-zA-Z]+\b', s)) for s in sentences]
|
|
75
|
+
mean_length = sum(lengths) / len(lengths)
|
|
76
|
+
variance = sum((x - mean_length) ** 2 for x in lengths) / len(lengths)
|
|
77
|
+
return round(math.sqrt(variance), 4)
|
|
78
|
+
|
|
79
|
+
def analyze(self, pr_text: str, code_tokens: set, raw_diff: str = "") -> Dict[str, Any]:
|
|
80
|
+
"""Orchestrates layer evaluation parameters into a unified penalty score."""
|
|
81
|
+
# Sanitize prose text before extraction loops
|
|
82
|
+
clean_prose = self.sanitize_prose(pr_text)
|
|
83
|
+
words = re.findall(r'\b[a-zA-Z]+\b', clean_prose)
|
|
84
|
+
total_words = len(words)
|
|
85
|
+
|
|
86
|
+
diff_telemetry = self.extract_diff_features(raw_diff)
|
|
87
|
+
ttr_score = self.compute_lexical_diversity(words)
|
|
88
|
+
burstiness_sigma = self.calculate_burstiness_variance(clean_prose)
|
|
89
|
+
|
|
90
|
+
# Calculate TF-IDF Cosine Similarity on normalized text structures
|
|
91
|
+
normalized_code_string = self.clean_and_explode_tokens(code_tokens)
|
|
92
|
+
semantic_distance = 1.0
|
|
93
|
+
|
|
94
|
+
if normalized_code_string.strip() and clean_prose.strip():
|
|
95
|
+
try:
|
|
96
|
+
vectorizer = TfidfVectorizer(stop_words='english')
|
|
97
|
+
tfidf_matrix = vectorizer.fit_transform([normalized_code_string, clean_prose])
|
|
98
|
+
similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]
|
|
99
|
+
semantic_distance = round(float(1.0 - similarity), 4)
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
total_churn_lines = diff_telemetry["lines_added"] + diff_telemetry["lines_removed"]
|
|
104
|
+
volatility_ratio = round(total_words / total_churn_lines, 2) if total_churn_lines > 0 else float(total_words)
|
|
105
|
+
|
|
106
|
+
# ⚖️ Penalty Weight Matrix
|
|
107
|
+
base_penalty = semantic_distance * 60
|
|
108
|
+
|
|
109
|
+
if ttr_score < 0.40:
|
|
110
|
+
base_penalty += 15
|
|
111
|
+
if burstiness_sigma < 2.0:
|
|
112
|
+
base_penalty += 15
|
|
113
|
+
if volatility_ratio > 40.0 and diff_telemetry["structural_keywords_count"] == 0:
|
|
114
|
+
base_penalty += 20
|
|
115
|
+
|
|
116
|
+
final_ml_penalty = min(max(int(base_penalty), 0), 100)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"score_penalty": final_ml_penalty,
|
|
120
|
+
"metrics": {
|
|
121
|
+
"semantic_distance": semantic_distance,
|
|
122
|
+
"lexical_diversity_ttr": ttr_score,
|
|
123
|
+
"sentence_burstiness_sigma": burstiness_sigma,
|
|
124
|
+
"volatility_ratio": volatility_ratio,
|
|
125
|
+
"diff_telemetry": diff_telemetry
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from typing import Set
|
|
5
|
+
from diffsniff.git_engine import GitEngine
|
|
6
|
+
from diffsniff.analyzers.ml_expert import SemanticsMLExpert
|
|
7
|
+
from diffsniff.analyzers.adversarial import AdversarialQAExpert
|
|
8
|
+
|
|
9
|
+
def load_config() -> dict:
|
|
10
|
+
"""Loads operational thresholds and parameter overrides from config.json."""
|
|
11
|
+
default_config = {
|
|
12
|
+
"model": "gemini/gemma-4-31b-it",
|
|
13
|
+
"temperature": 0.0,
|
|
14
|
+
"slop_threshold": 55,
|
|
15
|
+
"num_questions": 3
|
|
16
|
+
}
|
|
17
|
+
if os.path.exists("config.json"):
|
|
18
|
+
try:
|
|
19
|
+
with open("config.json", "r") as f:
|
|
20
|
+
user_config = json.load(f)
|
|
21
|
+
default_config.update(user_config)
|
|
22
|
+
except Exception:
|
|
23
|
+
print("warning: config.json is malformed. Using default internal parameters.")
|
|
24
|
+
return default_config
|
|
25
|
+
|
|
26
|
+
def main():
|
|
27
|
+
if len(sys.argv) < 2:
|
|
28
|
+
print("Usage: diffsniff <path_to_pr_description.md>")
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
pr_file_path = sys.argv[1]
|
|
32
|
+
if not os.path.exists(pr_file_path):
|
|
33
|
+
print(f"error: Target file not found: {pr_file_path}")
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
with open(pr_file_path, 'r', encoding='utf-8') as f:
|
|
37
|
+
pr_text = f.read()
|
|
38
|
+
|
|
39
|
+
print("diffsniff: extracting local repository telemetry...")
|
|
40
|
+
|
|
41
|
+
# 1. Gather Telemetry
|
|
42
|
+
raw_diff = GitEngine.get_live_diff()
|
|
43
|
+
code_tokens = GitEngine.extract_code_tokens(raw_diff)
|
|
44
|
+
|
|
45
|
+
if not raw_diff.strip():
|
|
46
|
+
print("warning: clean working directory. parsing cached modifications only.")
|
|
47
|
+
|
|
48
|
+
# 2. Load configurations
|
|
49
|
+
config = load_config()
|
|
50
|
+
threshold = config.get("slop_threshold", 55)
|
|
51
|
+
|
|
52
|
+
# 3. Initialize Experts
|
|
53
|
+
ml_expert = SemanticsMLExpert()
|
|
54
|
+
adv_expert = AdversarialQAExpert(default_num_questions=config.get("num_questions", 3))
|
|
55
|
+
|
|
56
|
+
print("diffsniff: running lexical statistical matching...")
|
|
57
|
+
ml_results = ml_expert.analyze(pr_text, code_tokens, raw_diff)
|
|
58
|
+
ml_penalty = ml_results.get("score_penalty", 0)
|
|
59
|
+
|
|
60
|
+
print("diffsniff: verifying structural change alignment...")
|
|
61
|
+
adv_results = adv_expert.analyze(pr_text, code_tokens, raw_diff)
|
|
62
|
+
adv_penalty = adv_results.get("score_penalty", 0)
|
|
63
|
+
|
|
64
|
+
# 4. Layer 3: Executive Judge Decision (Weighted Integration)
|
|
65
|
+
api_key_active = any(os.environ.get(k) for k in ["GEMINI_API_KEY", "OPENROUTER_API_KEY", "OPENAI_API_KEY"])
|
|
66
|
+
if not api_key_active:
|
|
67
|
+
print("info: offline fallback active. evaluating strictly using local lexical heuristics.")
|
|
68
|
+
total_penalty = ml_penalty
|
|
69
|
+
else:
|
|
70
|
+
total_penalty = int((ml_penalty * 0.40) + (adv_penalty * 0.60))
|
|
71
|
+
|
|
72
|
+
# 5. Render Terminal Diagnostic Report
|
|
73
|
+
print("\n--- DiffSniff Analysis Summary ---")
|
|
74
|
+
print(f" Lexical Deviation: {ml_penalty}/100")
|
|
75
|
+
if api_key_active:
|
|
76
|
+
print(f" Context Mismatch: {adv_penalty}/100")
|
|
77
|
+
else:
|
|
78
|
+
print(f" Context Mismatch: [OFFLINE]")
|
|
79
|
+
print(f" Evaluated Score: {total_penalty}/100 (limit: {threshold})")
|
|
80
|
+
print("----------------------------------")
|
|
81
|
+
|
|
82
|
+
if total_penalty > threshold:
|
|
83
|
+
print("\nFAIL: PR description lacks sufficient correlation with actual codebase changes.")
|
|
84
|
+
|
|
85
|
+
eval_matrix = adv_results.get("metrics", {}).get("evaluation_matrix", {})
|
|
86
|
+
if eval_matrix:
|
|
87
|
+
unverified_claims = eval_matrix.get("unverified_claims", [])
|
|
88
|
+
if unverified_claims:
|
|
89
|
+
print("\nUnverified assertions detected in description:")
|
|
90
|
+
for claim in unverified_claims:
|
|
91
|
+
print(f" - {claim}")
|
|
92
|
+
|
|
93
|
+
qs = adv_results.get("metrics", {}).get("questions_generated", "")
|
|
94
|
+
if qs:
|
|
95
|
+
print("\nEnsure the description clearly addresses these technical points:")
|
|
96
|
+
for q in qs.strip().splitlines():
|
|
97
|
+
if q.strip():
|
|
98
|
+
print(f" * {q}")
|
|
99
|
+
|
|
100
|
+
print("\nAction: Revise the summary to reflect the physical code changes.")
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
else:
|
|
103
|
+
print("\nPASS: PR description verified successfully.")
|
|
104
|
+
sys.exit(0)
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
class GitEngine:
|
|
6
|
+
"""
|
|
7
|
+
Handles local shell execution to extract live telemetry from the git tree
|
|
8
|
+
without relying on external remote connections or pre-existing pushed commits.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def get_live_diff() -> str:
|
|
13
|
+
"""
|
|
14
|
+
Gathers raw text patch changes from the local staging area or active workspace.
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
# 1. Try to read staged changes first
|
|
18
|
+
diff_bytes = subprocess.check_output(
|
|
19
|
+
['git', 'diff', '--cached'],
|
|
20
|
+
stderr=subprocess.DEVNULL
|
|
21
|
+
)
|
|
22
|
+
diff_text = diff_bytes.decode('utf-8', errors='ignore')
|
|
23
|
+
|
|
24
|
+
# 2. Fall back to unstaged changes if staging is empty
|
|
25
|
+
if not diff_text.strip():
|
|
26
|
+
diff_bytes = subprocess.check_output(
|
|
27
|
+
['git', 'diff'],
|
|
28
|
+
stderr=subprocess.DEVNULL
|
|
29
|
+
)
|
|
30
|
+
diff_text = diff_bytes.decode('utf-8', errors='ignore')
|
|
31
|
+
|
|
32
|
+
return diff_text
|
|
33
|
+
|
|
34
|
+
except subprocess.CalledProcessError:
|
|
35
|
+
print("❌ Telemetry Error: This command must be executed within a valid Git repository.")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
except FileNotFoundError:
|
|
38
|
+
print("❌ Dependency Error: 'git' CLI execution binary was not found on your system PATH.")
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def extract_code_tokens(raw_diff: str) -> set:
|
|
43
|
+
"""
|
|
44
|
+
Scans added lines inside the diff to isolate explicit programming identifiers
|
|
45
|
+
(filenames, function properties, variable references).
|
|
46
|
+
"""
|
|
47
|
+
tokens = set()
|
|
48
|
+
if not raw_diff.strip():
|
|
49
|
+
return tokens
|
|
50
|
+
|
|
51
|
+
for line in raw_diff.splitlines():
|
|
52
|
+
# Target explicit file renaming/addition lines
|
|
53
|
+
if line.startswith('+++ b/'):
|
|
54
|
+
filename = line.split('/')[-1].strip()
|
|
55
|
+
tokens.add(filename.lower())
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Target literal additions, stripping out lines that are purely deletions
|
|
59
|
+
if line.startswith('+') and not line.startswith('+++'):
|
|
60
|
+
# Extract alphanumerics starting with letters/underscores
|
|
61
|
+
found = re.findall(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', line)
|
|
62
|
+
for item in found:
|
|
63
|
+
if len(item) > 3: # Drop noisy shorthands (i, j, x, db, ok)
|
|
64
|
+
tokens.add(item.lower())
|
|
65
|
+
|
|
66
|
+
return tokens
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diffsniff-gatekeeper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A local git gatekeeper that blocks AI-generated slop in PR descriptions.
|
|
5
|
+
Author-email: Kaushal <tiwarikaushal2012@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: litellm>=1.0.0
|
|
12
|
+
Requires-Dist: scikit-learn>=1.0.0
|
|
13
|
+
Requires-Dist: numpy>=1.20.0
|
|
14
|
+
|
|
15
|
+
# DiffSniff
|
|
16
|
+
|
|
17
|
+
**Stop AI-generated filler from sneaking into your Git history.**
|
|
18
|
+
|
|
19
|
+
DiffSniff is a local, terminal-based gatekeeper that cross-references your staged Git changes against your Pull Request description. If it detects that the description is generic AI-generated filler that doesn't accurately reflect the code you've written, it blocks the commit.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Why DiffSniff?
|
|
24
|
+
|
|
25
|
+
Traditional AI detectors focus on writing style, which makes them easy to bypass. You can simply ask a model to "write like a human."
|
|
26
|
+
|
|
27
|
+
DiffSniff takes a different approach.
|
|
28
|
+
|
|
29
|
+
Instead of analyzing how text is written, it verifies whether the claims in your PR description actually match the code changes in your diff.
|
|
30
|
+
|
|
31
|
+
### How It Works
|
|
32
|
+
|
|
33
|
+
#### Local Heuristics
|
|
34
|
+
|
|
35
|
+
DiffSniff performs lightweight local analysis to identify low-effort copy-paste descriptions by measuring:
|
|
36
|
+
|
|
37
|
+
* Vocabulary overlap
|
|
38
|
+
* Structural variance
|
|
39
|
+
* Content specificity
|
|
40
|
+
|
|
41
|
+
This catches obvious mismatches instantly without requiring an API call.
|
|
42
|
+
|
|
43
|
+
#### Adversarial Q&A
|
|
44
|
+
|
|
45
|
+
DiffSniff then:
|
|
46
|
+
|
|
47
|
+
1. Feeds your Git diff to an LLM.
|
|
48
|
+
2. Generates highly specific technical questions about the code changes.
|
|
49
|
+
3. Checks whether your PR description answers those questions.
|
|
50
|
+
|
|
51
|
+
If your PR claims one thing while the code does another, DiffSniff flags it.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
Install directly from PyPI:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install diffsniff-gatekeeper
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
### Bring Your Own Model
|
|
68
|
+
|
|
69
|
+
DiffSniff uses LiteLLM under the hood, allowing you to use virtually any supported provider, including:
|
|
70
|
+
|
|
71
|
+
* Gemini
|
|
72
|
+
* OpenAI
|
|
73
|
+
* Anthropic
|
|
74
|
+
* Local models
|
|
75
|
+
|
|
76
|
+
### Example: Gemini
|
|
77
|
+
|
|
78
|
+
Export your API key:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
export GEMINI_API_KEY="your-google-ai-key"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Optional Configuration
|
|
85
|
+
|
|
86
|
+
Create a `config.json` file in your working directory to customize behavior:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"model": "gemini/gemma-4-31b-it",
|
|
91
|
+
"slop_threshold": 55,
|
|
92
|
+
"num_questions": 3
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Configuration Options
|
|
97
|
+
|
|
98
|
+
| Option | Description |
|
|
99
|
+
| ---------------- | ------------------------------------------- |
|
|
100
|
+
| `model` | LLM used for adversarial questioning |
|
|
101
|
+
| `slop_threshold` | Minimum score required to pass validation |
|
|
102
|
+
| `num_questions` | Number of generated code-specific questions |
|
|
103
|
+
|
|
104
|
+
### Using OpenAI
|
|
105
|
+
|
|
106
|
+
To switch providers:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"model": "gpt-4o"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Then export your API key:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
export OPENAI_API_KEY="your-openai-key"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Usage
|
|
123
|
+
|
|
124
|
+
1. Stage your code changes:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
git add .
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
2. Write a draft PR description in a Markdown file.
|
|
131
|
+
|
|
132
|
+
3. Run DiffSniff:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
diffsniff pr_draft.md
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Results
|
|
141
|
+
|
|
142
|
+
### ✅ Pass
|
|
143
|
+
|
|
144
|
+
Your PR description accurately reflects the code changes and you're ready to push.
|
|
145
|
+
|
|
146
|
+
### ❌ Fail
|
|
147
|
+
|
|
148
|
+
Your description doesn't sufficiently explain what the code actually does.
|
|
149
|
+
|
|
150
|
+
Rewrite the documentation and try again.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Philosophy
|
|
155
|
+
|
|
156
|
+
DiffSniff doesn't care whether a human or an AI wrote your PR description.
|
|
157
|
+
|
|
158
|
+
It cares whether the description is *true*.
|
|
159
|
+
|
|
160
|
+
If your documentation accurately explains the code, it passes.
|
|
161
|
+
|
|
162
|
+
If it's generic filler disconnected from reality, it fails.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/diffsniff/cli.py
|
|
4
|
+
src/diffsniff/git_engine.py
|
|
5
|
+
src/diffsniff/analyzers/__init__.py
|
|
6
|
+
src/diffsniff/analyzers/adversarial.py
|
|
7
|
+
src/diffsniff/analyzers/base.py
|
|
8
|
+
src/diffsniff/analyzers/ml_expert.py
|
|
9
|
+
src/diffsniff_gatekeeper.egg-info/PKG-INFO
|
|
10
|
+
src/diffsniff_gatekeeper.egg-info/SOURCES.txt
|
|
11
|
+
src/diffsniff_gatekeeper.egg-info/dependency_links.txt
|
|
12
|
+
src/diffsniff_gatekeeper.egg-info/entry_points.txt
|
|
13
|
+
src/diffsniff_gatekeeper.egg-info/requires.txt
|
|
14
|
+
src/diffsniff_gatekeeper.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
diffsniff
|