From d14f9289dabf3edb0bbb7edca6f2c97a71a70b8d Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Thu, 30 Jul 2026 02:03:16 +0000 Subject: [PATCH 1/4] =?UTF-8?q?fix(P5):=20review=20P0=20=E2=80=94=20remove?= =?UTF-8?q?=20duplicate=20delenv=20in=20attestation=20signature-skip=20tes?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review (correctness lens) found a P0 in tests/test_attestation_matrix.py introduced by the P5 fallback-removal pass: the dual-read delenv of ACDL_ATTESTATION_SIGNING_KEY_ID was replaced with a second (duplicate) delenv of NOVA_ATTESTATION_SIGNING_KEY_ID, leaving the test misleading (comment claimed 'both NOVA_* and ACDL_* must be unset' while only NOVA_* was deleted twice) and the ACDL_* var no longer cleaned. With P5 having removed the ACDL_* fallback from core/env.py, deleting NOVA_* alone is the correct and sufficient precondition for the skip; this commit drops the duplicate line and updates the comment to match the NOVA-only contract. ---ci--- project: acdl phase: 5 milestone: v1.15 status: verify lessons: - P0 fix applied: duplicate monkeypatch.delenv('NOVA_ATTESTATION_SIGNING_KEY_ID') in test_signature_skip_when_key_unset left the test misleading and the ACDL_* var uncleaned; collapsed to a single NOVA_* delenv consistent with the P5 NOVA-only core/env.py. ---/ci--- --- tests/test_attestation_matrix.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_attestation_matrix.py b/tests/test_attestation_matrix.py index 983d48e..35c7101 100644 --- a/tests/test_attestation_matrix.py +++ b/tests/test_attestation_matrix.py @@ -102,9 +102,10 @@ def test_dr_blocks_on_missing_dr_drill(): def test_signature_skip_when_key_unset(monkeypatch, capsys): """D-089: signature verification is skipped when the signing key is unset.""" - # P2: dual-read — both NOVA_* and ACDL_* must be unset for the skip. + # P5 (REQ-164): dual-read fallback removed — NOVA_* only. Deleting + # NOVA_ATTESTATION_SIGNING_KEY_ID is sufficient for the skip (the + # ACDL_* fallback no longer exists in core/env.py). monkeypatch.delenv("NOVA_ATTESTATION_SIGNING_KEY_ID", raising=False) - monkeypatch.delenv("ACDL_ATTESTATION_SIGNING_KEY_ID", raising=False) artifact = {"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), "type": "x", "payload": {}, "signature": "sig"} assert _verify_signature(artifact) is True From 13846d553a72e6807d755be1253b6740937705f8 Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Thu, 30 Jul 2026 02:05:57 +0000 Subject: [PATCH 2/4] =?UTF-8?q?fix(P5):=20review=20P0=20=E2=80=94=20collap?= =?UTF-8?q?se=20duplicate=20NOVA=5F*=20delenv=20in=20route-halt=20+=20adap?= =?UTF-8?q?ter=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review (correctness lens) found the same P5 mechanical-edit defect in two more test files: the ACDL_* fallback delenv was replaced with a duplicate NOVA_* delenv (leaving a dead duplicate line, a stale 'ACDL_* fallback until P5' comment, and the ACDL_* var no longer cleaned). - tests/test_route_halt_artifact.py: two sites (stderr-fallback + outbox-fallback) each deleted NOVA_SOD_HALT_TOPIC_ARN twice. - tests/test_adapter.py::test_default_remote_state_key: deleted NOVA_REMOTE_STATE_KEY twice. With core/env.py NOVA-only as of P5, a single NOVA_* delenv is the correct precondition. Collapsed to one delenv per var + updated comments. ---ci--- project: acdl phase: 5 milestone: v1.15 status: verify lessons: - P0 fix applied: duplicate monkeypatch.delenv('NOVA_*') in test_route_halt_artifact.py (2 sites) + test_adapter.py collapsed to a single delenv consistent with the P5 NOVA-only core/env.py. ---/ci--- --- tests/test_adapter.py | 4 ++-- tests/test_route_halt_artifact.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 68b1aee..0392eea 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -409,7 +409,7 @@ class TestAdapterDedupMergesSameModule: class TestAdapterRemoteStateKeyOverride: """P2-2 (v1.14, REQ-139): NOVA_REMOTE_STATE_KEY env var (P2 renamed from - ACDL_REMOTE_STATE_KEY; dual-read NOVA_* preferred, ACDL_* fallback until + NOVA_REMOTE_STATE_KEY; dual-read NOVA_* preferred, ACDL_* fallback until P5) overrides the default 'platform/terraform.tfstate' key in the emitted data terraform_remote_state block. This is the load-bearing correctness mechanism for the microservice L2 lifecycle (remote state points at the @@ -417,8 +417,8 @@ class TestAdapterRemoteStateKeyOverride: def test_default_remote_state_key(self, tmp_path, monkeypatch): """When NOVA_REMOTE_STATE_KEY is unset, the default key is used.""" + # P5 (REQ-164): ACDL_* fallback removed — NOVA_* only. monkeypatch.delenv("NOVA_REMOTE_STATE_KEY", raising=False) - monkeypatch.delenv("ACDL_REMOTE_STATE_KEY", raising=False) stack = { "resources": [ {"id": "s3", "type": "aws:s3:bucket", "module": "s3@1.0.0", "inputs": {"bucket_name": "test", "region": "us-east-1"}} diff --git a/tests/test_route_halt_artifact.py b/tests/test_route_halt_artifact.py index a8e3f1b..cd32442 100644 --- a/tests/test_route_halt_artifact.py +++ b/tests/test_route_halt_artifact.py @@ -14,8 +14,8 @@ from core.separation_of_duties import route_halt_artifact def test_route_halt_publishes_to_sns_when_arn_set(monkeypatch): - """With ACDL_SOD_HALT_TOPIC_ARN set, the SNS client receives the publish.""" - monkeypatch.setenv("ACDL_SOD_HALT_TOPIC_ARN", "arn:aws:sns:us-east-1:000000000000:nova-sod-halt") + """With NOVA_SOD_HALT_TOPIC_ARN set, the SNS client receives the publish.""" + monkeypatch.setenv("NOVA_SOD_HALT_TOPIC_ARN", "arn:aws:sns:us-east-1:000000000000:nova-sod-halt") sns_client = mock.MagicMock() route_halt_artifact("contract-123", "SEPARATION_OF_DUTIES_VIOLATION: x==y", oncall_client=sns_client) @@ -28,9 +28,9 @@ def test_route_halt_publishes_to_sns_when_arn_set(monkeypatch): def test_route_halt_falls_back_to_stderr_when_arn_unset(monkeypatch, capsys): - """Without ACDL_SOD_HALT_TOPIC_ARN, a stderr emission occurs.""" + """Without NOVA_SOD_HALT_TOPIC_ARN, a stderr emission occurs.""" + # P5 (REQ-164): ACDL_* fallback removed — NOVA_* only. monkeypatch.delenv("NOVA_SOD_HALT_TOPIC_ARN", raising=False) - monkeypatch.delenv("ACDL_SOD_HALT_TOPIC_ARN", raising=False) # Mock outbox_writer.write_event to avoid AWS calls. with mock.patch("core.outbox_writer.write_event", return_value=None): route_halt_artifact("contract-456", "violation", oncall_client=None) @@ -41,8 +41,8 @@ def test_route_halt_falls_back_to_stderr_when_arn_unset(monkeypatch, capsys): def test_route_halt_outbox_fallback_writes_event(monkeypatch): """Without the SNS ARN, the outbox fallback writes a SEPARATION_OF_DUTIES_VIOLATION event.""" + # P5 (REQ-164): ACDL_* fallback removed — NOVA_* only. monkeypatch.delenv("NOVA_SOD_HALT_TOPIC_ARN", raising=False) - monkeypatch.delenv("ACDL_SOD_HALT_TOPIC_ARN", raising=False) with mock.patch("core.outbox_writer.write_event") as mock_write: route_halt_artifact("contract-789", "sod violation", oncall_client=None) mock_write.assert_called_once() @@ -54,7 +54,7 @@ def test_route_halt_outbox_fallback_writes_event(monkeypatch): def test_route_halt_sns_failure_falls_back_to_outbox(monkeypatch): """If SNS publish raises, the outbox fallback is used.""" - monkeypatch.setenv("ACDL_SOD_HALT_TOPIC_ARN", "arn:aws:sns:us-east-1:000000000000:nova-sod-halt") + monkeypatch.setenv("NOVA_SOD_HALT_TOPIC_ARN", "arn:aws:sns:us-east-1:000000000000:nova-sod-halt") sns_client = mock.MagicMock() sns_client.publish.side_effect = Exception("SNS down") with mock.patch("core.outbox_writer.write_event") as mock_write: From eb7634da284039e7df874652d90883eab1e89758 Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Thu, 30 Jul 2026 02:11:12 +0000 Subject: [PATCH 3/4] =?UTF-8?q?fix(P5):=20doc=20drift=20=E2=80=94=20Nova?= =?UTF-8?q?=20rebrand=20stale=20refs=20in=20docs/decks/roadmap/arch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.15-Nova rebrand doc verification (phase/05-final-review-ship) found stale ACDL references where the code/terraform already uses Nova names. Critical drift fixed (doc said old name; code uses new): - README.md: `.acdl/contract.yml` -> `.nova/contract.yml`; `acdl-spike-runner` -> `nova-spike-runner`; ABAC tag keys `acdl:owner|contract` -> `nova:owner|contract`. - docs/environments/index.md: `acdl-contract-ingestor` -> `nova-contract-ingestor`; `aws:PrincipalTag/acdl:owner` -> `nova:owner`; `acdl-contracts` -> `nova-contracts`. - docs/consumer-guide.md: `acdl-change-requests` -> `nova-change-requests`; state bucket example `acdl-qa-state` -> `nova-qa-state`. - docs/presentations/* (4 decks + 2 HTML + talking-points): `acdl:owner|contract| environment|cost-center` -> `nova:*`; `ACDL_LIFECYCLE_MODE` -> `NOVA_LIFECYCLE_MODE`. - pipelines/modules-lifecycle.yml comments: `ACDL_LIFECYCLE_MODE` -> `NOVA_LIFECYCLE_MODE` (workflows already use NOVA_; the contract comments were stale). - docs/NOVA_MIGRATION.md: status banner -> COMPLETE (P5 cutoff passed). - .ciagent/ARCHITECTURE.md: header `ACDL` -> `Nova`; NOVA_LIFECYCLE_MODE rename noted in the live lifecycle-mode section (v1.15 addendum already correct). - .ciagent/ROADMAP.md: v1.15 phase statuses P1-P4 pending -> complete (v1.15.1..v1.15.4); P5 -> in progress (phase/05-final-review-ship). Verification: - grep for `acdl:*` tag keys / `.acdl/contract` / `acdl-contract-ingestor` / `acdl-contracts` / `ACDL_LIFECYCLE_MODE` in README/docs/pipelines -> 0 hits (excluding explicitly-unchanged repo path `acdl/.github/...`, `continuous-intelligence/acdl`, and historical narrative). - core/output_publisher.py uses `/nova`; schemas/tagging-standard.json uses `nova:*`; terraform uses `nova-*` (0 `acdl-` in *.tf) — docs now match. - git tag v1.15.0..v1.15.4 exist. - pytest tests/test_lifecycle_mode_flag.py tests/test_pipeline_contract.py -> 111 passed. ---ci--- project: acdl --- .ciagent/ARCHITECTURE.md | 15 ++++++------- .ciagent/ROADMAP.md | 10 ++++----- README.md | 8 +++---- docs/NOVA_MIGRATION.md | 21 ++++++++++--------- docs/consumer-guide.md | 4 ++-- docs/environments/index.md | 6 +++--- .../how-the-platform-works-marp.md | 4 ++-- .../how-the-platform-works-talking-points.md | 4 ++-- .../presentations/how-the-platform-works.html | 4 ++-- docs/presentations/how-the-platform-works.md | 12 +++++------ .../the-developer-experience-marp.md | 4 ++-- ...the-developer-experience-talking-points.md | 2 +- .../the-developer-experience.html | 4 ++-- .../presentations/the-developer-experience.md | 4 ++-- pipelines/modules-lifecycle.yml | 4 ++-- 15 files changed, 54 insertions(+), 52 deletions(-) diff --git a/.ciagent/ARCHITECTURE.md b/.ciagent/ARCHITECTURE.md index a990c6d..7bf0aef 100644 --- a/.ciagent/ARCHITECTURE.md +++ b/.ciagent/ARCHITECTURE.md @@ -1,8 +1,8 @@ -# ACDL — Architecture (v1.1 target) +# Nova — Architecture (v1.1 target) -> Target architecture for the real Agentic Cloud Delivery Platform. -> Source of truth for **how**: `docs/architecture.md` (v0.2) is the upstream -> draft; this file is the ACDL-repo operating copy, refined at phase +> Target architecture for the real Agentic Cloud Delivery Platform (rebranded +> Nova in v1.15). Source of truth for **how**: `docs/architecture.md` (v0.2) is the upstream +> draft; this file is the Nova-repo operating copy, refined at phase > boundaries. Where this file and `docs/vision.md` conflict, the vision wins. ## Status @@ -598,10 +598,11 @@ VPC; the microservice composition references it via `terraform_remote_state` (data source). State keys are deterministic and env-aware (`spike/{contract.id}/{contract.environment}/terraform.tfstate`). -**ACDL_LIFECYCLE_MODE (v1.12, REQ-134).** The lifecycle pipeline defaults +**NOVA_LIFECYCLE_MODE (v1.12, REQ-134; renamed ACDL→NOVA in v1.15 P2).** The lifecycle pipeline defaults to plan-only (fast, no AWS mutation, no cost). A CI variable -`ACDL_LIFECYCLE_MODE` (default `plan`) overrides to `full` for the real -apply→modify→destroy. +`NOVA_LIFECYCLE_MODE` (default `plan`) overrides to `full` for the real +apply→modify→destroy. (P2–P4 dual-read fallback to `ACDL_LIFECYCLE_MODE`; +fallback removed in P5 per the v1.15 addendum.) ## v1.12 Addendum — Presentation Refinement + CAP-013 Fix diff --git a/.ciagent/ROADMAP.md b/.ciagent/ROADMAP.md index b285fae..010363e 100644 --- a/.ciagent/ROADMAP.md +++ b/.ciagent/ROADMAP.md @@ -1504,7 +1504,7 @@ milestone release). (G-104 binding.) announcing the `.acdl/`→`.nova/` path, `ACDL_*`→`NOVA_*` env vars, `/acdl/`→`/nova/` SSM path, `acdl:*`→`nova:*` tag keys, and `acdl-*`→`nova-*` AWS resource names changes coming in P2–P4. -- **Status:** pending +- **Status:** complete (v1.15.1) - **Depends on:** — - **Requirements:** REQ-155, REQ-156, REQ-157 - **Success Criteria:** @@ -1536,7 +1536,7 @@ milestone release). (G-104 binding.) contract resolver, deploy workflow checkout path, consumer docs, and the contract schema description. Rotate Gitea repo secrets via API (rename keys `ACDL_*` → `NOVA_*`, values stay). -- **Status:** pending +- **Status:** complete (v1.15.2) - **Depends on:** [P1] - **Requirements:** REQ-158, REQ-159, REQ-160 - **Success Criteria:** @@ -1564,7 +1564,7 @@ milestone release). (G-104 binding.) to match `nova:*`, update `nova_tagging.py` to enforce `nova:*` (hard, no warn), then remove `acdl:*` tags once consumers are verified. Terraform tagging updated to emit `nova:*`. -- **Status:** pending +- **Status:** complete (v1.15.3) - **Depends on:** [P2] - **Requirements:** REQ-161, REQ-162 - **Success Criteria:** @@ -1596,7 +1596,7 @@ milestone release). (G-104 binding.) passes; live apply/modify/destroy is exercised by the modules-lifecycle workflow when `ACDL_LIFECYCLE_MODE` (now `NOVA_LIFECYCLE_MODE`) is set to full. -- **Status:** pending +- **Status:** complete (v1.15.4) - **Depends on:** [P3] - **Requirements:** REQ-163 - **Success Criteria:** @@ -1620,7 +1620,7 @@ milestone release). (G-104 binding.) update REQUIREMENTS.md (REQ-155..164 marked complete), ROADMAP.md (v1.15 complete), PROJECT.md. Tag final patch `v1.14.5` (IS the milestone release). Merge `milestone/v1.15-nova` → `main`. -- **Status:** pending +- **Status:** in progress (branch `phase/05-final-review-ship`) - **Depends on:** [P1-P4] - **Requirements:** REQ-164 - **Success Criteria:** diff --git a/README.md b/README.md index 8fe0bf4..2990cd4 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ There are two kinds of repository in the Nova model: A **consumer never clones it.** - **Consumer repo (yours).** A consumer repo contains only: 1. **Its application code** — the service or site being deployed. - 2. **One or more contracts** — small YAML files at `.acdl/contract.yml` + 2. **One or more contracts** — small YAML files at `.nova/contract.yml` that declare infrastructure (one or more modules by name + version), select an environment, and supply module-specific inputs. 3. **One or more CI definitions** — thin `.github/workflows/*.yml` files @@ -144,7 +144,7 @@ engine-specific code. `modules/`, `schemas/`, `contracts/`, ```bash # 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent) # (requires the bootstrap root key in env — skip if the state bucket + -# acdl-spike-runner already exist) +# nova-spike-runner already exist) ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \ python3 terraform/bootstrap/create_state_backend.py ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \ @@ -285,8 +285,8 @@ no static credentials in repo secrets. `repo:org/consumer-repo:ref:refs/heads/main`) binds the role's trust policy to the exact consumer repo + branch that invoked the workflow. - **Resource-creation attributes** — every resource the pipeline creates - is tagged with `acdl:owner=` and - `acdl:contract=`. The session policy grants + is tagged with `nova:owner=` and + `nova:contract=`. The session policy grants view/update/delete **only on resources whose tags match the calling repo**. diff --git a/docs/NOVA_MIGRATION.md b/docs/NOVA_MIGRATION.md index ef9c586..5995f4a 100644 --- a/docs/NOVA_MIGRATION.md +++ b/docs/NOVA_MIGRATION.md @@ -1,16 +1,17 @@ # Nova Migration Guide — What Consumers Must Know -> **Nova** is the new product brand for the platform formerly known as -> **ACDL** (Agentic Cloud Delivery Platform). This guide announces the -> scheduled breaking changes coming in the rebrand rollout (Phases P2–P4) -> and tells you exactly what to do, when, and how long you have. +> **STATUS: COMPLETE (milestone v1.15.4, 2026-07-30).** The Nova rebrand +> is fully rolled out. The dual-read / parallel-write grace period has +> ended (P5 cutoff passed). All `ACDL_*` env var fallbacks, `.acdl/` +> consumer-path fallbacks, `/acdl/` SSM-path fallbacks, `acdl:*` tag-key +> fallbacks, and `acdl-*` AWS resource names are removed. Consumers must +> use the `NOVA_*` / `.nova/` / `/nova/` / `nova:*` / `nova-*` names +> exclusively. If you have not yet migrated, follow the steps below. -The product is being rebranded **A C D L → Nova**. The rebrand is staged -across phases so that **no consumer deployment breaks during the -transition**. Phases P2–P4 ship the breaking changes behind a **dual-read -/ parallel-write grace period**; Phase P5 removes the fallback and the old -names stop working. This document is the consumer-facing contract for that -rollout. +> **Nova** is the new product brand for the platform formerly known as +> **ACDL** (Agentic Cloud Delivery Platform). This guide documents the +> breaking changes from the rebrand rollout (Phases P2–P4, cutoff P5) +> and tells you exactly what to do. ## What is NOT changing diff --git a/docs/consumer-guide.md b/docs/consumer-guide.md index 5665cbe..d52ce36 100644 --- a/docs/consumer-guide.md +++ b/docs/consumer-guide.md @@ -345,7 +345,7 @@ process is a 2-step pipeline with **HITL SRE gates** to prevent accidental destruction: 1. **Request a change request (CR):** Contact the platform team to create a - change request in the platform CMDB (DynamoDB `acdl-change-requests` + change request in the platform CMDB (DynamoDB `nova-change-requests` table). The CR must be approved before decommission can proceed. The CR includes the consumer repo, contract ID, and the reason for decommission. @@ -467,7 +467,7 @@ duties check blocks a prod promotion when `approver_qa == approver_prod` | `${env.environment}` | the environment name (dev/qa/prod/dr) | `qa` | | `${env.region}` | the environment's AWS region | `us-east-1` | | `${env.account_id}` | the environment's AWS account id | `123456789012` | -| `${env.state_backend.bucket}` | the environment's state bucket | `acdl-qa-state` | +| `${env.state_backend.bucket}` | the environment's state bucket | `nova-qa-state` | | `${env.network.vpc_cidr}` | the environment's VPC CIDR | `10.1.0.0/16` | | `${contract.id}` | the contract's operational acronym | `assets` | | `${contract.environment}` | the contract's environment field | `qa` | diff --git a/docs/environments/index.md b/docs/environments/index.md index adc131d..0cb9a0f 100644 --- a/docs/environments/index.md +++ b/docs/environments/index.md @@ -56,13 +56,13 @@ threshold. Staging does not exist. ## Cross-account contract ingestion grant (D-051) Onboarding now also grants the consumer repo's deploy role permission to -invoke the **platform Lambda** — `acdl-contract-ingestor` — across +invoke the **platform Lambda** — `nova-contract-ingestor` — across accounts. The Lambda is invoked via a Function URL with IAM auth, so the grant is an inline IAM policy applied to the consumer's deploy role. The policy template lives at [`terraform/platform/consumer_invoke_policy.json`](https://github.com/nova/nova/blob/main/terraform/platform/consumer_invoke_policy.json) and is scoped via **ABAC**: the condition -`aws:PrincipalTag/acdl:owner == ${consumerRepo}` ensures a repo can only +`aws:PrincipalTag/nova:owner == ${consumerRepo}` ensures a repo can only invoke the Lambda when its principal tag matches its claimed identity. The consumer's deploy workflow signs the Function URL request with @@ -75,7 +75,7 @@ is used for two purposes: 1. **Contract ingestion** — the consumer submits its resolved deployment contract (`action: "submit_contract"`) so the platform has a durable - record in the `acdl-contracts` DynamoDB table (PK `consumerRepo`, SK + record in the `nova-contracts` DynamoDB table (PK `consumerRepo`, SK `contractId#submittedAt`). 2. **Error reporting** (D-055) — the consumer reports a deployment error (`action: "report_error"`) which the platform turns into a GitHub diff --git a/docs/presentations/how-the-platform-works-marp.md b/docs/presentations/how-the-platform-works-marp.md index f6b3dfc..5c0d2a0 100644 --- a/docs/presentations/how-the-platform-works-marp.md +++ b/docs/presentations/how-the-platform-works-marp.md @@ -310,7 +310,7 @@ Nova runs at **zero cloud cost** for day-to-day development. AWS spend was measu - **S3 dominates** (98.8%, terraform state bucket) — no compute ran because v1.0→v1.10 was plan-only for IAM-gated capabilities - **Local emulators are the primary tier** — the full pipeline runs in-process, no AWS credentials -- **Live-AWS verification is milestone-scoped, then torn down.** The pipeline now **defaults to plan-only** on every PR; `ACDL_LIFECYCLE_MODE=full` overrides to apply→destroy for milestone verification (REQ-134, v1.12). +- **Live-AWS verification is milestone-scoped, then torn down.** The pipeline now **defaults to plan-only** on every PR; `NOVA_LIFECYCLE_MODE=full` overrides to apply→destroy for milestone verification (REQ-134, v1.12). - **Cost drivers** are spike-scoped: Terraform plan reads (free), S3 state storage (cents), DynamoDB outbox (cents). Any spike > $1/day is an anomaly. **Pre-mortem (`PRE_MORTEM.md`):** the v1.10 decay incident (diff-scoped VERIFY missed 7 adapter defects) is the root pattern: *a claim outruns the verification that backs it.* Four forward failure modes + structural mitigations (regression-tested IAM baseline, mandatory teardown, verified-only deck claims, honest scope). @@ -329,6 +329,6 @@ section { font-size: 20px; } Two architectural pillars make "Verified" a structural property, not a claim: - **The stateless adapter (918 → ~80 lines).** The Terraform adapter was a 918-line monolith with 3 constant tables and 39 type-specific branches. It is now a ~80-line **stateless assembler**: it owns no module content — no resource shape, no nested HCL blocks, no defaults. Each L1 module ships a real `terraform/` module dir owning its shape, nested blocks, and defaults. The adapter reads the registry and emits `module "x" { source = ... }` blocks. A new module is a new terraform dir, not a code change. *(The v1.12 P67 fix closed a dedup defect for multi-resource L1s — ecs-service, alb; CAP-013 now Verified.)* -- **Pipeline-driven lifecycle testing.** A `modules-lifecycle` pipeline matrix-runs each L1 and L2 module's `examples/{simple,complex}.yml` contracts through apply→modify→destroy against live AWS. **The "test" = the pipeline cell going green.** Defaults to **plan-only** on every PR (fast, no AWS mutation, no cost); `ACDL_LIFECYCLE_MODE=full` overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — **22/22 Verified** as of v1.12. +- **Pipeline-driven lifecycle testing.** A `modules-lifecycle` pipeline matrix-runs each L1 and L2 module's `examples/{simple,complex}.yml` contracts through apply→modify→destroy against live AWS. **The "test" = the pipeline cell going green.** Defaults to **plan-only** on every PR (fast, no AWS mutation, no cost); `NOVA_LIFECYCLE_MODE=full` overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — **22/22 Verified** as of v1.12. The v1.10 lesson is the negative space: a 918-line adapter with type-specific branches decayed silently. The ~80-line stateless adapter + the milestone regression gate are the structural fix. \ No newline at end of file diff --git a/docs/presentations/how-the-platform-works-talking-points.md b/docs/presentations/how-the-platform-works-talking-points.md index e7b85b9..d66060b 100644 --- a/docs/presentations/how-the-platform-works-talking-points.md +++ b/docs/presentations/how-the-platform-works-talking-points.md @@ -182,7 +182,7 @@ **Talking points:** - The phrase to land is "secure by default, not secure by effort" - The selling point is *normalization* — we can add a new security tool without changing the confidence model or the evidence stream -- For the Head of Security: tagging standards are enforced, not advisory — a missing `acdl:owner` tag fails the check, not a warning +- For the Head of Security: tagging standards are enforced, not advisory — a missing `nova:owner` tag fails the check, not a warning - The decommission flow is the counter-argument to "deletion protection makes cleanup impossible" — it's a deliberate, gated, two-approval path **Key takeaway:** Secure by default, not secure by effort. Checks run before infra is created. @@ -207,7 +207,7 @@ - Close on honesty — the platform delivers real, verifiable value today: 22/22 auto-verifiable capabilities Verified via the v1.11 lifecycle pipeline - The roadmap is concrete, not aspirational hand-waving — 9 planned items, each with a defined milestone and a clear reason it isn't shipped yet (usually an upstream dependency, not an engineering gap) - Emphasize: 0 consumer adoption today — "Testing" means it works internally and is dev pilot-ready, not that it's released -- The lifecycle pipeline defaults to plan-only on every PR; `ACDL_LIFECYCLE_MODE=full` overrides for milestone verification +- The lifecycle pipeline defaults to plan-only on every PR; `NOVA_LIFECYCLE_MODE=full` overrides for milestone verification **Key takeaway:** 22/22 Verified today. 9 planned, each with a clear milestone and reason. diff --git a/docs/presentations/how-the-platform-works.html b/docs/presentations/how-the-platform-works.html index 4535009..3a8a8f4 100644 --- a/docs/presentations/how-the-platform-works.html +++ b/docs/presentations/how-the-platform-works.html @@ -1035,7 +1035,7 @@ img { display: block; margin: 0 auto; max-height: 300px; }
  • S3 dominates (98.8%, terraform state bucket) — no compute ran because v1.0→v1.10 was plan-only for IAM-gated capabilities
  • Local emulators are the primary tier — the full pipeline runs in-process, no AWS credentials
  • -
  • Live-AWS verification is milestone-scoped, then torn down. The pipeline now defaults to plan-only on every PR; ACDL_LIFECYCLE_MODE=full overrides to apply→destroy for milestone verification (REQ-134, v1.12).
  • +
  • Live-AWS verification is milestone-scoped, then torn down. The pipeline now defaults to plan-only on every PR; NOVA_LIFECYCLE_MODE=full overrides to apply→destroy for milestone verification (REQ-134, v1.12).
  • Cost drivers are spike-scoped: Terraform plan reads (free), S3 state storage (cents), DynamoDB outbox (cents). Any spike > $1/day is an anomaly.

Pre-mortem (PRE_MORTEM.md): the v1.10 decay incident (diff-scoped VERIFY missed 7 adapter defects) is the root pattern: a claim outruns the verification that backs it. Four forward failure modes + structural mitigations (regression-tested IAM baseline, mandatory teardown, verified-only deck claims, honest scope).

@@ -1085,7 +1085,7 @@ img { display: block; margin: 0 auto; max-height: 300px; }

Two architectural pillars make "Verified" a structural property, not a claim:

  • The stateless adapter (918 → ~80 lines). The Terraform adapter was a 918-line monolith with 3 constant tables and 39 type-specific branches. It is now a ~80-line stateless assembler: it owns no module content — no resource shape, no nested HCL blocks, no defaults. Each L1 module ships a real terraform/ module dir owning its shape, nested blocks, and defaults. The adapter reads the registry and emits module "x" { source = ... } blocks. A new module is a new terraform dir, not a code change. (The v1.12 P67 fix closed a dedup defect for multi-resource L1s — ecs-service, alb; CAP-013 now Verified.)
  • -
  • Pipeline-driven lifecycle testing. A modules-lifecycle pipeline matrix-runs each L1 and L2 module's examples/{simple,complex}.yml contracts through apply→modify→destroy against live AWS. The "test" = the pipeline cell going green. Defaults to plan-only on every PR (fast, no AWS mutation, no cost); ACDL_LIFECYCLE_MODE=full overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — 22/22 Verified as of v1.12.
  • +
  • Pipeline-driven lifecycle testing. A modules-lifecycle pipeline matrix-runs each L1 and L2 module's examples/{simple,complex}.yml contracts through apply→modify→destroy against live AWS. The "test" = the pipeline cell going green. Defaults to plan-only on every PR (fast, no AWS mutation, no cost); NOVA_LIFECYCLE_MODE=full overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — 22/22 Verified as of v1.12.

The v1.10 lesson is the negative space: a 918-line adapter with type-specific branches decayed silently. The ~80-line stateless adapter + the milestone regression gate are the structural fix.

Internal
diff --git a/docs/presentations/how-the-platform-works.md b/docs/presentations/how-the-platform-works.md index 48caab0..895695e 100644 --- a/docs/presentations/how-the-platform-works.md +++ b/docs/presentations/how-the-platform-works.md @@ -242,7 +242,7 @@ flowchart LR - **Authentication — OIDC federation.** Each job mints a short-lived token; no credential stored in the consumer repo or runner secret. Planned: all runners - **Authorization — attribute-based (ABAC), not role-based.** Two attribute classes scope every action: - **Repository identity** — trust policy binds to the exact consumer repo + branch. - - **Resource tags** — every resource tagged `acdl:owner` + `acdl:contract`; session policy grants access **only to matching tags.** + - **Resource tags** — every resource tagged `nova:owner` + `nova:contract`; session policy grants access **only to matching tags.** - **The effect:** a consumer can only touch the resources it created. One consumer can never affect another. > **Speaker notes:** This is the slide for the Head of Cloud/Security. The key phrase is "blast radius contained to the consumer's own stack." Contrast with the common failure mode of shared CI roles that can touch any account resource. The static-key override exists for edge cases but is rotated daily on platform runners; it is never the default. @@ -376,7 +376,7 @@ Monitoring is **a platform default, not a per-team project.** *(Testing.)* Security defaults that **do not require a team to opt in.** Checks run on **every** deployment, normalized to a single schema regardless of which engine produced them. *(Testing.)* -- **Infrastructure-as-code policy** (Checkov) — secrets in plaintext, public ingress, IAM wildcards, KMS key references, **required tagging standards** (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`). All run *before* infra is created. +- **Infrastructure-as-code policy** (Checkov) — secrets in plaintext, public ingress, IAM wildcards, KMS key references, **required tagging standards** (`nova:owner`, `nova:contract`, `nova:environment`, `nova:cost-center`). All run *before* infra is created. - **Cloud security posture** (Wiz adapter) — translates cloud security findings into the same normalized record. *(Adapter testing; activates when a Wiz tenant is configured.)* - **Kubernetes-native policy** (Kyverno adapter) — ready for the GitOps reconciler roadmap item. *(Adapter testing; inactive for Terraform-only stacks.)* - **Encryption on every resource** — at-rest encryption is on by default for every primitive (S3, RDS, ECR, ECS, and more). *(Testing.)* @@ -385,7 +385,7 @@ Security defaults that **do not require a team to opt in.** Checks run on **ever - **Deletion protection on by default** — every resource has `prevent_destroy` on unless a consumer explicitly disables it via a documented feature flag. *(Testing.)* - **Safe decommission** — a 2-step pipeline (disable protection → zero counts → destroy) with **two SRE human-attestation gates** and a **change-request validated against the platform CMDB** before any destructive action. *(Testing.)* Encryption keys enter a grace window (default 30 days) so encrypted data remains recoverable during decommission. -> **Speaker notes:** The phrase to land is "secure by default, not secure by effort." The selling point is *normalization* — we can add a new security tool without changing the confidence model or the evidence stream. For the Head of Security: tagging standards are enforced, not advisory — a missing `acdl:owner` tag fails the check, not a warning. The decommission flow is the counter-argument to "deletion protection makes cleanup impossible" — it's a deliberate, gated, two-approval path, not a lock with no key. +> **Speaker notes:** The phrase to land is "secure by default, not secure by effort." The selling point is *normalization* — we can add a new security tool without changing the confidence model or the evidence stream. For the Head of Security: tagging standards are enforced, not advisory — a missing `nova:owner` tag fails the check, not a warning. The decommission flow is the counter-argument to "deletion protection makes cleanup impossible" — it's a deliberate, gated, two-approval path, not a lock with no key. --- @@ -434,7 +434,7 @@ A phased roadmap from the current Testing baseline to the full North Star: - Additional engine adapters (OpenTofu, Pulumi, Kubernetes CRDs). - Deeper observability bootstrap (dashboards, runbooks, on-call bindings). -> **Speaker notes:** Close on honesty. The platform delivers real, verifiable value today — 22/22 auto-verifiable capabilities are Verified via the v1.11 lifecycle pipeline (apply→modify→destroy against live AWS) + the D-091 regression gate. The roadmap is concrete, not aspirational hand-waving — 9 planned items, each with a defined milestone and a clear reason it isn't shipped yet (usually an upstream dependency, not an engineering gap). Emphasize: 0 consumer adoption today — "Testing" means it works internally and is dev pilot-ready, not that it's released. The lifecycle pipeline defaults to **plan-only** on every PR (fast, no AWS mutation, no cost); a CI variable (`ACDL_LIFECYCLE_MODE=full`) overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). +> **Speaker notes:** Close on honesty. The platform delivers real, verifiable value today — 22/22 auto-verifiable capabilities are Verified via the v1.11 lifecycle pipeline (apply→modify→destroy against live AWS) + the D-091 regression gate. The roadmap is concrete, not aspirational hand-waving — 9 planned items, each with a defined milestone and a clear reason it isn't shipped yet (usually an upstream dependency, not an engineering gap). Emphasize: 0 consumer adoption today — "Testing" means it works internally and is dev pilot-ready, not that it's released. The lifecycle pipeline defaults to **plan-only** on every PR (fast, no AWS mutation, no cost); a CI variable (`NOVA_LIFECYCLE_MODE=full`) overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). --- @@ -469,7 +469,7 @@ Nova runs at **zero cloud cost** for day-to-day development. The v1.0→v1.10 AW - **S3 dominates** (98.8%, terraform state bucket) — no compute (ECS/Lambda) ran because v1.0→v1.10 was plan-only for IAM-gated capabilities. - **Local emulators are the primary tier** — the full pipeline runs in-process, no AWS credentials, no Checkov, no DynamoDB. *(Testing.)* -- **Live-AWS verification is milestone-scoped, then torn down.** The v1.11 lifecycle pipeline ran apply→modify→destroy for every module, then tore down to zero-cost steady state (D-096 — teardown mandatory before milestone COMPLETE; no merge to main until `terraform show` confirms no resources). The lifecycle pipeline now **defaults to plan-only** on every PR (fast, no AWS mutation, no cost); a CI variable (`ACDL_LIFECYCLE_MODE=full`) overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). +- **Live-AWS verification is milestone-scoped, then torn down.** The v1.11 lifecycle pipeline ran apply→modify→destroy for every module, then tore down to zero-cost steady state (D-096 — teardown mandatory before milestone COMPLETE; no merge to main until `terraform show` confirms no resources). The lifecycle pipeline now **defaults to plan-only** on every PR (fast, no AWS mutation, no cost); a CI variable (`NOVA_LIFECYCLE_MODE=full`) overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). - **Cost drivers** are spike-scoped: Terraform plan reads (free), S3 state storage (cents), DynamoDB outbox (cents). Any cost spike > $1/day is an anomaly. **Pre-mortem (`PRE_MORTEM.md`):** the project's failure modes were pre-mortemed before the leadership pitch. The v1.10 decay incident (diff-scoped VERIFY missed 7 adapter defects across 8 NFR-patch phases — decks advertised capability that wasn't reproducible) is the root pattern: *a claim outruns the verification that backs it.* Four forward failure modes + structural mitigations: (FM-1) IAM-drift recurrence → IAM policy baseline is regression-tested; (FM-2) cost spike from un-torn-down stacks → D-096 mandatory teardown; (FM-3) deck overstates capability → verified-only claims + decks unfrozen only after re-verification; (FM-4) pilot contract gap → honest scope (microservice + static-assets today; the L2 pattern is extensible). All mitigations are structural, not procedural. @@ -483,6 +483,6 @@ Nova runs at **zero cloud cost** for day-to-day development. The v1.0→v1.10 AW v1.11 rebuilt the platform on two architectural pillars that make "Verified" a structural property, not a claim: - **The stateless adapter (REQ-123, 918 → ~80 lines).** The Terraform adapter was a 918-line monolith with 3 constant tables and 39 type-specific branches. It is now a ~80-line **stateless assembler**: it owns no module content — no resource shape, no nested HCL blocks, no defaults, no type-specific logic. Each L1 module ships a real `terraform/` module dir owning its resource shape, nested blocks, and defaults (centralized in `locals.tf`). The adapter reads the registry and emits `module "x" { source = ... }` blocks. No type-specific logic in the adapter means a new module is a new terraform dir, not a code change. *(The v1.12 P67 fix closed a dedup defect where multi-resource L1s — ecs-service, alb — produced invalid Terraform; CAP-013 now Verified.)* -- **Pipeline-driven lifecycle testing (REQ-127/128).** A `modules-lifecycle` pipeline matrix-runs each L1 and L2 module's `examples/{simple,complex}.yml` contracts through apply→modify→destroy against live AWS. No per-module Python. **The "test" = the pipeline cell going green.** Defaults to **plan-only** on every PR (fast, no AWS mutation, no cost); `ACDL_LIFECYCLE_MODE=full` runs the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — 22/22 Verified as of v1.12. +- **Pipeline-driven lifecycle testing (REQ-127/128).** A `modules-lifecycle` pipeline matrix-runs each L1 and L2 module's `examples/{simple,complex}.yml` contracts through apply→modify→destroy against live AWS. No per-module Python. **The "test" = the pipeline cell going green.** Defaults to **plan-only** on every PR (fast, no AWS mutation, no cost); `NOVA_LIFECYCLE_MODE=full` runs the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — 22/22 Verified as of v1.12. > **Speaker notes:** This is the deep-dive slide for the Head of Engineering / Architecture. The two pillars are the answer to "how do you keep the decks honest?" The adapter is simple enough to reason about (a stateless assembler), and the lifecycle pipeline is the automated verification that backs every "Testing" claim. The v1.10 lesson is the negative space: a 918-line adapter with type-specific branches decayed silently because the VERIFY gate was diff-scoped. The ~80-line stateless adapter + the milestone regression gate are the structural fix. The plan-only default (v1.12) means this verification runs on every PR at zero cost, with the full apply→destroy gated behind a CI variable override. \ No newline at end of file diff --git a/docs/presentations/the-developer-experience-marp.md b/docs/presentations/the-developer-experience-marp.md index 773293b..e02d550 100644 --- a/docs/presentations/the-developer-experience-marp.md +++ b/docs/presentations/the-developer-experience-marp.md @@ -297,7 +297,7 @@ Nova runs at **zero cloud cost** for day-to-day development. AWS spend was measu | Peak day | 2026-07-27 ($0.000867) | - **Local emulators are the primary tier** — the full pipeline runs in-process, no AWS credentials -- **Live-AWS verification is milestone-scoped, then torn down.** The pipeline now **defaults to plan-only** on every PR; `ACDL_LIFECYCLE_MODE=full` overrides to apply→destroy for milestone verification (REQ-134, v1.12). +- **Live-AWS verification is milestone-scoped, then torn down.** The pipeline now **defaults to plan-only** on every PR; `NOVA_LIFECYCLE_MODE=full` overrides to apply→destroy for milestone verification (REQ-134, v1.12). - **Cost drivers** are spike-scoped: Terraform plan reads (free), S3 state storage (cents), DynamoDB outbox (cents). No running infrastructure between milestones. **Pre-mortem (`PRE_MORTEM.md`):** the v1.10 decay incident (diff-scoped VERIFY missed 7 adapter defects) is the root pattern: *a claim outruns the verification that backs it.* Four forward failure modes + structural mitigations (regression-tested IAM baseline, mandatory teardown, verified-only deck claims, honest scope). @@ -316,6 +316,6 @@ section { font-size: 20px; } Two architectural pillars make "Verified" a structural property, not a claim: - **The stateless adapter (918 → ~80 lines).** The Terraform adapter was a 918-line monolith with 3 constant tables and 39 type-specific branches. It is now a ~80-line **stateless assembler**: it owns no module content — no resource shape, no nested HCL blocks, no defaults. Each L1 module ships a real `terraform/` module dir owning its shape, nested blocks, and defaults. The adapter reads the registry and emits `module "x" { source = ... }` blocks. A new module is a new terraform dir, not a code change. *(The v1.12 P67 fix closed a dedup defect for multi-resource L1s — ecs-service, alb; CAP-013 now Verified.)* -- **Pipeline-driven lifecycle testing.** A `modules-lifecycle` pipeline matrix-runs each L1 and L2 module's contracts through apply→modify→destroy against live AWS. **The "test" = the pipeline cell going green.** Defaults to **plan-only** on every PR (fast, no AWS mutation, no cost); `ACDL_LIFECYCLE_MODE=full` overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — **22/22 Verified** as of v1.12. +- **Pipeline-driven lifecycle testing.** A `modules-lifecycle` pipeline matrix-runs each L1 and L2 module's contracts through apply→modify→destroy against live AWS. **The "test" = the pipeline cell going green.** Defaults to **plan-only** on every PR (fast, no AWS mutation, no cost); `NOVA_LIFECYCLE_MODE=full` overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — **22/22 Verified** as of v1.12. The v1.10 lesson is the negative space: a 918-line adapter with type-specific branches decayed silently. The ~80-line stateless adapter + the milestone regression gate are the structural fix. \ No newline at end of file diff --git a/docs/presentations/the-developer-experience-talking-points.md b/docs/presentations/the-developer-experience-talking-points.md index d39cdce..b2f6079 100644 --- a/docs/presentations/the-developer-experience-talking-points.md +++ b/docs/presentations/the-developer-experience-talking-points.md @@ -232,7 +232,7 @@ **Talking points:** - The headline for the Head of Cloud / Finance: less than one cent over 8 days of active development; zero BAU cloud spend -- The lifecycle pipeline defaults to plan-only so the PR-time cost is zero; `ACDL_LIFECYCLE_MODE=full` overrides for milestone verification +- The lifecycle pipeline defaults to plan-only so the PR-time cost is zero; `NOVA_LIFECYCLE_MODE=full` overrides for milestone verification - The pre-mortem is the credibility slide — we already asked "how does this fail?" and the mitigations are structural - The v1.10 decay incident is disclosed honestly, not hidden — that disclosure IS the mitigation - Cost drivers are spike-scoped: Terraform plan reads (free), S3 state storage (cents), DynamoDB outbox (cents). No running infrastructure between milestones diff --git a/docs/presentations/the-developer-experience.html b/docs/presentations/the-developer-experience.html index f52dc9d..1d6de8d 100644 --- a/docs/presentations/the-developer-experience.html +++ b/docs/presentations/the-developer-experience.html @@ -1057,7 +1057,7 @@ img { display: block; margin: 0 auto; max-height: 280px; }
  • Local emulators are the primary tier — the full pipeline runs in-process, no AWS credentials
  • -
  • Live-AWS verification is milestone-scoped, then torn down. The pipeline now defaults to plan-only on every PR; ACDL_LIFECYCLE_MODE=full overrides to apply→destroy for milestone verification (REQ-134, v1.12).
  • +
  • Live-AWS verification is milestone-scoped, then torn down. The pipeline now defaults to plan-only on every PR; NOVA_LIFECYCLE_MODE=full overrides to apply→destroy for milestone verification (REQ-134, v1.12).
  • Cost drivers are spike-scoped: Terraform plan reads (free), S3 state storage (cents), DynamoDB outbox (cents). No running infrastructure between milestones.

Pre-mortem (PRE_MORTEM.md): the v1.10 decay incident (diff-scoped VERIFY missed 7 adapter defects) is the root pattern: a claim outruns the verification that backs it. Four forward failure modes + structural mitigations (regression-tested IAM baseline, mandatory teardown, verified-only deck claims, honest scope).

@@ -1111,7 +1111,7 @@ img { display: block; margin: 0 auto; max-height: 280px; }

Two architectural pillars make "Verified" a structural property, not a claim:

  • The stateless adapter (918 → ~80 lines). The Terraform adapter was a 918-line monolith with 3 constant tables and 39 type-specific branches. It is now a ~80-line stateless assembler: it owns no module content — no resource shape, no nested HCL blocks, no defaults. Each L1 module ships a real terraform/ module dir owning its shape, nested blocks, and defaults. The adapter reads the registry and emits module "x" { source = ... } blocks. A new module is a new terraform dir, not a code change. (The v1.12 P67 fix closed a dedup defect for multi-resource L1s — ecs-service, alb; CAP-013 now Verified.)
  • -
  • Pipeline-driven lifecycle testing. A modules-lifecycle pipeline matrix-runs each L1 and L2 module's contracts through apply→modify→destroy against live AWS. The "test" = the pipeline cell going green. Defaults to plan-only on every PR (fast, no AWS mutation, no cost); ACDL_LIFECYCLE_MODE=full overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — 22/22 Verified as of v1.12.
  • +
  • Pipeline-driven lifecycle testing. A modules-lifecycle pipeline matrix-runs each L1 and L2 module's contracts through apply→modify→destroy against live AWS. The "test" = the pipeline cell going green. Defaults to plan-only on every PR (fast, no AWS mutation, no cost); NOVA_LIFECYCLE_MODE=full overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — 22/22 Verified as of v1.12.

The v1.10 lesson is the negative space: a 918-line adapter with type-specific branches decayed silently. The ~80-line stateless adapter + the milestone regression gate are the structural fix.

Internal
diff --git a/docs/presentations/the-developer-experience.md b/docs/presentations/the-developer-experience.md index 4f2094a..19bc40e 100644 --- a/docs/presentations/the-developer-experience.md +++ b/docs/presentations/the-developer-experience.md @@ -438,7 +438,7 @@ Nova runs at **zero cloud cost** for day-to-day development. The v1.0→v1.10 AW | Peak day | 2026-07-27 ($0.000867 — v1.10 regression + verify run) | - **Local emulators are the primary tier** — the full pipeline runs in-process, no AWS credentials, no Checkov, no DynamoDB. -- **Live-AWS verification is milestone-scoped, then torn down.** The v1.11 lifecycle pipeline ran apply→modify→destroy for every module, then tore down to zero-cost steady state (D-096 — teardown mandatory before milestone COMPLETE). The lifecycle pipeline now **defaults to plan-only** on every PR (fast, no AWS mutation, no cost); a CI variable (`ACDL_LIFECYCLE_MODE=full`) overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). +- **Live-AWS verification is milestone-scoped, then torn down.** The v1.11 lifecycle pipeline ran apply→modify→destroy for every module, then tore down to zero-cost steady state (D-096 — teardown mandatory before milestone COMPLETE). The lifecycle pipeline now **defaults to plan-only** on every PR (fast, no AWS mutation, no cost); a CI variable (`NOVA_LIFECYCLE_MODE=full`) overrides to the real apply→destroy for milestone verification (REQ-134, v1.12). - **Cost drivers** are spike-scoped: Terraform plan reads (free), S3 state storage (cents), DynamoDB outbox (cents). No running infrastructure between milestones. **Pre-mortem (`PRE_MORTEM.md`):** the project's failure modes were pre-mortemed before the leadership pitch. The v1.10 decay incident (diff-scoped VERIFY missed 7 adapter defects — decks advertised capability that wasn't reproducible) is the root pattern: *a claim outruns the verification that backs it.* Four forward failure modes + structural mitigations (regression-tested IAM baseline, mandatory teardown, verified-only deck claims, honest scope). @@ -452,6 +452,6 @@ Nova runs at **zero cloud cost** for day-to-day development. The v1.0→v1.10 AW v1.11 rebuilt the platform on two architectural pillars that make "Verified" a structural property, not a claim: - **The stateless adapter (918 → ~80 lines).** The Terraform adapter was a 918-line monolith with 3 constant tables and 39 type-specific branches. It is now a ~80-line **stateless assembler**: it owns no module content. Each L1 module ships a real `terraform/` module dir owning its resource shape, nested blocks, and defaults. A new module is a new terraform dir, not a code change. *(The v1.12 P67 fix closed a dedup defect for multi-resource L1s — ecs-service, alb; CAP-013 now Verified.)* -- **Pipeline-driven lifecycle testing.** A `modules-lifecycle` pipeline matrix-runs each L1 and L2 module's contracts through apply→modify→destroy against live AWS. **The "test" = the pipeline cell going green.** Defaults to **plan-only** on every PR (fast, no AWS mutation, no cost); `ACDL_LIFECYCLE_MODE=full` runs the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — 22/22 Verified as of v1.12. +- **Pipeline-driven lifecycle testing.** A `modules-lifecycle` pipeline matrix-runs each L1 and L2 module's contracts through apply→modify→destroy against live AWS. **The "test" = the pipeline cell going green.** Defaults to **plan-only** on every PR (fast, no AWS mutation, no cost); `NOVA_LIFECYCLE_MODE=full` runs the real apply→destroy for milestone verification (REQ-134, v1.12). The regression gate (D-091) re-runs all 22 capabilities at milestone completion — 22/22 Verified as of v1.12. > **Speaker notes:** This is the deep-dive slide for the Head of Engineering / Architecture. The two pillars answer "how do you keep the decks honest?" The adapter is simple enough to reason about (a stateless assembler); the lifecycle pipeline is the automated verification that backs every "Testing" claim. The v1.10 lesson is the negative space: a 918-line adapter with type-specific branches decayed silently. The ~80-line stateless adapter + the milestone regression gate are the structural fix. The plan-only default (v1.12) means verification runs on every PR at zero cost, with the full apply→destroy gated behind a CI variable override. \ No newline at end of file diff --git a/pipelines/modules-lifecycle.yml b/pipelines/modules-lifecycle.yml index aece786..51fd1d1 100644 --- a/pipelines/modules-lifecycle.yml +++ b/pipelines/modules-lifecycle.yml @@ -13,7 +13,7 @@ name: acdl-modules-lifecycle # # Lifecycle mode (REQ-134, v1.12): `default_mode: plan` — the lifecycle # scripts run `run_platform.sh --plan-only` (fast, no AWS mutation, no -# AWS credentials, no cost) on every PR. Set the ACDL_LIFECYCLE_MODE CI +# AWS credentials, no cost) on every PR. Set the NOVA_LIFECYCLE_MODE CI # variable (workflow_dispatch input `lifecycle_mode`) to `full` for the # real apply→modify→destroy against live AWS. The CI VPC apply/destroy # jobs are skipped in plan mode (nothing is applied). @@ -36,7 +36,7 @@ terraform_version: "1.9.*" # Default lifecycle mode: "plan" (plan-only, fast, no AWS mutation) or "full" # (real apply→modify→destroy against live AWS). Overridable via the -# ACDL_LIFECYCLE_MODE CI variable / workflow_dispatch input. +# NOVA_LIFECYCLE_MODE CI variable / workflow_dispatch input. default_mode: plan stages: From e15eea067bf41bcb55af2004e8adb8933b5fa6ff Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Thu, 30 Jul 2026 02:23:55 +0000 Subject: [PATCH 4/4] =?UTF-8?q?docs(milestone):=20complete=20v1.15=20?= =?UTF-8?q?=E2=80=94=20Nova=20Rebrand=20(tag=20v1.15.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P5 final-review-ship complete: dual-read fallback removed (REQ-164) — core/env.py NOVA-only, .env.secrets load paths NOVA-only (G-106 retired), nova_tagging.py hard-fails any acdl:* tag, legacy ACDL_* Gitea secrets deleted, ACDL_LIFECYCLE_MODE/ACDL_LOCAL_TIER/ACDL_HITL_* exports removed from scripts, SNS subject → Nova SoD halt (P1-2), bootstrap scripts NOVA-only. Review: 2 P0 auto-fixed (duplicate delenv), P1-1/P1-2 resolved, doc-drift fixed. Audit: tags v1.15.0-4 exist; traceability REQ-155..164 all complete; ARCHITECTURE naming table matches codebase. 615 pytest PASS; run_ci.sh 3-stage PASS. NOVA_MIGRATION.md marked COMPLETE. ---ci--- project: acdl phase: 5 milestone: v1.15 status: complete phase_role: final requirements: covered: [REQ-155, REQ-156, REQ-157, REQ-158, REQ-159, REQ-160, REQ-161, REQ-162, REQ-163, REQ-164] partial: [] ---/ci--- --- .ciagent/CHECKPOINT.json | 10 ++-- .ciagent/REQUIREMENTS.md | 2 +- .ciagent/ROADMAP.md | 4 +- .../policy/custom_rules/nova_tagging.py | 27 ++++------ core/env.py | 45 ++++++----------- core/local_emulators.py | 2 +- core/regression_verify.py | 9 ++-- core/separation_of_duties.py | 2 +- scripts/push_consumer_image.py | 4 +- scripts/rotate_spike_key.sh | 12 ++--- scripts/run_l2_lifecycle_destroy.sh | 6 +-- scripts/run_l2_lifecycle_test.sh | 6 +-- scripts/run_lifecycle_destroy.sh | 2 +- scripts/run_lifecycle_test.sh | 2 +- scripts/run_platform.sh | 22 ++++---- scripts/run_regression.sh | 2 +- tests/test_env_helper.py | 50 +++++++++---------- tests/test_lifecycle_mode_flag.py | 16 +++--- tests/test_output_publisher.py | 18 +++---- tests/test_route_halt_artifact.py | 2 +- 20 files changed, 106 insertions(+), 137 deletions(-) diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json index 6e0f399..68dbd1a 100644 --- a/.ciagent/CHECKPOINT.json +++ b/.ciagent/CHECKPOINT.json @@ -1,11 +1,11 @@ { - "phase": 4, + "phase": 5, "stage": "complete", "milestone": "v1.15", - "phase_role": "execution", + "phase_role": "final", "attempts": 0, - "updated_at": "2026-07-30T00:10:00Z", - "milestone_complete": false, - "requirements": ["REQ-163"], + "updated_at": "2026-07-30T00:11:00Z", + "milestone_complete": true, + "requirements": ["REQ-155", "REQ-156", "REQ-157", "REQ-158", "REQ-159", "REQ-160", "REQ-161", "REQ-162", "REQ-163", "REQ-164"], "tag": "v1.15.4" } \ No newline at end of file diff --git a/.ciagent/REQUIREMENTS.md b/.ciagent/REQUIREMENTS.md index 4bdba10..a31c35a 100644 --- a/.ciagent/REQUIREMENTS.md +++ b/.ciagent/REQUIREMENTS.md @@ -828,7 +828,7 @@ IDEATE-01..IDEATE-10, mapped to REQ-155..REQ-164. | REQ-161 | P3 | complete | | REQ-162 | P3 | complete | | REQ-163 | P4 | complete | -| REQ-164 | P5 | pending | +| REQ-164 | P5 | complete | ### Out of Scope (v1.15) - Renaming the real Gitea org/repo (`continuous-intelligence/acdl`) or diff --git a/.ciagent/ROADMAP.md b/.ciagent/ROADMAP.md index 010363e..4214080 100644 --- a/.ciagent/ROADMAP.md +++ b/.ciagent/ROADMAP.md @@ -1435,7 +1435,7 @@ After Phase P21: milestone COMPLETE — `v1.13.24` IS the v1.14 release. --- -## v1.15 (active — Nova Rebrand, tag `v1.15.4`) +## v1.15 (complete — Nova Rebrand, tag `v1.15.4`) A full rebrand from **ACDL** / "Agentic Cloud Delivery Platform" → **Nova** / "The New Dawn of DevSecOps — security as a seamless enabler @@ -1620,7 +1620,7 @@ milestone release). (G-104 binding.) update REQUIREMENTS.md (REQ-155..164 marked complete), ROADMAP.md (v1.15 complete), PROJECT.md. Tag final patch `v1.14.5` (IS the milestone release). Merge `milestone/v1.15-nova` → `main`. -- **Status:** in progress (branch `phase/05-final-review-ship`) +- **Status:** complete (v1.15.4, milestone release) - **Depends on:** [P1-P4] - **Requirements:** REQ-164 - **Success Criteria:** diff --git a/adapters/terraform/policy/custom_rules/nova_tagging.py b/adapters/terraform/policy/custom_rules/nova_tagging.py index 03e4ac5..f46cc86 100644 --- a/adapters/terraform/policy/custom_rules/nova_tagging.py +++ b/adapters/terraform/policy/custom_rules/nova_tagging.py @@ -39,12 +39,11 @@ NON_TAGGABLE_TYPES = ( "aws_internet_gateway", ) -# P3 hard mode (D-109): hard-fail when a taggable resource is missing any -# required nova:* tag, or when a resource carries only legacy acdl:* tag -# keys and no nova:* tags. P2 shipped warn mode (`_WARN_MODE = True`); P3 -# flips to `False` (hard-fail) once terraform emits nova:* and the ABAC -# policy is swapped to nova:*. P5 keeps hard mode and additionally fails -# on any acdl:* tag key present at all. +# P5 hard mode (D-109, REQ-164): `_WARN_MODE = False` (set in P3) AND +# any `acdl:*` tag key present at all is a hard FAIL (P5 tightens from +# P3's "acdl:*-only fails" to "any acdl:* key fails"). The legacy tag +# keys are fully removed from terraform (P3); any remaining `acdl:*` key +# is a rebrand regression. _WARN_MODE = False @@ -71,21 +70,13 @@ class NovaTaggingStandard(BaseResourceCheck): tag_keys = set(tag_block.keys()) elif isinstance(tags, dict): tag_keys = set(tags.keys()) + # P5 (REQ-164): any legacy acdl:* tag key present = hard FAIL. + legacy_present = tag_keys & set(LEGACY_TAGS) + if legacy_present: + return CheckResult.FAILED missing = [t for t in REQUIRED_TAGS if t not in tag_keys] if not missing: return CheckResult.PASSED - # Warn mode (D-109, P2): if the resource carries the legacy acdl:* - # tag keys for every required tag, emit a warning rather than a - # hard fail — existing resources still carry acdl:* until P3. - if _WARN_MODE: - has_all_legacy = all(t in tag_keys for t in LEGACY_TAGS) - if has_all_legacy: - sys.stderr.write( - f"[nova_tagging] WARN: {entity_type} has legacy acdl:* tags " - f"but no nova:* tags (P2 warn mode, D-109). Migrate to " - f"nova:* tags before P5.\n" - ) - return CheckResult.PASSED return CheckResult.FAILED check = NovaTaggingStandard() \ No newline at end of file diff --git a/core/env.py b/core/env.py index afd295e..c9999e1 100644 --- a/core/env.py +++ b/core/env.py @@ -1,19 +1,14 @@ -"""Dual-read environment helper (D-108, REQ-159, G-106). +"""Environment helper (D-108, REQ-159, REQ-164). -During the Nova rebrand transition window (P2–P4), every `NOVA_*` -environment variable is the preferred source, with the legacy `ACDL_*` -name as the fallback. This keeps deployments from breaking while the -keys are rotated across `.env`, `.env.secrets`, Gitea repo secrets, and -operator-managed process environments. +During the Nova rebrand transition window (P2–P4), `get_env` read +`NOVA_*` preferred with the legacy `ACDL_*` name as the fallback. **P5 +(REQ-164) removed the fallback** — `get_env` now reads `NOVA_*` only. -`get_env(name, default=None)` resolves `NOVA_` first, then falls -back to `ACDL_`, then returns `default` if neither is set. - -This helper is removed (NOVA-only) in P5 (REQ-164). Direct-read paths -that bypass this helper (the `.env.secrets` shell export in -`scripts/run_platform.sh` and the Python parser in -`core/regression_verify.py`) mirror this contract inline per the G-106 -binding — see those sites for the dual-read shell/Python forms. +`get_env(name, default=None)` resolves `NOVA_`, then returns +`default` if unset. Direct-read paths that bypass this helper (the +`.env.secrets` shell export in `scripts/run_platform.sh` and the Python +parser in `core/regression_verify.py`) were updated to NOVA-only in P5 +(the G-106 dual-read contract was retired with the fallback). """ from __future__ import annotations @@ -25,24 +20,12 @@ __all__ = ["get_env"] def get_env(name: str, default: Optional[str] = None) -> Optional[str]: - """Resolve a config value with a NOVA-preferred / ACDL-fallback read. + """Resolve a config value from the `NOVA_*` environment. `name` is the bare key WITHOUT the prefix (e.g. ``"AWS_ACCOUNT_ID"``). - The lookup order is: - - 1. ``NOVA_`` (preferred) - 2. ``ACDL_`` (legacy fallback, removed in P5) - 3. ``default`` - - Returns the first value that is present and non-empty, or ``default`` - if neither env var is set. An explicitly-set empty string is treated - as "unset" so an operator cannot accidentally shadow the fallback - with a blank NOVA key. + Returns ``NOVA_`` if set and non-empty, else ``default``. """ - nova_val = os.environ.get(f"NOVA_{name}") - if nova_val: - return nova_val - acdl_val = os.environ.get(f"ACDL_{name}") - if acdl_val: - return acdl_val + val = os.environ.get(f"NOVA_{name}") + if val: + return val return default \ No newline at end of file diff --git a/core/local_emulators.py b/core/local_emulators.py index 5721171..59d9011 100644 --- a/core/local_emulators.py +++ b/core/local_emulators.py @@ -502,6 +502,6 @@ if __name__ == "__main__": # Set both so the dual-read in is_local_tier() finds NOVA_* (preferred); # the ACDL_* alias stays for any unmigrated reader until P5. os.environ["NOVA_LOCAL_TIER"] = "1" - os.environ["ACDL_LOCAL_TIER"] = "1" # legacy alias (dual-read fallback), removed in P5 + # P5 (REQ-164): ACDL_LOCAL_TIER legacy alias removed (NOVA_* only) result = run_local_e2e(contract) print(json.dumps(result, indent=2)) \ No newline at end of file diff --git a/core/regression_verify.py b/core/regression_verify.py index cf3fc64..e475323 100755 --- a/core/regression_verify.py +++ b/core/regression_verify.py @@ -315,13 +315,10 @@ def _load_aws_env() -> Dict[str, str]: continue if "=" in line: k, v = line.split("=", 1) - # G-106 binding: dual-read NOVA_* first, ACDL_* fallback. - # The .env.secrets keys are renamed to NOVA_* in P2; the - # ACDL_* fallback covers operators who haven't rotated - # their local .env.secrets yet. Removed in P5. - if k == "NOVA_AWS_ACCESS_KEY_ID" or k == "ACDL_AWS_ACCESS_KEY_ID": + # P5 (REQ-164): dual-read fallback removed — NOVA_* only. + if k == "NOVA_AWS_ACCESS_KEY_ID": env["AWS_ACCESS_KEY_ID"] = v - elif k == "NOVA_AWS_SECRET_ACCESS_KEY" or k == "ACDL_AWS_SECRET_ACCESS_KEY": + elif k == "NOVA_AWS_SECRET_ACCESS_KEY": env["AWS_SECRET_ACCESS_KEY"] = v elif k == "AWS_DEFAULT_REGION": env["AWS_DEFAULT_REGION"] = v diff --git a/core/separation_of_duties.py b/core/separation_of_duties.py index e3b893a..fd35a28 100644 --- a/core/separation_of_duties.py +++ b/core/separation_of_duties.py @@ -74,7 +74,7 @@ def route_halt_artifact(contract_id: str, violation_reason: str, sns.publish( TopicArn=topic_arn, Message=json.dumps(halt_payload), - Subject="ACDL SoD halt", + Subject="Nova SoD halt", ) print(f"[halt-artifact] SNS published contract={contract_id} " f"topic={topic_arn}", flush=True) diff --git a/scripts/push_consumer_image.py b/scripts/push_consumer_image.py index 672346f..8ba41d8 100644 --- a/scripts/push_consumer_image.py +++ b/scripts/push_consumer_image.py @@ -65,8 +65,8 @@ def main(): creds = _load_env(ENV_FILE) # Dual-read: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5). - access_key = creds.get("NOVA_AWS_ACCESS_KEY_ID") or creds.get("ACDL_AWS_ACCESS_KEY_ID") - secret_key = creds.get("NOVA_AWS_SECRET_ACCESS_KEY") or creds.get("ACDL_AWS_SECRET_ACCESS_KEY") + access_key = creds.get("NOVA_AWS_ACCESS_KEY_ID") + secret_key = creds.get("NOVA_AWS_SECRET_ACCESS_KEY") region = creds.get("AWS_DEFAULT_REGION", AWS_REGION) if not access_key or not secret_key: print("FAIL: NOVA_AWS_ACCESS_KEY_ID / NOVA_AWS_SECRET_ACCESS_KEY missing", diff --git a/scripts/rotate_spike_key.sh b/scripts/rotate_spike_key.sh index 28e40f5..c4e78d8 100755 --- a/scripts/rotate_spike_key.sh +++ b/scripts/rotate_spike_key.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # scripts/rotate_spike_key.sh - rotate the nova-spike-runner IAM access key. # -# Uses the bootstrap root key (NOVA_BOOTSTRAP_AWS_*, ACDL_BOOTSTRAP_AWS_* +# Uses the bootstrap root key (NOVA_BOOTSTRAP_AWS_* # fallback) from the env to: # 1. List nova-spike-runner's access keys. # 2. Create a new key. @@ -24,8 +24,8 @@ ENV_FILE="$ROOT/.env.secrets" fail() { echo "FAIL: $*" >&2; exit 1; } # Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (removed in P5). -: "${NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID:-${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID (or ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID) to the root key}}" -: "${NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:-${ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (or ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY) to the root key}}" +: "${NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID to the root key}" +: "${NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY to the root key}" REGION="${AWS_DEFAULT_REGION:-us-east-1}" USER_NAME="nova-spike-runner" @@ -43,8 +43,8 @@ user = "nova-spike-runner" env_file = os.path.join(os.getcwd(), ".env.secrets") # Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5). -bootstrap_key = os.environ.get("NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID") or os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"] -bootstrap_secret = os.environ.get("NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY") or os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"] +bootstrap_key = os.environ["NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID"] +bootstrap_secret = os.environ["NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"] session = boto3.Session( aws_access_key_id=bootstrap_key, @@ -84,7 +84,7 @@ print(f"rotated key written to {env_file} (chmod 600)", file=sys.stderr) # Optionally upload to Gitea secrets. # Dual-read token: NOVA_GITEA_TOKEN preferred, ACDL_GITEA_TOKEN fallback (G-106). -gitea_token = os.environ.get("NOVA_GITEA_TOKEN") or os.environ.get("ACDL_GITEA_TOKEN") +gitea_token = os.environ.get("NOVA_GITEA_TOKEN") if gitea_token: import urllib.request base = "https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/actions/secrets" diff --git a/scripts/run_l2_lifecycle_destroy.sh b/scripts/run_l2_lifecycle_destroy.sh index d28d067..e9dfdcb 100755 --- a/scripts/run_l2_lifecycle_destroy.sh +++ b/scripts/run_l2_lifecycle_destroy.sh @@ -4,7 +4,7 @@ # Usage: run_l2_lifecycle_destroy.sh # # Wraps run_platform.sh for L2 composition modules in the modules-lifecycle -# pipeline. Sets NOVA_REMOTE_STATE_KEY (ACDL_REMOTE_STATE_KEY fallback until +# pipeline. Sets NOVA_REMOTE_STATE_KEY (NOVA-only since P5, REQ-164) for # P5) to point to the CI VPC state. # # NOTE: unlike the L1 scripts (run_lifecycle_destroy.sh), the L2 path does @@ -26,7 +26,7 @@ MODULE="$1" # Lifecycle mode: "plan" (default) skips destroy; "full" runs the real destroy. # Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). -LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" if [ "$LIFECYCLE_MODE" != "full" ]; then echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0" @@ -38,7 +38,7 @@ CONTRACT="modules/l2/${MODULE}/examples/complex.yml" # Point terraform_remote_state to the CI VPC state (not the platform VPC). # Set both NOVA_* (preferred) and ACDL_* (legacy fallback) until P5. export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" -export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" # legacy fallback, removed in P5 + # Run the platform lifecycle destroy command bash scripts/run_platform.sh --destroy "$CONTRACT" \ No newline at end of file diff --git a/scripts/run_l2_lifecycle_test.sh b/scripts/run_l2_lifecycle_test.sh index 2d9a67f..48960b9 100755 --- a/scripts/run_l2_lifecycle_test.sh +++ b/scripts/run_l2_lifecycle_test.sh @@ -4,7 +4,7 @@ # Usage: run_l2_lifecycle_test.sh # # Wraps run_platform.sh for L2 composition modules in the modules-lifecycle -# pipeline. Sets NOVA_REMOTE_STATE_KEY (ACDL_REMOTE_STATE_KEY fallback until +# pipeline. Sets NOVA_REMOTE_STATE_KEY (NOVA-only since P5, REQ-164) for # P5) to point to the CI VPC state so the microservice composition's # terraform_remote_state data source reads from the short-lived CI VPC # (not the long-lived platform VPC). @@ -30,7 +30,7 @@ EXAMPLE="$2" # simple or complex # Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS). # Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). -LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml" @@ -38,7 +38,7 @@ CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml" # Set both NOVA_* (preferred by the dual-read helper) and ACDL_* (legacy # fallback) so any unmigrated reader finds the key until P5. export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" -export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" # legacy fallback, removed in P5 + # Run the platform lifecycle command (plan-only by default; full = apply). if [ "$LIFECYCLE_MODE" = "full" ]; then diff --git a/scripts/run_lifecycle_destroy.sh b/scripts/run_lifecycle_destroy.sh index dd3b8e9..43c380e 100755 --- a/scripts/run_lifecycle_destroy.sh +++ b/scripts/run_lifecycle_destroy.sh @@ -21,7 +21,7 @@ CI_VPC_OUTPUTS="${2:-}" # Lifecycle mode: "plan" (default) skips destroy (nothing was applied); # "full" runs the real terraform destroy. # Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). -LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" if [ "$LIFECYCLE_MODE" != "full" ]; then echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0" diff --git a/scripts/run_lifecycle_test.sh b/scripts/run_lifecycle_test.sh index d229e0e..2567112 100755 --- a/scripts/run_lifecycle_test.sh +++ b/scripts/run_lifecycle_test.sh @@ -27,7 +27,7 @@ CI_VPC_OUTPUTS="${3:-}" # Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS). # Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). -LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" CONTRACT="modules/l1/${MODULE}/examples/${EXAMPLE}.yml" diff --git a/scripts/run_platform.sh b/scripts/run_platform.sh index 1648c5a..966031a 100755 --- a/scripts/run_platform.sh +++ b/scripts/run_platform.sh @@ -121,9 +121,9 @@ if [ "$LOCAL_TIER" = "1" ]; then echo "=== Nova Local Emulating Tier (D-092) ===" echo "contract: $CONTRACT (no AWS credentials required)" echo "" - # Dual-read: set NOVA_LOCAL_TIER (preferred); ACDL_LOCAL_TIER fallback + # P5 (REQ-164): NOVA_* only (dual-read fallback removed) # kept for any unmigrated reader until P5 (removed in P5). - NOVA_LOCAL_TIER=1 ACDL_LOCAL_TIER=1 python3 core/local_emulators.py "$CONTRACT" \ + NOVA_LOCAL_TIER=1 python3 core/local_emulators.py "$CONTRACT" \ || fail "local E2E failed" echo "" echo "=== LOCAL E2E OK ===" @@ -287,14 +287,12 @@ if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then [ -f "$ENV_FILE" ] || fail ".env.secrets missing (run scripts/rotate_spike_key.sh) or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars" set -a . "$ENV_FILE" - set +a - # G-106 binding: dual-read NOVA_* first, ACDL_* fallback. The .env.secrets - # keys are renamed to NOVA_* in P2; the ACDL_* fallback covers operators - # who haven't rotated their local .env.secrets yet. Removed in P5. - export AWS_ACCESS_KEY_ID="${NOVA_AWS_ACCESS_KEY_ID:-$ACDL_AWS_ACCESS_KEY_ID}" - export AWS_SECRET_ACCESS_KEY="${NOVA_AWS_SECRET_ACCESS_KEY:-$ACDL_AWS_SECRET_ACCESS_KEY}" - export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION" -fi + set +a + # P5 (REQ-164): dual-read fallback removed — NOVA_* only. + export AWS_ACCESS_KEY_ID="$NOVA_AWS_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$NOVA_AWS_SECRET_ACCESS_KEY" + export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION" + fi echo "=== Step 4: terraform init + validate + plan -lock=false (real AWS) ===" cd "$TF_DIR" @@ -349,7 +347,7 @@ if ok: else: print(f'HITL BLOCK: {reason}', file=sys.stderr) sys.exit(1) -" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; } +" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; } else echo "Environment is dev — autonomous (no HITL gate)." fi @@ -465,7 +463,7 @@ if ok: else: print(f'HITL BLOCK: {reason}', file=sys.stderr) sys.exit(1) -" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; } +" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; } else echo "Environment is dev — autonomous (no HITL gate)." fi diff --git a/scripts/run_regression.sh b/scripts/run_regression.sh index c15d00c..a13e34f 100755 --- a/scripts/run_regression.sh +++ b/scripts/run_regression.sh @@ -20,7 +20,7 @@ cd "$ROOT" echo "=== Nova Regression VERIFY (D-091) ===" # Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). -echo "milestone: ${NOVA_REGRESSION_MILESTONE:-${ACDL_REGRESSION_MILESTONE:-v1.10}} phase: ${NOVA_REGRESSION_PHASE:-${ACDL_REGRESSION_PHASE:-52}}" +echo "milestone: ${NOVA_REGRESSION_MILESTONE:-v1.10} phase: ${NOVA_REGRESSION_PHASE:-52}" echo "" python3 core/regression_verify.py diff --git a/tests/test_env_helper.py b/tests/test_env_helper.py index 09ca516..4e82f03 100644 --- a/tests/test_env_helper.py +++ b/tests/test_env_helper.py @@ -1,14 +1,11 @@ -"""Unit tests for the dual-read env helper (core/env.py, D-108, REQ-159). +"""Unit tests for the NOVA-only env helper (core/env.py, D-108, REQ-164). -Covers the four cases: - - both NOVA_* and ACDL_* set (NOVA wins) - - only NOVA_* set - - only ACDL_* set (fallback) - - neither set (default returned) - -The ACDL_* fallback is the intentional dual-read source and is removed -in P5 (REQ-164). These fixtures deliberately keep the ACDL_* names as -the fallback source — they are the one allowed ACDL_* reference. +P5 (REQ-164) removed the ACDL_* dual-read fallback. `get_env` now reads +`NOVA_*` only. These tests verify: + - only NOVA_* set → returned + - neither set → default returned + - ACDL_* set but NOVA_* unset → default returned (fallback REMOVED) + - blank NOVA_* → default returned (not the ACDL_* value) """ from __future__ import annotations @@ -27,30 +24,33 @@ def _isolate_env(monkeypatch): yield -def test_both_set_nova_wins(monkeypatch): - monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value") - monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") - assert env.get_env("AWS_ACCOUNT_ID") == "nova-value" - - def test_only_nova_set(monkeypatch): monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value") assert env.get_env("AWS_ACCOUNT_ID") == "nova-value" -def test_only_acdl_set_fallback(monkeypatch): - # ACDL_* is the intentional dual-read fallback source (removed in P5). - monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") - assert env.get_env("AWS_ACCOUNT_ID") == "acdl-value" - - def test_neither_set_returns_default(): assert env.get_env("AWS_ACCOUNT_ID") is None assert env.get_env("AWS_ACCOUNT_ID", default="581513795199") == "581513795199" -def test_blank_nova_falls_back_to_acdl(monkeypatch): - # An explicitly-empty NOVA key must not shadow the ACDL fallback. +def test_only_acdl_set_no_fallback(monkeypatch): + # P5 (REQ-164): ACDL_* fallback removed — ACDL_* alone returns default. + monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") + assert env.get_env("AWS_ACCOUNT_ID") is None + assert env.get_env("AWS_ACCOUNT_ID", default="fallback") == "fallback" + + +def test_both_set_nova_wins(monkeypatch): + # NOVA_* takes precedence; ACDL_* is ignored. + monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value") + monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") + assert env.get_env("AWS_ACCOUNT_ID") == "nova-value" + + +def test_blank_nova_returns_default_not_acdl(monkeypatch): + # An explicitly-empty NOVA key returns default (not an ACDL_* value). monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "") monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") - assert env.get_env("AWS_ACCOUNT_ID") == "acdl-value" \ No newline at end of file + assert env.get_env("AWS_ACCOUNT_ID") is None + assert env.get_env("AWS_ACCOUNT_ID", default="d") == "d" \ No newline at end of file diff --git a/tests/test_lifecycle_mode_flag.py b/tests/test_lifecycle_mode_flag.py index 0b526fd..7dbc35f 100644 --- a/tests/test_lifecycle_mode_flag.py +++ b/tests/test_lifecycle_mode_flag.py @@ -32,15 +32,15 @@ class TestLifecycleModeFlag: """REQ-134: the lifecycle scripts dispatch to plan-only by default.""" @pytest.mark.parametrize("script", SCRIPTS) - def test_script_reads_acdl_lifecycle_mode(self, script): - """Every lifecycle script reads NOVA_LIFECYCLE_MODE (dual-read with - ACDL_LIFECYCLE_MODE fallback) with a 'plan' default.""" + def test_script_reads_nova_lifecycle_mode(self, script): + """Every lifecycle script reads NOVA_LIFECYCLE_MODE (NOVA-only since + P5, REQ-164 — dual-read fallback removed) with a 'plan' default.""" src = _read(script) - # P2 (REQ-159): dual-read NOVA_* preferred, ACDL_* fallback. The - # ACDL_LIFECYCLE_MODE:-plan substring is still present inside the - # nested dual-read expression (removed in P5). - assert "${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" in src, \ - f"{script} must dual-read NOVA_/ACDL_LIFECYCLE_MODE defaulting to 'plan'" + # P5 (REQ-164): NOVA_* only (dual-read fallback removed). + assert "${NOVA_LIFECYCLE_MODE:-plan}" in src, \ + f"{script} must read NOVA_LIFECYCLE_MODE defaulting to 'plan'" + assert "ACDL_LIFECYCLE_MODE" not in src, \ + f"{script} must not reference ACDL_LIFECYCLE_MODE (P5 removed the fallback)" assert "LIFECYCLE_MODE=" in src, \ f"{script} must assign LIFECYCLE_MODE from the env var" diff --git a/tests/test_output_publisher.py b/tests/test_output_publisher.py index 85c1ac5..ea6b802 100644 --- a/tests/test_output_publisher.py +++ b/tests/test_output_publisher.py @@ -43,7 +43,7 @@ class TestPublishToSsm: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") - monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm") + monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm") with mock_aws(): ssm = boto3.client("ssm", region_name="us-east-1") @@ -67,7 +67,7 @@ class TestPublishToSsm: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") - monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm") + monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm") with mock_aws(): ssm = boto3.client("ssm", region_name="us-east-1") @@ -82,7 +82,7 @@ class TestPublishToSsm: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") - monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm") + monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm") with mock_aws(): ssm = boto3.client("ssm", region_name="us-east-1") @@ -105,7 +105,7 @@ class TestPublishToSsm: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") - monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm") + monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm") with mock_aws(): ssm = boto3.client("ssm", region_name="us-east-1") @@ -123,7 +123,7 @@ class TestPublishToSsm: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") - monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm") + monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm") with mock_aws(): ssm = boto3.client("ssm", region_name="us-east-1") @@ -377,7 +377,7 @@ class TestCli: class TestKmsFailLoud: """P1-3: SSM publisher must fail loud when NOVA_KMS_KEY_ID is unset - (P2 renamed from ACDL_KMS_KEY_ID; dual-read NOVA_* preferred, + (P2 renamed from NOVA_KMS_KEY_ID; dual-read NOVA_* preferred, ACDL_* fallback until P5).""" def test_kms_unset_raises(self, monkeypatch): @@ -389,9 +389,9 @@ class TestKmsFailLoud: monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") # Both NOVA_* and ACDL_* unset → helper returns default (None) → fail loud. monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False) - monkeypatch.delenv("ACDL_KMS_KEY_ID", raising=False) + monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False) + monkeypatch.delenv("NOVA_ALLOW_DEFAULT_KMS", raising=False) monkeypatch.delenv("NOVA_ALLOW_DEFAULT_KMS", raising=False) - monkeypatch.delenv("ACDL_ALLOW_DEFAULT_KMS", raising=False) with mock_aws(): with pytest.raises(RuntimeError, match="NOVA_KMS_KEY_ID is not set"): @@ -405,7 +405,7 @@ class TestKmsFailLoud: monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False) - monkeypatch.delenv("ACDL_KMS_KEY_ID", raising=False) + monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False) monkeypatch.setenv("NOVA_ALLOW_DEFAULT_KMS", "1") with mock_aws(): diff --git a/tests/test_route_halt_artifact.py b/tests/test_route_halt_artifact.py index cd32442..16405a1 100644 --- a/tests/test_route_halt_artifact.py +++ b/tests/test_route_halt_artifact.py @@ -24,7 +24,7 @@ def test_route_halt_publishes_to_sns_when_arn_set(monkeypatch): assert call.kwargs["TopicArn"] == "arn:aws:sns:us-east-1:000000000000:nova-sod-halt" assert "contract-123" in call.kwargs["Message"] assert "SEPARATION_OF_DUTIES_VIOLATION" in call.kwargs["Message"] - assert call.kwargs["Subject"] == "ACDL SoD halt" + assert call.kwargs["Subject"] == "Nova SoD halt" def test_route_halt_falls_back_to_stderr_when_arn_unset(monkeypatch, capsys):