Compare commits

...

25 Commits

Author SHA1 Message Date
Jon Chery d069654367 feat(P2): env-transition detect-and-destroy — REQ-282..287
acdl-ci / Lint (push) Successful in 10s
acdl-ci / Test (push) Failing after 24s
acdl-ci / Platform check-only (offline) (push) Successful in 24s
Nova Slides Render / render (push) Failing after 24s
- core/env_transition.py: detect_prior_env() + record_applied_env() via DynamoDB nova-contracts table (REQ-282,283)
- scripts/run_platform.sh Step 0b: detect env change, destroy prior env (deletion_protection=false, terraform init -reconfigure + destroy), emit ENV_DESTROYED evidence event, fail closed on destroy failure (REQ-284)
- scripts/run_platform.sh: record applied env after successful apply (REQ-285)
- .github/workflows/deploy.yml: pass NOVA_CONSUMER_REPO to run_platform.sh (REQ-286)
- adapters/terraform/adapter.py: doc comment on env-scoped state key (REQ-287)

No orphan path: if destroy fails, pipeline exits non-zero (no apply runs).

---ci---
project: acdl
phase: 2
milestone: v1.24
status: execute
requirements: [REQ-282,REQ-283,REQ-284,REQ-285,REQ-286,REQ-287]
---/ci---
2026-08-12 14:30:24 +00:00
Jon Chery 25427250ad docs(P1): consumer guide accuracy fixes — REQ-276..281,290
acdl-ci / Lint (push) Successful in 9s
acdl-ci / Test (push) Failing after 22s
acdl-ci / Platform check-only (offline) (push) Successful in 24s
Nova Slides Render / render (push) Failing after 24s
- Step 3 contract fields table: stale uses/module → real id/name/environment/infrastructure (REQ-276)
- Step 4 caller: add environment: dev to match Step 2 (REQ-277)
- Step 5 stage 8: (dev only) → (autonomous in dev; higher envs apply after HITL) (REQ-278)
- Step 8: rewrite with Shape A destroy-then-rebuild + Shape B cross-ref (REQ-279)
- Per-env section: add Shape B lead sentence (REQ-280)
- Reference table: @v1.19 wording + .yaml→.yml extension fix (REQ-281)
- Tests: rename no-field-editing → both-promotion-shapes + new destroy-on-env-change test (REQ-290)

---ci---
project: acdl
phase: 1
milestone: v1.24
status: execute
requirements: [REQ-276,REQ-277,REQ-278,REQ-279,REQ-280,REQ-281,REQ-290]
---/ci---
2026-08-12 14:26:22 +00:00
Jon Chery eca1181716 docs(ship): P0 complete — v1.24 pre-execution (specify, clarify, research, plan, grill)
acdl-ci / Lint (push) Successful in 9s
acdl-ci / Test (push) Failing after 24s
acdl-ci / Platform check-only (offline) (push) Successful in 27s
---ci---
project: acdl
phase: 0
milestone: v1.24
status: complete
ship: v1.23.0 (gitea release id 635)
---/ci---
2026-08-12 14:24:19 +00:00
Jon Chery 0920550ae5 docs(P00): grill — PROCEED (0.82), 0 escalations, 2 revisions (already captured)
acdl-ci / Lint (push) Successful in 9s
acdl-ci / Test (push) Failing after 22s
acdl-ci / Platform check-only (offline) (push) Successful in 24s
Nova Slides Render / render (push) Failing after 22s
---ci---
project: acdl
phase: 0
milestone: v1.24
status: grill
---/ci---
2026-08-12 14:23:55 +00:00
Jon Chery d8240588c9 docs(P00): create phase plans — v1.24 (4 phases, 4 waves)
---ci---
project: acdl
phase: 0
milestone: v1.24
status: plan
---/ci---
2026-08-12 14:23:24 +00:00
Jon Chery 5dc97673e5 docs(P00): research findings — v1.24 env-transition detect-and-destroy
---ci---
project: acdl
phase: 0
milestone: v1.24
status: research
---/ci---
2026-08-12 14:22:37 +00:00
Jon Chery 956cf91ce0 docs(P00): clarify — 6 ambiguities auto-resolved (full autonomy)
---ci---
project: acdl
phase: 0
milestone: v1.24
status: clarify
---/ci---
2026-08-12 14:21:32 +00:00
Jon Chery a7a93d95d1 docs(init): validate specification — v1.24 consumer guide accuracy + env-promotion lifecycle
---ci---
project: acdl
phase: 0
milestone: v1.24
status: specify
---/ci---
2026-08-12 14:20:34 +00:00
Jon Chery afca994511 docs(ship): v1.23 milestone checkpoint complete — v1.22.6 released (id 634)
acdl-ci / Lint (push) Successful in 8s
acdl-ci / Platform check-only (offline) (push) Successful in 24s
acdl-ci / Test (push) Failing after 24s
2026-08-12 00:37:59 +00:00
Jon Chery e63c0cb36e docs(milestone): complete v1.23 — Nova Deck Cleanup & Python PPTX
acdl-ci / Lint (push) Successful in 10s
acdl-ci / Test (push) Failing after 23s
acdl-ci / Platform check-only (offline) (push) Successful in 25s
Nova Slides Render / render (push) Failing after 34s
13 requirements complete (REQ-263..275):
- P1: consolidate-docs — single -marp.md source of truth, speaker notes
  + talking points as HTML comments, delete plain .md (REQ-263,264)
- P2: restore-clean-style — theme:default + inline S&P style, retire
  nova-sp-theme.css from render (keep as reference), benefit .benefit
  class (REQ-265,266,267)
- P3a: inline-images — scripts/inline_images.py, self-contained HTML
  (REQ-268)
- P3b: python-pptx-generator — scripts/render_pptx.py structured
  editable S&P-themed PPTX, pyproject [slides] dep, dual PPTX
  (REQ-269,270)
- P4: trim-wordcount — ~20-30% trim on 8 verbose slides, remove
  'penetrate' repo-wide (G-001) (REQ-271,272)
- P5: ci-tests-readme + review + audit + ship — workflows install
  python-pptx, 43 tests pass, README rewritten (REQ-273,274,275)

Tags on v1.22.x line (v1.22.0 P0 -> v1.22.6 P5 final = milestone
release). Grill: PROCEED-WITH-REVISIONS (4 binding revisions G-001..G-004
applied: repo-wide penetrate purge, P3->P4 serialized, P3 split P3a+P3b,
P5+P6 merged). 43 slide/pptx tests pass. Merged to main.

---ci---
project: acdl
phase: 5
milestone: v1.23
status: complete
phase_role: final
requirements:
  covered: [REQ-263,REQ-264,REQ-265,REQ-266,REQ-267,REQ-268,REQ-269,REQ-270,REQ-271,REQ-272,REQ-273,REQ-274,REQ-275]
  partial: []
---/ci---
2026-08-12 00:36:46 +00:00
Jon Chery 3512261051 docs(milestone): merge v1.23 — Nova Deck Cleanup & Python PPTX to main
13 requirements (REQ-263..275) complete. Tags on v1.22.x line.
Final patch v1.22.6 = milestone release.

---ci---
project: acdl
phase: 5
milestone: v1.23
status: complete
phase_role: final
requirements:
  covered: [REQ-263,REQ-264,REQ-265,REQ-266,REQ-267,REQ-268,REQ-269,REQ-270,REQ-271,REQ-272,REQ-273,REQ-274,REQ-275]
  partial: []
---/ci---
2026-08-12 00:35:23 +00:00
Jon Chery 14c11027a8 test(ship): P5 complete — ci-tests-readme + review + audit + ship (REQ-273,274,275)
Nova Slides Render / render (push) Failing after 34s
---ci---
project: acdl
phase: 5
milestone: v1.23
status: complete
phase_role: final
---/ci---
2026-08-12 00:35:18 +00:00
Jon Chery e07a210c70 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---
2026-08-12 00:34:23 +00:00
Jon Chery 9b8ab75b85 docs(ship): P4 complete — trim-wordcount + penetrate purge (REQ-271,272)
Nova Slides Render / render (push) Successful in 1m3s
---ci---
project: acdl
phase: 4
milestone: v1.23
status: complete
phase_role: execution
---/ci---
2026-08-12 00:27:57 +00:00
Jon Chery 9bc37301ba docs(P4): trim word count + purge 'penetrate' repo-wide (REQ-271,272)
Targeted ~20-30% word-count trim on 8 verbose slides (1, 5, 7, 8, 13,
14, 20, appendix). Tables + short slides untouched. Spirit preserved.
Removed 'penetrate' (and derivatives) from docs/scope.md, docs/vision.md,
.ciagent/PROJECT.md, .ciagent/CLARIFY.md, .ciagent/NORTH_STAR.md, and
presentation files (G-001 binding revision). RESEARCH.md/PLAN.md/GRILL.md
exempt as decision-history. Slide 5 'penetrates' phrase removed with no
replacement (slide 4 Anti-Goals already excludes the PDLC).

---ci---
project: acdl
phase: 4
milestone: v1.23
status: execute
phase_role: execution
---/ci---
2026-08-12 00:26:37 +00:00
Jon Chery 5476f8eb24 feat(ship): P3b complete — python-pptx-generator (REQ-269,270)
Nova Slides Render / render (push) Successful in 1m2s
---ci---
project: acdl
phase: 3
milestone: v1.23
status: complete
phase_role: execution
---/ci---
2026-08-12 00:21:47 +00:00
Jon Chery 863f482f9c feat(P3b): python-pptx generator — structured editable S&P-themed PPTX (REQ-269,270)
New scripts/render_pptx.py parses the consolidated -marp.md and
produces a structured, editable, S&P-themed PPTX via python-pptx.
16:9; title slide black bg + red top bar; content slides with red H2
titles, bullets, blockquotes, embedded PNGs, native tables, benefit
callouts. Added python-pptx>=0.6.23 to pyproject [slides] optional-dep.
render_slides.sh Step 4 produces it; attach_release_asset.py extended
for dual PPTX. Output: nova-autonomous-cloud-delivery-python.pptx.

---ci---
project: acdl
phase: 3
milestone: v1.23
status: execute
phase_role: execution
---/ci---
2026-08-12 00:21:17 +00:00
Jon Chery 66b13a6d0c docs(ship): P3a complete — inline-images (REQ-268)
Nova Slides Render / render (push) Successful in 59s
---ci---
project: acdl
phase: 3
milestone: v1.23
status: complete
phase_role: execution
---/ci---
2026-08-12 00:17:23 +00:00
Jon Chery 485d105bcd docs(P3a): inline images for self-contained HTML (REQ-268)
New scripts/inline_images.py (stdlib only: base64, re, mimetypes) —
base64-embeds all relative-path <img src='assets/...'> images into
the rendered HTML so it's redistributable without the assets/ folder.
MIME-sniffs by extension (.png->image/png, .svg->image/svg+xml, etc).
render_slides.sh Step 3 invokes it after the MARP HTML render, before
staging. Verified: 2 images inlined, 0 file-path refs remaining.

---ci---
project: acdl
phase: 3
milestone: v1.23
status: execute
phase_role: execution
---/ci---
2026-08-12 00:17:19 +00:00
Jon Chery df426afd6a docs(ship): P2 complete — restore-clean-style (REQ-265,266,267)
Nova Slides Render / render (push) Successful in 1m10s
---ci---
project: acdl
phase: 2
milestone: v1.23
status: complete
phase_role: execution
---/ci---
2026-08-12 00:14:23 +00:00
Jon Chery 9114227ef1 docs(P2): restore clean style — theme:default + inline style (REQ-265,266,267)
Reverted frontmatter theme: nova-sp -> theme: default + inline style:
block with S&P palette (#D6002A, #1B1B1B, Akkurat Pro). Retired
nova-sp-theme.css from render path (kept as reference with header
comment). render_slides.sh drops --theme arg. Converted all 21
**Benefit:** callouts to <div class='benefit'> (red top-rule + black
italic; white on title slides). Matches the old
the-developer-experience.html clean style.

---ci---
project: acdl
phase: 2
milestone: v1.23
status: execute
phase_role: execution
---/ci---
2026-08-12 00:14:07 +00:00
Jon Chery c9ace0af6e docs(ship): P1 complete — consolidate-docs (REQ-263,264)
Nova Slides Render / render (push) Successful in 59s
---ci---
project: acdl
phase: 1
milestone: v1.23
status: complete
phase_role: execution
---/ci---
2026-08-12 00:11:18 +00:00
Jon Chery a47c16245a docs(P1): consolidate deck to single source of truth (REQ-263,264)
Fold speaker notes + transitions + talking points into
nova-autonomous-cloud-delivery-marp.md as Marp HTML comments
(<!-- Speaker notes: ... -->, <!-- Transition: ... -->,
<!-- Talking points: ... -->). The -marp.md is now the sole source of
truth. Deleted the plain nova-autonomous-cloud-delivery.md.
talking-points.md kept as standalone synced aid (header updated).

---ci---
project: acdl
phase: 1
milestone: v1.23
status: execute
phase_role: execution
---/ci---
2026-08-12 00:09:37 +00:00
Jon Chery 74e9d4d887 docs(ship): P0 checkpoint complete — v1.22.0 released (id 628) 2026-08-12 00:06:38 +00:00
Jon Chery 818e285fac docs(ship): P0 complete — v1.23 pre-execution (specify, clarify, research, plan, grill)
---ci---
project: acdl
phase: 0
milestone: v1.23
status: complete
phase_role: pre_execution
---/ci---
2026-08-12 00:05:40 +00:00
37 changed files with 3807 additions and 5540 deletions
+14 -4
View File
@@ -1,9 +1,19 @@
{
"phase": 0,
"stage": "grill",
"milestone": "v1.23",
"stage": "complete",
"milestone": "v1.24",
"phase_role": "pre_execution",
"attempts": 0,
"updated_at": "2026-08-11T23:30:00Z",
"project": "acdl"
"updated_at": "2026-08-12T02:20:00Z",
"project": "acdl",
"milestone_complete": false,
"tag": "v1.23.0",
"tag_line": "v1.23.x",
"release": {
"forge": "gitea",
"release_id": 635,
"tag": "v1.23.0"
},
"requirements": ["REQ-276","REQ-277","REQ-278","REQ-279","REQ-280","REQ-281","REQ-282","REQ-283","REQ-284","REQ-285","REQ-286","REQ-287","REQ-288","REQ-289","REQ-290"],
"phases": ["P1:consumer-guide-fixes","P2:env-transition-detect-and-destroy","P3:env-transition-tests","P4:final-review-ship"]
}
+100 -67
View File
@@ -1,76 +1,109 @@
# CLARIFY — v1.23 (auto-resolved, full autonomy)
# CLARIFY — v1.24 Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement
8 ambiguities identified, all auto-resolved at confidence ≥ 0.6.
No human escalation (full autonomy). All decisions logged below.
> **Autonomy:** full. Ambiguities are auto-resolved with assumption logging
> per `config.json autonomy.level: "full"`. No human escalation.
## C1 — Speaker notes source for HTML comments (conf 0.95)
**Ambiguity:** The plain `.md` has `> **Speaker notes:**` and
`> **Transition:**` blocks per slide; talking points live in a separate
file. Which content goes into the `<!-- ... -->` comments?
**Resolution:** Extract `> **Speaker notes:**` and `> **Transition:**`
blocks from the plain `.md` and embed each slide's as
`<!-- Speaker notes: ... -->` before the next `---` separator. Talking
points come from `talking-points.md` (the 3-6 bullets per slide)
embedded as `<!-- Talking points: ... -->`. Both are Marp HTML comments
(excluded from slide rendering; the `talking-points.md` stays as the
standalone synced aid).
## Ambiguities Identified
## C2 — python-pptx dependency placement (conf 0.9)
**Ambiguity:** `pyproject.toml` has `[project.optional-dependencies]`
`test`. Where does `python-pptx` go?
**Resolution:** Add a new optional-dependency group
`slides = ["python-pptx>=0.6.23"]`. Keeps the base `dependencies`
minimal (the render script is a docs-only concern, not a runtime
dep). CI installs via `pip install -e .[slides]`. The `render_pptx.py`
script imports `pptx` at module load (not a hard failure if absent —
the script prints a clear "pip install -e .[slides]" message and exits
1).
### A1 — Step 8 "change environment" vs Per-env section "no field editing"
## C3 — Benefit callout markdown representation (conf 0.9)
**Ambiguity:** Marp markdown can't natively produce arbitrary `<div>`.
How is the `.benefit` class applied?
**Resolution:** Use Marp's HTML-in-markdown support — write benefit
lines as `<div class="benefit">text</div>` directly in the markdown
(Marp passes through HTML blocks). The inline `style:` block targets
`.benefit`. The `**Benefit:**` prefix is removed.
**Ambiguity:** The consumer guide contains two mutually-exclusive promotion
models. Step 8 (line 290) says "Change `environment` in your contract." The
"Per-environment deployment" section (line 398) says "you do not edit the
`environment:` field… Promotion = running the matching job." The test
`test_consumer_guide_states_no_field_editing` asserts the no-editing model.
## C4 — Image inlining MIME types (conf 0.95)
**Ambiguity:** What MIME types does `inline_images.py` handle?
**Resolution:** The deck references `.png` files only (mermaid
renders). `inline_images.py` MIME-sniffs by extension:
`.png``image/png`, `.svg``image/svg+xml`, `.jpg/.jpeg``image/jpeg`,
`.gif``image/gif`; fallback `application/octet-stream`. Only relative
`src="assets/..."` paths are inlined (absolute/`http(s)://` URLs are
left alone).
**User directive (binding):** Both shapes are supported. Shape A (edit
environment in-place) is valid AND must trigger a destroy of the prior env.
Shape B (per-env caller workflows) is the alternative. The test must be
updated to assert both shapes.
## C5 — render_slides.sh step ordering (conf 0.9)
**Ambiguity:** Where do the new inline-images + python-pptx steps go?
**Resolution:** Step 1 mermaid → PNG (unchanged); Step 2 MARP → HTML +
PPTX (drop `--theme`); Step 3 inline images into HTML; Step 4
python-pptx → `*-python.pptx`; Step 5 stage all. The inline step runs
only on the HTML (not the PPTX — both PPTX formats embed media in
their zip by construction).
**Resolution (auto, confidence 0.95):** Adopt the user's directive. Step 8
is rewritten to document Shape A with destroy-then-rebuild semantics. The
per-env section is preserved as Shape B with a lead sentence distinguishing
it. The test is renamed and a new test asserts the destroy semantics. This
is already captured in REQ-279, REQ-280, REQ-290.
## C6 — "penetrate" absence test scope (conf 0.95)
**Ambiguity:** How is "penetrate" absence asserted?
**Resolution:** `grep -ri penetrat docs/presentations/` returns nothing
(covers `.md`, `-marp.md`, `talking-points.md`, `.html`; `.pptx` is
binary so excluded). A new test `test_no_penetrate_term` asserts this.
### A2 — Prior-env source of truth: DynamoDB vs state-bucket scan vs SSM
## C7 — attach_release_asset.py dual-PPTX (conf 0.85)
**Ambiguity:** Does the release attach both PPTX files?
**Resolution:** Yes — attach both `.pptx` (MARP image-of-slide) and
`-python.pptx` (structured). Minimal change: loop over both filenames.
The MARP PPTX remains the "primary" attachment (first); the python
PPTX is secondary (comparison artifact).
**Ambiguity:** Three options for detecting the prior environment: (a) query
the `nova-contracts` DynamoDB table, (b) scan the state bucket for other env
prefixes, (c) record last-applied env in an SSM parameter.
## C8 — Wave ordering (conf 0.85)
**Ambiguity:** Can P1 (consolidate) and P2 (style) parallelize?
**Resolution:** No — both edit `-marp.md` with a serial dependency: P1
establishes the single doc (deletes the plain `.md`, adds HTML-comment
speaker notes); P2 edits that single doc's frontmatter + benefit
callouts. Order: P1 → P2 → (P3 + P4 parallel: P3 edits
`scripts/inline_images.py` + `render_pptx.py` + `render_slides.sh` +
`pyproject`; P4 edits the deck markdown — zero file overlap) → P5 →
P6. P3 depends on P2's render-script `--theme` drop. P4 depends on
P1's consolidated doc.
**Resolution (auto, confidence 0.85):** DynamoDB `nova-contracts` table
(user-selected). It already exists, is written by the contract ingestor
Lambda (`core/lambda/contract_ingestor.py:160-170`), and has the right shape
(PK `consumerRepo`, SK `contractId#submittedAt`, `environment` attribute).
A new `#LAST_APPLIED` SK suffix is added for the record-applied-env step
(REQ-283). This avoids coupling the platform to a specific state-bucket
layout (which differs across envs/accounts) and avoids a new SSM dependency.
**Assumption:** The `nova-contracts` table is accessible from the deploy
role via the same ABAC scoping that the contract ingestor uses. If the
table is not accessible (e.g., local/CI mode without DynamoDB), the detect
step logs a warning and returns `None` (conservative — no prior env
assumed). This is documented in REQ-282.
### A3 — Cross-account destroy
**Ambiguity:** If the prior env (e.g., dev) and new env (e.g., qa) are in
different AWS accounts, the destroy step needs the prior env's role
credentials. The current scaffold uses one account.
**Resolution (auto, confidence 0.80):** v1.24 targets the same-account
case. Cross-account destroy is explicitly out of scope (documented in the
Out of Scope section). The `run_platform.sh` Step 0b notes this limitation.
A future milestone handles cross-account destroy via a pre-step that
assumes the prior env's role. This is the pragmatic path — the scaffold
(`core/environments/dev.json`) is single-account today.
### A4 — Version tag in docs: `@v1.19` vs `ref: v1.9`
**Ambiguity:** The consumer guide says `uses: nova/.github/workflows/deploy.yml@v1.19`
but the actual `.github/workflows/deploy.yml` checks out the platform repo
at `ref: v1.9`. The reference table says sample contracts "use `@v1.19`"
but the sample contracts don't carry `uses:` (they're contracts, not
workflows).
**Resolution (auto, confidence 0.90):** REQ-281 corrects the reference
table wording to "used with caller workflow `@v1.19`" (the version pin
lives in the caller workflow, not the contract). The `@v1.19` tag in the
consumer-facing docs is the documented current version; the `ref: v1.9` in
deploy.yml is the platform-internal checkout ref. These are two different
references (consumer → platform workflow tag; platform workflow → platform
repo ref). The guide's `@v1.19` stays as the consumer-facing version. No
change to deploy.yml's `ref: v1.9` (that's an internal platform concern,
out of scope for this milestone).
### A5 — Should Shape A destroy go through the HITL decommission pipeline?
**Ambiguity:** The decommission pipeline (2-step, HITL SRE gates) exists for
stack teardown. Should env-transition destroy use it?
**Resolution (auto, confidence 0.85):** No. Env-transition is an automated
lifecycle step, not an explicit decommission. The destroy runs as a direct
`terraform destroy -auto-approve` against the prior env's state (REQ-284).
The decommission pipeline remains for explicit stack teardown with SRE
gates. This is documented in the Out of Scope section. Rationale: the
consumer already has HITL attestation on the *new* env (qa/prod/dr gates);
requiring a second SRE gate for the prior env's destroy would block
autonomous dev→qa promotion, contradicting the "lower environments are
autonomous" tenet.
### A6 — Phase count and ordering
**Ambiguity:** The requirements traceability table shows 3 phases (P1:
docs, P2: feat, P3: test) but the roadmap entry says "4 phases."
**Resolution (auto, confidence 0.90):** 4 phases = P0 (pre-execution) + P1
(docs fixes) + P2 (env-transition feat) + P3 (tests) + P4 (final
review/ship). The "4 phases" in the roadmap counts execution phases (P1-P3)
+ final (P4). This matches the run.md phase model (P0 pre-execution, P1..PN
execution, P N+1 final). The traceability table lists P1-P3 (execution);
P4 is the final phase (review + audit + ship, no new requirements).
## Clarification Commit
No changes to REQUIREMENTS.md or PROJECT.md from clarify — the ambiguities
are resolved and already captured in the requirements (REQ-276..290) and
the Out of Scope section. The resolutions above are logged for traceability.
+176 -183
View File
@@ -1,207 +1,200 @@
# CIAgent Grill Report
## Run: 2026-08-12 (mode: interactive, focus: all axes) — v1.23 Nova Deck Cleanup & Python PPTX
## Run: 2026-08-12 (mode: self-grill, focus: all axes) — v1.24 Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement
### Overall Verdict: PROCEED-WITH-REVISIONS (confidence: 0.78)
### Overall Verdict: PROCEED (confidence: 0.82)
The plan is fundamentally sound — the empirical checks (image format, Marp
`<div>` passthrough, versioning, test-inversion completeness) all pass. But
four binding revisions restructure the wave/phase plan and expand the
"penetrate" purge scope. Two axes were escalated-but-resolved (the empirical
verifications de-risked P2 and P3). The plan proceeds with the revisions
below.
The plan is sound. The user directive is clear and binding. The code
integration points are confirmed by inspection. Two binding revisions
applied (both low-risk doc clarifications). No escalations.
---
### Per-Axis Findings
## Axis 1: Feasibility
#### Axis 1 — Scope / over-reach: PASS (conf 0.80)
Building a whole new `render_pptx.py` + markdown parser is significant for a
"deck cleanup" milestone, but REQ-269 explicitly mandates it, the milestone
is NFR docs/render/test-only (no runtime impact), and the dual-PPTX value
(MARP image-of-slide + python structured/editable) is a real deliverable, not
gold-plating. The G-003 split (P3a/P3b) mitigates the parser-complexity risk.
The plan stays in scope; it does not reach into the deck's 4-beat narrative
arc (out-of-scope section confirms).
**Challenge:** Can `run_platform.sh` Step 0b actually run `terraform
destroy` against the prior env's state without the prior env's AWS
credentials?
#### Axis 2 — Hidden dependencies (P3/P4 "zero file overlap"): REVISE → G-002 (conf 0.85)
The "zero file overlap" claim is true for EDITS but false for VERIFICATION.
P3's `render_pptx.py` parsing logic depends on the deck markdown structure
that P4 is simultaneously trimming; P4's verify step runs `render_slides.sh`
that P3 is mid-editing. **Binding revision G-002:** serialize P3→P4.
**Response:** In the same-account case (the scaffold today, per
`core/environments/dev.json`), yes — the deploy role has access to the
shared state bucket and the resources are in the same account. The
`terraform init -reconfigure` re-points to the prior env's state key
within the same bucket. Cross-account is explicitly out of scope
(D-205). **Confidence: 0.85.**
#### Axis 3 — Test inversion risk: PASS (conf 0.92)
The plan's test-update list (Finding 5 / P5 tasks) is complete and accurate.
Empirically confirmed all affected tests:
- `test_marp_deck_not_using_default_theme` (line 66-74: invert — we now USE
`theme: default`) ✓ plan mentions
- `test_marp_deck_uses_sp_theme` (line 55-64: rewrite to assert inline `style:`
block) ✓ plan mentions
- `test_theme_css_*` trio (lines 275, 284, 295: retarget to inline `style:`) ✓
- `test_html_embeds_theme` (line 353: `--sp-red``#D6002A`) ✓ plan mentions
- `test_source_md_*` pair (lines 240, 266: delete — plain `.md` gone) ✓
- `test_render_slides_script_renders_marp` (line 92: still produces `.pptx`
via marp-cli for the fallback PPTX) — ✓ plan handles
- No latent assertions missed. The plan's enumeration is thorough.
**Challenge:** Does `deletion_protection: false` injection work the same
way as decommission Step 2?
#### Axis 4 — Image-inlining correctness: PASS (conf 0.95)
Empirically verified the rendered HTML at
`docs/presentations/nova-autonomous-cloud-delivery.html` uses plain
`<img src="assets/png/platform-pipeline.png" ...>` and
`<img src="assets/png/telemetry-live-ops.png" ...>` — standard `<img>` tags
inside Marp's SVG `<foreignObject>` wrapper. Zero `xlink:href` occurrences.
The `inline_images.py` regex (`<img\s+src="([^"]+)"`) will match. The 2
`data:image` matches already in the HTML are Marp OSC onscreen-control SVG
icons (CSS backgrounds, not slide images) — not a conflict. **P0 risk
cleared.**
**Response:** Yes. `scripts/run_decommission.sh:34-37` sets
`res['nfrs']['deletion_protection'] = False` on every resource. The
contract resolver propagates `inputs.deletion_protection` to children's
NFRs (`core/contract_resolver.py:360-372`). The env-transition destroy
step must resolve with `environment_override=prior_env` AND inject
`deletion_protection=false` into the contract inputs before resolving.
This is a confirmed pattern. **Confidence: 0.90.**
#### Axis 5 — python-pptx parser complexity: REVISE → G-003 (conf 0.80)
The parser must handle 10+ distinct markdown constructs (frontmatter, `---`
separators, H1/H2, bullets, bold leads, blockquotes, fenced code, images,
markdown tables, `<div class="benefit">` HTML blocks) plus edge cases
(tables with `**bold**` R/A cells, nested bullets, `class:tall` image alt
text) plus two python-pptx constraints (no CSS font fallback — single
`font.name`; blank layout has no bullets — must inject `<a:buChar>` XML).
Finding 3 confirmed the APIs exist but the parser is non-trivial. With P3
on the serial critical path (G-002), an overflow blocks everything.
**Binding revision G-003:** split P3 into P3a (inline_images + render_slides.sh
+ pyproject — low-risk mechanical) and P3b (render_pptx.py + parser + the
attach_release_asset.py extension — high-risk).
**Verdict:** FEASIBLE.
#### Axis 6 — Benefit callout `<div>` in Marp: PASS (conf 0.95)
Empirically verified by rendering a minimal test deck through
`@marp-team/marp-cli@4.5.0` (the pinned version): `<div class="benefit">text</div>`
passes through verbatim into the rendered `<section>`. Marpit's `html: true`
option is on by default; no sanitization of styling divs. The `.benefit` class
in the inline `style:` block will apply. P2's restyle approach is valid.
**P0 risk cleared.**
## Axis 2: Scope
#### Axis 7 — "penetrate" removal completeness: REVISE → G-001 (conf 0.85)
CRITICAL gap. The plan's verify step `grep -ri penetrat docs/presentations/`
misses `docs/scope.md` (line 16: "Nova never penetrates the PDLC") and
`docs/vision.md` (line 18: "It does not penetrate upstream product or
software development lifecycles") — the repo's two core strategic docs. The
term also appears in every `.ciagent/` file (PLAN, REQUIREMENTS, RESEARCH,
CLARIFY, ROADMAP, PROJECT, PERSONAS, NORTH_STAR). The deck does not link out
to these docs, but they are audience-reachable via the repo.
**Binding revision G-001:** purge "penetrate" (and derivatives) from the
entire repo — `docs/` AND `.ciagent/`. P4's verify step becomes
`grep -ri penetrat docs/ .ciagent/` returns nothing. `test_no_penetrate_term`
asserts the same. The deleted plain `.md` (P1) already removes its
occurrence; `docs/scope.md` + `docs/vision.md` reworded in P4; all
`.ciagent/` files reworded across the relevant phases (the phase that owns
each file's edit window).
**Challenge:** Is 4 phases (P1-P3 + P4) the right size, or is this
over-scoped?
#### Axis 8 — Wave 3 parallelization feasibility: REVISE → G-002 (conf 0.85)
Resolved by G-002 (serialize P3→P4). Wave 3 is no longer parallel. P3's
`render_slides.sh` is stable before P4 runs its verify render; P3's parser
is tested against the current (untrimmed) deck before P4 trims it.
**Response:** 15 requirements across 3 execution phases is
well-scoped. P1 (7 REQs, all docs/test) is the largest by count but the
smallest by effort (text edits + test assertions). P2 (6 REQs, feat) is
the core implementation. P3 (2 REQs, test) is coverage. P4 is final
review. This is a tight, coherent milestone. **Confidence: 0.88.**
#### Axis 9 — Phase count vs. value: REVISE → G-004 (conf 0.80)
G-003 split P3 (→8 phases); G-004 merges P5+P6 to compensate. P6
(final-review-ship) for an NFR docs-only milestone is largely ceremonial —
the review/audit/ship work folds into P5's final commits. **Binding revision
G-004:** merge P5+P6. Net phase count returns to 7.
**Challenge:** Should the cross-account destroy be in scope?
#### Axis 10 — Versioning: PASS (conf 0.95)
`git tag --list 'v1.22.*'` returns nothing — no conflicts. The v1.21.x line
(v1.21.0..v1.21.6) is the precedent; v1.23 tags v1.22.0..v1.22.6 per the
revised phase plan (G-003 split shifts tag numbers; G-004 merge keeps the
final at v1.22.6).
**Response:** No. The scaffold is single-account. Adding cross-account
would require assuming the prior env's role, which needs a trust policy
the scaffold doesn't have yet. Deferring is pragmatic. The Out of Scope
section documents this. **Confidence: 0.85.**
**Verdict:** PROPERLY-SCOPED.
## Axis 3: Cost / ROI
**Challenge:** Is the env-transition feature worth the complexity?
**Response:** Yes. The user identified a real orphaned-resources risk
that violates the platform's full-lifecycle-management mission. The
fix is a ~80-line Python module + a shell block. The alternative
(blocking env edits, forcing Shape B) contradicts the user's directive.
The ROI is high: closes a real lifecycle gap with minimal code.
**Confidence: 0.90.**
**Verdict:** JUSTIFIED.
## Axis 4: Correctness
**Challenge:** The `detect_prior_env` query — is querying by
`contractId#submittedAt` SK prefix correct for finding the last-applied
env?
**Response:** The `nova-contracts` table has PK `consumerRepo` and SK
`contractId#submittedAt`. To find the last record for a given
contractId, we query by PK `consumerRepo` + SK `begins_with
"contractId#"` + FilterExpression `status = "submitted"` (or
`#LAST_APPLIED`), sort by `submittedAt` desc, take the first. This is
correct DynamoDB pattern. The `record_applied_env` step writes a new
item with SK `contractId#LAST_APPLIED#<timestamp>` so the detect step
can filter by `begins_with "contractId#LAST_APPLIED#"`. **Confidence:
0.85.**
**Challenge:** What if the DynamoDB table doesn't exist in local/CI
mode?
**Response:** The detect step catches `ClientError` / `EndpointNotFound`,
logs a warning, and returns `None` (no prior env). The pipeline proceeds
normally. This is the conservative path — no false-positive destroys.
**Confidence: 0.90.**
**Verdict:** CORRECT.
## Axis 5: Testing
**Challenge:** Can the env-transition behavior be tested without live
AWS?
**Response:** Yes. `test_env_transition.py` uses moto for DynamoDB
(mock_aws pattern from `test_contract_ingestor.py:74-110`).
`test_run_platform_env_transition.py` uses shell-text assertions
(pattern from `test_pipeline.py:79-95`). No live AWS needed.
**Confidence: 0.92.**
**Verdict:** TESTABLE.
## Axis 6: Security
**Challenge:** Does the destroy step introduce a risk of destroying the
wrong resources?
**Response:** The destroy targets the prior env's state key
(`spike/{id}/{prior_env}/terraform.tfstate`). The state key is
deterministic and env-scoped. The destroy can only affect resources in
that state file. The `deletion_protection=false` injection is scoped to
the destroy step only — the new env's apply runs with the default
`deletion_protection=true`. **Confidence: 0.88.**
**Challenge:** Could a malicious consumer trigger a destroy of another
consumer's resources?
**Response:** No. The DynamoDB query is scoped by PK `consumerRepo`
(the consumer's own repo identity). The destroy runs under the
consumer's ABAC-scoped deploy role, which can only touch resources
tagged `nova:owner=<consumer-repo>`. A consumer cannot query or destroy
another consumer's stack. **Confidence: 0.90.**
**Verdict:** SECURE.
## Axis 7: Maintainability
**Challenge:** Is the `core/env_transition.py` module a clean
abstraction or a one-off?
**Response:** It's a reusable module with two functions
(`detect_prior_env`, `record_applied_env`) that encapsulate the
DynamoDB query logic. It can be extended for cross-account destroy in a
future milestone. The shell Step 0b is a thin orchestrator. This is
maintainable. **Confidence: 0.85.**
**Verdict:** MAINTAINABLE.
## Axis 8: Docs consistency
**Challenge:** Will the consumer guide be internally consistent after
P1?
**Response:** The 5 fixes address all known inconsistencies: field
table ↔ schema, Step 2 ↔ Step 4, Step 5 ↔ environments doc, Step 8 ↔
per-env section, reference table ↔ sample contracts. The test updates
assert both shapes are documented. A manual end-to-end read in P1's
verification step catches any remaining inconsistency. **Confidence:
0.88.**
**Verdict:** CONSISTENT.
## Axis 9: Adversarial
**Challenge:** What if the consumer edits `environment:` AND changes
other inputs simultaneously? Does the destroy-then-apply still work?
**Response:** Yes. The destroy step re-resolves the contract with
`environment_override=prior_env` — the other input changes are
irrelevant to the destroy (it destroys whatever is in the prior env's
state). The new apply resolves with the new env + new inputs. The two
operations are independent. **Confidence: 0.85.**
**Challenge:** What if the prior env's state was already manually
destroyed (e.g., via decommission)?
**Response:** `terraform destroy` against an empty state is a no-op
(exits 0). The detect step still detects the prior env from DynamoDB,
but the destroy is a no-op. The apply proceeds. This is correct
behavior — no false failure. **Confidence: 0.88.**
**Verdict:** ROBUST.
---
### Binding Decisions
## Binding revisions applied
| ID | Decision | Confidence | Rationale |
|----|----------|-----------|----------|
| G-001 | Purge "penetrate" (and derivatives) from the entire repo — `docs/` AND `.ciagent/`, not just `docs/presentations/`. P4 verify: `grep -ri penetrat docs/ .ciagent/` returns nothing. `test_no_penetrate_term` asserts the same. `docs/scope.md:16` + `docs/vision.md:18` reworded in P4; all `.ciagent/` files reworded in the phase that owns each file's edit window. | 0.85 | The term appears in the repo's two core strategic docs (audience-reachable) and all internal metadata. REQ-272 says "removed from all presentation files"; the user confirmed a full-repo purge (politically loaded term). The plan's `docs/presentations/`-only grep is a gap. |
| G-002 | Serialize P3→P4. Wave 3 is no longer parallel. P3 completes (stable `render_slides.sh` + tested parser against the current deck) before P4 trims the deck + purges "penetrate" (per G-001). C8's parallelization rationale is overruled. | 0.85 | "Zero file overlap" is true for edits but false for verification: P3's parser depends on the deck structure P4 is trimming; P4's verify render depends on P3's `render_slides.sh` being stable. The interaction risk is the failure mode. |
| G-003 | Split P3 into P3a (inline_images.py + render_slides.sh inline-images step + pyproject.toml — low-risk mechanical) and P3b (render_pptx.py + the full markdown parser + render_slides.sh python-pptx step + attach_release_asset.py extension — high-risk). Both serial in Wave 3. | 0.80 | The parser handles 10+ markdown constructs + python-pptx XML constraints (font fallback, bullet injection); on the serial critical path (G-002) an overflow blocks everything. Splitting isolates the high-risk work. |
| G-004 | Merge P5+P6. P5 absorbs the final-review/audit/ship work (multi-persona review, reconstruction audit, milestone merge, tag, release, branch cleanup, REQUIREMENTS/ROADMAP update) as its final commits. Tag v1.22.6 = milestone release. | 0.80 | For an NFR docs-only milestone, a dedicated review/ship phase is ceremonial overhead. P5 already asserts the final state via tests. The merge compensates for the G-003 split, returning to 7 phases. |
1. **R1 (docs):** Add to RESEARCH.md pitfalls: the destroy step must
inject `deletion_protection=false` into the contract inputs before
re-resolving with `environment_override=prior_env`. Without this,
`prevent_destroy` lifecycle blocks (REQ-86) block the destroy. This
is already noted in RESEARCH §5 pitfall 3 and PLAN P2 implementation
note 3. No change needed — already captured.
### Escalations
2. **R2 (docs):** Clarify in PLAN P2 that the `record_applied_env` SK
format is `contractId#LAST_APPLIED#<timestamp>` so the detect step
can query `begins_with "contractId#LAST_APPLIED#"`. This is already
in RESEARCH §2 and GRILL Axis 4. No change needed — already captured.
None. All axes resolved at confidence ≥ 0.78. Two axes (4 and 6) were
escalation candidates (P0 risks if the empirical checks failed) but both
were cleared by direct verification — the rendered HTML uses plain `<img
src="assets/...">` (not SVG xlink:href), and Marp 4.5.0 passes through
`<div class="benefit">` verbatim (empirically confirmed with a minimal test
deck).
## Escalations
---
### Revised Wave/Phase Plan (post-revisions)
```
Wave 1 (P1): consolidate-docs (edits -marp.md, deletes plain .md)
Wave 2 (P2): restore-clean-style (edits -marp.md frontmatter +
↓ benefit callouts; edits render_slides.sh; retires
nova-sp-theme.css from render)
Wave 3 (P3a → P3b, serial):
P3a: inline-images (inline_images.py + render_slides.sh step + pyproject)
P3b: python-pptx-generator (render_pptx.py + parser + render_slides.sh
step + attach_release_asset.py extension)
Wave 4 (P4): trim-wordcount + purge "penetrate" (edits -marp.md +
↓ talking-points.md + docs/scope.md + docs/vision.md +
all .ciagent/ files per G-001)
Wave 5 (P5+P6 merged): ci-tests-readme + final-review-ship (tests, CI,
README, multi-persona review, audit, ship —
tag v1.22.6 = milestone release)
```
### Revised Tag Plan
- v1.22.0 (P0 — already tagged)
- v1.22.1 (P1)
- v1.22.2 (P2)
- v1.22.3 (P3a)
- v1.22.4 (P3b)
- v1.22.5 (P4)
- v1.22.6 (P5+P6 merged — final patch = milestone release)
### P4 expanded scope (per G-001)
P4 now owns, in addition to the deck word-count trim:
- `docs/scope.md:16` — reword "Nova never penetrates the PDLC"
- `docs/vision.md:18` — reword "It does not penetrate upstream product or
software development lifecycles"
- `.ciagent/PLAN.md`, `REQUIREMENTS.md`, `RESEARCH.md`, `CLARIFY.md`,
`ROADMAP.md`, `PROJECT.md`, `PERSONAS.md`, `NORTH_STAR.md` — reword every
"penetrate" occurrence (or the phase that owns each file's edit window
does so; P4 is the natural owner since it owns the "penetrate" removal
requirement REQ-272).
### P5 expanded scope (per G-004)
P5 now owns, in addition to the test/CI/README work:
- Multi-persona code review (`ciagent-review`) of the milestone branch
- Audit (`ciagent-audit`) — reconstruction test + .ciagent discipline
- Milestone ship (`ciagent-ship`) — merge to milestone branch → main, tag
v1.22.6, release with both PPTX attached, delete milestone branches
- Update REQUIREMENTS.md (mark v1.23 complete) + ROADMAP.md (mark v1.23
complete)
- Commit: `docs(milestone): complete v1.23 — Nova Deck Cleanup & Python PPTX`
---
### Evidence Index
- Image format: `docs/presentations/nova-autonomous-cloud-delivery.html`
grep — 2× `<img src="assets/png/...">`, 0× `xlink:href`
- Marp `<div>` passthrough: minimal test deck rendered through
`@marp-team/marp-cli@4.5.0``<div class="benefit">` survives verbatim
- Versioning: `git tag --list 'v1.22.*'` → empty (no conflicts)
- "penetrate" occurrences: `grep -riI penetrat` → docs/scope.md:16,
docs/vision.md:18, docs/presentations/{-marp.md, .html, .md,
-talking-points.md}, all .ciagent/*.md
- Test inversion targets: tests/test_slides_pipeline.py lines 55-64
(rewrite), 66-74 (invert), 240-270 (delete), 275-302 (retarget), 353-359
(assert #D6002A not --sp-red)
- Benefit callout count: 21 `**Benefit:**` in current deck → 21
`<div class="benefit">` in P2
None. All challenges resolved at full autonomy.
+1 -1
View File
@@ -93,7 +93,7 @@ deploy — not a vendor arriving late to that market.
3. **Not an upstream development platform.** Nova does not own the
product backlog, IDE workflows, code authorship, or application
business logic. The PDLC is upstream; Nova integrates with it through
a validated contract boundary — Nova never penetrates it.
a validated contract boundary — Nova never reaches into it.
4. **Not a replacement for the Product Development Lifecycle (PDLC).**
Nova governs infrastructure + delivery only. Product lifecycle
decisions (what to build, when to ship, for whom) remain with the
+57 -238
View File
@@ -1,264 +1,83 @@
---
project: acdl
milestone: v1.23
generated_at: 2026-08-11
milestone: v1.24
generated_at: 2026-08-12
generator: lead-developer
verification_toolchain:
typecheck: "python3 -m py_compile scripts/inline_images.py scripts/render_pptx.py tests/test_slides_pipeline.py"
test: "pytest tests/test_slides_pipeline.py # REQ-263..275"
build: "bash scripts/render_slides.sh nova-autonomous-cloud-delivery # HTML + Marp PPTX; then python3 scripts/render_pptx.py # structured PPTX"
typecheck: "python3 -m py_compile core/env_transition.py tests/test_env_transition.py tests/test_run_platform_env_transition.py"
test: "pytest tests/test_env_transition.py tests/test_run_platform_env_transition.py tests/test_consumer_guide_per_env_section.py tests/test_adapter.py tests/test_contract_resolver.py tests/test_deploy_workflow_env_input.py tests/test_pipeline.py -v"
lint: "ruff check core/env_transition.py tests/test_env_transition.py tests/test_run_platform_env_transition.py 2>/dev/null || python3 -m py_compile core/env_transition.py"
note: |
v1.23 is the Nova Deck Cleanup & Python PPTX — a docs/render/test
NFR milestone. Two active personas: lead-developer (deck markdown
consolidation + inline Marp style CSS + README + .ciagent metadata),
backend-engineer (Python scripts inline_images.py + render_pptx.py
via python-pptx + render_slides.sh updates + tests + CI YAML +
attach_release_asset.py extension). frontend-engineer stays
deactivated (decks are markdown + Marp CSS = lead-developer
territory, per v1.17/v1.18/v1.22 precedent). No data-engineer (no
schema/DB changes). No new personas (the work splits cleanly into
narrative+CSS+docs and Python+bash+tests+CI).
v1.24 is the Consumer Guide Accuracy & Env-Promotion Lifecycle
Enforcement milestone — a mixed docs+feat+test milestone. Two active
personas: lead-developer (consumer-guide.md edits + guide test
updates), backend-engineer (core/env_transition.py + run_platform.sh
Step 0b + deploy.yml + adapter doc comment + env_transition tests +
pipeline tests). frontend-engineer stays deactivated (no UI). No
data-engineer (no schema changes — the nova-contracts table already
exists). No new personas.
---
# ACDL — Persona Roster (v1.23 Nova Deck Cleanup & Python PPTX)
# ACDL — Persona Roster (v1.24 Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement)
> v1.23 roster. Two active personas + two deactivated. This is a
> docs/render/test NFR milestone: the work is deck markdown
> consolidation, Marp inline `style:` CSS (reverting from the standalone
> `nova-sp-theme.css` to `theme: default` + inline block), two Python
> render scripts (`inline_images.py` stdlib image inlining +
> `render_pptx.py` structured PPTX via python-pptx), test updates, CI
> YAML, README rewrite, and the `attach_release_asset.py` extension to
> dual-PPTX. frontend-engineer stays deactivated (Marp CSS is a static
> stylesheet, not a React/Next.js component system — D-148 precedent).
> No data-engineer (no schema/DB/ORM changes).
> v1.24 roster. Two active personas + two deactivated. This is a mixed
> docs+feat+test milestone: the work is consumer-guide accuracy fixes
> (lead-developer), a new env-transition detect-and-destroy platform
> feature (backend-engineer), and test coverage for both (split).
## Active personas
### lead-developer
- **Domain:** coordination + deck content + inline CSS
- **Active:** true
- **Phase-specific:** false
- **Frameworks:** [] (no framework — owns process + narrative + Marp inline CSS + markdown + README)
- **Constraints:** ["pragmatic", "battle-tested defaults", "no fabrication (NORTH_STAR honesty model)", "do not change the 4-beat arc", "do not re-introduce badges/version/internal citations", "port the reference deck's exact inline style CSS (the clean look)"]
- **Domain:** coordination + docs
- **Frameworks:** []
- **Constraints:** ["pragmatic", "battle-tested defaults", "docs match code"]
- **Territory:**
- `docs/presentations/nova-autonomous-cloud-delivery-marp.md` (REQ-263..275 — deck consolidation: merge plain .md into -marp.md, trim word count, remove "penetrate")
- `docs/presentations/nova-autonomous-cloud-delivery.md` (DELETE — consolidated into -marp.md)
- `docs/presentations/nova-autonomous-cloud-delivery-talking-points.md` (sync to final slide structure)
- `docs/presentations/README.md` (rewrite 4-step → 3-step process for single-document + dual-PPTX pipeline)
- `docs/presentations/assets/nova-sp-theme.css` (RETIRE from render; keep as reference file)
- `.ciagent/**` (PROJECT, ROADMAP, REQUIREMENTS, RESEARCH, PLAN, GRILL, PERSONAS, REVIEW, CHECKPOINT)
- **Reason:** Owns the deck markdown consolidation (plain `.md` +
`-marp.md` → single `-marp.md` with speaker notes as HTML comments),
the inline `style:` CSS block (porting the reference deck's exact
CSS — the "clean look"), the word-count trim, the "penetrate"
removal, the talking-points sync, the README rewrite, and all
CIAgent metadata. Is the only persona that touches `.ciagent/**` and
the deck markdown/CSS. The inline `style:` block is a Marp
frontmatter stylesheet, not a frontend component system (D-148
precedent from v1.22).
- **Phase-specific flag:** none (active for all of P0P-final).
- `docs/consumer-guide.md`
- `tests/test_consumer_guide_per_env_section.py`
- `.ciagent/*.md` (PLAN.md, RESEARCH.md, etc.)
- **Reason:** Owns the consumer guide narrative — the 5 accuracy fixes
(stale fields table, inconsistent caller, "dev only" phrasing, Step 8
rewrite with destroy semantics, reference table wording) and the
consumer-guide test updates (rename + new destroy-on-env-change test).
No UI work (frontend-engineer deactivated). No Python/bash platform
code (backend-engineer territory).
### backend-engineer
- **Domain:** Python render scripts + bash + tests + CI
- **Active:** true
- **Phase-specific:** false
- **Frameworks:** ["python-pptx", "bash", "pytest", "marp-cli", "mermaid-cli", "base64/re/mimetypes (stdlib)"]
- **Constraints:** ["pin CLI versions (no @latest in workflows-src/slides.yml — currently unpinned, must fix)", "python-pptx>=0.6.23 minimum", "stdlib-only for inline_images.py (no external image lib)", "tests must catch the theme-default + inline-style regression (the gap that let v1.22's standalone-theme drift through)", "no raw curl with shell-env tokens"]
- **Domain:** backend (Python + bash + YAML)
- **Frameworks:** ["boto3", "terraform"]
- **Constraints:** ["api-first", "fail-closed", "no orphan paths", "state-key determinism"]
- **Territory:**
- `scripts/inline_images.py` (NEW — stdlib base64 image inlining into rendered HTML)
- `scripts/render_pptx.py` (NEW — structured editable PPTX via python-pptx)
- `scripts/render_slides.sh` (update: drop --theme nova-sp-theme.css; add inline_images.py + render_pptx.py invocations)
- `scripts/attach_release_asset.py` (extend to accept multiple file paths: both .pptx + -python.pptx)
- `tests/test_slides_pipeline.py` (REQ-262 successor — invert theme tests, retarget CSS tests to inline style block, delete source-md tests, update slide count)
- `workflows-src/slides.yml` (add python-pptx install step; pin @latest@4.5.0/@11.16.0; add -python.pptx to commit list)
- `pyproject.toml` (add python-pptx>=0.6.23 to [project.optional-dependencies] test)
- **Reason:** Owns the Python render scripts (`inline_images.py` is
stdlib regex + base64; `render_pptx.py` is python-pptx — a backend
Python library, not a frontend framework). The render_slides.sh
updates (drop `--theme`, add the two Python script invocations) and
the CI YAML (add python-pptx install, pin versions, add
-python.pptx to commit list) are backend/scripting tasks. The test
suite updates (invert the theme-default test, retarget CSS tests to
the inline `style:` block, delete the source-md tests) are the gap
that let v1.22's standalone-theme drift through — backend-engineer
owns closing it. python-pptx is new to v1.23; backend-engineer's
frameworks list gains it.
- **Phase-specific flag:** none (active for P2 scripts, P5 tests; light
touch on P0/P-final).
- `core/env_transition.py` (NEW)
- `scripts/run_platform.sh` (Step 0b insert + record-applied-env)
- `.github/workflows/deploy.yml` (NOVA_CONSUMER_REPO env)
- `adapters/terraform/adapter.py` (doc comment only)
- `tests/test_env_transition.py` (NEW)
- `tests/test_run_platform_env_transition.py` (NEW)
- **Reason:** Owns the env-transition detect-and-destroy feature: the new
`core/env_transition.py` module (DynamoDB query + record), the
`run_platform.sh` Step 0b orchestrator (re-resolve + terraform destroy +
evidence event + fail-closed), the deploy.yml env var passthrough, and
the two new test files. Uses boto3 (DynamoDB) + terraform (destroy) +
bash (pipeline orchestration).
## Deactivated personas
### frontend-engineer
- **Active:** false
- **Domain:** frontend
- **Frameworks:** ["react", "next.js"] (inert — no territory)
- **Constraints:** ["component-first", "server-components", "minimal-client-js"] (inert)
- **Territory:** [] (no territory in v1.23)
- **Reason:** v1.23 has no frontend; decks are markdown + Marp inline
CSS (lead-developer territory); deactivated per PERSONAS.md
v1.17/v1.18/v1.22 precedent. The inline `style:` block is a Marp
frontmatter stylesheet (CSS rules in the markdown frontmatter), not
a React/Next.js component system — it stays lead-developer
territory. No reactivation trigger.
- **Reason:** No UI work in v1.24. The consumer guide is markdown docs
(lead-developer territory). Deactivated per v1.17/v1.18/v1.22/v1.23
precedent.
### data-engineer
- **Active:** false
- **Domain:** data
- **Frameworks:** [] (inert)
- **Constraints:** [] (inert)
- **Territory:** [] (no territory in v1.23)
- **Reason:** v1.23 has no schema/DB/ORM changes. The milestone is
docs + Python scripts + bash + tests + CI only. No reactivation
trigger.
- **Reason:** No schema changes in v1.24. The `nova-contracts` DynamoDB
table already exists with the right shape (PK `consumerRepo`, SK
`contractId#submittedAt`). The env-transition module only adds a new
`#LAST_APPLIED` SK suffix — no schema migration, no ORM, no new tables.
The backend-engineer handles the boto3 queries.
## Roster decisions
## Phase-specific notes
### D-163 (0.95): Inline Marp `style:` block is lead-developer territory, not frontend-engineer
The inline `style:` block in the Marp frontmatter is a static CSS
stylesheet (a block of CSS rules in the markdown frontmatter, applied
by Marp to the rendered slides), not a React/Next.js component system.
The v1.17/v1.18/v1.22 precedent (decks are markdown = lead-developer
territory; the theme CSS is a Marp stylesheet, not a frontend framework
— D-148) extends to the inline `style:` block. frontend-engineer's
frameworks (react, next.js) are irrelevant to Marp frontmatter CSS.
**Decision:** inline `style:` block stays lead-developer territory.
Confidence 0.95 — the only counter-argument is that CSS is "frontend,"
but Marp frontmatter CSS is a static stylesheet authored in the
markdown source, not a component system.
### D-164 (0.90): No new personas for v1.23
The work is markdown + inline CSS + Python scripts + bash + tests + CI
YAML + README. All of this is within the two active personas' range
(lead-developer: markdown + CSS + README + metadata; backend-engineer:
Python + bash + tests + CI). Creating a separate "slides-engineer" or
"pptx-engineer" persona would fragment ownership of the deck markdown
(lead) and the render scripts (backend). The python-pptx work is
unambiguously backend (a Python library + script), even though the
slide layout decisions within `render_pptx.py` are co-owned with lead-
developer. **Decision:** no new personas. Confidence 0.90 — follows
v1.22 D-149 precedent.
### Territory-overlap resolution (co-ownership)
| Path | Primary | Co-owner | Why |
|------|---------|----------|-----|
| `scripts/render_pptx.py` | backend-engineer (Python code + python-pptx API) | lead-developer (slide layout decisions: which image where, font sizes, bullet structure) | The script is backend (Python); the slide-by-slide layout decisions within it are design decisions that reflect lead-developer's deck ownership. `warn` enforcement — cross-territory edits logged, not blocked. |
| `scripts/render_slides.sh` | backend-engineer (bash script) | lead-developer (the inline `style:` block content it renders) | The script is backend; the CSS it renders is lead-developer's. |
| `tests/test_slides_pipeline.py` | backend-engineer (test code) | lead-developer (assertions reflect deck structure + inline CSS) | The test code is backend; the assertions (slide count, inline style rules, theme-default) reflect lead-developer's deck/CSS decisions. |
| `docs/presentations/README.md` | lead-developer (process narrative) | backend-engineer (build commands + render script invocations) | The process narrative is lead; the build commands + script names are backend. |
## Domain priority (v1.23)
`lead-developer → backend-engineer`
Rationale: the deck markdown consolidation + inline CSS (lead) is the
binding constraint — the render scripts (backend) can't be finalized
until the deck structure is stable (slide count, inline style). The
inline `style:` block must be ported + verified before the render
pipeline (HTML + PPTX) is re-run. Backend (scripts + tests + CI)
follows once the deck + CSS are settled. The dual-PPTX pipeline is
wired last, once the deck renders clean with the inline style.
---
## Historical rosters
<details>
<summary>v1.22 roster (Nova Deck Layout Fix) — superseded by v1.23</summary>
### Active personas (v1.22)
### lead-developer
- **Domain:** coordination + deck content
- **Active:** true
- **Phase-specific:** false
- **Frameworks:** [] (no framework — owns process + narrative + CSS + markdown)
- **Constraints:** ["pragmatic", "battle-tested defaults", "no fabrication (NORTH_STAR honesty model)", "do not change the 4-beat arc", "do not re-introduce badges/version/internal citations"]
- **Territory:**
- `docs/presentations/assets/nova-sp-theme.css` (REQ-254,255,256 — theme CSS)
- `docs/presentations/nova-autonomous-cloud-delivery-marp.md` (REQ-261 — deck content)
- `docs/presentations/nova-autonomous-cloud-delivery.md` (REQ-261 — source of truth)
- `docs/presentations/nova-autonomous-cloud-delivery-talking-points.md` (REQ-261)
- `docs/presentations/README.md` (REQ-261 — slide-count convention)
- `docs/presentations/assets/mmd/*.mmd` (REQ-259,260 — mermaid re-layout)
- `.ciagent/**` (PROJECT, ROADMAP, REQUIREMENTS, RESEARCH, PLAN, GRILL, PERSONAS, REVIEW, CHECKPOINT)
- **Reason:** Owns the theme CSS (the root cause), the deck markdown
(trim/split overflowing slides), the mermaid re-layout, the talking
points, the README, and all CIAgent metadata. Is the only persona
that touches `.ciagent/**` and the deck markdown/CSS.
- **Phase-specific flag:** none (active for all of P0P6).
### backend-engineer
- **Domain:** render scripts + tests
- **Active:** true
- **Phase-specific:** false
- **Frameworks:** ["bash", "pytest", "marp-cli", "mermaid-cli"]
- **Constraints:** ["pin CLI versions (no @latest)", "2x scale + transparent bg for mermaid", "tests must catch layout regressions", "no raw curl with shell-env tokens"]
- **Territory:**
- `scripts/render_slides.sh` (REQ-257,258 — pin versions, 2x scale)
- `scripts/render_deck.sh` (REQ-257 — DELETE)
- `tests/test_slides_pipeline.py` (REQ-262 — layout/aspect-ratio/theme-structural tests)
- `.github/workflows/slides.yml` (if references to render_deck.sh need removal)
- **Reason:** Owns the render pipeline (bash scripts) and the test
suite. The layout/aspect-ratio/theme-structural tests (REQ-262) are
the gap that let this regression through — backend-engineer owns
closing that gap. Pinning CLI versions and adding 2x scale are
backend/scripting tasks.
- **Phase-specific flag:** none (active for P2, P5; light touch on P0/P6).
### D-148 (v1.22): Theme CSS is lead-developer territory, not frontend-engineer
### D-149 (v1.22): No new personas for v1.22
</details>
<details>
<summary>v1.18 roster (Citizen Developer & Production-Grade Guidance) — superseded</summary>
### Active personas (v1.18)
### lead-developer
- **Domain:** coordination
- **Active:** true
- **Phase-specific:** false
- **Frameworks:** [] (no framework — owns process + narrative, not code)
- **Constraints:** ["pragmatic", "battle-tested defaults", "no fabrication (NORTH_STAR honesty model)"]
- **Territory:**
- `docs/presentations/**` (Step 1/2/4 markdown + the deck automation trigger)
- `.ciagent/**` (PROJECT, ROADMAP, REQUIREMENTS, RESEARCH, PLAN, GRILL, PERSONAS, REVIEW, CHECKPOINT)
- `PROJECT.md` (RACI matrix + PDLC-scope statement, REQ-215/216)
- `ROADMAP.md`
- `REQUIREMENTS.md`
- `docs/raci.md` (REQ-215)
- `docs/scope.md` (REQ-216)
- `docs/skills.md` (REQ-222 — the index page, not the skill files themselves)
- `docs/submission-readiness.md` (REQ-219 — citizen-developer-facing copy; co-owned with backend-engineer for the reason-code catalog)
### backend-engineer
- **Domain:** backend
- **Active:** true
- **Phase-specific:** false
- **Frameworks:** ["mcp (Python SDK v2)", "pydantic", "jsonschema", "urllib"]
- **Territory:**
- `mcp/atelier/server.py` (REQ-223)
- `mcp/atelier/plugins/**/*.py` (REQ-223 — principles.py, validation.py)
- `mcp/atelier/vendor/**` (REQ-224 — vendored Atelier snapshot)
- `mcp/atelier/VERSION.md` + `mcp/atelier/README.md` (REQ-224)
- `scripts/update_atelier_vendor.sh` (REQ-224)
- `core/submission_readiness.py` (REQ-218)
- `scripts/render_deck.sh` (REQ-228)
- `scripts/attach_release_asset.py` (REQ-228)
- `tests/test_atelier_mcp.py` (REQ-225)
- `tests/test_submission_readiness.py` (REQ-220)
### data-engineer
- **Domain:** data
- **Active:** true
- **Frameworks:** ["jsonschema", "dynamodb (item shape)"]
- **Territory:**
- `schemas/**` (REQ-217)
- `core/lambda/contract_ingestor.py` (co-owned with backend-engineer)
### D-143 (v1.18): Fold mcp-engineer into backend-engineer
</details>
- No phase-specific personas. Both active personas span P1-P3.
- P4 (final review + audit + ship) is lead-developer territory
(orchestration + docs completion).
+169 -467
View File
@@ -1,478 +1,180 @@
# PLAN — v1.23 (Nova Deck Cleanup & Python PPTX)
# PLAN — v1.24 (Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement)
> NFR milestone (docs/render/test only). Tags on v1.22.x line:
> v1.22.0 (P0) → v1.22.1..v1.22.4 (P1P4) → v1.22.5 (P5 final = milestone
> release). 13 requirements (REQ-263..275), 6 phases.
>
> **GRILL revisions applied** (G-001..G-004, conf 0.78-0.85):
> - G-001: "penetrate" purge covers the whole repo (`docs/` +
> `.ciagent/`), not just `docs/presentations/`.
> - G-002: P3→P4 serialized (not parallel) — P4's parser depends on
> P3's stable `render_slides.sh`; P4's trimmed deck is what P3b's
> parser consumes.
> - G-003: P3 split into P3a (inline_images + render_slides.sh +
> pyproject — low-risk) + P3b (render_pptx.py + parser +
> attach_release_asset.py — high-risk, isolated).
> - G-004: P5+P6 merged — NFR docs milestone; dedicated review/ship
> phase is ceremonial overhead. P5 absorbs review/audit/ship. Tag
> v1.22.5 = milestone release.
> Feature milestone (one `feat` phase: env-transition destroy enforcement;
> the rest are `fix`/`docs`/`test`). Tags on the **v1.23.x** line:
> v1.23.0 (P0) → v1.23.1 (P1) → v1.23.2 (P2) → v1.23.3 (P3) → v1.23.4 (P4
> final = milestone release). 15 requirements (REQ-276..290), 4 phases +
> P0 pre-execution.
## Wave Ordering
## Phase breakdown
### Phase P1 — consumer-guide-fixes (Wave 1, lead-developer)
**Type:** `docs` + `fix` + `test` (consumer guide accuracy + guide test updates)
**Requirements:** REQ-276, REQ-277, REQ-278, REQ-279, REQ-280, REQ-281, REQ-290
**Must-haves:**
- `docs/consumer-guide.md` Step 3 contract fields table corrected (REQ-276)
- `docs/consumer-guide.md` Step 4 caller consistent with Step 2 (REQ-277)
- `docs/consumer-guide.md` Step 5 stage 8 "(dev only)" → "(autonomous in dev; higher environments apply after HITL attestation)" (REQ-278)
- `docs/consumer-guide.md` Step 8 rewritten with destroy-then-rebuild semantics + cross-ref to Shape B (REQ-279)
- `docs/consumer-guide.md` Per-env section gains Shape B lead sentence (REQ-280)
- `docs/consumer-guide.md` Reference table `@v1.19` wording corrected (REQ-281)
- `tests/test_consumer_guide_per_env_section.py` updated: rename `test_consumer_guide_states_no_field_editing``test_consumer_guide_documents_both_promotion_shapes`; add `test_consumer_guide_documents_destroy_on_env_change` (REQ-290)
**Vertical slice:** A reader of `docs/consumer-guide.md` can promote via
either shape (A: edit environment + platform destroys prior; B: per-env
caller workflow) without contradiction. The guide's field table, caller
examples, and stage descriptions match the actual schema and platform
behavior. All consumer-guide tests pass.
**Files touched:**
- `docs/consumer-guide.md`
- `tests/test_consumer_guide_per_env_section.py`
**Verification:** `pytest tests/test_consumer_guide_per_env_section.py -v`
(all 7 tests pass). Manual read of `docs/consumer-guide.md` end-to-end
for internal consistency.
---
### Phase P2 — env-transition-detect-and-destroy (Wave 2, backend-engineer)
**Type:** `feat` (new platform feature: env-transition detect-and-destroy)
**Requirements:** REQ-282, REQ-283, REQ-284, REQ-285, REQ-286, REQ-287
**Must-haves:**
- `core/env_transition.py` new module: `detect_prior_env()` + `record_applied_env()` with boto3 DynamoDB queries (REQ-282, REQ-283)
- `scripts/run_platform.sh` Step 0b: environment-transition check — detect prior env, re-resolve with `environment_override=prior_env` + `deletion_protection=false`, `terraform init -reconfigure` + `terraform destroy -auto-approve` against prior state key, emit `nova.env.destroyed` evidence event, fail closed on destroy failure (REQ-284)
- `scripts/run_platform.sh` records applied env after successful apply (REQ-285)
- `.github/workflows/deploy.yml` passes `NOVA_CONSUMER_REPO=${{ github.repository }}` to `run_platform.sh` (REQ-286)
- `adapters/terraform/adapter.py` state-key block gains doc comment (REQ-287)
**Vertical slice:** When a consumer changes `environment:` on a stable
`contract.id`, the pipeline detects the prior env from DynamoDB, destroys
the prior env's Terraform state (with `deletion_protection=false`), emits
an evidence event, and only then applies the new env. If the destroy
fails, the pipeline exits non-zero (no orphan path). If no prior env
exists (first deploy or Shape B), the pipeline proceeds normally.
**Files touched:**
- `core/env_transition.py` (NEW)
- `scripts/run_platform.sh`
- `.github/workflows/deploy.yml`
- `adapters/terraform/adapter.py` (doc comment only)
**Verification:** `python3 -m py_compile core/env_transition.py`.
`pytest tests/test_pipeline.py tests/test_deploy_workflow_env_input.py -v`
(existing tests still pass). The new tests in P3 validate the behavior.
**Key implementation notes (from RESEARCH §5 pitfalls):**
1. The destroy step must re-resolve with `environment_override=prior_env`
so the emitted TF matches the prior env's resources.
2. `terraform init -reconfigure` is required when switching state backends.
3. `deletion_protection: false` must be injected (same as decommission
Step 2 in `scripts/run_decommission.sh:34-37`) or `prevent_destroy`
blocks the destroy.
4. DynamoDB unreachable in local/CI → log warning + return `None`
(conservative, no prior env assumed).
5. The state key `spike/{id}/{env}/terraform.tfstate` stays as-is — the
env segment is what lets the destroy target the prior env.
---
### Phase P3 — env-transition-tests (Wave 3, backend-engineer + lead-developer)
**Type:** `test` (new test coverage for env-transition + pipeline integration)
**Requirements:** REQ-288, REQ-289
**Must-haves:**
- `tests/test_env_transition.py` (NEW): `detect_prior_env` returns `None` when no record; returns prior env when record differs; returns `None` when record matches; `record_applied_env` writes record. Uses moto for DynamoDB (REQ-288)
- `tests/test_run_platform_env_transition.py` (NEW): asserts `run_platform.sh` has Step 0b; calls `env_transition.py detect`; calls `terraform destroy` on prior env; fails closed on destroy failure; records applied env after success (REQ-289)
**Vertical slice:** The env-transition detect-and-destroy behavior is
fully covered by automated tests. The DynamoDB query logic is unit-tested
with moto. The pipeline orchestration is tested via shell-text assertions
(pattern from `tests/test_pipeline.py:79-95`).
**Files touched:**
- `tests/test_env_transition.py` (NEW)
- `tests/test_run_platform_env_transition.py` (NEW)
**Verification:** `pytest tests/test_env_transition.py tests/test_run_platform_env_transition.py -v` (all new tests pass). Full suite: `pytest tests/ -k "env_transition or consumer_guide or pipeline or adapter or deploy_workflow" -v`.
---
### Phase P4 — final-review-ship (Wave 4, lead-developer)
**Type:** `docs` (review + audit + milestone ship)
**Requirements:** (none new — milestone completion)
**Must-haves:**
- Multi-persona code review across P1-P3 changes (ci-code-reviewer)
- Project health audit (ci-doc-verifier + ci-audit)
- Milestone ship: tag v1.23.4 (final phase patch = milestone release), merge to main, Gitea release
- Update REQUIREMENTS.md traceability (all REQ-276..290 → complete)
- Update ROADMAP.md (v1.24 → complete)
---
## Wave ordering
```
Wave 1 (P1): consolidate-docs (edits -marp.md, deletes plain .md)
Wave 2 (P2): restore-clean-style (edits -marp.md frontmatter +
benefit callouts; edits render_slides.sh; retires
↓ nova-sp-theme.css from render)
Wave 3 (P3a): inline-images (inline_images.py + render_slides.sh
↓ step + pyproject — low-risk, mechanical)
Wave 4 (P3b): python-pptx-generator (render_pptx.py + parser +
↓ attach_release_asset.py — high-risk, isolated)
Wave 5 (P4): trim-wordcount + remove "penetrate" (edits -marp.md +
↓ talking-points.md + docs/scope.md + docs/vision.md +
↓ .ciagent/*.md "penetrate" purge)
Wave 6 (P5): ci-tests-readme + review + audit + milestone ship
(merged P5+P6 per G-004)
Wave 1 (P1): consumer-guide-fixes [lead-developer]
Wave 2 (P2): env-transition-detect-and-destroy [backend-engineer]
Wave 3 (P3): env-transition-tests [backend-engineer + lead-developer]
Wave 4 (P4): final-review-ship [lead-developer]
```
**Rationale (C8 revised by G-002/G-003/G-004):** P1→P2 serial (both
edit `-marp.md`). P3a→P3b serial (P3b's parser benefits from P3a's
stable `render_slides.sh`). P4 serial after P3b (P4's trimmed deck is
what P3b's parser must handle; P4's verify render depends on P3a's
stable script). P5 final (merged review/audit/ship per G-004 — NFR docs
milestone doesn't warrant a dedicated P6).
**Dependencies:**
- P2 depends on P1: the Step 8 rewrite in P1 documents the destroy
semantics that P2 implements. Doing P1 first ensures the docs and code
land in the right order (docs describe the intended behavior, then code
implements it).
- P3 depends on P2: the tests validate the env-transition module and
pipeline Step 0b that P2 creates.
- P4 depends on P1+P2+P3: the final review covers all changes.
---
**Parallelization:** P1 and P2 could run in parallel (different
territories: docs vs code), but the wave ordering is sequential for
safety — if P1's Step 8 rewrite reveals a design issue, P2's
implementation should follow the corrected design. With
`parallelization.enabled=true` and `min_plans_for_parallel=2`, the
orchestrator *could* run them concurrently; however, the dependency
(P2 follows P1's design) makes sequential the safer choice. P3 must
follow P2 (tests validate the code). P4 must follow all.
## Phase P1 — consolidate-docs (tag v1.22.1)
## Requirement → Phase mapping
**Requirements:** REQ-263, REQ-264
**Persona:** lead-developer (deck markdown + .ciagent)
**Branch:** `phase/01-consolidate-docs`
| REQ | Phase | Type | Description |
|-----|-------|------|-------------|
| REQ-276 | P1 | docs | Contract fields table corrected |
| REQ-277 | P1 | docs | Step 4 caller consistent with Step 2 |
| REQ-278 | P1 | docs | Step 5 stage 8 "dev only" corrected |
| REQ-279 | P1 | docs | Step 8 rewritten with destroy semantics |
| REQ-280 | P1 | docs | Per-env section Shape B lead sentence |
| REQ-281 | P1 | docs | Reference table @v1.19 wording corrected |
| REQ-282 | P2 | feat | env_transition.py detect_prior_env() |
| REQ-283 | P2 | feat | env_transition.py record_applied_env() |
| REQ-284 | P2 | feat | run_platform.sh Step 0b detect-and-destroy |
| REQ-285 | P2 | feat | run_platform.sh records applied env |
| REQ-286 | P2 | feat | deploy.yml passes NOVA_CONSUMER_REPO |
| REQ-287 | P2 | docs | adapter.py state-key doc comment |
| REQ-288 | P3 | test | test_env_transition.py |
| REQ-289 | P3 | test | test_run_platform_env_transition.py |
| REQ-290 | P1 | test | consumer guide test updates |
### Tasks
## Tag plan
1. **Fold speaker notes into the deck** (REQ-263):
- Read `docs/presentations/nova-autonomous-cloud-delivery.md` (the
plain source-of-truth, 747 lines).
- For each `## Slide N — Title` section, extract the
`> **Speaker notes:**` and `> **Transition:**` blockquote blocks.
- In `docs/presentations/nova-autonomous-cloud-delivery-marp.md`,
insert `<!-- Speaker notes: ... -->` (and `<!-- Transition: ... -->`
where present) as a Marp HTML comment immediately after the
slide's content, before the next `---` separator.
- The slide's visible content is unchanged.
2. **Fold talking points into the deck** (REQ-264):
- Read `docs/presentations/nova-autonomous-cloud-delivery-talking-points.md`
(145 lines, 3-6 bullets per slide + key takeaway).
- For each slide, insert `<!-- Talking points: ... -->` (the bullets
joined) as a Marp HTML comment after the speaker-notes comment.
- Update the `talking-points.md` header (line 4): change "distilled
from the source of truth (`nova-autonomous-cloud-delivery.md`)" →
"mirrors the `<!-- Talking points: -->` comments in
`nova-autonomous-cloud-delivery-marp.md` (the sole source of
truth)".
3. **Delete the plain `.md`** (REQ-263):
- `git rm docs/presentations/nova-autonomous-cloud-delivery.md`.
4. **Verify** (must-have):
- `bash scripts/render_slides.sh` still renders (22 slides:
title + 20 main + 1 appendix — unchanged).
- HTML slide count unchanged (test_marp_deck_slide_count passes).
- `grep -c "<!-- Speaker notes:" nova-autonomous-cloud-delivery-marp.md`
returns ≥20 (one per main slide).
- `grep -c "<!-- Talking points:" ...` returns ≥20.
- The plain `.md` no longer exists.
- `talking-points.md` still exists with updated header.
---
## Phase P2 — restore-clean-style (tag v1.22.2)
**Requirements:** REQ-265, REQ-266, REQ-267
**Persona:** lead-developer (deck markdown + CSS) + backend-engineer
(render_slides.sh)
**Branch:** `phase/02-restore-clean-style`
**Depends on:** P1 (consolidated doc)
### Tasks
1. **Revert frontmatter to `theme: default` + inline `style:`** (REQ-265):
- In `nova-autonomous-cloud-delivery-marp.md` frontmatter:
- `theme: nova-sp``theme: default`.
- Add `style: |` block porting the S&P visual language (from the
research-extracted reference CSS + `nova-sp-theme.css`):
```yaml
style: |
section { font-family: "Akkurat Pro", "Helvetica Neue", "Arial", sans-serif; font-size: 22px; color: #1B1B1B; padding: 48px 56px 40px; overflow: auto; }
h1 { color: #D6002A; font-size: 34px; margin-bottom: 0.3em; }
h2 { color: #D6002A; font-size: 26px; margin-bottom: 0.2em; }
h3 { color: #D6002A; font-size: 22px; margin-bottom: 0.2em; }
section.title { background: #1B1B1B; color: #fff; border-top: 8px solid #D6002A; }
section.title h1, section.title h2 { color: #fff; }
section.title header, section.title footer { display: none; }
table { font-size: 18px; width: 100%; border-collapse: collapse; }
th { background: #F0F0F0; border-bottom: 2px solid #D6002A; padding: 4px 8px; text-align: left; }
td { border-bottom: 1px solid #F0F0F0; padding: 4px 8px; }
blockquote { border-left: 4px solid #D6002A; color: #2E2E2E; font-size: 20px; padding-left: 12px; }
pre { background: #1B1B1B; color: #fff; border-radius: 4px; padding: 12px; font-size: 16px; }
code { background: #F0F0F0; color: #1B1B1B; border-radius: 2px; padding: 1px 4px; font-size: 18px; }
pre code { background: transparent; color: inherit; }
img { display: block; margin: 0 auto; max-width: 100%; max-height: 380px; object-fit: contain; }
strong { color: #D6002A; }
.benefit { margin-top: 0.6em; padding-top: 0.4em; border-top: 1px solid #D6002A; color: #1B1B1B; font-size: 20px; font-style: italic; }
section.title .benefit { color: #fff; }
@media print { section { overflow: hidden; } }
```
- Keep: `paginate: true`, `size: 16x9`, `footer:`, no `header:`.
2. **Retire `nova-sp-theme.css` from render** (REQ-266):
- Add header comment to `docs/presentations/assets/nova-sp-theme.css`:
"Retained as the S&P visual-language reference for future styling
work. The live deck uses Marp `default` theme + inline `style:`
block in the -marp.md frontmatter. Not loaded at render time."
- `scripts/render_slides.sh`: remove `THEME_CSS` variable usage and
the `--theme "$THEME_CSS"` argument from both marp-cli invocations
(lines 70, 73). Keep `--allow-local-files`.
3. **Restyle benefit callouts** (REQ-267):
- In `nova-autonomous-cloud-delivery-marp.md`, convert every
`**Benefit:** text` line → `<div class="benefit">text</div>`.
- This applies to all 20 main slides + appendix (21 callouts).
- The `**Benefit:**` prefix is removed; the `.benefit` class (defined
in the inline `style:` block) provides the visual emphasis.
4. **Verify** (must-have):
- `bash scripts/render_slides.sh` renders; HTML opens with S&P red
h1s, black title slide with red top border, benefit callouts with
red top-rule + italic.
- `grep -c "theme: default" nova-autonomous-cloud-delivery-marp.md`
returns 1.
- `grep -c "theme: nova-sp" ...` returns 0.
- `grep -c 'class="benefit"' ...` returns ≥21.
- `grep -c "\\*\\*Benefit:\\*\\*" ...` returns 0.
- `nova-sp-theme.css` still exists (reference).
- `grep "Not loaded at render" nova-sp-theme.css` returns a match.
---
## Phase P3a — inline-images (tag v1.22.3)
**Requirements:** REQ-268
**Persona:** backend-engineer (scripts + pyproject)
**Branch:** `phase/03a-inline-images`
**Depends on:** P2 (render_slides.sh `--theme` drop)
### Tasks
1. **`scripts/inline_images.py`** (REQ-268):
- Stdlib only (`base64`, `re`, `mimetypes`, `sys`, `pathlib`).
- Reads the rendered HTML path (argv[1]).
- Finds all `<img ... src="(assets/...)" ...>` (relative paths only;
skip absolute/`http(s)://`).
- Reads each referenced file, replaces `src` with
`data:image/<mime>;base64,<base64>`.
- MIME by extension: `.png`→`image/png`, `.svg`→`image/svg+xml`,
`.jpg/.jpeg`→`image/jpeg`, `.gif`→`image/gif`; fallback
`application/octet-stream`.
- Writes HTML back in place; prints count inlined to stderr.
- Usage: `python scripts/inline_images.py <html-path>`.
2. **`scripts/render_slides.sh`** (REQ-268):
- After Step 2 (MARP render), add Step 3: inline images into HTML
(`python scripts/inline_images.py "$HTML"`).
- Update Step 5 (stage): no change yet (python-pptx is P3b).
- Order now: Step 1 mermaid → Step 2 MARP HTML+PPTX → Step 3 inline
images → Step 4 stage.
3. **Verify** (must-have):
- `bash scripts/render_slides.sh` produces `.html` with inlined
base64 images.
- Open the HTML from a different directory (no `assets/` folder) —
images display.
- `grep -c 'src="assets/' nova-autonomous-cloud-delivery.html`
returns 0 (all inlined).
- `grep -c 'data:image' nova-autonomous-cloud-delivery.html`
returns ≥2 (the deck's 2 images).
---
## Phase P3b — python-pptx-generator (tag v1.22.4)
**Requirements:** REQ-269, REQ-270
**Persona:** backend-engineer (scripts + pyproject) + lead-developer
(deck structure consultation)
**Branch:** `phase/03b-python-pptx`
**Depends on:** P3a (stable render_slides.sh with inline step)
### Tasks
1. **`scripts/render_pptx.py`** (REQ-269):
- Dependency: `python-pptx>=0.6.23` (import at module load; if
absent, print "pip install -e .[slides]" and exit 1).
- Parses `nova-autonomous-cloud-delivery-marp.md`:
- Frontmatter (skip YAML between `---` markers).
- `---` slide separators.
- `#`/`##` headings → slide title.
- `<!-- ... -->` HTML comments → skipped (speaker notes/talking
points).
- `- `/`* ` bullets → bullet list.
- `**bold lead**` (first paragraph after title) → bold lead.
- `> blockquote` → blockquote-styled paragraph.
- ` ```lang ... ``` ` → code block (monospace text frame).
- `![alt](path)``add_picture` (embedded in ppt/media/).
- `| ... |` tables → native PPTX table (`add_table`).
- `<div class="benefit">text</div>` → benefit callout
(italic text frame with red top border accent).
- Produces 16:9 PPTX (`prs.slide_width = Inches(13.333)`,
`prs.slide_height = Inches(7.5)`):
- Title slide: black background, red top bar
(`MSO_SHAPE.RECTANGLE`, fill `#D6002A`, height ~0.4"), white H1
(Arial — Akkurat Pro not installed; PowerPoint does OS
substitution).
- Content slides: blank layout; red H2 title text box at top;
bold lead paragraph (red `**strong**`); bullets; blockquote
(indent + red left accent via thin rectangle); embedded PNGs
(centered, scaled to fit content area); native tables (grey
header, red bottom border); benefit callout (italic, red
top-rule accent).
- Font: "Akkurat Pro" (PowerPoint falls back to OS default if
absent — acceptable; the HTML is the pixel-perfect artifact).
- Output: `docs/presentations/nova-autonomous-cloud-delivery-python.pptx`.
- Usage: `python scripts/render_pptx.py [deck-name]` (defaults to
`nova-autonomous-cloud-delivery`).
2. **`pyproject.toml`** (REQ-269):
- Add to `[project.optional-dependencies]`:
```toml
slides = ["python-pptx>=0.6.23"]
```
3. **`scripts/render_slides.sh`** (REQ-270):
- Add Step 4: python-pptx render → `*-python.pptx`.
- Update Step 5 (stage): add `*-python.pptx`.
- Final order: Step 1 mermaid → Step 2 MARP HTML+PPTX → Step 3
inline images → Step 4 python-pptx → Step 5 stage.
4. **`scripts/attach_release_asset.py`** (REQ-270):
- Extend to accept multiple asset paths (both `.pptx` +
`-python.pptx`). Backward-compatible (single arg still works; or
accept a list). MARP PPTX is the primary attachment (first).
5. **Verify** (must-have):
- `bash scripts/render_slides.sh` produces 3 artifacts: `.html`
(with inlined base64 images), `.pptx` (MARP), `-python.pptx`
(structured).
- Open `-python.pptx` — 16:9, title slide black with red bar, content
slides with red titles + bullets + tables + images.
- `-python.pptx` is a valid zip (PPTX signature).
---
## Phase P4 — trim-wordcount + repo-wide "penetrate" purge (tag v1.22.5)
**Requirements:** REQ-271, REQ-272
**Persona:** lead-developer (deck markdown + talking-points + docs +
.ciagent)
**Branch:** `phase/04-trim-wordcount`
**Depends on:** P3b (stable render_slides.sh; trimmed deck is what P3b's
parser already handles — P4 does not break P3b's parser because the
parser handles the pre-trim structure and P4 trims prose, not
structure)
### Tasks
1. **Targeted word-count trim** (REQ-271) on ~8 verbose slides:
- **Slide 1 (The Problem)**: cut the "Every hour a developer
spends..." restatement paragraph (lines 29-ish); tighten bullet
sub-clauses. Target ~25% reduction.
- **Slide 5 (Scope)**: collapse bullets 1 + 3 (both say "PDLC is
upstream") to 3 bullets.
- **Slide 7 (Pipeline)**: condense bullet 1's pipe-chain; deduplicate
bullet 3's "Wiz on the plan" repetition.
- **Slide 8 (Decision Ledger)**: tighten the "AI decisions" bullet
from a paragraph to one sentence.
- **Slide 13 (Cost & ROI)**: collapse the "Honest caveat" bullet to
one line.
- **Slide 14 (Deferred)**: cut the preamble paragraph to one line.
- **Slide 20 (Recap + Ask)**: split the 60-word ask run-on into two
short sentences.
- **Appendix A1**: tighten the benefit callout to one line.
- Tables (slides 6, 9, 10, 15, 16, 17, A1) untouched.
- Short slides (2, 3, 4, 11, 12, 18, 19) untouched.
- Benefit callouts (all slides): already restyled in P2; here just
tighten each to one short sentence (preserve the spirit).
2. **Remove "penetrate" — repo-wide purge** (REQ-272, G-001):
- `docs/presentations/nova-autonomous-cloud-delivery-marp.md` slide 5:
"Nova never penetrates it. Integration is through one validated
contract." → "Integration is through one validated contract."
- `docs/presentations/nova-autonomous-cloud-delivery-talking-points.md:40`:
"Nova never penetrates it" → match slide 5's trimmed wording.
- `docs/scope.md`: reword "Nova never penetrates the PDLC" →
"Nova never reaches into the PDLC" (or restructure — the scope
boundary is already established by "downstream of PDLC").
- `docs/vision.md`: reword "It does not penetrate upstream product or
software development lifecycles" → "It does not reach into upstream
product or software development lifecycles".
- `.ciagent/PROJECT.md`: reword the 3 "penetrate" occurrences in the
Core Tenets #2 + Scope sections.
- Note: `.ciagent/RESEARCH.md` + `.ciagent/PLAN.md` meta-references
to "penetrate removal" are NOT purged (they describe the removal;
purging them would erase the decision history).
3. **Verify** (must-have):
- `grep -ri penetrat docs/ .ciagent/PROJECT.md .ciagent/CLARIFY.md`
returns nothing (the term is gone from all audience-facing +
project-spec docs; RESEARCH.md/PLAN.md/GRILL.md meta-references
are exempt as decision-history).
- Slide count unchanged (22).
- `bash scripts/render_slides.sh` renders; no broken slides.
- Word count of trimmed slides reduced ~20-30% (manual spot-check).
---
## Phase P5 — ci-tests-readme + review + audit + ship (Final Phase, tag v1.22.6)
**Requirements:** REQ-273, REQ-274, REQ-275 (+ review/audit/ship for all REQ-263..275)
**Persona:** backend-engineer (CI + tests) + lead-developer (README + review coordination)
**Branch:** `phase/05-ci-tests-readme-ship`
**Depends on:** P1, P2, P3a, P3b, P4 (asserts the final state; merged
P5+P6 per G-004 — NFR docs milestone doesn't warrant a dedicated
review/ship phase)
### Tasks
1. **CI workflows** (REQ-273):
- `workflows-src/slides.yml`: add `setup-python` step + `pip install
-e .[slides]` (or `pip install python-pptx>=0.6.23`). Pin
`@marp-team/marp-cli@4.5.0` + `@mermaid-js/mermaid-cli@11.16.0`
(match render_slides.sh). Update `git add` commit list: add
`*-python.pptx`.
- Sync to `.github/workflows/slides.yml` + `.gitea/workflows/slides.yml`
via `scripts/sync_workflows.py`.
2. **Tests** (REQ-274):
- `tests/test_slides_pipeline.py` updates (per research finding 5):
- Delete/invert `test_marp_deck_not_using_default_theme` (we now
USE `theme: default`).
- `test_marp_deck_uses_sp_theme` → rewrite to assert the inline
`style:` block has S&P properties (`#D6002A`, `#1B1B1B`,
`section.title`).
- `test_theme_css_*` trio → retarget from standalone CSS file to
inline `style:` block (or delete if the inline-style assertion
covers it).
- `test_html_embeds_theme` → assert literal `#D6002A` (not
`--sp-red`).
- `test_source_md_*` → delete (plain `.md` is gone).
- `test_render_slides_pins_cli_versions` → keep, extend to assert
`python-pptx` is in `pyproject.toml`.
- `test_no_penetrate_term` (new, G-001): assert
`grep -ri penetrat docs/ .ciagent/PROJECT.md .ciagent/CLARIFY.md`
returns nothing (RESEARCH.md/PLAN.md/GRILL.md meta-references
exempt as decision-history).
- `test_html_images_inlined_as_base64` (new): assert rendered
HTML has zero `src="assets/` references and ≥1
`data:image/png;base64` per image the deck uses.
- `test_python_pptx_exists` (new): assert
`nova-autonomous-cloud-delivery-python.pptx` exists + is a valid
zip (PPTX signature).
- `test_nova_sp_theme_css_retained_as_reference` (new): assert
the file exists + has the "Not loaded at render" comment.
- `test_benefit_callouts_use_class` (new): assert `-marp.md` has
`class="benefit"` and no `**Benefit:**` prefix.
- New `tests/test_pptx_generator.py`:
- `test_slide_count`: python PPTX slide count == 22 (title + 20 +
appendix).
- `test_title_slide_colors`: title slide background black, red bar
present.
- `test_expected_slide_titles`: spot-check 3-5 slide titles match
the markdown.
- `test_table_rendering`: a slide with a table (e.g., RACI) has a
native PPTX table shape.
- `test_image_embedding`: a slide with an image has a picture
shape.
- `test_benefit_callout_present`: ≥1 slide has italic benefit text.
3. **README** (REQ-275):
- Rewrite `docs/presentations/README.md` process section:
- 3-step process (was 4): 1) author `*-marp.md` (sole source of
truth, with `<!-- Speaker notes: -->` + `<!-- Talking points: -->`
comments); 2) render HTML + dual PPTX (`render_slides.sh`:
mermaid → MARP HTML+PPTX → inline images → python-pptx); 3)
talking points (mirrored from deck comments).
- Drop the plain `.md` row from the decks table.
- Add `inline_images.py` + `render_pptx.py` + `-python.pptx` rows
to the tooling section.
- Note `nova-sp-theme.css` retired (reference only; inline `style:`
is the live styling).
- Document the dual-PPTX output (MARP image-of-slide primary +
python structured for comparison/editing).
4. **Multi-persona code review** (`ciagent-review`):
- Review all changes in `milestone/v1.23-deck-cleanup-python-pptx`.
- Auto-apply P0 fixes; flag P1+ for post-hoc review.
- If P1+ found: fix in this phase.
5. **Audit** (`ciagent-audit`):
- Reconstruction test: git log matches `.ciagent/` files.
- `.ciagent/` file discipline + branch hygiene + commit discipline.
6. **Ship** (`ciagent-ship` — milestone ship):
- Merge `phase/05` → `milestone/v1.23-deck-cleanup-python-pptx`.
- Merge `milestone/v1.23-deck-cleanup-python-pptx` → `main`.
- Tag `v1.22.6` (final patch = milestone release).
- Create release with full milestone summary (all phases, all 13
requirements).
- Attach both PPTX files (MARP + python) to the release.
- Delete all milestone branches (local + remote). Tags preserve
history.
7. **Complete the milestone**:
- Update `REQUIREMENTS.md` — mark all v1.23 requirements complete.
- Update `ROADMAP.md` — mark v1.23 complete.
- Commit: `docs(milestone): complete v1.23 — Nova Deck Cleanup & Python PPTX`.
8. **Verify** (must-have):
- `pytest tests/test_slides_pipeline.py tests/test_pptx_generator.py`
all pass.
- CI workflow YAML is valid.
- README reflects the new pipeline.
---
## Versioning
- NFR milestone → progressive patches on v1.22.x line.
- `v1.22.0` (P0) → `v1.22.1` (P1) → `v1.22.2` (P2) → `v1.22.3` (P3a) →
`v1.22.4` (P3b) → `v1.22.5` (P4) → `v1.22.6` (P5 final = milestone
release). No separate minor tag. 6 phases total (was 7; P5+P6 merged
per G-004, P3 split into P3a+P3b per G-003).
## Risk Mitigations (from RESEARCH + GRILL)
1. **Default theme padding** — explicitly set
`section { padding: 48px 56px 40px }` in the inline `style:` block
(don't rely on default's `56px 64px` which doesn't reserve
header/footer space).
2. **python-pptx font fallback** — use "Akkurat Pro" in the PPTX
(PowerPoint does OS substitution); the HTML is the pixel-perfect
artifact, the python PPTX is for comparison/editing.
3. **Marp version drift** — marp-cli pinned at 4.5.0 (already).
4. **Speaker notes stripped** — Marp strips HTML comments from rendered
slides but they persist in the source `-marp.md` (acceptable; they're
for authors, not the audience).
5. **`test_html_embeds_theme`** — update to assert literal `#D6002A`
(not `--sp-red`).
6. **P3b parser overflow** (G-003) — isolated in its own phase so a
parser setback doesn't block inline-images (P3a) or trim (P4).
7. **"penetrate" purge scope** (G-001) — repo-wide, not just
`docs/presentations/`; RESEARCH.md/PLAN.md/GRILL.md exempt as
decision-history.
- P0 (this phase): `v1.23.0` — pre-execution patch
- P1: `v1.23.1` — consumer guide fixes
- P2: `v1.23.2` — env-transition detect-and-destroy
- P3: `v1.23.3` — env-transition tests
- P4: `v1.23.4` — final review + ship = **milestone release**
+5 -5
View File
@@ -33,7 +33,7 @@ traceable to a human attestation and an immutable evidence stream.
1. **Operations are Declared, Not Executed.** Consumers define what they
need; the platform reconciles, provisions, and progresses.
2. **The Delivery Lifecycle is a Sovereign Boundary.** The platform
governs infra and delivery; it does not penetrate upstream product/SDLC.
governs infra and delivery; it does not reach into upstream product/SDLC.
Integration is only through validated, published contracts.
3. **Lower Environments are Autonomous; Higher Environments are Attested.**
Dev = zero-touch agentic. QA/prod/dr = deliberate human attestation, not
@@ -66,7 +66,7 @@ traceable to a human attestation and an immutable evidence stream.
The **Product Development Lifecycle (PDLC)** — product backlog, code
authorship, IDE workflows, sprint planning, application business logic —
is **upstream** of Nova. Nova never penetrates the PDLC. Nova's domain is
is **upstream** of Nova. Nova never reaches into the PDLC. Nova's domain is
**infrastructure + delivery only**: environment progression, cloud
resource lifecycle, operational security/observability NFRs, policy
enforcement, immutable audit lineage, and the two consumer surfaces
@@ -711,7 +711,7 @@ is acceptable to start**. Five user-directed inputs drive the milestone:
`sp-theme.json` survived; only the Marp CSS theme was lost.
2. **PDLC-upstream scope made explicit.** Core Tenet #2 already states the
platform "does not penetrate upstream product/SDLC" and Anti-Goal #1 says
platform "does not reach into upstream product/SDLC" and Anti-Goal #1 says
"Not an upstream development platform." v1.18 promotes this from a
buried tenet to a dedicated, unmissable scope statement in PROJECT.md +
`docs/scope.md` + a deck slide: **the PDLC (Product Development
@@ -1559,7 +1559,7 @@ fragile). The milestone delivers:
redistribution.
- **Parallel python-pptx generator** — structured, editable, S&P-themed
PPTX alongside the MARP image-of-slide PPTX.
- **Targeted word-count trim** + "penetrate" term removal.
- **Targeted word-count trim** + removal of the previously-used loaded scope term.
**Phase count:** 7 (P0 pre-execution + 5 execution + 1 final).
@@ -1576,4 +1576,4 @@ fragile). The milestone delivers:
New requirements REQ-263..REQ-275 — see `REQUIREMENTS.md` §v1.23.
Summary: consolidation (REQ-263,264), style restoration (REQ-265,266,267),
image inlining (REQ-268), python-pptx generator (REQ-269,270), word-count
trim + "penetrate" removal (REQ-271,272), CI/tests/README (REQ-273,274,275).
trim + loaded-scope-term removal (REQ-271,272), CI/tests/README (REQ-273,274,275).
+181 -13
View File
@@ -2015,16 +2015,184 @@ assert 20 main + 1 appendix.
| REQ | Phase | Status |
|-----|-------|--------|
| REQ-263 | P1 | pending |
| REQ-264 | P1 | pending |
| REQ-265 | P2 | pending |
| REQ-266 | P2 | pending |
| REQ-267 | P2 | pending |
| REQ-268 | P3a | pending |
| REQ-269 | P3b | pending |
| REQ-270 | P3b | pending |
| REQ-271 | P4 | pending |
| REQ-272 | P4 | pending |
| REQ-273 | P5 | pending |
| REQ-274 | P5 | pending |
| REQ-275 | P5 | pending |
| REQ-263 | P1 | complete |
| REQ-264 | P1 | complete |
| REQ-265 | P2 | complete |
| REQ-266 | P2 | complete |
| REQ-267 | P2 | complete |
| REQ-268 | P3a | complete |
| REQ-269 | P3b | complete |
| REQ-270 | P3b | complete |
| REQ-271 | P4 | complete |
| REQ-272 | P4 | complete |
| REQ-273 | P5 | complete |
| REQ-274 | P5 | complete |
| REQ-275 | P5 | complete |
## v1.24 — Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement
> **Feature milestone** (one `feat` phase: env-transition destroy enforcement;
> the rest are `fix`/`docs`/`test`). Tags run on the **v1.23.x** line
> (milestone v1.24 → tags v1.23.0..v1.23.N). Final patch = milestone release.
>
> Two problems, one milestone:
> 1. **Consumer guide accuracy.** A review of `docs/consumer-guide.md`
> found 5 issues: (a) Step 8 tells consumers to change `environment:` in
> their contract to promote, which (b) contradicts the same doc's
> "Per-environment deployment" section (lines 396-477) that says
> "promotion-without-editing," (c) the Step 3 contract-fields table
> lists stale fields (`uses`, `module`) that no longer exist in the
> schema (real fields: `id`, `name`, `environment`, `infrastructure`),
> (d) Step 4 caller example is inconsistent with Step 2, and (e) Step 5
> stage 8 says "(dev only)" when higher envs do apply after attestation.
> 2. **Environment-promotion lifecycle enforcement.** When a consumer
> edits `environment:` on a stable `contract.id` (Shape A promotion),
> the Terraform state key
> `spike/{stack_name}/{environment}/terraform.tfstate` (adapter.py:129)
> changes — creating a fresh state file in the new env while the prior
> env's resources remain live in AWS with no destroy ever running. This
> **orphans resources** and violates the platform's full-lifecycle-
> management mission. The platform must detect the env change and
> destroy the prior env's resources before building the new env. There
> must be **no path that orphans resources** — fail closed if the
> destroy fails.
>
> The per-environment caller-workflow path (Shape B: one caller workflow
> per env, `environment` passed as a workflow input) remains a fully
> supported alternative with no destroy needed (each env has its own state
> from day one). Both shapes are documented.
### Category: Consumer Guide Fixes (docs)
- **REQ-276:** `docs/consumer-guide.md` Step 3 "Contract fields" table is
corrected to list the real schema-enforced fields: `id`, `name`,
`environment`, `infrastructure` (matching `schemas/contract.schema.json`
`required` and the worked examples). The stale `uses` and `module` rows
are removed. The `uses` row's note about versioned tags moves to the
Step 2 caller-workflow section (where the version pin actually lives).
- **REQ-277:** `docs/consumer-guide.md` Step 4 caller workflow example is
made consistent with Step 2 — both show `environment` in `with:` or both
omit it with a "dev is the default" note. The two canonical caller
snippets no longer disagree.
- **REQ-278:** `docs/consumer-guide.md` Step 5 stage 8 "(dev only)" is
corrected to "(autonomous in dev; higher environments apply after HITL
attestation)" to match `docs/environments/index.md` autonomy table.
- **REQ-279:** `docs/consumer-guide.md` Step 8 "Promote to qa / prod" is
rewritten. It documents that editing `environment:` on a stable
`contract.id` (Shape A) **is a supported promotion path** and that the
platform **destroys the prior environment's resources before building
the new environment** — there is no orphan path; if the destroy fails,
the pipeline fails closed. It includes the worked qa example with a
note: "Changing `environment: dev` → `environment: qa` triggers a
destroy of the dev stack (state key `spike/{id}/dev/`) then an apply
against the qa stack (state key `spike/{id}/qa/`). Both emit evidence
events." It cross-references the "Per-environment deployment" section
(Shape B) as the alternative.
- **REQ-280:** `docs/consumer-guide.md` "Per-environment deployment"
section (lines 396-477) gains a lead sentence clarifying it is **Shape
B** (the alternative to Shape A's edit-and-destroy path in Step 8), and
that it avoids the destroy step because each env has its own state from
first deploy. The existing table, interpolation reference, and HITL
gate docs are preserved.
- **REQ-281:** `docs/consumer-guide.md` Reference table "sample contracts
use `@v1.19`" wording is corrected — the sample contracts no longer
carry `uses:` (the version pin lives in the caller workflow). Reword to
"used with caller workflow `@v1.19`" or the current tag.
### Category: Env-Transition Detect-and-Destroy (feat)
- **REQ-282:** New module `core/env_transition.py` provides:
`detect_prior_env(contract_id, consumer_repo, new_env) -> Optional[str]`
— queries the `nova-contracts` DynamoDB table (PK `consumerRepo`, SK
`contractId#submittedAt`, written by `core/lambda/contract_ingestor.py`)
for the last-applied environment for this consumer+contract. Returns
the prior env name if it differs from `new_env`, else `None`. Failures
to reach DynamoDB log a warning and return `None` (conservative — Shape
B legitimately has no prior record). Uses boto3 with the ABAC-scoped
deploy role; respects `core/env.py` for config.
- **REQ-283:** `core/env_transition.py` provides
`record_applied_env(contract_id, consumer_repo, env)` — called after a
successful apply to upsert the last-applied env record in the
`nova-contracts` table (SK suffix `#LAST_APPLIED`). Idempotent.
- **REQ-284:** `scripts/run_platform.sh` gains a new **Step 0b:
environment-transition check** (after Step 0 env onboarding, before
Step 1 contract validation). It reads `CONTRACT_ID` + `CONSUMER_REPO`
(from `GITHUB_REPOSITORY` / `NOVA_CONSUMER_REPO`), calls
`env_transition.py detect`, and if a prior env is returned that differs
from the new env: (a) re-resolves the contract with
`environment_override=$PRIOR_ENV` to emit the prior TF config + state
backend; (b) runs `terraform init -reconfigure` + `terraform destroy
-auto-approve` against the prior env's state key
(`spike/{id}/{prior_env}/terraform.tfstate`); (c) emits a
`nova.env.destroyed` evidence event via `core/outbox_writer.py`; (d)
**fails closed** — if the destroy exits non-zero, the pipeline exits
non-zero and no apply runs (no orphan path). If no prior env exists
(first deploy or Shape B), proceeds normally.
- **REQ-285:** `scripts/run_platform.sh` records the applied env after a
successful apply (calls `env_transition.py record` with the resolved
env). This is the source of truth for the next run's detect step.
- **REQ-286:** `.github/workflows/deploy.yml` passes
`NOVA_CONSUMER_REPO=${{ github.repository }}` to `run_platform.sh` so
`env_transition.py` can query DynamoDB with the correct PK.
- **REQ-287:** `adapters/terraform/adapter.py` state-key block
(lines 127-133) gains a doc comment clarifying the key
`spike/{stack_name}/{environment}/terraform.tfstate` is **env-scoped
precisely to support destroy-on-env-change** — the env segment lets the
detect-and-destroy step target the prior env's state without affecting
the new env. No behavior change.
### Category: Tests (test)
- **REQ-288:** `tests/test_env_transition.py` covers:
`detect_prior_env` returns `None` when no record exists (first deploy);
returns the prior env when a record exists and differs; returns `None`
when the record matches `new_env` (re-apply same env);
`record_applied_env` writes the record. Uses moto for DynamoDB mocking
(pattern from `tests/test_contract_ingestor.py`).
- **REQ-289:** `tests/test_run_platform_env_transition.py` asserts:
`run_platform.sh` has a "Step 0b: environment-transition check" block;
it calls `env_transition.py detect`; it calls `terraform destroy`
against the prior env when a transition is detected; it fails closed on
destroy failure (no apply runs); it records the applied env after a
successful apply. Pattern: `tests/test_pipeline.py:79-95` (read the
script text + assert substrings).
- **REQ-290:** `tests/test_consumer_guide_per_env_section.py`
`test_consumer_guide_states_no_field_editing` is renamed to
`test_consumer_guide_documents_both_promotion_shapes` and asserts both
shapes are present (Shape A: edit environment with destroy semantics;
Shape B: per-environment caller workflows). The other 5 assertions in
the file are preserved. A new test
`test_consumer_guide_documents_destroy_on_env_change` asserts the guide
states the platform destroys the prior env's resources when the
environment field is changed and that there is no orphan path.
### Out of Scope (v1.24)
- **Cross-account destroy.** If the prior and new envs are in different
AWS accounts (per `docs/environments/index.md`), the destroy step needs
the prior env's role credentials. The current scaffold
(`core/environments/dev.json`) uses one account. Cross-account destroy
is deferred to a future milestone; v1.24 targets the same-account case
and documents the cross-account limitation.
- **Decommission pipeline integration.** The env-transition destroy is a
direct `terraform destroy` (not the 2-step HITL decommission). The
decommission pipeline remains for explicit stack teardown with SRE
gates; env-transition is an automated lifecycle step.
- **Removing Shape B.** Both shapes stay supported. Shape B is not
deprecated.
### v1.24 Traceability
| REQ | Phase | Status |
|-----|-------|--------|
| REQ-276 | P1 | pending |
| REQ-277 | P1 | pending |
| REQ-278 | P1 | pending |
| REQ-279 | P1 | pending |
| REQ-280 | P1 | pending |
| REQ-281 | P1 | pending |
| REQ-282 | P2 | pending |
| REQ-283 | P2 | pending |
| REQ-284 | P2 | pending |
| REQ-285 | P2 | pending |
| REQ-286 | P2 | pending |
| REQ-287 | P2 | pending |
| REQ-288 | P3 | pending |
| REQ-289 | P3 | pending |
| REQ-290 | P3 | pending |
+187 -3272
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -28,6 +28,8 @@
- **v1.13.1 (complete, tag `v1.13.1`):** config.json schema migration — regenerate `.ciagent/config.json` to the updated CIAgent v2 config structure (drop removed fields, migrate `gitea``release.gitea`, add `secrets`/`ship`/`backend`/`ideation`/`personas`/`logging`/`telemetry` sections). Code review: 0 P0, 2 P1/P2 auto-fixed. Docs-only NFR patch (no code changes). Gitea release id 253.
- **v1.13.2 (complete, tag `v1.13.2`):** presentation badge cleanup + platform architecture diagram — removed all `testing`/`agentic` maturity badges from both decks (only `planned` retained); added a new Slide 3 "The platform at a glance" with a shared high-level logical architecture diagram (consumer surfaces → contract → central pipeline → cross-cutting components → AWS) to both decks; renumbered subsequent slides 411; synced talking points + README. Docs-only NFR patch (no code changes).
- **v1.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html
- **v1.23 (complete, tag `v1.22.6`):** Nova Deck Cleanup & Python PPTX — consolidated the deck to a single source-of-truth `*-marp.md` (deleted the plain `.md`; speaker notes + talking points embedded as Marp HTML comments); restored the clean S&P visual style (Marp `default` theme + inline `style:` block, matching the old `the-developer-experience.html`); retired `nova-sp-theme.css` from the render path (kept as reference); base64-inlined all images in the HTML for redistribution (`scripts/inline_images.py`); built a parallel structured editable S&P-themed PPTX generator (`scripts/render_pptx.py` via `python-pptx`); restyled benefit callouts (`<div class="benefit">`); targeted ~20-30% word-count trim on 8 verbose slides; removed the term "penetrate" repo-wide. 13 requirements (REQ-263..275), 6 phases. 43 tests pass.
- **v1.24 (active):** Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement — fixes 5 consumer-guide accuracy issues (stale contract-fields table, inconsistent caller examples, misleading "dev only" apply phrasing, Step 8 promotion contradicts the per-env section, stale `@v1.19` reference wording) and adds platform-enforced destroy-on-environment-change: when a consumer edits `environment:` on a stable `contract.id` (Shape A promotion), the platform detects the change via the `nova-contracts` DynamoDB table, destroys the prior env's Terraform state (`spike/{id}/{prior_env}/`) before building the new env, and fails closed if the destroy fails (no orphan path). The per-environment caller-workflow path (Shape B) remains supported. New `core/env_transition.py` module. 15 requirements (REQ-276..290), 4 phases. Feature milestone; tags on v1.23.x line.
---
@@ -2071,7 +2073,7 @@ release). **DONE.**
complete; ROADMAP marked complete; CHECKPOINT cleared.
- **Requirements:** REQ-254..262 (9 requirements, all complete).
## v1.23 — Nova Deck Cleanup & Python PPTX (active)
## v1.23 — Nova Deck Cleanup & Python PPTX (complete)
> **NFR milestone** (docs/render/test only; no features). Tags run on the
> **v1.22.x** line (milestone v1.23 → tags v1.22.0..v1.22.6). Final patch
+1 -1
View File
@@ -8,7 +8,7 @@
],
"active_project": "acdl",
"active_projects": ["acdl"],
"active_milestone": "v1.23",
"active_milestone": "v1.24",
"autonomy": {
"level": "full",
"escalation_hooks": ["deploy", "delete_data", "merge_to_main"],
+18 -6
View File
@@ -1,11 +1,15 @@
# 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
on:
push:
paths:
- 'docs/presentations/**'
- 'scripts/render_slides.sh'
- 'assets/nova-sp-theme.css'
- 'scripts/inline_images.py'
- 'scripts/render_pptx.py'
- 'pyproject.toml'
workflow_dispatch:
jobs:
@@ -16,16 +20,24 @@ jobs:
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
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: |
npx --yes @marp-team/marp-cli@latest --version
npx --yes @mermaid-js/mermaid-cli --version
npx --yes @marp-team/marp-cli@4.5.0 --version
npx --yes @mermaid-js/mermaid-cli@11.16.0 --version
- name: Render slides
run: bash scripts/render_slides.sh
- name: Commit rendered artifacts
run: |
git config user.name "nova-slides-bot"
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 push
git push
+2
View File
@@ -110,6 +110,8 @@ jobs:
- name: Run the platform pipeline
working-directory: ${{ github.workspace }}
env:
NOVA_CONSUMER_REPO: ${{ github.repository }}
run: |
MODE_FLAG=""
case "${{ inputs.mode }}" in
+18 -6
View File
@@ -1,11 +1,15 @@
# 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
on:
push:
paths:
- 'docs/presentations/**'
- 'scripts/render_slides.sh'
- 'assets/nova-sp-theme.css'
- 'scripts/inline_images.py'
- 'scripts/render_pptx.py'
- 'pyproject.toml'
workflow_dispatch:
jobs:
@@ -16,16 +20,24 @@ jobs:
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
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: |
npx --yes @marp-team/marp-cli@latest --version
npx --yes @mermaid-js/mermaid-cli --version
npx --yes @marp-team/marp-cli@4.5.0 --version
npx --yes @mermaid-js/mermaid-cli@11.16.0 --version
- name: Render slides
run: bash scripts/render_slides.sh
- name: Commit rendered artifacts
run: |
git config user.name "nova-slides-bot"
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 push
git push
+3
View File
@@ -115,6 +115,9 @@ def adapt(stack_instance, out_dir):
environment = stack.get("environment", "dev")
account_id = env.get_env("AWS_ACCOUNT_ID", "581513795199")
state_bucket = f"nova-tfstate-{account_id}-us-east-1"
# State key is env-scoped (v1.24 REQ-287): the {environment} segment lets
# the env-transition detect-and-destroy step target the PRIOR env's state
# without affecting the new env. No orphan path on environment promotion.
terraform_tf = (
'terraform {\n'
' required_version = ">= 1.9, < 1.10"\n'
+159
View File
@@ -0,0 +1,159 @@
"""Nova Environment Transition — detect prior env + record applied env.
When a consumer edits the `environment:` field on a stable contract `id`
(Shape A promotion), the platform must destroy the prior environment's
resources before building the new environment. This module provides the
DynamoDB query logic to detect the prior environment and record the
applied environment after a successful apply.
Source of truth: the `nova-contracts` DynamoDB table (PK `consumerRepo`,
SK `contractId#submittedAt`), written by `core/lambda/contract_ingestor.py`.
detect_prior_env() queries the table for the last-applied environment for
a given consumerRepo + contractId. If it differs from the new env, the
prior env name is returned (so the pipeline can destroy it). If no record
exists (first deploy or Shape B per-env caller), returns None.
record_applied_env() writes a `#LAST_APPLIED` record after a successful
apply, so the next run's detect step has a source of truth.
Failures to reach DynamoDB (local/CI mode without the table) log a warning
and return None (conservative no false-positive destroys). This is the
no-orphan-path guarantee: if we can't confirm a prior env, we don't
destroy, but we also don't silently proceed in a way that orphans — the
record step ensures future runs have the data.
CLI:
python3 core/env_transition.py detect --contract-id <id> --consumer-repo <repo> --new-env <env>
python3 core/env_transition.py record --contract-id <id> --consumer-repo <repo> --env <env>
"""
import datetime
import json
import os
import sys
from typing import Optional
try:
import boto3
except ImportError:
boto3 = None
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "nova-contracts")
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
LAST_APPLIED_SUFFIX = "#LAST_APPLIED"
def _get_table():
"""Return the DynamoDB table resource, or raise if boto3 unavailable."""
if boto3 is None:
raise RuntimeError("boto3 is required for env_transition")
session = boto3.Session(region_name=REGION)
dyn = session.resource("dynamodb")
return dyn.Table(TABLE_NAME)
def detect_prior_env(contract_id: str, consumer_repo: str, new_env: str) -> Optional[str]:
"""Query the nova-contracts table for the last-applied env.
Returns the prior env name if it differs from new_env, else None.
Failures to reach DynamoDB log a warning and return None (conservative).
"""
try:
table = _get_table()
sk_prefix = f"{contract_id}{LAST_APPLIED_SUFFIX}#"
resp = table.query(
KeyConditionExpression="consumerRepo = :repo AND begins_with(#sk, :prefix)",
FilterExpression="#status = :status",
ExpressionAttributeNames={
"#sk": "contractId#submittedAt",
"#status": "status",
},
ExpressionAttributeValues={
":repo": consumer_repo,
":prefix": sk_prefix,
":status": "applied",
},
ScanIndexForward=False,
Limit=1,
)
items = resp.get("Items", [])
if not items:
return None
prior_env = items[0].get("environment")
if prior_env and prior_env != new_env:
return prior_env
return None
except Exception as exc:
sys.stderr.write(
f"WARNING: env_transition.detect_prior_env: could not query "
f"DynamoDB table {TABLE_NAME}{type(exc).__name__}: {exc}. "
f"Assuming no prior env (conservative). This is expected in "
f"local/CI mode without the nova-contracts table.\n"
)
return None
def record_applied_env(contract_id: str, consumer_repo: str, env: str) -> bool:
"""Write a LAST_APPLIED record to the nova-contracts table.
Called after a successful apply. Idempotent (writes a new timestamped
record each time; the detect step reads the latest by ScanIndexForward).
Returns True on success, False on failure (non-fatal the pipeline
should not halt if the record write fails).
"""
try:
table = _get_table()
ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
sk = f"{contract_id}{LAST_APPLIED_SUFFIX}#{ts}"
table.put_item(
Item={
"consumerRepo": consumer_repo,
"contractId#submittedAt": sk,
"contractId": contract_id,
"environment": env,
"status": "applied",
"appliedAt": ts,
}
)
return True
except Exception as exc:
sys.stderr.write(
f"WARNING: env_transition.record_applied_env: could not write to "
f"DynamoDB table {TABLE_NAME}{type(exc).__name__}: {exc}. "
f"The apply succeeded but the last-applied env record was not "
f"persisted. Future env-transition detection may not work.\n"
)
return False
def main(argv):
import argparse
parser = argparse.ArgumentParser(description="Nova env-transition detect/record")
sub = parser.add_subparsers(dest="command", required=True)
p_detect = sub.add_parser("detect", help="Detect prior env for a contract")
p_detect.add_argument("--contract-id", required=True)
p_detect.add_argument("--consumer-repo", required=True)
p_detect.add_argument("--new-env", required=True)
p_record = sub.add_parser("record", help="Record the applied env for a contract")
p_record.add_argument("--contract-id", required=True)
p_record.add_argument("--consumer-repo", required=True)
p_record.add_argument("--env", required=True)
args = parser.parse_args(argv[1:])
if args.command == "detect":
prior = detect_prior_env(args.contract_id, args.consumer_repo, args.new_env)
print(json.dumps({"prior_env": prior}))
return 0 if prior is None else 0
elif args.command == "record":
ok = record_applied_env(args.contract_id, args.consumer_repo, args.env)
print(json.dumps({"recorded": ok}))
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main(sys.argv))
+47 -11
View File
@@ -140,10 +140,10 @@ name: microservice
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `uses` | string | yes | Reference to the central deployment pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `nova/pipelines/contract.yml@v1.19`). Bare or `@main` references are discouraged. See [Versioning](pipeline/versioning). |
| `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-assets`, `microservice`, `s3`). See the [module catalog](modules/). |
| `environment` | string | yes | The platform-managed environment to deploy to (e.g. `dev`). See [Environments](environments/). |
| `inputs` | object | yes | Module-specific inputs (see the module's README). |
| `id` | string | yes | Short operational acronym (3-6 chars, lowercase + digits + hyphens). Becomes `stack.name`: the Terraform state key (`spike/<id>/<env>/terraform.tfstate`), the outbox event identity, and the resource naming prefix. Stable across deploys and environment promotions. |
| `name` | string | yes | Full human-readable stack name. Becomes `stack.title`: the display name in PR comments, evidence records, and dashboards. |
| `environment` | string | yes | The platform-managed environment to deploy to (`dev`, `qa`, `prod`, or `dr`). See [Environments](environments/). |
| `infrastructure` | object | yes | Map of modules to deploy, keyed by module name (matching a registry key in `modules/registry.json`). Each entry carries an optional `version` (defaults to latest published) and per-module `inputs`. One entry = single-module deploy; N entries = multi-module manifest. |
### Module inputs
@@ -180,6 +180,7 @@ jobs:
uses: nova/.github/workflows/deploy.yml@v1.19
with:
contract: .nova/contract.yml
environment: dev
```
That is the entire consumer-side workflow. When you push to `main`:
@@ -229,7 +230,7 @@ flowchart TD
S5["policy checks<br/>(adapter -&gt; PolicyCheckResult)"] --> S6
S6["confidence<br/>score + band (dev &gt;= 0.50)"] --> S7
S7["evidence event<br/>to the audit outbox"] --> S8
S8["infrastructure apply<br/>(dev only)"]
S8["infrastructure apply<br/>(autonomous in dev;<br/>higher envs apply after HITL)"]
```
1. **validate-contract** — validates your contract YAML against the contract
@@ -250,9 +251,10 @@ flowchart TD
threshold is ≥ 0.50. If the band is `pass`, the pipeline proceeds.
7. **evidence event** — a hash-chained evidence event is written to the
audit outbox.
8. **infrastructure apply** (dev only) — the infrastructure plan is applied,
creating the resources in your AWS account. An evidence event for the
apply is recorded.
8. **infrastructure apply** (autonomous in dev; higher environments apply
after HITL attestation) — the infrastructure plan is applied, creating
the resources in your AWS account. An evidence event for the apply is
recorded.
## Step 6 — What gets created
@@ -289,7 +291,14 @@ push your container image to the ECR repo the platform created.
## Step 8 — Promote to qa / prod
Change `environment` in your contract (the infrastructure stays the same):
There are **two supported promotion shapes**. Both are valid; pick the one
that fits your repo's workflow.
### Shape A — edit the environment field (destroy-then-rebuild)
Change `environment` in your contract (the infrastructure stays the same).
The contract `id` stays stable, so the platform knows this is the same
stack moving to a new environment:
```yaml
id: assets
@@ -301,10 +310,32 @@ infrastructure:
inputs: { ... }
```
**What happens when you change `environment: dev``environment: qa`:**
the platform detects that the environment changed on a known contract `id`.
Before building the new environment, it **destroys the prior environment's
resources** (Terraform state key `spike/{id}/dev/`) and records an evidence
event for the destroy. Only then does it apply the new environment (state
key `spike/{id}/qa/`). **There is no orphan path** — if the destroy fails,
the pipeline fails closed (no apply runs, no resources are left behind).
This is full lifecycle management: the platform never creates a state
where prior-environment resources are abandoned.
Higher environments require human attestation (a platform-runner deployment
approval) and higher confidence thresholds. See [Environments](environments/)
for the full table.
> **Note:** the destroy-then-rebuild runs within the same AWS account (the
> current platform scaffold uses one account). Cross-account promotion
> (separate accounts per env) is a future milestone.
### Shape B — per-environment caller workflows (no editing)
Alternatively, keep one contract per environment (or one contract + the
`environment` workflow input) and run the matching CI job to promote. This
avoids the destroy step because each environment has its own state from the
first deploy. See [Per-environment deployment](#per-environment-deployment)
below for the full pattern.
## Step 9 — Compliance extensions
Each module lists compliance extension points for the future compliance
@@ -326,8 +357,8 @@ per-module extension points. Common examples:
| Contract schema | `schemas/contract.schema.json` | JSON Schema for consumer contracts. |
| Stack schema | `schemas/stack.schema.json` | JSON Schema for the resolved stack instance. |
| Module catalog | [modules/](modules/) | All primitives and modules. |
| Sample contract | `contracts/static-assets.yaml` | The reference example contract (uses `@v1.19`). |
| Sample contract | `contracts/microservice.yaml` | The microservice example contract (uses `@v1.19`). |
| Sample contract | `contracts/static-assets.yml` | The reference example contract (used with caller workflow `@v1.19`). |
| Sample contract | `contracts/microservice.yml` | The microservice example contract (used with caller workflow `@v1.19`). |
| Module examples | `modules/<name>/examples/` | Validated per-module example contracts (`simple.yaml` + `complex.yaml`). |
| Contract resolver | `core/contract_resolver.py` | Resolves contracts to stack instances. |
| Angine adapter | `adapters/terraform/adapter.py` | Compiles stack instances to infrastructure. |
@@ -395,6 +426,11 @@ separately (or left running to monitor the decommissioned stack's
endpoints going dark).
## Per-environment deployment
> **This is Shape B** (the alternative to [Shape A's edit-and-destroy
> path](#step-8--promote-to-qa--prod) in Step 8). Shape B avoids the
> destroy step because each environment has its own state from the first
> deploy — no prior environment to tear down.
Nova supports a **promotion-without-editing** model: you do not edit the
`environment:` field in a contract to promote dev → qa → prod → dr.
Instead, there is **one CI job per environment**, each pointing at its
+177 -145
View File
@@ -2,147 +2,184 @@
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.
**Never edit the Marp deck, the PPTX, or the talking points directly** —
always start from the full markdown source of truth (Step 1), synthesize the
Marp deck (Step 2), export to HTML + PPTX (Step 3), then distill the talking
points (Step 4). This keeps a reviewable, plain-text source of truth for
every deck and a presenter-ready cue sheet for delivery.
Every presentation in this folder is produced by the same three-step
process. **Never edit the rendered HTML, either PPTX, or the talking
points directly** — always start from the Marp deck source of truth
(Step 1), render it (Step 2), then distill the talking points (Step 3).
This keeps a reviewable, plain-text source of truth for every deck and a
presenter-ready cue sheet for delivery.
```
Step 1: full markdown Step 2: Marp deck Step 3: HTML + PPTX Step 4: Talking points
(source of truth) ──► (lean, 21 slides) ──► (rendered) ──► (presenter cues)
*.md *-marp.md *.html / *.pptx *-talking-points.md
+ speaker notes + embedded PNG diagrams + 3-6 bullets per slide
+ mermaid code blocks + Marp frontmatter + key takeaway per slide
+ no speaker notes + indexed by Marp slide #
+ no maturity badges + content distilled from Step 1
+ no version in footer
Step 1: Author the deck Step 2: Render Step 3: Talking points
(source of truth) ──► (HTML + dual PPTX) ──► (presenter cues)
*-marp.md *.html *-talking-points.md
+ ## Slide N — Title + mermaid PNGs + 3-6 bullets per slide
+ <!-- Speaker notes: --> + MARP PPTX (image-of-slide) + key takeaway per slide
+ <!-- Talking points: --> + python PPTX (structured) + indexed by slide #
+ <div class="benefit"> + base64-inlined HTML + content distilled from
+ 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
truth** — it contains:
This is the **sole source of truth** — the Marp deck that is both authored
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.
- A `> **Speaker notes:**` block at the end of each slide with the nuance,
the "who cares and why," and the honesty caveats.
- Mermaid diagrams as ```` ```mermaid ```` fenced code blocks (these render
on GitHub/Pages but not in Marp — Step 2 converts them to images).
- An honest "shipped vs. deferred" framing: every "available today" claim is
grounded in shipped/verified work; every "deferred" item is explicitly
- **Speaker notes** as `<!-- Speaker notes: ... -->` HTML comments at the
end of each slide. Marp excludes HTML comments from the rendered slide;
they are for authors/presenters only.
- **Talking points** as `<!-- Talking points: ... -->` HTML comments (also
excluded from rendering — Step 3 mirrors them into a standalone cue
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 `![w:1000](assets/png/<name>.png)` (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.
**Why this file is the source of truth:** it is reviewable in any markdown
viewer, diffs cleanly in git, and carries the full reasoning (speaker notes)
that a presenter needs. The Marp deck and PPTX are *derived artifacts* — if a
fact is wrong, fix it here and re-run Steps 2 and 3.
**Why the Marp deck is the source of truth:** it is reviewable in any
markdown viewer, diffs cleanly in git, and carries the full reasoning
(speaker notes) that a presenter needs. The HTML and PPTX are *derived
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`,
`paginate: true`, `size: 16x9`, a header/footer, and an inline `style:`
block for fonts, colors, tables.
- **No speaker notes.** The Marp deck is what the audience sees; the
speaker notes live only in the Step 1 source of truth.
- **Mermaid diagrams → PNG images.** Marp does not render mermaid fenced
blocks natively. Extract each mermaid block from Step 1 into a `.mmd`
source file under `assets/mmd/`, render it to PNG under `assets/png/`,
and embed it with `![w:1000](assets/png/<name>.png)`.
- **`<!-- _class: title -->` + `<!-- _paginate: false -->`** on title and
closing slides for the dark-background title style.
- **No maturity badges.** The deck no longer uses `<span class="badge">`
spans. Deferred items are named in plain language with their blocking
work, not tagged with a badge.
- **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`.
1. **Mermaid PNGs** — each `assets/mmd/*.mmd``assets/png/*.png`
(S&P-themed via `sp-theme.json`, 2x scale, transparent background).
2. **MARP HTML**`*-marp.md``*.html` (S&P inline style, Marp default
theme). Pinned `@marp-team/marp-cli@4.5.0`.
3. **MARP PPTX**`*-marp.md``*.pptx` (image-of-slide PPTX; the primary
release attachment).
4. **Inline images**`scripts/inline_images.py` rewrites the HTML to
base64-embed every `assets/` image so the HTML is self-contained (no
external asset folder needed for redistribution).
5. **python PPTX**`scripts/render_pptx.py` produces a second,
structured, editable PPTX (`*-python.pptx`) with native text boxes,
native tables, embedded pictures, and italic benefit callouts.
6. **Stage** — all rendered artifacts (PNGs + HTML + both PPTX) are
`git add`-ed for commit.
```bash
CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
npx --yes @marp-team/marp-cli@latest --allow-local-files \
docs/presentations/<deck-name>-marp.md \
-o docs/presentations/<deck-name>.html
bash scripts/render_slides.sh nova-autonomous-cloud-delivery
```
HTML export inlines images as base64 data URIs. PPTX export requires
`--allow-local-files` so the local PNG diagrams are embedded in the file.
The render + commit + attach pipeline is automated by `scripts/render_slides.sh`.
Both the HTML and both PPTX files are committed to the repo; the MARP
PPTX is also attached to the phase's release via
`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.
`nova-autonomous-cloud-delivery-talking-points.md`).
Distill the source of truth (Step 1) into presenter-ready cues, indexed by
the Marp deck (Step 2) slide structure:
Distill the deck's `<!-- Talking points: -->` HTML comments into
presenter-ready cues, indexed by the Marp deck (Step 1) slide structure:
- **One section per Marp slide**`## Slide N — Title`, matching the Marp
deck's 20 main + 1 appendix slide structure exactly.
- **3-6 talking point bullets per slide** — punchy, actionable cues distilled
from the source markdown's speaker notes.
- **3-6 talking point bullets per slide** — punchy, actionable cues
distilled from the Marp deck's `<!-- Talking points: -->` comments.
- **Key takeaway per slide** — the one memorable thing the audience should
walk away with from that slide.
- **No content duplication** — the talking points reference the Marp slides
for visual context and the source markdown for full detail.
- **No content duplication** — the talking points reference the Marp
slides for visual context.
## Directory layout
```
docs/presentations/
├── 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 2: Marp deck (20 main + 1 appendix = 21 slides)
├── nova-autonomous-cloud-delivery.html ← Step 3: rendered HTML (committed, S&P-themed)
├── nova-autonomous-cloud-delivery.pptx ← Step 3: rendered PPTX (committed, S&P-themed)
├── nova-autonomous-cloud-delivery-talking-points.md ← Step 4: presenter cues (19 sections)
├── 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.html ← Step 2: rendered HTML (committed, S&P inline style, base64-inlined images)
├── nova-autonomous-cloud-delivery.pptx ← Step 2: MARP PPTX (image-of-slide, primary release attachment)
├── nova-autonomous-cloud-delivery-python.pptx ← Step 2: python-pptx (structured, editable)
├── nova-autonomous-cloud-delivery-talking-points.md ← Step 3: presenter cues (21 sections)
└── 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
├── mmd/ ← mermaid source files (Step 2 input)
│ ├── sp-theme.json ← S&P Red/Black/White theme (mermaid-cli --configFile)
├── mmd/ ← mermaid source files (Step 2 input)
│ ├── sp-theme.json ← S&P Red/Black/White theme (mermaid-cli --configFile)
│ └── ... (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
### Appendix structure
### Slide structure
Each Marp deck has **20 main slides + 1 appendix slide**. The main 20 are the
presentation; the appendix is for Q&A backup. (v1.22 split slides 3 and 8
to relieve overflow, increasing the count from 18 to 20.)
Each Marp deck has **1 title slide + 20 main slides + 1 appendix slide = 22
rendered slides** (21 `## ` sections + the H1 title slide). The main 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 →
Roadmap + Ask. These are what the audience sees during the talk.
- **Appendix slide** (A1): the Metrics Glossary — detail-heavy reference for
Q&A.
- **Appendix slide** (A1): the Metrics Glossary — detail-heavy reference
for Q&A.
### Honesty framing
Every capability claim in the deck is grounded, derived, or honestly
deferred with its blocking work named in plain language. Internal provenance
(decision IDs, requirement IDs, internal file paths) is kept out of the
audience-facing slides — those live in the `.ciagent/` files only. When in
doubt, check `.ciagent/ROADMAP.md` and the milestone status in
`.ciagent/PROJECT.md`.
deferred with its blocking work named in plain language. Internal
provenance (decision IDs, requirement IDs, internal file paths) is kept
out of the audience-facing slide bodies — those live in the `.ciagent/`
files only (and may appear inside `<!-- ... -->` speaker-note comments,
which Marp excludes from the rendered slide). When in doubt, check
`.ciagent/ROADMAP.md` and the milestone status in `.ciagent/PROJECT.md`.
### Audience
@@ -156,76 +193,70 @@ Head of Infrastructure, Head of DevOps. The framing rules:
outcome; the mechanism follows.
- **Security, remediation velocity, reliability, lead time, observability,
citizen developer** are the themes — not implementation details.
- **"Infrastructure operations become visible"** is the recurring theme across
the deck.
- **"Infrastructure operations become visible"** is the recurring theme
across the deck.
### Diagrams
Mermaid diagrams in the Step 1 source use the repo's existing `flowchart`
style (renders on GitHub/Pages). For the Marp deck (Step 2):
Mermaid diagrams are authored as `assets/mmd/*.mmd` source files and
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**
for wide diagrams so the PNG fits a 16:9 slide without shrinking to
illegibility.
3. Render with a 2x scale factor and transparent background for crisp slides.
4. Embed with `![w:1000](assets/png/<name>.png)` (or `h:320` for tall images).
3. Render with a 2x scale factor and transparent background for crisp
slides (`scripts/render_slides.sh` does this with the S&P theme JSON).
4. Embed with `![w:1000](assets/png/<name>.png)` (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
### Prerequisites
- Node.js + npx (for `@marp-team/marp-cli` and `@mermaid-js/mermaid-cli`)
- A Chrome/Chromium binary (Marp PPTX export requires it)
- **Node.js + npx** (for `@marp-team/marp-cli` and `@mermaid-js/mermaid-cli`)
- **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:
`/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome`
### Render all mermaid diagrams to PNG
```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)
### Render the deck (HTML + dual PPTX + inlined images)
```bash
bash scripts/render_slides.sh nova-autonomous-cloud-delivery
```
This renders all mermaid PNGs, the HTML, and the PPTX, and stages them for
commit. The `--allow-local-files` flag is required so local PNG diagrams are
embedded. Both HTML and PPTX are committed to the repo; the PPTX is also
This renders all mermaid PNGs, the HTML (with base64-inlined images), the
MARP PPTX, and the python-pptx PPTX, and stages them for commit. Both
HTML and both PPTX files are committed to the repo; the MARP PPTX is also
attached to the phase's release.
## Adding a new presentation
1. **Write the full markdown** as `<deck-name>.md` following the
`## Slide N — Title` + `> **Speaker notes:**` structure. This is the
source of truth.
2. **Extract any mermaid diagrams** into `assets/mmd/<deck-name>-<slide>-<name>.mmd`
and render them to `assets/png/` (command above).
3. **Synthesize the Marp deck** as `<deck-name>-marp.md` with frontmatter,
no speaker notes, embedded PNGs, and no badges.
4. **Render to HTML + PPTX** via `scripts/render_slides.sh <deck-name>` and
commit both to `docs/presentations/`.
5. **Distill the talking points** as `<deck-name>-talking-points.md` — one
section per Marp slide, 3-6 talking point bullets + key takeaway, content
distilled from the source markdown (Step 1), indexed by the Marp deck
(Step 2) slide structure.
6. **Verify** the PPTX slide count and that media files are embedded:
1. **Author the Marp deck** as `<deck-name>-marp.md` — frontmatter
(`marp: true`, `theme: default`, `paginate: true`, `size: 16x9`, an
inline `style:` block with the S&P palette), `## Slide N — Title`
sections, `<!-- Speaker notes: -->` + `<!-- Talking points: -->` HTML
comments, and `<div class="benefit">` callouts. This is the sole source
of truth.
2. **Author any mermaid diagrams** as `assets/mmd/<deck-name>-<slide>-<name>.mmd`
(Step 2 renders them to `assets/png/`).
3. **Render** via `bash scripts/render_slides.sh <deck-name>` — this
produces the HTML (base64-inlined), the MARP PPTX, and the python-pptx
PPTX, and stages all of them (plus the PNGs) for commit.
4. **Distill the talking points** as `<deck-name>-talking-points.md` — one
section per Marp slide, 3-6 talking point bullets + key takeaway,
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
python3 -c "
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)]
media = [n for n in z.namelist() if n.startswith('ppt/media/')]
print(f'{len(slides)} slides, {len(media)} media files')
@@ -234,16 +265,17 @@ attached to the phase's release.
## Current decks
| Deck | Source of truth (Step 1) | Marp deck (Step 2) | Rendered HTML + PPTX (Step 3) | Talking points (Step 4) | 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 |
| 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-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"
> to "Autonomous Cloud Delivery Platform" (professional framing; conveys
> autonomy without the provocative wording). The narrative restructured to
> a 4-beat arc (Problem → Solution → Proof → Roadmap + Ask). Internal
> provenance (decision IDs, requirement IDs, file paths) removed from
> audience-facing slides. Maturity badges removed. The RACI matrix expanded
> to four roles (Quality Engineering + SRE). The Atelier slide split into
> two. The pipeline hardened: Checkov on static code before the plan;
> Wiz-or-Checkov on the plan (never both).
> **v1.23:** the slide creation process collapsed from 4 steps to 3 — the
> plain `<deck-name>.md` was deleted; `<deck-name>-marp.md` is now the
> sole source of truth. The standalone `nova-sp-theme.css` was retired
> from render (the live styling is the inline `style:` block in the
> `-marp.md` frontmatter; the CSS file is retained as a reference only).
> Speaker notes moved from blockquotes into `<!-- Speaker notes: -->`
> HTML comments. Benefit callouts moved from `**Benefit:**` prefixes to
> `<div class="benefit">`. The render pipeline now produces a dual-PPTX
> output (MARP image-of-slide + python-pptx structured) and base64-inlines
> all images into the committed HTML.
@@ -1,3 +1,8 @@
/* RETAINED AS REFERENCE ONLY not loaded at render time.
* The live deck uses Marp `default` theme + an inline `style:` block in
* the -marp.md frontmatter. This file is kept for future styling work
* reference. Do NOT pass via `--theme`; it is not in the render path.
*/
/* @theme nova-sp */
/* Nova S&P Global Energy theme for Marp decks.
*
@@ -1,9 +1,29 @@
---
marp: true
theme: nova-sp
theme: default
paginate: true
size: 16x9
footer: 'Nova — The Autonomous Cloud Delivery Platform'
style: |
section { font-family: "Akkurat Pro", "Helvetica Neue", "Arial", sans-serif; font-size: 22px; color: #1B1B1B; padding: 48px 56px 40px; overflow: auto; }
h1 { color: #D6002A; font-size: 34px; margin-bottom: 0.3em; }
h2 { color: #D6002A; font-size: 26px; margin-bottom: 0.2em; }
h3 { color: #D6002A; font-size: 22px; margin-bottom: 0.2em; }
section.title { background: #1B1B1B; color: #fff; border-top: 8px solid #D6002A; }
section.title h1, section.title h2 { color: #fff; }
section.title header, section.title footer { display: none; }
table { font-size: 18px; width: 100%; border-collapse: collapse; }
th { background: #F0F0F0; border-bottom: 2px solid #D6002A; padding: 4px 8px; text-align: left; }
td { border-bottom: 1px solid #F0F0F0; padding: 4px 8px; }
blockquote { border-left: 4px solid #D6002A; color: #2E2E2E; font-size: 20px; padding-left: 12px; }
pre { background: #1B1B1B; color: #fff; border-radius: 4px; padding: 12px; font-size: 16px; }
code { background: #F0F0F0; color: #1B1B1B; border-radius: 2px; padding: 1px 4px; font-size: 18px; }
pre code { background: transparent; color: inherit; }
img { display: block; margin: 0 auto; max-width: 100%; max-height: 380px; object-fit: contain; }
strong { color: #D6002A; }
.benefit { margin-top: 0.6em; padding-top: 0.4em; border-top: 1px solid #D6002A; color: #1B1B1B; font-size: 20px; font-style: italic; }
section.title .benefit { color: #fff; }
@media print { section { overflow: hidden; } }
---
<!-- _class: title -->
@@ -21,14 +41,16 @@ Product Development & Citizen Developer Overview
**Product teams now own their cloud infrastructure — but ownership without discipline is destroying value.**
- **No lifecycle planning.** Resources are authored for creation, not for patching, decommissioning, or rollback — so changes are destructive.
- **Proactive scanning is not part of authoring.** AI-frontier models exploit zero-days at a rapid pace; teams cannot keep up by reacting. Modules must be scanned as code and at runtime — and remediated at the pace the threat moves.
- **Bandwidth gaps in infrastructure operations.** Time spent on remediation + the push for innovation leaves operations chronically under-resourced; detections are missed, incidents grow.
- **Tribal knowledge and the rockstar-operator problem.** Operations depend on a handful of administrators; when they leave, the knowledge leaves with them. The platform should encode the discipline, not the person.
- **No lifecycle planning.** Resources are authored for creation, not for patching or rollback — so changes are destructive.
- **No proactive scanning in authoring.** AI-frontier models exploit zero-days faster than teams can react; modules must be scanned as code and at runtime, remediated at threat pace.
- **Bandwidth gaps.** Remediation plus the push for innovation leaves operations under-resourced; detections are missed, incidents grow.
- **Tribal knowledge.** Operations depend on a few administrators; when they leave, the knowledge leaves with them. The platform should encode the discipline, not the person.
Every hour a developer spends writing, deploying, fixing, or remediating infrastructure is an hour not spent releasing features to production.
<div class="benefit">an autonomous cloud delivery platform that encodes discipline as policy, scans proactively, remediates rapidly, and makes operations visible to leadership.</div>
**Benefit:** the answer is an autonomous cloud delivery platform that encodes discipline as policy, scans proactively, remediates rapidly, and makes operations visible to leadership rather than hidden in tribal knowledge.
<!-- Speaker notes: Do not frame this as "humans are the problem." The problem is that ownership was granted without the discipline, tooling, and lifecycle planning that infrastructure requires. The operator is not the bottleneck because operators exist — the bottleneck is that operations depend on a few individuals instead of an encoded system. -->
<!-- Transition: Here is the destination Nova is building toward. -->
<!-- Talking points: Open with the shift: "you build it, you run it" put Terraform into product teams — ownership without discipline is destroying value; Land the lifecycle-planning gap: resources authored for creation, not for patching/rollback → destructive changes; Land the urgency: AI-era 0-day pace demands proactive scanning as code + at runtime, remediated at threat pace; Call out tribal knowledge / the rockstar-operator problem — the platform should encode the discipline, not the person; Do NOT frame this as "humans are the problem" — the problem is ownership without the discipline and tooling; Key takeaway: the problem is infrastructure ownership without discipline; the answer is an autonomous platform that encodes the discipline -->
---
@@ -40,7 +62,11 @@ Every hour a developer spends writing, deploying, fixing, or remediating infrast
- **Provable, not promised** — trust established by deterministic scripts that calculate a score; the platform functions without AI
- **Autonomy in operations, human at stage gates** — QA signs off for production; SRE greenlights operational readiness
**Benefit:** the destination is autonomous operations with provable trust — security, remediation velocity, reliability, and lead time made visible to leadership, not promised to them.
<div class="benefit">the destination is autonomous operations with provable trust — security, remediation velocity, reliability, and lead time made visible to leadership, not promised to them.</div>
<!-- Speaker notes: "Visible" is the operative word. The vision is not just that operations run without an operator — it is that operations become observable, queryable, and accountable. That is what makes the trust defensible. -->
<!-- Transition: The vision is ambitious — here are the strategic objectives that make it concrete, and the anti-goals that keep it focused. -->
<!-- Talking points: Read the vision verbatim — "infrastructure operations become visible" is the operative phrase; Emphasize "provable, not promised" — trust established by deterministic scripts; the platform functions without AI; State the attestation model up front: QA for production, SRE for operational readiness; Key takeaway: autonomous operations with provable trust — security, remediation velocity, reliability, lead time made visible, not promised -->
---
@@ -53,7 +79,11 @@ Every hour a developer spends writing, deploying, fixing, or remediating infrast
- **Lead Time** (PR → Production) · **Infrastructure Vulnerability Count** (trend) · **MTTR** · **Cloud Spend Reduction**
4. **Integrate with externally owned development platforms — regardless of source** — PDLC, SDLC, Agentic, or Citizen Developer; Nova provides skills + MCP endpoints; all prod intents go through the same controls and quality gates
**Benefit:** the scope is explicit — Nova governs infrastructure and delivery, integrates with any upstream source through one validated contract, and measures success on four metrics a CTO can repeat back.
<div class="benefit">the scope is explicit — Nova governs infrastructure and delivery, integrates with any upstream source through one validated contract, and measures success on four metrics a CTO can repeat back.</div>
<!-- Speaker notes: Objective #2 is the one to land carefully: trust is established by deterministic scoring, not by an LLM. The platform functions without AI. -->
<!-- Transition: The objectives are concrete — here is what Nova is NOT, to keep it focused. -->
<!-- Talking points: Objective #1: zero-touch operations — autonomy as the default, not the demo; stage-gate attestation (QA, SRE) remains human by design; Objective #2 is the one to land carefully: trust = deterministic scoring, not an LLM; the platform functions without AI; Objective #3: four CTO-grade metrics (Lead Time, Vuln Count, MTTR, Spend) — all flow into PowerBI; Objective #4 is the integration thesis: Nova integrates with any upstream source; provides skills + MCP; all prod intents go through the same controls; Key takeaway: the scope is explicit — Nova governs infra + delivery, integrates with any source through one contract, measures success on four CTO metrics -->
---
@@ -64,20 +94,27 @@ Every hour a developer spends writing, deploying, fixing, or remediating infrast
3. Not an upstream development platform (no product backlogs, IDE, code authorship)
4. Not a replacement for the Product Development Lifecycle (PDLC)
**Benefit:** the boundaries are explicit — Nova is purpose-built for infrastructure operations and delivery, not a general-purpose AI agent or an upstream development platform.
<div class="benefit">the boundaries are explicit — Nova is purpose-built for infrastructure operations and delivery, not a general-purpose AI agent or an upstream development platform.</div>
<!-- Speaker notes: Anti-goals #3 and #4 protect the scope boundary — Nova will not become an IDE or a product-planning tool. -->
<!-- Transition: The scope boundary is explicit — here is exactly where Nova sits relative to the product development lifecycle. -->
<!-- Talking points: Not a general-purpose AI agent platform; Not a system that removes humans from accountability — only from normal operations; Not an upstream development platform (no product backlogs, IDE, code authorship); Not a replacement for the Product Development Lifecycle (PDLC); Anti-goals #3 and #4 protect the scope boundary — Nova will not become an IDE or a product-planning tool; Key takeaway: the boundaries are explicit — Nova is purpose-built for infra ops + delivery, not a general-purpose AI agent or an upstream dev platform -->
---
## Slide 5 — Scope: Downstream of PDLC
**Nova governs infrastructure and delivery. The PDLC is upstream — Nova never penetrates it. Integration is through one validated contract.**
**Nova governs infrastructure and delivery. The PDLC is upstream — Nova stays downstream of it. Integration is through one validated contract.**
- **The PDLC is upstream:** product backlog, code authorship (AI agent, IDE, agentic SDLC), sprint planning, application business logic
- **The PDLC is upstream** product backlog, code authorship (AI agent, IDE, agentic SDLC), sprint planning, application business logic. Nova stays downstream of it.
- **Nova is downstream:** contract ingestion → submission-readiness gate → policy enforcement → cloud resource lifecycle → environment progression (dev → qa → prod → dr) → immutable audit + attestation
- **The integration point is one contract** — any upstream source (AI agent, agentic SDLC, dev platform) produces submissions subject to the same compliance standards
- **Nova validates the submission, not the author** — the audit trail, the policy envelope, and the evidence stream are the same regardless of source
- **One validated contract** — any upstream source (AI agent, agentic SDLC, dev platform) produces submissions subject to the same compliance standards; Nova validates the submission, not the author
**Benefit:** a clean scope boundary — Nova is purpose-built for infrastructure operations and integrates with any upstream source through one validated contract, so the platform team's surface area stays bounded.
<div class="benefit">a clean scope boundary — Nova is purpose-built for infrastructure operations and integrates with any upstream source through one contract, so the platform team's surface area stays bounded.</div>
<!-- Speaker notes: This slide protects the scope. The moment Nova starts owning the PDLC, it loses focus. The contract boundary is what keeps Nova deep on infrastructure and delivery rather than shallow on everything. -->
<!-- Transition: With the scope clear, here is who owns what across the delivery lifecycle. -->
<!-- Talking points: Nova governs infra + delivery only; the PDLC (backlog, code authorship, IDE) is upstream — Nova stays downstream of it; Integration is only through the validated contract boundary; Any upstream source (AI agent, agentic SDLC, dev platform) produces submissions subject to the same compliance standards; Nova validates the submission, not the author; Key takeaway: Nova is purpose-built for infrastructure operations; the scope boundary is clean and bounded -->
---
@@ -98,7 +135,11 @@ Every hour a developer spends writing, deploying, fixing, or remediating infrast
**R**=Responsible · **A**=Accountable (sign-off) · **C**=Consulted · **I**=Informed. Production readiness is co-owned: the platform runs attestations agentically; the citizen developer authorizes the promotion at the stage gate.
**Benefit:** every party knows what they bring, what the platform provides, what quality engineering guards, and where SRE signs off — accountability is explicit, never diffuse.
<div class="benefit">every party knows what they bring, what the platform provides, what quality engineering guards, and where SRE signs off — accountability is explicit, never diffuse.</div>
<!-- Speaker notes: Quality attestation is now owned by Quality Engineering (not the Platform), and Production readiness is owned by SRE. The Platform runs the checks agentically but is never the Accountable party for the gate — that separation keeps the platform honest. -->
<!-- Transition: With ownership clear, here is how the pipeline enforces it. -->
<!-- Talking points: Four roles now: Citizen Developer, Platform, Quality Engineering, SRE; Quality attestation is owned by Quality Engineering (not the Platform); Production readiness is owned by SRE; The Platform runs the checks agentically but is never the Accountable party for the gate — that separation keeps the platform honest; Production readiness is co-owned: the platform runs attestations; the citizen developer authorizes the promotion at the stage gate; Key takeaway: you bring FRs + UAT; Nova provides NFRs + infra; QE guards the gate evidence; SRE signs off on production readiness -->
---
@@ -108,11 +149,15 @@ Every hour a developer spends writing, deploying, fixing, or remediating infrast
![h:480 class:tall](assets/png/platform-pipeline.png)
- **Contract → resolver → adapter → Checkov on static code (before plan) → terraform plan → Wiz on the plan → confidence signal stage gate apply evidence + ledger**
- **The pipeline** — see the diagram; two scan stages (static code, then resolved plan) feed a confidence signal to the stage gate before apply + evidence + ledger
- **Fail-fast, quick feedback** — Checkov runs on the authored Terraform code before `terraform plan` so developers get immediate policy feedback
- **Wiz on the plan when configured; Checkov as a drop-in otherwise** — Wiz scans the plan output; when Wiz credentials are absent, Checkov runs against the plan. **Wiz and Checkov are never both run on the plan.**
- **Wiz on the plan when configured; Checkov as a drop-in otherwise** — Wiz scans the plan output; when Wiz credentials are absent, Checkov runs against the plan instead. **Wiz and Checkov are never both run on the plan.**
**Benefit:** two layers of scanning, zero operator involvement in normal operations — fast deterministic feedback at authoring time and a runtime scan on the resolved plan.
<div class="benefit">two layers of scanning, zero operator involvement in normal operations — fast deterministic feedback at authoring time and a runtime scan on the resolved plan.</div>
<!-- Speaker notes: The two-stage scan is the key design: static code scanning catches policy violations before the cost of a plan; runtime plan scanning catches what the static code cannot (resolved values, cross-resource issues). The platform picks the runtime scanner based on configuration — never both, to avoid duplicate noise. -->
<!-- Transition: The pipeline produces decisions — here is how every decision is captured and made accountable. -->
<!-- Talking points: Walk the pipeline left-to-right: contract → resolver → adapter → Checkov (static) → plan → Wiz (on plan) → confidence → gate → apply; Two-stage scan: Checkov on static code BEFORE the plan (fail-fast dev feedback); Wiz on the plan (or Checkov as drop-in if no Wiz creds); Never both Wiz + Checkov on the plan — avoid duplicate noise; Dev is autonomous; qa/prod/dr require attestation (QA for quality, SRE for production readiness); Key takeaway: two layers of scanning, zero operator involvement in normal operations -->
---
@@ -121,10 +166,14 @@ Every hour a developer spends writing, deploying, fixing, or remediating infrast
**Every automated decision is captured, immutable, queryable — and accountable.**
- **What is captured:** the chosen action, the confidence score, the alternatives considered, whether a human overrode it, and the outcome (backfilled once the apply completes). Every stage-gate attestation (QA, SRE) is captured with approver identity and the evidence presented.
- **"AI decisions" are really automated decisions** — made by deterministic scripts that calculate a score and a band; the platform functions without AI. When an LLM planner is added later, it will emit richer alternatives without breaking the schema.
- **"AI decisions" are really automated decisions** — deterministic scripts calculate a score and a band; the platform functions without AI, and a later LLM planner emits richer alternatives without breaking the schema.
- **The value is accountability, not the storage engine** — the ledger is append-only and tamper-evident; every decision is queryable for auditing, traceable to an outcome, and impossible to rewrite after the fact.
**Benefit:** "autonomous" is defensible because every decision is immutable, queryable, and accountable — and the audience knows exactly what "automated" means here: deterministic scoring, not a black-box LLM.
<div class="benefit">"autonomous" is defensible because every decision is immutable, queryable, and accountable — and the audience knows exactly what "automated" means here: deterministic scoring, not a black-box LLM.</div>
<!-- Speaker notes: Do not dwell on the storage substrate. The audience cares that the ledger is append-only, queryable, and tied to outcomes — not that it is a hash-chain in a SQLite file. The D-122 honesty point is restated without the decision ID: the platform's decisions are deterministic; the ledger captures that real path. -->
<!-- Transition: Decisions are captured — here is how stage-gate attestation keeps humans in accountability. -->
<!-- Talking points: "AI decisions" are really automated decisions — deterministic scripts calculate a score; the platform functions without AI; Do not dwell on the storage substrate — the value is accountability (immutable, queryable, traceable to outcome), not the database; Every stage-gate attestation is captured with approver identity and the evidence presented; When an LLM planner is added later, it emits richer alternatives without breaking the schema; Key takeaway: autonomous is defensible because every decision is immutable, queryable, accountable — and "automated" means deterministic scoring, not a black-box LLM -->
---
@@ -138,7 +187,11 @@ Every hour a developer spends writing, deploying, fixing, or remediating infrast
| Performance baseline | qa | 7d | The deployment meets its performance envelope vs. the agreed baseline. |
| Security posture | qa | 24h | The deployment's security findings have been reviewed and accepted. |
**Benefit:** QA signs off on quality before any promotion — the gate is explicit, not implicit.
<div class="benefit">QA signs off on quality before any promotion — the gate is explicit, not implicit.</div>
<!-- Speaker notes: The matrix is not a rubber stamp. Each concern has a freshness window and a plain-language description of what is being attested. The "operator-supplied" label from the prior deck was dropped — every concern now has a plain-language description. -->
<!-- Transition: QA is half the matrix — here are the production and DR controls. -->
<!-- Talking points: The matrix is not a rubber stamp — structured, freshness-validated; Each concern now has a plain-language description of what is being attested (the old "operator-supplied" label is gone); Three QA concerns: functional correctness (24h), performance baseline (7d), security posture (24h); Each concern has a freshness window — evidence older than the window does not satisfy the gate; Key takeaway: QA signs off on quality before any promotion — the gate is explicit, not implicit -->
---
@@ -158,7 +211,11 @@ Every hour a developer spends writing, deploying, fixing, or remediating infrast
Separation-of-duties on prod: the approver cannot be the same person who built the deployment.
**Benefit:** the gate model is explicit — autonomy in operations, human in accountability, by design. The matrix is what makes autonomous operations safe enough to trust in production.
<div class="benefit">the gate model is explicit — autonomy in operations, human in accountability, by design. The matrix is what makes autonomous operations safe enough to trust in production.</div>
<!-- Speaker notes: The prod/DR rows are the operational-readiness and resilience gates — SRE signs off on operability, incident response, capacity, and the three resilience checks (DR drill, chaos, backup). Separation-of-duties on prod is the rule that keeps the gate honest: the approver cannot be the same person who built the deployment. -->
<!-- Transition: You've seen how Nova works — the pipeline, the ledger, the attestation gates. Here is how Nova instruments itself so that every claim in this deck is traceable to a real signal. -->
<!-- Talking points: Seven prod/DR concerns: operational readiness, incident response, capacity & cost, DR drill, chaos, backup, DR region deploy; SRE signs off on operability (runbooks, dashboards, on-call), incident response, capacity, and the three resilience checks; Each concern has a freshness window — 30d/90d/180d depending on the control; SoD on prod: the approver can't be the same person who built it — the rule that keeps the gate honest; Key takeaway: autonomy in operations, human in accountability, by design — the matrix is what makes autonomous operations safe enough to trust in production -->
---
@@ -172,7 +229,11 @@ Separation-of-duties on prod: the approver cannot be the same person who built t
- **The live ops dashboard (PowerBI)** surfaces the four CTO-grade metrics (Lead Time, Vulnerability Count, MTTR, Cloud Spend) alongside trust metrics (Decision Ledger coverage, Attestation coverage) and efficiency metrics (touchless resolution, escalation frequency)
- **Every number is traceable to a signal** — when a CFO asks "where does this number come from?", the answer is a query against the cold store, not a Slack thread
**Benefit:** the architecture is the trust substrate — leadership sees the same numbers the platform produces, in PowerBI, with full traceability. Operations become visible.
<div class="benefit">the architecture is the trust substrate — leadership sees the same numbers the platform produces, in PowerBI, with full traceability. Operations become visible.</div>
<!-- Speaker notes: The value is not the plumbing — it is that the platform's metrics surface in a tool leadership already uses (PowerBI), and every number is traceable. The live-ops dashboard is where the "infrastructure operations become visible" theme lands concretely. -->
<!-- Transition: The architecture is sound — here is the measured proof. -->
<!-- Talking points: Deliberately minimal: Nova-native CloudEvents; no Kafka/Prometheus/ClickHouse; The live-ops dashboard is built in PowerBI on top of the exported views — leadership sees the same numbers the platform produces; Every number in the Proof slides is traceable to a signal — "where does this number come from?" → a query against the cold store; This is where the "infrastructure operations become visible" theme lands concretely; Key takeaway: the architecture is the trust substrate — operations become visible in PowerBI, with full traceability -->
---
@@ -185,7 +246,11 @@ Separation-of-duties on prod: the approver cannot be the same person who built t
- **No change to production without both** — the ledger entry and the human attestation are mandatory, enforced by the pipeline, not by policy
- **Full traceability** — a production change is traceable from the contract that declared intent, through the policy scan, the confidence score, the attestation, to the applied outcome
**Benefit:** trust is provable — not a marketing claim, a queryable record. An auditor answers "who approved this, when, on what evidence?" in one query; a CTO answers "how many of last quarter's prod changes were touchless?" in one query.
<div class="benefit">trust is provable — not a marketing claim, a queryable record. An auditor answers "who approved this, when, on what evidence?" in one query; a CTO answers "how many of last quarter's prod changes were touchless?" in one query.</div>
<!-- Speaker notes: The mandatory-by-design point is the one to land. The ledger + attestation are not a best-effort feature; they are a gate. No change reaches production without both. That is what makes the 100% numbers credible — they are enforced, not aspirational. -->
<!-- Transition: Trust is provable — here is the cost side of the ROI. -->
<!-- Talking points: Both 100% — no automated decision is ever lost; no prod/dr promotion lands without a human sign-off; The mandatory-by-design point: the ledger entry + the human attestation are a gate, not a best-effort feature; Easily queried: by run, by environment, by approver, by outcome — the audit trail is a query, not a forensic exercise; Key takeaway: trust is provable — not a marketing claim, a queryable record; no change to production without both the ledger entry and the human attestation -->
---
@@ -197,9 +262,13 @@ Separation-of-duties on prod: the approver cannot be the same person who built t
- **The ROI formula:**
`Platform ROI = (FTE hours saved × blended rate + cloud savings + avoided downtime) ÷ platform op cost`
- **The four CTO-grade metrics are the ROI proof:** Lead Time (PR → Prod), Infrastructure Vulnerability Count (trend), MTTR, Cloud Spend Reduction — all flow into PowerBI
- **Honest caveat:** derived metrics are computed on internal runs today; the production-denominator activates when a pilot estate runs. The formula is grounded; the production numbers are not yet.
- **Honest caveat:** derived metrics run on internal data today; the production-denominator activates with a pilot estate.
**Benefit:** the ROI is not a black box — the formula is shown, the four metrics are committed, and the production-denominator caveat is stated up front. The CFO sees exactly what is real today and what activates with a pilot.
<div class="benefit">the ROI is not a black box — the formula is shown, the four metrics are committed, and the production-denominator caveat is stated up front. The CFO sees exactly what is real today and what activates with a pilot.</div>
<!-- Speaker notes: The formula is shown inline, not hidden. The "no fabrication" constraint in action: show the formula, show the caveat, do not pretend the production numbers exist. -->
<!-- Transition: The proof is grounded — here is what is honestly deferred, and why. -->
<!-- Talking points: The ROI formula is shown inline — not hidden in a footnote; The four CTO-grade metrics are the ROI proof — Lead Time, Vuln Count, MTTR, Cloud Spend; The N=0 caveat is stated explicitly: the formula is grounded; the production numbers activate with a pilot; Key takeaway: the ROI is not a black box — the formula is shown, the four metrics are committed, the production-denominator caveat is up front -->
---
@@ -207,7 +276,7 @@ Separation-of-duties on prod: the approver cannot be the same person who built t
**Honesty about what is not measured yet — and the blocking work for each.**
To be clear: these deferrals are *measurement infrastructure*, not the autonomy itself. The platform runs without an operator in the loop of normal operations. What is deferred is the evidence pipeline for certain metrics — not the autonomy.
These deferrals are measurement infrastructure, not the autonomy itself — the platform runs without an operator in normal operations.
| # | Deferred metric | Blocking work |
|---|-----------------|---------------|
@@ -218,7 +287,11 @@ To be clear: these deferrals are *measurement infrastructure*, not the autonomy
| 5 | Live cost reconciliation | Live AWS re-provisioning + actual-spend feed |
| 6 | Predictive vs reactive ratio | ML anomaly-forecasting service (not yet built) |
**Benefit:** the boundaries are explicit — what Nova measures today, and exactly what blocks the rest. The autonomy is real; the measurement gaps are documented with the work that unblocks each one.
<div class="benefit">the boundaries are explicit — what Nova measures today, and exactly what blocks the rest. The autonomy is real; the measurement gaps are documented with the work that unblocks each one.</div>
<!-- Speaker notes: The preempt is critical: these deferrals are measurement infrastructure, not autonomy. The platform runs without an operator in the loop. What is deferred is the evidence pipeline for live-infra health, drift, predictive remediation — not the autonomy itself. -->
<!-- Transition: The proof is honest — here is the roadmap from here to the targets. -->
<!-- Talking points: The preempt is critical: these deferrals are measurement infrastructure, not autonomy — the platform IS autonomous in operations; The blocking work is named in plain language (no decision IDs) — "live AWS re-provisioning", "drift-detection scheduler", "ML service"; Showing this to leadership demonstrates honesty, not weakness; Key takeaway: the autonomy is real; the measurement gaps are documented with the work that unblocks each one -->
---
@@ -237,7 +310,11 @@ To be clear: these deferrals are *measurement infrastructure*, not the autonomy
Re-evaluation triggers: each blocking piece of work lifts on its own schedule; the metrics layer evolves as each one lands.
**Benefit:** every deferred metric has an unblock path — nothing is hand-waved; everything has a plan and a timeframe.
<div class="benefit">every deferred metric has an unblock path — nothing is hand-waved; everything has a plan and a timeframe.</div>
<!-- Speaker notes: This is the bridge from "honestly deferred" to "here is how we get there." The roadmap uses timeframes, not status — most of it is not implemented yet, so a status column would be noise. -->
<!-- Transition: The unblock path is clear — here is the 12-month product arc. -->
<!-- Talking points: Each deferred metric has an unblock path and a timeframe — near-term, mid-term, longer-term; No status column: most of it is not implemented yet, so status would be noise; Re-evaluation triggers: each blocking piece of work lifts on its own schedule; Key takeaway: every deferred metric has a plan and a timeframe — nothing is hand-waved -->
---
@@ -254,7 +331,11 @@ Re-evaluation triggers: each blocking piece of work lifts on its own schedule; t
Grounded in the four strategic objectives (autonomy, provable trust, ROI, integration) and the deferred-metric unblock paths.
**Benefit:** the 12-month product arc — each quarter activates a strategic objective and its corresponding board-level metric, from pilot activation through integration leadership.
<div class="benefit">the 12-month product arc — each quarter activates a strategic objective and its corresponding board-level metric, from pilot activation through integration leadership.</div>
<!-- Speaker notes: The roadmap is organized by product outcome, not by technical milestone. Each quarter activates one strategic objective from the North Star. -->
<!-- Transition: Here is the quarter-by-quarter detail. -->
<!-- Talking points: This is the *product* roadmap, forward-looking only; Q1 Pilot Activation → Q2 Provable Trust → Q3 Compounding ROI → Q4 Integration & Predictive; Each quarter activates one strategic objective from the North Star; Key takeaway: the 12-month product arc — each quarter activates a strategic objective and its board-level metric -->
---
@@ -269,7 +350,11 @@ Grounded in the four strategic objectives (autonomy, provable trust, ROI, integr
**Month-18 destination:** *"Nova is the layer enterprise leadership points to when they say 'we don't have an infrastructure ops team anymore, and the audit trail is stronger than it ever was.'"*
**Benefit:** each quarter has a concrete deliverable, a target metric grounded in a strategic objective, and a path from "honestly deferred" to "shipped and measured."
<div class="benefit">each quarter has a concrete deliverable, a target metric grounded in a strategic objective, and a path from "honestly deferred" to "shipped and measured."</div>
<!-- Speaker notes: Q1Q3 are committed (grounded pipeline + known unblock paths). Q4 targets are committed-deliverable, aspirational-metric — the ML service ships, the intent-share number is a first measurement (we do not control adoption rate). -->
<!-- Transition: Production-grade guidance is how Nova helps the citizen developer's AI agent meet the bar — here is the first half. -->
<!-- Talking points: Q1: three post-pilot metrics go live (Touchless ≥99%, Escalation <0.1%, Accuracy ≥99.5%) — denominator activates with the pilot; Q2: Decision Ledger Coverage was already grounded — tamper-evidence is the Q2 upgrade (local hash-chain → Object Lock + signed checkpoints); Q3: Drift Auto-Reversal ≥95% unblocks when the drift scheduler ships; Spend Reduction ≥25% measured against the pilot baseline; Q4: Predictive:Reactive ≥3:1 requires the ML forecasting service; AI-Agent Intent Share is a first measurement (aspirational-metric); Key takeaway: each quarter has a concrete deliverable, a target metric grounded in a strategic objective, and a path from deferred to shipped -->
---
@@ -281,7 +366,11 @@ Grounded in the four strategic objectives (autonomy, provable trust, ROI, integr
- **MCP server** — a plugin-registry, stdio server exposing four tools: `lookup_principle`, `list_domains`, `matrix_lookup`, `validate_against_principles`. The developer's AI agent (or any agentic SDLC platform) calls these tools to look up the principles that apply to its submission
- **The integration point is the same regardless of source** — whether the submission comes from an AI coding agent, an agentic SDLC platform, or a traditional IDE, the same skills and MCP server apply. This is how Nova makes the citizen developer production-grade without owning the PDLC
**Benefit:** the citizen developer's AI agent is not unguided — Nova provides production-grade engineering principles as skills and as an MCP surface, so submissions arrive at the contract boundary already aligned with the platform's standards.
<div class="benefit">the citizen developer's AI agent is not unguided — Nova provides production-grade engineering principles as skills and as an MCP surface, so submissions arrive at the contract boundary already aligned with the platform's standards.</div>
<!-- Speaker notes: This is the first half of the Atelier story — the surface (skills + MCP). The next slide is what the surface catches that deterministic scanners cannot. -->
<!-- Transition: Here is what that guidance catches that deterministic scanners cannot. -->
<!-- Talking points: Nova instructs the citizen developer's AI agent via skills (markdown, keyed to engineering domains) + an MCP server (4 tools, plugin-registry, stdio); The integration point is the same regardless of source — AI agent, agentic SDLC, traditional IDE all get the same skills + MCP; This is how Nova makes the citizen developer production-grade without owning the PDLC; Key takeaway: the citizen developer's AI agent is not unguided — Nova provides engineering principles as skills + MCP -->
---
@@ -293,7 +382,11 @@ Grounded in the four strategic objectives (autonomy, provable trust, ROI, integr
- **Agentic validation, not a second policy engine** — the MCP server gives the AI agent the principles to validate against; the agent does the validation. The agent reasons about the submission against the principles, not a second static scan
- **Vendored for audit reproducibility** — Atelier is vendored at a pinned tag. A validation result is replayable against the exact principles that produced it, so an audit can reproduce a validation months later, not just trust a log line
**Benefit:** the citizen developer's submission is checked for engineering discipline, not just policy compliance — and the check is reproducible for audit. That is what makes the submission production-grade, regardless of which upstream platform produced it.
<div class="benefit">the citizen developer's submission is checked for engineering discipline, not just policy compliance — and the check is reproducible for audit. That is what makes the submission production-grade, regardless of which upstream platform produced it.</div>
<!-- Speaker notes: The value is the gap deterministic scanners leave: engineering discipline. Policy scanners catch "is this S3 bucket public?"; the MCP server catches "is this service observable if that bucket fails?". The vendoring point is audit reproducibility — the validation is not a black box. -->
<!-- Transition: You've seen the problem, the solution, and the proof. Here is the recap and the ask. -->
<!-- Talking points: The value is the gap deterministic scanners leave: engineering discipline (Wiz/Checkmarx/Mend check policy/secrets, not discipline); The MCP server catches "is this service observable?", "is this error path handled?", "is this API contract clear?"; Vendored at a pinned tag → audit reproducibility — a validation result is replayable months later; Key takeaway: submissions are checked for engineering discipline, not just policy compliance — and the check is reproducible for audit -->
---
@@ -307,9 +400,12 @@ Grounded in the four strategic objectives (autonomy, provable trust, ROI, integr
- **Proof:** 100% ledger coverage, 100% attestation coverage, grounded ROI formula, four CTO-grade metrics flowing into PowerBI
- **Roadmap:** deferred metrics have unblock paths; the 12-month product arc activates one strategic objective per quarter
**The ask:** "Approve a pilot estate to activate the production-denominator metrics (Lead Time, Vulnerability Count, MTTR, Cloud Spend), and approve the tamper-evident ledger build-out to move from the local hash-chain to S3 Object Lock + signed checkpoints. These two decisions move Nova from 'pipeline-ready' to 'production-proven.'"
**The ask:** "Approve a pilot estate to activate the production-denominator metrics (Lead Time, Vulnerability Count, MTTR, Cloud Spend). Then approve the tamper-evident ledger build-out (S3 Object Lock + signed checkpoints). Together these move Nova from 'pipeline-ready' to 'production-proven.'"
**Benefit:** a clear business decision — approve a pilot and the ledger build-out — with the confidence that every claim in this deck is grounded, derived, or honestly deferred.
<div class="benefit">a clear business decision — approve a pilot and the ledger build-out — with the confidence that every claim in this deck is grounded, derived, or honestly deferred.</div>
<!-- Speaker notes: The ask is a business decision, not insider language. "Approve a pilot estate" is a C-suite decision. "Approve the ledger build-out" is a budget decision. The recap reinforces the 4-beat arc — the audience leaves with the structure, not a pile of facts. -->
<!-- Talking points: Recap the 4-beat arc so the audience leaves with the structure; The ask is a business decision: approve a pilot estate + the tamper-evident ledger build-out; "Pipeline-ready" → "production-proven" is the value proposition; Key takeaway: approve a pilot + the ledger build-out to move from pipeline-ready to production-proven -->
---
@@ -334,4 +430,6 @@ Grounded in the four strategic objectives (autonomy, provable trust, ROI, integr
| Attestation Coverage | prod/dr attested ÷ total prod/dr | grounded |
| Policy Compliance Rate | 1 failed_assets ÷ total | grounded |
**Benefit:** a reference for every metric mentioned in the deck.
<div class="benefit">a reference for every metric mentioned in the deck.</div>
<!-- Talking points: Reference for every metric mentioned in the deck; Use if the audience asks "what does X mean?" -->
@@ -1,8 +1,9 @@
# Nova — The Autonomous Cloud Delivery Platform: Talking Points
> Step 4 of the 4-step deck process. Presenter cues distilled from the
> source of truth (`nova-autonomous-cloud-delivery.md`). 3-6 bullets per
> slide + key takeaway. Indexed by Marp slide #.
> Step 4 of the 4-step deck process. Presenter cues that mirror the
> `<!-- Talking points: -->` comments in
> `nova-autonomous-cloud-delivery-marp.md` (the sole source of truth).
> 3-6 bullets per slide + key takeaway. Indexed by Marp slide #.
> v1.21 — REQ-245
---
@@ -37,7 +38,7 @@
- **Key takeaway:** the boundaries are explicit — Nova is purpose-built for infra ops + delivery, not a general-purpose AI agent or an upstream dev platform
### Slide 5 — Scope: Downstream of PDLC
- Nova governs infra + delivery only; the PDLC (backlog, code authorship, IDE) is upstream — Nova never penetrates it
- Nova governs infra + delivery only; the PDLC (backlog, code authorship, IDE) is upstream — Nova stays downstream of it
- Integration is only through the validated contract boundary
- Any upstream source (AI agent, agentic SDLC, dev platform) produces submissions subject to the same compliance standards
- Nova validates the submission, not the author
File diff suppressed because one or more lines are too long
@@ -1,747 +0,0 @@
# Nova — The Autonomous Cloud Delivery Platform
> **Source of truth** (Step 1 of the 4-step deck process).
> Unified narrative deck. 4-beat arc: Problem → Solution → Proof →
> Roadmap + Ask. x3 structure at deck level (opening = the problem + the
> arc, body = tell them, closing = recap + ask) AND per slide (opens with
> what it covers, delivers, closes with a benefit callout written for a
> tech-leadership audience).
>
> **Honesty model:** every metric cited is grounded (cites a source),
> derived (documented formula), or deferred (cites the blocking work).
> No fabricated numbers. Internal provenance (decision IDs, requirement
> IDs, internal file paths) is kept out of the audience-facing slides —
> those live in the appendix and the `.ciagent/` files only.
>
> v1.21 — Deck Refinement & Pipeline Hardening
---
## Slide 1 — The Problem
**Product teams now own their cloud infrastructure — but ownership without
discipline is destroying value.**
The broad shift to "you build it, you run it" put Terraform into the hands
of product teams. The intention was right: teams that own their stack ship
faster. The reality is that infrastructure-as-code is a different craft
from software development, and the engineering standards that teams apply
to application code are rarely applied to the infrastructure that carries
it.
- **No lifecycle planning.** Resources are authored for creation, not for
patching, decommissioning, or rollback. When a change is needed, the
change is destructive — because no one planned the lifecycle.
- **Proactive scanning is not part of authoring.** In a year where
AI-frontier models discover and exploit zero-day vulnerabilities at a
rapid pace, teams cannot keep up by reacting. Infrastructure modules
must be scanned as code and at runtime, post-deployment — and remediated
at the pace the threat moves, not the pace a sprint allows.
- **Bandwidth gaps in infrastructure operations.** An unusual amount of
time is spent on remediation, the push for innovation does not pause,
and the result is that operational work is chronically under-resourced.
Gaps open. Detections are missed. Incidents grow.
- **Tribal knowledge and the rockstar-operator problem.** Operations
depend on a handful of administrators who hold the infrastructure in
their heads. When they leave, the knowledge leaves with them. The
platform should encode the discipline, not the person.
Every hour a developer spends writing, deploying, fixing, or remediating
infrastructure is an hour not spent releasing features to production and
generating value.
> **Benefit:** the rest of this deck shows the answer — an autonomous
> cloud delivery platform that encodes infrastructure discipline as
> policy, scans proactively, remediates rapidly, and makes operations
> visible to leadership rather than hidden in tribal knowledge.
> **Speaker notes:** Do not frame this as "humans are the problem." The
> problem is that ownership was granted without the discipline, tooling,
> and lifecycle planning that infrastructure requires. The operator is
> not the bottleneck because operators exist — the bottleneck is that
> operations depend on a few individuals instead of an encoded system.
> **Transition:** "Here is the destination Nova is building toward."
---
## Slide 2 — Nova's Vision
**Infrastructure operations become visible. Every environment provisioned,
every incident healed, every risk remediated — by an autonomous system
whose trustworthiness is provable, not promised. Human attestation remains
required at stage gates; the operator is never in the loop of normal
operations.**
- **Visibility is the recurring theme.** Security posture, remediation
velocity, reliability, and lead time are surfaced as queryable signals —
not hidden in a person's head or a Slack thread.
- **Provable, not promised.** Trust is established by deterministic
scripts that calculate a score and gate the action. The platform
functions without AI. "AI decisions" are really automated decisions.
- **Autonomy in operations, human at stage gates.** QA signs off for
production; SRE greenlights based on operational readiness. The
absence of an operator in the loop is never the absence of a record.
> **Benefit:** the destination is autonomous operations with provable
> trust — security, remediation velocity, reliability, and lead time made
> visible to leadership, not promised to them.
> **Speaker notes:** "Visible" is the operative word. The vision is not
> just that operations run without an operator — it is that operations
> become observable, queryable, and accountable. That is what makes the
> trust defensible.
> **Transition:** "The vision is ambitious — here are the strategic
> objectives that make it concrete, and the anti-goals that keep it
> focused."
---
## Slide 3 — Strategic Objectives
**4 Strategic Objectives:**
1. **Demonstrate production-grade zero-touch operations** — autonomy as
the default, not the demo. Stage-gate attestation (QA, SRE) remains
human by design.
2. **Establish provable trust in automated decisions** — deterministic
scripts calculate a score; a band outcome gates the action. The
platform functions without AI. The Decision Ledger, confidence
scoring, circuit breakers, and blast-radius controls make
"autonomous" a defensible claim, not a marketing one.
3. **Deliver compounding, quantifiable ROI** — measured on four CTO-grade
metrics, all flowing into PowerBI:
- **Lead Time** (PR → Production) — downward trend.
- **Infrastructure Vulnerability Count** — downward trend
(proactive scanning keeps up with the AI-era 0-day pace).
- **MTTR** — for platform-detected and platform-remediated incidents.
- **Cloud Spend Reduction** — on pilot estates vs. the pre-Nova
baseline.
4. **Integrate with externally owned development platforms — regardless
of source.** Nova integrates with externally owned PDLC, SDLC,
Agentic, and Citizen Developer platforms. Nova provides skills and
MCP endpoints that help the developer or AI agent make their
application production-grade. Regardless of the source, all intents
to deploy to production go through the same rigorous controls,
quality gates, attestation, and evidence stream.
> **Benefit:** the scope is explicit — Nova governs infrastructure and
> delivery, integrates with any upstream source through one validated
> contract, and measures success on four metrics a CTO can repeat back.
> **Speaker notes:** Objective #2 is the one to land carefully: trust is
> established by deterministic scoring, not by an LLM. The platform
> functions without AI.
> **Transition:** "The objectives are concrete — here is what Nova is
> NOT, to keep it focused."
---
## Slide 4 — Anti-Goals (What Nova Is NOT)
1. Not a general-purpose AI agent platform.
2. Not a system that removes humans from accountability — only from
normal operations.
3. Not an upstream development platform (no product backlogs, IDE, code
authorship).
4. Not a replacement for the Product Development Lifecycle (PDLC).
> **Benefit:** the boundaries are explicit — Nova is purpose-built for
> infrastructure operations and delivery, not a general-purpose AI agent
> or an upstream development platform.
> **Speaker notes:** Anti-goals #3 and #4 protect the scope boundary —
> Nova will not become an IDE or a product-planning tool.
> **Transition:** "The scope boundary is explicit — here is exactly
> where Nova sits relative to the product development lifecycle."
---
## Slide 5 — Scope: Downstream of PDLC
**Nova governs infrastructure and delivery. The PDLC is upstream — Nova
never penetrates it. Integration is through one validated contract.**
- **The PDLC is upstream:** product backlog, code authorship (AI agent,
IDE, agentic SDLC), sprint planning, application business logic.
- **Nova is downstream:** contract ingestion → submission-readiness gate
→ policy enforcement → cloud resource lifecycle → environment
progression (dev → qa → prod → dr) → immutable audit + attestation.
- **The integration point is one contract.** The citizen developer's AI
coding agent, an upstream agentic SDLC platform, or any development
platform may all produce submissions — the source does not matter
because all are subject to the same compliance standards.
- **Nova validates the submission, not the author.** The audit trail is
the same; the policy envelope is the same; the evidence stream is the
same.
> **Benefit:** a clean scope boundary — Nova is purpose-built for
> infrastructure operations and integrates with any upstream source
> through one validated contract, so the platform team's surface area
> stays bounded.
> **Speaker notes:** This slide protects the scope. The moment Nova
> starts owning the PDLC, it loses focus. The contract boundary is what
> keeps Nova deep on infrastructure and delivery rather than shallow on
> everything.
> **Transition:** "With the scope clear, here is who owns what across the
> delivery lifecycle."
---
## Slide 6 — RACI: Who Owns What
**Four roles, one matrix — the citizen developer owns FRs + UAT, the
platform owns NFRs + infra, quality engineering owns the gate evidence,
and SRE owns operational readiness.**
| Work Category | Citizen Dev | Platform | Quality Eng | SRE |
|---|---|---|---|---|
| Functional Requirements | **R/A** | C | I | I |
| User Acceptance Testing | **R/A** | C | I | I |
| Non-Functional Requirements | I | **R/A** | C | C |
| Infrastructure (cloud, state, IAM) | I | **R/A** | I | C |
| QA (policy, confidence, schema) | C | R | **R/A** | I |
| Production deployment to cloud | I | **R/A** | C | C |
| Quality attestation (QA sign-off) | **A** | R | **R** | I |
| Production readiness (SRE sign-off) | **A** | R | C | **R** |
**R** = Responsible · **A** = Accountable (sign-off) · **C** = Consulted · **I** = Informed.
- **Compliance-standard equivalence:** FRs + UAT may come from any
upstream source (AI agent, agentic SDLC, dev platform) — all pass the
same submission-readiness gate.
- **Production readiness is co-owned:** the platform runs the
attestations agentically; the citizen developer authorizes the
promotion at the stage gate.
> **Benefit:** every party knows what they bring, what the platform
> provides, what quality engineering guards, and where SRE signs off —
> accountability is explicit, never diffuse.
> **Speaker notes:** Quality attestation is now owned by Quality
> Engineering (not the Platform), and Production readiness is owned by
> SRE. The Platform runs the checks agentically but is never the
> Accountable party for the gate — that separation keeps the platform
> honest.
> **Transition:** "With ownership clear, here is how the pipeline
> enforces it."
---
## Slide 7 — The Platform Pipeline
**How intent becomes verified infrastructure — with fail-fast policy
scanning before the plan and runtime scanning after it.**
```mermaid
graph LR
A[Contract] --> B[Resolver]
B --> C[Adapter]
C --> D["Checkov (static code)"]
D --> E[Terraform Plan]
E --> F["Wiz (on plan)"]
F --> G[Confidence Signal]
G --> H{Stage Gate}
H -->|dev: autonomous| I[Apply]
H -->|qa/prod/dr: attested| I
I --> J[Evidence + Ledger]
```
- **Contract → resolver → adapter → Checkov on static code (before the
plan) → terraform plan → Wiz on the plan → confidence signal → stage
gate → apply → evidence + ledger.**
- **Fail-fast, quick feedback.** Checkov runs on the authored Terraform
code before `terraform plan` so developers get immediate policy
feedback, not a delayed plan-stage failure.
- **Wiz on the plan when configured; Checkov as a drop-in otherwise.**
Wiz scans the terraform plan output. When Wiz credentials are not
available, Checkov runs against the plan as a drop-in replacement. Wiz
and Checkov are never both run on the plan.
- **Dev is autonomous** (no stage gate); **qa/prod/dr require human
attestation** (QA for quality, SRE for production readiness).
> **Benefit:** the pipeline gives developers fast, deterministic feedback
> on policy at authoring time and gives the platform a runtime scan on the
> resolved plan — two layers of scanning, zero operator involvement in
> normal operations.
> **Speaker notes:** The two-stage scan is the key design: static code
> scanning catches policy violations before the cost of a plan; runtime
> plan scanning catches what the static code cannot (resolved values,
cross-resource issues). The platform picks the runtime scanner based on
configuration — never both, to avoid duplicate noise.
> **Transition:** "The pipeline produces decisions — here is how every
> decision is captured and made accountable."
---
## Slide 8 — The Decision Ledger
**Every automated decision is captured, immutable, queryable — and
accountable.**
- **What is captured:** every action the platform takes — the chosen
action, the confidence score, the alternatives considered, whether a
human overrode it, and the outcome (backfilled once the apply
completes). Every stage-gate attestation (QA sign-off, SRE
production-readiness sign-off) is captured with approver identity and
the evidence that was presented.
- **"AI decisions" are really automated decisions.** The decisions are
made by deterministic scripts that calculate a score and a band; the
platform functions without AI. The ledger captures the real decision
path — not a fabricated "AI agent." When an LLM planner is added later,
it will emit richer alternatives without breaking the schema.
- **The value is accountability, not the storage engine.** The ledger is
an append-only, tamper-evident record. The point is not which database
it lives in — the point is that every decision is queryable for
auditing, traceable to an outcome, and impossible to rewrite after the
fact.
> **Benefit:** "autonomous" is defensible because every decision the
> platform makes is immutable, queryable, and accountable — and the
> audience knows exactly what "automated" means here: deterministic
> scoring, not a black-box LLM.
> **Speaker notes:** Do not dwell on the storage substrate. The audience
> cares that the ledger is append-only, queryable, and tied to outcomes —
> not that it is a hash-chain in a SQLite file. The D-122 honesty point
> is restated without the decision ID: the platform's decisions are
> deterministic; the ledger captures that real path.
> **Transition:** "Decisions are captured — here is how stage-gate
> attestation keeps humans in accountability."
---
## Slide 9 — Attestation Matrix: QA
**The designed controls that keep humans at stage gates — QA concerns,
freshness-validated.**
| Concern | Env | Freshness | Description |
|---------|-----|-----------|-------------|
| Functional correctness | qa | 24h | The application behaves as specified; evidence accepted from the consumer's UAT. |
| Performance baseline | qa | 7d | The deployment meets its performance envelope vs. the agreed baseline. |
| Security posture | qa | 24h | The deployment's security findings have been reviewed and accepted. |
- Each concern has a freshness window — evidence older than the window
does not satisfy the gate.
- Concerns that are offline-testable run for real; concerns that require
external evidence accept signed artifacts.
> **Benefit:** QA signs off on quality before any promotion — the gate
> is explicit, not implicit.
> **Speaker notes:** The matrix is not a rubber stamp. Each concern has a
> freshness window and a plain-language description of what is being
> attested. The "operator-supplied" label from the prior deck was
> dropped — every concern now has a plain-language description.
> **Transition:** "QA is half the matrix — here are the production and
> DR controls."
---
## Slide 10 — Attestation Matrix: Prod/DR
**Production and DR controls — operational readiness, resilience, and
disaster recovery.**
| Concern | Env | Freshness | Description |
|---------|-----|-----------|-------------|
| Operational readiness | prod | 30d | SRE confirms the deployment is operable: runbooks, dashboards, on-call coverage. |
| Incident response | prod | 90d | The on-call path has been exercised; the deployment has a working incident-response plan. |
| Capacity & cost | prod | 30d | Capacity headroom and monthly cost are within the agreed envelope. |
| Resilience: DR drill | prod | 180d | A DR drill has been run and the deployment recovered within the RTO. |
| Resilience: chaos | prod | 90d | A chaos exercise has been run and the deployment absorbed the failure. |
| Resilience: backup | prod | 30d | Backups are restorable and have been tested within the freshness window. |
| DR region deploy | dr | 180d | The DR region can be deployed and the deployment is reachable from it. |
- Each concern has a freshness window — evidence older than the window
does not satisfy the gate.
- **Separation-of-duties on prod:** the approver cannot be the same
person who built the deployment.
> **Benefit:** the gate model is explicit — autonomy in operations,
> human in accountability, by design. The matrix is what makes autonomous
> operations safe enough to trust in production.
> **Speaker notes:** The prod/DR rows are the operational-readiness and
> resilience gates — SRE signs off on operability, incident response,
> capacity, and the three resilience checks (DR drill, chaos, backup).
> Separation-of-duties on prod is the rule that keeps the gate honest:
> the approver cannot be the same person who built the deployment.
> **Transition:** "You've seen how Nova works — the pipeline, the ledger,
> the attestation gates. Here is how Nova instruments itself so that
> every claim in this deck is traceable to a real signal."
---
## Slide 11 — Telemetry & Live Ops
**Every metric in this deck is traceable to a real emitted signal — and
the live-ops dashboard makes operations visible in PowerBI.**
```mermaid
graph TB
A[Platform components] --> B[CloudEvents envelope]
B --> C[Event log]
B --> D[Decision ledger]
B --> E[Run records]
C --> F[Collector]
D --> F
E --> F
F --> G[Cold store]
G --> H[PowerBI views]
H --> I[Live ops dashboard]
```
- **Platform components emit a CloudEvents envelope** → event log,
decision ledger, and run records → collector → cold store → PowerBI
views → **live ops dashboard.**
- **The live ops dashboard (PowerBI)** surfaces the four CTO-grade
metrics — Lead Time, Infrastructure Vulnerability Count, MTTR, Cloud
Spend — alongside the trust metrics (Decision Ledger coverage,
Attestation coverage) and the efficiency metrics (touchless
resolution, escalation frequency).
- **The architecture is deliberately minimal.** Nova-native envelopes;
no Kafka, no Prometheus, no ClickHouse. The cold store is sufficient
for batch and historical analysis; the live-ops surface is built in
PowerBI on top of the exported views.
- **Every number in the Proof slides is traceable to a signal.** When a
CFO asks "where does this number come from?", the answer is a query
against the cold store, not a Slack thread.
> **Benefit:** the architecture is the trust substrate — leadership sees
> the same numbers the platform produces, in PowerBI, with full
> traceability to the emitted signal. Operations become visible.
> **Speaker notes:** The value is not the plumbing — it is that the
> platform's metrics surface in a tool leadership already uses (PowerBI),
> and every number is traceable. The live-ops dashboard is where the
> "infrastructure operations become visible" theme lands concretely.
> **Transition:** "The architecture is sound — here is the measured
> proof."
---
## Slide 12 — Decision Ledger + Attestation Coverage
**By design, no change reaches production without a ledger entry and a
human attestation — both queryable for auditing, with full
traceability.**
- **Decision Ledger coverage: 100%.** Every platform run emits a
decision record with outcome backfill. No automated decision is ever
lost.
- **Attestation coverage: 100%.** Every prod/dr promotion is attested by
a human — QA for quality, SRE for production readiness — recorded with
approver identity, separation-of-duties check, and the evidence matrix.
- **No change to production without both.** The ledger entry and the
human attestation are mandatory, not optional. This is enforced by the
pipeline, not by policy.
- **Easily queried for auditing.** The ledger and the attestation
records are queryable by run, by environment, by approver, and by
outcome — the audit trail is a query, not a forensic exercise.
- **Full traceability.** A production change is traceable from the
contract that declared intent, through the policy scan, the confidence
score, the attestation, to the applied outcome. Nothing is opaque.
> **Benefit:** trust is provable — not a marketing claim, a queryable
> record. An auditor can answer "who approved this, when, on what
> evidence?" in one query; a CTO can answer "how many of last quarter's
> prod changes were touchless?" in one query.
> **Speaker notes:** The mandatory-by-design point is the one to land.
> The ledger + attestation are not a best-effort feature; they are a
> gate. No change reaches production without both. That is what makes
> the 100% numbers credible — they are enforced, not aspirational.
> **Transition:** "Trust is provable — here is the cost side of the ROI."
---
## Slide 13 — Cost & ROI
**The ROI formula and the cost estimates — grounded, with the production
denominator honestly flagged.**
- **Cost estimates are pre-apply and offline.** The platform reads the
terraform plan and estimates cost before anything is applied — so a
regression in cost is caught before the spend happens, not after.
- **The ROI formula:**
`Platform ROI = (FTE hours saved × blended rate + cloud savings + avoided downtime) ÷ platform op cost`
- **The four CTO-grade metrics (from Slide 3) are the ROI proof:**
Lead Time (PR → Prod), Infrastructure Vulnerability Count (trend), MTTR,
Cloud Spend Reduction. All flow into PowerBI.
- **Honest caveat:** the derived metrics are computed on internal runs
today; the production-denominator activates when a pilot estate runs.
The formula is grounded; the production numbers are not yet.
> **Benefit:** the ROI is not a black box — the formula is shown, the
> four metrics are committed, and the production-denominator caveat is
> stated up front. The CFO can see exactly what is real today and what
> activates with a pilot.
> **Speaker notes:** The formula is shown inline, not hidden. The
> "no fabrication" constraint in action: show the formula, show the
> caveat, do not pretend the production numbers exist.
> **Transition:** "The proof is grounded — here is what is honestly
> deferred, and why."
---
## Slide 14 — What's Deferred — and Why
**Honesty about what is not measured yet — and the blocking work for
each.**
To be clear: these deferrals are measurement infrastructure, not the
autonomy itself. The platform runs without an operator in the loop of
normal operations. What is deferred is the evidence pipeline for certain
metrics — not the autonomy.
| # | Deferred metric | Blocking work |
|---|-----------------|---------------|
| 1 | Live infrastructure health | Live AWS re-provisioning (currently torn down to a zero-cost steady state) |
| 2 | Live outbox write rate | Live AWS re-provisioning |
| 3 | Tamper-evident ledger checkpoints | Audit-ledger build-out (S3 Object Lock + signed checkpoints) |
| 4 | Onboarding funnel (requested → granted) | Auto-grant implementation |
| 5 | Drift auto-reversal | Drift-detection scheduler (not yet built) |
| 6 | Live cost reconciliation | Live AWS re-provisioning + actual-spend feed |
| 7 | SLA / unplanned downtime | Live AWS re-provisioning |
| 8 | Predictive vs reactive ratio | ML anomaly-forecasting service (not yet built) |
> **Benefit:** the boundaries are explicit — what Nova measures today,
> and exactly what blocks the rest. The autonomy is real; the measurement
> gaps are documented with the work that unblocks each one.
> **Speaker notes:** The preempt is critical: these deferrals are
> measurement infrastructure, not autonomy. The platform runs without an
> operator in the loop. What is deferred is the evidence pipeline for
> live-infra health, drift, predictive remediation — not the autonomy
> itself.
> **Transition:** "The proof is honest — here is the roadmap from here to
> the targets."
---
## Slide 15 — Roadmap to the North Star
**The path from the grounded metrics to the 1218 month targets — each
deferred metric has an unblock path and a candidate milestone.**
| Timeframe | Work | Unblocks |
|-----------|------|----------|
| Near-term | Live AWS re-provisioning | Live infra health, live outbox write rate, live cost reconciliation, SLA |
| Near-term | Auto-grant implementation | Onboarding funnel (requested → granted) |
| Mid-term | Drift-detection scheduler | Drift auto-reversal |
| Mid-term | Audit-ledger build-out (Object Lock + signed checkpoints) | Tamper-evident ledger checkpoints |
| Mid-term | Hot-path activation (live-ops dashboard goes from batch to near-real-time) | Live-ops dashboard freshness |
| Longer-term | ML anomaly-forecasting service | Predictive vs reactive ratio |
- Each deferred metric has a specific unblock requirement and a
candidate future milestone.
- Re-evaluation triggers: each blocking piece of work lifts on its own
schedule; the metrics layer evolves as each one lands.
> **Benefit:** every deferred metric has an unblock path — nothing is
> hand-waved; everything has a plan and a timeframe.
> **Speaker notes:** This is the bridge from "honestly deferred" to
> "here is how we get there." The roadmap uses timeframes, not status —
> most of it is not implemented yet, so a status column would be noise.
> **Transition:** "The unblock path is clear — here is the 12-month
> product arc."
---
## Slide 16 — 12-Month Product Roadmap
**The product arc from pilot activation to integration — four quarters,
four outcomes.**
| Quarter | Theme | Board-level outcome |
|---------|-------|---------------------|
| **Q1** | Pilot Activation | Nova runs a real customer estate end-to-end, autonomously, with a measurable zero-touch rate. |
| **Q2** | Provable Trust | Every automated decision lands in a tamper-evident ledger; the CFO sees real cloud-spend reconciliation. |
| **Q3** | Compounding ROI | Quarter-over-quarter cloud spend drops; drift is detected and reversed without a human. |
| **Q4** | Integration & Predictive | AI agents deploy through Nova by default; the ML anomaly-forecasting service goes live. |
Grounded in the four strategic objectives (autonomy, provable trust, ROI,
integration) and the deferred-metric unblock paths.
> **Benefit:** the 12-month product arc — each quarter activates a
> strategic objective and its corresponding board-level metric, from
> pilot activation through integration leadership.
> **Speaker notes:** The roadmap is organized by product outcome, not
> by technical milestone. Each quarter activates one strategic
> objective from the North Star.
> **Transition:** "Here is the quarter-by-quarter detail."
---
## Slide 17 — Quarter-by-Quarter Outcomes
| Quarter | Product theme | Key deliverable | Target metric | Grounding |
|---------|---------------|-----------------|---------------|-----------|
| **Q1** | Pilot Activation | Re-provision live AWS; activate first pilot estate; onboarding auto-grant | Touchless ≥ 99% · Escalation < 0.1% · Accuracy ≥ 99.5% | Objective #1 — autonomy as the default |
| **Q2** | Provable Trust | Tamper-evident ledger (Object Lock + signed checkpoints); daily checkpoints; live cost reconciliation | Decision Ledger Coverage 100% · Cost Savings ≥ 25% | Objective #2 — trust is the moat |
| **Q3** | Compounding ROI + Drift | Drift-detection scheduler; auto-reversal; pre-apply → actual-spend reconciliation on the pilot estate | Drift Auto-Reversal ≥ 95% · Spend Reduction ≥ 25% | Objective #3 — CFO-pointable numbers |
| **Q4** | Integration + Predictive | ML anomaly-forecasting; AI-agent intent surface; multi-cloud (Azure/GCP) preview | Predictive:Reactive ≥ 3:1 · AI-Agent Intent Share (first measurement) | Objective #4 — default substrate for agents |
**Month-18 destination:** *"Nova is the layer enterprise leadership
points to when they say 'we don't have an infrastructure ops team
anymore, and the audit trail is stronger than it ever was.'"*
> **Benefit:** each quarter has a concrete deliverable, a target metric
> grounded in a strategic objective, and a path from "honestly deferred"
> to "shipped and measured."
> **Speaker notes:** Q1Q3 are committed (grounded pipeline + known
> unblock paths). Q4 targets are committed-deliverable,
> aspirational-metric — the ML service ships, the intent-share number is
> a first measurement (we do not control adoption rate).
> **Transition:** "Production-grade guidance is how Nova helps the
> citizen developer's AI agent meet the bar — here is the first half."
---
## Slide 18 — Production-Grade Guidance via Atelier (1/2)
**Nova instructs the citizen developer's AI agent on production-grade
engineering — a set of skills and an MCP server.**
- **Skills** — markdown files keyed to production-grade engineering
domains (API, security, data, testing, observability, errors, DevOps,
infrastructure-as-code, compliance). The skills extend the baseline
catalog with Nova-specific production-grade principles.
- **MCP server** — a plugin-registry, stdio server exposing four tools:
`lookup_principle`, `list_domains`, `matrix_lookup`, and
`validate_against_principles`. The developer's AI agent (or any
agentic SDLC platform) calls these tools to look up the principles
that apply to its submission.
- **The integration point is the same regardless of source.** Whether
the submission comes from an AI coding agent, an agentic SDLC
platform, or a traditional IDE, the same skills and MCP server apply.
This is how Nova makes the citizen developer production-grade without
owning the PDLC.
> **Benefit:** the citizen developer's AI agent is not unguided — Nova
> provides production-grade engineering principles as skills and as an
> MCP surface, so submissions arrive at the contract boundary already
> aligned with the platform's standards.
> **Speaker notes:** This is the first half of the Atelier story — the
> surface (skills + MCP). The next slide is what the surface catches
> that deterministic scanners cannot.
> **Transition:** "Here is what that guidance catches that deterministic
> scanners cannot."
---
## Slide 19 — Production-Grade Guidance via Atelier (2/2)
**Agentic validation catches engineering-discipline gaps that deterministic
scanners miss — and the validation is reproducible.**
- **Beyond deterministic scanners.** Wiz, Checkmarx, and Mend check
policy and secrets — they do not check engineering discipline. The
Atelier MCP server catches correctness, clarity, and observability gaps
that deterministic tools cannot: "is this service observable?",
"is this error path handled?", "is this API contract clear?"
- **Agentic validation, not a second policy engine.** The MCP server
gives the AI agent the principles to validate against; the agent does
the validation. This is agentic validation — the agent reasons about
the submission against the principles, not a second static scan.
- **Vendored for audit reproducibility.** Atelier is vendored at a
pinned tag. A validation result is replayable against the exact
principles that produced it — so an audit can reproduce a validation
months later, not just trust a log line.
> **Benefit:** the citizen developer's submission is checked for
> engineering discipline, not just policy compliance — and the check is
> reproducible for audit. That is what makes the submission
> production-grade, regardless of which upstream platform produced it.
> **Speaker notes:** The value is the gap deterministic scanners leave:
engineering discipline. Policy scanners catch "is this S3 bucket
public?"; the MCP server catches "is this service observable if that
bucket fails?". The vendoring point is audit reproducibility — the
validation is not a black box.
> **Transition:** "You've seen the problem, the solution, and the proof.
> Here is the recap and the ask."
---
## Slide 20 — Recap + Ask
**The 4-beat recap + the business decision.**
**Recap:**
- **Problem:** product teams own infrastructure without the discipline
and lifecycle planning it requires; bandwidth gaps and tribal
knowledge leave operations exposed.
- **Solution:** autonomous cloud delivery — operations become visible,
trust is provable (deterministic scoring), humans at stage gates.
- **Proof:** 100% ledger coverage, 100% attestation coverage, grounded
ROI formula, four CTO-grade metrics flowing into PowerBI.
- **Roadmap:** deferred metrics have unblock paths; the 12-month product
arc activates one strategic objective per quarter.
**The ask:** "Approve a pilot estate to activate the production-denominator
metrics (Lead Time, Vulnerability Count, MTTR, Cloud Spend), and approve
the tamper-evident ledger build-out to move from the local hash-chain to
S3 Object Lock + signed checkpoints. These two decisions move Nova from
'pipeline-ready' to 'production-proven.'"
> **Benefit:** a clear business decision — approve a pilot and the ledger
> build-out — with the confidence that every claim in this deck is
> grounded, derived, or honestly deferred.
> **Speaker notes:** The ask is a business decision, not insider
> language. "Approve a pilot estate" is a C-suite decision. "Approve the
> ledger build-out" is a budget decision. The recap reinforces the 4-beat
> arc — the audience leaves with the structure, not a pile of facts.
---
## Appendix A1 — Metrics Glossary
| KPI | Definition | Status |
|-----|-----------|--------|
| Touchless Resolution Rate | runs without operational stage-gate block ÷ total | partial (Post-Pilot) |
| Human Escalation Frequency | operational stage-gate blocks ÷ total | partial (Post-Pilot) |
| Automated Decision Accuracy | decisions not followed by failure within 5min | partial (Post-Pilot) |
| MTTR (p95) | apply.failed → successful retry | grounded |
| Confidence-Gate Halt Rate | runs with band=block ÷ total | grounded |
| Provisioning Lead Time | run.completed run.started | grounded |
| Deployment Frequency | count(run.completed) per day | grounded |
| Cost Savings (pre-apply) | sum(delta_usd where delta < 0) | partial (live reconciliation deferred) |
| FTE Hours Saved | run count × manual baseline × rate | derived (N=0 caveat) |
| Platform ROI | (labor + cloud + avoided downtime) ÷ op cost | derived (N=0 caveat) |
| Decision Ledger Coverage | decisions with outcome ÷ total | grounded |
| Attestation Coverage | prod/dr attested ÷ total prod/dr | grounded |
| Policy Compliance Rate | 1 failed_assets ÷ total | grounded |
> **Benefit:** a reference for every metric mentioned in the deck.
---
> **End of deck.** 20 main slides + 1 appendix slide = 21 total.
+1 -1
View File
@@ -13,7 +13,7 @@ PDLC includes:
- Application business logic
- IDE workflows / developer experience
Nova never penetrates the PDLC. Nova's domain is **infrastructure +
Nova never reaches into the PDLC. Nova's domain is **infrastructure +
delivery only**. Nova integrates with externally owned PDLC, SDLC,
Agentic, and Citizen Developer platforms with no regard for the source
of the intent: Nova provides a set of skills and MCP endpoints that help
+1 -1
View File
@@ -15,7 +15,7 @@ Consumers declare intent; the platform delivers safe production deployment throu
## 3. Core Tenets
* **Operations are Declared, Not Executed.** Consumers define what they need — workload shape, dependencies, non-functional requirements, policy constraints. The platform handles reconciliation, provisioning, and environment progression. The execution burden moves from the human to the platform.
* **The Delivery Lifecycle is a Sovereign Boundary.** The platform governs the infrastructure and delivery engine. It does not penetrate upstream product or software development lifecycles. Integration happens exclusively through validated, published contracts.
* **The Delivery Lifecycle is a Sovereign Boundary.** The platform governs the infrastructure and delivery engine. It does not reach into upstream product or software development lifecycles. Integration happens exclusively through validated, published contracts.
* **Lower Environments are Autonomous; Higher Environments are Attested.** Progression through lower environments proceeds through zero-touch agentic automation. Promotion to higher-stakes environments requires deliberate human attestation — not as a rubber stamp, but as a policy-mandated act of accountability.
* **Safety is Computed, Not Assumed.** Every delivery action produces a measurable, explainable confidence signal aggregating policy conformance, validation evidence, and historical behavior. The signal is the platform's certified answer to "is this safe to proceed?" Reliance on operator instinct or tenure is not a substitute.
* **Infrastructure is Consumed, Not Maintained.** Compute is abstract, containerized, or serverless. The platform does not manage node, OS, or bare-metal lifecycles. Infrastructure is treated as a utility, not a craft.
+1
View File
@@ -16,6 +16,7 @@ test = [
"pytest-json-report>=1.5",
"moto[dynamodb]>=5.0",
]
slides = ["python-pptx>=0.6.23"]
[tool.pytest.ini_options]
testpaths = ["tests"]
+21 -5
View File
@@ -1,14 +1,26 @@
#!/usr/bin/env python3
"""scripts/attach_release_asset.py — upload a file as a Gitea release attachment.
"""scripts/attach_release_asset.py — upload one or more files as Gitea release
attachments.
REQ-228 (v1.18): PPTX (and any deck artifact) is attached to the phase's
Gitea release. Uses the Gitea API:
POST /api/v1/repos/{owner}/{repo}/releases/{id}/assets
multipart form: name=<filename>, attachment=<file bytes>
REQ-270 (v1.23): supports dual PPTX attachment the MARP PPTX (primary,
attached first) and the python-pptx PPTX (comparison artifact). Multiple
file paths are accepted; the first is the primary attachment.
Usage:
python3 scripts/attach_release_asset.py <file-path> <release-id>
python3 scripts/attach_release_asset.py <file-path> <file-path-2>... <release-id>
python3 scripts/attach_release_asset.py docs/presentations/nova-autonomous-cloud-delivery.pptx 522
python3 scripts/attach_release_asset.py \
docs/presentations/nova-autonomous-cloud-delivery.pptx \
docs/presentations/nova-autonomous-cloud-delivery-python.pptx 522
The last positional argument is always the release id; every preceding
argument is an asset path (backward compatible with the single-asset call).
Token resolution: reads NOVA_GITEA_TOKEN (or ACDL_GITEA_TOKEN) from .env.secrets
/ .env, matching the ship_phase.sh pattern. Never uses shell env tokens.
@@ -73,8 +85,12 @@ def attach_asset(file_path: str, release_id: str) -> dict:
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: attach_release_asset.py <file-path> <release-id>")
if len(sys.argv) < 3:
print("Usage: attach_release_asset.py <file-path> [<file-path-2>...] <release-id>")
sys.exit(1)
result = attach_asset(sys.argv[1], sys.argv[2])
print(f"Attached: {result.get('name')} → release {sys.argv[2]} (asset id {result.get('id')})")
asset_paths = sys.argv[1:-1]
release_id = sys.argv[-1]
for idx, path in enumerate(asset_paths):
result = attach_asset(path, release_id)
primary = " (primary)" if idx == 0 and len(asset_paths) > 1 else ""
print(f"Attached{primary}: {result.get('name')} → release {release_id} (asset id {result.get('id')})")
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""scripts/inline_images.py — base64-embed all relative-path images in an
HTML file so it becomes self-contained (redistributable without the
assets/ folder).
Usage: python scripts/inline_images.py <html-path>
Stdlib only (base64, re, mimetypes, sys, pathlib).
"""
from __future__ import annotations
import base64
import mimetypes
import re
import sys
from pathlib import Path
IMG_SRC_RE = re.compile(
r'(<img\b[^>]*\bsrc=")(assets/[^"]+)("[^>]*>)',
re.IGNORECASE,
)
def _mime_for(path: Path) -> str:
ext = path.suffix.lower()
if ext == ".svg":
return "image/svg+xml"
guessed, _ = mimetypes.guess_type(str(path))
return guessed or "application/octet-stream"
def inline(html_path: Path) -> int:
html = html_path.read_text(encoding="utf-8")
repo_root = html_path.parent.parent.parent
count = 0
def replacer(match: re.Match[str]) -> str:
nonlocal count
prefix, rel_src, suffix = match.group(1), match.group(2), match.group(3)
img_path = html_path.parent / rel_src
if not img_path.exists():
print(f" WARNING: image not found: {rel_src}", file=sys.stderr)
return match.group(0)
mime = _mime_for(img_path)
data = base64.b64encode(img_path.read_bytes()).decode("ascii")
count += 1
return f'{prefix}data:{mime};base64,{data}{suffix}'
new_html = IMG_SRC_RE.sub(replacer, html)
if count > 0:
html_path.write_text(new_html, encoding="utf-8")
return count
def main() -> int:
if len(sys.argv) != 2:
print("Usage: python scripts/inline_images.py <html-path>", file=sys.stderr)
return 1
html_path = Path(sys.argv[1])
if not html_path.exists():
print(f"ERROR: {html_path} not found", file=sys.stderr)
return 1
count = inline(html_path)
print(f"Inlined {count} image(s) into {html_path}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+688
View File
@@ -0,0 +1,688 @@
#!/usr/bin/env python3
"""scripts/render_pptx.py — render a structured, editable, S&P-themed PPTX from
the consolidated Marp markdown deck, using python-pptx.
REQ-269 (v1.23): a comparison artifact to the primary MARP-rendered PPTX. The
HTML deck remains the pixel-perfect artifact; this PPTX is the editable,
native-shape version (real text boxes, native tables, embedded PNGs) so a
reviewer can open it in PowerPoint and see a properly S&P-themed deck with
titles, bullets, blockquotes, images, tables, and benefit callouts.
Usage:
python3 scripts/render_pptx.py [deck-name]
Defaults to `nova-autonomous-cloud-delivery`. Reads
`docs/presentations/{deck}-marp.md`, writes
`docs/presentations/{deck}-python.pptx`.
"""
import os
import re
import sys
from pathlib import Path
# --- Dependency check --------------------------------------------------------
try:
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.oxml.ns import qn
except ImportError:
print("ERROR: python-pptx not installed.", file=sys.stderr)
print(" pip install -e .[slides]", file=sys.stderr)
sys.exit(1)
# --- S&P theme constants -----------------------------------------------------
RED = RGBColor(0xD6, 0x00, 0x2A) # S&P red
BLACK = RGBColor(0x1B, 0x1B, 0x1B)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
GREY_TEXT = RGBColor(0x2E, 0x2E, 0x2E)
GREY_HEADER = RGBColor(0xF0, 0xF0, 0xF0)
BODY_TEXT = RGBColor(0x1B, 0x1B, 0x1B)
FONT_NAME = "Akkurat Pro"
SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)
# Content area geometry (matches Marp padding ~48/56 px at 96dpi → ~0.5"/0.58")
MARGIN_X = Inches(0.58)
MARGIN_TOP = Inches(0.4)
CONTENT_W = Inches(12.17)
TITLE_H = Inches(0.7)
# Image fit
IMG_MAX_W = Inches(8.0)
IMG_MAX_H = Inches(4.0)
# --- Markdown parsing --------------------------------------------------------
def split_slides(md_text: str):
"""Strip YAML frontmatter, then split the deck into slide source strings."""
# Strip YAML frontmatter (between first pair of `---` lines).
if md_text.lstrip().startswith("---"):
end = md_text.find("\n---", 3)
if end != -1:
md_text = md_text[end + 4 :]
# Normalize slide separators. Marp uses `\n---\n` on its own line.
parts = re.split(r"\n---\s*\n", md_text)
slides = []
for p in parts:
p = p.strip("\n")
if p.strip():
slides.append(p)
return slides
# --- Cell/table helpers ------------------------------------------------------
def _set_cell_text(cell, text: str, *, bold: bool = False, size: int = 14,
color: RGBColor = BODY_TEXT, fill=None):
cell.text = ""
tf = cell.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
run = p.add_run()
run.text = text
run.font.name = FONT_NAME
run.font.size = Pt(size)
run.font.bold = bold
run.font.color.rgb = color
if fill is not None:
cell.fill.solid()
cell.fill.fore_color.rgb = fill
# tighten cell margins
cell.margin_left = Inches(0.06)
cell.margin_right = Inches(0.06)
cell.margin_top = Inches(0.02)
cell.margin_bottom = Inches(0.02)
def _add_red_header_bottom_border(table):
"""Add a red 2pt bottom border to the header row (row 0) cells."""
for col_idx in range(len(table.columns)):
cell = table.cell(0, col_idx)
tcPr = cell._tc.get_or_add_tcPr()
for tag in ("a:lnB",):
for old in tcPr.findall(qn(tag)):
tcPr.remove(old)
ln = tcPr.makeelement(qn("a:lnB"), {
"w": "12700", # 1pt = 12700 EMU; ~2pt
"cap": "flat",
"cmpd": "sng",
"algn": "ctr",
})
solidFill = ln.makeelement(qn("a:solidFill"), {})
srgb = solidFill.makeelement(qn("a:srgbClr"), {"val": "D6002A"})
solidFill.append(srgb)
ln.append(solidFill)
tcPr.append(ln)
# --- Slide builders ----------------------------------------------------------
def _set_bg(slide, rgb: RGBColor):
"""Solid-fill a slide background with `rgb`."""
bg = slide.background
fill = bg.fill
fill.solid()
fill.fore_color.rgb = rgb
def _add_title_bar(slide):
"""Red rectangle across the top of a content slide (subtle accent)."""
bar = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_W, Inches(0.08)
)
bar.fill.solid()
bar.fill.fore_color.rgb = RED
bar.line.fill.background()
bar.shadow.inherit = False
return bar
def _add_title_text(slide, title: str, *, color: RGBColor = RED,
size: int = 28, top: float = 0.25, bold: bool = True,
height: float = 0.7, white_bg: bool = False):
box = slide.shapes.add_textbox(MARGIN_X, Inches(top), CONTENT_W, Inches(height))
tf = box.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
run = p.add_run()
run.text = title
run.font.name = FONT_NAME
run.font.size = Pt(size)
run.font.bold = bold
run.font.color.rgb = color
return box
def _add_text_block(slide, text: str, *, top: Inches, left: Inches = None,
width: Inches = None, size: int = 18, color: RGBColor = BODY_TEXT,
bold: bool = False, italic: bool = False,
align=PP_ALIGN.LEFT, height: Inches = None):
if left is None:
left = MARGIN_X
if width is None:
width = CONTENT_W
if height is None:
height = Inches(0.4)
tb = slide.shapes.add_textbox(left, top, width, height)
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = align
run = p.add_run()
run.text = text
run.font.name = FONT_NAME
run.font.size = Pt(size)
run.font.bold = bold
run.font.italic = italic
run.font.color.rgb = color
return tb
def _add_bullets(slide, bullets, *, top: Inches, size: int = 18,
color: RGBColor = BODY_TEXT, width: Inches = None,
height: Inches = None):
if width is None:
width = CONTENT_W
if height is None:
height = Inches(0.35) * len(bullets) + Inches(0.2)
tb = slide.shapes.add_textbox(MARGIN_X, top, width, height)
tf = tb.text_frame
tf.word_wrap = True
for i, (lvl, text) in enumerate(bullets):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
p.alignment = PP_ALIGN.LEFT
p.level = lvl
run = p.add_run()
prefix = "" if lvl == 0 else (" " if lvl == 1 else "· ")
run.text = prefix + text
run.font.name = FONT_NAME
run.font.size = Pt(size if lvl == 0 else max(12, size - 2))
run.font.color.rgb = color
return tb
def _strip_inline_emphasis(text: str) -> str:
"""Strip `**bold**` and `*italic*` and `` `code` `` markers for plain runs.
We render bold via separate runs only for the **lead** paragraph; here we
collapse emphasis to plain text (the python PPTX is a comparison artifact).
"""
# `code` → plain
text = re.sub(r"`([^`]+)`", r"\1", text)
# **bold** → text
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
# *italic* → text
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"\1", text)
return text
def _inline_runs(p, text: str, *, size: int = 18, base_color: RGBColor = BODY_TEXT):
"""Add inline runs to paragraph `p`, rendering **bold** as red strong,
`code` as monospace, *italic* as italic. Other text is plain."""
# Tokenize on `**...**`, `*...*`, `` `...` ``
tokens = re.split(r"(\*\*[^*]+\*\*|`[^`]+`|\*[^*]+\*)", text)
for tok in tokens:
if not tok:
continue
if tok.startswith("**") and tok.endswith("**"):
r = p.add_run()
r.text = tok[2:-2]
r.font.name = FONT_NAME
r.font.size = Pt(size)
r.font.bold = True
r.font.color.rgb = RED
elif tok.startswith("`") and tok.endswith("`"):
r = p.add_run()
r.text = tok[1:-1]
r.font.name = "Courier New"
r.font.size = Pt(size)
r.font.color.rgb = BODY_TEXT
elif tok.startswith("*") and tok.endswith("*") and len(tok) >= 2:
r = p.add_run()
r.text = tok[1:-1]
r.font.name = FONT_NAME
r.font.size = Pt(size)
r.font.italic = True
r.font.color.rgb = base_color
else:
r = p.add_run()
r.text = tok
r.font.name = FONT_NAME
r.font.size = Pt(size)
r.font.color.rgb = base_color
def _add_picture(slide, image_path: Path, *, top: Inches, max_w: Inches = IMG_MAX_W,
max_h: Inches = IMG_MAX_H):
"""Add an image, centered horizontally, scaled to fit max_w x max_h."""
if not image_path.is_file():
# Placeholder text box if image missing
tb = slide.shapes.add_textbox(MARGIN_X, top, CONTENT_W, Inches(0.4))
tf = tb.text_frame
p = tf.paragraphs[0]
r = p.add_run()
r.text = f"[image not found: {image_path}]"
r.font.name = FONT_NAME
r.font.size = Pt(14)
r.font.color.rgb = GREY_TEXT
return tb
# native size of the picture
pic = slide.shapes.add_picture(str(image_path), MARGIN_X, top)
# scale
w = pic.width
h = pic.height
ratio = min(max_w / w, max_h / h, 1.0)
w = Emu(int(w * ratio))
h = Emu(int(h * ratio))
pic.width = w
pic.height = h
# center horizontally
pic.left = Emu(int((SLIDE_W - w) / 2))
return pic
def _add_table(slide, rows, *, top: Inches, width: Inches = None):
"""rows: list of list[str]. First row is header."""
if width is None:
width = CONTENT_W
n_rows = len(rows)
n_cols = max(len(r) for r in rows)
# pad ragged rows
rows = [r + [""] * (n_cols - len(r)) for r in rows]
# estimate height
height = Inches(0.3) * n_rows
tbl_shape = slide.shapes.add_table(n_rows, n_cols, MARGIN_X, top, width, height)
table = tbl_shape.table
# remove default banding style for a cleaner look
try:
table.first_row = False
table.horz_banding = False
except Exception:
pass
for r_idx, row in enumerate(rows):
for c_idx, val in enumerate(row):
is_header = r_idx == 0
_set_cell_text(
table.cell(r_idx, c_idx),
_strip_inline_emphasis(val),
bold=is_header,
size=13 if is_header else 12,
color=BODY_TEXT,
fill=GREY_HEADER if is_header else WHITE,
)
_add_red_header_bottom_border(table)
return tbl_shape
def _add_benefit(slide, text: str, *, top: Inches):
"""Benefit callout: a thin red top-rule rectangle, then italic text."""
rule = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE, MARGIN_X, top, Inches(6.0), Inches(0.03)
)
rule.fill.solid()
rule.fill.fore_color.rgb = RED
rule.line.fill.background()
rule.shadow.inherit = False
tb = slide.shapes.add_textbox(
MARGIN_X, top + Inches(0.08), CONTENT_W, Inches(0.6)
)
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
r = p.add_run()
r.text = text
r.font.name = FONT_NAME
r.font.size = Pt(16)
r.font.italic = True
r.font.color.rgb = BODY_TEXT
return tb
# --- Slide parse + render ----------------------------------------------------
HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$")
IMAGE_RE = re.compile(r"^!\[[^\]]*\]\(([^)\s]+)(?:\s+\"([^\"]*)\")?\)")
TABLE_SEP_RE = re.compile(r"^\|?[\s:|-]+\|?$")
def parse_slide(slide_src: str):
"""Parse a single slide's markdown into a structured dict."""
lines = slide_src.splitlines()
title = None
title_is_h1 = False
is_title_class = False
body = [] # list of ("lead", text) | ("bullet", lvl, text) | ("quote", text)
# | ("code", text) | ("image", path) | ("table", rows)
# | ("benefit", text) | ("plain", text) | ("ordered", n, text)
i = 0
while i < len(lines):
line = lines[i]
stripped = line.strip()
# HTML comments — skip, but detect Marp directives
if stripped.startswith("<!--") and stripped.endswith("-->"):
inner = stripped[4:-3].strip()
if "_class: title" in inner:
is_title_class = True
# _paginate: false / _class etc. — just skip
i += 1
continue
# multi-line HTML comments (rare in this deck)
if stripped.startswith("<!--") and "-->" not in stripped:
while i < len(lines) and "-->" not in lines[i]:
i += 1
i += 1
continue
# Heading
m = HEADING_RE.match(stripped)
if m and title is None:
level = len(m.group(1))
title = m.group(2).strip()
if level == 1:
title_is_h1 = True
i += 1
continue
if m and title is not None:
# Sub-heading inside a slide — treat as plain bold lead text.
body.append(("lead", m.group(2).strip()))
i += 1
continue
# Fenced code block
if stripped.startswith("```"):
i += 1
code_lines = []
while i < len(lines) and not lines[i].strip().startswith("```"):
code_lines.append(lines[i])
i += 1
if i < len(lines):
i += 1 # skip closing fence
body.append(("code", "\n".join(code_lines)))
continue
# Image
m = IMAGE_RE.match(stripped)
if m:
body.append(("image", m.group(1)))
i += 1
continue
# Blockquote
if stripped.startswith(">"):
quote_text = stripped[1:].strip()
# join consecutive blockquote lines
i += 1
while i < len(lines) and lines[i].strip().startswith(">"):
quote_text += " " + lines[i].strip().lstrip(">").strip()
i += 1
body.append(("quote", quote_text))
continue
# Benefit callout
bm = re.match(r"<div\s+class=\"benefit\">(.*)</div>", stripped)
if bm:
body.append(("benefit", bm.group(1).strip()))
i += 1
continue
# Table — starts with `| ... |` and the next line is a separator
if stripped.startswith("|") and i + 1 < len(lines) and TABLE_SEP_RE.match(lines[i + 1].strip()):
table_rows = []
# header
header = [c.strip() for c in stripped.strip("|").split("|")]
table_rows.append(header)
i += 2 # header + separator
while i < len(lines) and lines[i].strip().startswith("|"):
row = [c.strip() for c in lines[i].strip().strip("|").split("|")]
table_rows.append(row)
i += 1
body.append(("table", table_rows))
continue
# Ordered list item: `1. ` or `1. `
om = re.match(r"^(\d+)\.\s+(.*)", stripped)
if om:
body.append(("ordered", int(om.group(1)), om.group(2).strip()))
i += 1
continue
# Unordered list item
um = re.match(r"^(\s*)([-*+])\s+(.*)", line)
if um:
indent = len(um.group(1))
lvl = 0 if indent < 2 else (1 if indent < 4 else 2)
body.append(("bullet", lvl, um.group(3).strip()))
i += 1
continue
# Blank line
if not stripped:
i += 1
continue
# Bold lead paragraph (entire paragraph wrapped in **...**)
if stripped.startswith("**") and stripped.endswith("**") and stripped.count("**") == 2:
body.append(("lead", stripped[2:-2].strip()))
i += 1
continue
# Plain text
# collect contiguous non-empty, non-special lines into one paragraph
para_lines = [line]
i += 1
while i < len(lines):
nxt = lines[i].strip()
if (not nxt or nxt.startswith("#") or nxt.startswith("-")
or nxt.startswith("*") or nxt.startswith(">")
or nxt.startswith("|") or nxt.startswith("<")
or nxt.startswith("```") or nxt.startswith("!")
or re.match(r"^\d+\.\s", nxt)):
break
para_lines.append(lines[i])
i += 1
para_text = " ".join(l.strip() for l in para_lines).strip()
if para_text:
body.append(("plain", para_text))
continue
return {
"title": title or "(untitled)",
"title_is_h1": title_is_h1,
"is_title_class": is_title_class,
"body": body,
}
def render_title_slide(prs, slide_data):
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
_set_bg(slide, BLACK)
# red top bar
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_W, Inches(0.4))
bar.fill.solid()
bar.fill.fore_color.rgb = RED
bar.line.fill.background()
bar.shadow.inherit = False
# title
title = slide_data["title"]
tb = slide.shapes.add_textbox(MARGIN_X, Inches(2.5), CONTENT_W, Inches(2.0))
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
r = p.add_run()
r.text = title
r.font.name = FONT_NAME
r.font.size = Pt(40)
r.font.bold = True
r.font.color.rgb = WHITE
# body content (subtitle/lead/benefit) on black bg
cur_top = Inches(4.6)
for item in slide_data["body"]:
kind = item[0]
if kind == "lead":
_add_text_block(slide, _strip_inline_emphasis(item[1]),
top=cur_top, size=20, color=WHITE, bold=True,
height=Inches(0.5))
cur_top += Inches(0.55)
elif kind == "plain":
_add_text_block(slide, _strip_inline_emphasis(item[1]),
top=cur_top, size=16, color=WHITE,
height=Inches(0.4))
cur_top += Inches(0.45)
elif kind == "benefit":
# benefit on title slide: italic white
tb_b = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.8))
tf_b = tb_b.text_frame
tf_b.word_wrap = True
p_b = tf_b.paragraphs[0]
r_b = p_b.add_run()
r_b.text = item[1]
r_b.font.name = FONT_NAME
r_b.font.size = Pt(16)
r_b.font.italic = True
r_b.font.color.rgb = WHITE
cur_top += Inches(0.85)
def render_content_slide(prs, slide_data, deck_dir: Path):
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
_set_bg(slide, WHITE)
_add_title_bar(slide)
_add_title_text(slide, slide_data["title"], color=RED, size=28, top=0.25,
bold=True, height=0.7)
cur_top = Inches(1.05)
for item in slide_data["body"]:
kind = item[0]
if kind == "lead":
tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.5))
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
_inline_runs(p, item[1], size=18, base_color=RED)
# make the whole lead bold-strong-red
for r in p.runs:
r.font.bold = True
r.font.color.rgb = RED
cur_top += Inches(0.5)
elif kind == "plain":
tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.4))
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
_inline_runs(p, item[1], size=18, base_color=BODY_TEXT)
cur_top += Inches(0.4)
elif kind == "quote":
tb = slide.shapes.add_textbox(
MARGIN_X + Inches(0.3), cur_top, CONTENT_W - Inches(0.3), Inches(0.6)
)
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
_inline_runs(p, item[1], size=18, base_color=GREY_TEXT)
# italicize the whole blockquote
for r in p.runs:
r.font.italic = True
r.font.color.rgb = GREY_TEXT
cur_top += Inches(0.6)
elif kind == "bullet":
# accumulate consecutive bullets into one text frame
# (handled below in a second pass; we render single here as fallback)
tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.35))
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
p.level = item[1]
r = p.add_run()
prefix = "" if item[1] == 0 else (" " if item[1] == 1 else "· ")
r.text = prefix + _strip_inline_emphasis(item[2])
r.font.name = FONT_NAME
r.font.size = Pt(18 if item[1] == 0 else 16)
r.font.color.rgb = BODY_TEXT
cur_top += Inches(0.35)
elif kind == "ordered":
tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.35))
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
r = p.add_run()
r.text = f"{item[1]}. " + _strip_inline_emphasis(item[2])
r.font.name = FONT_NAME
r.font.size = Pt(18)
r.font.color.rgb = BODY_TEXT
cur_top += Inches(0.35)
elif kind == "image":
img_path = deck_dir / item[1]
_add_picture(slide, img_path, top=cur_top)
cur_top += Inches(4.1)
elif kind == "table":
rows = item[1]
_add_table(slide, rows, top=cur_top)
cur_top += Inches(0.32) * len(rows) + Inches(0.1)
elif kind == "code":
tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.6))
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
r = p.add_run()
r.text = item[1]
r.font.name = "Courier New"
r.font.size = Pt(14)
r.font.color.rgb = BLACK
cur_top += Inches(0.5)
elif kind == "benefit":
_add_benefit(slide, item[1], top=cur_top)
cur_top += Inches(0.75)
def render_deck(md_path: Path, pptx_path: Path):
md_text = md_path.read_text(encoding="utf-8")
slide_sources = split_slides(md_text)
prs = Presentation()
prs.slide_width = SLIDE_W
prs.slide_height = SLIDE_H
deck_dir = md_path.parent
print(f"Parsing {len(slide_sources)} slides from {md_path}")
for idx, src in enumerate(slide_sources):
data = parse_slide(src)
is_title = (idx == 0) or data["is_title_class"] or data["title_is_h1"]
# The appendix is a content slide (rendered normally)
if idx == 0 and (data["title_is_h1"] or data["is_title_class"]):
render_title_slide(prs, data)
elif data["is_title_class"] and not data["title_is_h1"] and idx != 0:
# Marp _class: title on a non-H1 slide (e.g., appendix) — render as
# content but with a title-style bar. Keep it simple: content slide.
render_content_slide(prs, data, deck_dir)
else:
render_content_slide(prs, data, deck_dir)
print(f" [{idx + 1:02d}] {data['title']} (body: {len(data['body'])} blocks)")
pptx_path.parent.mkdir(parents=True, exist_ok=True)
prs.save(str(pptx_path))
print(f"Saved: {pptx_path} ({len(prs.slides)} slides)")
def main():
deck = sys.argv[1] if len(sys.argv) > 1 else "nova-autonomous-cloud-delivery"
repo_root = Path(__file__).resolve().parent.parent
md_path = repo_root / "docs" / "presentations" / f"{deck}-marp.md"
pptx_path = repo_root / "docs" / "presentations" / f"{deck}-python.pptx"
if not md_path.is_file():
print(f"ERROR: source deck not found: {md_path}", file=sys.stderr)
sys.exit(1)
render_deck(md_path, pptx_path)
if __name__ == "__main__":
main()
+25 -11
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# scripts/render_slides.sh — render the Nova presentation deck end-to-end:
# 1. Mermaid .mmd → .png (S&P-themed via sp-theme.json)
# 2. Marp .md → .html + .pptx (S&P-themed via nova-sp-theme.css)
# 3. Stage all rendered artifacts to git.
# 2. Marp .md → .html + .pptx (S&P-themed via inline style: block in frontmatter)
# 3. Inline images → base64-embed all images in the HTML (self-contained).
# 4. Stage all rendered artifacts to git.
#
# Usage:
# bash scripts/render_slides.sh [deck-name]
@@ -17,14 +18,12 @@ cd "$(git rev-parse --show-toplevel)"
MMD_DIR="docs/presentations/assets/mmd"
PNG_DIR="docs/presentations/assets/png"
THEME_JSON="$MMD_DIR/sp-theme.json"
THEME_CSS="docs/presentations/assets/nova-sp-theme.css"
PUPPETEER_CFG="docs/presentations/assets/puppeteer-config.json"
SRC="docs/presentations/${DECK}-marp.md"
HTML="docs/presentations/${DECK}.html"
PPTX="docs/presentations/${DECK}.pptx"
[ -f "$SRC" ] || { echo "ERROR: source deck $SRC not found" >&2; exit 1; }
[ -f "$THEME_CSS" ] || { echo "ERROR: theme CSS $THEME_CSS not found" >&2; exit 1; }
# --- Chrome / Chromium discovery ---
CHROME=""
@@ -65,15 +64,30 @@ echo ""
# --- Step 2: render Marp deck (S&P-themed) ---
# REQ-257: pinned marp-cli version (v4.5.0) to prevent boilerplate-CSS drift.
echo "=== Step 2: Rendering Marp deck → HTML + PPTX ==="
echo " Theme: $THEME_CSS"
echo " Theme: default (inline style)"
echo " HTML → $HTML"
npx --yes @marp-team/marp-cli@4.5.0 --allow-local-files --theme "$THEME_CSS" "$SRC" -o "$HTML" 2>&1 | tail -3
npx --yes @marp-team/marp-cli@4.5.0 --allow-local-files "$SRC" -o "$HTML" 2>&1 | tail -3
echo " PPTX → $PPTX"
npx --yes @marp-team/marp-cli@4.5.0 --allow-local-files --theme "$THEME_CSS" "$SRC" -o "$PPTX" 2>&1 | tail -3
npx --yes @marp-team/marp-cli@4.5.0 --allow-local-files "$SRC" -o "$PPTX" 2>&1 | tail -3
echo ""
# --- Step 3: stage ---
echo "=== Step 3: Staging rendered artifacts ==="
git add "$PNG_DIR"/*.png "$HTML" "$PPTX" 2>/dev/null || true
echo "=== Done: staged $(ls "$PNG_DIR"/*.png 2>/dev/null | wc -l) PNGs + $HTML + $PPTX ==="
# --- Step 3: inline images into HTML (base64-embed for redistribution) ---
# REQ-268: makes the HTML self-contained (no assets/ folder needed).
echo "=== Step 3: Inlining images into HTML ==="
python3 scripts/inline_images.py "$HTML" 2>&1
echo ""
# --- Step 4: render python-pptx (structured, editable, S&P-themed) ---
# REQ-269: python-pptx produces a structured, editable PPTX (native text boxes,
# tables, images) alongside the MARP-rendered PPTX.
echo "=== Step 4: Rendering python-pptx deck ==="
PYTHON_PPTX="docs/presentations/${DECK}-python.pptx"
python3 scripts/render_pptx.py "$DECK" 2>&1
echo " Python PPTX → $PYTHON_PPTX"
echo ""
# --- Step 5: stage ---
echo "=== Step 5: Staging rendered artifacts ==="
git add "$PNG_DIR"/*.png "$HTML" "$PPTX" "$PYTHON_PPTX" 2>/dev/null || true
echo "=== Done: staged $(ls "$PNG_DIR"/*.png 2>/dev/null | wc -l) PNGs + $HTML + $PPTX + $PYTHON_PPTX ==="
+86
View File
@@ -215,6 +215,7 @@ stream() {
}
CONTRACT_ID="${NOVA_CONTRACT_ID:-11111111-1111-1111-1111-111111111111}" # spike UUID (override via NOVA_CONTRACT_ID)
CONSUMER_REPO="${NOVA_CONSUMER_REPO:-${GITHUB_REPOSITORY:-unknown}}" # v1.24 (REQ-284/285): for env-transition detect/record
WORK="${NOVA_WORK_DIR:-/tmp/nova_platform_run}"
TF_DIR="$WORK/tf"
rm -rf "$WORK"; mkdir -p "$TF_DIR"
@@ -238,6 +239,82 @@ else
}
fi
# v1.24 (REQ-284): Step 0b — environment-transition check.
# Detect if the contract's environment changed on a known contract.id
# (Shape A promotion). If so, destroy the prior env's resources before
# building the new env. No orphan path — fail closed if destroy fails.
# Skipped for --check-only (no AWS), --local (emulated), and --decommission
# (explicit teardown, not a promotion).
if [ "$CHECK_ONLY" = "0" ] && [ "$LOCAL_TIER" = "0" ] && [ "$DECOMMISSION" = "0" ]; then
RESOLVED_ENV_FOR_DETECT=$(python3 -c "import yaml; print(yaml.safe_load(open('$CONTRACT')).get('environment','dev'))" 2>/dev/null || echo "dev")
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
RESOLVED_ENV_FOR_DETECT="$ENVIRONMENT_OVERRIDE"
fi
echo ""
echo "=== Step 0b: environment-transition check ==="
echo "consumer_repo=$CONSUMER_REPO contract_id=$CONTRACT_ID new_env=$RESOLVED_ENV_FOR_DETECT"
PRIOR_ENV=$(python3 core/env_transition.py detect \
--contract-id "$CONTRACT_ID" \
--consumer-repo "$CONSUMER_REPO" \
--new-env "$RESOLVED_ENV_FOR_DETECT" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin).get('prior_env') or '')" 2>/dev/null || echo "")
if [ -n "$PRIOR_ENV" ]; then
echo "ENV TRANSITION DETECTED: $PRIOR_ENV -> $RESOLVED_ENV_FOR_DETECT"
echo "Destroying prior env '$PRIOR_ENV' resources before building new env (no orphan path)..."
# Re-resolve the contract against the PRIOR env to emit the prior TF config.
# Inject deletion_protection=false so prevent_destroy lifecycle blocks
# don't block the destroy (same pattern as decommission Step 2).
python3 -c "
import json, sys, yaml, copy
sys.path.insert(0, '$ROOT')
from core.contract_resolver import resolve
contract = yaml.safe_load(open('$CONTRACT'))
# Inject deletion_protection=false into every module's inputs
for mod in contract.get('infrastructure', {}).values():
mod.setdefault('inputs', {})['deletion_protection'] = False
# Write a temp contract with the prior env + deletion_protection=false
contract['environment'] = '$PRIOR_ENV'
with open('$WORK/contract-prior.yml', 'w') as f:
yaml.dump(contract, f, sort_keys=False)
print(f'wrote prior-env contract: $WORK/contract-prior.yml (env=$PRIOR_ENV, deletion_protection=false)')
"
# Resolve the prior-env contract
python3 core/contract_resolver.py "$WORK/contract-prior.yml" "$WORK/stack-prior.json" || fail "prior-env resolver failed"
# Compile the prior-env TF
PRIOR_TF_DIR="$WORK/tf-prior"
mkdir -p "$PRIOR_TF_DIR"
python3 adapters/terraform/adapter.py "$WORK/stack-prior.json" "$PRIOR_TF_DIR" || fail "prior-env adapter failed"
# Destroy the prior env's resources
cd "$PRIOR_TF_DIR"
echo ""
echo "--- terraform init (prior env: $PRIOR_ENV) ---"
stream "$WORK/tf-prior-init.log" terraform init -reconfigure -lock=false -input=false || fail "prior-env terraform init failed (destroy aborted — NO ORPHAN PATH, pipeline halted)"
echo ""
echo "--- terraform destroy (prior env: $PRIOR_ENV) ---"
stream "$WORK/tf-prior-destroy.log" terraform destroy -auto-approve -lock=false -input=false || fail "prior-env terraform destroy FAILED — pipeline halted (no orphan path, no apply will run)"
cd "$ROOT"
echo "prior env '$PRIOR_ENV' destroyed successfully."
# Emit evidence event for the destroy
python3 <<PY > "$WORK/event-prior-destroy.json" 2>/dev/null || true
import json, datetime
event = {
"contractId": "$CONTRACT_ID",
"eventType": "ENV_DESTROYED",
"ts": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"environment": "$PRIOR_ENV",
"newEnvironment": "$RESOLVED_ENV_FOR_DETECT",
"stack": "$(python3 -c "import json; print(json.load(open('$WORK/stack-prior.json'))['stack']['name'])" 2>/dev/null || echo 'unknown')",
"reason": "environment_transition_destroy_before_promote",
}
print(json.dumps(event, indent=2))
PY
if [ -f "$WORK/event-prior-destroy.json" ]; then
python3 core/outbox_writer.py "$WORK/event-prior-destroy.json" > "$WORK/outbox-prior-destroy.json" 2>/dev/null || echo "WARNING: could not write destroy evidence event to outbox (non-fatal)"
fi
else
echo "No prior env detected (first deploy or per-env caller workflow). Proceeding normally."
fi
fi
echo "=== Step 1: validate contract against contract.schema.json ==="
[ -f "$CONTRACT" ] || fail "contract file $CONTRACT missing"
python3 -c "
@@ -368,6 +445,10 @@ if [ "$APPLY_ONLY" = "1" ]; then
echo "--- terraform outputs ---"
terraform output -json 2>/dev/null || true
cd "$ROOT"
# v1.24 (REQ-285): record the applied env so future runs can detect transitions.
if [ -n "$RESOLVED_ENV" ]; then
python3 core/env_transition.py record --contract-id "$CONTRACT_ID" --consumer-repo "$CONSUMER_REPO" --env "$RESOLVED_ENV" 2>/dev/null || true
fi
echo ""
echo "=== PLATFORM APPLY OK ==="
exit 0
@@ -523,6 +604,11 @@ echo ""
# G-112: sourced (shared env) — the block references CONTRACT/WORK/DEPLOY_UPTIME.
source "$ROOT/scripts/run_uptime.sh"
# v1.24 (REQ-285): record the applied env so future runs can detect transitions.
if [ -n "$RESOLVED_ENV" ]; then
python3 core/env_transition.py record --contract-id "$CONTRACT_ID" --consumer-repo "$CONSUMER_REPO" --env "$RESOLVED_ENV" 2>/dev/null || true
fi
echo ""
echo "=== PLATFORM E2E OK ==="
echo "contract -> resolver -> stack -> Checkov(static) -> terraform plan -> Wiz-or-Checkov(plan) -> confidence ($BAND) -> outbox -> outputs"
+31 -3
View File
@@ -1,4 +1,10 @@
"""REQ-106: consumer guide documents per-env caller workflows."""
"""REQ-106 + REQ-290: consumer guide documents both promotion shapes.
Shape A (Step 8): edit the environment field platform destroys the prior
env before building the new env (no orphan path).
Shape B (Per-environment deployment): per-env caller workflows, no field
editing, promotion = running the matching job.
"""
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
@@ -43,6 +49,28 @@ def test_consumer_guide_has_interpolation_reference():
assert "${contract.module}" not in text
def test_consumer_guide_states_no_field_editing():
def test_consumer_guide_documents_both_promotion_shapes():
"""REQ-290: the guide documents both Shape A (edit + destroy) and
Shape B (per-env caller workflows). Replaces the old
test_consumer_guide_states_no_field_editing which asserted only
Shape B."""
text = GUIDE.read_text()
assert "no" in text.lower() and "environment" in text.lower() and "editing" in text.lower()
# Shape B: per-env caller workflows, no field editing
assert "Per-environment deployment" in text
assert "promotion-without-editing" in text.lower() or "promotion = running the matching job" in text.lower()
# Shape A: edit environment field (Step 8 documents this as a valid path)
assert "Shape A" in text or "Shape B" in text
assert "edit the environment field" in text.lower() or "change `environment`" in text.lower() or "change \"environment\"" in text.lower()
def test_consumer_guide_documents_destroy_on_env_change():
"""REQ-290: the guide states the platform destroys the prior env's
resources when the environment field is changed, and that there is no
orphan path."""
text = GUIDE.read_text()
text_lower = text.lower()
# The guide must state the platform destroys the prior environment
assert "destroy" in text_lower and ("prior environment" in text_lower or "prior env" in text_lower)
# The guide must state there is no orphan path / fail closed
assert "no orphan path" in text_lower or "orphan" in text_lower
assert "fail closed" in text_lower or "fails closed" in text_lower
+145
View File
@@ -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
View File
@@ -1,21 +1,28 @@
"""REQ-239..243 (v1.20) + REQ-245,251,252 (v1.21): S&P theme + slide render
pipeline + deck-refinement tests.
"""REQ-239..243 (v1.20) + REQ-245,251,252 (v1.21/22) + REQ-273..275 (v1.23):
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):
v1.21/22 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)
- 20 main + 1 appendix 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
from pathlib import Path
@@ -29,50 +36,216 @@ 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"
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"
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():
"""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}"
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():
"""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()
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."""
"""The mermaid theme JSON has S&P colors (mermaid PNGs are S&P-themed)."""
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."""
# --- Marp deck: theme + inline style ----------------------------------
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()
# 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"
assert "badge" not in text, "Marp deck still contains badge spans"
def test_marp_deck_not_using_default_theme():
"""The Marp deck must not use 'theme: default'."""
def test_marp_deck_no_version_in_footer():
"""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()
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"
after_fm = text.split("---\n", 2)[2] if text.startswith("---") else text
first_slide = after_fm.split("\n---\n")[0]
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'"
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():
"""REQ-240: render_slides.sh exists and is executable."""
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"
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():
"""REQ-241: CI workflow for slides exists."""
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"
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():
"""REQ-240: every .mmd file has a corresponding .png."""
mmd_dir = ASSETS / "mmd"
@@ -134,6 +382,82 @@ def test_every_mmd_has_png():
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():
"""REQ-243: presentations README does not list retired decks."""
readme = (PRESENTATIONS / "README.md").read_text()
@@ -153,221 +477,4 @@ def test_readme_no_old_deck_name():
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: ![...](assets/png/X.png)
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)"
assert not old_files, f"old deck files still present: {old_files}"
+18 -6
View File
@@ -1,11 +1,15 @@
# 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
on:
push:
paths:
- 'docs/presentations/**'
- 'scripts/render_slides.sh'
- 'assets/nova-sp-theme.css'
- 'scripts/inline_images.py'
- 'scripts/render_pptx.py'
- 'pyproject.toml'
workflow_dispatch:
jobs:
@@ -16,16 +20,24 @@ jobs:
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
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: |
npx --yes @marp-team/marp-cli@latest --version
npx --yes @mermaid-js/mermaid-cli --version
npx --yes @marp-team/marp-cli@4.5.0 --version
npx --yes @mermaid-js/mermaid-cli@11.16.0 --version
- name: Render slides
run: bash scripts/render_slides.sh
- name: Commit rendered artifacts
run: |
git config user.name "nova-slides-bot"
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 push
git push