From 6dbbe3ea57ef95f1cfd603457eed2b0928a20862 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Fri, 14 Nov 2025 11:39:03 +0100 Subject: [PATCH] feat: add granular phase-level cards to Nextcloud Deck sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- scripts/sync-roadmap-to-deck.py | 175 +++++++++++++++++++------------- 1 file changed, 106 insertions(+), 69 deletions(-) diff --git a/scripts/sync-roadmap-to-deck.py b/scripts/sync-roadmap-to-deck.py index 6488653..db72ff4 100755 --- a/scripts/sync-roadmap-to-deck.py +++ b/scripts/sync-roadmap-to-deck.py @@ -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