Report Generator Agent — Implementation Spec

Purpose: Transform M1 pipeline output (full.json) into investor-grade PPTX presentation with modular slide library that scales with available data. Author: Co-Developer Claude (Opus 4.7), 2026-04-21 Status: Ready to execute Estimated effort: 3-5 days of Claude Code work (4 phases, ship-able incrementally) Expected impact: M1 output becomes shareable artifact (investor decks, client deliverables) instead of wall-of-text markdown. Unlocks Combined M1+M2 pricing tier justification ($5-20K).


Why this matters

Current state: M1 pipeline produces report.md (narrative text) + full.json (structured data). Users cannot easily read markdown on mobile. Hard to share with investors or clients. Reference: Denis manually fed M1 output to Genspark (external tool) to get a 20-slide PPTX — because markdown was unreadable.

Gap: No native presentation-grade output. Reports remain engineer-readable, not investor-readable.

The test case proved viability: Genspark-generated PPTX from M1 full.json produced a professional deck (dark navy Tailwind palette, Inter font, KPI tiles, 3-column competitor layouts). That output serves as our design reference — we recreate it natively inside the pipeline.

Critical insight: Scope is dynamic. MVP today produces ~14-20 slides from M1 output alone. Future with M2 UA audit + Chamber deliberation transcripts + accumulated Learning Loops outcomes produces 80-150 slides. Each slide must be backed by real research, not padding.


Core architectural decision

Modular slide library, not fixed template

Every slide is a SlideModule class with four responsibilities:

  1. Availability check — does the data to build this slide exist?
  2. Data extraction — pull required fields from full.json (or m2_*.json, chamber/*.json, etc. in future)
  3. Layout rendering — place shapes and text on slide canvas
  4. Priority score — position in final deck order

The generator collects all available modules, filters by availability, sorts by priority, renders into PPTX. No hardcoded slide order.

Consequence: deck scales with research depth

  • M1-only run (today): 14-20 slides
  • M1 + M2 audit (Combined v1): 30-50 slides
  • Combined + Chamber transcripts + Learning Loops history (mature product): 80-150 slides

Every additional slide corresponds to real AI work done. No padding. This aligns with Swiss Neutrality brand (ADR-0025) — honesty in what we deliver.


Design system (from Genspark reference analysis)

Fixed for v1. Do NOT expose customization to user in MVP.

Colors

# Base palette
BG_PRIMARY = "#0F172A"      # Dark navy (slate-900)
BG_SECONDARY = "#1E293B"    # Slate-800 (cards on dark bg)
TEXT_PRIMARY = "#FFFFFF"    # White
TEXT_MUTED = "#94A3B8"      # Slate-400
TEXT_SUBTLE = "#CBD5E1"     # Slate-300
 
# Accent colors (Tailwind-inspired)
ACCENT_BLUE = "#3B82F6"     # Blue-500 (primary accent)
ACCENT_INDIGO = "#6366F1"   # Indigo-500
ACCENT_EMERALD = "#22C55E"  # Green-500 (positive metrics)
ACCENT_AMBER = "#F97316"    # Orange-500 (caution)
ACCENT_ROSE = "#EC4899"     # Pink-500 (risks)
 
# Semantic mapping
COLOR_POSITIVE = ACCENT_EMERALD
COLOR_NEUTRAL = ACCENT_BLUE
COLOR_CAUTION = ACCENT_AMBER
COLOR_NEGATIVE = ACCENT_ROSE

Typography

FONT_FAMILY = "Inter"
SIZE_TITLE = 44       # Page title
SIZE_SUBTITLE = 20    # Subtitle under title
SIZE_KPI_BIG = 48     # Large number display
SIZE_KPI_LABEL = 12   # Category under KPI
SIZE_BODY = 14        # Regular text
SIZE_CAPTION = 10     # Small caption text

Layout constants

SLIDE_WIDTH = Inches(13.333)   # Widescreen 16:9
SLIDE_HEIGHT = Inches(7.5)
MARGIN = Inches(0.6)
GUTTER = Inches(0.25)

Hero background

Single reusable dark navy gradient PNG (~50KB). Applied to all slides as background layer. Generated once at package install (saved to src/synth_brain/reporting/assets/hero_bg.png).


Module architecture

Base class

# src/synth_brain/reporting/modules/base.py
 
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
from pptx.slide import Slide
 
@dataclass
class ModuleContext:
    """Data sources available to slide modules."""
    full_json: dict[str, Any]                    # M1 director_report output
    m2_verification: dict[str, Any] | None       # Future: M2 UA verification
    m2_scoring: dict[str, Any] | None            # Future: M2 fit scoring
    chamber_transcripts: list[dict] | None       # Future: Chamber deliberations
    outcome_history: list[dict] | None           # Future: Learning Loops
    meta: dict[str, Any]                         # meta.json (run_id, query, etc)
    judgement: dict[str, Any]                    # Judge scores
 
class SlideModule(ABC):
    """Base class for all slide modules."""
    
    # Class-level metadata (override in subclasses)
    section: str = "other"           # "executive" | "market" | "competitive" | "audience" | "financial" | "implementation" | "verdict"
    priority: int = 500              # Sort order within section (lower = earlier)
    title: str = ""                  # Display name for debugging/logs
    
    @abstractmethod
    def is_available(self, ctx: ModuleContext) -> bool:
        """Return True if data exists to render this slide. Checked before extraction."""
        ...
    
    @abstractmethod
    def extract(self, ctx: ModuleContext) -> dict[str, Any]:
        """Extract and normalize data needed for rendering. Return structured dict."""
        ...
    
    @abstractmethod
    def render(self, slide: Slide, data: dict[str, Any]) -> None:
        """Render content onto slide canvas. Assumes slide has hero background applied."""
        ...
    
    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} section={self.section} priority={self.priority}>"

Section ordering (global)

SECTION_ORDER = [
    "cover",          # Title slide
    "executive",      # Exec summary, TL;DR
    "methodology",    # How we did the research (data sources, scores)
    "market",         # TAM/SAM/SOM, regional, growth
    "competitive",    # Competitor analysis
    "audience",       # Segments, JTBD
    "funnel",         # CJM, acquisition funnels
    "monetization",   # Pricing, tiers, WTP
    "financial",      # Unit economics, scenarios
    "implementation", # MVP, tech stack (M2 section anchors here too in future)
    "risks",          # Risk matrix
    "verdict",        # Final GO/PIVOT/PASS + next steps
]

Generator sorts modules by (SECTION_ORDER.index(section), priority).


MVP module library (v1 = 14 modules)

Each module implements the base class. Full implementation details in separate module specs; here only scope overview.

Cover section

M01_Cover

  • Title: niche name from brief, extracted from meta.query
  • Subtitle: “Инвестиционный отчёт” / “Investment Report”
  • 4-6 KPI tiles: Judge score, SAM, CAGR, target audience, LTV, CAC
  • Small metadata footer: run_id, date, model Judge used

Executive section

M10_ExecutiveSummary

  • Title + subtitle
  • Weighted score big display
  • 2-column: “Opportunities” (HIGH marker) / “Risks” (MEDIUM marker)
  • 3-5 bullet findings per column
  • Final verdict badge: GO / CONDITIONAL GO / PASS

Methodology section

M20_ResearchQuality

  • Research quality score (from Judge)
  • Per-stage cost + duration breakdown
  • Source tier distribution (from Scout tier filter, ADR Scout+Director fixes)
  • Data confidence intervals

Market section

M30_MarketSizing — TAM/SAM/SOM + funnel visualization M31_RegionalDistribution — Regional breakdown with WTP per region M32_GrowthDrivers — CAGR timeline + top 5-6 drivers (ARPU, Pandemic effect, Mobile, etc)

Competitive section

M40_CompetitorsTop3 — Detail cards for top 3 competitors (revenue, users, model, UX notes) M41_CompetitorsExtended — Additional competitors + key insights summary (market share, consolidation, whitespace)

Audience section

M50_AudienceSegments — 3-column segments (Core Believers / Explorers / Optimizers) with demographics, JTBD, pain points

Funnel section

M60_FunnelAnalysis — CJM stages + conversion % + drop-off points + channel mix

Monetization section

M70_PricingTiers — 3-column pricing tiers (15-50 / $50-150+) with features + conversion rates M71_RegionalWTP — Regional willingness-to-pay comparison

Financial section

M80_UnitEconomics — LTV/CAC by tier, LTV/CAC ratio, break-even analysis M81_Scenarios — Pessimistic / Base / Optimistic scenario comparison table + ROI per year

Implementation section

M90_MVPTechStack — MVP components (landing, AI, payment), budget, timeline + recommended tech stack M91_Roadmap — 3-6 month roadmap with KPIs per milestone

Risks section

M95_RiskMatrix — Top risks with severity score + mitigation actions

Verdict section

M99_NextSteps — Final verdict badge + 3-5 prioritized actions with timelines, budgets, success metrics


Future module additions (not in MVP, design-ready)

When M2 UA audit ships, add:

  • M85_UnfairAdvantageVerification — per-UA verdict cards
  • M86_NicheMoatRequirements — weighted moat category breakdown
  • M87_UAMarketFitScoring — alignment score + gap analysis
  • M88_M2FinalVerdict — Combined M1 verdict × M2 verdict matrix

When Chamber deliberations become available:

  • M200_ChamberSummary — L3 decision points + dissent recap
  • M201_ChamberPanelistViews — per-panelist position card

When Learning Loops outcomes accumulate:

  • M300_OutcomeCalibration — Judge score vs actual outcomes (after 10+ labeled runs)
  • M301_PatternInsights — recurring success/failure patterns across niches

Each future module ships as separate Claude Code task. No changes needed to generator core.


Generator core

# src/synth_brain/reporting/generator.py
 
from pathlib import Path
from pptx import Presentation
from pptx.util import Inches
 
from synth_brain.reporting.modules.base import SlideModule, ModuleContext
from synth_brain.reporting.modules import ALL_MODULES  # Registry of module classes
from synth_brain.reporting.design import apply_hero_background
from synth_brain.reporting.section_order import SECTION_ORDER
 
 
def generate_report_pptx(
    run_dir: Path,
    output_path: Path | None = None,
) -> Path:
    """
    Generate PPTX report from M1 pipeline run artifacts.
    
    Returns path to generated PPTX.
    Raises RuntimeError on fundamental failure (e.g., no full.json).
    """
    # Load context
    ctx = _load_module_context(run_dir)
    
    # Filter available modules, sort by section + priority
    modules = [cls() for cls in ALL_MODULES]
    available = [m for m in modules if m.is_available(ctx)]
    ordered = sorted(
        available,
        key=lambda m: (SECTION_ORDER.index(m.section), m.priority)
    )
    
    # Build presentation
    prs = Presentation()
    prs.slide_width = Inches(13.333)
    prs.slide_height = Inches(7.5)
    
    for module in ordered:
        try:
            data = module.extract(ctx)
            slide = prs.slides.add_slide(prs.slide_layouts[6])  # Blank layout
            apply_hero_background(slide)
            module.render(slide, data)
        except Exception as exc:
            # Skip failing modules gracefully — don't break whole deck
            _log_module_failure(module, exc)
            # Optionally add a "section unavailable" placeholder
    
    # Save
    output_path = output_path or (run_dir / "report.pptx")
    prs.save(str(output_path))
    return output_path
 
 
def _load_module_context(run_dir: Path) -> ModuleContext:
    """Load all available JSON artifacts from run directory into context."""
    import json
    
    def _load_json(filename: str) -> dict | None:
        path = run_dir / filename
        if path.exists():
            try:
                return json.loads(path.read_text())
            except Exception:
                return None
        return None
    
    return ModuleContext(
        full_json=_load_json("full.json") or {},
        m2_verification=_load_json("m2_ua_verification.json"),
        m2_scoring=_load_json("m2_fit_scoring.json"),
        chamber_transcripts=_load_chamber_transcripts(run_dir),
        outcome_history=_load_outcome_history(run_dir),
        meta=_load_json("meta.json") or {},
        judgement=_load_json("judgement.json") or {},
    )

Integration into pipeline

Automatic trigger (pipeline end)

In orchestrator (likely src/synth_brain_ui/run_m1_query.py near where _auto_ingest_run was added in ADR-0018 Phase 2):

def _auto_generate_report(run_dir: Path) -> None:
    """Generate PPTX report after pipeline completion. Never raises."""
    if os.environ.get("PPTX_GENERATOR_DISABLED", "0") == "1":
        return
    
    try:
        from synth_brain.reporting.generator import generate_report_pptx
        output = generate_report_pptx(run_dir)
        logger.info(f"report_generated path={output}")
    except Exception as e:
        logger.warning(f"report_generation_failed error={e}")
 
# Call at end of pipeline, after all artifacts written

On-demand regeneration

Streamlit UI adds button on run detail page: “Regenerate PPTX Report”. Runs generate_report_pptx(run_dir) on click, replaces existing report.pptx.

Rationale: allows iteration when modules are updated, or when M2 data added to an M1-only run later.

Streamlit download button

On run detail page, next to existing artifacts: “Download PPTX Report” button. Uses standard Streamlit st.download_button with file bytes.


Implementation phases (ship-able increments)

Phase 1 (Day 1) — Foundation

Deliverables:

  • src/synth_brain/reporting/__init__.py
  • src/synth_brain/reporting/design.py — colors, fonts, sizes, layout constants + apply_hero_background()
  • src/synth_brain/reporting/assets/hero_bg.png — pre-generated gradient (one-time)
  • src/synth_brain/reporting/modules/base.py — SlideModule abstract class + ModuleContext dataclass
  • src/synth_brain/reporting/modules/__init__.py — ALL_MODULES registry
  • src/synth_brain/reporting/section_order.py — SECTION_ORDER constant
  • src/synth_brain/reporting/generator.pygenerate_report_pptx() core
  • tests/test_reporting_generator_smoke.py — smoke test (no modules, just scaffolding)

Phase 1 exit: imports work, generator runs on empty module list producing blank 0-slide PPTX.

Phase 2 (Day 2-3) — 6 high-priority modules

Implement in order (simplest first):

  1. M01_Cover — easiest, sets precedent for design application
  2. M10_ExecutiveSummary — most visible, high-value
  3. M99_NextSteps — pairs with M01 bookend
  4. M30_MarketSizing — first complex data extraction
  5. M50_AudienceSegments — first 3-column layout pattern
  6. M80_UnitEconomics — uses alias groups pattern from section_renderers.py fix

Tests per module:

  • Module declares availability correctly (positive + negative cases)
  • Data extraction handles missing fields gracefully
  • Rendering doesn’t crash on realistic R26/R27 data
  • Output PPTX opens in LibreOffice (headless verification)

Phase 2 exit: 6-slide deck from R27 artifacts looks close to Genspark reference on those 6 topics.

Phase 3 (Day 4) — Remaining 8 MVP modules

Implement 8 remaining from MVP list. Pattern is established; faster than Phase 2.

Phase 3 exit: Full 14-slide deck from R27 artifacts.

Phase 4 (Day 5) — Integration + polish

Deliverables:

  • _auto_generate_report() hook in pipeline orchestrator
  • PPTX_GENERATOR_DISABLED environment flag wired
  • Streamlit UI changes: download button + regenerate button
  • End-to-end test: fresh M1 run produces report.pptx successfully
  • Module failure graceful degradation verified (crash one module, others continue)
  • Performance check: generation completes in < 15 seconds for 14-slide deck

Phase 4 exit: Ship-ready, R28 and future runs auto-generate PPTX.


Testing strategy

Unit tests per module:

  • is_available() returns True on populated R27 full.json, False on empty dict
  • extract() returns expected keys, handles missing fields with sensible defaults
  • render() doesn’t crash; can verify shape count on output slide

Integration tests:

  • Full generate_report_pptx(run_dir) on R27 → 14+ slides produced
  • File opens cleanly (use python-pptx to reopen + count slides + verify no errors)
  • Headless LibreOffice check: soffice --headless --convert-to pdf report.pptx succeeds without errors

Regression test:

  • After each module change, re-run against R27 artifacts + compare slide count + visual diff (screenshot first slide, pixel compare to baseline)

Stress test (Phase 4):

  • Generate on R28 (different niche — e-commerce SMB from 2026-04-20 run) to ensure modules handle variance

Cost and performance

PPTX generation is pure Python, no LLM calls. Deterministic. Fast.

Estimated generation time for 14-slide deck: 5-10 seconds on synth-nova-prod VPS.

Estimated generation time for hypothetical 100-slide deck (Combined + Chamber + outcomes): 30-60 seconds. Acceptable.

**Cost per report: 2.68/run (current M1 baseline) unchanged.

File size per 14-slide deck with reused hero background: ~500KB-1.5MB. Reasonable for email sharing.


Rollback

PPTX_GENERATOR_DISABLED=1 — disables pipeline end hook. No PPTX generated. Existing report.md still produced.

Module-level failures handled gracefully: if M40_CompetitorsTop3 crashes on edge-case data, generator logs warning, skips module, continues. Whole deck doesn’t fail because one section has bad data.

rm /home/developer/projects/synth-brain/src/synth_brain/reporting/ — complete uninstall. Pipeline continues producing only report.md and full.json as before.


Risks and mitigations

RiskLikelihoodMitigation
Genspark’s design is copyrightedLowWe’re not copying a specific design; Tailwind-inspired dark UI is common. Implementation is original.
PPTX appearance inconsistent across LibreOffice / PowerPoint / KeynoteMediumTest in all three during Phase 4. Use only widely-supported shapes (rectangles, text boxes, tables). Avoid 3D effects.
Inter font not available on Windows client machinesMediumEmbed font into PPTX (python-pptx supports) OR fallback to Calibri / system sans-serif
Module availability logic misses edge casesMediumis_available() defensive design; render-time try/except catches the rest
Deck becomes too long for some nichesLow (MVP)Not an issue at 14 modules. When we hit 100+ modules, add min_priority_threshold param to trim low-priority modules.
Shape placement math wrong on different slide aspect ratiosLowLock to 16:9 widescreen in v1. Don’t support 4:3.
Unicode text (Cyrillic) rendering bugs in python-pptxLowTest with Russian-language R27 content in Phase 1 smoke test

Success criteria

  • All 14 MVP modules implement SlideModule interface
  • Full 537+ test suite still passes + 20+ new reporting tests pass
  • R27 artifacts produce 14-slide PPTX opening cleanly in LibreOffice + PowerPoint + Keynote
  • R28 (different niche) produces correctly-scoped PPTX without crashes
  • PPTX_GENERATOR_DISABLED=1 cleanly disables generation
  • Streamlit UI download button works
  • Streamlit UI regenerate button works
  • PPTX generation completes in < 15s on VPS
  • File size < 2MB per deck
  • Visual quality comparable to Genspark reference on same data
  • Commit pushed to origin/main

Deliverables checklist

Phase 1

  • src/synth_brain/reporting/ module scaffold
  • Design system constants
  • Hero background PNG pre-generated
  • Generator core + ModuleContext
  • Smoke test passes
  • Commit: “feat: report generator scaffold (Phase 1)“

Phase 2

  • 6 high-priority modules implemented
  • Tests per module (3 tests × 6 = 18 tests)
  • End-to-end test on R27 produces 6-slide deck
  • Visual spot-check vs Genspark reference on same 6 topics
  • Commit: “feat: report generator 6 high-priority slide modules (Phase 2)“

Phase 3

  • 8 remaining MVP modules implemented
  • Tests per module
  • End-to-end test on R27 produces 14-slide deck
  • Commit: “feat: report generator complete MVP 14 slide modules (Phase 3)“

Phase 4

  • Auto-generation hook in pipeline orchestrator
  • PPTX_GENERATOR_DISABLED feature flag
  • Streamlit download + regenerate buttons
  • End-to-end with fresh M1 run (no caching)
  • R28 run (different niche) produces correct deck
  • LibreOffice + PowerPoint + Keynote compatibility verified
  • Commit: “feat: report generator pipeline integration + UI (Phase 4)”
  • Push all 4 commits to origin/main

Handoff to Claude Code

Claude Code should execute Phase 1 first and STOP. Wait for Denis review. Then Phase 2. And so on. This prevents over-commitment on unreviewed approach.

Phase 1 task

Task: implement Phase 1 of Report Generator per spec at /home/developer/projects/manifest/07-Roadmap/Report-Generator-Spec.md

Read spec first.

Install python-pptx: .venv/bin/pip install python-pptx

Create:
1. src/synth_brain/reporting/__init__.py
2. src/synth_brain/reporting/design.py with colors, fonts, sizes, apply_hero_background() helper
3. src/synth_brain/reporting/modules/__init__.py with ALL_MODULES = [] placeholder
4. src/synth_brain/reporting/modules/base.py with SlideModule abstract class + ModuleContext dataclass
5. src/synth_brain/reporting/section_order.py with SECTION_ORDER constant
6. src/synth_brain/reporting/generator.py with generate_report_pptx() that handles empty module list gracefully (produces valid 0-slide PPTX)
7. src/synth_brain/reporting/assets/hero_bg.png — generate once with Pillow or stock gradient. 50-80KB target size. 1920x1080 dark navy gradient (#0F172A to #1E293B).
8. tests/test_reporting_generator_smoke.py with 3 tests:
   - Import succeeds
   - generate_report_pptx on empty run_dir returns valid PPTX
   - ModuleContext loads full.json from a fixture run_dir

Run full tests: .venv/bin/python -m pytest tests/ 2>&1 | tail -15 (expect 540+ passed, 0 failed)

Commit: "feat: report generator scaffold (Phase 1)"

Do NOT push. Do NOT proceed to Phase 2.

Report back:
- Test output
- Commit hash
- Files created (with sizes)
- Any deviations from spec with rationale

Phase 2 / 3 / 4 tasks — written after each prior phase approved

Each phase task references this spec + prior phase’s commit. Do NOT run all phases in one session.


Estimated total duration: 3-5 Claude Code sessions of 1-3 hours each. Phase 1 is ~90 minutes.