Skip to content

Commit 1b3cc51

Browse files
committed
feat: add AI-powered spec upgrader for semantic improvements
Add Claude-powered upgrade system for intelligent spec improvements beyond structural changes. New Features: - upgrade_specs_ai.py: AI-powered spec upgrader using Claude - Semantic improvements (better wording, clearer criteria) - Preserves spec ID and core intent - Automatic backup creation (.backup-{version} files) - Dry-run mode for preview - Single spec or batch upgrade - specs/upgrades/: Directory for version-specific upgrade instructions - Custom instructions per version transition - Examples and before/after comparisons - Rationale for changes - Updated VERSIONING.md with AI upgrade documentation Why AI-Powered Upgrades? - Structural changes (new sections) are easy with regex - Semantic improvements (better wording, specificity) need understanding - AI can reformulate quality criteria to be measurable - AI can enhance parameter descriptions with types and ranges - AI preserves intent while improving clarity Usage: export ANTHROPIC_API_KEY=sk-ant-... # Preview changes python automation/scripts/upgrade_specs_ai.py --dry-run # Upgrade all specs (with backups) python automation/scripts/upgrade_specs_ai.py --version 1.0.0 # Upgrade single spec python automation/scripts/upgrade_specs_ai.py --spec scatter-basic-001 Combined Strategy: 1. upgrade_specs.py - For structural changes (fast, no API calls) 2. upgrade_specs_ai.py - For semantic improvements (AI-powered) This enables continuous spec quality improvement as we learn better documentation patterns.
1 parent 34c9a2b commit 1b3cc51

File tree

3 files changed

+570
-1
lines changed

3 files changed

+570
-1
lines changed
Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
#!/usr/bin/env python3
2+
"""
3+
AI-Powered Spec Upgrader
4+
5+
Uses Claude to intelligently upgrade spec files with semantic improvements,
6+
not just structural changes.
7+
"""
8+
9+
import os
10+
import re
11+
from pathlib import Path
12+
from typing import Dict, Tuple
13+
import anthropic
14+
15+
16+
def get_spec_version(spec_content: str) -> str:
17+
"""Extract spec version from content"""
18+
match = re.search(r'\*\*Spec Version:\*\*\s+(\d+\.\d+\.\d+)', spec_content)
19+
if match:
20+
return match.group(1)
21+
return "0.0.0"
22+
23+
24+
def load_template(version: str) -> str:
25+
"""Load template for target version"""
26+
if version == "1.0.0":
27+
template_path = Path("specs/.template.md")
28+
else:
29+
# Future: specs/.template-v{version}.md for historical templates
30+
template_path = Path("specs/.template.md")
31+
32+
if not template_path.exists():
33+
raise FileNotFoundError(f"Template not found: {template_path}")
34+
35+
return template_path.read_text()
36+
37+
38+
def load_upgrade_instructions(from_version: str, to_version: str) -> str:
39+
"""Load specific upgrade instructions for version transition"""
40+
instructions_path = Path(f"specs/upgrades/{from_version}-to-{to_version}.md")
41+
42+
if instructions_path.exists():
43+
return instructions_path.read_text()
44+
45+
# Default generic instructions
46+
return f"""
47+
# Upgrade Instructions: {from_version}{to_version}
48+
49+
## Goals
50+
- Maintain the core intent and requirements of the spec
51+
- Improve clarity and specificity
52+
- Ensure quality criteria are measurable and actionable
53+
- Update to latest template structure
54+
55+
## Specific Improvements
56+
- Quality criteria should be concrete and verifiable (not vague like "looks good")
57+
- Parameter descriptions should include types and ranges
58+
- Use cases should be specific with domain examples
59+
- Expected output should describe visual elements precisely
60+
61+
## Preserve
62+
- Spec ID and title
63+
- Core functionality and requirements
64+
- Existing optional parameters (unless deprecated)
65+
"""
66+
67+
68+
def upgrade_spec_with_ai(
69+
spec_content: str,
70+
spec_id: str,
71+
target_version: str,
72+
api_key: str,
73+
dry_run: bool = False
74+
) -> Tuple[bool, str, str]:
75+
"""
76+
Use Claude to upgrade a spec to target version
77+
78+
Returns:
79+
(success: bool, upgraded_content: str, message: str)
80+
"""
81+
current_version = get_spec_version(spec_content)
82+
83+
if current_version == target_version:
84+
return True, spec_content, f"Already at version {target_version}"
85+
86+
# Load template and instructions
87+
template = load_template(target_version)
88+
upgrade_instructions = load_upgrade_instructions(current_version, target_version)
89+
90+
# Build prompt for Claude
91+
prompt = f"""You are a technical documentation expert specializing in data visualization specifications.
92+
93+
# Task
94+
Upgrade this plot specification from version {current_version} to version {target_version}.
95+
96+
# Current Spec
97+
{spec_content}
98+
99+
# Target Template (v{target_version})
100+
{template}
101+
102+
# Upgrade Instructions
103+
{upgrade_instructions}
104+
105+
# Requirements
106+
107+
1. **Preserve Core Intent**
108+
- Keep spec ID and title identical
109+
- Maintain all required data parameters
110+
- Preserve optional parameters (unless explicitly deprecated)
111+
- Keep the fundamental purpose of the plot
112+
113+
2. **Improve Quality Criteria**
114+
- Make criteria specific and measurable
115+
- Replace vague terms ("looks good") with concrete checks
116+
- Ensure each criterion can be verified visually or programmatically
117+
- Examples:
118+
❌ "Plot looks nice"
119+
✅ "Grid is visible but subtle with alpha=0.3 and dashed linestyle"
120+
121+
3. **Enhance Parameter Descriptions**
122+
- Add explicit types (numeric, categorical, datetime, etc.)
123+
- Specify ranges where applicable (e.g., "0.0-1.0")
124+
- Include examples in descriptions
125+
126+
4. **Improve Use Cases**
127+
- Make use cases domain-specific
128+
- Include concrete examples with data types
129+
- Example: "Correlation analysis between height and weight in healthcare data"
130+
131+
5. **Update Metadata**
132+
- Set Spec Version to {target_version}
133+
- Set Template Version to {target_version}
134+
- Keep Created date (if present)
135+
- Update Last Updated to today (2025-01-24)
136+
137+
6. **Maintain Structure**
138+
- Follow the template structure exactly
139+
- Keep all required sections
140+
- Add any new sections from template
141+
142+
# Output Format
143+
Provide ONLY the upgraded spec content in Markdown format.
144+
Do NOT include explanations, just the spec file content.
145+
146+
Generate the upgraded spec now:"""
147+
148+
if dry_run:
149+
return True, spec_content, f"Would upgrade from {current_version} to {target_version} using AI"
150+
151+
# Call Claude
152+
client = anthropic.Anthropic(api_key=api_key)
153+
154+
response = client.messages.create(
155+
model="claude-sonnet-4-20250514",
156+
max_tokens=4000,
157+
messages=[{"role": "user", "content": prompt}]
158+
)
159+
160+
upgraded_content = response.content[0].text
161+
162+
# Clean markdown code blocks if present
163+
if "```markdown" in upgraded_content:
164+
upgraded_content = upgraded_content.split("```markdown")[1].split("```")[0].strip()
165+
elif "```" in upgraded_content:
166+
upgraded_content = upgraded_content.split("```")[1].split("```")[0].strip()
167+
168+
# Verify upgrade was successful
169+
new_version = get_spec_version(upgraded_content)
170+
if new_version != target_version:
171+
return False, spec_content, f"Upgrade failed: version is {new_version}, expected {target_version}"
172+
173+
# Verify spec ID is preserved
174+
spec_id_pattern = r'^#\s+([a-z]+-[a-z]+-\d{3}):'
175+
old_match = re.search(spec_id_pattern, spec_content, re.MULTILINE)
176+
new_match = re.search(spec_id_pattern, upgraded_content, re.MULTILINE)
177+
178+
if old_match and new_match:
179+
if old_match.group(1) != new_match.group(1):
180+
return False, spec_content, f"Upgrade failed: spec ID changed from {old_match.group(1)} to {new_match.group(1)}"
181+
elif old_match and not new_match:
182+
return False, spec_content, "Upgrade failed: spec ID was removed"
183+
184+
return True, upgraded_content, f"Successfully upgraded from {current_version} to {target_version}"
185+
186+
187+
def upgrade_spec_file_ai(
188+
spec_path: Path,
189+
target_version: str,
190+
api_key: str,
191+
dry_run: bool = False,
192+
backup: bool = True
193+
) -> Tuple[bool, str]:
194+
"""
195+
Upgrade a single spec file using AI
196+
197+
Args:
198+
spec_path: Path to spec file
199+
target_version: Target version
200+
api_key: Anthropic API key
201+
dry_run: Don't write changes
202+
backup: Create .backup file before upgrading
203+
204+
Returns:
205+
(success: bool, message: str)
206+
"""
207+
try:
208+
content = spec_path.read_text()
209+
spec_id = spec_path.stem
210+
211+
success, upgraded_content, message = upgrade_spec_with_ai(
212+
content, spec_id, target_version, api_key, dry_run
213+
)
214+
215+
if not success:
216+
return False, message
217+
218+
if dry_run:
219+
return True, message
220+
221+
# Create backup
222+
if backup:
223+
backup_path = spec_path.with_suffix(f".md.backup-{get_spec_version(content)}")
224+
backup_path.write_text(content)
225+
print(f" 💾 Backup saved: {backup_path.name}")
226+
227+
# Write upgraded content
228+
spec_path.write_text(upgraded_content)
229+
230+
return True, message
231+
232+
except Exception as e:
233+
return False, f"Error: {str(e)}"
234+
235+
236+
def upgrade_all_specs_ai(
237+
target_version: str,
238+
api_key: str,
239+
dry_run: bool = False,
240+
backup: bool = True
241+
) -> Dict[str, Tuple[bool, str]]:
242+
"""
243+
Upgrade all spec files using AI
244+
245+
Args:
246+
target_version: Target version
247+
api_key: Anthropic API key
248+
dry_run: Don't write changes
249+
backup: Create backup files
250+
251+
Returns:
252+
Dict mapping spec_id to (success, message)
253+
"""
254+
results = {}
255+
specs_dir = Path("specs")
256+
257+
# Get all spec files (exclude template and backups)
258+
spec_files = [
259+
f for f in specs_dir.glob("*.md")
260+
if f.name != ".template.md" and not f.name.endswith(".backup")
261+
]
262+
263+
for spec_path in spec_files:
264+
spec_id = spec_path.stem
265+
print(f"\n🔄 Processing: {spec_id}")
266+
267+
success, message = upgrade_spec_file_ai(
268+
spec_path, target_version, api_key, dry_run, backup
269+
)
270+
271+
results[spec_id] = (success, message)
272+
status = "✅" if success else "❌"
273+
print(f"{status} {message}")
274+
275+
return results
276+
277+
278+
if __name__ == "__main__":
279+
import argparse
280+
281+
parser = argparse.ArgumentParser(
282+
description="AI-powered spec upgrader using Claude"
283+
)
284+
parser.add_argument(
285+
"--version",
286+
default="1.0.0",
287+
help="Target version (default: 1.0.0)"
288+
)
289+
parser.add_argument(
290+
"--dry-run",
291+
action="store_true",
292+
help="Show what would be changed without modifying files"
293+
)
294+
parser.add_argument(
295+
"--spec",
296+
help="Upgrade specific spec file (e.g., scatter-basic-001)"
297+
)
298+
parser.add_argument(
299+
"--no-backup",
300+
action="store_true",
301+
help="Don't create backup files"
302+
)
303+
304+
args = parser.parse_args()
305+
306+
# Get API key
307+
api_key = os.getenv("ANTHROPIC_API_KEY")
308+
if not api_key:
309+
print("❌ ANTHROPIC_API_KEY environment variable not set")
310+
exit(1)
311+
312+
print(f"🤖 AI-Powered Spec Upgrader")
313+
print(f"Target version: {args.version}")
314+
print(f"Dry run: {args.dry_run}")
315+
print(f"Backup: {not args.no_backup}")
316+
print("=" * 60)
317+
318+
if args.spec:
319+
# Upgrade single spec
320+
spec_path = Path(f"specs/{args.spec}.md")
321+
if not spec_path.exists():
322+
print(f"❌ Spec not found: {spec_path}")
323+
exit(1)
324+
325+
success, message = upgrade_spec_file_ai(
326+
spec_path,
327+
args.version,
328+
api_key,
329+
args.dry_run,
330+
not args.no_backup
331+
)
332+
333+
status = "✅" if success else "❌"
334+
print(f"\n{status} {args.spec}: {message}")
335+
336+
if not success:
337+
exit(1)
338+
else:
339+
# Upgrade all specs
340+
results = upgrade_all_specs_ai(
341+
args.version,
342+
api_key,
343+
args.dry_run,
344+
not args.no_backup
345+
)
346+
347+
print("\n" + "=" * 60)
348+
print("Summary")
349+
print("=" * 60)
350+
351+
total = len(results)
352+
succeeded = sum(1 for success, _ in results.values() if success)
353+
failed = total - succeeded
354+
355+
print(f"Total: {total} | Succeeded: {succeeded} | Failed: {failed}")
356+
357+
if failed > 0:
358+
print("\n❌ Failed specs:")
359+
for spec_id, (success, message) in results.items():
360+
if not success:
361+
print(f" - {spec_id}: {message}")
362+
exit(1)
363+
364+
print("\n✅ Upgrade complete!")

0 commit comments

Comments
 (0)