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:
- Availability check — does the data to build this slide exist?
- Data extraction — pull required fields from
full.json(orm2_*.json,chamber/*.json, etc. in future) - Layout rendering — place shapes and text on slide canvas
- 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_ROSETypography
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 textLayout 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 writtenOn-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__.pysrc/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 dataclasssrc/synth_brain/reporting/modules/__init__.py— ALL_MODULES registrysrc/synth_brain/reporting/section_order.py— SECTION_ORDER constantsrc/synth_brain/reporting/generator.py—generate_report_pptx()coretests/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):
- M01_Cover — easiest, sets precedent for design application
- M10_ExecutiveSummary — most visible, high-value
- M99_NextSteps — pairs with M01 bookend
- M30_MarketSizing — first complex data extraction
- M50_AudienceSegments — first 3-column layout pattern
- 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 orchestratorPPTX_GENERATOR_DISABLEDenvironment 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 dictextract()returns expected keys, handles missing fields with sensible defaultsrender()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-pptxto reopen + count slides + verify no errors) - Headless LibreOffice check:
soffice --headless --convert-to pdf report.pptxsucceeds 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
| Risk | Likelihood | Mitigation |
|---|---|---|
| Genspark’s design is copyrighted | Low | We’re not copying a specific design; Tailwind-inspired dark UI is common. Implementation is original. |
| PPTX appearance inconsistent across LibreOffice / PowerPoint / Keynote | Medium | Test 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 machines | Medium | Embed font into PPTX (python-pptx supports) OR fallback to Calibri / system sans-serif |
| Module availability logic misses edge cases | Medium | is_available() defensive design; render-time try/except catches the rest |
| Deck becomes too long for some niches | Low (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 ratios | Low | Lock to 16:9 widescreen in v1. Don’t support 4:3. |
| Unicode text (Cyrillic) rendering bugs in python-pptx | Low | Test 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.