Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d069654367 | |||
| 25427250ad | |||
| eca1181716 | |||
| 0920550ae5 | |||
| d8240588c9 | |||
| 5dc97673e5 | |||
| 956cf91ce0 | |||
| a7a93d95d1 | |||
| afca994511 | |||
| e63c0cb36e | |||
| 3512261051 | |||
| 14c11027a8 | |||
| e07a210c70 | |||
| 9b8ab75b85 | |||
| 9bc37301ba | |||
| 5476f8eb24 | |||
| 863f482f9c | |||
| 66b13a6d0c | |||
| 485d105bcd | |||
| df426afd6a | |||
| 9114227ef1 | |||
| c9ace0af6e | |||
| a47c16245a | |||
| 74e9d4d887 | |||
| 818e285fac |
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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 P0–P-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 P0–P6).
|
||||
|
||||
### 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
@@ -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 (P1–P4) → 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).
|
||||
- `` → `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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -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 4–11; 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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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 -> PolicyCheckResult)"] --> S6
|
||||
S6["confidence<br/>score + band (dev >= 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
@@ -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 `` (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 ``.
|
||||
- **`<!-- _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 `` (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 `` (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
|
||||
|
||||

|
||||
|
||||
- **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: Q1–Q3 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?" -->
|
||||
Binary file not shown.
@@ -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 12–18 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:** Q1–Q3 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.
|
||||
Binary file not shown.
+1
-1
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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')})")
|
||||
@@ -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())
|
||||
@@ -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
@@ -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 ==="
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,145 @@
|
||||
"""REQ-274: tests for `scripts/render_pptx.py` — the structured, editable
|
||||
python-pptx deck produced alongside the MARP-rendered PPTX.
|
||||
|
||||
The python-pptx deck is a native OOXML presentation: real text boxes,
|
||||
native tables, embedded pictures, and italic benefit callouts. These
|
||||
tests are offline (no AWS, no network) and assert the structural
|
||||
properties of the committed `*-python.pptx` artifact.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
from pptx import Presentation
|
||||
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
||||
from pptx.presentation import Presentation as PresentationT
|
||||
from pptx.shapes.autoshape import Shape
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
PRESENTATIONS = ROOT / "docs" / "presentations"
|
||||
MARPT_DECK = PRESENTATIONS / "nova-autonomous-cloud-delivery-marp.md"
|
||||
PYTHON_PPTX = PRESENTATIONS / "nova-autonomous-cloud-delivery-python.pptx"
|
||||
|
||||
# Title slide + 20 main slides + 1 appendix slide.
|
||||
EXPECTED_SLIDE_COUNT = 22
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def prs() -> PresentationT:
|
||||
"""Load the committed python-pptx deck once for the whole module."""
|
||||
assert PYTHON_PPTX.is_file(), f"python-pptx PPTX not found: {PYTHON_PPTX}"
|
||||
return Presentation(str(PYTHON_PPTX))
|
||||
|
||||
|
||||
def _slide_titles(prs: PresentationT) -> list[str]:
|
||||
"""Return the first non-empty text-frame line per slide (the title)."""
|
||||
titles: list[str] = []
|
||||
for slide in prs.slides:
|
||||
for shape in slide.shapes:
|
||||
if not shape.has_text_frame:
|
||||
continue
|
||||
text = cast(Shape, shape).text_frame.text.strip()
|
||||
if not text:
|
||||
continue
|
||||
# The title is the first non-empty line of the first non-empty
|
||||
# text frame we find on the slide.
|
||||
first_line = text.split("\n")[0].strip()
|
||||
if first_line:
|
||||
titles.append(first_line)
|
||||
break
|
||||
else:
|
||||
titles.append("")
|
||||
return titles
|
||||
|
||||
|
||||
def test_slide_count(prs: PresentationT):
|
||||
"""REQ-269/274: the python-pptx deck has 22 slides
|
||||
(title + 20 main + 1 appendix)."""
|
||||
assert len(prs.slides) == EXPECTED_SLIDE_COUNT, \
|
||||
f"expected {EXPECTED_SLIDE_COUNT} slides, got {len(prs.slides)}"
|
||||
|
||||
|
||||
def test_title_slide_colors(prs: PresentationT):
|
||||
"""REQ-269: the title slide (slide 0) has a solid-filled background
|
||||
shape carrying the S&P Red (#D6002A) brand color (the title slide is
|
||||
a red-bar-on-black layout)."""
|
||||
title_slide = prs.slides[0]
|
||||
red_found = False
|
||||
black_found = False
|
||||
for shape in title_slide.shapes:
|
||||
fill = getattr(shape, "fill", None)
|
||||
if fill is None:
|
||||
continue
|
||||
try:
|
||||
if fill.type != 1: # MSO_FILL.SOLID
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
rgb = str(fill.fore_color.rgb).upper()
|
||||
if rgb == "D6002A":
|
||||
red_found = True
|
||||
if rgb == "1B1B1B":
|
||||
black_found = True
|
||||
assert red_found, \
|
||||
"title slide has no solid-fill shape with S&P Red (#D6002A)"
|
||||
|
||||
|
||||
def test_expected_slide_titles(prs: PresentationT):
|
||||
"""REQ-269/274: spot-check that key slide titles match the markdown
|
||||
deck (The Problem, Nova's Vision, Recap + Ask)."""
|
||||
titles = _slide_titles(prs)
|
||||
# Build a flat lowercase concatenation for substring checks.
|
||||
flat = " | ".join(titles).lower()
|
||||
expected = [
|
||||
"the problem",
|
||||
"nova's vision",
|
||||
"recap + ask",
|
||||
]
|
||||
missing = [t for t in expected if t not in flat]
|
||||
assert not missing, \
|
||||
f"missing expected slide titles in python-pptx deck: {missing}; " \
|
||||
f"found titles: {titles}"
|
||||
|
||||
|
||||
def test_table_rendering(prs: PresentationT):
|
||||
"""REQ-269: a slide with a table (the RACI slide) has a native PPTX
|
||||
table shape (GraphicFrame with has_table=True)."""
|
||||
table_slides = []
|
||||
for idx, slide in enumerate(prs.slides):
|
||||
for shape in slide.shapes:
|
||||
if shape.shape_type == MSO_SHAPE_TYPE.TABLE or getattr(
|
||||
shape, "has_table", False
|
||||
):
|
||||
table_slides.append(idx)
|
||||
break
|
||||
assert table_slides, \
|
||||
"no slide in the python-pptx deck has a native PPTX table shape"
|
||||
|
||||
|
||||
def test_image_embedding(prs: PresentationT):
|
||||
"""REQ-269: a slide with an image (the Platform Pipeline slide)
|
||||
has a native PPTX picture shape."""
|
||||
picture_slides = []
|
||||
for idx, slide in enumerate(prs.slides):
|
||||
for shape in slide.shapes:
|
||||
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
|
||||
picture_slides.append(idx)
|
||||
break
|
||||
assert picture_slides, \
|
||||
"no slide in the python-pptx deck has a native PPTX picture shape"
|
||||
|
||||
|
||||
def test_benefit_callout_present(prs: PresentationT):
|
||||
"""REQ-269/274: at least one slide has an italic text run (the
|
||||
benefit callout, rendered as italic body text by render_pptx.py)."""
|
||||
italic_runs = 0
|
||||
for slide in prs.slides:
|
||||
for shape in slide.shapes:
|
||||
if not shape.has_text_frame:
|
||||
continue
|
||||
for paragraph in cast(Shape, shape).text_frame.paragraphs:
|
||||
for run in paragraph.runs:
|
||||
if run.font.italic and run.text.strip():
|
||||
italic_runs += 1
|
||||
assert italic_runs > 0, \
|
||||
"no italic text runs found in the python-pptx deck (benefit callout)"
|
||||
+350
-243
@@ -1,21 +1,28 @@
|
||||
"""REQ-239..243 (v1.20) + REQ-245,251,252 (v1.21): S&P theme + slide render
|
||||
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: 
|
||||
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}"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user