Skip to content

Commit b645a6f

Browse files
committed
feat: add spec template versioning system
Add version tracking to spec files for maintainability and automated upgrades. Changes: - Add version marker to spec template (v1.0.0) - Update scatter-basic-001.md with version info - Create upgrade_specs.py script for automated spec upgrades - Add specs/VERSIONING.md documentation - Update plot_generator.py to check and display spec version Benefits: - Easy template evolution without breaking existing specs - Automated migration when template improves - Clear tracking of which template version each spec uses - Version upgrade can be batch-processed Usage: # Check what would change python automation/scripts/upgrade_specs.py --dry-run # Upgrade all specs to latest version python automation/scripts/upgrade_specs.py # Upgrade specific spec python automation/scripts/upgrade_specs.py --spec scatter-basic-001 This enables continuous improvement of the spec template while maintaining backward compatibility.
1 parent 9007e6a commit b645a6f

5 files changed

Lines changed: 426 additions & 1 deletion

File tree

automation/generators/plot_generator.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,19 @@ def load_spec(spec_id: str) -> str:
2020
spec_path = Path(f"specs/{spec_id}.md")
2121
if not spec_path.exists():
2222
raise FileNotFoundError(f"Spec file not found: {spec_path}")
23-
return spec_path.read_text()
23+
24+
content = spec_path.read_text()
25+
26+
# Check spec version
27+
import re
28+
version_match = re.search(r'\*\*Spec Version:\*\*\s+(\d+\.\d+\.\d+)', content)
29+
if version_match:
30+
spec_version = version_match.group(1)
31+
print(f"📋 Spec version: {spec_version}")
32+
else:
33+
print(f"⚠️ Warning: Spec has no version marker. Consider upgrading with upgrade_specs.py")
34+
35+
return content
2436

2537

2638
def load_generation_rules(version: str = "v1.0.0-draft") -> str:
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Spec Version Upgrader
4+
5+
Automatically upgrades spec files to the latest template version.
6+
"""
7+
8+
import re
9+
from pathlib import Path
10+
from typing import Dict, List, Tuple
11+
12+
13+
def get_spec_version(spec_content: str) -> str:
14+
"""Extract spec version from content"""
15+
match = re.search(r'\*\*Spec Version:\*\*\s+(\d+\.\d+\.\d+)', spec_content)
16+
if match:
17+
return match.group(1)
18+
19+
# Check comment version (older format)
20+
match = re.search(r'Spec Template Version:\s+(\d+\.\d+\.\d+)', spec_content)
21+
if match:
22+
return match.group(1)
23+
24+
return "0.0.0" # Unknown/very old
25+
26+
27+
def upgrade_to_1_0_0(spec_content: str, spec_id: str) -> str:
28+
"""Upgrade spec to version 1.0.0 format"""
29+
# Check if already has version
30+
if "**Spec Version:**" in spec_content:
31+
return spec_content
32+
33+
# Extract title
34+
title_match = re.search(r'^#\s+(.+)$', spec_content, re.MULTILINE)
35+
if not title_match:
36+
raise ValueError(f"Could not find title in spec: {spec_id}")
37+
38+
title_line = title_match.group(0)
39+
40+
# Insert version info after title
41+
version_block = f"""
42+
<!--
43+
Spec Template Version: 1.0.0
44+
Created: 2025-01-24
45+
Last Updated: 2025-01-24
46+
-->
47+
48+
**Spec Version:** 1.0.0
49+
"""
50+
51+
# Replace title with title + version block
52+
upgraded = spec_content.replace(
53+
title_line,
54+
title_line + "\n" + version_block,
55+
1
56+
)
57+
58+
return upgraded
59+
60+
61+
def upgrade_spec_file(spec_path: Path, target_version: str = "1.0.0") -> Tuple[bool, str]:
62+
"""
63+
Upgrade a single spec file to target version
64+
65+
Returns:
66+
(success: bool, message: str)
67+
"""
68+
try:
69+
content = spec_path.read_text()
70+
current_version = get_spec_version(content)
71+
72+
if current_version == target_version:
73+
return True, f"Already at version {target_version}"
74+
75+
# Apply upgrades based on version
76+
if current_version < "1.0.0" and target_version >= "1.0.0":
77+
content = upgrade_to_1_0_0(content, spec_path.stem)
78+
79+
# Future upgrades would go here:
80+
# if current_version < "1.1.0" and target_version >= "1.1.0":
81+
# content = upgrade_to_1_1_0(content, spec_path.stem)
82+
83+
# Write back
84+
spec_path.write_text(content)
85+
86+
return True, f"Upgraded from {current_version} to {target_version}"
87+
88+
except Exception as e:
89+
return False, f"Error: {str(e)}"
90+
91+
92+
def find_all_specs() -> List[Path]:
93+
"""Find all spec files (excluding template)"""
94+
specs_dir = Path("specs")
95+
return [f for f in specs_dir.glob("*.md") if f.name != ".template.md"]
96+
97+
98+
def upgrade_all_specs(target_version: str = "1.0.0", dry_run: bool = False) -> Dict[str, Tuple[bool, str]]:
99+
"""
100+
Upgrade all spec files to target version
101+
102+
Args:
103+
target_version: Target template version
104+
dry_run: If True, don't write changes
105+
106+
Returns:
107+
Dict mapping spec_id to (success, message)
108+
"""
109+
results = {}
110+
specs = find_all_specs()
111+
112+
for spec_path in specs:
113+
spec_id = spec_path.stem
114+
115+
if dry_run:
116+
content = spec_path.read_text()
117+
current_version = get_spec_version(content)
118+
results[spec_id] = (True, f"Would upgrade from {current_version} to {target_version}")
119+
else:
120+
success, message = upgrade_spec_file(spec_path, target_version)
121+
results[spec_id] = (success, message)
122+
123+
return results
124+
125+
126+
if __name__ == "__main__":
127+
import argparse
128+
129+
parser = argparse.ArgumentParser(description="Upgrade spec files to latest template version")
130+
parser.add_argument("--version", default="1.0.0", help="Target version (default: 1.0.0)")
131+
parser.add_argument("--dry-run", action="store_true", help="Show what would be changed without modifying files")
132+
parser.add_argument("--spec", help="Upgrade specific spec file (e.g., scatter-basic-001)")
133+
134+
args = parser.parse_args()
135+
136+
print(f"🔄 Spec Version Upgrader")
137+
print(f"Target version: {args.version}")
138+
print(f"Dry run: {args.dry_run}")
139+
print("=" * 60)
140+
141+
if args.spec:
142+
# Upgrade single spec
143+
spec_path = Path(f"specs/{args.spec}.md")
144+
if not spec_path.exists():
145+
print(f"❌ Spec not found: {spec_path}")
146+
exit(1)
147+
148+
if args.dry_run:
149+
content = spec_path.read_text()
150+
current = get_spec_version(content)
151+
print(f"📄 {args.spec}")
152+
print(f" Current: {current}")
153+
print(f" Would upgrade to: {args.version}")
154+
else:
155+
success, message = upgrade_spec_file(spec_path, args.version)
156+
status = "✅" if success else "❌"
157+
print(f"{status} {args.spec}: {message}")
158+
else:
159+
# Upgrade all specs
160+
results = upgrade_all_specs(args.version, args.dry_run)
161+
162+
total = len(results)
163+
succeeded = sum(1 for success, _ in results.values() if success)
164+
failed = total - succeeded
165+
166+
for spec_id, (success, message) in results.items():
167+
status = "✅" if success else "❌"
168+
print(f"{status} {spec_id}: {message}")
169+
170+
print("=" * 60)
171+
print(f"Total: {total} | Succeeded: {succeeded} | Failed: {failed}")
172+
173+
if failed > 0:
174+
exit(1)

specs/.template.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# {spec-id}: {Title}
22

33
<!--
4+
Spec Template Version: 1.0.0
5+
Last Updated: 2025-01-24
46
Naming convention: {type}-{variant}-{number}.md
57
Examples: scatter-basic-001.md, heatmap-corr-002.md, bar-grouped-004.md
68
-->
79

10+
**Spec Version:** 1.0.0
11+
812
## Description
913

1014
{2-3 sentences describing what this plot does and when to use it}

0 commit comments

Comments
 (0)