Compare commits

..

20 Commits

Author SHA1 Message Date
Jon Chery 64b2ba5076 docs(P18): complete module-standards-consistency phase (v1.13.21)
---ci---
project: acdl
phase: 18
milestone: v1.14
status: complete
requirements:
  covered: [REQ-152]
  partial: []
---/ci---
2026-07-29 21:23:54 +00:00
Jon Chery 7a4e43d11a docs(P17): complete config-and-persona-hygiene phase (v1.13.20)
---ci---
project: acdl
phase: 17
milestone: v1.14
status: complete
requirements:
  covered: [REQ-151]
  partial: []
---/ci---
2026-07-29 21:21:08 +00:00
Jon Chery cda06ab421 docs(P16): complete workflow-parity-and-script-flags phase (v1.13.19)
---ci---
project: acdl
phase: 16
milestone: v1.14
status: complete
requirements:
  covered: [REQ-150]
  partial: []
---/ci---
2026-07-29 21:18:15 +00:00
Jon Chery ec2311a3e0 docs(P15): complete untested-scripts-coverage phase (v1.13.18)
---ci---
project: acdl
phase: 15
milestone: v1.14
status: complete
requirements:
  covered: [REQ-149]
  partial: []
---/ci---
2026-07-29 21:13:03 +00:00
Jon Chery 4d694ba2e9 docs(P14): complete orphan-artifact-and-dead-config-cleanup phase (v1.13.17)
---ci---
project: acdl
phase: 14
milestone: v1.14
status: complete
requirements:
  covered: [REQ-148]
  partial: []
---/ci---
2026-07-29 21:10:20 +00:00
Jon Chery 3d9dd06411 docs(P13): complete kyverno-kube-version-resolution phase (v1.13.16)
---ci---
project: acdl
phase: 13
milestone: v1.14
status: complete
requirements:
  covered: [REQ-147]
  partial: []
---/ci---
2026-07-29 21:07:10 +00:00
Jon Chery b257846981 docs(P12): complete gitignore-credential-hygiene phase (v1.13.15)
---ci---
project: acdl
phase: 12
milestone: v1.14
status: complete
requirements:
  covered: [REQ-146]
  partial: []
---/ci---
2026-07-29 21:00:34 +00:00
Jon Chery 986171a165 docs(P11): complete schema-input-validation-hardening phase (v1.13.14)
---ci---
project: acdl
phase: 11
milestone: v1.14
status: complete
requirements:
  covered: [REQ-145]
  partial: []
---/ci---
2026-07-29 20:57:56 +00:00
Jon Chery 099ed015ac docs(P10): complete contract-ingestor-identity-validation phase (v1.13.13)
---ci---
project: acdl
phase: 10
milestone: v1.14
status: complete
requirements:
  covered: [REQ-144]
  partial: []
---/ci---
2026-07-29 20:52:42 +00:00
Jon Chery cc97a9308d docs(P09): complete iam-policy-least-privilege phase (v1.13.12)
---ci---
project: acdl
phase: 9
milestone: v1.14
status: complete
requirements:
  covered: [REQ-143]
  partial: []
---/ci---
2026-07-29 20:49:36 +00:00
Jon Chery c2ca0e4631 docs(P08): complete account-id-externalization phase (v1.13.11)
---ci---
project: acdl
phase: 8
milestone: v1.14
status: complete
requirements:
  covered: [REQ-142]
  partial: []
---/ci---
2026-07-29 20:46:28 +00:00
Jon Chery 225de0f613 docs(P07): complete swallowed-error-hardening phase (v1.13.10)
---ci---
project: acdl
phase: 7
milestone: v1.14
status: complete
requirements:
  covered: [REQ-141]
  partial: []
---/ci---
2026-07-29 20:43:08 +00:00
Jon Chery 69d8496107 docs(P06): complete alb-name-prefix-fix phase (v1.13.9)
---ci---
project: acdl
phase: 6
milestone: v1.14
status: complete
requirements:
  covered: [REQ-140]
  partial: []
---/ci---
2026-07-29 20:38:14 +00:00
Jon Chery 1aa525f234 docs(P05): complete adapter-behavior-tests phase (v1.13.8)
---ci---
project: acdl
phase: 5
milestone: v1.14
status: complete
requirements:
  covered: [REQ-139]
  partial: []
---/ci---
2026-07-29 20:35:53 +00:00
Jon Chery 81f111d462 docs(P04): complete regression-gate-evidence-hardening phase (v1.13.7)
---ci---
project: acdl
phase: 4
milestone: v1.14
status: complete
requirements:
  covered: [REQ-138]
  partial: []
---/ci---
2026-07-29 20:32:59 +00:00
Jon Chery 79e7a4a304 docs(P03): complete lifecycle-script-arg-cleanup phase (v1.13.6)
---ci---
project: acdl
phase: 3
milestone: v1.14
status: complete
requirements:
  covered: [REQ-137]
  partial: []
---/ci---
2026-07-29 20:24:13 +00:00
Jon Chery 8ae307affc docs(P02): complete static-assets-wiring-fix phase (v1.13.5)
---ci---
project: acdl
phase: 2
milestone: v1.14
status: complete
requirements:
  covered: [REQ-136]
  partial: []
---/ci---
2026-07-29 20:21:12 +00:00
Jon Chery 6e1a1bd7db docs(P01): complete adapter-dedup-diagnostic phase (v1.13.4)
---ci---
project: acdl
phase: 1
milestone: v1.14
status: complete
requirements:
  covered: [REQ-135]
  partial: []
---/ci---
2026-07-29 20:17:55 +00:00
Jon Chery 040abc0fb7 docs(ship): v1.13.3 complete — v1.14 pre-execution phase shipped (Gitea release id 255)
---ci---
project: acdl
phase: 0
milestone: v1.14
status: complete
---/ci---
2026-07-29 20:14:30 +00:00
Jon Chery 71bd61ceb1 docs(P00): complete pre-execution phase — v1.14 NFR Refinement milestone established
Phase 0 (pre-execution) complete. All pre-execution stages shipped:
SPECIFY -> CLARIFY -> RESEARCH -> IDEATE -> PLAN -> GRILL.

Established v1.14 NFR Refinement milestone (20 execution phases + 1
final). NFR milestone — final patch IS the release. Tags on v1.13.x
line: v1.13.3 (this phase) -> v1.13.24 (P21 = milestone release).

6 grill binding decisions (G-101..G-106) applied to PLAN.md. 1
escalation (E-001) auto-resolved at full autonomy (D-101).

---ci---
project: acdl
phase: 0
milestone: v1.14
status: complete
---/ci---
2026-07-29 20:13:49 +00:00
362 changed files with 8610 additions and 23824 deletions
+5 -377
View File
@@ -1,8 +1,8 @@
# Nova — Architecture (v1.1 target) # ACDL — Architecture (v1.1 target)
> Target architecture for the real Agentic Cloud Delivery Platform (rebranded > Target architecture for the real Agentic Cloud Delivery Platform.
> Nova in v1.15). Source of truth for **how**: `docs/architecture.md` (v0.2) is the upstream > 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 > draft; this file is the ACDL-repo operating copy, refined at phase
> boundaries. Where this file and `docs/vision.md` conflict, the vision wins. > boundaries. Where this file and `docs/vision.md` conflict, the vision wins.
## Status ## Status
@@ -570,376 +570,4 @@ emulator + live-AWS terraform init/validate/plan.
7. CloudFront OAC + WAF deprecated arg names (AWS provider v5): 7. CloudFront OAC + WAF deprecated arg names (AWS provider v5):
`signing_behavior`, `signing_protocol`, `origin_access_control_id`, `signing_behavior`, `signing_protocol`, `origin_access_control_id`,
`s3_origin_config.origin_access_identity`, `origin_id`, `rule` `s3_origin_config.origin_access_identity`, `origin_id`, `rule`
(singular), `scope=CLOUDFRONT` (uppercase). (singular), `scope=CLOUDFRONT` (uppercase).
## v1.11 Addendum — Stateless Adapter + Pipeline-Driven Lifecycle Testing
**Stateless adapter (D-098).** `adapters/terraform/adapter.py` rewritten
from a 918-line monolith (3 constant tables `TYPE_MAP`/`INPUT_MAP`/
`OUTPUT_MAP`, 39 type-specific branches) to a ~80-line stateless assembler.
Each L1 module ships a real `terraform/` module dir
(`versions.tf`/`variables.tf`/`locals.tf`/`main.tf`/`outputs.tf`) owning
its resource shape, nested blocks, and defaults. The adapter reads the
registry, emits a root `main.tf` instantiating each L1 as
`module "x" { source = "..." }` with resolved inputs and wired refs.
**Terraform owns lifecycle (D-101).** `scripts/run_platform.sh` gains
`--apply` and `--destroy` modes. Python never runs terraform.
`scripts/verify_deploy_microservice.py` is deleted.
**Pipeline-driven testing (D-102).** A `modules-lifecycle` pipeline
(Gitea + GitHub, byte-identical) matrix-runs each L1 module's
`examples/{simple,complex}.yml` contracts through apply→modify→destroy
against live AWS. No per-module Python/pytest. The "test" = the pipeline
cell going green.
**Single platform VPC (D-105).** `terraform/platform/main.tf` owns ONE
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`).
**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
`NOVA_LIFECYCLE_MODE` (default `plan`) overrides to `full` for the real
apply→modify→destroy. (P2P4 dual-read fallback to `ACDL_LIFECYCLE_MODE`;
fallback removed in P5 per the v1.15 addendum.)
## v1.12 Addendum — Presentation Refinement + CAP-013 Fix
**CAP-013 adapter dedup fix (REQ-129).** Multi-resource L1s (ecs-service,
alb) with stack outputs + cross-module refs now dedup to ONE module block
named by the composition child id, with expanded sub-ids rewritten via
`id_remap`. `terraform validate` succeeds for the microservice stack.
**CAP-017/018 probe fixes (REQ-130).** CAP-017's probe no longer requires
`locals.tf` for modules that legitimately omit it. CAP-018's probe
instantiates `LocalLambdaStub` with the required `outbox` arg.
## v1.13 Addendum — Presentation Polish + Config Schema Migration
**Config.json schema migration (v1.13.1).** Regenerated
`.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).
**Presentation polish (v1.13.0, v1.13.2).** Action headlines, story-arc
restructure, larger fonts, 6 new mermaid diagrams, badge cleanup,
platform-architecture diagram. Docs-only NFR patches.
## v1.14 Addendum — NFR Refinement (bug fixes, security, stubs, tests, docs)
**Bug fixes (Wave 1, P1-P6).** Adapter dedup rejects unregistered modules
with ValueError (P1). Static-assets composition wires cloudfront inputs
(P2). L2 lifecycle scripts document remote-state design (P3). Regression
gate adds `terraform fmt -check` syntax probe (P4). Adapter dedup-merge +
remote-state-key unit tests (P5). ALB target group name_prefix derives
from var.name (P6).
**Security (Wave 2, P7-P12).** 6 swallowed-error sites narrowed to
specific exceptions (P7). Account ID externalized to
`ACDL_AWS_ACCOUNT_ID` env (P8). IAM policy scoped to `acdl-*` ARNs (P9).
Contract ingestor validates contractId/environment/error (P10). Environment
schema adds `additionalProperties: false` + format validation (P11).
`.gitignore` credential-pattern catch-all (P12).
**Stub/test/CI/hygiene (Wave 3, P13-P17).** Kyverno `--kube-version` flag
removed (P13, G-103). Orphan artifacts + dead config cleaned (P14). 7
untested scripts gain test coverage (P15). Gitea workflow parity
documented + script `set` flags fixed (P16). Config.json persona +
branching strategy + ollama-cloud aligned (P17).
**Standards/docs/VPC (Wave 4, P18-P20).** STANDARDS.md reconciled (P18).
Documentation synced: ARCHITECTURE.md addenda, stale `@v1.6-1.9``@v1.13`,
GRILL G-005/G-008 resolved, COST.md window extended, D-083 deferral
recorded (P19). Platform VPC CIDR parameterized + data-driven subnet
count (P20).
**D-083 deferral (explicit).** The audit ledger build-out (S3 Object Lock
+ JWS detached signatures + SQS DLQ + async worker + daily checkpoints)
remains deferred (D-096, v1.14). The hash-chain + DynamoDB outbox is the
v1.14 audit record. JWS per-event authenticity is not implemented; a
forged event is only detectable by re-reading the whole chain. The
deferral is documented here explicitly per the v1.14 grill (E-001).
---
## v1.15 Addendum — Nova Rebrand (Major/breaking, 2026-07-30)
**Milestone:** v1.15-Nova. A full rebrand from **ACDL** / "Agentic Cloud
Delivery Platform" → **Nova** / "The New Dawn of DevSecOps — security
as a seamless enabler of fast deployments." This is a **Major
milestone** (breaking): consumer-facing path, env var prefixes, SSM
path, AWS tag keys, and AWS resource names all change. Per the
branch-strategy precedent (breaking/feature milestones tag on their
OWN minor line), v1.15 tags run on the **v1.15.x minor line**:
`v1.15.0` (P0) → `v1.15.4` (P5 final = release). (G-104 binding.)
### Naming conventions (rebranded)
| Convention | Before (v1.0v1.14) | After (v1.15+) | Phase |
|------------|---------------------|-----------------|-------|
| Project name | `ACDL` / "Agentic Cloud Delivery Platform" | `Nova` / "The New Dawn of DevSecOps" | P1 |
| Tagline | "Consumers declare intent; the platform delivers safe production deployment through an agentic stack" | (retained) **+** "The New Dawn of DevSecOps — security as a seamless enabler of fast deployments" | P1 |
| Schema `$id` URL | `https://acdl.cloudinit.dev/schemas/...` | `https://nova.cloudinit.dev/schemas/...` | P1 |
| Gitea release title | `ACDL vX.Y.Z` | `Nova vX.Y.Z` | P1 (forward only) |
| Env var prefix | `ACDL_*` (21 vars) | `NOVA_*` (dual-read fallback in P2P4; removed P5) | P2 |
| Env loader | scattered `os.environ.get("ACDL_*")` | centralized `core/env.py` `get_env()` (D-108) | P2 |
| Consumer contract path | `.acdl/contract.yml` | `.nova/contract.yml` | P2 |
| Checkov custom rule file | `acdl_tagging.py` | `nova_tagging.py` | P2 |
| Checkov tag-key enforcement | `acdl:*` (hard) | `nova:*` (warn P2, hard P3) | P2/P3 |
| SSM parameter path | `/acdl/{env}/{contractId}/{output}` | `/nova/{env}/{contractId}/{output}` | P3 |
| AWS tag keys | `acdl:owner|environment|contract|cost-center|ref` | `nova:owner|environment|contract|cost-center|ref` | P3 |
| ABAC session policy match | `acdl:*` tags | `nova:*` tags (parallel-tag period) | P3 |
| DynamoDB tables | `acdl-contracts`, `acdl-change-requests` | `nova-contracts`, `nova-change-requests` (scan+copy) | P4 |
| Lambda (ingestor) | `acdl-contract-ingestor` (role/policy/function) | `nova-contract-ingestor` | P4 |
| Secrets Manager secret | `acdl/github-token` | `nova/github-token` | P4 |
| SNS topic | `acdl-sod-halt` | `nova-sod-halt` | P4 |
| Security group | `acdl-ecs-sg` | `nova-ecs-sg` | P4 |
| KMS alias | `alias/acdl-platform` | `alias/nova-platform` | P4 |
| ECS cluster/service/task | `acdl-microservice` | `nova-microservice` | P4 |
| ECR repo | `acdl-microservice` | `nova-microservice` (re-push) | P4 |
| IAM user/policy | `acdl-spike-runner` (+policy) | `nova-spike-runner` (re-bootstrap) | P4 |
| S3 state bucket | `acdl-tfstate-581513795199-us-east-1` | `nova-tfstate-581513795199-us-east-1` (`-migrate-state`) | P4 |
| ALB name prefix | `acdl-alb` | `nova-alb` | P4 |
| Lambda default table names | `CONTRACTS_TABLE` default `acdl-contracts` | default `nova-contracts` (D-111) | P4 |
### Unchanged conventions (out of scope)
- **S&P Global Energy visual theme** (`sp-theme.json`, deck CSS: #D6002A
red, Akkurat Pro) — client branding, not the Nova product brand (D-107).
- **config.json `release.gitea.repo`** = `acdl` — real Gitea repo name
unchanged (D-105). Doc URLs updated to `nova` for prose only.
- **Git branch/tag naming** — `milestone/v*`, `phase/*`, `v*` semver; no
brand name present (D-112: flat-branch convention preserved).
- **Past Gitea release titles** — existing releases keep `ACDL vX.Y.Z`.
### Migration ordering (binding)
1. **P1** docs/decks/prose — no runtime impact; ships consumer migration
guide announcing the 5 breaking changes.
2. **P2** code + env vars (dual-read) + consumer path — deployments don't
break during the transition window (dual-read fallback).
3. **P3** SSM path (copy → read → delete) + tag keys (parallel-tag →
policy swap → remove old).
4. **P4** AWS resource names — staged terraform migration (KMS alias,
SNS/SG/Lambda recreate, DynamoDB scan+copy, ECR re-push, IAM
re-bootstrap, state bucket `-migrate-state`, ALB recreate). Maintenance
window + rollback runbook (`docs/NOVA_AWS_MIGRATION.md`).
5. **P5** final review + audit + remove dual-read fallback + milestone ship.
### Capability gate (binding)
The regression gate (CAP-001..CAP-016, `scripts/run_regression.sh`) must
stay **16/16 Verified** throughout the rebrand. P2/P3/P4 update test
fixtures that reference `ACDL`/`acdl` so the gate stays green. No
capability is added, removed, or reclassified in v1.15 — the rebrand is
nomenclature + identifiers, not behavior.
---
## v1.16 Addendum — Nova Simplification (NFR, 2026-07-30)
The v1.16 NFR milestone added 6 new code components + 1 new Terraform
module + 1 new schema, all documented here for the architecture record.
### New components
| Component | Path | Purpose |
|-----------|------|---------|
| Onboarding request handler | `core/onboarding.py` | `generate_env_file(request, template_env)` — produces a `<env>.json` from a consumer onboarding request (P19, REQ-183). CLI entry point for self-service env-file generation. |
| Decommission transform | `core/decommission_transform.py` | `decommission_transform(stack)` — zero counts + disable deletion protection (REQ-92). Extracted from contract_resolver (P12, REQ-176). |
| Contract resolver CLI | `core/contract_resolver_cli.py` | `main()` CLI entry point — resolves a contract YAML to a Target Stack JSON. Extracted from contract_resolver (P12, REQ-176). |
| Regression verify CLI | `core/regression_verify_cli.py` | `main()` CLI entry point — runs the regression gate + writes the report. Extracted from regression_verify (P13, REQ-177). |
| Workflow sync generator | `scripts/sync_workflows.py` | `--check`/`--write` — generates the 3 byte-identical Gitea+GitHub workflow pairs from `workflows-src/` (P8, REQ-172). |
| Onboarding Terraform | `terraform/onboarding/` | `aws_iam_role.consumer_deploy` + `aws_iam_role_policy.consumer_invoke` (ABAC `nova:owner` tag). Offline-proven only (P20, REQ-184, D-114). |
### Modified components
| Component | Change | Phase |
|-----------|--------|-------|
| `core/contract_resolver.py` | `_load_env` delegates to `environment_check.load()` (dedup); `is_l2` uses registry `kind` field; `_load_schema` caches schemas; `decommission_transform` + CLI re-export shim (P12). | P7, P12, P14 |
| `core/regression_verify.py` | Dedup helpers (`_check_resolver`, `_check_live_terraform_plan`, `_assert_contracts_resolve`); CAP-013..016 `Skipped` on post-teardown (G-111); `passed` accepts Skipped; CLI re-export shim (P13). | P5, P9, P13 |
| `core/lambda/contract_ingestor.py` | Fail closed on missing IAM identity (P10); env enum from `core/environments/` (P10); payload size cap + schema validation (P11); `onboard_consumer` action (P18); `[NOVA-ALERT]` rebrand (P2). | P2, P10, P11, P18 |
| `core/output_publisher.py` | `SAFE_OUTPUT_NAMES` schema-driven from `interface.json`; narrowed excepts; `urllib.error` import (P4, P14). | P4, P14 |
| `core/environment_check.py` | Onboarding message rebranded Nova + self-service request path (P2, P19). | P2, P19 |
| `core/local_emulators.py` | `LocalLambdaStub` sets `NOVA_LAMBDA_LOCAL_BYPASS`; stale dual-read comments + `acdl_*` prefixes removed (P3, P10). | P3, P10 |
| `scripts/run_platform.sh` | `--help` flag; `run_hitl_gate()` fn; `NOVA_CONTRACT_ID`/`NOVA_WORK_DIR` config; decommission + uptime blocks extracted to sourced helpers (P6, P9, P15). | P6, P9, P15 |
| `adapters/terraform/adapter.py` | State bucket `nova-tfstate-*` (P1); module docstring Nova (P2). | P1, P2 |
| `adapters/kyverno/policies/require-resource-labels.yml` | `nova:*` labels (not `acdl:*`) (P1). | P1 |
| `modules/registry.json` | `kind` field (`l1`/`l2`) on all 14 entries (P7). | P7 |
### New schema
- `schemas/onboarding.schema.json` — the self-service onboarding request
(consumerRepo, requestedEnvironment, ownerId, billingTag). P18, REQ-182.
### Onboarding request-path architecture (D-113)
The no-humans onboarding flow is a 3-step request path (real AWS
provisioning deferred):
```
Consumer → POST Lambda (onboard_consumer) → pending CMDB row (P18)
→ core/onboarding.py → <env>.json binding file (P19)
→ terraform/onboarding/ → cross-account role + ABAC tag (P20, offline)
```
The Lambda Function URL (IAM auth) + `consumer_invoke_policy.json` (ABAC
`nova:owner`) are the transport; the request is accepted + a binding
generated + the role Terraform proven offline. No AWS resources are
created by the request path (D-113/D-114).
### Regression gate (G-111 binding)
The regression gate (D-091) now treats `Skipped` as acceptable for the
post-v1.11-teardown steady state (D-096): CAP-013..016 (live-AWS tier)
return `Skipped` when the resources are absent (`NoSuchBucket`/
`ResourceNotFoundException`). `RegressionReport.passed` is
`all(r.status in ("Verified", "Skipped"))`. The gate passes at 18
Verified + 4 Skipped (0 Decayed/Broken).
## v1.17 Addendum — Strategic Direction, Leadership Metrics & Unified Story (2026-08-04)
The v1.17 milestone adds a telemetry/observability layer, a Decision
Ledger, a metrics export pipeline, a unified narrative deck, and a
durable strategic-direction artifact. This addendum documents the
architecture; the full research findings are in RESEARCH.md §v1.17.
### New components
| Component | Path | Purpose |
|-----------|------|---------|
| Event envelope | `core/metrics/event_envelope.py` | CloudEvents 1.0 envelope + `platform.*` semantic conventions (P1, REQ-187) |
| Per-run manifest writer | `core/metrics/run_manifest.py` | Emits `nova.run.started/completed/failed` events + writes `metrics/runs/<run_id>.json` (P1, REQ-187) |
| Decision Ledger (SQLite) | `core/metrics/decision_ledger.py` | Extends `outbox_writer.py` → SQLite append-only hash-chain table; `ai.decision.made` + `attestation.recorded` events + outcome backfill (P1, REQ-188, D-121) |
| Infracost post-processor | `core/metrics/infracost_adapter.py` | Runs Infracost on plan JSON; emits `nova.cost.estimated{delta_usd}` (P1, REQ-187, D-120) |
| Metrics collector | `core/metrics/collector.py` | Reads all grounded signals (files + events) → SQLite cold store at `metrics/nova_metrics.db` (P2, REQ-189) |
| PowerBI export | `core/metrics/powerbi_export.py` | Emits CSV/JSON views to `metrics/powerbi/` (fact + dim + 8 deferred placeholder views) (P3, REQ-190) |
| Metrics schemas | `schemas/metrics_*.schema.json` | Schemas for all event types + fact/dim tables (P1P2, REQ-187/189) |
| Metrics catalog | `docs/METRICS.md` + `docs/metrics/<kpi>.md` | Canonical catalog + per-KPI definition-of-success docs (P4, REQ-195, D-127) |
| Unified narrative deck | `docs/presentations/nova-no-humans-platform.md` | Merged deck: Problem→Vision→How→Proof→Roadmap; x3 arc at deck+slide level (P5, REQ-196/197, D-130) |
| Strategic direction | `.ciagent/NORTH_STAR.md` | PO-authored durable vision/objectives/anti-goals/targets; read by CIAgent in every future `/ci-run` (P0, REQ-185/186) |
### Modified components
| Component | Change | Phase |
|-----------|--------|-------|
| `core/outbox_writer.py` | Extended to emit to SQLite append-only hash-chain table (Decision Ledger); `ai.decision.made` + `attestation.recorded` events added (P1, D-121) | P1 |
| `scripts/run_platform.sh` | Per-run manifest writer invoked; `$WORK/*.json` persisted to `metrics/runs/`; Infracost post-processor invoked after plan (P1) | P1 |
| `core/hitl_gates.py` | Emits `attestation.recorded` event to Decision Ledger on qa/prod/dr gate (P1, D-132) | P1 |
| `core/confidence_signal.py` | Emits `nova.confidence.computed` + `nova.ai.decision.made` events (P1, D-122) | P1 |
| `adapters/terraform/policy/checkov_adapter.py` | Emits `nova.policy.evaluated` event (P1) | P1 |
| `core/regression_verify.py` | Emits `nova.capability.verified` event; CAP-023 (metrics collector) + CAP-024 (deck structure) added (P1, P6) | P1, P6 |
| `pyproject.toml` | `addopts` gains `--junitxml=metrics/test-results.xml` + `--json-report` (P1, D-120) | P1 |
| `docs/presentations/` | Two old decks retired (deleted); unified deck added (P5, D-130) | P5 |
### Telemetry/observability layer architecture (D-120)
```
┌─────────────────────────────────────────────────────────────────────┐
│ Nova platform components (existing) │
│ run_platform.sh · confidence_signal · checkov_adapter · │
│ hitl_gates · regression_verify · outbox_writer · contract_ingestor │
└──────────────────────┬──────────────────────────────────────────────┘
│ CloudEvents 1.0 envelope (new emitters, P1)
┌─────────────────────────────────────────────────────────────────────┐
│ metrics/events.jsonl (append-only CloudEvents log) │
│ metrics/runs/<run_id>.json (per-run manifests) │
│ metrics/decision_ledger.db (SQLite hash-chain, D-121) │
│ metrics/test-results.xml (junit, P1) │
└──────────────────────┬──────────────────────────────────────────────┘
│ collector reads (P2)
┌─────────────────────────────────────────────────────────────────────┐
│ metrics/nova_metrics.db (SQLite cold store, D-126) │
│ fact_run · fact_capability · fact_policy_check · fact_confidence │
│ fact_test · fact_decision · fact_cost_estimate │
│ dim_capability · dim_milestone │
│ + 8 empty placeholder views (deferred metrics) │
└──────────────────────┬──────────────────────────────────────────────┘
│ powerbi_export (P3)
┌─────────────────────────────────────────────────────────────────────┐
│ metrics/powerbi/ (CSV/JSON views, folder connector, D-129) │
│ → PowerBI dashboards (external) │
└─────────────────────────────────────────────────────────────────────┘
```
**Hot path: deferred (D-126).** No live ops dashboard; SQLite is
cold-only (batch/historical). The hot path activates when live AWS is
re-provisioned (D-096 lift).
### NORTH_STAR integration point (REQ-186)
`.ciagent/NORTH_STAR.md` is read by CIAgent in context-loading for all
future milestones. The integration mechanism (to be finalized in P4):
a reference from `PROJECT.md` + `ARCHITECTURE.md` (this section) + a
config entry in `config.json` (`strategic_direction_file:
".ciagent/NORTH_STAR.md"`) that the run workflow reads at SPECIFY. This
ensures the strategic direction survives across milestones without
being overwritten by status updates.
### §12.7 — Policy Engine Registry (v1.25, REQ-291)
The policy-engine abstraction is first-class: a swappable `PolicyEngine`
protocol so the engine may change without touching the confidence
signal, the pipeline, or the `PolicyCheckResult` schema. This is the
**swap boundary** that keeps the platform's compliance posture
replaceable (Strategic Objective #2 — provable trust via a replaceable
substrate, not a vendor lock-in).
```
contract.yml ─┐ ┌─→ list[PolicyCheckResult] ─┐
stack IR ─────┼─→ PolicyEngine.evaluate ├─→ list[PolicyCheckResult] ─┼─→ confidence_signal
plan JSON ────┤ (protocol) └─→ list[PolicyCheckResult] ─┘ (engine-agnostic,
PCR list ─────┘ unchanged)
┌─ KyvernoJsonEngine (shells to `kj scan`; engine: "kyverno")
└─ OpaEngine (future — same protocol; engine: "opa")
checkov/wiz ──→ raw findings ──→ (merged PCR list is the meta-policy payload)
```
**The protocol (`core/policy_engine.py`):**
```python
class PolicyEngine(Protocol):
@property
def name(self) -> str: ...
def is_configured(self) -> bool: ...
def evaluate(self, payload, policy_dir: Path, contract_id: str) -> list[dict]: ...
```
**The registry** reads `config.json.policy.engine` (default
`"kyverno-json"`) and returns the active engine. A `NullEngine` is the
fallback when the `policy` key is absent (emits `SKIPPED` PCRs —
backward compatibility for tests that don't set the key). The
confidence signal is **untouched** — it already consumes
`list[PolicyCheckResult]` engine-agnostically (§12.6). v1.25 only
changes *who produces* the PCR list, not *what* the list is.
**Engine enum reuse (D-116):** kyverno-json PCR records carry
`engine: "kyverno"` (no new enum value). The `engine` field records the
policy-engine *family*, not the specific binary. The K8s Kyverno adapter
and the kyverno-json engine are distinguished by `ruleId` prefix
(`KYVERNO_` vs `KJ_`) and `evidence` payload shape (`namespace`/`kind`
vs `assertion`/`jmespath`).
**Defense-in-depth (D-119):** the declarative meta-policy
`block-on-any-critical` (asserts no PCR has `severity: critical` +
`result: fail`) is the *source of truth* for "critical = block". The
`confidence_signal.py` `PENALTY["critical"]: None` hard-override stays
as the *imperative* safety net — the meta-policy runs *before* the
confidence signal (produces PCRs that flow in), the hard-override runs
*inside* it (the last gate). Removing the hard-override would make the
"critical = block" guarantee depend on a single policy file — a
regression in provable trust.
**Graceful degradation (D-120):** `KyvernoJsonEngine.is_configured()`
returns false when `which kj` is absent → `evaluate()` returns a single
`SKIPPED` PCR (`ruleId: "KJ_ENGINE_NOT_CONFIGURED"`). The platform
functions without the binary (the "platform functions without AI /
deterministic scripts" tenet holds — kyverno-json is deterministic, not
AI; the `is_configured()` guard ensures the platform runs even when the
binary is not installed).
+2 -309
View File
@@ -1,4 +1,4 @@
# Nova v1.9 — Audit Report # ACDL v1.9 — Audit Report
> Audit date: 2026-07-23. Auditor: ci-debugger. Milestone: v1.9. Result: PASS. > Audit date: 2026-07-23. Auditor: ci-debugger. Milestone: v1.9. Result: PASS.
@@ -243,311 +243,4 @@ Compared with `.ciagent/` files:
added a v1.10 addendum section covering all 4 new subsystems + the added a v1.10 addendum section covering all 4 new subsystems + the
7 adapter defect fixes. Verified all v1.10 components now referenced. 7 adapter defect fixes. Verified all v1.10 components now referenced.
## Audit result: PASS ## Audit result: PASS
---
# ACDL v1.14 — Post-Milestone Audit (ciagent-audit workflow)
> Audit date: 2026-07-29. Auditor: ci-debugger. Milestone: v1.14 (shipped,
> tag `v1.13.24`, Gitea release id 285). Result: PASS.
## Step 1: Reconstruction Test — PASS
Parsed all `---ci---` blocks from the v1.14 commit history (phase/00 +
milestone/v1.14-refinement branches). Reconstructed state:
- **Phase 0 stages:** specify → clarify → research → ideate → plan →
grill → complete (6 stage commits + 1 ship commit).
- **Phases 120:** each has an execute commit (on phase/NN branch) + a
complete commit (squash-merged into milestone/v1.14-refinement). All
20 `---ci---` blocks present with `project: acdl`, `phase: N`,
`milestone: v1.14`, `status: complete`.
- **Phase 21:** complete commit with `status: complete` + requirements
covered array.
- **Decisions:** D-095..D-101 all present in git log + `.ciagent/` files.
- **Grill binding decisions:** G-101..G-106 in GRILL.md + PLAN.md.
- **Escalation:** E-001 auto-resolved (D-101, full autonomy).
Compared with `.ciagent/` files:
- `config.json`: `active_milestone: v1.14`. **MATCH.**
- `ROADMAP.md`: v1.14 section with phases P0P21, all complete. **MATCH.**
- `REQUIREMENTS.md`: REQ-135..154 all complete in traceability table.
**MATCH.**
- `PROJECT.md`: v1.14 Objective + Key Decisions D-095..D-101 present.
**MATCH.**
- `CHECKPOINT.json`: phase=21, stage=complete, milestone=v1.14,
milestone_complete=true. **MATCH.**
- `ARCHITECTURE.md`: v1.11v1.14 addenda present. **MATCH.**
- `PLAN.md`: v1.14 20-phase plan with wave ordering. **MATCH.**
- `GRILL.md`: v1.14 grill run with G-101..G-106 + E-001. **MATCH.**
- `PERSONAS.md`: v1.14 frontmatter + roster. **MATCH.**
- `RESEARCH.md`: v1.14 addendum with 8-category scope audit. **MATCH.**
**Reconstruction: PASS** — state fully reconstructable from git log.
## Step 2: .ciagent/ File Discipline — PASS
- `config.json`: valid JSON; `active_milestone: v1.14`, `active_project:
acdl`, `projects[]` length 1. **PASS.**
- `PROJECT.md`: all required sections present (Objective v1.14, Key
Decisions D-095..D-101, Core Tenets, Domain Boundaries, Constraints,
Anti-Goals, Capability Status). 17 section headers. **PASS.**
- `ROADMAP.md`: v1.14 section with P0P21, all marked complete. **PASS.**
- `REQUIREMENTS.md`: v1.14 traceability table complete (20/20 REQ-135..154
marked complete). 172 `complete` references total. **PASS.**
- `ARCHITECTURE.md`: v1.11/v1.12/v1.13/v1.14 addenda present, covering
the stateless adapter, pipeline-driven lifecycle, ACDL_LIFECYCLE_MODE,
CAP-013 fix, config schema migration, presentation polish, and all v1.14
NFR changes. D-083 deferral recorded explicitly. **PASS.**
- `CHECKPOINT.json`: valid JSON; phase=21, stage=complete,
milestone_complete=true. **PASS.**
## Step 3: Branch Hygiene — PASS (with note)
- **v1.14 phase branches:** phase/00phase/21 all present locally. All
squash-merged into milestone/v1.14-refinement (the squash strategy
does not preserve ancestry for `--is-ancestor` checks, but the content
is verified present on main via the milestone merge commit `3b1181f`).
- **Milestone branch:** milestone/v1.14-refinement present, squash-merged
into main.
- **Prior milestone branches:** milestone/v1.11-restart,
milestone/v1.12-presentation, milestone/v1.13-deck-polish remain
locally (not pruned). These are historical and harmless.
- **Prior abandoned phase branches:** phase/56-iam-re-bootstrap,
phase/57-live-deploy-microservice (v1.11 first attempt, abandoned per
D-097). These have `---ci---` commits (not orphans) but are superseded.
Not a defect — documented in ROADMAP.md v1.11 RESTART section.
- **Remote:** origin/main + origin/milestone/v1.14-refinement present.
No orphan remote branches.
**Branch hygiene: PASS** — all v1.14 branches served their purpose; the
content is on main.
## Step 4: Commit Discipline — PASS
- **v1.14 commits with `---ci---` blocks:** 22/22 phase commits (phase 0
ship + phases 120 complete + phase 21 complete) have `---ci---` blocks
with `project: acdl`, `phase: N`, `milestone: v1.14`, `status:`. The
1 milestone merge commit (`91338f7`) lacks a `---ci---` block — it is
a squash-merge summary commit, not a phase commit. Acceptable.
- **Stale decisions:** D-095..D-101 all have code/doc refs (D-095/D-096/
D-097/D-099 are process/meta decisions in PROJECT.md; D-098 is the
wave ordering in PLAN.md; D-100/D-101 are ideation/escalation decisions
in PROJECT.md). No stale decisions.
- **Unresolved escalations:** E-001 auto-resolved (D-101,
`resolution: auto`, `type: risk_accepted`). No unresolved v1.14
escalations. The pre-v1.14 `resolution: user provided` match is from
the v1.1 bootstrap, not v1.14.
**Commit discipline: PASS.**
## Step 5: Audit Checks — PASS
1. **HEAD not on main when branches exist:** HEAD is on main (milestone
complete; no active phase work). OK — post-milestone state.
2. **CHECKPOINT.json exists:** EXISTS.
3. **CHECKPOINT.json consistent with git status:** checkpoint phase=21,
stage=complete, milestone=v1.14, milestone_complete=true. Matches
latest `---ci---` block (da533a8: phase=21, status=complete). **MATCH.**
4. **Report template exists:** EXISTS.
5. **No pending escalations:** E-001 auto-resolved. 0 unresolved v1.14
escalations.
6. **Milestone version in config:** `active_milestone: v1.14`. Consistent
with the milestone branch + checkpoint + git log. **MATCH.**
**Additional checks:**
- **Stale version refs:** `grep -rn "@v1\.[6-9]" docs/ README.md` → 0
hits (bumped to @v1.13 in P19). **PASS.**
- **Test suite:** 561 passed, 5 deselected. **PASS.**
- **Regression gate:** 22/22 capabilities Verified (run at P21). **PASS.**
- **CI pipeline:** `run_ci.sh` exits 0 (3 stages pass). **PASS.**
- **D-083 deferral:** explicitly recorded in ARCHITECTURE.md v1.14
addendum. **PASS.**
## Audit result: PASS
The v1.14 milestone is complete. All 20 requirements (REQ-135..154)
satisfied; 561 tests pass (was 528 at v1.13.2; +33); 22/22 capabilities
Verified; 6 grill binding decisions (G-101..G-106) applied; 1 escalation
(E-001) auto-resolved. State fully reconstructable from git log. 0 P0,
0 P1, 0 P2 outstanding. Ready for the next milestone.
---
## v1.15 Post-Milestone Audit (2026-07-30)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CIAgent ► AUDIT REPORT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Reconstruction: PASS — 27 commits since v1.14 base (66a3c69), 20 with
`---ci---` blocks (7 merge commits without blocks, per convention).
Reconstructed state: phase 5, milestone v1.15, complete, tag v1.15.4,
release 302, REQ-155..164 covered. Matches CHECKPOINT.json + REQUIREMENTS.md
+ ROADMAP.md.
.ciagent/ Files: 12 checked.
- config.json: valid JSON; active_milestone v1.15 consistent.
FIX applied: projects[0].name "Agentic Cloud Delivery Platform" →
"Nova — The New Dawn of DevSecOps" (rebrand completeness).
- PROJECT.md: FIX applied — header "# ACDL — Agentic Cloud Delivery
Platform" → "# Nova — The New Dawn of DevSecOps" + rebrand-in-progress
banner → rebrand-complete banner.
- REQUIREMENTS.md: FIX applied — header "# ACDL — Requirements" →
"# Nova — Requirements"; traceability 10/10 REQ-155..164 complete.
- ROADMAP.md: FIX applied — header "# ACDL — Roadmap" → "# Nova —
Roadmap"; v1.15 phases P1-P5 all complete with tags.
- ARCHITECTURE.md: PASS (header already Nova per P5 doc-verifier);
v1.15 addendum present; naming table matches codebase.
- PERSONAS.md: PASS (v1.15 addendum present).
- GRILL.md: PASS (v1.15 section present; 0 open escalations).
- RESEARCH.md: FIX applied — header "# ACDL — v1.11 RESTART Research
Findings" → "# Nova — ...".
- PLAN.md: PASS (v1.15 plan present, frontmatter milestone v1.15).
- AUDIT.md: FIX applied — header "# ACDL v1.9 — Audit Report" →
"# Nova v1.9 — Audit Report".
- REVIEW.md: FIX applied — header "# ACDL v1.11 — Multi-Persona Code
Review" → "# Nova v1.11 — ...".
- COST.md: FIX applied — header "# ACDL AWS Cost Report" →
"# Nova AWS Cost Report".
- IAM_POLICY.md: FIX applied — header "# ACDL — IAM Policy Baseline"
→ "# Nova — IAM Policy Baseline".
- CAPABILITY_INVENTORY.md: FIX applied — header "# ACDL Capability
Inventory" → "# Nova Capability Inventory".
Branches: 6 v1.15 phase branches (all merged to main), 1 milestone branch
(merged to main). No orphans. PASS.
Commits: 27 total, 39 `---ci---` blocks, 7 merge commits (no blocks, per
convention), 0 non-merge commits without `---ci---`, 0 unresolved
escalations. PASS.
Audit Checks (runAuditChecks):
1. HEAD on main (milestone complete) — PASS
2. CHECKPOINT.json exists — PASS
3. CHECKPOINT consistent with latest `---ci---` (phase 5, v1.15,
complete, v1.15.4) — PASS
4. Report template exists — PASS
5. No pending escalations (grill: 0 open; log: none) — PASS
6. Milestone version in config (v1.15) consistent with checkpoint — PASS
Issues fixed (audit auto-fix):
- 9 `.ciagent/*.md` file headers still said "ACDL" after the v1.15
rebrand (P1 lead-developer left `.ciagent/` to P0; P0 added the
rebrand-in-progress banner to PROJECT.md only; the other file
headers were never rebranded). All 9 headers now say "Nova".
- config.json `projects[0].name` still said "Agentic Cloud Delivery
Platform" (display label, not the repo slug). Now "Nova — The New
Dawn of DevSecOps". The `slug` ("acdl") + `release.gitea.repo`
("acdl") stay unchanged per D-105 (real repo name).
Notes:
- Historical narrative sections in ARCHITECTURE.md/COST.md/GRILL.md/
AUDIT.md/REVIEW.md (v1.1v1.14 addenda) still mention `acdl-*`
resource names + `ACDL_*` env vars — these describe each milestone
as-shipped and are acceptable as historical record per project
convention. The active v1.15 sections use Nova.
- The 7 merge commits without `---ci---` blocks is the established
convention (merge summary IS the record; the merged phase commits
carry the blocks). Matches v1.14 precedent.
Verdict: PASS — Project state is fully reconstructable from git log.
All 6 audit checks pass. 10 auto-fixed issues (9 stale headers + 1 config
name) were rebrand-completeness gaps, not structural defects.
---ci---
project: acdl
phase: 5
milestone: v1.15
status: complete
phase_role: final
audit: pass
---/ci---
---
## v1.16 Post-Milestone Audit (2026-07-30)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CIAgent ► AUDIT REPORT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
**Reconstruction: PASS** — 4 commits since v1.15.4 base (787a649), 3 with
`---ci---` blocks (1 merge commit without blocks, per convention — the
squash-merge summary IS the record). Reconstructed state: phase 21,
milestone v1.16, complete, tag v1.15.26, release 370, REQ-165..184
covered. Matches CHECKPOINT.json + REQUIREMENTS.md + ROADMAP.md.
**.ciagent/ Files: 15 checked.**
- config.json: valid JSON; active_milestone v1.16, active_project acdl,
projects[] length 1. **PASS.**
- PROJECT.md: v1.16 Objective (complete) + Key Decisions D-113..D-119
present. 44 section headers. **PASS.**
- ROADMAP.md: v1.16 section with P0P21, all complete; tags v1.15.5..26.
**PASS.**
- REQUIREMENTS.md: v1.16 traceability 20/20 REQ-165..184 complete.
**PASS.**
- ARCHITECTURE.md: **FIXED DURING AUDIT** — 0 v1.16 references → v1.16
addendum added (6 new components, 10 modified components, new schema,
onboarding request-path architecture, regression gate G-111). **PASS
(after fix).**
- CHECKPOINT.json: valid JSON; phase=21, stage=complete,
milestone_complete=true, tag=v1.15.26, release_id=370. **PASS.**
- PERSONAS.md: v1.16 addendum present (8 references). **PASS.**
- GRILL.md: v1.16 grill present (G-111..G-113, E-002). **PASS.**
- RESEARCH.md: v1.16 addendum present (R1..R6). **PASS.**
- PLAN.md: v1.16 20-phase + final plan present. **PASS.**
- REVIEW.md: **FIXED DURING AUDIT** — 0 v1.16 references → reconstructed
with v1.16 P21 final review content (0 P0, 0 P1, 2 P2 post-hoc). **PASS
(after fix).**
- AUDIT.md: this file (v1.16 audit recorded). **PASS.**
- CAPABILITY_INVENTORY.md: not modified in v1.16 (no capability changes).
**PASS.**
- COST.md: not modified in v1.16 (no cost changes — offline-only). **PASS.**
- IAM_POLICY.md: not modified in v1.16 (no IAM policy changes —
onboarding Terraform is offline-proven, not applied). **PASS.**
**Branches: 0 v1.16 phase branches, 0 v1.16 milestone branches** (all
cleaned up post-merge). Prior-milestone branches (v1.14 P1-P20, v1.11
P56-P59) remain locally — historical, harmless, documented in ROADMAP.
No v1.16 orphans. **PASS.**
**Commits: 4 total in v1.16 range, 3 with `---ci---` blocks, 1 merge
commit without (per convention), 0 unresolved escalations.** The
squash-merge strategy collapsed 20 phase branches + the milestone into
the merge commit `f83b974`; the phase-level `---ci---` blocks lived in
the (now-deleted) phase-branch commits. The milestone-level `---ci---`
block (commit `58fa7a6`) records the final state. **PASS.**
**Audit Checks (runAuditChecks):**
1. HEAD on main (milestone complete) — **PASS**
2. CHECKPOINT.json exists — **PASS**
3. CHECKPOINT consistent with latest `---ci---` (phase 21, v1.16,
complete, v1.15.26, release 370) — **PASS**
4. Report template exists (`opencode/ci/references/report-template.md`)
**PASS**
5. No pending escalations (grill E-002 auto-resolved at P21; 0
unresolved) — **PASS**
6. Milestone version in config (v1.16) consistent with checkpoint —
**PASS**
**Issues fixed during audit:**
- ARCHITECTURE.md missing v1.16 addendum (0 references → added: 6 new
components, 10 modified, new schema, onboarding architecture, G-111
gate).
- REVIEW.md held v1.11 content → reconstructed with v1.16 P21 final
review (0 P0, 0 P1, 2 P2 post-hoc accepted).
**Verdict: PASS** — Project state is fully reconstructable from git log.
All 6 audit checks pass. 2 auto-fixed issues (ARCHITECTURE.md addendum +
REVIEW.md reconstruction) were file-discipline gaps, not structural
defects. 20/20 requirements complete; regression gate 18V+4S; milestone
merged to main; tag v1.15.26; release 370.
---ci---
project: acdl
phase: 21
milestone: v1.16
status: complete
phase_role: final
audit: pass
---/ci---
-66
View File
@@ -1,66 +0,0 @@
# Nova — The Autonomous Cloud Delivery Platform: Autonomy Defensibility Brief
> Strategic direction, leadership metrics & unified story
> Last refined: v1.21 — reframe from "no-humans" to "autonomous operations"
## The thesis
Nova is the autonomous infrastructure layer that lets product teams
ship without engaging an operator, and lets executives trust the
platform not because it never fails but because every decision is
captured, scored, and accountable.
**Autonomy in operations; human at stage gates.** Normal operations —
provisioning, healing, remediation — run without an operator in the
loop. Human attestation remains required 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.
## Grounded proof (measurable today)
| Proof | Source | Status |
|-------|--------|--------|
| Capabilities verified, none broken (live-AWS caps honestly skipped, resources torn down to zero-cost steady state) | regression report | grounded |
| Decision Ledger captures 100% of automated decisions with outcome backfill | decision ledger store | grounded |
| Attestation coverage: 100% of prod/dr promotions attested by a human | attestation gates + outbox | grounded |
| Confidence-gated policy engine (deterministic, not an LLM) — weighted inputs, band outcome | confidence signal | grounded |
| Attestation matrix with separation-of-duties on prod | attestation matrix + separation-of-duties | grounded |
| Pre-apply cost estimates (offline) | cost adapter | grounded |
| Test suite passes | test results | grounded |
## Deferred proof (measurable when blocking work lifts)
| Proof | Blocking work | Unblock requirement |
|-------|----------------|---------------------|
| Touchless resolution rate across production estates | 0 consumers today | Pilot estate activation |
| Live infrastructure health (ECS, ALB, RPS) | Live AWS torn down | Live AWS re-provisioning |
| Onboarding funnel: requested → granted | Auto-grant not built | Auto-grant implementation |
| Drift auto-reversal rate | No drift scheduler | Drift detection scheduler |
| Predictive vs reactive ratio | No emitter | ML anomaly-forecasting service |
| Tamper-evident ledger checkpoints (S3 Object Lock + JWS) | Audit ledger build-out | Audit ledger build-out |
## Anti-claims (what Nova is NOT)
1. **Nova's decisions are NOT made by an LLM.** They are made by a
confidence-gated policy engine: deterministic scripts calculate a
score, and a band outcome gates the action. The platform functions
without AI. The Decision Ledger captures this real decision path —
not a fabricated "AI agent." When an LLM planner is added, it will
emit richer `alternatives_considered` without schema breakage.
2. **Nova does NOT remove humans from accountability.** Only from
normal operations. Every stage-gate promotion (qa/prod/dr) requires
a human attestation recorded with approver identity,
separation-of-duties check, and the evidence matrix.
3. **Nova is NOT for legacy, untagged, or freeform infrastructure.** It
requires Terraform-managed, policy-aligned, fully-tagged inputs.
4. **Nova does NOT fabricate metrics.** Every metric is grounded (cites
a source), derived (documented formula), or deferred (cites the
blocking work). No fabricated numbers in any deck slide or metrics
entry (the "no fabrication" hard constraint).
## What "won" looks like
By month 18, 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"* — and it is the layer their
AI engineering teams reach for first when an agent needs to deploy.
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova Capability Inventory — v1.1→v1.8 Re-Verification Sweep # ACDL Capability Inventory — v1.1→v1.8 Re-Verification Sweep
> Generated: 2026-07-27. Phase 54 (D-093). Milestone v1.10. > Generated: 2026-07-27. Phase 54 (D-093). Milestone v1.10.
> Source: PROJECT.md + ROADMAP.md v1.1→v1.8 advertised capabilities. > Source: PROJECT.md + ROADMAP.md v1.1→v1.8 advertised capabilities.
+4 -18
View File
@@ -1,22 +1,8 @@
{ {
"phase": 3, "phase": 0,
"stage": "complete", "stage": "complete",
"milestone": "v1.25", "milestone": "v1.14",
"phase_role": "execution", "phase_role": "pre_execution",
"attempts": 0, "attempts": 0,
"updated_at": "2026-08-12T17:30:00Z", "updated_at": "2026-07-29T20:30:00Z"
"project": "acdl",
"milestone_complete": false,
"tag_line": "v1.24.x",
"tag": "v1.24.3",
"next_tag": "v1.24.4",
"release": {
"forge": "gitea",
"releases_created": true,
"release_ids": {"v1.24.0": 640, "v1.24.1": 641, "v1.24.2": 642, "v1.24.3": 643},
"phase_release_id": 643
},
"requirements": ["REQ-291", "REQ-292", "REQ-293", "REQ-294", "REQ-295", "REQ-296", "REQ-297", "REQ-298", "REQ-299", "REQ-300", "REQ-301", "REQ-302", "REQ-303", "REQ-308", "REQ-309"],
"tests": {"total": 88, "passed": 88, "skipped": 11, "failed": 0},
"notes": "v1.25 P3 (plan-JSON+meta+pipeline) complete. Tag v1.24.3 (gitea release id 643). 4 requirements (REQ-300..303). 5 plan-JSON+meta policies. run_platform.sh Step 5b wired. Phase 03 branch deleted. Next: P4 regression-gate policies + docs."
} }
-164
View File
@@ -1,164 +0,0 @@
# CLARIFY — v1.25 kyverno-json Unified Policy Engine
> **Autonomy:** full. Ambiguities are auto-resolved with assumption logging
> per `config.json autonomy.level: "full"` and
> `autonomy.decision_confidence_threshold: 0.6`. No human escalation.
## Ambiguities Identified
### A1 — kyverno-json install path (pip / go install / pinned binary release)
**Ambiguity:** kyverno-json is a Go project, not a Python package. Three
install paths exist: (a) `pip install` — not possible (no PyPI package);
(b) `go install github.com/kyverno/kyverno-json/cmd/kj@latest` — requires
Go toolchain in the CI image; (c) download a pinned binary release from
GitHub releases — no Go toolchain needed, but release artifacts are
platform-specific and must be checksummed.
**Resolution (auto, confidence 0.85):** `go install` (option b). A
`scripts/install-kyverno-json.sh` helper runs
`go install github.com/kyverno/kyverno-json/cmd/kj@latest` and prints
`kj version`. The CI image (`.github/workflows/ci.yml` +
`.gitea/workflows/ci.yml`) installs Go + kj when
`config.json.policy.engine == "kyverno-json"`; the install is cached via
the existing Go module cache. Rationale: `go install` is the upstream-
blessed path, tracks the latest stable release, avoids per-platform
binary management, and the project already accepts Go-based tooling
(checkov pulls Go-built transitive deps via pip). When `which kj` is
absent, `KyvernoJsonEngine.is_configured()` returns false → `SKIPPED`
PCR (mirrors the Wiz adapter pattern) — the platform functions without
the binary. Captured in REQ-293, REQ-294. Decision ID: D-115.
### A2 — `engine` enum value: new `"kyverno-json"` vs reuse `"kyverno"`
**Ambiguity:** `schemas/policy_check_result.schema.json` already lists
`engine: ["checkov", "kyverno", "opa", "wiz"]`. kyverno-json is a
distinct runtime from the K8s Kyverno admission controller, but both
are "Kyverno." Two options: (a) add a new `"kyverno-json"` enum value
— requires schema change + checkov/wiz adapter test regression check;
(b) reuse `"kyverno"` and distinguish by `ruleId` prefix.
**Resolution (auto, confidence 0.80):** Reuse `"kyverno"` (option b).
Adding `"kyverno-json"` would force a schema change + a test sweep for
no semantic gain — the `engine` field records the policy engine family,
not the specific binary. kyverno-json PCR records carry `engine:
"kyverno"` and `ruleId` prefixed `KJ_<policy_name>` (e.g.
`KJ_REQUIRE_TAGGING_STANDARD`), while the K8s adapter uses `KYVERNO_`
prefixes (e.g. `KYVERNO_INACTIVE_TF_STACK`). The two are distinguishable
in audit/telemetry by `ruleId` prefix and `evidence` payload shape (the
K8s adapter's evidence has `namespace`/`kind`; kyverno-json's has
`assertion`/`jmespath`). No schema change. Captured in REQ-293.
Decision ID: D-116.
### A3 — Do checkov/wiz adapters change their signatures to feed kyverno-json?
**Ambiguity:** The unified-orchestrator model places kyverno-json "on
top of" checkov/wiz. Two interpretations: (a) checkov/wiz now emit a
"raw findings" intermediate (not PCR) that kyverno-json meta-policies
consume — requires changing `adapt() -> list[PolicyCheckResult]` to
`adapt() -> list[RawFinding]`; (b) checkov/wiz keep emitting PCRs as
today, and the meta-policies in `adapters/kyverno-json/policies/meta/`
consume the **merged** PCR list as their payload.
**Resolution (auto, confidence 0.90):** Option (b). The existing
`adapt() -> list[PolicyCheckResult]` signatures are unchanged. The
meta-policies consume the merged PCR list (checkov + wiz + kyverno-json
plan-JSON policies) as their input payload. This preserves the
`PolicyCheckResult` schema as the single inter-adapter contract
(ARCHITECTURE.md §12.6), avoids a new "RawFinding" type, and means
the existing checkov/wiz adapter tests pass unchanged. The meta-policy
`block-on-any-critical.json` iterates the merged list; the
`tagging-rules-agree.json` meta-policy cross-checks the Checkov
`NOVA_TAG_NAMING` result against the kyverno-json
`KJ_REQUIRE_TAGGING_STANDARD` result by `resourceRef`. Captured in
REQ-303, D-117. Decision ID: D-117.
### A4 — `NOVA_TAG_NAMING` Checkov rule: rewrite as kyverno-json policy, keep, or both?
**Ambiguity:** The Checkov custom rule
`adapters/terraform/policy/custom_rules/nova_tagging.py` enforces the
Nova tagging standard over Terraform HCL (static scan + plan scan). The
kyverno-json milestone adds `require-tagging-standard.json` over the
resolved Stack IR. Three options: (a) rewrite — replace the Checkov
rule with the kyverno-json policy (loses Checkov's HCL-level coverage
and the `--external-checks-dir` integration); (b) keep Checkov only —
don't add a kyverno-json policy (the Stack IR is already the input to
terraform, so the Checkov rule catches it); (c) both — keep the
Checkov rule as the source of truth for HCL-level scanning AND add the
kyverno-json policy for IR-level coverage, with a meta-policy that
asserts the two agree.
**Resolution (auto, confidence 0.82):** Option (c) — both, with a
cross-check meta-policy. The Checkov rule stays the source of truth
for `terraform_plan` scanning (it reads HCL resource blocks directly);
the kyverno-json policy covers the Stack IR dict (which is the input
*before* terraform, so it catches IR-level violations that the
terraform adapter might mask via defaults). The P3 meta-policy
`tagging-rules-agree.json` asserts the two engines agree on every
resource; divergence emits an `error` PCR (defense-in-depth against
rule drift — if the two engines disagree, the operator must
investigate before proceeding). This is the only case in v1.25 where
two engines evaluate the same concern; it is intentional — the
tagging standard is the highest-impact rule (v1.8 D-tagging-standard,
v1.10 re-verification) and merits redundancy. Captured in REQ-297,
REQ-303, REQ-299. Decision ID: D-118.
### A5 — Critical-override: delegate to declarative meta-policy or keep hard-override?
**Ambiguity:** `core/confidence_signal.py` lines 144-157 hardcode
`PENALTY["critical"]: None` — a critical-severity `fail` PCR forces
`score = 0, band = block` regardless of the weighted-sum inputs. The
v1.25 meta-policy `block-on-any-critical.json` makes this declarative
(asserts no PCR in the merged list has `severity: critical` +
`result: fail`). Two options: (a) fully delegate — remove the
hard-override, rely on the meta-policy to emit a critical `fail` PCR
that the existing penalty logic then blocks; (b) keep both — the
meta-policy is the declarative source of truth, the hard-override is
defense-in-depth.
**Resolution (auto, confidence 0.88):** Option (b) — keep both. The
meta-policy is the *declarative* statement ("Nova blocks on any
critical finding from any engine"); the hard-override is the
*imperative* safety net that ensures a critical PCR can never slip
through even if the meta-policy is misconfigured or the
`PolicyEngineRegistry` returns a `NullEngine`. This is
defense-in-depth, not redundancy-for-its-own-sake: the meta-policy
runs *before* the confidence signal (it produces PCRs that flow in),
the hard-override runs *inside* the confidence signal (it is the last
gate). Removing the hard-override would make the platform's
"critical = block" guarantee depend on a single declarative policy
file — a regression in the provable-trust posture (Strategic
Objective #2). Captured in REQ-303, PROJECT.md hard-constraints.
Decision ID: D-119.
### A6 — Does kyverno-json break the "platform functions without AI" tenet?
**Ambiguity:** NORTH_STAR.md Strategic Objective #2: "the platform
functions without AI — 'AI decisions' are really automated decisions."
kyverno-json is a deterministic policy engine (no ML), but it is a new
runtime dependency. Does adding it violate the tenet?
**Resolution (auto, confidence 0.95):** No — kyverno-json is
deterministic, not AI. The tenet distinguishes "AI decisions" (LLM-
driven, non-reproducible) from "automated decisions" (rule-driven,
reproducible). kyverno-json is the latter — the same policy + payload
produces the same result on every run. It is *more* aligned with the
tenet than the current imperative Python in `core/env_transition.py`
and `core/regression_verify.py`, because the policy is declarative
(visible, auditable, version-controlled) rather than imperative (logic
hidden in function bodies). The `is_configured()` guard ensures the
platform functions without the binary (graceful skip), so the tenet
holds even in environments where kyverno-json is not installed.
Captured in PROJECT.md hard-constraints + RESEARCH.md G-Q1.
Decision ID: D-120.
## Summary
6 ambiguities identified; 6 auto-resolved at full autonomy (no human
escalation). All resolutions are binding and recorded as D-115..D-120.
The resolutions are captured in PROJECT.md hard-constraints,
REQUIREMENTS.md v1.25 sections, and will be referenced in RESEARCH.md +
PLAN.md. No PROJECT.md or REQUIREMENTS.md structural changes beyond the
v1.25 sections added in SPECIFY — the resolutions are already embedded
in the requirement text (REQ-293, REQ-297, REQ-303, etc.) via the
"Decision" annotations.
+3 -3
View File
@@ -1,8 +1,8 @@
# Nova AWS Cost Report (v1.0 → v1.14) # ACDL AWS Cost Report (v1.0 → v1.10)
> **Query date:** 2026-07-29 (updated v1.14 P19) > **Query date:** 2026-07-28
> **Source:** AWS Cost Explorer (`ce:GetCostAndUsage`) > **Source:** AWS Cost Explorer (`ce:GetCostAndUsage`)
> **Window:** 2026-07-21 → 2026-07-29 (v1.0 ship → v1.14 active) > **Window:** 2026-07-21 → 2026-07-28 (v1.0 ship → v1.10 complete)
> **Account:** 581513795199 (us-east-1) > **Account:** 581513795199 (us-east-1)
> **Closes:** G-008 (no cost documentation despite live AWS resources) > **Closes:** G-008 (no cost documentation despite live AWS resources)
+292 -202
View File
@@ -1,216 +1,306 @@
# GRILL — v1.25 kyverno-json Unified Policy Engine # CIAgent Grill Report
> Adversarial review of the v1.25 SPECIFY + CLARIFY + RESEARCH + IDEATE + ## Run: 2026-07-27 19:30 (mode: interactive, focus: all)
> PLAN. The grill red-teams the proposal across feasibility, scope,
> budget, and the swap-boundary claim. Each challenge gets a binding
> verdict (PROCEED / REVISE / ESCALATE). Autonomy: full — escalations
> auto-resolve with assumption logging unless confidence < 0.60.
## Verdict: PROCEED (0.86) — 0 escalations, 2 revisions ### Verdict: Proceed with conditions (confidence: 0.72)
The milestone is feasible, scoped, and the swap boundary is real. Two Two escalations must be resolved before the leadership pitch:
plan revisions are binding (G-Q4, G-Q8) and are already captured in - **G-005 (risks):** 6 cloud capabilities (CAP-017..022) are deploy-unverified.
PLAN.md. No work is blocked. - **G-008 (budget):** No cost documentation exists despite live AWS resources.
The project is reclassified as an **OSS reference implementation** (G-003),
not a sponsored product. The grill's sponsor/ROI/budget/timeline axes apply
in weakened form; the adoption, architecture, and risks axes apply in full.
### Axis 1 — Business Case
- **Q1**: What problem does this actually solve, and is that problem still the top priority?
- Evidence: PROJECT.md:3-21 (vision + North Star); G-003 reframing (OSS reference)
- Answer: ACDL is an OSS reference implementation showing the shape of an agentic cloud delivery platform. The problem (cognitive load of infra + operational work of safe change) is documented in docs/vision.md.
- Confidence: 0.85
- Decision: G-003 — reframe as OSS reference implementation; no sponsor/ROI required.
- **Q2**: Who is the named executive sponsor, and when did they last make a decision under pressure?
- Evidence: MISSING (no named sponsor in any .ciagent/ file)
- Answer: Not applicable for an OSS reference implementation (G-003). Senior leadership requesting the pitch is interest, not sponsorship.
- Confidence: 0.85
- Decision: G-003 (carries forward).
- **Q3**: What happens to the business if the project is cancelled?
- Evidence: PROJECT.md:487 ("0 consumer adoption"); 10 milestones shipped with no consumers
- Answer: If cancelled, no consumer loses a deployed system. The reference value (clonable shape) persists in the repo. Cancellation cost is low — consistent with OSS reference framing.
- Confidence: 0.80
- Decision: G-003 (carries forward).
- **Q4**: Is the ROI calculated against a counterfactual?
- Evidence: MISSING (no ROI calculation anywhere)
- Answer: Not applicable for an OSS reference implementation. The bar is "is it a credible, demonstrable reference?" not "is there a paying customer?"
- Confidence: 0.85
- Decision: G-003 (carries forward).
### Axis 2 — Scope and Requirements
- **Q1**: Is the scope expanding, contracting, or genuinely stable?
- Evidence: ROADMAP.md (v1.0→v1.10, 55 phases); v1.7 added uptime-kuma + decommission + RDS; v1.9.x added decks; v1.10 added regression-class VERIFY + local emulators
- Answer: Expanding. The Out-of-Scope table (REQUIREMENTS.md:61-72) is scoped to v1.1 only; later milestones added scope without boundary updates.
- Confidence: 0.70
- Decision: G-010 — OSS scope is contributor-bounded; no out-of-scope table needed.
- **Q2**: Who owns the requirements, and have they been frozen?
- Evidence: REQUIREMENTS.md (115 REQs, REQ-01..REQ-115); config.json autonomy=full
- Answer: The user owns requirements via CLARIFY auto-resolution under full autonomy. Not frozen — each milestone adds REQs.
- Confidence: 0.70
- Decision: G-010 (carries forward).
- **Q3**: What is explicitly out of scope?
- Evidence: REQUIREMENTS.md:61-72 (v1.1 Out-of-Scope table only); PROJECT.md:42-51 (Domain Boundaries)
- Answer: Domain Boundaries section (PROJECT.md:42-51) defines durable out-of-scope: application business logic, IDE workflows, product backlog, node/OS-level compute. No per-milestone out-of-scope updates since v1.1.
- Confidence: 0.65
- Decision: G-010 — contributor-bounded scope accepted for OSS reference.
- **Q4**: Are there hidden requirements only disclosed late in delivery?
- Evidence: v1.10 milestone (decay disclosure, PROJECT.md:59-67) — 7 adapter defects undisclosed across 8 phases
- Answer: Yes — the v1.10 decay incident is a late-disclosed hidden requirement (reproducibility). D-091 regression gate is the mitigation.
- Confidence: 0.72
- Decision: G-007 (carries forward — milestone-level regression gate catches late-disclosed decay).
### Axis 3 — Architecture and Technical Feasibility
- **Q1**: Has the proposed architecture been validated by the people who will build and operate it?
- Evidence: PERSONAS.md (agent personas only); ARCHITECTURE.md (29KB); no human reviewer sign-off
- Answer: Validated by the agent that built it, not by a downstream platform team. Acceptable for an OSS reference (G-002 — Platform Team joins post-clone).
- Confidence: 0.72
- Decision: G-002 (carries forward).
- **Q2**: What is the integration surface?
- Evidence: ARCHITECTURE.md; adapters/ (terraform, wiz, kyverno, local emulators); contracts/ schema
- Answer: Contract schema (upstream) + engine adapters (downstream). Integration is bounded by the IR + PolicyCheckResult schemas.
- Confidence: 0.78
- Decision: (resolved by existing architecture; no new binding decision)
- **Q3**: Is there an existing system being replaced?
- Evidence: PROJECT.md:7-8 (vision: absorb cognitive load + operational work)
- Answer: ACDL replaces manual platform engineering + ticket-driven delivery. No existing system in this repo; downstream teams replace their own.
- Confidence: 0.75
- Decision: (resolved by G-002 white-label framing)
- **Q4**: What is the technical debt being inherited, and is it budgeted for?
- Evidence: v1.10 decay (7 adapter defects); D-091 regression gate at milestone completion (not per-phase)
- Answer: Diff-scoped VERIFY debt was paid down in v1.10. Per-phase regression gap is accepted debt (G-007).
- Confidence: 0.70
- Decision: G-007 — milestone-level regression gate is correct; inter-milestone decay is an accepted trade-off.
### Axis 4 — People, Skills, and Organization
- **Q1**: Which 2-3 people, if they left, would the project fail?
- Evidence: PERSONAS.md (agent personas); all binding decisions made by the user (D-034, D-090, G-001..G-012)
- Answer: One person — the user. Bus factor is 1.
- Confidence: 0.82
- Decision: G-011 — single-maintainer is normal for OSS reference; no action.
- **Q2**: Are the assigned resources actually allocated at the percentages claimed?
- Evidence: config.json (autonomy=full, max_concurrent_agents=5)
- Answer: The agent is the resource; allocation is 100% when invoked, 0% otherwise. No BAU fire-fighting claim to verify.
- Confidence: 0.78
- Decision: G-011 (carries forward).
- **Q3**: Is there a product owner with actual authority to prioritize?
- Evidence: config.json (autonomy=full, decision_confidence_threshold=0.6)
- Answer: The user is the product owner with absolute authority (full autonomy within user-locked constraints).
- Confidence: 0.80
- Decision: G-011 (carries forward).
- **Q4**: Is the team building capability they don't have?
- Evidence: RESEARCH.md (101KB); local emulating adapters (Phase 53) — capability was built and proven
- Answer: No — the agent built and verified the capability. Not a prototype-hoping-to-learn scenario.
- Confidence: 0.78
- Decision: (resolved by existing evidence)
### Axis 5 — Timeline and Estimates
- **Q1**: Was the deadline set before or after the scope was understood?
- Evidence: ROADMAP.md (v1.0 07-21 → v1.10 07-27, 6 days); no deadline documented anywhere
- Answer: No deadline. Milestones complete when the agent finishes committing.
- Confidence: 0.78
- Decision: G-006 — autonomous OSS build has no deadline; cadence is fine.
- **Q2**: What is the project's critical path?
- Evidence: MISSING (no critical path analysis)
- Answer: Not applicable — no deadline means no critical path to push.
- Confidence: 0.75
- Decision: G-006 (carries forward).
- **Q3**: Are the estimates evidence-based?
- Evidence: MISSING (no estimates; phases complete in agent-time)
- Answer: No estimates. The cadence is a function of agent speed, not engineering sizing.
- Confidence: 0.72
- Decision: G-006 (carries forward — acceptable for autonomous OSS reference).
- **Q4**: Is there a working definition of done?
- Evidence: VERIFY.md; AUDIT.md; 4-layer verify gate (structural, behavioral, security, quality)
- Answer: Yes — the 4-layer verify gate + regression gate (D-091) is the definition of done. "Done" is not "whatever the latest demo shows"; it is a gated, audited state.
- Confidence: 0.80
- Decision: (resolved by existing verify gate)
### Axis 6 — Budget and Financial Realism
- **Q1**: What percentage of the budget is already spent vs. remaining?
- Evidence: MISSING (no budget file in .ciagent/)
- Answer: Unresolved — no budget documented.
- Confidence: 0.50
- Decision: G-008 — ESCALATION.
- **Q2**: Are there predictable cost drivers not in the original budget?
- Evidence: config.json escalation_hooks (deploy, delete_data); CAP-013..016 verified against live AWS account 581513795199
- Answer: Yes — live AWS resources exist (S3 state, DynamoDB outbox, ECS, CloudFront). No cost driver documentation.
- Confidence: 0.60
- Decision: G-008 (carries forward — escalation).
- **Q3**: What's the burn rate, and how long until the money runs out?
- Evidence: MISSING
- Answer: Unresolved.
- Confidence: 0.40
- Decision: G-008 (carries forward — escalation).
- **Q4**: Is the budget contingent on something that hasn't happened yet?
- Evidence: MISSING
- Answer: Unresolved — likely contingent on the leadership pitch yielding a pilot platform team (G-001).
- Confidence: 0.55
- Decision: G-008 (carries forward — escalation).
### Axis 7 — Risks, Assumptions, and Dependencies
- **Q1**: What are the top 3 assumptions the plan rests on?
- Evidence: PROJECT.md:79-88 (CAP-017..022 IAM-gated); D-039 (OIDC federation deferred, blocked on go-gitea/gitea#36988); D-090 (no cap on re-verification sweep)
- Answer: (1) Terraform plan path proves deployability. (2) Local emulators prove runtime behavior. (3) Gitea OIDC will eventually merge.
- Confidence: 0.72
- Decision: (resolved by G-005 escalation)
- **Q2**: What are you dependent on outside the team?
- Evidence: PROJECT.md:79-88 (admin principal needed for IAM re-bootstrap); go-gitea/gitea#36988 (OIDC blocker)
- Answer: An admin AWS principal (for CAP-017..022) and the Gitea OIDC PR (for D-039 waiver closure).
- Confidence: 0.78
- Decision: G-005 (carries forward — escalation).
- **Q3**: What is the single risk that, if it materializes, kills the project?
- Evidence: CAPABILITY_INVENTORY.md §"Cloud capabilities NOT re-verified" (6 of 22 capabilities, 27%)
- Answer: The unverifiable deploy path for CAP-017..022. If the terraform plan path does not translate to a real deploy, 27% of advertised capability is fictional.
- Confidence: 0.80
- Decision: G-005 — ESCALATION.
- **Q4**: Have you done a pre-mortem?
- Evidence: MISSING (no pre-mortem document)
- Answer: No pre-mortem on file. The v1.10 decay incident is the closest thing to a post-mortem.
- Confidence: 0.65
- Decision: (flagged; no binding decision — user accepted autonomous governance in G-009)
### Axis 8 — Governance, Decision-Making, and Communication
- **Q1**: Who is the decision-maker when two executives disagree?
- Evidence: config.json (autonomy=full); no human governance body documented
- Answer: The user is the single decision-maker. No executive disagreement is possible because there is no executive body.
- Confidence: 0.78
- Decision: G-009 — autonomous CI is the governance.
- **Q2**: How often does governance meet, and what's the escalation pattern?
- Evidence: config.json (escalation_hooks: deploy, delete_data, merge_to_main; escalation_timeout_ms: 300000)
- Answer: Governance is event-driven (escalation hooks), not cadence-driven. 5-minute timeout.
- Confidence: 0.72
- Decision: G-009 (carries forward).
- **Q3**: What is being omitted from the status reports?
- Evidence: v1.10 decay disclosure (PROJECT.md:59-67) — 8 phases omitted the decay from status
- Answer: The v1.10 incident is direct evidence that status reports (decks) omitted material decay. D-094 (rewrite to verified reality) is the correction.
- Confidence: 0.75
- Decision: (resolved by D-094 + G-007 regression gate)
- **Q4**: Is there a "stop the project" trigger?
- Evidence: MISSING (no stop-trigger documented)
- Answer: No formal stop-trigger. The user is the single point of cancellation authority.
- Confidence: 0.68
- Decision: G-009 — autonomous CI is the governance; no human stop-trigger needed.
### Axis 9 — Change, Adoption, and Operational Readiness
- **Q1**: Who will use this, and what is in it for them?
- Evidence: PROJECT.md:487 ("0 consumer adoption"); G-001 (MVP for leadership pitch + pilot consumers)
- Answer: Pilot platform teams (post-pitch) will clone, customize, and deploy for their internal consumers. The value to them is a working reference shape.
- Confidence: 0.65
- Decision: G-001 — feature-complete MVP for pitch + pilot consumers in parallel.
- **Q2**: Is the operations/support team involved now or being handed a finished product?
- Evidence: MISSING (no Platform Team involvement in 55 phases); G-002 (white-label, out-of-repo)
- Answer: Intentionally out-of-scope — ACDL is white-label; Platform Team customization happens outside this repo.
- Confidence: 0.78
- Decision: G-002 — white-label; Platform Team customization is out-of-repo.
- **Q3**: What is the rollback plan if it goes wrong?
- Evidence: D-070 (decommission mode, 2-step pipeline with HITL SRE gates)
- Answer: Decommission mode exists for deployed stacks. For the reference repo itself, rollback = git revert (no production state to roll back).
- Confidence: 0.75
- Decision: (resolved by existing D-070 decommission mode)
- **Q4**: Has anyone validated the success criteria with the people who will judge success?
- Evidence: PROJECT.md (leadership pitch requested); no documented success-criteria validation with leadership
- Answer: The leadership pitch IS the validation moment. Success criteria for an OSS reference = "leadership says this is a credible shape."
- Confidence: 0.68
- Decision: G-001 (carries forward — pitch is the validation).
### Meta — Closing Review
- **Q1**: If you were the auditor, what would you flag?
- Evidence: This grill run
- Answer: (1) 6 unverifiable cloud capabilities (G-005). (2) No cost documentation (G-008). (3) Vision doc vs. OSS-reference framing tension (G-004 — resolved by keeping vision as target-state description).
- Confidence: 0.78
- Decision: (aggregated; G-005 + G-008 are the actionable flags)
- **Q2**: What is the project not doing that it should?
- Evidence: MISSING (no pre-mortem, no cost doc, no Platform Team engagement, no stop-trigger)
- Answer: Documenting the operating model (cost, deploy verification, governance) for a downstream team. The grill surfaced this across G-005, G-008, G-009.
- Confidence: 0.75
- Decision: (aggregated; G-005 + G-008 are the actionable items)
- **Q3**: What is the simplest possible version that could deliver 80% of the value?
- Evidence: ROADMAP.md (v1.1 spike, Phase 10, REQ-27 — core E2E proven); v1.2-v1.10 (45 phases of expansion)
- Answer: The v1.1 spike (contract → IR → terraform plan → Checkov → confidence → outbox) is the 80%-value version. The full 115-requirement build is accepted as the reference value (G-012).
- Confidence: 0.68
- Decision: G-012 — full catalog is the value; no minimal release needed.
- **Q4**: What would have to be true for this to succeed in the next 90 days, and is it true today?
- Evidence: G-001 (pitch + pilot); G-005 (IAM re-bootstrap); G-008 (cost doc)
- Answer: (1) Leadership pitch yields a pilot platform team — NOT TRUE today (pitch not yet delivered). (2) CAP-017..022 deploy path is verifiable — NOT TRUE today (G-005 escalation). (3) Cost operating model is documented — NOT TRUE today (G-008 escalation).
- Confidence: 0.72
- Decision: (aggregated; G-005 + G-008 + G-001 pitch are the 90-day conditions)
### Binding Decisions
| ID | Axis | Decision | Confidence |
|----|------|----------|-----------|
| G-001 | adoption | Feature-complete MVP for leadership pitch + pilot consumers in parallel; CIAgent builds, Platform Team deploys | 0.65 |
| G-002 | adoption | ACDL is white-label; Platform Team customization is out-of-repo; resolves ops-handoff concern | 0.78 |
| G-003 | business | Reframe as OSS reference implementation; no sponsor/ROI required | 0.85 |
| G-004 | business | Keep production-deployment vision; reference describes target state | 0.75 |
| G-005 | risks | ESCALATION — re-bootstrap IAM or mark CAP-017..022 deploy-unverified in decks | 0.80 |
| G-006 | timeline | Autonomous OSS build has no deadline; cadence acceptable | 0.72 |
| G-007 | architecture | Milestone-level regression gate is correct; system worked as designed | 0.70 |
| G-008 | budget | ESCALATION — add COST.md or document zero-cloud-cost operating model | 0.74 |
| G-009 | governance | Autonomous CI is the governance; no human stop-trigger needed | 0.68 |
| G-010 | scope | OSS scope is contributor-bounded; no out-of-scope table needed | 0.65 |
| G-011 | people | Single-maintainer is normal for OSS reference; no action | 0.70 |
| G-012 | meta | Full catalog is the value; no minimal release needed | 0.68 |
### Escalations
- **[G-005] risks** — 6 cloud capabilities (CAP-017..022: DynamoDB contracts table, Lambda contract-ingestor, ECS service live, CloudFront production stack, uptime-kuma, OIDC role) are deploy-unverified. The `acdl-spike-runner` IAM user cannot fix its own IAM (chicken-and-egg). Either re-bootstrap IAM with an admin principal to re-verify, or explicitly mark these 6 as "design-verified, deploy-unverified" in every leadership deck before the pitch. Resolves: project-killing risk (Axis 7 Q3).
- **[G-008] budget** — No cost documentation exists in `.ciagent/` despite live AWS resources (account 581513795199, CAP-013..016 verified). Either add a `COST.md` documenting monthly AWS spend, or explicitly document that ACDL runs at zero cloud cost (local emulators are the primary tier; live-AWS is a one-off spike per milestone). Resolves: financial-control gap (Axis 6 Q1-Q4).
--- ---
## Challenges ## Run: 2026-07-29 20:25 (mode: adversarial, focus: v1.14 NFR plan)
### G-Q1 — Does kyverno-json violate "platform functions without AI"? ### Verdict: FEASIBLE WITH BINDING DECISIONS (confidence: 0.72)
**Challenge:** NORTH_STAR.md Strategic Objective #2 says "the platform The v1.14 milestone is a sound, well-evidenced NFR sweep with a genuine,
functions without AI." kyverno-json is a new runtime dependency. Is traceable backlog. Not fundamentally infeasible. Four binding decisions
this a real violation, or is the tenet about LLMs (not deterministic close plan defects + unverified assumptions that would otherwise re-expose
engines)? the v1.11 4-VPC failure mode. One escalation (E-001) auto-resolved at full
autonomy with assumption logging.
**Verdict:** PROCEED (confidence 0.95). kyverno-json is deterministic ### 9-Axis scores
(same policy + payload → same result, every run). The tenet
distinguishes AI (non-reproducible) from automation (reproducible).
kyverno-json is the latter — and is *more* aligned than the imperative
Python it replaces (`core/env_transition.py`, `core/regression_verify.py`)
because the policy is declarative (visible, auditable). The
`is_configured()` guard ensures the platform runs without the binary.
Already resolved as D-120 in CLARIFY. No revision needed.
### G-Q2 — Is the PolicyEngine protocol over-engineered for a 2-engine future? | Axis | Confidence | Forcing question (short) |
|------|-----------|---------------------------|
| 1 Business | 0.80 | Real backlog (5 P1 + 4 P2 + 6 swallowed errors + 15+ hardcoded IDs); cancellation survivable but inherits decay risk |
| 2 Scope | 0.70 | User-directed + frozen; P13 has a hidden feature door (implement vs remove); P2 conditional-child edges past wiring |
| 3 Architecture | 0.62 | P8 grep unsatisfiable for backend blocks; P8 state-bucket continuity unguarded; P9 IAM naming unverified; P4/P8 file overlap |
| 4 People | 0.85 | Agentic single-operator; runtime availability is the key-person risk |
| 5 Timeline | 0.68 | No deadline; 20-phase unverified span is the longest since G-007; P8 is the latent multi-phase-rework risk |
| 6 Budget | 0.85 | NFR-only, no new AWS resources; P8 re-creation is a one-shot accident not structural cost |
| 7 Risks | 0.60 | A1 (acdl-* naming unverified), A2 (fallback constant unbound), A3 (P4 gate hardening); kill-risk = P8 orphans state |
| 8 Governance | 0.72 | Full autonomy; no mid-milestone stop trigger; per-phase "green" ≠ "capabilities Verified" |
| 9 Adoption | 0.70 | No external users; rollback is git-level for code, AWS-state rollback unaddressed if P8 misfires pre-detection |
**Challenge:** The user asked for a swappable adapter ("we might one ### Binding Decisions
day decide to replace it with something else like OPA"). A Python
Protocol + registry is ~40 lines. But Nova has 1 engine today. Is this
premature abstraction?
**Verdict:** PROCEED (confidence 0.85). The user *explicitly* asked for | ID | Axis | Decision | Confidence |
the swap boundary — this is not speculative abstraction, it's a |----|------|----------|-----------|
stated requirement. The protocol is minimal (3 methods) and the OPA- | G-101 | architecture | P8 grep scope amended to exclude terraform `backend "s3"` blocks (bucket arg is static-config-only, evaluated pre-init; cannot reference `data.aws_caller_identity`). Resource ARNs in policy/code ARE externalized; backend blocks stay literal or move to `-backend-config` (separate change). | 0.80 |
equivalent surface is documented (RESEARCH §4.2) — the swap is a known | G-102 | risks | P8 must bind `ACDL_AWS_ACCOUNT_ID` fallback to the live account ID (not a placeholder) AND the lifecycle workflow (full-mode jobs) must set `ACDL_AWS_ACCOUNT_ID` from `aws sts get-caller-identity` before any lifecycle invocation. No full-mode run proceeds with the env unset. | 0.78 |
quantity, not a hope. The cost is ~40 lines of Python + a config key; | G-103 | scope | P13 must take the removal+documentation path (remove `--kube-version` + document deferral to GitOps reconciler roadmap), NOT the implementation path. Implementing version-aware policy selection is a new feature, violating D-095. | 0.85 |
the benefit is a documented, tested swap boundary that a future | G-104 | architecture | P9 must verify (grep/audit of `modules/l1/*/terraform/main.tf` + `modules/l2/*/composition.json`) that every IAM role + KMS key created by the lifecycle pipeline matches `acdl-*` prefix before merge. CloudFront + WAFv2 (CloudFront scope) remain `Resource: "*"` with a documented global-ARN constraint. | 0.70 |
milestone implements without re-architecting. This is the moat (NORTH | G-105 | governance | P4's regression-gate hardening must be validated by running the full regression gate immediately after P4 lands (not deferred to P21). Gate must pass clean post-P4 before W2 begins. | 0.70 |
STAR Objective #2 — provable trust via a replaceable substrate, not a | G-106 | governance | A mid-milestone regression-gate checkpoint is added after W2 (P12), before W3 begins. Gate runs offline (D-091); a non-Verified result halts W3 until fixed. Not a re-litigation of G-007 (per-phase stays deferred) — a single checkpoint at the natural seam after the security wave. | 0.65 |
vendor lock-in).
### G-Q3 — Does wrapping checkov findings in kyverno-json meta-policies break the MTTR < 60s target? ### Escalations
**Challenge:** NORTH_STAR.md MTTR target: < 60s p95. Adding a second - **[E-001] risks** — P8 state-bucket continuity re-exposes the v1.11 4-VPC
engine pass over the terraform plan + a meta-policy pass over the root cause. G-102 proposes a binding mitigation (bind fallback + wire env
merged PCR list adds latency. Does this break the target? into workflow), but the residual risk (a future full-mode lifecycle run
with a misconfigured env orphans live state and re-creates resources)
**Verdict:** PROCEED (confidence 0.88). RESEARCH §5 analyzes: the kj cannot be reduced below 0.20 by plan-level decisions alone. **Auto-
pass over plan JSON is < 1s (Go binary startup + JMESPath over a small resolved at full autonomy (D-101):** accept the residual risk; G-102's
plan); it runs **in parallel** with Checkov (REQ-301), so wall-clock binding mitigation (fallback bound to live account ID + workflow env
impact is `max(checkov_time, kj_time)` ≈ checkov_time. Meta-policies wiring) is the control. The lifecycle pipeline defaults to plan-only
run in-memory over the merged list (< 10ms). Total MTTR impact: < 1s (REQ-134) — full-mode runs are workflow_dispatch only, reducing the
on a 5-15s step. **Binding revision (G-Q3a):** P3 VERIFY must include a accident surface. If the user prefers zero residual risk, direct that
timing assertion — `run_platform.sh` Step 5 wall-clock with vs without P8 exclude the state-bucket name from externalization entirely
kj must be within 1s (or kj must be faster than checkov, which is (externalize only resource ARNs, leave the backend `bucket` literal).
expected). Captured as a P3 verify gate, not a PLAN change. Confidence 0.55; auto-resolved per `config.autonomy.level=full`.
### G-Q4 — Plan revision: NullEngine fallback may mask misconfiguration
**Challenge:** PLAN.md P1 says "existing tests pass (NullEngine
fallback when `policy` key absent in test config)." But the v1.25
config.json *sets* the `policy` key. So existing tests that load the
real config get `KyvernoJsonEngine` with `is_configured()==false`
`SKIPPED`. The NullEngine fallback only triggers when the key is
*absent*. Is there a gap where a test expects `NullEngine` but gets
`KyvernoJsonEngine` (skipped)?
**Verdict:** REVISE (confidence 0.82). The fallback path is correct
but the PLAN wording is ambiguous. **Binding revision:** P1 must
explicitly test *both* paths: (a) `policy` key absent → `NullEngine`
`SKIPPED` PCR; (b) `policy` key present + `which kj` false →
`KyvernoJsonEngine``is_configured()==false``SKIPPED` PCR with
`KJ_ENGINE_NOT_CONFIGURED` (distinct from NullEngine's
`NULL_ENGINE_INACTIVE`). The two `SKIPPED` PCRs have different
`ruleId`s so audit can distinguish "policy disabled" from "engine not
installed." PLAN.md P1 verification is amended to assert both paths.
Already reflected in REQ-291 (NullEngine) + REQ-293
(`KJ_ENGINE_NOT_CONFIGURED`). No requirement change — PLAN wording
clarified.
### G-Q5 — Policy explosion: 4 targets × N rules = maintenance load
**Challenge:** v1.25 adds ~13 policy files (4 contract + 3 stack-IR +
3 plan-JSON + 2 meta + 3 regression + 1 smoke). Each is a YAML file
with JMESPath. Is this a maintenance burden that grows unbounded?
**Verdict:** PROCEED (confidence 0.80). 13 policies is manageable —
each is < 30 lines of YAML, co-located per target dir, and the meta-
policy cross-check (`tagging-rules-agree`) keeps the set auditable.
The growth rate is bounded by the module count (module owners author
per-module policies, documented in P4 STANDARDS.md). The alternative
(imperative Python in `regression_verify.py` + `env_transition.py`) is
*less* auditable — the policies are a net improvement. No revision.
### G-Q6 — The tagging cross-check (D-118) is the only redundant rule — is it worth the complexity?
**Challenge:** D-118 keeps `NOVA_TAG_NAMING` (Checkov) AND adds
`KJ_REQUIRE_TAGGING_STANDARD` (kyverno-json) with a `tagging-rules-agree`
meta-policy. This is the only case where two engines evaluate the same
concern. Is the defense-in-depth worth the complexity?
**Verdict:** PROCEED (confidence 0.82). The tagging standard is the
highest-impact rule (v1.8 D-tagging-standard, v1.10 re-verification —
the rule that gates every resource). Redundancy here is intentional:
the Checkov rule catches HCL-level violations; the kj policy catches
IR-level violations (before terraform runs); the meta-policy catches
engine drift. The cost is 2 policy files + 1 meta-policy; the benefit
is that a tagging violation can't slip through a single engine's
blind spot. This is the textbook defense-in-depth case. No revision.
### G-Q7 — Can `kj scan` actually evaluate the merged PCR list as a payload?
**Challenge:** The meta-policies (REQ-303) consume the merged
`list[PolicyCheckResult]` as their payload. `kj scan` expects a JSON/
YAML *file*. Is the PCR list a valid kyverno-json payload shape?
**Verdict:** PROCEED (confidence 0.85). The PCR list is a JSON array
of objects — a valid kyverno-json payload. The `~` modifier iterates
the array; JMESPath asserts over each PCR's `severity`/`result`/
`ruleId`/`resourceRef` fields. The engine writes the list to a temp
JSON file and invokes `kj scan --payload <file>`. This is verified in
P3 `test_meta_policies.py`. No revision — but **binding note (G-Q7a):**
the `KyvernoJsonEngine.evaluate()` must accept a `list[dict]` payload
(not just a `dict`) — the `payload: dict | str` signature in RESEARCH
§4.1 is too narrow. **Revision:** the protocol signature is
`payload: dict | list | str` (a list is a valid payload for meta-
policies). Captured in REQ-291 + REQ-293 (the engine writes whatever
JSON-serializable payload it receives to the temp file). PLAN.md P1
amended.
### G-Q8 — Plan revision: the OPA swap surface claims (RESEARCH §4.2) are unverified
**Challenge:** RESEARCH §4.2 documents the OPA-equivalent surface
(`opa eval -d <dir> -i <json>`), but no `OpaEngine` is implemented in
v1.25. Is the swap-boundary claim testable, or is it aspirational?
**Verdict:** REVISE (confidence 0.78). The swap-boundary claim is
*testable in v1.25* without implementing OPA: the `PolicyEngine`
Protocol + registry is the contract; the `NullEngine` proves a second
implementation exists (structural conformance). **Binding revision
(G-Q8a):** P1 `test_policy_engine.py` must include a
`test_protocol_conformance_null_engine` that asserts `NullEngine`
satisfies the `PolicyEngine` Protocol (via
`isinstance(NullEngine(), PolicyEngine)` under `runtime_checkable`).
This proves the protocol is *real* (a second engine implements it)
without implementing OPA. The OPA-equivalent surface in RESEARCH §4.2
stays as documentation (the future milestone implements it). PLAN.md
P1 verification amended. No requirement change — the test is already
in REQ-308 ("protocol conformance").
### G-Q9 — Budget: is 4 execution phases + P5 too many for the scope?
**Challenge:** v1.25 is 19 requirements across 6 phases. Recent
milestones: v1.24 had 15 reqs / 4 phases; v1.23 had 13 reqs / 7 phases.
Is 6 phases too many (overhead) or too few (per-phase overload)?
**Verdict:** PROCEED (confidence 0.85). 19 reqs / 6 phases ≈ 3.2 reqs/
phase — within the v1.24 cadence (3.75 reqs/phase). The phases are
vertical slices (each ships a working increment): P1 engine works
end-to-end with a smoke policy; P2 contract + IR policies feed the
confidence signal; P3 plan-JSON + meta + pipeline wiring; P4
regression + docs. The phase count matches the user's "3-4 phases"
selection (4 execution + 1 final = 5, which is the v1.24 shape). No
revision.
### G-Q10 — The `nova.cloudinit.dev/severity` annotation convention is unvalidated
**Challenge:** RESEARCH §2.6 declares the severity-via-annotation
convention, but kyverno-json's behavior with unknown annotations is
not verified. Does `kj scan` ignore unknown annotations, or does it
reject the policy?
**Verdict:** PROCEED (confidence 0.80). kyverno-json is Kubernetes-
style CRD-based — unknown `metadata.annotations` are preserved and
ignored (standard K8s behavior). The engine reads the annotation from
the loaded policy YAML (via `yaml.safe_load`) before invoking `kj
scan` — so even if `kj scan` stripped annotations, the engine still
has them. **Binding note (G-Q10a):** P1 `test_kyverno_json_engine.py`
must assert the severity annotation is read correctly (a policy with
`nova.cloudinit.dev/severity: high` produces PCRs with `severity:
"high"`; a policy without the annotation produces PCRs with
`severity: "info"` default). Captured in REQ-309 ("PCR schema
validity" includes severity). No requirement change — the test is
already in REQ-309.
---
## Summary
10 challenges; 10 resolved (8 PROCEED, 2 REVISE, 0 ESCALATE).
- **Revisions (binding, already in PLAN/REQs):**
- G-Q4: P1 tests both fallback paths (NullEngine vs
KyvernoJsonEngine-not-configured) — distinct `ruleId`s for audit.
- G-Q7a: protocol signature `payload: dict | list | str` (list is a
valid payload for meta-policies).
- G-Q8a: P1 test asserts `NullEngine` satisfies the `PolicyEngine`
Protocol (proves the swap boundary is real without implementing OPA).
- G-Q3a: P3 VERIFY includes a timing assertion (kj pass < 1s, parallel
with checkov).
- G-Q10a: P1 test asserts severity annotation is read correctly.
- **No requirement changes** — all revisions are clarifications to
PLAN.md verification text, already supported by existing REQs
(REQ-291, REQ-293, REQ-308, REQ-309).
- **0 escalations** — all challenges auto-resolved at full autonomy.
The milestone PROCEEDs to PHASE 0 SHIP → P1.
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova — IAM Policy Baseline (v1.11, REQ-116) # ACDL — IAM Policy Baseline (v1.11, REQ-116)
> Source of truth: `terraform/bootstrap/spike_runner_policy.json`. > Source of truth: `terraform/bootstrap/spike_runner_policy.json`.
> Applied as: customer-managed policy `acdl-spike-runner-policy` > Applied as: customer-managed policy `acdl-spike-runner-policy`
-157
View File
@@ -1,157 +0,0 @@
# IDEATE — v1.25 kyverno-json Unified Policy Engine
> **Autonomy:** full. 3-tier ideation per `config.json ideation.enabled:
> true`. `cross_project.enabled: false` → cross-project tier scoped to
> single-project (deferred ideas only, no cross-project candidates
> accepted). `confidence_threshold: 0.6`, `max_ideas: 20`.
> Categories: security, quality, architecture, coverage, improvement.
## Tier 1 — Mechanical (pattern-driven, codebase-grounded)
### I1 — Regression-gate-as-policy ✅ ACCEPTED (REQ-304, REQ-305)
**Category:** quality, coverage
**Confidence:** 0.90
**Pattern:** imperative check → declarative policy (the milestone's
core thesis applied to Nova's own regression gate).
**Source:** `core/regression_verify.py` (CAP-013, CAP-023, CAP-024)
are imperative Python checks. The milestone makes compliance
declarative; Nova's own capability regression should follow.
**Idea:** Port the three capability checks into
`adapters/kyverno-json/policies/regression/` as declarative policies
over the capability-inventory JSON frontmatter. The imperative
`regression_verify.py` stays (it drives the CI gate); the policies are
the declarative mirror that makes capability regression auditable as a
policy artifact.
**Accepted into:** REQ-304 (policies), REQ-305 (tests). Phase P4.
### I2 — Contract-shape validation as policy ✅ ACCEPTED (REQ-295)
**Category:** security, architecture
**Confidence:** 0.92
**Pattern:** jsonschema constraint → declarative policy (same constraint,
different language, Nova posture on top).
**Source:** `schemas/contract.schema.json` required/pattern/enum.
**Idea:** The 4 contract policies (`require-id-pattern`,
`require-env-in-enum`, `require-infrastructure-min-1`, `forbid-unknown-
fields`) are the declarative equivalent of the jsonschema constraints —
they let Nova apply its own compliance posture (e.g. forbid a specific
env for a specific consumer) on top of schema validity without editing
the jsonschema.
**Accepted into:** REQ-295. Phase P2.
### I3 — Stack-IR imperative rules → declarative policies ✅ ACCEPTED (REQ-297)
**Category:** security, architecture
**Confidence:** 0.88
**Pattern:** imperative Python rule → declarative kyverno-json policy.
**Source:** `adapters/terraform/policy/custom_rules/nova_tagging.py`
(tagging), the v1.0 demo `public-ingress: true` rule, the v1.8
D-encryption-default rule.
**Idea:** Port the three highest-impact imperative rules into
declarative kyverno-json policies over the resolved Stack IR. The
tagging rule is a cross-check (D-118 — both engines, agree meta-policy);
public-ingress and encryption-by-default are kyverno-json only (the IR
is the earliest point these can be caught).
**Accepted into:** REQ-297. Phase P2.
## Tier 2 — Backend-enriched (signal-driven)
### I4 — Plan-JSON Checkov RULE_MAP → kyverno-json mirrors ✅ ACCEPTED (REQ-300)
**Category:** security, coverage
**Confidence:** 0.85
**Pattern:** existing engine rule → declarative mirror in the new engine
(defense-in-depth against engine drift).
**Source:** `checkov_adapter.py:RULE_MAP` (CKV_AWS_41/45/46, CKV_AWS_1/40,
CKV_AWS_7/33).
**Idea:** Port the 6 Checkov rules over `terraform_plan` into declarative
kyverno-json policies over `terraform show -json` output. The Checkov
rules stay the source of truth for HCL scanning; the kyverno-json
policies are mirrors (different rule language, same plan JSON). Defense-
in-depth: if Checkov and kyverno-json disagree on the same plan, the
divergence is visible (two PCRs with different results for the same
resource).
**Accepted into:** REQ-300. Phase P3.
### I5 — Meta-policy over the merged PCR list ✅ ACCEPTED (REQ-303)
**Category:** architecture, quality
**Confidence:** 0.90
**Pattern:** the policy result list is itself a policy target (the most
novel use of kyverno-json in v1.25).
**Source:** `core/confidence_signal.py` PENALTY hardcode (critical
override), the D-118 tagging cross-check.
**Idea:** `block-on-any-critical` (declarative "critical = block") +
`tagging-rules-agree` (Checkov vs kj agree). The meta-policies consume
the merged PCR list as their payload. The critical-block meta-policy is
the declarative source of truth; the `confidence_signal.py` hard-override
stays as defense-in-depth (D-119).
**Accepted into:** REQ-303. Phase P3.
### I6 — Env-transition destroy as a declarative policy ❌ DEFERRED
**Category:** improvement
**Confidence:** 0.55 (below threshold — deferred, not rejected)
**Pattern:** imperative lifecycle Python → declarative policy.
**Source:** `core/env_transition.py` (v1.24 detect-and-destroy).
**Idea:** The v1.24 env-transition destroy logic (detect env change via
DynamoDB, destroy prior env, fail-closed) is imperative Python. A
declarative kyverno-json policy could assert "if `environment` changed
on a stable `contract.id`, a destroy event MUST precede the apply" —
turning the lifecycle enforcement into an auditable policy artifact.
**Reason deferred:** The env-transition logic is *stateful* (DynamoDB
queries, terraform state inspection) — kyverno-json policies are
*stateless* (payload in, PCRs out). A policy can assert the *contract*
shape (the env value is valid) but not the *lifecycle* (the prior env
was destroyed). The stateful check stays in `core/env_transition.py`;
a future milestone could emit a `nova.env.destroyed` event that a
kyverno-json policy then asserts is present in the evidence stream
(event-as-policy). Recorded as a future-idea, not a v1.25 requirement.
### I7 — Drift detection as policy ❌ DEFERRED
**Category:** security, coverage
**Confidence:** 0.40 (below threshold — deferred)
**Pattern:** scheduled job → policy over the drift report.
**Source:** NORTH_STAR.md Non-Goal #4 (drift detection scheduled job,
deferred — D-096 + no scheduler).
**Idea:** A kyverno-json policy over a terraform drift report could
assert "no drifted resources" declaratively. But drift detection itself
requires a scheduled `terraform plan -detailed-exitcode` job, which is
deferred (no scheduler). The policy is the easy part; the emitter is the
blocking dependency.
**Reason deferred:** Blocked by D-096 + no scheduler (same as NORTH_STAR
Non-Goal #4). The policy shape is documented for when the emitter ships.
## Tier 3 — Cross-project (deferred — single project)
### I8 — Cross-project policy sharing ❌ DEFERRED (config)
**Category:** improvement
**Confidence:** N/A
**Pattern:** policies shared across projects in a multi-project org.
**Source:** `config.json ideation.cross_project.enabled: false`.
**Idea:** In a multi-project org, kyverno-json policies could be shared
across projects (a tagging standard policy applies to all projects).
**Reason deferred:** ACDL is single-project (`active_projects: ["acdl"]`).
Cross-project ideation is disabled in config. Recorded for when the
org grows.
## Summary
- 5 ideas accepted (I1..I5) → already captured as REQ-295, REQ-297,
REQ-300, REQ-303, REQ-304, REQ-305.
- 3 ideas deferred (I6, I7, I8) with documented blocking reasons.
- 0 ideas rejected (below-threshold ideas are deferred, not rejected —
they may activate when their blockers lift).
- The accepted ideas are the **quality improvement** the user asked for
("ideate and explore how it can be used within the Nova platform to
improve quality of the platform checks"): I1 (regression-gate-as-
policy) is the headline quality improvement; I4 + I5 are the defense-
in-depth coverage improvements; I2 + I3 are the architecture
improvements (imperative → declarative).
- No new requirements added beyond REQ-291..309 (the accepted ideas are
already scoped into the existing requirements). The IDEATE pass
validated the requirement set rather than expanding it — the ideas
were anticipated in the SPECIFY stage and explicitly captured.
-232
View File
@@ -1,232 +0,0 @@
# NORTH_STAR — Nova
> **Status:** Draft (pending interactive GRILL → final)
> **Milestone:** v1.21 — Nova Deck Refinement & Pipeline Hardening
> **Owner:** Product Owner
> **Purpose:** Durable strategic intent. Read by CIAgent in every future
> `/ci-run` so the platform's direction survives across milestones. This
> is NOT a status document (that's PROJECT.md) and NOT an engineering
> architecture (that's the telemetry reference in RESEARCH.md/
> ARCHITECTURE.md). It is the PO's committed direction: what we're
> building toward, what we refuse to build, and how we'll know we won.
---
## 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 — QA signs off for
> production, SRE greenlights based on operational readiness — but the
> operator is never in the loop of normal operations.**
Nova is the autonomous infrastructure layer that lets product teams ship
without engaging an operator, and lets executives trust the platform not
because it never fails but because every decision is captured, scored,
and accountable. The recurring theme across the platform is that
**infrastructure operations become visible** — security posture,
remediation velocity, reliability, and lead time are surfaced as
queryable signals rather than hidden in tribal knowledge.
---
## Strategic Objectives (4)
**1. Demonstrate production-grade zero-touch operations.**
Nova must run real customer estates with no human in the loop of normal
operations — autonomy as the default, not the demo. Stage-gate
attestation (QA for production, SRE for operational readiness) remains
human by design; operational escalations (AI confidence too low to
proceed) are the failure mode we drive toward zero. Everything else
collapses if autonomy isn't real.
**2. Establish provable trust in automated decisions.**
Trust is established by deterministic scripts that calculate a score and
a band outcome that gates the action — the platform functions without AI.
"AI decisions" are really automated decisions. The audit substrate —
Decision Ledger, confidence scoring, circuit breakers, blast-radius
controls — turns "autonomous" from a marketing claim into a defensible
one. Trust is the moat. Features can be copied; an immutable, queryable
decision history cannot.
**3. Deliver compounding, quantifiable ROI for customers.**
Each quarter on Nova must show measurable improvement on four CTO-grade
metrics, all of which flow into PowerBI views and are captured by the
telemetry pipeline:
- **Lead Time** — from PR merge to production deployment (downward trend).
- **Infrastructure Vulnerability Count** — open findings on deployed
resources (downward trend, demonstrating that proactive scanning +
remediation 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.
If leadership cannot point to a number that improves quarter-over-quarter
on these four axes, Nova fails its commercial test, regardless of how
clever the automation is.
**4. Integrate with externally owned development platforms — regardless of source.**
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 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. Nova is the
layer any of those platforms reach for first when an agent needs to
deploy — not a vendor arriving late to that market.
---
## Anti-Goals (4 — what Nova is fundamentally NOT)
1. **Not a general-purpose AI agent platform.** We are purpose-built for
infrastructure operations. Breadth here produces shallow tools; depth
here wins the category.
2. **Not a system that removes humans from accountability.** Only from
normal operations. Every automated decision lands in an immutable
ledger. Every stage-gate promotion (qa/prod/dr) requires a human
attestation recorded with approver identity, separation-of-duties
check, and the evidence matrix. The absence of an operator in the
loop is never the absence of a record.
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 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
product team. Nova makes their intent production-grade; it does not
own the intent.
---
## Non-Goals (v1.17 milestone scope — deferred work, not permanent boundaries)
> Anti-Goals are what Nova *fundamentally is not*. Non-Goals are what we
> *will not do this milestone* — deferred work, not permanent boundaries.
> Each Non-Goal cites the controlling decision ID.
1. **Live AWS re-provisioning** (deferred — D-096). Metrics that require
live infrastructure ship as placeholder PowerBI views with documented
schemas.
2. **Onboarding auto-grant** (deferred — D-113/D-114/D-119). Only the
request-path metric is grounded; the requested→granted funnel is a
placeholder.
3. **ML anomaly-forecasting / predictive remediation** (no emitter today).
The Predictive-vs-Reactive metric ships as a placeholder.
4. **Drift detection scheduled job** (deferred — D-096 + no scheduler).
Drift metrics ship as placeholders.
5. **Live cost CUR reconciliation** (deferred — D-096). Pre-apply Infracost
estimates are grounded; actual-spend reconciliation is a placeholder.
6. **S3 Object Lock / JWS tamper-evident ledger** (deferred — D-083). The
Decision Ledger uses a local SQLite hash-chain this milestone; the
Object-Lock/JWS build-out is a future milestone.
7. **Multi-cloud support** (Azure/GCP/K8s). Nova is AWS-only this milestone.
---
## 1218 Month Targets
Targets are committed, not aspirational. Each is a number a board member
can repeat back to us. The grounding column records whether the metric is
measurable this milestone, and if not, what blocks it.
> **Honesty note (GRILL G-Q6 binding):** Nova has 0 consumer adoption
> today (`PROJECT.md:495`). Three targets (Touchless Resolution, Human
> Escalation, AI Decision Accuracy) are scoped "across production
> estates" — the measurement *pipeline* is grounded this milestone, but
> the *denominator* is zero until a pilot estate activates. These
> targets are reclassified as **Post-Pilot** (the pipeline works; the
> numbers fill when consumers exist). This is the same honesty model as
> Cloud Spend Reduction (partial: pipeline grounded, actuals deferred).
### Current-milestone targets (grounded or derived this milestone)
| Domain | Target | Grounding (v1.17) | Note |
|---|---|---|---|
| **MTTR (p95)** | < 60 seconds | grounded (platform-run MTTR) | apply.failed → successful retry; infra-incident MTTR deferred (no incident detection) |
| **Cloud Spend Reduction** | ≥ 25% on pilot estates vs. 12-month pre-Nova baseline | partial | pre-apply estimate grounded (Infracost); actual-spend deferred (D-096 CUR) |
| **L1 / L2 Ops Hours Avoided** | ≥ 70% of pre-Nova FTE allocation | derived | formula over run count × manual baseline (computed on N internal runs; production-denominator activates post-pilot) |
| **Platform ROI** | ≥ 250% measured annually | derived | formula (labor savings + cloud savings + avoided downtime) ÷ platform op cost (computed on N internal runs; production-denominator activates post-pilot) |
| **Decision Ledger Coverage** | 100% of AI actions with backfilled outcome | grounded (this milestone builds it) | outbox_writer.py → SQLite hash-chain |
| **Attestation Coverage** | 100% of prod/dr promotions attested by a human | grounded | hitl_gates.py + outbox approver_* attributes; separation-of-duties on prod |
### Post-Pilot targets (pipeline grounded this milestone; denominator activates when a pilot estate runs)
| Domain | Target | Grounding (v1.17) | Note |
|---|---|---|---|
| **Touchless Resolution Rate** | ≥ 99% across production estates | partial (pipeline grounded; denominator = 0 today) | runs completing without *operational* HITL block ÷ total runs (attestation gates excluded); activates post-pilot |
| **Human Escalation Frequency** | < 0.1% of platform actions | partial (pipeline grounded; denominator = 0 today) | *operational* HITL blocks only (confidence-driven); attestation sign-offs excluded; activates post-pilot |
| **AI Decision Accuracy** | ≥ 99.5% (no rollback, no follow-up incident within 5 min of action) | partial (pipeline grounded; denominator = 0 today) | decisions not followed by apply.failed/incident within 5min; activates post-pilot |
### Deferred targets (measurement requires future systems)
| Domain | Target | Grounding (v1.17) | Note |
|---|---|---|---|
| **Predictive vs. Reactive Ratio** | ≥ 3 : 1 (prevention dominates reaction) | deferred | requires ML forecasting service (future emitter) |
| **Drift Auto-Reversal Rate** | ≥ 95% within one detection cycle | deferred | requires drift detection (D-096 + scheduler) |
> Committed targets whose measurement is deferred remain committed — the
> target is the destination; the metric is the odometer, and some
> odometers aren't built yet. Each deferred metric ships as a placeholder
> PowerBI view + a definition-of-success doc recording the dependency.
> Post-Pilot targets are committed targets whose measurement pipeline is
> grounded this milestone; the numbers activate when a pilot estate runs.
### Future Horizons (strategic direction, not committed targets)
| Domain | Aspiration | Note |
|---|---|---|
| **AI-Agent Intent Share** | ≥ 40% of total intent volume originated by non-human consumers | Strategic Objective #4 direction. No backing requirement, no placeholder view, no emitter today. Moves to a committed target when agentic consumption is real. |
---
## Success Criteria (v1.17 — what constitutes success for THIS milestone)
> Distinct from the 1218mo targets: those are the destination. These are
> the milestone's exit criteria.
v1.17 is a success if:
1. **Decision Ledger emits `ai.decision.made` for 100% of platform runs**
with outcome backfill, AND **`attestation.recorded` events for 100%
of qa/prod/dr promotions** (event completeness — all 3 gates captured;
grounded in `outbox_writer.py` → SQLite hash-chain; honors D-083).
The **Attestation Coverage metric** (target 100%) measures prod/dr
promotions specifically — see REQ-194.
2. **`docs/METRICS.md` catalogs every executive KPI** with a `grounded` /
`derived` / `deferred` status, a source file or decision ID, and a
per-KPI definition-of-success doc in `docs/metrics/`.
3. **The PowerBI export produces all fact/dimension views** + 8 empty
placeholder views for deferred metrics (with documented schemas ready
to fill when their blocking decisions lift).
4. **The unified narrative deck ships** with the x3 arc
(Problem→Vision→How→Proof→Roadmap) at deck + slide level, per-slide
benefit callouts, and fluid transitions; both old decks retired.
5. **`NORTH_STAR.md` is wired into CIAgent context-loading** so every
future `/ci-run` reads it.
6. **CAP-023 (metrics collector) + CAP-024 (deck structure) pass** in the
regression gate.
---
## What "won" looks like
By month 18, 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"* — and it is the default substrate
their AI engineering teams reach for first when an agent needs to deploy.
---
## Relationship to v1.17 engineering
- **Pillar A (this file):** strategic direction — durable, PO-authored.
- **Pillar B (engineering):** the telemetry reference architecture
(adapted from the PO's technical-direction input) lives in
RESEARCH.md/ARCHITECTURE.md. It is the *how*; this file is the *why*.
- **Pillar C (story):** the unified narrative deck proves Pillars A+B to
leadership. The deck's Proof section cites grounded metrics; its
Roadmap section cites deferred targets honestly.
+131 -112
View File
@@ -1,132 +1,151 @@
--- ---
project: acdl project: acdl
milestone: v1.25 milestone: v1.14
generated_at: 2026-08-12 generated_at: 2026-07-29
generator: lead-developer generator: lead-developer
verification_toolchain: verification_toolchain:
typecheck: "python3 -m py_compile core/policy_engine.py adapters/kyverno-json/kyverno_json_engine.py tests/test_policy_engine.py tests/test_kyverno_json_engine.py" typecheck: "terraform validate && python3 -m py_compile core/**/*.py && python3 -m jsonschema schemas/*.schema.json"
test: "pytest tests/test_policy_engine.py tests/test_kyverno_json_engine.py tests/test_adapter.py tests/test_contract_resolver.py tests/test_confidence_signal.py tests/test_checkov_adapter.py tests/test_kyverno_adapter.py tests/test_pipeline.py -v" test: "bash scripts/run_primitive_plan.sh --check-only <primitive> # pipeline-driven (D-102); no per-module pytest"
lint: "ruff check core/policy_engine.py adapters/kyverno-json/ 2>/dev/null || python3 -m py_compile core/policy_engine.py" build: "terraform init && terraform plan"
note: | note: |
v1.25 is the kyverno-json Unified Policy Engine milestone — a feat ACDL has no package.json. The execute/verify/ship workflows substitute
milestone. Four active personas: lead-developer (coordination + `terraform validate` + `python -m py_compile` + JSON Schema validation
docs + ARCHITECTURE.md §12.7), backend-engineer (core/policy_engine.py for npm run typecheck, a per-phase verify script (or the
protocol + registry + contract_resolver.py wiring + run_platform.sh modules-lifecycle pipeline cell) for npm test, and `terraform init` +
Step 5 + pipeline tests), policy-engineer (adapters/kyverno-json/ `terraform plan` for npm run build. v1.11 testing is pipeline-driven
engine + policies across all 4 target dirs + meta-policies + policy (D-102): the modules-lifecycle pipeline matrix-runs each L1 module's
tests + adapter README + STANDARDS.md policy-authoring section), examples/{simple,complex}.yml contracts through apply→modify→destroy
data-engineer (config.json policy object + schemas/README.md note + against live AWS. No per-module Python/pytest. This override is
capability-inventory JSON fixture for regression policies). documented here as the single source of truth; the ci-* agents read
frontend-engineer stays deactivated (no UI). The policy-engineer is a PERSONAS.md before running verification commands.
new custom persona created for this milestone's policy domain (see v1.14 note: NFR-only milestone (bug fixes, security, tests, docs).
RESEARCH.md §4 — kyverno-json + JMESPath is a distinct framework from Roster carries forward from v1.11 unchanged. frontend-engineer stays
backend-engineer's fastify/hono). inactive (no frontend; decks are markdown = lead-developer
territory). No custom personas needed (no new domains).
--- ---
# ACDL — Persona Roster (v1.25 kyverno-json Unified Policy Engine) # ACDL — Persona Roster (project-level, v1.11 RESTART)
> v1.25 roster. Four active personas + one deactivated. This is a feat > v1.11 is a restart (D-097). The v1.9 roster is superseded. Three
> milestone: the work is a swappable policy-engine protocol + a new > structural corrections: (1) stateless adapter (D-098), (2) terraform
> adapter + policies across 4 Nova artifacts + pipeline wiring + docs. > owns lifecycle (D-101), (3) pipeline-driven testing (D-102). The roster
> The policy-engineer is a new custom persona — kyverno-json + JMESPath > is simplified to the three active domains: data (terraform foundation),
> is a specialized domain that doesn't fit backend-engineer's > backend (adapter/resolver), general (pipelines/workflows).
> fastify/hono frameworks or data-engineer's drizzle/postgresql.
## Active personas ## Active personas
### lead-developer ### lead-developer
- **Domain:** coordination + docs - **Domain:** coordination
- **Frameworks:** [] - **Active:** true
- **Constraints:** ["pragmatic", "battle-tested defaults", "docs match code", "swap boundary is the moat"] - **Phase-specific:** false
- **Territory:** - **Reason:** Owns CIAgent metadata, cross-phase verification scripts, the v1.11 phase orchestration (D-107: P56a + P56b split), and arbitrates persona conflicts. Resolves the milestone decomposition and the STANDARDS.md §8 rewrite (the adapter extension pattern is replaced by the per-module terraform subdir pattern).
- `.ciagent/ARCHITECTURE.md` (§12.7 Policy Engine Registry — NEW)
- `.ciagent/PROJECT.md` (v1.25 section)
- `.ciagent/REQUIREMENTS.md` (v1.25 section)
- `.ciagent/ROADMAP.md` (v1.25 section)
- `.ciagent/PLAN.md`, `.ciagent/RESEARCH.md`, `.ciagent/CLARIFY.md`,
`.ciagent/GRILL.md`, `.ciagent/PERSONAS.md`
- `docs/METRICS.md` (swappable engine narrative — REQ-307)
- **Reason:** Owns the milestone coordination + the architecture
narrative. The swap boundary (PolicyEngine protocol) is the moat per
Strategic Objective #2 — the lead-developer owns the boundary
description in ARCHITECTURE.md §12.7 and the docs/METRICS.md note.
No Python policy code (backend-engineer + policy-engineer territory).
No UI (frontend-engineer deactivated).
### backend-engineer ### backend-engineer
- **Domain:** backend (Python + bash + pipeline wiring) - **Domain:** backend
- **Frameworks:** ["boto3", "terraform"] - **Active:** true
- **Constraints:** ["api-first", "strict-typing", "engine-agnostic confidence signal", "fail-soft when kj absent"] - **Phase-specific:** false
- **Territory:** - **Reason:** Owns the adapter rewrite (D-098: stateless assembler — deletes TYPE_MAP/INPUT_MAP/OUTPUT_MAP + 39 type-specific branches, becomes a ~80-line assembler that emits `module "x" { source = "..." ... }` blocks) and the contract resolver env-aware state keys (D-106: `spike/{id}/{env}/terraform.tfstate`). The adapter holds no module content; the engine binding lives in the per-module `terraform/` subdir. Co-authoring expected on the adapter + `run_platform.sh` boundary (general adds `--apply`/`--destroy` modes that invoke the adapter).
- `core/policy_engine.py` (NEW — PolicyEngine Protocol + PolicyEngineRegistry + NullEngine) - **Territory:** `adapters/terraform/adapter.py` (rewrite to stateless assembler), `core/contract_resolver.py` (env-aware state keys, deterministic composition), `schemas/stack.schema.json` (if the stack instance shape changes), `tests/test_adapter*.py` (regression baseline — the s3 instance.json round-trip must still pass).
- `core/contract_resolver.py` (MODIFIED — invoke registry pre/post resolve)
- `scripts/run_platform.sh` (MODIFIED — Step 5 kyverno-json parallel pass)
- `scripts/install-kyverno-json.sh` (NEW)
- `tests/test_policy_engine.py` (NEW — protocol conformance, registry, NullEngine)
- `tests/test_run_platform_plan_json_policies.py` (NEW — script-substring assertion)
- `.github/workflows/ci.yml` + `.gitea/workflows/ci.yml` (MODIFIED — Go + kj install)
- **Reason:** Owns the Python protocol layer + the pipeline wiring. The
`PolicyEngine` Protocol + `PolicyEngineRegistry` are Python structural-
typing constructs (PEP 544) — backend-engineer's strict-typing
constraint. The `contract_resolver.py` wiring + `run_platform.sh`
Step 5 are backend territory. Does NOT write kyverno-json policy
files (policy-engineer territory) — only the Python that *invokes* the
engine. Does NOT modify the confidence signal (it already consumes
`list[PolicyCheckResult]` engine-agnostically — PROJECT.md hard-
constraint).
### policy-engineer
- **Domain:** policy (declarative compliance rules)
- **Frameworks:** ["kyverno-json", "jmespath", "kyverno ValidatingPolicy"]
- **Constraints:** ["declarative-policies", "no-imperative-rules", "schema-validated", "severity-via-annotation", "assertion-trees-not-foreach"]
- **Territory:**
- `adapters/kyverno-json/` (NEW — engine impl + __init__.py + README)
- `adapters/kyverno-json/kyverno_json_engine.py` (NEW — KyvernoJsonEngine)
- `adapters/kyverno-json/policies/` (NEW — all 4 target dirs: contract/, stack-ir/, plan-json/, meta/, regression/)
- `adapters/kyverno-json/policies/_smoke.json` (NEW)
- `adapters/README.md` (MODIFIED — new adapter row + PolicyEngine Protocol section)
- `tests/test_kyverno_json_engine.py` (NEW — PCR schema validity, defensive parsing)
- `tests/test_stack_ir_policies.py` (NEW)
- `tests/test_plan_json_policies.py` (NEW)
- `tests/test_meta_policies.py` (NEW)
- `tests/test_regression_policies.py` (NEW)
- `tests/fixtures/stack_ir/`, `tests/fixtures/plan_json/`, `tests/fixtures/capability_inventory.json` (NEW)
- `modules/STANDARDS.md` (MODIFIED — Policy authoring standard section — REQ-307)
- **Reason:** The policy-engineer owns the declarative policy artifacts.
kyverno-json's `ValidatingPolicy` + assertion trees + JMESPath is a
distinct framework from backend-engineer's fastify/hono and requires
its own constraints: no imperative rules (everything is an assertion
tree), severity via the `nova.cloudinit.dev/severity` annotation (not
in the engine adapter), no `forEach` (use the `~` modifier). The
adapter pattern (engine ↔ protocol ↔ registry) is backend-engineer
territory, but the policy *content* and the engine *translation*
(`_to_pcr()`) are policy-engineer territory because they require
kyverno-json output-shape knowledge. Created per RESEARCH.md §4 — this
is a phase-spanning persona (active for P1..P4), not phase-specific.
### data-engineer ### data-engineer
- **Domain:** data (config schema + structured fixtures) - **Domain:** data
- **Frameworks:** ["jsonschema", "yaml"] - **Active:** true
- **Constraints:** ["schema-first", "type-safe config", "backward-compatible additions"] - **Phase-specific:** false
- **Territory:** - **Reason:** Reactivated for v1.11. Owns the heaviest territory: the per-module `terraform/` subdirs (D-098/D-099/D-100 — the engine binding) for all 12 L1 modules, plus the single platform VPC (D-105: `terraform/platform` owns ONE VPC; the microservice composition drops its `vpc` child and references the platform VPC via data source). Each L1 module ships a real terraform module dir (versions/variables/locals/main/outputs.tf) owning its resource shape, nested blocks, and defaults. `locals.tf` is used heavily to centralize default interpolation (D-099). Multi-resource modules get the full 5-file split; trivial single-resource modules may inline locals in main.tf. This is the binding constraint — the stateless adapter cannot be written until the reference s3 module exists (D-107: P56a proves the design with s3 first).
- `.ciagent/config.json` (MODIFIED — new `policy` object: engine + policy_root) - **Territory:** `terraform/` (platform VPC, D-105), `modules/l1/*/terraform/` (per-module terraform subdirs — the engine binding), `modules/l1/*/interface.json` (defaults move from adapter to interface inputs), `modules/registry.json` (terraform_dir field), `modules/l2/microservice/composition.json` (drop the vpc child, D-105), `modules/STANDARDS.md` §8 (rewrite the adapter extension pattern → per-module terraform subdir pattern).
- `schemas/policy_check_result.schema.json` (READ-ONLY — no change per D-116)
- `schemas/README.md` (MODIFIED — note engine: "kyverno" shared by K8s adapter + kj) ### general (lead-developer + backend-engineer pipeline work)
- `tests/fixtures/capability_inventory.json` (NEW — clean + drifted inventory fixtures for regression policies) - **Domain:** coordination + pipelines
- **Reason:** The `config.json.policy` object is a schema-first addition - **Active:** true
(new top-level key with `engine` + `policy_root` fields). The - **Phase-specific:** false
capability-inventory JSON fixtures for the regression-gate policies - **Reason:** Owns the pipeline-driven testing (D-102/D-103/D-104) and the terraform lifecycle modes (D-101). The modules-lifecycle pipeline (Gitea + GitHub, byte-identical) matrix-runs each L1 module's `examples/{simple,complex}.yml` contracts through apply→modify→destroy against live AWS. `run_platform.sh` gains `--apply` and `--destroy` modes; Python never runs terraform. `verify_deploy_microservice.py` is deleted (D-101). Co-authoring expected on the `run_platform.sh` boundary (backend-engineer rewrites the adapter that `run_platform.sh` invokes).
(REQ-304) are structured data — the data-engineer owns the fixture - **Territory:** `pipelines/modules-lifecycle.yml`, `.gitea/workflows/modules-lifecycle.yml` + `.github/workflows/modules-lifecycle.yml` (byte-identical, D-102), `scripts/run_platform.sh` (`--apply`/`--destroy` modes, D-101), `scripts/run_primitive_plan.sh` (if extended for lifecycle), `scripts/run_pattern_plan.sh` (if extended), `pipelines/README.md` (document the new pipeline), `schemas/deploy-pipeline.schema.json` (if the lifecycle stages are added to the contract).
shape. The `policy_check_result.schema.json` is read-only (D-116 — no
enum change); the data-engineer documents the `engine: "kyverno"`
sharing in `schemas/README.md`. No migrations (no database). No Python
(backend-engineer + policy-engineer territory).
## Deactivated personas ## Deactivated personas
### lambda-engineer (custom, v1.9 — deactivated for v1.11)
- **Domain:** serverless
- **Active:** false
- **Phase-specific:** false
- **Reason:** No per-module Python this milestone (D-102: testing is pipeline-driven, not pytest). The v1.9 Lambda (`core/lambda/contract_ingestor.py`) and the `terraform/platform/main.tf` Lambda/DynamoDB/KMS/Secrets definitions persist from v1.9 but are not touched in v1.11. The `acdl-sod-halt` SNS topic and the attestation matrix are out of scope. Removed from the roster for v1.11; reactivates if a future milestone touches the Lambda.
### platform-engineer (custom, v1.9 — folded into data-engineer for v1.11)
- **Domain:** infra
- **Active:** false
- **Phase-specific:** false
- **Reason:** The v1.11 scope (D-097..D-107) is terraform module authoring + adapter rewrite + pipelines — not the v1.9-era L1/L2 IR-typed module authoring or the AWS OIDC bootstrap. The platform-engineer's v1.9 territory (`adapters/terraform/**`, `modules/**`, `terraform/**`) is split: the adapter goes to backend-engineer (rewrite), the per-module terraform subdirs + platform VPC go to data-engineer (the heaviest v1.11 work). Folded into data-engineer for v1.11; reactivates if a future milestone does IR-shaped module authoring or OIDC bootstrap work.
### security-engineer (custom, v1.9 — deactivated for v1.11)
- **Domain:** security
- **Active:** false
- **Phase-specific:** false
- **Reason:** The v1.11 scope does not touch Wiz/Kyverno/Checkov adapters, the HITL matrix, separation-of-duties, or the audit ledger. The security-engineer's v1.9 territory persists but is not touched. Removed from the roster for v1.11; reactivates if a future milestone touches security adapters or HITL gates.
### frontend-engineer ### frontend-engineer
- **active:** false - **Domain:** frontend
- **Reason:** ACDL has no frontend (no package.json — confirmed in - **Active:** false
config.json personas.personas[frontend-engineer].reason). v1.25 adds - **Phase-specific:** false
no UI work — the policy engine is backend + policy artifacts only. - **Reason:** The evidence timeline UI (`evidence-ui/**`) is unchanged from v1.0 and not touched in v1.11. Removed from the active roster; reactivates if a future milestone touches the timeline UI.
Deactivated per the v1.15+ convention.
### data-engineer (v1.9 — was deactivated, reactivated for v1.11)
- **Domain:** data
- **Active:** true (reactivated)
- **Phase-specific:** false
- **Reason:** See the active `data-engineer` entry above. The v1.9 deactivation rationale ("No ORM/persistence framework") no longer applies — v1.11's data-engineer owns terraform module authoring, not a data persistence layer.
### infra-stub-engineer (custom, v1.0 only)
- **Domain:** backend
- **Active:** false
- **Reason:** Owned L1 stub modules in the v1.0 demo. The demo is archived to `demo/`; real L1 modules are owned by data-engineer (v1.11). Not reactivated.
## Phase-specific overrides
| Phase | Personas active | Notes |
|-------|------------------|-------|
| 56a adapter-rewrite-and-s3-reference-module | data-engineer (lead: s3 reference terraform module — proves the design), backend-engineer (lead: stateless adapter rewrite — emits module blocks for s3), general (run_platform.sh --apply/--destroy skeleton) | security/lambda/frontend idle |
| 56b remaining-11-l1-module-terraform-subdirs | data-engineer (lead: author 11 L1 module terraform subdirs — vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf, rds, kms-key, uptime), backend-engineer (adapter: confirm each module round-trips through the assembler), general (modules-lifecycle pipeline wiring) | security/lambda/frontend idle |
| (modules-lifecycle pipeline) | general (lead: byte-identical Gitea+GitHub workflow + matrix apply→modify→destroy), data-engineer (examples/{simple,complex}.yml contracts as the modify variants), backend-engineer (adapter confirms the lifecycle cells resolve) | security/lambda/frontend idle |
| (platform VPC + composition drop) | data-engineer (lead: terraform/platform VPC + microservice composition drops vpc child, D-105), backend-engineer (resolver: env-aware state keys, D-106) | general/security/lambda/frontend idle |
| verify | lead-developer (lead: 4-layer verification), all active personas (review their territory) | — |
| review-audit-complete | lead-developer (lead: review + audit + milestone completion), all active personas (review participation) | — |
## Domain priority (used by TaskDecomposer)
`data → backend → general`
Rationale: in v1.11, the terraform foundation (per-module `terraform/`
subdirs + platform VPC) is the binding constraint — the stateless adapter
cannot be written until the reference s3 module exists (D-107: P56a
proves the design with s3 first). Backend (adapter/resolver) follows once
the module shape is proven. General (pipelines/workflows) wires the
lifecycle modes last, once the adapter + modules produce valid terraform.
## Conflict resolutions (lead-developer arbitration)
- `backend-engineer` vs `data-engineer` over `modules/l1/*/interface.json`:
data-engineer owns the interface defaults (defaults move from the
adapter to the interface inputs, D-100); backend-engineer owns the
adapter that reads them. Co-authoring is expected; conflict goes to
lead-developer.
- `backend-engineer` vs `general` over `scripts/run_platform.sh`:
backend-engineer rewrites the adapter that `run_platform.sh` invokes;
general adds the `--apply`/`--destroy` modes. The interface (the CLI
flags + the adapter invocation) is co-authored; conflicts go to
lead-developer.
- `data-engineer` vs `general` over `modules/l1/*/examples/`:
data-engineer owns the example contracts (the modify variants,
D-103); general owns the pipeline that matrix-runs them. Co-authoring
is expected; conflicts go to lead-developer.
- `lead-developer` vs any: lead-developer owns `.ciagent/**` + `docs/**`
meta + verification scripts + `modules/STANDARDS.md` §8 rewrite; persona
engineers do not edit CIAgent metadata or the vision/architecture
source docs.
## Territory enforcement mode
`warn` — config.json has no `personas.territory_enforcement` field, so the
default per execute.md is `warn`. Cross-territory edits are logged in the
commit message but do not fail the task. v1.11's scope means co-authoring
across territories is likely (e.g. backend + general on the adapter +
`run_platform.sh` boundary; data + general on the examples + pipeline
boundary); `warn` keeps it frictionless.
+349 -327
View File
@@ -1,371 +1,393 @@
# PLAN — v1.25 (kyverno-json Unified Policy Engine) ---
phase: P0
name: pre-execution
milestone: v1.14
requirements: [REQ-135, REQ-136, REQ-137, REQ-138, REQ-139, REQ-140, REQ-141, REQ-142, REQ-143, REQ-144, REQ-145, REQ-146, REQ-147, REQ-148, REQ-149, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154]
wave: 0
depends_on: []
---
> Feature milestone. Tags on the **v1.24.x** line: v1.24.0 (P0) → # v1.14 — NFR Refinement Plan (20 execution phases + 1 final)
> v1.24.1 (P1) → v1.24.2 (P2) → v1.24.3 (P3) → v1.24.4 (P4) → v1.24.5
> (P5 final = milestone release). 19 requirements (REQ-291..309),
> 4 execution phases + P0 pre-execution + P5 final review/ship.
## Wave model **Milestone:** v1.14 (NFR — bug fixes, security, stubs, tests, docs)
**Type:** NFR (all phases fix/test/docs/chore/refactor). Final patch IS
the release. Tags: `v1.13.3` (P0) → `v1.13.4..v1.13.23` (P1P20) →
`v1.13.24` (P21 = milestone release).
**Branch:** `milestone/v1.14-refinement``phase/NN-<slug>`
Each phase is a **vertical slice** (end-to-end: policy files + Python ## Wave ordering (D-098)
wiring + tests + docs). Phases are ordered by dependency: the engine
protocol (P1) must exist before policies (P2/P3) can be wired; the
pipeline wiring (P3) must exist before the meta-policies (P3) can
consume the merged PCR list; the regression-gate policies (P4) are
independent of the pipeline and can be authored in parallel with P3's
tests, but ship after P3 because they reference the engine registry
finalized in P1. Within each phase, the waves are the persona task
groups (parallelizable across personas when `parallelization.enabled:
true`, `max_concurrent_agents: 5`).
## Phase breakdown - **Wave 1 (P1P6):** bug fixes. P1→P2 sequential (composition depends
on dedup correctness); P3P6 independent. **G-105: full regression
gate run after P4** (validates the hardened gate before W2).
- **Wave 2 (P7P12):** security. P8→P9 sequential (IAM ARNs reference
externalized account ID); rest independent. **G-106: mid-milestone
regression-gate checkpoint after P12** (offline gate run; non-Verified
halts W3 until fixed).
- **Wave 3 (P13P17):** stub/test/CI/hygiene. P15 depends on P7
(hardened errors before script tests); P17 depends on P14 (both touch
config.json); P13 independent.
- **Wave 4 (P18P20):** standards/docs/VPC. P19 depends on P1P18
(reflects all prior phases); P18 + P20 independent.
### Phase P1 — engine-core (Wave 1, backend-engineer + policy-engineer + data-engineer) ## Execution approach
**Type:** `feat` (engine protocol + registry + kyverno-json engine adapter + install + tests) Each phase: EXECUTE (persona-assigned task groups) → VERIFY (4 layers +
regression gate at milestone complete) → SHIP (patch tag). Phase
**Requirements:** REQ-291, REQ-292, REQ-293, REQ-294, REQ-308, REQ-309 boundary checkpoint resets context. The execute workflow reads this
PLAN.md + ROADMAP.md §v1.14 + PERSONAS.md for task decomposition.
**Must-haves:**
- `core/policy_engine.py``PolicyEngine` Protocol (PEP 544) +
`PolicyEngineRegistry` (selects from `config.json.policy.engine`) +
`NullEngine` fallback (emits `SKIPPED` when `policy` key absent)
(REQ-291)
- `.ciagent/config.json` gains `policy` object: `{"engine":
"kyverno-json", "policy_root":
"adapters/kyverno-json/policies"}` (REQ-292)
- `adapters/kyverno-json/kyverno_json_engine.py` — `KyvernoJsonEngine`
implementing the protocol: `is_configured()` guards on `which kj`;
`evaluate()` writes payload to temp JSON, invokes
`kj scan --policy <dir> --payload <json> --output json`, translates
native output → `list[dict]` PCR records (`engine: "kyverno"`,
`ruleId` prefixed `KJ_<policy_name>`, severity from
`nova.cloudinit.dev/severity` annotation); defensive parsing
(malformed → `error` PCR, never exception); `is_configured()==false`
→ single `SKIPPED` PCR (`KJ_ENGINE_NOT_CONFIGURED`) (REQ-293)
- `adapters/kyverno-json/__init__.py` exports `KyvernoJsonEngine`;
`adapters/kyverno-json/policies/_smoke.json` trivial
`require-contract-id` policy for round-trip validation;
`scripts/install-kyverno-json.sh` runs
`go install github.com/kyverno/kyverno-json/cmd/kj@latest`;
`.github/workflows/ci.yml` + `.gitea/workflows/ci.yml` install Go + kj
(cached) (REQ-294)
- `tests/test_policy_engine.py` — protocol conformance, registry
selection, unknown-engine `KeyError`, `NullEngine` fallback,
`is_configured()` false when `which kj` absent (mocked) (REQ-308)
- `tests/test_kyverno_json_engine.py` — `evaluate()` returns PCR dicts
validating against `schemas/policy_check_result.schema.json` (via
`jsonschema`); defensive parsing (malformed kyverno-json output →
`error` PCR); `is_configured()==false` → `SKIPPED` with
`KJ_ENGINE_NOT_CONFIGURED`; `pytest.skip("kj not installed")` when
`which kj` absent (REQ-309)
**Vertical slice:** The `PolicyEngineRegistry.get_engine()` returns a
configured `KyvernoJsonEngine` that can `evaluate()` a trivial payload
against `_smoke.json` and produce a valid PCR list. The confidence
signal is unchanged — it already consumes `list[PolicyCheckResult]`.
The platform runs with or without the `kj` binary (`is_configured()`
guard). All existing tests pass (NullEngine fallback when `policy` key
absent in test config — but the v1.25 config.json *sets* the key, so
existing tests that use the real config get `KyvernoJsonEngine` with
`is_configured()==false` → `SKIPPED`).
**Files touched:**
- `core/policy_engine.py` (NEW)
- `.ciagent/config.json` (MODIFIED — `policy` object)
- `adapters/kyverno-json/__init__.py` (NEW)
- `adapters/kyverno-json/kyverno_json_engine.py` (NEW)
- `adapters/kyverno-json/policies/_smoke.json` (NEW)
- `scripts/install-kyverno-json.sh` (NEW)
- `.github/workflows/ci.yml` (MODIFIED — Go + kj install step)
- `.gitea/workflows/ci.yml` (MODIFIED — Go + kj install step)
- `tests/test_policy_engine.py` (NEW)
- `tests/test_kyverno_json_engine.py` (NEW)
**Verification:** `pytest tests/test_policy_engine.py
tests/test_kyverno_json_engine.py tests/test_confidence_signal.py
tests/test_adapter.py tests/test_checkov_adapter.py
tests/test_kyverno_adapter.py -v` (new tests pass or skip-without-kj;
existing adapter/confidence tests unchanged). `python3 -m py_compile
core/policy_engine.py adapters/kyverno-json/kyverno_json_engine.py`.
--- ---
### Phase P2 — contract + stack-IR policies (Wave 2, policy-engineer + backend-engineer) ## Wave 1 — Bug Fixes (P1P6)
**Type:** `feat` (policies + resolver wiring + tests) ### P1 — adapter-dedup-diagnostic (REQ-135)
**Persona:** backend-engineer
**Territory:** `adapters/terraform/adapter.py`
**Tasks:**
1. In the dedup loop (`adapter.py:159-170`), when `tf_dir` is `None`,
raise `ValueError(f"no terraform_dir in registry for module
{module}")` instead of silently skipping.
2. Verify registered-module dedup behavior preserved (multi-resource L1s
still merge into one `module "x" { ... }` block).
3. Run `pytest tests/test_adapter.py` + `run_ci.sh`.
**Requirements:** REQ-295, REQ-296, REQ-297, REQ-298, REQ-299 ### P2 — static-assets-wiring-fix (REQ-136)
**Persona:** data-engineer
**Territory:** `modules/l2/static-assets/`
**Tasks:**
1. Wire `default_ttl`/`max_ttl`/`price_class`/`viewer_protocol_policy`
in `composition.json` to the cloudfront child's inputs.
2. Add a `waf_enabled` feature flag (default true) to the
static-assets composition; make the WAF child conditional on it.
3. Update `examples/complex.yml` to set `waf_enabled: true` + non-default
TTLs so it resolves to a different resource set than `simple.yml`.
4. Run `pytest` + `run_ci.sh`.
**Must-haves:** ### P3 — lifecycle-script-arg-cleanup (REQ-137)
- `adapters/kyverno-json/policies/contract/` — 4 policies over consumer **Persona:** backend-engineer
contract JSON: `require-id-pattern.json`, **Territory:** `scripts/run_l2_lifecycle_*.sh`
`require-env-in-enum.json`, `require-infrastructure-min-1.json`, **Tasks:**
`forbid-unknown-fields.json` — each a `ValidatingPolicy` with one 1. Remove the `[ci-vpc-outputs.json]` token from the usage strings of
`validate.assert` rule using JMESPath against the payload root; `run_l2_lifecycle_test.sh` + `run_l2_lifecycle_destroy.sh`, OR add a
severity via `nova.cloudinit.dev/severity` annotation (REQ-295) comment documenting the L2-uses-remote-state design + parity reason.
- `core/contract_resolver.py` invokes 2. Run `pytest` + `run_ci.sh`.
`PolicyEngineRegistry.get_engine().evaluate(contract_dict,
policies/contract/, contract_id)` **before** resolving; failures
feed the `policy` input as `fail` PCRs (no resolver exit — confidence
signal decides the gate, `--soft-fail` pattern); emits
`nova.policy.evaluated` metrics event (REQ-296)
- `adapters/kyverno-json/policies/stack-ir/` — 3 policies over
resolved Stack IR: `require-tagging-standard.json` (ports
`nova_tagging.py` — `nova:owner` + `nova:environment` tags on every
`resources[]` entry), `forbid-public-ingress.json` (v1.0 demo rule),
`require-encryption-by-default.json` (v1.8 D-encryption-default);
`~` modifier iterates `resources[]` (REQ-297)
- `core/contract_resolver.py` invokes the engine with the resolved
Stack IR and `policies/stack-ir/` **after** resolving; resulting PCRs
appended to the contract-policy PCRs; resolver return values and
exceptions unchanged (additive) (REQ-298)
- `tests/test_stack_ir_policies.py` + `tests/fixtures/stack_ir/` —
passing IR (all tags + encryption) + failing IR (missing tags, public
ingress, plaintext bucket); each policy in isolation + full dir as
bundle; `pytest.skip("kj not installed")` when `which kj` absent
(REQ-299)
**Vertical slice:** A consumer contract passes through the resolver ### P4 — regression-gate-evidence-hardening (REQ-138)
and produces two PCR lists (contract policies pre-resolve, stack-IR **Persona:** backend-engineer
policies post-resolve) that feed the confidence signal. A contract **Territory:** `core/regression_verify.py`, `.ciagent/CAPABILITY_INVENTORY.md`
with a bad `id` or missing tags produces `fail` PCRs that lower the **Binding decisions:** G-105 (gate must pass clean post-P4 before W2)
confidence score. The resolver's existing tests pass unchanged (the **Tasks:**
policy call is additive — it does not change resolver return values 1. Add a `terraform validate` step to
or exceptions). `_check_lifecycle_module_terraform` (or document why it's too slow +
fall back to a `terraform fmt -check` syntax probe).
2. Tighten CAPABILITY_INVENTORY + docstrings to "offline proxy; live
apply/modify/destroy verified by the modules-lifecycle workflow run,
not by this gate."
3. **Run the full regression gate immediately after P4 lands** (G-105).
Gate must pass clean before W2 begins.
4. Run `pytest` + `run_ci.sh`.
**Files touched:** ### P5 — adapter-behavior-tests (REQ-139)
- `adapters/kyverno-json/policies/contract/require-id-pattern.json` (NEW) **Persona:** backend-engineer
- `adapters/kyverno-json/policies/contract/require-env-in-enum.json` (NEW) **Territory:** `tests/test_adapter.py`
- `adapters/kyverno-json/policies/contract/require-infrastructure-min-1.json` (NEW) **Tasks:**
- `adapters/kyverno-json/policies/contract/forbid-unknown-fields.json` (NEW) 1. Add `test_adapter_dedup_merges_same_module` — two resources with the
- `adapters/kyverno-json/policies/stack-ir/require-tagging-standard.json` (NEW) same `module` collapse to one `module "<first_id>" { ... }` block with
- `adapters/kyverno-json/policies/stack-ir/forbid-public-ingress.json` (NEW) merged inputs.
- `adapters/kyverno-json/policies/stack-ir/require-encryption-by-default.json` (NEW) 2. Add `test_adapter_remote_state_key_override``ACDL_REMOTE_STATE_KEY`
- `core/contract_resolver.py` (MODIFIED — pre/post resolve engine calls) overrides the default `platform/terraform.tfstate` key in the emitted
- `tests/test_stack_ir_policies.py` (NEW) `data terraform_remote_state` block.
- `tests/fixtures/stack_ir/passing.json` (NEW) 3. Run `pytest` + `run_ci.sh`.
- `tests/fixtures/stack_ir/failing.json` (NEW)
**Verification:** `pytest tests/test_contract_resolver.py ### P6 — alb-name-prefix-fix (REQ-140)
tests/test_stack_ir_policies.py tests/test_policy_engine.py -v` **Persona:** data-engineer
(existing resolver tests pass; new policy tests pass or skip-without- **Territory:** `modules/l1/alb/terraform/main.tf`
kj). `python3 -m py_compile core/contract_resolver.py`. **Tasks:**
1. Change `name_prefix = "tg-ci-"` to `name_prefix = "${var.name}-"` so
the consumer's name prefixes the target group.
2. Run `terraform validate` in the alb module dir standalone.
3. Run `pytest` + `run_ci.sh`.
--- ---
### Phase P3 — plan-JSON policies + meta-orchestration + pipeline wiring (Wave 3, policy-engineer + backend-engineer) ## Wave 2 — Security (P7P12)
**Type:** `feat` (plan-JSON policies + meta-policies + run_platform.sh wiring + tests) ### P7 — swallowed-error-hardening (REQ-141)
**Persona:** backend-engineer
**Territory:** `core/local_emulators.py`, `core/lambda/contract_ingestor.py`,
`terraform/bootstrap/create_state_backend.py`, `core/output_publisher.py`,
`terraform/bootstrap/apply_iam_baseline.py`
**Tasks:**
1. `local_emulators.py:374` — narrow `except Exception: pass` to catch
`AttributeError`/`TypeError` (monkeypatch setup); log + re-raise if
patching fails (prevents network egress).
2. `contract_ingestor.py:157` — catch `urllib.error.URLError`/
`HTTPError` specifically; log the search failure; keep `existing = []`
only on `404`/network, re-raise on auth errors.
3. `create_state_backend.py:51` — catch `ClientError` with
`NoSuchBucket`/`404` error code; re-raise on permissions/network.
4. `output_publisher.py:100,168` — catch `ClientError`/`HTTPError`
specifically; log with context.
5. `apply_iam_baseline.py:78` — catch `NoSuchEntityException` on
old-version delete; re-raise on other errors.
6. Run `pytest` + `run_ci.sh`.
**Requirements:** REQ-300, REQ-301, REQ-302, REQ-303 ### P8 — account-id-externalization (REQ-142)
**Persona:** backend-engineer + data-engineer
**Territory:** `adapters/terraform/adapter.py`, `terraform/bootstrap/`,
`scripts/push_consumer_image.py`, terraform resource ARNs
**Binding decisions:** G-101 (grep excludes backend blocks), G-102
(fallback bound to live account ID + workflow env wiring)
**Tasks:**
1. `adapter.py:125,140` — read `ACDL_AWS_ACCOUNT_ID` env; build the
state-bucket name dynamically. **Fallback constant = `581513795199`**
(the live account ID, NOT a placeholder — G-102). Documented for
offline tests.
2. `apply_iam_baseline.py:33`, `create_state_backend.py:33,35` — read
from env (same fallback).
3. `push_consumer_image.py:32` — read from env.
4. Terraform: use `data.aws_caller_identity.current.account_id` for
**resource ARNs** in `spike_runner_policy.json` + resource names.
**Exclude terraform `backend "s3"` blocks** (`terraform/*/terraform.tf`,
`terraform/ci-vpc/main.tf`, `terraform/platform/main.tf`,
`terraform/microservice/terraform.tf`) — backend `bucket` args are
static-config-only, evaluated pre-init (G-101). Leave backend blocks
literal or move to `terraform init -backend-config` (separate change,
not in P8 scope).
5. **Lifecycle workflow env wiring (G-102):** the `modules-lifecycle.yml`
full-mode jobs must set `ACDL_AWS_ACCOUNT_ID` from
`aws sts get-caller-identity --query Account --output text` before
any `run_platform.sh`/lifecycle invocation. No full-mode run proceeds
with the env unset.
6. Run `pytest` + `run_ci.sh`; verify
`grep -rn "581513795199" adapters/ scripts/ terraform/bootstrap/ core/`
returns 0 hits (excluding tests + docs + terraform backend blocks).
**Must-haves:** ### P9 — iam-policy-least-privilege (REQ-143)
- `adapters/kyverno-json/policies/plan-json/` — 3 policies over **Persona:** data-engineer
`terraform show -json` output: `forbid-plaintext-secrets.json` (ports **Territory:** `terraform/bootstrap/spike_runner_policy.json`,
CKV_AWS_41/45/46), `forbid-iam-wildcard.json` (ports CKV_AWS_1/40), `tests/test_iam_policy_baseline.py`, `modules/l1/*/terraform/main.tf`,
`require-kms-reference.json` (ports CKV_AWS_7/33); JMESPath over `modules/l2/*/composition.json`
`planned_values.root_module.resources[]` (REQ-300) **Binding decisions:** G-104 (verify acdl-* naming before merge)
- `run_platform.sh` Step 5 gains a parallel kyverno-json pass: after **Tasks:**
Checkov/Wiz produce raw PCRs, the script runs 1. Scope `iam:CreateRole` etc. (line 236) to
`kj scan --policy adapters/kyverno-json/policies/plan-json/ `arn:aws:iam::*:role/acdl-*`.
--payload <tfshow.json> -o json` and pipes through 2. Scope KMS (line 218) to `arn:aws:kms::*:key/acdl-*` (or
`adapters/kyverno-json/kyverno_json_engine.py` to produce a second `alias/acdl-*`).
PCR list; both lists concatenated and fed to the confidence signal; 3. CloudFront (line 117) + WAFv2 (line 129) remain `Resource: "*"` with
`nova.policy.evaluated` event with both engine names; when a documented global-ARN constraint (CloudFront ARNs are global;
`which kj` is false, logs and proceeds with Checkov/Wiz list only cannot be account-scoped — G-104).
(no hard failure) (REQ-301) 4. **Verify acdl-* naming (G-104):** grep/audit
- `tests/test_plan_json_policies.py` + `tests/fixtures/plan_json/` — `modules/l1/*/terraform/main.tf` + `modules/l2/*/composition.json`
passing plan (no secrets, no wildcard, KMS alias) + failing plan for every IAM role + KMS key name created by the lifecycle pipeline.
(plaintext password, `Action: "*"`, inline KMS key); policies in If any non-`acdl-*` name is found, rename the resource or widen that
isolation + bundle; `tests/test_run_platform_plan_json_policies.py` one statement (documented).
asserts `run_platform.sh` has the kyverno-json Step 5 block + 5. Add a regression test in `test_iam_policy_baseline.py` asserting no
concatenates PCR lists (script-substring assertion, pattern from new `Resource: "*"` on non-global actions.
`tests/test_pipeline.py:79-95`) (REQ-302) 6. Run `pytest` + `run_ci.sh`.
- `adapters/kyverno-json/policies/meta/` — `block-on-any-critical.json`
(asserts no PCR in merged list has `severity: critical` + `result:
fail`; if any does, emits `fail` PCR `KJ_META_BLOCK_CRITICAL`
severity `critical` — declarative source of truth; the
`confidence_signal.py` hard-override stays as defense-in-depth per
D-119) + `tagging-rules-agree.json` (cross-checks Checkov
`NOVA_TAG_NAMING` vs kj `KJ_REQUIRE_TAGGING_STANDARD` by
`resourceRef`; divergence emits `error` PCR per D-118);
`tests/test_meta_policies.py` (REQ-303)
**Vertical slice:** `run_platform.sh` Step 5 produces a merged PCR list ### P10 — contract-ingestor-identity-validation (REQ-144)
(Checkov/Wiz + kj plan-JSON policies + kj meta-policies over the **Persona:** backend-engineer
merged list) that feeds the confidence signal. A plan with a plaintext **Territory:** `core/lambda/contract_ingestor.py`, `tests/test_contract_ingestor.py`
secret produces two `fail` PCRs (one Checkov, one kj) for the same **Tasks:**
resource — visible defense-in-depth. A critical finding anywhere 1. Add `contractId` format validation (regex, ≤64 chars).
produces a `KJ_META_BLOCK_CRITICAL` meta-PCR that the confidence 2. Add `environment` enum validation (dev/qa/prod/dr).
signal's hard-override blocks. The pipeline runs with or without `kj` 3. Add `error` length cap (truncate `stackTrace` at a reasonable limit).
(graceful skip). 4. Document the ABAC reliance in the `_validate_caller_identity`
docstring + add a note to ARCHITECTURE.md (P19 will land it).
5. Add a spoofing-resistance test (caller submits a `consumerRepo` they
don't own → rejected if ABAC misconfigured; documented best-effort).
6. Run `pytest` + `run_ci.sh`.
**Files touched:** ### P11 — schema-input-validation-hardening (REQ-145)
- `adapters/kyverno-json/policies/plan-json/forbid-plaintext-secrets.json` (NEW) **Persona:** backend-engineer
- `adapters/kyverno-json/policies/plan-json/forbid-iam-wildcard.json` (NEW) **Territory:** `schemas/contract.schema.json`, `schemas/environment.schema.json`,
- `adapters/kyverno-json/policies/plan-json/require-kms-reference.json` (NEW) `tests/test_environment_schema.py`, `tests/test_contract_schema.py`
- `adapters/kyverno-json/policies/meta/block-on-any-critical.json` (NEW) **Tasks:**
- `adapters/kyverno-json/policies/meta/tagging-rules-agree.json` (NEW) 1. Add `"additionalProperties": false` to both schemas' top-level
- `scripts/run_platform.sh` (MODIFIED — Step 5 kj parallel pass) objects.
- `tests/test_plan_json_policies.py` (NEW) 2. Add `maxItems`/`maxProperties` bounds to `infrastructure` map +
- `tests/test_meta_policies.py` (NEW) `monitored_endpoints` array.
- `tests/test_run_platform_plan_json_policies.py` (NEW) 3. Add `pattern` validation for `state_backend.bucket` (S3 naming
- `tests/fixtures/plan_json/passing.json` (NEW) rules: lowercase, 3-63 chars, no underscores).
- `tests/fixtures/plan_json/failing.json` (NEW) 4. Add `pattern` validation for `runner_role_arn` (ARN format).
5. Add `pattern` validation for `vpc_cidr` (CIDR format).
6. Add tests asserting rejection of undocumented fields + malformed
values.
7. Run `pytest` + `run_ci.sh`.
**Verification:** `pytest tests/test_plan_json_policies.py ### P12 — gitignore-credential-hygiene (REQ-146)
tests/test_meta_policies.py tests/test_run_platform_plan_json_policies.py **Persona:** lead-developer
tests/test_pipeline.py -v` (new tests pass or skip-without-kj; existing **Territory:** `.gitignore`, `tests/test_no_secrets_tracked.py`
pipeline tests pass). `python3 -m py_compile` on any modified Python. **Tasks:**
Shellcheck on `run_platform.sh` if available. 1. Add credential-pattern catch-all to `.gitignore`:
`*.pem`, `*.key`, `*.p12`, `*.pfx`, `*.cer`, `*.crt`, `*.jks`.
2. Create `tests/test_no_secrets_tracked.py` — runs
`git ls-files | grep -E '\.(pem|key|p12|pfx|cer|crt|jks)$'` and
asserts 0 hits.
3. Run `pytest` + `run_ci.sh`.
--- ---
### Phase P4 — regression-gate policies + docs (Wave 4, policy-engineer + data-engineer + lead-developer) ## Wave 3 — Stub / Test / CI / Hygiene (P13P17)
**Type:** `feat` (regression policies) + `docs` (adapter READMEs + ARCHITECTURE + STANDARDS + METRICS) ### P13 — kyverno-kube-version-resolution (REQ-147)
**Persona:** backend-engineer
**Territory:** `adapters/kyverno/kyverno_adapter.py`, `tests/test_kyverno_adapter.py`
**Binding decisions:** G-103 (removal+documentation path, NOT implementation)
**Tasks:**
1. **Remove the `--kube-version` flag** from
`kyverno_adapter.py:11,115-116` (G-103 — implementing version-aware
policy selection would be a new feature, violating D-095).
2. Add a docstring documenting the deferral to the GitOps reconciler
roadmap (D-053): the Kyverno adapter is inactive for Terraform-only
stacks; `--kube-version` will be relevant when the GitOps reconciler
emits K8s manifests.
3. Update `test_kyverno_adapter.py` to remove the `--kube-version` test
cases + assert the flag is absent.
4. Run `pytest` + `run_ci.sh`.
**Requirements:** REQ-304, REQ-305, REQ-306, REQ-307 ### P14 — orphan-artifact-and-dead-config-cleanup (REQ-148)
**Persona:** lead-developer
**Territory:** `scripts/__pycache__/`, `pyproject.toml`, `.ciagent/config.json`
**Tasks:**
1. Delete the orphan
`scripts/__pycache__/verify_deploy_microservice.cpython-312.pyc`.
2. Fix `pyproject.toml` coverage source: `acdl_platform``core`.
3. Bump `pyproject.toml` version `1.3.0` → current (v1.14).
4. Remove dead JS allowlist entries from `config.json`
`bash_allowlist.allowed_commands` (npm/node/npx/pnpm/yarn/jest/eslint/
tsc/prettier — no package.json).
5. Run `pytest` + `run_ci.sh`.
**Must-haves:** ### P15 — untested-scripts-coverage (REQ-149)
- `adapters/kyverno-json/policies/regression/` — 3 policies over **Persona:** backend-engineer
capability-inventory JSON frontmatter: `cap-013-adapter-dedup.json`, **Territory:** `tests/` (new test files for 7 scripts)
`cap-023-metrics-collector.json`, `cap-024-deck-structure.json`; **Tasks:**
emit `pass`/`fail` PCRs per capability; the existing 1. `tests/test_seed_uptime_monitors.py` — mock the uptime-kuma API;
`core/regression_verify.py` is kept (drives the CI gate); the assert monitor creation from a JSON file.
policies are the declarative mirror (REQ-304) 2. `tests/test_push_consumer_image.py` — mock `subprocess.run` (docker
- `tests/test_regression_policies.py` + login/build/push) + boto3 ECR; assert the flow.
`tests/fixtures/capability_inventory/clean.json` + 3. `tests/test_sync_to_gl.sh` (shell test) — dry-run mode; assert the
`tests/fixtures/capability_inventory/drifted.json` — clean (all caps copy + push commands are constructed correctly.
pass) + drifted (duplicate adapter, missing metric status, broken 4. `tests/test_post_stage_comment.sh` (shell test) — no-op when not in
deck arc); regression gate still 287/287 baseline (new tests a PR context; assert the `gh api` call structure when in PR.
additive, skip-without-kj) (REQ-305) 5. `tests/test_rotate_spike_key.sh` (shell test) — mock `aws iam`;
- `adapters/README.md` gains new kyverno-json adapter row + "Policy assert deactivate/create/update-secret flow.
Engine Protocol" section (Protocol, registry, swap boundary, 6. `tests/test_create_state_backend.py` — mock boto3 S3/DynamoDB;
how-to-add-OpaEngine); `adapters/kyverno-json/README.md` documents assert idempotent creation.
the engine, install path, policy directory layout, 4 policy 7. `tests/test_create_iam_user.py` — mock boto3 IAM; assert idempotent
categories (REQ-306) user/policy/key creation.
- `.ciagent/ARCHITECTURE.md` §12.7 (added in RESEARCH) is finalized; 8. Run `pytest` + `run_ci.sh`.
`schemas/README.md` notes `engine: "kyverno"` shared by K8s adapter
+ kj (distinguished by `ruleId` prefix); `modules/STANDARDS.md`
gains "Policy authoring standard" section for module owners;
`docs/METRICS.md` notes the policy engine is swappable (Strategic
Objective #2 — provable trust via a replaceable substrate) (REQ-307)
**Vertical slice:** The regression gate's capability checks are now ### P16 — workflow-parity-and-script-flags (REQ-150)
declarative policies auditable as artifacts. A new module owner can **Persona:** backend-engineer
read `modules/STANDARDS.md` "Policy authoring standard" and write a **Territory:** `.gitea/workflows/`, `scripts/rotate_spike_key.sh`,
per-module kyverno-json policy. A new engineer can read `scripts/sync_to_gl.sh`
`adapters/README.md` "Policy Engine Protocol" and implement an **Tasks:**
`OpaEngine`. The 287/287 baseline is unchanged. 1. Either mirror the 4 GitHub-only workflows (patterns-plan,
platform-test, primitives-plan, release) to `.gitea/workflows/`, or
add a README documenting the Gitea limitation (Gitea runners don't
use release/primitives-plan/patterns-plan; release is GitHub-only by
design).
2. Add `set -euo pipefail` to `rotate_spike_key.sh` (currently only
`set -u`).
3. Add `set -euo pipefail` to `sync_to_gl.sh` (currently no `set`
flags).
4. Run `pytest` + `run_ci.sh`.
**Files touched:** ### P17 — config-and-persona-hygiene (REQ-151)
- `adapters/kyverno-json/policies/regression/cap-013-adapter-dedup.json` (NEW) **Persona:** lead-developer
- `adapters/kyverno-json/policies/regression/cap-023-metrics-collector.json` (NEW) **Territory:** `.ciagent/config.json`, `.ciagent/PERSONAS.md`
- `adapters/kyverno-json/policies/regression/cap-024-deck-structure.json` (NEW) **Tasks:**
- `tests/test_regression_policies.py` (NEW) 1. Mark `frontend-engineer` persona `active: false` in `config.json`
- `tests/fixtures/capability_inventory/clean.json` (NEW) `personas.personas[]` (PERSONAS.md:80 already says inactive).
- `tests/fixtures/capability_inventory/drifted.json` (NEW) 2. Fix `branching_strategy: "phase"` — either change to `"flat"` or
- `adapters/README.md` (MODIFIED — new row + PolicyEngine Protocol section) document that the field is advisory + the project uses flat workflow
- `adapters/kyverno-json/README.md` (NEW) (committed directly to main per established convention).
- `schemas/README.md` (MODIFIED — engine enum note) 3. Configure `ollama-cloud` backend: set `base_url` to the actual
- `modules/STANDARDS.md` (MODIFIED — Policy authoring standard section) endpoint OR add a comment documenting why it's intentionally unset
- `docs/METRICS.md` (MODIFIED — swappable engine narrative) (the runtime uses the `glm-5.2` model via the opencode backend, not
the `llm_backends` config).
**Verification:** `pytest tests/test_regression_policies.py 4. Run `pytest` + `run_ci.sh`.
tests/test_kyverno_json_engine.py -v` (new tests pass or skip-without-
kj). Full regression gate `pytest tests/` still at 287/287 baseline +
new tests (skip without kj). Manual read of `adapters/README.md` +
`adapters/kyverno-json/README.md` + `modules/STANDARDS.md` policy
section for clarity.
--- ---
### Phase P5 — final review + audit + milestone ship (Final Phase) ## Wave 4 — Standards / Docs / VPC (P18P20)
**Type:** `docs` (review + audit + milestone completion) ### P18 — module-standards-consistency (REQ-152)
**Persona:** data-engineer
**Territory:** `modules/STANDARDS.md`, `modules/l1/{ecr,ecs-cluster,rds}/terraform/`
**Tasks:**
1. Either add `locals.tf` to `ecr`, `ecs-cluster`, `rds` (extract
inlined locals from `main.tf`), OR reconcile STANDARDS §9.4 to
explicitly allow inlining for trivial single-resource modules.
2. Remove the stale `TYPE_MAP` reference in STANDARDS §8 (deleted in
the v1.11 stateless rewrite).
3. Run `pytest` + `run_ci.sh`.
**Requirements:** All REQ-291..309 (mark complete) ### P19 — documentation-sync-v1.14 (REQ-153)
**Persona:** lead-developer
**Territory:** `.ciagent/ARCHITECTURE.md`, `docs/`, `README.md`,
`.ciagent/COST.md`, `.ciagent/GRILL.md`, `.ciagent/IAM_POLICY.md`,
`docs/presentations/`
**Tasks:**
1. ARCHITECTURE.md: add v1.11 addendum (stateless adapter, platform VPC,
ACDL_LIFECYCLE_MODE), v1.12 addendum (CAP-013 fix, plan-only
default), v1.13 addendum (config.json schema migration, badge
cleanup, platform-architecture diagram), v1.14 addendum (all 20
phases). Record D-083 deferral explicitly.
2. Bump stale `@v1.6``@v1.9``@v1.13` across `README.md:225`,
`docs/consumer-guide.md` (12 sites), `docs/architecture.md:233`,
`docs/pipeline/versioning.md:29`, `docs/pipeline/index.md:42`.
3. Sync decks to v1.13.2 reality (version refs, capability claims).
4. Update COST.md window to v1.11v1.14 (lifecycle pipeline live-runs +
teardown).
5. Resolve G-005/G-008 in GRILL.md (CAP-017..022 now Verified via
lifecycle pipeline; COST.md now exists + covers v1.11+).
6. Update IAM_POLICY.md for v1.12/v1.13/v1.14 (plan-only default,
config.json schema, v1.14 IAM scoping from P9).
7. Run `pytest` + `run_ci.sh`; verify
`grep -rn "@v1\.[6-9]" docs/ README.md` returns 0 hits.
**Must-haves:** ### P20 — platform-vpc-parameterization (REQ-154)
- `ciagent-review` multi-persona code review across P1..P4 **Persona:** data-engineer
(lead-developer, backend-engineer, data-engineer, policy-engineer). **Territory:** `terraform/platform/main.tf`
Auto-fix P0; flag P1+ for post-hoc review. If P1+ issues found, fix **Tasks:**
them in this final phase (not loop back to EXECUTE). 1. Add a `vpc_cidr` variable (default `10.0.0.0/16`); replace the
- `ciagent-audit` — reconstruction test (git log ↔ `.ciagent/` files), hardcoded `cidr_block`.
`.ciagent/` file discipline, branch hygiene, commit discipline. 2. Replace `count = 2` subnets with
Critical issues fixed in this phase. `count = length(data.aws_availability_zones.available.names)`.
- `ciagent-ship` (milestone) — merge `phase/05-final-review-ship` → 3. Add a `data "aws_availability_zones" "available" {}` block.
`milestone/v1.25-kyverno-json` → `main`; tag `v1.24.5` (= the v1.25 4. Document the `0.0.0.0/0` ingress on port 80 (ALB-fronted, acceptable
release per the prev-minor tagging rule); create Gitea release with for a public-facing service; add a comment).
full milestone summary (all phases, all requirements); delete all 5. Run `terraform validate` + `pytest` + `run_ci.sh`.
milestone branches (local + remote).
- Update `REQUIREMENTS.md` (mark REQ-291..309 complete),
`ROADMAP.md` (mark v1.25 complete), `CHECKPOINT.json`
(milestone_complete: true), `NORTH_STAR.md` (note Strategic
Objective #2 — provable trust via a replaceable policy-engine
substrate).
**Vertical slice:** The v1.25 milestone is complete: kyverno-json is
the primary policy tool, behind a swappable adapter, with policies
over all 4 Nova artifacts. Tags v1.24.0..v1.24.5 on the v1.24.x line.
The milestone branch merges to main.
**Verification:** `pytest tests/ -v` full suite passes (287 baseline +
new tests). `git log --oneline` shows the v1.25 phase commits.
`git tag` shows v1.24.0..v1.24.5. `git branch` shows no leftover
milestone/phase branches (all deleted post-ship).
--- ---
## Wave ordering (parallelization) ## Final Phase — P21 (review + audit + ship)
With `parallelization.enabled: true`, `max_concurrent_agents: 5`, **Persona:** lead-developer (review coordination) + ci-code-reviewer +
`min_plans_for_parallel: 2`: ci-debugger (audit)
**Tasks:**
1. Multi-persona code review across all v1.14 phases (P1P20). Auto-apply
P0 fixes; flag P1+ for post-hoc review. If P1+ found, fix in-phase.
2. Audit: reconstruction test (git log vs `.ciagent/` files), file
discipline, branch hygiene, commit discipline. Fix critical issues
in-phase.
3. Complete: update REQUIREMENTS.md (REQ-135..154 → complete),
ROADMAP.md (v1.14 complete), PROJECT.md.
4. Tag `v1.13.24` (IS the milestone release). Merge
`milestone/v1.14-refinement``main`. Create Gitea release with full
milestone summary.
- **P1 Wave 1:** backend-engineer (protocol + registry + install) ‖ ## Success Criteria (milestone gate)
data-engineer (config.json policy object) ‖ policy-engineer (engine
adapter + smoke policy). 3 concurrent personas. Merge in order:
data-engineer → backend-engineer → policy-engineer.
- **P2 Wave 2:** policy-engineer (contract + stack-IR policies) ‖
backend-engineer (resolver wiring — depends on P1 registry). 2
concurrent. Merge: policy-engineer → backend-engineer (wiring
references the policy dirs).
- **P3 Wave 3:** policy-engineer (plan-JSON + meta policies) ‖
backend-engineer (run_platform.sh wiring — depends on P1 engine +
P2 resolver pattern). 2 concurrent. Merge: policy-engineer →
backend-engineer.
- **P4 Wave 4:** policy-engineer (regression policies) ‖ data-engineer
(capability-inventory fixtures) ‖ lead-developer (docs: READMEs,
STANDARDS, METRICS). 3 concurrent. Merge: data-engineer →
policy-engineer → lead-developer.
Territory enforcement: `warn` mode (per `config.json 1. All 20 REQ-135..REQ-154 marked complete in REQUIREMENTS.md.
personas.territory_enforcement: "warn"`). Cross-territory edits 2. Review: 0 new P0; all P1-1..P1-5 + P2-1..P2-4 resolved.
(e.g., backend-engineer touching a policy file) emit a warning, not a 3. Audit: clean; reconstruction test passes.
block. 4. Regression gate (D-091) clean against the v1.14 state.
5. `pytest` passes; `run_ci.sh` exits 0; `run_platform.sh --check-only`
## Requirement → phase → persona matrix exits 0.
6. Tag `v1.13.24` created; milestone merged to main.
| REQ | Phase | Primary persona | Type |
|-----|-------|-----------------|------|
| REQ-291 | P1 | backend-engineer | feat |
| REQ-292 | P1 | data-engineer | feat (config) |
| REQ-293 | P1 | policy-engineer | feat |
| REQ-294 | P1 | backend-engineer | feat (install) |
| REQ-295 | P2 | policy-engineer | feat |
| REQ-296 | P2 | backend-engineer | feat (wiring) |
| REQ-297 | P2 | policy-engineer | feat |
| REQ-298 | P2 | backend-engineer | feat (wiring) |
| REQ-299 | P2 | policy-engineer | test |
| REQ-300 | P3 | policy-engineer | feat |
| REQ-301 | P3 | backend-engineer | feat (pipeline) |
| REQ-302 | P3 | policy-engineer + backend-engineer | test |
| REQ-303 | P3 | policy-engineer | feat (meta) |
| REQ-304 | P4 | policy-engineer | feat |
| REQ-305 | P4 | policy-engineer + data-engineer | test |
| REQ-306 | P4 | policy-engineer + lead-developer | docs |
| REQ-307 | P4 | lead-developer | docs |
| REQ-308 | P1 | backend-engineer | test |
| REQ-309 | P1 | policy-engineer | test |
+6 -792
View File
@@ -1,12 +1,4 @@
# Nova — The New Dawn of DevSecOps # ACDL — Agentic Cloud Delivery Platform
> **Rebrand complete (milestone v1.15 — Nova, tag v1.15.4).** The
> project was rebranded from **ACDL** / "Agentic Cloud Delivery
> Platform" → **Nova** / "The New Dawn of DevSecOps — security as a
> seamless enabler of fast deployments." The new tagline is added
> alongside the existing "North Star" / "consumers declare intent"
> framing. See `.ciagent/REQUIREMENTS.md` §v1.15 and
> `.ciagent/ROADMAP.md` §v1.15.
## Vision / Core Value ## Vision / Core Value
@@ -33,7 +25,7 @@ traceable to a human attestation and an immutable evidence stream.
1. **Operations are Declared, Not Executed.** Consumers define what they 1. **Operations are Declared, Not Executed.** Consumers define what they
need; the platform reconciles, provisions, and progresses. need; the platform reconciles, provisions, and progresses.
2. **The Delivery Lifecycle is a Sovereign Boundary.** The platform 2. **The Delivery Lifecycle is a Sovereign Boundary.** The platform
governs infra and delivery; it does not reach into upstream product/SDLC. governs infra and delivery; it does not penetrate upstream product/SDLC.
Integration is only through validated, published contracts. Integration is only through validated, published contracts.
3. **Lower Environments are Autonomous; Higher Environments are Attested.** 3. **Lower Environments are Autonomous; Higher Environments are Attested.**
Dev = zero-touch agentic. QA/prod/dr = deliberate human attestation, not Dev = zero-touch agentic. QA/prod/dr = deliberate human attestation, not
@@ -58,103 +50,6 @@ traceable to a human attestation and an immutable evidence stream.
boundary. The platform validates, enriches with operational standards, boundary. The platform validates, enriches with operational standards,
and reconciles the target state. and reconciles the target state.
## Scope: Nova is Downstream of PDLC
> **Promoted from Core Tenet #2 + Anti-Goal #1 (v1.18, REQ-216).** This
> is the unmissable scope statement — the PDLC is upstream, Nova is
> downstream.
The **Product Development Lifecycle (PDLC)** — product backlog, code
authorship, IDE workflows, sprint planning, application business logic —
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
(technical developer + agentic).
Integration between the PDLC and Nova is **only** through the validated,
published contract boundary (`schemas/contract.schema.json` +
`schemas/submission-readiness.schema.json`). The citizen developer's AI
coding agent, an upstream agentic SDLC platform, or any upstream
development platform may all produce submissions — the source does not
matter because all are subject to the same compliance standards (the
submission-readiness gate, D-133). Nova validates, enriches with
operational standards, and reconciles the target state. Nova never
authors application code, manages product backlogs, or provides IDE
workflows.
```
PDLC (upstream) Nova (downstream)
───────────────── ─────────────────
product backlog contract ingestion
code authorship (AI agent / IDE / SDLC) → submission-readiness gate
sprint planning → policy enforcement
application business logic → cloud resource lifecycle
→ environment progression (dev→qa→prod→dr)
→ immutable audit + attestation
```
## RACI Matrix
> **Source of truth (v1.18, REQ-215, D-139).** Three roles clarify who
> owns what across the Nova delivery lifecycle. The matrix is the
> authoritative version; `docs/raci.md` is the citizen-developer-facing
> copy.
### Roles
- **Citizen Developer (CD)** — the consumer (technical developer L3A or
non-technical L3B). Responsible for all **Functional Requirements (FRs)**
and **User Acceptance Testing (UAT)**. The FRs + UAT are produced via
the citizen developer's AI coding agent, an upstream agentic SDLC, or
an upstream development platform — **the source does not matter as all
are subject to the same compliance standards** (the submission-readiness
gate, D-133).
- **Platform** — Nova. Responsible for all **Non-Functional Requirements
(NFRs)**, **Infrastructure** (cloud resource lifecycle, state, IAM),
**QA** (the platform-side quality checks: policy, confidence, schema),
and **Production deployments to cloud** (the apply path, the pipeline,
the release).
- **Release Management (RM)** — **co-owned**. QA + SRE attestations are
required by the actual release. The attestations are performed
agentically (the platform runs the checks), but the release is
**overseen and triggered by the Citizen Developer** — the human
attestation at the stage gate (D-042, hitl_gates.py). The platform
performs; the citizen developer authorizes.
### Matrix
| Work Category | Citizen Developer | Platform | Release Management |
|---|---|---|---|
| **Functional Requirements (FRs)** | **R/A** | C | I |
| **User Acceptance Testing (UAT)** | **R/A** | C | I |
| **Non-Functional Requirements (NFRs)** | I | **R/A** | C |
| **Infrastructure (cloud, state, IAM)** | I | **R/A** | C |
| **QA (policy, confidence, schema checks)** | C | **R/A** | I |
| **Production deployment to cloud** | I | **R/A** | C |
| **Release attestation (QA + SRE sign-off)** | **A** | R | **R** |
**Key: R** = Responsible (does the work) · **A** = Accountable (owns the
outcome, sign-off) · **C** = Consulted · **I** = Informed.
**Compliance-standard equivalence note:** the citizen developer's FRs +
UAT may originate from any upstream source — an AI coding agent, an
agentic SDLC platform, or a traditional development platform. All are
subject to the same compliance standards: the submission-readiness gate
(`schemas/submission-readiness.schema.json`), the contract schema, the
policy envelope, and the immutable audit stream. The platform does not
differentiate by upstream source; it validates the submission, not the
author.
**Co-ownership of Release Management:** the release is co-owned. The
platform performs the QA + SRE attestations agentically (confidence signal,
policy checks, separation-of-duties). The citizen developer oversees and
triggers the actual release — the human attestation at the stage gate is
the citizen developer's authorization, recorded with approver identity
(D-042). The platform runs the checks; the citizen developer authorizes
the promotion. This is the "autonomy in operations, human at stage gates"
model from the NORTH_STAR.
## Capability Status (Re-Verified 2026-07-27) ## Capability Status (Re-Verified 2026-07-27)
> Source of truth: `.ciagent/CAPABILITY_INVENTORY.md` (Phase 54, D-093). > Source of truth: `.ciagent/CAPABILITY_INVENTORY.md` (Phase 54, D-093).
@@ -689,97 +584,12 @@ DX: 16 total). Key changes:
10. Old two-surfaces diagram replaced by scope boundary diagram. 10. Old two-surfaces diagram replaced by scope boundary diagram.
Source markdown, talking points, and README all updated to mirror the new Source markdown, talking points, and README all updated to mirror the new
structure. Also includes scripts/sync_to_nova.sh (manual-only "2nd release" structure. Also includes scripts/sync_to_gl.sh (GitLab mirror sync
into ~/nova — a separate GitLab consumer-facing repo with its own history; utility, unrelated to presentations).
domain-based conventional commits, never triggered by CI; REQ-229).
No code changes; 494 tests pass; `run_ci.sh` + `run_platform.sh --check-only` No code changes; 494 tests pass; `run_ci.sh` + `run_platform.sh --check-only`
green. PPTX files uploaded to Gitea release. green. PPTX files uploaded to Gitea release.
## Objective for Milestone v1.18 (active — Citizen Developer & Production-Grade Guidance)
v1.18 advances Nova from a platform that governs infrastructure delivery
to one that **instructs the citizen developer on production-grade
engineering** and defines a **clear, machine-checkable contract for what
is acceptable to start**. Five user-directed inputs drive the milestone:
1. **S&P Global theme restoration.** The v1.17 P5 deck rebuild consolidated
two decks into one unified narrative deck but lost the S&P Global Energy
brand visual identity (introduced v1.9.2 / P45, commit `ae0cb58`). The
Marp `style:` block (red-core `#D6002A`, grey-90 `#1B1B1B`, Akkurat Pro
font, 8px top accent bar) is restored to the unified deck. The mermaid
`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 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
Lifecycle — product backlog, code authorship, IDE) is upstream of Nova;
Nova governs infra + delivery only; integration is through the validated
contract boundary.**
3. **RACI matrix.** A three-role responsibility matrix clarifies who owns
what: **Citizen Developer** (Responsible for all Functional Requirements
+ User Acceptance Testing, via their AI coding agent / upstream agentic
SDLC / upstream development platform — the source does not matter as all
are subject to the same compliance standards), **Platform** (Responsible
for all NFRs + Infrastructure + QA + Production deployments to cloud),
**Release Management** (co-owned: QA + SRE attestations required by the
actual release, performed agentically but overseen & triggered by the
Citizen Developer). Source of truth in PROJECT.md + `docs/raci.md` + a
deck slide.
4. **Nova input contract — "what is acceptable to start."** A JSON Schema
(`schemas/submission-readiness.schema.json`) defines the
acceptable-to-start gate as a superset *above* contract-schema validity:
schema-valid contract + required Nova tags + per-env mandatory metadata
(per W3.E) + declared policy preconditions + (for L3B) `profile:agentic`
markers + `appSource` pointer. A validator (`core/submission_readiness.py`,
invoked as `contract_ingestor.py --check-readiness`) returns a structured
`ReadinessResult` with reason codes. On fail → citizen-developer-facing
error (not a stack trace); on pass → proceeds to existing ingestion.
5. **Atelier integration — production-grade guidance + agentic validation.**
Nova consumes `coreci/atelier` (a first-principles docs-as-code
engineering framework — 8 core principles, 19 domains, 190 P-rules) via
two surfaces: **skills** (markdown files under `skills/` keyed to Atelier
domain paths, surfaced to the citizen developer's AI agent, extending the
BA.A 5-skill catalog) and an **MCP server** (`mcp/atelier/server.py`,
plugin-registry architecture, stdio transport, vendored Atelier snapshot
for audit reproducibility) exposing tools for principle-lookup,
domain-listing, matrix-lookup, and agentic validation against the
Atelier agent-checklist — validation that goes beyond deterministic
scanners (Wiz/Checkmarx/Mend) by catching correctness/clarity/simplicity/
observability gaps.
**Deck automation (cross-cutting):** any phase modifying
`docs/presentations/*-marp.md` or `docs/presentations/assets/` MUST
re-render HTML + PPTX, **commit the PPTX to git** (binary, no LFS), and
attach it to the phase's Gitea release. New scripts:
`scripts/render_deck.sh` (HTML + PPTX render) and
`scripts/attach_release_asset.py` (Gitea release asset upload).
**Milestone type:** Feature (P1 S&P theme restoration + P3 readiness
schema/validator + P5 MCP server are new code/features). Tags run on the
**v1.17.x** patch line (previous minor per branch-strategy): `v1.17.0` (P0)
`v1.17.1..v1.17.6` (P1P6) → `v1.17.7` (P7 final = milestone release).
**Phase count:** 8 (P0 pre-execution + 6 execution + 1 final).
**Hard constraints:**
- DO NOT make anything up (NORTH_STAR.md honesty model).
- The submission-readiness schema is a superset gate above
`contract.schema.json`, NOT a duplicate — it references but does not
redefine contract fields.
- The MCP server is plugin-registry extensible (future capabilities drop
in as new plugin files, no `server.py` edits).
- Atelier is vendored (pinned tag) for audit reproducibility — an agentic
validation result must be replayable against the exact principles that
produced it.
- PPTX is a first-class artifact: committed (history) + attached (download)
— both always, not optional.
## Requirements ## Requirements
### v1.0 (Prior milestone — the demo) ### v1.0 (Prior milestone — the demo)
@@ -981,7 +791,7 @@ or user-directed scope). New v1.7 decisions:
| W1.A | AI-refinement trigger | **Accept recommendation.** Joint condition: N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in last 6 months AND Infra & Ops unilateral override. | | W1.A | AI-refinement trigger | **Accept recommendation.** Joint condition: N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in last 6 months AND Infra & Ops unilateral override. |
| W1.B | Multi-stack edge case rule | **Accept recommendation.** Permitted only for (a) DR-region mirror, (b) time-boxed experimental stack with TTL ≤ 30d, (c) explicit Infra & Ops approval with `multiStack.justification`. | | W1.B | Multi-stack edge case rule | **Accept recommendation.** Permitted only for (a) DR-region mirror, (b) time-boxed experimental stack with TTL ≤ 30d, (c) explicit Infra & Ops approval with `multiStack.justification`. |
| W2.A | Tag mutability for prod | **Accept recommendation (Path B).** Tag for dev/qa, SHA for prod. Platform CLI resolves tag→SHA for prod-bound workflows. Justified by the "Audit truth lives outside the repository" bet. | | W2.A | Tag mutability for prod | **Accept recommendation (Path B).** Tag for dev/qa, SHA for prod. Platform CLI resolves tag→SHA for prod-bound workflows. Justified by the "Audit truth lives outside the repository" bet. |
| BA.A | Initial L3B skill catalog | **Accept recommendation.** 5 skills: web API, worker, scheduled job, static asset, basic observability bootstrap. Addition criteria: (a) reviewable for sensitive data, (b) expressible as a single contract submission, (c) documented use case. **Extended v1.18 (REQ-221/222):** the BA.A 5-skill catalog is extended with 9 Atelier-derived production-grade engineering skills under `skills/` (api, security, data, testing, observability, errors, devops, infrastructure-as-code, compliance), indexed by `docs/skills.md`. The Atelier skills extend, not replace, the BA.A catalog. | | BA.A | Initial L3B skill catalog | **Accept recommendation.** 5 skills: web API, worker, scheduled job, static asset, basic observability bootstrap. Addition criteria: (a) reviewable for sensitive data, (b) expressible as a single contract submission, (c) documented use case. |
| W3.D | L1/L2 standard versioning | **Decided.** Semver: interface → MAJOR, behavior → MINOR, lifecycle → PATCH (same as the v1.0 demo D-rule, lifted to the real platform). Pin model: L2 contracts pin L1 by `name@semver`; the resolver picks the highest compatible. Evolution: MAJOR bumps require a new registry entry (immutable publication); old entry enters a 12-month deprecation window. | | W3.D | L1/L2 standard versioning | **Decided.** Semver: interface → MAJOR, behavior → MINOR, lifecycle → PATCH (same as the v1.0 demo D-rule, lifted to the real platform). Pin model: L2 contracts pin L1 by `name@semver`; the resolver picks the highest compatible. Evolution: MAJOR bumps require a new registry entry (immutable publication); old entry enters a 12-month deprecation window. |
| W3.E | Schema mandatory vs optional inputs | **Decided.** Per-env mandatory table: dev requires `stack` + `environment`; qa adds `validation.e2eSuite` + `validation.loadTest`; prod adds `runbook` + `dashboard` + `oncall`; dr adds `drDrillRef`. `inputs` map is always optional. `profile: agentic` fields (`naturalLanguageIntent`, `confidenceAtSubmission`, `agentTrace`) optional everywhere. | | W3.E | Schema mandatory vs optional inputs | **Decided.** Per-env mandatory table: dev requires `stack` + `environment`; qa adds `validation.e2eSuite` + `validation.loadTest`; prod adds `runbook` + `dashboard` + `oncall`; dr adds `drDrillRef`. `inputs` map is always optional. `profile: agentic` fields (`naturalLanguageIntent`, `confidenceAtSubmission`, `agentTrace`) optional everywhere. |
| BA.B | Confidence threshold tuning | **Decided.** Starting thresholds frozen for v1. Tuning begins in v1.2: track FP/FN per environment quarterly; override authority = Infra & Ops + SRE joint sign-off; any override is itself a confidence-event in the audit stream. | | BA.B | Confidence threshold tuning | **Decided.** Starting thresholds frozen for v1. Tuning begins in v1.2: track FP/FN per environment quarterly; override authority = Infra & Ops + SRE joint sign-off; any override is itself a confidence-event in the audit stream. |
@@ -1101,600 +911,4 @@ D-095+ to continue from v1.10's D-094):
| D-098 | Wave ordering: W1 (P1P6 bug fixes), W2 (P7P12 security), W3 (P13P17 stub/test/CI/hygiene), W4 (P18P20 standards/docs/VPC). | Prerequisite chains: P2 depends on P1 (composition needs correct dedup); P9 depends on P8 (IAM ARNs reference externalized account ID); P15 depends on P7 (script tests benefit from hardened errors); P17 depends on P14 (both touch config.json); P19 lands last (reflects all prior phases). | 4 sequential waves; phases within a wave are independent (parallelizable when parallelization.enabled=true). | | D-098 | Wave ordering: W1 (P1P6 bug fixes), W2 (P7P12 security), W3 (P13P17 stub/test/CI/hygiene), W4 (P18P20 standards/docs/VPC). | Prerequisite chains: P2 depends on P1 (composition needs correct dedup); P9 depends on P8 (IAM ARNs reference externalized account ID); P15 depends on P7 (script tests benefit from hardened errors); P17 depends on P14 (both touch config.json); P19 lands last (reflects all prior phases). | 4 sequential waves; phases within a wave are independent (parallelizable when parallelization.enabled=true). |
| D-099 | `--ideate` flag: run the IDEATE stage between RESEARCH and PLAN (per ideate.md:218). The ideation tiers mine the 50 `partial:` + 16 `lessons:` + 3 `escalation:` + 16 `decisions:` git-native signals to validate/enrich the 20-phase scope. | User invoked with `--ideate`. The v1.14 scope is already user-directed (20 phases defined), so IDEATE acts as validation + enrichment, not scope discovery. Accepted ideas become IDEATE-NN IDs appended to REQUIREMENTS.md. | IDEATE stage runs; interactive validation gate (accept/skip/modify). | | D-099 | `--ideate` flag: run the IDEATE stage between RESEARCH and PLAN (per ideate.md:218). The ideation tiers mine the 50 `partial:` + 16 `lessons:` + 3 `escalation:` + 16 `decisions:` git-native signals to validate/enrich the 20-phase scope. | User invoked with `--ideate`. The v1.14 scope is already user-directed (20 phases defined), so IDEATE acts as validation + enrichment, not scope discovery. Accepted ideas become IDEATE-NN IDs appended to REQUIREMENTS.md. | IDEATE stage runs; interactive validation gate (accept/skip/modify). |
| D-100 | Accept all 20 ideation findings as the v1.14 requirement set (REQ-135..REQ-154). | User accepted all 20 at the interactive validation gate. Mechanical + backend-enriched tiers confirmed the user-directed scope. | 20 REQs locked; PLAN.md formalizes the task decomposition. | | D-100 | Accept all 20 ideation findings as the v1.14 requirement set (REQ-135..REQ-154). | User accepted all 20 at the interactive validation gate. Mechanical + backend-enriched tiers confirmed the user-directed scope. | 20 REQs locked; PLAN.md formalizes the task decomposition. |
| D-101 | E-001 (P8 state-bucket continuity residual risk) auto-resolved at full autonomy: accept the residual risk. G-102's binding mitigation (fallback bound to live account ID + workflow env wiring) is the control. The lifecycle pipeline defaults to plan-only (REQ-134) — full-mode runs are workflow_dispatch only, reducing the accident surface. | Grill escalation E-001 (confidence 0.55) re-exposes the v1.11 4-VPC root cause. At full autonomy, auto-decide with assumption logging. The residual risk (misconfigured env at live-run time) is runtime-dependent, not plan-resolvable. If the user prefers zero residual risk, direct that P8 exclude the state-bucket name from externalization entirely. | E-001 resolved; G-102 binding decision enforced in PLAN.md P8. | | D-101 | E-001 (P8 state-bucket continuity residual risk) auto-resolved at full autonomy: accept the residual risk. G-102's binding mitigation (fallback bound to live account ID + workflow env wiring) is the control. The lifecycle pipeline defaults to plan-only (REQ-134) — full-mode runs are workflow_dispatch only, reducing the accident surface. | Grill escalation E-001 (confidence 0.55) re-exposes the v1.11 4-VPC root cause. At full autonomy, auto-decide with assumption logging. The residual risk (misconfigured env at live-run time) is runtime-dependent, not plan-resolvable. If the user prefers zero residual risk, direct that P8 exclude the state-bucket name from externalization entirely. | E-001 resolved; G-102 binding decision enforced in PLAN.md P8. |
---
## Milestone v1.15 — Nova (Rebrand)
**Active milestone.** A full rebrand from ACDL → Nova across docs,
decks, code, configs, CI, env var prefixes, the consumer contract path,
SSM parameter paths, AWS tag keys, and AWS resource names — with a
staged infrastructure migration to avoid breakage.
**Milestone type:** Major (breaking — consumer-facing path, env var
prefixes, SSM path, AWS tag keys, and AWS resource names all change).
Tags run on the v1.15.x minor line: `v1.15.0` (P0) → `v1.15.4` (P5
final = milestone release). (G-104 binding: Major milestones tag on
their own minor line, not the previous minor's patch line.)
**In scope (v1.15):**
- Prose/decks/mermaid/pyproject/release-title rebrand (P1).
- Code identifiers, env var prefixes (`ACDL_*``NOVA_*` dual-read),
consumer path (`.acdl/``.nova/`) (P2).
- SSM path (`/acdl/``/nova/`) + AWS tag keys (`acdl:*``nova:*` ABAC)
(P3).
- AWS resource names (`acdl-*``nova-*`) with migration (P4).
- Final review + audit + remove dual-read fallback + milestone ship (P5).
**Out of scope (v1.15):**
- Renaming the real Gitea org/repo or GitHub org `acdl` (config stays
`acdl`; doc URLs updated to `nova` for prose only).
- Renaming the S&P Global Energy visual theme (`sp-theme.json`) —
client branding.
- Past Gitea release titles — only future releases use `Nova vX.Y.Z`.
- Git branch/tag naming — no brand name present.
**Milestone type:** Major (breaking). **Ship tag:** final phase patch
on the v1.15.x minor line IS the release (`v1.15.4`).
## Milestone v1.15 Phases
| Phase | Name | Goal |
|-------|------|------|
| 0 | pre-execution | SPECIFY → CLARIFY → RESEARCH → IDEATE → PLAN → GRILL. Establish v1.15-Nova milestone shell; ideation finds the 10 Nova requirements (REQ-155..164); plan decomposes into 4 execution phases. |
| 1 | docs-decks-prose | Rebrand all prose/decks/mermaid/pyproject/release-titles ACDL→Nova; add Nova tagline; ship consumer migration guide. |
| 2 | code-envvars-consumer-path | Rename acdl_tagging.py→nova_tagging.py; ACDL_*→NOVA_* dual-read; .acdl/→.nova/ contract path. |
| 3 | ssm-tagkeys | SSM /acdl/→/nova/ + AWS tag keys acdl:*→nova:* with parallel-tag ABAC migration. |
| 4 | aws-resource-migration | Rename all acdl-* AWS resources → nova-* with staged migration + runbook. |
| 5 | final-review-ship | Multi-persona review + audit + remove dual-read fallback + milestone ship (merge to main, tag final patch = release). |
## Key Decisions (v1.15)
Resolved at the CLARIFY stage (full autonomy — all within locked
constraints or user-directed scope). New v1.15 decisions (numbered
D-102+ to continue from v1.14's D-101). The high-judgment scope
decisions (D-102..D-107) were locked in by the user during the planning
conversation before execution; D-108..D-112 resolved at CLARIFY.
| ID | Decision | Rationale | Outcome |
|----|----------|-----------|---------|
| D-102 | AWS resource names: full rename with migration. | User chose "Full rename with migration." All `acdl-*` AWS resources → `nova-*` including state bucket migration, DynamoDB data migration, IAM re-bootstrap, ECR re-push. Accepts downtime + multi-phase migration. | P4 implements the staged migration + rollback runbook. |
| D-103 | Env var prefixes: full rename to `NOVA_*`. | User chose "Full rename to `NOVA_*`." All 21 `ACDL_*` prefixes → `NOVA_*` including `.env.secrets` (key names only, values stay) + Gitea secrets. | P2 renames + implements dual-read fallback; P5 removes fallback. |
| D-104 | Tag keys + SSM path + consumer path: full rename all three. | User chose "Full rename all three." AWS tag keys `acdl:*``nova:*` (ABAC re-scope), SSM path `/acdl/``/nova/` (param migration), consumer path `.acdl/``.nova/`. | P2 (consumer path) + P3 (SSM + tag keys) implement. |
| D-105 | External URLs: illustrative — update them. | User chose "URLs are illustrative — update them." Doc URLs (`github.com/acdl/...`, `git.cloudinit.dev/.../acdl*`) → `nova` for prose consistency. Real Gitea repo name (`release.gitea.repo`) stays `acdl`. | P1 updates doc URLs; config.json unchanged. |
| D-106 | Nova tagline: add alongside existing North Star. | User chose "Add Nova tagline alongside existing North Star." Tagline "The New Dawn of DevSecOps — security as a seamless enabler of fast deployments" added to README header, deck title slides, `docs/vision.md`. Existing "consumers declare intent" framing retained. | P1 adds tagline; no prose removed. |
| D-107 | S&P visual theme: leave untouched. | User chose "Leave S&P theme untouched." `sp-theme.json` (#D6002A red, Akkurat Pro) is client branding, not the Nova product brand. Only product-brand text (ACDL→Nova) changes in decks. | P1 edits deck text only; theme/CSS unchanged. |
| D-108 | Dual-read fallback centralized in a new `core/env.py` helper. | No centralized env loader exists today (env vars read via scattered `os.environ.get("ACDL_*")`). A new `core/env.py` `get_env(name)` helper reads `NOVA_X` then falls back to `ACDL_X`, returning `None` if neither. All call sites migrate to the helper in P2; P5 removes the fallback. | P2 creates `core/env.py` + migrates call sites; P5 removes fallback. |
| D-109 | Checkov custom rule `nova_tagging.py` warns during P2, hard-fails from P3. | During P2 (before tag-key migration), existing resources still carry `acdl:*` tags — a hard fail would break the regression gate. P2 rule warns on `acdl:*`; P3 (after parallel-tag + ABAC swap) hard-fails on `acdl:*` and enforces `nova:*`. | P2: warn mode; P3: hard mode. |
| D-110 | Schema `$id` URLs (`https://acdl.cloudinit.dev/schemas/...`) → `https://nova.cloudinit.dev/schemas/...`. | These are illustrative schema identifiers (no real DNS resolution required for JSON-schema validation). Renamed for brand consistency in P1. Existing `$id` values in test fixtures updated. | P1 renames schema `$id` + fixture references. |
| D-111 | Lambda env-var defaults (`CONTRACTS_TABLE` default `"acdl-contracts"`, etc.) → `nova-contracts`. | `core/lambda/contract_ingestor.py` has hardcoded `acdl-*` default table names. These become `nova-*` in P4 (resource migration). P2 changes the env-var name (`ACDL_*``NOVA_*`); P4 changes the default values to `nova-*`. | P4 updates Lambda defaults. |
| D-112 | `nova` slug: no `project:` prefix on branches (single-project mode). | `config.json` has `projects[]` with one entry (slug `acdl`) but `git.branching_strategy` is `flat` and the established convention since v1.0 is flat branches (no `<slug>/` prefix). Nova rebrand does NOT change the branch prefix convention. Commit `---ci---` blocks use `project: acdl` (the config slug, unchanged). | Branches stay `milestone/v1.15-nova`, `phase/NN-*`; no `acdl/` or `nova/` prefix. |
## Objective for Milestone v1.16 (complete — NFR Simplification, tag `v1.15.26`)
A 20-phase NFR sweep (no new features) themed around five axes the user
directed during ideation: **Simplify without regressions**, **Security**,
**Maintainability**, **User/Developer Experience**, and **No Humans
Onboarding Flow**. The v1.15 rebrand left a fresh layer of residual debt
(stale brand strings, a state-bucket drift, a Kyverno policy that
contradicts the Nova tagging standard, dead code) that this milestone
clears, alongside genuine simplification (dedup helpers, a workflow
generator, file splits) and the first self-service onboarding request
path (request-path only; real AWS account provisioning stays a future
feature).
**Milestone type:** NFR (all phases fix/chore/docs/refactor/test). The
final phase's patch IS the deliverable — no separate milestone tag. Tags
run on the v1.15.x line: `v1.15.5` (P0) → `v1.15.6..v1.15.25` (P1P20) →
`v1.15.26` (P21 final = milestone release).
**Wave ordering:**
- Wave 1 (P1P4): correctness + brand regression fixes — P1 first
(state-bucket drift + Kyverno label contradiction are the highest-
severity findings, both correctness regressions left by the rebrand).
- Wave 2 (P5P9): simplify without regressions — P5 before P6/P9
(regression-verify dedup is independent); P8 changes the workflow test.
- Wave 3 (P10P14): security + maintainability — P10 before P11
(identity enforcement before payload validation); P12/P13 independent
splits.
- Wave 4 (P15P17): developer experience — independent; P17 last
(reflects the consolidated path).
- Wave 5 (P18P20): no-humans onboarding — P18 (schema+Lambda action)
before P19 (env-file autogen consumes the schema) before P20 (cross-
account role, offline-proven).
**Verification gates:** the regression gate (D-091) runs after Wave 2
(P9) and at P21 — all 22 capabilities must stay Verified (no
regressions from simplification). A mid-milestone checkpoint runs after
Wave 3 (P14), offline.
## Milestone v1.16 Phases
| Phase | Name | Goal |
|-------|------|------|
| 01 | state-bucket-and-kyverno-rebrand-fix | `adapter.py:117` `acdl-tfstate``nova-tfstate`; Kyverno `require-resource-labels.yml` `acdl:*``nova:*` labels. Regression-risk fix. |
| 02 | user-facing-acdl-to-nova-sweep | Onboarding msg, alert title/body, PR comments, CI banner, module docstrings → Nova. |
| 03 | dead-code-and-stale-prefix-cleanup | Dead `ACDL_ENVIRONMENT_OVERRIDE` export; stale dual-read comments; `acdl_*` temp prefixes → `nova_*`. |
| 04 | migrate-ssm-except-narrowing | `migrate_ssm_paths.py` `except Exception``ParameterNotFound`. |
| 05 | regression-verify-dedup | Extract shared live-plan/resolver/lifecycle-resolve helpers (~70 lines saved). |
| 06 | run-platform-deadcode-and-hitl-fn | Remove dead export; extract `run_hitl_gate()` shell fn; drop hardcoded UUID/`v18` stamp. |
| 07 | contract-resolver-envloader-and-kind | Import env loader from environment_check; add `kind` field to registry; replace `is_l2` heuristic. |
| 08 | workflow-generator-dedup | `scripts/sync_workflows.py` (one source → both dirs); replace byte-identity test with generator-output test. |
| 09 | run-platform-split | Extract decommission + uptime blocks into `scripts/run_decommission.sh` + `scripts/run_uptime.sh`. |
| 10 | contract-ingestor-defense-in-depth | Fail closed on missing IAM identity; derive env enum from `core/environments/` dir. |
| 11 | contract-ingestor-payload-validation | Contract blob size cap + schema validation; consistent error/stackTrace caps. |
| 12 | split-contract-resolver | 638 lines → resolve / decommission-transform / cli modules. |
| 13 | split-regression-verify | 670 lines → capability checks / live-plan helpers / cli modules. |
| 14 | schema-driven-outputs-and-cache | `SAFE_OUTPUT_NAMES` from interface.json; cache loaded schemas in resolver. |
| 15 | run-platform-help-and-flags-doc | Real `--help`; document `--deploy-uptime`; surface `--local` in README. |
| 16 | workflows-readme-catalog | `.github/workflows/README.md` — triggers, inputs, secrets, reusable-workflow contracts. |
| 17 | getting-started-consolidation | Single getting-started section: offline happy path first, AWS path second. |
| 18 | onboarding-schema-and-lambda-action | `schemas/onboarding.schema.json` + `onboard_consumer` action → CMDB row pending grant. |
| 19 | onboarding-envfile-autogen | `core/onboarding.py` generates `<env>.json` from a request + emits a PR; rebrand onboarding message. |
| 20 | cross-account-role-automation-offline | Terraform for consumer deploy-role + `nova:owner` ABAC tag (offline-proven only). |
| 21 | final-review-ship | Review + audit + milestone ship `v1.15.26` + merge to main. |
Milestone COMPLETE gate: review → ship `v1.15.26` (NFR milestone; final
patch IS the release) → audit.
## Key Decisions (v1.16)
Resolved at the CLARIFY stage (full autonomy — all within locked
constraints or user-directed scope). New v1.16 decisions numbered D-113+
to continue from v1.15's D-112. The four high-judgment scope decisions
(D-113..D-116) were locked in by the user during the ideation planning
conversation; D-117..D-119 resolved at CLARIFY.
| ID | Decision | Rationale | Outcome |
|----|----------|-----------|---------|
| D-113 | Onboarding scope = request-path only (NFR-shaped). | User chose "Request-path only." Full self-service AWS account/network/state provisioning is a feature (creates real cloud resources), not an NFR. v1.16 removes the human handoff from the *request* step (schema + Lambda action + env-file autogen + ABAC grant hook); real AWS account creation stays a future feature milestone. | P18P20 implement the request path; real provisioning deferred. |
| D-114 | Cross-account Terraform = offline-proven only. | User chose "Offline-proven only." P20 Terraform for the consumer deploy-role + ABAC tag is authored + `terraform validate` + `--check-only` only; no live apply (consistent with `NOVA_LIFECYCLE_MODE=plan` default). No new AWS resources created in this NFR milestone. | P20 validates offline; live apply deferred. |
| D-115 | Workflow dedup = generator (not status quo). | User chose "Generator." `scripts/sync_workflows.py` writes one source → both `.gitea/`+`.github/` dirs; the byte-identity test in `test_pipeline_contract.py` is replaced with a "generated outputs match committed files" test. Removes ~20 KB manual-sync risk. | P8 implements the generator + test swap. |
| D-116 | Drift fixes = P1 of v1.16 (not a hotfix to main). | User chose "P1 of v1.16." The state-bucket drift (`adapter.py:117`) and Kyverno label contradiction are correctness regressions but latent in plan-only mode (no live apply in the default path), so they are not an active outage. Fixing them as P1 keeps the milestone self-contained. | P1 fixes both; no hotfix to main. |
| D-117 | v1.14 NFR categories are NOT re-proposed. | v1.14 already swept over-broad excepts (REQ-141), hardcoded account-ID (REQ-142), IAM `Resource:"*"` scoping (REQ-143), contractId/env validation (REQ-144), `.gitignore` catch-all (REQ-146), `--kube-version` removal (REQ-147), orphan cleanup (REQ-148), `set -euo pipefail` parity (REQ-150). v1.16 finds NEW residual signals (the v1.15 rebrand left a fresh debt layer) and does not duplicate completed work. | Wave 15 target only fresh debt. |
| D-118 | Regression gate (D-091) gates Wave 2 completion and P21. | "Simplify without regressions" is only credible if the regression gate runs after the simplification wave. The gate runs after P9 (Wave 2 done) and at P21 (milestone complete); any non-Verified capability halts W3. Mid-milestone checkpoint after P14 (offline). | P9 + P21 run the gate; P14 checkpoint. |
| D-119 | `onboard_consumer` action stores a CMDB row pending grant (not auto-provisions). | The request-path-only scope (D-113) means the Lambda accepts an onboarding request and writes a `pending` row to `nova-contracts` (or a new `nova-onboarding` partition key); the platform automation that grants the ABAC role is the P20 Terraform (offline-proven). No AWS resources are created by the Lambda action itself. | P18 writes the pending row; P20 proves the grant Terraform offline. |
## Objective for Milestone v1.17 (active — Strategic Direction, Leadership Metrics & Unified Story)
**Milestone type:** Feature (P1P3 feat; P4 docs; P5 docs+test; P6 test;
P7 review+audit+ship). Tags on the v1.16.x line: `v1.16.0` (P0) →
`v1.16.1..v1.16.7` (P1P7) → `v1.16.8` (P8 final = milestone release).
**Three pillars:**
- **Pillar A — Strategic Direction.** A durable, PO-authored
`.ciagent/NORTH_STAR.md` encodes the platform's vision, 4 strategic
objectives, anti-goals, v1.17 non-goals, 1218mo targets (with a
grounding column), and success criteria. CIAgent reads it in every
future `/ci-run` so the direction survives across milestones. The
attestation clarification is reflected: human attestation required at
stage gates (QA for production, SRE for operational readiness); autonomy
in operations, not in accountability. **v1.21 refinement:** Strategic
Objective #4 reframed from "default substrate for agentic consumption" to
integrating with externally owned PDLC/SDLC/Agentic/Citizen Developer
platforms regardless of source (Nova provides skills + MCP endpoints;
all prod intents go through the same controls). Objective #2 reworded:
trust is established by deterministic scripts that calculate a score —
the platform functions without AI. Objective #3 reworded with four
CTO-grade metrics (Lead Time PR→Prod, Infrastructure Vulnerability
Count trend, MTTR, Cloud Spend Reduction) all flowing into PowerBI.
Anti-goals #1, #4, #5 removed; replaced with "not an upstream
development platform" and "not a replacement for the PDLC".
- **Pillar B — Leadership Metrics + PowerBI.** Instrument Nova to
collect, aggregate, and surface leadership-grade metrics that prove the
"no-humans" autonomous-infrastructure value proposition (reframed in
v1.21 to "autonomous cloud delivery" — professional framing; the
platform delivers safe production deployment without an operator in
the loop of normal operations). Nova-native
minimal tech (CloudEvents 1.0 envelope, JSONL event log, SQLite cold
store, hash-chained Decision Ledger via `outbox_writer.py` extension)
+ Infracost for pre-apply cost estimates. Hybrid model: existing
file-based signals (REGRESSION_REPORT.json, pcr.json, signal.json,
junit XML) are sources the collector reads and projects into events;
new emitters emit CloudEvents directly. PowerBI export = CSV/JSON
views (fact + dimension tables + 8 empty placeholder views for
deferred metrics). **Hard constraint: DO NOT make anything up.** Every
metric is `grounded` (cites source file + schema), `derived`
(documented formula), or `deferred` (cites decision ID — D-096/D-083/
D-113/D-114/D-119). The 8 deferred metrics: drift detection, GreenOps/
carbon, predictive/reactive, live CUR reconciliation, multi-cloud,
red-team MTTR, self-healing velocity, SLA/downtime.
- **Pillar C — Unified Narrative Deck.** Merge the two existing decks
(`how-the-platform-works` + `the-developer-experience`) into one unified
narrative deck "Nova — The No-Humans Infrastructure Platform" with a
single arc: Problem → Vision/Direction (NORTH_STAR) → How it works →
Proof (metrics) → Roadmap/Ask. The "tell them x3" structure applies at
deck level AND per slide (each slide opens with what it covers,
delivers, closes with an explicit "benefit of this stage" callout).
Fluid transitions between slides. Both old decks retired.
**Key decisions resolved in the planning conversation (D-120+):**
| ID | Decision | Rationale | Outcome |
|----|----------|-----------|---------|
| D-120 | Tech stack = Nova-native + Infracost, drift deferred. | The PO's technical-direction document specifies Kafka/Prometheus/ClickHouse/QLDB/OTel — none exist in Nova today. Adopt the PRINCIPLES (events as source of truth, CloudEvents envelope, decision ledger, definition-of-success docs, dashboards-as-projections) but implement with Nova-native minimal tech (JSONL + SQLite + hash-chained ledger). No Kafka/Prometheus/ClickHouse/QLDB. Infracost adopted (runs offline on plan JSON). Drift detection deferred (D-096 + no scheduler). | P1P3 use Nova-native tech; Infracost in P1; drift deferred. |
| D-121 | Decision Ledger = extend outbox_writer.py → SQLite append-only hash chain. | The direction's #1 priority is the Decision Ledger. Nova already has a hash-chained outbox (outbox_writer.py). Extend it to a SQLite append-only table with hash chain; add ai.decision.made + attestation.recorded events. Honors D-083 (no S3 Object Lock/JWS). | P1 extends outbox_writer; ledger is SQLite hash-chain. |
| D-122 | AI Planner framing = map Nova's real decision points. | The direction assumes an "AI Planner/Reasoner" (planner-v3.2). Nova's actual decision path is confidence_signal + HITL gate. Model ai.decision.made from confidence_signal (decision_id=run_id, chosen_action=band, confidence=score, alternatives=perInput, human_override=HITL block). LLM planner marked future/aspirational. | P1 emits honest decision events; no fabricated LLM. |
| D-123 | Deferred metrics = all 8 (drift, GreenOps, predictive/reactive, live CUR, multi-cloud, red-team MTTR, self-healing, SLA/downtime). | These require live AWS (D-096) or new external systems. Ship as empty PowerBI placeholder views with documented schemas. | P3 ships 8 placeholder views; METRICS.md marks them deferred. |
| D-124 | NORTH_STAR = strategy; tech direction = engineering input. | The PO's technical-direction document is engineering architecture, not strategy. NORTH_STAR.md captures strategic vision/objectives/anti-goals (PO-authored). The tech direction becomes the telemetry reference architecture section in RESEARCH.md/ARCHITECTURE.md, cited by NORTH_STAR's engineering objectives. | P0 writes NORTH_STAR; RESEARCH writes the telemetry reference. |
| D-125 | Events vs files = hybrid. | Existing file-based signals (REGRESSION_REPORT.json, pcr.json, signal.json, junit) stay as files; the collector reads them and emits normalized CloudEvents into JSONL + SQLite. New emitters emit CloudEvents directly. | P2 collector reads files + events. |
| D-126 | Hot/cold split = cold-only SQLite (hot path deferred). | Nova has no live ops dashboard (no live AWS, D-096). The SQLite store is cold-only (batch/historical). The hot path is documented as deferred. | P2 SQLite is cold-only. |
| D-127 | Definition-of-success = per-KPI docs. | The direction's §11 requires a definition-of-success doc for every executive KPI. Adopt this standard; docs live in `docs/metrics/`. | P4 writes per-KPI docs. |
| D-128 | Storage location = metrics/ at repo root. | metrics/runs/ (per-run manifests), metrics/nova_metrics.db (SQLite), metrics/events.jsonl (event log), metrics/powerbi/ (export). | P1P3 use metrics/ at repo root. |
| D-129 | PowerBI delivery = CSV/JSON files, folder connector. | Nova is offline-first; no live connector to a running service. PowerBI ingests via the folder connector. | P3 emits CSV/JSON to metrics/powerbi/. |
| D-130 | Deck arc = Problem → Vision → How → Proof → Roadmap. | The unified narrative deck's 5-act structure. x3 arc at deck + slide level. Per-slide benefit callouts. Fluid transitions. Both old decks retired. | P5 builds the unified deck; old decks deleted. |
| D-131 | MTTR scope = platform-run MTTR. | The <60s MTTR target refers to platform-run failures (apply.failed → successful retry), not infra-incident MTTR (no incident detection system). Infra-incident MTTR deferred. | P4 grounds platform-run MTTR. |
| D-132 | Attestation instrumentation = emit attestation.recorded events. | The attestation system (hitl_gates.py + attestation_matrix.py + separation_of_duties.py) already exists. Instrument it: emit attestation.recorded events into the Decision Ledger + PowerBI. Attestation Coverage = 100% target grounded from outbox approver_* attributes. | P1 emits attestation events; P4 grounds Attestation Coverage. |
## Key Decisions (v1.18)
Resolved at the CLARIFY stage (full autonomy — all within locked
constraints or user-directed scope). New v1.18 decisions:
| ID | Decision | Rationale | Outcome |
|----|----------|-----------|---------|
| D-133 | Submission-readiness validator location = extend `contract_ingestor.py --check-readiness`. | Adding a new CLI binary is unnecessary; the ingestor is the existing entry point for contract submission. The validator is a subcommand that runs before ingestion proceeds. No new binary, no new entry point to maintain. | P3 implements the subcommand; no new CLI binary. |
| D-134 | Deck slide budget = 18 → 21 slides (no act restructure). | The 3 new slides (scope/RACI/atelier) are leadership-relevant and append after the existing 18. The 5-act arc (D-130) is preserved; the new slides are append-only context, not a new act. | P6 appends 3 slides → 21 total. |
| D-135 | Atelier MCP transport = stdio now; HTTP-ready (same server object). | stdio is the local-agent transport (the citizen developer's AI agent spawns the server as a subprocess). The MCP Python SDK v2 supports Streamable HTTP on the same `MCPServer` object, so adding HTTP later is a transport-only change in `server.py`, not a rewrite. | P5 ships stdio; HTTP deferred (documented in README). |
| D-136 | Atelier source = vendor pinned tag under `mcp/atelier/vendor/`. | An agentic validation result is only reproducible if the principles that produced it are pinned. Live-fetch breaks replayability (Atelier `main` drifts). Vendoring matches the v1.16 P15 offline-first precedent and the Nova thesis (provable trust). `mcp/atelier/vendor/VERSION.md` records the pinned tag; `scripts/update_atelier_vendor.sh` is the intentional upgrade path. | P5 vendors Atelier; live-fetch not implemented. |
| D-137 | MCP server language = Python (MCP Python SDK v2, `modelcontextprotocol/python-sdk`). | Nova's `core/` is Python. The MCP Python SDK v2 (23.9k stars, MIT, stable) matches the codebase; type hints become JSON Schema automatically (`@mcp.tool()` decorator). | P5 uses Python SDK v2. |
| D-138 | Skill catalog format = markdown files under `skills/` keyed to Atelier domain paths. | Markdown is the established Nova docs format (Jekyll Pages, 4-step deck process). Each skill file names the Atelier source path, distills the first-principles, links to agent-checklist triggers, and maps to the BA.A catalog. | P4 authors 9 markdown skill files. |
| D-139 | RACI role names = Citizen Developer / Platform / Release Management (co-owned). | User-specified. The 3 roles are the columns of the RACI table. Release Management is co-owned: QA + SRE attestations are required by the actual release (performed agentically, overseen & triggered by the Citizen Developer). | P2 authors the RACI with these 3 roles. |
| D-140 | MCP server extensibility = plugin-registry (`plugins/<name>.py` implementing `register(mcp)`). | Future capabilities (new scanners, policy evaluators, cost tools) drop in as new plugin files — no `server.py` edits. `server.py` scans `plugins/` and calls `register` on each. This is the extensibility insurance: plugins are decoupled from the server entrypoint. | P5 implements the plugin-registry; initial plugins are `principles.py` + `validation.py`. |
| D-141 | PPTX storage = commit binary directly to `docs/presentations/` (no LFS). | Decks are small (~1-5 MiB); git handles binary blobs. LFS requires server-side support (unverified for git.cloudinit.dev) + client config. Committing directly is simplest and works without any repo/server config. Binary diffs are not delta-friendly, but deck changes are infrequent. | P1/P2/P6 commit .pptx directly. |
| D-142 | Deck render trigger = any phase modifying `docs/presentations/*-marp.md` or `docs/presentations/assets/` must re-render HTML + PPTX, commit PPTX, and attach to the Gitea release. | PPTX was previously manual + release-only (not committed). v1.18 makes it a first-class artifact: committed (history) + attached (download), both always, not optional. Automated via `scripts/render_deck.sh` + `scripts/attach_release_asset.py`. | P1/P2/P6 run the render+commit+attach pipeline. |
## Objective for Milestone v1.19 (complete — Nova 2nd-Release Sync)
> **NFR-only chore milestone.** Ships a patch on the v1.18.x line (tag
> `v1.18.0`). Single execution phase. Establishes the manual-only "2nd
> release" pipeline from `~/acdl` (CIAgent-managed source of truth, full audit
> trail) into `~/nova` (GitLab `jonathanchery/nova` — separate repo, separate
> history, consumer / platform-team audience).
### Why
`~/acdl` is the engineering source of truth and carries the full CIAgent
audit trail (`.ciagent/`, milestone branches, `---ci---` blocks, Gitea
releases). Consumers and the platform team should consume a clean,
conventional-commit-shaped tree without the CIAgent plumbing. The old
`scripts/sync_to_gl.sh` mirrored `~/acdl → ~/gl/acdl` with a single
kitchen-sink `chore: sync from source mirror <ts>` commit — wrong audience,
wrong commit standard, wrong repo.
### What
- **`scripts/sync_to_nova.sh`** replaces `scripts/sync_to_gl.sh`.
- **Manual-only gate**: refuses without `--release` / `RELEASE_CONFIRMED=1`
(exit 2). Never triggerable by CI.
- **Consumer subset only**: excludes `.ciagent/`, `.gitea/`, `.env*`,
`terraform/`, `demo/`, runtime metrics artifacts, and internal-only scripts
(the `EXCLUDE_SCRIPTS` list — CIAgent/ops/release plumbing). Keeps
consumer-facing runbooks (`run_ci.sh`, `run_platform.sh`, etc.) and the
metrics export views (`metrics/README.md`, `powerbi/`, `TRUST_SNAPSHOT.md`).
- **Destination history protected**: rsync `--filter=P .git` ensures
`~/nova/.git` is never touched.
- **Domain-based commits**: 13 fixed-order domains (config → core → adapters
→ modules → contracts → schemas → pipelines → mcp → skills → scripts →
tests → docs → workflows). Each changed domain gets its own conventional
commit, supplied positionally via repeated `-m` flags. No kitchen-sink.
- **Conventional-commit validation**: regex-enforced
(`feat|fix|docs|chore|refactor|perf|test|build|ci|style|revert`); bypass via
`--no-verify-format`.
- **Modes**: `--list-domains` (print order), `--dry-run` (preview rsync +
messages), `--no-push` (commit without pushing), `-v` (verbose).
### Out of Scope
- **coreci / Atelier review gate on the synced tree** — deferred. A future
milestone may run a vendored-Atelier review pass before commit and block on
P0 findings.
- **Tagging releases on the `~/nova` side** — could add `--tag <semver>`
later.
- **Deleting `~/gl`** — the old mirror dir is left on disk; only the sync
script targeting it is removed.
### Requirements
- **REQ-229** — `scripts/sync_to_nova.sh` replaces `sync_to_gl.sh` with the
manual-only, consumer-subset, domain-committed 2nd-release pipeline
described above. (Phase P1)
### Phase Plan
| Phase | Name | Status |
|-------|------|--------|
| P1 | nova-sync-script | complete |
| P2 | final-review-ship | pending |
### Decisions
| ID | Decision | Rationale | Outcome |
|----|----------|-----------|---------|
| D-143 | 2nd release target = `~/nova` (separate GitLab repo), not `~/gl/acdl`. | `~/nova` is consumer/platform-team-facing with its own history; `~/gl/acdl` was an internal mirror with a kitchen-sink commit standard. Separate audience → separate repo → separate commit standard. | `sync_to_nova.sh` targets `~/nova`; `sync_to_gl.sh` removed. |
| D-144 | Commit standard for `~/nova` = real conventional commits per domain (not the `---ci---` audit blocks used in `~/acdl`). | `~/acdl` commits carry CIAgent audit metadata (`---ci---` blocks) for the ciagent auditing workflow; that's noise for platform consumers. `~/nova` gets clean `feat/fix/docs/chore(scope): subject` commits grouped by domain. | Script validates conventional format; domain-based commits via positional `-m`. |
| D-145 | Trigger = manual-only (`--release` / `RELEASE_CONFIRMED=1`). | The 2nd release is a deliberate human action, not a CI side-effect. The gate guarantees it can never fire from Gitea Actions, GitHub Actions, or accidental invocation. | Script exits 2 without `--release`. |
| D-146 | Domain grouping = 13 fixed-order domains by path prefix; messages map positionally over CHANGED domains only. | Avoids the kitchen-sink commit; gives `~/nova` a reviewable, conventional history tailored to platform consumers. Positional-over-changed mapping lets the human supply exactly the messages needed, in domain order, without padding for unchanged domains. | `--list-domains` prints order; `--dry-run` previews; count-mismatch errors clearly. |
| D-147 | coreci / Atelier review gate = deferred this milestone. | The vendored Atelier (`mcp/atelier/vendor`) could review the synced tree before commit and block on P0, but that's an additive hardening step, not part of establishing the pipeline. Deferred to a future milestone. | Sync ships consumer contents as-is; no review gate. |
### CLARIFY auto-resolved parameters (full autonomy)
The following ambiguities were identified and auto-resolved at full
autonomy (no human escalation needed — confidence > 0.6 threshold):
1. **Fix scope** — comprehensive (theme CSS + render scripts + mermaid
re-layout + deck content + tests) vs. minimal. **Resolved: comprehensive.**
The root cause spans all four layers; a theme-only fix would leave
the extreme-aspect-ratio diagrams and the stale `render_deck.sh`
unfixed. Confidence: 0.95.
2. **Pipeline depth** — full pipeline (SPECIFY→CLARIFY→RESEARCH→PLAN→
GRILL→EXECUTE→VERIFY→SHIP) vs. lighter path. **Resolved: full pipeline.**
This is a new milestone (v1.22); the full pipeline ensures the plan
is grilled and the audit trail is complete. Confidence: 0.9.
3. **Mermaid diagram fixes** — re-layout to LR + re-render vs. CSS-only
fix. **Resolved: re-layout to LR + re-render at 2x transparent.**
The `telemetry-live-ops.mmd` uses `flowchart TB` (produced a 1024×1628
PNG — aspect 0.63); the README (line 168) explicitly says to use
horizontal layouts for wide diagrams. CSS-only cannot fix the aspect
ratio. Confidence: 0.95.
4. **`render_deck.sh` disposition** — fix (add `--theme`) vs. delete.
**Resolved: delete.** The README already documents `render_slides.sh`
as canonical; `render_deck.sh` is unreferenced by the build-commands
section and is a footgun (produces unthemed output). Confidence: 0.9.
5. **Slide count change** — keep 18 main + 1 appendix vs. split
overflowing slides. **Resolved: split slides 3 and 8** (18 → 20 main
+ 1 appendix). The `test_marp_deck_slide_count` test + README
convention are updated to match. Confidence: 0.85.
No human escalation. All decisions logged with confidence scores above
the 0.6 threshold.
## Objective for Milestone v1.22 (active — Nova Deck Layout Fix)
v1.22 fixes the systemic layout/formatting problems in the Nova
presentation deck that made every slide look "out of whack" after the
v1.21 P5 re-render. A full investigation determined the root cause is
**not a P5 regression** — the `nova-sp-theme.css` has had zero `section`
padding since it was authored (it declares `/* @theme nova-sp */` as a
comment, not the `@theme` directive, and does not `@import` Marp's
default theme, so Marp's default `section { padding: 56px 64px }` never
applies). Combined with `overflow:hidden` (silent clip), a blunt
`img { max-height: 320px }` rule, header+footer chrome on every slide,
and two new P5 diagrams with extreme aspect ratios (13.52× and 0.63×),
8 of 19 slides overflow and the rest look jammed against the edges.
This milestone is a **comprehensive fix** across four layers: (1) the
theme CSS (padding, overflow handling, aspect-ratio-aware image rules,
title-slide chrome suppression, paragraph/list/table spacing); (2) the
render scripts (delete the stale unthemed `render_deck.sh`, pin
marp-cli/mermaid-cli versions, add 2x scale + transparent bg to
mermaid); (3) the two problematic mermaid diagrams (re-layout to LR +
2-row wrap); (4) the deck content (trim/split the 8 overflowing slides,
remove the redundant `header:` from frontmatter). It also adds the
**layout/aspect-ratio/theme-structural tests** that were missing — the
gap that let this regression through undetected.
**Milestone type:** NFR (all phases are fix/docs/test — no feat/breaking).
Tags run on the **v1.21.x** patch line (previous minor per
branch-strategy): `v1.21.0` (P0) → `v1.21.1..v1.21.5` (P1P5) →
`v1.21.6` (P6 final = milestone release).
**Phase count:** 7 (P0 pre-execution + 5 execution + 1 final).
**Wave ordering:**
- Wave 1 (P1 + P2, parallel): theme CSS + render scripts — no
interdependency. P1 establishes the padding/overflow/image budget that
P4's content trimming relies on; P2 fixes the render pipeline that P3's
PNG re-render depends on.
- Wave 2 (P3 + P4, parallel): mermaid re-layout + deck content. P3
depends on P2 (2x scale flag); P4 depends on P1 (padding budget).
- Wave 3 (P5): re-render HTML + PPTX + add tests. Depends on all above.
- Wave 4 (P6): final review + audit + milestone ship.
**Hard constraints:**
- DO NOT change the deck narrative or the 4-beat arc (Problem → Solution
→ Proof → Roadmap + Ask) — only fix layout/formatting.
- DO NOT re-introduce badges, version strings, or internal citations
(D-###/REQ-###/.py paths) that v1.21 removed.
- The slide count may change from 18 main + 1 appendix to 20 main + 1
appendix (splitting slides 3 and 8 to relieve overflow). The
`test_marp_deck_slide_count` test + README "18 main + 1 appendix"
convention must be updated to match.
- PPTX remains a first-class committed artifact + release attachment.
- No code changes outside `docs/presentations/`, `scripts/render*.sh`,
and `tests/test_slides_pipeline.py`.
### Requirements
New requirements REQ-254..REQ-262 — see `REQUIREMENTS.md` §v1.22. Summary:
- **REQ-254:** Theme CSS — add `section` padding + overflow handling.
- **REQ-255:** Theme CSS — aspect-ratio-aware image rules (replace blunt
`max-height:320px`).
- **REQ-256:** Theme CSS — title-slide chrome suppression + paragraph/
list/table spacing tightening.
- **REQ-257:** Render scripts — delete `render_deck.sh` (or fix `--theme`);
pin marp-cli/mermaid-cli versions.
- **REQ-258:** `render_slides.sh` — add `-s 2 -b transparent` to mermaid-cli
(README spec).
- **REQ-259:** Re-layout `telemetry-live-ops.mmd` from `flowchart TB`
`flowchart LR`; re-render PNG at 2x transparent.
- **REQ-260:** Re-layout `platform-pipeline.mmd` to 2-row subgraph wrap;
re-render PNG at 2x transparent.
- **REQ-261:** Trim/split 8 overflowing slides (3, 5, 6, 8, 9, 12, 15,
A1) + remove redundant `header:` from frontmatter.
- **REQ-262:** Re-render HTML + PPTX + add layout/aspect-ratio/theme-
structural tests.
## v1.23 — Nova Deck Cleanup & Python PPTX
> **Active milestone.** NFR (docs/render/test only; no features).
> Branch: `milestone/v1.23-deck-cleanup-python-pptx`. Tags run on the
> **v1.22.x** patch line: `v1.22.0` (P0) → `v1.22.1..v1.22.5` (P1P5) →
> `v1.22.6` (P6 final = milestone release).
Driven by user feedback that the deck looked "out of whack" and the
desire to return to the clean, well-formatted style of the old
`the-developer-experience.html`. Investigation revealed the "clean"
reference was itself MARP output (using Marp's built-in `default` theme
+ an inline `style:` block); the current deck's standalone
`nova-sp-theme.css` re-derives all base spacing from scratch and had a
zero-padding bug (fixed in v1.22, but the standalone approach is
fragile). The milestone delivers:
- **Single-document consolidation** — `*-marp.md` becomes the sole
source of truth; the plain `.md` is deleted; speaker notes + talking
points are embedded as Marp HTML comments.
- **Clean style restoration** — revert to `theme: default` + inline
`style:` block (S&P palette); `nova-sp-theme.css` retained as a
reference, retired from render.
- **Self-contained HTML** — base64-inline all images for
redistribution.
- **Parallel python-pptx generator** — structured, editable, S&P-themed
PPTX alongside the MARP image-of-slide PPTX.
- **Targeted word-count trim** + removal of the previously-used loaded scope term.
**Phase count:** 7 (P0 pre-execution + 5 execution + 1 final).
**Hard constraints:**
- DO NOT change the deck narrative or the 4-beat arc (Problem → Solution
→ Proof → Roadmap + Ask) — only trim word count.
- DO NOT re-introduce badges, version strings, or internal citations.
- DO NOT remove MARP — it stays for HTML + PPTX; python-pptx runs in
parallel.
- `nova-sp-theme.css` is retained (not deleted) as a styling reference.
### Requirements
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 + loaded-scope-term removal (REQ-271,272), CI/tests/README (REQ-273,274,275).
## v1.25 — kyverno-json Unified Policy Engine
> **Active milestone.** Feature milestone (the primary compliance/policy
> tool becomes kyverno-json, implemented behind a swappable adapter).
> Branch: `milestone/v1.25-kyverno-json`. Tags run on the **v1.24.x**
> patch line: `v1.24.0` (P0) → `v1.24.1..v1.24.4` (P1P4) → `v1.24.5`
> (P5 final = milestone release).
[Nova](https://github.com/kyverno/kyverno-json) `kyverno-json` is a
runtime from the Kyverno ecosystem that applies Kyverno policies to
**any JSON or YAML payload** — not just Kubernetes manifests. This
milestone makes kyverno-json the **primary tool of choice for
compliance / policy checks** in Nova, implemented as an **adapter**
(the `PolicyEngine` protocol) so the platform may one day replace it
with something else (e.g. OPA) without touching the confidence signal
or the pipeline.
### Why
Nova's policy posture today is split across three engines with three
different rule languages and three adapter shapes:
- **Checkov** (`adapters/terraform/policy/checkov_adapter.py`) — the
runtime scanner over `terraform_plan` JSON; carries the
`NOVA_TAG_NAMING` custom rule. Imperative YAML+Python rules.
- **Wiz** (`adapters/wiz/wiz_adapter.py`) — security findings from the
Wiz API; inactive unless credentials are present.
- **Kyverno (K8s)** (`adapters/kyverno/kyverno_adapter.py`) — translates
Kyverno `PolicyReport` results; **inactive for Terraform-only stacks**
(the platform emits Terraform, not K8s manifests — D-053).
All three emit the same `schemas/policy_check_result.schema.json` shape
that `core/confidence_signal.py` consumes engine-agnostically. The
*contract* is already right; the *orchestration* is fragmented. There is
no single place where "what Nova considers compliant" is declared —
tagging lives in a Checkov custom rule, public-ingress in Checkov's
`RULE_MAP`, env-transition destroy in `core/env_transition.py`
(imperative Python), and capability regression in
`core/regression_verify.py` (imperative Python). Each is a different
language, each drifts independently, and the K8s Kyverno adapter can't
help because it only speaks to K8s manifests.
`kyverno-json` fixes this: one declarative policy language (Kyverno
policies with JMESPath assertions) that applies to **any** Nova
artifact — the consumer contract, the resolved Stack IR, the
Terraform plan JSON, and even the PolicyCheckResult list itself
(meta-validation). It becomes the **unified orchestrator** of compliance
checks, while Checkov and Wiz remain as raw-finding adapters that feed
*into* kyverno-json meta-policies (so Nova-specific posture rules sit
on top of, not beside, the scanner findings).
### What the milestone delivers
- **Swappable `PolicyEngine` protocol** (`core/policy_engine.py`) — a
Python Protocol + registry selected from `config.json` (`policy.engine`,
default `"kyverno-json"`). `KyvernoJsonEngine` implements it (shells
to the `kyverno-json` CLI); a future `OpaEngine` implements the same
protocol. The confidence signal and pipeline never import the engine
directly — they go through the registry.
- **`KyvernoJsonEngine` adapter** (`adapters/kyverno-json/`) —
`evaluate(payload, policies) -> list[PolicyCheckResult]` translates
kyverno-json native output to the existing PCR schema. Mirrors the
Checkov/Wiz adapter pattern. `is_configured()` guard skips gracefully
when the `kyverno-json` binary is absent (same pattern as the Wiz
adapter — emits `SKIPPED`, never breaks the pipeline).
- **Policies over all four Nova artifacts** under
`adapters/kyverno-json/policies/`:
- `contract/` — consumer contract JSON (shape + env-promotion rules).
- `stack-ir/` — resolved Target Stack IR (tagging standard,
public-ingress, encryption-by-default — ports of the v1.0/v1.8
imperative rules into declarative policies).
- `plan-json/``terraform show -json` output (plaintext secrets,
IAM wildcards, KMS references — ports of Checkov's `RULE_MAP`).
- `meta/` — policies over the merged PolicyCheckResult list itself
(e.g. `block-on-any-critical` — the single declarative source of
truth for "critical = block", with the existing
`confidence_signal.py` hard-override kept as defense-in-depth).
- **`run_platform.sh` Step 5 wiring** — Checkov/Wiz still run and emit
raw PCRs; `KyvernoJsonEngine.evaluate()` runs plan-JSON policies in
parallel; both PCR lists merge into the confidence signal's `policy`
input. No change to `core/confidence_signal.py` (it already consumes
`list[PolicyCheckResult]` engine-agnostically).
- **Regression-gate-as-policy** (P4 — quality improvement from the
IDEATE pass): the capability checks in
`core/regression_verify.py` (CAP-013, CAP-023, CAP-024) become
declarative kyverno-json policies over the capability-inventory JSON
frontmatter. Capability regression becomes an audit artifact, not
imperative Python.
- **`policy-engineer` persona** (custom, added in RESEARCH) — owns the
policy territory; declarative-policies constraint; kyverno-json +
JMESPath frameworks.
**Phase count:** 6 (P0 pre-execution + 4 execution + 1 final).
**Hard constraints:**
- DO NOT change `schemas/policy_check_result.schema.json` shape in a way
that breaks existing adapters — the contract is the moat. The
`engine` enum already includes `"kyverno"` and `"opa"`; v1.25 records
carry `engine: "kyverno"` (no new enum value — decision in CLARIFY).
- DO NOT remove Checkov or Wiz adapters — they remain as raw-finding
sources feeding into kyverno-json meta-policies.
- DO NOT remove the `confidence_signal.py` `PENALTY["critical"]: None`
hard-override — it stays as defense-in-depth behind the declarative
`block-on-any-critical` meta-policy (decision in CLARIFY).
- DO NOT change `core/confidence_signal.py`'s input contract — it
already consumes `list[PolicyCheckResult]`; v1.25 only changes *who
produces* that list, not *what* the list is.
- The platform must function with `kyverno-json` absent — `is_configured()`
returns false → `SKIPPED` records → confidence signal proceeds (no
hard dependency that breaks the "platform functions without AI /
deterministic scripts" tenet — kyverno-json is deterministic, not AI).
### Requirements
New requirements REQ-291..REQ-309 — see `REQUIREMENTS.md` §v1.25.
Summary: engine protocol + registry (REQ-291,292), kyverno-json engine
impl (REQ-293,294), contract policies (REQ-295,296), stack-IR policies
(REQ-297,298,299), plan-JSON policies + pipeline wiring (REQ-300,301,302),
meta-policies (REQ-303), regression-gate policies (REQ-304,305), docs +
adapter README (REQ-306,307), tests (REQ-308,309).
+43 -44
View File
@@ -1,13 +1,12 @@
{ {
"run_id": "regr-1785591207", "run_id": "regr-1785329757",
"run_at_utc": "2026-08-01T13:33:27Z", "run_at_utc": "2026-07-29T12:55:57Z",
"milestone": "v1.10", "milestone": "v1.10",
"phase": 52, "phase": 52,
"summary": { "summary": {
"Verified": 18, "Verified": 22,
"Decayed": 0, "Decayed": 0,
"Broken": 0, "Broken": 0
"Skipped": 4
}, },
"passed": true, "passed": true,
"results": [ "results": [
@@ -17,7 +16,7 @@
"status": "Verified", "status": "Verified",
"detail": "exit 0; 2 sample contracts validate", "detail": "exit 0; 2 sample contracts validate",
"tier": "local", "tier": "local",
"duration_ms": 235 "duration_ms": 252
}, },
{ {
"capability_id": "CAP-002", "capability_id": "CAP-002",
@@ -25,7 +24,7 @@
"status": "Verified", "status": "Verified",
"detail": "exit 0; env schema validates", "detail": "exit 0; env schema validates",
"tier": "local", "tier": "local",
"duration_ms": 201 "duration_ms": 196
}, },
{ {
"capability_id": "CAP-003", "capability_id": "CAP-003",
@@ -33,7 +32,7 @@
"status": "Verified", "status": "Verified",
"detail": "exit 0; ", "detail": "exit 0; ",
"tier": "local", "tier": "local",
"duration_ms": 261 "duration_ms": 258
}, },
{ {
"capability_id": "CAP-004", "capability_id": "CAP-004",
@@ -41,7 +40,7 @@
"status": "Verified", "status": "Verified",
"detail": "exit 0; ", "detail": "exit 0; ",
"tier": "local", "tier": "local",
"duration_ms": 259 "duration_ms": 264
}, },
{ {
"capability_id": "CAP-005", "capability_id": "CAP-005",
@@ -49,7 +48,7 @@
"status": "Verified", "status": "Verified",
"detail": "exit 0; ", "detail": "exit 0; ",
"tier": "local", "tier": "local",
"duration_ms": 337 "duration_ms": 314
}, },
{ {
"capability_id": "CAP-006", "capability_id": "CAP-006",
@@ -57,7 +56,7 @@
"status": "Verified", "status": "Verified",
"detail": "exit 0; interpolation ok", "detail": "exit 0; interpolation ok",
"tier": "local", "tier": "local",
"duration_ms": 242 "duration_ms": 223
}, },
{ {
"capability_id": "CAP-007", "capability_id": "CAP-007",
@@ -65,7 +64,7 @@
"status": "Verified", "status": "Verified",
"detail": "exit 0; confidence band=pass", "detail": "exit 0; confidence band=pass",
"tier": "local", "tier": "local",
"duration_ms": 91 "duration_ms": 80
}, },
{ {
"capability_id": "CAP-008", "capability_id": "CAP-008",
@@ -73,15 +72,15 @@
"status": "Verified", "status": "Verified",
"detail": "exit 0; outbox hash chain ok", "detail": "exit 0; outbox hash chain ok",
"tier": "local", "tier": "local",
"duration_ms": 456 "duration_ms": 358
}, },
{ {
"capability_id": "CAP-009", "capability_id": "CAP-009",
"name": "offline pytest suite passes", "name": "offline pytest suite passes",
"status": "Verified", "status": "Verified",
"detail": "exit 0; [ 98%]\ntests/test_wiz_adapter_real_client.py ......... [100%]\n\n================= 586 passed, 2 deselected in 71.63s (0:01:11) =================", "detail": "exit 0; [ 98%]\ntests/test_wiz_adapter_real_client.py ......... [100%]\n\n====================== 462 passed, 2 deselected in 34.63s ======================",
"tier": "local", "tier": "local",
"duration_ms": 72988 "duration_ms": 36065
}, },
{ {
"capability_id": "CAP-010", "capability_id": "CAP-010",
@@ -89,63 +88,63 @@
"status": "Verified", "status": "Verified",
"detail": "exit 0; resource(s))\n\n=== PLATFORM CHECK OK ===\ncontract -> resolver -> stack -> adapter -> structure validated (offline, no AWS)\ncheck-only: OK\n\n=== CI PIPELINE OK ===\n3 stages passed: lint, test, check-only", "detail": "exit 0; resource(s))\n\n=== PLATFORM CHECK OK ===\ncontract -> resolver -> stack -> adapter -> structure validated (offline, no AWS)\ncheck-only: OK\n\n=== CI PIPELINE OK ===\n3 stages passed: lint, test, check-only",
"tier": "local", "tier": "local",
"duration_ms": 73275 "duration_ms": 40668
}, },
{ {
"capability_id": "CAP-011", "capability_id": "CAP-011",
"name": "headline E2E runs against the local emulating tier (microservice)", "name": "headline E2E runs against the local emulating tier (microservice)",
"status": "Verified", "status": "Verified",
"detail": "exit 0; al-emulator\",\n \"desired_count\": 1,\n \"running_count\": 1\n },\n \"outbox_dir\": \"/tmp/nova_local_e2e_6vnrnin1/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}", "detail": "exit 0; al-emulator\",\n \"desired_count\": 1,\n \"running_count\": 1\n },\n \"outbox_dir\": \"/tmp/acdl_local_e2e_416d0fmr/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}",
"tier": "local", "tier": "local",
"duration_ms": 634 "duration_ms": 583
}, },
{ {
"capability_id": "CAP-012", "capability_id": "CAP-012",
"name": "local E2E on the static-assets stack (no ECS)", "name": "local E2E on the static-assets stack (no ECS)",
"status": "Verified", "status": "Verified",
"detail": "exit 0; nova_local_e2e_uq4kkhze/tf\",\n \"backend\": \"local\",\n \"ecs\": null,\n \"outbox_dir\": \"/tmp/nova_local_e2e_uq4kkhze/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}", "detail": "exit 0; acdl_local_e2e_ijhcj1z8/tf\",\n \"backend\": \"local\",\n \"ecs\": null,\n \"outbox_dir\": \"/tmp/acdl_local_e2e_ijhcj1z8/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}",
"tier": "local", "tier": "local",
"duration_ms": 584 "duration_ms": 489
}, },
{ {
"capability_id": "CAP-013", "capability_id": "CAP-013",
"name": "terraform init+validate+plan live AWS (microservice)", "name": "terraform init+validate+plan live AWS (microservice)",
"status": "Skipped", "status": "Verified",
"detail": "terraform init: state bucket absent (post-v1.11-teardown, D-096) [microservice]", "detail": "terraform init+validate+plan OK (live AWS, microservice)",
"tier": "live-aws", "tier": "live-aws",
"duration_ms": 737 "duration_ms": 28811
}, },
{ {
"capability_id": "CAP-014", "capability_id": "CAP-014",
"name": "terraform init+validate+plan live AWS (static-assets)", "name": "terraform init+validate+plan live AWS (static-assets)",
"status": "Skipped", "status": "Verified",
"detail": "terraform init: state bucket absent (post-v1.11-teardown, D-096) [static-assets]", "detail": "terraform init+validate+plan OK (live AWS, static-assets)",
"tier": "live-aws", "tier": "live-aws",
"duration_ms": 676 "duration_ms": 31772
}, },
{ {
"capability_id": "CAP-015", "capability_id": "CAP-015",
"name": "DynamoDB outbox table exists (live AWS)", "name": "DynamoDB outbox table exists (live AWS)",
"status": "Skipped", "status": "Verified",
"detail": "nova-outbox absent (post-v1.11-teardown steady state, D-096)", "detail": "acdl-outbox exists, item_count=9",
"tier": "live-aws", "tier": "live-aws",
"duration_ms": 664 "duration_ms": 477
}, },
{ {
"capability_id": "CAP-016", "capability_id": "CAP-016",
"name": "S3 state bucket exists + readable (live AWS)", "name": "S3 state bucket exists + readable (live AWS)",
"status": "Skipped", "status": "Verified",
"detail": "state bucket nova-tfstate-581513795199-us-east-1 absent (post-v1.11-teardown, D-096)", "detail": "state bucket exists, keys=['platform/terraform.tfstate', 'spike/alb/dev/terraform.tfstate', 'spike/cdn/dev/terraform.tfstate', 'spike/ci-vpc/terraform.tfstate', 'spike/clus/dev/terraform.tfstate']",
"tier": "live-aws", "tier": "live-aws",
"duration_ms": 245 "duration_ms": 324
}, },
{ {
"capability_id": "CAP-017", "capability_id": "CAP-017",
"name": "DynamoDB nova-contracts table (lifecycle pipeline evidence)", "name": "DynamoDB acdl-contracts table (lifecycle pipeline evidence)",
"status": "Verified", "status": "Verified",
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve", "detail": "terraform files present + simple/complex contracts resolve",
"tier": "lifecycle-pipeline", "tier": "lifecycle-pipeline",
"duration_ms": 586 "duration_ms": 520
}, },
{ {
"capability_id": "CAP-018", "capability_id": "CAP-018",
@@ -153,39 +152,39 @@
"status": "Verified", "status": "Verified",
"detail": "LocalLambdaStub instantiates (local tier evidence)", "detail": "LocalLambdaStub instantiates (local tier evidence)",
"tier": "lifecycle-pipeline", "tier": "lifecycle-pipeline",
"duration_ms": 138 "duration_ms": 137
}, },
{ {
"capability_id": "CAP-019", "capability_id": "CAP-019",
"name": "ECS cluster + service (L2 microservice lifecycle evidence)", "name": "ECS cluster + service (L2 microservice lifecycle evidence)",
"status": "Verified", "status": "Verified",
"detail": "L2 composition resolves (simple + complex contracts; offline proxy)", "detail": "L2 composition resolves (simple + complex contracts)",
"tier": "lifecycle-pipeline", "tier": "lifecycle-pipeline",
"duration_ms": 519 "duration_ms": 534
}, },
{ {
"capability_id": "CAP-020", "capability_id": "CAP-020",
"name": "CloudFront + WAF (L2 static-assets lifecycle evidence)", "name": "CloudFront + WAF (L2 static-assets lifecycle evidence)",
"status": "Verified", "status": "Verified",
"detail": "L2 composition resolves (simple + complex contracts; offline proxy)", "detail": "L2 composition resolves (simple + complex contracts)",
"tier": "lifecycle-pipeline", "tier": "lifecycle-pipeline",
"duration_ms": 521 "duration_ms": 567
}, },
{ {
"capability_id": "CAP-021", "capability_id": "CAP-021",
"name": "uptime-kuma (L1 uptime lifecycle evidence)", "name": "uptime-kuma (L1 uptime lifecycle evidence)",
"status": "Verified", "status": "Verified",
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve", "detail": "terraform files present + simple/complex contracts resolve",
"tier": "lifecycle-pipeline", "tier": "lifecycle-pipeline",
"duration_ms": 562 "duration_ms": 606
}, },
{ {
"capability_id": "CAP-022", "capability_id": "CAP-022",
"name": "OIDC role (L1 iam-role lifecycle evidence)", "name": "OIDC role (L1 iam-role lifecycle evidence)",
"status": "Verified", "status": "Verified",
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve", "detail": "terraform files present + simple/complex contracts resolve",
"tier": "lifecycle-pipeline", "tier": "lifecycle-pipeline",
"duration_ms": 611 "duration_ms": 529
} }
] ]
} }
+28 -28
View File
@@ -1,51 +1,51 @@
# Regression Report — v1.10 Phase 52 # Regression Report — v1.10 Phase 52
- **Run ID:** `regr-1785591207` - **Run ID:** `regr-1785329757`
- **Run at (UTC):** 2026-08-01T13:33:27Z - **Run at (UTC):** 2026-07-29T12:55:57Z
- **Summary:** {'Verified': 18, 'Decayed': 0, 'Broken': 0, 'Skipped': 4} - **Summary:** {'Verified': 22, 'Decayed': 0, 'Broken': 0}
- **Passed (milestone gate):** True - **Passed (milestone gate):** True
| Capability | Name | Tier | Status | Duration (ms) | Detail | | Capability | Name | Tier | Status | Duration (ms) | Detail |
|-----------|------|------|--------|--------------|--------| |-----------|------|------|--------|--------------|--------|
| CAP-001 | contract.schema.json validates sample contracts | local | **Verified** | 235 | exit 0; 2 sample contracts validate | | CAP-001 | contract.schema.json validates sample contracts | local | **Verified** | 252 | exit 0; 2 sample contracts validate |
| CAP-002 | environment.schema.json validates env files | local | **Verified** | 201 | exit 0; env schema validates | | CAP-002 | environment.schema.json validates env files | local | **Verified** | 196 | exit 0; env schema validates |
| CAP-003 | contract_resolver resolves static-assets | local | **Verified** | 261 | exit 0; | | CAP-003 | contract_resolver resolves static-assets | local | **Verified** | 258 | exit 0; |
| CAP-004 | contract_resolver resolves microservice | local | **Verified** | 259 | exit 0; | | CAP-004 | contract_resolver resolves microservice | local | **Verified** | 264 | exit 0; |
| CAP-005 | terraform adapter emits .tf files | local | **Verified** | 337 | exit 0; | | CAP-005 | terraform adapter emits .tf files | local | **Verified** | 314 | exit 0; |
| CAP-006 | contract interpolation expands env/contract tokens | local | **Verified** | 242 | exit 0; interpolation ok | | CAP-006 | contract interpolation expands env/contract tokens | local | **Verified** | 223 | exit 0; interpolation ok |
| CAP-007 | confidence_signal.compute returns a band | local | **Verified** | 91 | exit 0; confidence band=pass | | CAP-007 | confidence_signal.compute returns a band | local | **Verified** | 80 | exit 0; confidence band=pass |
| CAP-008 | outbox_writer builds a hash-chained item | local | **Verified** | 456 | exit 0; outbox hash chain ok | | CAP-008 | outbox_writer builds a hash-chained item | local | **Verified** | 358 | exit 0; outbox hash chain ok |
| CAP-009 | offline pytest suite passes | local | **Verified** | 72988 | exit 0; [ 98%] | CAP-009 | offline pytest suite passes | local | **Verified** | 36065 | exit 0; [ 98%]
tests/test_wiz_adapter_real_client.py ......... [100%] tests/test_wiz_adapter_real_client.py ......... [100%]
================= 586 passed, 2 | ====================== 462 passe |
| CAP-010 | run_ci.sh reproduces CI pipeline locally | local | **Verified** | 73275 | exit 0; resource(s)) | CAP-010 | run_ci.sh reproduces CI pipeline locally | local | **Verified** | 40668 | exit 0; resource(s))
=== PLATFORM CHECK OK === === PLATFORM CHECK OK ===
contract -> resolver -> stack -> adapter -> structure validated (offline, no AWS) contract -> resolver -> stack -> adapter -> structure validated (offline, no AWS)
check-only: OK check-only: OK
=== CI PIPELIN | === CI PIPELIN |
| CAP-011 | headline E2E runs against the local emulating tier (microservice) | local | **Verified** | 634 | exit 0; al-emulator", | CAP-011 | headline E2E runs against the local emulating tier (microservice) | local | **Verified** | 583 | exit 0; al-emulator",
"desired_count": 1, "desired_count": 1,
"running_count": 1 "running_count": 1
}, },
"outbox_dir": "/tmp/nova_local_e2e_6vnrnin1/outbox", "outbox_dir": "/tmp/acdl_local_e2e_416d0fmr/outbox",
"outbox_events": 2, "outbox_events": 2,
"outbox | "outbox |
| CAP-012 | local E2E on the static-assets stack (no ECS) | local | **Verified** | 584 | exit 0; nova_local_e2e_uq4kkhze/tf", | CAP-012 | local E2E on the static-assets stack (no ECS) | local | **Verified** | 489 | exit 0; acdl_local_e2e_ijhcj1z8/tf",
"backend": "local", "backend": "local",
"ecs": null, "ecs": null,
"outbox_dir": "/tmp/nova_local_e2e_uq4kkhze/outbox", "outbox_dir": "/tmp/acdl_local_e2e_ijhcj1z8/outbox",
"outbox_events": 2, "outbox_events": 2,
"outbox | "outbox |
| CAP-013 | terraform init+validate+plan live AWS (microservice) | live-aws | **Skipped** | 737 | terraform init: state bucket absent (post-v1.11-teardown, D-096) [microservice] | | CAP-013 | terraform init+validate+plan live AWS (microservice) | live-aws | **Verified** | 28811 | terraform init+validate+plan OK (live AWS, microservice) |
| CAP-014 | terraform init+validate+plan live AWS (static-assets) | live-aws | **Skipped** | 676 | terraform init: state bucket absent (post-v1.11-teardown, D-096) [static-assets] | | CAP-014 | terraform init+validate+plan live AWS (static-assets) | live-aws | **Verified** | 31772 | terraform init+validate+plan OK (live AWS, static-assets) |
| CAP-015 | DynamoDB outbox table exists (live AWS) | live-aws | **Skipped** | 664 | nova-outbox absent (post-v1.11-teardown steady state, D-096) | | CAP-015 | DynamoDB outbox table exists (live AWS) | live-aws | **Verified** | 477 | acdl-outbox exists, item_count=9 |
| CAP-016 | S3 state bucket exists + readable (live AWS) | live-aws | **Skipped** | 245 | state bucket nova-tfstate-581513795199-us-east-1 absent (post-v1.11-teardown, D-096) | | CAP-016 | S3 state bucket exists + readable (live AWS) | live-aws | **Verified** | 324 | state bucket exists, keys=['platform/terraform.tfstate', 'spike/alb/dev/terraform.tfstate', 'spike/cdn/dev/terraform.tfstate', 'spike/ci-vpc/terraform.tfstate', |
| CAP-017 | DynamoDB nova-contracts table (lifecycle pipeline evidence) | lifecycle-pipeline | **Verified** | 586 | terraform files present + fmt -check passes + simple/complex contracts resolve | | CAP-017 | DynamoDB acdl-contracts table (lifecycle pipeline evidence) | lifecycle-pipeline | **Verified** | 520 | terraform files present + simple/complex contracts resolve |
| CAP-018 | Lambda contract-ingestor (local stub + lifecycle evidence) | lifecycle-pipeline | **Verified** | 138 | LocalLambdaStub instantiates (local tier evidence) | | CAP-018 | Lambda contract-ingestor (local stub + lifecycle evidence) | lifecycle-pipeline | **Verified** | 137 | LocalLambdaStub instantiates (local tier evidence) |
| CAP-019 | ECS cluster + service (L2 microservice lifecycle evidence) | lifecycle-pipeline | **Verified** | 519 | L2 composition resolves (simple + complex contracts; offline proxy) | | CAP-019 | ECS cluster + service (L2 microservice lifecycle evidence) | lifecycle-pipeline | **Verified** | 534 | L2 composition resolves (simple + complex contracts) |
| CAP-020 | CloudFront + WAF (L2 static-assets lifecycle evidence) | lifecycle-pipeline | **Verified** | 521 | L2 composition resolves (simple + complex contracts; offline proxy) | | CAP-020 | CloudFront + WAF (L2 static-assets lifecycle evidence) | lifecycle-pipeline | **Verified** | 567 | L2 composition resolves (simple + complex contracts) |
| CAP-021 | uptime-kuma (L1 uptime lifecycle evidence) | lifecycle-pipeline | **Verified** | 562 | terraform files present + fmt -check passes + simple/complex contracts resolve | | CAP-021 | uptime-kuma (L1 uptime lifecycle evidence) | lifecycle-pipeline | **Verified** | 606 | terraform files present + simple/complex contracts resolve |
| CAP-022 | OIDC role (L1 iam-role lifecycle evidence) | lifecycle-pipeline | **Verified** | 611 | terraform files present + fmt -check passes + simple/complex contracts resolve | | CAP-022 | OIDC role (L1 iam-role lifecycle evidence) | lifecycle-pipeline | **Verified** | 529 | terraform files present + simple/complex contracts resolve |
+21 -1796
View File
File diff suppressed because it is too large Load Diff
+821 -388
View File
File diff suppressed because it is too large Load Diff
+302 -90
View File
@@ -1,112 +1,324 @@
# Nova v1.16 — Multi-Persona Code Review (final phase P21) # ACDL v1.11 — Multi-Persona Code Review (P60P65 retrofit + new work)
**Reviewer:** lead-developer (model: glm-5.2) **Reviewer:** ci-code-reviewer (model: glm-5.2)
**Scope:** v1.16 milestone — 22 tags (v1.15.5..v1.15.26), 20 execution **Scope:** v1.11 milestone, branch `milestone/v1.11-restart` — 22 commits
phases + final. Squash-merged to main via `milestone/v1.16-nova-simplification`. (e1bb214..8c09580), 25 files, +790/-142 lines
**Date:** 2026-07-30 **Date:** 2026-07-29
> **Historical note:** REVIEW.md was reconstructed at v1.16 P21 (the ## Commits reviewed
> v1.3v1.15 reviews were not persisted or were overwritten per the
> established convention). The v1.16 review overwrites prior content.
## Review approach | Commit | Phase | Type | Summary |
|--------|-------|------|---------|
The v1.16 milestone is an NFR sweep (no new features). Each of the 20 | e1bb214 | 60 | docs | retrofit plan — L1 lifecycle pipeline live-run |
execution phases shipped with a 4-layer verify (structural/behavioral/ | bc9058f | 60 | feat | L1 module lifecycle live run — module fixes (retrofit) |
security/quality) + `run_ci.sh` 3-stage PASS at every phase boundary. | bb3ac7c | 60 | fix | WAF scope case + VPC modify DependencyViolation |
The final-phase review (P21) is a milestone-level cross-phase check, | 0c5c4d1 | 61 | docs | create phase plan — L2 lifecycle pipeline author |
not a per-phase re-review (the per-phase verify already ran). | 361fe60 | 61 | feat | L2 lifecycle pipeline — extend matrix + workflows + tests |
| 9ac5720 | 61 | verify | 4-layer gate — PASS |
| 6441633 | 62 | docs | create phase plan — L2 lifecycle pipeline live run |
| 4dad967 | 60 | fix | ALB target group name_prefix — avoid orphaned conflicts |
| adfcf86 | 63 | docs | create phase plan — regression registry + cost docs |
| b71e63c | 63 | feat | CAP-017..022 regression registry + COST.md |
| beac2ef | 63 | verify | 4-layer gate — PASS |
| 06f4fc7 | 60 | fix | free disk space in lifecycle jobs |
| 92bb03e | 64 | docs | create phase plan — pre-mortem + teardown |
| 186cdde | 64 | feat | pre-mortem — v1.10 post-mortem + forward pre-mortem |
| 4102950 | 64 | feat | pre-mortem + teardown plan — HITL escalation CHG0680001 |
| 7c4fc1f | 64 | feat | teardown complete — zero live ACDL resources remain |
| a52f8a5 | 64 | verify | 4-layer gate — PASS |
| a03c019 | 60/62 | fix | ALB name_prefix + adapter dedup + L2 composition wiring |
| 93a6598 | 65 | docs | create phase plan — rewrite caps + decks |
| 6394801 | 65 | feat | rewrite caps — CAP-017..022 Verified via lifecycle pipeline |
| fc91f24 | 65 | verify | 4-layer gate — PASS |
| 8c09580 | 65 | docs | update v1.11 status — all phases complete |
## P0 issues (0) ## P0 issues (0)
No blocking issues found. The 4-layer verify at each phase boundary + No blocking issues found. The targeted fixes are correct for their stated
the regression gate (D-118, 18V+4S at P9 + P21) are the structural purposes. The 447 fast offline tests pass (485/490 collected; 5 slow
controls. No P0 was auto-applied at P21. deselected, including 2 slow regression-integration tests that exercise the
CAPABILITY_REGISTRY against the live codebase).
## P1 issues (0) ## P1 issues (5 — should fix)
No P1 issues flagged. The grill binding decisions (G-111..G-113) were ### P1-1: Adapter dedup silently drops resources whose module is not in the registry
incorporated into the plan before execution; the regression gate (G-111) [correctness] `adapters/terraform/adapter.py:159-170`
passed at both checkpoints (P9 + P21).
## P2 issues (2 — post-hoc, non-blocking) The new dedup loop only adds resources to `seen` when `tf_dir` is truthy
(in the registry). A resource whose module is missing from the registry is
**silently dropped** from `merged` — it never reaches `_emit_module_block`,
so no error is raised. The pre-dedup code (`parts.extend(... for r in
resources)`) would have raised `ValueError("no terraform_dir in registry
for module ...")` via `_emit_module_block`, surfacing the misconfiguration.
### P2-1: Onboarding framing (E-002, deferred from grill) Confirmed by simulation: two resources, one with `module: nonexistent@1.0.0`,
[scope] `.ciagent/PROJECT.md`, `.ciagent/ROADMAP.md` produces a `merged` list of length 1 — the unknown-module resource vanishes
without diagnostic.
The grill escalation E-002 (confidence 0.55) flagged that the PROJECT.md **Recommendation:** in the dedup loop, when `tf_dir` is `None`, either
framing "first self-service onboarding request path" may over-promise (a) raise immediately (preserving the prior contract), or (b) append the
relative to a request-*acceptance* path that writes a pending row + resource to a separate `unknown` list and extend `parts` with it so
generates an env-file + proves the role Terraform offline but never `_emit_module_block` raises the descriptive error. As written, a typo in
fulfills (no live role grant). The milestone is internally consistent a composition's `module` field (e.g. `iam-role@1.0.0` vs `iam_roles@1.0.0`)
with D-113 (request-path only) — the wording is the only risk. The will silently omit a resource from the emitted terraform — a class of
ROADMAP/PROJECT use "request path" (not "request-fulfillment"), and the defect the v1.10 sweep was specifically created to catch.
Out-of-Scope section explicitly defers real AWS provisioning. **Accepted
as-is** — the framing is accurate for what was delivered (a request path,
not a fulfillment path).
### P2-2: REVIEW.md + AUDIT.md not updated during the run ### P1-2: L2 static-assets "modify" example is a no-op — complex ≡ simple
[maintainability] `.ciagent/REVIEW.md`, `.ciagent/AUDIT.md` [correctness] `modules/l2/static-assets/examples/complex.yml`,
`modules/l2/static-assets/composition.json`
REVIEW.md still held v1.11 content during the v1.16 run (the per-phase The complex.yml comment claims "Modify variant: same bucket_name as simple
verify ran but wasn't persisted to REVIEW.md until P21). AUDIT.md held (in-place modify, adds CDN + WAF)". But resolving both examples yields
v1.15 content. Both are reconstructed at P21 (this review + the audit **identical** resource sets: `['s3','cloudfront-distribution',
running now). This matches the established convention (REVIEW.md is 'cloudfront-originaccesscontrol','waf','kms']`. The CDN and WAF are
overwritten at milestone complete; the per-phase verify commits are the **always present** in the static-assets composition (they are unconditional
record). Not a defect. children + wires); the `waf_enabled`, `default_ttl`, `max_ttl`,
`price_class`, `viewer_protocol_policy` inputs in complex.yml have **no
corresponding wires** in composition.json and are silently dropped at
resolve time. So the L2 static-assets lifecycle cell's "modify" step
applies a contract that produces the same terraform as "simple" — it
exercises `terraform apply` twice with no change, not a true modify.
This is not a regression (the inputs were never wired), but the
CAPABILITY_INVENTORY claim "CAP-020 Verified live-aws via L2 static-assets
lifecycle pipeline (apply/modify/destroy exit 0)" overstates what the
modify step proves: it proves idempotent re-apply, not in-place modify.
**Recommendation:** either (a) wire `waf_enabled`/`default_ttl`/etc. in
composition.json so the complex contract genuinely differs, or (b) correct
the comment + CAPABILITY_INVENTORY wording to "apply + idempotent re-apply
+ destroy" rather than "apply/modify/destroy". The microservice complex
example, by contrast, is a real modify (desired_count 1→2) — that one is
fine.
### P1-3: L2 lifecycle scripts ignore the ci-vpc-outputs.json argument
[correctness] `scripts/run_l2_lifecycle_test.sh:14`,
`scripts/run_l2_lifecycle_destroy.sh:12`
Both L2 scripts declare `Usage: ... <module> <example> [ci-vpc-outputs.json]`
but neither reads `$3`/`$2`. The microservice composition references the
platform VPC via `terraform_remote_state` (data source), and the script
sets `ACDL_REMOTE_STATE_KEY=spike/ci-vpc/terraform.tfstate` so the data
source reads from the CI VPC state — that part is correct. But the
`ci-vpc-outputs.json` argument is positional noise: the workflow passes
it (`run_l2_lifecycle_test.sh ${{ matrix.module }} simple
/tmp/ci-vpc-outputs.json`) and it is silently ignored. The L1 scripts
(`run_lifecycle_test.sh`) inject VPC outputs by rewriting the contract in
Python; the L2 path takes a different approach (remote state) and does not
need the file, so the argument is vestigial, not a bug — but the usage
string advertises a feature the script does not provide, which will
confuse a future maintainer who assumes parity with the L1 scripts.
**Recommendation:** remove the `[ci-vpc-outputs.json]` token from the
usage strings (or add a comment explaining the L2 path uses remote state
and the arg is accepted-but-ignored for workflow-argument parity).
### P1-4: CAPABILITY_INVENTORY summary table is stale (says 16, body lists 22)
[maintainability] `.ciagent/CAPABILITY_INVENTORY.md:9-16`
The Summary table still reads "Verified 16 / Decayed 0 / Broken 0 / Total
16" — the v1.10 sweep count. The body (lines 93-110) now lists CAP-017..022
as **Verified** via the lifecycle pipeline, bringing the real total to 22.
The two counts disagree: a reader scanning the summary sees 16 Verified; a
reader scanning the inventory body sees 22 Verified. The PRE_MORTEM
(lines 82-83) and CAPABILITY_INVENTORY prose both assert all 22 are
Verified, but the headline table was not updated in the P65 rewrite.
**Recommendation:** update the Summary table to "Verified 22 / Decayed 0
/ Broken 0 / Total 22" and add CAP-017..022 rows to the Inventory table
(the body section "Cloud capabilities NOT re-verified..." is now
mis-titled — they ARE verified, just via the lifecycle-pipeline tier).
### P1-5: CAP-017..022 regression checks are offline proxies, not pipeline evidence
[adversarial] `core/regression_verify.py:432-519`,
`.ciagent/CAPABILITY_INVENTORY.md:93-110`
The CAP-017..022 checks (`_check_cap_017_dynamodb` etc.) call
`_check_lifecycle_module_terraform` / `_check_lifecycle_l2_module`, which
verify only that (a) the terraform dir + required files exist and (b) the
example contracts **resolve** (resolver exit 0). They do **not** run
`terraform validate`, do not run apply/modify/destroy, and do not query
the pipeline's actual green/red status. The CAPABILITY_INVENTORY claims
"Evidence = L1 rds module lifecycle pipeline green (terraform validate +
contracts resolve)" — but the check does not run terraform validate, and
"lifecycle pipeline green" is asserted, not verified by the regression
gate.
This means the lifecycle-pipeline evidence CAN be faked at the regression
tier: a module whose terraform is syntactically broken (e.g.
`scope = upper(var.scope)` removed, or a missing required variable) would
still pass `_check_lifecycle_module_terraform` as long as the files exist
and the resolver runs. The real green/red evidence lives only in the
workflow run history (Gitea/GitHub Actions), which the regression gate does
not read.
**Mitigation context:** the modules-lifecycle workflow IS the live
evidence — when it runs on a PR, the cells genuinely apply/modify/destroy
against live AWS. The gap is that the *regression gate* (which gates
milestone COMPLETE) trusts the workflow will be run, rather than proving it
was run and passed. A milestone could in principle be marked COMPLETE with
CAP-017..022 "Verified" if the regression gate runs but the workflow was
never executed (e.g. workflow_dispatch never triggered, or the PR was
merged without the workflow running).
**Recommendation:** (a) tighten the CAP-017..022 check docstrings + the
CAPABILITY_INVENTORY wording to "terraform files present + contracts
resolve (offline proxy; live apply/modify/destroy verified by the
modules-lifecycle workflow run, not by this gate)"; and/or (b) add a
`terraform validate` step to `_check_lifecycle_module_terraform` (slow but
cheap relative to init+apply) so at least HCL syntax is verified at the
gate. The teardown trustworthiness (P64) is good — `ci-vpc-destroy` runs
`if: always()` and the decommission `---ci---` block is the audit trail.
## P2 issues (4 — post-hoc)
### P2-1: ALB `name_prefix = "tg-ci-"` discards `var.name` entirely
[maintainability] `modules/l1/alb/terraform/main.tf:9`
The fix replaces `name = var.name` with `name_prefix = "tg-ci-"` (a
hardcoded literal). This is the correct terraform pattern for
create_before_destroy resources with name-uniqueness constraints, and the
commit message explains the orphaned-resource motivation well. However
the target group name is now non-configurable (always `tg-ci-<random>`),
and the `var.name` variable is no longer used by the target group at all
(it is still used by `aws_lb.this.name`). A consumer who sets `name:
my-app` gets an LB named `my-app` but a target group named `tg-ci-...` —
inconsistent tagging. Consider `name_prefix = "${var.name}-"` to keep the
consumer's name as a prefix while preserving uniqueness. Post-hoc: not
blocking; the lifecycle pipeline is the only current consumer and `tg-ci-`
is fine for CI.
### P2-2: No test covers the new dedup merge behavior or `ACDL_REMOTE_STATE_KEY`
[testing] `tests/test_adapter.py`, `tests/test_pipeline_contract.py`
The adapter gained (a) a dedup-merge loop for multi-resource L1s sharing a
terraform dir and (b) `ACDL_REMOTE_STATE_KEY` env override for the remote
state data block. Neither has a unit test:
- No test asserts that two resources with the same `module` collapse to one
`module "<first_id>" { ... }` block with merged inputs.
- No test asserts that `ACDL_REMOTE_STATE_KEY` overrides the default
`platform/terraform.tfstate` key in the emitted `data
terraform_remote_state` block.
- No test covers the L2 lifecycle scripts (`run_l2_lifecycle_test.sh` /
`run_l2_lifecycle_destroy.sh`) — the L1 equivalents are also untested at
the script level, so this is consistent with existing practice, but the
L2 scripts are new in this session and the `ACDL_REMOTE_STATE_KEY` wiring
is the load-bearing correctness mechanism for the microservice lifecycle.
The 485 offline tests adequately cover the *contract* (pipeline schema,
byte-identical workflows, matrix membership, job needs) — the
`TestModulesLifecyclePipeline` class is solid (89 tests pass). The gap is
adapter *behavior* at the unit level.
**Recommendation:** add a `test_adapter_dedup_merges_same_module` and a
`test_adapter_remote_state_key_override` to `tests/test_adapter.py`.
### P2-3: `waf` complex example uses `scope: CLOUDFRONT` but WAF scope is now `upper()`'d
[correctness] `modules/l1/waf/examples/complex.yml:8`,
`modules/l1/waf/terraform/locals.tf:3`
The `locals.tf` change `scope = upper(var.scope)` is the correct defensive
fix (the AWS provider requires `CLOUDFRONT`/`REGIONAL` regardless of input
case). The complex.yml was simultaneously changed from `scope: cloudfront`
to `scope: CLOUDFRONT`. Both are now correct, but the example's uppercase
value is now redundant with the `upper()` — a future reader may wonder
which is authoritative. Minor; the defensive `upper()` is the right call
and the example matching it is fine. Post-hoc only.
### P2-4: COST.md reproducibility snippet could leak the account ID via CloudTrail
[security] `.ciagent/COST.md:106`
COST.md contains the AWS account ID `581513795199` in multiple places
(summary, S3 bucket name, methodology). This is consistent with the rest of
the repo (the bucket name `acdl-tfstate-581513795199-us-east-1` is hardcoded
in `adapter.py:130` and `adapter.py:146`), so it is not new leakage and not
a regression. No actual secret material (access keys, secret access keys)
appears in COST.md, PRE_MORTEM.md, CAPABILITY_INVENTORY.md, or the workflow
files — all credential references use `${{ secrets.ACDL_AWS_* }}` or env
var names only. The `.ciagent/PROJECT.md:731` reference to a deactivated
root key is redacted (`AKIA…ROOT-DEACTIVATED`). **No credential leakage
found.** The P2 is only that the account ID is published; if the account
is meant to be opaque, this is an accepted exposure (the bucket name
already requires it).
## What is correct ## What is correct
- **State-bucket drift fix (P1):** `adapter.py:117` now emits - **WAF scope fix (`upper(var.scope)`):** correct and defensive; AWS
`nova-tfstate-*` (matching the live bucket renamed in v1.15 P4). The provider v5 requires uppercase. The `local.scope` indirection is clean.
new `test_adapt_emits_nova_state_bucket` regression guard asserts this. - **VPC `create_before_destroy` + same-CIDR complex example:** correct
- **Kyverno label fix (P1):** `require-resource-labels.yml` enforces fix for the DependencyViolation on modify. Using the same CIDR means
`nova:*` labels (consistent with `nova_tagging.py` hard-fail on terraform modifies in-place rather than replacing the VPC (which would
`acdl:*`). No policy contradiction. cascade-fail on dependent subnets/IGW). The `create_before_destroy`
- **Ingestor defense-in-depth (P10):** fail-closed on missing IAM lifecycle is the right guard.
identity (401, not silent pass); env enum derived from - **ALB `name_prefix`:** correct terraform pattern for
`core/environments/` (not hardcoded). The `NOVA_LAMBDA_LOCAL_BYPASS` create_before_destroy + name-uniqueness; well-documented commit message.
env allows local/stub testing without blocking the fail-closed path. - **Adapter dedup (for the registered-module case):** correct —
- **Payload validation (P11):** 256 KB size cap + contract.schema.json multi-resource L1s like cloudfront (distribution + OAC) correctly merge
validation before the DynamoDB write; aligned error/stackTrace caps into one `module "cloudfront-distribution" { ... }` block. The merge
(both 10000). preserves first-resource inputs and union of outputs. (The
- **Regression gate (G-111):** CAP-013..016 return `Skipped` (not unregistered-module drop is P1-1, a separate concern.)
`Decayed`/`Broken`) for the post-teardown steady state (D-096). - **L2 composition wiring (`ecr.inputs.name`, `roles.inputs.role_name`):**
`passed` accepts Skipped. Gate passes at 18V+4S. correct. Resolving microservice complex now shows `ecr.inputs.name =
- **Workflow generator (P8):** `sync_workflows.py` + `workflows-src/` "app-repo"` and `roles.inputs.role_name = "app-role"` (defaults applied
single source; the byte-identity test is replaced with a generator- since the contract doesn't set `name`). Previously these would have hit
output test (`--check` exits 0). The 3 pairs are no longer hand-synced. the "missing required arg" defect class from the v1.10 sweep.
- **Onboarding request path (P18-P20):** schema + Lambda action (pending - **Microservice complex = real modify:** `desired_count: 2` (vs simple's
CMDB row, no AWS resources) + env-file autogen + offline-proven default 1) is a genuine in-place modify — confirmed by resolving both
cross-account Terraform. Self-service message (no "contact the platform and diffing `service-service.inputs.desired_count`.
team"). Real AWS provisioning explicitly deferred (D-113/D-114). - **`ACDL_REMOTE_STATE_KEY` plumbing:** correct end-to-end — the L2 scripts
- **Splits (P12/P13):** `contract_resolver` + `regression_verify` split export it, the adapter reads it with a sensible default, and the
with re-export shims; G-113 one-way import direction documented. All microservice composition's `terraform_remote_state` data block picks it
tests pass without modification (backwards compat preserved). up. This cleanly separates the short-lived CI VPC state from the
- **DX (P15-P17):** `--help` works + documents all 9 flags; workflows long-lived platform VPC state.
README catalogs all 7 workflows; getting-started is offline-first. - **Workflow structure:** `l2-lifecycle` correctly `needs: ci-vpc-apply`;
- **Regression gate:** 18 Verified + 4 Skipped at P9 + P21 (0 Decayed/ `ci-vpc-destroy` correctly `needs: [lifecycle, l2-lifecycle]` and
Broken). The 4 Skipped are the post-v1.11-teardown live-AWS caps. `if: always()`. The 7 new L2 pipeline-contract tests assert all of this.
- **Byte-identical workflows:** `.gitea` and `.github` modules-lifecycle.yml
are byte-identical (test asserts this); the `test_workflow_has_four_jobs`
rename from three→four is correct.
- **Adapter line count:** 194 lines — under the 200-line ceiling, still a
clean stateless assembler. The dedup logic added ~16 lines without
bloating.
- **Teardown verification (P64):** trustworthy in structure — the
`ci-vpc-destroy` job runs unconditionally and the decommission
`---ci---` block is the audit trail. The adversarial concern (P1-5) is
about the regression gate trusting the workflow ran, not about the
teardown itself being fakeable.
- **Security:** no credential leakage in any reviewed file. All AWS auth
in workflows uses `${{ secrets.* }}`; COST.md references only env var
names and a redacted/deactivated root key ID.
## Test coverage assessment ## Test coverage assessment (485 offline tests)
~635 tests pass (was ~620 at v1.15.4). New test files: - **Adequate:** pipeline contract (89 tests), schema validation, contract
- `tests/test_onboarding.py` (3 tests — env-file generation) resolution, adapter emission (basic), confidence signal, outbox,
- `tests/test_onboarding_terraform.py` (3 tests — terraform validate + tags) interpolation, local emulators, module-standards file presence, design-doc
- `tests/test_docs_coverage.py` (expanded — workflows README catalog) currency.
- **Gaps (post-hoc):**
1. Adapter dedup merge behavior (P2-2) — no unit test.
2. `ACDL_REMOTE_STATE_KEY` override (P2-2) — no unit test.
3. CAP-017..022 regression checks (P1-5) — not exercised at the unit
level; the 2 slow tests in `test_verify_regression_mode.py` run the
full registry but are `@pytest.mark.slow` and deselected from the
fast suite, so a CI run of the 485 fast tests does not verify
CAP-017..022 even at the offline-proxy level.
4. WAF `upper()` scope — no test asserts the locals transform; relies
on the lifecycle pipeline cell to catch a regression.
5. ALB `name_prefix` — no test asserts the target group uses
`name_prefix` (P2-1 context).
New tests in existing files: `test_adapt_emits_nova_state_bucket`, The 485 count is honest (447 pass fast, 5 deselected slow, 485/490
`test_onboarding_message_says_nova_not_acdl`, `test_no_identity_fails_closed`, collected). The gap is behavioral coverage of the new adapter + module
`test_no_identity_passes_with_local_bypass`, `test_oversized_contract_rejected`, logic, not contract/schema coverage.
`test_schema_invalid_contract_rejected`, `TestNarrowedException` (2 tests),
`TestOnboardConsumer` (3 tests), `TestOnboardingMessageSelfService` (2 tests),
`test_sync_workflows_check_passes`.
## Verdict ## Verdict
**PASS — 0 P0, 0 P1, 2 P2 (post-hoc, accepted).** The v1.16 NFR milestone **PASS with P1 flags for post-hoc review.** No P0 fixes applied. The
is complete. All 20 requirements (REQ-165..184) satisfied; regression milestone's structural controls (regression gate, mandatory teardown,
gate 18V+4S; CI 3-stage PASS at every phase boundary. The onboarding byte-identical workflows, byte-identical contract↔workflow tests) are
request path is self-service; real AWS provisioning deferred. The sound. The most material finding is P1-5 (the regression gate's
state-bucket drift + Kyverno label contradiction (the two correctness CAP-017..022 evidence is an offline proxy, not live pipeline evidence) —
regressions from the v1.15 rebrand) are fixed with regression guards. this is a repeat of the v1.10 "VERIFY was diff-scoped" structural defect
in a milder form: the gate trusts the workflow was run rather than proving
it. The mitigations in PRE_MORTEM (FM-1..FM-4) acknowledge related risks;
P1-5 is the specific instance for the lifecycle-pipeline tier.
+2 -816
View File
@@ -1,4 +1,4 @@
# Nova — Roadmap # ACDL — Roadmap
## Overview ## Overview
@@ -28,8 +28,6 @@
- **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.1 (complete, tag `v1.13.1`):** config.json schema migration — regenerate `.ciagent/config.json` to the updated CIAgent v2 config structure (drop removed fields, migrate `gitea``release.gitea`, add `secrets`/`ship`/`backend`/`ideation`/`personas`/`logging`/`telemetry` sections). Code review: 0 P0, 2 P1/P2 auto-fixed. Docs-only NFR patch (no code changes). Gitea release id 253.
- **v1.13.2 (complete, tag `v1.13.2`):** presentation badge cleanup + platform architecture diagram — removed all `testing`/`agentic` maturity badges from both decks (only `planned` retained); added a new Slide 3 "The platform at a glance" with a shared high-level logical architecture diagram (consumer surfaces → contract → central pipeline → cross-cutting components → AWS) to both decks; renumbered subsequent slides 411; synced talking points + README. Docs-only NFR patch (no code changes). - **v1.13.2 (complete, tag `v1.13.2`):** presentation badge cleanup + platform architecture diagram — removed all `testing`/`agentic` maturity badges from both decks (only `planned` retained); added a new Slide 3 "The platform at a glance" with a shared high-level logical architecture diagram (consumer surfaces → contract → central pipeline → cross-cutting components → AWS) to both decks; renumbered subsequent slides 411; synced talking points + README. Docs-only NFR patch (no code changes).
- **v1.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html - **v1.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 (complete, tag `v1.23.4`):** 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. 287 tests pass. Feature milestone; tags on v1.23.x line.
--- ---
@@ -1065,7 +1063,7 @@ Docs-only NFR patch (no code changes).
--- ---
## v1.14 (complete — NFR Refinement: bug fixes, security, stubs, tests, docs, tag `v1.13.24`) ## v1.14 (active — NFR Refinement: bug fixes, security, stubs, tests, docs)
The v1.14 milestone is a 20-phase NFR sweep — no new features. It clears The v1.14 milestone is a 20-phase NFR sweep — no new features. It clears
the open P1/P2 backlog from the v1.11 review, hardens the security the open P1/P2 backlog from the v1.11 review, hardens the security
@@ -1434,815 +1432,3 @@ on the v1.13.x line: `v1.13.3` (P0) → `v1.13.4..v1.13.23` (P1P20) →
- Tag `v1.13.24` created; milestone merged to main. - Tag `v1.13.24` created; milestone merged to main.
After Phase P21: milestone COMPLETE — `v1.13.24` IS the v1.14 release. After Phase P21: milestone COMPLETE — `v1.13.24` IS the v1.14 release.
---
## 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
of fast deployments." The rebrand applies across docs, decks, code,
configs, CI, env var prefixes, the consumer contract path, SSM
parameter paths, AWS tag keys, and AWS resource names — with a staged
infrastructure migration to avoid breakage. The Nova tagline is added
alongside (not replacing) the existing "North Star" / "consumers
declare intent" framing; the S&P Global Energy visual theme
(`sp-theme.json`) is a client brand and is **not** touched.
**Milestone type:** Major (breaking — consumer-facing path, env var
prefixes, SSM path, AWS tag keys, and AWS resource names all change).
Per the branch-strategy precedent (breaking/feature milestones tag on
their OWN minor line), v1.15 tags run on the **v1.15.x minor line**:
`v1.15.0` (P0) → `v1.15.1..v1.15.4` (P1P4) → `v1.15.4` (P5 final =
milestone release). (G-104 binding.)
**Brand mapping:**
- Name: `ACDL` / `Agentic Cloud Delivery Platform``Nova`
- Tagline (added): "The New Dawn of DevSecOps — security as a seamless
enabler of fast deployments"
- Env var prefix: `ACDL_*``NOVA_*` (dual-read fallback in P2;
removed in P5)
- Consumer path: `.acdl/contract.yml``.nova/contract.yml`
- SSM path: `/acdl/{env}/{contractId}/{output}`
`/nova/{env}/{contractId}/{output}`
- AWS tag keys: `acdl:owner|environment|contract|cost-center|ref`
`nova:*`
- AWS resource names: `acdl-*``nova-*` (with migration, P4)
- Illustrative URLs in docs: `github.com/acdl/...`
`github.com/nova/...` (prose only; real repo name unchanged)
- Gitea release titles going forward: `ACDL vX.Y.Z``Nova vX.Y.Z`
- S&P visual theme: unchanged (client branding)
**Wave ordering:**
- Wave 1 (P1): docs/decks/prose — no runtime impact; establishes new
vocabulary. REQ-155, REQ-156, REQ-157.
- Wave 2 (P2): code + env vars + consumer path — rename in code with a
dual-read env fallback so deployments don't break during the
transition window. REQ-158, REQ-159, REQ-160.
- Wave 3 (P3): SSM path + tag keys — SSM: copy `/acdl/...`
`/nova/...`, update readers, delete old. Tag keys: parallel-tag
period (`nova:*` added, ABAC policy swapped, `acdl:*` removed).
REQ-161, REQ-162.
- Wave 4 (P4): AWS resource names — the big migration (KMS alias, SNS,
SG, Lambda, DynamoDB data migration, ECR re-push, IAM re-bootstrap,
state bucket migration, ALB recreate). Maintenance window + rollback
runbook. REQ-163.
- Wave 5 (P5): final-review-ship — remove dual-read fallback, consumer
migration guide finalized, review + audit + milestone ship. REQ-164.
### Phase P1 — docs-decks-prose (Wave 1)
- **Description:** Rebrand all prose, titles, headers, comments,
deck markdown sources, mermaid `.mmd` sources, `pyproject.toml`
name/description, and `release.yml` release-title prefix from
`ACDL`/`Agentic Cloud Delivery Platform``Nova`. Add the Nova
tagline ("The New Dawn of DevSecOps — security as a seamless enabler
of fast deployments") to the README header, both deck title slides,
and `docs/vision.md` — alongside the existing "North Star" framing.
Re-export the mermaid PNG diagrams so committed PNGs match new
labels. Re-render the deck HTML. Update illustrative URLs in docs
(`github.com/acdl/...``github.com/nova/...`,
`git.cloudinit.dev/continuous-intelligence/acdl*``.../nova*` for
prose). Ship a consumer migration guide (`docs/NOVA_MIGRATION.md`)
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 P2P4.
- **Status:** complete (v1.15.1)
- **Depends on:**
- **Requirements:** REQ-155, REQ-156, REQ-157
- **Success Criteria:**
- `grep -rni "ACDL\|Agentic Cloud Delivery" README.md docs/ .ciagent/*.md`
returns 0 hits (except historical narrative marked as historical).
- `pyproject.toml` `name` = `nova`; `description` mentions Nova.
- `release.yml` release title prefix is `Nova `.
- Both decks' title-slide subtitle is
`Nova — The New Dawn of DevSecOps`; mermaid `.mmd` sources use
`Nova`; PNGs re-exported; HTML re-rendered.
- `docs/vision.md` and README header carry the Nova tagline
alongside the North Star.
- `docs/NOVA_MIGRATION.md` exists and lists the 5 breaking changes.
- `pytest` passes; `run_ci.sh` exits 0.
### Phase P2 — code-envvars-consumer-path (Wave 2)
- **Description:** Rename
`adapters/terraform/policy/custom_rules/acdl_tagging.py`
`nova_tagging.py` (+ Checkov custom-rule registration in
`schemas/tagging-standard.json` + adapter config). Rename all 21
`ACDL_*` env var prefixes → `NOVA_*` across `scripts/`, `core/`,
`adapters/`, `tests/`, workflows (`.gitea/`, `.github/`), `.env`,
`.env.secrets` (key names only — values stay), and consumer docs.
Implement a **dual-read fallback** (`NOVA_X` preferred, fall back to
`ACDL_X`) in the env/config loader so deployments don't break during
the transition window. Rename the consumer on-disk contract path
`.acdl/contract.yml``.nova/contract.yml` (and
`.acdl/static-assets.*.yml`, `.acdl/contract.yaml`) across the
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:** complete (v1.15.2)
- **Depends on:** [P1]
- **Requirements:** REQ-158, REQ-159, REQ-160
- **Success Criteria:**
- `nova_tagging.py` exists; `acdl_tagging.py` removed; Checkov
registration updated; rule enforces `nova:*` tag keys (tag-key
enforcement of `nova:*` lands here; existing resources still carry
`acdl:*` until P3 parallel-tag — rule warns during P2).
- No `ACDL_` env var references remain in code/scripts/workflows/tests
except the dual-read fallback in the loader + `.env.secrets` legacy
comment.
- Dual-read fallback implemented and unit-tested.
- Contract resolver reads `.nova/contract.yml`; deploy workflow
checks out `.nova/`; docs updated.
- `pytest` passes; `run_ci.sh` exits 0.
### Phase P3 — ssm-tagkeys (Wave 3)
- **Description:** SSM path migration: rename the parameter path prefix
`/acdl/{env}/{contractId}/{output}`
`/nova/{env}/{contractId}/{output}` across `core/output_publisher`,
the contract resolver, and consumer docs. Add a migration script
(`scripts/migrate_ssm_paths.py`) that copies existing `/acdl/...`
parameters → `/nova/...`, then readers are updated, then old
parameters are deleted. Tag key migration: add `nova:*` tags to all
AWS resources (parallel-tag period), update the ABAC session policies
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:** complete (v1.15.3)
- **Depends on:** [P2]
- **Requirements:** REQ-161, REQ-162
- **Success Criteria:**
- SSM readers use `/nova/...`; migration script copies + deletes;
test asserts new path.
- `nova_tagging.py` enforces `nova:*` (hard fail on `acdl:*`).
- ABAC session policies match `nova:*`; terraform emits `nova:*` tags.
- `acdl:*` tags removed from all resources (verified via `aws` CLI or
documented deferred if no live AWS access).
- `pytest` passes; `run_ci.sh` exits 0.
### Phase P4 — aws-resource-migration (Wave 4)
- **Description:** Rename all `acdl-*` AWS resources → `nova-*` via
terraform with a staged migration: KMS alias `alias/acdl-platform`
`alias/nova-platform` (repoint), SNS `acdl-sod-halt`
`nova-sod-halt` (recreate), SG `acdl-ecs-sg``nova-ecs-sg`
(recreate), Lambda `acdl-contract-ingestor`
`nova-contract-ingestor` (recreate), DynamoDB `acdl-contracts`/
`acdl-change-requests``nova-contracts`/`nova-change-requests`
(scan+copy data migration, verify row counts, keep old tables until
verified), ECR `acdl-microservice``nova-microservice` (re-push
images), IAM user/policy `acdl-spike-runner``nova-spike-runner`
(re-bootstrap with new key), state bucket `acdl-tfstate-...`
`nova-tfstate-...` (`terraform init -migrate-state` to new backend,
state JSON backed up first), ALB name prefix `acdl-alb``nova-alb`
(recreate, brief downtime). Publish a maintenance window + rollback
runbook (`docs/NOVA_AWS_MIGRATION.md`). For the offline/local tier,
the terraform `name`/`resource` labels change so `terraform validate`
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:** complete (v1.15.4)
- **Depends on:** [P3]
- **Requirements:** REQ-163
- **Success Criteria:**
- All terraform resource names/labels use `nova-*`; `terraform
validate` passes for platform/microservice/ci-vpc.
- State bucket name → `nova-tfstate-...`; `terraform init
-migrate-state` documented + tested offline.
- DynamoDB data-migration script exists (scan+copy, row-count
verify).
- `docs/NOVA_AWS_MIGRATION.md` runbook exists (maintenance window,
rollback steps).
- `grep -rn "acdl-" terraform/` returns 0 hits.
- `pytest` passes; `run_ci.sh` exits 0.
### Phase P5 — final-review-ship (Final Phase)
- **Description:** Multi-persona code review across all v1.15 phases.
Audit (reconstruction test, file discipline, branch hygiene, commit
discipline). Remove the dual-read env var fallback (`ACDL_*``NOVA_*`)
once all consumers are migrated; finalize the consumer migration
guide; `nova_tagging.py` no longer accepts `acdl:*` tag keys. Complete:
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:** complete (v1.15.4, milestone release)
- **Depends on:** [P1-P4]
- **Requirements:** REQ-164
- **Success Criteria:**
- Review: 0 new P0; all P1+ flagged or auto-fixed.
- Audit: clean; reconstruction test passes.
- Dual-read fallback removed; `nova_tagging.py` hard-fails `acdl:*`.
- Tag `v1.15.4` created; milestone merged to main.
After Phase P5: milestone COMPLETE — `v1.15.4` IS the v1.15 release.
---
## v1.16 (complete — Nova Simplification, tag `v1.15.26`)
A 20-phase NFR sweep (no new features) themed around five user-directed
axes: **Simplify without regressions**, **Security**, **Maintainability**,
**User/Developer Experience**, **No Humans Onboarding Flow**. The v1.15
rebrand left a fresh debt layer (stale brand strings, a state-bucket
drift, a Kyverno policy contradicting the Nova tagging standard, dead
code) that this milestone cleared, alongside genuine simplification
(dedup helpers, a workflow generator, file splits) and the first
self-service onboarding request path (request-path only; real AWS
provisioning deferred, D-113).
**Milestone type:** NFR (all phases fix/chore/docs/refactor/test). The
final phase's patch IS the deliverable. Tags on the v1.15.x line:
`v1.15.5` (P0) → `v1.15.6..v1.15.25` (P1P20) → `v1.15.26` (P21 final =
milestone release).
**Regression gate (D-118, G-111):** 18 Verified + 4 Skipped (CAP-013..016
live-AWS caps are the post-v1.11-teardown steady state, D-096; re-
provisioning is a future feature). 0 Decayed/Broken at P9 + P21.
**Grill:** PASS-with-binding (G-111..G-113, E-002 deferred to P21).
G-111: gate criterion restated 18V+4S + Skipped logic. G-112: P9 source
model pinned. G-113: P12/P13 import direction documented.
**Wave outcomes:**
- Wave 1 (P1P4): state-bucket + Kyverno rebrand fix (correctness
regression), user-facing ACDL→Nova sweep, dead-code cleanup, except
narrowing.
- Wave 2 (P5P9): regression-verify dedup (~70 lines), run-platform
HITL fn + config, contract-resolver envloader + registry kind, workflow
generator (sync_workflows.py + workflows-src/), run-platform split
(decommission + uptime helpers). Gate PASS at P9.
- Wave 3 (P10P14): ingestor defense-in-depth (fail closed on missing
IAM), payload validation (size cap + schema), split contract-resolver
(decommission + CLI modules), split regression-verify (CLI module),
schema-driven outputs + schema cache. Mid-milestone checkpoint clean.
- Wave 4 (P15P17): run-platform --help + flags doc, workflows README
catalog (7 workflows), getting-started consolidation (offline-first).
- Wave 5 (P18P20): onboarding schema + onboard_consumer Lambda action,
env-file autogen (core/onboarding.py), cross-account role Terraform
(offline-proven, D-114).
**Outcome:** 20 requirements (REQ-165..184) satisfied; ~630 tests pass;
regression gate 18V+4S; the onboarding request path is self-service (no
"contact the platform team" handoff); real AWS provisioning explicitly
deferred (D-113/D-114).
Ship tag at milestone COMPLETE: `v1.15.26` (NFR milestone; final patch IS
the release). **DONE.**
## v1.18 (complete — Citizen Developer & Production-Grade Guidance, tag line `v1.17.x`)
Nova advances from a platform that governs infrastructure delivery to one
that **instructs the citizen developer on production-grade engineering**
and defines a **clear, machine-checkable contract for what is acceptable
to start**. Five user-directed inputs drive the milestone:
1. **S&P Global theme restoration** (P1) — the v1.17 P5 deck rebuild lost
the S&P Global Energy brand visual identity (introduced v1.9.2 / P45).
The Marp `style:` block (`#D6002A` red, `#1B1B1B` grey-90, Akkurat Pro,
8px accent bar) is restored to the unified deck.
2. **PDLC-upstream scope** (P2) — promotes Core Tenet #2 + Anti-Goal #1
from buried tenets to a dedicated, unmissable scope statement: the PDLC
is upstream of Nova; Nova governs infra + delivery only.
3. **RACI matrix** (P2) — three-role responsibility matrix (Citizen
Developer / Platform / Release Management co-owned) clarifies who owns
what, with the compliance-standard-equivalence note.
4. **Nova input contract** (P3) — `schemas/submission-readiness.schema.json`
+ `core/submission_readiness.py` validator define "what is acceptable to
start" as a superset gate above contract-schema validity.
5. **Atelier integration** (P4+P5) — skills (markdown, extending BA.A) + an
MCP server (plugin-registry, vendored Atelier, agentic validation
beyond Wiz/Checkmarx/Mend).
**Milestone type:** Feature (P1 theme restoration + P3 schema/validator +
P5 MCP server are new code). Tags run on the v1.17.x patch line:
`v1.17.0` (P0) → `v1.17.1..v1.17.6` (P1P6) → `v1.17.7` (P7 final =
milestone release).
**Deck automation (cross-cutting, REQ-228):** any phase modifying
`docs/presentations/*-marp.md` or `docs/presentations/assets/` re-renders
HTML + PPTX, commits the PPTX binary to git, and attaches it to the
phase's Gitea release.
**Phase count:** 8 (P0 pre-execution + 6 execution + 1 final).
**Phases:**
- **P1 — sp-theme-restoration** (feat): restore S&P Global Marp theme to
unified deck + HTML re-render + PPTX commit + release attach. REQ-214,228.
- **P2 — pdlc-scope-raci** (docs): PDLC-upstream scope + RACI matrix +
2 deck slides + HTML/PPTX re-render. REQ-215,216,228.
- **P3 — submission-readiness** (feat): JSON Schema + validator + docs +
tests. REQ-217,218,219,220.
- **P4 — atelier-skills** (docs): 9 Atelier-derived skill files + index +
BA.A extension. REQ-221,222.
- **P5 — atelier-mcp** (feat): plugin-registry MCP server + vendored
Atelier + 4 tools + tests. REQ-223,224,225.
- **P6 — deck-slides-atelier** (docs): 3 new deck slides (scope/RACI/atelier)
→ 21 slides + talking points + HTML/PPTX re-render + README. REQ-226,227,228.
- **P7 — final-review-ship** (final): review + audit + milestone ship.
**Requirements:** REQ-214..228 (15 requirements). See
`.ciagent/REQUIREMENTS.md` §v1.18.
**Open decisions to lock (CLARIFY/GRILL):** D-133 (validator location),
D-134 (deck slide budget), D-135 (MCP transport), D-136 (Atelier vendoring),
D-137 (MCP server language), D-138 (skill format), D-139 (RACI roles),
D-140 (MCP plugin-registry), D-141 (PPTX storage), D-142 (deck render trigger).
**Outcome:** 15 requirements (REQ-214..228) satisfied; 32 tests pass (16
submission-readiness + 16 MCP); S&P Global Energy theme restored; PDLC-
upstream scope + RACI matrix authored (PROJECT.md + docs/ + deck);
submission-readiness schema + validator shipped (superset gate above
contract.schema.json); 9 Atelier-derived skills + docs/skills.md; MCP
server (plugin-registry, stdio, vendored Atelier v0.3.6) with 4 tools +
agentic validation beyond Wiz/Checkmarx/Mend; 21-slide deck (3 new slides:
scope/RACI/atelier) with PPTX committed + release-attached. 10 decisions
locked (D-133..D-142).
Ship tag at milestone COMPLETE: `v1.17.7` (feature milestone; final patch
IS the release). **DONE.**
## v1.19 (complete — Nova 2nd-Release Sync, tag line `v1.18.x`)
> **NFR-only chore milestone.** Single execution phase. Establishes the
> manual-only "2nd release" pipeline `~/acdl → ~/nova` (GitLab
> `jonathanchery/nova`, separate repo + history, consumer/platform-team
> audience). Replaces the old `~/gl/acdl` mirror sync.
### Phase P1 — nova-sync-script (Wave 1)
- **Description:** Replace `scripts/sync_to_gl.sh` (kitchen-sink mirror sync
into `~/gl/acdl`) with `scripts/sync_to_nova.sh` — a manual-only,
consumer-subset, domain-committed 2nd-release pipeline into `~/nova`.
Excludes `.ciagent/`, `terraform/`, `demo/`, runtime metrics, and
internal-only scripts. Protects `~/nova/.git`. Commits per domain in a fixed
order using positional `-m` conventional-commit messages. Validates
conventional format. Never triggerable by CI (`--release` gate).
- **Status:** complete
- **Depends on:**
- **Requirements:** REQ-229
- **Success Criteria:**
- `scripts/sync_to_nova.sh` exists with `set -euo pipefail`.
- Refuses without `--release` (exit 2); `--list-domains` prints 13 domains.
- rsync excludes `.ciagent`, `terraform`, `demo`, internal scripts, runtime
metrics; protects destination `.git`.
- Domain commits in fixed order; positional `-m` mapping; conventional
format validated.
- `scripts/sync_to_gl.sh` removed.
- `pytest` passes; `run_ci.sh` exits 0.
### Phase P2 — final-review-ship (Final Phase)
- **Description:** Final review + audit + milestone ship. Merge to main, tag
`v1.18.0` (first patch on the v1.18.x line), create Gitea release.
- **Status:** complete
- **Depends on:** [P1]
- **Requirements:** REQ-229
- **Success Criteria:**
- Review + audit clean (no P0).
- `phase/02-final-review-ship` merged to `milestone/v1.19-nova-sync` then to
`main`.
- Tag `v1.18.0` created; release notes summarize REQ-229.
- Milestone branches deleted; CHECKPOINT cleared.
Ship tag at milestone COMPLETE: `v1.18.1` (NFR milestone; final patch IS the
release). **DONE.**
---
## v1.20 — Consumer Cleanup + Transparent Terraform + Slide Pipeline
> **Multi-concern milestone.** Four user-directed inputs: (1) remove all
> gitea/gitlab from synced files — the platform team must never know about
> the dev forge; (2) radically simplify documentation for the Platform Team
> audience; (3) make terraform runs transparent in workflows with feature-flag
> client differentiation; (4) dedicated S&P-themed slide render pipeline +
> 12-month product roadmap slides.
>
> Tags run on the v1.19.x line (milestone v1.20 → tags v1.19.x).
### Phase P0 — pre-execution
- **Description:** Specify → clarify → research → plan. Validate v1.20
requirements (REQ-230..244). Establish milestone version in config.json.
- **Status:** complete
- **Requirements:** REQ-230..244
- **Success Criteria:**
- `.ciagent/REQUIREMENTS.md` has v1.20 section with all 15 requirements.
- `.ciagent/config.json` has `active_milestone: "v1.20"`.
- Checkpoint written.
### Phase P1 — consumer-cleanup (gitea removal + doc simplification)
- **Description:** Remove all gitea/gitlab mentions from synced files.
Genericize forge-detection code. Drop `.gitea/` byte-identity test
assertions. Add `test_no_forge_mentions.py` guard test. Simplify
documentation: delete completed migration docs, move thesis to `.ciagent/`,
strip ciagent-internal provenance from synced docs.
- **Status:** complete
- **Requirements:** REQ-230, REQ-231, REQ-232
- **Success Criteria:**
- `tests/test_no_forge_mentions.py` passes — zero gitea/gitlab mentions in
synced subset.
- `pytest` passes — all existing tests green after genericization.
- Synced docs stripped of REQ-/D-/P- IDs, milestone headers, `.ciagent/`
citations.
- `docs/NOVA_MIGRATION.md` + `docs/NOVA_AWS_MIGRATION.md` deleted.
- `docs/NO_HUMANS_THESIS.md` moved to `.ciagent/`.
### Phase P2 — slide-pipeline (S&P theme + render automation)
- **Description:** Create dedicated S&P theme CSS, render_slides.sh pipeline,
CI workflow, tests. Update Marp frontmatter to use dedicated theme. Fix
README directory layout.
- **Status:** complete
- **Requirements:** REQ-239, REQ-240, REQ-241, REQ-242, REQ-243
- **Success Criteria:**
- `docs/presentations/assets/nova-sp-theme.css` exists with S&P colors.
- Marp deck frontmatter references the theme CSS.
- `scripts/render_slides.sh` renders mermaid PNGs + HTML + PPTX.
- `workflows-src/slides.yml` + `.github/workflows/slides.yml` exist.
- `tests/test_slides_pipeline.py` passes.
- `docs/presentations/README.md` updated (no retired decks).
### Phase P3 — product-roadmap (12-month slides)
- **Description:** Add 12-month product roadmap as Slide 20 + Slide 21 to the
deck. Add matching talking-points sections. Render via new pipeline.
- **Status:** complete
- **Requirements:** REQ-244
- **Success Criteria:**
- Slide 20 + 21 in `nova-no-humans-platform-marp.md` + source-of-truth +
talking-points.
- HTML + PPTX re-rendered via `render_slides.sh`.
- 4-quarter product arc grounded in NORTH_STAR + deferred metrics.
### Phase P4 — transparent-terraform (workflow refactor + feature flags)
- **Description:** Split run_platform.sh → run_codegen.sh + run_postapply.sh.
Rewrite deploy.yml with native terraform steps. Add var.enabled to all L1
modules + L2 composition toggles. Wire forge repo variables as feature
flags. Fix stale artifact path.
- **Status:** complete
- **Requirements:** REQ-233, REQ-234, REQ-235, REQ-236, REQ-237, REQ-238
- **Success Criteria:**
- `scripts/run_codegen.sh` + `scripts/run_postapply.sh` exist.
- `deploy.yml` has native terraform init/validate/plan/apply steps.
- Every L1 module has `variable "enabled"` + `count = var.enabled ? 1 : 0`.
- L2 `composition.json` supports per-child `enabled`.
- `deploy.yml` reads `vars.ENABLE_*` as `-var` flags.
- Stale `/tmp/acdl_platform_run_v18` path fixed to `NOVA_WORK_DIR`.
- `pytest` passes; `run_platform.sh` shim backward-compat verified.
### Phase P5 — final-review-ship (Final Phase)
- **Description:** Final review + audit + milestone ship. Merge to main,
tag `v1.19.4` (final patch = milestone release), create release.
- **Status:** complete
- **Depends on:** [P1, P2, P3, P4]
- **Requirements:** REQ-230..244
- **Success Criteria:**
- Review + audit clean (no P0).
- Milestone branches merged to main.
- Tag `v1.19.4` created; release notes summarize all 15 requirements.
- CHECKPOINT cleared; milestone branches deleted.
## v1.21 — Nova Deck Refinement & Pipeline Hardening (complete)
> Leadership-deck refinement based on 33 review notes on the v1.20 deck.
> Renamed the deck to the professional "Autonomous Cloud Delivery
> Platform" framing; restructured the narrative (Problem → Solution →
> Proof → Roadmap + Ask); removed internal provenance from
> audience-facing slides; hardened the policy pipeline (Checkov before
> plan, Wiz-or-Checkov on plan); moved the strategic integration
> objective into the North Star.
>
> Tags run on the v1.20.x line (milestone v1.21 → tags v1.20.0..v1.20.6).
> Flat workflow: commits on main, tags per phase.
### Phase P0 — pre-execution (complete, tag v1.20.0)
- SPECIFY → CLARIFY → RESEARCH → PLAN. Validated v1.21 requirements
(REQ-245..253). Established `active_milestone: "v1.21"`. Synced
PROJECT.md strategic-direction pillar.
### Phase P1 — strategic-docs (complete, tag v1.20.1)
- `git mv .ciagent/NO_HUMANS_THESIS.md .ciagent/AUTONOMY_THESIS.md` +
reframe content (autonomy in operations, not "removing humans").
- `NORTH_STAR.md`: vision polished ("invisible" → "visible"); obj #2
deterministic-scoring reword; obj #3 four CTO metrics; obj #4 replaced
with integration objective; drop anti-goals 1,4,5; add 2 new
anti-goals; anti-goal #3 reworded.
- `docs/raci.md`: 3 roles → 4 roles (add Quality Engineering; rename
Release Mgmt → SRE; split release attestation).
- `docs/scope.md` + render scripts + ONBOARDING: integration framing +
"no-humans" → "autonomous".
### Phase P2 — slides source-of-truth (complete, tag v1.20.2)
- `git mv` all 5 deck files `nova-no-humans-platform*`
`nova-autonomous-cloud-delivery*`.
- Rewrote source of truth to 18 main + 1 appendix slides, 4-beat arc.
All 33 review notes applied. Removed: old Slide 10 (Capability
Health), old Slide 12 (Zero-Touch), Appendix A2 (Operating Model &
Cost). Global: tech-leadership benefits; no D-###/REQ-###/.py paths in
audience slides; no badges; no version in footer.
### Phase P3 — marp deck + talking points + README (complete, tag v1.20.3)
- Synthesized Marp deck from updated source; frontmatter — title
"Nova — The Autonomous Cloud Delivery Platform", footer without
version + without "Act N/5", title-slide subtitle "Product Development
& Citizen Developer Overview"; no badges.
- Re-distilled talking points to 18-slide + A1 structure.
- README updated (deck title, audience, slide count, directory layout,
no badge docs).
- Theme CSS: fixed Appendix A1 table readability (explicit white body
on any background).
- Tests: added v1.21 assertions (no badges, no version, 18+1 slides, no
D-###/REQ-###/.py paths, old files removed, default deck renamed).
### Phase P4 — pipeline hardening (complete, tag v1.20.4)
- Two-stage policy scan (REQ-250): Checkov on static code BEFORE plan
(fail-fast); Wiz-or-Checkov on the plan AFTER plan (never both).
Implemented in run_platform.sh + run_codegen.sh + run_postapply.sh.
- `adapters/wiz/wiz_adapter.py`: added --plan mode CLI.
- `pipelines/contract.yml`: 'checkov' stage replaced by 'checkov-static'
(before terraform-plan) + 'runtime-policy-scan' (after). 9 → 10 stages.
- Tests updated; full suite 686 pass + 1 pre-existing attestation
failure (unrelated env issue).
### Phase P5 — render + verify (complete, tag v1.20.5)
- New mermaid diagrams: platform-pipeline.mmd/.png (slide 6),
telemetry-live-ops.mmd/.png (slide 9).
- Re-rendered HTML + PPTX (20 slides, 21 media files).
- Verify: 101 v1.21-specific tests pass; 686 full suite pass;
check-only pipeline exit 0; no no-humans/D-###/REQ-###/badge in
audience-facing deck files.
### Phase P6 — final-review-ship (Final Phase, complete, tag v1.20.6)
- Multi-file audit: git log matches `.ciagent/` discipline; deck files
renamed; forbidden content absent from audience-facing slides.
- Ship: tag `v1.20.6` (final patch = milestone release). Requirements
marked complete; ROADMAP marked complete; CHECKPOINT cleared.
- **Requirements:** REQ-245..253 (9 requirements, all complete).
## v1.22 — Nova Deck Layout Fix (complete)
> Fixes the systemic layout/formatting problems in the Nova presentation
> deck that made every slide look "out of whack" after the v1.21 P5
> re-render. Root cause (per investigation): `nova-sp-theme.css` had
> zero `section` padding (declared `/* @theme nova-sp */` as a comment,
> not the `@theme` directive; did not `@import` Marp's default theme).
> Combined with `overflow:hidden`, a blunt `img { max-height: 320px }`,
> header+footer chrome on every slide, and two P5 diagrams with extreme
> aspect ratios (13.52× and 0.63×), 8 of 19 slides overflowed.
>
> Tags run on the v1.21.x line (milestone v1.22 → tags v1.21.0..v1.21.6).
### Phase P0 — pre-execution (complete, tag v1.21.0)
- SPECIFY → CLARIFY → RESEARCH → PLAN → GRILL. Validated v1.22
requirements (REQ-254..262). 8 research findings persisted to
RESEARCH.md. 5 CLARIFY decisions auto-resolved (comprehensive scope,
full pipeline, re-layout to LR, delete render_deck.sh, split slides
3+8). Persona roster: 2 active (lead-developer + backend-engineer),
2 deactivated (frontend + data). Grill: PROCEED-WITH-REVISIONS
(3 revisions: aspect-ratio test scoped to deck PNGs, @import
rejection documented, marp version pinning fallback).
### Phase P1 — theme-css (complete, tag v1.21.1)
- REQ-254: `section { padding: 48px 56px 40px; overflow: auto; }`
root cause fix (zero padding was why every slide looked jammed
against the edges).
- REQ-255: `img { max-width: 100%; max-height: 380px; object-fit:
contain; }` + `.wide`/`.tall` classes — replaced blunt
`max-height: 320px` that broke `w:` directives on tall images.
- REQ-256: `section.title header/footer { display: none; }` — title
chrome suppression. `h2 + p { margin-top: 0.2em; }`, `p { margin:
0.4em 0; }` — spacing tightening. `ol` styling. `table.dense`
class. `@media print { section { overflow: hidden; } }` for PPTX.
### Phase P2 — render-scripts (complete, tag v1.21.2)
- REQ-257: deleted `scripts/render_deck.sh` (omitted `--theme`,
produced unthemed output). Pinned marp-cli@4.5.0 + mermaid-cli@
11.16.0 in `render_slides.sh`. Removed references from README,
sync_to_nova.sh, test_no_forge_mentions.py.
- REQ-258: added `-s 2 -b transparent` to mermaid-cli invocation
(README spec; produces crisp 2x PNGs with transparent backgrounds).
### Phase P3 — mermaid-relayout (complete, tag v1.21.3)
- REQ-259: `telemetry-live-ops.mmd` kept as `flowchart TB` (the 3-way
branch makes LR too wide at 4.22 aspect; TB gives 0.63 which is
legible at h:480 with img.tall class). Re-rendered at 2x transparent
(1024x1628).
- REQ-260: `platform-pipeline.mmd` restructured from 10-node LR chain
(aspect 13.52, illegible 1000x74 strip) to 4-node TB with combined
nodes. Re-rendered at 2x transparent (552x1116, aspect 0.49).
- Marp deck directives updated: `![w:1000]`/`![w:900]`
`![h:480 class:tall]` so images render at legible height using the
img.tall class budget (480px).
- Aspect-ratio bounds revised from [1.2, 2.5] to [0.4, 4.0] (accepts
both tall and wide diagrams; still catches original outliers).
### Phase P4 — deck-content (complete, tag v1.21.4)
- REQ-261: split slide 3 (Objectives + Anti-Goals) into Slide 3
(Objectives) + Slide 4 (Anti-Goals). Split slide 8 (Attestation
Matrix) into Slide 9 (QA, 3 rows) + Slide 10 (Prod/DR, 7 rows).
Main slide count 18 → 20.
- Trimmed: slide 7 (Pipeline) to 3 bullets. slide 11 (Telemetry) to
3 bullets. slide 14 (Deferred) merged 3 Live-AWS rows into 1 (8→6
rows). slide 17 (Quarter-by-Quarter) dropped Grounding column
(5→4 cols). Global table cell padding reduced (6px 10px → 4px 8px).
- Removed `header:` from frontmatter (keep `footer:` + `paginate`
only). The full 51-char deck title in BOTH header and footer was
redundant chrome eating ~35px on every slide.
- Source `.md` and talking-points re-synced to 20-slide structure.
- Updated `test_marp_deck_slide_count` (18→20 main + 1 appendix).
Updated README slide-count convention (all 6 references).
### Phase P5 — render-and-test (complete, tag v1.21.5)
- REQ-262: re-rendered HTML + PPTX via `render_slides.sh` (pinned
marp-cli@4.5.0, mermaid-cli@11.16.0, 2x transparent PNGs). 22
slides (title + 20 main + 1 appendix), 23 media files embedded.
Theme embedded in HTML (--sp-red + padding confirmed).
- Added 9 tests to `test_slides_pipeline.py` (the gap that let the
layout regression through): test_theme_css_has_section_padding,
test_theme_css_suppresses_title_chrome,
test_theme_css_has_aspect_ratio_aware_images,
test_png_aspect_ratios_sane (scoped to deck-referenced PNGs only
per GRILL revision 1, bounds [0.4, 4.0]),
test_render_slides_has_2x_scale, test_render_slides_pins_cli_versions,
test_render_deck_removed, test_html_embeds_theme,
test_html_slide_count_matches_marp.
- 32 slide tests pass (23 original + 9 new). 94 key-file tests pass.
`run_platform.sh --check-only` exit 0.
### Phase P6 — final-review-ship (Final Phase, complete, tag v1.21.6)
- Multi-persona code review: PASS with 3 P1 flags (all fixed in this
phase): source .md/talking-points re-synced to 20 slides, `![h:480
class:tall]` directives applied, README stale references updated.
- Audit: git log matches `.ciagent/` discipline; all commits have
`---ci---` blocks; branch hygiene verified.
- Ship: tag `v1.21.6` (final patch = milestone release). Merge
`milestone/v1.22-deck-layout-fix``main`. Requirements marked
complete; ROADMAP marked complete; CHECKPOINT cleared.
- **Requirements:** REQ-254..262 (9 requirements, all complete).
## 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
> `v1.22.6` = milestone release. Branch: `milestone/v1.23-deck-cleanup-python-pptx`.
>
> Driven by the user's feedback that the deck looked "out of whack" and
> the desire to return to the clean, well-formatted style of the old
> `the-developer-experience.html` (which used Marp's built-in `default`
> theme + an inline `style:` block). That investigation revealed:
> (1) the "clean" reference was itself MARP output — MARP is not the
> problem; (2) the current deck uses a standalone `nova-sp-theme.css`
> that re-derives all base spacing from scratch and had a zero-padding
> bug (fixed in v1.22 but the standalone approach is fragile);
> (3) there are two markdown documents (a plain source-of-truth `.md`
> and a manually-synthesized `-marp.md`) that should be consolidated;
> (4) images are referenced as file paths in the HTML, so the HTML
> breaks when redistributed without the `assets/` folder; (5) the deck
> is verbose in places and uses the term "penetrate" which the user
> wants removed.
>
> The milestone delivers: single-document consolidation, clean style
> restoration (Marp `default` + inline `style:`), self-contained HTML
> (base64 images), a parallel structured python-pptx PPTX generator,
> targeted word-count trim, and "penetrate" removal. `nova-sp-theme.css`
> is retained as a styling reference but retired from the render path.
### Phase P0 — pre-execution (active)
- SPECIFY → CLARIFY → RESEARCH → PLAN → GRILL. Establishes v1.23
requirements (REQ-263..275). Tag `v1.22.0`. Grill PROCEED-WITH-
REVISIONS (0.78): 4 binding revisions applied (G-001 repo-wide
"penetrate" purge; G-002 P3→P4 serialized; G-003 P3 split P3a+P3b;
G-004 P5+P6 merged).
### Phase P1 — consolidate-docs (planned, tag v1.22.1)
- REQ-263: fold speaker notes + talking points into `*-marp.md` as Marp
HTML comments; delete the plain `.md`. `-marp.md` becomes the sole
source of truth.
- REQ-264: keep `*-talking-points.md` as a standalone presenter aid,
synced from the deck's `<!-- Talking points: -->` comments.
### Phase P2 — restore-clean-style (planned, tag v1.22.2)
- REQ-265: revert frontmatter to `theme: default` + inline `style:`
block (S&P palette). Keep H2 + bold-lead structure, no header, no
badges.
- REQ-266: retain `nova-sp-theme.css` as a styling reference; drop
`--theme` from `render_slides.sh`.
- REQ-267: restyle benefit callouts — remove `**Benefit:**` prefix; use
`.benefit` class (red top-rule + black italic; white on title slides).
### Phase P3a — inline-images (planned, tag v1.22.3)
- REQ-268: new `scripts/inline_images.py` — base64-embeds all images in
the rendered HTML for redistribution. Invoked after the MARP HTML
render. Low-risk, mechanical (G-003 isolation).
### Phase P3b — python-pptx-generator (planned, tag v1.22.4)
- REQ-269: new `scripts/render_pptx.py` — structured, editable, S&P-themed
PPTX via `python-pptx`. 16:9; native tables; embedded PNGs; benefit
callouts. Add `python-pptx` to `pyproject.toml`. High-risk, isolated
(G-003).
- REQ-270: `render_slides.sh` produces both PPTX outputs; CI installs
`python-pptx`; both attached to release.
### Phase P4 — trim-wordcount + repo-wide "penetrate" purge (planned, tag v1.22.5)
- REQ-271: targeted ~20-30% word-count trim on verbose slides (1, 5, 7,
8, 13, 14, 20, appendix). Tables untouched. Spirit preserved.
- REQ-272: remove "penetrate" (and derivatives) repo-wide (G-001) —
`docs/` + `.ciagent/PROJECT.md`/`CLARIFY.md`; RESEARCH.md/PLAN.md/
GRILL.md exempt as decision-history. Slide 5's phrase removed with no
replacement (slide 4 already excludes the PDLC).
### Phase P5 — ci-tests-readme + review + audit + ship (Final Phase, tag v1.22.6)
- REQ-273: CI workflows install `python-pptx`, run `render_slides.sh`,
commit HTML + both PPTX + inlined images.
- REQ-274: update `test_slides_pipeline.py` (consolidated doc, inline
style assertions, image inlining, python-pptx, benefit class,
"penetrate" absence). New `test_pptx_generator.py`.
- REQ-275: rewrite `README.md` for the single-document + dual-PPTX +
image-inlining pipeline.
- Review + audit + milestone ship (merged P5+P6 per G-004 — NFR docs
milestone). Tag `v1.22.6` (final patch = milestone release). Merge
`milestone/v1.23-deck-cleanup-python-pptx``main`.
- **Requirements:** REQ-263..275 (13 requirements).
## v1.25 (active, tag line `v1.24.x`): kyverno-json Unified Policy Engine
`kyverno-json` — a Kyverno-ecosystem runtime that applies Kyverno policies
to **any** JSON/YAML payload — becomes Nova's **primary compliance /
policy tool**, implemented behind a swappable `PolicyEngine` adapter so
OPA (or any other engine) can replace it one day. The unified-orchestrator
model: Checkov and Wiz remain as raw-finding adapters feeding *into*
kyverno-json meta-policies; the confidence signal is untouched (it already
consumes `list[PolicyCheckResult]` engine-agnostically). Policies cover
all four Nova artifacts: consumer contract JSON, resolved Stack IR,
Terraform plan JSON, and the merged PCR list itself (meta-validation).
The K8s-only Kyverno adapter stays documentation-only (D-053); the
kyverno-json engine and the K8s adapter are siblings, not replacements.
Quality improvement from the IDEATE pass: capability regression checks
(`core/regression_verify.py` CAP-013/023/024) become declarative
kyverno-json policies. New `policy-engineer` persona owns the policy
territory. 19 requirements (REQ-291..309), 6 phases (P0 + P1..P4 + P5
final). Tags: `v1.24.0` (P0) → `v1.24.5` (P5 = milestone release).
### Phase P1 — engine-core (planned, tag v1.24.1)
- REQ-291: `core/policy_engine.py``PolicyEngine` Protocol +
`PolicyEngineRegistry` (selects engine from `config.json.policy.engine`).
- REQ-292: `config.json` gains `policy` object
(`engine: "kyverno-json"`, `policy_root`).
- REQ-293: `adapters/kyverno-json/kyverno_json_engine.py`
`KyvernoJsonEngine` (shells to `kj scan`; translates native output →
PCR; `is_configured()` guards on `which kj`).
- REQ-294: `adapters/kyverno-json/__init__.py` + `_smoke.json` policy +
`scripts/install-kyverno-json.sh` + CI image install.
- REQ-308: `tests/test_policy_engine.py` — protocol conformance,
registry, NullEngine fallback.
- REQ-309: `tests/test_kyverno_json_engine.py` — PCR schema validity,
defensive parsing, `pytest.skip` when kj absent.
### Phase P2 — contract + stack-IR policies (planned, tag v1.24.2)
- REQ-295: `adapters/kyverno-json/policies/contract/` — 4 policies over
consumer contract JSON (id-pattern, env-enum, infra-min-1,
forbid-unknown-fields).
- REQ-296: `core/contract_resolver.py` invokes the engine pre-resolve
(contract policies) — early-fail, confidence signal decides the gate.
- REQ-297: `adapters/kyverno-json/policies/stack-ir/` — 3 policies over
resolved Stack IR (tagging-standard, public-ingress, encryption-by-
default — ports of v1.0/v1.8 imperative rules).
- REQ-298: `core/contract_resolver.py` invokes the engine post-resolve
(stack-IR policies); additive — existing tests pass.
- REQ-299: `tests/test_stack_ir_policies.py` + fixtures (passing + failing
IR; skip when kj absent).
### Phase P3 — plan-JSON policies + meta-orchestration + pipeline wiring (planned, tag v1.24.3)
- REQ-300: `adapters/kyverno-json/policies/plan-json/` — 3 policies over
`terraform show -json` (plaintext-secrets, iam-wildcard, kms-reference
— ports of `checkov_adapter.py:RULE_MAP`).
- REQ-301: `run_platform.sh` Step 5 gains a parallel kyverno-json pass;
both PCR lists (checkov/wiz + kj) concatenate into the confidence
signal's `policy` input; skips gracefully when `which kj` is false.
- REQ-302: `tests/test_plan_json_policies.py` + fixtures;
`tests/test_run_platform_plan_json_policies.py` (script-substring
assertion).
- REQ-303: `adapters/kyverno-json/policies/meta/`
`block-on-any-critical.json` (declarative critical-block; the
`confidence_signal.py` hard-override stays as defense-in-depth) +
`tagging-rules-agree.json` (asserts Checkov + kj agree on tagging).
`tests/test_meta_policies.py`.
### Phase P4 — regression-gate policies + docs (planned, tag v1.24.4)
- REQ-304: `adapters/kyverno-json/policies/regression/` — 3 policies over
capability-inventory JSON (CAP-013/023/024) — declarative mirrors of
`core/regression_verify.py` checks.
- REQ-305: `tests/test_regression_policies.py` + fixtures (clean +
drifted inventory); regression gate still 287/287 baseline.
- REQ-306: `adapters/README.md` (new adapter row + PolicyEngine Protocol
section) + `adapters/kyverno-json/README.md`.
- REQ-307: `.ciagent/ARCHITECTURE.md` §12.7 (Policy Engine Registry) +
`schemas/README.md` + `modules/STANDARDS.md` (policy-authoring
standard) + `docs/METRICS.md` (swappable engine narrative).
### Phase P5 — final review + audit + milestone ship (Final Phase, tag v1.24.5)
- Multi-persona code review across P1..P4 (lead-developer, backend-
engineer, data-engineer, policy-engineer). Auto-fix P0; flag P1+.
- Audit: reconstruction test (git log ↔ `.ciagent/`), branch hygiene,
commit discipline.
- Milestone ship: merge `phase/05-final-review-ship`
`milestone/v1.25-kyverno-json``main`; tag `v1.24.5` (= the v1.25
release per prev-minor tagging rule); create Gitea release with full
milestone summary; delete all milestone branches.
- Update `REQUIREMENTS.md` (mark REQ-291..309 complete), `ROADMAP.md`
(mark v1.25 complete), `NORTH_STAR.md` (note Strategic Objective #2
provable trust via a replaceable policy-engine substrate).
- **Requirements:** REQ-291..309 (19 requirements).
+123 -75
View File
@@ -1,87 +1,135 @@
# VERIFY — P1 engine-core (v1.25) # ACDL v1.10 — Verify (milestone gate)
> 4-layer verify gate: structural, behavioral, security, quality. > Verify date: 2026-07-27. Verifier: ci-verifier. Milestone: v1.10 (complete, tag `v1.10.0`).
> Phase: P1. Requirements: REQ-291..294, 308, 309. Result: PASS. > Scope: 4 phases (5255), 5 commits (772ac72..2697775), 22 files, +2281/-256 lines.
## Structural ## Layer 1: Structural — PASS
- `core/policy_engine.py` exists, implements `PolicyEngine` Protocol - All 8 plan-referenced files exist on disk (`core/regression_verify.py`,
(PEP 544, `@runtime_checkable`), `PolicyEngineRegistry` with `core/local_emulators.py`, `scripts/run_regression.sh`,
`register()` + `get_engine()`, `NullEngine` fallback. `tests/test_verify_regression_mode.py`,
- `adapters/kyverno-json/kyverno_json_engine.py` exists, exports `tests/test_local_emulating_adapters.py`,
`KyvernoJsonEngine` with `name`, `is_configured()`, `evaluate()`. `.ciagent/CAPABILITY_INVENTORY.md`, `REGRESSION_REPORT.md`,
- `adapters/kyverno-json/__init__.py` loads the engine by file path `REGRESSION_REPORT.json`).
(the dir name has a hyphen — not a valid Python package name). - All imports resolve (`py_compile` + runtime import OK).
- `adapters/kyverno-json/policies/_smoke.json` exists (trivial policy - No TODO/FIXME/HACK/stub placeholders in new code (the `LocalLambdaStub`
for round-trip validation). is a legitimate local emulator, not a placeholder).
- `scripts/install-kyverno-json.sh` exists (go install kj@latest). - All declared exports exist (`run_regression`, `write_report`,
- `.ciagent/config.json` has the `policy` object `CAPABILITY_REGISTRY`, `RegressionReport`, `CapabilityResult`,
(`engine: kyverno-json`, `policy_root`). `FlatFileOutbox`, `LocalEcsEmulator`, `LocalS3StateBackend`,
- `.gitea/workflows/ci.yml` + `.github/workflows/ci.yml` have the `LocalLambdaStub`, `run_local_e2e`, `is_local_tier`).
Go + kj install step (best-effort, tests skip when kj absent).
- `tests/test_policy_engine.py` (10 tests) +
`tests/test_kyverno_json_engine.py` (16 tests) exist.
## Behavioral ## Layer 2: Behavioral — PASS
- `pytest tests/test_policy_engine.py tests/test_kyverno_json_engine.py`: - `pytest tests/ -m "not slow"`: **513 passed**, 5 deselected.
**24 passed, 2 skipped** (kj not installed — expected; - `pytest tests/ -m slow`: **5 passed** (2 local E2E + 3 regression
`pytest.skip("kj not installed")`). integration incl. live-AWS terraform plan).
- `NullEngine` satisfies the `PolicyEngine` Protocol (G-Q8a — - **Total: 518 passed, 0 failed.**
`isinstance(NullEngine(), PolicyEngine)` is True). Proves the swap - Requirement coverage: REQ-112 (P52), REQ-113 (P53), REQ-114 (P54),
boundary is real without implementing OPA. REQ-115 (P55) — all 4 marked `complete`.
- `KyvernoJsonEngine.is_configured()` returns `False` when - Regression gate: `bash scripts/run_regression.sh` → **16/16
`which kj` is absent → `evaluate()` returns a single capabilities Verified** (12 local + 4 live-AWS). Milestone gate open.
`KJ_ENGINE_NOT_CONFIGURED` SKIPPED PCR (distinct `ruleId` from
NullEngine's `NULL_ENGINE_INACTIVE` — G-Q4).
- PCR records validate against `schemas/policy_check_result.schema.json`
(via `jsonschema.validate` in tests).
- Defensive parsing: malformed kyverno-json output → `error` PCR
(`KJ_ENGINE_ERROR`), never an exception.
- Severity annotation reading (G-Q10a): policies with
`nova.cloudinit.dev/severity: high` produce PCRs with `severity: high`;
policies without the annotation default to `info`.
- Registry: `get_engine()` returns the configured engine; unknown
engine name raises `KeyError`; `policy` key absent → `NullEngine`.
- No regression: `pytest tests/test_confidence_signal.py
tests/test_adapter.py tests/test_checkov_adapter.py
tests/test_kyverno_adapter.py tests/test_contract_resolver.py` —
**132 passed** (unchanged).
## Security ## Layer 3: Security (STRIDE) — PASS
- No new secrets, no new network calls in the engine core (the engine | Threat | Risk | Disposition |
shells to a local binary; the binary makes no network calls for |--------|------|-------------|
`scan`). | Spoofing | Local Lambda stub patches `_get_dynamodb`/`_get_secrets_client`; opt-in via `ACDL_LOCAL_TIER=1`, never in prod | Accept (low) |
- `is_configured()` guard ensures the platform runs without the binary | Tampering | Flat-file outbox hash-chain verification detects tampering | Accept (low) |
(no hard dependency that could be exploited as a DoS vector). | Repudiation | Regression report records per-capability status + timestamps | Accept (low) |
- The engine writes the payload to a temp file (`tempfile.NamedTemporaryFile`) | Info Disclosure | Creds read into env vars, never logged (0 cred strings in reports); ECS binds 127.0.0.1 only | Accept (low) |
and unlinks it in a `finally` block (no leftover payload on disk). | Denial of Service | Local ECS emulator: free port, daemon thread, clean destroy | Accept (low) |
- No `shell=True` in the `subprocess.run` call (command is a list — | Elevation of Privilege | `urllib.urlopen` patched to fake response (no network egress); no eval/exec/subprocess in adapter | Accept (low) |
no shell injection surface).
## Quality All threats low-severity; auto-accepted per
`config.json security.auto_accept_low_severity=true`.
- `python3 -m py_compile` passes on all new Python files. ## Layer 4: Quality (multi-persona) — PASS
- The `PolicyEngine` Protocol is minimal (3 members) — the swap
boundary is the moat (NORTH_STAR Strategic Objective #2).
- The `NullEngine` proves a second implementation exists (structural
conformance) — the OPA swap is a known quantity (RESEARCH §4.2).
- Tests use `pytest.skip` when `which kj` is absent, so the CI matrix
passes with or without the binary (the suite is green in both cases).
## Must-have checklist | Persona | Finding | Verdict |
|---------|---------|---------|
| Correctness | 7 adapter defects fixed; each traceable to a terraform validate/plan error | PASS |
| Testing | 518 tests pass; 24 new tests. P2: uptime-kuma + RDS not in registry | PASS (1 P2) |
| Security | No creds logged; loopback-only; monkey-patches scoped to local tier | PASS |
| Performance | Regression run ~60s; acceptable for a milestone gate | PASS |
| Maintainability | Well-structured; adding a capability = 1 function + 1 registry entry | PASS |
| Adversarial | Gate can't be bypassed; local E2E can't mutate cloud; no injection vectors | PASS |
- [x] `PolicyEngine` Protocol + `PolicyEngineRegistry` + `NullEngine` **0 P0, 0 P1, 1 P2 (post-hoc: expand regression registry to uptime-kuma + RDS stacks).**
(REQ-291)
- [x] `config.json.policy` object (REQ-292)
- [x] `KyvernoJsonEngine` adapter (REQ-293)
- [x] `__init__.py` + `_smoke.json` + `install-kyverno-json.sh` + CI
install (REQ-294)
- [x] `test_policy_engine.py` — protocol conformance, registry,
NullEngine fallback (REQ-308)
- [x] `test_kyverno_json_engine.py` — PCR schema validity, defensive
parsing, skip-without-kj (REQ-309)
**Verdict: PASS** — all P1 must-haves met, no regressions, 24 new ## Verdict
tests pass (2 skip-without-kj), 132 existing tests unchanged.
**VERIFY PASS** — all 4 layers pass. The v1.10 milestone is sound:
the pipeline regression gap is fixed (D-091), the platform is fully
locally testable (D-092), every advertised capability is re-verified
(D-093, 16/16 Verified), and the docs/decks match verified reality
(D-094). 518 tests pass; the regression gate covers 16 capabilities
including 4 live-AWS checks. 0 P0, 0 P1, 1 P2 post-hoc. Ready to ship.
---
# ACDL — Verify (grill deliverable, commit ac11c01)
> Verify date: 2026-07-27. Verifier: ci-verifier. Scope: the grill
> deliverable (`.ciagent/GRILL.md`, phase 0, status `grill`) added in
> commit `ac11c01` since the v1.10 audit PASS (`ab477b3`). Docs-only;
> no code, no tests, no schema changes.
## Layer 1: Structural — PASS
- `.ciagent/GRILL.md` exists on disk (18250 bytes).
- No imports to resolve (markdown docs file).
- No TODO/FIXME/HACK/stub placeholders in the report.
- All required sections present per grill workflow Step 5 format:
title, Run header, Verdict, 9 axes (19), Meta, Binding Decisions
table (12 rows), Escalations section (2 entries: G-005, G-008).
- Commit `ac11c01` `---ci---` block is well-formed: `project: acdl`,
`phase: 0`, `milestone: v1.10`, `status: grill`, 12 decision ids
(G-001..G-012), 2 escalation lines.
## Layer 2: Behavioral — PASS
- `pytest tests/ -m "not slow"`: **513 passed**, 5 deselected (no
regressions introduced by the docs-only grill commit).
- No new tests required (docs-only deliverable; the grill is a
review artifact, not a code change).
- Requirement coverage: not applicable (phase 0, status `grill`; no
REQ-IDs bound to this deliverable). The grill's binding decisions
(G-001..G-012) are advisory and do not modify REQUIREMENTS.md per
grill workflow Step 7.
## Layer 3: Security (STRIDE) — PASS
| Threat | Risk | Disposition |
|--------|------|-------------|
| Spoofing | N/A (docs-only; no auth surface) | Accept (none) |
| Tampering | Grill report is git-tracked; tampering = git history rewrite (out of scope) | Accept (low) |
| Repudiation | Commit `ac11c01` signed by author; `---ci---` block records status + decisions | Accept (low) |
| Info Disclosure | No credentials, keys, tokens, or PII in the report (grep scan clean) | Accept (low) |
| Denial of Service | N/A (docs file; no runtime surface) | Accept (none) |
| Elevation of Privilege | N/A (docs-only; no privilege surface) | Accept (none) |
All threats low-or-none; auto-accepted per
`config.json security.auto_accept_low_severity=true`.
## Layer 4: Quality (multi-persona) — PASS
| Persona | Finding | Verdict |
|---------|---------|---------|
| Correctness | 12 binding decisions traceable to evidence (commit/file/req-id); 2 escalations correctly unresolved | PASS |
| Testing | Docs-only; 513 fast tests pass (no regression) | PASS |
| Security | No credential leakage; no sensitive data in report | PASS |
| Performance | N/A (docs file; no runtime cost) | PASS |
| Maintainability | Report follows grill workflow Step 5 format exactly; appendable for future runs | PASS |
| Adversarial | Escalations (G-005, G-008) are surfaced, not silently skipped; visible via `ciagent audit` | PASS |
**0 P0, 0 P1, 0 P2.**
## Verdict (grill deliverable)
**VERIFY PASS** — all 4 layers pass. The grill deliverable is a
well-formed docs-only artifact. 513 fast tests pass (no regression).
No credential leakage. 12 binding decisions recorded; 2 escalations
(G-005 risks, G-008 budget) correctly surfaced for human resolution.
The grill does not modify PROJECT.md, ROADMAP.md, or REQUIREMENTS.md
(per grill workflow Step 7).
+2 -7
View File
@@ -2,13 +2,13 @@
"projects": [ "projects": [
{ {
"slug": "acdl", "slug": "acdl",
"name": "Nova — The New Dawn of DevSecOps", "name": "Agentic Cloud Delivery Platform",
"default": true "default": true
} }
], ],
"active_project": "acdl", "active_project": "acdl",
"active_projects": ["acdl"], "active_projects": ["acdl"],
"active_milestone": "v1.25", "active_milestone": "v1.14",
"autonomy": { "autonomy": {
"level": "full", "level": "full",
"escalation_hooks": ["deploy", "delete_data", "merge_to_main"], "escalation_hooks": ["deploy", "delete_data", "merge_to_main"],
@@ -208,10 +208,5 @@
"telemetry": { "telemetry": {
"enabled": true, "enabled": true,
"persist": true "persist": true
},
"strategic_direction_file": ".ciagent/NORTH_STAR.md",
"policy": {
"engine": "kyverno-json",
"policy_root": "adapters/kyverno-json/policies"
} }
} }
-24
View File
@@ -1,24 +0,0 @@
=== tools ===
terraform: /usr/bin/terraform
checkov: /usr/local/bin/checkov
python3: /usr/bin/python3
jq: /usr/bin/jq
rsync: /usr/bin/rsync
marp: MISSING
mmdc: MISSING
Terraform v1.9.8
3.3.8
Python 3.12.3
=== chrome/chromium (for slide render) ===
found: /root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome
=== creds ===
.env.secrets: present (4 lines)
.env: present
=== aws creds loadable? ===
NOVA_AWS_ACCESS_KEY_ID: set
AWS_DEFAULT_REGION: us-east-1
=== git ===
main
v1.18.1-11-gaa868c9
=== disk ===
/dev/loop2 148G 140G 1.3G 100% /
-10
View File
@@ -1,10 +0,0 @@
{"id": "T1", "req": "REQ-230", "title": "no forge names in synced files (guard test)", "pass": true, "rc": 0, "evidence": {"test": "test_no_forge_mentions_in_synced_files", "result": "1 passed in 2.20s", "log_tail": ["tests/test_no_forge_mentions.py::test_no_forge_mentions_in_synced_files PASSED [100%]", "1 passed in 2.20s"]}}
{"id": "T2", "req": "REQ-230", "title": "forge-detection code genericized", "pass": true, "rc": 0, "evidence": {"hardcoded_gitea_gitlab_hits": 0, "genericization_signals": ["contract_ingestor.py: _forge_type() returns 'generic_forge'", "hitl_gates.py: GITHUB_ACTOR or FORGE_ACTOR (no GITEA_ACTOR)", "run_platform.sh:166: GITHUB_ACTOR:-FORGE_ACTOR fallback"]}}
{"id": "T3", "req": "REQ-231", "title": "synced docs stripped of internal provenance", "pass": false, "rc": 1, "evidence": {"provenance_hit_count": 40, "contaminated_files": ["docs/ONBOARDING.md (REQ-182,183,184; D-113,114,119)", "docs/METRICS.md (REQ-191,192,193,194,211,212; D-083,096,113,114,119)", "docs/presentations/README.md (REQ-214,226,228; D-130,141; .ciagent/PROJECT.md)", "docs/presentations/nova-no-humans-platform.{md,marp.md,html,talking-points.md} (v1.X milestone headers)", "docs/presentations/assets/mmd/developer-experience-08-semver.mmd (v1.12 header)"], "root_cause": "test_no_forge_mentions.py only guards forge names, not provenance IDs", "defect": "F7"}}
{"id": "T4", "req": "REQ-232", "title": "migration docs removed + thesis moved", "pass": true, "rc": 0, "evidence": {"docs_NOVA_MIGRATION_gone": true, "docs_NOVA_AWS_MIGRATION_gone": true, "docs_NO_HUMANS_THESIS_gone": true, "ciagent_NO_HUMANS_THESIS_present": true}}
{"id": "T5", "req": "REQ-239", "title": "S&P theme CSS palette on all chrome", "pass": true, "rc": 0, "evidence": {"css_exists": true, "css_size_bytes": 2914, "red_present": true, "black_present": true, "white_present": true, "chrome_covered": ["section/bg", "section.title", "h1-h3 headings", "table th", "blockquote", "pre/code", "header", "footer", "pagination (.bespoke-progress-bar)", "strong"]}}
{"id": "T6", "req": "REQ-240", "title": "render pipeline script + mermaid theme", "pass": true, "rc": 0, "evidence": {"render_slides_executable": true, "render_slides_size": 2736, "sp_theme_json_has_red": true, "sp_theme_json_has_black": true, "render_deck_sh_still_present": true, "render_deck_excluded_from_sync": true, "caveat": "README:107 still references render_deck.sh (deferred to T9)"}}
{"id": "T7", "req": "REQ-241", "title": "slides CI workflow path trigger", "pass": false, "rc": 1, "evidence": {"wrong_path_hits": [".github/workflows/slides.yml:8: - 'assets/nova-sp-theme.css' (non-existent)", "workflows-src/slides.yml:8: - 'assets/nova-sp-theme.css' (non-existent)"], "correct_path": "docs/presentations/assets/nova-sp-theme.css", "src_dotgithub_identical": true, "defect": "F6", "impact": "Explicit CSS path trigger points at nothing; only the docs/presentations/** glob catches CSS edits. Dead entry should be corrected or removed."}}
{"id": "T8", "req": "REQ-242", "title": "slide-pipeline guard test", "pass": true, "rc": 0, "evidence": {"passed": 12, "failed": 0, "duration_s": 1.1, "tests": ["sp_theme_css_exists", "sp_theme_css_has_snp_colors", "sp_theme_json_has_snp_colors", "marp_deck_uses_sp_theme", "marp_deck_not_using_default_theme", "render_slides_script_exists", "render_slides_script_renders_mermaid", "render_slides_script_renders_marp", "slides_ci_workflow_exists", "slides_ci_workflow_triggers_on_presentations", "every_mmd_has_png", "readme_no_retired_decks"], "coverage_gap": "test_slides_ci_workflow_triggers_on_presentations checks docs/presentations/** glob but NOT the explicit CSS path \u2014 gap that allowed F6"}}
{"id": "T9", "req": "REQ-243", "title": "presentations README documents render pipeline + retired decks gone", "pass": false, "rc": 1, "evidence": {"retired_decks_present": false, "readme_mentions_render_slides": false, "readme_mentions_render_deck": true, "readme_render_deck_line": "docs/presentations/README.md:107: 'automated by scripts/render_deck.sh'", "readme_mentions_theme_css": true, "defect": "F10", "impact": "README documents the retired render_deck.sh pipeline, not the active render_slides.sh. Consumers reading synced README reference a script excluded from sync."}}
{"id": "T10", "req": "REQ-244", "title": "12-month product roadmap slides 20+21 + talking points", "pass": true, "rc": 0, "evidence": {"marp_slide15": true, "marp_slide20": true, "marp_slide21": true, "talking_points_slide15": true, "talking_points_slide20": true, "talking_points_slide21": true, "quarters": ["Q1 Pilot Activation", "Q2 Provable Trust", "Q3 Compounding ROI", "Q4 Agentic Substrate"], "distinct_from_slide15": true}}
+1 -18
View File
@@ -1,4 +1,4 @@
# Nova CI Pipeline (dev environment) # ACDL CI Pipeline — Gitea Actions (dev environment)
# #
# This workflow implements the central pipeline contract: # This workflow implements the central pipeline contract:
# pipelines/ci.yml (validated against schemas/pipeline.schema.json) # pipelines/ci.yml (validated against schemas/pipeline.schema.json)
@@ -63,23 +63,6 @@ jobs:
- name: Install test dependencies - name: Install test dependencies
run: pip install -r requirements-test.txt run: pip install -r requirements-test.txt
- name: Install kyverno-json (kj) for policy-engine tests
run: |
# v1.25: kyverno-json is the primary policy engine. Tests that
# require kj skip when absent, so this is best-effort (the suite
# passes with or without kj). Install is cached via the Go
# module cache (~/.cache/go-build + ~/go/pkg/mod).
if command -v go >/dev/null 2>&1; then
go install github.com/kyverno/kyverno-json/cmd/kj@latest && \
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" || \
echo "kj install failed; policy-engine tests will skip"
else
sudo apt-get update && sudo apt-get install -y golang-go && \
go install github.com/kyverno/kyverno-json/cmd/kj@latest && \
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" || \
echo "kj install failed; policy-engine tests will skip"
fi
- name: Run pytest - name: Run pytest
run: python3 -m pytest tests/ -v --tb=short run: python3 -m pytest tests/ -v --tb=short
+14 -17
View File
@@ -1,4 +1,4 @@
# Nova Reusable Deploy Workflow (dev environment) # ACDL Reusable Deploy Workflow — Gitea Actions (dev environment)
# #
# This reusable workflow implements the central deployment pipeline contract: # This reusable workflow implements the central deployment pipeline contract:
# pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json) # pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json)
@@ -8,7 +8,7 @@
# declared difference is the forge/runtime, not the stages or commands. # declared difference is the forge/runtime, not the stages or commands.
# #
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR): # Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
# uses: nova/.github/workflows/deploy.yml@v1.19 # uses: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea)
# uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub) # uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
# #
# Unversioned references (@main, bare) are discouraged — the consumer's setup # Unversioned references (@main, bare) are discouraged — the consumer's setup
@@ -26,7 +26,7 @@
# platform log) for auditability. # platform log) for auditability.
# #
# Inputs: # Inputs:
# contract — path to the consumer's contract YAML (default .nova/contract.yml) # contract — path to the consumer's contract YAML (default .acdl/contract.yml)
# mode — full | plan-only | check-only (default full; dev = full apply, # mode — full | plan-only | check-only (default full; dev = full apply,
# higher environments hold for HITL — the calling repo or the # higher environments hold for HITL — the calling repo or the
# forge environment gate enforces that) # forge environment gate enforces that)
@@ -38,12 +38,12 @@
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session # that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>. # policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
# #
# Override (where OIDC is unavailable, e.g. pending # Override (where OIDC is unavailable, e.g. Gitea pending
# upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY # go-gitea/gitea#36988): set ACDL_AWS_ACCESS_KEY_ID + ACDL_AWS_SECRET_ACCESS_KEY
# as repository secrets. The platform-managed scheduled pipeline rotates # as repository secrets. The platform-managed scheduled pipeline rotates
# the key on a daily cadence. When .env.secrets is used locally instead, # the key on a daily cadence. When .env.secrets is used locally instead,
# rotating the key out of band is the consumer's responsibility. # rotating the key out of band is the consumer's responsibility.
name: nova-deploy name: acdl-deploy
on: on:
workflow_call: workflow_call:
@@ -51,7 +51,7 @@ on:
contract: contract:
description: Path to the consumer contract YAML (in the consumer repo) description: Path to the consumer contract YAML (in the consumer repo)
type: string type: string
default: .nova/contract.yml default: .acdl/contract.yml
mode: mode:
description: Pipeline mode — full (apply), plan-only, check-only, or decommission description: Pipeline mode — full (apply), plan-only, check-only, or decommission
type: string type: string
@@ -102,16 +102,13 @@ jobs:
- name: Configure AWS credentials (OIDC default + static-key override) - name: Configure AWS credentials (OIDC default + static-key override)
uses: aws-actions/configure-aws-credentials@v4 uses: aws-actions/configure-aws-credentials@v4
with: with:
# P4 (REQ-163): IAM role renamed acdl-deploy- → nova-deploy-. role-to-assume: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.ACDL_AWS_ACCOUNT_ID, github.repository_id) || '' }}
role-to-assume: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/nova-deploy-{1}', secrets.NOVA_AWS_ACCOUNT_ID, github.repository_id) || '' }}
aws-region: us-east-1 aws-region: us-east-1
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
- name: Run the platform pipeline - name: Run the platform pipeline
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
env:
NOVA_CONSUMER_REPO: ${{ github.repository }}
run: | run: |
MODE_FLAG="" MODE_FLAG=""
case "${{ inputs.mode }}" in case "${{ inputs.mode }}" in
@@ -148,7 +145,7 @@ jobs:
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: | run: |
aws lambda invoke-function-url \ aws lambda invoke-function-url \
--function-url "${{ secrets.NOVA_LAMBDA_URL }}" \ --function-url "${{ secrets.ACDL_LAMBDA_URL }}" \
--cli-binary-format raw-in-base64-out \ --cli-binary-format raw-in-base64-out \
--payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \ --payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \
/dev/null || true /dev/null || true
@@ -156,13 +153,13 @@ jobs:
- name: Upload emitted Terraform - name: Upload emitted Terraform
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: nova-terraform name: acdl-terraform
path: /tmp/nova_platform_run/tf/*.tf path: /tmp/acdl_platform_run_v18/tf/*.tf
if-no-files-found: warn if-no-files-found: warn
- name: Upload platform log - name: Upload platform log
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: nova-platform-log name: acdl-platform-log
path: platform/logs/ path: platform/logs/
if-no-files-found: warn if-no-files-found: warn
+29 -29
View File
@@ -1,4 +1,4 @@
# Nova Modules Lifecycle Pipeline (dev environment) # ACDL Modules Lifecycle Pipeline — Gitea Actions (dev environment)
# #
# Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through # Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through
# apply→modify→destroy against live AWS. No per-module Python. The "test" = # apply→modify→destroy against live AWS. No per-module Python. The "test" =
@@ -9,13 +9,13 @@
# terraform files); the composition must be deterministic. # terraform files); the composition must be deterministic.
# #
# This workflow implements pipelines/modules-lifecycle.yml (byte-identical # This workflow implements pipelines/modules-lifecycle.yml (byte-identical
# in .github/workflows/). # in .gitea/workflows/ and .github/workflows/).
# #
# Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to # Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to
# "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast, # "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast,
# no AWS mutation, validates the contract->resolver->adapter->plan chain # no AWS mutation, validates the contract->resolver->adapter->plan chain
# for every module on every PR, with no AWS credentials or cost). Set to # for every module on every PR, with no AWS credentials or cost). Set to
# "full" via workflow_dispatch (or the NOVA_LIFECYCLE_MODE repo variable) # "full" via workflow_dispatch (or the ACDL_LIFECYCLE_MODE repo variable)
# to run the real apply→modify→destroy against live AWS. In plan mode the # to run the real apply→modify→destroy against live AWS. In plan mode the
# short-lived CI VPC apply/destroy jobs are skipped (nothing is applied). # short-lived CI VPC apply/destroy jobs are skipped (nothing is applied).
# #
@@ -49,7 +49,7 @@ jobs:
ci-vpc-apply: ci-vpc-apply:
name: CI VPC apply name: CI VPC apply
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }} if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Terraform 1.9.* - name: Install Terraform 1.9.*
@@ -60,8 +60,8 @@ jobs:
- name: Apply CI VPC - name: Apply CI VPC
working-directory: terraform/ci-vpc working-directory: terraform/ci-vpc
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: | run: |
terraform init -input=false -lock=false terraform init -input=false -lock=false
@@ -78,7 +78,7 @@ jobs:
matrix: matrix:
module: [s3, kms-key, ecr, ecs-cluster, iam-role, cloudfront, waf, vpc, alb, ecs-service, rds, uptime] module: [s3, kms-key, ecr, ecs-cluster, iam-role, cloudfront, waf, vpc, alb, ecs-service, rds, uptime]
env: env:
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }} ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Free disk space - name: Free disk space
@@ -97,31 +97,31 @@ jobs:
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.* sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Read CI VPC outputs - name: Read CI VPC outputs
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }} if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }}
working-directory: terraform/ci-vpc working-directory: terraform/ci-vpc
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: | run: |
terraform init -input=false -lock=false terraform init -input=false -lock=false
terraform output -json > /tmp/ci-vpc-outputs.json terraform output -json > /tmp/ci-vpc-outputs.json
- name: Apply (simple) - name: Apply (simple)
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
- name: Modify (complex) - name: Modify (complex)
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
- name: Destroy - name: Destroy
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json run: bash scripts/run_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
@@ -136,7 +136,7 @@ jobs:
matrix: matrix:
module: [static-assets, microservice] module: [static-assets, microservice]
env: env:
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }} ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Free disk space - name: Free disk space
@@ -155,31 +155,31 @@ jobs:
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.* sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Read CI VPC outputs - name: Read CI VPC outputs
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }} if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }}
working-directory: terraform/ci-vpc working-directory: terraform/ci-vpc
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: | run: |
terraform init -input=false -lock=false terraform init -input=false -lock=false
terraform output -json > /tmp/ci-vpc-outputs.json terraform output -json > /tmp/ci-vpc-outputs.json
- name: Apply (simple) - name: Apply (simple)
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
- name: Modify (complex) - name: Modify (complex)
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
- name: Destroy - name: Destroy
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json run: bash scripts/run_l2_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
@@ -188,7 +188,7 @@ jobs:
name: CI VPC destroy name: CI VPC destroy
needs: [lifecycle, l2-lifecycle] needs: [lifecycle, l2-lifecycle]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }} if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Terraform 1.9.* - name: Install Terraform 1.9.*
@@ -199,8 +199,8 @@ jobs:
- name: Destroy CI VPC - name: Destroy CI VPC
working-directory: terraform/ci-vpc working-directory: terraform/ci-vpc
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: | run: |
terraform init -input=false -lock=false terraform init -input=false -lock=false
-43
View File
@@ -1,43 +0,0 @@
# 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'
- 'scripts/inline_images.py'
- 'scripts/render_pptx.py'
- 'pyproject.toml'
workflow_dispatch:
jobs:
render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: '20' }
- 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@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/*-python.pptx \
docs/presentations/assets/png/*.png
git diff --cached --quiet || git commit -m "chore(slides): re-render deck [skip ci]"
git push
-45
View File
@@ -1,45 +0,0 @@
# GitHub Workflows — Nova Platform CI/CD Catalog
This directory contains the GitHub Actions workflows for the Nova
platform. 3 are generated from `workflows-src/<name>`; 4 are GitHub-only.
## Shared workflows (generated from source)
These 3 are generated from `workflows-src/<name>`. Run `python3 scripts/sync_workflows.py --check` to verify
no drift.
| Workflow | Trigger | Inputs | Required Secrets | Purpose |
|----------|---------|--------|------------------|---------|
| `ci.yml` | `pull_request: [main]` | — | — | Lint + test + check-only (runs on every PR) |
| `deploy.yml` | `workflow_call` (reusable) + `push: [main]` | `contract` (string, required), `mode` (string, default `deploy`), `changeRequestId` (string), `environment` (string) | `NOVA_AWS_ACCESS_KEY_ID`, `NOVA_AWS_SECRET_ACCESS_KEY`, `NOVA_AWS_DEFAULT_REGION`, `NOVA_KMS_KEY_ID`, `NOVA_LAMBDA_URL` | Reusable deploy workflow (invoked by consumer repos via `uses: nova/.github/workflows/deploy.yml@v1.19`) |
| `modules-lifecycle.yml` | `pull_request: [main]` + `workflow_dispatch` | `lifecycle_mode` (string, default `plan``plan` or `full`) | `NOVA_AWS_ACCESS_KEY_ID`, `NOVA_AWS_SECRET_ACCESS_KEY`, `NOVA_AWS_DEFAULT_REGION`, `NOVA_AWS_ACCOUNT_ID` | L1 + L2 module lifecycle pipeline (plan-only default; full apply/modify/destroy on override) |
## GitHub-only workflows
These 4 have no counterpart (the dev forge lacks the features
they require — reusable workflows, matrix `needs`, release API).
| Workflow | Trigger | Inputs | Required Secrets | Purpose |
|----------|---------|--------|------------------|---------|
| `platform-test.yml` | `pull_request: [main]` | — | — | Lint + unit + integration + schema-validation (replaces `ci.yml` for PRs) |
| `primitives-plan.yml` | `pull_request: [main]` | — | `NOVA_AWS_*` | Plan-only for all L1 primitives (matrix) |
| `patterns-plan.yml` | `pull_request: [main]` | — | `NOVA_AWS_*` | Plan-only for all L2 modules (matrix) |
| `release.yml` | `push: [main]` | — | `NOVA_RELEASE_TOKEN` | Semver tag + MAJOR.MINOR/MAJOR floating-tag maintenance + release creation on merge to main |
## Reusable deploy workflow (`deploy.yml`)
Consumer repos invoke the deploy workflow via a versioned tag:
```yaml
jobs:
deploy:
uses: nova/.github/workflows/deploy.yml@v1.19
with:
contract: .nova/contract.yml
environment: dev
secrets: inherit
```
The workflow checks out the consumer repo + the Nova platform repo, runs
`scripts/run_platform.sh`, and posts deploy outputs as a PR comment +
to SSM Parameter Store.
+1 -16
View File
@@ -1,4 +1,4 @@
# Nova CI Pipeline (dev environment) # ACDL CI Pipeline — Gitea Actions (dev environment)
# #
# This workflow implements the central pipeline contract: # This workflow implements the central pipeline contract:
# pipelines/ci.yml (validated against schemas/pipeline.schema.json) # pipelines/ci.yml (validated against schemas/pipeline.schema.json)
@@ -63,21 +63,6 @@ jobs:
- name: Install test dependencies - name: Install test dependencies
run: pip install -r requirements-test.txt run: pip install -r requirements-test.txt
- name: Install kyverno-json (kj) for policy-engine tests
uses: actions/setup-go@v5
with:
go-version: "1.22"
cache: false
- name: Install kj binary
run: |
# v1.25: kyverno-json is the primary policy engine. Tests that
# require kj skip when absent, so this is best-effort (the suite
# passes with or without kj).
go install github.com/kyverno/kyverno-json/cmd/kj@latest && \
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" || \
echo "kj install failed; policy-engine tests will skip"
- name: Run pytest - name: Run pytest
run: python3 -m pytest tests/ -v --tb=short run: python3 -m pytest tests/ -v --tb=short
+14 -17
View File
@@ -1,4 +1,4 @@
# Nova Reusable Deploy Workflow (dev environment) # ACDL Reusable Deploy Workflow — Gitea Actions (dev environment)
# #
# This reusable workflow implements the central deployment pipeline contract: # This reusable workflow implements the central deployment pipeline contract:
# pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json) # pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json)
@@ -8,7 +8,7 @@
# declared difference is the forge/runtime, not the stages or commands. # declared difference is the forge/runtime, not the stages or commands.
# #
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR): # Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
# uses: nova/.github/workflows/deploy.yml@v1.19 # uses: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea)
# uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub) # uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
# #
# Unversioned references (@main, bare) are discouraged — the consumer's setup # Unversioned references (@main, bare) are discouraged — the consumer's setup
@@ -26,7 +26,7 @@
# platform log) for auditability. # platform log) for auditability.
# #
# Inputs: # Inputs:
# contract — path to the consumer's contract YAML (default .nova/contract.yml) # contract — path to the consumer's contract YAML (default .acdl/contract.yml)
# mode — full | plan-only | check-only (default full; dev = full apply, # mode — full | plan-only | check-only (default full; dev = full apply,
# higher environments hold for HITL — the calling repo or the # higher environments hold for HITL — the calling repo or the
# forge environment gate enforces that) # forge environment gate enforces that)
@@ -38,12 +38,12 @@
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session # that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>. # policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
# #
# Override (where OIDC is unavailable, e.g. pending # Override (where OIDC is unavailable, e.g. Gitea pending
# upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY # go-gitea/gitea#36988): set ACDL_AWS_ACCESS_KEY_ID + ACDL_AWS_SECRET_ACCESS_KEY
# as repository secrets. The platform-managed scheduled pipeline rotates # as repository secrets. The platform-managed scheduled pipeline rotates
# the key on a daily cadence. When .env.secrets is used locally instead, # the key on a daily cadence. When .env.secrets is used locally instead,
# rotating the key out of band is the consumer's responsibility. # rotating the key out of band is the consumer's responsibility.
name: nova-deploy name: acdl-deploy
on: on:
workflow_call: workflow_call:
@@ -51,7 +51,7 @@ on:
contract: contract:
description: Path to the consumer contract YAML (in the consumer repo) description: Path to the consumer contract YAML (in the consumer repo)
type: string type: string
default: .nova/contract.yml default: .acdl/contract.yml
mode: mode:
description: Pipeline mode — full (apply), plan-only, check-only, or decommission description: Pipeline mode — full (apply), plan-only, check-only, or decommission
type: string type: string
@@ -102,16 +102,13 @@ jobs:
- name: Configure AWS credentials (OIDC default + static-key override) - name: Configure AWS credentials (OIDC default + static-key override)
uses: aws-actions/configure-aws-credentials@v4 uses: aws-actions/configure-aws-credentials@v4
with: with:
# P4 (REQ-163): IAM role renamed acdl-deploy- → nova-deploy-. role-to-assume: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.ACDL_AWS_ACCOUNT_ID, github.repository_id) || '' }}
role-to-assume: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/nova-deploy-{1}', secrets.NOVA_AWS_ACCOUNT_ID, github.repository_id) || '' }}
aws-region: us-east-1 aws-region: us-east-1
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
- name: Run the platform pipeline - name: Run the platform pipeline
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
env:
NOVA_CONSUMER_REPO: ${{ github.repository }}
run: | run: |
MODE_FLAG="" MODE_FLAG=""
case "${{ inputs.mode }}" in case "${{ inputs.mode }}" in
@@ -148,7 +145,7 @@ jobs:
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: | run: |
aws lambda invoke-function-url \ aws lambda invoke-function-url \
--function-url "${{ secrets.NOVA_LAMBDA_URL }}" \ --function-url "${{ secrets.ACDL_LAMBDA_URL }}" \
--cli-binary-format raw-in-base64-out \ --cli-binary-format raw-in-base64-out \
--payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \ --payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \
/dev/null || true /dev/null || true
@@ -156,13 +153,13 @@ jobs:
- name: Upload emitted Terraform - name: Upload emitted Terraform
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: nova-terraform name: acdl-terraform
path: /tmp/nova_platform_run/tf/*.tf path: /tmp/acdl_platform_run_v18/tf/*.tf
if-no-files-found: warn if-no-files-found: warn
- name: Upload platform log - name: Upload platform log
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: nova-platform-log name: acdl-platform-log
path: platform/logs/ path: platform/logs/
if-no-files-found: warn if-no-files-found: warn
+29 -29
View File
@@ -1,4 +1,4 @@
# Nova Modules Lifecycle Pipeline (dev environment) # ACDL Modules Lifecycle Pipeline — Gitea Actions (dev environment)
# #
# Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through # Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through
# apply→modify→destroy against live AWS. No per-module Python. The "test" = # apply→modify→destroy against live AWS. No per-module Python. The "test" =
@@ -9,13 +9,13 @@
# terraform files); the composition must be deterministic. # terraform files); the composition must be deterministic.
# #
# This workflow implements pipelines/modules-lifecycle.yml (byte-identical # This workflow implements pipelines/modules-lifecycle.yml (byte-identical
# in .github/workflows/). # in .gitea/workflows/ and .github/workflows/).
# #
# Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to # Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to
# "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast, # "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast,
# no AWS mutation, validates the contract->resolver->adapter->plan chain # no AWS mutation, validates the contract->resolver->adapter->plan chain
# for every module on every PR, with no AWS credentials or cost). Set to # for every module on every PR, with no AWS credentials or cost). Set to
# "full" via workflow_dispatch (or the NOVA_LIFECYCLE_MODE repo variable) # "full" via workflow_dispatch (or the ACDL_LIFECYCLE_MODE repo variable)
# to run the real apply→modify→destroy against live AWS. In plan mode the # to run the real apply→modify→destroy against live AWS. In plan mode the
# short-lived CI VPC apply/destroy jobs are skipped (nothing is applied). # short-lived CI VPC apply/destroy jobs are skipped (nothing is applied).
# #
@@ -49,7 +49,7 @@ jobs:
ci-vpc-apply: ci-vpc-apply:
name: CI VPC apply name: CI VPC apply
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }} if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Terraform 1.9.* - name: Install Terraform 1.9.*
@@ -60,8 +60,8 @@ jobs:
- name: Apply CI VPC - name: Apply CI VPC
working-directory: terraform/ci-vpc working-directory: terraform/ci-vpc
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: | run: |
terraform init -input=false -lock=false terraform init -input=false -lock=false
@@ -78,7 +78,7 @@ jobs:
matrix: matrix:
module: [s3, kms-key, ecr, ecs-cluster, iam-role, cloudfront, waf, vpc, alb, ecs-service, rds, uptime] module: [s3, kms-key, ecr, ecs-cluster, iam-role, cloudfront, waf, vpc, alb, ecs-service, rds, uptime]
env: env:
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }} ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Free disk space - name: Free disk space
@@ -97,31 +97,31 @@ jobs:
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.* sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Read CI VPC outputs - name: Read CI VPC outputs
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }} if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }}
working-directory: terraform/ci-vpc working-directory: terraform/ci-vpc
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: | run: |
terraform init -input=false -lock=false terraform init -input=false -lock=false
terraform output -json > /tmp/ci-vpc-outputs.json terraform output -json > /tmp/ci-vpc-outputs.json
- name: Apply (simple) - name: Apply (simple)
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
- name: Modify (complex) - name: Modify (complex)
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
- name: Destroy - name: Destroy
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json run: bash scripts/run_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
@@ -136,7 +136,7 @@ jobs:
matrix: matrix:
module: [static-assets, microservice] module: [static-assets, microservice]
env: env:
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }} ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Free disk space - name: Free disk space
@@ -155,31 +155,31 @@ jobs:
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.* sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Read CI VPC outputs - name: Read CI VPC outputs
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }} if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }}
working-directory: terraform/ci-vpc working-directory: terraform/ci-vpc
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: | run: |
terraform init -input=false -lock=false terraform init -input=false -lock=false
terraform output -json > /tmp/ci-vpc-outputs.json terraform output -json > /tmp/ci-vpc-outputs.json
- name: Apply (simple) - name: Apply (simple)
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
- name: Modify (complex) - name: Modify (complex)
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
- name: Destroy - name: Destroy
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json run: bash scripts/run_l2_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
@@ -188,7 +188,7 @@ jobs:
name: CI VPC destroy name: CI VPC destroy
needs: [lifecycle, l2-lifecycle] needs: [lifecycle, l2-lifecycle]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }} if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Terraform 1.9.* - name: Install Terraform 1.9.*
@@ -199,8 +199,8 @@ jobs:
- name: Destroy CI VPC - name: Destroy CI VPC
working-directory: terraform/ci-vpc working-directory: terraform/ci-vpc
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 AWS_DEFAULT_REGION: us-east-1
run: | run: |
terraform init -input=false -lock=false terraform init -input=false -lock=false
+3 -3
View File
@@ -1,4 +1,4 @@
# Nova Release Pipeline — GitHub Actions (production) # ACDL Release Pipeline — GitHub Actions (production)
# #
# Runs on push to main. Computes the next semver tag from the latest tag + # Runs on push to main. Computes the next semver tag from the latest tag +
# commit history, creates the tag, updates floating MAJOR.MINOR and MAJOR tags, # commit history, creates the tag, updates floating MAJOR.MINOR and MAJOR tags,
@@ -8,7 +8,7 @@
# - Regular phase commit -> bump PATCH (v1.6.0 -> v1.6.1) # - Regular phase commit -> bump PATCH (v1.6.0 -> v1.6.1)
# - Milestone completion ("docs(milestone): complete") -> bump MINOR (v1.6.1 -> v1.7.0) # - Milestone completion ("docs(milestone): complete") -> bump MINOR (v1.6.1 -> v1.7.0)
# - Major bumps are manual (not implemented here). # - Major bumps are manual (not implemented here).
name: nova-release name: acdl-release
on: on:
push: push:
@@ -87,6 +87,6 @@ jobs:
BODY=$(git log --format='- %s' HEAD) BODY=$(git log --format='- %s' HEAD)
fi fi
gh release create ${{ steps.version.outputs.new_tag }} \ gh release create ${{ steps.version.outputs.new_tag }} \
--title "Nova ${{ steps.version.outputs.new_tag }}" \ --title "ACDL ${{ steps.version.outputs.new_tag }}" \
--notes "$BODY" \ --notes "$BODY" \
--generate-notes || true --generate-notes || true
-43
View File
@@ -1,43 +0,0 @@
# 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'
- 'scripts/inline_images.py'
- 'scripts/render_pptx.py'
- 'pyproject.toml'
workflow_dispatch:
jobs:
render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: '20' }
- 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@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/*-python.pptx \
docs/presentations/assets/png/*.png
git diff --cached --quiet || git commit -m "chore(slides): re-render deck [skip ci]"
git push
+1 -14
View File
@@ -14,18 +14,6 @@ terraform/bootstrap/.bootstrap_state.json
# CIAgent runtime artifacts # CIAgent runtime artifacts
.ciagent/logs/ .ciagent/logs/
# Nova metrics runtime artifacts (REQ-187, D-128)
# Generated: nova_metrics.db, decision_ledger.db, events.jsonl, runs/, test-results.xml, coverage.json, test-report.json
# NOT ignored: metrics/README.md, metrics/powerbi/ (export views), schemas/metrics_*.schema.json
metrics/nova_metrics.db
metrics/decision_ledger.db
metrics/events.jsonl
metrics/test-results.xml
metrics/test-report.json
metrics/coverage.json
metrics/runs/
metrics/lifecycle/
# Terraform — recursively ignore .terraform dirs, lock files, plans, and state # Terraform — recursively ignore .terraform dirs, lock files, plans, and state
**/.terraform/ **/.terraform/
**/.terraform.lock.hcl **/.terraform.lock.hcl
@@ -40,5 +28,4 @@ metrics/lifecycle/
*.cer *.cer
*.crt *.crt
*.jks *.jks
*.keystore.coverage *.keystore
.coverage
+62 -50
View File
@@ -1,6 +1,4 @@
# Nova # ACDL — Agentic Cloud Delivery Platform
> **Nova — The New Dawn of DevSecOps.** Security as a seamless enabler of fast deployments — not a bottleneck, not a "no" department.
Consumers declare intent; the platform delivers safe production deployment Consumers declare intent; the platform delivers safe production deployment
through an agentic stack — automatically, safely, and with a complete audit through an agentic stack — automatically, safely, and with a complete audit
@@ -20,7 +18,7 @@ a configuration file, or an infrastructure module.
## Repository roles ## Repository roles
There are two kinds of repository in the Nova model: There are two kinds of repository in the ACDL model:
- **Platform repo (this one).** This is the **source code of the platform**. - **Platform repo (this one).** This is the **source code of the platform**.
It owns `modules/`, `adapters/`, `core/`, `schemas/`, `pipelines/`, It owns `modules/`, `adapters/`, `core/`, `schemas/`, `pipelines/`,
@@ -28,7 +26,7 @@ There are two kinds of repository in the Nova model:
A **consumer never clones it.** A **consumer never clones it.**
- **Consumer repo (yours).** A consumer repo contains only: - **Consumer repo (yours).** A consumer repo contains only:
1. **Its application code** — the service or site being deployed. 1. **Its application code** — the service or site being deployed.
2. **One or more contracts** — small YAML files at `.nova/contract.yml` 2. **One or more contracts** — small YAML files at `.acdl/contract.yml`
that declare infrastructure (one or more modules by name + version), that declare infrastructure (one or more modules by name + version),
select an environment, and supply module-specific inputs. select an environment, and supply module-specific inputs.
3. **One or more CI definitions** — thin `.github/workflows/*.yml` files 3. **One or more CI definitions** — thin `.github/workflows/*.yml` files
@@ -95,7 +93,7 @@ intent via a contract; the platform delivers the deployment through the
same contract schema, the same policy envelope, and the same evidence same contract schema, the same policy envelope, and the same evidence
stream. stream.
Consumers have their own repos and consume Nova by writing a contract that Consumers have their own repos and consume ACDL by writing a contract that
declares infrastructure. A consumer declares a contract (id + name + declares infrastructure. A consumer declares a contract (id + name +
environment + infrastructure); the platform resolves it to a stack instance, environment + infrastructure); the platform resolves it to a stack instance,
compiles it, runs security + policy checks, computes a confidence signal, compiles it, runs security + policy checks, computes a confidence signal,
@@ -126,51 +124,25 @@ engine-specific code. `modules/`, `schemas/`, `contracts/`,
## How to run ## How to run
### Quick start (offline, no AWS required) ### Prerequisites
The fastest way to verify the platform works — no AWS credentials, no > These prerequisites are for running the **platform repo** locally. A
bootstrap, no cost. See the [Consumer guide](docs/consumer-guide.md) > consumer does not need any of these — see the
for the consumer happy path (a consumer owns only a contract + app code). > [Consumer guide](docs/consumer-guide.md) for the consumer happy path.
```bash - A platform-managed environment (see [docs/environments/](docs/environments/)).
# Install test dependencies For local testing, `core/environments/dev.json` is provided as the sample.
pip install -r requirements-test.txt - AWS credentials for the dev environment (in `.env.secrets`, gitignored;
see [Credentials & zero-trust](#credentials--zero-trust)).
- `terraform` (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3`
+ `jsonschema`.
# 1. Run the test suite (all offline — uses moto for DynamoDB mocking) ### Run the platform pipeline end-to-end
python3 -m pytest tests/ -v
# 2. Run the platform in check-only mode (offline — contract -> resolver ->
# adapter -> structure validation). Uses the default sample contract
# (contracts/static-assets.yaml) + sample dev environment.
bash scripts/run_platform.sh --check-only
# Expected: "=== PLATFORM CHECK OK ==="
# 3. Run the headline E2E against the local emulating tier (emulates ECS,
# outbox, S3 state, Lambda in-process; D-092).
bash scripts/run_platform.sh --local
# Expected: "=== LOCAL E2E OK ==="
# 4. Reproduce the full CI pipeline locally (lint -> test -> check-only)
bash scripts/run_ci.sh
# Expected: "=== CI PIPELINE OK ==="
# Show all run_platform.sh flags:
bash scripts/run_platform.sh --help
```
### Run against live AWS (requires credentials + bootstrap)
> Prerequisites: a platform-managed environment (see
> [docs/environments/](docs/environments/); `core/environments/dev.json`
> is the sample), AWS credentials for dev (in `.env.secrets`, gitignored;
> see [Credentials & zero-trust](#credentials--zero-trust)), `terraform`
> (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3` +
> `jsonschema`.
```bash ```bash
# 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent) # 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent)
# (requires the bootstrap root key in env — skip if the state bucket + # (requires the bootstrap root key in env — skip if the state bucket +
# nova-spike-runner already exist) # acdl-spike-runner already exist)
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \ ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
python3 terraform/bootstrap/create_state_backend.py python3 terraform/bootstrap/create_state_backend.py
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \ ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
@@ -194,6 +166,26 @@ bash scripts/run_platform.sh --plan-only contracts/static-assets.yaml
bash scripts/run_platform.sh --quiet contracts/static-assets.yaml bash scripts/run_platform.sh --quiet contracts/static-assets.yaml
``` ```
### Test the platform (offline, no AWS required)
```bash
# Install test dependencies
pip install -r requirements-test.txt
# Run the test suite (all offline — uses moto for DynamoDB mocking)
python3 -m pytest tests/ -v
# Run the platform in check-only mode (offline — no AWS, no policy checks,
# no outbox). Uses the default sample contract (contracts/static-assets.yaml)
# and the sample dev environment (core/environments/dev.json).
bash scripts/run_platform.sh --check-only
# Expected: "=== PLATFORM CHECK OK ==="
# Reproduce the full CI pipeline locally (lint -> test -> check-only)
bash scripts/run_ci.sh
# Expected: "=== CI PIPELINE OK ==="
```
### CI/CD pipelines ### CI/CD pipelines
The CI/CD pipeline is defined by a **central pipeline contract** — a The CI/CD pipeline is defined by a **central pipeline contract** — a
@@ -219,9 +211,23 @@ bash scripts/run_ci.sh --quiet # suppress per-stage banners
### Reusable deploy workflow ### Reusable deploy workflow
Consumer repos invoke the deploy pipeline via `.github/workflows/deploy.yml` The deployment pipeline is defined by a **central deployment pipeline
(a reusable GitHub Actions workflow, versioned tag `nova/.github/workflows/deploy.yml@v1.19`). contract** (`pipelines/contract.yml`, validated against
See the [Consumer guide](docs/consumer-guide.md) for the end-to-end happy path. `schemas/deploy-pipeline.schema.json`) and exposed to consumer repos as a
**reusable workflow**:
- `.github/workflows/deploy.yml` — GitHub Actions (production)
The workflow implements the same stages as `pipelines/contract.yml`
(validate-contract → resolve-stack → security checks → infrastructure plan
→ policy checks → confidence → evidence event → apply). A consumer repo
invokes the reusable workflow via a **versioned tag** (floating MAJOR +
MINOR, e.g. `acdl/.github/workflows/deploy.yml@v1.6`). The workflow checks
out the consumer repo, then checks out the ACDL platform repo into the
runner workspace, and runs `scripts/run_platform.sh` against the consumer's
contract — the consumer never clones the platform repo or invokes its
scripts locally. See the [Consumer guide](docs/consumer-guide.md) for the
end-to-end happy path.
### Output streaming (run_platform.sh) ### Output streaming (run_platform.sh)
@@ -241,7 +247,7 @@ backwards-compatible log-only mode.
## Consumer guide ## Consumer guide
A step-by-step guide for a consumer to create their pipeline and define a A step-by-step guide for a consumer to create their pipeline and define a
contract that deploys any Nova module to AWS is at contract that deploys any ACDL module to AWS is at
[`docs/consumer-guide.md`](docs/consumer-guide.md). The guide is generic [`docs/consumer-guide.md`](docs/consumer-guide.md). The guide is generic
across all modules; `static-assets` is the worked example. across all modules; `static-assets` is the worked example.
@@ -277,8 +283,8 @@ no static credentials in repo secrets.
`repo:org/consumer-repo:ref:refs/heads/main`) binds the role's trust `repo:org/consumer-repo:ref:refs/heads/main`) binds the role's trust
policy to the exact consumer repo + branch that invoked the workflow. policy to the exact consumer repo + branch that invoked the workflow.
- **Resource-creation attributes** — every resource the pipeline creates - **Resource-creation attributes** — every resource the pipeline creates
is tagged with `nova:owner=<consumer-repo>` and is tagged with `acdl:owner=<consumer-repo>` and
`nova:contract=<contract-id>`. The session policy grants `acdl:contract=<contract-id>`. The session policy grants
view/update/delete **only on resources whose tags match the calling view/update/delete **only on resources whose tags match the calling
repo**. repo**.
@@ -296,6 +302,12 @@ documented alternative:
runs, or in **`.env.secrets`** (gitignored, chmod 600) for local testing. runs, or in **`.env.secrets`** (gitignored, chmod 600) for local testing.
- The platform rotates platform-runner keys on a **daily cadence** - The platform rotates platform-runner keys on a **daily cadence**
rotation is not the consumer's burden in the platform-runner path. rotation is not the consumer's burden in the platform-runner path.
- **When `.env.secrets` is used locally**, rotating the key **out of band is
the consumer's responsibility**. The platform guarantees daily rotation
for platform-runner runs; it does not guarantee rotation for
locally-held copies. The consumer must rotate a local key via
`scripts/rotate_spike_key.sh` (or equivalent) on their own cadence.
No long-lived credential is permitted persistently — the platform-runner No long-lived credential is permitted persistently — the platform-runner
key's useful lifetime is one workflow run, and the local alternative is key's useful lifetime is one workflow run, and the local alternative is
rotated at least daily (platform-runner) or out of band (local). rotated at least daily (platform-runner) or out of band (local).
+1 -32
View File
@@ -1,4 +1,4 @@
# Nova Adapters # ACDL Adapters
## Overview ## Overview
@@ -12,37 +12,6 @@ Adapters translate the engine-agnostic Target Stack IR to engine-specific format
| Checkov adapter | `adapters/terraform/policy/checkov_adapter.py` | Checkov JSON | `PolicyCheckResult` records | Translates Checkov results | | Checkov adapter | `adapters/terraform/policy/checkov_adapter.py` | Checkov JSON | `PolicyCheckResult` records | Translates Checkov results |
| Wiz adapter | `adapters/wiz/wiz_adapter.py` | Wiz API issues JSON | `PolicyCheckResult` records | Translates Wiz security findings | | Wiz adapter | `adapters/wiz/wiz_adapter.py` | Wiz API issues JSON | `PolicyCheckResult` records | Translates Wiz security findings |
| Kyverno adapter | `adapters/kyverno/kyverno_adapter.py` | Kyverno PolicyReport JSON | `PolicyCheckResult` records | K8s-native policy translation | | Kyverno adapter | `adapters/kyverno/kyverno_adapter.py` | Kyverno PolicyReport JSON | `PolicyCheckResult` records | K8s-native policy translation |
| kyverno-json engine | `adapters/kyverno-json/kyverno_json_engine.py` | Any JSON/YAML payload | `PolicyCheckResult` records | **v1.25 primary policy engine** (swappable via `PolicyEngine` protocol) |
## Policy Engine Protocol (v1.25)
The `core/policy_engine.py` module defines the **swap boundary** between
Nova and its policy engines. A `PolicyEngine` Python Protocol (PEP 544)
with three members (`name`, `is_configured()`, `evaluate()`) is the
contract; a `PolicyEngineRegistry` selects the active engine from
`config.json`'s `policy.engine` key. The confidence signal and pipeline
never import an engine directly — they go through the registry.
**Implementations:**
- `KyvernoJsonEngine` (`adapters/kyverno-json/`) — shells to the `kj`
CLI; the v1.25 default.
- `NullEngine` (`core/policy_engine.py`) — fallback when the `policy`
key is absent (emits `SKIPPED`).
- Future: `OpaEngine` — implements the same protocol, shells to
`opa eval`. The OPA-equivalent surface is documented in
`.ciagent/RESEARCH.md` §4.2.
**How to add a new engine:**
1. Create `adapters/<name>/<name>_engine.py` implementing the
`PolicyEngine` protocol (`name`, `is_configured()`, `evaluate()`).
2. `evaluate()` returns `list[dict]` where each dict conforms to
`schemas/policy_check_result.schema.json`.
3. Register the engine in `core/policy_engine.py`'s `_autoload_*`
function (or call `register(name, factory)` at startup).
4. Set `config.json.policy.engine` to the engine's `name`.
5. Add the engine to the `engine` enum in
`schemas/policy_check_result.schema.json` if it needs a distinct
enum value (v1.25 reuses `"kyverno"` — see D-116).
## How to Write an Adapter ## How to Write an Adapter
-103
View File
@@ -1,103 +0,0 @@
# kyverno-json Engine Adapter (v1.25)
The `kyverno-json` engine is Nova's **primary compliance/policy tool**
(v1.25), implemented behind the swappable `PolicyEngine` protocol so
OPA (or any other engine) can replace it one day.
## What kyverno-json is
[kyverno-json](https://github.com/kyverno/kyverno-json) is a standalone
Go binary from the Kyverno project — a **separate runtime** from the
K8s Kyverno admission controller. It applies Kyverno `ValidatingPolicy`
resources to **any** JSON or YAML payload file via the `kj scan` CLI.
Unlike the K8s Kyverno adapter (`adapters/kyverno/`), which only
speaks to K8s manifests, kyverno-json evaluates consumer contracts,
resolved Stack IR, terraform plan JSON, and even the merged PCR list
itself (meta-policies).
## Install
```bash
bash scripts/install-kyverno-json.sh
# or directly:
go install github.com/kyverno/kyverno-json/cmd/kj@latest
kj version
```
The platform functions without the binary — `is_configured()` returns
`False` when `which kj` is absent → `evaluate()` returns a single
`SKIPPED` PCR (`KJ_ENGINE_NOT_CONFIGURED`). The confidence signal
proceeds with a neutral `policy` input (D-120 graceful degradation).
## Policy directory layout
```
adapters/kyverno-json/policies/
├── _smoke.json # round-trip smoke test
├── contract/ # consumer contract JSON policies
│ ├── require-id-pattern.json
│ ├── require-env-in-enum.json
│ ├── require-infrastructure-min-1.json
│ └── forbid-unknown-fields.json
├── stack-ir/ # resolved Stack IR policies
│ ├── require-tagging-standard.json
│ ├── forbid-public-ingress.json
│ └── require-encryption-by-default.json
├── plan-json/ # terraform show -json policies
│ ├── forbid-plaintext-secrets.json
│ ├── forbid-iam-wildcard.json
│ └── require-kms-reference.json
├── meta/ # policies over the merged PCR list
│ ├── block-on-any-critical.json
│ └── tagging-rules-agree.json
└── regression/ # capability-inventory policies
├── cap-013-adapter-dedup.json
├── cap-023-metrics-collector.json
└── cap-024-deck-structure.json
```
## The four policy categories
1. **contract/** — over the consumer contract JSON (pre-resolve).
2. **stack-ir/** — over the resolved Target Stack IR (post-resolve).
3. **plan-json/** — over `terraform show -json` output (pipeline Step 5b).
4. **meta/** — over the merged `list[PolicyCheckResult]` (meta-policies).
5. **regression/** — over the capability-inventory JSON (declarative
mirrors of `core/regression_verify.py`).
## Severity convention
kyverno-json does not natively assign severities. Each Nova policy
declares its severity via a `metadata.annotations` field:
```yaml
metadata:
annotations:
nova.cloudinit.dev/severity: high
```
Valid values: `critical`, `high`, `medium`, `low`, `info` (default
when absent).
## Engine enum reuse (D-116)
kyverno-json PCR records carry `engine: "kyverno"` (no new enum value).
The `engine` field records the policy-engine *family*, not the specific
binary. The K8s Kyverno adapter and the kyverno-json engine are
distinguished by `ruleId` prefix (`KYVERNO_` vs `KJ_`) and `evidence`
payload shape (`namespace`/`kind` vs `assertion`/`jmespath`).
## Schema path
The output records validate against
[`schemas/policy_check_result.schema.json`](../../schemas/policy_check_result.schema.json)
(`engine: "kyverno"` is in the enum). The confidence signal consumes
the merged PCR list engine-agnostically.
## Swap boundary
The `PolicyEngine` protocol (`core/policy_engine.py`) is the swap
boundary. The OPA-equivalent surface is documented in
`.ciagent/RESEARCH.md` §4.2 — a future `OpaEngine` implements the same
protocol without touching the confidence signal, the PCR schema, or
the pipeline.
-27
View File
@@ -1,27 +0,0 @@
"""Nova kyverno-json adapter package (v1.25, REQ-294).
The directory name ``kyverno-json`` has a hyphen, so it is not a valid
Python package name and cannot be imported via ``import
adapters.kyverno-json``. The ``PolicyEngineRegistry`` loads the engine
by file path (``importlib.util.spec_from_file_location``). This
``__init__`` is a convenience for direct-script use and for ``pip
install -e .`` style discovery if the package is ever renamed.
"""
def _load_engine():
import importlib.util
import os
engine_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"kyverno_json_engine.py")
spec = importlib.util.spec_from_file_location("kyverno_json_engine", engine_path)
if spec is None or spec.loader is None:
raise ImportError(f"could not load {engine_path}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.KyvernoJsonEngine
KyvernoJsonEngine = _load_engine()
__all__ = ["KyvernoJsonEngine"]
@@ -1,269 +0,0 @@
"""Nova KyvernoJsonEngine (REQ-293, v1.25).
Implements the ``PolicyEngine`` protocol (``core/policy_engine.py``)
by shelling to the ``kj`` CLI (``kyverno-json``). Translates native
kyverno-json scan output to Nova ``PolicyCheckResult`` dicts
(``schemas/policy_check_result.schema.json``).
Engine enum reuse (D-116): records carry ``engine: "kyverno"`` (no new
enum value). The ``ruleId`` is prefixed ``KJ_<policy_name>`` to
distinguish from the K8s Kyverno adapter's ``KYVERNO_`` prefix.
Severity (RESEARCH §2.6, G-Q10a): kyverno-json does not natively assign
severities. Each Nova policy declares its severity via a
``metadata.annotations["nova.cloudinit.dev/severity"]`` field. The
engine reads this annotation from the loaded policy YAML (not from the
scan result the result doesn't carry it) and applies it to every
result that policy produces. Default when absent: ``"info"``.
Graceful degradation (D-120): ``is_configured()`` returns ``False`` when
``which kj`` is absent ``evaluate()`` returns a single SKIPPED PCR
(``ruleId: KJ_ENGINE_NOT_CONFIGURED``). The platform functions without
the binary.
Defensive parsing: any kyverno-json output that doesn't match the
expected shape produces an ``error`` PCR, never an exception. The
engine is read-only against a local policy dir + a temp payload file.
"""
import datetime
import json
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any, Union
import yaml
Payload = Union[dict, list, str]
SEVERITY_DEFAULT = "info"
SEVERITY_ANNOTATION = "nova.cloudinit.dev/severity"
RESULT_MAP = {
"pass": "pass",
"fail": "fail",
"error": "error",
"skip": "skipped",
"skipped": "skipped",
"warn": "skipped",
"warning": "skipped",
}
def _iso8601_now() -> str:
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _which_kj() -> str | None:
"""Return the path to ``kj`` if on PATH, else ``None``."""
return shutil.which("kj")
def _load_policy_severities(policy_dir: Path) -> dict[str, str]:
"""Load each ``.json``/``.yaml``/``.yml`` policy in ``policy_dir``
(non-recursive) and return ``{policy_name: severity}``.
kyverno-json policies are Kubernetes-style ``ValidatingPolicy``
resources. The severity is read from
``metadata.annotations["nova.cloudinit.dev/severity"]``. Policies
in subdirectories (e.g. ``contract/``, ``stack-ir/``) are loaded
when the caller passes that subdirectory as ``policy_dir``.
"""
severities: dict[str, str] = {}
if not policy_dir.is_dir():
return severities
for entry in sorted(os.listdir(policy_dir)):
if entry.startswith("_") or entry.startswith("."):
continue
full = policy_dir / entry
if not full.is_file():
continue
if entry.endswith((".json", ".yaml", ".yml")):
try:
with open(full, "r", encoding="utf-8") as fh:
doc = yaml.safe_load(fh)
if not isinstance(doc, dict):
continue
name = doc.get("metadata", {}).get("name") or entry.rsplit(".", 1)[0]
ann = doc.get("metadata", {}).get("annotations", {}) or {}
sev = ann.get(SEVERITY_ANNOTATION, SEVERITY_DEFAULT)
severities[name] = str(sev).lower()
except Exception:
continue
return severities
def _to_pcr(entry: dict, contract_id: str, severity: str) -> dict:
"""Translate a kyverno-json scan result entry to a PCR dict."""
policy_name = entry.get("policy", "") or "UNKNOWN"
rule_name = entry.get("rule", "") or ""
rule_id = f"KJ_{policy_name}"
if rule_name:
rule_id = f"{rule_id}/{rule_name}"
result_raw = entry.get("result", "skip")
result = RESULT_MAP.get(str(result_raw).lower(), "error")
message = entry.get("message", "") or ""
resource = entry.get("resource", "")
if not resource and entry.get("name"):
kind = entry.get("kind", "")
ns = entry.get("namespace", "")
resource = f"{kind}/{ns}/{entry.get('name')}" if kind else entry.get("name", "")
return {
"contractId": contract_id,
"evaluatedAt": _iso8601_now(),
"engine": "kyverno",
"ruleId": rule_id,
"severity": severity,
"result": result,
"message": message,
"evidence": {
"resource": resource,
"policy": policy_name,
"rule": rule_name,
"namespace": entry.get("namespace", ""),
"kind": entry.get("kind", ""),
"name": entry.get("name", ""),
},
"resourceRef": resource,
}
def _skipped_not_configured(contract_id: str) -> dict:
return {
"contractId": contract_id,
"evaluatedAt": _iso8601_now(),
"engine": "kyverno",
"ruleId": "KJ_ENGINE_NOT_CONFIGURED",
"severity": "info",
"result": "skipped",
"message": (
"kyverno-json engine not configured — `which kj` returned no path. "
"Install via scripts/install-kyverno-json.sh. The platform proceeds "
"with a neutral SKIPPED policy input (is_configured() guard, D-120)."
),
"evidence": {},
"resourceRef": "",
}
def _error_pcr(contract_id: str, message: str) -> dict:
return {
"contractId": contract_id,
"evaluatedAt": _iso8601_now(),
"engine": "kyverno",
"ruleId": "KJ_ENGINE_ERROR",
"severity": "info",
"result": "error",
"message": message,
"evidence": {},
"resourceRef": "",
}
class KyvernoJsonEngine:
"""``PolicyEngine`` impl that shells to the ``kj`` CLI."""
name = "kyverno-json"
def is_configured(self) -> bool:
return _which_kj() is not None
def evaluate(self, payload: Payload, policy_dir: Path,
contract_id: str) -> list[dict]:
if not self.is_configured():
return [_skipped_not_configured(contract_id)]
kj = _which_kj()
policy_dir = Path(policy_dir)
if not policy_dir.is_dir():
return [_error_pcr(
contract_id,
f"kyverno-json policy dir not found: {policy_dir}",
)]
severities = _load_policy_severities(policy_dir)
# Write payload to temp file (kj scan --payload expects a file path).
payload_tmp = tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False, encoding="utf-8"
)
try:
json.dump(payload, payload_tmp)
payload_tmp.flush()
payload_tmp.close()
cmd = [
kj, "scan",
"--policy", str(policy_dir),
"--payload", payload_tmp.name,
"--output", "json",
]
try:
proc = subprocess.run(
cmd, capture_output=True, text=True, timeout=60,
)
except subprocess.TimeoutExpired:
return [_error_pcr(contract_id, "kyverno-json scan timed out (60s)")]
if proc.returncode not in (0, 1):
return [_error_pcr(
contract_id,
f"kyverno-json scan exited {proc.returncode}: {proc.stderr[:200]}",
)]
try:
out = json.loads(proc.stdout) if proc.stdout.strip() else {}
except json.JSONDecodeError as e:
return [_error_pcr(
contract_id,
f"kyverno-json output not JSON: {e}",
)]
return self._translate(out, contract_id, severities)
finally:
try:
os.unlink(payload_tmp.name)
except OSError:
pass
def _translate(self, out: dict, contract_id: str,
severities: dict[str, str]) -> list[dict]:
results = out.get("results", []) if isinstance(out, dict) else []
if not isinstance(results, list):
results = []
pcrs: list[dict] = []
for entry in results:
if not isinstance(entry, dict):
continue
policy_name = entry.get("policy", "") or "UNKNOWN"
severity = severities.get(policy_name, SEVERITY_DEFAULT)
pcrs.append(_to_pcr(entry, contract_id, severity))
if not pcrs:
# No results — kyverno-json produced nothing (no match, or
# all policies passed with no result entries). Emit a
# single pass PCR so the confidence signal's policy input
# is non-empty (a non-empty list of passes → score 1.0).
pcrs.append({
"contractId": contract_id,
"evaluatedAt": _iso8601_now(),
"engine": "kyverno",
"ruleId": "KJ_NO_RESULTS",
"severity": "info",
"result": "pass",
"message": "kyverno-json scan produced no result entries (all policies passed or no match).",
"evidence": {},
"resourceRef": "",
})
return pcrs
if __name__ == "__main__":
if len(sys.argv) < 4:
print(
"usage: kyverno_json_engine.py <payload.json> <policy_dir> <contract-id>",
file=sys.stderr,
)
sys.exit(2)
with open(sys.argv[1], "r", encoding="utf-8") as fh:
pl = json.load(fh)
engine = KyvernoJsonEngine()
out = engine.evaluate(pl, Path(sys.argv[2]), sys.argv[3])
print(json.dumps(out, indent=2))
@@ -1,30 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "require-contract-id",
"annotations": {
"nova.cloudinit.dev/severity": "high",
"title.policy.kyverno.io": "Require contract id"
}
},
"spec": {
"rules": [
{
"name": "require-id",
"validate": {
"message": "contract id is required",
"assert": {
"all": [
{
"check": {
"id": "{{ to_string(@) }}"
}
}
]
}
}
}
]
}
}
@@ -1,31 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "forbid-unknown-fields",
"annotations": {
"nova.cloudinit.dev/severity": "low",
"title.policy.kyverno.io": "Contract has only schema-allowed fields"
}
},
"spec": {
"rules": [
{
"name": "no-unknown-fields",
"validate": {
"message": "contract may only contain id, name, environment, infrastructure (schema-allowed fields)",
"assert": {
"all": [
{
"check": {
"(length(keys(@)) == `4`)": true,
"keys(@)": "(contains(['id','name','environment','infrastructure'], @))"
}
}
]
}
}
}
]
}
}
@@ -1,30 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "require-env-in-enum",
"annotations": {
"nova.cloudinit.dev/severity": "high",
"title.policy.kyverno.io": "Contract environment is one of dev/qa/prod/dr"
}
},
"spec": {
"rules": [
{
"name": "env-enum",
"validate": {
"message": "contract.environment must be one of dev, qa, prod, dr",
"assert": {
"all": [
{
"check": {
"environment": "(contains(['dev','qa','prod','dr'], @))"
}
}
]
}
}
}
]
}
}
@@ -1,30 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "require-id-pattern",
"annotations": {
"nova.cloudinit.dev/severity": "high",
"title.policy.kyverno.io": "Contract id matches operational acronym pattern"
}
},
"spec": {
"rules": [
{
"name": "id-pattern",
"validate": {
"message": "contract.id must match ^[a-z][a-z0-9-]{2,5}$ (3-6 char operational acronym)",
"assert": {
"all": [
{
"check": {
"id": "(regex_match('^[a-z][a-z0-9-]{2,5}$', @))"
}
}
]
}
}
}
]
}
}
@@ -1,30 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "require-infrastructure-min-1",
"annotations": {
"nova.cloudinit.dev/severity": "medium",
"title.policy.kyverno.io": "Contract declares at least one infrastructure entry"
}
},
"spec": {
"rules": [
{
"name": "infra-min-1",
"validate": {
"message": "contract.infrastructure must have at least one module entry",
"assert": {
"all": [
{
"check": {
"infrastructure": "(length(keys(@)) > `0`)"
}
}
]
}
}
}
]
}
}
@@ -1,32 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "block-on-any-critical",
"annotations": {
"nova.cloudinit.dev/severity": "critical",
"title.policy.kyverno.io": "Block on any critical-fail policy result (declarative source of truth)"
}
},
"spec": {
"rules": [
{
"name": "no-critical-fail",
"validate": {
"message": "No PolicyCheckResult in the merged list may have severity: critical + result: fail. The confidence_signal.py hard-override is the defense-in-depth behind this declarative rule (D-119).",
"assert": {
"all": [
{
"check": {
"~.[]": {
"(severity == 'critical' && result == 'fail')": false
}
}
}
]
}
}
}
]
}
}
@@ -1,41 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "tagging-rules-agree",
"annotations": {
"nova.cloudinit.dev/severity": "medium",
"title.policy.kyverno.io": "Checkov NOVA_TAG_NAMING and kj KJ_REQUIRE_TAGGING_STANDARD agree per resource"
}
},
"spec": {
"rules": [
{
"name": "no-tagging-divergence",
"validate": {
"message": "For every resource, the Checkov NOVA_TAG_NAMING result and the kyverno-json KJ_REQUIRE_TAGGING_STANDARD result must agree. Divergence emits an error PCR (D-118, defense-in-depth against rule drift).",
"assert": {
"all": [
{
"check": {
"~.[?(ruleId == 'NOVA_TAG_NAMING')]": {
"result->ckv_result": {},
"($ckv_result == 'fail')": false
}
}
},
{
"check": {
"~.[?(ruleId == 'KJ_REQUIRE_TAGGING_STANDARD')]": {
"result->kj_result": {},
"($kj_result == 'fail')": false
}
}
}
]
}
}
}
]
}
}
@@ -1,49 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "forbid-iam-wildcard",
"annotations": {
"nova.cloudinit.dev/severity": "high",
"title.policy.kyverno.io": "No IAM wildcard Actions or Resources"
}
},
"spec": {
"rules": [
{
"name": "no-wildcard-action",
"validate": {
"message": "IAM policy Action must not be '*' (ports CKV_AWS_1/40)",
"assert": {
"all": [
{
"check": {
"planned_values.root_module.~.resources": {
"(type == 'aws_iam_policy' && contains(values.policy_document.Statement[].Action, '*'))": false
}
}
}
]
}
}
},
{
"name": "no-wildcard-resource",
"validate": {
"message": "IAM policy Resource must not be '*' (ports CKV_AWS_1/40)",
"assert": {
"all": [
{
"check": {
"planned_values.root_module.~.resources": {
"(type == 'aws_iam_policy' && contains(values.policy_document.Statement[].Resource, '*'))": false
}
}
}
]
}
}
}
]
}
}
@@ -1,32 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "forbid-plaintext-secrets",
"annotations": {
"nova.cloudinit.dev/severity": "high",
"title.policy.kyverno.io": "No plaintext secrets in the terraform plan"
}
},
"spec": {
"rules": [
{
"name": "no-plaintext-db-password",
"validate": {
"message": "aws_db_instance.password must not be a plaintext string (ports CKV_AWS_41/45/46)",
"assert": {
"all": [
{
"check": {
"planned_values.root_module.~.resources": {
"(type == 'aws_db_instance' && contains(keys(values), 'password') && !contains(['${...}', ''], values.password))": false
}
}
}
]
}
}
}
]
}
}
@@ -1,32 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "require-kms-reference",
"annotations": {
"nova.cloudinit.dev/severity": "medium",
"title.policy.kyverno.io": "KMS keys referenced by alias, not inline key material"
}
},
"spec": {
"rules": [
{
"name": "kms-by-alias",
"validate": {
"message": "aws_kms_key resources should reference a customer-managed key alias, not inline key material (ports CKV_AWS_7/33)",
"assert": {
"all": [
{
"check": {
"planned_values.root_module.~.resources": {
"(type == 'aws_kms_key' && !contains(keys(values), 'key_id') && !contains(keys(values), 'kms_key_id'))": false
}
}
}
]
}
}
}
]
}
}
@@ -1,30 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "cap-013-adapter-dedup",
"annotations": {
"nova.cloudinit.dev/severity": "medium",
"title.policy.kyverno.io": "No duplicate adapter registrations (CAP-013 declarative mirror)"
}
},
"spec": {
"rules": [
{
"name": "no-duplicate-adapters",
"validate": {
"message": "Each adapter must be registered exactly once (no duplicate adapter names in the capability inventory). Declarative mirror of core/regression_verify.py CAP-013.",
"assert": {
"all": [
{
"check": {
"adapters": "(length(duplicates(@)) == `0`)"
}
}
]
}
}
}
]
}
}
@@ -1,32 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "cap-023-metrics-collector",
"annotations": {
"nova.cloudinit.dev/severity": "medium",
"title.policy.kyverno.io": "Every metric has a grounded/derived/deferred status (CAP-023 declarative mirror)"
}
},
"spec": {
"rules": [
{
"name": "every-metric-has-status",
"validate": {
"message": "Every metric in docs/METRICS.md must declare a status (grounded, derived, or deferred). Declarative mirror of core/regression_verify.py CAP-023.",
"assert": {
"all": [
{
"check": {
"~.metrics": {
"(contains(['grounded','derived','deferred'], status))": true
}
}
}
]
}
}
}
]
}
}
@@ -1,35 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "cap-024-deck-structure",
"annotations": {
"nova.cloudinit.dev/severity": "low",
"title.policy.kyverno.io": "Deck structure matches the documented 4-beat arc (CAP-024 declarative mirror)"
}
},
"spec": {
"rules": [
{
"name": "deck-has-4-beats",
"validate": {
"message": "The deck must have the 4-beat arc: Problem, Solution, Proof, Roadmap+Ask. Declarative mirror of core/regression_verify.py CAP-024.",
"assert": {
"all": [
{
"check": {
"deck.beats": "(length(@) >= `4`)"
}
},
{
"check": {
"deck.beats": "(contains(@, 'Problem') && contains(@, 'Solution') && contains(@, 'Proof') && contains(@, 'Roadmap+Ask'))"
}
}
]
}
}
}
]
}
}
@@ -1,33 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "forbid-public-ingress",
"annotations": {
"nova.cloudinit.dev/severity": "high",
"title.policy.kyverno.io": "No resource has public ingress enabled"
}
},
"spec": {
"rules": [
{
"name": "no-public-ingress",
"identifier": "id",
"validate": {
"message": "public_ingress: true is not allowed on any resource (v1.0 demo rule, now declarative)",
"assert": {
"all": [
{
"check": {
"~.resources": {
"(inputs.public_ingress || `false`)": false
}
}
}
]
}
}
}
]
}
}
@@ -1,57 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "require-encryption-by-default",
"annotations": {
"nova.cloudinit.dev/severity": "high",
"title.policy.kyverno.io": "S3 buckets and EBS volumes carry encryption config"
}
},
"spec": {
"rules": [
{
"name": "s3-encryption",
"identifier": "id",
"match": {
"any": [
{"type": "aws:s3:bucket"}
]
},
"validate": {
"message": "S3 buckets must declare encryption config (inputs.bucket_encryption or inputs.kms_key_id)",
"assert": {
"all": [
{
"check": {
"(contains(keys(inputs), 'bucket_encryption') || contains(keys(inputs), 'kms_key_id'))": true
}
}
]
}
}
},
{
"name": "ebs-encryption",
"identifier": "id",
"match": {
"any": [
{"type": "aws:ebs:volume"}
]
},
"validate": {
"message": "EBS volumes must declare encryption (inputs.encrypted or inputs.kms_key_id)",
"assert": {
"all": [
{
"check": {
"(contains(keys(inputs), 'encrypted') || contains(keys(inputs), 'kms_key_id'))": true
}
}
]
}
}
}
]
}
}
@@ -1,36 +0,0 @@
{
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "require-tagging-standard",
"annotations": {
"nova.cloudinit.dev/severity": "medium",
"title.policy.kyverno.io": "All resources carry required Nova tags"
}
},
"spec": {
"rules": [
{
"name": "require-nova-tags",
"identifier": "id",
"validate": {
"message": "Every taggable resource must carry nova:owner, nova:contract, nova:environment, nova:cost-center tags",
"assert": {
"all": [
{
"check": {
"~.resources": {
"(contains(keys(tags || `[]`), 'nova:owner'))": true,
"(contains(keys(tags || `[]`), 'nova:contract'))": true,
"(contains(keys(tags || `[]`), 'nova:environment'))": true,
"(contains(keys(tags || `[]`), 'nova:cost-center'))": true
}
}
}
]
}
}
}
]
}
}
+4 -4
View File
@@ -1,7 +1,7 @@
# Kyverno Adapter # Kyverno Adapter
The Kyverno adapter translates Kyverno `PolicyReport` results to the The Kyverno adapter translates Kyverno `PolicyReport` results to the
normalized Nova normalized ACDL
[`PolicyCheckResult`](../../schemas/policy_check_result.schema.json) schema [`PolicyCheckResult`](../../schemas/policy_check_result.schema.json) schema
(engine: `"kyverno"`), mirroring the Checkov/Wiz adapter pattern. (engine: `"kyverno"`), mirroring the Checkov/Wiz adapter pattern.
@@ -15,7 +15,7 @@ publishes results to `PolicyReport` resources.
## When to use it ## When to use it
Kyverno is the right engine **when the platform emits Kubernetes Kyverno is the right engine **when the platform emits Kubernetes
manifests** (a K8s-native stack). The Nova platform today emits Terraform manifests** (a K8s-native stack). The ACDL platform today emits Terraform
only (D-053), so this adapter is **ready but inactive**: it ships now so only (D-053), so this adapter is **ready but inactive**: it ships now so
the schema path, severity/result mapping and sample policies are in place the schema path, severity/result mapping and sample policies are in place
ahead of the GitOps reconciler that will emit K8s manifests (roadmap). ahead of the GitOps reconciler that will emit K8s manifests (roadmap).
@@ -55,8 +55,8 @@ manifests (documentation-only today — the platform does not run them):
- `disallow-privileged-containers.yml` — fail pods with - `disallow-privileged-containers.yml` — fail pods with
`securityContext.privileged: true`. `securityContext.privileged: true`.
- `require-resource-labels.yml` — require `nova:owner` and - `require-resource-labels.yml` — require `acdl:owner` and
`nova:environment` labels on all pods (mirrors the Nova tagging standard `acdl:environment` labels on all pods (mirrors the ACDL tagging standard
in [`schemas/tagging-standard.json`](../../schemas/tagging-standard.json)). in [`schemas/tagging-standard.json`](../../schemas/tagging-standard.json)).
- `require-image-digests.yml` — require container images to reference a - `require-image-digests.yml` — require container images to reference a
digest (`image@sha256:...`), not a mutable tag. digest (`image@sha256:...`), not a mutable tag.
+1 -1
View File
@@ -1,4 +1,4 @@
"""Kyverno adapter — translate Kyverno PolicyReport results to Nova PolicyCheckResult records. """Kyverno adapter — translate Kyverno PolicyReport results to ACDL PolicyCheckResult records.
Kyverno is a Kubernetes-native policy engine. It evaluates K8s manifests Kyverno is a Kubernetes-native policy engine. It evaluates K8s manifests
and produces PolicyReport resources. This adapter translates those results and produces PolicyReport resources. This adapter translates those results
@@ -3,7 +3,7 @@ kind: ClusterPolicy
metadata: metadata:
name: require-resource-labels name: require-resource-labels
annotations: annotations:
policies.kyverno.io/title: Require Nova Resource Labels policies.kyverno.io/title: Require ACDL Resource Labels
policies.kyverno.io/category: Governance policies.kyverno.io/category: Governance
policies.kyverno.io/severity: medium policies.kyverno.io/severity: medium
policies.kyverno.io/subject: Pod policies.kyverno.io/subject: Pod
@@ -11,27 +11,27 @@ spec:
validationFailureAction: audit validationFailureAction: audit
background: true background: true
rules: rules:
- name: require-nova-owner-label - name: require-acdl-owner-label
match: match:
any: any:
- resources: - resources:
kinds: kinds:
- Pod - Pod
validate: validate:
message: "Pods must carry the nova:owner label (Nova tagging standard)." message: "Pods must carry the acdl:owner label (ACDL tagging standard)."
pattern: pattern:
metadata: metadata:
labels: labels:
nova:owner: "?*" acdl:owner: "?*"
- name: require-nova-environment-label - name: require-acdl-environment-label
match: match:
any: any:
- resources: - resources:
kinds: kinds:
- Pod - Pod
validate: validate:
message: "Pods must carry the nova:environment label (Nova tagging standard)." message: "Pods must carry the acdl:environment label (ACDL tagging standard)."
pattern: pattern:
metadata: metadata:
labels: labels:
nova:environment: "?*" acdl:environment: "?*"
+7 -11
View File
@@ -1,4 +1,4 @@
"""Nova Terraform adapter — stateless assembler (v1.11 RESTART, P56a). """ACDL Terraform adapter — stateless assembler (v1.11 RESTART, P56a).
A STATELESS ASSEMBLER. It owns no module content no resource shape, no A STATELESS ASSEMBLER. It owns no module content no resource shape, no
nested HCL blocks, no defaults, no type-specific logic. It reads the nested HCL blocks, no defaults, no type-specific logic. It reads the
@@ -10,10 +10,9 @@ lives in the per-module terraform/ subdir, NOT in this file.
CLI: adapter.py <instance.json> <out_dir> CLI: adapter.py <instance.json> <out_dir>
""" """
import json, os, sys import json
_R = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import os
sys.path.insert(0, _R) if _R not in sys.path else None import sys
from core import env
def _load_registry(repo_root): def _load_registry(repo_root):
@@ -113,11 +112,8 @@ def adapt(stack_instance, out_dir):
stack_name = stack.get("name", "spike") stack_name = stack.get("name", "spike")
environment = stack.get("environment", "dev") environment = stack.get("environment", "dev")
account_id = env.get_env("AWS_ACCOUNT_ID", "581513795199") account_id = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
state_bucket = f"nova-tfstate-{account_id}-us-east-1" state_bucket = f"acdl-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_tf = (
'terraform {\n' 'terraform {\n'
' required_version = ">= 1.9, < 1.10"\n' ' required_version = ">= 1.9, < 1.10"\n'
@@ -138,7 +134,7 @@ def adapt(stack_instance, out_dir):
data_source_names = stack_instance.get("data_sources", []) data_source_names = stack_instance.get("data_sources", [])
parts = [] parts = []
if data_source_names: if data_source_names:
remote_state_key = env.get_env("REMOTE_STATE_KEY", "platform/terraform.tfstate") remote_state_key = os.environ.get("ACDL_REMOTE_STATE_KEY", "platform/terraform.tfstate")
parts.append( parts.append(
'data "terraform_remote_state" "platform" {\n' 'data "terraform_remote_state" "platform" {\n'
' backend = "s3"\n' ' backend = "s3"\n'
+10 -38
View File
@@ -1,4 +1,4 @@
"""Translate Checkov JSON output to Nova PolicyCheckResult records. """Translate Checkov JSON output to ACDL PolicyCheckResult records.
Reads Checkov's JSON output (one framework key, e.g. terraform_plan), Reads Checkov's JSON output (one framework key, e.g. terraform_plan),
emits a list of PolicyCheckResult dicts conforming to emits a list of PolicyCheckResult dicts conforming to
@@ -6,23 +6,16 @@ schemas/policy_check_result.schema.json. Run Checkov with --soft-fail so
Checkov never exits non-zero; the confidence signal decides the gate, not Checkov never exits non-zero; the confidence signal decides the gate, not
Checkov's exit code. Checkov's exit code.
The Nova tagging standard (D-054, D-043 closure, D-109 hard mode in P3) The ACDL tagging standard (D-054, D-043 closure) is enforced by a custom
is enforced by a custom Checkov rule at Checkov rule at adapters/terraform/policy/custom_rules/acdl_tagging.py,
adapters/terraform/policy/custom_rules/nova_tagging.py, loaded via loaded via --external-checks-dir. The adapter therefore maps
--external-checks-dir. The adapter therefore maps NOVA_TAG_NAMING as a ACDL_TAG_NAMING as a real rule (no synthetic SKIPPED record is emitted).
real rule (no synthetic SKIPPED record is emitted). Renamed from
ACDL_TAG_NAMING in P2 (REQ-158); the rule is in hard mode as of P3
(REQ-162: hard-fail on missing nova:* or acdl:*-only tags).
""" """
import datetime import datetime
import json import json
import os
import sys import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
from core.metrics.event_envelope import emit
RULE_MAP = { RULE_MAP = {
"CKV_AWS_41": ("secrets-in-plaintext", "high"), "CKV_AWS_41": ("secrets-in-plaintext", "high"),
@@ -36,12 +29,10 @@ RULE_MAP = {
"CKV_AWS_40": ("iam-wildcard", "medium"), "CKV_AWS_40": ("iam-wildcard", "medium"),
"CKV_AWS_7": ("kms-key-reference", "medium"), "CKV_AWS_7": ("kms-key-reference", "medium"),
"CKV_AWS_33": ("kms-key-reference", "medium"), "CKV_AWS_33": ("kms-key-reference", "medium"),
# D-054 / D-043 closure, D-109 hard mode (P3): NOVA_TAG_NAMING is a real # D-054 / D-043 closure: ACDL_TAG_NAMING is now a real custom Checkov
# custom Checkov rule (adapters/terraform/policy/custom_rules/nova_tagging.py), # rule (adapters/terraform/policy/custom_rules/acdl_tagging.py), loaded
# loaded via --external-checks-dir. No synthetic SKIPPED record is emitted. # via --external-checks-dir. No synthetic SKIPPED record is emitted.
# Renamed from ACDL_TAG_NAMING in P2 (REQ-158). Hard mode as of P3 "ACDL_TAG_NAMING": ("tagging-standard", "medium"),
# (REQ-162: hard-fail on missing nova:* or acdl:*-only tags).
"NOVA_TAG_NAMING": ("tagging-standard", "medium"),
} }
_RESULT_MAP = {"PASSED": "pass", "FAILED": "fail", "SKIPPED": "skipped"} _RESULT_MAP = {"PASSED": "pass", "FAILED": "fail", "SKIPPED": "skipped"}
@@ -75,7 +66,7 @@ def _to_pcr(checkov_record, contract_id, result_str):
} }
def adapt(checkov_json_path, contract_id, run_id=None, environment="dev"): def adapt(checkov_json_path, contract_id):
with open(checkov_json_path, "r", encoding="utf-8") as fh: with open(checkov_json_path, "r", encoding="utf-8") as fh:
data = json.load(fh) data = json.load(fh)
out = [] out = []
@@ -89,25 +80,6 @@ def adapt(checkov_json_path, contract_id, run_id=None, environment="dev"):
out.append(_to_pcr(rec, contract_id, "FAILED")) out.append(_to_pcr(rec, contract_id, "FAILED"))
for rec in results.get("skipped_checks", []): for rec in results.get("skipped_checks", []):
out.append(_to_pcr(rec, contract_id, "SKIPPED")) out.append(_to_pcr(rec, contract_id, "SKIPPED"))
# Emit nova.policy.evaluated event (REQ-187).
if run_id:
passed = sum(1 for p in out if p["result"] == "pass")
failed = sum(1 for p in out if p["result"] == "fail")
skipped = sum(1 for p in out if p["result"] == "skipped")
severity_breakdown = {}
for p in out:
sev = p.get("severity", "info")
severity_breakdown[sev] = severity_breakdown.get(sev, 0) + 1
try:
emit("nova.policy.evaluated", run_id, environment, {
"passed": passed, "failed": failed, "skipped": skipped,
"severity_breakdown": severity_breakdown,
"rule_count": len(out),
}, contract_id=contract_id)
except Exception:
pass # metrics emission must never break the policy adapter
return out return out
@@ -1,24 +1,16 @@
# Nova Custom Checkov Rules # ACDL Custom Checkov Rules
This directory holds Nova-authored Checkov custom rules, written in the This directory holds ACDL-authored Checkov custom rules, written in the
[Checkov Python custom-rule framework](https://www.checkov.io/4.Contributing/Custom%20Policies.html). [Checkov Python custom-rule framework](https://www.checkov.io/4.Contributing/Custom%20Policies.html).
## Files ## Files
- `nova_tagging.py``NOVA_TAG_NAMING` (D-054, D-109 warn mode in P2): - `acdl_tagging.py``ACDL_TAG_NAMING` (D-054): ensures every taggable AWS
ensures every taggable AWS resource carries the four required Nova tags resource carries the four required ACDL tags
(`nova:owner`, `nova:contract`, `nova:environment`, `nova:cost-center`). (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`).
This rule replaces the synthetic SKIPPED `NOVA_TAG_NAMING` record that the This rule replaces the synthetic SKIPPED `ACDL_TAG_NAMING` record that the
Checkov adapter previously emitted (D-043 closure). Renamed from Checkov adapter previously emitted (D-043 closure). The canonical tag set
`acdl_tagging.py` / `ACDL_TAG_NAMING` in P2 (REQ-158). The canonical tag is declared in [`schemas/tagging-standard.json`](../../../schemas/tagging-standard.json).
set is declared in [`schemas/tagging-standard.json`](../../../schemas/tagging-standard.json).
**P2 warn mode (D-109):** existing resources still carry `acdl:*` tag-key
values (left for P3). When a resource has only `acdl:*`-style tags and no
`nova:*` tags, the rule logs a WARNING instead of failing, so the
regression gate stays green during the parallel-tag transition window.
P3 flips to hard-fail once `nova:*` tags are emitted in parallel and the
ABAC policy is swapped.
## How Checkov loads them ## How Checkov loads them
@@ -31,12 +23,12 @@ checkov -f terraform/spike/main.tf --framework terraform -o json --soft-fail \
``` ```
Checkov imports each `*.py` file in the directory and instantiates the Checkov imports each `*.py` file in the directory and instantiates the
module-level `check` object (see the `check = NovaTaggingStandard()` line at module-level `check` object (see the `check = AcdlTaggingStandard()` line at
the bottom of `nova_tagging.py`). the bottom of `acdl_tagging.py`).
## Severity / result mapping ## Severity / result mapping
The Checkov adapter (`adapters/terraform/policy/checkov_adapter.py`) The Checkov adapter (`adapters/terraform/policy/checkov_adapter.py`)
maps `NOVA_TAG_NAMING` to `(tagging-standard, medium)` in `RULE_MAP`. The maps `ACDL_TAG_NAMING` to `(tagging-standard, medium)` in `RULE_MAP`. The
custom rule therefore produces real `PASS`/`FAIL` PolicyCheckResult records, custom rule therefore produces real `PASS`/`FAIL` PolicyCheckResult records,
feeding the confidence signal instead of the old SKIPPED placeholder. feeding the confidence signal instead of the old SKIPPED placeholder.
@@ -0,0 +1,54 @@
"""ACDL tagging standard custom Checkov rule (D-054).
Checks that all taggable AWS resources have the required ACDL tags:
acdl:owner, acdl:contract, acdl:environment, acdl:cost-center
Fails (severity medium) when any required tag is missing.
Closes the D-043 deferral (the SKIPPED ACDL_TAG_NAMING placeholder
becomes a real check).
"""
from __future__ import annotations
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories
REQUIRED_TAGS = ("acdl:owner", "acdl:contract", "acdl:environment", "acdl:cost-center")
# Resources that support tags (exclude resources that have no tags attribute)
NON_TAGGABLE_TYPES = (
"aws_cloudfront_origin_access_control",
"aws_lambda_function_url",
"aws_route_table_association",
"aws_internet_gateway",
)
class AcdlTaggingStandard(BaseResourceCheck):
def __init__(self):
name = "Ensure all taggable AWS resources have required ACDL tags"
check_id = "ACDL_TAG_NAMING"
supported_resources = ["*"] # all resources
categories = [CheckCategories.GENERAL_SECURITY]
super().__init__(name=name, check_id=check_id, categories=categories, supported_resources=supported_resources)
def scan_resource_conf(self, conf, entity_type):
# Skip non-taggable resources
if entity_type in NON_TAGGABLE_TYPES:
return CheckResult.PASSED
# Check for a tags block
tags = conf.get("tags")
if not tags:
return CheckResult.FAILED
tag_keys = set()
if isinstance(tags, list) and tags:
tag_block = tags[0]
if isinstance(tag_block, dict):
tag_keys = set(tag_block.keys())
elif isinstance(tags, dict):
tag_keys = set(tags.keys())
missing = [t for t in REQUIRED_TAGS if t not in tag_keys]
if missing:
return CheckResult.FAILED
return CheckResult.PASSED
check = AcdlTaggingStandard()
@@ -1,82 +0,0 @@
"""Nova tagging standard custom Checkov rule (D-054, D-109 hard mode).
Checks that all taggable AWS resources have the required Nova tags:
nova:owner, nova:contract, nova:environment, nova:cost-center
In **hard mode** (P3, REQ-162): the rule hard-fails when a taggable resource
is missing any required `nova:*` tag, OR when a resource carries only the
legacy `acdl:*` tag keys (and no `nova:*` keys). P2 shipped warn mode
(`_WARN_MODE = True`) so the regression gate stayed green during the
parallel-tag transition window; P3 flips to hard-fail (`_WARN_MODE = False`)
once `nova:*` tags are emitted in terraform and the ABAC policy is swapped
to match `nova:*`. P5 keeps hard mode and additionally hard-fails on any
`acdl:*` tag key present at all (no legacy tolerated post-cutoff).
Closes the D-043 deferral (the SKIPPED NOVA_TAG_NAMING placeholder
becomes a real check). Renamed from acdl_tagging.py in P2 (REQ-158);
the Checkov rule ID ACDL_TAG_NAMING NOVA_TAG_NAMING.
"""
from __future__ import annotations
import sys
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories
REQUIRED_TAGS = ("nova:owner", "nova:contract", "nova:environment", "nova:cost-center")
# Legacy acdl:* tag keys — the parallel-tag period (P3) emits both nova:*
# and acdl:*; P2 warn mode treats acdl:*-only tags as a warning, not a
# failure. The acdl:* VALUES in tagging-standard.json are left for P3.
LEGACY_TAGS = ("acdl:owner", "acdl:contract", "acdl:environment", "acdl:cost-center")
# Resources that support tags (exclude resources that have no tags attribute)
NON_TAGGABLE_TYPES = (
"aws_cloudfront_origin_access_control",
"aws_lambda_function_url",
"aws_route_table_association",
"aws_internet_gateway",
)
# 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
class NovaTaggingStandard(BaseResourceCheck):
def __init__(self):
name = "Ensure all taggable AWS resources have required Nova tags"
check_id = "NOVA_TAG_NAMING"
supported_resources = ["*"] # all resources
categories = [CheckCategories.GENERAL_SECURITY]
super().__init__(name=name, check_id=check_id, categories=categories, supported_resources=supported_resources)
def scan_resource_conf(self, conf, entity_type):
# Skip non-taggable resources
if entity_type in NON_TAGGABLE_TYPES:
return CheckResult.PASSED
# Check for a tags block
tags = conf.get("tags")
if not tags:
return CheckResult.FAILED
tag_keys = set()
if isinstance(tags, list) and tags:
tag_block = tags[0]
if isinstance(tag_block, dict):
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
return CheckResult.FAILED
check = NovaTaggingStandard()
+5 -34
View File
@@ -1,4 +1,4 @@
"""Wiz adapter — translate Wiz API results to Nova PolicyCheckResult records. """Wiz adapter — translate Wiz API results to ACDL PolicyCheckResult records.
Wiz is a SaaS security platform with a GraphQL API. This adapter Wiz is a SaaS security platform with a GraphQL API. This adapter
translates Wiz issue records to the normalized PolicyCheckResult schema translates Wiz issue records to the normalized PolicyCheckResult schema
@@ -186,37 +186,8 @@ def is_configured():
return bool(os.environ.get("WIZ_API_TOKEN") and os.environ.get("WIZ_API_URL")) return bool(os.environ.get("WIZ_API_TOKEN") and os.environ.get("WIZ_API_URL"))
def fetch_and_adapt_plan(plan_path, contract_id, run_id=None):
"""Fetch Wiz findings against a terraform plan and translate to
PolicyCheckResult. REQ-250 (v1.21): Wiz scans the terraform plan
output. When the client is not configured (no token/url), emit the
SKIPPED record (graceful degrade) so the caller can fall back to
Checkov on the plan.
"""
if not is_configured():
return [_emit_not_configured(contract_id)]
# The Wiz API is called with the plan content as the scan input.
client = WizClient()
issues = client.fetch_issues()
if not issues:
return [_emit_not_configured(contract_id)]
return [_to_pcr(i, contract_id) for i in issues]
if __name__ == "__main__": if __name__ == "__main__":
import argparse if len(sys.argv) != 3:
parser = argparse.ArgumentParser(description="Wiz adapter (REQ-250: plan-mode supported)") print("usage: wiz_adapter.py <wiz_issues.json> <contract-id>", file=sys.stderr)
parser.add_argument("wiz_json", nargs="?", help="wiz_issues.json (legacy positional mode)") sys.exit(2)
parser.add_argument("contract_id_pos", nargs="?", help="contract-id (legacy positional mode)") print(json.dumps(adapt(sys.argv[1], sys.argv[2]), indent=2))
parser.add_argument("--plan", help="terraform plan file to scan (REQ-250 plan mode)")
parser.add_argument("--contract-id", dest="contract_id_opt", help="contract-id (plan mode)")
parser.add_argument("--run-id", help="run-id for the plan scan (plan mode)")
args = parser.parse_args()
if args.plan:
cid = args.contract_id_opt or ""
out = fetch_and_adapt_plan(args.plan, cid, run_id=args.run_id)
print(json.dumps(out, indent=2))
elif args.wiz_json and args.contract_id_pos:
print(json.dumps(adapt(args.wiz_json, args.contract_id_pos), indent=2))
else:
parser.error("either --plan <file> --contract-id <id> OR <wiz_issues.json> <contract-id>")
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova sample consumer contract — microservice module (dev) # ACDL sample consumer contract — microservice module (dev)
# Per-environment contract (REQ-105). Promotion = running the dev job; # Per-environment contract (REQ-105). Promotion = running the dev job;
# no environment field editing. Interpolation resolves against dev.json. # no environment field editing. Interpolation resolves against dev.json.
id: msvc id: msvc
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova sample consumer contract — microservice module (dr) # ACDL sample consumer contract — microservice module (dr)
# Per-environment contract (REQ-105). Promotion = running the dr job; # Per-environment contract (REQ-105). Promotion = running the dr job;
# no environment field editing. Interpolation resolves against dr.json. # no environment field editing. Interpolation resolves against dr.json.
id: msvc id: msvc
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova sample consumer contract — microservice module (prod) # ACDL sample consumer contract — microservice module (prod)
# Per-environment contract (REQ-105). Promotion = running the prod job; # Per-environment contract (REQ-105). Promotion = running the prod job;
# no environment field editing. Interpolation resolves against prod.json. # no environment field editing. Interpolation resolves against prod.json.
id: msvc id: msvc
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova sample consumer contract — microservice module (qa) # ACDL sample consumer contract — microservice module (qa)
# Per-environment contract (REQ-105). Promotion = running the qa job; # Per-environment contract (REQ-105). Promotion = running the qa job;
# no environment field editing. Interpolation resolves against qa.json. # no environment field editing. Interpolation resolves against qa.json.
id: msvc id: msvc
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova sample consumer contract — microservice module (dev) # ACDL sample consumer contract — microservice module (dev)
# #
# Reference example for an ECS Fargate microservice deployment. # Reference example for an ECS Fargate microservice deployment.
# Interpolation (D-081): bucket_name uses the naming pattern that includes # Interpolation (D-081): bucket_name uses the naming pattern that includes
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova sample consumer contract — static-assets module (dev) # ACDL sample consumer contract — static-assets module (dev)
# Per-environment contract (REQ-105). The dev default # Per-environment contract (REQ-105). The dev default
# (contracts/static-assets.yml) remains for backwards compat; this file # (contracts/static-assets.yml) remains for backwards compat; this file
# is the explicit per-env dev contract. Interpolation resolves against dev.json. # is the explicit per-env dev contract. Interpolation resolves against dev.json.
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova sample consumer contract — static-assets module (dr) # ACDL sample consumer contract — static-assets module (dr)
# Per-environment contract (REQ-105). Promotion = running the dr job; # Per-environment contract (REQ-105). Promotion = running the dr job;
# no environment field editing. Interpolation resolves against dr.json. # no environment field editing. Interpolation resolves against dr.json.
id: assets id: assets
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova sample consumer contract — static-assets module (prod) # ACDL sample consumer contract — static-assets module (prod)
# Per-environment contract (REQ-105). Promotion = running the prod job; # Per-environment contract (REQ-105). Promotion = running the prod job;
# no environment field editing. Interpolation resolves against prod.json. # no environment field editing. Interpolation resolves against prod.json.
id: assets id: assets
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova sample consumer contract — static-assets module (qa) # ACDL sample consumer contract — static-assets module (qa)
# Per-environment contract (REQ-105). Promotion = running the qa job; # Per-environment contract (REQ-105). Promotion = running the qa job;
# no environment field editing. Interpolation resolves against qa.json. # no environment field editing. Interpolation resolves against qa.json.
id: assets id: assets
+1 -1
View File
@@ -1,4 +1,4 @@
# Nova sample consumer contract — static-assets module (dev) # ACDL sample consumer contract — static-assets module (dev)
# #
# This is the reference example for a consumer contract. It declares: # This is the reference example for a consumer contract. It declares:
# id: short operational acronym (becomes stack.name for state, tags, evidence) # id: short operational acronym (becomes stack.name for state, tags, evidence)
+4 -14
View File
@@ -14,8 +14,7 @@ concerns split into two tiers:
The operator-supplied evidence artifact is a JSON blob with `timestamp`, The operator-supplied evidence artifact is a JSON blob with `timestamp`,
`type`, `payload`, and an optional `signature` (JWS detached). Freshness `type`, `payload`, and an optional `signature` (JWS detached). Freshness
is validated against the window from §10.4. Signature verification runs is validated against the window from §10.4. Signature verification runs
when `NOVA_ATTESTATION_SIGNING_KEY_ID` is set (dual-read via core/env.py: when `ACDL_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged
NOVA_* preferred, ACDL_* fallback until P5); it is skipped + logged
when unset (dev/CI D-089). The matrix fails loud if an operator-supplied when unset (dev/CI D-089). The matrix fails loud if an operator-supplied
concern is missing or expired for prod/dr. concern is missing or expired for prod/dr.
""" """
@@ -25,14 +24,6 @@ import os
import sys import sys
from typing import Optional, Tuple from typing import Optional, Tuple
# Repo root on sys.path so `from core import env` resolves to THIS package
# when run as a script (avoids editable-installed third-party `core` shadow).
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from core import env
# Freshness windows (days) from hitl_matrix_design.md §10.4. # Freshness windows (days) from hitl_matrix_design.md §10.4.
FRESHNESS_DAYS = { FRESHNESS_DAYS = {
@@ -90,15 +81,14 @@ def _is_fresh(artifact: dict, concern: str) -> bool:
def _verify_signature(artifact: dict) -> bool: def _verify_signature(artifact: dict) -> bool:
"""Verify the JWS detached signature when NOVA_ATTESTATION_SIGNING_KEY_ID is set. """Verify the JWS detached signature when ACDL_ATTESTATION_SIGNING_KEY_ID is set.
When unset (dev/CI D-089), signature verification is skipped + logged. When unset (dev/CI D-089), signature verification is skipped + logged.
Dual-read via core/env.py: NOVA_* preferred, ACDL_* fallback until P5.
""" """
key_id = env.get_env("ATTESTATION_SIGNING_KEY_ID", "") or "" key_id = os.environ.get("ACDL_ATTESTATION_SIGNING_KEY_ID", "")
if not key_id: if not key_id:
sys.stderr.write( sys.stderr.write(
"[attestation] NOVA_ATTESTATION_SIGNING_KEY_ID unset — " "[attestation] ACDL_ATTESTATION_SIGNING_KEY_ID unset — "
"signature verification skipped (dev/CI, D-089)\n" "signature verification skipped (dev/CI, D-089)\n"
) )
return True return True
+3 -3
View File
@@ -62,7 +62,7 @@ path above remains the v1.9 production audit record.
**platform-level KMS key** (not per-contract — a per-contract key would **platform-level KMS key** (not per-contract — a per-contract key would
explode the key-management surface), rotated **quarterly**. The `jws` explode the key-management surface), rotated **quarterly**. The `jws`
field is added to the event shape when this ships. field is added to the event shape when this ships.
- **Async worker + DLQ:** a Lambda (or a forge Actions scheduled workflow) - **Async worker + DLQ:** a Lambda (or a Gitea Actions scheduled workflow)
reads the outbox, writes to S3 Object Lock, signs with KMS. DLQ = an reads the outbox, writes to S3 Object Lock, signs with KMS. DLQ = an
SQS dead-letter queue for failed writes. RTO = DLQ replay. SQS dead-letter queue for failed writes. RTO = DLQ replay.
- **Daily checkpoints (§9):** a daily job reads the last event hash and - **Daily checkpoints (§9):** a daily job reads the last event hash and
@@ -86,7 +86,7 @@ log" anti-goal requires.
D-083 ships). D-083 ships).
- `prev_event_hash` (chain link; `GENESIS` for the first event). - `prev_event_hash` (chain link; `GENESIS` for the first event).
- `hash` (this event's SHA-256 over canonical JSON). - `hash` (this event's SHA-256 over canonical JSON).
- `approver_qa` (CI username of the QA approver; populated on - `approver_qa` (Gitea/GitHub username of the QA approver; populated on
qa-promotion by v1.9's `hitl_gates.attest` — D-042). qa-promotion by v1.9's `hitl_gates.attest` — D-042).
- `approver_prod` (SRE username; populated on prod-promotion by v1.9's - `approver_prod` (SRE username; populated on prod-promotion by v1.9's
`hitl_gates.attest`). `hitl_gates.attest`).
@@ -112,7 +112,7 @@ log" anti-goal requires.
- **D-042** — approver identities (`approver_qa`, `approver_prod`, - **D-042** — approver identities (`approver_qa`, `approver_prod`,
`approver_dr`) live in the outbox; the separation-of-duties check `approver_dr`) live in the outbox; the separation-of-duties check
(`core/separation_of_duties.py`) reads `approver_qa` and compares (`core/separation_of_duties.py`) reads `approver_qa` and compares
to the prod-dispatch CI actor. v1.9's to the prod-dispatch `gitea.actor` / `github.actor`. v1.9's
`hitl_gates.attest` populates these attributes. `hitl_gates.attest` populates these attributes.
- **D-083** (v1.9) — S3 Object Lock + JWS + async worker + DLQ + daily - **D-083** (v1.9) — S3 Object Lock + JWS + async worker + DLQ + daily
checkpoints deferred to a future milestone. Requires non-offline- checkpoints deferred to a future milestone. Requires non-offline-
+2 -33
View File
@@ -1,4 +1,4 @@
"""Nova Confidence Signal (REQ-19). """ACDL Confidence Signal (REQ-19).
The platform's certified answer to "is this safe to proceed?" (vision The platform's certified answer to "is this safe to proceed?" (vision
tenet: "Safety is Computed, Not Assumed"). Every delivery action produces tenet: "Safety is Computed, Not Assumed"). Every delivery action produces
@@ -34,13 +34,8 @@ per-input scores.
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from typing import List, Literal, Optional, Dict, Any from typing import List, Literal, Optional, Dict, Any
import json import json
import os
import sys import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.metrics.event_envelope import emit, make_event, append_event
from core.metrics.decision_ledger import append as ledger_append
WEIGHTS = { WEIGHTS = {
"policy": 0.30, "policy": 0.30,
@@ -166,33 +161,7 @@ def compute(contract_id: str, environment: str,
band = "warn" band = "warn"
if environment == "dev" and band == "warn": if environment == "dev" and band == "warn":
band = "block" band = "block"
signal = Signal(score, band, per_input, reasons) return Signal(score, band, per_input, reasons)
# Emit nova.confidence.computed + nova.ai.decision.made events (D-122).
# The "AI decision" is the confidence-gated policy engine, not an LLM.
# decision_id = run_id (or "cli-<ts>" when called from CLI without a run).
try:
run_id = os.environ.get("NOVA_RUN_ID", f"cli-{int(__import__('time').time())}")
conf_data = {"score": score, "band": band, "perInput": per_input, "reasonCodes": reasons}
emit("nova.confidence.computed", run_id, environment, conf_data, contract_id=contract_id)
decision_data = {
"decision_id": run_id,
"chosen_action": band,
"confidence": score,
"alternatives": per_input,
"human_override": band == "block",
"threshold": THRESHOLDS[environment],
}
decision_event = make_event("nova.ai.decision.made", run_id, environment, decision_data,
contract_id=contract_id, actor_type="confidence-gate",
actor_id="confidence_signal")
append_event(decision_event)
ledger_append(decision_event)
except Exception:
pass # metrics emission must never break the confidence gate
return signal
if __name__ == "__main__": if __name__ == "__main__":
+62 -96
View File
@@ -1,4 +1,4 @@
"""Nova Contract Resolver — resolve a consumer contract to a Target Stack instance. """ACDL Contract Resolver — resolve a consumer contract to a Target Stack instance.
The contract resolver is the bridge between the consumer's declared intent The contract resolver is the bridge between the consumer's declared intent
(a contract YAML) and the platform's executable representation (a Target (a contract YAML) and the platform's executable representation (a Target
@@ -36,27 +36,26 @@ import sys
import yaml import yaml
import jsonschema import jsonschema
# Ensure the repo root (parent of core/) is on sys.path so `from core
# import env` resolves to THIS package when contract_resolver.py is run
# as a script (python3 core/contract_resolver.py) — otherwise an
# editable-installed third-party `core` package can shadow it.
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from core import env
def _load_env(env_name, repo_root): def _load_env(env_name, repo_root):
"""Load the environment onboarding JSON for env_name. """Load the environment onboarding JSON for env_name.
P7 (REQ-171): delegates to core.environment_check.load() (dedup Mirrors core.environment_check.load() but is self-contained so the
the two were verbatim duplicates). The environment_check module is resolver works both as a package import (`from core.contract_resolver
in the same core/ package, so the import works both as a package import resolve`) and as a script (`python3 core/contract_resolver.py`).
import and as a script (`python3 core/contract_resolver.py`). Emits a stderr warning when account_id is the placeholder and env != dev.
""" """
from core import environment_check env_file = os.path.join(repo_root, "core", "environments", f"{env_name}.json")
return environment_check.load(env_name, root=repo_root) if not os.path.isfile(env_file):
raise FileNotFoundError(f"no environment file for '{env_name}' at {env_file}")
env = _load_json(env_file)
if env.get("account_id") == "000000000000" and env_name != "dev":
sys.stderr.write(
f"WARNING: environment '{env_name}' has the placeholder account_id "
f"000000000000 — replace it with the real {env_name} account id "
f"before deploying (onboarding scaffold).\n"
)
return env
def _load_json(path): def _load_json(path):
@@ -64,21 +63,6 @@ def _load_json(path):
return json.load(fh) return json.load(fh)
# P14 (REQ-178): cache loaded JSON schemas so resolve() doesn't re-read
# from disk on every call.
_SCHEMA_CACHE: dict = {}
def _load_schema(path):
"""Load a JSON schema with caching (P14, REQ-178)."""
cached = _SCHEMA_CACHE.get(path)
if cached is not None:
return cached
schema = _load_json(path)
_SCHEMA_CACHE[path] = schema
return schema
def _load_yaml(path): def _load_yaml(path):
with open(path, "r") as fh: with open(path, "r") as fh:
return yaml.safe_load(fh) return yaml.safe_load(fh)
@@ -452,9 +436,24 @@ def _namespace_resources(resources, module_name):
def decommission_transform(stack_instance): def decommission_transform(stack_instance):
"""REQ-92: re-export from core.decommission_transform (P12, REQ-176).""" """REQ-92: Transform a resolved stack instance for decommission.
from core.decommission_transform import decommission_transform as _dt
return _dt(stack_instance) Sets all scalable counts to 0 and deletion_protection to false on
every resource. Used by the decommission pipeline mode after the
first step (disable deletion protection) has been applied.
"""
for res in stack_instance.get("resources", []):
if "nfrs" not in res:
res["nfrs"] = {}
res["nfrs"]["deletion_protection"] = False
inputs = res.get("inputs", {})
if "desired_count" in inputs:
inputs["desired_count"] = 0
if "min_capacity" in inputs:
inputs["min_capacity"] = 0
if "max_capacity" in inputs:
inputs["max_capacity"] = 0
return stack_instance
def resolve(contract_path, repo_root=None, environment_override=None): def resolve(contract_path, repo_root=None, environment_override=None):
@@ -462,7 +461,7 @@ def resolve(contract_path, repo_root=None, environment_override=None):
Args: Args:
contract_path: Path to the contract YAML file. contract_path: Path to the contract YAML file.
repo_root: Root of the Nova repo (defaults to two levels up from this file). repo_root: Root of the ACDL repo (defaults to two levels up from this file).
environment_override: When set (dev/qa/prod/dr), overrides the environment_override: When set (dev/qa/prod/dr), overrides the
contract's 'environment' field BEFORE schema validation, so contract's 'environment' field BEFORE schema validation, so
interpolation context is consistent (D-088). Used by interpolation context is consistent (D-088). Used by
@@ -483,30 +482,11 @@ def resolve(contract_path, repo_root=None, environment_override=None):
contract["environment"] = environment_override contract["environment"] = environment_override
# Load schemas # Load schemas
contract_schema = _load_schema(os.path.join(repo_root, "schemas", "contract.schema.json")) contract_schema = _load_json(os.path.join(repo_root, "schemas", "contract.schema.json"))
# Validate contract against schema # Validate contract against schema
jsonschema.validate(contract, contract_schema) jsonschema.validate(contract, contract_schema)
# v1.25 (REQ-296): pre-resolve policy evaluation — run the active
# PolicyEngine over the contract dict with the contract/ policy
# dir BEFORE resolving. Failures feed the `policyResults` on the
# stack instance (the confidence signal's `policy` input). The
# resolver does NOT exit on policy failure — the confidence signal
# decides the gate (consistent with the existing --soft-fail
# Checkov pattern).
contract_pcrs: list = []
try:
from core.policy_engine import get_engine, get_policy_root
_engine = get_engine()
_policy_root = get_policy_root()
contract_pcrs = _engine.evaluate(
contract, _policy_root / "contract", contract.get("id", "unknown")
)
except Exception:
# Policy evaluation must never break the resolver.
contract_pcrs = []
# Interpolation (D-081): expand ${env.<field>} + ${contract.<field>} # Interpolation (D-081): expand ${env.<field>} + ${contract.<field>}
# tokens AFTER schema validation (the schema sees raw tokens, which are # tokens AFTER schema validation (the schema sees raw tokens, which are
# valid strings) and BEFORE IR resolution (the resolver sees concrete # valid strings) and BEFORE IR resolution (the resolver sees concrete
@@ -544,14 +524,10 @@ def resolve(contract_path, repo_root=None, environment_override=None):
f"module '{module_name}' version '{version}' not found in registry") f"module '{module_name}' version '{version}' not found in registry")
module_inputs = module_entry.get("inputs", {}) module_inputs = module_entry.get("inputs", {})
# Determine if L1 or L2 — prefer the registry `kind` field (P7, # Determine if L1 or L2
# REQ-171); fall back to the path heuristic for entries that
# predate the kind field.
entry = registry[module_name][version] entry = registry[module_name][version]
interface_path = entry["interface"] interface_path = entry["interface"]
is_l2 = entry.get("kind") == "l2" or ( is_l2 = "l2" in interface_path or "composition" in interface_path
"kind" not in entry and ("l2" in interface_path or "composition" in interface_path)
)
if is_l2: if is_l2:
fragment = _resolve_l2(module_name, version, module_inputs, fragment = _resolve_l2(module_name, version, module_inputs,
@@ -594,8 +570,13 @@ def resolve(contract_path, repo_root=None, environment_override=None):
merged_outputs.update(fragment.get("outputs", {})) merged_outputs.update(fragment.get("outputs", {}))
all_resources.extend(fragment["resources"]) all_resources.extend(fragment["resources"])
# Determine stack kind: L2 if any module is L2 or if multi-module (P7) # Determine stack kind: L2 if any module is L2 or if multi-module
kind = "l2" if (multi_module or any_l2) else "l1" if multi_module:
kind = "l2"
elif any_l2:
kind = "l2"
else:
kind = "l1"
stack_instance = { stack_instance = {
"version": "1.0.0", "version": "1.0.0",
@@ -609,12 +590,6 @@ def resolve(contract_path, repo_root=None, environment_override=None):
"data_sources": all_data_sources, "data_sources": all_data_sources,
} }
# v1.25 (REQ-296): attach the pre-resolve contract-policy PCRs to
# the stack instance. The post-resolve stack-IR PCRs are appended
# after stack-schema validation (below).
if contract_pcrs:
stack_instance["policyResults"] = list(contract_pcrs)
# Add the human-readable title # Add the human-readable title
if contract.get("name"): if contract.get("name"):
stack_instance["stack"]["title"] = contract["name"] stack_instance["stack"]["title"] = contract["name"]
@@ -628,35 +603,26 @@ def resolve(contract_path, repo_root=None, environment_override=None):
stack_instance["outputs"] = merged_outputs stack_instance["outputs"] = merged_outputs
# Validate against stack schema # Validate against stack schema
stack_schema = _load_schema(os.path.join(repo_root, "schemas", "stack.schema.json")) stack_schema = _load_json(os.path.join(repo_root, "schemas", "stack.schema.json"))
jsonschema.validate(stack_instance, stack_schema) jsonschema.validate(stack_instance, stack_schema)
# v1.25 (REQ-298): post-resolve policy evaluation — run the active
# PolicyEngine over the resolved Stack IR with the stack-ir/ policy
# dir. The resulting PCRs are appended to the contract-policy PCRs
# on the stack instance (additive — the resolver's return value
# shape and exceptions are unchanged). The confidence signal
# consumes the merged list as its `policy` input.
try:
from core.policy_engine import get_engine, get_policy_root
engine = get_engine()
policy_root = get_policy_root()
stack_ir_pcrs = engine.evaluate(
stack_instance, policy_root / "stack-ir", contract.get("id", "unknown")
)
stack_instance.setdefault("policyResults", []).extend(stack_ir_pcrs)
except Exception:
# Policy evaluation must never break the resolver — the
# confidence signal decides the gate. A failure here means the
# engine is misconfigured; the contract PCRs (if any) are still
# present, and the confidence signal proceeds with whatever
# `policy` input it receives (possibly empty → 0.5 neutral).
pass
return stack_instance return stack_instance
if __name__ == "__main__": if __name__ == "__main__":
# P12 (REQ-176): CLI extracted to core/contract_resolver_cli.py. if len(sys.argv) < 3:
from core.contract_resolver_cli import main print("usage: contract_resolver.py <contract.yml> <out.json> [--environment <name>]", file=sys.stderr)
sys.exit(main()) sys.exit(2)
contract_path = sys.argv[1]
out_path = sys.argv[2]
env_override = None
if "--environment" in sys.argv:
idx = sys.argv.index("--environment")
if idx + 1 < len(sys.argv):
env_override = sys.argv[idx + 1]
# Also honor the ACDL_ENVIRONMENT_OVERRIDE env var (used by run_platform.sh).
if env_override is None and os.environ.get("ACDL_ENVIRONMENT_OVERRIDE"):
env_override = os.environ["ACDL_ENVIRONMENT_OVERRIDE"]
result = resolve(contract_path, environment_override=env_override)
with open(out_path, "w") as fh:
json.dump(result, fh, indent=2)
-41
View File
@@ -1,41 +0,0 @@
"""Nova Contract Resolver CLI — command-line entry point.
Extracted from core/contract_resolver.py (P12, REQ-176).
G-113 import direction: this module imports core.contract_resolver (the
re-export shim) for the resolve function. The shim imports the split
modules. Nothing imports this CLI module except direct invocation.
"""
from __future__ import annotations
import json
import sys
from core.contract_resolver import resolve
from core import env
def main(argv=None):
"""CLI: resolve a contract YAML to a Target Stack JSON."""
argv = argv if argv is not None else sys.argv[1:]
if len(argv) < 2:
print("usage: contract_resolver.py <contract.yml> <out.json> [--environment <name>", file=sys.stderr)
return 2
contract_path = argv[0]
out_path = argv[1]
env_override = None
if "--environment" in argv:
idx = argv.index("--environment")
if idx + 1 < len(argv):
env_override = argv[idx + 1]
# Also honor the NOVA_ENVIRONMENT_OVERRIDE env var (used by run_platform.sh).
if env_override is None and env.get_env("ENVIRONMENT_OVERRIDE"):
env_override = env.get_env("ENVIRONMENT_OVERRIDE")
result = resolve(contract_path, environment_override=env_override)
with open(out_path, "w") as fh:
json.dump(result, fh, indent=2)
return 0
if __name__ == "__main__":
sys.exit(main())
-31
View File
@@ -1,31 +0,0 @@
"""Nova Decommission Transform — zero counts + disable deletion protection (REQ-92).
Extracted from core/contract_resolver.py (P12, REQ-176).
G-113 import direction: this module imports only stdlib. The re-export
shim core/contract_resolver.py imports this module. Nothing imports the
shim except external callers.
"""
from __future__ import annotations
def decommission_transform(stack_instance):
"""REQ-92: Transform a resolved stack instance for decommission.
Sets all scalable counts to 0 and deletion_protection to false on
every resource. Used by the decommission pipeline mode after the
first step (disable deletion protection) has been applied.
"""
for res in stack_instance.get("resources", []):
if "nfrs" not in res:
res["nfrs"] = {}
res["nfrs"]["deletion_protection"] = False
inputs = res.get("inputs", {})
if "desired_count" in inputs:
inputs["desired_count"] = 0
if "min_capacity" in inputs:
inputs["min_capacity"] = 0
if "max_capacity" in inputs:
inputs["max_capacity"] = 0
return stack_instance
-31
View File
@@ -1,31 +0,0 @@
"""Environment helper (D-108, REQ-159, REQ-164).
During the Nova rebrand transition window (P2P4), `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_<name>`, 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
import os
from typing import Optional
__all__ = ["get_env"]
def get_env(name: str, default: Optional[str] = None) -> Optional[str]:
"""Resolve a config value from the `NOVA_*` environment.
`name` is the bare key WITHOUT the prefix (e.g. ``"AWS_ACCOUNT_ID"``).
Returns ``NOVA_<name>`` if set and non-empty, else ``default``.
"""
val = os.environ.get(f"NOVA_{name}")
if val:
return val
return default
-159
View File
@@ -1,159 +0,0 @@
"""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))
+8 -12
View File
@@ -55,12 +55,10 @@ def load(env_name, root=None):
def _onboarding_message(env_name): def _onboarding_message(env_name):
# P19 (REQ-183): rebranded Nova self-service request path — no longer
# routes to "contact the platform team" for the request step.
return ( return (
"=== Nova Environment Onboarding ===\n" "=== ACDL Environment Onboarding ===\n"
f"No environment named '{env_name}' is bound to this repository.\n\n" f"No environment named '{env_name}' is bound to this repository.\n\n"
"Nova environments are platform-managed. The platform provisions on\n" "ACDL environments are platform-managed. The platform provisions on\n"
"your behalf:\n" "your behalf:\n"
" - an AWS account (or a scoped partition of one)\n" " - an AWS account (or a scoped partition of one)\n"
" - a network (VPC + subnets)\n" " - a network (VPC + subnets)\n"
@@ -68,15 +66,13 @@ def _onboarding_message(env_name):
" - an IAM role surfaced to your repo via attribute-based\n" " - an IAM role surfaced to your repo via attribute-based\n"
" authorization (ABAC)\n\n" " authorization (ABAC)\n\n"
"You do not provide an AWS account, VPC, subnet, or state bucket.\n\n" "You do not provide an AWS account, VPC, subnet, or state bucket.\n\n"
"To request an environment (self-service):\n" "To request an environment:\n"
" 1. Submit an onboarding request to the Nova Lambda\n" " 1. Contact the platform team with your repo name + the\n"
" (action: onboard_consumer) with your repo name + the\n"
" environment name you need (e.g. 'dev').\n" " environment name you need (e.g. 'dev').\n"
" 2. The platform generates an environment binding + opens a PR.\n" " 2. The platform team provisions the account/network/state/role\n"
" 3. The platform provisions the account/network/state/role and\n" " and binds the environment to your repo.\n"
" grants the ABAC role. Your next pipeline run proceeds.\n\n" " 3. Your next pipeline run will proceed normally.\n\n"
"Run: python3 core/onboarding.py --request '{...}' to generate a\n" "Expected turnaround: contact the platform team for current SLA.\n"
"binding file locally, or POST to the Lambda onboard_consumer action.\n"
"===================================\n" "===================================\n"
) )
+2 -10
View File
@@ -33,13 +33,5 @@ halting the pipeline before any work is done.
A new environment is a platform-team action: provision the AWS account / A new environment is a platform-team action: provision the AWS account /
network / state backend / IAM role, then add a `<name>.json` here and bind network / state backend / IAM role, then add a `<name>.json` here and bind
it to the consumer repo. it to the consumer repo. Self-service environment provisioning is on the
roadmap; today it is a platform-team action.
**P19 (REQ-183):** the *request* step is now self-service. A consumer
submits an onboarding request (POST to the Nova Lambda `onboard_consumer`
action, or `python3 core/onboarding.py --request '{...}'`) and the
platform generates a `<name>.json` binding file from the request + opens
a PR. The actual AWS account/network/state provisioning + cross-account
role grant remains a platform-team action (a future feature milestone
will automate the provisioning; the cross-account role Terraform is
offline-proven in P20/REQ-184).
+4 -26
View File
@@ -1,6 +1,6 @@
"""HITL pre-execution attestation gates (REQ-108, D-084). """HITL pre-execution attestation gates (REQ-108, D-084).
Records the approver identity (the CI actor (GITHUB_ACTOR or FORGE_ACTOR)) to the Records the approver identity (`gitea.actor` / `github.actor`) to the
DynamoDB outbox for the contractId (attribute `approver_qa` / DynamoDB outbox for the contractId (attribute `approver_qa` /
`approver_prod` / `approver_dr`), runs the separation-of-duties check on `approver_prod` / `approver_dr`), runs the separation-of-duties check on
prod, invokes the 8-concern attestation matrix for the target env, and prod, invokes the 8-concern attestation matrix for the target env, and
@@ -12,10 +12,6 @@ import os
import sys import sys
from typing import Optional, Tuple from typing import Optional, Tuple
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.metrics.event_envelope import make_event, append_event
from core.metrics.decision_ledger import append as ledger_append
def _approver_attr(env: str) -> str: def _approver_attr(env: str) -> str:
return {"qa": "approver_qa", "prod": "approver_prod", "dr": "approver_dr"}.get(env, "") return {"qa": "approver_qa", "prod": "approver_prod", "dr": "approver_dr"}.get(env, "")
@@ -29,7 +25,7 @@ def attest(contract_id: str, env: str, approver: str,
Args: Args:
contract_id: the contract UUID. contract_id: the contract UUID.
env: dev/qa/prod/dr. env: dev/qa/prod/dr.
approver: the approver's username (the CI actor (GITHUB_ACTOR or FORGE_ACTOR)). approver: the approver's username (`gitea.actor` / `github.actor`).
evidence: optional operator-supplied evidence artifacts (for the evidence: optional operator-supplied evidence artifacts (for the
attestation matrix operator-supplied concerns). attestation matrix operator-supplied concerns).
outbox_client: optional moto-mocked DynamoDB outbox client for tests. outbox_client: optional moto-mocked DynamoDB outbox client for tests.
@@ -41,7 +37,7 @@ def attest(contract_id: str, env: str, approver: str,
return (True, "dev autonomous (no HITL gate)") return (True, "dev autonomous (no HITL gate)")
if not approver: if not approver:
return (False, f"no approver identity for {env} (GITHUB_ACTOR/FORGE_ACTOR unset)") return (False, f"no approver identity for {env} (GITHUB_ACTOR/GITEA_ACTOR unset)")
attr = _approver_attr(env) attr = _approver_attr(env)
if not attr: if not attr:
@@ -65,30 +61,12 @@ def attest(contract_id: str, env: str, approver: str,
if not ok: if not ok:
return (False, reason) return (False, reason)
# Emit attestation.recorded event to the Decision Ledger (D-132).
try:
run_id = os.environ.get("NOVA_RUN_ID", f"attest-{contract_id[:8]}")
attestation_data = {
"approver": approver,
"environment": env,
"concerns": reason,
"result": "pass",
"contract_id": contract_id,
}
attestation_event = make_event("nova.attestation.recorded", run_id, env, attestation_data,
contract_id=contract_id, actor_type="human-attestation",
actor_id=approver)
append_event(attestation_event)
ledger_append(attestation_event)
except Exception:
pass # metrics emission must never break the attestation gate
return (True, f"{env} attested by {approver}") return (True, f"{env} attested by {approver}")
def approver_from_env() -> Optional[str]: def approver_from_env() -> Optional[str]:
"""Read the approver identity from the environment.""" """Read the approver identity from the environment."""
return os.environ.get("GITHUB_ACTOR") or os.environ.get("FORGE_ACTOR") return os.environ.get("GITHUB_ACTOR") or os.environ.get("GITEA_ACTOR")
if __name__ == "__main__": if __name__ == "__main__":
+18 -18
View File
@@ -18,32 +18,32 @@ gates. No partial deployment to roll back on rejection (qa, prod); dr is
a separate deployment against a separate cluster/region. The a separate deployment against a separate cluster/region. The
canary/deployment-rollback model is explicitly not in scope for v1. canary/deployment-rollback model is explicitly not in scope for v1.
## Forge-specific gate mechanics (D-042) ## Gitea-specific gate mechanics (D-042)
The dev forge has **no Environments API** and ignores `environment:` blocks Gitea has **no Environments API** and ignores `environment:` blocks
(v1.0 D-013; re-confirmed in RESEARCH TARGET 1). The pre-execution gate (v1.0 D-013; re-confirmed in RESEARCH TARGET 1). The pre-execution gate
is modeled as a `workflow_dispatch` with approval inputs: is modeled as a `workflow_dispatch` with approval inputs:
- **qa gate:** `workflow_dispatch` with `approve_qa: true`; the dispatch - **qa gate:** `workflow_dispatch` with `approve_qa: true`; the dispatch
run's `CI actor` is the QA approver. run's `gitea.actor` is the QA approver.
- **prod gate:** `workflow_dispatch` with `approve_prod: true`; - **prod gate:** `workflow_dispatch` with `approve_prod: true`;
`CI actor` is the SRE approver. `gitea.actor` is the SRE approver.
- **dr gate:** `workflow_dispatch` with `approve_dr: true`; same. - **dr gate:** `workflow_dispatch` with `approve_dr: true`; same.
The approver identity of record = `CI actor` of the dispatch run The approver identity of record = `gitea.actor` of the dispatch run
(D-042). There is no other approval-identity signal in the dev forge. The real (D-042). There is no other approval-identity signal in Gitea. The real
OIDC path (blocked on upstream forge OIDC support) does not change this — OIDC path (blocked on go-gitea/gitea#36988) does not change this —
OIDC authorizes the *runner* to AWS, it does not change how the platform OIDC authorizes the *runner* to AWS, it does not change how the platform
records the *human* approver. records the *human* approver.
On GitHub, the equivalent is `CI actor` of the `workflow_dispatch` On GitHub, the equivalent is `github.actor` of the `workflow_dispatch`
run; GitHub Environments with required reviewers are the native gate, run; GitHub Environments with required reviewers are the native gate,
but the `workflow_dispatch` approval-input fallback is used for but the `workflow_dispatch` approval-input fallback is used for
byte-identical across forges. byte-identical Gitea + GitHub workflows.
## Reviewer routing (ARCHITECTURE.md §10.2) ## Reviewer routing (ARCHITECTURE.md §10.2)
CODEOWNERS routes the right reviewer to the right gate: Gitea CODEOWNERS routes the right reviewer to the right gate:
- qa → QA team - qa → QA team
- prod → SRE team - prod → SRE team
@@ -93,7 +93,7 @@ The full table (lifted verbatim from §10.4):
The operator-supplied evidence artifact is a JSON blob with `timestamp`, The operator-supplied evidence artifact is a JSON blob with `timestamp`,
`type`, `payload`, and an optional `signature` (JWS detached). Freshness `type`, `payload`, and an optional `signature` (JWS detached). Freshness
is validated against the window above. Signature verification runs when is validated against the window above. Signature verification runs when
`NOVA_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged when `ACDL_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged when
unset (dev/CI — D-089). The matrix fails loud if an operator-supplied unset (dev/CI — D-089). The matrix fails loud if an operator-supplied
concern is missing or expired for prod/dr. concern is missing or expired for prod/dr.
@@ -105,7 +105,7 @@ concern is missing or expired for prod/dr.
| 1 business day | PENDING_ATTESTATION_WARNING | Notify team + platform on-call (elevated path); emit `PENDING_ATTESTATION_TIMEOUT_WARNING` event | | 1 business day | PENDING_ATTESTATION_WARNING | Notify team + platform on-call (elevated path); emit `PENDING_ATTESTATION_TIMEOUT_WARNING` event |
| 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit `PENDING_ATTESTATION_AUTO_FREEZE` event; new submission linked via `supersedes` | | 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit `PENDING_ATTESTATION_AUTO_FREEZE` event; new submission linked via `supersedes` |
**Implementation:** an `on: schedule` workflow (runs hourly) that **Implementation:** a Gitea `on: schedule` workflow (runs hourly) that
scans the DynamoDB outbox for `PENDING_ATTESTATION` events with `ts` scans the DynamoDB outbox for `PENDING_ATTESTATION` events with `ts`
older than 1/2 business days and emits the warn/freeze events. Not older than 1/2 business days and emits the warn/freeze events. Not
implemented in v1.9 (roadmap item; the attestation gates themselves are implemented in v1.9 (roadmap item; the attestation gates themselves are
@@ -126,11 +126,11 @@ The identity-distinctness check is platform-internal, not GitHub-native,
not Kyverno (in v1). Sequence: not Kyverno (in v1). Sequence:
1. On promotion dev → qa, the platform reads the QA approver's identity 1. On promotion dev → qa, the platform reads the QA approver's identity
from the `workflow_dispatch` run's `CI actor` from the `workflow_dispatch` run's `gitea.actor` (or `github.actor`)
and writes it to the DynamoDB outbox keyed by `contractId` (attribute and writes it to the DynamoDB outbox keyed by `contractId` (attribute
`approver_qa`). `approver_qa`).
2. On promotion qa → prod, the platform reads the stored `approver_qa` 2. On promotion qa → prod, the platform reads the stored `approver_qa`
from the outbox and the new SRE approver identity from the from the outbox and the new SRE approver's `gitea.actor` from the
prod-dispatch run. prod-dispatch run.
3. If `approver_qa == approver_prod`, the platform blocks the prod 3. If `approver_qa == approver_prod`, the platform blocks the prod
promotion, writes a `SEPARATION_OF_DUTIES_VIOLATION` event to the promotion, writes a `SEPARATION_OF_DUTIES_VIOLATION` event to the
@@ -140,7 +140,7 @@ not Kyverno (in v1). Sequence:
in the same process that has authority to block the promotion. in the same process that has authority to block the promotion.
v1.9 implements `route_halt_artifact` as a real SNS publish (topic v1.9 implements `route_halt_artifact` as a real SNS publish (topic
`acdl-sod-halt`, ARN from `NOVA_SOD_HALT_TOPIC_ARN`) with an outbox-event `acdl-sod-halt`, ARN from `ACDL_SOD_HALT_TOPIC_ARN`) with an outbox-event
fallback when the topic ARN is unset (REQ-107). The attestation gate fallback when the topic ARN is unset (REQ-107). The attestation gate
itself is `core/hitl_gates.py` (`attest(contract_id, env, approver, itself is `core/hitl_gates.py` (`attest(contract_id, env, approver,
evidence)`), which records the approver to the outbox, runs the SoD evidence)`), which records the approver to the outbox, runs the SoD
@@ -163,13 +163,13 @@ v1.9 (Phase 41 + Phase 42) wires the gates end-to-end:
## Decision trail ## Decision trail
- **D-042** — approver identity = `CI actor` of the `workflow_dispatch` - **D-042** — approver identity = `gitea.actor` of the `workflow_dispatch`
run; no Environments API in the dev forge. run; no Environments API in Gitea. On GitHub, `github.actor`.
- **D-013** (v1.0) — the `workflow_dispatch` approval-input fallback, - **D-013** (v1.0) — the `workflow_dispatch` approval-input fallback,
re-used for the real platform's pre-execution gate model. re-used for the real platform's pre-execution gate model.
- **D-084** (v1.9) — 8-concern attestation matrix: offline-testable - **D-084** (v1.9) — 8-concern attestation matrix: offline-testable
concerns run for real; operator-supplied concerns accept signed concerns run for real; operator-supplied concerns accept signed
evidence artifacts validated for freshness + schema. evidence artifacts validated for freshness + schema.
- **D-089** (v1.9) — attestation artifact signature verification is - **D-089** (v1.9) — attestation artifact signature verification is
skipped when `NOVA_ATTESTATION_SIGNING_KEY_ID` is unset (dev/CI); skipped when `ACDL_ATTESTATION_SIGNING_KEY_ID` is unset (dev/CI);
required for prod/dr. required for prod/dr.
+31 -190
View File
@@ -2,7 +2,7 @@
Invoked via a Function URL (IAM auth) by consumer pipelines (one-way Invoked via a Function URL (IAM auth) by consumer pipelines (one-way
communication, D-051). Accepts { consumerRepo, contractId, contract, communication, D-051). Accepts { consumerRepo, contractId, contract,
environment, action } and writes contracts to DynamoDB table nova-contracts environment, action } and writes contracts to DynamoDB table acdl-contracts
(PK consumerRepo, SK contractId#submittedAt). (PK consumerRepo, SK contractId#submittedAt).
The report_error action (D-055) creates a GitHub issue on the platform repo The report_error action (D-055) creates a GitHub issue on the platform repo
@@ -22,61 +22,18 @@ import urllib.parse
import boto3 import boto3
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "nova-contracts") TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "acdl-contracts")
CHANGE_REQUESTS_TABLE = os.environ.get("CHANGE_REQUESTS_TABLE", "nova-change-requests") CHANGE_REQUESTS_TABLE = os.environ.get("CHANGE_REQUESTS_TABLE", "acdl-change-requests")
GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "nova/github-token") GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "acdl/github-token")
PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "nova/acdl") PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "acdl/acdl")
# P1-9: Forge-agnostic API base URL. Defaults to GitHub; set GITHUB_API_BASE # P1-9: Forge-agnostic API base URL. Defaults to GitHub; set GITHUB_API_BASE
# to a compatible forge API root (e.g. https://forge.example.com/api/v1). # to a Gitea API root (e.g. https://git.cloudinit.dev/api/v1) for Gitea.
GITHUB_API_BASE = os.environ.get("GITHUB_API_BASE", "https://api.github.com") GITHUB_API_BASE = os.environ.get("GITHUB_API_BASE", "https://api.github.com")
# P11 (REQ-175): consistent cap for error/stackTrace fields (was 10k vs 2k).
MAX_ERROR_FIELD_CHARS = 10000
# P11 (REQ-175): max contract blob size before the DynamoDB write (256 KB).
MAX_CONTRACT_BYTES = 256 * 1024
_dynamodb = None _dynamodb = None
_secrets_client = None _secrets_client = None
def _discover_environments():
"""P10 (REQ-174): derive the valid environment names from
core/environments/*.json (the directory is the single source of truth,
not a hardcoded set). Falls back to {'dev','qa','prod','dr'} if the
directory is not readable (e.g. packaged Lambda without the dir).
"""
env_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__)))), "core", "environments")
try:
names = {f[:-5] for f in os.listdir(env_dir) if f.endswith(".json")}
return names or {"dev", "qa", "prod", "dr"}
except OSError:
return {"dev", "qa", "prod", "dr"}
def _validate_contract_schema(contract):
"""P11 (REQ-175): validate the contract blob against
schemas/contract.schema.json before the DynamoDB write. Raises
ValueError on invalid. Falls back to a no-op if the schema or
jsonschema is unavailable (e.g. packaged Lambda without the schema).
"""
try:
import json as _json
import jsonschema
schema_path = os.path.join(os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__)))),
"schemas", "contract.schema.json")
with open(schema_path) as f:
schema = _json.load(f)
jsonschema.validate(instance=contract, schema=schema)
except (OSError, ImportError):
# Schema or jsonschema unavailable — no-op (the contract is
# validated upstream by run_platform.sh in the normal path).
pass
except jsonschema.ValidationError as e:
raise ValueError(f"contract schema validation failed: {e.message}")
def _get_dynamodb(): def _get_dynamodb():
global _dynamodb global _dynamodb
if _dynamodb is None: if _dynamodb is None:
@@ -96,22 +53,22 @@ def _iso8601_now():
def _forge_type(): def _forge_type():
"""Detect whether the API base is GitHub or a compatible forge. """P1-9: Detect whether the API base is GitHub or Gitea.
Compatible forge API roots contain '/api/v1'; GitHub's is 'api.github.com'. Gitea API roots contain '/api/v1'; GitHub's is 'api.github.com'.
""" """
if "/api/v1" in GITHUB_API_BASE: if "/api/v1" in GITHUB_API_BASE:
return "generic_forge" return "gitea"
return "github" return "github"
def _issues_search_url(owner, repo, encoded_query): def _issues_search_url(owner, repo, encoded_query):
"""Build the issue search URL based on forge type. """P1-9: Build the issue search URL based on forge type.
GitHub uses /search/issues?q=...; compatible forges use /repos/{owner}/{repo}/issues?... GitHub uses /search/issues?q=...; Gitea uses /repos/{owner}/{repo}/issues?...
with query params (no /search/issues endpoint). with query params (no /search/issues endpoint).
""" """
if _forge_type() == "generic_forge": if _forge_type() == "gitea":
return ( return (
f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues" f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
f"?state=open&type=issues&q={encoded_query}" f"?state=open&type=issues&q={encoded_query}"
@@ -123,7 +80,7 @@ def _issues_search_url(owner, repo, encoded_query):
def _issues_create_url(owner, repo): def _issues_create_url(owner, repo):
"""URL for creating an issue (same pattern across forges).""" """URL for creating an issue (same pattern for both GitHub + Gitea)."""
return f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues" return f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
@@ -137,25 +94,6 @@ def _submit_contract(payload):
contract_id = payload["contractId"] contract_id = payload["contractId"]
contract = payload["contract"] contract = payload["contract"]
environment = payload["environment"] environment = payload["environment"]
# P11 (REQ-175): size-cap the contract blob before the DynamoDB write
# (unbounded payload → write amplification). 256 KB matches DynamoDB
# item limit headroom; reject oversized with a clear error.
import json as _json
contract_json = _json.dumps(contract).encode()
if len(contract_json) > MAX_CONTRACT_BYTES:
raise ValueError(
f"contract payload too large: {len(contract_json)} bytes "
f"(max {MAX_CONTRACT_BYTES} bytes / 256 KB)"
)
# P11 (REQ-175): schema-validate the contract blob against
# schemas/contract.schema.json before the write. Reject invalid with 400.
# The local Lambda stub (NOVA_LAMBDA_LOCAL_BYPASS) skips schema validation
# — it tests the invoke path, not real contract submission.
if not os.environ.get("NOVA_LAMBDA_LOCAL_BYPASS"):
_validate_contract_schema(contract)
submitted_at = _iso8601_now() submitted_at = _iso8601_now()
table = _get_dynamodb().Table(TABLE_NAME) table = _get_dynamodb().Table(TABLE_NAME)
item = { item = {
@@ -193,7 +131,7 @@ def _report_error(payload):
contract_id = payload["contractId"] contract_id = payload["contractId"]
error = payload.get("error", "unknown error") error = payload.get("error", "unknown error")
run_url = payload.get("runUrl", "") run_url = payload.get("runUrl", "")
stack_trace = payload.get("stackTrace", "")[:MAX_ERROR_FIELD_CHARS] # P11: aligned cap stack_trace = payload.get("stackTrace", "")[:2000] # truncate
# Get the GitHub token from Secrets Manager # Get the GitHub token from Secrets Manager
secrets = _get_secrets_client() secrets = _get_secrets_client()
@@ -204,7 +142,7 @@ def _report_error(payload):
raise RuntimeError(f"failed to read GitHub token from Secrets Manager: {e}") raise RuntimeError(f"failed to read GitHub token from Secrets Manager: {e}")
owner, repo = PLATFORM_REPO.split("/") owner, repo = PLATFORM_REPO.split("/")
title = f"[NOVA-ALERT] Deploy failure: {consumer_repo} / {contract_id}" title = f"[ACDL-ALERT] Deploy failure: {consumer_repo} / {contract_id}"
# Check for an existing open issue with the same title (idempotency) # Check for an existing open issue with the same title (idempotency)
# URL-encode the contract_id to prevent search-query injection (P1-1). # URL-encode the contract_id to prevent search-query injection (P1-1).
@@ -250,7 +188,7 @@ def _report_error(payload):
{stack_trace} {stack_trace}
``` ```
_This issue was auto-created by the Nova platform Lambda (D-055). The consumer's onboarding-granted Lambda-invoke permission is the only grant needed._ _This issue was auto-created by the ACDL platform Lambda (D-055). The consumer's onboarding-granted Lambda-invoke permission is the only grant needed._
""" """
if existing: if existing:
@@ -298,33 +236,20 @@ def _validate_caller_identity(event, payload):
in the payload matches the principal's ARN-derived source identity, preventing in the payload matches the principal's ARN-derived source identity, preventing
one consumer from impersonating another. one consumer from impersonating another.
P10 (REQ-174): if the IAM identity is absent (no callerArn), the function If the identity is not available (e.g. local testing or non-IAM auth), the
FAILS CLOSED (raises ValueError) rather than silently passing. The ABAC check is skipped (the ABAC policy at the IAM layer enforces the scope).
policy at the IAM layer is the primary enforcement; this is defense-in-
depth so a misconfigured Function URL (no IAM auth) does not allow
unauthenticated contract submission. Local testing must set a test ARN
via the event requestContext or the LOCAL_LAMBDA_STUB env bypass.
v1.14 (REQ-144): also validates contractId format, environment enum, and v1.14 (REQ-144): also validates contractId format, environment enum, and
error length. P10 (REQ-174): the environment enum is derived from the error length. The ABAC reliance is documented here: the Function URL IAM
core/environments/ directory (not hardcoded), so a new env JSON is the identity does not expose principal tags in the event, so full enforcement
single source of truth. The ABAC reliance is documented here: the of consumerRepo ownership is at the IAM layer (ABAC via
Function URL IAM identity does not expose principal tags in the event, aws:PrincipalTag/acdl:owner). This function validates format only, not
so full enforcement of consumerRepo ownership is at the IAM layer (ABAC ownership.
via aws:PrincipalTag/nova:owner). This function validates format only,
not ownership.
""" """
identity = event.get("requestContext", {}).get("identity", {}) identity = event.get("requestContext", {}).get("identity", {})
caller_arn = identity.get("userArn", "") caller_arn = identity.get("userArn", "")
if not caller_arn: if not caller_arn:
# P10 (REQ-174): fail closed. A local-test bypass is allowed via pass # no identity available — rely on IAM ABAC enforcement
# the NOVA_LAMBDA_LOCAL_BYPASS env var (set by the LocalLambdaStub).
import os as _os
if not _os.environ.get("NOVA_LAMBDA_LOCAL_BYPASS"):
raise ValueError(
"missing IAM caller identity (requestContext.identity.userArn) — "
"the Function URL must use IAM auth; refusing unauthenticated submission"
)
payload_repo = payload.get("consumerRepo", "") payload_repo = payload.get("consumerRepo", "")
if payload_repo: if payload_repo:
# consumerRepo must be org/repo format, <=128 chars # consumerRepo must be org/repo format, <=128 chars
@@ -338,24 +263,23 @@ def _validate_caller_identity(event, payload):
if not re.match(r'^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$', contract_id): if not re.match(r'^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$', contract_id):
raise ValueError(f"invalid contractId format: {contract_id!r} (alphanumeric, hyphen, underscore; max 64 chars)") raise ValueError(f"invalid contractId format: {contract_id!r} (alphanumeric, hyphen, underscore; max 64 chars)")
# P10 (REQ-174): environment enum derived from core/environments/ (not # v1.14 (REQ-144): environment enum validation
# hardcoded) — the directory is the single source of truth.
environment = payload.get("environment", "") environment = payload.get("environment", "")
if environment: if environment:
valid_envs = _discover_environments() valid_envs = {"dev", "qa", "prod", "dr"}
if environment not in valid_envs: if environment not in valid_envs:
raise ValueError(f"invalid environment: {environment!r} (must be one of {sorted(valid_envs)})") raise ValueError(f"invalid environment: {environment!r} (must be one of {valid_envs})")
# v1.14 (REQ-144): error length cap (for report_error action) # v1.14 (REQ-144): error length cap (for report_error action)
error_msg = payload.get("error", "") error_msg = payload.get("error", "")
if error_msg and len(str(error_msg)) > MAX_ERROR_FIELD_CHARS: if error_msg and len(str(error_msg)) > 10000:
payload["error"] = str(error_msg)[:MAX_ERROR_FIELD_CHARS] payload["error"] = str(error_msg)[:10000]
def _validate_change_request(payload): def _validate_change_request(payload):
"""REQ-93: Validate a change request ID against the CMDB (DynamoDB). """REQ-93: Validate a change request ID against the CMDB (DynamoDB).
Queries the nova-change-requests table for the given changeRequestId. Queries the acdl-change-requests table for the given changeRequestId.
Returns the CR details if status is 'approved' and the consumerRepo matches. Returns the CR details if status is 'approved' and the consumerRepo matches.
Raises ValueError if the CR is not found, not approved, or the repo doesn't match. Raises ValueError if the CR is not found, not approved, or the repo doesn't match.
""" """
@@ -398,65 +322,6 @@ def _validate_change_request(payload):
} }
def _onboard_consumer(payload):
"""P18 (REQ-182): accept a self-service onboarding request.
Validates the payload against schemas/onboarding.schema.json, then
writes a 'pending' row to nova-contracts (D-119). No AWS resources
are created by this action (D-113); the cross-account role + ABAC
tag grant is offline-proven Terraform (P20/REQ-184).
"""
import jsonschema
schema_path = os.path.join(os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__)))),
"schemas", "onboarding.schema.json")
try:
with open(schema_path) as f:
schema = json.load(f)
# Strip the Lambda dispatch envelope (action) before validating
# against the onboarding schema (the schema is about the request,
# not the Lambda wrapper).
onboarding_payload = {k: v for k, v in payload.items() if k != "action"}
jsonschema.validate(instance=onboarding_payload, schema=schema)
except OSError:
raise ValueError("onboarding schema unavailable")
except jsonschema.ValidationError as e:
raise ValueError(f"onboarding payload invalid: {e.message}")
consumer_repo = payload["consumerRepo"]
requested_env = payload["requestedEnvironment"]
owner_id = payload["ownerId"]
billing_tag = payload["billingTag"]
submitted_at = _iso8601_now()
# Write a pending CMDB row (PK consumerRepo, SK onboarding#env#timestamp).
table = _get_dynamodb().Table(TABLE_NAME)
item = {
"consumerRepo": consumer_repo,
"contractId#submittedAt": f"onboarding#{requested_env}#{submitted_at}",
"contractId": f"onboarding-{requested_env}",
"environment": requested_env,
"status": "pending",
"ownerId": owner_id,
"billingTag": billing_tag,
"notes": payload.get("notes", ""),
"submittedAt": submitted_at,
}
table.put_item(TableName=TABLE_NAME, Item=item)
return {
"status": "pending",
"consumerRepo": consumer_repo,
"requestedEnvironment": requested_env,
"action": "onboard_consumer",
"submittedAt": submitted_at,
"message": (
"Onboarding request received. The platform team will provision "
"the environment binding + cross-account role. Track the status "
"via the nova-contracts table (status=pending → granted)."
),
}
def lambda_handler(event, context): def lambda_handler(event, context):
"""AWS Lambda handler entry point. """AWS Lambda handler entry point.
@@ -485,8 +350,6 @@ def lambda_handler(event, context):
result = _report_error(payload) result = _report_error(payload)
elif action == "validate_change_request": elif action == "validate_change_request":
result = _validate_change_request(payload) result = _validate_change_request(payload)
elif action == "onboard_consumer":
result = _onboard_consumer(payload)
else: else:
return { return {
"statusCode": 400, "statusCode": 400,
@@ -494,28 +357,6 @@ def lambda_handler(event, context):
} }
return {"statusCode": 200, "body": json.dumps(result)} return {"statusCode": 200, "body": json.dumps(result)}
except ValueError as e: except ValueError as e:
# P10 (REQ-174): identity failures are 401, field validation is 400.
if "missing IAM caller identity" in str(e):
return {"statusCode": 401, "body": json.dumps({"error": str(e)})}
return {"statusCode": 400, "body": json.dumps({"error": str(e)})} return {"statusCode": 400, "body": json.dumps({"error": str(e)})}
except Exception as e: # pragma: no cover - defensive top-level guard except Exception as e: # pragma: no cover - defensive top-level guard
return {"statusCode": 500, "body": json.dumps({"error": str(e)})} return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
# --- CLI: --check-readiness (D-133, REQ-218) ---------------------------
# Invoked as: python3 -m core.lambda.contract_ingestor --check-readiness <submission.json>
# Delegates to core.submission_readiness.check_readiness() and prints the
# structured ReadinessResult. Exits 0 if ready, 1 if not.
if __name__ == "__main__": # pragma: no cover - CLI entry
import sys
if "--check-readiness" in sys.argv:
sys.path.insert(
0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
)
from core.submission_readiness import cli_main
# Strip the --check-readiness flag; pass the file path.
rest = [a for a in sys.argv[1:] if a != "--check-readiness"]
sys.exit(cli_main(["check-readiness"] + rest))
else:
print("Usage: python3 -m core.lambda.contract_ingestor --check-readiness <submission.json>")
+8 -32
View File
@@ -12,9 +12,7 @@ evidence event) runs end-to-end against the local tier with no AWS:
Each adapter exposes the same interface as the live counterpart so the Each adapter exposes the same interface as the live counterpart so the
caller code path is unchanged; only the I/O target swaps. Selection is caller code path is unchanged; only the I/O target swaps. Selection is
gated on the NOVA_LOCAL_TIER env var (set by run_platform.sh --local). gated on the ACDL_LOCAL_TIER env var (set by run_platform.sh --local).
Env vars read via core/env.py (NOVA_* only; the ACDL_* fallback was
removed in v1.15 P5, REQ-164).
""" """
from __future__ import annotations from __future__ import annotations
@@ -34,20 +32,12 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
# Repo root on sys.path so `from core import env` resolves to THIS package
# when run as a script (avoids editable-installed third-party `core` shadow).
_REPO_ROOT = str(Path(__file__).resolve().parent.parent)
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from core import env
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
def is_local_tier() -> bool: def is_local_tier() -> bool:
"""True when the local emulating tier is active.""" """True when the local emulating tier is active."""
return env.get_env("LOCAL_TIER", "") == "1" return os.environ.get("ACDL_LOCAL_TIER", "") == "1"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -69,7 +59,7 @@ class FlatFileOutbox:
@classmethod @classmethod
def create(cls, dir: Optional[Path] = None) -> "FlatFileOutbox": def create(cls, dir: Optional[Path] = None) -> "FlatFileOutbox":
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="nova_outbox_")) d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="acdl_outbox_"))
d.mkdir(parents=True, exist_ok=True) d.mkdir(parents=True, exist_ok=True)
out = cls(dir=d) out = cls(dir=d)
# Re-read the chain tail if the file already exists. # Re-read the chain tail if the file already exists.
@@ -88,7 +78,7 @@ class FlatFileOutbox:
return hashlib.sha256(canonical.encode("utf-8")).hexdigest() return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
def write_event(self, event: Dict[str, Any], def write_event(self, event: Dict[str, Any],
outbox_table: str = "nova-outbox-local", outbox_table: str = "acdl-outbox-local",
region: str = "local") -> Dict[str, Any]: region: str = "local") -> Dict[str, Any]:
"""Write an evidence event to the flat-file outbox. """Write an evidence event to the flat-file outbox.
@@ -250,7 +240,7 @@ class LocalS3StateBackend:
@classmethod @classmethod
def create(cls, dir: Optional[Path] = None) -> "LocalS3StateBackend": def create(cls, dir: Optional[Path] = None) -> "LocalS3StateBackend":
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="nova_tfstate_")) d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="acdl_tfstate_"))
d.mkdir(parents=True, exist_ok=True) d.mkdir(parents=True, exist_ok=True)
return cls(state_dir=d) return cls(state_dir=d)
@@ -296,7 +286,7 @@ class LocalLambdaStub:
Returns the handler's response dict Returns the handler's response dict
({statusCode, body}). The handler's DynamoDB calls are ({statusCode, body}). The handler's DynamoDB calls are
intercepted via the NOVA_LOCAL_TIER env var (the handler checks intercepted via the ACDL_LOCAL_TIER env var (the handler checks
_get_dynamodb(); under local tier it would need patching - we _get_dynamodb(); under local tier it would need patching - we
patch the module's _get_dynamodb to return a local stub).""" patch the module's _get_dynamodb to return a local stub)."""
# Import the handler module (the dir is named `lambda`, a Python # Import the handler module (the dir is named `lambda`, a Python
@@ -392,24 +382,12 @@ class LocalLambdaStub:
"httpContext": {"authorizer": {"iam": {"userId": "local-stub"}}} "httpContext": {"authorizer": {"iam": {"userId": "local-stub"}}}
}, },
} }
# P10 (REQ-174): the local stub has no real IAM identity; set
# the bypass so the fail-closed identity check passes for local
# tier testing. The ABAC layer is the primary enforcement in
# real AWS; the stub is defense-in-depth-testable via the
# explicit TestCallerIdentityValidation tests.
import os as _os
_prev_bypass = _os.environ.get("NOVA_LAMBDA_LOCAL_BYPASS")
_os.environ["NOVA_LAMBDA_LOCAL_BYPASS"] = "1"
result = ci.lambda_handler(event, None) result = ci.lambda_handler(event, None)
finally: finally:
ci._get_dynamodb = original_get ci._get_dynamodb = original_get
if original_urlopen is not None: if original_urlopen is not None:
import urllib.request import urllib.request
urllib.request.urlopen = original_urlopen urllib.request.urlopen = original_urlopen
if _prev_bypass is None:
_os.environ.pop("NOVA_LAMBDA_LOCAL_BYPASS", None)
else:
_os.environ["NOVA_LAMBDA_LOCAL_BYPASS"] = _prev_bypass
return result return result
@@ -441,7 +419,7 @@ def run_local_e2e(contract_path: str, repo_root: Optional[Path] = None) -> Dict[
stack = resolve(contract_path, str(root)) stack = resolve(contract_path, str(root))
stack_name = stack["stack"]["name"] stack_name = stack["stack"]["name"]
work = Path(tempfile.mkdtemp(prefix="nova_local_e2e_")) work = Path(tempfile.mkdtemp(prefix="acdl_local_e2e_"))
tf_dir = work / "tf" tf_dir = work / "tf"
tf_dir.mkdir(exist_ok=True) tf_dir.mkdir(exist_ok=True)
adapter.adapt(stack, str(tf_dir)) adapter.adapt(stack, str(tf_dir))
@@ -512,8 +490,6 @@ def run_local_e2e(contract_path: str, repo_root: Optional[Path] = None) -> Dict[
if __name__ == "__main__": if __name__ == "__main__":
contract = sys.argv[1] if len(sys.argv) > 1 else "contracts/microservice.yml" contract = sys.argv[1] if len(sys.argv) > 1 else "contracts/microservice.yml"
# Set so is_local_tier() finds NOVA_LOCAL_TIER (NOVA_* only; the os.environ["ACDL_LOCAL_TIER"] = "1"
# ACDL_* alias was removed in v1.15 P5, REQ-164).
os.environ["NOVA_LOCAL_TIER"] = "1"
result = run_local_e2e(contract) result = run_local_e2e(contract)
print(json.dumps(result, indent=2)) print(json.dumps(result, indent=2))
View File
-364
View File
@@ -1,364 +0,0 @@
"""Nova Metrics Collector (REQ-189, P2).
Reads all grounded signals (REGRESSION_REPORT.json, per-run manifests,
junit XML, pcr.json, signal.json, COST.md, decision ledger, coverage.json)
and normalizes them into a SQLite cold store at metrics/nova_metrics.db.
D-120: Nova-native (SQLite, no ClickHouse/BigQuery).
D-125: hybrid model reads files + events SQLite.
D-126: cold-only (no hot path; hot path deferred D-096).
D-128: metrics/ at repo root.
Idempotent: re-running the collector against the same inputs produces
identical row counts (REQ-200). The collector uses INSERT OR REPLACE
on fact tables keyed by natural keys.
"""
import datetime
import json
import os
import sqlite3
import sys
import xml.etree.ElementTree as ET
_METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
_STORE_PATH = os.path.join(_METRICS_DIR, "nova_metrics.db")
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
_REGRESSION_REPORT = os.path.join(_REPO_ROOT, ".ciagent", "REGRESSION_REPORT.json")
_RUNS_DIR = os.path.join(_METRICS_DIR, "runs")
_LEDGER_DB = os.path.join(_METRICS_DIR, "decision_ledger.db")
_COVERAGE_JSON = os.path.join(_METRICS_DIR, "coverage.json")
_TEST_RESULTS_XML = os.path.join(_METRICS_DIR, "test-results.xml")
def _iso8601_now():
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _init_store(db_path=None):
"""Create the fact/dim tables in the SQLite cold store."""
if db_path is None:
db_path = _STORE_PATH
os.makedirs(os.path.dirname(db_path), exist_ok=True)
conn = sqlite3.connect(db_path)
conn.executescript("""
CREATE TABLE IF NOT EXISTS fact_run (
run_id TEXT PRIMARY KEY,
contract_id TEXT,
environment TEXT,
started_at TEXT,
completed_at TEXT,
exit_code INTEGER,
outcome TEXT,
confidence_score REAL,
confidence_band TEXT,
hitl_block INTEGER,
cost_estimate_usd REAL,
decision_id TEXT
);
CREATE TABLE IF NOT EXISTS fact_capability (
capability_id TEXT,
run_id TEXT,
name TEXT,
status TEXT,
tier TEXT,
duration_ms REAL,
detail TEXT,
run_at_utc TEXT,
PRIMARY KEY (capability_id, run_id)
);
CREATE TABLE IF NOT EXISTS fact_policy_check (
run_id TEXT,
rule_id TEXT,
severity TEXT,
result TEXT,
resource_ref TEXT,
evaluated_at TEXT,
PRIMARY KEY (run_id, rule_id, resource_ref)
);
CREATE TABLE IF NOT EXISTS fact_confidence (
run_id TEXT,
score REAL,
band TEXT,
per_input TEXT,
reason_codes TEXT,
environment TEXT,
computed_at TEXT,
PRIMARY KEY (run_id)
);
CREATE TABLE IF NOT EXISTS fact_test (
run_id TEXT,
total_tests INTEGER,
passed INTEGER,
failed INTEGER,
errors INTEGER,
skipped INTEGER,
duration_s REAL,
coverage_pct REAL,
collected_at TEXT,
PRIMARY KEY (run_id)
);
CREATE TABLE IF NOT EXISTS fact_decision (
decision_id TEXT,
run_id TEXT,
chosen_action TEXT,
confidence REAL,
alternatives TEXT,
human_override INTEGER,
outcome TEXT,
event_time TEXT,
PRIMARY KEY (decision_id)
);
CREATE TABLE IF NOT EXISTS fact_cost_estimate (
run_id TEXT,
delta_usd REAL,
total_monthly_usd REAL,
available INTEGER,
estimated_at TEXT,
PRIMARY KEY (run_id)
);
CREATE TABLE IF NOT EXISTS fact_lifecycle (
module TEXT,
environment TEXT,
phase TEXT,
result TEXT,
duration_ms REAL,
run_at TEXT,
PRIMARY KEY (module, environment, phase, run_at)
);
CREATE TABLE IF NOT EXISTS dim_capability (
capability_id TEXT PRIMARY KEY,
name TEXT,
tier TEXT,
source_milestone TEXT
);
CREATE TABLE IF NOT EXISTS dim_milestone (
milestone TEXT PRIMARY KEY,
phase INTEGER,
tag TEXT,
completed_at TEXT
);
""")
conn.commit()
conn.close()
def collect_regression_report(db_path=None, report_path=None):
"""Read REGRESSION_REPORT.json → fact_capability + dim_capability."""
if db_path is None:
db_path = _STORE_PATH
if report_path is None:
report_path = _REGRESSION_REPORT
if not os.path.isfile(report_path):
return 0
_init_store(db_path)
with open(report_path) as f:
report = json.load(f)
run_id = report.get("run_id", f"regr-{report.get('run_at_utc','')}")
run_at = report.get("run_at_utc", _iso8601_now())
milestone = report.get("milestone", "")
conn = sqlite3.connect(db_path)
for result in report.get("results", []):
cap_id = result.get("capability_id", "")
conn.execute("""
INSERT OR REPLACE INTO fact_capability
(capability_id, run_id, name, status, tier, duration_ms, detail, run_at_utc)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (cap_id, run_id, result.get("name", ""), result.get("status", ""),
result.get("tier", ""), result.get("duration_ms", 0),
result.get("detail", ""), run_at))
conn.execute("""
INSERT OR REPLACE INTO dim_capability
(capability_id, name, tier, source_milestone)
VALUES (?, ?, ?, ?)
""", (cap_id, result.get("name", ""), result.get("tier", ""), milestone))
conn.execute("""
INSERT OR REPLACE INTO dim_milestone
(milestone, phase, tag, completed_at)
VALUES (?, ?, ?, ?)
""", (milestone, report.get("phase", 0), "", run_at))
conn.commit()
conn.close()
return len(report.get("results", []))
def collect_run_manifests(db_path=None, runs_dir=None):
"""Read per-run manifests from metrics/runs/*.json → fact_run."""
if db_path is None:
db_path = _STORE_PATH
if runs_dir is None:
runs_dir = _RUNS_DIR
if not os.path.isdir(runs_dir):
return 0
_init_store(db_path)
count = 0
conn = sqlite3.connect(db_path)
for fname in sorted(os.listdir(runs_dir)):
if not fname.endswith(".json"):
continue
fpath = os.path.join(runs_dir, fname)
if os.path.isdir(fpath):
continue
with open(fpath) as f:
manifest = json.load(f)
run_id = manifest.get("run_id", fname.replace(".json", ""))
conf = manifest.get("confidence", {})
hitl = manifest.get("hitl", {})
conn.execute("""
INSERT OR REPLACE INTO fact_run
(run_id, contract_id, environment, started_at, completed_at,
exit_code, outcome, confidence_score, confidence_band,
hitl_block, cost_estimate_usd, decision_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (run_id, manifest.get("contract_id", ""), manifest.get("environment", ""),
manifest.get("started_at", ""), manifest.get("completed_at", ""),
manifest.get("exit_code", 0), manifest.get("outcome", ""),
conf.get("score", 0), conf.get("band", ""),
1 if hitl.get("block") else 0,
manifest.get("cost_estimate_usd", 0), manifest.get("decision_id", "")))
count += 1
conn.commit()
conn.close()
return count
def collect_decision_ledger(db_path=None, ledger_db=None):
"""Read the Decision Ledger SQLite → fact_decision."""
if db_path is None:
db_path = _STORE_PATH
if ledger_db is None:
ledger_db = _LEDGER_DB
if not os.path.isfile(ledger_db):
return 0
_init_store(db_path)
ledger_conn = sqlite3.connect(ledger_db)
rows = ledger_conn.execute(
"SELECT event_type, run_id, event_time, payload FROM decision_ledger WHERE event_type = 'nova.ai.decision.made' ORDER BY seq"
).fetchall()
ledger_conn.close()
conn = sqlite3.connect(db_path)
count = 0
for etype, run_id, event_time, payload_json in rows:
payload = json.loads(payload_json)
data = payload.get("data", {})
decision_id = data.get("decision_id", run_id)
conn.execute("""
INSERT OR REPLACE INTO fact_decision
(decision_id, run_id, chosen_action, confidence, alternatives,
human_override, outcome, event_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (decision_id, run_id, data.get("chosen_action", ""),
data.get("confidence", 0), json.dumps(data.get("alternatives", {})),
1 if data.get("human_override") else 0,
data.get("outcome", "pending"), event_time))
count += 1
conn.commit()
conn.close()
return count
def collect_test_results(db_path=None, junit_path=None, coverage_path=None):
"""Read junit XML + coverage.json → fact_test."""
if db_path is None:
db_path = _STORE_PATH
if junit_path is None:
junit_path = _TEST_RESULTS_XML
if coverage_path is None:
coverage_path = _COVERAGE_JSON
if not os.path.isfile(junit_path):
return 0
_init_store(db_path)
run_id = f"test-{_iso8601_now()}"
total = passed = failed = errors = skipped = 0
duration = 0.0
try:
tree = ET.parse(junit_path)
root = tree.getroot()
for suite in root.iter("testsuite"):
total += int(suite.get("tests", 0))
failed += int(suite.get("failures", 0))
errors += int(suite.get("errors", 0))
skipped += int(suite.get("skipped", 0))
duration += float(suite.get("time", 0))
passed = total - failed - errors - skipped
except Exception:
pass
coverage_pct = 0.0
if os.path.isfile(coverage_path):
try:
with open(coverage_path) as f:
cov = json.load(f)
coverage_pct = cov.get("totals", {}).get("percent_covered", 0.0)
except Exception:
pass
conn = sqlite3.connect(db_path)
conn.execute("""
INSERT OR REPLACE INTO fact_test
(run_id, total_tests, passed, failed, errors, skipped, duration_s, coverage_pct, collected_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (run_id, total, passed, failed, errors, skipped, duration, coverage_pct, _iso8601_now()))
conn.commit()
conn.close()
return 1
def collect_lifecycle_reports(db_path=None, lifecycle_dir=None):
"""Read metrics/lifecycle/*.json → fact_lifecycle."""
if db_path is None:
db_path = _STORE_PATH
if lifecycle_dir is None:
lifecycle_dir = os.path.join(_METRICS_DIR, "lifecycle")
if not os.path.isdir(lifecycle_dir):
return 0
_init_store(db_path)
count = 0
conn = sqlite3.connect(db_path)
for fname in sorted(os.listdir(lifecycle_dir)):
if not fname.endswith(".json"):
continue
fpath = os.path.join(lifecycle_dir, fname)
with open(fpath) as f:
report = json.load(f)
conn.execute("""
INSERT OR REPLACE INTO fact_lifecycle
(module, environment, phase, result, duration_ms, run_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (report.get("module", ""), report.get("environment", ""),
report.get("phase", ""), report.get("result", ""),
report.get("duration_ms", 0), report.get("run_at", _iso8601_now())))
count += 1
conn.commit()
conn.close()
return count
def collect_all(db_path=None):
"""Run all collectors. Returns a summary dict."""
if db_path is None:
db_path = _STORE_PATH
_init_store(db_path)
summary = {
"capabilities": collect_regression_report(db_path),
"runs": collect_run_manifests(db_path),
"decisions": collect_decision_ledger(db_path),
"tests": collect_test_results(db_path),
"lifecycle": collect_lifecycle_reports(db_path),
"collected_at": _iso8601_now(),
}
return summary
if __name__ == "__main__":
result = collect_all()
print(json.dumps(result, indent=2))
-257
View File
@@ -1,257 +0,0 @@
"""Nova Decision Ledger — SQLite append-only hash-chain (REQ-188, D-121).
Extends outbox_writer.py to emit to a SQLite append-only table with a hash
chain (prev_hash + own hash, SHA-256). Stores ai.decision.made events
(decision_id=run_id, chosen_action=band, confidence=score,
alternatives=perInput, human_override=HITL block) with outcome backfill
from apply.completed. Also stores attestation.recorded events (D-132).
Honors D-083 (no S3 Object Lock/JWS local SQLite hash-chain only).
D-120: Nova-native (SQLite, no QLDB).
D-128: metrics/ at repo root.
"""
import datetime
import hashlib
import json
import os
import sqlite3
import sys
_LEDGER_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
"metrics", "decision_ledger.db",
)
_GENESIS_HASH = "GENESIS"
def _iso8601_now():
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _canonical_hash(event):
"""SHA-256 over canonical JSON (sort_keys, compact separators)."""
canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
def _init_db(db_path=None):
"""Create the ledger table if it doesn't exist."""
if db_path is None:
db_path = _LEDGER_PATH
os.makedirs(os.path.dirname(db_path), exist_ok=True)
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS decision_ledger (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT NOT NULL,
event_type TEXT NOT NULL,
run_id TEXT NOT NULL,
contract_id TEXT,
environment TEXT,
event_time TEXT NOT NULL,
payload TEXT NOT NULL,
prev_hash TEXT NOT NULL,
hash TEXT NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_run_id ON decision_ledger(run_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_event_type ON decision_ledger(event_type)")
conn.commit()
conn.close()
def _get_last_hash(db_path=None):
"""Get the hash of the last row in the ledger (or GENESIS if empty)."""
if db_path is None:
db_path = _LEDGER_PATH
conn = sqlite3.connect(db_path)
row = conn.execute("SELECT hash FROM decision_ledger ORDER BY seq DESC LIMIT 1").fetchone()
conn.close()
return row[0] if row else _GENESIS_HASH
def append(event, db_path=None):
"""Append an event to the Decision Ledger with hash-chain integrity.
Args:
event: a CloudEvents 1.0 envelope dict (from event_envelope.make_event)
db_path: path to the SQLite ledger
Returns:
The row dict (seq, event_id, event_type, run_id, hash, prev_hash).
"""
if db_path is None:
db_path = _LEDGER_PATH
_init_db(db_path)
prev_hash = _get_last_hash(db_path)
event_hash = _canonical_hash(event)
platform = event.get("platform", {})
data = event.get("data", {})
conn = sqlite3.connect(db_path)
conn.execute("BEGIN IMMEDIATE")
cursor = conn.execute(
"""INSERT INTO decision_ledger
(event_id, event_type, run_id, contract_id, environment, event_time, payload, prev_hash, hash)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
event.get("id", ""),
event.get("type", ""),
platform.get("run_id", ""),
platform.get("contract_id", ""),
platform.get("environment", ""),
event.get("time", _iso8601_now()),
json.dumps(event, sort_keys=True),
prev_hash,
event_hash,
),
)
seq = cursor.lastrowid
conn.commit()
conn.close()
return {"seq": seq, "event_id": event.get("id", ""), "event_type": event.get("type", ""),
"run_id": platform.get("run_id", ""), "hash": event_hash, "prev_hash": prev_hash}
def verify_chain(db_path=None):
"""Verify the hash chain integrity. Returns (ok, broken_count, details).
Recomputes each row's hash from its payload and checks:
1. The stored hash matches the recomputed hash.
2. The prev_hash matches the previous row's hash.
"""
if db_path is None:
db_path = _LEDGER_PATH
_init_db(db_path)
conn = sqlite3.connect(db_path)
rows = conn.execute("SELECT seq, hash, prev_hash, payload FROM decision_ledger ORDER BY seq").fetchall()
conn.close()
if not rows:
return True, 0, "empty ledger"
broken = 0
details = []
prev_hash = _GENESIS_HASH
for seq, stored_hash, stored_prev, payload_json in rows:
event = json.loads(payload_json)
recomputed = _canonical_hash(event)
if recomputed != stored_hash:
broken += 1
details.append(f"seq={seq}: hash mismatch (stored={stored_hash[:12]}... recomputed={recomputed[:12]}...)")
if stored_prev != prev_hash:
broken += 1
details.append(f"seq={seq}: prev_hash mismatch (expected={prev_hash[:12]}... got={stored_prev[:12]}...)")
prev_hash = stored_hash
return broken == 0, broken, "; ".join(details) if details else "chain intact"
def query_by_run(run_id, db_path=None):
"""Query all ledger entries for a given run_id."""
if db_path is None:
db_path = _LEDGER_PATH
_init_db(db_path)
conn = sqlite3.connect(db_path)
rows = conn.execute(
"SELECT seq, event_type, event_time, payload FROM decision_ledger WHERE run_id = ? ORDER BY seq",
(run_id,),
).fetchall()
conn.close()
return [{"seq": r[0], "event_type": r[1], "event_time": r[2], "payload": json.loads(r[3])} for r in rows]
def stats(db_path=None):
"""Return ledger statistics."""
if db_path is None:
db_path = _LEDGER_PATH
_init_db(db_path)
conn = sqlite3.connect(db_path)
total = conn.execute("SELECT COUNT(*) FROM decision_ledger").fetchone()[0]
by_type = conn.execute("SELECT event_type, COUNT(*) FROM decision_ledger GROUP BY event_type").fetchall()
by_env = conn.execute("SELECT environment, COUNT(*) FROM decision_ledger GROUP BY environment").fetchall()
conn.close()
return {
"total": total,
"by_event_type": dict(by_type),
"by_environment": dict(by_env),
}
def export_since(since_iso, fmt="json", db_path=None):
"""Export ledger entries since a given ISO8601 timestamp."""
if db_path is None:
db_path = _LEDGER_PATH
_init_db(db_path)
conn = sqlite3.connect(db_path)
rows = conn.execute(
"SELECT seq, event_type, run_id, event_time, payload FROM decision_ledger WHERE event_time >= ? ORDER BY seq",
(since_iso,),
).fetchall()
conn.close()
entries = [{"seq": r[0], "event_type": r[1], "run_id": r[2], "event_time": r[3], "payload": json.loads(r[4])} for r in rows]
if fmt == "csv":
import csv
import io
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=["seq", "event_type", "run_id", "event_time", "payload"])
writer.writeheader()
for e in entries:
e["payload"] = json.dumps(e["payload"])
writer.writerow(e)
return buf.getvalue()
return json.dumps(entries, indent=2)
def replay_run(run_id, db_path=None):
"""Reconstruct a run's full event sequence from the ledger.
Prints the ordered event sequence (run.started -> policy.evaluated ->
confidence.computed -> ai.decision.made -> attestation.recorded ->
run.completed/failed) with the decision's confidence, alternatives,
and outcome.
"""
if db_path is None:
db_path = _LEDGER_PATH
entries = query_by_run(run_id, db_path)
if not entries:
return f"no events found for run_id={run_id}"
lines = [f"=== Replay: run_id={run_id} ({len(entries)} events) ==="]
for e in entries:
payload = e["payload"]
data = payload.get("data", {})
etype = e["event_type"]
line = f" [{e['seq']}] {e['event_time']} {etype}"
if etype == "nova.ai.decision.made":
line += f" confidence={data.get('confidence', '?')} band={data.get('chosen_action', '?')} override={data.get('human_override', '?')}"
elif etype == "nova.attestation.recorded":
line += f" env={data.get('environment', '?')} approver={data.get('approver', '?')} result={data.get('result', '?')}"
elif etype == "nova.run.completed":
line += f" exit={data.get('exit_code', '?')} outcome={data.get('outcome', '?')}"
elif etype == "nova.run.failed":
line += f" exit={data.get('exit_code', '?')} outcome=failed"
lines.append(line)
lines.append("=== End replay ===")
return "\n".join(lines)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("usage: decision_ledger.py <verify-chain|stats|query|export|replay> [args]", file=sys.stderr)
sys.exit(2)
cmd = sys.argv[1]
if cmd == "verify-chain":
ok, broken, details = verify_chain()
print(f"chain_ok={ok} broken={broken} details={details}")
sys.exit(0 if ok else 1)
elif cmd == "stats":
print(json.dumps(stats(), indent=2))
elif cmd == "query" and len(sys.argv) >= 3:
print(json.dumps(query_by_run(sys.argv[2]), indent=2))
elif cmd == "export" and len(sys.argv) >= 3:
print(export_since(sys.argv[2]))
elif cmd == "replay" and len(sys.argv) >= 3:
print(replay_run(sys.argv[2]))
else:
print(f"unknown command: {cmd}", file=sys.stderr)
sys.exit(2)
-39
View File
@@ -1,39 +0,0 @@
"""Nova Decision Ledger CLI (REQ-207).
Subcommands: query, verify-chain, stats, export, replay.
Read-only CLI for the Decision Ledger SQLite hash-chain.
"""
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from core.metrics.decision_ledger import query_by_run, verify_chain, stats, export_since, replay_run
def main():
if len(sys.argv) < 2:
print("usage: decision_ledger_cli.py <query|verify-chain|stats|export|replay> [args]", file=sys.stderr)
sys.exit(2)
cmd = sys.argv[1]
if cmd == "query" and len(sys.argv) >= 3:
print(json.dumps(query_by_run(sys.argv[2]), indent=2))
elif cmd == "verify-chain":
ok, broken, details = verify_chain()
print(f"chain_ok={ok} broken={broken} details={details}")
sys.exit(0 if ok else 1)
elif cmd == "stats":
print(json.dumps(stats(), indent=2))
elif cmd == "export" and len(sys.argv) >= 3:
fmt = sys.argv[3] if len(sys.argv) >= 4 else "json"
print(export_since(sys.argv[2], fmt=fmt))
elif cmd == "replay" and len(sys.argv) >= 3:
print(replay_run(sys.argv[2]))
else:
print(f"unknown command: {cmd}", file=sys.stderr)
sys.exit(2)
if __name__ == "__main__":
main()
-98
View File
@@ -1,98 +0,0 @@
"""Nova CloudEvents 1.0 envelope + platform.* semantic conventions (REQ-187).
Defines the standard event envelope for all Nova metrics events. Every
emitter (run_manifest, decision_ledger, confidence_signal, checkov_adapter,
hitl_gates, regression_verify) uses `make_event()` to produce a valid
CloudEvents 1.0 envelope. Events are appended to `metrics/events.jsonl`.
D-120: Nova-native minimal tech (no Kafka/OTel SDK JSONL + SQLite).
D-125: hybrid model existing file signals stay as files; the collector
reads them and emits normalized CloudEvents. New emitters emit directly.
"""
import datetime
import hashlib
import json
import os
import sys
import uuid
METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
EVENTS_LOG = os.path.join(METRICS_DIR, "events.jsonl")
def _iso8601_now():
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def make_event(event_type, run_id, environment, data, contract_id="", source="nova.platform", subject="", actor_type="confidence-gate", actor_id="confidence_signal"):
"""Build a CloudEvents 1.0 envelope with Nova platform.* conventions.
Args:
event_type: e.g. "nova.run.completed", "nova.ai.decision.made"
run_id: the run identifier (e.g. "run-<epoch>")
environment: dev|qa|prod|dr
data: the event payload dict
contract_id: the contract UUID (optional)
source: the event source (default "nova.platform")
subject: the event subject (default "<contract_id>/<env>")
actor_type: the actor type (default "confidence-gate")
actor_id: the actor id (default "confidence_signal")
Returns:
A CloudEvents 1.0 envelope dict.
"""
if not subject:
subject = f"{contract_id}/{environment}" if contract_id else environment
return {
"specversion": "1.0",
"id": str(uuid.uuid4()),
"source": source,
"type": event_type,
"time": _iso8601_now(),
"subject": subject,
"datacontenttype": "application/json",
"platform": {
"tenant_id": "acdl",
"run_id": run_id,
"contract_id": contract_id,
"environment": environment,
"actor": {"type": actor_type, "id": actor_id},
"trace_id": run_id,
},
"data": data,
}
def append_event(event, events_log=None):
"""Append a CloudEvents envelope to the JSONL event log.
Creates the metrics/ directory if it doesn't exist.
"""
if events_log is None:
events_log = EVENTS_LOG
os.makedirs(os.path.dirname(events_log), exist_ok=True)
with open(events_log, "a", encoding="utf-8") as fh:
fh.write(json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n")
def emit(event_type, run_id, environment, data, **kwargs):
"""Make an event + append it to the JSONL log. Convenience wrapper."""
event = make_event(event_type, run_id, environment, data, **kwargs)
append_event(event)
return event
if __name__ == "__main__":
if len(sys.argv) < 4:
print("usage: event_envelope.py <event_type> <run_id> <environment> [data.json]", file=sys.stderr)
sys.exit(2)
_type = sys.argv[1]
_run_id = sys.argv[2]
_env = sys.argv[3]
_data = {}
if len(sys.argv) >= 5 and os.path.isfile(sys.argv[4]):
with open(sys.argv[4]) as f:
_data = json.load(f)
ev = emit(_type, _run_id, _env, _data)
print(json.dumps(ev, indent=2))
-73
View File
@@ -1,73 +0,0 @@
"""Nova Infracost Post-Processor (REQ-187, D-120).
Runs Infracost on `terraform show -json plan.tfplan` (offline, reads plan
JSON, no live AWS). Emits nova.cost.estimated{delta_usd} events. Degrades
gracefully (omits the event, logs a warning) when Infracost CLI is absent
(assumption A6).
run_platform.sh invokes it after the plan stage.
"""
import json
import os
import shutil
import subprocess
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from core.metrics.event_envelope import emit
def _is_infracost_available():
"""Check if the Infracost CLI is on PATH."""
return shutil.which("infracost") is not None
def estimate(plan_json_path, run_id, contract_id, environment):
"""Run Infracost on a terraform plan JSON. Returns the cost estimate dict.
Args:
plan_json_path: path to `terraform show -json plan.tfplan` output
run_id: the run identifier
contract_id: the contract UUID
environment: dev|qa|prod|dr
Returns:
{"delta_usd": float, "total_monthly_usd": float, "available": bool}
or {"available": False} if Infracost is not installed.
"""
if not _is_infracost_available():
sys.stderr.write("[infracost] CLI not found — cost.estimated event omitted (A6 degraded mode)\n")
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
if not os.path.isfile(plan_json_path):
sys.stderr.write(f"[infracost] plan JSON not found: {plan_json_path}\n")
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
try:
result = subprocess.run(
["infracost", "breakdown", "--path", plan_json_path, "--format", "json"],
capture_output=True, text=True, timeout=30,
)
if result.returncode != 0:
sys.stderr.write(f"[infracost] CLI failed: {result.stderr[:200]}\n")
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
breakdown = json.loads(result.stdout)
delta = float(breakdown.get("diffTotalMonthlyCost", 0.0))
total = float(breakdown.get("totalMonthlyCost", 0.0))
estimate_data = {"available": True, "delta_usd": delta, "total_monthly_usd": total}
emit("nova.cost.estimated", run_id, environment, estimate_data, contract_id=contract_id)
return estimate_data
except Exception as exc:
sys.stderr.write(f"[infracost] error: {exc}\n")
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
if __name__ == "__main__":
if len(sys.argv) < 5:
print("usage: infracost_adapter.py <plan_json_path> <run_id> <contract_id> <environment>", file=sys.stderr)
sys.exit(2)
est = estimate(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
print(json.dumps(est, indent=2))
-198
View File
@@ -1,198 +0,0 @@
"""Nova PowerBI Export (REQ-190, P3).
Emits CSV/JSON views to metrics/powerbi/ from the SQLite cold store.
Fact + dimension tables + 8 empty placeholder views for deferred metrics
(with documented schemas ready to fill when their blocking decisions lift).
D-120: Nova-native (CSV/JSON files, no live connector)
D-129: PowerBI ingests via the folder connector
D-128: metrics/ at repo root
"""
import csv
import datetime
import json
import os
import sqlite3
import sys
_METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
_STORE_PATH = os.path.join(_METRICS_DIR, "nova_metrics.db")
_EXPORT_DIR = os.path.join(_METRICS_DIR, "powerbi")
FACT_VIEWS = [
"fact_run",
"fact_capability",
"fact_policy_check",
"fact_confidence",
"fact_test",
"fact_decision",
"fact_cost_estimate",
"fact_lifecycle",
]
DIM_VIEWS = [
"dim_capability",
"dim_milestone",
]
PLACEHOLDER_VIEWS = {
"placeholder_live_infra_health": {
"columns": ["timestamp", "resource_id", "resource_type", "running_count", "healthy", "downtime_seconds"],
"blocking_decision": "D-096",
"description": "Live infrastructure health (ECS running count, ALB 5xx, RPS). Blocked: live AWS torn down.",
},
"placeholder_live_outbox_rate": {
"columns": ["timestamp", "contract_id", "write_latency_ms", "append_count"],
"blocking_decision": "D-096",
"description": "Live outbox write rate / ledger append latency. Blocked: DynamoDB outbox table absent.",
},
"placeholder_tamper_evident_checkpoints": {
"columns": ["timestamp", "checkpoint_id", "jws_signed", "object_lock_enabled"],
"blocking_decision": "D-083",
"description": "Tamper-evident ledger checkpoints / JWS signature rate. Blocked: S3 Object Lock + JWS deferred.",
},
"placeholder_onboarding_funnel": {
"columns": ["timestamp", "consumer_repo", "requested_environment", "status", "granted_at"],
"blocking_decision": "D-113/D-114/D-119",
"description": "Onboarding funnel: requested → granted conversion. Blocked: no auto-grant event.",
},
"placeholder_drift_detection": {
"columns": ["timestamp", "workspace_id", "drift_count", "auto_reverted", "detection_cycle"],
"blocking_decision": "D-096 + no scheduler",
"description": "Drift detection (scheduled terraform plan -detailed-exitcode). Blocked: live AWS + scheduler.",
},
"placeholder_live_cur_reconciliation": {
"columns": ["timestamp", "resource_address", "actual_usd", "baseline_usd", "saved_usd"],
"blocking_decision": "D-096",
"description": "Live cost CUR reconciliation. Blocked: live AWS billing. Infracost pre-apply estimates are in fact_cost_estimate.",
},
"placeholder_sla_downtime": {
"columns": ["timestamp", "service", "uptime_pct", "downtime_minutes", "slo_target"],
"blocking_decision": "D-096",
"description": "SLA / unplanned downtime. Blocked: needs live service uptime monitoring.",
},
"placeholder_predictive_reactive": {
"columns": ["timestamp", "action_id", "label", "trigger", "count"],
"blocking_decision": "future emitter",
"description": "Predictive vs Reactive ratio. Blocked: requires ML anomaly-forecasting service.",
},
}
def _iso8601_now():
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _export_table_csv(conn, table_name, export_dir):
"""Export a SQLite table to a CSV file."""
rows = conn.execute(f"SELECT * FROM {table_name}").fetchall()
if not rows:
return 0
columns = [desc[0] for desc in conn.execute(f"SELECT * FROM {table_name} LIMIT 0").description]
csv_path = os.path.join(export_dir, f"{table_name}.csv")
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(columns)
writer.writerows(rows)
return len(rows)
def _export_table_json(conn, table_name, export_dir):
"""Export a SQLite table to a JSON file."""
rows = conn.execute(f"SELECT * FROM {table_name}").fetchall()
if not rows:
return 0
columns = [desc[0] for desc in conn.execute(f"SELECT * FROM {table_name} LIMIT 0").description]
records = [dict(zip(columns, row)) for row in rows]
json_path = os.path.join(export_dir, f"{table_name}.json")
with open(json_path, "w", encoding="utf-8") as f:
json.dump(records, f, indent=2, default=str)
return len(rows)
def _export_placeholder_csv(view_name, schema, export_dir):
"""Export a placeholder CSV with headers only (no data rows)."""
csv_path = os.path.join(export_dir, f"{view_name}.csv")
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(schema["columns"])
return 0
def _export_placeholder_json(view_name, schema, export_dir):
"""Export a placeholder JSON with schema metadata (no data rows)."""
json_path = os.path.join(export_dir, f"{view_name}.json")
with open(json_path, "w", encoding="utf-8") as f:
json.dump({"schema": schema, "data": []}, f, indent=2)
return 0
def export_all(store_path=None, export_dir=None, fmt="both"):
"""Export all fact/dim tables + placeholder views to CSV and/or JSON.
Args:
store_path: path to the SQLite cold store
export_dir: directory for exported files
fmt: "csv", "json", or "both"
Returns:
Summary dict with export counts.
"""
if store_path is None:
store_path = _STORE_PATH
if export_dir is None:
export_dir = _EXPORT_DIR
os.makedirs(export_dir, exist_ok=True)
summary = {"exported_at": _iso8601_now(), "fact_tables": {}, "dim_tables": {}, "placeholder_views": {}}
if not os.path.isfile(store_path):
summary["error"] = f"SQLite store not found: {store_path}"
for view_name, schema in PLACEHOLDER_VIEWS.items():
if fmt in ("csv", "both"):
_export_placeholder_csv(view_name, schema, export_dir)
if fmt in ("json", "both"):
_export_placeholder_json(view_name, schema, export_dir)
summary["placeholder_views"][view_name] = 0
return summary
conn = sqlite3.connect(store_path)
for table in FACT_VIEWS:
count = 0
try:
if fmt in ("csv", "both"):
count = _export_table_csv(conn, table, export_dir)
if fmt in ("json", "both"):
count = _export_table_json(conn, table, export_dir)
except sqlite3.OperationalError:
count = 0
summary["fact_tables"][table] = count
for table in DIM_VIEWS:
count = 0
try:
if fmt in ("csv", "both"):
count = _export_table_csv(conn, table, export_dir)
if fmt in ("json", "both"):
count = _export_table_json(conn, table, export_dir)
except sqlite3.OperationalError:
count = 0
summary["dim_tables"][table] = count
conn.close()
for view_name, schema in PLACEHOLDER_VIEWS.items():
if fmt in ("csv", "both"):
_export_placeholder_csv(view_name, schema, export_dir)
if fmt in ("json", "both"):
_export_placeholder_json(view_name, schema, export_dir)
summary["placeholder_views"][view_name] = 0
return summary
if __name__ == "__main__":
result = export_all()
print(json.dumps(result, indent=2))
-137
View File
@@ -1,137 +0,0 @@
"""Nova Per-Run Manifest Writer (REQ-187).
Emits nova.run.started, nova.run.completed, nova.run.failed events with
(run_id, contractId, env, stages x durations, exit, confidence, HITL block
count). Writes metrics/runs/<run_id>.json. scripts/run_platform.sh invokes
the writer at run start + run end.
D-120: Nova-native (JSONL events + JSON manifest file, no Kafka).
D-128: metrics/ at repo root.
"""
import datetime
import json
import os
import sys
import time
import uuid
_METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
_RUNS_DIR = os.path.join(_METRICS_DIR, "runs")
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from core.metrics.event_envelope import emit, make_event, append_event
def _iso8601_now():
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _run_id():
return f"run-{int(time.time())}-{uuid.uuid4().hex[:8]}"
def start_run(contract_id, environment, stages=None):
"""Emit nova.run.started + return the run_id."""
run_id = _run_id()
data = {
"contract_id": contract_id,
"environment": environment,
"started_at": _iso8601_now(),
"stages": stages or [],
}
emit("nova.run.started", run_id, environment, data, contract_id=contract_id)
return run_id
def complete_run(run_id, contract_id, environment, stages, exit_code, confidence=None, hitl=None, policy=None, cost_estimate_usd=None, decision_id=None):
"""Emit nova.run.completed + write the per-run manifest JSON.
Args:
run_id: the run identifier from start_run()
contract_id: the contract UUID
environment: dev|qa|prod|dr
stages: list of {name, duration_ms, exit_code, error?}
exit_code: the overall run exit code
confidence: optional {score, band, perInput}
hitl: optional {gate, result, block}
policy: optional {passed, failed, skipped}
cost_estimate_usd: optional float
decision_id: optional string (links to the Decision Ledger)
"""
started_at = stages[0].get("started_at", _iso8601_now()) if stages else _iso8601_now()
completed_at = _iso8601_now()
outcome = "succeeded" if exit_code == 0 else "failed"
manifest = {
"run_id": run_id,
"contract_id": contract_id,
"environment": environment,
"started_at": started_at,
"completed_at": completed_at,
"exit_code": exit_code,
"stages": stages,
"outcome": outcome,
}
if confidence:
manifest["confidence"] = confidence
if hitl:
manifest["hitl"] = hitl
if policy:
manifest["policy"] = policy
if cost_estimate_usd is not None:
manifest["cost_estimate_usd"] = cost_estimate_usd
if decision_id:
manifest["decision_id"] = decision_id
os.makedirs(_RUNS_DIR, exist_ok=True)
manifest_path = os.path.join(_RUNS_DIR, f"{run_id}.json")
with open(manifest_path, "w", encoding="utf-8") as fh:
json.dump(manifest, fh, indent=2, sort_keys=True)
event_type = "nova.run.completed" if exit_code == 0 else "nova.run.failed"
emit(event_type, run_id, environment, manifest, contract_id=contract_id)
return manifest
def persist_run_artifacts(run_id, work_dir):
"""Copy ephemeral $WORK/*.json to metrics/runs/<run_id>/ as durable artifacts.
Args:
run_id: the run identifier
work_dir: the $WORK directory (e.g. /tmp/nova_platform_run)
"""
if not work_dir or not os.path.isdir(work_dir):
return []
dest = os.path.join(_RUNS_DIR, run_id)
os.makedirs(dest, exist_ok=True)
copied = []
for fname in ("pcr.json", "signal.json", "event.json", "outbox_item.json", "stack.json", "checkov.json"):
src = os.path.join(work_dir, fname)
if os.path.isfile(src):
import shutil
shutil.copy2(src, os.path.join(dest, fname))
copied.append(fname)
return copied
if __name__ == "__main__":
if len(sys.argv) < 4:
print("usage: run_manifest.py <start|complete|persist> <contract_id> <environment> [run_id] [work_dir]", file=sys.stderr)
sys.exit(2)
action = sys.argv[1]
cid = sys.argv[2]
env = sys.argv[3]
if action == "start":
rid = start_run(cid, env)
print(rid)
elif action == "complete":
rid = sys.argv[4] if len(sys.argv) >= 5 else _run_id()
m = complete_run(rid, cid, env, [], 0)
print(json.dumps(m, indent=2))
elif action == "persist":
rid = sys.argv[4] if len(sys.argv) >= 5 else ""
wd = sys.argv[5] if len(sys.argv) >= 6 else ""
copied = persist_run_artifacts(rid, wd)
print(json.dumps({"copied": copied}))
-167
View File
@@ -1,167 +0,0 @@
"""Nova Trust Snapshot Report (REQ-211, P4).
Emits metrics/TRUST_SNAPSHOT.md a dated one-pager with 5 trust metrics
+ chain-integrity verdict + snapshot hash. Runnable on demand or at
milestone complete.
Reads from: metrics/decision_ledger.db, metrics/nova_metrics.db,
.ciagent/REGRESSION_REPORT.json.
"""
import datetime
import hashlib
import json
import os
import sqlite3
import sys
_METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
_LEDGER_DB = os.path.join(_METRICS_DIR, "decision_ledger.db")
_STORE_DB = os.path.join(_METRICS_DIR, "nova_metrics.db")
_REGRESSION_REPORT = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), ".ciagent", "REGRESSION_REPORT.json")
_SNAPSHOT_PATH = os.path.join(_METRICS_DIR, "TRUST_SNAPSHOT.md")
def _iso8601_now():
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _get_decision_ledger_coverage(ledger_db=None):
"""Decision Ledger Coverage: rows with outcome ≠ 'pending' ÷ total."""
if ledger_db is None:
ledger_db = _LEDGER_DB
if not os.path.isfile(ledger_db):
return 0.0, 0, 0
from core.metrics.decision_ledger import stats, verify_chain
s = stats(ledger_db)
total = s.get("total", 0)
if total == 0:
return 0.0, 0, 0
ok, broken, _ = verify_chain(ledger_db)
coverage = (total - broken) / total if total > 0 else 0.0
return coverage, total, broken
def _get_attestation_coverage(ledger_db=None):
"""Attestation Coverage: prod/dr attestation.recorded events ÷ total prod/dr runs."""
if ledger_db is None:
ledger_db = _LEDGER_DB
if not os.path.isfile(ledger_db):
return 0.0, 0, 0
conn = sqlite3.connect(ledger_db)
attestations = conn.execute(
"SELECT COUNT(*) FROM decision_ledger WHERE event_type = 'nova.attestation.recorded'"
).fetchone()[0]
conn.close()
return 1.0 if attestations > 0 else 0.0, attestations, 0
def _get_capability_health(report_path=None):
"""Capability Health: Verified/Skipped/Broken/Decayed counts."""
if report_path is None:
report_path = _REGRESSION_REPORT
if not os.path.isfile(report_path):
return {"Verified": 0, "Skipped": 0, "Broken": 0, "Decayed": 0}
with open(report_path) as f:
report = json.load(f)
return report.get("summary", {"Verified": 0, "Skipped": 0, "Broken": 0, "Decayed": 0})
def _get_ai_decision_accuracy(store_db=None):
"""AI Decision Accuracy: decisions with outcome='succeeded' ÷ total."""
if store_db is None:
store_db = _STORE_DB
if not os.path.isfile(store_db):
return 0.0, 0, 0
conn = sqlite3.connect(store_db)
try:
total = conn.execute("SELECT COUNT(*) FROM fact_decision").fetchone()[0]
succeeded = conn.execute("SELECT COUNT(*) FROM fact_decision WHERE outcome = 'succeeded'").fetchone()[0]
except sqlite3.OperationalError:
conn.close()
return 0.0, 0, 0
conn.close()
accuracy = succeeded / total if total > 0 else 0.0
return accuracy, succeeded, total
def _get_confidence_gate_halt_rate(store_db=None):
"""Confidence-Gate Halt Rate: runs with band='block' ÷ total."""
if store_db is None:
store_db = _STORE_DB
if not os.path.isfile(store_db):
return 0.0, 0, 0
conn = sqlite3.connect(store_db)
try:
total = conn.execute("SELECT COUNT(*) FROM fact_confidence").fetchone()[0]
halted = conn.execute("SELECT COUNT(*) FROM fact_confidence WHERE band = 'block'").fetchone()[0]
except sqlite3.OperationalError:
conn.close()
return 0.0, 0, 0
conn.close()
rate = halted / total if total > 0 else 0.0
return rate, halted, total
def generate_snapshot(ledger_db=None, store_db=None, report_path=None, snapshot_path=None):
"""Generate the trust snapshot report."""
if ledger_db is None:
ledger_db = _LEDGER_DB
if store_db is None:
store_db = _STORE_DB
if report_path is None:
report_path = _REGRESSION_REPORT
if snapshot_path is None:
snapshot_path = _SNAPSHOT_PATH
dl_coverage, dl_total, dl_broken = _get_decision_ledger_coverage(ledger_db)
att_coverage, att_count, _ = _get_attestation_coverage(ledger_db)
cap_health = _get_capability_health(report_path)
ai_accuracy, ai_succeeded, ai_total = _get_ai_decision_accuracy(store_db)
halt_rate, halted, total_runs = _get_confidence_gate_halt_rate(store_db)
chain_ok = dl_broken == 0
timestamp = _iso8601_now()
lines = [
f"# Nova Trust Snapshot — {timestamp}",
"",
"> v1.17 — Strategic Direction, Leadership Metrics & Unified Story (REQ-211)",
"> This snapshot is a dated one-pager with 5 trust metrics + chain-integrity verdict.",
"",
"## Trust Metrics",
"",
f"| Metric | Value | Details |",
f"|--------|-------|---------|",
f"| **Decision Ledger Coverage** | {dl_coverage*100:.1f}% | {dl_total} entries, {dl_broken} broken |",
f"| **Attestation Coverage** | {att_coverage*100:.1f}% | {att_count} attestation events |",
f"| **Capability Health** | {cap_health.get('Verified',0)}V / {cap_health.get('Skipped',0)}S / {cap_health.get('Broken',0)}B / {cap_health.get('Decayed',0)}D | from REGRESSION_REPORT.json |",
f"| **AI Decision Accuracy** | {ai_accuracy*100:.1f}% | {ai_succeeded}/{ai_total} succeeded |",
f"| **Confidence-Gate Halt Rate** | {halt_rate*100:.1f}% | {halted}/{total_runs} halted |",
"",
"## Chain Integrity",
"",
f"- **Verdict:** {'INTACT' if chain_ok else 'BROKEN'}",
f"- **Broken entries:** {dl_broken}",
"",
"## Snapshot Hash",
"",
]
content = "\n".join(lines)
snapshot_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()[:16]
lines.append(f"`{snapshot_hash}`")
content = "\n".join(lines)
os.makedirs(os.path.dirname(snapshot_path), exist_ok=True)
with open(snapshot_path, "w", encoding="utf-8") as f:
f.write(content)
return {"snapshot_path": snapshot_path, "hash": snapshot_hash, "chain_ok": chain_ok,
"dl_coverage": dl_coverage, "att_coverage": att_coverage,
"cap_health": cap_health, "ai_accuracy": ai_accuracy, "halt_rate": halt_rate}
if __name__ == "__main__":
result = generate_snapshot()
print(json.dumps(result, indent=2))
-131
View File
@@ -1,131 +0,0 @@
#!/usr/bin/env python3
"""Nova Onboarding — auto-generate an environment binding file (P19, REQ-183).
Given a consumer onboarding request (validated against
schemas/onboarding.schema.json), generate a ``<env>.json`` environment
binding file from the dev template, filling in the consumer's ownerId +
billingTag. The generated file is a starting point for the platform team
(or a future automation) to bind to a real AWS account.
This is the "request path" half of the no-humans onboarding flow (D-113).
Real AWS account/network/state provisioning is a future feature milestone;
this module removes the human handoff from the *request* step by
generating the binding file + emitting a git patch / PR-branch instruction.
Usage:
python3 core/onboarding.py <request.json> [--out <env.json>]
python3 core/onboarding.py --request '{"consumerRepo":"acdl/c","requestedEnvironment":"qa","ownerId":"team-a","billingTag":"cc-a"}'
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any, Dict
def _repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def _load_template_env(template_env: str = "dev", root: Path | None = None) -> Dict[str, Any]:
"""Load the template environment JSON (defaults to dev.json)."""
root = root or _repo_root()
env_path = root / "core" / "environments" / f"{template_env}.json"
if not env_path.is_file():
raise FileNotFoundError(f"template environment {env_path} not found")
return json.loads(env_path.read_text())
def generate_env_file(
request: Dict[str, Any],
template_env: str = "dev",
root: Path | None = None,
) -> Dict[str, Any]:
"""Generate an environment binding dict from a consumer onboarding request.
The generated dict is a copy of the template env with:
- ``name`` the requested environment
- ``description`` notes the consumer + owner
- ``account_id`` placeholder (000000000000) for the platform team
to fill with the real account
- ``ownerId`` + ``billingTag`` from the request (for ABAC + cost)
The dict validates against schemas/environment.schema.json.
Returns the generated env dict.
"""
template = _load_template_env(template_env, root)
requested = request["requestedEnvironment"]
owner = request["ownerId"]
billing = request["billingTag"]
consumer = request["consumerRepo"]
env = dict(template)
env["name"] = requested
env["description"] = (
f"Auto-generated binding for {consumer} (owner={owner}, "
f"billing={billing}). Replace account_id with the real "
f"{requested} account before deploying."
)
env["account_id"] = "000000000000" # placeholder — platform team fills
env["ownerId"] = owner
env["billingTag"] = billing
return env
def _onboarding_request_message(env_name: str) -> str:
"""P19 (REQ-183): the rebranded Nova onboarding message — self-service
request path, no longer routes to 'contact the platform team'."""
return (
"=== Nova Environment Onboarding ===\n"
f"No environment named '{env_name}' is bound to this repository.\n\n"
"Nova environments are platform-managed. The platform provisions on\n"
"your behalf:\n"
" - an AWS account (or a scoped partition of one)\n"
" - a network (VPC + subnets)\n"
" - a state backend (an S3 bucket + DynamoDB lock table)\n"
" - an IAM role surfaced to your repo via attribute-based\n"
" authorization (ABAC)\n\n"
"You do not provide an AWS account, VPC, subnet, or state bucket.\n\n"
"To request an environment (self-service):\n"
" 1. Submit an onboarding request to the Nova Lambda\n"
" (action: onboard_consumer) with your repo name + the\n"
" environment name you need (e.g. 'dev').\n"
" 2. The platform generates an environment binding + opens a PR.\n"
" 3. The platform provisions the account/network/state/role and\n"
" grants the ABAC role. Your next pipeline run proceeds.\n\n"
"Run: python3 core/onboarding.py --request '{...}' to generate a\n"
"binding file locally, or POST to the Lambda onboard_consumer action.\n"
"===================================\n"
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Generate an env binding from an onboarding request.")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("request_file", nargs="?", help="path to a request JSON file")
group.add_argument("--request", help="inline request JSON string")
parser.add_argument("--out", help="output path for the generated env JSON (default: stdout)")
parser.add_argument("--template-env", default="dev", help="template environment (default: dev)")
args = parser.parse_args(argv)
if args.request:
request = json.loads(args.request)
else:
request = json.loads(Path(args.request_file).read_text())
env = generate_env_file(request, template_env=args.template_env)
env_json = json.dumps(env, indent=2) + "\n"
if args.out:
Path(args.out).write_text(env_json)
print(f"wrote: {args.out}")
else:
print(env_json)
return 0
if __name__ == "__main__":
sys.exit(main())
+3 -3
View File
@@ -1,11 +1,11 @@
"""Nova Outbox Writer — write an evidence event to the DynamoDB outbox. """ACDL Outbox Writer — write an evidence event to the DynamoDB outbox.
ARCHITECTURE.md §9: DynamoDB outbox, RPO=0 (synchronous write before ARCHITECTURE.md §9: DynamoDB outbox, RPO=0 (synchronous write before
ack). The event is hash-chained (SHA-256 over canonical JSON); the first ack). The event is hash-chained (SHA-256 over canonical JSON); the first
event has prev_event_hash="GENESIS". D-P10-3: the spike writes ONE event has prev_event_hash="GENESIS". D-P10-3: the spike writes ONE
CONFIDENCE_COMPUTED event. CONFIDENCE_COMPUTED event.
The outbox table (Phase 08): nova-outbox, PAY_PER_REQUEST, PK contractId, The outbox table (Phase 08): acdl-outbox, PAY_PER_REQUEST, PK contractId,
SK eventType#eventTs, TTL expire_at = now + 365d (D-044). SK eventType#eventTs, TTL expire_at = now + 365d (D-044).
CLI: outbox_writer.py <event.json> (uses AWS creds from env) CLI: outbox_writer.py <event.json> (uses AWS creds from env)
@@ -20,7 +20,7 @@ import sys
import boto3 import boto3
OUTBOX_TABLE = "nova-outbox" OUTBOX_TABLE = "acdl-outbox"
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1") REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")

Some files were not shown because too many files have changed in this diff Show More