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:
mindesbunister
2025-11-14 11:39:03 +01:00
parent a49db192f4
commit 6dbbe3ea57

View File

@@ -131,83 +131,120 @@ class RoadmapParser:
return []
content = filepath.read_text()
filename = filepath.name
tasks = []
# Parse initiatives (main sections) - handle different emoji prefixes
initiative_pattern = r'## (?:🎯|📐|📊) Initiative (\d+): (.+?)(?=\n##|\Z)'
initiatives = re.finditer(initiative_pattern, content, re.DOTALL)
for initiative in initiatives:
initiative_num = initiative.group(1)
initiative_title = initiative.group(2).strip()
initiative_content = initiative.group(0)
# Master roadmap: Parse initiatives (high-level overview only)
if filename == 'OPTIMIZATION_MASTER_ROADMAP.md':
initiative_pattern = r'## (?:🎯|📐|📊) Initiative (\d+): (.+?)(?=\n##|\Z)'
initiatives = re.finditer(initiative_pattern, content, re.DOTALL)
# Extract file reference
file_match = re.search(r'\*\*File:\*\* \[`(.+?)`\]', initiative_content)
file_ref = file_match.group(1) if file_match else ''
# Extract purpose
purpose_match = re.search(r'### Purpose\n(.+?)(?=\n###|\Z)', initiative_content, re.DOTALL)
purpose = purpose_match.group(1).strip() if purpose_match else ''
# Extract current status
status_match = re.search(r'### Current Status\n(.+?)(?=\n###|\Z)', initiative_content, re.DOTALL)
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
phase_status = 'FUTURE'
if '✅ COMPLETE' in status or '✅ **COMPLETE' in status:
phase_status = 'COMPLETE'
elif '🔄 IN PROGRESS' in status or '🔄 **IN PROGRESS' in status:
phase_status = 'IN PROGRESS'
elif '🔜 NEXT' in status or 'Phase 2' in status:
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 = {
'title': f"Initiative {initiative_num}: {initiative_title}",
'description': f"""**Initiative {initiative_num}: {initiative_title}**
for initiative in initiatives:
initiative_num = initiative.group(1)
initiative_title = initiative.group(2).strip()
initiative_content = initiative.group(0)
# Extract file reference
file_match = re.search(r'\*\*File:\*\* \[`(.+?)`\]', initiative_content)
file_ref = file_match.group(1) if file_match else ''
# Extract purpose
purpose_match = re.search(r'### Purpose\n(.+?)(?=\n###|\Z)', initiative_content, re.DOTALL)
purpose = purpose_match.group(1).strip() if purpose_match else ''
# Extract current status
status_match = re.search(r'### Current Status\n(.+?)(?=\n###|\Z)', initiative_content, re.DOTALL)
status = status_match.group(1).strip() if status_match else ''
# Determine phase status
phase_status = 'FUTURE'
if '✅ COMPLETE' in status or 'COMPLETE' in status:
phase_status = 'COMPLETE'
elif '🔄 IN PROGRESS' in status or 'IN PROGRESS' in status:
phase_status = 'IN PROGRESS'
elif '🔜 NEXT' in status:
phase_status = 'PENDING'
task = {
'title': f"Initiative {initiative_num}: {initiative_title}",
'description': f"""**File:** [{file_ref}](./{file_ref})
{purpose}
**Current Status:**
{status}
**Timeline:**
{timeline}
**Success Metrics:**
{metrics}
**File:** {file_ref}
**Status:** {status}
""",
'status': phase_status,
'initiative_num': initiative_num,
'progress': progress,
'due_date': due_date,
'labels': [f'initiative-{initiative_num}', 'optimization'],
}
'status': phase_status,
'progress': '',
'due_date': None,
'labels': [f'initiative-{initiative_num}'],
}
tasks.append(task)
# 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))
tasks.append(task)
# 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,
'progress': progress,
'due_date': None,
'labels': [filename.replace('.md', '').lower(), f'phase-{phase_num}'],
}
tasks.append(task)
return tasks