#!/usr/bin/env python3
"""
Post-process BMM workflow diagram SVG.
Transforms raw D2 output into the final styled diagram.
Run from docs/diagrams directory.
"""
import re
import base64
# =============================================================================
# CONFIGURATION
# =============================================================================
INPUT_FILE = "bmm-workflow-technical.svg"
OUTPUT_FILE = "bmm-workflow.svg"
STAMP_FILE = "red-herring.png"
# =============================================================================
# PROCESSING STEPS
# =============================================================================
def process_svg():
"""Run all SVG post-processing steps in order."""
with open(INPUT_FILE, 'r') as f:
content = f.read()
content = shrink_output_labels(content)
content = outline_title(content)
content = left_align_title(content)
content = inject_legend(content)
content = inject_stamp(content)
with open(OUTPUT_FILE, 'w') as f:
f.write(content)
print(f"Post-processed: {INPUT_FILE} -> {OUTPUT_FILE}")
# =============================================================================
# STEP 1: Shrink output labels (@*.md)
# =============================================================================
def shrink_output_labels(content):
"""Make @*.md output labels smaller and grayed."""
def shrink_tspan(match):
tspan = match.group(0)
return tspan.replace(']*>@[^<]*'
content = re.sub(pattern, shrink_tspan, content)
return content
# =============================================================================
# STEP 2: Outline title text
# =============================================================================
def outline_title(content):
"""Make the title text have an outline/hollow effect."""
# Pattern:
pattern = r'(]*)(fill="#1a3a5c")([^>]*style="[^"]*font-size:4\dpx[^"]*")'
replacement = r'\1fill="none" stroke="#1a3a5c" stroke-width="1.5"\3'
return re.sub(pattern, replacement, content)
# =============================================================================
# STEP 3: Left-align title
# =============================================================================
def left_align_title(content):
"""Left-align the title text."""
def align_title(match):
text_elem = match.group(0)
text_elem = text_elem.replace('text-anchor:middle', 'text-anchor:start')
text_elem = re.sub(r'x="[^"]*"', 'x="50"', text_elem)
return text_elem
pattern = r']*font-size:4[0-9]px[^>]*>BMAD METHOD[^<]*'
return re.sub(pattern, align_title, content)
# =============================================================================
# STEP 4: Inject legend
# =============================================================================
LEGEND_WIDTH = 1200
LEGEND_SVG = '''
OUTPUTS
@bmm-workflow-status.yaml
· Workflow tracking
@product-brief.md
· Product vision and scope
@tech-spec.md
· Quick-flow technical spec
@PRD.md
· Product requirements
@ux-design.md
· UX/UI design spec
@architecture.md
· System architecture
@impl-readiness-report.md
· Readiness validation
@epics.md
· Epic and story breakdown
@sprint-status.yaml
· Sprint progress tracking
@{{epic}}-{{story}}-*.md
· Implementation stories
@sprint-change-proposal.md
· Course correction
'''
def inject_legend(content):
"""Inject legend box in top-right corner."""
legend_y = 180
# Find Phase 4 right edge
phase4_match = re.search(
r'phase4-box[^>]*>.*?]*x="([0-9.]+)"[^>]*width="([0-9.]+)"',
content, re.DOTALL
)
if phase4_match:
phase4_x = float(phase4_match.group(1))
phase4_w = float(phase4_match.group(2))
legend_x = phase4_x + phase4_w - LEGEND_WIDTH + 97
else:
legend_x = 2314 - LEGEND_WIDTH + 97
legend = LEGEND_SVG.format(x=legend_x, y=legend_y)
last_svg_pos = content.rfind('')
if last_svg_pos != -1:
content = content[:last_svg_pos] + legend + '\n' + content[last_svg_pos:]
return content
# =============================================================================
# STEP 5: Inject red herring stamp
# =============================================================================
STAMP_WIDTH = 200
STAMP_HEIGHT = 134
STAMP_MARGIN_X = 220
STAMP_MARGIN_Y = 85
STAMP_ROTATION = 10
def inject_stamp(content):
"""Inject red herring stamp in lower-right corner."""
# Read and encode stamp image
with open(STAMP_FILE, 'rb') as f:
stamp_data = base64.b64encode(f.read()).decode('utf-8')
# Get SVG dimensions
viewbox_match = re.search(r'viewBox="(\d+)\s+(\d+)\s+(\d+)\s+(\d+)"', content)
if viewbox_match:
svg_width = int(viewbox_match.group(3))
svg_height = int(viewbox_match.group(4))
else:
svg_width, svg_height = 2000, 1500
stamp_x = svg_width - STAMP_WIDTH - STAMP_MARGIN_X
stamp_y = svg_height - STAMP_HEIGHT - STAMP_MARGIN_Y
center_x = STAMP_WIDTH / 2
center_y = STAMP_HEIGHT / 2
stamp_svg = f'''
'''
last_svg_pos = content.rfind('')
if last_svg_pos != -1:
content = content[:last_svg_pos] + stamp_svg + '\n' + content[last_svg_pos:]
return content
# =============================================================================
# MAIN
# =============================================================================
if __name__ == "__main__":
process_svg()