diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json index 493749d..7405cc8 100644 --- a/.ciagent/CHECKPOINT.json +++ b/.ciagent/CHECKPOINT.json @@ -1,37 +1,19 @@ { - "phase": 2, + "phase": 3, "stage": "complete", "milestone": "v1.30", - "phase_role": "final", + "phase_role": "execution", "attempts": 0, - "updated_at": "2026-08-20T14:40:00Z", + "updated_at": "2026-08-20T15:30:00Z", "project": "acdl", "projects": ["acdl", "nova-blockchain-exchange"], "active_milestone": "v1.30", - "milestone_branch": null, - "phase_branch": null, + "milestone_branch": "milestone/v1.30-leadership-deck", + "phase_branch": "phase/03-deck-polish", "tag_line": "v1.29.x", - "phase_name": "final-review-ship", + "phase_name": "deck-polish", "milestone_type": "feature", "reqs_covered": ["REQ-372.1","REQ-372.2","REQ-372.3","REQ-372.4","REQ-372.5","REQ-372.6","REQ-372.7","REQ-372.8","REQ-372.9","REQ-372.10","REQ-372.11","REQ-372.12"], "reqs_partial": [], - "previous_milestone": { - "milestone": "v1.30", - "tag": "v1.29.3", - "complete": true, - "merged_to_main": "96765fe", - "branches_deleted": true, - "releases_created": true, - "release_ids": [811, 812, 813] - }, - "ship": { - "tag": "v1.29.3", - "release_id": 813, - "release_url": "https://git.cloudinit.dev/continuous-intelligence/acdl/releases/tag/v1.29.3", - "merged_to_main": true, - "merge_commit": "96765fe", - "branches_deleted": ["phase/02-final-review-ship", "milestone/v1.30-leadership-deck"], - "local_only": false - }, - "notes": "v1.30 MILESTONE COMPLETE. Tag v1.29.3 (milestone release), Gitea release id=813. Merged to main 96765fe. All milestone branches deleted (local + remote). REQ-372.1..12 all complete. CAP-042 + D-241..D-243 recorded. 3 Gitea releases: 811 (P0 v1.29.1), 812 (P1 v1.29.2), 813 (P2 v1.29.3). Checkpoint cleared — next run starts a new milestone." + "notes": "v1.30 P3 (deck-polish) COMPLETE. D-244 (cover), D-245 (slide 7 what-works-now), D-246 (7 mermaid diagrams). F1-F5 polish fixes. 7 S&P-themed diagrams rendered via local mmdc (vendored Chromium libs). All checks pass. Next: SHIP (v1.29.4)." } \ No newline at end of file diff --git a/.ciagent/CLARIFY.md b/.ciagent/CLARIFY.md index c6fbecd..ac916f7 100644 --- a/.ciagent/CLARIFY.md +++ b/.ciagent/CLARIFY.md @@ -206,8 +206,118 @@ at the v1.30 ship wave (CAP-042 row + intake assumption correction). --- +## Polish-phase decisions (D-244, D-245, D-246 — full autonomy) + +The first draft (v1.29.3) passed all 12 REQs but the rendered PPTX +has 7 polish issues (vertical imbalance, text clipping, double bullet +markers, no visual variety, near-invisible footer, slide-6 red wall, +blockquote grey). The user requested: (1) polish all 7 issues, (2) +add a "What works now" emphasis to slide 7, (3) generate mermaid +diagrams for every slide. These decisions record the deviations from +prior binding decisions. + +### D-244 — Slide 1 becomes a black-background cover (overrides grill G-1) + +**Polish issue #4.** Grill G-1 locked "all 7 slides as white content +slides." The user approved making slide 1 a black-background cover +(strong opener for a live leadership presentation) while keeping +slides 2-7 as white content slides. The on-slide body text from the +Slide Content Map is preserved verbatim — only the visual treatment +(background + text color) changes. REQ-372.7's "deviation requires +CLARIFY" clause applies to *content* deviations; this is a +presentation polish, not a content change. The source change: slide +1's `## The friction...` → `# The friction...` (H1) + `` directive, which triggers `render_title_slide` (black bg, +red top bar, white text). +**Confidence:** 1.0 (user-confirmed). + +### D-245 — Slide 7 "What works now" content addition (deviation from Slide Content Map) + +**User request.** A new "What works now" block is added to slide 7's +on-slide body, before the existing "What we ask" block. This is a +*content* deviation — the Slide Content Map specifies slide 7 as: +What we ask / Why now / What comes back + closer. The new block +emphasizes what's proven today (L1/L2 stack deployment works, live +apply to the sandbox/dev AWS account, confidence 0.800) and the +next-steps arc (ingest greenfield pilot projects → promote from +sandbox to production → integrate with the SPGE constitutional +library → serve as the infrastructure layer). + +**Factual grounding (verified in RESEARCH):** +- 13 L1 primitives + 2 L2 modules in `modules/registry.json` (all at + `1.0.0`): alb, cloudfront, dynamodb, ecr, ecs-cluster, ecs-service, + iam-role, kms-key, rds, s3, uptime, vpc, waf + microservice, static- + assets. +- CAP-031: live `terraform apply` against AWS account `581513795199` + (the dev environment, `mode: full`, dev-only per D-209), producing + real ARNs (ALB, ECS, DynamoDB, S3, KMS) at confidence 0.800. This is + a **live AWS apply**, NOT "tested locally" and NOT a "sandbox" + account (the codebase has no sandbox account; the pilot-readiness + policy D-208 blocks placeholder accounts). + +**"sandbox" colloquial term note:** The user explicitly chose to keep +"sandbox" as a colloquial term for the dev AWS account after I flagged +that the codebase fact is "dev account `581513795199`, live apply." +The slide uses "sandbox" (colloquial); the speaker notes carry the +precise facts (dev account, live apply, confidence 0.800). + +**"SPGE constitutional library" unverified-in-repo note:** "SPGE" +and "constitutional library" appear nowhere in the codebase (0 +matches across all `.md`/`.py`/`.json`/`.yml`). The user confirmed it +is an external system the audience recognizes. The slide includes it +as a named next-step integration target, not as a current capability. +The speaker notes do not assert it exists in the codebase. + +The Slide Content Map in PROJECT.md is updated to reflect the new +slide 7 structure. +**Confidence:** 0.90. + +### D-246 — Mermaid diagrams on all 7 slides + +**User request.** Each of the 7 slides gets a leader-friendly, +non-technical mermaid diagram covering its core concept. Different +mermaid shapes per slide for visual variety. + +**Render path (verified):** The local `mmdc` (mermaid-cli) is blocked +by missing Chromium shared libraries (10+ system libs absent, no +root). Resolution: 24 Debian bookworm `.deb` packages fetched from the +Debian mirror, `.so` files extracted to `/home/opencode/tmp/so2/`, +`LD_LIBRARY_PATH` set to include them. Chromium (puppeteer cache at +`/home/opencode/.cache/puppeteer/chrome/.../chrome`) launches with +the vendored libs; `mmdc` renders with the S&P theme (`sp-theme.json`) ++ 2x scale + transparent background. Verified: test diagram renders +to a valid PNG with all 4 S&P colors present. The `.mmd` source files ++ rendered PNGs are committed for reproducibility; a future +environment with system Chromium can re-render via the existing +`scripts/render_slides.sh` mermaid step. + +**Theme:** Inline `%%{init: {...}}%%` directive in each `.mmd` with +S&P theme variables (`primaryColor:#1B1B1B`, `primaryBorderColor: +#D6002A`, `primaryTextColor:#fff`, `secondaryColor:#fff`, +`tertiaryColor:#F0F0F0`, `lineColor:#1B1B1B`) — only the 4 S&P tokens. +Transparent background. No component names, no technical jargon — +concepts only (leader-friendly). + +**Diagram-type-per-slide mapping:** +1. flowchart LR — 3 frictions → Nova absorbs them +2. flowchart TB — Central IT golden image → Nova one layer up +3. flowchart LR with subgraphs — two principles → everything inherits +4. flowchart LR (3 columns) — Live · Attested · Stays human +5. flowchart LR (two halves) — In Nova's lane / Outside Nova's lane +6. timeline — the 18-month shape (α→β→γ→δ) +7. flowchart LR — what works now → next steps arc + +The Slide Content Map in PROJECT.md is updated to include a "Diagram" +field per slide. +**Confidence:** 0.88. + +--- + ## Requirements impact -No requirements are added, removed, or re-scoped by these decisions. -D-241–D-243 are load-bearing context for executing REQ-372.1–.12 as -written. The spec is locked (v1.0, 2026-08-20); no spec text changes. \ No newline at end of file +No requirements are added, removed, or re-scoped by D-241–D-243. +D-244–D-246 are polish-phase decisions that record deviations from +prior binding decisions (G-1) and the Slide Content Map (slide 7). +The spec is locked (v1.0, 2026-08-20); no spec text changes. The +Slide Content Map in PROJECT.md is updated to reflect D-245 (slide 7 +structure) and D-246 (diagram field per slide). \ No newline at end of file diff --git a/docs/presentations/assets/mmd/leadership-slide-1.mmd b/docs/presentations/assets/mmd/leadership-slide-1.mmd new file mode 100644 index 0000000..21d11e0 --- /dev/null +++ b/docs/presentations/assets/mmd/leadership-slide-1.mmd @@ -0,0 +1,23 @@ +%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%% +flowchart LR + subgraph F["The friction"] + F1["Authored by
non-specialists"] + F2["Every change
gated"] + F3["Compliance
checked late"] + end + F1 --- F2 + F2 --- F3 + F --> N + subgraph N["Nova absorbs all three"] + N1["Owned
building blocks"] + N2["Separation
of concerns"] + N3["Attested compliance
up front"] + end + style F fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style N fill:#1B1B1B,color:#fff,stroke:#D6002A + style F1 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style F2 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style F3 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style N1 fill:#1B1B1B,color:#fff,stroke:#D6002A + style N2 fill:#1B1B1B,color:#fff,stroke:#D6002A + style N3 fill:#1B1B1B,color:#fff,stroke:#D6002A \ No newline at end of file diff --git a/docs/presentations/assets/mmd/leadership-slide-2.mmd b/docs/presentations/assets/mmd/leadership-slide-2.mmd new file mode 100644 index 0000000..3d34143 --- /dev/null +++ b/docs/presentations/assets/mmd/leadership-slide-2.mmd @@ -0,0 +1,10 @@ +%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%% +flowchart TB + CIT["Central IT curates
the golden OS image"] + NOVA["Nova curates
the cloud infrastructure"] + CONSUMER["Consumer declares
a contract"] + CIT -->|own, patch, ship| NOVA + NOVA -->|own, patch, attest| CONSUMER + style CIT fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style NOVA fill:#1B1B1B,color:#fff,stroke:#D6002A + style CONSUMER fill:#fff,color:#1B1B1B,stroke:#D6002A \ No newline at end of file diff --git a/docs/presentations/assets/mmd/leadership-slide-3.mmd b/docs/presentations/assets/mmd/leadership-slide-3.mmd new file mode 100644 index 0000000..22aac2e --- /dev/null +++ b/docs/presentations/assets/mmd/leadership-slide-3.mmd @@ -0,0 +1,17 @@ +%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%% +flowchart LR + T1["Sovereign
boundary"] + T2["Lower autonomous
higher attested"] + I1["Four-layer
model"] + I2["HITL
gates"] + I3["Contract
schema"] + T1 --> I1 + T1 --> I3 + T2 --> I2 + T2 --> I3 + T1 -.-> T2 + style T1 fill:#1B1B1B,color:#fff,stroke:#D6002A + style T2 fill:#1B1B1B,color:#fff,stroke:#D6002A + style I1 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style I2 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style I3 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A \ No newline at end of file diff --git a/docs/presentations/assets/mmd/leadership-slide-4.mmd b/docs/presentations/assets/mmd/leadership-slide-4.mmd new file mode 100644 index 0000000..cbff1d7 --- /dev/null +++ b/docs/presentations/assets/mmd/leadership-slide-4.mmd @@ -0,0 +1,9 @@ +%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%% +flowchart LR + L["Live today
41 capabilities
12 domains"] + A["Attested on
promotion
named human approver"] + H["Stays human
by design
escalation below threshold"] + L --> A --> H + style L fill:#1B1B1B,color:#fff,stroke:#D6002A + style A fill:#1B1B1B,color:#fff,stroke:#D6002A + style H fill:#1B1B1B,color:#fff,stroke:#D6002A \ No newline at end of file diff --git a/docs/presentations/assets/mmd/leadership-slide-5.mmd b/docs/presentations/assets/mmd/leadership-slide-5.mmd new file mode 100644 index 0000000..b895af6 --- /dev/null +++ b/docs/presentations/assets/mmd/leadership-slide-5.mmd @@ -0,0 +1,23 @@ +%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%% +flowchart LR + subgraph IN["In Nova's lane"] + I1["Infrastructure
primitives"] + I2["Operational
guardrails"] + I3["CVE response
infra layer"] + end + C["The
contract"] + subgraph OUT["Outside Nova's lane"] + O1["Application
business logic"] + O2["IDE & sprint
workflows"] + O3["AppSec &
dependency review"] + end + IN --- C --- OUT + style IN fill:#1B1B1B,color:#fff,stroke:#D6002A + style OUT fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style C fill:#fff,color:#D6002A,stroke:#D6002A + style I1 fill:#1B1B1B,color:#fff,stroke:#D6002A + style I2 fill:#1B1B1B,color:#fff,stroke:#D6002A + style I3 fill:#1B1B1B,color:#fff,stroke:#D6002A + style O1 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style O2 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style O3 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A \ No newline at end of file diff --git a/docs/presentations/assets/mmd/leadership-slide-6.mmd b/docs/presentations/assets/mmd/leadership-slide-6.mmd new file mode 100644 index 0000000..5f817b7 --- /dev/null +++ b/docs/presentations/assets/mmd/leadership-slide-6.mmd @@ -0,0 +1,11 @@ +%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%% +flowchart LR + A["α
Now → Q4'26
Operating model +
federated governance"] + B["β
Q1'27
Auto-published
infra observability"] + G["γ
Q2'27
Runbook generation
from telemetry"] + D["δ
Q3'27 → Q4'27
Audit ledger
tamper-resistant"] + A --> B --> G --> D + style A fill:#1B1B1B,color:#fff,stroke:#D6002A + style B fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style G fill:#1B1B1B,color:#fff,stroke:#D6002A + style D fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A \ No newline at end of file diff --git a/docs/presentations/assets/mmd/leadership-slide-7.mmd b/docs/presentations/assets/mmd/leadership-slide-7.mmd new file mode 100644 index 0000000..b116909 --- /dev/null +++ b/docs/presentations/assets/mmd/leadership-slide-7.mmd @@ -0,0 +1,13 @@ +%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%% +flowchart LR + W["What works now
L1 & L2 stacks live
confidence 0.800"] + N1["Ingest greenfield
pilot projects"] + N2["Promote sandbox
to production"] + N3["Integrate SPGE
constitutional library"] + N4["Serve as the
infrastructure layer"] + W --> N1 --> N2 --> N3 --> N4 + style W fill:#1B1B1B,color:#fff,stroke:#D6002A + style N1 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style N2 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style N3 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A + style N4 fill:#1B1B1B,color:#fff,stroke:#D6002A \ No newline at end of file diff --git a/docs/presentations/assets/png/leadership-slide-1.png b/docs/presentations/assets/png/leadership-slide-1.png new file mode 100644 index 0000000..d6a037d Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-1.png differ diff --git a/docs/presentations/assets/png/leadership-slide-2.png b/docs/presentations/assets/png/leadership-slide-2.png new file mode 100644 index 0000000..1c4c8a5 Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-2.png differ diff --git a/docs/presentations/assets/png/leadership-slide-3.png b/docs/presentations/assets/png/leadership-slide-3.png new file mode 100644 index 0000000..94a4cef Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-3.png differ diff --git a/docs/presentations/assets/png/leadership-slide-4.png b/docs/presentations/assets/png/leadership-slide-4.png new file mode 100644 index 0000000..24fd85f Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-4.png differ diff --git a/docs/presentations/assets/png/leadership-slide-5.png b/docs/presentations/assets/png/leadership-slide-5.png new file mode 100644 index 0000000..672fb78 Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-5.png differ diff --git a/docs/presentations/assets/png/leadership-slide-6.png b/docs/presentations/assets/png/leadership-slide-6.png new file mode 100644 index 0000000..d19a496 Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-6.png differ diff --git a/docs/presentations/assets/png/leadership-slide-7.png b/docs/presentations/assets/png/leadership-slide-7.png new file mode 100644 index 0000000..4dde44e Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-7.png differ diff --git a/docs/presentations/nova-leadership-deck-marp.md b/docs/presentations/nova-leadership-deck-marp.md index 2af7634..92eebf9 100644 --- a/docs/presentations/nova-leadership-deck-marp.md +++ b/docs/presentations/nova-leadership-deck-marp.md @@ -14,6 +14,11 @@ Vision grounding: [1] citations in slides 3, 5, 7 speaker notes resolve to docs/vision.md (the spec's acdl-vision.md reference). + Diagrams: 7 S&P-themed mermaid diagrams (D-246), one per slide, + rendered via scripts/render_leadership_diagrams.sh (mmdc + vendored + Chromium libs). Sources: assets/mmd/leadership-slide-N.mmd. + PNGs: assets/png/leadership-slide-N.png. + Render: python3 scripts/render_pptx.py docs/presentations/nova-leadership-deck-marp.md \ --output docs/presentations/nova-leadership-deck.pptx Smoke test: bash scripts/check_leadership_deck.sh @@ -42,7 +47,10 @@ style: | @media print { section { overflow: hidden; } } --- -## The friction every delivery team lives today + + + +# The friction every delivery team lives today > *Velocity is up; the coordination surface around each change is up faster.* @@ -52,6 +60,8 @@ style: | > *Nova absorbs all three — owned building blocks, separation of concerns, attested compliance up front.* +![diagram](assets/png/leadership-slide-1.png) + \ No newline at end of file diff --git a/docs/presentations/nova-leadership-deck.pptx b/docs/presentations/nova-leadership-deck.pptx index 20774fb..120bed2 100644 Binary files a/docs/presentations/nova-leadership-deck.pptx and b/docs/presentations/nova-leadership-deck.pptx differ diff --git a/scripts/render_leadership_diagrams.sh b/scripts/render_leadership_diagrams.sh new file mode 100755 index 0000000..cebba80 --- /dev/null +++ b/scripts/render_leadership_diagrams.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# render_leadership_diagrams.sh — render the 7 leadership-deck mermaid +# diagrams to PNG via mmdc. Single-shot helper (NOT a CI gate, per +# D-241/D-246). Uses vendored Chromium shared libraries (D-246). +# +# Usage: bash scripts/render_leadership_diagrams.sh +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +MMD_DIR="docs/presentations/assets/mmd" +PNG_DIR="docs/presentations/assets/png" +THEME_JSON="$MMD_DIR/sp-theme.json" +PUPPETEER_CFG="docs/presentations/assets/puppeteer-config.json" + +# Vendored Chromium shared libraries (D-246) — extracted from Debian +# bookworm .deb packages to /home/opencode/tmp/so2/ +VENDORED_SO="/home/opencode/tmp/so2" +export LD_LIBRARY_PATH="${VENDORED_SO}:${LD_LIBRARY_PATH:-}" + +export CHROME_PATH="/home/opencode/.cache/puppeteer/chrome/linux-152.0.7977.42/chrome-linux64/chrome" + +echo "=== Rendering leadership deck mermaid diagrams (S&P theme, 2x, transparent) ===" +for mmd in "$MMD_DIR"/leadership-slide-*.mmd; do + name="$(basename "$mmd" .mmd)" + png="$PNG_DIR/$name.png" + echo " $name.mmd → $name.png" + npx --yes @mermaid-js/mermaid-cli@11.16.0 \ + --configFile "$THEME_JSON" \ + --puppeteerConfigFile "$PUPPETEER_CFG" \ + -s 2 -b transparent \ + --input "$mmd" \ + --output "$png" 2>&1 | grep -v "^$" || true +done +echo " Rendered $(ls "$PNG_DIR"/leadership-slide-*.png 2>/dev/null | wc -l) leadership PNGs." \ No newline at end of file diff --git a/scripts/render_pptx.py b/scripts/render_pptx.py index 79391ef..a1ddbef 100644 --- a/scripts/render_pptx.py +++ b/scripts/render_pptx.py @@ -31,7 +31,7 @@ try: from pptx.util import Inches, Pt, Emu from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_SHAPE - from pptx.enum.text import PP_ALIGN, MSO_ANCHOR + from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE from pptx.oxml.ns import qn except ImportError: print("ERROR: python-pptx not installed.", file=sys.stderr) @@ -206,11 +206,13 @@ def _add_title_bar(slide): return bar -def _add_footer(slide, text: str): +def _add_footer(slide, text: str, dark_bg: bool = False): """Right-aligned footer textbox at the bottom of every slide. REQ-372.5 / D-242: the python-pptx path does not read the Marp `footer:` directive, so the footer is rendered as a textbox. + F4 polish: dark text on white content slides, light text on the + black cover slide. """ if not text: return None @@ -225,7 +227,7 @@ def _add_footer(slide, text: str): r.text = text r.font.name = FONT_NAME r.font.size = Pt(10) - r.font.color.rgb = GREY_HEADER + r.font.color.rgb = GREY_HEADER if dark_bg else BODY_TEXT return tb @@ -308,9 +310,15 @@ def _strip_inline_emphasis(text: str) -> str: return text -def _inline_runs(p, text: str, *, size: int = 18, base_color: RGBColor = BODY_TEXT): - """Add inline runs to paragraph `p`, rendering **bold** as red strong, - `code` as monospace, *italic* as italic. Other text is plain.""" +def _inline_runs(p, text: str, *, size: int = 18, base_color: RGBColor = BODY_TEXT, + bold_color: RGBColor = None): + """Add inline runs to paragraph `p`, rendering **bold** as strong, + `code` as monospace, *italic* as italic. Other text is plain. + F5(b): bold_color defaults to RED (legacy behavior) but can be + overridden to base_color to de-emphasize bold on slides with many + bold sections (avoids a red wall).""" + if bold_color is None: + bold_color = RED # Tokenize on `**...**`, `*...*`, `` `...` `` tokens = re.split(r"(\*\*[^*]+\*\*|`[^`]+`|\*[^*]+\*)", text) for tok in tokens: @@ -322,7 +330,7 @@ def _inline_runs(p, text: str, *, size: int = 18, base_color: RGBColor = BODY_TE r.font.name = FONT_NAME r.font.size = Pt(size) r.font.bold = True - r.font.color.rgb = RED + r.font.color.rgb = bold_color elif tok.startswith("`") and tok.endswith("`"): r = p.add_run() r.text = tok[1:-1] @@ -586,7 +594,7 @@ def parse_slide(slide_src: str): } -def render_title_slide(prs, slide_data, footer_text: str = ""): +def render_title_slide(prs, slide_data, deck_dir: Path, footer_text: str = ""): slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank _set_bg(slide, BLACK) # red top bar @@ -635,7 +643,12 @@ def render_title_slide(prs, slide_data, footer_text: str = ""): r_b.font.italic = True r_b.font.color.rgb = WHITE cur_top += Inches(0.85) - _add_footer(slide, footer_text) + elif kind == "image": + # image on title slide (D-246 diagrams on cover) + img_path = deck_dir / item[1] + _add_picture(slide, img_path, top=cur_top) + cur_top += Inches(3.6) + _add_footer(slide, footer_text, dark_bg=True) def render_content_slide(prs, slide_data, deck_dir: Path, footer_text: str = ""): @@ -644,63 +657,102 @@ def render_content_slide(prs, slide_data, deck_dir: Path, footer_text: str = "") _add_title_bar(slide) _add_title_text(slide, slide_data["title"], color=RED, size=28, top=0.25, bold=True, height=0.7) - cur_top = Inches(1.05) + # F5(b): if a slide has >=4 lead/bold-prefixed blocks, de-emphasize + # them to black+bold (section labels) instead of red+bold (avoids a + # red wall on milestone-timeline slides like slide 6). Count both + # explicit lead blocks (**...** on own line) and plain blocks that + # start with **bold** inline. + def _is_bold_block(item): + if item[0] == "lead": + return True + if item[0] == "plain" and item[1].lstrip().startswith("**"): + return True + return False + bold_count = sum(1 for item in slide_data["body"] if _is_bold_block(item)) + lead_color = BODY_TEXT if bold_count >= 4 else RED + # F1: vertical balance — pre-compute the content height to center + # the block between the title (bottom ~0.95") and the footer + # (top ~7.12"). Content starts at 1.05" by default; if the total + # content height is short, push it down to vertically center. + INC = {"lead": 0.50, "plain": 0.45, "quote": 0.55, "bullet": 0.40, + "ordered": 0.40, "image": 4.10, "table": 0.42, "code": 0.50, + "benefit": 0.75} + total_h = sum(INC.get(item[0], 0.40) for item in slide_data["body"]) + content_start = 1.05 + available = 7.12 - content_start # footer at 7.12 + if total_h < available - 0.5 and total_h > 0: + offset = (available - total_h) / 2.0 + offset = min(max(offset, 0), 1.2) # cap at +1.2" + content_start = round(content_start + offset, 2) + cur_top = Inches(content_start) for item in slide_data["body"]: kind = item[0] if kind == "lead": tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.5)) tf = tb.text_frame tf.word_wrap = True + tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT p = tf.paragraphs[0] p.alignment = PP_ALIGN.LEFT - _inline_runs(p, item[1], size=18, base_color=RED) - # make the whole lead bold-strong-red + _inline_runs(p, item[1], size=18, base_color=lead_color, + bold_color=lead_color) + # make the whole lead bold for r in p.runs: r.font.bold = True - r.font.color.rgb = RED - cur_top += Inches(0.5) + r.font.color.rgb = lead_color + cur_top += Inches(0.50) elif kind == "plain": tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.4)) tf = tb.text_frame tf.word_wrap = True + tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT p = tf.paragraphs[0] p.alignment = PP_ALIGN.LEFT - _inline_runs(p, item[1], size=18, base_color=BODY_TEXT) - cur_top += Inches(0.4) + _inline_runs(p, item[1], size=18, base_color=BODY_TEXT, + bold_color=lead_color) + cur_top += Inches(0.45) elif kind == "quote": tb = slide.shapes.add_textbox( MARGIN_X + Inches(0.3), cur_top, CONTENT_W - Inches(0.3), Inches(0.6) ) tf = tb.text_frame tf.word_wrap = True + tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT p = tf.paragraphs[0] p.alignment = PP_ALIGN.LEFT - _inline_runs(p, item[1], size=18, base_color=GREY_TEXT) + _inline_runs(p, item[1], size=18, base_color=BODY_TEXT) # italicize the whole blockquote for r in p.runs: r.font.italic = True - r.font.color.rgb = GREY_TEXT - cur_top += Inches(0.6) + r.font.color.rgb = BODY_TEXT + cur_top += Inches(0.55) elif kind == "bullet": # accumulate consecutive bullets into one text frame # (handled below in a second pass; we render single here as fallback) tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.35)) tf = tb.text_frame tf.word_wrap = True + tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT p = tf.paragraphs[0] p.alignment = PP_ALIGN.LEFT p.level = item[1] r = p.add_run() - prefix = "• " if item[1] == 0 else ("– " if item[1] == 1 else "· ") - r.text = prefix + _strip_inline_emphasis(item[2]) + # F3: if the bullet text starts with →, omit the bullet glyph + stripped_text = _strip_inline_emphasis(item[2]) + if item[1] == 0: + prefix = "" if stripped_text.lstrip().startswith("→") else "• " + else: + prefix = "– " if item[1] == 1 else "· " + r.text = prefix + stripped_text r.font.name = FONT_NAME r.font.size = Pt(18 if item[1] == 0 else 16) r.font.color.rgb = BODY_TEXT - cur_top += Inches(0.35) + cur_top += Inches(0.40) elif kind == "ordered": tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.35)) tf = tb.text_frame tf.word_wrap = True + tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT p = tf.paragraphs[0] p.alignment = PP_ALIGN.LEFT r = p.add_run() @@ -708,7 +760,7 @@ def render_content_slide(prs, slide_data, deck_dir: Path, footer_text: str = "") r.font.name = FONT_NAME r.font.size = Pt(18) r.font.color.rgb = BODY_TEXT - cur_top += Inches(0.35) + cur_top += Inches(0.40) elif kind == "image": img_path = deck_dir / item[1] _add_picture(slide, img_path, top=cur_top) @@ -721,6 +773,7 @@ def render_content_slide(prs, slide_data, deck_dir: Path, footer_text: str = "") tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.6)) tf = tb.text_frame tf.word_wrap = True + tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT p = tf.paragraphs[0] p.alignment = PP_ALIGN.LEFT r = p.add_run() @@ -751,7 +804,7 @@ def render_deck(md_path: Path, pptx_path: Path): is_title = (idx == 0) or data["is_title_class"] or data["title_is_h1"] # The appendix is a content slide (rendered normally) if idx == 0 and (data["title_is_h1"] or data["is_title_class"]): - render_title_slide(prs, data, footer_text=footer_text) + render_title_slide(prs, data, deck_dir, footer_text=footer_text) elif data["is_title_class"] and not data["title_is_h1"] and idx != 0: # Marp _class: title on a non-H1 slide (e.g., appendix) — render as # content but with a title-style bar. Keep it simple: content slide.