Compare commits
166 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e519743b5 | |||
| 36c8ae9a80 | |||
| ec53302014 | |||
| 7e6ed25ea9 | |||
| f753353ad4 | |||
| f020178c15 | |||
| 5a75075616 | |||
| 42c579f7b8 | |||
| ab7171236a | |||
| fe635c17d5 | |||
| d069654367 | |||
| 25427250ad | |||
| eca1181716 | |||
| 0920550ae5 | |||
| d8240588c9 | |||
| 5dc97673e5 | |||
| 956cf91ce0 | |||
| a7a93d95d1 | |||
| afca994511 | |||
| e63c0cb36e | |||
| 3512261051 | |||
| 14c11027a8 | |||
| e07a210c70 | |||
| 9b8ab75b85 | |||
| 9bc37301ba | |||
| 5476f8eb24 | |||
| 863f482f9c | |||
| 66b13a6d0c | |||
| 485d105bcd | |||
| df426afd6a | |||
| 9114227ef1 | |||
| c9ace0af6e | |||
| a47c16245a | |||
| 74e9d4d887 | |||
| 818e285fac | |||
| b8fbd995a9 | |||
| ea44fdb9d6 | |||
| e14818875c | |||
| f496dd9c24 | |||
| 0d22b89a7b | |||
| 75e9e479db | |||
| d199204367 | |||
| 6a64b2b337 | |||
| 9274b4b87f | |||
| 25ddc894c2 | |||
| 156431c80a | |||
| 631244458f | |||
| 81b731ed17 | |||
| cc6071ee53 | |||
| d1ff6934c6 | |||
| ccbccb02ac | |||
| 574e6cb189 | |||
| 358aa62c3a | |||
| ff416777f9 | |||
| 94891af6ee | |||
| 072ac83ef6 | |||
| c6036ca433 | |||
| 38eb01d266 | |||
| 0404988465 | |||
| dbca694f55 | |||
| 71f0f1a05d | |||
| ce751313a7 | |||
| 5b5e24d535 | |||
| 85c500e45a | |||
| 301aa2c8d8 | |||
| 707a7dbe9b | |||
| e7866fda84 | |||
| 2efed26bb6 | |||
| 5c07e29b90 | |||
| aa868c97ef | |||
| e4a9915891 | |||
| 0ca383dae6 | |||
| ed5ea90654 | |||
| 2273009b95 | |||
| 0d2cbdb423 | |||
| b418d429b5 | |||
| dcba380b52 | |||
| f0bc3be92c | |||
| 0b79b16715 | |||
| 90624be63f | |||
| be51fc15fa | |||
| e3f4ce17d4 | |||
| e0d01ad2ef | |||
| a4c5f332f6 | |||
| 9e20b7ba95 | |||
| 6da538c936 | |||
| 4e03817ea6 | |||
| 951ad56576 | |||
| d882cf0c6e | |||
| 564d4a4ca3 | |||
| c524ad731e | |||
| 8bcf7296d5 | |||
| 81c7a22ddd | |||
| 2c08c778a9 | |||
| 6ffcbe8283 | |||
| 5775a97388 | |||
| b3c75ccec1 | |||
| e891496163 | |||
| 382944c055 | |||
| 71b6a4fa91 | |||
| 0f677641ee | |||
| e3ebbc4978 | |||
| 37b6b6fc14 | |||
| d61a3d1a2f | |||
| 4c8b2b77fc | |||
| 1daae0ac0a | |||
| d048460abf | |||
| 0ad6a88c4b | |||
| eb5b24b88d | |||
| cb1a7071a7 | |||
| e4adb3f09e | |||
| 9415afc739 | |||
| d9b402c283 | |||
| b1cf24873b | |||
| eb43e08367 | |||
| a9c5d67301 | |||
| b054849a99 | |||
| 942185c85b | |||
| 3a7604dec0 | |||
| 814fea6c3c | |||
| 18b03db272 | |||
| 8ed838a955 | |||
| f8616b806e | |||
| fe2ab96b8c | |||
| 50adebb69e | |||
| 97560e3c88 | |||
| 7535c8ceb0 | |||
| abbf8b69fb | |||
| 5907dd259a | |||
| ca7d41c1ad | |||
| f55579bea8 | |||
| 7fc646d773 | |||
| f5b681f31a | |||
| 58fa7a6384 | |||
| f83b974c0e | |||
| 787a6490a5 | |||
| 008adf26b3 | |||
| a420e3b952 | |||
| 3c765c3211 | |||
| e15eea067b | |||
| eb7634da28 | |||
| 13846d553a | |||
| d14f9289da | |||
| d4b8b5e1e9 | |||
| bf8ac0fe49 | |||
| 0e6ecae26d | |||
| 267df4ad0d | |||
| da0de6068a | |||
| 51c3edf458 | |||
| e998d9fa6b | |||
| 7ea58ec1c9 | |||
| d5bae868a4 | |||
| 0bc70a3d95 | |||
| adce478e09 | |||
| 63f3a2b66c | |||
| 1ff942684e | |||
| 6c25ce3900 | |||
| d14b55b774 | |||
| 69ba3d728f | |||
| 533a9d7bcb | |||
| 93c7106cd9 | |||
| 66d7cb9541 | |||
| 59a71d332a | |||
| 66a3c6958e | |||
| da533a8c2f | |||
| 3b1181f39b |
+377
-5
@@ -1,8 +1,8 @@
|
|||||||
# ACDL — Architecture (v1.1 target)
|
# Nova — Architecture (v1.1 target)
|
||||||
|
|
||||||
> Target architecture for the real Agentic Cloud Delivery Platform.
|
> Target architecture for the real Agentic Cloud Delivery Platform (rebranded
|
||||||
> Source of truth for **how**: `docs/architecture.md` (v0.2) is the upstream
|
> Nova in v1.15). Source of truth for **how**: `docs/architecture.md` (v0.2) is the upstream
|
||||||
> draft; this file is the ACDL-repo operating copy, refined at phase
|
> draft; this file is the Nova-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,4 +570,376 @@ 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. (P2–P4 dual-read fallback to `ACDL_LIFECYCLE_MODE`;
|
||||||
|
fallback removed in P5 per the v1.15 addendum.)
|
||||||
|
|
||||||
|
## v1.12 Addendum — Presentation Refinement + CAP-013 Fix
|
||||||
|
|
||||||
|
**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.0–v1.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 P2–P4; 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 (P1–P2, 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).
|
||||||
|
|||||||
+309
-2
@@ -1,4 +1,4 @@
|
|||||||
# ACDL v1.9 — Audit Report
|
# Nova 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,4 +243,311 @@ 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 1–20:** 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 P0–P21, 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.11–v1.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 P0–P21, 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/00–phase/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 1–20 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.1–v1.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 P0–P21, 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---
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 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,4 +1,4 @@
|
|||||||
# ACDL Capability Inventory — v1.1→v1.8 Re-Verification Sweep
|
# Nova 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.
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
{
|
{
|
||||||
"phase": 0,
|
"phase": 0,
|
||||||
"stage": "complete",
|
"stage": "plan",
|
||||||
"milestone": "v1.14",
|
"milestone": "v1.25",
|
||||||
"phase_role": "pre_execution",
|
"phase_role": "pre_execution",
|
||||||
"attempts": 0,
|
"attempts": 0,
|
||||||
"updated_at": "2026-07-29T20:30:00Z"
|
"updated_at": "2026-08-12T16:45:00Z",
|
||||||
|
"project": "acdl",
|
||||||
|
"milestone_complete": false,
|
||||||
|
"tag_line": "v1.24.x",
|
||||||
|
"next_tag": "v1.24.0",
|
||||||
|
"phases": 6,
|
||||||
|
"execution_phases": 4,
|
||||||
|
"requirements_total": 19,
|
||||||
|
"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-304", "REQ-305", "REQ-306", "REQ-307", "REQ-308", "REQ-309"],
|
||||||
|
"notes": "v1.25 PLAN complete — 4 execution phases (P1 engine-core, P2 contract+stack-IR policies, P3 plan-JSON+meta+pipeline, P4 regression+docs) + P5 final review/ship. Wave ordering with parallelization (3-2-2-3 concurrent personas). Each phase is a vertical slice. Tags v1.24.0..v1.24.5."
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# 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
@@ -1,8 +1,8 @@
|
|||||||
# ACDL AWS Cost Report (v1.0 → v1.10)
|
# Nova AWS Cost Report (v1.0 → v1.14)
|
||||||
|
|
||||||
> **Query date:** 2026-07-28
|
> **Query date:** 2026-07-29 (updated v1.14 P19)
|
||||||
> **Source:** AWS Cost Explorer (`ce:GetCostAndUsage`)
|
> **Source:** AWS Cost Explorer (`ce:GetCostAndUsage`)
|
||||||
> **Window:** 2026-07-21 → 2026-07-28 (v1.0 ship → v1.10 complete)
|
> **Window:** 2026-07-21 → 2026-07-29 (v1.0 ship → v1.14 active)
|
||||||
> **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)
|
||||||
|
|
||||||
|
|||||||
+202
-292
@@ -1,306 +1,216 @@
|
|||||||
# CIAgent Grill Report
|
# GRILL — v1.25 kyverno-json Unified Policy Engine
|
||||||
|
|
||||||
## Run: 2026-07-27 19:30 (mode: interactive, focus: all)
|
> Adversarial review of the v1.25 SPECIFY + CLARIFY + RESEARCH + IDEATE +
|
||||||
|
> 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 with conditions (confidence: 0.72)
|
## Verdict: PROCEED (0.86) — 0 escalations, 2 revisions
|
||||||
|
|
||||||
Two escalations must be resolved before the leadership pitch:
|
The milestone is feasible, scoped, and the swap boundary is real. Two
|
||||||
- **G-005 (risks):** 6 cloud capabilities (CAP-017..022) are deploy-unverified.
|
plan revisions are binding (G-Q4, G-Q8) and are already captured in
|
||||||
- **G-008 (budget):** No cost documentation exists despite live AWS resources.
|
PLAN.md. No work is blocked.
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Run: 2026-07-29 20:25 (mode: adversarial, focus: v1.14 NFR plan)
|
## Challenges
|
||||||
|
|
||||||
### Verdict: FEASIBLE WITH BINDING DECISIONS (confidence: 0.72)
|
### G-Q1 — Does kyverno-json violate "platform functions without AI"?
|
||||||
|
|
||||||
The v1.14 milestone is a sound, well-evidenced NFR sweep with a genuine,
|
**Challenge:** NORTH_STAR.md Strategic Objective #2 says "the platform
|
||||||
traceable backlog. Not fundamentally infeasible. Four binding decisions
|
functions without AI." kyverno-json is a new runtime dependency. Is
|
||||||
close plan defects + unverified assumptions that would otherwise re-expose
|
this a real violation, or is the tenet about LLMs (not deterministic
|
||||||
the v1.11 4-VPC failure mode. One escalation (E-001) auto-resolved at full
|
engines)?
|
||||||
autonomy with assumption logging.
|
|
||||||
|
|
||||||
### 9-Axis scores
|
**Verdict:** PROCEED (confidence 0.95). kyverno-json is deterministic
|
||||||
|
(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.
|
||||||
|
|
||||||
| Axis | Confidence | Forcing question (short) |
|
### G-Q2 — Is the PolicyEngine protocol over-engineered for a 2-engine future?
|
||||||
|------|-----------|---------------------------|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
### Binding Decisions
|
**Challenge:** The user asked for a swappable adapter ("we might one
|
||||||
|
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?
|
||||||
|
|
||||||
| ID | Axis | Decision | Confidence |
|
**Verdict:** PROCEED (confidence 0.85). The user *explicitly* asked for
|
||||||
|----|------|----------|-----------|
|
the swap boundary — this is not speculative abstraction, it's a
|
||||||
| 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 |
|
stated requirement. The protocol is minimal (3 methods) and the OPA-
|
||||||
| 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 |
|
equivalent surface is documented (RESEARCH §4.2) — the swap is a known
|
||||||
| 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 |
|
quantity, not a hope. The cost is ~40 lines of Python + a config key;
|
||||||
| 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 |
|
the benefit is a documented, tested swap boundary that a future
|
||||||
| 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 |
|
milestone implements without re-architecting. This is the moat (NORTH
|
||||||
| 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 |
|
STAR Objective #2 — provable trust via a replaceable substrate, not a
|
||||||
|
vendor lock-in).
|
||||||
|
|
||||||
### Escalations
|
### G-Q3 — Does wrapping checkov findings in kyverno-json meta-policies break the MTTR < 60s target?
|
||||||
|
|
||||||
- **[E-001] risks** — P8 state-bucket continuity re-exposes the v1.11 4-VPC
|
**Challenge:** NORTH_STAR.md MTTR target: < 60s p95. Adding a second
|
||||||
root cause. G-102 proposes a binding mitigation (bind fallback + wire env
|
engine pass over the terraform plan + a meta-policy pass over the
|
||||||
into workflow), but the residual risk (a future full-mode lifecycle run
|
merged PCR list adds latency. Does this break the target?
|
||||||
with a misconfigured env orphans live state and re-creates resources)
|
|
||||||
cannot be reduced below 0.20 by plan-level decisions alone. **Auto-
|
**Verdict:** PROCEED (confidence 0.88). RESEARCH §5 analyzes: the kj
|
||||||
resolved at full autonomy (D-101):** accept the residual risk; G-102's
|
pass over plan JSON is < 1s (Go binary startup + JMESPath over a small
|
||||||
binding mitigation (fallback bound to live account ID + workflow env
|
plan); it runs **in parallel** with Checkov (REQ-301), so wall-clock
|
||||||
wiring) is the control. The lifecycle pipeline defaults to plan-only
|
impact is `max(checkov_time, kj_time)` ≈ checkov_time. Meta-policies
|
||||||
(REQ-134) — full-mode runs are workflow_dispatch only, reducing the
|
run in-memory over the merged list (< 10ms). Total MTTR impact: < 1s
|
||||||
accident surface. If the user prefers zero residual risk, direct that
|
on a 5-15s step. **Binding revision (G-Q3a):** P3 VERIFY must include a
|
||||||
P8 exclude the state-bucket name from externalization entirely
|
timing assertion — `run_platform.sh` Step 5 wall-clock with vs without
|
||||||
(externalize only resource ARNs, leave the backend `bucket` literal).
|
kj must be within 1s (or kj must be faster than checkov, which is
|
||||||
Confidence 0.55; auto-resolved per `config.autonomy.level=full`.
|
expected). Captured as a P3 verify gate, not a PLAN change.
|
||||||
|
|
||||||
|
### 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,4 +1,4 @@
|
|||||||
# ACDL — IAM Policy Baseline (v1.11, REQ-116)
|
# Nova — 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`
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12–18 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 12–18mo 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.
|
||||||
+112
-131
@@ -1,151 +1,132 @@
|
|||||||
---
|
---
|
||||||
project: acdl
|
project: acdl
|
||||||
milestone: v1.14
|
milestone: v1.25
|
||||||
generated_at: 2026-07-29
|
generated_at: 2026-08-12
|
||||||
generator: lead-developer
|
generator: lead-developer
|
||||||
verification_toolchain:
|
verification_toolchain:
|
||||||
typecheck: "terraform validate && python3 -m py_compile core/**/*.py && python3 -m jsonschema schemas/*.schema.json"
|
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"
|
||||||
test: "bash scripts/run_primitive_plan.sh --check-only <primitive> # pipeline-driven (D-102); no per-module pytest"
|
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"
|
||||||
build: "terraform init && terraform plan"
|
lint: "ruff check core/policy_engine.py adapters/kyverno-json/ 2>/dev/null || python3 -m py_compile core/policy_engine.py"
|
||||||
note: |
|
note: |
|
||||||
ACDL has no package.json. The execute/verify/ship workflows substitute
|
v1.25 is the kyverno-json Unified Policy Engine milestone — a feat
|
||||||
`terraform validate` + `python -m py_compile` + JSON Schema validation
|
milestone. Four active personas: lead-developer (coordination +
|
||||||
for npm run typecheck, a per-phase verify script (or the
|
docs + ARCHITECTURE.md §12.7), backend-engineer (core/policy_engine.py
|
||||||
modules-lifecycle pipeline cell) for npm test, and `terraform init` +
|
protocol + registry + contract_resolver.py wiring + run_platform.sh
|
||||||
`terraform plan` for npm run build. v1.11 testing is pipeline-driven
|
Step 5 + pipeline tests), policy-engineer (adapters/kyverno-json/
|
||||||
(D-102): the modules-lifecycle pipeline matrix-runs each L1 module's
|
engine + policies across all 4 target dirs + meta-policies + policy
|
||||||
examples/{simple,complex}.yml contracts through apply→modify→destroy
|
tests + adapter README + STANDARDS.md policy-authoring section),
|
||||||
against live AWS. No per-module Python/pytest. This override is
|
data-engineer (config.json policy object + schemas/README.md note +
|
||||||
documented here as the single source of truth; the ci-* agents read
|
capability-inventory JSON fixture for regression policies).
|
||||||
PERSONAS.md before running verification commands.
|
frontend-engineer stays deactivated (no UI). The policy-engineer is a
|
||||||
v1.14 note: NFR-only milestone (bug fixes, security, tests, docs).
|
new custom persona created for this milestone's policy domain (see
|
||||||
Roster carries forward from v1.11 unchanged. frontend-engineer stays
|
RESEARCH.md §4 — kyverno-json + JMESPath is a distinct framework from
|
||||||
inactive (no frontend; decks are markdown = lead-developer
|
backend-engineer's fastify/hono).
|
||||||
territory). No custom personas needed (no new domains).
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# ACDL — Persona Roster (project-level, v1.11 RESTART)
|
# ACDL — Persona Roster (v1.25 kyverno-json Unified Policy Engine)
|
||||||
|
|
||||||
> v1.11 is a restart (D-097). The v1.9 roster is superseded. Three
|
> v1.25 roster. Four active personas + one deactivated. This is a feat
|
||||||
> structural corrections: (1) stateless adapter (D-098), (2) terraform
|
> milestone: the work is a swappable policy-engine protocol + a new
|
||||||
> owns lifecycle (D-101), (3) pipeline-driven testing (D-102). The roster
|
> adapter + policies across 4 Nova artifacts + pipeline wiring + docs.
|
||||||
> is simplified to the three active domains: data (terraform foundation),
|
> The policy-engineer is a new custom persona — kyverno-json + JMESPath
|
||||||
> backend (adapter/resolver), general (pipelines/workflows).
|
> is a specialized domain that doesn't fit backend-engineer's
|
||||||
|
> fastify/hono frameworks or data-engineer's drizzle/postgresql.
|
||||||
|
|
||||||
## Active personas
|
## Active personas
|
||||||
|
|
||||||
### lead-developer
|
### lead-developer
|
||||||
- **Domain:** coordination
|
- **Domain:** coordination + docs
|
||||||
- **Active:** true
|
- **Frameworks:** []
|
||||||
- **Phase-specific:** false
|
- **Constraints:** ["pragmatic", "battle-tested defaults", "docs match code", "swap boundary is the moat"]
|
||||||
- **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).
|
- **Territory:**
|
||||||
|
- `.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
|
- **Domain:** backend (Python + bash + pipeline wiring)
|
||||||
- **Active:** true
|
- **Frameworks:** ["boto3", "terraform"]
|
||||||
- **Phase-specific:** false
|
- **Constraints:** ["api-first", "strict-typing", "engine-agnostic confidence signal", "fail-soft when kj absent"]
|
||||||
- **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).
|
- **Territory:**
|
||||||
- **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/policy_engine.py` (NEW — PolicyEngine Protocol + PolicyEngineRegistry + NullEngine)
|
||||||
|
- `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
|
- **Domain:** data (config schema + structured fixtures)
|
||||||
- **Active:** true
|
- **Frameworks:** ["jsonschema", "yaml"]
|
||||||
- **Phase-specific:** false
|
- **Constraints:** ["schema-first", "type-safe config", "backward-compatible additions"]
|
||||||
- **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).
|
- **Territory:**
|
||||||
- **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).
|
- `.ciagent/config.json` (MODIFIED — new `policy` object: engine + policy_root)
|
||||||
|
- `schemas/policy_check_result.schema.json` (READ-ONLY — no change per D-116)
|
||||||
### general (lead-developer + backend-engineer pipeline work)
|
- `schemas/README.md` (MODIFIED — note engine: "kyverno" shared by K8s adapter + kj)
|
||||||
- **Domain:** coordination + pipelines
|
- `tests/fixtures/capability_inventory.json` (NEW — clean + drifted inventory fixtures for regression policies)
|
||||||
- **Active:** true
|
- **Reason:** The `config.json.policy` object is a schema-first addition
|
||||||
- **Phase-specific:** false
|
(new top-level key with `engine` + `policy_root` fields). The
|
||||||
- **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).
|
capability-inventory JSON fixtures for the regression-gate policies
|
||||||
- **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).
|
(REQ-304) are structured data — the data-engineer owns the fixture
|
||||||
|
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
|
||||||
- **Domain:** frontend
|
- **active:** false
|
||||||
- **Active:** false
|
- **Reason:** ACDL has no frontend (no package.json — confirmed in
|
||||||
- **Phase-specific:** false
|
config.json personas.personas[frontend-engineer].reason). v1.25 adds
|
||||||
- **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.
|
no UI work — the policy engine is backend + policy artifacts only.
|
||||||
|
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.
|
|
||||||
+327
-349
@@ -1,393 +1,371 @@
|
|||||||
---
|
# 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: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# v1.14 — NFR Refinement Plan (20 execution phases + 1 final)
|
> Feature milestone. Tags on the **v1.24.x** line: v1.24.0 (P0) →
|
||||||
|
> 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.
|
||||||
|
|
||||||
**Milestone:** v1.14 (NFR — bug fixes, security, stubs, tests, docs)
|
## Wave model
|
||||||
**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` (P1–P20) →
|
|
||||||
`v1.13.24` (P21 = milestone release).
|
|
||||||
**Branch:** `milestone/v1.14-refinement` → `phase/NN-<slug>`
|
|
||||||
|
|
||||||
## Wave ordering (D-098)
|
Each phase is a **vertical slice** (end-to-end: policy files + Python
|
||||||
|
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`).
|
||||||
|
|
||||||
- **Wave 1 (P1–P6):** bug fixes. P1→P2 sequential (composition depends
|
## Phase breakdown
|
||||||
on dedup correctness); P3–P6 independent. **G-105: full regression
|
|
||||||
gate run after P4** (validates the hardened gate before W2).
|
|
||||||
- **Wave 2 (P7–P12):** 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 (P13–P17):** 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 (P18–P20):** standards/docs/VPC. P19 depends on P1–P18
|
|
||||||
(reflects all prior phases); P18 + P20 independent.
|
|
||||||
|
|
||||||
## Execution approach
|
### Phase P1 — engine-core (Wave 1, backend-engineer + policy-engineer + data-engineer)
|
||||||
|
|
||||||
Each phase: EXECUTE (persona-assigned task groups) → VERIFY (4 layers +
|
**Type:** `feat` (engine protocol + registry + kyverno-json engine adapter + install + tests)
|
||||||
regression gate at milestone complete) → SHIP (patch tag). Phase
|
|
||||||
boundary checkpoint resets context. The execute workflow reads this
|
**Requirements:** REQ-291, REQ-292, REQ-293, REQ-294, REQ-308, REQ-309
|
||||||
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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Wave 1 — Bug Fixes (P1–P6)
|
### Phase P2 — contract + stack-IR policies (Wave 2, policy-engineer + backend-engineer)
|
||||||
|
|
||||||
### P1 — adapter-dedup-diagnostic (REQ-135)
|
**Type:** `feat` (policies + resolver wiring + tests)
|
||||||
**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`.
|
|
||||||
|
|
||||||
### P2 — static-assets-wiring-fix (REQ-136)
|
**Requirements:** REQ-295, REQ-296, REQ-297, REQ-298, REQ-299
|
||||||
**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`.
|
|
||||||
|
|
||||||
### P3 — lifecycle-script-arg-cleanup (REQ-137)
|
**Must-haves:**
|
||||||
**Persona:** backend-engineer
|
- `adapters/kyverno-json/policies/contract/` — 4 policies over consumer
|
||||||
**Territory:** `scripts/run_l2_lifecycle_*.sh`
|
contract JSON: `require-id-pattern.json`,
|
||||||
**Tasks:**
|
`require-env-in-enum.json`, `require-infrastructure-min-1.json`,
|
||||||
1. Remove the `[ci-vpc-outputs.json]` token from the usage strings of
|
`forbid-unknown-fields.json` — each a `ValidatingPolicy` with one
|
||||||
`run_l2_lifecycle_test.sh` + `run_l2_lifecycle_destroy.sh`, OR add a
|
`validate.assert` rule using JMESPath against the payload root;
|
||||||
comment documenting the L2-uses-remote-state design + parity reason.
|
severity via `nova.cloudinit.dev/severity` annotation (REQ-295)
|
||||||
2. Run `pytest` + `run_ci.sh`.
|
- `core/contract_resolver.py` invokes
|
||||||
|
`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)
|
||||||
|
|
||||||
### P4 — regression-gate-evidence-hardening (REQ-138)
|
**Vertical slice:** A consumer contract passes through the resolver
|
||||||
**Persona:** backend-engineer
|
and produces two PCR lists (contract policies pre-resolve, stack-IR
|
||||||
**Territory:** `core/regression_verify.py`, `.ciagent/CAPABILITY_INVENTORY.md`
|
policies post-resolve) that feed the confidence signal. A contract
|
||||||
**Binding decisions:** G-105 (gate must pass clean post-P4 before W2)
|
with a bad `id` or missing tags produces `fail` PCRs that lower the
|
||||||
**Tasks:**
|
confidence score. The resolver's existing tests pass unchanged (the
|
||||||
1. Add a `terraform validate` step to
|
policy call is additive — it does not change resolver return values
|
||||||
`_check_lifecycle_module_terraform` (or document why it's too slow +
|
or exceptions).
|
||||||
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`.
|
|
||||||
|
|
||||||
### P5 — adapter-behavior-tests (REQ-139)
|
**Files touched:**
|
||||||
**Persona:** backend-engineer
|
- `adapters/kyverno-json/policies/contract/require-id-pattern.json` (NEW)
|
||||||
**Territory:** `tests/test_adapter.py`
|
- `adapters/kyverno-json/policies/contract/require-env-in-enum.json` (NEW)
|
||||||
**Tasks:**
|
- `adapters/kyverno-json/policies/contract/require-infrastructure-min-1.json` (NEW)
|
||||||
1. Add `test_adapter_dedup_merges_same_module` — two resources with the
|
- `adapters/kyverno-json/policies/contract/forbid-unknown-fields.json` (NEW)
|
||||||
same `module` collapse to one `module "<first_id>" { ... }` block with
|
- `adapters/kyverno-json/policies/stack-ir/require-tagging-standard.json` (NEW)
|
||||||
merged inputs.
|
- `adapters/kyverno-json/policies/stack-ir/forbid-public-ingress.json` (NEW)
|
||||||
2. Add `test_adapter_remote_state_key_override` — `ACDL_REMOTE_STATE_KEY`
|
- `adapters/kyverno-json/policies/stack-ir/require-encryption-by-default.json` (NEW)
|
||||||
overrides the default `platform/terraform.tfstate` key in the emitted
|
- `core/contract_resolver.py` (MODIFIED — pre/post resolve engine calls)
|
||||||
`data terraform_remote_state` block.
|
- `tests/test_stack_ir_policies.py` (NEW)
|
||||||
3. Run `pytest` + `run_ci.sh`.
|
- `tests/fixtures/stack_ir/passing.json` (NEW)
|
||||||
|
- `tests/fixtures/stack_ir/failing.json` (NEW)
|
||||||
|
|
||||||
### P6 — alb-name-prefix-fix (REQ-140)
|
**Verification:** `pytest tests/test_contract_resolver.py
|
||||||
**Persona:** data-engineer
|
tests/test_stack_ir_policies.py tests/test_policy_engine.py -v`
|
||||||
**Territory:** `modules/l1/alb/terraform/main.tf`
|
(existing resolver tests pass; new policy tests pass or skip-without-
|
||||||
**Tasks:**
|
kj). `python3 -m py_compile core/contract_resolver.py`.
|
||||||
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`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Wave 2 — Security (P7–P12)
|
### Phase P3 — plan-JSON policies + meta-orchestration + pipeline wiring (Wave 3, policy-engineer + backend-engineer)
|
||||||
|
|
||||||
### P7 — swallowed-error-hardening (REQ-141)
|
**Type:** `feat` (plan-JSON policies + meta-policies + run_platform.sh wiring + tests)
|
||||||
**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`.
|
|
||||||
|
|
||||||
### P8 — account-id-externalization (REQ-142)
|
**Requirements:** REQ-300, REQ-301, REQ-302, REQ-303
|
||||||
**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).
|
|
||||||
|
|
||||||
### P9 — iam-policy-least-privilege (REQ-143)
|
**Must-haves:**
|
||||||
**Persona:** data-engineer
|
- `adapters/kyverno-json/policies/plan-json/` — 3 policies over
|
||||||
**Territory:** `terraform/bootstrap/spike_runner_policy.json`,
|
`terraform show -json` output: `forbid-plaintext-secrets.json` (ports
|
||||||
`tests/test_iam_policy_baseline.py`, `modules/l1/*/terraform/main.tf`,
|
CKV_AWS_41/45/46), `forbid-iam-wildcard.json` (ports CKV_AWS_1/40),
|
||||||
`modules/l2/*/composition.json`
|
`require-kms-reference.json` (ports CKV_AWS_7/33); JMESPath over
|
||||||
**Binding decisions:** G-104 (verify acdl-* naming before merge)
|
`planned_values.root_module.resources[]` (REQ-300)
|
||||||
**Tasks:**
|
- `run_platform.sh` Step 5 gains a parallel kyverno-json pass: after
|
||||||
1. Scope `iam:CreateRole` etc. (line 236) to
|
Checkov/Wiz produce raw PCRs, the script runs
|
||||||
`arn:aws:iam::*:role/acdl-*`.
|
`kj scan --policy adapters/kyverno-json/policies/plan-json/
|
||||||
2. Scope KMS (line 218) to `arn:aws:kms::*:key/acdl-*` (or
|
--payload <tfshow.json> -o json` and pipes through
|
||||||
`alias/acdl-*`).
|
`adapters/kyverno-json/kyverno_json_engine.py` to produce a second
|
||||||
3. CloudFront (line 117) + WAFv2 (line 129) remain `Resource: "*"` with
|
PCR list; both lists concatenated and fed to the confidence signal;
|
||||||
a documented global-ARN constraint (CloudFront ARNs are global;
|
`nova.policy.evaluated` event with both engine names; when
|
||||||
cannot be account-scoped — G-104).
|
`which kj` is false, logs and proceeds with Checkov/Wiz list only
|
||||||
4. **Verify acdl-* naming (G-104):** grep/audit
|
(no hard failure) (REQ-301)
|
||||||
`modules/l1/*/terraform/main.tf` + `modules/l2/*/composition.json`
|
- `tests/test_plan_json_policies.py` + `tests/fixtures/plan_json/` —
|
||||||
for every IAM role + KMS key name created by the lifecycle pipeline.
|
passing plan (no secrets, no wildcard, KMS alias) + failing plan
|
||||||
If any non-`acdl-*` name is found, rename the resource or widen that
|
(plaintext password, `Action: "*"`, inline KMS key); policies in
|
||||||
one statement (documented).
|
isolation + bundle; `tests/test_run_platform_plan_json_policies.py`
|
||||||
5. Add a regression test in `test_iam_policy_baseline.py` asserting no
|
asserts `run_platform.sh` has the kyverno-json Step 5 block +
|
||||||
new `Resource: "*"` on non-global actions.
|
concatenates PCR lists (script-substring assertion, pattern from
|
||||||
6. Run `pytest` + `run_ci.sh`.
|
`tests/test_pipeline.py:79-95`) (REQ-302)
|
||||||
|
- `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)
|
||||||
|
|
||||||
### P10 — contract-ingestor-identity-validation (REQ-144)
|
**Vertical slice:** `run_platform.sh` Step 5 produces a merged PCR list
|
||||||
**Persona:** backend-engineer
|
(Checkov/Wiz + kj plan-JSON policies + kj meta-policies over the
|
||||||
**Territory:** `core/lambda/contract_ingestor.py`, `tests/test_contract_ingestor.py`
|
merged list) that feeds the confidence signal. A plan with a plaintext
|
||||||
**Tasks:**
|
secret produces two `fail` PCRs (one Checkov, one kj) for the same
|
||||||
1. Add `contractId` format validation (regex, ≤64 chars).
|
resource — visible defense-in-depth. A critical finding anywhere
|
||||||
2. Add `environment` enum validation (dev/qa/prod/dr).
|
produces a `KJ_META_BLOCK_CRITICAL` meta-PCR that the confidence
|
||||||
3. Add `error` length cap (truncate `stackTrace` at a reasonable limit).
|
signal's hard-override blocks. The pipeline runs with or without `kj`
|
||||||
4. Document the ABAC reliance in the `_validate_caller_identity`
|
(graceful skip).
|
||||||
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`.
|
|
||||||
|
|
||||||
### P11 — schema-input-validation-hardening (REQ-145)
|
**Files touched:**
|
||||||
**Persona:** backend-engineer
|
- `adapters/kyverno-json/policies/plan-json/forbid-plaintext-secrets.json` (NEW)
|
||||||
**Territory:** `schemas/contract.schema.json`, `schemas/environment.schema.json`,
|
- `adapters/kyverno-json/policies/plan-json/forbid-iam-wildcard.json` (NEW)
|
||||||
`tests/test_environment_schema.py`, `tests/test_contract_schema.py`
|
- `adapters/kyverno-json/policies/plan-json/require-kms-reference.json` (NEW)
|
||||||
**Tasks:**
|
- `adapters/kyverno-json/policies/meta/block-on-any-critical.json` (NEW)
|
||||||
1. Add `"additionalProperties": false` to both schemas' top-level
|
- `adapters/kyverno-json/policies/meta/tagging-rules-agree.json` (NEW)
|
||||||
objects.
|
- `scripts/run_platform.sh` (MODIFIED — Step 5 kj parallel pass)
|
||||||
2. Add `maxItems`/`maxProperties` bounds to `infrastructure` map +
|
- `tests/test_plan_json_policies.py` (NEW)
|
||||||
`monitored_endpoints` array.
|
- `tests/test_meta_policies.py` (NEW)
|
||||||
3. Add `pattern` validation for `state_backend.bucket` (S3 naming
|
- `tests/test_run_platform_plan_json_policies.py` (NEW)
|
||||||
rules: lowercase, 3-63 chars, no underscores).
|
- `tests/fixtures/plan_json/passing.json` (NEW)
|
||||||
4. Add `pattern` validation for `runner_role_arn` (ARN format).
|
- `tests/fixtures/plan_json/failing.json` (NEW)
|
||||||
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`.
|
|
||||||
|
|
||||||
### P12 — gitignore-credential-hygiene (REQ-146)
|
**Verification:** `pytest tests/test_plan_json_policies.py
|
||||||
**Persona:** lead-developer
|
tests/test_meta_policies.py tests/test_run_platform_plan_json_policies.py
|
||||||
**Territory:** `.gitignore`, `tests/test_no_secrets_tracked.py`
|
tests/test_pipeline.py -v` (new tests pass or skip-without-kj; existing
|
||||||
**Tasks:**
|
pipeline tests pass). `python3 -m py_compile` on any modified Python.
|
||||||
1. Add credential-pattern catch-all to `.gitignore`:
|
Shellcheck on `run_platform.sh` if available.
|
||||||
`*.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`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Wave 3 — Stub / Test / CI / Hygiene (P13–P17)
|
### Phase P4 — regression-gate policies + docs (Wave 4, policy-engineer + data-engineer + lead-developer)
|
||||||
|
|
||||||
### P13 — kyverno-kube-version-resolution (REQ-147)
|
**Type:** `feat` (regression policies) + `docs` (adapter READMEs + ARCHITECTURE + STANDARDS + METRICS)
|
||||||
**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`.
|
|
||||||
|
|
||||||
### P14 — orphan-artifact-and-dead-config-cleanup (REQ-148)
|
**Requirements:** REQ-304, REQ-305, REQ-306, REQ-307
|
||||||
**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`.
|
|
||||||
|
|
||||||
### P15 — untested-scripts-coverage (REQ-149)
|
**Must-haves:**
|
||||||
**Persona:** backend-engineer
|
- `adapters/kyverno-json/policies/regression/` — 3 policies over
|
||||||
**Territory:** `tests/` (new test files for 7 scripts)
|
capability-inventory JSON frontmatter: `cap-013-adapter-dedup.json`,
|
||||||
**Tasks:**
|
`cap-023-metrics-collector.json`, `cap-024-deck-structure.json`;
|
||||||
1. `tests/test_seed_uptime_monitors.py` — mock the uptime-kuma API;
|
emit `pass`/`fail` PCRs per capability; the existing
|
||||||
assert monitor creation from a JSON file.
|
`core/regression_verify.py` is kept (drives the CI gate); the
|
||||||
2. `tests/test_push_consumer_image.py` — mock `subprocess.run` (docker
|
policies are the declarative mirror (REQ-304)
|
||||||
login/build/push) + boto3 ECR; assert the flow.
|
- `tests/test_regression_policies.py` +
|
||||||
3. `tests/test_sync_to_gl.sh` (shell test) — dry-run mode; assert the
|
`tests/fixtures/capability_inventory/clean.json` +
|
||||||
copy + push commands are constructed correctly.
|
`tests/fixtures/capability_inventory/drifted.json` — clean (all caps
|
||||||
4. `tests/test_post_stage_comment.sh` (shell test) — no-op when not in
|
pass) + drifted (duplicate adapter, missing metric status, broken
|
||||||
a PR context; assert the `gh api` call structure when in PR.
|
deck arc); regression gate still 287/287 baseline (new tests
|
||||||
5. `tests/test_rotate_spike_key.sh` (shell test) — mock `aws iam`;
|
additive, skip-without-kj) (REQ-305)
|
||||||
assert deactivate/create/update-secret flow.
|
- `adapters/README.md` gains new kyverno-json adapter row + "Policy
|
||||||
6. `tests/test_create_state_backend.py` — mock boto3 S3/DynamoDB;
|
Engine Protocol" section (Protocol, registry, swap boundary,
|
||||||
assert idempotent creation.
|
how-to-add-OpaEngine); `adapters/kyverno-json/README.md` documents
|
||||||
7. `tests/test_create_iam_user.py` — mock boto3 IAM; assert idempotent
|
the engine, install path, policy directory layout, 4 policy
|
||||||
user/policy/key creation.
|
categories (REQ-306)
|
||||||
8. Run `pytest` + `run_ci.sh`.
|
- `.ciagent/ARCHITECTURE.md` §12.7 (added in RESEARCH) is finalized;
|
||||||
|
`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)
|
||||||
|
|
||||||
### P16 — workflow-parity-and-script-flags (REQ-150)
|
**Vertical slice:** The regression gate's capability checks are now
|
||||||
**Persona:** backend-engineer
|
declarative policies auditable as artifacts. A new module owner can
|
||||||
**Territory:** `.gitea/workflows/`, `scripts/rotate_spike_key.sh`,
|
read `modules/STANDARDS.md` "Policy authoring standard" and write a
|
||||||
`scripts/sync_to_gl.sh`
|
per-module kyverno-json policy. A new engineer can read
|
||||||
**Tasks:**
|
`adapters/README.md` "Policy Engine Protocol" and implement an
|
||||||
1. Either mirror the 4 GitHub-only workflows (patterns-plan,
|
`OpaEngine`. The 287/287 baseline is unchanged.
|
||||||
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`.
|
|
||||||
|
|
||||||
### P17 — config-and-persona-hygiene (REQ-151)
|
**Files touched:**
|
||||||
**Persona:** lead-developer
|
- `adapters/kyverno-json/policies/regression/cap-013-adapter-dedup.json` (NEW)
|
||||||
**Territory:** `.ciagent/config.json`, `.ciagent/PERSONAS.md`
|
- `adapters/kyverno-json/policies/regression/cap-023-metrics-collector.json` (NEW)
|
||||||
**Tasks:**
|
- `adapters/kyverno-json/policies/regression/cap-024-deck-structure.json` (NEW)
|
||||||
1. Mark `frontend-engineer` persona `active: false` in `config.json`
|
- `tests/test_regression_policies.py` (NEW)
|
||||||
`personas.personas[]` (PERSONAS.md:80 already says inactive).
|
- `tests/fixtures/capability_inventory/clean.json` (NEW)
|
||||||
2. Fix `branching_strategy: "phase"` — either change to `"flat"` or
|
- `tests/fixtures/capability_inventory/drifted.json` (NEW)
|
||||||
document that the field is advisory + the project uses flat workflow
|
- `adapters/README.md` (MODIFIED — new row + PolicyEngine Protocol section)
|
||||||
(committed directly to main per established convention).
|
- `adapters/kyverno-json/README.md` (NEW)
|
||||||
3. Configure `ollama-cloud` backend: set `base_url` to the actual
|
- `schemas/README.md` (MODIFIED — engine enum note)
|
||||||
endpoint OR add a comment documenting why it's intentionally unset
|
- `modules/STANDARDS.md` (MODIFIED — Policy authoring standard section)
|
||||||
(the runtime uses the `glm-5.2` model via the opencode backend, not
|
- `docs/METRICS.md` (MODIFIED — swappable engine narrative)
|
||||||
the `llm_backends` config).
|
|
||||||
4. Run `pytest` + `run_ci.sh`.
|
**Verification:** `pytest tests/test_regression_policies.py
|
||||||
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Wave 4 — Standards / Docs / VPC (P18–P20)
|
### Phase P5 — final review + audit + milestone ship (Final Phase)
|
||||||
|
|
||||||
### P18 — module-standards-consistency (REQ-152)
|
**Type:** `docs` (review + audit + milestone completion)
|
||||||
**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`.
|
|
||||||
|
|
||||||
### P19 — documentation-sync-v1.14 (REQ-153)
|
**Requirements:** All REQ-291..309 (mark complete)
|
||||||
**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.11–v1.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.
|
|
||||||
|
|
||||||
### P20 — platform-vpc-parameterization (REQ-154)
|
**Must-haves:**
|
||||||
**Persona:** data-engineer
|
- `ciagent-review` multi-persona code review across P1..P4
|
||||||
**Territory:** `terraform/platform/main.tf`
|
(lead-developer, backend-engineer, data-engineer, policy-engineer).
|
||||||
**Tasks:**
|
Auto-fix P0; flag P1+ for post-hoc review. If P1+ issues found, fix
|
||||||
1. Add a `vpc_cidr` variable (default `10.0.0.0/16`); replace the
|
them in this final phase (not loop back to EXECUTE).
|
||||||
hardcoded `cidr_block`.
|
- `ciagent-audit` — reconstruction test (git log ↔ `.ciagent/` files),
|
||||||
2. Replace `count = 2` subnets with
|
`.ciagent/` file discipline, branch hygiene, commit discipline.
|
||||||
`count = length(data.aws_availability_zones.available.names)`.
|
Critical issues fixed in this phase.
|
||||||
3. Add a `data "aws_availability_zones" "available" {}` block.
|
- `ciagent-ship` (milestone) — merge `phase/05-final-review-ship` →
|
||||||
4. Document the `0.0.0.0/0` ingress on port 80 (ALB-fronted, acceptable
|
`milestone/v1.25-kyverno-json` → `main`; tag `v1.24.5` (= the v1.25
|
||||||
for a public-facing service; add a comment).
|
release per the prev-minor tagging rule); create Gitea release with
|
||||||
5. Run `terraform validate` + `pytest` + `run_ci.sh`.
|
full milestone summary (all phases, all requirements); delete all
|
||||||
|
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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Final Phase — P21 (review + audit + ship)
|
## Wave ordering (parallelization)
|
||||||
|
|
||||||
**Persona:** lead-developer (review coordination) + ci-code-reviewer +
|
With `parallelization.enabled: true`, `max_concurrent_agents: 5`,
|
||||||
ci-debugger (audit)
|
`min_plans_for_parallel: 2`:
|
||||||
**Tasks:**
|
|
||||||
1. Multi-persona code review across all v1.14 phases (P1–P20). 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.
|
|
||||||
|
|
||||||
## Success Criteria (milestone gate)
|
- **P1 Wave 1:** backend-engineer (protocol + registry + install) ‖
|
||||||
|
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.
|
||||||
|
|
||||||
1. All 20 REQ-135..REQ-154 marked complete in REQUIREMENTS.md.
|
Territory enforcement: `warn` mode (per `config.json
|
||||||
2. Review: 0 new P0; all P1-1..P1-5 + P2-1..P2-4 resolved.
|
personas.territory_enforcement: "warn"`). Cross-territory edits
|
||||||
3. Audit: clean; reconstruction test passes.
|
(e.g., backend-engineer touching a policy file) emit a warning, not a
|
||||||
4. Regression gate (D-091) clean against the v1.14 state.
|
block.
|
||||||
5. `pytest` passes; `run_ci.sh` exits 0; `run_platform.sh --check-only`
|
|
||||||
exits 0.
|
## Requirement → phase → persona matrix
|
||||||
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 |
|
||||||
+792
-6
@@ -1,4 +1,12 @@
|
|||||||
# ACDL — Agentic Cloud Delivery Platform
|
# Nova — The New Dawn of DevSecOps
|
||||||
|
|
||||||
|
> **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
|
||||||
|
|
||||||
@@ -25,7 +33,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 penetrate upstream product/SDLC.
|
governs infra and delivery; it does not reach into 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
|
||||||
@@ -50,6 +58,103 @@ 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).
|
||||||
@@ -584,12 +689,97 @@ 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_gl.sh (GitLab mirror sync
|
structure. Also includes scripts/sync_to_nova.sh (manual-only "2nd release"
|
||||||
utility, unrelated to presentations).
|
into ~/nova — a separate GitLab consumer-facing repo with its own history;
|
||||||
|
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` (P1–P6) → `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)
|
||||||
@@ -791,7 +981,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. |
|
| 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. |
|
||||||
| 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. |
|
||||||
@@ -911,4 +1101,600 @@ D-095+ to continue from v1.10's D-094):
|
|||||||
| D-098 | Wave ordering: W1 (P1–P6 bug fixes), W2 (P7–P12 security), W3 (P13–P17 stub/test/CI/hygiene), W4 (P18–P20 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 (P1–P6 bug fixes), W2 (P7–P12 security), W3 (P13–P17 stub/test/CI/hygiene), W4 (P18–P20 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` (P1–P20) →
|
||||||
|
`v1.15.26` (P21 final = milestone release).
|
||||||
|
|
||||||
|
**Wave ordering:**
|
||||||
|
- Wave 1 (P1–P4): 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 (P5–P9): simplify without regressions — P5 before P6/P9
|
||||||
|
(regression-verify dedup is independent); P8 changes the workflow test.
|
||||||
|
- Wave 3 (P10–P14): security + maintainability — P10 before P11
|
||||||
|
(identity enforcement before payload validation); P12/P13 independent
|
||||||
|
splits.
|
||||||
|
- Wave 4 (P15–P17): developer experience — independent; P17 last
|
||||||
|
(reflects the consolidated path).
|
||||||
|
- Wave 5 (P18–P20): 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. | P18–P20 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 1–5 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 (P1–P3 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` (P1–P7) → `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, 12–18mo 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). | P1–P3 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). | P1–P3 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` (P1–P5) →
|
||||||
|
`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` (P1–P5) →
|
||||||
|
> `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` (P1–P4) → `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).
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"run_id": "regr-1785329757",
|
"run_id": "regr-1785591207",
|
||||||
"run_at_utc": "2026-07-29T12:55:57Z",
|
"run_at_utc": "2026-08-01T13:33:27Z",
|
||||||
"milestone": "v1.10",
|
"milestone": "v1.10",
|
||||||
"phase": 52,
|
"phase": 52,
|
||||||
"summary": {
|
"summary": {
|
||||||
"Verified": 22,
|
"Verified": 18,
|
||||||
"Decayed": 0,
|
"Decayed": 0,
|
||||||
"Broken": 0
|
"Broken": 0,
|
||||||
|
"Skipped": 4
|
||||||
},
|
},
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"results": [
|
"results": [
|
||||||
@@ -16,7 +17,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": 252
|
"duration_ms": 235
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_id": "CAP-002",
|
"capability_id": "CAP-002",
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
"status": "Verified",
|
"status": "Verified",
|
||||||
"detail": "exit 0; env schema validates",
|
"detail": "exit 0; env schema validates",
|
||||||
"tier": "local",
|
"tier": "local",
|
||||||
"duration_ms": 196
|
"duration_ms": 201
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_id": "CAP-003",
|
"capability_id": "CAP-003",
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
"status": "Verified",
|
"status": "Verified",
|
||||||
"detail": "exit 0; ",
|
"detail": "exit 0; ",
|
||||||
"tier": "local",
|
"tier": "local",
|
||||||
"duration_ms": 258
|
"duration_ms": 261
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_id": "CAP-004",
|
"capability_id": "CAP-004",
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
"status": "Verified",
|
"status": "Verified",
|
||||||
"detail": "exit 0; ",
|
"detail": "exit 0; ",
|
||||||
"tier": "local",
|
"tier": "local",
|
||||||
"duration_ms": 264
|
"duration_ms": 259
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_id": "CAP-005",
|
"capability_id": "CAP-005",
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
"status": "Verified",
|
"status": "Verified",
|
||||||
"detail": "exit 0; ",
|
"detail": "exit 0; ",
|
||||||
"tier": "local",
|
"tier": "local",
|
||||||
"duration_ms": 314
|
"duration_ms": 337
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_id": "CAP-006",
|
"capability_id": "CAP-006",
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
"status": "Verified",
|
"status": "Verified",
|
||||||
"detail": "exit 0; interpolation ok",
|
"detail": "exit 0; interpolation ok",
|
||||||
"tier": "local",
|
"tier": "local",
|
||||||
"duration_ms": 223
|
"duration_ms": 242
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_id": "CAP-007",
|
"capability_id": "CAP-007",
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
"status": "Verified",
|
"status": "Verified",
|
||||||
"detail": "exit 0; confidence band=pass",
|
"detail": "exit 0; confidence band=pass",
|
||||||
"tier": "local",
|
"tier": "local",
|
||||||
"duration_ms": 80
|
"duration_ms": 91
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_id": "CAP-008",
|
"capability_id": "CAP-008",
|
||||||
@@ -72,15 +73,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": 358
|
"duration_ms": 456
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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====================== 462 passed, 2 deselected in 34.63s ======================",
|
"detail": "exit 0; [ 98%]\ntests/test_wiz_adapter_real_client.py ......... [100%]\n\n================= 586 passed, 2 deselected in 71.63s (0:01:11) =================",
|
||||||
"tier": "local",
|
"tier": "local",
|
||||||
"duration_ms": 36065
|
"duration_ms": 72988
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_id": "CAP-010",
|
"capability_id": "CAP-010",
|
||||||
@@ -88,63 +89,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": 40668
|
"duration_ms": 73275
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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/acdl_local_e2e_416d0fmr/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/nova_local_e2e_6vnrnin1/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}",
|
||||||
"tier": "local",
|
"tier": "local",
|
||||||
"duration_ms": 583
|
"duration_ms": 634
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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; 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}",
|
"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}",
|
||||||
"tier": "local",
|
"tier": "local",
|
||||||
"duration_ms": 489
|
"duration_ms": 584
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": "Verified",
|
"status": "Skipped",
|
||||||
"detail": "terraform init+validate+plan OK (live AWS, microservice)",
|
"detail": "terraform init: state bucket absent (post-v1.11-teardown, D-096) [microservice]",
|
||||||
"tier": "live-aws",
|
"tier": "live-aws",
|
||||||
"duration_ms": 28811
|
"duration_ms": 737
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": "Verified",
|
"status": "Skipped",
|
||||||
"detail": "terraform init+validate+plan OK (live AWS, static-assets)",
|
"detail": "terraform init: state bucket absent (post-v1.11-teardown, D-096) [static-assets]",
|
||||||
"tier": "live-aws",
|
"tier": "live-aws",
|
||||||
"duration_ms": 31772
|
"duration_ms": 676
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_id": "CAP-015",
|
"capability_id": "CAP-015",
|
||||||
"name": "DynamoDB outbox table exists (live AWS)",
|
"name": "DynamoDB outbox table exists (live AWS)",
|
||||||
"status": "Verified",
|
"status": "Skipped",
|
||||||
"detail": "acdl-outbox exists, item_count=9",
|
"detail": "nova-outbox absent (post-v1.11-teardown steady state, D-096)",
|
||||||
"tier": "live-aws",
|
"tier": "live-aws",
|
||||||
"duration_ms": 477
|
"duration_ms": 664
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": "Verified",
|
"status": "Skipped",
|
||||||
"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']",
|
"detail": "state bucket nova-tfstate-581513795199-us-east-1 absent (post-v1.11-teardown, D-096)",
|
||||||
"tier": "live-aws",
|
"tier": "live-aws",
|
||||||
"duration_ms": 324
|
"duration_ms": 245
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_id": "CAP-017",
|
"capability_id": "CAP-017",
|
||||||
"name": "DynamoDB acdl-contracts table (lifecycle pipeline evidence)",
|
"name": "DynamoDB nova-contracts table (lifecycle pipeline evidence)",
|
||||||
"status": "Verified",
|
"status": "Verified",
|
||||||
"detail": "terraform files present + simple/complex contracts resolve",
|
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve",
|
||||||
"tier": "lifecycle-pipeline",
|
"tier": "lifecycle-pipeline",
|
||||||
"duration_ms": 520
|
"duration_ms": 586
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_id": "CAP-018",
|
"capability_id": "CAP-018",
|
||||||
@@ -152,39 +153,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": 137
|
"duration_ms": 138
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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)",
|
"detail": "L2 composition resolves (simple + complex contracts; offline proxy)",
|
||||||
"tier": "lifecycle-pipeline",
|
"tier": "lifecycle-pipeline",
|
||||||
"duration_ms": 534
|
"duration_ms": 519
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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)",
|
"detail": "L2 composition resolves (simple + complex contracts; offline proxy)",
|
||||||
"tier": "lifecycle-pipeline",
|
"tier": "lifecycle-pipeline",
|
||||||
"duration_ms": 567
|
"duration_ms": 521
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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 + simple/complex contracts resolve",
|
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve",
|
||||||
"tier": "lifecycle-pipeline",
|
"tier": "lifecycle-pipeline",
|
||||||
"duration_ms": 606
|
"duration_ms": 562
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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 + simple/complex contracts resolve",
|
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve",
|
||||||
"tier": "lifecycle-pipeline",
|
"tier": "lifecycle-pipeline",
|
||||||
"duration_ms": 529
|
"duration_ms": 611
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,51 +1,51 @@
|
|||||||
# Regression Report — v1.10 Phase 52
|
# Regression Report — v1.10 Phase 52
|
||||||
|
|
||||||
- **Run ID:** `regr-1785329757`
|
- **Run ID:** `regr-1785591207`
|
||||||
- **Run at (UTC):** 2026-07-29T12:55:57Z
|
- **Run at (UTC):** 2026-08-01T13:33:27Z
|
||||||
- **Summary:** {'Verified': 22, 'Decayed': 0, 'Broken': 0}
|
- **Summary:** {'Verified': 18, 'Decayed': 0, 'Broken': 0, 'Skipped': 4}
|
||||||
- **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** | 252 | exit 0; 2 sample contracts validate |
|
| CAP-001 | contract.schema.json validates sample contracts | local | **Verified** | 235 | exit 0; 2 sample contracts validate |
|
||||||
| CAP-002 | environment.schema.json validates env files | local | **Verified** | 196 | exit 0; env schema validates |
|
| CAP-002 | environment.schema.json validates env files | local | **Verified** | 201 | exit 0; env schema validates |
|
||||||
| CAP-003 | contract_resolver resolves static-assets | local | **Verified** | 258 | exit 0; |
|
| CAP-003 | contract_resolver resolves static-assets | local | **Verified** | 261 | exit 0; |
|
||||||
| CAP-004 | contract_resolver resolves microservice | local | **Verified** | 264 | exit 0; |
|
| CAP-004 | contract_resolver resolves microservice | local | **Verified** | 259 | exit 0; |
|
||||||
| CAP-005 | terraform adapter emits .tf files | local | **Verified** | 314 | exit 0; |
|
| CAP-005 | terraform adapter emits .tf files | local | **Verified** | 337 | exit 0; |
|
||||||
| CAP-006 | contract interpolation expands env/contract tokens | local | **Verified** | 223 | exit 0; interpolation ok |
|
| CAP-006 | contract interpolation expands env/contract tokens | local | **Verified** | 242 | exit 0; interpolation ok |
|
||||||
| CAP-007 | confidence_signal.compute returns a band | local | **Verified** | 80 | exit 0; confidence band=pass |
|
| CAP-007 | confidence_signal.compute returns a band | local | **Verified** | 91 | exit 0; confidence band=pass |
|
||||||
| CAP-008 | outbox_writer builds a hash-chained item | local | **Verified** | 358 | exit 0; outbox hash chain ok |
|
| CAP-008 | outbox_writer builds a hash-chained item | local | **Verified** | 456 | exit 0; outbox hash chain ok |
|
||||||
| CAP-009 | offline pytest suite passes | local | **Verified** | 36065 | exit 0; [ 98%]
|
| CAP-009 | offline pytest suite passes | local | **Verified** | 72988 | exit 0; [ 98%]
|
||||||
tests/test_wiz_adapter_real_client.py ......... [100%]
|
tests/test_wiz_adapter_real_client.py ......... [100%]
|
||||||
|
|
||||||
====================== 462 passe |
|
================= 586 passed, 2 |
|
||||||
| CAP-010 | run_ci.sh reproduces CI pipeline locally | local | **Verified** | 40668 | exit 0; resource(s))
|
| CAP-010 | run_ci.sh reproduces CI pipeline locally | local | **Verified** | 73275 | 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** | 583 | exit 0; al-emulator",
|
| CAP-011 | headline E2E runs against the local emulating tier (microservice) | local | **Verified** | 634 | exit 0; al-emulator",
|
||||||
"desired_count": 1,
|
"desired_count": 1,
|
||||||
"running_count": 1
|
"running_count": 1
|
||||||
},
|
},
|
||||||
"outbox_dir": "/tmp/acdl_local_e2e_416d0fmr/outbox",
|
"outbox_dir": "/tmp/nova_local_e2e_6vnrnin1/outbox",
|
||||||
"outbox_events": 2,
|
"outbox_events": 2,
|
||||||
"outbox |
|
"outbox |
|
||||||
| CAP-012 | local E2E on the static-assets stack (no ECS) | local | **Verified** | 489 | exit 0; acdl_local_e2e_ijhcj1z8/tf",
|
| CAP-012 | local E2E on the static-assets stack (no ECS) | local | **Verified** | 584 | exit 0; nova_local_e2e_uq4kkhze/tf",
|
||||||
"backend": "local",
|
"backend": "local",
|
||||||
"ecs": null,
|
"ecs": null,
|
||||||
"outbox_dir": "/tmp/acdl_local_e2e_ijhcj1z8/outbox",
|
"outbox_dir": "/tmp/nova_local_e2e_uq4kkhze/outbox",
|
||||||
"outbox_events": 2,
|
"outbox_events": 2,
|
||||||
"outbox |
|
"outbox |
|
||||||
| CAP-013 | terraform init+validate+plan live AWS (microservice) | live-aws | **Verified** | 28811 | terraform init+validate+plan OK (live AWS, microservice) |
|
| 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-014 | terraform init+validate+plan live AWS (static-assets) | live-aws | **Verified** | 31772 | terraform init+validate+plan OK (live AWS, static-assets) |
|
| 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-015 | DynamoDB outbox table exists (live AWS) | live-aws | **Verified** | 477 | acdl-outbox exists, item_count=9 |
|
| CAP-015 | DynamoDB outbox table exists (live AWS) | live-aws | **Skipped** | 664 | nova-outbox absent (post-v1.11-teardown steady state, 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-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-017 | DynamoDB acdl-contracts table (lifecycle pipeline evidence) | lifecycle-pipeline | **Verified** | 520 | terraform files present + simple/complex contracts resolve |
|
| CAP-017 | DynamoDB nova-contracts table (lifecycle pipeline evidence) | lifecycle-pipeline | **Verified** | 586 | terraform files present + fmt -check passes + simple/complex contracts resolve |
|
||||||
| CAP-018 | Lambda contract-ingestor (local stub + lifecycle evidence) | lifecycle-pipeline | **Verified** | 137 | LocalLambdaStub instantiates (local tier evidence) |
|
| CAP-018 | Lambda contract-ingestor (local stub + lifecycle evidence) | lifecycle-pipeline | **Verified** | 138 | LocalLambdaStub instantiates (local tier evidence) |
|
||||||
| CAP-019 | ECS cluster + service (L2 microservice lifecycle evidence) | lifecycle-pipeline | **Verified** | 534 | L2 composition resolves (simple + complex contracts) |
|
| CAP-019 | ECS cluster + service (L2 microservice lifecycle evidence) | lifecycle-pipeline | **Verified** | 519 | 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-020 | CloudFront + WAF (L2 static-assets lifecycle evidence) | lifecycle-pipeline | **Verified** | 521 | L2 composition resolves (simple + complex contracts; offline proxy) |
|
||||||
| CAP-021 | uptime-kuma (L1 uptime lifecycle evidence) | lifecycle-pipeline | **Verified** | 606 | terraform files present + simple/complex contracts resolve |
|
| CAP-021 | uptime-kuma (L1 uptime lifecycle evidence) | lifecycle-pipeline | **Verified** | 562 | 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 |
|
| CAP-022 | OIDC role (L1 iam-role lifecycle evidence) | lifecycle-pipeline | **Verified** | 611 | terraform files present + fmt -check passes + simple/complex contracts resolve |
|
||||||
|
|||||||
+1796
-21
File diff suppressed because it is too large
Load Diff
+408
-841
File diff suppressed because it is too large
Load Diff
+90
-302
@@ -1,324 +1,112 @@
|
|||||||
# ACDL v1.11 — Multi-Persona Code Review (P60–P65 retrofit + new work)
|
# Nova v1.16 — Multi-Persona Code Review (final phase P21)
|
||||||
|
|
||||||
**Reviewer:** ci-code-reviewer (model: glm-5.2)
|
**Reviewer:** lead-developer (model: glm-5.2)
|
||||||
**Scope:** v1.11 milestone, branch `milestone/v1.11-restart` — 22 commits
|
**Scope:** v1.16 milestone — 22 tags (v1.15.5..v1.15.26), 20 execution
|
||||||
(e1bb214..8c09580), 25 files, +790/-142 lines
|
phases + final. Squash-merged to main via `milestone/v1.16-nova-simplification`.
|
||||||
**Date:** 2026-07-29
|
**Date:** 2026-07-30
|
||||||
|
|
||||||
## Commits reviewed
|
> **Historical note:** REVIEW.md was reconstructed at v1.16 P21 (the
|
||||||
|
> v1.3–v1.15 reviews were not persisted or were overwritten per the
|
||||||
|
> established convention). The v1.16 review overwrites prior content.
|
||||||
|
|
||||||
| Commit | Phase | Type | Summary |
|
## Review approach
|
||||||
|--------|-------|------|---------|
|
|
||||||
| e1bb214 | 60 | docs | retrofit plan — L1 lifecycle pipeline live-run |
|
The v1.16 milestone is an NFR sweep (no new features). Each of the 20
|
||||||
| bc9058f | 60 | feat | L1 module lifecycle live run — module fixes (retrofit) |
|
execution phases shipped with a 4-layer verify (structural/behavioral/
|
||||||
| bb3ac7c | 60 | fix | WAF scope case + VPC modify DependencyViolation |
|
security/quality) + `run_ci.sh` 3-stage PASS at every phase boundary.
|
||||||
| 0c5c4d1 | 61 | docs | create phase plan — L2 lifecycle pipeline author |
|
The final-phase review (P21) is a milestone-level cross-phase check,
|
||||||
| 361fe60 | 61 | feat | L2 lifecycle pipeline — extend matrix + workflows + tests |
|
not a per-phase re-review (the per-phase verify already ran).
|
||||||
| 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 targeted fixes are correct for their stated
|
No blocking issues found. The 4-layer verify at each phase boundary +
|
||||||
purposes. The 447 fast offline tests pass (485/490 collected; 5 slow
|
the regression gate (D-118, 18V+4S at P9 + P21) are the structural
|
||||||
deselected, including 2 slow regression-integration tests that exercise the
|
controls. No P0 was auto-applied at P21.
|
||||||
CAPABILITY_REGISTRY against the live codebase).
|
|
||||||
|
|
||||||
## P1 issues (5 — should fix)
|
## P1 issues (0)
|
||||||
|
|
||||||
### P1-1: Adapter dedup silently drops resources whose module is not in the registry
|
No P1 issues flagged. The grill binding decisions (G-111..G-113) were
|
||||||
[correctness] `adapters/terraform/adapter.py:159-170`
|
incorporated into the plan before execution; the regression gate (G-111)
|
||||||
|
passed at both checkpoints (P9 + P21).
|
||||||
|
|
||||||
The new dedup loop only adds resources to `seen` when `tf_dir` is truthy
|
## P2 issues (2 — post-hoc, non-blocking)
|
||||||
(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.
|
|
||||||
|
|
||||||
Confirmed by simulation: two resources, one with `module: nonexistent@1.0.0`,
|
### P2-1: Onboarding framing (E-002, deferred from grill)
|
||||||
produces a `merged` list of length 1 — the unknown-module resource vanishes
|
[scope] `.ciagent/PROJECT.md`, `.ciagent/ROADMAP.md`
|
||||||
without diagnostic.
|
|
||||||
|
|
||||||
**Recommendation:** in the dedup loop, when `tf_dir` is `None`, either
|
The grill escalation E-002 (confidence 0.55) flagged that the PROJECT.md
|
||||||
(a) raise immediately (preserving the prior contract), or (b) append the
|
framing "first self-service onboarding request path" may over-promise
|
||||||
resource to a separate `unknown` list and extend `parts` with it so
|
relative to a request-*acceptance* path that writes a pending row +
|
||||||
`_emit_module_block` raises the descriptive error. As written, a typo in
|
generates an env-file + proves the role Terraform offline but never
|
||||||
a composition's `module` field (e.g. `iam-role@1.0.0` vs `iam_roles@1.0.0`)
|
fulfills (no live role grant). The milestone is internally consistent
|
||||||
will silently omit a resource from the emitted terraform — a class of
|
with D-113 (request-path only) — the wording is the only risk. The
|
||||||
defect the v1.10 sweep was specifically created to catch.
|
ROADMAP/PROJECT use "request path" (not "request-fulfillment"), and the
|
||||||
|
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).
|
||||||
|
|
||||||
### P1-2: L2 static-assets "modify" example is a no-op — complex ≡ simple
|
### P2-2: REVIEW.md + AUDIT.md not updated during the run
|
||||||
[correctness] `modules/l2/static-assets/examples/complex.yml`,
|
[maintainability] `.ciagent/REVIEW.md`, `.ciagent/AUDIT.md`
|
||||||
`modules/l2/static-assets/composition.json`
|
|
||||||
|
|
||||||
The complex.yml comment claims "Modify variant: same bucket_name as simple
|
REVIEW.md still held v1.11 content during the v1.16 run (the per-phase
|
||||||
(in-place modify, adds CDN + WAF)". But resolving both examples yields
|
verify ran but wasn't persisted to REVIEW.md until P21). AUDIT.md held
|
||||||
**identical** resource sets: `['s3','cloudfront-distribution',
|
v1.15 content. Both are reconstructed at P21 (this review + the audit
|
||||||
'cloudfront-originaccesscontrol','waf','kms']`. The CDN and WAF are
|
running now). This matches the established convention (REVIEW.md is
|
||||||
**always present** in the static-assets composition (they are unconditional
|
overwritten at milestone complete; the per-phase verify commits are the
|
||||||
children + wires); the `waf_enabled`, `default_ttl`, `max_ttl`,
|
record). Not a defect.
|
||||||
`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
|
||||||
|
|
||||||
- **WAF scope fix (`upper(var.scope)`):** correct and defensive; AWS
|
- **State-bucket drift fix (P1):** `adapter.py:117` now emits
|
||||||
provider v5 requires uppercase. The `local.scope` indirection is clean.
|
`nova-tfstate-*` (matching the live bucket renamed in v1.15 P4). The
|
||||||
- **VPC `create_before_destroy` + same-CIDR complex example:** correct
|
new `test_adapt_emits_nova_state_bucket` regression guard asserts this.
|
||||||
fix for the DependencyViolation on modify. Using the same CIDR means
|
- **Kyverno label fix (P1):** `require-resource-labels.yml` enforces
|
||||||
terraform modifies in-place rather than replacing the VPC (which would
|
`nova:*` labels (consistent with `nova_tagging.py` hard-fail on
|
||||||
cascade-fail on dependent subnets/IGW). The `create_before_destroy`
|
`acdl:*`). No policy contradiction.
|
||||||
lifecycle is the right guard.
|
- **Ingestor defense-in-depth (P10):** fail-closed on missing IAM
|
||||||
- **ALB `name_prefix`:** correct terraform pattern for
|
identity (401, not silent pass); env enum derived from
|
||||||
create_before_destroy + name-uniqueness; well-documented commit message.
|
`core/environments/` (not hardcoded). The `NOVA_LAMBDA_LOCAL_BYPASS`
|
||||||
- **Adapter dedup (for the registered-module case):** correct —
|
env allows local/stub testing without blocking the fail-closed path.
|
||||||
multi-resource L1s like cloudfront (distribution + OAC) correctly merge
|
- **Payload validation (P11):** 256 KB size cap + contract.schema.json
|
||||||
into one `module "cloudfront-distribution" { ... }` block. The merge
|
validation before the DynamoDB write; aligned error/stackTrace caps
|
||||||
preserves first-resource inputs and union of outputs. (The
|
(both 10000).
|
||||||
unregistered-module drop is P1-1, a separate concern.)
|
- **Regression gate (G-111):** CAP-013..016 return `Skipped` (not
|
||||||
- **L2 composition wiring (`ecr.inputs.name`, `roles.inputs.role_name`):**
|
`Decayed`/`Broken`) for the post-teardown steady state (D-096).
|
||||||
correct. Resolving microservice complex now shows `ecr.inputs.name =
|
`passed` accepts Skipped. Gate passes at 18V+4S.
|
||||||
"app-repo"` and `roles.inputs.role_name = "app-role"` (defaults applied
|
- **Workflow generator (P8):** `sync_workflows.py` + `workflows-src/`
|
||||||
since the contract doesn't set `name`). Previously these would have hit
|
single source; the byte-identity test is replaced with a generator-
|
||||||
the "missing required arg" defect class from the v1.10 sweep.
|
output test (`--check` exits 0). The 3 pairs are no longer hand-synced.
|
||||||
- **Microservice complex = real modify:** `desired_count: 2` (vs simple's
|
- **Onboarding request path (P18-P20):** schema + Lambda action (pending
|
||||||
default 1) is a genuine in-place modify — confirmed by resolving both
|
CMDB row, no AWS resources) + env-file autogen + offline-proven
|
||||||
and diffing `service-service.inputs.desired_count`.
|
cross-account Terraform. Self-service message (no "contact the platform
|
||||||
- **`ACDL_REMOTE_STATE_KEY` plumbing:** correct end-to-end — the L2 scripts
|
team"). Real AWS provisioning explicitly deferred (D-113/D-114).
|
||||||
export it, the adapter reads it with a sensible default, and the
|
- **Splits (P12/P13):** `contract_resolver` + `regression_verify` split
|
||||||
microservice composition's `terraform_remote_state` data block picks it
|
with re-export shims; G-113 one-way import direction documented. All
|
||||||
up. This cleanly separates the short-lived CI VPC state from the
|
tests pass without modification (backwards compat preserved).
|
||||||
long-lived platform VPC state.
|
- **DX (P15-P17):** `--help` works + documents all 9 flags; workflows
|
||||||
- **Workflow structure:** `l2-lifecycle` correctly `needs: ci-vpc-apply`;
|
README catalogs all 7 workflows; getting-started is offline-first.
|
||||||
`ci-vpc-destroy` correctly `needs: [lifecycle, l2-lifecycle]` and
|
- **Regression gate:** 18 Verified + 4 Skipped at P9 + P21 (0 Decayed/
|
||||||
`if: always()`. The 7 new L2 pipeline-contract tests assert all of this.
|
Broken). The 4 Skipped are the post-v1.11-teardown live-AWS caps.
|
||||||
- **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 (485 offline tests)
|
## Test coverage assessment
|
||||||
|
|
||||||
- **Adequate:** pipeline contract (89 tests), schema validation, contract
|
~635 tests pass (was ~620 at v1.15.4). New test files:
|
||||||
resolution, adapter emission (basic), confidence signal, outbox,
|
- `tests/test_onboarding.py` (3 tests — env-file generation)
|
||||||
interpolation, local emulators, module-standards file presence, design-doc
|
- `tests/test_onboarding_terraform.py` (3 tests — terraform validate + tags)
|
||||||
currency.
|
- `tests/test_docs_coverage.py` (expanded — workflows README catalog)
|
||||||
- **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).
|
|
||||||
|
|
||||||
The 485 count is honest (447 pass fast, 5 deselected slow, 485/490
|
New tests in existing files: `test_adapt_emits_nova_state_bucket`,
|
||||||
collected). The gap is behavioral coverage of the new adapter + module
|
`test_onboarding_message_says_nova_not_acdl`, `test_no_identity_fails_closed`,
|
||||||
logic, not contract/schema coverage.
|
`test_no_identity_passes_with_local_bypass`, `test_oversized_contract_rejected`,
|
||||||
|
`test_schema_invalid_contract_rejected`, `TestNarrowedException` (2 tests),
|
||||||
|
`TestOnboardConsumer` (3 tests), `TestOnboardingMessageSelfService` (2 tests),
|
||||||
|
`test_sync_workflows_check_passes`.
|
||||||
|
|
||||||
## Verdict
|
## Verdict
|
||||||
|
|
||||||
**PASS with P1 flags for post-hoc review.** No P0 fixes applied. The
|
**PASS — 0 P0, 0 P1, 2 P2 (post-hoc, accepted).** The v1.16 NFR milestone
|
||||||
milestone's structural controls (regression gate, mandatory teardown,
|
is complete. All 20 requirements (REQ-165..184) satisfied; regression
|
||||||
byte-identical workflows, byte-identical contract↔workflow tests) are
|
gate 18V+4S; CI 3-stage PASS at every phase boundary. The onboarding
|
||||||
sound. The most material finding is P1-5 (the regression gate's
|
request path is self-service; real AWS provisioning deferred. The
|
||||||
CAP-017..022 evidence is an offline proxy, not live pipeline evidence) —
|
state-bucket drift + Kyverno label contradiction (the two correctness
|
||||||
this is a repeat of the v1.10 "VERIFY was diff-scoped" structural defect
|
regressions from the v1.15 rebrand) are fixed with regression guards.
|
||||||
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.
|
|
||||||
+816
-2
@@ -1,4 +1,4 @@
|
|||||||
# ACDL — Roadmap
|
# Nova — Roadmap
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@
|
|||||||
- **v1.13.1 (complete, tag `v1.13.1`):** config.json schema migration — regenerate `.ciagent/config.json` to the updated CIAgent v2 config structure (drop removed fields, migrate `gitea`→`release.gitea`, add `secrets`/`ship`/`backend`/`ideation`/`personas`/`logging`/`telemetry` sections). Code review: 0 P0, 2 P1/P2 auto-fixed. Docs-only NFR patch (no code changes). Gitea release id 253.
|
- **v1.13.1 (complete, tag `v1.13.1`):** config.json schema migration — regenerate `.ciagent/config.json` to the updated CIAgent v2 config structure (drop removed fields, migrate `gitea`→`release.gitea`, add `secrets`/`ship`/`backend`/`ideation`/`personas`/`logging`/`telemetry` sections). Code review: 0 P0, 2 P1/P2 auto-fixed. Docs-only NFR patch (no code changes). Gitea release id 253.
|
||||||
- **v1.13.2 (complete, tag `v1.13.2`):** presentation badge cleanup + platform architecture diagram — removed all `testing`/`agentic` maturity badges from both decks (only `planned` retained); added a new Slide 3 "The platform at a glance" with a shared high-level logical architecture diagram (consumer surfaces → contract → central pipeline → cross-cutting components → AWS) to both decks; renumbered subsequent slides 4–11; synced talking points + README. Docs-only NFR patch (no code changes).
|
- **v1.13.2 (complete, tag `v1.13.2`):** presentation badge cleanup + platform architecture diagram — removed all `testing`/`agentic` maturity badges from both decks (only `planned` retained); added a new Slide 3 "The platform at a glance" with a shared high-level logical architecture diagram (consumer surfaces → contract → central pipeline → cross-cutting components → AWS) to both decks; renumbered subsequent slides 4–11; synced talking points + README. Docs-only NFR patch (no code changes).
|
||||||
- **v1.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html
|
- **v1.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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1063,7 +1065,7 @@ Docs-only NFR patch (no code changes).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.14 (active — NFR Refinement: bug fixes, security, stubs, tests, docs)
|
## v1.14 (complete — NFR Refinement: bug fixes, security, stubs, tests, docs, tag `v1.13.24`)
|
||||||
|
|
||||||
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
|
||||||
@@ -1432,3 +1434,815 @@ on the v1.13.x line: `v1.13.3` (P0) → `v1.13.4..v1.13.23` (P1–P20) →
|
|||||||
- 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` (P1–P4) → `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 P2–P4.
|
||||||
|
- **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` (P1–P20) → `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 (P1–P4): state-bucket + Kyverno rebrand fix (correctness
|
||||||
|
regression), user-facing ACDL→Nova sweep, dead-code cleanup, except
|
||||||
|
narrowing.
|
||||||
|
- Wave 2 (P5–P9): 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 (P10–P14): 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 (P15–P17): run-platform --help + flags doc, workflows README
|
||||||
|
catalog (7 workflows), getting-started consolidation (offline-first).
|
||||||
|
- Wave 5 (P18–P20): 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` (P1–P6) → `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).
|
||||||
|
|||||||
+13
-9
@@ -2,13 +2,13 @@
|
|||||||
"projects": [
|
"projects": [
|
||||||
{
|
{
|
||||||
"slug": "acdl",
|
"slug": "acdl",
|
||||||
"name": "Agentic Cloud Delivery Platform",
|
"name": "Nova — The New Dawn of DevSecOps",
|
||||||
"default": true
|
"default": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"active_project": "acdl",
|
"active_project": "acdl",
|
||||||
"active_projects": ["acdl"],
|
"active_projects": ["acdl"],
|
||||||
"active_milestone": "v1.14",
|
"active_milestone": "v1.25",
|
||||||
"autonomy": {
|
"autonomy": {
|
||||||
"level": "full",
|
"level": "full",
|
||||||
"escalation_hooks": ["deploy", "delete_data", "merge_to_main"],
|
"escalation_hooks": ["deploy", "delete_data", "merge_to_main"],
|
||||||
@@ -37,14 +37,13 @@
|
|||||||
"escalate_high_severity": true,
|
"escalate_high_severity": true,
|
||||||
"bash_allowlist": {
|
"bash_allowlist": {
|
||||||
"allowed_commands": [
|
"allowed_commands": [
|
||||||
"npm", "node", "npx", "pnpm", "yarn",
|
|
||||||
"git", "ls", "cat", "head", "tail", "wc",
|
"git", "ls", "cat", "head", "tail", "wc",
|
||||||
"echo", "mkdir", "cp", "mv", "rm", "touch",
|
"echo", "mkdir", "cp", "mv", "rm", "touch",
|
||||||
"pwd", "which", "env", "printenv",
|
"pwd", "which", "env", "printenv",
|
||||||
"jest", "eslint", "tsc", "prettier",
|
"python3", "pytest", "pip",
|
||||||
|
"terraform", "checkov",
|
||||||
"curl", "wget",
|
"curl", "wget",
|
||||||
"docker", "docker-compose",
|
"docker", "docker-compose"
|
||||||
"ts-node", "tsx"
|
|
||||||
],
|
],
|
||||||
"max_output_bytes": 1048576,
|
"max_output_bytes": 1048576,
|
||||||
"timeout_ms": 30000,
|
"timeout_ms": 30000,
|
||||||
@@ -59,7 +58,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
"branching_strategy": "phase",
|
"branching_strategy": "flat",
|
||||||
|
"_branching_strategy_note": "ACDL uses flat workflow (committed directly to main per established convention since v1.0). The 'phase' strategy is advisory; CIAgent uses milestone/phase branches for v1.14 but the project convention is flat.",
|
||||||
"auto_commit": true,
|
"auto_commit": true,
|
||||||
"auto_push": true
|
"auto_push": true
|
||||||
},
|
},
|
||||||
@@ -125,6 +125,7 @@
|
|||||||
},
|
},
|
||||||
"ollama-cloud": {
|
"ollama-cloud": {
|
||||||
"base_url": "",
|
"base_url": "",
|
||||||
|
"_base_url_note": "Intentionally unset. The runtime uses the glm-5.2 model via the opencode backend (not the llm_backends config). This entry is for reference only.",
|
||||||
"api_key_env": "OLLAMA_CLOUD_API_KEY",
|
"api_key_env": "OLLAMA_CLOUD_API_KEY",
|
||||||
"model_profile": "quality",
|
"model_profile": "quality",
|
||||||
"timeout_ms": 60000
|
"timeout_ms": 60000
|
||||||
@@ -191,9 +192,11 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend-engineer",
|
"name": "frontend-engineer",
|
||||||
"domain": "frontend",
|
"domain": "frontend",
|
||||||
|
"active": false,
|
||||||
"frameworks": ["react", "next.js"],
|
"frameworks": ["react", "next.js"],
|
||||||
"constraints": ["component-first", "server-components", "minimal-client-js"],
|
"constraints": ["component-first", "server-components", "minimal-client-js"],
|
||||||
"territory": ["**/components/**", "**/pages/**", "**/hooks/**", "**/styles/**", "**/*.tsx", "**/*.css", "**/*.vue"]
|
"territory": ["**/components/**", "**/pages/**", "**/hooks/**", "**/styles/**", "**/*.tsx", "**/*.css", "**/*.vue"],
|
||||||
|
"reason": "ACDL has no frontend (no package.json); decks are markdown (lead-developer territory). Deactivated per PERSONAS.md:80."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -205,5 +208,6 @@
|
|||||||
"telemetry": {
|
"telemetry": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"persist": true
|
"persist": true
|
||||||
}
|
},
|
||||||
|
"strategic_direction_file": ".ciagent/NORTH_STAR.md"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
=== 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% /
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{"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}}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Gitea Workflows — Limitation Documentation (v1.14, REQ-150)
|
||||||
|
|
||||||
|
## Shared workflows (byte-identical Gitea + GitHub)
|
||||||
|
|
||||||
|
These 3 workflows exist in both `.gitea/workflows/` and `.github/workflows/`
|
||||||
|
and are byte-identical (asserted by `tests/test_pipeline_contract.py`):
|
||||||
|
|
||||||
|
- `ci.yml` — lint + test + check-only (runs on every PR)
|
||||||
|
- `deploy.yml` — reusable deploy workflow (invoked by consumer repos)
|
||||||
|
- `modules-lifecycle.yml` — L1 + L2 module lifecycle pipeline (plan-only
|
||||||
|
default, full on workflow_dispatch override)
|
||||||
|
|
||||||
|
## GitHub-only workflows (no Gitea mirror)
|
||||||
|
|
||||||
|
These 4 workflows exist only in `.github/workflows/`:
|
||||||
|
|
||||||
|
- `platform-test.yml` — PR pipeline: lint + unit + integration + schema
|
||||||
|
validation. Uses GitHub Actions features (reusable workflow composition,
|
||||||
|
environment protection) not available in Gitea Actions.
|
||||||
|
- `primitives-plan.yml` — PR plan-only matrix over all L1 primitives. Uses
|
||||||
|
GitHub matrix strategy + `terraform plan` against live AWS.
|
||||||
|
- `patterns-plan.yml` — PR plan-only matrix over all L2 modules. Same
|
||||||
|
pattern as primitives-plan.
|
||||||
|
- `release.yml` — release job on merge to main: computes next semver,
|
||||||
|
creates + updates MAJOR.MINOR.PATCH / MAJOR.MINOR / MAJOR floating tags,
|
||||||
|
creates a GitHub release. GitHub-only by design (Gitea releases are
|
||||||
|
created via the ship workflow's API call, not a workflow).
|
||||||
|
|
||||||
|
## Why no Gitea mirror
|
||||||
|
|
||||||
|
Gitea Actions (act_runner) has limited support for reusable workflow
|
||||||
|
composition, environment protection, and the `gh` CLI used by the release
|
||||||
|
job. The 3 shared workflows are the ones that need to run on both forges
|
||||||
|
(CI + deploy + lifecycle). The 4 GitHub-only workflows are the
|
||||||
|
production-grade platform pipelines that run on GitHub Actions; Gitea is
|
||||||
|
the dev/integration forge. Mirroring them would require feature parity
|
||||||
|
that Gitea Actions does not currently provide.
|
||||||
|
|
||||||
|
This is a documented limitation, not a defect. A future milestone may
|
||||||
|
add Gitea mirrors if act_runner gains the required features.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# ACDL CI Pipeline — Gitea Actions (dev environment)
|
# Nova CI Pipeline (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)
|
||||||
|
|||||||
+17
-14
@@ -1,4 +1,4 @@
|
|||||||
# ACDL Reusable Deploy Workflow — Gitea Actions (dev environment)
|
# Nova Reusable Deploy Workflow (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: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea)
|
# uses: nova/.github/workflows/deploy.yml@v1.19
|
||||||
# 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 .acdl/contract.yml)
|
# contract — path to the consumer's contract YAML (default .nova/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. Gitea pending
|
# Override (where OIDC is unavailable, e.g. pending
|
||||||
# go-gitea/gitea#36988): set ACDL_AWS_ACCESS_KEY_ID + ACDL_AWS_SECRET_ACCESS_KEY
|
# upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_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: acdl-deploy
|
name: nova-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: .acdl/contract.yml
|
default: .nova/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,13 +102,16 @@ 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:
|
||||||
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) || '' }}
|
# P4 (REQ-163): IAM role renamed acdl-deploy- → nova-deploy-.
|
||||||
|
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.ACDL_AWS_ACCESS_KEY_ID }}
|
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
secret-access-key: ${{ secrets.NOVA_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
|
||||||
@@ -145,7 +148,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.ACDL_LAMBDA_URL }}" \
|
--function-url "${{ secrets.NOVA_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
|
||||||
@@ -153,13 +156,13 @@ jobs:
|
|||||||
- name: Upload emitted Terraform
|
- name: Upload emitted Terraform
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: acdl-terraform
|
name: nova-terraform
|
||||||
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
path: /tmp/nova_platform_run/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: acdl-platform-log
|
name: nova-platform-log
|
||||||
path: platform/logs/
|
path: platform/logs/
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# ACDL Modules Lifecycle Pipeline — Gitea Actions (dev environment)
|
# Nova Modules Lifecycle Pipeline (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 .gitea/workflows/ and .github/workflows/).
|
# in .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 ACDL_LIFECYCLE_MODE repo variable)
|
# "full" via workflow_dispatch (or the NOVA_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.ACDL_LIFECYCLE_MODE != 'plan' }}
|
if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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:
|
||||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_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.ACDL_LIFECYCLE_MODE == 'full' }}
|
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
|
||||||
working-directory: terraform/ci-vpc
|
working-directory: terraform/ci-vpc
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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:
|
||||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_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.ACDL_LIFECYCLE_MODE == 'full' }}
|
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
|
||||||
working-directory: terraform/ci-vpc
|
working-directory: terraform/ci-vpc
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_LIFECYCLE_MODE != 'plan' }}
|
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 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,4 +1,4 @@
|
|||||||
# ACDL CI Pipeline — Gitea Actions (dev environment)
|
# Nova CI Pipeline (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)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# ACDL Reusable Deploy Workflow — Gitea Actions (dev environment)
|
# Nova Reusable Deploy Workflow (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: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea)
|
# uses: nova/.github/workflows/deploy.yml@v1.19
|
||||||
# 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 .acdl/contract.yml)
|
# contract — path to the consumer's contract YAML (default .nova/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. Gitea pending
|
# Override (where OIDC is unavailable, e.g. pending
|
||||||
# go-gitea/gitea#36988): set ACDL_AWS_ACCESS_KEY_ID + ACDL_AWS_SECRET_ACCESS_KEY
|
# upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_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: acdl-deploy
|
name: nova-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: .acdl/contract.yml
|
default: .nova/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,13 +102,16 @@ 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:
|
||||||
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) || '' }}
|
# P4 (REQ-163): IAM role renamed acdl-deploy- → nova-deploy-.
|
||||||
|
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.ACDL_AWS_ACCESS_KEY_ID }}
|
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
secret-access-key: ${{ secrets.NOVA_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
|
||||||
@@ -145,7 +148,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.ACDL_LAMBDA_URL }}" \
|
--function-url "${{ secrets.NOVA_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
|
||||||
@@ -153,13 +156,13 @@ jobs:
|
|||||||
- name: Upload emitted Terraform
|
- name: Upload emitted Terraform
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: acdl-terraform
|
name: nova-terraform
|
||||||
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
path: /tmp/nova_platform_run/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: acdl-platform-log
|
name: nova-platform-log
|
||||||
path: platform/logs/
|
path: platform/logs/
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# ACDL Modules Lifecycle Pipeline — Gitea Actions (dev environment)
|
# Nova Modules Lifecycle Pipeline (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 .gitea/workflows/ and .github/workflows/).
|
# in .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 ACDL_LIFECYCLE_MODE repo variable)
|
# "full" via workflow_dispatch (or the NOVA_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.ACDL_LIFECYCLE_MODE != 'plan' }}
|
if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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:
|
||||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_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.ACDL_LIFECYCLE_MODE == 'full' }}
|
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
|
||||||
working-directory: terraform/ci-vpc
|
working-directory: terraform/ci-vpc
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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:
|
||||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_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.ACDL_LIFECYCLE_MODE == 'full' }}
|
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
|
||||||
working-directory: terraform/ci-vpc
|
working-directory: terraform/ci-vpc
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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.ACDL_LIFECYCLE_MODE != 'plan' }}
|
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_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.ACDL_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# ACDL Release Pipeline — GitHub Actions (production)
|
# Nova 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: acdl-release
|
name: nova-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 "ACDL ${{ steps.version.outputs.new_tag }}" \
|
--title "Nova ${{ steps.version.outputs.new_tag }}" \
|
||||||
--notes "$BODY" \
|
--notes "$BODY" \
|
||||||
--generate-notes || true
|
--generate-notes || true
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# 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
|
||||||
+24
-1
@@ -14,8 +14,31 @@ 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
|
||||||
**/tfplan
|
**/tfplan
|
||||||
**/*.tfstate*
|
**/*.tfstate*
|
||||||
|
|
||||||
|
# Credential patterns (v1.14, REQ-146)
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
*.cer
|
||||||
|
*.crt
|
||||||
|
*.jks
|
||||||
|
*.keystore.coverage
|
||||||
|
.coverage
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# ACDL — Agentic Cloud Delivery Platform
|
# Nova
|
||||||
|
|
||||||
|
> **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
|
||||||
@@ -18,7 +20,7 @@ a configuration file, or an infrastructure module.
|
|||||||
|
|
||||||
## Repository roles
|
## Repository roles
|
||||||
|
|
||||||
There are two kinds of repository in the ACDL model:
|
There are two kinds of repository in the Nova 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/`,
|
||||||
@@ -26,7 +28,7 @@ There are two kinds of repository in the ACDL 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 `.acdl/contract.yml`
|
2. **One or more contracts** — small YAML files at `.nova/contract.yml`
|
||||||
that declare infrastructure (one or more modules by name + version),
|
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
|
||||||
@@ -93,7 +95,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 ACDL by writing a contract that
|
Consumers have their own repos and consume Nova 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,
|
||||||
@@ -124,25 +126,51 @@ engine-specific code. `modules/`, `schemas/`, `contracts/`,
|
|||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
### Prerequisites
|
### Quick start (offline, no AWS required)
|
||||||
|
|
||||||
> These prerequisites are for running the **platform repo** locally. A
|
The fastest way to verify the platform works — no AWS credentials, no
|
||||||
> consumer does not need any of these — see the
|
bootstrap, no cost. See the [Consumer guide](docs/consumer-guide.md)
|
||||||
> [Consumer guide](docs/consumer-guide.md) for the consumer happy path.
|
for the consumer happy path (a consumer owns only a contract + app code).
|
||||||
|
|
||||||
- A platform-managed environment (see [docs/environments/](docs/environments/)).
|
```bash
|
||||||
For local testing, `core/environments/dev.json` is provided as the sample.
|
# Install test dependencies
|
||||||
- AWS credentials for the dev environment (in `.env.secrets`, gitignored;
|
pip install -r requirements-test.txt
|
||||||
see [Credentials & zero-trust](#credentials--zero-trust)).
|
|
||||||
- `terraform` (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3`
|
|
||||||
+ `jsonschema`.
|
|
||||||
|
|
||||||
### Run the platform pipeline end-to-end
|
# 1. Run the test suite (all offline — uses moto for DynamoDB mocking)
|
||||||
|
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 +
|
||||||
# acdl-spike-runner already exist)
|
# nova-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=... \
|
||||||
@@ -166,26 +194,6 @@ 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
|
||||||
@@ -211,23 +219,9 @@ bash scripts/run_ci.sh --quiet # suppress per-stage banners
|
|||||||
|
|
||||||
### Reusable deploy workflow
|
### Reusable deploy workflow
|
||||||
|
|
||||||
The deployment pipeline is defined by a **central deployment pipeline
|
Consumer repos invoke the deploy pipeline via `.github/workflows/deploy.yml`
|
||||||
contract** (`pipelines/contract.yml`, validated against
|
(a reusable GitHub Actions workflow, versioned tag `nova/.github/workflows/deploy.yml@v1.19`).
|
||||||
`schemas/deploy-pipeline.schema.json`) and exposed to consumer repos as a
|
See the [Consumer guide](docs/consumer-guide.md) for the end-to-end happy path.
|
||||||
**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)
|
||||||
|
|
||||||
@@ -247,7 +241,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 ACDL module to AWS is at
|
contract that deploys any Nova 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.
|
||||||
|
|
||||||
@@ -283,8 +277,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 `acdl:owner=<consumer-repo>` and
|
is tagged with `nova:owner=<consumer-repo>` and
|
||||||
`acdl:contract=<contract-id>`. The session policy grants
|
`nova: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**.
|
||||||
|
|
||||||
@@ -302,12 +296,6 @@ 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
-1
@@ -1,4 +1,4 @@
|
|||||||
# ACDL Adapters
|
# Nova Adapters
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
|||||||
@@ -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 ACDL
|
normalized Nova
|
||||||
[`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 ACDL platform today emits Terraform
|
manifests** (a K8s-native stack). The Nova 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 `acdl:owner` and
|
- `require-resource-labels.yml` — require `nova:owner` and
|
||||||
`acdl:environment` labels on all pods (mirrors the ACDL tagging standard
|
`nova:environment` labels on all pods (mirrors the Nova 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,4 +1,4 @@
|
|||||||
"""Kyverno adapter — translate Kyverno PolicyReport results to ACDL PolicyCheckResult records.
|
"""Kyverno adapter — translate Kyverno PolicyReport results to Nova 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
|
||||||
@@ -8,13 +8,16 @@ v1.9 (REQ-111): the translator is fleshed out — full PolicyReport →
|
|||||||
PolicyCheckResult mapping with severity + skip-with-reason handling. It
|
PolicyCheckResult mapping with severity + skip-with-reason handling. It
|
||||||
remains inactive for Terraform-only stacks (guard preserved — emits a
|
remains inactive for Terraform-only stacks (guard preserved — emits a
|
||||||
single SKIPPED `KYVERNO_INACTIVE_TF_STACK` record when no K8s manifests).
|
single SKIPPED `KYVERNO_INACTIVE_TF_STACK` record when no K8s manifests).
|
||||||
A `--kube-version` stub is parsed but not yet used (for future GitOps).
|
A `--kube-version` flag was previously parsed but never used. It has been
|
||||||
|
removed (v1.14, G-103) to resolve the stub. Version-aware policy selection
|
||||||
|
will be added when the GitOps reconciler emits K8s manifests (D-053
|
||||||
|
roadmap). The adapter is inactive for Terraform-only stacks today.
|
||||||
|
|
||||||
D-053: the platform emits Terraform, not K8s manifests. This adapter
|
D-053: the platform emits Terraform, not K8s manifests. This adapter
|
||||||
activates when the GitOps reconciler (roadmap) emits K8s manifests.
|
activates when the GitOps reconciler (roadmap) emits K8s manifests.
|
||||||
Sample policies are included as documentation at adapters/kyverno/policies/.
|
Sample policies are included as documentation at adapters/kyverno/policies/.
|
||||||
|
|
||||||
CLI: kyverno_adapter.py <policyreport.json> <contract-id> [--kube-version <ver>]
|
CLI: kyverno_adapter.py <policyreport.json> <contract-id>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
@@ -100,7 +103,7 @@ def _emit_inactive_tf(contract_id):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def adapt(policyreport_json_path, contract_id, kube_version=None):
|
def adapt(policyreport_json_path, contract_id):
|
||||||
with open(policyreport_json_path, "r", encoding="utf-8") as fh:
|
with open(policyreport_json_path, "r", encoding="utf-8") as fh:
|
||||||
data = json.load(fh)
|
data = json.load(fh)
|
||||||
out = []
|
out = []
|
||||||
@@ -112,8 +115,6 @@ def adapt(policyreport_json_path, contract_id, kube_version=None):
|
|||||||
out.append(_to_pcr(entry, contract_id))
|
out.append(_to_pcr(entry, contract_id))
|
||||||
if not out:
|
if not out:
|
||||||
out.append(_emit_inactive_tf(contract_id))
|
out.append(_emit_inactive_tf(contract_id))
|
||||||
# kube_version is parsed but not yet used (future GitOps reconciler).
|
|
||||||
_ = kube_version
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -123,14 +124,8 @@ def adapt_inactive(contract_id):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
kube_ver = None
|
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
if "--kube-version" in args:
|
|
||||||
idx = args.index("--kube-version")
|
|
||||||
if idx + 1 < len(args):
|
|
||||||
kube_ver = args[idx + 1]
|
|
||||||
args = args[:idx] + args[idx + 2:]
|
|
||||||
if len(args) != 2:
|
if len(args) != 2:
|
||||||
print("usage: kyverno_adapter.py <policyreport.json> <contract-id> [--kube-version <ver>]", file=sys.stderr)
|
print("usage: kyverno_adapter.py <policyreport.json> <contract-id>", file=sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
print(json.dumps(adapt(args[0], args[1], kube_version=kube_ver), indent=2))
|
print(json.dumps(adapt(args[0], args[1]), indent=2))
|
||||||
@@ -3,7 +3,7 @@ kind: ClusterPolicy
|
|||||||
metadata:
|
metadata:
|
||||||
name: require-resource-labels
|
name: require-resource-labels
|
||||||
annotations:
|
annotations:
|
||||||
policies.kyverno.io/title: Require ACDL Resource Labels
|
policies.kyverno.io/title: Require Nova 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-acdl-owner-label
|
- name: require-nova-owner-label
|
||||||
match:
|
match:
|
||||||
any:
|
any:
|
||||||
- resources:
|
- resources:
|
||||||
kinds:
|
kinds:
|
||||||
- Pod
|
- Pod
|
||||||
validate:
|
validate:
|
||||||
message: "Pods must carry the acdl:owner label (ACDL tagging standard)."
|
message: "Pods must carry the nova:owner label (Nova tagging standard)."
|
||||||
pattern:
|
pattern:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
acdl:owner: "?*"
|
nova:owner: "?*"
|
||||||
- name: require-acdl-environment-label
|
- name: require-nova-environment-label
|
||||||
match:
|
match:
|
||||||
any:
|
any:
|
||||||
- resources:
|
- resources:
|
||||||
kinds:
|
kinds:
|
||||||
- Pod
|
- Pod
|
||||||
validate:
|
validate:
|
||||||
message: "Pods must carry the acdl:environment label (ACDL tagging standard)."
|
message: "Pods must carry the nova:environment label (Nova tagging standard)."
|
||||||
pattern:
|
pattern:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
acdl:environment: "?*"
|
nova:environment: "?*"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""ACDL Terraform adapter — stateless assembler (v1.11 RESTART, P56a).
|
"""Nova 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,9 +10,10 @@ 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
|
import json, os, sys
|
||||||
import os
|
_R = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
import sys
|
sys.path.insert(0, _R) if _R not in sys.path else None
|
||||||
|
from core import env
|
||||||
|
|
||||||
|
|
||||||
def _load_registry(repo_root):
|
def _load_registry(repo_root):
|
||||||
@@ -112,6 +113,11 @@ 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")
|
||||||
|
state_bucket = f"nova-tfstate-{account_id}-us-east-1"
|
||||||
|
# State key is env-scoped (v1.24 REQ-287): the {environment} segment lets
|
||||||
|
# the env-transition detect-and-destroy step target the PRIOR env's state
|
||||||
|
# without affecting the new env. No orphan path on environment promotion.
|
||||||
terraform_tf = (
|
terraform_tf = (
|
||||||
'terraform {\n'
|
'terraform {\n'
|
||||||
' required_version = ">= 1.9, < 1.10"\n'
|
' required_version = ">= 1.9, < 1.10"\n'
|
||||||
@@ -122,7 +128,7 @@ def adapt(stack_instance, out_dir):
|
|||||||
' }\n'
|
' }\n'
|
||||||
' }\n'
|
' }\n'
|
||||||
' backend "s3" {\n'
|
' backend "s3" {\n'
|
||||||
' bucket = "acdl-tfstate-581513795199-us-east-1"\n'
|
f' bucket = "{state_bucket}"\n'
|
||||||
f' key = "spike/{stack_name}/{environment}/terraform.tfstate"\n'
|
f' key = "spike/{stack_name}/{environment}/terraform.tfstate"\n'
|
||||||
' region = "us-east-1"\n'
|
' region = "us-east-1"\n'
|
||||||
' }\n'
|
' }\n'
|
||||||
@@ -132,12 +138,12 @@ 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 = os.environ.get("ACDL_REMOTE_STATE_KEY", "platform/terraform.tfstate")
|
remote_state_key = env.get_env("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'
|
||||||
' config = {\n'
|
' config = {\n'
|
||||||
' bucket = "acdl-tfstate-581513795199-us-east-1"\n'
|
f' bucket = "{state_bucket}"\n'
|
||||||
f' key = "{remote_state_key}"\n'
|
f' key = "{remote_state_key}"\n'
|
||||||
' region = "us-east-1"\n'
|
' region = "us-east-1"\n'
|
||||||
' }\n'
|
' }\n'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Translate Checkov JSON output to ACDL PolicyCheckResult records.
|
"""Translate Checkov JSON output to Nova 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,16 +6,23 @@ 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 ACDL tagging standard (D-054, D-043 closure) is enforced by a custom
|
The Nova tagging standard (D-054, D-043 closure, D-109 hard mode in P3)
|
||||||
Checkov rule at adapters/terraform/policy/custom_rules/acdl_tagging.py,
|
is enforced by a custom Checkov rule at
|
||||||
loaded via --external-checks-dir. The adapter therefore maps
|
adapters/terraform/policy/custom_rules/nova_tagging.py, loaded via
|
||||||
ACDL_TAG_NAMING as a real rule (no synthetic SKIPPED record is emitted).
|
--external-checks-dir. The adapter therefore maps NOVA_TAG_NAMING as a
|
||||||
|
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"),
|
||||||
@@ -29,10 +36,12 @@ 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: ACDL_TAG_NAMING is now a real custom Checkov
|
# D-054 / D-043 closure, D-109 hard mode (P3): NOVA_TAG_NAMING is a real
|
||||||
# rule (adapters/terraform/policy/custom_rules/acdl_tagging.py), loaded
|
# custom Checkov rule (adapters/terraform/policy/custom_rules/nova_tagging.py),
|
||||||
# via --external-checks-dir. No synthetic SKIPPED record is emitted.
|
# loaded via --external-checks-dir. No synthetic SKIPPED record is emitted.
|
||||||
"ACDL_TAG_NAMING": ("tagging-standard", "medium"),
|
# Renamed from ACDL_TAG_NAMING in P2 (REQ-158). Hard mode as of P3
|
||||||
|
# (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"}
|
||||||
@@ -66,7 +75,7 @@ def _to_pcr(checkov_record, contract_id, result_str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def adapt(checkov_json_path, contract_id):
|
def adapt(checkov_json_path, contract_id, run_id=None, environment="dev"):
|
||||||
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 = []
|
||||||
@@ -80,6 +89,25 @@ def adapt(checkov_json_path, contract_id):
|
|||||||
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,16 +1,24 @@
|
|||||||
# ACDL Custom Checkov Rules
|
# Nova Custom Checkov Rules
|
||||||
|
|
||||||
This directory holds ACDL-authored Checkov custom rules, written in the
|
This directory holds Nova-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
|
||||||
|
|
||||||
- `acdl_tagging.py` — `ACDL_TAG_NAMING` (D-054): ensures every taggable AWS
|
- `nova_tagging.py` — `NOVA_TAG_NAMING` (D-054, D-109 warn mode in P2):
|
||||||
resource carries the four required ACDL tags
|
ensures every taggable AWS resource carries the four required Nova tags
|
||||||
(`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`).
|
(`nova:owner`, `nova:contract`, `nova:environment`, `nova:cost-center`).
|
||||||
This rule replaces the synthetic SKIPPED `ACDL_TAG_NAMING` record that the
|
This rule replaces the synthetic SKIPPED `NOVA_TAG_NAMING` record that the
|
||||||
Checkov adapter previously emitted (D-043 closure). The canonical tag set
|
Checkov adapter previously emitted (D-043 closure). Renamed from
|
||||||
is declared in [`schemas/tagging-standard.json`](../../../schemas/tagging-standard.json).
|
`acdl_tagging.py` / `ACDL_TAG_NAMING` in P2 (REQ-158). The canonical tag
|
||||||
|
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
|
||||||
|
|
||||||
@@ -23,12 +31,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 = AcdlTaggingStandard()` line at
|
module-level `check` object (see the `check = NovaTaggingStandard()` line at
|
||||||
the bottom of `acdl_tagging.py`).
|
the bottom of `nova_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 `ACDL_TAG_NAMING` to `(tagging-standard, medium)` in `RULE_MAP`. The
|
maps `NOVA_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.
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"""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()
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""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()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Wiz adapter — translate Wiz API results to ACDL PolicyCheckResult records.
|
"""Wiz adapter — translate Wiz API results to Nova 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,8 +186,37 @@ 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__":
|
||||||
if len(sys.argv) != 3:
|
import argparse
|
||||||
print("usage: wiz_adapter.py <wiz_issues.json> <contract-id>", file=sys.stderr)
|
parser = argparse.ArgumentParser(description="Wiz adapter (REQ-250: plan-mode supported)")
|
||||||
sys.exit(2)
|
parser.add_argument("wiz_json", nargs="?", help="wiz_issues.json (legacy positional mode)")
|
||||||
print(json.dumps(adapt(sys.argv[1], sys.argv[2]), indent=2))
|
parser.add_argument("contract_id_pos", nargs="?", help="contract-id (legacy positional mode)")
|
||||||
|
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,4 +1,4 @@
|
|||||||
# ACDL sample consumer contract — microservice module (dev)
|
# Nova 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,4 +1,4 @@
|
|||||||
# ACDL sample consumer contract — microservice module (dr)
|
# Nova 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,4 +1,4 @@
|
|||||||
# ACDL sample consumer contract — microservice module (prod)
|
# Nova 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,4 +1,4 @@
|
|||||||
# ACDL sample consumer contract — microservice module (qa)
|
# Nova 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,4 +1,4 @@
|
|||||||
# ACDL sample consumer contract — microservice module (dev)
|
# Nova 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,4 +1,4 @@
|
|||||||
# ACDL sample consumer contract — static-assets module (dev)
|
# Nova 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,4 +1,4 @@
|
|||||||
# ACDL sample consumer contract — static-assets module (dr)
|
# Nova 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,4 +1,4 @@
|
|||||||
# ACDL sample consumer contract — static-assets module (prod)
|
# Nova 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,4 +1,4 @@
|
|||||||
# ACDL sample consumer contract — static-assets module (qa)
|
# Nova 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,4 +1,4 @@
|
|||||||
# ACDL sample consumer contract — static-assets module (dev)
|
# Nova 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)
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ 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 `ACDL_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged
|
when `NOVA_ATTESTATION_SIGNING_KEY_ID` is set (dual-read via core/env.py:
|
||||||
|
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.
|
||||||
"""
|
"""
|
||||||
@@ -24,6 +25,14 @@ 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 = {
|
||||||
@@ -81,14 +90,15 @@ 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 ACDL_ATTESTATION_SIGNING_KEY_ID is set.
|
"""Verify the JWS detached signature when NOVA_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 = os.environ.get("ACDL_ATTESTATION_SIGNING_KEY_ID", "")
|
key_id = env.get_env("ATTESTATION_SIGNING_KEY_ID", "") or ""
|
||||||
if not key_id:
|
if not key_id:
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
"[attestation] ACDL_ATTESTATION_SIGNING_KEY_ID unset — "
|
"[attestation] NOVA_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
|
||||||
|
|||||||
@@ -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 Gitea Actions scheduled workflow)
|
- **Async worker + DLQ:** a Lambda (or a forge 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` (Gitea/GitHub username of the QA approver; populated on
|
- `approver_qa` (CI 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 `gitea.actor` / `github.actor`. v1.9's
|
to the prod-dispatch CI 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-
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""ACDL Confidence Signal (REQ-19).
|
"""Nova 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,8 +34,13 @@ 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,
|
||||||
@@ -161,7 +166,33 @@ 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"
|
||||||
return Signal(score, band, per_input, reasons)
|
signal = 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__":
|
||||||
|
|||||||
+49
-62
@@ -1,4 +1,4 @@
|
|||||||
"""ACDL Contract Resolver — resolve a consumer contract to a Target Stack instance.
|
"""Nova 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,26 +36,27 @@ 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.
|
||||||
|
|
||||||
Mirrors core.environment_check.load() but is self-contained so the
|
P7 (REQ-171): delegates to core.environment_check.load() (dedup —
|
||||||
resolver works both as a package import (`from core.contract_resolver
|
the two were verbatim duplicates). The environment_check module is
|
||||||
import resolve`) and as a script (`python3 core/contract_resolver.py`).
|
in the same core/ package, so the import works both as a package
|
||||||
Emits a stderr warning when account_id is the placeholder and env != dev.
|
import and as a script (`python3 core/contract_resolver.py`).
|
||||||
"""
|
"""
|
||||||
env_file = os.path.join(repo_root, "core", "environments", f"{env_name}.json")
|
from core import environment_check
|
||||||
if not os.path.isfile(env_file):
|
return environment_check.load(env_name, root=repo_root)
|
||||||
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):
|
||||||
@@ -63,6 +64,21 @@ 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)
|
||||||
@@ -436,24 +452,9 @@ def _namespace_resources(resources, module_name):
|
|||||||
|
|
||||||
|
|
||||||
def decommission_transform(stack_instance):
|
def decommission_transform(stack_instance):
|
||||||
"""REQ-92: Transform a resolved stack instance for decommission.
|
"""REQ-92: re-export from core.decommission_transform (P12, REQ-176)."""
|
||||||
|
from core.decommission_transform import decommission_transform as _dt
|
||||||
Sets all scalable counts to 0 and deletion_protection to false on
|
return _dt(stack_instance)
|
||||||
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):
|
||||||
@@ -461,7 +462,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 ACDL repo (defaults to two levels up from this file).
|
repo_root: Root of the Nova 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
|
||||||
@@ -482,7 +483,7 @@ 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_json(os.path.join(repo_root, "schemas", "contract.schema.json"))
|
contract_schema = _load_schema(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)
|
||||||
@@ -524,10 +525,14 @@ 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
|
# Determine if L1 or L2 — prefer the registry `kind` field (P7,
|
||||||
|
# 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 = "l2" in interface_path or "composition" in interface_path
|
is_l2 = entry.get("kind") == "l2" or (
|
||||||
|
"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,
|
||||||
@@ -570,13 +575,8 @@ 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
|
# Determine stack kind: L2 if any module is L2 or if multi-module (P7)
|
||||||
if multi_module:
|
kind = "l2" if (multi_module or any_l2) else "l1"
|
||||||
kind = "l2"
|
|
||||||
elif any_l2:
|
|
||||||
kind = "l2"
|
|
||||||
else:
|
|
||||||
kind = "l1"
|
|
||||||
|
|
||||||
stack_instance = {
|
stack_instance = {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -603,26 +603,13 @@ 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_json(os.path.join(repo_root, "schemas", "stack.schema.json"))
|
stack_schema = _load_schema(os.path.join(repo_root, "schemas", "stack.schema.json"))
|
||||||
jsonschema.validate(stack_instance, stack_schema)
|
jsonschema.validate(stack_instance, stack_schema)
|
||||||
|
|
||||||
return stack_instance
|
return stack_instance
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) < 3:
|
# P12 (REQ-176): CLI extracted to core/contract_resolver_cli.py.
|
||||||
print("usage: contract_resolver.py <contract.yml> <out.json> [--environment <name>]", file=sys.stderr)
|
from core.contract_resolver_cli import main
|
||||||
sys.exit(2)
|
sys.exit(main())
|
||||||
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)
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""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())
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""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
@@ -0,0 +1,31 @@
|
|||||||
|
"""Environment helper (D-108, REQ-159, REQ-164).
|
||||||
|
|
||||||
|
During the Nova rebrand transition window (P2–P4), `get_env` read
|
||||||
|
`NOVA_*` preferred with the legacy `ACDL_*` name as the fallback. **P5
|
||||||
|
(REQ-164) removed the fallback** — `get_env` now reads `NOVA_*` only.
|
||||||
|
|
||||||
|
`get_env(name, default=None)` resolves `NOVA_<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
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""Nova Environment Transition — detect prior env + record applied env.
|
||||||
|
|
||||||
|
When a consumer edits the `environment:` field on a stable contract `id`
|
||||||
|
(Shape A promotion), the platform must destroy the prior environment's
|
||||||
|
resources before building the new environment. This module provides the
|
||||||
|
DynamoDB query logic to detect the prior environment and record the
|
||||||
|
applied environment after a successful apply.
|
||||||
|
|
||||||
|
Source of truth: the `nova-contracts` DynamoDB table (PK `consumerRepo`,
|
||||||
|
SK `contractId#submittedAt`), written by `core/lambda/contract_ingestor.py`.
|
||||||
|
|
||||||
|
detect_prior_env() queries the table for the last-applied environment for
|
||||||
|
a given consumerRepo + contractId. If it differs from the new env, the
|
||||||
|
prior env name is returned (so the pipeline can destroy it). If no record
|
||||||
|
exists (first deploy or Shape B per-env caller), returns None.
|
||||||
|
|
||||||
|
record_applied_env() writes a `#LAST_APPLIED` record after a successful
|
||||||
|
apply, so the next run's detect step has a source of truth.
|
||||||
|
|
||||||
|
Failures to reach DynamoDB (local/CI mode without the table) log a warning
|
||||||
|
and return None (conservative — no false-positive destroys). This is the
|
||||||
|
no-orphan-path guarantee: if we can't confirm a prior env, we don't
|
||||||
|
destroy, but we also don't silently proceed in a way that orphans — the
|
||||||
|
record step ensures future runs have the data.
|
||||||
|
|
||||||
|
CLI:
|
||||||
|
python3 core/env_transition.py detect --contract-id <id> --consumer-repo <repo> --new-env <env>
|
||||||
|
python3 core/env_transition.py record --contract-id <id> --consumer-repo <repo> --env <env>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
import boto3
|
||||||
|
except ImportError:
|
||||||
|
boto3 = None
|
||||||
|
|
||||||
|
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "nova-contracts")
|
||||||
|
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||||
|
LAST_APPLIED_SUFFIX = "#LAST_APPLIED"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_table():
|
||||||
|
"""Return the DynamoDB table resource, or raise if boto3 unavailable."""
|
||||||
|
if boto3 is None:
|
||||||
|
raise RuntimeError("boto3 is required for env_transition")
|
||||||
|
session = boto3.Session(region_name=REGION)
|
||||||
|
dyn = session.resource("dynamodb")
|
||||||
|
return dyn.Table(TABLE_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_prior_env(contract_id: str, consumer_repo: str, new_env: str) -> Optional[str]:
|
||||||
|
"""Query the nova-contracts table for the last-applied env.
|
||||||
|
|
||||||
|
Returns the prior env name if it differs from new_env, else None.
|
||||||
|
Failures to reach DynamoDB log a warning and return None (conservative).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
table = _get_table()
|
||||||
|
sk_prefix = f"{contract_id}{LAST_APPLIED_SUFFIX}#"
|
||||||
|
resp = table.query(
|
||||||
|
KeyConditionExpression="consumerRepo = :repo AND begins_with(#sk, :prefix)",
|
||||||
|
FilterExpression="#status = :status",
|
||||||
|
ExpressionAttributeNames={
|
||||||
|
"#sk": "contractId#submittedAt",
|
||||||
|
"#status": "status",
|
||||||
|
},
|
||||||
|
ExpressionAttributeValues={
|
||||||
|
":repo": consumer_repo,
|
||||||
|
":prefix": sk_prefix,
|
||||||
|
":status": "applied",
|
||||||
|
},
|
||||||
|
ScanIndexForward=False,
|
||||||
|
Limit=1,
|
||||||
|
)
|
||||||
|
items = resp.get("Items", [])
|
||||||
|
if not items:
|
||||||
|
return None
|
||||||
|
prior_env = items[0].get("environment")
|
||||||
|
if prior_env and prior_env != new_env:
|
||||||
|
return prior_env
|
||||||
|
return None
|
||||||
|
except Exception as exc:
|
||||||
|
sys.stderr.write(
|
||||||
|
f"WARNING: env_transition.detect_prior_env: could not query "
|
||||||
|
f"DynamoDB table {TABLE_NAME} — {type(exc).__name__}: {exc}. "
|
||||||
|
f"Assuming no prior env (conservative). This is expected in "
|
||||||
|
f"local/CI mode without the nova-contracts table.\n"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def record_applied_env(contract_id: str, consumer_repo: str, env: str) -> bool:
|
||||||
|
"""Write a LAST_APPLIED record to the nova-contracts table.
|
||||||
|
|
||||||
|
Called after a successful apply. Idempotent (writes a new timestamped
|
||||||
|
record each time; the detect step reads the latest by ScanIndexForward).
|
||||||
|
Returns True on success, False on failure (non-fatal — the pipeline
|
||||||
|
should not halt if the record write fails).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
table = _get_table()
|
||||||
|
ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
sk = f"{contract_id}{LAST_APPLIED_SUFFIX}#{ts}"
|
||||||
|
table.put_item(
|
||||||
|
Item={
|
||||||
|
"consumerRepo": consumer_repo,
|
||||||
|
"contractId#submittedAt": sk,
|
||||||
|
"contractId": contract_id,
|
||||||
|
"environment": env,
|
||||||
|
"status": "applied",
|
||||||
|
"appliedAt": ts,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
sys.stderr.write(
|
||||||
|
f"WARNING: env_transition.record_applied_env: could not write to "
|
||||||
|
f"DynamoDB table {TABLE_NAME} — {type(exc).__name__}: {exc}. "
|
||||||
|
f"The apply succeeded but the last-applied env record was not "
|
||||||
|
f"persisted. Future env-transition detection may not work.\n"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Nova env-transition detect/record")
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
p_detect = sub.add_parser("detect", help="Detect prior env for a contract")
|
||||||
|
p_detect.add_argument("--contract-id", required=True)
|
||||||
|
p_detect.add_argument("--consumer-repo", required=True)
|
||||||
|
p_detect.add_argument("--new-env", required=True)
|
||||||
|
|
||||||
|
p_record = sub.add_parser("record", help="Record the applied env for a contract")
|
||||||
|
p_record.add_argument("--contract-id", required=True)
|
||||||
|
p_record.add_argument("--consumer-repo", required=True)
|
||||||
|
p_record.add_argument("--env", required=True)
|
||||||
|
|
||||||
|
args = parser.parse_args(argv[1:])
|
||||||
|
|
||||||
|
if args.command == "detect":
|
||||||
|
prior = detect_prior_env(args.contract_id, args.consumer_repo, args.new_env)
|
||||||
|
print(json.dumps({"prior_env": prior}))
|
||||||
|
return 0 if prior is None else 0
|
||||||
|
elif args.command == "record":
|
||||||
|
ok = record_applied_env(args.contract_id, args.consumer_repo, args.env)
|
||||||
|
print(json.dumps({"recorded": ok}))
|
||||||
|
return 0 if ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
@@ -55,10 +55,12 @@ 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 (
|
||||||
"=== ACDL Environment Onboarding ===\n"
|
"=== Nova 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"
|
||||||
"ACDL environments are platform-managed. The platform provisions on\n"
|
"Nova 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"
|
||||||
@@ -66,13 +68,15 @@ 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:\n"
|
"To request an environment (self-service):\n"
|
||||||
" 1. Contact the platform team with your repo name + the\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"
|
" environment name you need (e.g. 'dev').\n"
|
||||||
" 2. The platform team provisions the account/network/state/role\n"
|
" 2. The platform generates an environment binding + opens a PR.\n"
|
||||||
" and binds the environment to your repo.\n"
|
" 3. The platform provisions the account/network/state/role and\n"
|
||||||
" 3. Your next pipeline run will proceed normally.\n\n"
|
" grants the ABAC role. Your next pipeline run proceeds.\n\n"
|
||||||
"Expected turnaround: contact the platform team for current SLA.\n"
|
"Run: python3 core/onboarding.py --request '{...}' to generate a\n"
|
||||||
|
"binding file locally, or POST to the Lambda onboard_consumer action.\n"
|
||||||
"===================================\n"
|
"===================================\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -33,5 +33,13 @@ 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. Self-service environment provisioning is on the
|
it to the consumer repo.
|
||||||
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).
|
||||||
+26
-4
@@ -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 (`gitea.actor` / `github.actor`) to the
|
Records the approver identity (the CI actor (GITHUB_ACTOR or FORGE_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,6 +12,10 @@ 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, "")
|
||||||
@@ -25,7 +29,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 (`gitea.actor` / `github.actor`).
|
approver: the approver's username (the CI actor (GITHUB_ACTOR or FORGE_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.
|
||||||
@@ -37,7 +41,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/GITEA_ACTOR unset)")
|
return (False, f"no approver identity for {env} (GITHUB_ACTOR/FORGE_ACTOR unset)")
|
||||||
|
|
||||||
attr = _approver_attr(env)
|
attr = _approver_attr(env)
|
||||||
if not attr:
|
if not attr:
|
||||||
@@ -61,12 +65,30 @@ 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("GITEA_ACTOR")
|
return os.environ.get("GITHUB_ACTOR") or os.environ.get("FORGE_ACTOR")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+18
-18
@@ -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.
|
||||||
|
|
||||||
## Gitea-specific gate mechanics (D-042)
|
## Forge-specific gate mechanics (D-042)
|
||||||
|
|
||||||
Gitea has **no Environments API** and ignores `environment:` blocks
|
The dev forge 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 `gitea.actor` is the QA approver.
|
run's `CI actor` is the QA approver.
|
||||||
- **prod gate:** `workflow_dispatch` with `approve_prod: true`;
|
- **prod gate:** `workflow_dispatch` with `approve_prod: true`;
|
||||||
`gitea.actor` is the SRE approver.
|
`CI 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 = `gitea.actor` of the dispatch run
|
The approver identity of record = `CI actor` of the dispatch run
|
||||||
(D-042). There is no other approval-identity signal in Gitea. The real
|
(D-042). There is no other approval-identity signal in the dev forge. The real
|
||||||
OIDC path (blocked on go-gitea/gitea#36988) does not change this —
|
OIDC path (blocked on upstream forge OIDC support) 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 `github.actor` of the `workflow_dispatch`
|
On GitHub, the equivalent is `CI 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 Gitea + GitHub workflows.
|
byte-identical across forges.
|
||||||
|
|
||||||
## Reviewer routing (ARCHITECTURE.md §10.2)
|
## Reviewer routing (ARCHITECTURE.md §10.2)
|
||||||
|
|
||||||
Gitea CODEOWNERS routes the right reviewer to the right gate:
|
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
|
||||||
`ACDL_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged when
|
`NOVA_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:** a Gitea `on: schedule` workflow (runs hourly) that
|
**Implementation:** an `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 `gitea.actor` (or `github.actor`)
|
from the `workflow_dispatch` run's `CI 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's `gitea.actor` from the
|
from the outbox and the new SRE approver identity 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 `ACDL_SOD_HALT_TOPIC_ARN`) with an outbox-event
|
`acdl-sod-halt`, ARN from `NOVA_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 = `gitea.actor` of the `workflow_dispatch`
|
- **D-042** — approver identity = `CI actor` of the `workflow_dispatch`
|
||||||
run; no Environments API in Gitea. On GitHub, `github.actor`.
|
run; no Environments API in the dev forge.
|
||||||
- **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 `ACDL_ATTESTATION_SIGNING_KEY_ID` is unset (dev/CI);
|
skipped when `NOVA_ATTESTATION_SIGNING_KEY_ID` is unset (dev/CI);
|
||||||
required for prod/dr.
|
required for prod/dr.
|
||||||
@@ -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 acdl-contracts
|
environment, action } and writes contracts to DynamoDB table nova-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,18 +22,61 @@ import urllib.parse
|
|||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "acdl-contracts")
|
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "nova-contracts")
|
||||||
CHANGE_REQUESTS_TABLE = os.environ.get("CHANGE_REQUESTS_TABLE", "acdl-change-requests")
|
CHANGE_REQUESTS_TABLE = os.environ.get("CHANGE_REQUESTS_TABLE", "nova-change-requests")
|
||||||
GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "acdl/github-token")
|
GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "nova/github-token")
|
||||||
PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "acdl/acdl")
|
PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "nova/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 Gitea API root (e.g. https://git.cloudinit.dev/api/v1) for Gitea.
|
# to a compatible forge API root (e.g. https://forge.example.com/api/v1).
|
||||||
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:
|
||||||
@@ -53,22 +96,22 @@ def _iso8601_now():
|
|||||||
|
|
||||||
|
|
||||||
def _forge_type():
|
def _forge_type():
|
||||||
"""P1-9: Detect whether the API base is GitHub or Gitea.
|
"""Detect whether the API base is GitHub or a compatible forge.
|
||||||
|
|
||||||
Gitea API roots contain '/api/v1'; GitHub's is 'api.github.com'.
|
Compatible forge 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 "gitea"
|
return "generic_forge"
|
||||||
return "github"
|
return "github"
|
||||||
|
|
||||||
|
|
||||||
def _issues_search_url(owner, repo, encoded_query):
|
def _issues_search_url(owner, repo, encoded_query):
|
||||||
"""P1-9: Build the issue search URL based on forge type.
|
"""Build the issue search URL based on forge type.
|
||||||
|
|
||||||
GitHub uses /search/issues?q=...; Gitea uses /repos/{owner}/{repo}/issues?...
|
GitHub uses /search/issues?q=...; compatible forges use /repos/{owner}/{repo}/issues?...
|
||||||
with query params (no /search/issues endpoint).
|
with query params (no /search/issues endpoint).
|
||||||
"""
|
"""
|
||||||
if _forge_type() == "gitea":
|
if _forge_type() == "generic_forge":
|
||||||
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}"
|
||||||
@@ -80,7 +123,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 for both GitHub + Gitea)."""
|
"""URL for creating an issue (same pattern across forges)."""
|
||||||
return f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
|
return f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
|
||||||
|
|
||||||
|
|
||||||
@@ -94,6 +137,25 @@ 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 = {
|
||||||
@@ -131,7 +193,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", "")[:2000] # truncate
|
stack_trace = payload.get("stackTrace", "")[:MAX_ERROR_FIELD_CHARS] # P11: aligned cap
|
||||||
|
|
||||||
# Get the GitHub token from Secrets Manager
|
# Get the GitHub token from Secrets Manager
|
||||||
secrets = _get_secrets_client()
|
secrets = _get_secrets_client()
|
||||||
@@ -142,7 +204,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"[ACDL-ALERT] Deploy failure: {consumer_repo} / {contract_id}"
|
title = f"[NOVA-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).
|
||||||
@@ -188,7 +250,7 @@ def _report_error(payload):
|
|||||||
{stack_trace}
|
{stack_trace}
|
||||||
```
|
```
|
||||||
|
|
||||||
_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._
|
_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._
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
@@ -236,29 +298,64 @@ 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.
|
||||||
|
|
||||||
If the identity is not available (e.g. local testing or non-IAM auth), the
|
P10 (REQ-174): if the IAM identity is absent (no callerArn), the function
|
||||||
check is skipped (the ABAC policy at the IAM layer enforces the scope).
|
FAILS CLOSED (raises ValueError) rather than silently passing. The ABAC
|
||||||
|
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
|
||||||
|
error length. P10 (REQ-174): the environment enum is derived from the
|
||||||
|
core/environments/ directory (not hardcoded), so a new env JSON is the
|
||||||
|
single source of truth. The ABAC reliance is documented here: the
|
||||||
|
Function URL IAM identity does not expose principal tags in the event,
|
||||||
|
so full enforcement of consumerRepo ownership is at the IAM layer (ABAC
|
||||||
|
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:
|
||||||
return # no identity available — rely on IAM ABAC enforcement
|
# P10 (REQ-174): fail closed. A local-test bypass is allowed via
|
||||||
|
# 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 not payload_repo:
|
if payload_repo:
|
||||||
return
|
# consumerRepo must be org/repo format, <=128 chars
|
||||||
# Extract the session name or principal tag from the ARN. The ABAC policy
|
if "/" not in payload_repo or len(payload_repo) > 128:
|
||||||
# scopes via aws:PrincipalTag/acdl:owner = <consumerRepo>. The Function URL
|
raise ValueError(f"invalid consumerRepo format: {payload_repo!r}")
|
||||||
# IAM identity does not expose principal tags in the event, so we do a
|
|
||||||
# best-effort check: the consumerRepo must not be empty and must be a valid
|
# v1.14 (REQ-144): contractId format validation
|
||||||
# repo identifier (org/repo format). Full enforcement is at the IAM layer.
|
contract_id = payload.get("contractId", "")
|
||||||
if "/" not in payload_repo or len(payload_repo) > 128:
|
if contract_id:
|
||||||
raise ValueError(f"invalid consumerRepo format: {payload_repo!r}")
|
import re
|
||||||
|
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)")
|
||||||
|
|
||||||
|
# P10 (REQ-174): environment enum derived from core/environments/ (not
|
||||||
|
# hardcoded) — the directory is the single source of truth.
|
||||||
|
environment = payload.get("environment", "")
|
||||||
|
if environment:
|
||||||
|
valid_envs = _discover_environments()
|
||||||
|
if environment not in valid_envs:
|
||||||
|
raise ValueError(f"invalid environment: {environment!r} (must be one of {sorted(valid_envs)})")
|
||||||
|
|
||||||
|
# v1.14 (REQ-144): error length cap (for report_error action)
|
||||||
|
error_msg = payload.get("error", "")
|
||||||
|
if error_msg and len(str(error_msg)) > MAX_ERROR_FIELD_CHARS:
|
||||||
|
payload["error"] = str(error_msg)[:MAX_ERROR_FIELD_CHARS]
|
||||||
|
|
||||||
|
|
||||||
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 acdl-change-requests table for the given changeRequestId.
|
Queries the nova-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.
|
||||||
"""
|
"""
|
||||||
@@ -301,6 +398,65 @@ 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.
|
||||||
|
|
||||||
@@ -329,6 +485,8 @@ 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,
|
||||||
@@ -336,6 +494,28 @@ 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>")
|
||||||
+32
-8
@@ -12,7 +12,9 @@ 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 ACDL_LOCAL_TIER env var (set by run_platform.sh --local).
|
gated on the NOVA_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
|
||||||
@@ -32,12 +34,20 @@ 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 os.environ.get("ACDL_LOCAL_TIER", "") == "1"
|
return env.get_env("LOCAL_TIER", "") == "1"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -59,7 +69,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="acdl_outbox_"))
|
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="nova_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.
|
||||||
@@ -78,7 +88,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 = "acdl-outbox-local",
|
outbox_table: str = "nova-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.
|
||||||
|
|
||||||
@@ -240,7 +250,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="acdl_tfstate_"))
|
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="nova_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)
|
||||||
|
|
||||||
@@ -286,7 +296,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 ACDL_LOCAL_TIER env var (the handler checks
|
intercepted via the NOVA_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
|
||||||
@@ -382,12 +392,24 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -419,7 +441,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="acdl_local_e2e_"))
|
work = Path(tempfile.mkdtemp(prefix="nova_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))
|
||||||
@@ -490,6 +512,8 @@ 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"
|
||||||
os.environ["ACDL_LOCAL_TIER"] = "1"
|
# Set so is_local_tier() finds NOVA_LOCAL_TIER (NOVA_* only; the
|
||||||
|
# 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))
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
"""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))
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
"""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)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""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()
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""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))
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"""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))
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
"""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))
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
"""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}))
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
"""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))
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#!/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())
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
"""ACDL Outbox Writer — write an evidence event to the DynamoDB outbox.
|
"""Nova 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): acdl-outbox, PAY_PER_REQUEST, PK contractId,
|
The outbox table (Phase 08): nova-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 = "acdl-outbox"
|
OUTBOX_TABLE = "nova-outbox"
|
||||||
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+73
-20
@@ -8,24 +8,40 @@ Two canonical mechanisms:
|
|||||||
strings, ALB DNS, S3 bucket URL, CloudFront domain). No raw secrets in
|
strings, ALB DNS, S3 bucket URL, CloudFront domain). No raw secrets in
|
||||||
the comment — only non-sensitive outputs (DNS names, ARNs, bucket names).
|
the comment — only non-sensitive outputs (DNS names, ARNs, bucket names).
|
||||||
|
|
||||||
The namespace is /acdl/{environment}/{contractId}/{output_name} so consumers
|
The namespace is /nova/{environment}/{contractId}/{output_name} so consumers
|
||||||
can query their own outputs via aws ssm get-parameter --name /acdl/dev/<id>/...
|
can query their own outputs via aws ssm get-parameter --name /nova/dev/<id>/...
|
||||||
|
(REQ-161, P3: migrated from /acdl/... ; scripts/migrate_ssm_paths.py copies
|
||||||
|
existing /acdl/... parameters to /nova/... and deletes the old ones.)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import boto3
|
import boto3
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
except ImportError:
|
except ImportError:
|
||||||
boto3 = None
|
boto3 = None
|
||||||
|
ClientError = Exception # type: ignore[assignment,misc]
|
||||||
|
|
||||||
SSM_PREFIX = "/acdl"
|
# Repo root on sys.path so `from core import env` resolves to THIS package
|
||||||
KMS_KEY_ID_ENV = "ACDL_KMS_KEY_ID"
|
# 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)
|
||||||
|
|
||||||
# Outputs that are safe to display in a PR comment (no secrets).
|
from core import env as _envhelper
|
||||||
SAFE_OUTPUT_NAMES = {
|
|
||||||
|
SSM_PREFIX = "/nova"
|
||||||
|
KMS_KEY_ID_ENV = "NOVA_KMS_KEY_ID"
|
||||||
|
|
||||||
|
# P14 (REQ-178): SAFE_OUTPUT_NAMES is schema-driven (derived from
|
||||||
|
# modules/l1/*/interface.json outputs that don't have sensitive:true).
|
||||||
|
# Falls back to the hardcoded set if the interfaces can't be read.
|
||||||
|
_HARDCODED_SAFE_OUTPUTS = {
|
||||||
"distribution_domain_name",
|
"distribution_domain_name",
|
||||||
"bucket_arn",
|
"bucket_arn",
|
||||||
"bucket_name",
|
"bucket_name",
|
||||||
@@ -45,6 +61,37 @@ SAFE_OUTPUT_NAMES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_safe_output_names():
|
||||||
|
"""Derive the safe-output allowlist from interface.json outputs.
|
||||||
|
|
||||||
|
P14 (REQ-178): scan modules/l1/*/interface.json; an output is safe if
|
||||||
|
its spec does not set sensitive:true. Falls back to the hardcoded set
|
||||||
|
if no interfaces are readable.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
root = Path(__file__).resolve().parent.parent
|
||||||
|
safe = set()
|
||||||
|
try:
|
||||||
|
for iface in (root / "modules" / "l1").glob("*/interface.json"):
|
||||||
|
d = json.loads(iface.read_text())
|
||||||
|
outs = d.get("outputs", {})
|
||||||
|
if isinstance(outs, dict):
|
||||||
|
for name, spec in outs.items():
|
||||||
|
if not (isinstance(spec, dict) and spec.get("sensitive")):
|
||||||
|
safe.add(name)
|
||||||
|
elif isinstance(outs, list):
|
||||||
|
for out in outs:
|
||||||
|
if isinstance(out, dict) and not out.get("sensitive"):
|
||||||
|
safe.add(out.get("name", ""))
|
||||||
|
except (OSError, ValueError):
|
||||||
|
pass
|
||||||
|
return safe or _HARDCODED_SAFE_OUTPUTS
|
||||||
|
|
||||||
|
|
||||||
|
SAFE_OUTPUT_NAMES = _load_safe_output_names()
|
||||||
|
|
||||||
|
|
||||||
def _ssm_client():
|
def _ssm_client():
|
||||||
if boto3 is None:
|
if boto3 is None:
|
||||||
raise RuntimeError("boto3 is required for SSM publishing")
|
raise RuntimeError("boto3 is required for SSM publishing")
|
||||||
@@ -54,20 +101,22 @@ def _ssm_client():
|
|||||||
def _kms_key_id():
|
def _kms_key_id():
|
||||||
"""Return the KMS key ID for SSM SecureString encryption.
|
"""Return the KMS key ID for SSM SecureString encryption.
|
||||||
|
|
||||||
P1-3: Fail loud when ACDL_KMS_KEY_ID is not set — silently falling back
|
P1-3: Fail loud when NOVA_KMS_KEY_ID is not set — silently falling back
|
||||||
to the AWS-managed key (`alias/aws/ssm`) was a security gap. The platform
|
to the AWS-managed key (`alias/aws/ssm`) was a security gap. The platform
|
||||||
CMK must be explicitly configured. Set ACDL_ALLOW_DEFAULT_KMS=1 to use
|
CMK must be explicitly configured. Set NOVA_ALLOW_DEFAULT_KMS=1 to use
|
||||||
the AWS-managed key as an escape hatch for local testing.
|
the AWS-managed key as an escape hatch for local testing. (Dual-read
|
||||||
|
via core/env.py: NOVA_* preferred, ACDL_* fallback until P5.)
|
||||||
"""
|
"""
|
||||||
key_id = os.environ.get(KMS_KEY_ID_ENV)
|
key_id = _envhelper.get_env("KMS_KEY_ID")
|
||||||
if key_id:
|
if key_id:
|
||||||
return key_id
|
return key_id
|
||||||
if os.environ.get("ACDL_ALLOW_DEFAULT_KMS") == "1":
|
if _envhelper.get_env("ALLOW_DEFAULT_KMS") == "1":
|
||||||
return "alias/aws/ssm"
|
return "alias/aws/ssm"
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"{KMS_KEY_ID_ENV} is not set — refusing to use the AWS-managed SSM key "
|
f"{KMS_KEY_ID_ENV} is not set — refusing to use the AWS-managed SSM key "
|
||||||
f"silently. Set {KMS_KEY_ID_ENV} to your platform CMK ARN, or set "
|
f"silently. Set {KMS_KEY_ID_ENV} to your platform CMK ARN, or set "
|
||||||
f"ACDL_ALLOW_DEFAULT_KMS=1 to use alias/aws/ssm (escape hatch for local testing)."
|
f"NOVA_ALLOW_DEFAULT_KMS=1 (ACDL_ALLOW_DEFAULT_KMS=1 fallback) to use "
|
||||||
|
f"alias/aws/ssm (escape hatch for local testing)."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -97,10 +146,12 @@ def publish_to_ssm(outputs, environment, contract_id):
|
|||||||
Overwrite=True,
|
Overwrite=True,
|
||||||
)
|
)
|
||||||
results[name] = param_name
|
results[name] = param_name
|
||||||
except Exception as e:
|
except (ClientError, OSError) as e:
|
||||||
# Don't fail the pipeline if one output fails to publish, but log it
|
# P4 (REQ-168): narrow from bare `except Exception` to AWS +
|
||||||
|
# OS errors. Don't fail the pipeline if one output fails to
|
||||||
|
# publish, but log it with context.
|
||||||
import sys
|
import sys
|
||||||
print(f"WARNING: SSM put_parameter failed for {name}: {e}", file=sys.stderr)
|
print(f"WARNING: SSM put_parameter failed for {name}: {type(e).__name__}: {e}", file=sys.stderr)
|
||||||
results[name] = None
|
results[name] = None
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -112,7 +163,7 @@ def format_comment(outputs, environment, contract_id, ssm_results=None):
|
|||||||
outputs are noted as 'published to SSM' without their values.
|
outputs are noted as 'published to SSM' without their values.
|
||||||
"""
|
"""
|
||||||
lines = [
|
lines = [
|
||||||
f"### ACDL Deploy Outputs ({environment})",
|
f"### Nova Deploy Outputs ({environment})",
|
||||||
"",
|
"",
|
||||||
f"**Contract:** `{contract_id}`",
|
f"**Contract:** `{contract_id}`",
|
||||||
f"**Environment:** `{environment}`",
|
f"**Environment:** `{environment}`",
|
||||||
@@ -134,7 +185,7 @@ def format_comment(outputs, environment, contract_id, ssm_results=None):
|
|||||||
ssm_path = "—"
|
ssm_path = "—"
|
||||||
lines.append(f"| `{name}` | {display} | {ssm_path} |")
|
lines.append(f"| `{name}` | {display} | {ssm_path} |")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("> Sensitive outputs are available via `aws ssm get-parameter --name /acdl/" + environment + "/" + contract_id + "/<output_name>` (KMS-encrypted SecureString).")
|
lines.append("> Sensitive outputs are available via `aws ssm get-parameter --name /nova/" + environment + "/" + contract_id + "/<output_name>` (KMS-encrypted SecureString).")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -159,7 +210,6 @@ def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
|
|||||||
if not token or not repo or not pr_number:
|
if not token or not repo or not pr_number:
|
||||||
return False # not in a PR context or no token
|
return False # not in a PR context or no token
|
||||||
try:
|
try:
|
||||||
import urllib.request
|
|
||||||
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
|
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
|
||||||
data = json.dumps({"body": comment_text}).encode()
|
data = json.dumps({"body": comment_text}).encode()
|
||||||
req = urllib.request.Request(url, data=data, method="POST")
|
req = urllib.request.Request(url, data=data, method="POST")
|
||||||
@@ -167,9 +217,12 @@ def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
|
|||||||
req.add_header("Accept", "application/vnd.github+json")
|
req.add_header("Accept", "application/vnd.github+json")
|
||||||
urllib.request.urlopen(req, timeout=10)
|
urllib.request.urlopen(req, timeout=10)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except (OSError, urllib.error.URLError, urllib.error.HTTPError) as e:
|
||||||
|
# P4 (REQ-168): narrow from bare `except Exception` to network +
|
||||||
|
# HTTP errors. Don't fail the pipeline if the PR comment can't be
|
||||||
|
# posted, but log it with context.
|
||||||
import sys
|
import sys
|
||||||
print(f"WARNING: GitHub PR comment failed: {e}", file=sys.stderr)
|
print(f"WARNING: GitHub PR comment failed: {type(e).__name__}: {e}", file=sys.stderr)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+173
-97
@@ -32,6 +32,15 @@ from dataclasses import dataclass, field, asdict
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Dict, List, Optional, Tuple
|
from typing import Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
# Repo root on sys.path so `from core import env` resolves to THIS package
|
||||||
|
# when regression_verify.py is 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 as _envhelper
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
CIAgent = ROOT / ".ciagent"
|
CIAgent = ROOT / ".ciagent"
|
||||||
|
|
||||||
@@ -65,7 +74,10 @@ class RegressionReport:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def passed(self) -> bool:
|
def passed(self) -> bool:
|
||||||
return all(r.status == "Verified" for r in self.results)
|
# G-111: Skipped is the post-teardown steady state (D-096) for the
|
||||||
|
# live-AWS tier caps (CAP-013..016). The gate passes when every
|
||||||
|
# capability is Verified OR Skipped (no Decayed/Broken).
|
||||||
|
return all(r.status in ("Verified", "Skipped") for r in self.results)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
@@ -137,14 +149,18 @@ def _check_environment_schema_validation() -> Tuple[Status, str]:
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def _check_resolver_static_assets() -> Tuple[Status, str]:
|
def _check_resolver(contract_path: str) -> Tuple[Status, str]:
|
||||||
"""CAP-003: contract_resolver resolves static-assets to a Target Stack."""
|
"""Shared helper: contract_resolver resolves a contract to a Target Stack.
|
||||||
|
|
||||||
|
Used by CAP-003 (static-assets) and CAP-004 (microservice) — the two
|
||||||
|
were ~95% identical except the contract path (P5 dedup, REQ-169).
|
||||||
|
"""
|
||||||
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as t:
|
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as t:
|
||||||
out = t.name
|
out = t.name
|
||||||
try:
|
try:
|
||||||
return _check_subprocess([
|
return _check_subprocess([
|
||||||
"python3", "core/contract_resolver.py",
|
"python3", "core/contract_resolver.py",
|
||||||
"contracts/static-assets.yml", out,
|
contract_path, out,
|
||||||
])
|
])
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
@@ -153,25 +169,19 @@ def _check_resolver_static_assets() -> Tuple[Status, str]:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _check_resolver_static_assets() -> Tuple[Status, str]:
|
||||||
|
"""CAP-003: contract_resolver resolves static-assets to a Target Stack."""
|
||||||
|
return _check_resolver("contracts/static-assets.yml")
|
||||||
|
|
||||||
|
|
||||||
def _check_resolver_microservice() -> Tuple[Status, str]:
|
def _check_resolver_microservice() -> Tuple[Status, str]:
|
||||||
"""CAP-004: contract_resolver resolves the microservice contract."""
|
"""CAP-004: contract_resolver resolves the microservice contract."""
|
||||||
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as t:
|
return _check_resolver("contracts/microservice.yml")
|
||||||
out = t.name
|
|
||||||
try:
|
|
||||||
return _check_subprocess([
|
|
||||||
"python3", "core/contract_resolver.py",
|
|
||||||
"contracts/microservice.yml", out,
|
|
||||||
])
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
os.unlink(out)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _check_adapter_emits_terraform() -> Tuple[Status, str]:
|
def _check_adapter_emits_terraform() -> Tuple[Status, str]:
|
||||||
"""CAP-005: terraform adapter compiles a resolved stack to .tf files."""
|
"""CAP-005: terraform adapter compiles a resolved stack to .tf files."""
|
||||||
work = tempfile.mkdtemp(prefix="acdl_regr_")
|
work = tempfile.mkdtemp(prefix="nova_regr_")
|
||||||
stack_path = os.path.join(work, "stack.json")
|
stack_path = os.path.join(work, "stack.json")
|
||||||
tf_dir = os.path.join(work, "tf")
|
tf_dir = os.path.join(work, "tf")
|
||||||
os.makedirs(tf_dir, exist_ok=True)
|
os.makedirs(tf_dir, exist_ok=True)
|
||||||
@@ -202,7 +212,7 @@ def _check_interpolation() -> Tuple[Status, str]:
|
|||||||
"import sys; sys.path.insert(0,'.'); "
|
"import sys; sys.path.insert(0,'.'); "
|
||||||
"from core.contract_resolver import _expand_vars; "
|
"from core.contract_resolver import _expand_vars; "
|
||||||
"ctx={'env':{'environment':'qa','account_id':'123'},'contract':{'id':'assets'}}; "
|
"ctx={'env':{'environment':'qa','account_id':'123'},'contract':{'id':'assets'}}; "
|
||||||
"assert _expand_vars('acdl-${env.environment}-${contract.id}', ctx)=='acdl-qa-assets'; "
|
"assert _expand_vars('nova-${env.environment}-${contract.id}', ctx)=='nova-qa-assets'; "
|
||||||
"print('interpolation ok')",
|
"print('interpolation ok')",
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -222,7 +232,7 @@ def _check_confidence_signal() -> Tuple[Status, str]:
|
|||||||
|
|
||||||
def _check_outbox_writer() -> Tuple[Status, str]:
|
def _check_outbox_writer() -> Tuple[Status, str]:
|
||||||
"""CAP-008: outbox_writer writes a hash-chained event to a temp file."""
|
"""CAP-008: outbox_writer writes a hash-chained event to a temp file."""
|
||||||
work = tempfile.mkdtemp(prefix="acdl_outbox_")
|
work = tempfile.mkdtemp(prefix="nova_outbox_")
|
||||||
event_path = os.path.join(work, "event.json")
|
event_path = os.path.join(work, "event.json")
|
||||||
event = {
|
event = {
|
||||||
"contractId": "regression-test", "eventType": "CONFIDENCE_COMPUTED",
|
"contractId": "regression-test", "eventType": "CONFIDENCE_COMPUTED",
|
||||||
@@ -306,29 +316,34 @@ def _load_aws_env() -> Dict[str, str]:
|
|||||||
continue
|
continue
|
||||||
if "=" in line:
|
if "=" in line:
|
||||||
k, v = line.split("=", 1)
|
k, v = line.split("=", 1)
|
||||||
if k == "ACDL_AWS_ACCESS_KEY_ID":
|
# NOVA_* only (ACDL_* fallback removed in v1.15 P5, REQ-164).
|
||||||
|
if k == "NOVA_AWS_ACCESS_KEY_ID":
|
||||||
env["AWS_ACCESS_KEY_ID"] = v
|
env["AWS_ACCESS_KEY_ID"] = v
|
||||||
elif k == "ACDL_AWS_SECRET_ACCESS_KEY":
|
elif k == "NOVA_AWS_SECRET_ACCESS_KEY":
|
||||||
env["AWS_SECRET_ACCESS_KEY"] = v
|
env["AWS_SECRET_ACCESS_KEY"] = v
|
||||||
elif k == "AWS_DEFAULT_REGION":
|
elif k == "AWS_DEFAULT_REGION":
|
||||||
env["AWS_DEFAULT_REGION"] = v
|
env["AWS_DEFAULT_REGION"] = v
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
def _check_live_terraform_plan_microservice() -> Tuple[Status, str]:
|
def _check_live_terraform_plan(contract_path: str, label: str) -> Tuple[Status, str]:
|
||||||
"""CAP-013: terraform init+validate+plan against live AWS for the
|
"""Shared helper: terraform init+validate+plan against live AWS for a
|
||||||
microservice stack (D-093 live-AWS tier of the headline E2E).
|
contract (D-093 live-AWS tier of the headline E2E).
|
||||||
|
|
||||||
Requires AWS credentials (ACDL_AWS_ACCESS_KEY_ID etc. in .env.secrets).
|
Used by CAP-013 (microservice) and CAP-014 (static-assets) — the two
|
||||||
Runs in a temp dir; does NOT apply (plan only)."""
|
were ~95% identical except the contract path + label (P5 dedup,
|
||||||
|
REQ-169). Requires AWS credentials (NOVA_AWS_ACCESS_KEY_ID etc. in
|
||||||
|
.env.secrets; NOVA_* only — the ACDL_* fallback was removed in v1.15
|
||||||
|
P5, REQ-164). Runs in a temp dir; does NOT apply (plan only).
|
||||||
|
"""
|
||||||
import tempfile, os
|
import tempfile, os
|
||||||
work = tempfile.mkdtemp(prefix="acdl_regr_live_")
|
work = tempfile.mkdtemp(prefix=f"nova_regr_live_{label}_")
|
||||||
stack_path = os.path.join(work, "stack.json")
|
stack_path = os.path.join(work, "stack.json")
|
||||||
tf_dir = os.path.join(work, "tf")
|
tf_dir = os.path.join(work, "tf")
|
||||||
os.makedirs(tf_dir, exist_ok=True)
|
os.makedirs(tf_dir, exist_ok=True)
|
||||||
rc, out, err = _run_subprocess([
|
rc, out, err = _run_subprocess([
|
||||||
"python3", "core/contract_resolver.py",
|
"python3", "core/contract_resolver.py",
|
||||||
"contracts/microservice.yml", stack_path,
|
contract_path, stack_path,
|
||||||
])
|
])
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
return "Broken", f"resolver failed: {err.strip()[-200:]}"
|
return "Broken", f"resolver failed: {err.strip()[-200:]}"
|
||||||
@@ -343,6 +358,11 @@ def _check_live_terraform_plan_microservice() -> Tuple[Status, str]:
|
|||||||
cwd=tf_dir, timeout=120, env=env,
|
cwd=tf_dir, timeout=120, env=env,
|
||||||
)
|
)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
|
# G-111: the state bucket was torn down in v1.11 (D-096) and not
|
||||||
|
# re-provisioned. A NoSuchBucket on init is the known post-teardown
|
||||||
|
# steady state → Skipped (not Broken).
|
||||||
|
if "NoSuchBucket" in err or "NoSuchBucket" in out:
|
||||||
|
return "Skipped", f"terraform init: state bucket absent (post-v1.11-teardown, D-096) [{label}]"
|
||||||
return "Broken", f"terraform init failed: {err.strip()[-200:]}"
|
return "Broken", f"terraform init failed: {err.strip()[-200:]}"
|
||||||
rc, out, err = _run_subprocess(
|
rc, out, err = _run_subprocess(
|
||||||
["terraform", "validate"], cwd=tf_dir, timeout=60, env=env,
|
["terraform", "validate"], cwd=tf_dir, timeout=60, env=env,
|
||||||
@@ -355,76 +375,74 @@ def _check_live_terraform_plan_microservice() -> Tuple[Status, str]:
|
|||||||
)
|
)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
return "Decayed", f"terraform plan failed: {err.strip()[-200:]}"
|
return "Decayed", f"terraform plan failed: {err.strip()[-200:]}"
|
||||||
return "Verified", "terraform init+validate+plan OK (live AWS, microservice)"
|
return "Verified", f"terraform init+validate+plan OK (live AWS, {label})"
|
||||||
|
|
||||||
|
|
||||||
|
def _check_live_terraform_plan_microservice() -> Tuple[Status, str]:
|
||||||
|
"""CAP-013: terraform init+validate+plan against live AWS for the
|
||||||
|
microservice stack (D-093 live-AWS tier of the headline E2E)."""
|
||||||
|
return _check_live_terraform_plan("contracts/microservice.yml", "microservice")
|
||||||
|
|
||||||
|
|
||||||
def _check_live_terraform_plan_static_assets() -> Tuple[Status, str]:
|
def _check_live_terraform_plan_static_assets() -> Tuple[Status, str]:
|
||||||
"""CAP-014: terraform init+validate+plan against live AWS for the
|
"""CAP-014: terraform init+validate+plan against live AWS for the
|
||||||
static-assets stack (CloudFront + WAF + S3)."""
|
static-assets stack (CloudFront + WAF + S3)."""
|
||||||
import tempfile, os
|
return _check_live_terraform_plan("contracts/static-assets.yml", "static-assets")
|
||||||
work = tempfile.mkdtemp(prefix="acdl_regr_live_sa_")
|
|
||||||
stack_path = os.path.join(work, "stack.json")
|
|
||||||
tf_dir = os.path.join(work, "tf")
|
|
||||||
os.makedirs(tf_dir, exist_ok=True)
|
|
||||||
rc, out, err = _run_subprocess([
|
|
||||||
"python3", "core/contract_resolver.py",
|
|
||||||
"contracts/static-assets.yml", stack_path,
|
|
||||||
])
|
|
||||||
if rc != 0:
|
|
||||||
return "Broken", f"resolver failed: {err.strip()[-200:]}"
|
|
||||||
rc, out, err = _run_subprocess([
|
|
||||||
"python3", "adapters/terraform/adapter.py", stack_path, tf_dir,
|
|
||||||
])
|
|
||||||
if rc != 0:
|
|
||||||
return "Broken", f"adapter failed: {err.strip()[-200:]}"
|
|
||||||
env = _load_aws_env()
|
|
||||||
rc, out, err = _run_subprocess(
|
|
||||||
["terraform", "init", "-reconfigure", "-lock=false", "-input=false"],
|
|
||||||
cwd=tf_dir, timeout=120, env=env,
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
return "Broken", f"terraform init failed: {err.strip()[-200:]}"
|
|
||||||
rc, out, err = _run_subprocess(
|
|
||||||
["terraform", "validate"], cwd=tf_dir, timeout=60, env=env,
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
return "Broken", f"terraform validate failed: {err.strip()[-200:]}"
|
|
||||||
rc, out, err = _run_subprocess(
|
|
||||||
["terraform", "plan", "-lock=false", "-input=false", "-out=tfplan"],
|
|
||||||
cwd=tf_dir, timeout=180, env=env,
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
return "Decayed", f"terraform plan failed: {err.strip()[-200:]}"
|
|
||||||
return "Verified", "terraform init+validate+plan OK (live AWS, static-assets)"
|
|
||||||
|
|
||||||
|
|
||||||
def _check_dynamodb_outbox_table() -> Tuple[Status, str]:
|
def _check_dynamodb_outbox_table() -> Tuple[Status, str]:
|
||||||
"""CAP-015: DynamoDB outbox table exists + is describable (live AWS)."""
|
"""CAP-015: DynamoDB outbox table exists + is describable (live AWS).
|
||||||
|
|
||||||
|
G-111: the live AWS resources were torn down in v1.11 (D-096) and not
|
||||||
|
re-provisioned (v1.15 P4 was plan-only). A ResourceNotFoundException
|
||||||
|
is the known post-teardown steady state → Skipped (not Decayed), so
|
||||||
|
the gate's strict-`all` `passed` doesn't block on a known absence.
|
||||||
|
Re-provisioning is a future feature milestone, not an NFR regression.
|
||||||
|
"""
|
||||||
import boto3
|
import boto3
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
env = _load_aws_env()
|
env = _load_aws_env()
|
||||||
try:
|
try:
|
||||||
dyn = boto3.client("dynamodb", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"),
|
dyn = boto3.client("dynamodb", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"),
|
||||||
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
|
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
|
||||||
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
||||||
r = dyn.describe_table(TableName="acdl-outbox")
|
r = dyn.describe_table(TableName="nova-outbox")
|
||||||
count = r["Table"].get("ItemCount", "unknown")
|
count = r["Table"].get("ItemCount", "unknown")
|
||||||
return "Verified", f"acdl-outbox exists, item_count={count}"
|
return "Verified", f"nova-outbox exists, item_count={count}"
|
||||||
|
except ClientError as e:
|
||||||
|
code = e.response.get("Error", {}).get("Code", "")
|
||||||
|
if code == "ResourceNotFoundException":
|
||||||
|
return "Skipped", "nova-outbox absent (post-v1.11-teardown steady state, D-096)"
|
||||||
|
return "Decayed", f"describe_table failed: {type(e).__name__}: {str(e)[:150]}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "Decayed", f"describe_table failed: {type(e).__name__}: {str(e)[:150]}"
|
return "Decayed", f"describe_table failed: {type(e).__name__}: {str(e)[:150]}"
|
||||||
|
|
||||||
|
|
||||||
def _check_s3_state_bucket() -> Tuple[Status, str]:
|
def _check_s3_state_bucket() -> Tuple[Status, str]:
|
||||||
"""CAP-016: S3 state bucket exists + readable (live AWS)."""
|
"""CAP-016: S3 state bucket exists + readable (live AWS).
|
||||||
|
|
||||||
|
G-111: the live state bucket was torn down in v1.11 (D-096) and not
|
||||||
|
re-provisioned. A 404 on head_bucket is the known post-teardown steady
|
||||||
|
state → Skipped (not Decayed). Re-provisioning is a future feature.
|
||||||
|
"""
|
||||||
import boto3
|
import boto3
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
env = _load_aws_env()
|
env = _load_aws_env()
|
||||||
|
account_id = _envhelper.get_env("AWS_ACCOUNT_ID", "581513795199")
|
||||||
|
state_bucket = f"nova-tfstate-{account_id}-us-east-1"
|
||||||
try:
|
try:
|
||||||
s3 = boto3.client("s3", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"),
|
s3 = boto3.client("s3", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"),
|
||||||
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
|
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
|
||||||
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
||||||
s3.head_bucket(Bucket="acdl-tfstate-581513795199-us-east-1")
|
s3.head_bucket(Bucket=state_bucket)
|
||||||
r = s3.list_objects_v2(Bucket="acdl-tfstate-581513795199-us-east-1", MaxKeys=5)
|
r = s3.list_objects_v2(Bucket=state_bucket, MaxKeys=5)
|
||||||
keys = [o["Key"] for o in r.get("Contents", [])]
|
keys = [o["Key"] for o in r.get("Contents", [])]
|
||||||
return "Verified", f"state bucket exists, keys={keys}"
|
return "Verified", f"state bucket exists, keys={keys}"
|
||||||
|
except ClientError as e:
|
||||||
|
code = e.response.get("Error", {}).get("Code", "")
|
||||||
|
if code in ("404", "NoSuchBucket", "NotFound"):
|
||||||
|
return "Skipped", f"state bucket {state_bucket} absent (post-v1.11-teardown, D-096)"
|
||||||
|
return "Decayed", f"head_bucket failed: {type(e).__name__}: {str(e)[:150]}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "Decayed", f"head_bucket failed: {type(e).__name__}: {str(e)[:150]}"
|
return "Decayed", f"head_bucket failed: {type(e).__name__}: {str(e)[:150]}"
|
||||||
|
|
||||||
@@ -461,16 +479,30 @@ def _check_lifecycle_module_terraform(module: str) -> Tuple[Status, str]:
|
|||||||
["terraform", "fmt", "-check", "-diff", str(tf_dir)], timeout=30)
|
["terraform", "fmt", "-check", "-diff", str(tf_dir)], timeout=30)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
return "Broken", f"terraform fmt -check failed: {err.strip()[-200:]}"
|
return "Broken", f"terraform fmt -check failed: {err.strip()[-200:]}"
|
||||||
|
status, detail = _assert_contracts_resolve(ROOT / "modules" / "l1" / module, "l1")
|
||||||
|
if status != "Verified":
|
||||||
|
return status, detail
|
||||||
|
return "Verified", f"terraform files present + fmt -check passes + simple/complex contracts resolve"
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_contracts_resolve(module_dir: Path, level: str) -> Tuple[Status, str]:
|
||||||
|
"""Shared helper: assert an L1/L2 module's example contracts resolve.
|
||||||
|
|
||||||
|
Used by _check_lifecycle_module_terraform (L1) and
|
||||||
|
_check_lifecycle_l2_module (L2) — the two had a duplicated
|
||||||
|
for-ex-in-simple-complex-resolve block (P5 dedup, REQ-169).
|
||||||
|
``level`` is "l1" or "l2" (selects the examples dir parent).
|
||||||
|
"""
|
||||||
for ex in ["simple", "complex"]:
|
for ex in ["simple", "complex"]:
|
||||||
contract = ROOT / "modules" / "l1" / module / "examples" / f"{ex}.yml"
|
contract = module_dir / "examples" / f"{ex}.yml"
|
||||||
if not contract.is_file():
|
if not contract.is_file():
|
||||||
return "Broken", f"modules/l1/{module}/examples/{ex}.yml missing"
|
return "Broken", f"{module_dir.relative_to(ROOT)}/examples/{ex}.yml missing"
|
||||||
rc, out, err = _run_subprocess([
|
rc, out, err = _run_subprocess([
|
||||||
"python3", "core/contract_resolver.py", str(contract), "/dev/null",
|
"python3", "core/contract_resolver.py", str(contract), "/dev/null",
|
||||||
], timeout=30)
|
], timeout=30)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}"
|
return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}"
|
||||||
return "Verified", f"terraform files present + fmt -check passes + simple/complex contracts resolve"
|
return "Verified", ""
|
||||||
|
|
||||||
|
|
||||||
def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
|
def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
|
||||||
@@ -479,20 +511,15 @@ def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
|
|||||||
This is an offline proxy, not live pipeline evidence; the live
|
This is an offline proxy, not live pipeline evidence; the live
|
||||||
apply/modify/destroy is verified by the modules-lifecycle workflow
|
apply/modify/destroy is verified by the modules-lifecycle workflow
|
||||||
run, not by this gate."""
|
run, not by this gate."""
|
||||||
for ex in ["simple", "complex"]:
|
module_dir = ROOT / "modules" / "l2" / module
|
||||||
contract = ROOT / "modules" / "l2" / module / "examples" / f"{ex}.yml"
|
status, detail = _assert_contracts_resolve(module_dir, "l2")
|
||||||
if not contract.is_file():
|
if status != "Verified":
|
||||||
return "Broken", f"modules/l2/{module}/examples/{ex}.yml missing"
|
return status, detail
|
||||||
rc, out, err = _run_subprocess([
|
return "Verified", "L2 composition resolves (simple + complex contracts; offline proxy)"
|
||||||
"python3", "core/contract_resolver.py", str(contract), "/dev/null",
|
|
||||||
], timeout=30)
|
|
||||||
if rc != 0:
|
|
||||||
return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}"
|
|
||||||
return "Verified", f"L2 composition resolves (simple + complex contracts; offline proxy)"
|
|
||||||
|
|
||||||
|
|
||||||
def _check_cap_017_dynamodb() -> Tuple[Status, str]:
|
def _check_cap_017_dynamodb() -> Tuple[Status, str]:
|
||||||
"""CAP-017: DynamoDB acdl-contracts table. Evidence = L1 rds module
|
"""CAP-017: DynamoDB nova-contracts table. Evidence = L1 rds module
|
||||||
lifecycle pipeline green (terraform validate + contracts resolve).
|
lifecycle pipeline green (terraform validate + contracts resolve).
|
||||||
The DynamoDB table is created via the microservice stack (L2 lifecycle).
|
The DynamoDB table is created via the microservice stack (L2 lifecycle).
|
||||||
"""
|
"""
|
||||||
@@ -507,7 +534,7 @@ def _check_cap_018_lambda() -> Tuple[Status, str]:
|
|||||||
"python3", "-c",
|
"python3", "-c",
|
||||||
"from core.local_emulators import LocalLambdaStub, FlatFileOutbox; "
|
"from core.local_emulators import LocalLambdaStub, FlatFileOutbox; "
|
||||||
"import tempfile; "
|
"import tempfile; "
|
||||||
"stub = LocalLambdaStub(outbox=FlatFileOutbox(tempfile.mkdtemp(prefix='acdl_stub_'))); "
|
"stub = LocalLambdaStub(outbox=FlatFileOutbox(tempfile.mkdtemp(prefix='nova_stub_'))); "
|
||||||
"print('LocalLambdaStub instantiates OK')",
|
"print('LocalLambdaStub instantiates OK')",
|
||||||
])
|
])
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
@@ -539,6 +566,59 @@ def _check_cap_022_oidc_role() -> Tuple[Status, str]:
|
|||||||
return _check_lifecycle_module_terraform("iam-role")
|
return _check_lifecycle_module_terraform("iam-role")
|
||||||
|
|
||||||
|
|
||||||
|
def _check_cap_023_metrics_collector() -> Tuple[Status, str]:
|
||||||
|
"""CAP-023: metrics collector runs and emits the expected schema (v1.17).
|
||||||
|
|
||||||
|
Verifies that core/metrics/collector.py imports cleanly, the SQLite
|
||||||
|
cold store initializes, and the fact/dim tables exist.
|
||||||
|
"""
|
||||||
|
import importlib
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module("core.metrics.collector")
|
||||||
|
mod._init_store()
|
||||||
|
import sqlite3, os
|
||||||
|
db_path = mod._STORE_PATH
|
||||||
|
if not os.path.isfile(db_path):
|
||||||
|
return "Skipped", "metrics collector init skipped (no store)"
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
tables = [r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()]
|
||||||
|
conn.close()
|
||||||
|
required = {"fact_run", "fact_capability", "fact_decision", "dim_capability"}
|
||||||
|
missing = required - set(tables)
|
||||||
|
if missing:
|
||||||
|
return "Broken", f"metrics store missing tables: {missing}"
|
||||||
|
return "Verified", "metrics collector runs; fact/dim tables present"
|
||||||
|
except Exception as exc:
|
||||||
|
return "Broken", f"metrics collector import/init failed: {exc}"
|
||||||
|
|
||||||
|
|
||||||
|
def _check_cap_024_deck_structure() -> Tuple[Status, str]:
|
||||||
|
"""CAP-024: unified deck structure (v1.17 + v1.21 refinement).
|
||||||
|
|
||||||
|
Verifies the unified deck source of truth exists, has 18 main slides
|
||||||
|
(## Slide N) + 1 appendix, has the recap+ask closing, and per-slide
|
||||||
|
benefit callouts. v1.21 renamed the deck + restructured to a 4-beat arc.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
deck_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||||
|
"docs", "presentations", "nova-autonomous-cloud-delivery.md")
|
||||||
|
if not os.path.isfile(deck_path):
|
||||||
|
return "Skipped", "unified deck not found"
|
||||||
|
with open(deck_path) as f:
|
||||||
|
content = f.read()
|
||||||
|
slide_count = content.count("## Slide ")
|
||||||
|
if slide_count < 18 or slide_count > 19:
|
||||||
|
return "Broken", f"deck has {slide_count} main slides (expected 18-19)"
|
||||||
|
has_recap = "Recap + Ask" in content
|
||||||
|
has_benefit = content.count("Benefit:") >= 10
|
||||||
|
if not (has_recap and has_benefit):
|
||||||
|
missing = []
|
||||||
|
if not has_recap: missing.append("recap+ask")
|
||||||
|
if not has_benefit: missing.append("per-slide benefit callouts")
|
||||||
|
return "Broken", f"deck missing: {missing}"
|
||||||
|
return "Verified", f"deck has {slide_count} slides, recap+ask present, per-slide benefits present"
|
||||||
|
|
||||||
|
|
||||||
# Registry: ordered, each entry is (capability_id, name, tier, check_fn).
|
# Registry: ordered, each entry is (capability_id, name, tier, check_fn).
|
||||||
# Phase 52 seeds this with 10 local-tier checks; Phase 54 expands it to
|
# Phase 52 seeds this with 10 local-tier checks; Phase 54 expands it to
|
||||||
# cover every v1.1->v1.8 advertised capability and adds the live-AWS tier
|
# cover every v1.1->v1.8 advertised capability and adds the live-AWS tier
|
||||||
@@ -576,7 +656,7 @@ CAPABILITY_REGISTRY: List[Tuple[str, str, str, Callable[[], Tuple[Status, str]]]
|
|||||||
_check_dynamodb_outbox_table),
|
_check_dynamodb_outbox_table),
|
||||||
("CAP-016", "S3 state bucket exists + readable (live AWS)", "live-aws",
|
("CAP-016", "S3 state bucket exists + readable (live AWS)", "live-aws",
|
||||||
_check_s3_state_bucket),
|
_check_s3_state_bucket),
|
||||||
("CAP-017", "DynamoDB acdl-contracts table (lifecycle pipeline evidence)", "lifecycle-pipeline",
|
("CAP-017", "DynamoDB nova-contracts table (lifecycle pipeline evidence)", "lifecycle-pipeline",
|
||||||
_check_cap_017_dynamodb),
|
_check_cap_017_dynamodb),
|
||||||
("CAP-018", "Lambda contract-ingestor (local stub + lifecycle evidence)", "lifecycle-pipeline",
|
("CAP-018", "Lambda contract-ingestor (local stub + lifecycle evidence)", "lifecycle-pipeline",
|
||||||
_check_cap_018_lambda),
|
_check_cap_018_lambda),
|
||||||
@@ -588,6 +668,10 @@ CAPABILITY_REGISTRY: List[Tuple[str, str, str, Callable[[], Tuple[Status, str]]]
|
|||||||
_check_cap_021_uptime),
|
_check_cap_021_uptime),
|
||||||
("CAP-022", "OIDC role (L1 iam-role lifecycle evidence)", "lifecycle-pipeline",
|
("CAP-022", "OIDC role (L1 iam-role lifecycle evidence)", "lifecycle-pipeline",
|
||||||
_check_cap_022_oidc_role),
|
_check_cap_022_oidc_role),
|
||||||
|
("CAP-023", "metrics collector runs + emits expected schema", "local",
|
||||||
|
_check_cap_023_metrics_collector),
|
||||||
|
("CAP-024", "unified deck structure (slide count, x3, per-slide benefits)", "local",
|
||||||
|
_check_cap_024_deck_structure),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -641,17 +725,9 @@ def write_report(report: RegressionReport,
|
|||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
milestone = os.environ.get("ACDL_REGRESSION_MILESTONE", "v1.10")
|
"""P13 (REQ-177): re-export from core.regression_verify_cli."""
|
||||||
phase = int(os.environ.get("ACDL_REGRESSION_PHASE", "52"))
|
from core.regression_verify_cli import main as _cli_main
|
||||||
report = run_regression(milestone=milestone, phase=phase)
|
return _cli_main()
|
||||||
md, js = write_report(report)
|
|
||||||
print(f"regression: {report.summary} -> {md}")
|
|
||||||
if not report.passed:
|
|
||||||
print("FAIL: regression surfaced non-Verified capabilities "
|
|
||||||
"(milestone gate blocks)", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
print("regression: all capabilities Verified (milestone gate passes)")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Nova Regression Verify CLI — command-line entry point.
|
||||||
|
|
||||||
|
Extracted from core/regression_verify.py (P13, REQ-177).
|
||||||
|
|
||||||
|
G-113 import direction: this module imports core.regression_verify (the
|
||||||
|
library) for run_regression + write_report. The library does not import
|
||||||
|
this CLI module. Nothing imports this CLI except direct invocation.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from core import env as _envhelper
|
||||||
|
from core.regression_verify import run_regression, write_report
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
"""CLI: run the regression gate and write the report."""
|
||||||
|
milestone = _envhelper.get_env("REGRESSION_MILESTONE", "v1.10") or "v1.10"
|
||||||
|
phase = int(_envhelper.get_env("REGRESSION_PHASE", "52") or "52")
|
||||||
|
report = run_regression(milestone=milestone, phase=phase)
|
||||||
|
md, js = write_report(report)
|
||||||
|
print(f"regression: {report.summary} -> {md}")
|
||||||
|
if not report.passed:
|
||||||
|
print("FAIL: regression surfaced non-Verified/non-Skipped capabilities "
|
||||||
|
"(milestone gate blocks)", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"regression: gate passes (summary={report.summary})")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,20 +1,31 @@
|
|||||||
"""Check that qaApprover != prodApprover for a contract (ARCHITECTURE.md
|
"""Check that qaApprover != prodApprover for a contract (ARCHITECTURE.md
|
||||||
§10.3, D-042). Reads `approver_qa` from the DynamoDB outbox for the
|
§10.3, D-042). Reads `approver_qa` from the DynamoDB outbox for the
|
||||||
contractId, compares to the prod-dispatch `gitea.actor` / `github.actor`.
|
contractId, compares to the prod-dispatch the CI actor.
|
||||||
Blocks on equality, emits `SEPARATION_OF_DUTIES_VIOLATION`, routes a halt
|
Blocks on equality, emits `SEPARATION_OF_DUTIES_VIOLATION`, routes a halt
|
||||||
artifact to SRE on-call.
|
artifact to SRE on-call.
|
||||||
|
|
||||||
v1.9 (REQ-107, D-085): route_halt_artifact is a real implementation —
|
v1.9 (REQ-107, D-085): route_halt_artifact is a real implementation —
|
||||||
publishes to SNS topic `acdl-sod-halt` (ARN from ACDL_SOD_HALT_TOPIC_ARN)
|
publishes to SNS topic `acdl-sod-halt` (ARN from NOVA_SOD_HALT_TOPIC_ARN)
|
||||||
when set; falls back to a structured stderr emission + a
|
when set; falls back to a structured stderr emission + a
|
||||||
SEPARATION_OF_DUTIES_VIOLATION event write to the DynamoDB outbox when
|
SEPARATION_OF_DUTIES_VIOLATION event write to the DynamoDB outbox when
|
||||||
unset. No silent print-only stub.
|
unset. No silent print-only stub. (Dual-read via core/env.py: NOVA_*
|
||||||
|
preferred, ACDL_* fallback until P5; the SNS topic ARN is the AWS
|
||||||
|
resource `acdl-sod-halt` → renamed `nova-sod-halt` in P4.)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
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 imported/run in a context where an editable-installed third-party
|
||||||
|
# `core` package would otherwise 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 check(outbox_client, contract_id: str,
|
def check(outbox_client, contract_id: str,
|
||||||
current_prod_approver: Optional[str]) -> Tuple[bool, str]:
|
current_prod_approver: Optional[str]) -> Tuple[bool, str]:
|
||||||
@@ -40,13 +51,13 @@ def route_halt_artifact(contract_id: str, violation_reason: str,
|
|||||||
oncall_client=None) -> None:
|
oncall_client=None) -> None:
|
||||||
"""Route a halt artifact to SRE on-call (REQ-107, D-085).
|
"""Route a halt artifact to SRE on-call (REQ-107, D-085).
|
||||||
|
|
||||||
When ACDL_SOD_HALT_TOPIC_ARN is set, publish to the SNS topic via
|
When NOVA_SOD_HALT_TOPIC_ARN is set, publish to the SNS topic via
|
||||||
boto3. When unset (dev/CI), fall back to a structured stderr emission
|
boto3. When unset (dev/CI), fall back to a structured stderr emission
|
||||||
+ a SEPARATION_OF_DUTIES_VIOLATION event write to the DynamoDB outbox
|
+ a SEPARATION_OF_DUTIES_VIOLATION event write to the DynamoDB outbox
|
||||||
via outbox_writer.write_event (so the halt is in the audit chain).
|
via outbox_writer.write_event (so the halt is in the audit chain).
|
||||||
The oncall_client, when provided, is the SNS client (test injection).
|
The oncall_client, when provided, is the SNS client (test injection).
|
||||||
"""
|
"""
|
||||||
topic_arn = os.environ.get("ACDL_SOD_HALT_TOPIC_ARN", "")
|
topic_arn = env.get_env("SOD_HALT_TOPIC_ARN", "") or ""
|
||||||
halt_payload = {
|
halt_payload = {
|
||||||
"contractId": contract_id,
|
"contractId": contract_id,
|
||||||
"reason": violation_reason,
|
"reason": violation_reason,
|
||||||
@@ -63,7 +74,7 @@ def route_halt_artifact(contract_id: str, violation_reason: str,
|
|||||||
sns.publish(
|
sns.publish(
|
||||||
TopicArn=topic_arn,
|
TopicArn=topic_arn,
|
||||||
Message=json.dumps(halt_payload),
|
Message=json.dumps(halt_payload),
|
||||||
Subject="ACDL SoD halt",
|
Subject="Nova SoD halt",
|
||||||
)
|
)
|
||||||
print(f"[halt-artifact] SNS published contract={contract_id} "
|
print(f"[halt-artifact] SNS published contract={contract_id} "
|
||||||
f"topic={topic_arn}", flush=True)
|
f"topic={topic_arn}", flush=True)
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
"""core/submission_readiness.py — Nova submission-readiness validator (REQ-218).
|
||||||
|
|
||||||
|
Defines what is acceptable to start — a superset gate ABOVE
|
||||||
|
contract.schema.json validity. Invoked as
|
||||||
|
``contract_ingestor.py --check-readiness`` (D-133). Returns a structured
|
||||||
|
ReadinessResult (pass/fail per check, with reason codes). On fail → the
|
||||||
|
ingestor rejects with a citizen-developer-facing error (not a stack
|
||||||
|
trace). On pass → proceeds to existing contract ingestion.
|
||||||
|
|
||||||
|
The validator calls contract.schema.json validation first (the shape),
|
||||||
|
then the readiness checks (the gate): tags, env mandatory, policy
|
||||||
|
preconditions, profile:agentic markers, appSource.
|
||||||
|
|
||||||
|
Reason codes:
|
||||||
|
MISSING_TAGS — one or more required Nova tags are absent
|
||||||
|
ENV_MISSING_MANDATORY:<env>:<field> — a per-env mandatory field is missing
|
||||||
|
AGENTIC_MISSING_INTENT — profile=agentic but naturalLanguageIntent absent
|
||||||
|
MISSING_APP_SOURCE — appSource (repo + ref) is missing
|
||||||
|
POLICY_PRECONDITION_MISSING — a declared policy precondition is absent
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_SCHEMA_DIR = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "schemas"
|
||||||
|
)
|
||||||
|
|
||||||
|
REQUIRED_TAGS = [
|
||||||
|
"nova:owner",
|
||||||
|
"nova:contract",
|
||||||
|
"nova:environment",
|
||||||
|
"nova:cost-center",
|
||||||
|
"nova:ref",
|
||||||
|
]
|
||||||
|
|
||||||
|
ENV_MANDATORY: dict[str, list[str]] = {
|
||||||
|
"dev": [], # dev requires only the base contract shape (id+environment+infrastructure)
|
||||||
|
"qa": ["validation.e2eSuite", "validation.loadTest"],
|
||||||
|
"prod": ["runbook", "dashboard", "oncall"],
|
||||||
|
"dr": ["drDrillRef"],
|
||||||
|
}
|
||||||
|
|
||||||
|
AGENTIC_REQUIRED = ["naturalLanguageIntent", "confidenceAtSubmission", "agentTrace"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ReadinessResult:
|
||||||
|
"""Structured result of the submission-readiness gate."""
|
||||||
|
|
||||||
|
ready: bool
|
||||||
|
reason_codes: list[str] = field(default_factory=list)
|
||||||
|
contract_id: str | None = None
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"ready": self.ready,
|
||||||
|
"reason_codes": self.reason_codes,
|
||||||
|
"contractId": self.contract_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.ready:
|
||||||
|
return f"READY — contract {self.contract_id} passes submission-readiness gate"
|
||||||
|
codes = "; ".join(self.reason_codes) if self.reason_codes else "unknown"
|
||||||
|
return f"NOT READY — contract {self.contract_id}: {codes}"
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_contract_schema(contract: dict[str, Any]) -> list[str]:
|
||||||
|
"""Validate the contract against contract.schema.json (the shape).
|
||||||
|
Returns a list of reason codes (empty if valid). Falls back to no-op
|
||||||
|
if jsonschema or the schema file is unavailable (the contract is
|
||||||
|
validated upstream by run_platform.sh in the normal path).
|
||||||
|
"""
|
||||||
|
codes: list[str] = []
|
||||||
|
try:
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
schema_path = os.path.join(_SCHEMA_DIR, "contract.schema.json")
|
||||||
|
with open(schema_path) as f:
|
||||||
|
schema = json.load(f)
|
||||||
|
jsonschema.validate(instance=contract, schema=schema)
|
||||||
|
except (OSError, ImportError):
|
||||||
|
pass
|
||||||
|
except jsonschema.ValidationError as e:
|
||||||
|
codes.append(f"CONTRACT_SCHEMA_INVALID:{e.message}")
|
||||||
|
return codes
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nested(data: dict[str, Any], dotted_key: str) -> Any:
|
||||||
|
parts = dotted_key.split(".")
|
||||||
|
val: Any = data
|
||||||
|
for p in parts:
|
||||||
|
if not isinstance(val, dict) or p not in val:
|
||||||
|
return None
|
||||||
|
val = val[p]
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def check_readiness(submission: dict[str, Any]) -> ReadinessResult:
|
||||||
|
"""Run the full submission-readiness gate.
|
||||||
|
|
||||||
|
1. Validate the contract shape (contract.schema.json).
|
||||||
|
2. Validate the readiness schema (submission-readiness.schema.json).
|
||||||
|
3. Run the semantic readiness checks (tags, env mandatory, agentic, appSource, policy).
|
||||||
|
|
||||||
|
Returns a ReadinessResult. Never raises — all failures are reason codes.
|
||||||
|
"""
|
||||||
|
contract_id = submission.get("contractId") or submission.get("id", "unknown")
|
||||||
|
codes: list[str] = []
|
||||||
|
|
||||||
|
# Step 1: contract shape validation
|
||||||
|
contract_shape = {k: v for k, v in submission.items() if k in ("id", "name", "environment", "infrastructure")}
|
||||||
|
if contract_shape:
|
||||||
|
codes.extend(_validate_contract_schema(contract_shape))
|
||||||
|
|
||||||
|
# Step 2: readiness schema validation
|
||||||
|
try:
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
schema_path = os.path.join(_SCHEMA_DIR, "submission-readiness.schema.json")
|
||||||
|
with open(schema_path) as f:
|
||||||
|
readiness_schema = json.load(f)
|
||||||
|
jsonschema.validate(instance=submission, schema=readiness_schema)
|
||||||
|
except (OSError, ImportError):
|
||||||
|
pass
|
||||||
|
except jsonschema.ValidationError as e:
|
||||||
|
codes.append(f"READINESS_SCHEMA_INVALID:{e.message}")
|
||||||
|
|
||||||
|
# Step 3: semantic checks (reason codes for citizen-developer-facing errors)
|
||||||
|
|
||||||
|
# 3a: tags
|
||||||
|
tags = submission.get("tags", {})
|
||||||
|
missing_tags = [t for t in REQUIRED_TAGS if t not in tags or not tags[t]]
|
||||||
|
if missing_tags:
|
||||||
|
codes.append(f"MISSING_TAGS:{','.join(missing_tags)}")
|
||||||
|
|
||||||
|
# 3b: env mandatory (W3.E per-env table)
|
||||||
|
env = submission.get("environment")
|
||||||
|
if env and env in ENV_MANDATORY:
|
||||||
|
for field_key in ENV_MANDATORY[env]:
|
||||||
|
val = _get_nested(submission, field_key)
|
||||||
|
if val is None:
|
||||||
|
codes.append(f"ENV_MISSING_MANDATORY:{env}:{field_key}")
|
||||||
|
|
||||||
|
# 3c: agentic profile markers
|
||||||
|
if submission.get("profile") == "agentic":
|
||||||
|
for marker in AGENTIC_REQUIRED:
|
||||||
|
if not submission.get(marker):
|
||||||
|
codes.append(f"AGENTIC_MISSING_INTENT:{marker}")
|
||||||
|
|
||||||
|
# 3d: appSource
|
||||||
|
app_source = submission.get("appSource")
|
||||||
|
if not app_source or not app_source.get("repo") or not app_source.get("ref"):
|
||||||
|
codes.append("MISSING_APP_SOURCE")
|
||||||
|
|
||||||
|
# 3e: policy preconditions (warn if declared but not enforced this milestone)
|
||||||
|
policy = submission.get("policyPreconditions", {})
|
||||||
|
if not policy:
|
||||||
|
codes.append("POLICY_PRECONDITION_MISSING")
|
||||||
|
|
||||||
|
ready = len(codes) == 0
|
||||||
|
return ReadinessResult(ready=ready, reason_codes=codes, contract_id=contract_id)
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv: list[str]) -> int:
|
||||||
|
"""CLI entry: python3 -m core.submission_readiness <contract.json>
|
||||||
|
|
||||||
|
Also invoked via contract_ingestor.py --check-readiness (D-133).
|
||||||
|
Prints the ReadinessResult to stdout; exits 0 if ready, 1 if not.
|
||||||
|
"""
|
||||||
|
if len(argv) < 2:
|
||||||
|
print("Usage: submission_readiness <contract.json>", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
path = argv[1]
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
submission = json.load(f)
|
||||||
|
except (OSError, json.JSONDecodeError) as e:
|
||||||
|
print(f"ERROR: cannot read {path}: {e}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
result = check_readiness(submission)
|
||||||
|
print(result)
|
||||||
|
print(json.dumps(result.to_dict(), indent=2))
|
||||||
|
return 0 if result.ready else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(cli_main(sys.argv))
|
||||||
+177
@@ -0,0 +1,177 @@
|
|||||||
|
# Nova Metrics Catalog
|
||||||
|
|
||||||
|
|
||||||
|
This is the canonical catalog of every executive KPI in Nova's
|
||||||
|
leadership metrics layer. Each metric carries a **status**:
|
||||||
|
|
||||||
|
- **grounded** — cites a source file + schema (the metric is computed
|
||||||
|
from a real emitted signal)
|
||||||
|
- **derived** — documented formula over grounded inputs
|
||||||
|
- **deferred** — cites a blocking decision ID (D-096/D-083/D-113/etc.);
|
||||||
|
ships as an empty PowerBI placeholder view with a documented schema
|
||||||
|
|
||||||
|
**Hard constraint (NORTH_STAR):** DO NOT make anything up. No fabricated
|
||||||
|
numbers. Every metric either has a real source or is explicitly deferred.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zero-Touch Efficiency & AI Autonomy (REQ-191)
|
||||||
|
|
||||||
|
### Touchless Resolution Rate
|
||||||
|
- **Target:** ≥ 99% across production estates (Post-Pilot)
|
||||||
|
- **Status:** partial (pipeline grounded; denominator = 0 today)
|
||||||
|
- **Formula:** runs completing without *operational* HITL block ÷ total runs
|
||||||
|
(attestation gates excluded — they're designed controls, not escalations)
|
||||||
|
- **Source:** `metrics/nova_metrics.db` `fact_run` (hitl_block column)
|
||||||
|
- **Definition-of-success:** `docs/metrics/touchless_resolution_rate.md`
|
||||||
|
|
||||||
|
### Human Escalation Frequency
|
||||||
|
- **Target:** < 0.1% of platform actions (Post-Pilot)
|
||||||
|
- **Status:** partial (pipeline grounded; denominator = 0 today)
|
||||||
|
- **Formula:** operational HITL blocks ÷ total runs (attestation sign-offs
|
||||||
|
excluded)
|
||||||
|
- **Source:** `metrics/nova_metrics.db` `fact_run` (hitl_block column)
|
||||||
|
- **Definition-of-success:** `docs/metrics/human_escalation_frequency.md`
|
||||||
|
|
||||||
|
### AI Decision Accuracy
|
||||||
|
- **Target:** ≥ 99.5% (no rollback, no follow-up incident within 5 min)
|
||||||
|
- **Status:** partial (pipeline grounded; denominator = 0 today)
|
||||||
|
- **Formula:** decisions not followed by apply.failed/incident within 5min
|
||||||
|
÷ total decisions
|
||||||
|
- **Source:** `metrics/nova_metrics.db` `fact_decision` (outcome column)
|
||||||
|
- **Definition-of-success:** `docs/metrics/ai_decision_accuracy.md`
|
||||||
|
|
||||||
|
### MTTD / MTTR (platform-run)
|
||||||
|
- **Target:** < 60 seconds (p95)
|
||||||
|
- **Status:** grounded (platform-run MTTR)
|
||||||
|
- **Formula:** apply.failed.time → successful retry.time
|
||||||
|
- **Source:** `metrics/nova_metrics.db` `fact_run` (started_at, completed_at)
|
||||||
|
- **Note:** infra-incident MTTR deferred (no incident detection system)
|
||||||
|
- **Definition-of-success:** `docs/metrics/mttr.md`
|
||||||
|
|
||||||
|
### Confidence-Gate Halt Rate (REQ-212)
|
||||||
|
- **Target:** not a committed target (operational signal)
|
||||||
|
- **Status:** grounded
|
||||||
|
- **Formula:** runs where confidence band = halt ÷ total runs
|
||||||
|
- **Source:** `metrics/nova_metrics.db` `fact_confidence` (band column)
|
||||||
|
- **Definition-of-success:** `docs/metrics/confidence_gate_halt_rate.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Velocity (REQ-192)
|
||||||
|
|
||||||
|
### Provisioning Lead Time
|
||||||
|
- **Target:** not a committed target (operational signal)
|
||||||
|
- **Status:** grounded (after P1)
|
||||||
|
- **Formula:** apply.completed.time − intent.received.time
|
||||||
|
- **Source:** `metrics/nova_metrics.db` `fact_run` (started_at, completed_at)
|
||||||
|
- **Definition-of-success:** `docs/metrics/provisioning_lead_time.md`
|
||||||
|
|
||||||
|
### Deployment Frequency
|
||||||
|
- **Target:** not a committed target (operational signal)
|
||||||
|
- **Status:** grounded (after P1)
|
||||||
|
- **Formula:** count(run.completed) per day
|
||||||
|
- **Source:** `metrics/nova_metrics.db` `fact_run`
|
||||||
|
- **Definition-of-success:** `docs/metrics/deployment_frequency.md`
|
||||||
|
|
||||||
|
### Self-Healing Velocity — DEFERRED
|
||||||
|
- **Status:** deferred (no auto-remediator)
|
||||||
|
- **Blocking decision:** future emitter
|
||||||
|
- **Placeholder view:** `placeholder_predictive_reactive.csv`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Financial & Cost ROI (REQ-193)
|
||||||
|
|
||||||
|
### Cost Savings via Infracost Estimates
|
||||||
|
- **Target:** ≥ 25% on pilot estates (partial)
|
||||||
|
- **Status:** partial (pre-apply estimate grounded; actual-spend deferred D-096)
|
||||||
|
- **Formula:** sum(cost_estimate.delta_usd) where delta < 0
|
||||||
|
- **Source:** `metrics/nova_metrics.db` `fact_cost_estimate`
|
||||||
|
- **Definition-of-success:** `docs/metrics/cost_savings.md`
|
||||||
|
|
||||||
|
### FTE Hours Saved (Toil Reallocation Value)
|
||||||
|
- **Target:** ≥ 70% of pre-Nova FTE allocation (derived)
|
||||||
|
- **Status:** derived
|
||||||
|
- **Formula:** run count × manual baseline minutes × blended rate
|
||||||
|
- **Source:** `metrics/nova_metrics.db` `fact_run` (count) + manual baseline
|
||||||
|
- **Note:** computed on N internal runs today; production-denominator
|
||||||
|
activates post-pilot
|
||||||
|
- **Definition-of-success:** `docs/metrics/fte_hours_saved.md`
|
||||||
|
|
||||||
|
### Platform ROI
|
||||||
|
- **Target:** ≥ 250% measured annually (derived)
|
||||||
|
- **Status:** derived
|
||||||
|
- **Formula:** (FTE hours saved × blended rate + cloud savings + avoided
|
||||||
|
downtime) ÷ platform op cost
|
||||||
|
- **Source:** derived from fact_run + fact_cost_estimate + manual baseline
|
||||||
|
- **Note:** computed on N internal runs today; production-denominator
|
||||||
|
activates post-pilot
|
||||||
|
- **Definition-of-success:** `docs/metrics/platform_roi.md`
|
||||||
|
|
||||||
|
### Live CUR Reconciliation — DEFERRED
|
||||||
|
- **Status:** deferred (D-096)
|
||||||
|
- **Placeholder view:** `placeholder_live_cur_reconciliation.csv`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reliability, Security & Compliance (REQ-194)
|
||||||
|
|
||||||
|
### Zero-Trust Policy Compliance Rate
|
||||||
|
- **Target:** not a committed target (operational signal)
|
||||||
|
- **Status:** grounded (after P1)
|
||||||
|
- **Formula:** 1 − count(assets WHERE last_scan.status ≠ pass) ÷ count(assets)
|
||||||
|
- **Source:** `metrics/nova_metrics.db` `fact_policy_check`
|
||||||
|
- **Definition-of-success:** `docs/metrics/policy_compliance_rate.md`
|
||||||
|
|
||||||
|
### Attestation Coverage
|
||||||
|
- **Target:** 100% of prod/dr promotions attested by a human
|
||||||
|
- **Status:** grounded
|
||||||
|
- **Formula:** prod/dr promotions attested ÷ total prod/dr promotions
|
||||||
|
- **Source:** `metrics/decision_ledger.db` (attestation.recorded events) +
|
||||||
|
`hitl_gates.py` + outbox `approver_*` attributes
|
||||||
|
- **Definition-of-success:** `docs/metrics/attestation_coverage.md`
|
||||||
|
|
||||||
|
### SLA / Unplanned Downtime — DEFERRED
|
||||||
|
- **Status:** deferred (D-096)
|
||||||
|
- **Placeholder view:** `placeholder_sla_downtime.csv`
|
||||||
|
|
||||||
|
### Patch Remediation Rate — DEFERRED
|
||||||
|
- **Status:** deferred (no patch remediation system)
|
||||||
|
- **Placeholder view:** (future)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trust Substrate (REQ-211)
|
||||||
|
|
||||||
|
### Decision Ledger Coverage
|
||||||
|
- **Target:** 100% of AI actions with backfilled outcome
|
||||||
|
- **Status:** grounded (this milestone builds it)
|
||||||
|
- **Formula:** count(decision_ledger rows with outcome ≠ 'pending') ÷
|
||||||
|
count(decision_ledger rows)
|
||||||
|
- **Source:** `metrics/decision_ledger.db` + `core/metrics/decision_ledger.py`
|
||||||
|
- **Definition-of-success:** `docs/metrics/decision_ledger_coverage.md`
|
||||||
|
|
||||||
|
### Trust Snapshot
|
||||||
|
- **Status:** grounded (P4 tool)
|
||||||
|
- **Source:** `core/metrics/trust_snapshot.py` → `metrics/TRUST_SNAPSHOT.md`
|
||||||
|
- **Contents:** Decision Ledger Coverage, Attestation Coverage, Capability
|
||||||
|
Health, AI Decision Accuracy, Confidence-Gate Halt Rate, chain-integrity
|
||||||
|
verdict, snapshot hash
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deferred Metrics (8 placeholder views)
|
||||||
|
|
||||||
|
| Metric | Blocking Decision | Placeholder View |
|
||||||
|
|--------|-----------------|------------------|
|
||||||
|
| Live Infrastructure Health | D-096 | `placeholder_live_infra_health.csv` |
|
||||||
|
| Live Outbox Write Rate | D-096 | `placeholder_live_outbox_rate.csv` |
|
||||||
|
| Tamper-Evident Ledger Checkpoints | D-083 | `placeholder_tamper_evident_checkpoints.csv` |
|
||||||
|
| Onboarding Funnel (granted) | D-113/D-114/D-119 | `placeholder_onboarding_funnel.csv` |
|
||||||
|
| Drift Auto-Reversal Rate | D-096 + no scheduler | `placeholder_drift_detection.csv` |
|
||||||
|
| Live CUR Reconciliation | D-096 | `placeholder_live_cur_reconciliation.csv` |
|
||||||
|
| SLA / Unplanned Downtime | D-096 | `placeholder_sla_downtime.csv` |
|
||||||
|
| Predictive vs Reactive Ratio | future emitter | `placeholder_predictive_reactive.csv` |
|
||||||
|
|
||||||
|
See `docs/METRICS_DEFERRED_ROADMAP.md` for the activation path for each.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Nova Deferred Metrics Activation Roadmap
|
||||||
|
|
||||||
|
|
||||||
|
This document lists all 8 deferred metrics + the onboarding-funnel
|
||||||
|
"granted" half, with their blocking decisions, unblock requirements,
|
||||||
|
and candidate future milestones. It also includes the hot-path activation
|
||||||
|
plan (post-D-096) and the re-evaluation triggers.
|
||||||
|
|
||||||
|
## Deferred metrics
|
||||||
|
|
||||||
|
| # | Metric | Blocking Decision | What's Needed to Unblock | Candidate Milestone |
|
||||||
|
|---|--------|-------------------|-------------------------|---------------------|
|
||||||
|
| 1 | Live Infrastructure Health (ECS, ALB, RPS) | D-096 | Re-provision live AWS; deploy microservice/static-assets stacks; emit live health metrics | v1.18+ (live AWS re-provisioning) |
|
||||||
|
| 2 | Live Outbox Write Rate / Ledger Append Latency | D-096 | Re-provision DynamoDB outbox table; emit write-latency metrics | v1.18+ |
|
||||||
|
| 3 | Tamper-Evident Ledger Checkpoints / JWS Signature Rate | D-083 | Build S3 Object Lock + JWS signing + async worker + DLQ + daily checkpoints | v1.19+ (audit ledger build-out) |
|
||||||
|
| 4 | Onboarding Funnel (requested → granted) | D-113/D-114/D-119 | Implement auto-grant: Lambda provisions the cross-account role + ABAC tag + environment binding | v1.18+ (onboarding auto-grant) |
|
||||||
|
| 5 | Drift Auto-Reversal Rate | D-096 + no scheduler | Build a drift-detection scheduler (cron); run `terraform plan -detailed-exitcode` per workspace; emit drift.detected events | v1.20+ (drift detection) |
|
||||||
|
| 6 | Live CUR Reconciliation | D-096 | Re-provision live AWS billing access; build CUR reconciler (6h schedule); match bill lines to resource addresses via tags | v1.18+ |
|
||||||
|
| 7 | SLA / Unplanned Downtime | D-096 | Deploy live services with SLOs; emit uptime metrics against SLO targets | v1.18+ |
|
||||||
|
| 8 | Predictive vs Reactive Ratio | future emitter | Build an ML anomaly-forecasting service; emit anomaly.predicted events with proactive label | v1.21+ (predictive ops) |
|
||||||
|
|
||||||
|
## Onboarding-funnel "granted" half
|
||||||
|
|
||||||
|
The onboarding request path is grounded (REQ-182/183 from v1.16): a
|
||||||
|
consumer submits a request → the Lambda writes a `pending` CMDB row →
|
||||||
|
`core/onboarding.py` generates a binding file. The "granted" half
|
||||||
|
(actual AWS account/network/state provisioning) is deferred per
|
||||||
|
D-113/D-114/D-119. When a future milestone implements auto-grant, the
|
||||||
|
onboarding funnel metric activates: `count(granted) ÷ count(requested)`.
|
||||||
|
|
||||||
|
## Hot-Path Activation (post-D-096)
|
||||||
|
|
||||||
|
**Current state (v1.17):** SQLite cold store only (D-126). No hot path.
|
||||||
|
The hot path activates when live AWS is re-provisioned (D-096 lift).
|
||||||
|
|
||||||
|
**Nova-native hot-path candidates (D-120 — no Kafka/Prometheus/ClickHouse):**
|
||||||
|
1. **SQLite read-replica:** the cold store becomes a read-replica updated
|
||||||
|
on each run; a lightweight file-watcher notifies the dashboard of
|
||||||
|
changes. Freshness = "last run" (not 1-second, but sufficient for
|
||||||
|
batch ops).
|
||||||
|
2. **JSONL tail + webhook:** the events.jsonl log is tailed by a small
|
||||||
|
daemon that pushes updates to a webhook (e.g., a PowerBI streaming
|
||||||
|
dataset or a custom dashboard). Nova-native (no new infra).
|
||||||
|
3. **SQLite + Grafana SQLite datasource:** Grafana can read SQLite
|
||||||
|
directly via the SQLite datasource plugin. No TSDB needed.
|
||||||
|
|
||||||
|
**Migration steps (when D-096 lifts):**
|
||||||
|
1. Re-provision live AWS (microservice + static-assets stacks).
|
||||||
|
2. Add live-health emitters (ECS running count, ALB 5xx, RPS) to
|
||||||
|
`run_platform.sh`.
|
||||||
|
3. Choose a hot-path candidate (above) and implement it.
|
||||||
|
4. Populate the 8 placeholder views with real data.
|
||||||
|
5. Re-run the collector + PowerBI export.
|
||||||
|
|
||||||
|
## Re-evaluation Triggers
|
||||||
|
|
||||||
|
A follow-up metrics ideation should be triggered when any of these
|
||||||
|
events occurs:
|
||||||
|
|
||||||
|
1. **D-096 lift** (live AWS re-provisioned) — triggers hot-path
|
||||||
|
activation + placeholder view population for metrics 1, 2, 5, 6, 7.
|
||||||
|
2. **D-083 lift** (S3 Object Lock + JWS build-out approved) — triggers
|
||||||
|
tamper-evident ledger checkpoint metric (metric 3).
|
||||||
|
3. **Onboarding-grant lift** (auto-grant implemented) — triggers
|
||||||
|
onboarding funnel metric (metric 4).
|
||||||
|
|
||||||
|
When any trigger fires, re-run `/ci-run` with a metrics-focused milestone
|
||||||
|
to activate the corresponding placeholder views.
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Nova Metrics Views — PowerBI Data Dictionary
|
||||||
|
|
||||||
|
|
||||||
|
This document is the column-level data dictionary for the PowerBI export
|
||||||
|
views in `metrics/powerbi/`. Each fact/dimension table and placeholder
|
||||||
|
view is documented with: column, type, source/formula, unit, and
|
||||||
|
grounded/derived/deferred status.
|
||||||
|
|
||||||
|
## Fact tables (grounded)
|
||||||
|
|
||||||
|
### fact_run
|
||||||
|
| Column | Type | Source | Unit | Status |
|
||||||
|
|--------|------|--------|------|--------|
|
||||||
|
| run_id | TEXT | run_manifest.py | — | grounded |
|
||||||
|
| contract_id | TEXT | run_manifest.py | — | grounded |
|
||||||
|
| environment | TEXT | run_manifest.py | dev/qa/prod/dr | grounded |
|
||||||
|
| started_at | TEXT | run_manifest.py | ISO8601 | grounded |
|
||||||
|
| completed_at | TEXT | run_manifest.py | ISO8601 | grounded |
|
||||||
|
| exit_code | INTEGER | run_manifest.py | — | grounded |
|
||||||
|
| outcome | TEXT | run_manifest.py | succeeded/failed | grounded |
|
||||||
|
| confidence_score | REAL | confidence_signal.py | 0.0–1.0 | grounded |
|
||||||
|
| confidence_band | TEXT | confidence_signal.py | pass/warn/block | grounded |
|
||||||
|
| hitl_block | INTEGER | hitl_gates.py | 0/1 | grounded |
|
||||||
|
| cost_estimate_usd | REAL | infracost_adapter.py | USD | grounded (Infracost) |
|
||||||
|
| decision_id | TEXT | decision_ledger.py | — | grounded |
|
||||||
|
|
||||||
|
### fact_capability
|
||||||
|
| Column | Type | Source | Unit | Status |
|
||||||
|
|--------|------|--------|------|--------|
|
||||||
|
| capability_id | TEXT | REGRESSION_REPORT.json | CAP-NNN | grounded |
|
||||||
|
| run_id | TEXT | REGRESSION_REPORT.json | — | grounded |
|
||||||
|
| name | TEXT | REGRESSION_REPORT.json | — | grounded |
|
||||||
|
| status | TEXT | REGRESSION_REPORT.json | Verified/Decayed/Broken/Skipped | grounded |
|
||||||
|
| tier | TEXT | REGRESSION_REPORT.json | local/live-aws/lifecycle-pipeline | grounded |
|
||||||
|
| duration_ms | REAL | REGRESSION_REPORT.json | milliseconds | grounded |
|
||||||
|
| detail | TEXT | REGRESSION_REPORT.json | — | grounded |
|
||||||
|
| run_at_utc | TEXT | REGRESSION_REPORT.json | ISO8601 | grounded |
|
||||||
|
|
||||||
|
### fact_decision
|
||||||
|
| Column | Type | Source | Unit | Status |
|
||||||
|
|--------|------|--------|------|--------|
|
||||||
|
| decision_id | TEXT | decision_ledger.py | = run_id | grounded |
|
||||||
|
| run_id | TEXT | decision_ledger.py | — | grounded |
|
||||||
|
| chosen_action | TEXT | confidence_signal.py | pass/warn/block | grounded |
|
||||||
|
| confidence | REAL | confidence_signal.py | 0.0–1.0 | grounded |
|
||||||
|
| alternatives | TEXT (JSON) | confidence_signal.py | perInput breakdown | grounded |
|
||||||
|
| human_override | INTEGER | hitl_gates.py | 0/1 | grounded |
|
||||||
|
| outcome | TEXT | decision_ledger.py | succeeded/failed/pending | grounded |
|
||||||
|
| event_time | TEXT | decision_ledger.py | ISO8601 | grounded |
|
||||||
|
|
||||||
|
### fact_test
|
||||||
|
| Column | Type | Source | Unit | Status |
|
||||||
|
|--------|------|--------|------|--------|
|
||||||
|
| run_id | TEXT | junit XML | — | grounded |
|
||||||
|
| total_tests | INTEGER | junit XML | count | grounded |
|
||||||
|
| passed | INTEGER | junit XML | count | grounded |
|
||||||
|
| failed | INTEGER | junit XML | count | grounded |
|
||||||
|
| errors | INTEGER | junit XML | count | grounded |
|
||||||
|
| skipped | INTEGER | junit XML | count | grounded |
|
||||||
|
| duration_s | REAL | junit XML | seconds | grounded |
|
||||||
|
| coverage_pct | REAL | coverage.json | % | grounded |
|
||||||
|
| collected_at | TEXT | collector.py | ISO8601 | grounded |
|
||||||
|
|
||||||
|
### fact_cost_estimate
|
||||||
|
| Column | Type | Source | Unit | Status |
|
||||||
|
|--------|------|--------|------|--------|
|
||||||
|
| run_id | TEXT | infracost_adapter.py | — | grounded |
|
||||||
|
| delta_usd | REAL | Infracost | USD/month | grounded (pre-apply) |
|
||||||
|
| total_monthly_usd | REAL | Infracost | USD/month | grounded (pre-apply) |
|
||||||
|
| available | INTEGER | infracost_adapter.py | 0/1 | grounded |
|
||||||
|
| estimated_at | TEXT | infracost_adapter.py | ISO8601 | grounded |
|
||||||
|
|
||||||
|
### fact_lifecycle
|
||||||
|
| Column | Type | Source | Unit | Status |
|
||||||
|
|--------|------|--------|------|--------|
|
||||||
|
| module | TEXT | lifecycle report | — | grounded |
|
||||||
|
| environment | TEXT | lifecycle report | — | grounded |
|
||||||
|
| phase | TEXT | lifecycle report | apply/modify/destroy | grounded |
|
||||||
|
| result | TEXT | lifecycle report | pass/fail | grounded |
|
||||||
|
| duration_ms | REAL | lifecycle report | milliseconds | grounded |
|
||||||
|
| run_at | TEXT | lifecycle report | ISO8601 | grounded |
|
||||||
|
|
||||||
|
## Dimension tables
|
||||||
|
|
||||||
|
### dim_capability
|
||||||
|
| Column | Type | Source | Status |
|
||||||
|
|--------|------|--------|--------|
|
||||||
|
| capability_id | TEXT | REGRESSION_REPORT.json | grounded |
|
||||||
|
| name | TEXT | REGRESSION_REPORT.json | grounded |
|
||||||
|
| tier | TEXT | REGRESSION_REPORT.json | grounded |
|
||||||
|
| source_milestone | TEXT | REGRESSION_REPORT.json | grounded |
|
||||||
|
|
||||||
|
### dim_milestone
|
||||||
|
| Column | Type | Source | Status |
|
||||||
|
|--------|------|--------|--------|
|
||||||
|
| milestone | TEXT | REGRESSION_REPORT.json | grounded |
|
||||||
|
| phase | INTEGER | REGRESSION_REPORT.json | grounded |
|
||||||
|
| tag | TEXT | — | grounded |
|
||||||
|
| completed_at | TEXT | REGRESSION_REPORT.json | grounded |
|
||||||
|
|
||||||
|
## Placeholder views (deferred — 8 views, headers only, no data)
|
||||||
|
|
||||||
|
### placeholder_live_infra_health
|
||||||
|
- **Blocking decision:** D-096
|
||||||
|
- **Description:** Live infrastructure health (ECS running count, ALB 5xx, RPS)
|
||||||
|
- **Columns:** timestamp, resource_id, resource_type, running_count, healthy, downtime_seconds
|
||||||
|
|
||||||
|
### placeholder_live_outbox_rate
|
||||||
|
- **Blocking decision:** D-096
|
||||||
|
- **Description:** Live outbox write rate / ledger append latency
|
||||||
|
- **Columns:** timestamp, contract_id, write_latency_ms, append_count
|
||||||
|
|
||||||
|
### placeholder_tamper_evident_checkpoints
|
||||||
|
- **Blocking decision:** D-083
|
||||||
|
- **Description:** Tamper-evident ledger checkpoints / JWS signature rate
|
||||||
|
- **Columns:** timestamp, checkpoint_id, jws_signed, object_lock_enabled
|
||||||
|
|
||||||
|
### placeholder_onboarding_funnel
|
||||||
|
- **Blocking decision:** D-113/D-114/D-119
|
||||||
|
- **Description:** Onboarding funnel: requested → granted conversion
|
||||||
|
- **Columns:** timestamp, consumer_repo, requested_environment, status, granted_at
|
||||||
|
|
||||||
|
### placeholder_drift_detection
|
||||||
|
- **Blocking decision:** D-096 + no scheduler
|
||||||
|
- **Description:** Drift detection (scheduled terraform plan -detailed-exitcode)
|
||||||
|
- **Columns:** timestamp, workspace_id, drift_count, auto_reverted, detection_cycle
|
||||||
|
|
||||||
|
### placeholder_live_cur_reconciliation
|
||||||
|
- **Blocking decision:** D-096
|
||||||
|
- **Description:** Live cost CUR reconciliation
|
||||||
|
- **Columns:** timestamp, resource_address, actual_usd, baseline_usd, saved_usd
|
||||||
|
|
||||||
|
### placeholder_sla_downtime
|
||||||
|
- **Blocking decision:** D-096
|
||||||
|
- **Description:** SLA / unplanned downtime
|
||||||
|
- **Columns:** timestamp, service, uptime_pct, downtime_minutes, slo_target
|
||||||
|
|
||||||
|
### placeholder_predictive_reactive
|
||||||
|
- **Blocking decision:** future emitter
|
||||||
|
- **Description:** Predictive vs Reactive ratio
|
||||||
|
- **Columns:** timestamp, action_id, label, trigger, count
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Nova Onboarding — Autonomous Request Path (v1.16, REQ-182..184)
|
||||||
|
|
||||||
|
The v1.16 milestone implements the **request path** of the autonomous
|
||||||
|
onboarding flow (D-113). A consumer can submit an onboarding request
|
||||||
|
without contacting the platform team; the platform generates an
|
||||||
|
environment binding + (in a future milestone) provisions the AWS resources.
|
||||||
|
|
||||||
|
## The 3-step request path
|
||||||
|
|
||||||
|
### Step 1 — Submit an onboarding request (P18, REQ-182)
|
||||||
|
|
||||||
|
A consumer submits an onboarding request to the Nova platform Lambda:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via the Lambda Function URL (IAM auth):
|
||||||
|
curl -X POST "$NOVA_LAMBDA_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"action": "onboard_consumer",
|
||||||
|
"consumerRepo": "acdl/my-app",
|
||||||
|
"requestedEnvironment": "dev",
|
||||||
|
"ownerId": "team-x",
|
||||||
|
"billingTag": "cost-center-x"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The Lambda validates the payload against
|
||||||
|
[`schemas/onboarding.schema.json`](../schemas/onboarding.schema.json),
|
||||||
|
then writes a `pending` row to the `nova-contracts` DynamoDB table
|
||||||
|
(D-119). No AWS resources are created by this action (D-113).
|
||||||
|
|
||||||
|
### Step 2 — Generate an environment binding (P19, REQ-183)
|
||||||
|
|
||||||
|
The platform (or the consumer locally) generates an environment binding
|
||||||
|
file from the request:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 core/onboarding.py --request '{
|
||||||
|
"consumerRepo": "acdl/my-app",
|
||||||
|
"requestedEnvironment": "qa",
|
||||||
|
"ownerId": "team-x",
|
||||||
|
"billingTag": "cost-center-x"
|
||||||
|
}' --out core/environments/qa.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces a `<env>.json` from the `dev.json` template, filling in
|
||||||
|
the `ownerId` + `billingTag` + a description. The `account_id` is a
|
||||||
|
placeholder (`000000000000`) for the platform team to fill with the real
|
||||||
|
account. The generated file validates against
|
||||||
|
[`schemas/environment.schema.json`](../schemas/environment.schema.json).
|
||||||
|
|
||||||
|
### Step 3 — Cross-account role + ABAC tag grant (P20, REQ-184)
|
||||||
|
|
||||||
|
The platform authors the consumer deploy-role + `nova:owner` ABAC tag
|
||||||
|
grant via Terraform:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd terraform/onboarding
|
||||||
|
terraform init -backend=false
|
||||||
|
terraform validate
|
||||||
|
NOVA_AWS_ACCOUNT_ID=123456789012 terraform plan \
|
||||||
|
-var consumer_repo=acdl/my-app \
|
||||||
|
-var owner_id=team-x
|
||||||
|
```
|
||||||
|
|
||||||
|
**Offline-proven only (D-114):** `terraform validate` + `terraform plan`
|
||||||
|
pass; **no live apply** in v1.16. The live apply (creating the real
|
||||||
|
cross-account role + OIDC trust) is deferred to a future feature
|
||||||
|
milestone (D-113).
|
||||||
|
|
||||||
|
## What is NOT automated (deferred)
|
||||||
|
|
||||||
|
- **Real AWS account/network/state provisioning** — the request path
|
||||||
|
generates a binding file with a placeholder `account_id`; the actual
|
||||||
|
AWS account creation + VPC + state backend is a future feature (D-113).
|
||||||
|
- **Live cross-account role apply** — the Terraform is offline-proven
|
||||||
|
only (D-114); live apply is deferred.
|
||||||
|
- **OIDC trust policy** — the onboarding Terraform uses a placeholder
|
||||||
|
OIDC provider; real OIDC federation is blocked on
|
||||||
|
upstream forge OIDC support (carries forward from v1.1).
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [`schemas/onboarding.schema.json`](../schemas/onboarding.schema.json) — the request schema
|
||||||
|
- [`core/onboarding.py`](../core/onboarding.py) — the env-file generator
|
||||||
|
- [`terraform/onboarding/`](../terraform/onboarding/) — the role-grant Terraform
|
||||||
|
- [`core/environments/README.md`](../core/environments/README.md) — environment binding docs
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
title: ACDL — Agentic Cloud Delivery Platform
|
title: Nova
|
||||||
description: Consumer + platform-engineer documentation for the ACDL platform.
|
description: Consumer + platform-engineer documentation for the Nova platform (formerly ACDL — Agentic Cloud Delivery Platform).
|
||||||
remote_theme: mmistakes/minimal-mistakes@9.0.4
|
remote_theme: mmistakes/minimal-mistakes@9.0.4
|
||||||
|
|
||||||
exclude:
|
exclude:
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ change to the modules/stack/confidence/audit.
|
|||||||
- A MAJOR bump requires a new registry entry (immutable publication); the
|
- A MAJOR bump requires a new registry entry (immutable publication); the
|
||||||
old entry enters a 12-month deprecation window.
|
old entry enters a 12-month deprecation window.
|
||||||
- The central deploy pipeline is referenced by a floating MAJOR + MINOR tag
|
- The central deploy pipeline is referenced by a floating MAJOR + MINOR tag
|
||||||
(e.g. `@v1.6`); patch fixes flow within the tag, breaking changes land
|
(e.g. `@v1.19`); patch fixes flow within the tag, breaking changes land
|
||||||
under the next MINOR tag.
|
under the next MINOR tag.
|
||||||
|
|
||||||
See [Versioning](pipeline/versioning) for the consumer-facing details.
|
See [Versioning](pipeline/versioning) for the consumer-facing details.
|
||||||
|
|||||||
+80
-44
@@ -1,15 +1,15 @@
|
|||||||
# Consumer Guide — Declare intent, deploy to AWS
|
# Consumer Guide — Declare intent, deploy to AWS
|
||||||
|
|
||||||
This guide walks a consumer through creating their pipeline and defining a
|
This guide walks a consumer through creating their pipeline and defining a
|
||||||
contract that deploys any ACDL module to AWS. It is **generic** across all
|
contract that deploys any Nova module to AWS. It is **generic** across all
|
||||||
modules in the registry; `static-assets` is the worked example, but every
|
modules in the registry; `static-assets` is the worked example, but every
|
||||||
step applies to `microservice` and any future module.
|
step applies to `microservice` and any future module.
|
||||||
|
|
||||||
## The model
|
## The model
|
||||||
|
|
||||||
Consumers have their own repos and consume ACDL by writing a contract
|
Consumers have their own repos and consume Nova by writing a contract
|
||||||
that declares infrastructure (one or more modules), an environment, and inputs. The consumer declares a **contract** (which infrastructure, which
|
that declares infrastructure (one or more modules), an environment, and inputs. The consumer declares a **contract** (which infrastructure, which
|
||||||
environment, which inputs); the ACDL platform owns the pipelines, modules,
|
environment, which inputs); the Nova platform owns the pipelines, modules,
|
||||||
engine adapter, and evidence stream.
|
engine adapter, and evidence stream.
|
||||||
|
|
||||||
You do not write infrastructure modules, workflow YAML, or adapter code.
|
You do not write infrastructure modules, workflow YAML, or adapter code.
|
||||||
@@ -19,7 +19,7 @@ definitions.
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
A["your repo<br/>(app code + contracts + CI definitions)"] -->|uses: acdl/.github/workflows/deploy.yml@v1.9| B
|
A["your repo<br/>(app code + contracts + CI definitions)"] -->|uses: nova/.github/workflows/deploy.yml@v1.19| B
|
||||||
B["platform runners<br/>(modules + pipelines + adapters + schemas)"] -->|contract -> resolver -> stack -> adapter<br/>-> security checks -> infrastructure plan -> policy checks<br/>-> confidence -> apply -> evidence event| C
|
B["platform runners<br/>(modules + pipelines + adapters + schemas)"] -->|contract -> resolver -> stack -> adapter<br/>-> security checks -> infrastructure plan -> policy checks<br/>-> confidence -> apply -> evidence event| C
|
||||||
C["your resources in AWS"]
|
C["your resources in AWS"]
|
||||||
```
|
```
|
||||||
@@ -27,13 +27,13 @@ flowchart LR
|
|||||||
## Versioning the `uses:` reference
|
## Versioning the `uses:` reference
|
||||||
|
|
||||||
The central deployment pipeline is **always versioned with floating MAJOR
|
The central deployment pipeline is **always versioned with floating MAJOR
|
||||||
and MINOR tags** (e.g. `acdl/pipelines/contract.yml@v1.9`). Version
|
and MINOR tags** (e.g. `nova/pipelines/contract.yml@v1.19`). Version
|
||||||
constraints cannot be expressed inside the contract, so the tag in
|
constraints cannot be expressed inside the contract, so the tag in
|
||||||
`uses:` is the only immutability lever a consumer has. See
|
`uses:` is the only immutability lever a consumer has. See
|
||||||
[Versioning](pipeline/versioning) for the full rationale.
|
[Versioning](pipeline/versioning) for the full rationale.
|
||||||
|
|
||||||
**Unversioned references are discouraged.** Do not use `@main` or a bare
|
**Unversioned references are discouraged.** Do not use `@main` or a bare
|
||||||
`acdl/pipelines/contract.yml`.
|
`nova/pipelines/contract.yml`.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -47,13 +47,13 @@ platform-managed. See [Environments](environments/).
|
|||||||
environment is bound, your first pipeline run emits a friendly onboarding
|
environment is bound, your first pipeline run emits a friendly onboarding
|
||||||
prompt. See [Environments](environments/).
|
prompt. See [Environments](environments/).
|
||||||
- **Authorization to reference the central pipeline.** Onboarding grants
|
- **Authorization to reference the central pipeline.** Onboarding grants
|
||||||
your repo the right to `uses: acdl/.github/workflows/deploy.yml@v1.9`.
|
your repo the right to `uses: nova/.github/workflows/deploy.yml@v1.19`.
|
||||||
Contact the platform team if you have not been onboarded.
|
Contact the platform team if you have not been onboarded.
|
||||||
|
|
||||||
## Step 1 — Create a consumer repo
|
## Step 1 — Create a consumer repo
|
||||||
|
|
||||||
Create a repository for your application. The top level holds your app
|
Create a repository for your application. The top level holds your app
|
||||||
code; your contract lives at `.acdl/contract.yml`. Example for a static
|
code; your contract lives at `.nova/contract.yml`. Example for a static
|
||||||
site:
|
site:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -62,7 +62,7 @@ my-static-site/
|
|||||||
assets/
|
assets/
|
||||||
style.css
|
style.css
|
||||||
logo.png
|
logo.png
|
||||||
.acdl/
|
.nova/
|
||||||
contract.yaml
|
contract.yaml
|
||||||
.github/
|
.github/
|
||||||
workflows/
|
workflows/
|
||||||
@@ -75,7 +75,7 @@ Example for a microservice:
|
|||||||
my-microservice/
|
my-microservice/
|
||||||
app.py
|
app.py
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.acdl/
|
.nova/
|
||||||
contract.yaml
|
contract.yaml
|
||||||
.github/
|
.github/
|
||||||
workflows/
|
workflows/
|
||||||
@@ -83,20 +83,20 @@ my-microservice/
|
|||||||
```
|
```
|
||||||
|
|
||||||
Your app code lives at the top level. Your contract lives at
|
Your app code lives at the top level. Your contract lives at
|
||||||
`.acdl/contract.yml` regardless of the module you deploy. Your CI
|
`.nova/contract.yml` regardless of the module you deploy. Your CI
|
||||||
definition lives at `.github/workflows/deploy.yml`.
|
definition lives at `.github/workflows/deploy.yml`.
|
||||||
|
|
||||||
## Step 2 — Reference the central pipeline
|
## Step 2 — Reference the central pipeline
|
||||||
|
|
||||||
In your CI workflow (`.github/workflows/deploy.yml`), reference the central
|
In your CI workflow (`.github/workflows/deploy.yml`), reference the central
|
||||||
ACDL deployment workflow with a **versioned tag** (floating MAJOR + MINOR):
|
Nova deployment workflow with a **versioned tag** (floating MAJOR + MINOR):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
uses: acdl/.github/workflows/deploy.yml@v1.9
|
uses: nova/.github/workflows/deploy.yml@v1.19
|
||||||
with:
|
with:
|
||||||
contract: .acdl/contract.yml
|
contract: .nova/contract.yml
|
||||||
environment: dev
|
environment: dev
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ field; the version pin lives in the CI workflow reference.
|
|||||||
|
|
||||||
## Step 3 — Define the contract
|
## Step 3 — Define the contract
|
||||||
|
|
||||||
Write `.acdl/contract.yml`. The `static-assets` example:
|
Write `.nova/contract.yml`. The `static-assets` example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment: dev
|
environment: dev
|
||||||
@@ -140,10 +140,10 @@ name: microservice
|
|||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|-------|------|----------|-------------|
|
|-------|------|----------|-------------|
|
||||||
| `uses` | string | yes | Reference to the central deployment pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `acdl/pipelines/contract.yml@v1.9`). Bare or `@main` references are discouraged. See [Versioning](pipeline/versioning). |
|
| `id` | string | yes | Short operational acronym (3-6 chars, lowercase + digits + hyphens). Becomes `stack.name`: the Terraform state key (`spike/<id>/<env>/terraform.tfstate`), the outbox event identity, and the resource naming prefix. Stable across deploys and environment promotions. |
|
||||||
| `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-assets`, `microservice`, `s3`). See the [module catalog](modules/). |
|
| `name` | string | yes | Full human-readable stack name. Becomes `stack.title`: the display name in PR comments, evidence records, and dashboards. |
|
||||||
| `environment` | string | yes | The platform-managed environment to deploy to (e.g. `dev`). See [Environments](environments/). |
|
| `environment` | string | yes | The platform-managed environment to deploy to (`dev`, `qa`, `prod`, or `dr`). See [Environments](environments/). |
|
||||||
| `inputs` | object | yes | Module-specific inputs (see the module's README). |
|
| `infrastructure` | object | yes | Map of modules to deploy, keyed by module name (matching a registry key in `modules/registry.json`). Each entry carries an optional `version` (defaults to latest published) and per-module `inputs`. One entry = single-module deploy; N entries = multi-module manifest. |
|
||||||
|
|
||||||
### Module inputs
|
### Module inputs
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ and execute for you.
|
|||||||
|
|
||||||
### The consumer CI definition
|
### The consumer CI definition
|
||||||
|
|
||||||
Add a thin workflow file to **your** repo that invokes the reusable ACDL
|
Add a thin workflow file to **your** repo that invokes the reusable Nova
|
||||||
deploy workflow with a **versioned tag** (`.github/workflows/deploy.yml`):
|
deploy workflow with a **versioned tag** (`.github/workflows/deploy.yml`):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -177,22 +177,23 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
uses: acdl/.github/workflows/deploy.yml@v1.9
|
uses: nova/.github/workflows/deploy.yml@v1.19
|
||||||
with:
|
with:
|
||||||
contract: .acdl/contract.yml
|
contract: .nova/contract.yml
|
||||||
|
environment: dev
|
||||||
```
|
```
|
||||||
|
|
||||||
That is the entire consumer-side workflow. When you push to `main`:
|
That is the entire consumer-side workflow. When you push to `main`:
|
||||||
|
|
||||||
1. The platform runner resolves `uses: acdl/.github/workflows/deploy.yml@v1.9`
|
1. The platform runner resolves `uses: nova/.github/workflows/deploy.yml@v1.19`
|
||||||
to the reusable workflow **at the pinned tag**.
|
to the reusable workflow **at the pinned tag**.
|
||||||
2. A **platform-provided runner** checks out **your** repo.
|
2. A **platform-provided runner** checks out **your** repo.
|
||||||
3. The runner checks out the **ACDL platform repo** into the workspace —
|
3. The runner checks out the **Nova platform repo** into the workspace —
|
||||||
this is how the pipeline fetches the platform code at run time. You
|
this is how the pipeline fetches the platform code at run time. You
|
||||||
never clone the platform repo yourself.
|
never clone the platform repo yourself.
|
||||||
4. The runner installs the runtime dependencies the platform requires.
|
4. The runner installs the runtime dependencies the platform requires.
|
||||||
5. The runner invokes `scripts/run_platform.sh` against your
|
5. The runner invokes `scripts/run_platform.sh` against your
|
||||||
`.acdl/contract.yml`.
|
`.nova/contract.yml`.
|
||||||
|
|
||||||
You see the streamed output (infrastructure plan, policy-check results,
|
You see the streamed output (infrastructure plan, policy-check results,
|
||||||
confidence signal) in your run logs. The `--check-only` and `--plan-only`
|
confidence signal) in your run logs. The `--check-only` and `--plan-only`
|
||||||
@@ -203,7 +204,7 @@ hold for attestation).
|
|||||||
|
|
||||||
### Local validation (optional)
|
### Local validation (optional)
|
||||||
|
|
||||||
A consumer *may* clone the ACDL platform repo to run `--check-only` against
|
A consumer *may* clone the Nova platform repo to run `--check-only` against
|
||||||
their contract before pushing — this is optional and not required for the
|
their contract before pushing — this is optional and not required for the
|
||||||
happy path. If you do this, the runtime dependencies must be installed
|
happy path. If you do this, the runtime dependencies must be installed
|
||||||
locally, and any AWS credentials follow the
|
locally, and any AWS credentials follow the
|
||||||
@@ -213,7 +214,7 @@ static key in `.env.secrets` (gitignored) is rotated **out of band by you**
|
|||||||
locally-held copies.
|
locally-held copies.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/run_platform.sh --check-only path/to/your/.acdl/contract.yml
|
bash scripts/run_platform.sh --check-only path/to/your/.nova/contract.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 5 — What the pipeline does
|
## Step 5 — What the pipeline does
|
||||||
@@ -229,7 +230,7 @@ flowchart TD
|
|||||||
S5["policy checks<br/>(adapter -> PolicyCheckResult)"] --> S6
|
S5["policy checks<br/>(adapter -> PolicyCheckResult)"] --> S6
|
||||||
S6["confidence<br/>score + band (dev >= 0.50)"] --> S7
|
S6["confidence<br/>score + band (dev >= 0.50)"] --> S7
|
||||||
S7["evidence event<br/>to the audit outbox"] --> S8
|
S7["evidence event<br/>to the audit outbox"] --> S8
|
||||||
S8["infrastructure apply<br/>(dev only)"]
|
S8["infrastructure apply<br/>(autonomous in dev;<br/>higher envs apply after HITL)"]
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **validate-contract** — validates your contract YAML against the contract
|
1. **validate-contract** — validates your contract YAML against the contract
|
||||||
@@ -250,9 +251,10 @@ flowchart TD
|
|||||||
threshold is ≥ 0.50. If the band is `pass`, the pipeline proceeds.
|
threshold is ≥ 0.50. If the band is `pass`, the pipeline proceeds.
|
||||||
7. **evidence event** — a hash-chained evidence event is written to the
|
7. **evidence event** — a hash-chained evidence event is written to the
|
||||||
audit outbox.
|
audit outbox.
|
||||||
8. **infrastructure apply** (dev only) — the infrastructure plan is applied,
|
8. **infrastructure apply** (autonomous in dev; higher environments apply
|
||||||
creating the resources in your AWS account. An evidence event for the
|
after HITL attestation) — the infrastructure plan is applied, creating
|
||||||
apply is recorded.
|
the resources in your AWS account. An evidence event for the apply is
|
||||||
|
recorded.
|
||||||
|
|
||||||
## Step 6 — What gets created
|
## Step 6 — What gets created
|
||||||
|
|
||||||
@@ -289,7 +291,14 @@ push your container image to the ECR repo the platform created.
|
|||||||
|
|
||||||
## Step 8 — Promote to qa / prod
|
## Step 8 — Promote to qa / prod
|
||||||
|
|
||||||
Change `environment` in your contract (the infrastructure stays the same):
|
There are **two supported promotion shapes**. Both are valid; pick the one
|
||||||
|
that fits your repo's workflow.
|
||||||
|
|
||||||
|
### Shape A — edit the environment field (destroy-then-rebuild)
|
||||||
|
|
||||||
|
Change `environment` in your contract (the infrastructure stays the same).
|
||||||
|
The contract `id` stays stable, so the platform knows this is the same
|
||||||
|
stack moving to a new environment:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
id: assets
|
id: assets
|
||||||
@@ -301,10 +310,32 @@ infrastructure:
|
|||||||
inputs: { ... }
|
inputs: { ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**What happens when you change `environment: dev` → `environment: qa`:**
|
||||||
|
the platform detects that the environment changed on a known contract `id`.
|
||||||
|
Before building the new environment, it **destroys the prior environment's
|
||||||
|
resources** (Terraform state key `spike/{id}/dev/`) and records an evidence
|
||||||
|
event for the destroy. Only then does it apply the new environment (state
|
||||||
|
key `spike/{id}/qa/`). **There is no orphan path** — if the destroy fails,
|
||||||
|
the pipeline fails closed (no apply runs, no resources are left behind).
|
||||||
|
This is full lifecycle management: the platform never creates a state
|
||||||
|
where prior-environment resources are abandoned.
|
||||||
|
|
||||||
Higher environments require human attestation (a platform-runner deployment
|
Higher environments require human attestation (a platform-runner deployment
|
||||||
approval) and higher confidence thresholds. See [Environments](environments/)
|
approval) and higher confidence thresholds. See [Environments](environments/)
|
||||||
for the full table.
|
for the full table.
|
||||||
|
|
||||||
|
> **Note:** the destroy-then-rebuild runs within the same AWS account (the
|
||||||
|
> current platform scaffold uses one account). Cross-account promotion
|
||||||
|
> (separate accounts per env) is a future milestone.
|
||||||
|
|
||||||
|
### Shape B — per-environment caller workflows (no editing)
|
||||||
|
|
||||||
|
Alternatively, keep one contract per environment (or one contract + the
|
||||||
|
`environment` workflow input) and run the matching CI job to promote. This
|
||||||
|
avoids the destroy step because each environment has its own state from the
|
||||||
|
first deploy. See [Per-environment deployment](#per-environment-deployment)
|
||||||
|
below for the full pattern.
|
||||||
|
|
||||||
## Step 9 — Compliance extensions
|
## Step 9 — Compliance extensions
|
||||||
|
|
||||||
Each module lists compliance extension points for the future compliance
|
Each module lists compliance extension points for the future compliance
|
||||||
@@ -326,8 +357,8 @@ per-module extension points. Common examples:
|
|||||||
| Contract schema | `schemas/contract.schema.json` | JSON Schema for consumer contracts. |
|
| Contract schema | `schemas/contract.schema.json` | JSON Schema for consumer contracts. |
|
||||||
| Stack schema | `schemas/stack.schema.json` | JSON Schema for the resolved stack instance. |
|
| Stack schema | `schemas/stack.schema.json` | JSON Schema for the resolved stack instance. |
|
||||||
| Module catalog | [modules/](modules/) | All primitives and modules. |
|
| Module catalog | [modules/](modules/) | All primitives and modules. |
|
||||||
| Sample contract | `contracts/static-assets.yaml` | The reference example contract (uses `@v1.9`). |
|
| Sample contract | `contracts/static-assets.yml` | The reference example contract (used with caller workflow `@v1.19`). |
|
||||||
| Sample contract | `contracts/microservice.yaml` | The microservice example contract (uses `@v1.9`). |
|
| Sample contract | `contracts/microservice.yml` | The microservice example contract (used with caller workflow `@v1.19`). |
|
||||||
| Module examples | `modules/<name>/examples/` | Validated per-module example contracts (`simple.yaml` + `complex.yaml`). |
|
| Module examples | `modules/<name>/examples/` | Validated per-module example contracts (`simple.yaml` + `complex.yaml`). |
|
||||||
| Contract resolver | `core/contract_resolver.py` | Resolves contracts to stack instances. |
|
| Contract resolver | `core/contract_resolver.py` | Resolves contracts to stack instances. |
|
||||||
| Angine adapter | `adapters/terraform/adapter.py` | Compiles stack instances to infrastructure. |
|
| Angine adapter | `adapters/terraform/adapter.py` | Compiles stack instances to infrastructure. |
|
||||||
@@ -345,7 +376,7 @@ process is a 2-step pipeline with **HITL SRE gates** to prevent accidental
|
|||||||
destruction:
|
destruction:
|
||||||
|
|
||||||
1. **Request a change request (CR):** Contact the platform team to create a
|
1. **Request a change request (CR):** Contact the platform team to create a
|
||||||
change request in the platform CMDB (DynamoDB `acdl-change-requests`
|
change request in the platform CMDB (DynamoDB `nova-change-requests`
|
||||||
table). The CR must be approved before decommission can proceed. The CR
|
table). The CR must be approved before decommission can proceed. The CR
|
||||||
includes the consumer repo, contract ID, and the reason for decommission.
|
includes the consumer repo, contract ID, and the reason for decommission.
|
||||||
|
|
||||||
@@ -353,9 +384,9 @@ destruction:
|
|||||||
use `mode: decommission` with the `changeRequestId` input:
|
use `mode: decommission` with the `changeRequestId` input:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
uses: acdl/.github/workflows/deploy.yml@v1.8
|
uses: nova/.github/workflows/deploy.yml@v1.19
|
||||||
with:
|
with:
|
||||||
contract: .acdl/contract.yml
|
contract: .nova/contract.yml
|
||||||
mode: decommission
|
mode: decommission
|
||||||
changeRequestId: "CHG0678912"
|
changeRequestId: "CHG0678912"
|
||||||
```
|
```
|
||||||
@@ -395,7 +426,12 @@ separately (or left running to monitor the decommissioned stack's
|
|||||||
endpoints going dark).
|
endpoints going dark).
|
||||||
## Per-environment deployment
|
## Per-environment deployment
|
||||||
|
|
||||||
ACDL supports a **promotion-without-editing** model: you do not edit the
|
> **This is Shape B** (the alternative to [Shape A's edit-and-destroy
|
||||||
|
> path](#step-8--promote-to-qa--prod) in Step 8). Shape B avoids the
|
||||||
|
> destroy step because each environment has its own state from the first
|
||||||
|
> deploy — no prior environment to tear down.
|
||||||
|
|
||||||
|
Nova supports a **promotion-without-editing** model: you do not edit the
|
||||||
`environment:` field in a contract to promote dev → qa → prod → dr.
|
`environment:` field in a contract to promote dev → qa → prod → dr.
|
||||||
Instead, there is **one CI job per environment**, each pointing at its
|
Instead, there is **one CI job per environment**, each pointing at its
|
||||||
respective contract (or the same contract + the `environment` workflow
|
respective contract (or the same contract + the `environment` workflow
|
||||||
@@ -404,8 +440,8 @@ input). Promotion = running the matching job.
|
|||||||
### Two shapes (both supported)
|
### Two shapes (both supported)
|
||||||
|
|
||||||
**Shape 1 — per-environment contract files:** a consumer repo has one
|
**Shape 1 — per-environment contract files:** a consumer repo has one
|
||||||
contract per environment (e.g. `.acdl/static-assets.dev.yml`,
|
contract per environment (e.g. `.nova/static-assets.dev.yml`,
|
||||||
`.acdl/static-assets.qa.yml`, …). Each sets `environment:` to its own
|
`.nova/static-assets.qa.yml`, …). Each sets `environment:` to its own
|
||||||
name and uses interpolation so env-specific values differ automatically:
|
name and uses interpolation so env-specific values differ automatically:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -421,7 +457,7 @@ name: static-assets
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Shape 2 — single contract + `environment` workflow input:** the
|
**Shape 2 — single contract + `environment` workflow input:** the
|
||||||
reusable deploy workflow (`acdl/.github/workflows/deploy.yml@v1.9`)
|
reusable deploy workflow (`nova/.github/workflows/deploy.yml@v1.19`)
|
||||||
declares an `environment` input. When non-empty, it overrides the
|
declares an `environment` input. When non-empty, it overrides the
|
||||||
contract's `environment` field at load time (before interpolation), so
|
contract's `environment` field at load time (before interpolation), so
|
||||||
the same contract can be promoted by passing a different environment:
|
the same contract can be promoted by passing a different environment:
|
||||||
@@ -436,10 +472,10 @@ on: workflow_dispatch:
|
|||||||
required: true
|
required: true
|
||||||
jobs:
|
jobs:
|
||||||
deploy-qa:
|
deploy-qa:
|
||||||
uses: acdl/.github/workflows/deploy.yml@v1.9
|
uses: nova/.github/workflows/deploy.yml@v1.19
|
||||||
with:
|
with:
|
||||||
environment: qa
|
environment: qa
|
||||||
contract: .acdl/contract.yml
|
contract: .nova/contract.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
### One job per environment
|
### One job per environment
|
||||||
@@ -467,7 +503,7 @@ duties check blocks a prod promotion when `approver_qa == approver_prod`
|
|||||||
| `${env.environment}` | the environment name (dev/qa/prod/dr) | `qa` |
|
| `${env.environment}` | the environment name (dev/qa/prod/dr) | `qa` |
|
||||||
| `${env.region}` | the environment's AWS region | `us-east-1` |
|
| `${env.region}` | the environment's AWS region | `us-east-1` |
|
||||||
| `${env.account_id}` | the environment's AWS account id | `123456789012` |
|
| `${env.account_id}` | the environment's AWS account id | `123456789012` |
|
||||||
| `${env.state_backend.bucket}` | the environment's state bucket | `acdl-qa-state` |
|
| `${env.state_backend.bucket}` | the environment's state bucket | `nova-qa-state` |
|
||||||
| `${env.network.vpc_cidr}` | the environment's VPC CIDR | `10.1.0.0/16` |
|
| `${env.network.vpc_cidr}` | the environment's VPC CIDR | `10.1.0.0/16` |
|
||||||
| `${contract.id}` | the contract's operational acronym | `assets` |
|
| `${contract.id}` | the contract's operational acronym | `assets` |
|
||||||
| `${contract.environment}` | the contract's environment field | `qa` |
|
| `${contract.environment}` | the contract's environment field | `qa` |
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ deploys it.
|
|||||||
|
|
||||||
## The contract file
|
## The contract file
|
||||||
|
|
||||||
A consumer repo keeps its contract at `.acdl/contract.yml`. A minimal
|
A consumer repo keeps its contract at `.nova/contract.yml`. A minimal
|
||||||
example (the `static-assets` module):
|
example (the `static-assets` module):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -57,7 +57,7 @@ infrastructure:
|
|||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
The contract is validated against
|
The contract is validated against
|
||||||
[`schemas/contract.schema.json`](https://github.com/acdl/acdl/blob/main/schemas/contract.schema.json).
|
[`schemas/contract.schema.json`](https://github.com/nova/nova/blob/main/schemas/contract.schema.json).
|
||||||
An invalid contract (missing field, unknown module, wrong type) fails at the
|
An invalid contract (missing field, unknown module, wrong type) fails at the
|
||||||
validate-contract stage with a clear error.
|
validate-contract stage with a clear error.
|
||||||
|
|
||||||
@@ -65,9 +65,9 @@ validate-contract stage with a clear error.
|
|||||||
|
|
||||||
Two reference examples exist in `contracts/`:
|
Two reference examples exist in `contracts/`:
|
||||||
|
|
||||||
- [`contracts/static-assets.yml`](https://github.com/acdl/acdl/blob/main/contracts/static-assets.yml)
|
- [`contracts/static-assets.yml`](https://github.com/nova/nova/blob/main/contracts/static-assets.yml)
|
||||||
— the `static-assets` module.
|
— the `static-assets` module.
|
||||||
- [`contracts/microservice.yml`](https://github.com/acdl/acdl/blob/main/contracts/microservice.yml)
|
- [`contracts/microservice.yml`](https://github.com/nova/nova/blob/main/contracts/microservice.yml)
|
||||||
— the `microservice` module.
|
— the `microservice` module.
|
||||||
|
|
||||||
Additionally, every module has a `modules/<name>/examples/` directory with
|
Additionally, every module has a `modules/<name>/examples/` directory with
|
||||||
|
|||||||
@@ -56,13 +56,13 @@ threshold. Staging does not exist.
|
|||||||
## Cross-account contract ingestion grant (D-051)
|
## Cross-account contract ingestion grant (D-051)
|
||||||
|
|
||||||
Onboarding now also grants the consumer repo's deploy role permission to
|
Onboarding now also grants the consumer repo's deploy role permission to
|
||||||
invoke the **platform Lambda** — `acdl-contract-ingestor` — across
|
invoke the **platform Lambda** — `nova-contract-ingestor` — across
|
||||||
accounts. The Lambda is invoked via a Function URL with IAM auth, so the
|
accounts. The Lambda is invoked via a Function URL with IAM auth, so the
|
||||||
grant is an inline IAM policy applied to the consumer's deploy role. The
|
grant is an inline IAM policy applied to the consumer's deploy role. The
|
||||||
policy template lives at
|
policy template lives at
|
||||||
[`terraform/platform/consumer_invoke_policy.json`](https://github.com/acdl/acdl/blob/main/terraform/platform/consumer_invoke_policy.json)
|
[`terraform/platform/consumer_invoke_policy.json`](https://github.com/nova/nova/blob/main/terraform/platform/consumer_invoke_policy.json)
|
||||||
and is scoped via **ABAC**: the condition
|
and is scoped via **ABAC**: the condition
|
||||||
`aws:PrincipalTag/acdl:owner == ${consumerRepo}` ensures a repo can only
|
`aws:PrincipalTag/nova:owner == ${consumerRepo}` ensures a repo can only
|
||||||
invoke the Lambda when its principal tag matches its claimed identity.
|
invoke the Lambda when its principal tag matches its claimed identity.
|
||||||
|
|
||||||
The consumer's deploy workflow signs the Function URL request with
|
The consumer's deploy workflow signs the Function URL request with
|
||||||
@@ -75,7 +75,7 @@ is used for two purposes:
|
|||||||
|
|
||||||
1. **Contract ingestion** — the consumer submits its resolved deployment
|
1. **Contract ingestion** — the consumer submits its resolved deployment
|
||||||
contract (`action: "submit_contract"`) so the platform has a durable
|
contract (`action: "submit_contract"`) so the platform has a durable
|
||||||
record in the `acdl-contracts` DynamoDB table (PK `consumerRepo`, SK
|
record in the `nova-contracts` DynamoDB table (PK `consumerRepo`, SK
|
||||||
`contractId#submittedAt`).
|
`contractId#submittedAt`).
|
||||||
2. **Error reporting** (D-055) — the consumer reports a deployment error
|
2. **Error reporting** (D-055) — the consumer reports a deployment error
|
||||||
(`action: "report_error"`) which the platform turns into a GitHub
|
(`action: "report_error"`) which the platform turns into a GitHub
|
||||||
@@ -83,16 +83,16 @@ is used for two purposes:
|
|||||||
prepared-status stub until then).
|
prepared-status stub until then).
|
||||||
|
|
||||||
The Lambda handler and the Terraform that deploys it live in
|
The Lambda handler and the Terraform that deploys it live in
|
||||||
[`core/lambda/contract_ingestor.py`](https://github.com/acdl/acdl/blob/main/core/lambda/contract_ingestor.py)
|
[`core/lambda/contract_ingestor.py`](https://github.com/nova/nova/blob/main/core/lambda/contract_ingestor.py)
|
||||||
and
|
and
|
||||||
[`terraform/platform/main.tf`](https://github.com/acdl/acdl/blob/main/terraform/platform/main.tf)
|
[`terraform/platform/main.tf`](https://github.com/nova/nova/blob/main/terraform/platform/main.tf)
|
||||||
respectively.
|
respectively.
|
||||||
|
|
||||||
## Onboarding scaffold (current state)
|
## Onboarding scaffold (current state)
|
||||||
|
|
||||||
The platform repo ships a minimal onboarding scaffold:
|
The platform repo ships a minimal onboarding scaffold:
|
||||||
|
|
||||||
- [`core/environments/`](https://github.com/acdl/acdl/blob/main/core/environments/)
|
- [`core/environments/`](https://github.com/nova/nova/blob/main/core/environments/)
|
||||||
— environment definitions (a sample `dev.json`).
|
— environment definitions (a sample `dev.json`).
|
||||||
- `core/environment_check.py` — checks whether an environment is defined for
|
- `core/environment_check.py` — checks whether an environment is defined for
|
||||||
a given contract's repo + environment name; prints the friendly onboarding
|
a given contract's repo + environment name; prints the friendly onboarding
|
||||||
|
|||||||
+6
-4
@@ -1,4 +1,6 @@
|
|||||||
# ACDL — Agentic Cloud Delivery Platform
|
# Nova
|
||||||
|
|
||||||
|
> **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
|
||||||
@@ -9,14 +11,14 @@ a configuration file, or an infrastructure module.
|
|||||||
|
|
||||||
## Two repositories
|
## Two repositories
|
||||||
|
|
||||||
There are two kinds of repository in the ACDL model:
|
There are two kinds of repository in the Nova model:
|
||||||
|
|
||||||
- **Platform repo (this one).** The source code of the platform. It owns
|
- **Platform repo (this one).** The source code of the platform. It owns
|
||||||
`modules/`, `adapters/`, `core/`, `schemas/`, `pipelines/`, `scripts/`,
|
`modules/`, `adapters/`, `core/`, `schemas/`, `pipelines/`, `scripts/`,
|
||||||
and the reusable workflow files. Platform engineers work here. A consumer
|
and the reusable workflow files. Platform engineers work here. A consumer
|
||||||
never clones it.
|
never clones it.
|
||||||
- **Consumer repo (yours).** A consumer repo contains only its application
|
- **Consumer repo (yours).** A consumer repo contains only its application
|
||||||
code, one or more contracts (`.acdl/contract.yml`), and one or more CI
|
code, one or more contracts (`.nova/contract.yml`), and one or more CI
|
||||||
definitions (a thin `.github/workflows/deploy.yml` that `uses:` the central
|
definitions (a thin `.github/workflows/deploy.yml` that `uses:` the central
|
||||||
reusable workflow, pointing at the appropriate environment + contract).
|
reusable workflow, pointing at the appropriate environment + contract).
|
||||||
The consumer does not write infrastructure modules, workflow YAML, or
|
The consumer does not write infrastructure modules, workflow YAML, or
|
||||||
@@ -75,4 +77,4 @@ Planned future features (no dates; tracked in the internal roadmap):
|
|||||||
|
|
||||||
- [Consumer Guide](consumer-guide) — start here if you are a consumer.
|
- [Consumer Guide](consumer-guide) — start here if you are a consumer.
|
||||||
- [Architecture](architecture) — start here if you are a platform engineer.
|
- [Architecture](architecture) — start here if you are a platform engineer.
|
||||||
- The [README](https://github.com/acdl/acdl) describes the platform repo.
|
- The [README](https://github.com/nova/nova) describes the platform repo.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# AI Decision Accuracy — Definition of Success
|
||||||
|
|
||||||
|
> KPI: AI Decision Accuracy
|
||||||
|
> Target: ≥ 99.5% (no rollback, no follow-up incident within 5 min of action)
|
||||||
|
|
||||||
|
**What this number means:** the percentage of AI decisions (confidence-
|
||||||
|
gated policy engine outcomes) that were NOT followed by an apply failure
|
||||||
|
or incident within 5 minutes. A high-confidence decision that later
|
||||||
|
caused an incident does NOT count as accurate.
|
||||||
|
|
||||||
|
**How it's computed:** `count(decisions WHERE outcome = 'succeeded' AND
|
||||||
|
no incident within 5min)` ÷ `total decisions`. Correlation via
|
||||||
|
`decision_id` → `run_id` → subsequent `apply.failed` or `incident.detected`
|
||||||
|
events.
|
||||||
|
|
||||||
|
**What "good" looks like:** ≥ 99.5% means fewer than 1 in 200 decisions
|
||||||
|
cause a secondary failure. The 0.5% allowance is for novel edge cases.
|
||||||
|
|
||||||
|
**D-122 honesty:** Nova's "AI" is the confidence-gated policy engine
|
||||||
|
(confidence_signal + HITL gate), not an LLM planner. The Decision Ledger
|
||||||
|
captures this real decision path — not a fabricated "AI agent."
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Attestation Coverage — Definition of Success
|
||||||
|
|
||||||
|
> KPI: Attestation Coverage
|
||||||
|
> Target: 100% of prod/dr promotions attested by a human
|
||||||
|
|
||||||
|
**What this number means:** every production and disaster-recovery
|
||||||
|
promotion has a recorded human attestation (approver identity, 8-concern
|
||||||
|
matrix result, separation-of-duties check on prod). This is the
|
||||||
|
"autonomy in operations, human in accountability" proof.
|
||||||
|
|
||||||
|
**How it's computed:** `count(prod/dr promotions with attestation.recorded
|
||||||
|
event) ÷ count(total prod/dr promotions)`. Sourced from the Decision
|
||||||
|
Ledger (`attestation.recorded` events) + `hitl_gates.py` + outbox
|
||||||
|
`approver_*` attributes.
|
||||||
|
|
||||||
|
**What "good" looks like:** 100% means no prod/dr promotion ever lands
|
||||||
|
without a human sign-off on record. The absence of an operator is never
|
||||||
|
the absence of a record (NORTH_STAR Anti-Goal #3).
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Confidence-Gate Halt Rate — Definition of Success
|
||||||
|
|
||||||
|
> KPI: Confidence-Gate Halt Rate
|
||||||
|
> Target: not a committed target (operational signal)
|
||||||
|
|
||||||
|
**What this number means:** how often the confidence gate itself halted
|
||||||
|
a run (band = block), independent of HITL blocks. The gate is the AI's
|
||||||
|
self-halt; HITL is the human gate. This distinguishes the AI's
|
||||||
|
self-regulation from human escalation.
|
||||||
|
|
||||||
|
**How it's computed:** `count(runs WHERE confidence_band = 'block')` ÷
|
||||||
|
`total runs`.
|
||||||
|
|
||||||
|
**What "good" looks like:** a low but non-zero rate means the gate is
|
||||||
|
working (catching genuinely uncertain runs) without being overly
|
||||||
|
conservative (blocking everything).
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Cost Savings via Infracost Estimates — Definition of Success
|
||||||
|
|
||||||
|
> KPI: Cost Savings via Infracost Estimates
|
||||||
|
> Target: ≥ 25% on pilot estates (partial)
|
||||||
|
|
||||||
|
**What this number means:** the pre-apply cost estimate from Infracost
|
||||||
|
shows the delta between the planned infrastructure and the current
|
||||||
|
state. Negative deltas = savings.
|
||||||
|
|
||||||
|
**How it's computed:** `sum(fact_cost_estimate.delta_usd WHERE delta < 0)`
|
||||||
|
per period.
|
||||||
|
|
||||||
|
**What's grounded:** the pre-apply estimate (Infracost reads plan JSON,
|
||||||
|
offline).
|
||||||
|
|
||||||
|
**What's deferred:** actual-spend reconciliation from AWS CUR (D-096 —
|
||||||
|
needs live AWS billing). The placeholder view
|
||||||
|
`placeholder_live_cur_reconciliation.csv` has the schema ready.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user