feat: add granular phase-level cards to Nextcloud Deck sync
- Updated parser to extract phases from detailed roadmap files - Cleaner card titles: 'Phase X: Description' instead of file paths - Improved status detection: CURRENT/DEPLOYED → In Progress, NEXT → Planning - Code block removal to prevent API 400/500 errors - Shorter descriptions (400 chars max) for better readability - All 21 cards created: 3 initiatives + 18 phases Card distribution: - Backlog: 6 cards (future work) - Planning: 1 card (next phase) - In Progress: 10 cards (active work) - Complete: 4 cards (done) Changes: - scripts/sync-roadmap-to-deck.py: Complete parser rewrite for phase-level granularity - Handles both ## Phase and ### Phase patterns - Removes markdown/emojis from titles for clean display
This commit is contained in:
@@ -131,9 +131,11 @@ class RoadmapParser:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
content = filepath.read_text()
|
content = filepath.read_text()
|
||||||
|
filename = filepath.name
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
# Parse initiatives (main sections) - handle different emoji prefixes
|
# Master roadmap: Parse initiatives (high-level overview only)
|
||||||
|
if filename == 'OPTIMIZATION_MASTER_ROADMAP.md':
|
||||||
initiative_pattern = r'## (?:🎯|📐|📊) Initiative (\d+): (.+?)(?=\n##|\Z)'
|
initiative_pattern = r'## (?:🎯|📐|📊) Initiative (\d+): (.+?)(?=\n##|\Z)'
|
||||||
initiatives = re.finditer(initiative_pattern, content, re.DOTALL)
|
initiatives = re.finditer(initiative_pattern, content, re.DOTALL)
|
||||||
|
|
||||||
@@ -154,57 +156,92 @@ class RoadmapParser:
|
|||||||
status_match = re.search(r'### Current Status\n(.+?)(?=\n###|\Z)', initiative_content, re.DOTALL)
|
status_match = re.search(r'### Current Status\n(.+?)(?=\n###|\Z)', initiative_content, re.DOTALL)
|
||||||
status = status_match.group(1).strip() if status_match else ''
|
status = status_match.group(1).strip() if status_match else ''
|
||||||
|
|
||||||
# Extract timeline
|
|
||||||
timeline_match = re.search(r'### Timeline\n(.+?)(?=\n###|\Z)', initiative_content, re.DOTALL)
|
|
||||||
timeline = timeline_match.group(1).strip() if timeline_match else ''
|
|
||||||
|
|
||||||
# Extract success metrics
|
|
||||||
metrics_match = re.search(r'### Success Metrics\n(.+?)(?=\n###|\Z)', initiative_content, re.DOTALL)
|
|
||||||
metrics = metrics_match.group(1).strip() if metrics_match else ''
|
|
||||||
|
|
||||||
# Determine phase status
|
# Determine phase status
|
||||||
phase_status = 'FUTURE'
|
phase_status = 'FUTURE'
|
||||||
if '✅ COMPLETE' in status or '✅ **COMPLETE' in status:
|
if '✅ COMPLETE' in status or 'COMPLETE' in status:
|
||||||
phase_status = 'COMPLETE'
|
phase_status = 'COMPLETE'
|
||||||
elif '🔄 IN PROGRESS' in status or '🔄 **IN PROGRESS' in status:
|
elif '🔄 IN PROGRESS' in status or 'IN PROGRESS' in status:
|
||||||
phase_status = 'IN PROGRESS'
|
phase_status = 'IN PROGRESS'
|
||||||
elif '🔜 NEXT' in status or 'Phase 2' in status:
|
elif '🔜 NEXT' in status:
|
||||||
phase_status = 'PENDING'
|
phase_status = 'PENDING'
|
||||||
|
|
||||||
# Extract progress
|
|
||||||
progress_match = re.search(r'Progress:.*?(\d+)/(\d+)', status)
|
|
||||||
progress = f"{progress_match.group(1)}/{progress_match.group(2)}" if progress_match else ''
|
|
||||||
|
|
||||||
# Calculate due date from timeline
|
|
||||||
due_date = None
|
|
||||||
if 'weeks' in timeline.lower():
|
|
||||||
weeks = re.search(r'(\d+)-?(\d+)?\s*weeks', timeline.lower())
|
|
||||||
if weeks:
|
|
||||||
max_weeks = int(weeks.group(2) if weeks.group(2) else weeks.group(1))
|
|
||||||
due_date = (datetime.now() + timedelta(weeks=max_weeks)).strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
task = {
|
task = {
|
||||||
'title': f"Initiative {initiative_num}: {initiative_title}",
|
'title': f"Initiative {initiative_num}: {initiative_title}",
|
||||||
'description': f"""**Initiative {initiative_num}: {initiative_title}**
|
'description': f"""**File:** [{file_ref}](./{file_ref})
|
||||||
|
|
||||||
{purpose}
|
{purpose}
|
||||||
|
|
||||||
**Current Status:**
|
**Status:** {status}
|
||||||
{status}
|
""",
|
||||||
|
'status': phase_status,
|
||||||
**Timeline:**
|
'progress': '',
|
||||||
{timeline}
|
'due_date': None,
|
||||||
|
'labels': [f'initiative-{initiative_num}'],
|
||||||
**Success Metrics:**
|
}
|
||||||
{metrics}
|
|
||||||
|
tasks.append(task)
|
||||||
**File:** {file_ref}
|
|
||||||
|
# Detailed roadmaps: Parse phases (use ## or ### pattern depending on file)
|
||||||
|
else:
|
||||||
|
# Try ## Phase pattern first (SIGNAL_QUALITY, POSITION_SCALING)
|
||||||
|
phase_pattern = r'## Phase (\d+): (.+?)(?=\n(?:##|\Z))'
|
||||||
|
phases = list(re.finditer(phase_pattern, content, re.DOTALL))
|
||||||
|
|
||||||
|
# If no ## Phase found, try ### Phase pattern (ATR_BASED_TP)
|
||||||
|
if not phases:
|
||||||
|
phase_pattern = r'### Phase (\d+): (.+?)(?=\n(?:###|##|\Z))'
|
||||||
|
phases = list(re.finditer(phase_pattern, content, re.DOTALL))
|
||||||
|
|
||||||
|
for phase in phases:
|
||||||
|
phase_num = phase.group(1)
|
||||||
|
phase_title_raw = phase.group(2).strip()
|
||||||
|
|
||||||
|
# Clean up phase title (remove status emojis and extra text in parens)
|
||||||
|
phase_title = re.sub(r'\s*\(.*?\)\s*', ' ', phase_title_raw)
|
||||||
|
phase_title = re.sub(r'[🔄✅🔜🎯⏳🤖📏📊🔮]+', '', phase_title).strip()
|
||||||
|
|
||||||
|
# Extract full phase content for description
|
||||||
|
phase_start = phase.end()
|
||||||
|
next_phase_match = re.search(r'\n(?:##|###) Phase \d+:', content[phase_start:])
|
||||||
|
phase_end = phase_start + next_phase_match.start() if next_phase_match else len(content)
|
||||||
|
phase_content = content[phase_start:phase_end].strip()
|
||||||
|
|
||||||
|
# Remove code blocks FIRST (they cause 400/500 errors)
|
||||||
|
phase_content_clean = re.sub(r'```[\s\S]*?```', '[Code block omitted - see roadmap file]', phase_content)
|
||||||
|
|
||||||
|
# Then limit description to first 400 chars
|
||||||
|
description_preview = phase_content_clean[:400]
|
||||||
|
if len(phase_content_clean) > 400:
|
||||||
|
description_preview += '...'
|
||||||
|
|
||||||
|
# Determine status from title
|
||||||
|
phase_status = 'FUTURE'
|
||||||
|
title_upper = phase_title_raw.upper()
|
||||||
|
|
||||||
|
# Check for status indicators in the raw title
|
||||||
|
if ('CURRENT' in title_upper or '✅' in phase_title_raw or 'DEPLOYED' in title_upper) and 'FUTURE' not in title_upper:
|
||||||
|
phase_status = 'IN PROGRESS'
|
||||||
|
elif '🔜' in phase_title_raw or ('NEXT' in title_upper and 'FUTURE' not in title_upper):
|
||||||
|
phase_status = 'PENDING'
|
||||||
|
elif 'COMPLETE' in title_upper:
|
||||||
|
phase_status = 'COMPLETE'
|
||||||
|
# Otherwise stays FUTURE
|
||||||
|
|
||||||
|
# Extract progress if present
|
||||||
|
progress_match = re.search(r'(\d+)/(\d+)', phase_content[:200])
|
||||||
|
progress = f"{progress_match.group(1)}/{progress_match.group(2)}" if progress_match else ''
|
||||||
|
|
||||||
|
task = {
|
||||||
|
'title': f"Phase {phase_num}: {phase_title}",
|
||||||
|
'description': f"""**Initiative:** {filepath.stem.replace('_', ' ').title().replace('Roadmap', '')}
|
||||||
|
**File:** [{filename}](./{filename})
|
||||||
|
|
||||||
|
{description_preview}
|
||||||
""",
|
""",
|
||||||
'status': phase_status,
|
'status': phase_status,
|
||||||
'initiative_num': initiative_num,
|
|
||||||
'progress': progress,
|
'progress': progress,
|
||||||
'due_date': due_date,
|
'due_date': None,
|
||||||
'labels': [f'initiative-{initiative_num}', 'optimization'],
|
'labels': [filename.replace('.md', '').lower(), f'phase-{phase_num}'],
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
|
|||||||
Reference in New Issue
Block a user