SnowOps report theme (PA08 / PA09)¶
The theme behind the Discovery Audit report — the single highest-value asset in the business. It renders Markdown into a branded, print-perfect report as both a self-contained HTML file and a PDF.
This is a theme, not a document. The audit report is generated by apps/discovery-auditor, so everything here is driven by element type and a small decorator script — it must work on generated content of variable length, never on hand-placed markup.
| File | What it is |
|---|---|
snowops-report.css |
The theme. Signal tokens only — no new colors |
snowops-report.html |
Pandoc template: metadata-driven cover + the severity-chip decorator |
render.js |
Markdown → self-contained HTML → PDF (with a running footer) |
fonts/ |
IBM Plex Sans/Mono, latin subset (SIL OFL 1.1) — inlined at render time |
assets/ |
The Signet mark used on the cover |
Produced with it: PA09 sanitized sample audit.
Render¶
npm i puppeteer-core # once — drives your installed Chrome, downloads no browser
node render.js path/to/report.md --outdir path/to/out
Outputs report.html (single file, no external requests — safe to email or host) and report.pdf (Letter, running footer with page numbers).
Set CHROME_PATH if Chrome isn't at the default macOS location. Pass --html-only to skip the PDF.
The metadata contract¶
The report Markdown must not contain its own cover. The cover is built from YAML front matter, so it stays consistent across every report and can't drift:
---
title: "Cloud Posture Discovery Audit"
eyebrow: "SnowOps Free Discovery Audit" # optional
client: "Contoso Ltd" # required — no client, no cover
scope: "sub-8f2a1c: contoso-prod"
date: "17 July 2026"
auditor: "SnowOps — automated scan, human-reviewed"
access: "Reader + Security Reader (read-only)"
runid: "run-2026-07-17-a91f"
window: "Time-boxed · credential revoked at close"
confidential: true # prints the "Confidential — prepared for <client>" badge + footer
sample: true # prints the synthetic-data banner + a SAMPLE footer on every page
---
The read-only assurance block is rendered from the template, not the content — it's a standing contract, so it can't be edited away by accident.
What the theme does for you¶
- Severity chips — the decorator finds severities as whole table cells (
CRITICAL, from the auditor's findings table) or bracketed tokens in prose ([critical] NET-002 — …, case-insensitive, because the auditor emits the enum lowercase) and renders them as chips. - Grayscale-legible severity (brand rule): every chip carries a distinct glyph + label, so it survives a black-and-white print. Never signal severity by color alone.
- Column widths — Pandoc emits an equal
width: N%on every column regardless of content; the theme neutralises that so columns size to their content. - Print hardening — table headers repeat across page breaks, rows/callouts/code don't split, headings don't strand at a page foot.
⚠️ Before any client sees a report¶
- Human review is a hard contract (G4) — the report claims it was reviewed before delivery. Make that true.
- Claims: the report maps findings to controls and must never state or imply the client "is" or "will be" certified. The wording is gaps against a framework. Nidhi gates compliance claims.
- Samples: any report built on synthetic data must set
sample: true. It banners the cover and stamps every page footer, so a sample can never be mistaken for a real posture.
Known gap — wiring this into the auditor¶
apps/discovery-auditor/src/render/pdf.ts renders Markdown → PDF via Pandoc + xelatex, which does not use this theme; its output is unbranded default LaTeX. Two changes are needed to close that, and both are app work, not design work:
- Point the render step at this theme (HTML template + CSS + Chrome) instead of xelatex.
- Move the cover out of
src/render/markdown.ts(cover()) and emit those fields as YAML front matter instead — otherwise every report prints two covers, the theme's and the renderer's.
Until then, the theme is verified to render the auditor's exact output shape (executive summary → findings table → roadmap → appendix); only the cover section needs to move.