test(P5): ci + tests + readme for single-doc dual-pptx pipeline (REQ-273,274,275)
CI workflows: install python-pptx, pin CLI versions, stage both PPTX + inlined HTML. test_slides_pipeline.py: inverted theme assertion (now default+inline), deleted source-md tests, added 8 new tests (penetrate absence, image inlining, python-pptx, benefit class, single source, speaker-notes comments, default theme, css retained). New test_pptx_generator.py: slide count, title colors, slide titles, table rendering, image embedding, benefit callout. README rewritten for 3-step single-document + dual-PPTX + image-inlining pipeline. ---ci--- project: acdl phase: 5 milestone: v1.23 status: execute phase_role: execution ---/ci---
This commit is contained in:
@@ -1,11 +1,15 @@
|
|||||||
# Nova Slides Render — re-renders presentation deck when source files change.
|
# Nova Slides Render — re-renders presentation deck when source files change.
|
||||||
|
# REQ-273: install python-pptx, pin CLI versions, stage HTML + both PPTX +
|
||||||
|
# base64-inlined images.
|
||||||
name: Nova Slides Render
|
name: Nova Slides Render
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- 'docs/presentations/**'
|
- 'docs/presentations/**'
|
||||||
- 'scripts/render_slides.sh'
|
- 'scripts/render_slides.sh'
|
||||||
- 'assets/nova-sp-theme.css'
|
- 'scripts/inline_images.py'
|
||||||
|
- 'scripts/render_pptx.py'
|
||||||
|
- 'pyproject.toml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -16,16 +20,24 @@ jobs:
|
|||||||
with: { fetch-depth: 0 }
|
with: { fetch-depth: 0 }
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with: { node-version: '20' }
|
with: { node-version: '20' }
|
||||||
- name: Install Chrome
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: Install python-pptx (slides extra)
|
||||||
|
run: pip install -e ".[slides]"
|
||||||
|
- name: Install + pin render CLIs
|
||||||
run: |
|
run: |
|
||||||
npx --yes @marp-team/marp-cli@latest --version
|
npx --yes @marp-team/marp-cli@4.5.0 --version
|
||||||
npx --yes @mermaid-js/mermaid-cli --version
|
npx --yes @mermaid-js/mermaid-cli@11.16.0 --version
|
||||||
- name: Render slides
|
- name: Render slides
|
||||||
run: bash scripts/render_slides.sh
|
run: bash scripts/render_slides.sh
|
||||||
- name: Commit rendered artifacts
|
- name: Commit rendered artifacts
|
||||||
run: |
|
run: |
|
||||||
git config user.name "nova-slides-bot"
|
git config user.name "nova-slides-bot"
|
||||||
git config user.email "bot@nova.local"
|
git config user.email "bot@nova.local"
|
||||||
git add docs/presentations/*.html docs/presentations/*.pptx docs/presentations/assets/png/*.png
|
git add docs/presentations/*.html \
|
||||||
|
docs/presentations/*.pptx \
|
||||||
|
docs/presentations/*-python.pptx \
|
||||||
|
docs/presentations/assets/png/*.png
|
||||||
git diff --cached --quiet || git commit -m "chore(slides): re-render deck [skip ci]"
|
git diff --cached --quiet || git commit -m "chore(slides): re-render deck [skip ci]"
|
||||||
git push
|
git push
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
# Nova Slides Render — re-renders presentation deck when source files change.
|
# Nova Slides Render — re-renders presentation deck when source files change.
|
||||||
|
# REQ-273: install python-pptx, pin CLI versions, stage HTML + both PPTX +
|
||||||
|
# base64-inlined images.
|
||||||
name: Nova Slides Render
|
name: Nova Slides Render
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- 'docs/presentations/**'
|
- 'docs/presentations/**'
|
||||||
- 'scripts/render_slides.sh'
|
- 'scripts/render_slides.sh'
|
||||||
- 'assets/nova-sp-theme.css'
|
- 'scripts/inline_images.py'
|
||||||
|
- 'scripts/render_pptx.py'
|
||||||
|
- 'pyproject.toml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -16,16 +20,24 @@ jobs:
|
|||||||
with: { fetch-depth: 0 }
|
with: { fetch-depth: 0 }
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with: { node-version: '20' }
|
with: { node-version: '20' }
|
||||||
- name: Install Chrome
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: Install python-pptx (slides extra)
|
||||||
|
run: pip install -e ".[slides]"
|
||||||
|
- name: Install + pin render CLIs
|
||||||
run: |
|
run: |
|
||||||
npx --yes @marp-team/marp-cli@latest --version
|
npx --yes @marp-team/marp-cli@4.5.0 --version
|
||||||
npx --yes @mermaid-js/mermaid-cli --version
|
npx --yes @mermaid-js/mermaid-cli@11.16.0 --version
|
||||||
- name: Render slides
|
- name: Render slides
|
||||||
run: bash scripts/render_slides.sh
|
run: bash scripts/render_slides.sh
|
||||||
- name: Commit rendered artifacts
|
- name: Commit rendered artifacts
|
||||||
run: |
|
run: |
|
||||||
git config user.name "nova-slides-bot"
|
git config user.name "nova-slides-bot"
|
||||||
git config user.email "bot@nova.local"
|
git config user.email "bot@nova.local"
|
||||||
git add docs/presentations/*.html docs/presentations/*.pptx docs/presentations/assets/png/*.png
|
git add docs/presentations/*.html \
|
||||||
|
docs/presentations/*.pptx \
|
||||||
|
docs/presentations/*-python.pptx \
|
||||||
|
docs/presentations/assets/png/*.png
|
||||||
git diff --cached --quiet || git commit -m "chore(slides): re-render deck [skip ci]"
|
git diff --cached --quiet || git commit -m "chore(slides): re-render deck [skip ci]"
|
||||||
git push
|
git push
|
||||||
+177
-145
@@ -2,147 +2,184 @@
|
|||||||
|
|
||||||
Leadership-facing presentation decks for the Nova platform.
|
Leadership-facing presentation decks for the Nova platform.
|
||||||
|
|
||||||
## The 4-step slide creation process
|
## The 3-step slide creation process
|
||||||
|
|
||||||
Every presentation in this folder is produced by the same four-step process.
|
Every presentation in this folder is produced by the same three-step
|
||||||
**Never edit the Marp deck, the PPTX, or the talking points directly** —
|
process. **Never edit the rendered HTML, either PPTX, or the talking
|
||||||
always start from the full markdown source of truth (Step 1), synthesize the
|
points directly** — always start from the Marp deck source of truth
|
||||||
Marp deck (Step 2), export to HTML + PPTX (Step 3), then distill the talking
|
(Step 1), render it (Step 2), then distill the talking points (Step 3).
|
||||||
points (Step 4). This keeps a reviewable, plain-text source of truth for
|
This keeps a reviewable, plain-text source of truth for every deck and a
|
||||||
every deck and a presenter-ready cue sheet for delivery.
|
presenter-ready cue sheet for delivery.
|
||||||
|
|
||||||
```
|
```
|
||||||
Step 1: full markdown Step 2: Marp deck Step 3: HTML + PPTX Step 4: Talking points
|
Step 1: Author the deck Step 2: Render Step 3: Talking points
|
||||||
(source of truth) ──► (lean, 21 slides) ──► (rendered) ──► (presenter cues)
|
(source of truth) ──► (HTML + dual PPTX) ──► (presenter cues)
|
||||||
*.md *-marp.md *.html / *.pptx *-talking-points.md
|
*-marp.md *.html *-talking-points.md
|
||||||
+ speaker notes + embedded PNG diagrams + 3-6 bullets per slide
|
+ ## Slide N — Title + mermaid PNGs + 3-6 bullets per slide
|
||||||
+ mermaid code blocks + Marp frontmatter + key takeaway per slide
|
+ <!-- Speaker notes: --> + MARP PPTX (image-of-slide) + key takeaway per slide
|
||||||
+ no speaker notes + indexed by Marp slide #
|
+ <!-- Talking points: --> + python PPTX (structured) + indexed by slide #
|
||||||
+ no maturity badges + content distilled from Step 1
|
+ <div class="benefit"> + base64-inlined HTML + content distilled from
|
||||||
+ no version in footer
|
+ embedded PNG diagrams (self-contained) the Marp deck
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 1 — Full markdown (source of truth)
|
### Step 1 — Author the deck (source of truth)
|
||||||
|
|
||||||
**File convention:** `<deck-name>.md` (e.g. `nova-autonomous-cloud-delivery.md`).
|
**File convention:** `<deck-name>-marp.md` (e.g.
|
||||||
|
`nova-autonomous-cloud-delivery-marp.md`).
|
||||||
|
|
||||||
Write the complete deck as a standard markdown file. This is the **source of
|
This is the **sole source of truth** — the Marp deck that is both authored
|
||||||
truth** — it contains:
|
and rendered. It contains:
|
||||||
|
|
||||||
- Every slide as an `## Slide N — Title` H2 section.
|
- **Marp frontmatter** at the top: `marp: true`, `theme: default`,
|
||||||
|
`paginate: true`, `size: 16x9`, a header/footer, and an inline `style:`
|
||||||
|
block carrying the S&P palette (`#D6002A` red, `#1B1B1B` black, the
|
||||||
|
`section.title` rule). The styling is **inline** — no standalone theme
|
||||||
|
CSS is loaded at render time.
|
||||||
|
- Every slide as an `## Slide N — Title` (or `## Appendix A1 — Title`) H2
|
||||||
|
section. The H1 title slide precedes slide 1.
|
||||||
- Tight bullets with leadership-relevant content.
|
- Tight bullets with leadership-relevant content.
|
||||||
- A `> **Speaker notes:**` block at the end of each slide with the nuance,
|
- **Speaker notes** as `<!-- Speaker notes: ... -->` HTML comments at the
|
||||||
the "who cares and why," and the honesty caveats.
|
end of each slide. Marp excludes HTML comments from the rendered slide;
|
||||||
- Mermaid diagrams as ```` ```mermaid ```` fenced code blocks (these render
|
they are for authors/presenters only.
|
||||||
on GitHub/Pages but not in Marp — Step 2 converts them to images).
|
- **Talking points** as `<!-- Talking points: ... -->` HTML comments (also
|
||||||
- An honest "shipped vs. deferred" framing: every "available today" claim is
|
excluded from rendering — Step 3 mirrors them into a standalone cue
|
||||||
grounded in shipped/verified work; every "deferred" item is explicitly
|
sheet).
|
||||||
|
- **Benefit callouts** as `<div class="benefit">...</div>` (styled by the
|
||||||
|
inline `style:` block — italic, S&P-red top border). No `**Benefit:**`
|
||||||
|
text prefixes.
|
||||||
|
- Mermaid diagrams **pre-rendered to PNG** under `assets/png/` and embedded
|
||||||
|
with `` (or `h:480 class:tall` for tall
|
||||||
|
images). The `.mmd` sources live under `assets/mmd/`.
|
||||||
|
- **No maturity badges**, **no version in the footer**, **no internal
|
||||||
|
decision/requirement IDs or `.py` file paths** in the slide bodies
|
||||||
|
(those live in the `.ciagent/` files only; speaker-note HTML comments are
|
||||||
|
exempt).
|
||||||
|
- An honest "shipped vs. deferred" framing: every "available today" claim
|
||||||
|
is grounded in shipped/verified work; every "deferred" item is explicitly
|
||||||
marked with the blocking work in plain language.
|
marked with the blocking work in plain language.
|
||||||
|
|
||||||
**Why this file is the source of truth:** it is reviewable in any markdown
|
**Why the Marp deck is the source of truth:** it is reviewable in any
|
||||||
viewer, diffs cleanly in git, and carries the full reasoning (speaker notes)
|
markdown viewer, diffs cleanly in git, and carries the full reasoning
|
||||||
that a presenter needs. The Marp deck and PPTX are *derived artifacts* — if a
|
(speaker notes) that a presenter needs. The HTML and PPTX are *derived
|
||||||
fact is wrong, fix it here and re-run Steps 2 and 3.
|
artifacts* — if a fact is wrong, fix it here and re-run Step 2.
|
||||||
|
|
||||||
### Step 2 — Marp deck synthesis
|
> **`nova-sp-theme.css` is RETIRED from render.** The standalone theme
|
||||||
|
> stylesheet under `assets/nova-sp-theme.css` is kept as a **reference
|
||||||
|
> only** and is **not loaded at render time**. The live styling is the
|
||||||
|
> inline `style:` block in the `-marp.md` frontmatter. Do NOT pass the CSS
|
||||||
|
> via `--theme`; it is not in the render path.
|
||||||
|
|
||||||
**File convention:** `<deck-name>-marp.md` (e.g. `nova-autonomous-cloud-delivery-marp.md`).
|
### Step 2 — Render (HTML + dual PPTX)
|
||||||
|
|
||||||
Synthesize the full markdown into a lean Marp deck:
|
`bash scripts/render_slides.sh [deck-name]` renders the Marp deck
|
||||||
|
end-to-end:
|
||||||
|
|
||||||
- **Marp frontmatter** at the top: `marp: true`, `theme: nova-sp`,
|
1. **Mermaid PNGs** — each `assets/mmd/*.mmd` → `assets/png/*.png`
|
||||||
`paginate: true`, `size: 16x9`, a header/footer, and an inline `style:`
|
(S&P-themed via `sp-theme.json`, 2x scale, transparent background).
|
||||||
block for fonts, colors, tables.
|
2. **MARP HTML** — `*-marp.md` → `*.html` (S&P inline style, Marp default
|
||||||
- **No speaker notes.** The Marp deck is what the audience sees; the
|
theme). Pinned `@marp-team/marp-cli@4.5.0`.
|
||||||
speaker notes live only in the Step 1 source of truth.
|
3. **MARP PPTX** — `*-marp.md` → `*.pptx` (image-of-slide PPTX; the primary
|
||||||
- **Mermaid diagrams → PNG images.** Marp does not render mermaid fenced
|
release attachment).
|
||||||
blocks natively. Extract each mermaid block from Step 1 into a `.mmd`
|
4. **Inline images** — `scripts/inline_images.py` rewrites the HTML to
|
||||||
source file under `assets/mmd/`, render it to PNG under `assets/png/`,
|
base64-embed every `assets/` image so the HTML is self-contained (no
|
||||||
and embed it with ``.
|
external asset folder needed for redistribution).
|
||||||
- **`<!-- _class: title -->` + `<!-- _paginate: false -->`** on title and
|
5. **python PPTX** — `scripts/render_pptx.py` produces a second,
|
||||||
closing slides for the dark-background title style.
|
structured, editable PPTX (`*-python.pptx`) with native text boxes,
|
||||||
- **No maturity badges.** The deck no longer uses `<span class="badge">`
|
native tables, embedded pictures, and italic benefit callouts.
|
||||||
spans. Deferred items are named in plain language with their blocking
|
6. **Stage** — all rendered artifacts (PNGs + HTML + both PPTX) are
|
||||||
work, not tagged with a badge.
|
`git add`-ed for commit.
|
||||||
- **No version in the footer.** The footer carries the deck title only.
|
|
||||||
- **Tighter prose** than Step 1 — strip the speaker-note nuance; keep the
|
|
||||||
leadership-relevant selling points.
|
|
||||||
|
|
||||||
### Step 3 — Render to HTML and PPTX
|
|
||||||
|
|
||||||
Both formats are derived from the Marp deck. **HTML is committed to the repo**
|
|
||||||
(viewable in any browser, self-contained with base64-embedded images). **PPTX
|
|
||||||
is also committed to the repo** as a first-class binary artifact and is
|
|
||||||
attached to the phase's release via `scripts/attach_release_asset.py`.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
|
bash scripts/render_slides.sh nova-autonomous-cloud-delivery
|
||||||
npx --yes @marp-team/marp-cli@latest --allow-local-files \
|
|
||||||
docs/presentations/<deck-name>-marp.md \
|
|
||||||
-o docs/presentations/<deck-name>.html
|
|
||||||
```
|
```
|
||||||
|
|
||||||
HTML export inlines images as base64 data URIs. PPTX export requires
|
Both the HTML and both PPTX files are committed to the repo; the MARP
|
||||||
`--allow-local-files` so the local PNG diagrams are embedded in the file.
|
PPTX is also attached to the phase's release via
|
||||||
The render + commit + attach pipeline is automated by `scripts/render_slides.sh`.
|
`scripts/attach_release_asset.py`.
|
||||||
|
|
||||||
### Step 4 — Talking points (presenter cues)
|
#### Dual-PPTX output
|
||||||
|
|
||||||
|
| PPTX | File | Render | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **MARP PPTX** | `*.pptx` | `@marp-team/marp-cli` (Chrome screenshot of each slide) | Image-of-slide; the primary release attachment (pixel-perfect, not editable) |
|
||||||
|
| **python PPTX** | `*-python.pptx` | `scripts/render_pptx.py` (python-pptx) | Structured, editable PPTX (native text boxes, tables, pictures) for comparison/editing |
|
||||||
|
|
||||||
|
### Step 3 — Talking points (presenter cues)
|
||||||
|
|
||||||
**File convention:** `<deck-name>-talking-points.md` (e.g.
|
**File convention:** `<deck-name>-talking-points.md` (e.g.
|
||||||
`nova-autonomous-cloud-delivery-talking-points.md`).
|
`nova-autonomous-cloud-delivery-talking-points.md`).
|
||||||
|
|
||||||
Distill the source of truth (Step 1) into presenter-ready cues, indexed by
|
Distill the deck's `<!-- Talking points: -->` HTML comments into
|
||||||
the Marp deck (Step 2) slide structure:
|
presenter-ready cues, indexed by the Marp deck (Step 1) slide structure:
|
||||||
|
|
||||||
- **One section per Marp slide** — `## Slide N — Title`, matching the Marp
|
- **One section per Marp slide** — `## Slide N — Title`, matching the Marp
|
||||||
deck's 20 main + 1 appendix slide structure exactly.
|
deck's 20 main + 1 appendix slide structure exactly.
|
||||||
- **3-6 talking point bullets per slide** — punchy, actionable cues distilled
|
- **3-6 talking point bullets per slide** — punchy, actionable cues
|
||||||
from the source markdown's speaker notes.
|
distilled from the Marp deck's `<!-- Talking points: -->` comments.
|
||||||
- **Key takeaway per slide** — the one memorable thing the audience should
|
- **Key takeaway per slide** — the one memorable thing the audience should
|
||||||
walk away with from that slide.
|
walk away with from that slide.
|
||||||
- **No content duplication** — the talking points reference the Marp slides
|
- **No content duplication** — the talking points reference the Marp
|
||||||
for visual context and the source markdown for full detail.
|
slides for visual context.
|
||||||
|
|
||||||
## Directory layout
|
## Directory layout
|
||||||
|
|
||||||
```
|
```
|
||||||
docs/presentations/
|
docs/presentations/
|
||||||
├── README.md ← this file
|
├── README.md ← this file
|
||||||
├── nova-autonomous-cloud-delivery.md ← Step 1: full source of truth (20 main slides + speaker notes)
|
├── nova-autonomous-cloud-delivery-marp.md ← Step 1: sole source of truth (title + 20 main + 1 appendix = 22 slides + speaker notes + talking points)
|
||||||
├── nova-autonomous-cloud-delivery-marp.md ← Step 2: Marp deck (20 main + 1 appendix = 21 slides)
|
├── nova-autonomous-cloud-delivery.html ← Step 2: rendered HTML (committed, S&P inline style, base64-inlined images)
|
||||||
├── nova-autonomous-cloud-delivery.html ← Step 3: rendered HTML (committed, S&P-themed)
|
├── nova-autonomous-cloud-delivery.pptx ← Step 2: MARP PPTX (image-of-slide, primary release attachment)
|
||||||
├── nova-autonomous-cloud-delivery.pptx ← Step 3: rendered PPTX (committed, S&P-themed)
|
├── nova-autonomous-cloud-delivery-python.pptx ← Step 2: python-pptx (structured, editable)
|
||||||
├── nova-autonomous-cloud-delivery-talking-points.md ← Step 4: presenter cues (19 sections)
|
├── nova-autonomous-cloud-delivery-talking-points.md ← Step 3: presenter cues (21 sections)
|
||||||
└── assets/
|
└── assets/
|
||||||
├── nova-sp-theme.css ← S&P Global Energy Marp theme (all slide chrome)
|
├── nova-sp-theme.css ← RETIRED from render — reference only (not loaded; live styling is the inline `style:` block)
|
||||||
├── puppeteer-config.json ← no-sandbox config for mmdc
|
├── puppeteer-config.json ← no-sandbox config for mmdc
|
||||||
├── mmd/ ← mermaid source files (Step 2 input)
|
├── mmd/ ← mermaid source files (Step 2 input)
|
||||||
│ ├── sp-theme.json ← S&P Red/Black/White theme (mermaid-cli --configFile)
|
│ ├── sp-theme.json ← S&P Red/Black/White theme (mermaid-cli --configFile)
|
||||||
│ └── ... (per-slide .mmd files)
|
│ └── ... (per-slide .mmd files)
|
||||||
└── png/ ← rendered mermaid PNGs (committed, S&P-themed)
|
└── png/ ← rendered mermaid PNGs (committed, S&P-themed, 2x, transparent)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Tooling & scripts
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `scripts/render_slides.sh` | End-to-end render: mermaid PNGs → MARP HTML + PPTX → base64-inlined HTML → python-pptx PPTX → stage all artifacts. Pinned `@marp-team/marp-cli@4.5.0` + `@mermaid-js/mermaid-cli@11.16.0`. |
|
||||||
|
| `scripts/inline_images.py` | Rewrites the rendered HTML to base64-embed every `assets/` image (self-contained HTML for redistribution). |
|
||||||
|
| `scripts/render_pptx.py` | Produces the structured, editable `*-python.pptx` (native text boxes, tables, pictures, italic benefit callouts) via `python-pptx`. |
|
||||||
|
| `scripts/attach_release_asset.py` | Attaches the MARP PPTX to the phase's release. |
|
||||||
|
|
||||||
|
| Dependency | Where declared | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `@marp-team/marp-cli@4.5.0` | `scripts/render_slides.sh` (pinned) | Marp → HTML + PPTX |
|
||||||
|
| `@mermaid-js/mermaid-cli@11.16.0` | `scripts/render_slides.sh` (pinned) | Mermaid → PNG |
|
||||||
|
| `python-pptx>=0.6.23` | `pyproject.toml` `[project.optional-dependencies] slides` | Structured PPTX (`pip install -e ".[slides]"`) |
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
### Appendix structure
|
### Slide structure
|
||||||
|
|
||||||
Each Marp deck has **20 main slides + 1 appendix slide**. The main 20 are the
|
Each Marp deck has **1 title slide + 20 main slides + 1 appendix slide = 22
|
||||||
presentation; the appendix is for Q&A backup. (v1.22 split slides 3 and 8
|
rendered slides** (21 `## ` sections + the H1 title slide). The main 20
|
||||||
to relieve overflow, increasing the count from 18 to 20.)
|
are the presentation; the appendix is for Q&A backup. (v1.22 split slides
|
||||||
|
3 and 8 to relieve overflow, increasing the main count from 18 to 20.)
|
||||||
|
|
||||||
|
- **Title slide** (H1): `<!-- _class: title -->` + `<!-- _paginate: false -->`
|
||||||
|
for the dark-background title style (S&P-red top border on black).
|
||||||
- **Main slides** (1-20): the story arc — Problem → Solution → Proof →
|
- **Main slides** (1-20): the story arc — Problem → Solution → Proof →
|
||||||
Roadmap + Ask. These are what the audience sees during the talk.
|
Roadmap + Ask. These are what the audience sees during the talk.
|
||||||
- **Appendix slide** (A1): the Metrics Glossary — detail-heavy reference for
|
- **Appendix slide** (A1): the Metrics Glossary — detail-heavy reference
|
||||||
Q&A.
|
for Q&A.
|
||||||
|
|
||||||
### Honesty framing
|
### Honesty framing
|
||||||
|
|
||||||
Every capability claim in the deck is grounded, derived, or honestly
|
Every capability claim in the deck is grounded, derived, or honestly
|
||||||
deferred with its blocking work named in plain language. Internal provenance
|
deferred with its blocking work named in plain language. Internal
|
||||||
(decision IDs, requirement IDs, internal file paths) is kept out of the
|
provenance (decision IDs, requirement IDs, internal file paths) is kept
|
||||||
audience-facing slides — those live in the `.ciagent/` files only. When in
|
out of the audience-facing slide bodies — those live in the `.ciagent/`
|
||||||
doubt, check `.ciagent/ROADMAP.md` and the milestone status in
|
files only (and may appear inside `<!-- ... -->` speaker-note comments,
|
||||||
`.ciagent/PROJECT.md`.
|
which Marp excludes from the rendered slide). When in doubt, check
|
||||||
|
`.ciagent/ROADMAP.md` and the milestone status in `.ciagent/PROJECT.md`.
|
||||||
|
|
||||||
### Audience
|
### Audience
|
||||||
|
|
||||||
@@ -156,76 +193,70 @@ Head of Infrastructure, Head of DevOps. The framing rules:
|
|||||||
outcome; the mechanism follows.
|
outcome; the mechanism follows.
|
||||||
- **Security, remediation velocity, reliability, lead time, observability,
|
- **Security, remediation velocity, reliability, lead time, observability,
|
||||||
citizen developer** are the themes — not implementation details.
|
citizen developer** are the themes — not implementation details.
|
||||||
- **"Infrastructure operations become visible"** is the recurring theme across
|
- **"Infrastructure operations become visible"** is the recurring theme
|
||||||
the deck.
|
across the deck.
|
||||||
|
|
||||||
### Diagrams
|
### Diagrams
|
||||||
|
|
||||||
Mermaid diagrams in the Step 1 source use the repo's existing `flowchart`
|
Mermaid diagrams are authored as `assets/mmd/*.mmd` source files and
|
||||||
style (renders on GitHub/Pages). For the Marp deck (Step 2):
|
rendered to PNG under `assets/png/`:
|
||||||
|
|
||||||
1. Extract the mermaid block into `assets/mmd/<deck>-<slide>-<name>.mmd`.
|
1. Author the mermaid block as `assets/mmd/<deck>-<slide>-<name>.mmd`.
|
||||||
2. Use **horizontal layouts** (`flowchart LR`) or **subgraph row-wrapping**
|
2. Use **horizontal layouts** (`flowchart LR`) or **subgraph row-wrapping**
|
||||||
for wide diagrams so the PNG fits a 16:9 slide without shrinking to
|
for wide diagrams so the PNG fits a 16:9 slide without shrinking to
|
||||||
illegibility.
|
illegibility.
|
||||||
3. Render with a 2x scale factor and transparent background for crisp slides.
|
3. Render with a 2x scale factor and transparent background for crisp
|
||||||
4. Embed with `` (or `h:320` for tall images).
|
slides (`scripts/render_slides.sh` does this with the S&P theme JSON).
|
||||||
|
4. Embed with `` (or `h:480 class:tall`
|
||||||
|
for tall images).
|
||||||
|
5. The render pipeline base64-inlines the PNGs into the committed HTML so
|
||||||
|
the HTML is self-contained.
|
||||||
|
|
||||||
## Build commands
|
## Build commands
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Node.js + npx (for `@marp-team/marp-cli` and `@mermaid-js/mermaid-cli`)
|
- **Node.js + npx** (for `@marp-team/marp-cli` and `@mermaid-js/mermaid-cli`)
|
||||||
- A Chrome/Chromium binary (Marp PPTX export requires it)
|
- **A Chrome/Chromium binary** (Marp PPTX export requires it)
|
||||||
|
- **Python 3.10+** with the `slides` extra: `pip install -e ".[slides]"`
|
||||||
|
(installs `python-pptx>=0.6.23`)
|
||||||
|
|
||||||
This environment has a working Chromium at:
|
This environment has a working Chromium at:
|
||||||
`/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome`
|
`/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome`
|
||||||
|
|
||||||
### Render all mermaid diagrams to PNG
|
### Render the deck (HTML + dual PPTX + inlined images)
|
||||||
|
|
||||||
```bash
|
|
||||||
cd docs/presentations/assets
|
|
||||||
for f in mmd/*.mmd; do
|
|
||||||
name=$(basename "$f" .mmd)
|
|
||||||
PUPPETEER_EXECUTABLE_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
|
|
||||||
npx --yes @mermaid-js/mermaid-cli@latest \
|
|
||||||
-i "$f" -o "png/$name.png" \
|
|
||||||
-p puppeteer-config.json -s 2 -b transparent \
|
|
||||||
--configFile mmd/sp-theme.json
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
### Render a Marp deck to HTML + PPTX (committed artifacts)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/render_slides.sh nova-autonomous-cloud-delivery
|
bash scripts/render_slides.sh nova-autonomous-cloud-delivery
|
||||||
```
|
```
|
||||||
|
|
||||||
This renders all mermaid PNGs, the HTML, and the PPTX, and stages them for
|
This renders all mermaid PNGs, the HTML (with base64-inlined images), the
|
||||||
commit. The `--allow-local-files` flag is required so local PNG diagrams are
|
MARP PPTX, and the python-pptx PPTX, and stages them for commit. Both
|
||||||
embedded. Both HTML and PPTX are committed to the repo; the PPTX is also
|
HTML and both PPTX files are committed to the repo; the MARP PPTX is also
|
||||||
attached to the phase's release.
|
attached to the phase's release.
|
||||||
|
|
||||||
## Adding a new presentation
|
## Adding a new presentation
|
||||||
|
|
||||||
1. **Write the full markdown** as `<deck-name>.md` following the
|
1. **Author the Marp deck** as `<deck-name>-marp.md` — frontmatter
|
||||||
`## Slide N — Title` + `> **Speaker notes:**` structure. This is the
|
(`marp: true`, `theme: default`, `paginate: true`, `size: 16x9`, an
|
||||||
source of truth.
|
inline `style:` block with the S&P palette), `## Slide N — Title`
|
||||||
2. **Extract any mermaid diagrams** into `assets/mmd/<deck-name>-<slide>-<name>.mmd`
|
sections, `<!-- Speaker notes: -->` + `<!-- Talking points: -->` HTML
|
||||||
and render them to `assets/png/` (command above).
|
comments, and `<div class="benefit">` callouts. This is the sole source
|
||||||
3. **Synthesize the Marp deck** as `<deck-name>-marp.md` with frontmatter,
|
of truth.
|
||||||
no speaker notes, embedded PNGs, and no badges.
|
2. **Author any mermaid diagrams** as `assets/mmd/<deck-name>-<slide>-<name>.mmd`
|
||||||
4. **Render to HTML + PPTX** via `scripts/render_slides.sh <deck-name>` and
|
(Step 2 renders them to `assets/png/`).
|
||||||
commit both to `docs/presentations/`.
|
3. **Render** via `bash scripts/render_slides.sh <deck-name>` — this
|
||||||
5. **Distill the talking points** as `<deck-name>-talking-points.md` — one
|
produces the HTML (base64-inlined), the MARP PPTX, and the python-pptx
|
||||||
section per Marp slide, 3-6 talking point bullets + key takeaway, content
|
PPTX, and stages all of them (plus the PNGs) for commit.
|
||||||
distilled from the source markdown (Step 1), indexed by the Marp deck
|
4. **Distill the talking points** as `<deck-name>-talking-points.md` — one
|
||||||
(Step 2) slide structure.
|
section per Marp slide, 3-6 talking point bullets + key takeaway,
|
||||||
6. **Verify** the PPTX slide count and that media files are embedded:
|
content distilled from the Marp deck's `<!-- Talking points: -->`
|
||||||
|
comments, indexed by the Marp deck slide structure.
|
||||||
|
5. **Verify** the PPTX slide count and that media files are embedded:
|
||||||
```bash
|
```bash
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import zipfile, re
|
import zipfile, re
|
||||||
with zipfile.ZipFile('<output>.pptx') as z:
|
with zipfile.ZipFile('docs/presentations/<deck-name>.pptx') as z:
|
||||||
slides = [n for n in z.namelist() if re.match(r'ppt/slides/slide\d+\.xml$', n)]
|
slides = [n for n in z.namelist() if re.match(r'ppt/slides/slide\d+\.xml$', n)]
|
||||||
media = [n for n in z.namelist() if n.startswith('ppt/media/')]
|
media = [n for n in z.namelist() if n.startswith('ppt/media/')]
|
||||||
print(f'{len(slides)} slides, {len(media)} media files')
|
print(f'{len(slides)} slides, {len(media)} media files')
|
||||||
@@ -234,16 +265,17 @@ attached to the phase's release.
|
|||||||
|
|
||||||
## Current decks
|
## Current decks
|
||||||
|
|
||||||
| Deck | Source of truth (Step 1) | Marp deck (Step 2) | Rendered HTML + PPTX (Step 3) | Talking points (Step 4) | Slides | Audience |
|
| Deck | Source of truth (Step 1) | Rendered HTML + dual PPTX (Step 2) | Talking points (Step 3) | Slides | Audience |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| Nova — The Autonomous Cloud Delivery Platform | `nova-autonomous-cloud-delivery.md` | `nova-autonomous-cloud-delivery-marp.md` | `nova-autonomous-cloud-delivery.html` + `.pptx` (committed + release-attached) | `nova-autonomous-cloud-delivery-talking-points.md` | 20 main + 1 appendix (21) | CTO, Head of Cloud, Head of Infra, Head of DevOps |
|
| Nova — The Autonomous Cloud Delivery Platform | `nova-autonomous-cloud-delivery-marp.md` | `nova-autonomous-cloud-delivery.html` (inlined) + `nova-autonomous-cloud-delivery.pptx` (MARP, release-attached) + `nova-autonomous-cloud-delivery-python.pptx` (structured) | `nova-autonomous-cloud-delivery-talking-points.md` | title + 20 main + 1 appendix (22) | CTO, Head of Cloud, Head of Infra, Head of DevOps |
|
||||||
|
|
||||||
> **v1.21:** the deck was renamed from "No-Humans Infrastructure Platform"
|
> **v1.23:** the slide creation process collapsed from 4 steps to 3 — the
|
||||||
> to "Autonomous Cloud Delivery Platform" (professional framing; conveys
|
> plain `<deck-name>.md` was deleted; `<deck-name>-marp.md` is now the
|
||||||
> autonomy without the provocative wording). The narrative restructured to
|
> sole source of truth. The standalone `nova-sp-theme.css` was retired
|
||||||
> a 4-beat arc (Problem → Solution → Proof → Roadmap + Ask). Internal
|
> from render (the live styling is the inline `style:` block in the
|
||||||
> provenance (decision IDs, requirement IDs, file paths) removed from
|
> `-marp.md` frontmatter; the CSS file is retained as a reference only).
|
||||||
> audience-facing slides. Maturity badges removed. The RACI matrix expanded
|
> Speaker notes moved from blockquotes into `<!-- Speaker notes: -->`
|
||||||
> to four roles (Quality Engineering + SRE). The Atelier slide split into
|
> HTML comments. Benefit callouts moved from `**Benefit:**` prefixes to
|
||||||
> two. The pipeline hardened: Checkov on static code before the plan;
|
> `<div class="benefit">`. The render pipeline now produces a dual-PPTX
|
||||||
> Wiz-or-Checkov on the plan (never both).
|
> output (MARP image-of-slide + python-pptx structured) and base64-inlines
|
||||||
|
> all images into the committed HTML.
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
"""REQ-274: tests for `scripts/render_pptx.py` — the structured, editable
|
||||||
|
python-pptx deck produced alongside the MARP-rendered PPTX.
|
||||||
|
|
||||||
|
The python-pptx deck is a native OOXML presentation: real text boxes,
|
||||||
|
native tables, embedded pictures, and italic benefit callouts. These
|
||||||
|
tests are offline (no AWS, no network) and assert the structural
|
||||||
|
properties of the committed `*-python.pptx` artifact.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pptx import Presentation
|
||||||
|
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
||||||
|
from pptx.presentation import Presentation as PresentationT
|
||||||
|
from pptx.shapes.autoshape import Shape
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
PRESENTATIONS = ROOT / "docs" / "presentations"
|
||||||
|
MARPT_DECK = PRESENTATIONS / "nova-autonomous-cloud-delivery-marp.md"
|
||||||
|
PYTHON_PPTX = PRESENTATIONS / "nova-autonomous-cloud-delivery-python.pptx"
|
||||||
|
|
||||||
|
# Title slide + 20 main slides + 1 appendix slide.
|
||||||
|
EXPECTED_SLIDE_COUNT = 22
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def prs() -> PresentationT:
|
||||||
|
"""Load the committed python-pptx deck once for the whole module."""
|
||||||
|
assert PYTHON_PPTX.is_file(), f"python-pptx PPTX not found: {PYTHON_PPTX}"
|
||||||
|
return Presentation(str(PYTHON_PPTX))
|
||||||
|
|
||||||
|
|
||||||
|
def _slide_titles(prs: PresentationT) -> list[str]:
|
||||||
|
"""Return the first non-empty text-frame line per slide (the title)."""
|
||||||
|
titles: list[str] = []
|
||||||
|
for slide in prs.slides:
|
||||||
|
for shape in slide.shapes:
|
||||||
|
if not shape.has_text_frame:
|
||||||
|
continue
|
||||||
|
text = cast(Shape, shape).text_frame.text.strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
# The title is the first non-empty line of the first non-empty
|
||||||
|
# text frame we find on the slide.
|
||||||
|
first_line = text.split("\n")[0].strip()
|
||||||
|
if first_line:
|
||||||
|
titles.append(first_line)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
titles.append("")
|
||||||
|
return titles
|
||||||
|
|
||||||
|
|
||||||
|
def test_slide_count(prs: PresentationT):
|
||||||
|
"""REQ-269/274: the python-pptx deck has 22 slides
|
||||||
|
(title + 20 main + 1 appendix)."""
|
||||||
|
assert len(prs.slides) == EXPECTED_SLIDE_COUNT, \
|
||||||
|
f"expected {EXPECTED_SLIDE_COUNT} slides, got {len(prs.slides)}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_title_slide_colors(prs: PresentationT):
|
||||||
|
"""REQ-269: the title slide (slide 0) has a solid-filled background
|
||||||
|
shape carrying the S&P Red (#D6002A) brand color (the title slide is
|
||||||
|
a red-bar-on-black layout)."""
|
||||||
|
title_slide = prs.slides[0]
|
||||||
|
red_found = False
|
||||||
|
black_found = False
|
||||||
|
for shape in title_slide.shapes:
|
||||||
|
fill = getattr(shape, "fill", None)
|
||||||
|
if fill is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if fill.type != 1: # MSO_FILL.SOLID
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
rgb = str(fill.fore_color.rgb).upper()
|
||||||
|
if rgb == "D6002A":
|
||||||
|
red_found = True
|
||||||
|
if rgb == "1B1B1B":
|
||||||
|
black_found = True
|
||||||
|
assert red_found, \
|
||||||
|
"title slide has no solid-fill shape with S&P Red (#D6002A)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_expected_slide_titles(prs: PresentationT):
|
||||||
|
"""REQ-269/274: spot-check that key slide titles match the markdown
|
||||||
|
deck (The Problem, Nova's Vision, Recap + Ask)."""
|
||||||
|
titles = _slide_titles(prs)
|
||||||
|
# Build a flat lowercase concatenation for substring checks.
|
||||||
|
flat = " | ".join(titles).lower()
|
||||||
|
expected = [
|
||||||
|
"the problem",
|
||||||
|
"nova's vision",
|
||||||
|
"recap + ask",
|
||||||
|
]
|
||||||
|
missing = [t for t in expected if t not in flat]
|
||||||
|
assert not missing, \
|
||||||
|
f"missing expected slide titles in python-pptx deck: {missing}; " \
|
||||||
|
f"found titles: {titles}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_table_rendering(prs: PresentationT):
|
||||||
|
"""REQ-269: a slide with a table (the RACI slide) has a native PPTX
|
||||||
|
table shape (GraphicFrame with has_table=True)."""
|
||||||
|
table_slides = []
|
||||||
|
for idx, slide in enumerate(prs.slides):
|
||||||
|
for shape in slide.shapes:
|
||||||
|
if shape.shape_type == MSO_SHAPE_TYPE.TABLE or getattr(
|
||||||
|
shape, "has_table", False
|
||||||
|
):
|
||||||
|
table_slides.append(idx)
|
||||||
|
break
|
||||||
|
assert table_slides, \
|
||||||
|
"no slide in the python-pptx deck has a native PPTX table shape"
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_embedding(prs: PresentationT):
|
||||||
|
"""REQ-269: a slide with an image (the Platform Pipeline slide)
|
||||||
|
has a native PPTX picture shape."""
|
||||||
|
picture_slides = []
|
||||||
|
for idx, slide in enumerate(prs.slides):
|
||||||
|
for shape in slide.shapes:
|
||||||
|
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
|
||||||
|
picture_slides.append(idx)
|
||||||
|
break
|
||||||
|
assert picture_slides, \
|
||||||
|
"no slide in the python-pptx deck has a native PPTX picture shape"
|
||||||
|
|
||||||
|
|
||||||
|
def test_benefit_callout_present(prs: PresentationT):
|
||||||
|
"""REQ-269/274: at least one slide has an italic text run (the
|
||||||
|
benefit callout, rendered as italic body text by render_pptx.py)."""
|
||||||
|
italic_runs = 0
|
||||||
|
for slide in prs.slides:
|
||||||
|
for shape in slide.shapes:
|
||||||
|
if not shape.has_text_frame:
|
||||||
|
continue
|
||||||
|
for paragraph in cast(Shape, shape).text_frame.paragraphs:
|
||||||
|
for run in paragraph.runs:
|
||||||
|
if run.font.italic and run.text.strip():
|
||||||
|
italic_runs += 1
|
||||||
|
assert italic_runs > 0, \
|
||||||
|
"no italic text runs found in the python-pptx deck (benefit callout)"
|
||||||
+350
-243
@@ -1,21 +1,28 @@
|
|||||||
"""REQ-239..243 (v1.20) + REQ-245,251,252 (v1.21): S&P theme + slide render
|
"""REQ-239..243 (v1.20) + REQ-245,251,252 (v1.21/22) + REQ-273..275 (v1.23):
|
||||||
pipeline + deck-refinement tests.
|
S&P theme + slide render pipeline + deck-refinement tests.
|
||||||
|
|
||||||
v1.20 validates:
|
v1.20 validates:
|
||||||
- The Marp deck frontmatter references nova-sp-theme.css
|
|
||||||
- The CSS file contains the S&P colors (#D6002A, #1B1B1B)
|
|
||||||
- The mermaid theme JSON contains the S&P colors
|
- The mermaid theme JSON contains the S&P colors
|
||||||
- Every .mmd has a corresponding .png
|
- Every .mmd has a corresponding .png
|
||||||
- The render_slides.sh script exists and is executable
|
- The render_slides.sh script exists and is executable
|
||||||
- The CI workflow file exists
|
- The CI workflow file exists
|
||||||
|
|
||||||
v1.21 adds (REQ-245,251,252):
|
v1.21/22 adds (REQ-245,251,252):
|
||||||
- Deck renamed to nova-autonomous-cloud-delivery*
|
- Deck renamed to nova-autonomous-cloud-delivery*
|
||||||
- No maturity badges in the Marp deck
|
- No maturity badges in the Marp deck
|
||||||
- No version in the Marp footer/title slide
|
- No version in the Marp footer/title slide
|
||||||
- 20 main + 1 appendix slides (v1.22 split slides 3+8 to relieve overflow)
|
- 20 main + 1 appendix slides
|
||||||
- No D-###/REQ-###/internal .py paths in audience-facing slides
|
- No D-###/REQ-###/internal .py paths in audience-facing slides
|
||||||
- Title is "Nova — The Autonomous Cloud Delivery Platform"
|
|
||||||
|
v1.23 (REQ-273,274,275) — single-document + dual-PPTX + image-inlining pipeline:
|
||||||
|
- The plain `.md` is gone; `*-marp.md` is the sole source of truth.
|
||||||
|
- Marp deck uses `theme: default` + an inline `style:` block (S&P colors).
|
||||||
|
- `nova-sp-theme.css` is RETAINED AS REFERENCE (not loaded at render).
|
||||||
|
- HTML has base64-inlined images (zero `src="assets/` references).
|
||||||
|
- A second PPTX (`*-python.pptx`) is produced by `scripts/render_pptx.py`.
|
||||||
|
- Speaker notes live as `<!-- Speaker notes: ... -->` HTML comments.
|
||||||
|
- Benefit callouts use `<div class="benefit">` (no `**Benefit:**` prefixes).
|
||||||
|
- The purged term "penetrate" is absent repo-wide.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -29,50 +36,216 @@ THEME_CSS = ASSETS / "nova-sp-theme.css"
|
|||||||
THEME_JSON = ASSETS / "mmd" / "sp-theme.json"
|
THEME_JSON = ASSETS / "mmd" / "sp-theme.json"
|
||||||
MARP_DECK = PRESENTATIONS / "nova-autonomous-cloud-delivery-marp.md"
|
MARP_DECK = PRESENTATIONS / "nova-autonomous-cloud-delivery-marp.md"
|
||||||
SOURCE_MD = PRESENTATIONS / "nova-autonomous-cloud-delivery.md"
|
SOURCE_MD = PRESENTATIONS / "nova-autonomous-cloud-delivery.md"
|
||||||
|
HTML = PRESENTATIONS / "nova-autonomous-cloud-delivery.html"
|
||||||
|
PYTHON_PPTX = PRESENTATIONS / "nova-autonomous-cloud-delivery-python.pptx"
|
||||||
|
MARP_PPTX = PRESENTATIONS / "nova-autonomous-cloud-delivery.pptx"
|
||||||
RENDER_SCRIPT = ROOT / "scripts" / "render_slides.sh"
|
RENDER_SCRIPT = ROOT / "scripts" / "render_slides.sh"
|
||||||
SLIDES_WORKFLOW = ROOT / ".github" / "workflows" / "slides.yml"
|
SLIDES_WORKFLOW = ROOT / ".github" / "workflows" / "slides.yml"
|
||||||
|
|
||||||
|
|
||||||
|
def _frontmatter(text: str) -> str:
|
||||||
|
"""Return the Marp frontmatter block (between the first two `---`)."""
|
||||||
|
fm_match = re.match(r'^---\n(.*?)\n---', text, re.DOTALL)
|
||||||
|
assert fm_match, "Marp frontmatter not found"
|
||||||
|
return fm_match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
# --- S&P theme reference + mermaid theme -------------------------------
|
||||||
|
|
||||||
def test_sp_theme_css_exists():
|
def test_sp_theme_css_exists():
|
||||||
"""REQ-239: nova-sp-theme.css exists."""
|
"""REQ-239: nova-sp-theme.css exists (retained as a reference)."""
|
||||||
assert THEME_CSS.is_file(), f"theme CSS not found: {THEME_CSS}"
|
assert THEME_CSS.is_file(), f"theme CSS not found: {THEME_CSS}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_nova_sp_theme_css_retained_as_reference():
|
||||||
|
"""REQ-274: nova-sp-theme.css is retained as a REFERENCE only and is
|
||||||
|
explicitly NOT loaded at render time (the live styling is the inline
|
||||||
|
`style:` block in the -marp.md frontmatter)."""
|
||||||
|
assert THEME_CSS.is_file(), f"theme CSS not found: {THEME_CSS}"
|
||||||
|
css = THEME_CSS.read_text()
|
||||||
|
assert "not loaded at render" in css.lower(), \
|
||||||
|
"nova-sp-theme.css does not document itself as 'not loaded at render'"
|
||||||
|
|
||||||
|
|
||||||
def test_sp_theme_css_has_snp_colors():
|
def test_sp_theme_css_has_snp_colors():
|
||||||
"""REQ-239: CSS contains S&P Red and Black."""
|
"""REQ-239: the reference CSS still carries S&P Red and Black."""
|
||||||
css = THEME_CSS.read_text()
|
css = THEME_CSS.read_text()
|
||||||
assert "#D6002A" in css, "S&P Red (#D6002A) missing from theme CSS"
|
assert "#D6002A" in css, "S&P Red (#D6002A) missing from theme CSS"
|
||||||
assert "#1B1B1B" in css, "S&P Black (#1B1B1B) missing from theme CSS"
|
assert "#1B1B1B" in css, "S&P Black (#1B1B1B) missing from theme CSS"
|
||||||
|
|
||||||
|
|
||||||
def test_sp_theme_json_has_snp_colors():
|
def test_sp_theme_json_has_snp_colors():
|
||||||
"""The mermaid theme JSON also has S&P colors."""
|
"""The mermaid theme JSON has S&P colors (mermaid PNGs are S&P-themed)."""
|
||||||
json_text = THEME_JSON.read_text()
|
json_text = THEME_JSON.read_text()
|
||||||
assert "#D6002A" in json_text, "S&P Red missing from mermaid theme"
|
assert "#D6002A" in json_text, "S&P Red missing from mermaid theme"
|
||||||
assert "#1B1B1B" in json_text, "S&P Black missing from mermaid theme"
|
assert "#1B1B1B" in json_text, "S&P Black missing from mermaid theme"
|
||||||
|
|
||||||
|
|
||||||
def test_marp_deck_uses_sp_theme():
|
# --- Marp deck: theme + inline style ----------------------------------
|
||||||
"""REQ-239: Marp deck frontmatter references nova-sp-theme.css."""
|
|
||||||
|
def test_marp_deck_uses_default_theme():
|
||||||
|
"""REQ-274: the Marp deck frontmatter uses `theme: default` (not the
|
||||||
|
retired `theme: nova-sp`). S&P styling is delivered by the inline
|
||||||
|
`style:` block, not the standalone CSS."""
|
||||||
|
frontmatter = _frontmatter(MARP_DECK.read_text())
|
||||||
|
assert re.search(r"^theme:\s*default\s*$", frontmatter, re.MULTILINE), \
|
||||||
|
"Marp deck does not set `theme: default` in the frontmatter"
|
||||||
|
assert "nova-sp" not in frontmatter, \
|
||||||
|
"Marp deck still references the retired `nova-sp` theme"
|
||||||
|
|
||||||
|
|
||||||
|
def test_marp_deck_has_sp_inline_style():
|
||||||
|
"""REQ-274: the inline `style:` block carries the S&P properties
|
||||||
|
(#D6002A, #1B1B1B, and the `section.title` rule)."""
|
||||||
|
frontmatter = _frontmatter(MARP_DECK.read_text())
|
||||||
|
assert "style:" in frontmatter, "frontmatter has no inline `style:` block"
|
||||||
|
# The inline style block extends past the frontmatter close in Marp
|
||||||
|
# (the `style:` value is a multi-line YAML literal). Read the whole
|
||||||
|
# deck so we capture the full style block.
|
||||||
|
deck = MARP_DECK.read_text()
|
||||||
|
assert "#D6002A" in deck, "inline style: block missing #D6002A"
|
||||||
|
assert "#1B1B1B" in deck, "inline style: block missing #1B1B1B"
|
||||||
|
assert "section.title" in deck, \
|
||||||
|
"inline style: block missing the `section.title` rule"
|
||||||
|
|
||||||
|
|
||||||
|
def test_marp_deck_no_badges():
|
||||||
|
"""REQ-252: no maturity badges in the Marp deck."""
|
||||||
text = MARP_DECK.read_text()
|
text = MARP_DECK.read_text()
|
||||||
# The frontmatter is between the first two ---
|
assert "badge" not in text, "Marp deck still contains badge spans"
|
||||||
fm_match = re.match(r'^---\n(.*?)\n---', text, re.DOTALL)
|
|
||||||
assert fm_match, "Marp frontmatter not found"
|
|
||||||
frontmatter = fm_match.group(1)
|
|
||||||
assert "nova-sp" in frontmatter, \
|
|
||||||
"Marp deck does not reference nova-sp theme"
|
|
||||||
|
|
||||||
|
|
||||||
def test_marp_deck_not_using_default_theme():
|
def test_marp_deck_no_version_in_footer():
|
||||||
"""The Marp deck must not use 'theme: default'."""
|
"""REQ-251: no version (v1.x) in the Marp frontmatter footer/header."""
|
||||||
|
frontmatter = _frontmatter(MARP_DECK.read_text())
|
||||||
|
assert not re.search(r"v1\.\d+", frontmatter), \
|
||||||
|
f"Marp frontmatter still contains a version: {frontmatter}"
|
||||||
|
assert "Act %" not in frontmatter, \
|
||||||
|
"Marp frontmatter still contains 'Act %{page}' artifact"
|
||||||
|
|
||||||
|
|
||||||
|
def test_marp_deck_title_slide_no_version_subtitle():
|
||||||
|
"""REQ-251: the title slide does not carry a version subtitle."""
|
||||||
text = MARP_DECK.read_text()
|
text = MARP_DECK.read_text()
|
||||||
fm_match = re.match(r'^---\n(.*?)\n---', text, re.DOTALL)
|
after_fm = text.split("---\n", 2)[2] if text.startswith("---") else text
|
||||||
assert fm_match, "Marp frontmatter not found"
|
first_slide = after_fm.split("\n---\n")[0]
|
||||||
frontmatter = fm_match.group(1)
|
assert "v1.18" not in first_slide, \
|
||||||
assert "theme: default" not in frontmatter, \
|
"Title slide still contains 'v1.18' subtitle"
|
||||||
"Marp deck still uses 'theme: default' — should use nova-sp-theme.css"
|
assert "Citizen Developer & Production-Grade Guidance" not in first_slide, \
|
||||||
|
"Title slide still contains the old version subtitle"
|
||||||
|
|
||||||
|
|
||||||
|
def test_marp_deck_title_is_autonomous_cloud_delivery():
|
||||||
|
"""REQ-245: the deck title is 'Nova — The Autonomous Cloud Delivery Platform'."""
|
||||||
|
text = MARP_DECK.read_text()
|
||||||
|
assert "Autonomous Cloud Delivery Platform" in text, \
|
||||||
|
"Deck title is not 'Autonomous Cloud Delivery Platform'"
|
||||||
|
assert "No-Humans Infrastructure Platform" not in text, \
|
||||||
|
"Deck still carries the old 'No-Humans Infrastructure Platform' title"
|
||||||
|
|
||||||
|
|
||||||
|
def test_marp_deck_slide_count():
|
||||||
|
"""REQ-245/261: 20 main slides + 1 appendix = 21 slide sections
|
||||||
|
(22 rendered sections incl. the H1 title slide)."""
|
||||||
|
text = MARP_DECK.read_text()
|
||||||
|
main_slides = re.findall(r"^## Slide ", text, re.MULTILINE)
|
||||||
|
appendix_slides = re.findall(r"^## Appendix ", text, re.MULTILINE)
|
||||||
|
assert len(main_slides) == 20, \
|
||||||
|
f"expected 20 main slides, found {len(main_slides)}"
|
||||||
|
assert len(appendix_slides) == 1, \
|
||||||
|
f"expected 1 appendix slide, found {len(appendix_slides)}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_marp_deck_no_internal_citations():
|
||||||
|
"""REQ-252: no D-### decision IDs, REQ-### requirement IDs, or internal
|
||||||
|
.py file paths in the audience-facing Marp deck SLIDE BODIES. Internal
|
||||||
|
provenance is allowed inside `<!-- ... -->` HTML comments (speaker
|
||||||
|
notes / talking points), which Marp excludes from the rendered slide."""
|
||||||
|
text = MARP_DECK.read_text()
|
||||||
|
# Strip HTML comments (speaker notes + talking points) before checking.
|
||||||
|
body = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
|
||||||
|
assert not re.search(r"\bD-\d{3}\b", body), \
|
||||||
|
"Marp deck slide body contains D-### decision IDs"
|
||||||
|
assert not re.search(r"\bREQ-\d{3}\b", body), \
|
||||||
|
"Marp deck slide body contains REQ-### requirement IDs"
|
||||||
|
assert not re.search(r"\b(outbox_writer|confidence_signal|hitl_gates|"
|
||||||
|
r"attestation_matrix|checkov_adapter|infracost_adapter|"
|
||||||
|
r"contract_resolver|run_platform)\.py\b", body), \
|
||||||
|
"Marp deck slide body contains internal .py file paths"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Speaker notes + benefit callouts (REQ-274) ----------------------
|
||||||
|
|
||||||
|
def test_speaker_notes_as_html_comments():
|
||||||
|
"""REQ-274: speaker notes are embedded as `<!-- Speaker notes: ... -->`
|
||||||
|
HTML comments (Marp excludes HTML comments from the rendered slide;
|
||||||
|
the comments are for authors/presenters). Expect >= 20 (one per main
|
||||||
|
slide) + the appendix slide."""
|
||||||
|
text = MARP_DECK.read_text()
|
||||||
|
count = len(re.findall(r"<!-- Speaker notes:", text))
|
||||||
|
assert count >= 20, \
|
||||||
|
f"expected >=20 `<!-- Speaker notes:` comments, found {count}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_benefit_callouts_use_class():
|
||||||
|
"""REQ-274: benefit callouts use `<div class="benefit">` (>= 21
|
||||||
|
occurrences — one per slide section incl. the title slide) and zero
|
||||||
|
`**Benefit:**` text prefixes."""
|
||||||
|
text = MARP_DECK.read_text()
|
||||||
|
class_count = text.count('class="benefit"')
|
||||||
|
assert class_count >= 21, \
|
||||||
|
f"expected >=21 `class=\"benefit\"` callouts, found {class_count}"
|
||||||
|
assert "**Benefit:**" not in text, \
|
||||||
|
"Marp deck still uses the retired `**Benefit:**` prefix"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Single source of truth (REQ-274) --------------------------------
|
||||||
|
|
||||||
|
def test_single_source_of_truth():
|
||||||
|
"""REQ-274: the plain `nova-autonomous-cloud-delivery.md` is deleted;
|
||||||
|
`nova-autonomous-cloud-delivery-marp.md` is the sole source of truth."""
|
||||||
|
assert not SOURCE_MD.exists(), \
|
||||||
|
f"plain source markdown still exists (should be deleted): {SOURCE_MD}"
|
||||||
|
assert MARP_DECK.is_file(), \
|
||||||
|
f"Marp deck (sole source of truth) not found: {MARP_DECK}"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Purged term (REQ-274) -------------------------------------------
|
||||||
|
|
||||||
|
def test_no_purged_loaded_term():
|
||||||
|
"""REQ-274: the purged term 'penetrate' (case-insensitive, any
|
||||||
|
inflection: penetrate, penetrating, penetration, ...) is absent
|
||||||
|
from docs/, .ciagent/PROJECT.md, and .ciagent/CLARIFY.md."""
|
||||||
|
targets = [
|
||||||
|
ROOT / "docs",
|
||||||
|
ROOT / ".ciagent" / "PROJECT.md",
|
||||||
|
ROOT / ".ciagent" / "CLARIFY.md",
|
||||||
|
]
|
||||||
|
hits = []
|
||||||
|
for target in targets:
|
||||||
|
if target.is_dir():
|
||||||
|
for path in target.rglob("*"):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
if path.suffix in {".png", ".pptx", ".html", ".zip", ".json"}:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if "penetrat" in path.read_text().lower():
|
||||||
|
hits.append(str(path))
|
||||||
|
except (UnicodeDecodeError, OSError):
|
||||||
|
continue
|
||||||
|
elif target.is_file():
|
||||||
|
try:
|
||||||
|
if "penetrat" in target.read_text().lower():
|
||||||
|
hits.append(str(target))
|
||||||
|
except (UnicodeDecodeError, OSError):
|
||||||
|
hits.append(f"<unreadable {target}>")
|
||||||
|
assert not hits, \
|
||||||
|
f"purged term 'penetrate' still present in: {hits}"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Render script ---------------------------------------------------
|
||||||
|
|
||||||
def test_render_slides_script_exists():
|
def test_render_slides_script_exists():
|
||||||
"""REQ-240: render_slides.sh exists and is executable."""
|
"""REQ-240: render_slides.sh exists and is executable."""
|
||||||
assert RENDER_SCRIPT.is_file(), "render_slides.sh not found"
|
assert RENDER_SCRIPT.is_file(), "render_slides.sh not found"
|
||||||
@@ -104,6 +277,52 @@ def test_render_slides_default_deck_renamed():
|
|||||||
"render_slides.sh does not default to nova-autonomous-cloud-delivery"
|
"render_slides.sh does not default to nova-autonomous-cloud-delivery"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_slides_has_2x_scale():
|
||||||
|
"""REQ-258: render_slides.sh uses -s 2 (2x scale) and -b transparent."""
|
||||||
|
text = RENDER_SCRIPT.read_text()
|
||||||
|
assert "-s 2" in text, "render_slides.sh does not use -s 2 (2x scale)"
|
||||||
|
assert "-b transparent" in text, \
|
||||||
|
"render_slides.sh does not use -b transparent"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_slides_pins_cli_versions():
|
||||||
|
"""REQ-257: render_slides.sh pins marp-cli and mermaid-cli versions
|
||||||
|
(no @latest). REQ-273: pyproject.toml declares python-pptx in the
|
||||||
|
`slides` optional-dependency group."""
|
||||||
|
text = RENDER_SCRIPT.read_text()
|
||||||
|
assert "marp-cli@" in text, "render_slides.sh does not pin marp-cli"
|
||||||
|
assert "mermaid-cli@" in text, \
|
||||||
|
"render_slides.sh does not pin mermaid-cli"
|
||||||
|
assert "@latest" not in text, \
|
||||||
|
"render_slides.sh still uses @latest (not pinned)"
|
||||||
|
pyproject = (ROOT / "pyproject.toml").read_text()
|
||||||
|
assert "python-pptx" in pyproject, \
|
||||||
|
"pyproject.toml does not declare python-pptx"
|
||||||
|
# python-pptx is in the [project.optional-dependencies] `slides` group.
|
||||||
|
# Locate the optional-dependencies table block, then check the `slides`
|
||||||
|
# array within it.
|
||||||
|
block_match = re.search(
|
||||||
|
r"\[project\.optional-dependencies\](.*?)(?=\n\[|\Z)",
|
||||||
|
pyproject, re.DOTALL)
|
||||||
|
assert block_match, \
|
||||||
|
"pyproject.toml has no [project.optional-dependencies] table"
|
||||||
|
block = block_match.group(1)
|
||||||
|
slides_match = re.search(r"slides\s*=\s*\[([^\]]*)\]", block, re.DOTALL)
|
||||||
|
assert slides_match, \
|
||||||
|
"pyproject.toml has no `slides` optional-dependency group"
|
||||||
|
assert "python-pptx" in slides_match.group(1), \
|
||||||
|
"python-pptx is not in the `slides` optional-dependency group"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_deck_removed():
|
||||||
|
"""REQ-257: render_deck.sh has been deleted (produced unthemed output)."""
|
||||||
|
old_script = ROOT / "scripts" / "render_deck.sh"
|
||||||
|
assert not old_script.exists(), \
|
||||||
|
"render_deck.sh still exists (should be deleted — produced unthemed output)"
|
||||||
|
|
||||||
|
|
||||||
|
# --- CI workflow (REQ-273) -------------------------------------------
|
||||||
|
|
||||||
def test_slides_ci_workflow_exists():
|
def test_slides_ci_workflow_exists():
|
||||||
"""REQ-241: CI workflow for slides exists."""
|
"""REQ-241: CI workflow for slides exists."""
|
||||||
assert SLIDES_WORKFLOW.is_file(), "slides.yml workflow not found"
|
assert SLIDES_WORKFLOW.is_file(), "slides.yml workflow not found"
|
||||||
@@ -118,6 +337,35 @@ def test_slides_ci_workflow_triggers_on_presentations():
|
|||||||
"slides.yml does not invoke render_slides.sh"
|
"slides.yml does not invoke render_slides.sh"
|
||||||
|
|
||||||
|
|
||||||
|
def test_slides_ci_workflow_installs_python_pptx():
|
||||||
|
"""REQ-273: CI workflow installs python-pptx (via the `slides` extra)."""
|
||||||
|
text = SLIDES_WORKFLOW.read_text()
|
||||||
|
assert "python-pptx" in text or "[slides]" in text, \
|
||||||
|
"slides.yml does not install python-pptx / the slides extra"
|
||||||
|
assert "setup-python" in text, \
|
||||||
|
"slides.yml has no setup-python step"
|
||||||
|
|
||||||
|
|
||||||
|
def test_slides_ci_workflow_pins_cli_versions():
|
||||||
|
"""REQ-273: CI workflow pins marp-cli + mermaid-cli (no @latest)."""
|
||||||
|
text = SLIDES_WORKFLOW.read_text()
|
||||||
|
assert "marp-cli@4.5.0" in text, \
|
||||||
|
"slides.yml does not pin @marp-team/marp-cli@4.5.0"
|
||||||
|
assert "mermaid-cli@11.16.0" in text, \
|
||||||
|
"slides.yml does not pin @mermaid-js/mermaid-cli@11.16.0"
|
||||||
|
assert "@latest" not in text, \
|
||||||
|
"slides.yml still uses @latest (not pinned)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_slides_ci_workflow_stages_python_pptx():
|
||||||
|
"""REQ-273: CI workflow `git add` list includes *-python.pptx."""
|
||||||
|
text = SLIDES_WORKFLOW.read_text()
|
||||||
|
assert "*-python.pptx" in text, \
|
||||||
|
"slides.yml git-add list does not stage *-python.pptx"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Mermaid PNGs ----------------------------------------------------
|
||||||
|
|
||||||
def test_every_mmd_has_png():
|
def test_every_mmd_has_png():
|
||||||
"""REQ-240: every .mmd file has a corresponding .png."""
|
"""REQ-240: every .mmd file has a corresponding .png."""
|
||||||
mmd_dir = ASSETS / "mmd"
|
mmd_dir = ASSETS / "mmd"
|
||||||
@@ -134,6 +382,82 @@ def test_every_mmd_has_png():
|
|||||||
assert not missing, f"PNGs missing for: {missing}"
|
assert not missing, f"PNGs missing for: {missing}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_png_aspect_ratios_sane():
|
||||||
|
"""REQ-259/260: PNGs referenced in the marp deck have aspect ratios
|
||||||
|
in [0.4, 4.0] (suitable for 16:9 slides)."""
|
||||||
|
import struct
|
||||||
|
deck_text = MARP_DECK.read_text()
|
||||||
|
referenced = re.findall(r'!\[[^\]]*\]\(assets/png/([^)]+\.png)\)', deck_text)
|
||||||
|
assert referenced, "no PNGs referenced in the marp deck"
|
||||||
|
for png_name in referenced:
|
||||||
|
png_path = ASSETS / "png" / png_name
|
||||||
|
assert png_path.is_file(), f"referenced PNG not found: {png_name}"
|
||||||
|
with open(png_path, "rb") as fh:
|
||||||
|
data = fh.read(24)
|
||||||
|
assert data[:8] == b"\x89PNG\r\n\x1a\n", f"{png_name} is not a PNG"
|
||||||
|
w = struct.unpack(">I", data[16:20])[0]
|
||||||
|
h = struct.unpack(">I", data[20:24])[0]
|
||||||
|
ar = w / h
|
||||||
|
assert 0.4 <= ar <= 4.0, \
|
||||||
|
f"{png_name} aspect ratio {ar:.2f} outside [0.4, 4.0] ({w}x{h})"
|
||||||
|
|
||||||
|
|
||||||
|
# --- HTML: theme embed + image inlining + slide count ----------------
|
||||||
|
|
||||||
|
def test_html_embeds_theme():
|
||||||
|
"""REQ-262/274: the committed HTML embeds the S&P theme as literal
|
||||||
|
S&P colors (#D6002A — not just the --sp-red variable) + padding."""
|
||||||
|
html = HTML.read_text()
|
||||||
|
assert "#D6002A" in html, \
|
||||||
|
"committed HTML does not embed the literal S&P Red (#D6002A)"
|
||||||
|
assert "padding:" in html, "committed HTML does not embed padding rule"
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_images_inlined_as_base64():
|
||||||
|
"""REQ-268/274: the rendered HTML is self-contained — zero
|
||||||
|
`src="assets/` references and at least one `data:image` per image
|
||||||
|
referenced in the -marp.md deck."""
|
||||||
|
html = HTML.read_text()
|
||||||
|
assert len(re.findall(r'src=["\']assets/', html)) == 0, \
|
||||||
|
"HTML still references external `assets/` images (not inlined)"
|
||||||
|
deck_text = MARP_DECK.read_text()
|
||||||
|
image_count = len(re.findall(r'!\[[^\]]*\]\(assets/', deck_text))
|
||||||
|
assert image_count > 0, "no images referenced in the marp deck"
|
||||||
|
data_uri_count = html.count("data:image")
|
||||||
|
assert data_uri_count >= image_count, \
|
||||||
|
f"HTML has {data_uri_count} data:image URIs but the deck " \
|
||||||
|
f"references {image_count} images (should be >=)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_slide_count_matches_marp():
|
||||||
|
"""REQ-262: the committed HTML <section> count matches the marp deck
|
||||||
|
slide count (title + 20 main + 1 appendix = 22)."""
|
||||||
|
html = HTML.read_text()
|
||||||
|
section_count = html.count("<section ")
|
||||||
|
deck_text = MARP_DECK.read_text()
|
||||||
|
main_slides = len(re.findall(r"^## Slide ", deck_text, re.MULTILINE))
|
||||||
|
appendix_slides = len(re.findall(r"^## Appendix ", deck_text, re.MULTILINE))
|
||||||
|
expected = main_slides + appendix_slides + 1
|
||||||
|
assert section_count == expected, \
|
||||||
|
f"HTML has {section_count} sections, expected {expected} " \
|
||||||
|
f"({main_slides} main + {appendix_slides} appendix + 1 title)"
|
||||||
|
|
||||||
|
|
||||||
|
# --- python-pptx artifact (REQ-273/274) ------------------------------
|
||||||
|
|
||||||
|
def test_python_pptx_exists():
|
||||||
|
"""REQ-273/274: the python-pptx PPTX exists and is a valid OOXML zip
|
||||||
|
(the PPTX/zip signature `PK\x03\x04`)."""
|
||||||
|
assert PYTHON_PPTX.is_file(), \
|
||||||
|
f"python-pptx PPTX not found: {PYTHON_PPTX}"
|
||||||
|
with open(PYTHON_PPTX, "rb") as fh:
|
||||||
|
sig = fh.read(4)
|
||||||
|
assert sig == b"PK\x03\x04", \
|
||||||
|
f"python-pptx PPTX is not a valid zip (bad signature: {sig!r})"
|
||||||
|
|
||||||
|
|
||||||
|
# --- README (REQ-275) ------------------------------------------------
|
||||||
|
|
||||||
def test_readme_no_retired_decks():
|
def test_readme_no_retired_decks():
|
||||||
"""REQ-243: presentations README does not list retired decks."""
|
"""REQ-243: presentations README does not list retired decks."""
|
||||||
readme = (PRESENTATIONS / "README.md").read_text()
|
readme = (PRESENTATIONS / "README.md").read_text()
|
||||||
@@ -153,221 +477,4 @@ def test_readme_no_old_deck_name():
|
|||||||
def test_old_deck_files_removed():
|
def test_old_deck_files_removed():
|
||||||
"""REQ-245: the old nova-no-humans-platform* files are gone."""
|
"""REQ-245: the old nova-no-humans-platform* files are gone."""
|
||||||
old_files = sorted(PRESENTATIONS.glob("nova-no-humans-platform*"))
|
old_files = sorted(PRESENTATIONS.glob("nova-no-humans-platform*"))
|
||||||
assert not old_files, f"old deck files still present: {old_files}"
|
assert not old_files, f"old deck files still present: {old_files}"
|
||||||
|
|
||||||
|
|
||||||
def test_marp_deck_no_badges():
|
|
||||||
"""REQ-252: no maturity badges in the Marp deck."""
|
|
||||||
text = MARP_DECK.read_text()
|
|
||||||
assert "badge" not in text, "Marp deck still contains badge spans"
|
|
||||||
|
|
||||||
|
|
||||||
def test_marp_deck_no_version_in_footer():
|
|
||||||
"""REQ-251: no version (v1.x) in the Marp frontmatter footer/header."""
|
|
||||||
text = MARP_DECK.read_text()
|
|
||||||
fm_match = re.match(r'^---\n(.*?)\n---', text, re.DOTALL)
|
|
||||||
assert fm_match, "Marp frontmatter not found"
|
|
||||||
frontmatter = fm_match.group(1)
|
|
||||||
# No v1.x version string in the footer or header lines
|
|
||||||
assert not re.search(r"v1\.\d+", frontmatter), \
|
|
||||||
f"Marp frontmatter still contains a version: {frontmatter}"
|
|
||||||
# No "Act" pagination artifact
|
|
||||||
assert "Act %" not in frontmatter, \
|
|
||||||
"Marp frontmatter still contains 'Act %{page}' artifact"
|
|
||||||
|
|
||||||
|
|
||||||
def test_marp_deck_title_slide_no_version_subtitle():
|
|
||||||
"""REQ-251: the title slide does not carry a version subtitle."""
|
|
||||||
text = MARP_DECK.read_text()
|
|
||||||
# The title slide is the first slide after the frontmatter
|
|
||||||
# Find the title block (between the frontmatter and the first --- separator)
|
|
||||||
after_fm = text.split("---\n", 2)[2] if text.startswith("---") else text
|
|
||||||
first_slide = after_fm.split("\n---\n")[0]
|
|
||||||
# The old subtitle was "v1.18 — Citizen Developer & Production-Grade Guidance"
|
|
||||||
assert "v1.18" not in first_slide, \
|
|
||||||
"Title slide still contains 'v1.18' subtitle"
|
|
||||||
assert "Citizen Developer & Production-Grade Guidance" not in first_slide, \
|
|
||||||
"Title slide still contains the old version subtitle"
|
|
||||||
|
|
||||||
|
|
||||||
def test_marp_deck_title_is_autonomous_cloud_delivery():
|
|
||||||
"""REQ-245: the deck title is 'Nova — The Autonomous Cloud Delivery Platform'."""
|
|
||||||
text = MARP_DECK.read_text()
|
|
||||||
assert "Autonomous Cloud Delivery Platform" in text, \
|
|
||||||
"Deck title is not 'Autonomous Cloud Delivery Platform'"
|
|
||||||
# The old title should not appear in the audience-facing deck
|
|
||||||
# (speaker notes are not in the marp deck, so this is safe)
|
|
||||||
assert "No-Humans Infrastructure Platform" not in text, \
|
|
||||||
"Deck still carries the old 'No-Humans Infrastructure Platform' title"
|
|
||||||
|
|
||||||
|
|
||||||
def test_marp_deck_slide_count():
|
|
||||||
"""REQ-245/261: 20 main slides + 1 appendix = 21 slides total.
|
|
||||||
v1.22 split slides 3 (Objectives+Anti-Goals) and 8 (Attestation
|
|
||||||
Matrix) to relieve overflow, increasing the count from 18 to 20."""
|
|
||||||
text = MARP_DECK.read_text()
|
|
||||||
# Count slide separators: each slide ends with --- (except the last)
|
|
||||||
# The frontmatter is one --- ... --- block, then each slide is separated by ---
|
|
||||||
# Count "## Slide" and "## Appendix" headings
|
|
||||||
slide_headings = re.findall(r"^## (?:Slide|Appendix) ", text, re.MULTILINE)
|
|
||||||
main_slides = re.findall(r"^## Slide ", text, re.MULTILINE)
|
|
||||||
appendix_slides = re.findall(r"^## Appendix ", text, re.MULTILINE)
|
|
||||||
assert len(main_slides) == 20, \
|
|
||||||
f"expected 20 main slides, found {len(main_slides)}: {slide_headings}"
|
|
||||||
assert len(appendix_slides) == 1, \
|
|
||||||
f"expected 1 appendix slide, found {len(appendix_slides)}"
|
|
||||||
|
|
||||||
|
|
||||||
def test_marp_deck_no_internal_citations():
|
|
||||||
"""REQ-252: no D-### decision IDs, REQ-### requirement IDs, or internal
|
|
||||||
.py file paths in the audience-facing Marp deck."""
|
|
||||||
text = MARP_DECK.read_text()
|
|
||||||
# Decision IDs like D-121, D-083
|
|
||||||
assert not re.search(r"\bD-\d{3}\b", text), \
|
|
||||||
"Marp deck contains D-### decision IDs"
|
|
||||||
# Requirement IDs like REQ-245
|
|
||||||
assert not re.search(r"\bREQ-\d{3}\b", text), \
|
|
||||||
"Marp deck contains REQ-### requirement IDs"
|
|
||||||
# Internal python file paths like outbox_writer.py, confidence_signal.py
|
|
||||||
# (allow .py only inside code blocks for the ROI formula? No — the deck
|
|
||||||
# should not cite internal file paths at all)
|
|
||||||
assert not re.search(r"\b(outbox_writer|confidence_signal|hitl_gates|"
|
|
||||||
r"attestation_matrix|checkov_adapter|infracost_adapter|"
|
|
||||||
r"contract_resolver|run_platform)\.py\b", text), \
|
|
||||||
"Marp deck contains internal .py file paths"
|
|
||||||
|
|
||||||
|
|
||||||
def test_source_md_no_internal_citations_in_slides():
|
|
||||||
"""REQ-252: the source-of-truth markdown keeps internal citations only
|
|
||||||
in speaker notes, not in the audience-facing slide body. Speaker notes
|
|
||||||
are blockquoted (> ) — we check non-blockquote lines for D-###/REQ-###."""
|
|
||||||
text = SOURCE_MD.read_text()
|
|
||||||
# Split into lines; exclude blockquote lines (speaker notes) and the
|
|
||||||
# header frontmatter (> ... at the top)
|
|
||||||
in_note = False
|
|
||||||
body_lines = []
|
|
||||||
for line in text.splitlines():
|
|
||||||
if line.lstrip().startswith(">"):
|
|
||||||
in_note = True
|
|
||||||
continue
|
|
||||||
if in_note and line.strip() == "":
|
|
||||||
in_note = False
|
|
||||||
continue
|
|
||||||
if not in_note:
|
|
||||||
body_lines.append(line)
|
|
||||||
body = "\n".join(body_lines)
|
|
||||||
# Decision IDs and REQ IDs should not appear in the slide body
|
|
||||||
assert not re.search(r"\bD-\d{3}\b", body), \
|
|
||||||
"Source markdown slide body contains D-### decision IDs"
|
|
||||||
assert not re.search(r"\bREQ-\d{3}\b", body), \
|
|
||||||
"Source markdown slide body contains REQ-### requirement IDs"
|
|
||||||
|
|
||||||
|
|
||||||
def test_source_md_no_badges():
|
|
||||||
"""REQ-252: no maturity badges in the source-of-truth markdown."""
|
|
||||||
text = SOURCE_MD.read_text()
|
|
||||||
assert "badge" not in text.lower(), \
|
|
||||||
"Source markdown still contains badge spans"
|
|
||||||
|
|
||||||
|
|
||||||
# --- v1.22 layout/aspect-ratio/theme-structural tests (REQ-262) ---
|
|
||||||
|
|
||||||
def test_theme_css_has_section_padding():
|
|
||||||
"""REQ-254: theme CSS has a section padding rule (root cause fix)."""
|
|
||||||
css = THEME_CSS.read_text()
|
|
||||||
assert "padding:" in css, "theme CSS has no padding rule"
|
|
||||||
# The section rule must have padding (not just table/td padding)
|
|
||||||
assert re.search(r"section\s*\{[^}]*padding:", css, re.DOTALL), \
|
|
||||||
"theme CSS has no padding on the section rule"
|
|
||||||
|
|
||||||
|
|
||||||
def test_theme_css_suppresses_title_chrome():
|
|
||||||
"""REQ-256: title slides suppress header/footer chrome."""
|
|
||||||
css = THEME_CSS.read_text()
|
|
||||||
assert "section.title header" in css, \
|
|
||||||
"theme CSS does not suppress title-slide header"
|
|
||||||
assert "section.title footer" in css, \
|
|
||||||
"theme CSS does not suppress title-slide footer"
|
|
||||||
assert "display: none" in css, \
|
|
||||||
"theme CSS does not set display:none on title chrome"
|
|
||||||
|
|
||||||
|
|
||||||
def test_theme_css_has_aspect_ratio_aware_images():
|
|
||||||
"""REQ-255: image rules use object-fit + max-width (not blunt max-height only)."""
|
|
||||||
css = THEME_CSS.read_text()
|
|
||||||
assert "object-fit" in css, \
|
|
||||||
"theme CSS does not use object-fit for images"
|
|
||||||
assert "max-width" in css, \
|
|
||||||
"theme CSS does not set max-width for images"
|
|
||||||
|
|
||||||
|
|
||||||
def test_png_aspect_ratios_sane():
|
|
||||||
"""REQ-259/260: PNGs referenced in the marp deck have aspect ratios
|
|
||||||
in [0.4, 4.0] (suitable for 16:9 slides with img.tall/img.wide classes).
|
|
||||||
Only checks PNGs actually referenced in the current marp deck —
|
|
||||||
legacy/unused PNGs are not checked (GRILL revision 1)."""
|
|
||||||
import struct
|
|
||||||
deck_text = MARP_DECK.read_text()
|
|
||||||
# Extract all referenced PNG paths: 
|
|
||||||
referenced = re.findall(r'!\[[^\]]*\]\(assets/png/([^)]+\.png)\)', deck_text)
|
|
||||||
assert referenced, "no PNGs referenced in the marp deck"
|
|
||||||
for png_name in referenced:
|
|
||||||
png_path = ASSETS / "png" / png_name
|
|
||||||
assert png_path.is_file(), f"referenced PNG not found: {png_name}"
|
|
||||||
with open(png_path, "rb") as fh:
|
|
||||||
data = fh.read(24)
|
|
||||||
assert data[:8] == b"\x89PNG\r\n\x1a\n", f"{png_name} is not a PNG"
|
|
||||||
w = struct.unpack(">I", data[16:20])[0]
|
|
||||||
h = struct.unpack(">I", data[20:24])[0]
|
|
||||||
ar = w / h
|
|
||||||
assert 0.4 <= ar <= 4.0, \
|
|
||||||
f"{png_name} aspect ratio {ar:.2f} outside [0.4, 4.0] ({w}x{h})"
|
|
||||||
|
|
||||||
|
|
||||||
def test_render_slides_has_2x_scale():
|
|
||||||
"""REQ-258: render_slides.sh uses -s 2 (2x scale) and -b transparent."""
|
|
||||||
text = RENDER_SCRIPT.read_text()
|
|
||||||
assert "-s 2" in text, "render_slides.sh does not use -s 2 (2x scale)"
|
|
||||||
assert "-b transparent" in text, \
|
|
||||||
"render_slides.sh does not use -b transparent"
|
|
||||||
|
|
||||||
|
|
||||||
def test_render_slides_pins_cli_versions():
|
|
||||||
"""REQ-257: render_slides.sh pins marp-cli and mermaid-cli versions
|
|
||||||
(no @latest)."""
|
|
||||||
text = RENDER_SCRIPT.read_text()
|
|
||||||
assert "marp-cli@" in text, "render_slides.sh does not pin marp-cli"
|
|
||||||
assert "mermaid-cli@" in text, \
|
|
||||||
"render_slides.sh does not pin mermaid-cli"
|
|
||||||
assert "@latest" not in text, \
|
|
||||||
"render_slides.sh still uses @latest (not pinned)"
|
|
||||||
|
|
||||||
|
|
||||||
def test_render_deck_removed():
|
|
||||||
"""REQ-257: render_deck.sh has been deleted (produced unthemed output)."""
|
|
||||||
old_script = ROOT / "scripts" / "render_deck.sh"
|
|
||||||
assert not old_script.exists(), \
|
|
||||||
"render_deck.sh still exists (should be deleted — produced unthemed output)"
|
|
||||||
|
|
||||||
|
|
||||||
def test_html_embeds_theme():
|
|
||||||
"""REQ-262: the committed HTML embeds the S&P theme (--sp-red + padding
|
|
||||||
in the inline <style> block)."""
|
|
||||||
html = (PRESENTATIONS / "nova-autonomous-cloud-delivery.html").read_text()
|
|
||||||
assert "--sp-red" in html, "committed HTML does not embed --sp-red"
|
|
||||||
assert "padding:" in html, "committed HTML does not embed padding rule"
|
|
||||||
|
|
||||||
|
|
||||||
def test_html_slide_count_matches_marp():
|
|
||||||
"""REQ-262: the committed HTML <section> count matches the marp deck
|
|
||||||
slide count (title + 20 main + 1 appendix = 22)."""
|
|
||||||
html = (PRESENTATIONS / "nova-autonomous-cloud-delivery.html").read_text()
|
|
||||||
section_count = html.count("<section ")
|
|
||||||
deck_text = MARP_DECK.read_text()
|
|
||||||
main_slides = len(re.findall(r"^## Slide ", deck_text, re.MULTILINE))
|
|
||||||
appendix_slides = len(re.findall(r"^## Appendix ", deck_text, re.MULTILINE))
|
|
||||||
# +1 for the title slide (which is an H1, not "## Slide")
|
|
||||||
expected = main_slides + appendix_slides + 1
|
|
||||||
assert section_count == expected, \
|
|
||||||
f"HTML has {section_count} sections, expected {expected} " \
|
|
||||||
f"({main_slides} main + {appendix_slides} appendix + 1 title)"
|
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
# Nova Slides Render — re-renders presentation deck when source files change.
|
# Nova Slides Render — re-renders presentation deck when source files change.
|
||||||
|
# REQ-273: install python-pptx, pin CLI versions, stage HTML + both PPTX +
|
||||||
|
# base64-inlined images.
|
||||||
name: Nova Slides Render
|
name: Nova Slides Render
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- 'docs/presentations/**'
|
- 'docs/presentations/**'
|
||||||
- 'scripts/render_slides.sh'
|
- 'scripts/render_slides.sh'
|
||||||
- 'assets/nova-sp-theme.css'
|
- 'scripts/inline_images.py'
|
||||||
|
- 'scripts/render_pptx.py'
|
||||||
|
- 'pyproject.toml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -16,16 +20,24 @@ jobs:
|
|||||||
with: { fetch-depth: 0 }
|
with: { fetch-depth: 0 }
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with: { node-version: '20' }
|
with: { node-version: '20' }
|
||||||
- name: Install Chrome
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: Install python-pptx (slides extra)
|
||||||
|
run: pip install -e ".[slides]"
|
||||||
|
- name: Install + pin render CLIs
|
||||||
run: |
|
run: |
|
||||||
npx --yes @marp-team/marp-cli@latest --version
|
npx --yes @marp-team/marp-cli@4.5.0 --version
|
||||||
npx --yes @mermaid-js/mermaid-cli --version
|
npx --yes @mermaid-js/mermaid-cli@11.16.0 --version
|
||||||
- name: Render slides
|
- name: Render slides
|
||||||
run: bash scripts/render_slides.sh
|
run: bash scripts/render_slides.sh
|
||||||
- name: Commit rendered artifacts
|
- name: Commit rendered artifacts
|
||||||
run: |
|
run: |
|
||||||
git config user.name "nova-slides-bot"
|
git config user.name "nova-slides-bot"
|
||||||
git config user.email "bot@nova.local"
|
git config user.email "bot@nova.local"
|
||||||
git add docs/presentations/*.html docs/presentations/*.pptx docs/presentations/assets/png/*.png
|
git add docs/presentations/*.html \
|
||||||
|
docs/presentations/*.pptx \
|
||||||
|
docs/presentations/*-python.pptx \
|
||||||
|
docs/presentations/assets/png/*.png
|
||||||
git diff --cached --quiet || git commit -m "chore(slides): re-render deck [skip ci]"
|
git diff --cached --quiet || git commit -m "chore(slides): re-render deck [skip ci]"
|
||||||
git push
|
git push
|
||||||
Reference in New Issue
Block a user