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.
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ diffsniff = diffsniff.cli:main
@@ -0,0 +1,3 @@
1
+ litellm>=1.0.0
2
+ scikit-learn>=1.0.0
3
+ numpy>=1.20.0