25ddc894c2
9 requirements complete (REQ-254..262): - P1: theme-css — section padding + overflow + image rules + title chrome + spacing tightening (REQ-254,255,256) - P2: render-scripts — delete render_deck.sh, pin CLI versions, 2x scale + transparent bg (REQ-257,258) - P3: mermaid-relayout — telemetry TB + platform-pipeline 4-node TB, re-rendered 2x transparent (REQ-259,260) - P4: deck-content — split slides 3+8 (18->20 main), trim 8 overflowing slides, remove redundant header (REQ-261) - P5: render-and-test — re-render HTML+PPTX, add 9 layout/aspect- ratio/theme-structural tests (REQ-262) - P6: final review + audit + ship (this commit) Final review fixes: source .md + talking-points re-synced to 20-slide structure; ![h:480 class:tall] directives applied; README stale references updated; CSS trailing newline added. Root cause: nova-sp-theme.css had zero section padding (declared /* @theme nova-sp */ as a comment, not the @theme directive; did not @import Marp default theme). Combined with overflow:hidden, blunt img max-height:320px, header+footer chrome on every slide, and two P5 diagrams with extreme aspect ratios (13.52x and 0.63x), 8 of 19 slides overflowed. NOT a P5 regression — theme CSS byte-identical P3->P5; P5 denser content made pre-existing flaws visible. Tags on v1.21.x line (v1.21.0 P0 -> v1.21.6 P6 final = milestone release). 32 slide tests pass (23 original + 9 new). 94 key-file tests pass. Pipeline check exit 0. ---ci--- project: acdl phase: 6 milestone: v1.22 status: complete phase_role: final requirements: covered: [REQ-254,REQ-255,REQ-256,REQ-257,REQ-258,REQ-259,REQ-260,REQ-261,REQ-262] partial: [] ---/ci---
373 lines
16 KiB
Python
373 lines
16 KiB
Python
"""REQ-239..243 (v1.20) + REQ-245,251,252 (v1.21): S&P theme + slide render
|
|
pipeline + deck-refinement tests.
|
|
|
|
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
|
|
- Every .mmd has a corresponding .png
|
|
- The render_slides.sh script exists and is executable
|
|
- The CI workflow file exists
|
|
|
|
v1.21 adds (REQ-245,251,252):
|
|
- Deck renamed to nova-autonomous-cloud-delivery*
|
|
- No maturity badges in the Marp deck
|
|
- No version in the Marp footer/title slide
|
|
- 20 main + 1 appendix slides (v1.22 split slides 3+8 to relieve overflow)
|
|
- No D-###/REQ-###/internal .py paths in audience-facing slides
|
|
- Title is "Nova — The Autonomous Cloud Delivery Platform"
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
PRESENTATIONS = ROOT / "docs" / "presentations"
|
|
ASSETS = PRESENTATIONS / "assets"
|
|
THEME_CSS = ASSETS / "nova-sp-theme.css"
|
|
THEME_JSON = ASSETS / "mmd" / "sp-theme.json"
|
|
MARP_DECK = PRESENTATIONS / "nova-autonomous-cloud-delivery-marp.md"
|
|
SOURCE_MD = PRESENTATIONS / "nova-autonomous-cloud-delivery.md"
|
|
RENDER_SCRIPT = ROOT / "scripts" / "render_slides.sh"
|
|
SLIDES_WORKFLOW = ROOT / ".github" / "workflows" / "slides.yml"
|
|
|
|
|
|
def test_sp_theme_css_exists():
|
|
"""REQ-239: nova-sp-theme.css exists."""
|
|
assert THEME_CSS.is_file(), f"theme CSS not found: {THEME_CSS}"
|
|
|
|
|
|
def test_sp_theme_css_has_snp_colors():
|
|
"""REQ-239: CSS contains S&P Red and Black."""
|
|
css = THEME_CSS.read_text()
|
|
assert "#D6002A" in css, "S&P Red (#D6002A) missing from theme CSS"
|
|
assert "#1B1B1B" in css, "S&P Black (#1B1B1B) missing from theme CSS"
|
|
|
|
|
|
def test_sp_theme_json_has_snp_colors():
|
|
"""The mermaid theme JSON also has S&P colors."""
|
|
json_text = THEME_JSON.read_text()
|
|
assert "#D6002A" in json_text, "S&P Red missing from mermaid theme"
|
|
assert "#1B1B1B" in json_text, "S&P Black missing from mermaid theme"
|
|
|
|
|
|
def test_marp_deck_uses_sp_theme():
|
|
"""REQ-239: Marp deck frontmatter references nova-sp-theme.css."""
|
|
text = MARP_DECK.read_text()
|
|
# The frontmatter is between the first two ---
|
|
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():
|
|
"""The Marp deck must not use 'theme: default'."""
|
|
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)
|
|
assert "theme: default" not in frontmatter, \
|
|
"Marp deck still uses 'theme: default' — should use nova-sp-theme.css"
|
|
|
|
|
|
def test_render_slides_script_exists():
|
|
"""REQ-240: render_slides.sh exists and is executable."""
|
|
assert RENDER_SCRIPT.is_file(), "render_slides.sh not found"
|
|
import os
|
|
assert os.access(RENDER_SCRIPT, os.X_OK), "render_slides.sh not executable"
|
|
|
|
|
|
def test_render_slides_script_renders_mermaid():
|
|
"""REQ-240: render_slides.sh renders mermaid diagrams."""
|
|
text = RENDER_SCRIPT.read_text()
|
|
assert "mermaid-cli" in text or "mmdc" in text, \
|
|
"render_slides.sh does not invoke mermaid-cli"
|
|
assert "sp-theme.json" in text, \
|
|
"render_slides.sh does not reference sp-theme.json"
|
|
|
|
|
|
def test_render_slides_script_renders_marp():
|
|
"""REQ-240: render_slides.sh renders Marp HTML + PPTX."""
|
|
text = RENDER_SCRIPT.read_text()
|
|
assert "marp-cli" in text, "render_slides.sh does not invoke marp-cli"
|
|
assert ".html" in text, "render_slides.sh does not produce HTML"
|
|
assert ".pptx" in text, "render_slides.sh does not produce PPTX"
|
|
|
|
|
|
def test_render_slides_default_deck_renamed():
|
|
"""REQ-245: render_slides.sh default deck is nova-autonomous-cloud-delivery."""
|
|
text = RENDER_SCRIPT.read_text()
|
|
assert "nova-autonomous-cloud-delivery" in text, \
|
|
"render_slides.sh does not default to nova-autonomous-cloud-delivery"
|
|
|
|
|
|
def test_slides_ci_workflow_exists():
|
|
"""REQ-241: CI workflow for slides exists."""
|
|
assert SLIDES_WORKFLOW.is_file(), "slides.yml workflow not found"
|
|
|
|
|
|
def test_slides_ci_workflow_triggers_on_presentations():
|
|
"""REQ-241: CI workflow triggers on docs/presentations/ changes."""
|
|
text = SLIDES_WORKFLOW.read_text()
|
|
assert "docs/presentations" in text, \
|
|
"slides.yml does not trigger on docs/presentations/"
|
|
assert "render_slides.sh" in text, \
|
|
"slides.yml does not invoke render_slides.sh"
|
|
|
|
|
|
def test_every_mmd_has_png():
|
|
"""REQ-240: every .mmd file has a corresponding .png."""
|
|
mmd_dir = ASSETS / "mmd"
|
|
png_dir = ASSETS / "png"
|
|
if not mmd_dir.is_dir():
|
|
pytest.skip("no .mmd directory")
|
|
mmd_files = sorted(mmd_dir.glob("*.mmd"))
|
|
assert len(mmd_files) > 0, "no .mmd files found"
|
|
missing = []
|
|
for mmd in mmd_files:
|
|
png = png_dir / f"{mmd.stem}.png"
|
|
if not png.is_file():
|
|
missing.append(mmd.name)
|
|
assert not missing, f"PNGs missing for: {missing}"
|
|
|
|
|
|
def test_readme_no_retired_decks():
|
|
"""REQ-243: presentations README does not list retired decks."""
|
|
readme = (PRESENTATIONS / "README.md").read_text()
|
|
assert "how-the-platform-works" not in readme, \
|
|
"README still references retired 'how-the-platform-works' deck"
|
|
assert "the-developer-experience" not in readme, \
|
|
"README still references retired 'the-developer-experience' deck"
|
|
|
|
|
|
def test_readme_no_old_deck_name():
|
|
"""REQ-245: README references the new deck name, not the old one."""
|
|
readme = (PRESENTATIONS / "README.md").read_text()
|
|
assert "nova-autonomous-cloud-delivery" in readme, \
|
|
"README does not reference nova-autonomous-cloud-delivery"
|
|
|
|
|
|
def test_old_deck_files_removed():
|
|
"""REQ-245: the old nova-no-humans-platform* files are gone."""
|
|
old_files = sorted(PRESENTATIONS.glob("nova-no-humans-platform*"))
|
|
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)" |