promptdiff-ai 1.0.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.
- promptdiff_ai-1.0.0/.gitignore +43 -0
- promptdiff_ai-1.0.0/PKG-INFO +183 -0
- promptdiff_ai-1.0.0/README.md +153 -0
- promptdiff_ai-1.0.0/pyproject.toml +63 -0
- promptdiff_ai-1.0.0/src/promptdiff/__init__.py +52 -0
- promptdiff_ai-1.0.0/src/promptdiff/analyzer.py +163 -0
- promptdiff_ai-1.0.0/src/promptdiff/cli.py +123 -0
- promptdiff_ai-1.0.0/src/promptdiff/differ.py +393 -0
- promptdiff_ai-1.0.0/src/promptdiff/models.py +135 -0
- promptdiff_ai-1.0.0/src/promptdiff/reporter.py +266 -0
- promptdiff_ai-1.0.0/tests/conftest.py +185 -0
- promptdiff_ai-1.0.0/tests/test_analyzer.py +246 -0
- promptdiff_ai-1.0.0/tests/test_cli.py +157 -0
- promptdiff_ai-1.0.0/tests/test_differ.py +284 -0
- promptdiff_ai-1.0.0/tests/test_models.py +250 -0
- promptdiff_ai-1.0.0/tests/test_reporter.py +180 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg-info/
|
|
7
|
+
*.egg
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.eggs/
|
|
11
|
+
*.whl
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
ENV/
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.swp
|
|
22
|
+
*.swo
|
|
23
|
+
*~
|
|
24
|
+
|
|
25
|
+
# Testing
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
.coverage
|
|
28
|
+
htmlcov/
|
|
29
|
+
.mypy_cache/
|
|
30
|
+
|
|
31
|
+
# OS
|
|
32
|
+
.DS_Store
|
|
33
|
+
Thumbs.db
|
|
34
|
+
|
|
35
|
+
# Secrets - NEVER commit these
|
|
36
|
+
.env
|
|
37
|
+
.env.*
|
|
38
|
+
secrets/
|
|
39
|
+
*.key
|
|
40
|
+
*.pem
|
|
41
|
+
|
|
42
|
+
# Cache
|
|
43
|
+
.prompttools-cache/
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: promptdiff-ai
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Semantic diff for LLM prompt changes
|
|
5
|
+
Project-URL: Homepage, https://github.com/scottconverse/promptdiff
|
|
6
|
+
Project-URL: Repository, https://github.com/scottconverse/promptdiff
|
|
7
|
+
Project-URL: Issues, https://github.com/scottconverse/promptdiff/issues
|
|
8
|
+
Author: Scott Converse
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: ai,breaking-changes,diff,llm,prompt,semantic
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: prompttools-core-ai<2.0,>=1.0
|
|
22
|
+
Requires-Dist: rich>=13.0
|
|
23
|
+
Requires-Dist: typer[all]>=0.12
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# promptdiff
|
|
32
|
+
|
|
33
|
+
Semantic diff for LLM prompt changes. Part of the [prompttools](https://github.com/scottconverse/prompttools) suite.
|
|
34
|
+
|
|
35
|
+
Unlike generic text diffing, promptdiff understands prompt structure: messages, variables, metadata, and token counts. It classifies changes as breaking or non-breaking and outputs structured reports suitable for CI/CD pipelines and GitHub PR comments.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install promptdiff-ai
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Compare two prompt files
|
|
47
|
+
promptdiff old_prompt.yaml new_prompt.yaml
|
|
48
|
+
|
|
49
|
+
# JSON output for CI pipelines
|
|
50
|
+
promptdiff old.yaml new.yaml --format json
|
|
51
|
+
|
|
52
|
+
# Markdown output for GitHub PR comments
|
|
53
|
+
promptdiff old.yaml new.yaml --format markdown
|
|
54
|
+
|
|
55
|
+
# Exit with code 1 if breaking changes are found (CI gate)
|
|
56
|
+
promptdiff old.yaml new.yaml --exit-on-breaking
|
|
57
|
+
|
|
58
|
+
# Show per-message token breakdowns
|
|
59
|
+
promptdiff old.yaml new.yaml --token-detail
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## What It Detects
|
|
63
|
+
|
|
64
|
+
### Message-Level Changes
|
|
65
|
+
- Added, removed, or modified messages
|
|
66
|
+
- Per-message token deltas
|
|
67
|
+
- Unified content diffs within modified messages
|
|
68
|
+
|
|
69
|
+
### Variable Changes
|
|
70
|
+
- New variables (with or without defaults)
|
|
71
|
+
- Removed variables
|
|
72
|
+
- Modified default values
|
|
73
|
+
|
|
74
|
+
### Metadata Changes
|
|
75
|
+
- Model changes
|
|
76
|
+
- Added/removed/modified metadata keys
|
|
77
|
+
|
|
78
|
+
### Token Deltas
|
|
79
|
+
- Total token count comparison
|
|
80
|
+
- Percentage change
|
|
81
|
+
- Per-message breakdowns (with `--token-detail`)
|
|
82
|
+
|
|
83
|
+
## Breaking Change Classification
|
|
84
|
+
|
|
85
|
+
### Breaking (High Severity)
|
|
86
|
+
- **New required variable** -- a variable added without a default value; existing callers will fail
|
|
87
|
+
- **Removed variable** -- callers referencing this variable will break
|
|
88
|
+
- **Removed message** -- changes the prompt structure
|
|
89
|
+
|
|
90
|
+
### Breaking (Medium Severity)
|
|
91
|
+
- **Model change** -- may affect behavior, pricing, and capabilities
|
|
92
|
+
- **Role ordering change** -- may affect model behavior
|
|
93
|
+
|
|
94
|
+
### Non-Breaking
|
|
95
|
+
- Added variable with a default value
|
|
96
|
+
- Added messages (extends the prompt)
|
|
97
|
+
- Content modifications within existing messages
|
|
98
|
+
- Metadata changes (except model)
|
|
99
|
+
|
|
100
|
+
## Output Formats
|
|
101
|
+
|
|
102
|
+
### Text (default)
|
|
103
|
+
Rich terminal output with color-coded diffs:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Prompt Diff: new_prompt.yaml
|
|
107
|
+
old: a1b2c3d4e5f6 new: f6e5d4c3b2a1
|
|
108
|
+
|
|
109
|
+
BREAKING CHANGES (2):
|
|
110
|
+
HIGH [variable] Variable 'tone' was removed
|
|
111
|
+
MEDIUM [model] Model changed from 'gpt-4' to 'gpt-4o'
|
|
112
|
+
|
|
113
|
+
Token Delta:
|
|
114
|
+
150 -> 165 (+15, +10.0%)
|
|
115
|
+
|
|
116
|
+
Messages (1 changed):
|
|
117
|
+
~ system
|
|
118
|
+
System message content modified
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### JSON
|
|
122
|
+
Structured JSON for programmatic consumption:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
promptdiff old.yaml new.yaml --format json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Markdown
|
|
129
|
+
GitHub-flavored Markdown for PR comments:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
promptdiff old.yaml new.yaml --format markdown
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Python API
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from promptdiff import diff_files, format_text, format_json
|
|
139
|
+
|
|
140
|
+
# Compare two files
|
|
141
|
+
result = diff_files("prompts/v1.yaml", "prompts/v2.yaml")
|
|
142
|
+
|
|
143
|
+
# Check for breaking changes
|
|
144
|
+
if result.is_breaking:
|
|
145
|
+
for bc in result.breaking_changes:
|
|
146
|
+
print(f"[{bc.severity}] {bc.description}")
|
|
147
|
+
|
|
148
|
+
# Get token delta
|
|
149
|
+
print(f"Tokens: {result.token_delta.old_total} -> {result.token_delta.new_total}")
|
|
150
|
+
|
|
151
|
+
# Format output
|
|
152
|
+
print(format_text(result))
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## CLI Reference
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
Usage: promptdiff [OPTIONS] FILE_A FILE_B
|
|
159
|
+
|
|
160
|
+
Arguments:
|
|
161
|
+
FILE_A Path to the old prompt file
|
|
162
|
+
FILE_B Path to the new prompt file
|
|
163
|
+
|
|
164
|
+
Options:
|
|
165
|
+
-f, --format [text|json|markdown] Output format (default: text)
|
|
166
|
+
--exit-on-breaking Exit with code 1 if breaking changes found
|
|
167
|
+
--token-detail Show per-message token breakdowns
|
|
168
|
+
-e, --encoding TEXT tiktoken encoding (default: cl100k_base)
|
|
169
|
+
-V, --version Show version and exit
|
|
170
|
+
--help Show this message and exit
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Supported File Formats
|
|
174
|
+
|
|
175
|
+
All formats supported by prompttools-core:
|
|
176
|
+
- YAML (`.yaml`, `.yml`)
|
|
177
|
+
- JSON (`.json`)
|
|
178
|
+
- Markdown (`.md`)
|
|
179
|
+
- Text (`.txt`)
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# promptdiff
|
|
2
|
+
|
|
3
|
+
Semantic diff for LLM prompt changes. Part of the [prompttools](https://github.com/scottconverse/prompttools) suite.
|
|
4
|
+
|
|
5
|
+
Unlike generic text diffing, promptdiff understands prompt structure: messages, variables, metadata, and token counts. It classifies changes as breaking or non-breaking and outputs structured reports suitable for CI/CD pipelines and GitHub PR comments.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install promptdiff-ai
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Compare two prompt files
|
|
17
|
+
promptdiff old_prompt.yaml new_prompt.yaml
|
|
18
|
+
|
|
19
|
+
# JSON output for CI pipelines
|
|
20
|
+
promptdiff old.yaml new.yaml --format json
|
|
21
|
+
|
|
22
|
+
# Markdown output for GitHub PR comments
|
|
23
|
+
promptdiff old.yaml new.yaml --format markdown
|
|
24
|
+
|
|
25
|
+
# Exit with code 1 if breaking changes are found (CI gate)
|
|
26
|
+
promptdiff old.yaml new.yaml --exit-on-breaking
|
|
27
|
+
|
|
28
|
+
# Show per-message token breakdowns
|
|
29
|
+
promptdiff old.yaml new.yaml --token-detail
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## What It Detects
|
|
33
|
+
|
|
34
|
+
### Message-Level Changes
|
|
35
|
+
- Added, removed, or modified messages
|
|
36
|
+
- Per-message token deltas
|
|
37
|
+
- Unified content diffs within modified messages
|
|
38
|
+
|
|
39
|
+
### Variable Changes
|
|
40
|
+
- New variables (with or without defaults)
|
|
41
|
+
- Removed variables
|
|
42
|
+
- Modified default values
|
|
43
|
+
|
|
44
|
+
### Metadata Changes
|
|
45
|
+
- Model changes
|
|
46
|
+
- Added/removed/modified metadata keys
|
|
47
|
+
|
|
48
|
+
### Token Deltas
|
|
49
|
+
- Total token count comparison
|
|
50
|
+
- Percentage change
|
|
51
|
+
- Per-message breakdowns (with `--token-detail`)
|
|
52
|
+
|
|
53
|
+
## Breaking Change Classification
|
|
54
|
+
|
|
55
|
+
### Breaking (High Severity)
|
|
56
|
+
- **New required variable** -- a variable added without a default value; existing callers will fail
|
|
57
|
+
- **Removed variable** -- callers referencing this variable will break
|
|
58
|
+
- **Removed message** -- changes the prompt structure
|
|
59
|
+
|
|
60
|
+
### Breaking (Medium Severity)
|
|
61
|
+
- **Model change** -- may affect behavior, pricing, and capabilities
|
|
62
|
+
- **Role ordering change** -- may affect model behavior
|
|
63
|
+
|
|
64
|
+
### Non-Breaking
|
|
65
|
+
- Added variable with a default value
|
|
66
|
+
- Added messages (extends the prompt)
|
|
67
|
+
- Content modifications within existing messages
|
|
68
|
+
- Metadata changes (except model)
|
|
69
|
+
|
|
70
|
+
## Output Formats
|
|
71
|
+
|
|
72
|
+
### Text (default)
|
|
73
|
+
Rich terminal output with color-coded diffs:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Prompt Diff: new_prompt.yaml
|
|
77
|
+
old: a1b2c3d4e5f6 new: f6e5d4c3b2a1
|
|
78
|
+
|
|
79
|
+
BREAKING CHANGES (2):
|
|
80
|
+
HIGH [variable] Variable 'tone' was removed
|
|
81
|
+
MEDIUM [model] Model changed from 'gpt-4' to 'gpt-4o'
|
|
82
|
+
|
|
83
|
+
Token Delta:
|
|
84
|
+
150 -> 165 (+15, +10.0%)
|
|
85
|
+
|
|
86
|
+
Messages (1 changed):
|
|
87
|
+
~ system
|
|
88
|
+
System message content modified
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### JSON
|
|
92
|
+
Structured JSON for programmatic consumption:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
promptdiff old.yaml new.yaml --format json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Markdown
|
|
99
|
+
GitHub-flavored Markdown for PR comments:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
promptdiff old.yaml new.yaml --format markdown
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Python API
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from promptdiff import diff_files, format_text, format_json
|
|
109
|
+
|
|
110
|
+
# Compare two files
|
|
111
|
+
result = diff_files("prompts/v1.yaml", "prompts/v2.yaml")
|
|
112
|
+
|
|
113
|
+
# Check for breaking changes
|
|
114
|
+
if result.is_breaking:
|
|
115
|
+
for bc in result.breaking_changes:
|
|
116
|
+
print(f"[{bc.severity}] {bc.description}")
|
|
117
|
+
|
|
118
|
+
# Get token delta
|
|
119
|
+
print(f"Tokens: {result.token_delta.old_total} -> {result.token_delta.new_total}")
|
|
120
|
+
|
|
121
|
+
# Format output
|
|
122
|
+
print(format_text(result))
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## CLI Reference
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
Usage: promptdiff [OPTIONS] FILE_A FILE_B
|
|
129
|
+
|
|
130
|
+
Arguments:
|
|
131
|
+
FILE_A Path to the old prompt file
|
|
132
|
+
FILE_B Path to the new prompt file
|
|
133
|
+
|
|
134
|
+
Options:
|
|
135
|
+
-f, --format [text|json|markdown] Output format (default: text)
|
|
136
|
+
--exit-on-breaking Exit with code 1 if breaking changes found
|
|
137
|
+
--token-detail Show per-message token breakdowns
|
|
138
|
+
-e, --encoding TEXT tiktoken encoding (default: cl100k_base)
|
|
139
|
+
-V, --version Show version and exit
|
|
140
|
+
--help Show this message and exit
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Supported File Formats
|
|
144
|
+
|
|
145
|
+
All formats supported by prompttools-core:
|
|
146
|
+
- YAML (`.yaml`, `.yml`)
|
|
147
|
+
- JSON (`.json`)
|
|
148
|
+
- Markdown (`.md`)
|
|
149
|
+
- Text (`.txt`)
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "promptdiff-ai"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Semantic diff for LLM prompt changes"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Scott Converse"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["llm", "prompt", "diff", "semantic", "ai", "breaking-changes"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"prompttools-core-ai>=1.0,<2.0",
|
|
29
|
+
"typer[all]>=0.12",
|
|
30
|
+
"rich>=13.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
promptdiff = "promptdiff.cli:app"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/scottconverse/promptdiff"
|
|
38
|
+
Repository = "https://github.com/scottconverse/promptdiff"
|
|
39
|
+
Issues = "https://github.com/scottconverse/promptdiff/issues"
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=8.0",
|
|
44
|
+
"pytest-cov",
|
|
45
|
+
"ruff",
|
|
46
|
+
"mypy",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/promptdiff"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
addopts = "-v --tb=short"
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
target-version = "py39"
|
|
58
|
+
line-length = 100
|
|
59
|
+
|
|
60
|
+
[tool.mypy]
|
|
61
|
+
python_version = "3.9"
|
|
62
|
+
warn_return_any = true
|
|
63
|
+
warn_unused_configs = true
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""promptdiff: Semantic diff for LLM prompt changes.
|
|
2
|
+
|
|
3
|
+
Public API exports for convenience imports::
|
|
4
|
+
|
|
5
|
+
from promptdiff import diff_files, PromptDiff, format_text
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from promptdiff.models import (
|
|
9
|
+
BreakingChange,
|
|
10
|
+
ChangeStatus,
|
|
11
|
+
MessageDiff,
|
|
12
|
+
MetadataDiff,
|
|
13
|
+
PromptDiff,
|
|
14
|
+
TokenDelta,
|
|
15
|
+
VariableDiff,
|
|
16
|
+
)
|
|
17
|
+
from promptdiff.differ import (
|
|
18
|
+
compute_token_delta,
|
|
19
|
+
diff_files,
|
|
20
|
+
diff_messages,
|
|
21
|
+
diff_metadata,
|
|
22
|
+
diff_variables,
|
|
23
|
+
)
|
|
24
|
+
from promptdiff.analyzer import analyze_breaking_changes
|
|
25
|
+
from promptdiff.reporter import format_json, format_markdown, format_text
|
|
26
|
+
|
|
27
|
+
__version__ = "1.0.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Models
|
|
31
|
+
"BreakingChange",
|
|
32
|
+
"ChangeStatus",
|
|
33
|
+
"MessageDiff",
|
|
34
|
+
"MetadataDiff",
|
|
35
|
+
"PromptDiff",
|
|
36
|
+
"TokenDelta",
|
|
37
|
+
"VariableDiff",
|
|
38
|
+
# Differ
|
|
39
|
+
"compute_token_delta",
|
|
40
|
+
"diff_files",
|
|
41
|
+
"diff_messages",
|
|
42
|
+
"diff_metadata",
|
|
43
|
+
"diff_variables",
|
|
44
|
+
# Analyzer
|
|
45
|
+
"analyze_breaking_changes",
|
|
46
|
+
# Reporter
|
|
47
|
+
"format_json",
|
|
48
|
+
"format_markdown",
|
|
49
|
+
"format_text",
|
|
50
|
+
# Version
|
|
51
|
+
"__version__",
|
|
52
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Breaking change detection for promptdiff.
|
|
2
|
+
|
|
3
|
+
Analyzes a PromptDiff and classifies changes by severity.
|
|
4
|
+
|
|
5
|
+
Breaking changes:
|
|
6
|
+
- New required variable (no default) -- high severity
|
|
7
|
+
- Removed variable -- high severity
|
|
8
|
+
- Removed message -- high severity
|
|
9
|
+
- Changed role ordering -- medium severity
|
|
10
|
+
- Model change (in metadata) -- medium severity
|
|
11
|
+
|
|
12
|
+
Non-breaking changes:
|
|
13
|
+
- Added variable with default
|
|
14
|
+
- Added messages
|
|
15
|
+
- Content modifications
|
|
16
|
+
- Metadata changes (except model)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from promptdiff.models import (
|
|
22
|
+
BreakingChange,
|
|
23
|
+
ChangeStatus,
|
|
24
|
+
PromptDiff,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def analyze_breaking_changes(diff: PromptDiff) -> list[BreakingChange]:
|
|
29
|
+
"""Analyze a diff for breaking changes.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
diff:
|
|
34
|
+
The prompt diff to analyze.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
list[BreakingChange]
|
|
39
|
+
All detected breaking changes sorted by severity.
|
|
40
|
+
"""
|
|
41
|
+
changes: list[BreakingChange] = []
|
|
42
|
+
changes.extend(_check_variable_changes(diff))
|
|
43
|
+
changes.extend(_check_message_changes(diff))
|
|
44
|
+
changes.extend(_check_metadata_changes(diff))
|
|
45
|
+
changes.extend(_check_role_ordering(diff))
|
|
46
|
+
|
|
47
|
+
# Sort by severity: high first, then medium, then low.
|
|
48
|
+
severity_order = {"high": 0, "medium": 1, "low": 2}
|
|
49
|
+
changes.sort(key=lambda c: severity_order.get(c.severity, 3))
|
|
50
|
+
|
|
51
|
+
return changes
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _check_variable_changes(diff: PromptDiff) -> list[BreakingChange]:
|
|
55
|
+
"""Detect breaking variable changes."""
|
|
56
|
+
changes: list[BreakingChange] = []
|
|
57
|
+
|
|
58
|
+
for vd in diff.variable_diffs:
|
|
59
|
+
if vd.status == ChangeStatus.REMOVED:
|
|
60
|
+
changes.append(
|
|
61
|
+
BreakingChange(
|
|
62
|
+
category="variable",
|
|
63
|
+
description=f"Variable '{vd.name}' was removed",
|
|
64
|
+
severity="high",
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
elif vd.status == ChangeStatus.ADDED and vd.is_breaking:
|
|
68
|
+
changes.append(
|
|
69
|
+
BreakingChange(
|
|
70
|
+
category="variable",
|
|
71
|
+
description=(
|
|
72
|
+
f"New required variable '{vd.name}' added without a default value"
|
|
73
|
+
),
|
|
74
|
+
severity="high",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return changes
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _check_message_changes(diff: PromptDiff) -> list[BreakingChange]:
|
|
82
|
+
"""Detect breaking message changes."""
|
|
83
|
+
changes: list[BreakingChange] = []
|
|
84
|
+
|
|
85
|
+
for md in diff.message_diffs:
|
|
86
|
+
if md.status == ChangeStatus.REMOVED:
|
|
87
|
+
changes.append(
|
|
88
|
+
BreakingChange(
|
|
89
|
+
category="message",
|
|
90
|
+
description=f"{md.role.capitalize()} message was removed",
|
|
91
|
+
severity="high",
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return changes
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _check_metadata_changes(diff: PromptDiff) -> list[BreakingChange]:
|
|
99
|
+
"""Detect breaking metadata changes (model changes)."""
|
|
100
|
+
changes: list[BreakingChange] = []
|
|
101
|
+
|
|
102
|
+
for md in diff.metadata_diffs:
|
|
103
|
+
if md.key == "model" and md.status == ChangeStatus.MODIFIED:
|
|
104
|
+
changes.append(
|
|
105
|
+
BreakingChange(
|
|
106
|
+
category="model",
|
|
107
|
+
description=(
|
|
108
|
+
f"Model changed from '{md.old_value}' to '{md.new_value}'"
|
|
109
|
+
),
|
|
110
|
+
severity="medium",
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
elif md.key == "model" and md.status == ChangeStatus.REMOVED:
|
|
114
|
+
changes.append(
|
|
115
|
+
BreakingChange(
|
|
116
|
+
category="model",
|
|
117
|
+
description="Model specification was removed",
|
|
118
|
+
severity="medium",
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return changes
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _check_role_ordering(diff: PromptDiff) -> list[BreakingChange]:
|
|
126
|
+
"""Detect role ordering changes.
|
|
127
|
+
|
|
128
|
+
If the sequence of roles (ignoring unchanged content) changes between
|
|
129
|
+
old and new, this is potentially breaking.
|
|
130
|
+
"""
|
|
131
|
+
changes: list[BreakingChange] = []
|
|
132
|
+
|
|
133
|
+
# Reconstruct old and new role sequences from the message diffs.
|
|
134
|
+
old_roles: list[str] = []
|
|
135
|
+
new_roles: list[str] = []
|
|
136
|
+
|
|
137
|
+
for md in diff.message_diffs:
|
|
138
|
+
if md.status == ChangeStatus.REMOVED:
|
|
139
|
+
old_roles.append(md.role)
|
|
140
|
+
elif md.status == ChangeStatus.ADDED:
|
|
141
|
+
new_roles.append(md.role)
|
|
142
|
+
elif md.status in (ChangeStatus.MODIFIED, ChangeStatus.UNCHANGED):
|
|
143
|
+
old_roles.append(md.role)
|
|
144
|
+
new_roles.append(md.role)
|
|
145
|
+
|
|
146
|
+
if old_roles != new_roles and len(old_roles) > 0 and len(new_roles) > 0:
|
|
147
|
+
# Only flag if neither list is a subset scenario (pure additions/removals
|
|
148
|
+
# are already caught above). Check if the common elements changed order.
|
|
149
|
+
common_old = [r for r in old_roles if r in new_roles]
|
|
150
|
+
common_new = [r for r in new_roles if r in old_roles]
|
|
151
|
+
if common_old != common_new:
|
|
152
|
+
changes.append(
|
|
153
|
+
BreakingChange(
|
|
154
|
+
category="role",
|
|
155
|
+
description=(
|
|
156
|
+
f"Message role ordering changed: "
|
|
157
|
+
f"{' -> '.join(old_roles)} to {' -> '.join(new_roles)}"
|
|
158
|
+
),
|
|
159
|
+
severity="medium",
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return changes
|