Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81f111d462 | |||
| 79e7a4a304 | |||
| 8ae307affc | |||
| 6e1a1bd7db | |||
| 040abc0fb7 | |||
| 71bd61ceb1 |
+5
-313
@@ -1,8 +1,8 @@
|
||||
# Nova — Architecture (v1.1 target)
|
||||
# ACDL — Architecture (v1.1 target)
|
||||
|
||||
> Target architecture for the real Agentic Cloud Delivery Platform (rebranded
|
||||
> Nova in v1.15). Source of truth for **how**: `docs/architecture.md` (v0.2) is the upstream
|
||||
> draft; this file is the Nova-repo operating copy, refined at phase
|
||||
> Target architecture for the real Agentic Cloud Delivery Platform.
|
||||
> Source of truth for **how**: `docs/architecture.md` (v0.2) is the upstream
|
||||
> draft; this file is the ACDL-repo operating copy, refined at phase
|
||||
> boundaries. Where this file and `docs/vision.md` conflict, the vision wins.
|
||||
|
||||
## Status
|
||||
@@ -570,312 +570,4 @@ emulator + live-AWS terraform init/validate/plan.
|
||||
7. CloudFront OAC + WAF deprecated arg names (AWS provider v5):
|
||||
`signing_behavior`, `signing_protocol`, `origin_access_control_id`,
|
||||
`s3_origin_config.origin_access_identity`, `origin_id`, `rule`
|
||||
(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.
|
||||
(singular), `scope=CLOUDFRONT` (uppercase).
|
||||
+2
-309
@@ -1,4 +1,4 @@
|
||||
# Nova v1.9 — Audit Report
|
||||
# ACDL v1.9 — Audit Report
|
||||
|
||||
> Audit date: 2026-07-23. Auditor: ci-debugger. Milestone: v1.9. Result: PASS.
|
||||
|
||||
@@ -243,311 +243,4 @@ Compared with `.ciagent/` files:
|
||||
added a v1.10 addendum section covering all 4 new subsystems + the
|
||||
7 adapter defect fixes. Verified all v1.10 components now referenced.
|
||||
|
||||
## 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---
|
||||
## Audit result: PASS
|
||||
@@ -1,66 +0,0 @@
|
||||
# Nova — The Autonomous Cloud Delivery Platform: Autonomy Defensibility Brief
|
||||
|
||||
> Strategic direction, leadership metrics & unified story
|
||||
> Last refined: v1.21 — reframe from "no-humans" to "autonomous operations"
|
||||
|
||||
## The thesis
|
||||
|
||||
Nova is the autonomous infrastructure layer that lets product teams
|
||||
ship without engaging an operator, and lets executives trust the
|
||||
platform not because it never fails but because every decision is
|
||||
captured, scored, and accountable.
|
||||
|
||||
**Autonomy in operations; human at stage gates.** Normal operations —
|
||||
provisioning, healing, remediation — run without an operator in the
|
||||
loop. Human attestation remains required at stage gates: QA signs off
|
||||
for production, SRE greenlights based on operational readiness. The
|
||||
absence of an operator in the loop is never the absence of a record.
|
||||
|
||||
## Grounded proof (measurable today)
|
||||
|
||||
| Proof | Source | Status |
|
||||
|-------|--------|--------|
|
||||
| Capabilities verified, none broken (live-AWS caps honestly skipped, resources torn down to zero-cost steady state) | regression report | grounded |
|
||||
| Decision Ledger captures 100% of automated decisions with outcome backfill | decision ledger store | grounded |
|
||||
| Attestation coverage: 100% of prod/dr promotions attested by a human | attestation gates + outbox | grounded |
|
||||
| Confidence-gated policy engine (deterministic, not an LLM) — weighted inputs, band outcome | confidence signal | grounded |
|
||||
| Attestation matrix with separation-of-duties on prod | attestation matrix + separation-of-duties | grounded |
|
||||
| Pre-apply cost estimates (offline) | cost adapter | grounded |
|
||||
| Test suite passes | test results | grounded |
|
||||
|
||||
## Deferred proof (measurable when blocking work lifts)
|
||||
|
||||
| Proof | Blocking work | Unblock requirement |
|
||||
|-------|----------------|---------------------|
|
||||
| Touchless resolution rate across production estates | 0 consumers today | Pilot estate activation |
|
||||
| Live infrastructure health (ECS, ALB, RPS) | Live AWS torn down | Live AWS re-provisioning |
|
||||
| Onboarding funnel: requested → granted | Auto-grant not built | Auto-grant implementation |
|
||||
| Drift auto-reversal rate | No drift scheduler | Drift detection scheduler |
|
||||
| Predictive vs reactive ratio | No emitter | ML anomaly-forecasting service |
|
||||
| Tamper-evident ledger checkpoints (S3 Object Lock + JWS) | Audit ledger build-out | Audit ledger build-out |
|
||||
|
||||
## Anti-claims (what Nova is NOT)
|
||||
|
||||
1. **Nova's decisions are NOT made by an LLM.** They are made by a
|
||||
confidence-gated policy engine: deterministic scripts calculate a
|
||||
score, and a band outcome gates the action. The platform functions
|
||||
without AI. The Decision Ledger captures this real decision path —
|
||||
not a fabricated "AI agent." When an LLM planner is added, it will
|
||||
emit richer `alternatives_considered` without schema breakage.
|
||||
2. **Nova does NOT remove humans from accountability.** Only from
|
||||
normal operations. Every stage-gate promotion (qa/prod/dr) requires
|
||||
a human attestation recorded with approver identity,
|
||||
separation-of-duties check, and the evidence matrix.
|
||||
3. **Nova is NOT for legacy, untagged, or freeform infrastructure.** It
|
||||
requires Terraform-managed, policy-aligned, fully-tagged inputs.
|
||||
4. **Nova does NOT fabricate metrics.** Every metric is grounded (cites
|
||||
a source), derived (documented formula), or deferred (cites the
|
||||
blocking work). No fabricated numbers in any deck slide or metrics
|
||||
entry (the "no fabrication" hard constraint).
|
||||
|
||||
## What "won" looks like
|
||||
|
||||
By month 18, Nova is the layer enterprise leadership points to when
|
||||
they say *"we don't have an infrastructure ops team anymore, and the
|
||||
audit trail is stronger than it ever was"* — and it is the layer their
|
||||
AI engineering teams reach for first when an agent needs to deploy.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova Capability Inventory — v1.1→v1.8 Re-Verification Sweep
|
||||
# ACDL Capability Inventory — v1.1→v1.8 Re-Verification Sweep
|
||||
|
||||
> Generated: 2026-07-27. Phase 54 (D-093). Milestone v1.10.
|
||||
> Source: PROJECT.md + ROADMAP.md v1.1→v1.8 advertised capabilities.
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
{
|
||||
"phase": 0,
|
||||
"stage": "plan",
|
||||
"milestone": "v1.21",
|
||||
"stage": "complete",
|
||||
"milestone": "v1.14",
|
||||
"phase_role": "pre_execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-11T00:01:00Z",
|
||||
"milestone_complete": false,
|
||||
"requirements": ["REQ-245","REQ-246","REQ-247","REQ-248","REQ-249","REQ-250","REQ-251","REQ-252","REQ-253"],
|
||||
"notes": "v1.21 P0 plan stage complete. PLAN.md v1.21 section written. 5 execution phases (P1 strategic-docs, P2 slides, P3 marp+talking-points+README, P4 pipeline-hardening, P5 render+verify) + P6 final-review-ship. Wave 1 (P1/P2/P4 parallelizable), Wave 2 (P3), Wave 3 (P5), Wave 4 (P6). CLARIFY+RESEARCH minimal at full autonomy — domain known, requirements confirmed with user. Proceeding to P0 ship then execution."
|
||||
"updated_at": "2026-07-29T20:30:00Z"
|
||||
}
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
# Nova AWS Cost Report (v1.0 → v1.14)
|
||||
# ACDL AWS Cost Report (v1.0 → v1.10)
|
||||
|
||||
> **Query date:** 2026-07-29 (updated v1.14 P19)
|
||||
> **Query date:** 2026-07-28
|
||||
> **Source:** AWS Cost Explorer (`ce:GetCostAndUsage`)
|
||||
> **Window:** 2026-07-21 → 2026-07-29 (v1.0 ship → v1.14 active)
|
||||
> **Window:** 2026-07-21 → 2026-07-28 (v1.0 ship → v1.10 complete)
|
||||
> **Account:** 581513795199 (us-east-1)
|
||||
> **Closes:** G-008 (no cost documentation despite live AWS resources)
|
||||
|
||||
|
||||
@@ -6,13 +6,7 @@
|
||||
|
||||
Two escalations must be resolved before the leadership pitch:
|
||||
- **G-005 (risks):** 6 cloud capabilities (CAP-017..022) are deploy-unverified.
|
||||
**RESOLVED (v1.11):** CAP-017..022 are now Verified live-aws via the
|
||||
modules-lifecycle pipeline (apply/modify/destroy exit 0). The IAM-drift
|
||||
framing is removed. See CAPABILITY_INVENTORY.md.
|
||||
- **G-008 (budget):** No cost documentation exists despite live AWS resources.
|
||||
**RESOLVED (v1.11):** COST.md now exists, documenting the v1.0→v1.10 spend
|
||||
window + the v1.11 cost projection. The v1.14 P19 phase extends the
|
||||
window to v1.11–v1.14.
|
||||
|
||||
The project is reclassified as an **OSS reference implementation** (G-003),
|
||||
not a sponsored product. The grill's sponsor/ROI/budget/timeline axes apply
|
||||
@@ -310,588 +304,3 @@ autonomy with assumption logging.
|
||||
P8 exclude the state-bucket name from externalization entirely
|
||||
(externalize only resource ARNs, leave the backend `bucket` literal).
|
||||
Confidence 0.55; auto-resolved per `config.autonomy.level=full`.
|
||||
|
||||
---
|
||||
|
||||
## Run: 2026-07-30 (mode: interactive, focus: v1.15-Nova rebrand, all 9 axes)
|
||||
|
||||
### Verdict: Proceed with conditions (confidence: 0.82)
|
||||
|
||||
A Major/breaking rebrand (ACDL → Nova) across prose, decks, code, env vars,
|
||||
consumer path, SSM path, AWS tag keys, and AWS resource names — 4 execution
|
||||
phases + 1 final. The plan is technically sound and the scope is user-directed
|
||||
(D-102..D-112). Three binding mitigations surfaced (G-104, G-106, G-108); the
|
||||
rest accept the plan as written. Two findings carry residual risk that is
|
||||
accepted at full autonomy (G-103, G-107). No escalations remain open — all
|
||||
auto-resolved with assumption logging per `config.autonomy.level=full`.
|
||||
|
||||
The single most material correction: **the versioning scheme was wrong**.
|
||||
The plan tagged a Major/breaking milestone on the v1.14.x PATCH line
|
||||
(`v1.14.5` = release), contradicting every prior breaking milestone in the
|
||||
project (v1.1→v1.2.0, v1.5→v1.5.0, v1.11→v1.11.0 — all minor bumps). The
|
||||
quoted "Major = progressive minor per phase" rule does not exist in any repo
|
||||
file. **G-104 binds: re-tag as v1.15.x minor-bumped phases** (P1→v1.15.0 …
|
||||
P5→v1.15.4, with v1.15.4 IS the milestone release).
|
||||
|
||||
### Per-axis findings
|
||||
|
||||
#### Axis 1 — Feasibility
|
||||
**Challenge:** Can the full rebrand (1,465 `ACDL`/`acdl` occurrences across 205
|
||||
files, 21 env vars, 11 AWS resources, 5 tag keys, 67 SSM refs, 23 consumer-path
|
||||
refs) actually be done in 4 execution phases? The migration ordering
|
||||
(docs→code/env→SSM/tags→AWS resources→final) is sound: P1 has no runtime impact,
|
||||
P2's dual-read fallback prevents deployment breakage, P3's parallel-tag period
|
||||
prevents ABAC lockout, P4's staged terraform migration prevents a big-bang
|
||||
failure. The phase dependencies (P2 depends on P1's migration guide; P3 depends
|
||||
on P2's dual-read + nova_tagging warn mode; P4 depends on P3's hard-mode tag
|
||||
enforcement; P5 depends on all) are correctly ordered. **Confidence 0.85** that
|
||||
the 4-phase structure is feasible. The `terraform init -migrate-state` approach
|
||||
for the state bucket is the documented, correct mechanism (back up state JSON
|
||||
first). No hidden dependencies found: the `.env.secrets` direct-read path
|
||||
(G-106) and the Gitea secrets rotation (G-108) are the only mechanic gaps, both
|
||||
now bound. **Verdict: ACCEPT-AS-IS.** **G-103.**
|
||||
|
||||
#### Axis 2 — Scope
|
||||
**Challenge:** Is the full AWS resource rename WITH migration (downtime
|
||||
accepted) over-scoped for a rebrand? D-102 locked this as user-directed. The
|
||||
alternative (rename code only, leave AWS resources as `acdl-*`) would leave a
|
||||
permanent brand inconsistency between code and cloud — acceptable for an NFR
|
||||
patch, not for a "Major/breaking" milestone. The S&P visual theme is correctly
|
||||
out of scope (D-107). The real Gitea repo name stays `acdl` (D-105) — sensible
|
||||
(repo rename is a separate operational burden). Past Gitea release titles stay
|
||||
`ACDL vX.Y.Z` (forward-only) — sensible (no history rewrite). Git branch/tag
|
||||
naming has no brand name (D-112) — sensible. **Missing from scope:** the CI
|
||||
workflow secret-references (`.gitea/workflows/*` `secrets.ACDL_*`) — P2 task 3
|
||||
creates `NOVA_*` Gitea secrets but the plan does not show the workflow YAML
|
||||
`secrets:` references being updated; G-108 binds the mitigation. **Confidence
|
||||
0.80.** **Verdict: ACCEPT-AS-IS.** **G-104** (versioning — see Axis 5).
|
||||
|
||||
#### Axis 3 — Cost
|
||||
**Challenge:** What's the real cost (downtime, person-hours, risk) and is it
|
||||
justified for a *rebrand*? Per A1 (conf 0.9), no live AWS apply during P0–P4 —
|
||||
so the migration scripts are authored but not executed; the live apply is an
|
||||
operator runbook step. Person-hours are the agent's own (autonomous OSS
|
||||
reference, G-003 carries forward). Downtime is accepted (D-102) but deferred to
|
||||
the operator runbook. Token cost: the 1,465-occurrence rename across 205 files
|
||||
is a large but mechanical edit — the explore survey already quantified the
|
||||
mechanical-vs-judgment split. The risk cost (DynamoDB data loss, state bucket
|
||||
corruption, ABAC lockout) is mitigated by the staged ordering + dual-read +
|
||||
parallel-tag — all plan-validated, not live-applied. For an OSS reference with
|
||||
0 consumer adoption (PROJECT.md:487), the cost is bounded. **Confidence 0.80.**
|
||||
**Verdict: ACCEPT-AS-IS.** **G-105.**
|
||||
|
||||
#### Axis 4 — Schedule / risk
|
||||
**Challenge:** DynamoDB data loss, state bucket migration, ABAC breakage,
|
||||
consumer disruption. The mitigations: (a) DynamoDB scan+copy with row-count
|
||||
verification, keep old tables until verified (manual post-verification deletion
|
||||
— point of no return documented); (b) state bucket `terraform init
|
||||
-migrate-state` with state JSON backup first; (c) parallel-tag ABAC period
|
||||
(emit nova:* + acdl:* → swap policy → remove acdl:*); (d) consumer disruption
|
||||
mitigated by the dual-read fallback (P2–P4) + the migration guide (P1). The top
|
||||
3 assumptions: A1 (no live apply — conf 0.9, verified by the established
|
||||
v1.11–v1.14 pattern), A2 (.env.secrets keys renamed, values stay — conf 0.85,
|
||||
now bound by G-106), A3 (Gitea release API reachable — conf 0.8, verified HTTP
|
||||
200). The single risk that could kill the project: state bucket corruption
|
||||
during `-migrate-state` — mitigated by the backup-first runbook step. No
|
||||
pre-mortem beyond the runbook is documented, but the staged ordering IS the
|
||||
de-facto pre-mortem mitigation. **Confidence 0.78.** **Verdict: ACCEPT-AS-IS.**
|
||||
**G-106.**
|
||||
|
||||
#### Axis 5 — Technical soundness
|
||||
**Challenge:** Is the dual-read fallback design sound? Is the parallel-tag ABAC
|
||||
migration safe? Is `terraform init -migrate-state` correct? **Dual-read:**
|
||||
sound in principle (NOVA_X preferred, ACDL_X fallback), BUT the `.env.secrets`
|
||||
load path bypasses the `core/env.py` helper — `run_platform.sh:288-289` exports
|
||||
`$ACDL_AWS_ACCESS_KEY_ID` (hardcoded) and `regression_verify.py:309-312`
|
||||
parses the file matching `k == "ACDL_AWS_ACCESS_KEY_ID"` (hardcoded). If P2
|
||||
renames the `.env.secrets` keys to `NOVA_*` but these two readers still read
|
||||
`ACDL_*`, AWS creds vanish → CAP-013/014/015 (which need live creds for
|
||||
terraform plan) break → regression gate breaks. **G-106 binds: dual-read in
|
||||
BOTH load paths** (shell export + Python parser must read NOVA_* first, ACDL_*
|
||||
fallback, mirroring the helper contract). **Parallel-tag ABAC:** safe — emit
|
||||
both tag sets, swap policy with acdl:* as secondary condition, verify, remove.
|
||||
Plan-validated only per A1 (live ABAC stays acdl:* until operator runbook).
|
||||
**`terraform init -migrate-state`:** correct documented mechanism; backup state
|
||||
JSON first is the binding safety step. **Versioning contradiction:** the plan
|
||||
tags a Major milestone on the v1.14.x PATCH line — G-104 binds re-tag as
|
||||
v1.15.x minor-bumped. **Confidence 0.85.** **Verdict: MITIGATE-BINDING (G-106).**
|
||||
**G-104, G-106.**
|
||||
|
||||
#### Axis 6 — Testability / verifiability
|
||||
**Challenge:** Can the success criteria actually be verified? Will the
|
||||
regression gate stay 16/16 across a 1,465-occurrence rename? Is `grep -rni ACDL`
|
||||
returning 0 realistic? The gate-stays-16/16 binding constraint (PLAN.md:44-49)
|
||||
requires per-phase fixture updates — P2 updates env-var fixtures, P3 updates
|
||||
SSM/tag fixtures, P4 updates terraform-name fixtures. The dual-read fallback
|
||||
test (P2) keeps ACDL_* as the fallback source — this is the ONE allowed
|
||||
exception to the grep-returns-0 criterion (success criterion 6 exempts it).
|
||||
`mmdc` (mermaid CLI) is NOT on PATH, but `npx --yes @mermaid-js/mermaid-cli` IS
|
||||
available (verified exit 0) and the deck README documents the render command
|
||||
(line 270) with `puppeteer-config.json` for no-sandbox — so the 5 `.mmd` PNG
|
||||
re-exports in P1 task 3 are feasible. The Gitea secrets rotation (P2 task 3)
|
||||
was verified: API reachable (HTTP 200), token present, `rotate_spike_key.sh`
|
||||
pattern exists. **Confidence 0.82.** **Verdict: ACCEPT-AS-IS.** **G-107.**
|
||||
|
||||
#### Axis 7 — Security
|
||||
**Challenge:** Does the rebrand introduce a security regression? (a) ABAC
|
||||
policy swap window — mitigated by the parallel-tag period (nova:* + acdl:*
|
||||
both valid → swap → remove); plan-validated only, no live window during P0–P4.
|
||||
(b) Secret rotation — `.env.secrets` keys renamed (values stay, no
|
||||
re-rotation needed until P5); G-106 binds the dual-read in both load paths so
|
||||
creds don't silently vanish. (c) `.env.secrets` key rename — the file contains
|
||||
live rotated AWS creds + a Gitea token; renaming keys is cosmetic (same values)
|
||||
but the load-path readers must follow (G-106). (d) IAM policy scope (v1.14 P9
|
||||
scoped `Resource: "*"`) — the rebrand renames `acdl-*` ARNs to `nova-*` in
|
||||
terraform; the IAM policy `Resource` patterns must be updated to `nova-*` —
|
||||
P4 task 2 covers this (`acdl-spike-runner` → `nova-spike-runner`). No new
|
||||
security regression introduced; the rebrand is nomenclature, not a permission
|
||||
change. **Confidence 0.80.** **Verdict: ACCEPT-AS-IS.** **G-108.**
|
||||
|
||||
#### Axis 8 — Maintainability
|
||||
**Challenge:** Will the dual-read fallback + parallel-tag period create
|
||||
technical debt that's hard to clean up? Is P5 (remove fallback) realistic? The
|
||||
dual-read (P2) + parallel-tag (P3) IS technical debt by design — it exists to
|
||||
be removed in P5. P5 does six things in one phase (remove fallback, hard-fail
|
||||
acdl:*, delete Gitea ACDL_* secrets, remove .env.secrets legacy comment,
|
||||
multi-persona review + audit, milestone ship). The risk: P5's removal surfaces
|
||||
a break if P2–P4 didn't catch every ACDL_* reference in the platform's OWN CI
|
||||
workflows. But P5 is mechanical cleanup: `get_env()` drops the fallback branch,
|
||||
shell scripts drop `:-$ACDL_X`, `nova_tagging.py` flips warn→hard-fail. The
|
||||
grep-returns-0 success criteria are verifiable. The 0-consumer-adoption state
|
||||
(PROJECT.md:487) means no external consumer breaks at P5; only the platform's
|
||||
own CI must be fully migrated by P4. **Confidence 0.78.** **Verdict:
|
||||
ACCEPT-AS-IS.** **G-109.**
|
||||
|
||||
#### Axis 9 — Adversarial
|
||||
**Challenge:** Worst-case scenario? What breaks first? Rollback plan if P4
|
||||
goes wrong mid-flight? **Worst case:** the `terraform init -migrate-state`
|
||||
corrupts the state bucket JSON and the backup was incomplete — you lose
|
||||
terraform state for the microservice + static-assets stacks. **Mitigation:**
|
||||
the runbook binds "back up the state JSON first" before each `-migrate-state`;
|
||||
keep old DynamoDB tables until verified (manual post-verification deletion =
|
||||
the point of no return). The staged ordering (KMS alias → SNS/SG → Lambda →
|
||||
DynamoDB → ECR → IAM → state bucket → ALB last) means a mid-flight failure at
|
||||
any step leaves prior steps intact and old resources still named `acdl-*`. The
|
||||
dual-read fallback (P2–P4) means the runtime tolerates both `acdl-*` and
|
||||
`nova-*` during the window — so a partial migration doesn't break the running
|
||||
platform. **What breaks first:** the `.env.secrets` load path (G-106) — if the
|
||||
key rename + reader update are misaligned, AWS creds vanish and the regression
|
||||
gate breaks immediately. G-106 binds the mitigation. **Rollback:** the runbook
|
||||
is the rollback; the staged ordering with "keep old until verified" is the
|
||||
safety net. ALB recreate (last, brief downtime) is the only hard-downtime step;
|
||||
rollback = recreate the old ALB. **Confidence 0.75.** **Verdict: ACCEPT-AS-IS.**
|
||||
**G-110.**
|
||||
|
||||
### Binding decisions (G-103..G-110)
|
||||
|
||||
| ID | Axis | Decision | Confidence | Rationale |
|
||||
|----|------|----------|-----------|-----------|
|
||||
| G-103 | 1 (Feasibility) | ACCEPT-AS-IS | 0.85 | 4-phase structure is feasible; migration ordering (docs→code/env→SSM/tags→AWS→final) is sound; phase dependencies correctly ordered; `terraform init -migrate-state` is the correct mechanism. |
|
||||
| G-104 | 2/5 (Scope/Technical) | MITIGATE-BINDING | 0.90 | **Re-tag as v1.15.x minor-bumped phases** (P1→v1.15.0 … P5→v1.15.4, v1.15.4 IS the milestone release). The v1.14.x PATCH-line scheme contradicts every prior breaking milestone (v1.1→v1.2.0, v1.5→v1.5.0, v1.11→v1.11.0). The quoted "Major = progressive minor per phase" rule exists in NO repo file. A Major/breaking milestone shipping as v1.14.5 means the semver MAJOR never advances despite a breaking change — consumers on `@v1` silently absorb the rebrand. Update PLAN.md, ROADMAP.md §v1.15, PROJECT.md §v1.15, and ARCHITECTURE.md §v1.15 Addendum tag references. |
|
||||
| G-105 | 3 (Cost) | ACCEPT-AS-IS | 0.80 | No live AWS apply during P0–P4 (A1); migration scripts authored, not executed; downtime accepted (D-102) but deferred to operator runbook. For an OSS reference with 0 consumer adoption, cost is bounded. |
|
||||
| G-106 | 4/5 (Risk/Technical) | MITIGATE-BINDING | 0.88 | **Dual-read in BOTH `.env.secrets` load paths.** `run_platform.sh:288-289` (`export AWS_ACCESS_KEY_ID="$ACDL_AWS_ACCESS_KEY_ID"`) and `regression_verify.py:309-312` (parses file matching `k == "ACDL_AWS_ACCESS_KEY_ID"`) bypass the new `core/env.py get_env()` helper. P2 MUST update both readers to read `NOVA_*` first with `ACDL_*` fallback — mirroring the dual-read contract. Without this, renaming `.env.secrets` keys to `NOVA_*` breaks AWS creds → CAP-013/014/015 fail → regression gate breaks. Old `ACDL_*` keys removed in P5. |
|
||||
| G-107 | 6 (Testability) | ACCEPT-AS-IS | 0.82 | Per-phase fixture updates keep the gate 16/16 (PLAN.md:44-49 binding constraint). `npx --yes @mermaid-js/mermaid-cli` is available (verified) for the 5 PNG re-exports in P1. Gitea API reachable (HTTP 200) + token present for P2 task 3. |
|
||||
| G-108 | 7 (Security) | MITIGATE-BINDING | 0.80 | **P2 task 3 must update the CI workflow `secrets:` references** (`.gitea/workflows/*`, `.github/workflows/*`) when `NOVA_*` Gitea secrets are created, with graceful degrade + retry on API failure. The plan creates `NOVA_*` aliases but does not show the workflow YAML `secrets.ACDL_*` references being updated. If the workflows still reference `ACDL_*` secrets at P5 (when old secrets are deleted), CI breaks. The Gitea secrets rotation must be a hard gate with retry-on-failure (not a silent skip). |
|
||||
| G-109 | 8 (Maintainability) | ACCEPT-AS-IS | 0.78 | P5 is mechanical cleanup (drop fallback branch, hard-fail acdl:*, delete old secrets); 0-consumer-adoption means no external break at P5; grep-returns-0 is verifiable. |
|
||||
| G-110 | 9 (Adversarial) | ACCEPT-AS-IS | 0.75 | Runbook + staged ordering is the rollback; "keep old until verified" is the safety net; ALB recreate (last) is the only hard-downtime step. The `.env.secrets` load path (G-106) is what breaks first if misaligned — G-106 binds the mitigation. |
|
||||
|
||||
### Escalations
|
||||
|
||||
None remain open. All material questions resolved with confidence ≥ 0.60.
|
||||
Two findings carry accepted residual risk (auto-resolved at full autonomy
|
||||
with assumption logging):
|
||||
|
||||
- **G-103 (Axis 1):** residual risk that the 4-phase structure underestimates
|
||||
the 1,465-occurrence rename effort — accepted; per-phase fixture updates
|
||||
(G-107) + the explore survey's mechanical-vs-judgment split bound the effort.
|
||||
- **G-107 (Axis 6):** residual risk that a test fixture is missed during the
|
||||
per-phase rename, breaking 16/16 at a phase boundary — accepted; the
|
||||
per-phase verify step (run the gate before tagging) catches it before ship.
|
||||
|
||||
### Forcing questions asked (7)
|
||||
|
||||
1. **Versioning contradiction** — Major milestone on v1.14.x PATCH line vs.
|
||||
prior breaking milestones all minor-bumped. → **G-104 MITIGATE-BINDING**
|
||||
(re-tag as v1.15.x).
|
||||
2. **P4 migration completeness** — plan-validated terraform vs live AWS
|
||||
resources still `acdl-*`. → **G-103/105 ACCEPT-AS-IS** (runbook for live).
|
||||
3. **`.env.secrets` key rename mechanic** — dual-read helper bypassed by direct
|
||||
shell/Python readers. → **G-106 MITIGATE-BINDING** (dual-read in both load
|
||||
paths).
|
||||
4. **Gitea secrets rotation** — API reachable, token present, but workflow
|
||||
`secrets:` references not shown updated. → **G-108 MITIGATE-BINDING** (update
|
||||
workflow refs, hard gate + retry).
|
||||
5. **ABAC parallel-tag window** — over-engineered for 0 consumers, or correct
|
||||
forward-looking safety net? → **G-108/Axis-4 ACCEPT-AS-IS** (parallel-tag is
|
||||
the mitigation, plan-validated).
|
||||
6. **Regression gate during rebrand** — 16/16 across 1,465-occurrence rename?
|
||||
→ **G-107 ACCEPT-AS-IS** (per-phase fixture updates).
|
||||
7. **P5 fallback removal realism** — cleanup + review + audit + ship in one
|
||||
phase? → **G-109 ACCEPT-AS-IS** (mechanical cleanup).
|
||||
8. **P4 rollback plan** — runbook + staged ordering sufficient? → **G-110
|
||||
ACCEPT-AS-IS** (staged ordering is the rollback).
|
||||
|
||||
### What the project is NOT doing that it should (adversarial close)
|
||||
|
||||
- **Documenting the versioning rule it now follows.** G-104 binds the
|
||||
v1.15.x minor-bumped scheme, but no `.ciagent/` file records the
|
||||
versioning convention. The plan should add a one-line versioning note to
|
||||
PROJECT.md §v1.15 or a `VERSIONING.md` so the next milestone doesn't
|
||||
re-litigate this.
|
||||
- **Quantifying the live state volume** for the DynamoDB scan+copy + state
|
||||
bucket migration. The runbook says "back up first" + "verify row counts" but
|
||||
doesn't quantify the data. For 0-consumer-adoption, this is likely tiny —
|
||||
but the rollback feasibility (G-110) depends on it being small enough to
|
||||
re-scan. Accepted residual risk.
|
||||
|
||||
### Simplest 80%-value version
|
||||
|
||||
The simplest version that delivers 80% of the rebrand value: **P1 (docs/decks)
|
||||
+ P2 (code/env dual-read) + P5 (ship)** — skip the live AWS resource migration
|
||||
(P3 SSM/tags + P4 AWS resources) entirely. The code + docs would say Nova; the
|
||||
cloud would still say `acdl-*`. This is the "rename code only, leave cloud"
|
||||
option D-102 rejected. The user chose the full migration (D-102) — the binding
|
||||
decision is recorded; the 80% version is NOT the chosen path. The full scope is
|
||||
accepted as user-directed.
|
||||
|
||||
### What must be true for success in the next 90 days, and is it true today?
|
||||
|
||||
1. **The dual-read helper + both `.env.secrets` load paths are updated in
|
||||
lockstep (G-106).** — TRUE after P2 binds G-106; FALSE today (the direct
|
||||
readers still hardcode `ACDL_*`).
|
||||
2. **The regression gate stays 16/16 at every phase boundary (G-107).** —
|
||||
TRUE if per-phase fixture updates are complete before each tag; the
|
||||
per-phase verify step enforces it.
|
||||
3. **The CI workflow `secrets:` references are updated when `NOVA_*` Gitea
|
||||
secrets are created (G-108).** — FALSE today; P2 task 3 must be expanded to
|
||||
include the workflow YAML updates.
|
||||
4. **The versioning scheme is corrected to v1.15.x (G-104).** — FALSE today;
|
||||
the plan says v1.14.x. Must be corrected before P0 ship.
|
||||
|
||||
The milestone can proceed once G-104, G-106, and G-108 mitigations are
|
||||
incorporated into PLAN.md. Confidence 0.82.
|
||||
|
||||
---
|
||||
|
||||
# v1.16 NFR Simplification — Grill (2026-07-30)
|
||||
|
||||
**Griller:** ci-griller (glm-5.2). **Milestone:** v1.16 (NFR).
|
||||
**Verdict:** PASS-with-binding (3 binding decisions G-111..G-113, 1
|
||||
escalation E-002). The plan is evidence-grounded and does not re-litigate
|
||||
v1.14 (D-117 clean). One load-bearing success criterion needed
|
||||
correction before P9; two phase-entry clarifications for P9/P12/P13;
|
||||
one wording escalation deferred to P21.
|
||||
|
||||
## Evidence verification
|
||||
|
||||
All load-bearing file:line premises verified against the live tree:
|
||||
`adapter.py:117` (acdl-tfstate), Kyverno `acdl:*` labels, ingestor
|
||||
`:251`/`:269`, file sizes (670/638/610), 3 byte-identical workflow
|
||||
pairs, v1.14 grill G-101..G-106 + E-001 all CLOSED.
|
||||
|
||||
## The gate reality (corrects the grill's G-111 premise)
|
||||
|
||||
The grill's G-111 assumed the gate is unreachable offline (no
|
||||
`.env.secrets`). **Corrected via live run:** `.env.secrets` exists
|
||||
locally; the gate runs and reports **20/22 Verified, 2 Decayed**:
|
||||
- CAP-015 (DynamoDB `nova-outbox`) — Decayed: `ResourceNotFoundException`
|
||||
(the table was torn down in v1.11 D-096 and never re-provisioned; v1.15
|
||||
P4 was plan-only, no live apply).
|
||||
- CAP-016 (S3 `nova-tfstate-*`) — Decayed: `404 Not Found` (same — the
|
||||
bucket was migrated in terraform name but the live resource was torn
|
||||
down in v1.11 and not re-created).
|
||||
|
||||
This is the **documented post-v1.11-teardown steady state** (D-096:
|
||||
"live resources do not persist past v1.11"). CAP-015/016 Decayed is not
|
||||
a v1.16 regression — it is the known, accepted zero-cost state. The
|
||||
v1.16 P1 state-bucket fix (`adapter.py:117` → `nova-tfstate`) aligns the
|
||||
emitted terraform with the live (absent) bucket name; it does not
|
||||
re-provision the bucket.
|
||||
|
||||
## Binding decisions (G-111..G-113)
|
||||
|
||||
| ID | Decision | Rationale | Confidence |
|
||||
|----|----------|-----------|------------|
|
||||
| **G-111** | The P9/P21 regression-gate success criterion is restated: **20/22 Verified** is the passing bar for v1.16. CAP-015/016 (DynamoDB outbox + S3 state bucket) are the documented post-v1.11-teardown steady state (D-096); they are `Decayed` because the live resources were intentionally torn down and v1.15 P4 was plan-only (no live apply). Re-provisioning them is a future feature milestone, not an NFR. The gate (`regression_verify.py:77` `passed = all(...)`) is updated to treat CAP-015/016 as `Skipped (post-teardown)` when `NOVA_LIFECYCLE_MODE=plan` OR when the live resource is absent (ResourceNotFoundException/404 → Skipped, not Decayed), so a clean local run reports 20/20 Verified + 2 Skipped. The PLAN.md/PROJECT.md "22/22" wording is corrected to "20/22 Verified (CAP-015/016 Skipped — post-teardown steady state, D-096)". | Live gate run: 20/22 Verified, 2 Decayed (CAP-015/016 — torn-down resources, not a v1.16 regression). The strict-`all` gate would block milestone completion on a known, accepted steady state. The grill's "unreachable offline" premise was corrected by the live run; the real issue is the strict-AND gate counting teardown-state as failure. | **0.90** |
|
||||
| **G-112** | P9 MUST pin the sourcing model for `run_decommission.sh`/`run_uptime.sh`: **`source`** (shared shell env), not `invoke` (subshell). The extracted blocks reference `run_platform.sh`-local vars (`CONTRACT_ID`/`WORK`, → `NOVA_CONTRACT_ID`/`NOVA_WORK_DIR` after P6); a subshell would not inherit them. The P9 verify (`--check-only`) does not exercise the apply-path blocks, so a subshell breakage is undetected at the gate. | PLAN.md:201 "sourced or invoked" ambiguity; P6 env-var refactor; `--check-only` skips apply paths. | **0.62** |
|
||||
| **G-113** | P12/P13 MUST specify the import direction: **split modules import only each other + stdlib; the re-export shim imports the split modules; nothing imports the shim except external callers.** This prevents the latent cycle (shim → split → split → shim). Documented in the phase plan. | Re-export shim pattern; no import-direction stated in PLAN.md. | **0.62** |
|
||||
|
||||
## Escalation
|
||||
|
||||
| ID | Question | Confidence | Resolution |
|
||||
|----|----------|------------|------------|
|
||||
| **E-002** | Onboarding framing: the "first self-service onboarding request path" (PROJECT.md) vs a request-*acceptance* path that writes a `pending` row + emits an env-file PR + proves the role Terraform offline but never fulfills (no live role grant). Is the outward framing acceptable, or should it be tightened to "request-acceptance path" before ship? | **0.55** | Deferred to P21 final review (wording tightening, not a scope change). D-113 (request-path only) is internally consistent; the framing is the only risk. |
|
||||
|
||||
## Mitigations incorporated into PLAN.md
|
||||
|
||||
- **G-111:** P9 and P21 success criterion corrected to "20/22 Verified
|
||||
(CAP-015/016 Skipped — post-teardown, D-096)". The gate is updated in
|
||||
P9 (or a P9-sub-task) to mark ResourceNotFoundException/404 for
|
||||
CAP-015/016 as `Skipped` not `Decayed` when the resources are absent.
|
||||
- **G-112:** P9 pins `source` (shared env) for the extracted helpers.
|
||||
- **G-113:** P12/P13 document the one-way import rule.
|
||||
|
||||
## Can the milestone proceed?
|
||||
|
||||
YES, once G-111's criterion restatement + gate update are incorporated
|
||||
(into P9's must-haves). G-112/G-113 are phase-entry clarifications for
|
||||
P9/P12/P13. E-002 is deferred to P21. Confidence 0.85.
|
||||
|
||||
---
|
||||
|
||||
# GRILL — v1.17 "Strategic Direction, Leadership Metrics & Unified Story" (2026-08-04)
|
||||
|
||||
> **Griller:** CIAgent (red-team mode). **Milestone:** v1.17. **Axes:** 3
|
||||
> (NORTH_STAR alignment, Deck story & arc, Deck per-slide rigor) per PO
|
||||
> direction. **Stance:** adversarial — presumed over-scoped / infeasible /
|
||||
> storytelling-weak until evidence forced otherwise.
|
||||
|
||||
## Evidence base
|
||||
|
||||
- `NORTH_STAR.md` (183 lines, draft), `PLAN.md` (1,114 lines, deck rebuild
|
||||
plan incl. slide-by-slide), `REQUIREMENTS.md` v1.17 (REQ-185..213),
|
||||
`RESEARCH.md` v1.17 (signal inventory, scorecard, deferred-decision
|
||||
ledger, deck research).
|
||||
- Codebase cross-checks: `REGRESSION_REPORT.json` = **18 Verified + 4
|
||||
Skipped** (NOT "22/22 Verified" — the new deck plan correctly says
|
||||
18V+4S; the *existing* decks still claim 22/22). `PROJECT.md:495` =
|
||||
**0 consumer adoption**. `docs/NO_HUMANS_THESIS.md`, `docs/METRICS.md`,
|
||||
`metrics/` do not yet exist (P4/P5 deliverables — expected).
|
||||
- Decisions locked (D-120..D-132) — not re-litigated.
|
||||
|
||||
## The central contradiction
|
||||
|
||||
**NORTH_STAR.md:111** states: *"Targets are committed, not aspirational."*
|
||||
**PO's G-Q6 answer:** *"the goal is simply to target a high touchless
|
||||
resolution rate, not to say we have reached those targets given there are
|
||||
0 consumers."*
|
||||
|
||||
These two statements are in direct conflict. "Committed, not aspirational"
|
||||
+ "simply to target" = the document is lying about its own epistemic
|
||||
status. This is the v1.10 decay root cause (PRE_MORTEM FM-3: decks
|
||||
outrunning verified reality) repeating itself in the document meant to
|
||||
prevent it.
|
||||
|
||||
## Axis 1 — NORTH_STAR alignment
|
||||
|
||||
### G-Q1 — Target with no backing REQ / placeholder
|
||||
**Finding:** AI-Agent Intent Share (≥40%) is a committed 12–18mo target
|
||||
(NORTH_STAR:128) with "placeholder view" claimed, but it is NOT among the
|
||||
8 placeholder views in PLAN P3 (lines 309–315), and no REQ-185..213 builds
|
||||
an emitter or placeholder for it. RESEARCH §3 marks it "future" with no
|
||||
controlling decision ID (unlike every other deferred metric). NORTH_STAR:128
|
||||
falsely claims a placeholder view exists → violates the "no fabrication"
|
||||
hard constraint.
|
||||
**Verdict: BIND.** Add a 9th placeholder view OR move the target to a
|
||||
"Future Horizons" section; correct NORTH_STAR:128. **Confidence: 0.90.**
|
||||
|
||||
### G-Q2 — Anti-goal pursuit
|
||||
**Finding:** No REQ builds an anti-goal. Deck title "No-Humans Infrastructure
|
||||
Platform" is one weak slide away from violating anti-goal #3 (not removing
|
||||
humans from accountability) — mitigation is entirely in slide 3's execution.
|
||||
**Verdict: PASS (conditional on slide 3 landing the attestation model).**
|
||||
**Confidence: 0.75.**
|
||||
|
||||
### G-Q3 — Attestation clarification consistency
|
||||
**Finding:** The attestation clarification is the most consistently
|
||||
propagated concept in the plan — NORTH_STAR (3 places), REQUIREMENTS
|
||||
(3 REQs), deck (3 slides). Well done.
|
||||
**Verdict: PASS.** **Confidence: 0.92.**
|
||||
|
||||
### G-Q4 — "AI decision" framing (D-122 honesty)
|
||||
**Finding:** D-122 (confidence_signal + HITL gate, NOT an LLM) is cited on
|
||||
slide 7 and required in NO_HUMANS_THESIS.md (REQ-213). BUT slide 7's
|
||||
*Delivers* says "every AI decision captured" without ever telling the
|
||||
audience what the "AI" is. The honesty is buried in a linked doc + a
|
||||
decision ID the audience has never heard.
|
||||
**Verdict: BIND.** Add one sentence to slide 7 *Delivers*: "Nova's 'AI
|
||||
decision' is the confidence-gated policy engine, not an LLM planner
|
||||
(D-122)." **Confidence: 0.85.**
|
||||
|
||||
### G-Q5 — Secretly ungrounded metrics
|
||||
**Finding:** The 8 deferred placeholder views cover their list. BUT (a)
|
||||
AI-Agent Intent Share's placeholder is falsely claimed (G-Q1), and (b)
|
||||
derived metrics (FTE Hours Saved, Platform ROI) are computed on zero
|
||||
production runs yet shown on slide 12 without the zero-denominator caveat.
|
||||
A "derived" metric from zero runs is technically not fabricated but is
|
||||
misleading.
|
||||
**Verdict: BIND.** (1) Resolve G-Q1; (2) slide 12 must annotate derived
|
||||
metrics with "(computed on N internal runs; production-denominator activates
|
||||
post-pilot)." **Confidence: 0.82.**
|
||||
|
||||
### G-Q6 — 12–18mo target feasibility (0 consumers)
|
||||
**Finding:** PO's answer ("simply to target") conflicts with NORTH_STAR:111
|
||||
("committed, not aspirational"). 3 "grounded (after P1)" targets (Touchless
|
||||
Resolution, Human Escalation, AI Decision Accuracy) have scope "across
|
||||
production estates" — but PROJECT.md:495 = 0 consumer adoption. The metric
|
||||
IS computable on internal dev runs, but the target scope doesn't exist.
|
||||
Marking "grounded" while the scope is absent is the overclaim the "no
|
||||
fabrication" constraint exists to prevent.
|
||||
**Verdict: BIND.** (1) Rewrite NORTH_STAR:111 → "Targets are committed
|
||||
destinations; the grounding column records whether each is measurable this
|
||||
milestone." (2) Reclassify the 3 targets to `partial — measurement pipeline
|
||||
grounded on internal runs; production-estate scope activates post-pilot`
|
||||
(the Cloud Spend Reduction precedent at NORTH_STAR:123). (3) Deck slide 5
|
||||
regroup as "Measurable today (internal runs)" vs "Activates post-pilot
|
||||
(production estates)." Requires NORTH_STAR-CHANGE commit trailer (REQ-204).
|
||||
**Confidence: 0.80.**
|
||||
|
||||
## Axis 2 — Deck plan: story & arc
|
||||
|
||||
### G-Q7 — Arc order (Problem→Vision→How→Proof→Roadmap vs Proof-first)
|
||||
**Finding:** Current arc puts Proof at Act 4 (slides 10–13) — 40% of the
|
||||
deck before a number. For a leadership audience that has seen 10+ milestone
|
||||
decks, this risks losing the room by slide 4. BUT the "no-humans" thesis
|
||||
is contentious; jumping to proof without the attestation model invites the
|
||||
"removing humans from accountability" objection. The Vision act makes the
|
||||
Proof credible.
|
||||
**Verdict: PASS (marginal).** Defensible IF the Problem act is tight and
|
||||
slide 3 front-loads the attestation clarification. **Confidence: 0.62.**
|
||||
|
||||
### G-Q8 — x3 structure at deck level
|
||||
**Finding:** Slide 1's 5-act preview is orienting (a table of contents),
|
||||
not too much meta-structure. BUT it's also not a hook — it gives structure,
|
||||
not stakes. A C-suite audience decides in the first 30 seconds.
|
||||
**Verdict: BIND (minor).** Add one stake-establishing line to slide 1
|
||||
*Delivers* with a real number (18 verified, 0 consumers, honest deferral
|
||||
list). **Confidence: 0.70.**
|
||||
|
||||
### G-Q9 — Per-slide benefit callouts (substantive vs filler)
|
||||
**Finding:** 4 of 17 closes are filler (slides 1, 4, 12, 15); 2 borderline
|
||||
(8, A1). Worst offender: slide 12 (ROI) restates the *objective* ("ROI is
|
||||
quantifiable") rather than giving the *number* or the *honest caveat*.
|
||||
**Verdict: BIND.** Rewrite 4 filler closes. Slide 12's close must be:
|
||||
"Benefit: you now know the ROI formula — (labor + cloud + avoided downtime)
|
||||
÷ platform cost — and that it computes on internal runs today, with
|
||||
production-denominator activating post-pilot." **Confidence: 0.78.**
|
||||
|
||||
### G-Q10 — Deck length (17 slides)
|
||||
**Finding:** 17 is at the upper bound but justifiable for 5 acts. The risk
|
||||
is density, not length: slide 12 crams 6 metrics (Touchless, Human
|
||||
Escalation, MTTR, Cost, FTE, ROI) into one slide — a wall of bullets.
|
||||
**Verdict: BIND (minor).** Split slide 12 into "Zero-Touch Efficiency"
|
||||
(Touchless, Human Escalation, MTTR) + "Cost & ROI" (Cost, FTE, ROI). Deck
|
||||
→ 18 slides, each earning its place. **Confidence: 0.68.**
|
||||
|
||||
### G-Q11 — "What's Deferred" slide (13)
|
||||
**Finding:** The honesty strengthens the grounded claims BUT surfaces the
|
||||
gap: Nova claims "no-humans in operations" while deferring the metrics
|
||||
that would prove operations are healthy without humans (Live Infra Health,
|
||||
SLA, Drift Auto-Reversal). A skeptical viewer notes the contradiction.
|
||||
**Verdict: BIND.** Add preempt to slide 13: "These deferrals are about
|
||||
*measurement infrastructure*, not about whether the platform runs without
|
||||
humans — the platform runs autonomously today on internal runs; what's
|
||||
deferred is the production-estate dashboard that would prove it at scale."
|
||||
**Confidence: 0.75.**
|
||||
|
||||
## Axis 3 — Deck plan: per-slide rigor
|
||||
|
||||
### G-Q12 — Slide opening lines
|
||||
**Finding:** The "This slide shows X" formula is orienting, not patronizing,
|
||||
because each includes a stake-bearing clause. Consistent without being empty.
|
||||
**Verdict: PASS.** **Confidence: 0.80.**
|
||||
|
||||
### G-Q13 — Transitions (written vs hand-waved)
|
||||
**Finding:** ~10 of 13 transitions are written (specific reference to prior
|
||||
close). 3 are hand-waved (slides 8→9, 11→12, 13→14). Worst: the Act 3→4
|
||||
boundary (slide 8→9, How→Proof) — the most important transition in the deck
|
||||
— is the weakest.
|
||||
**Verdict: BIND.** Rewrite the 3 hand-waved transitions. The 8→9 Act
|
||||
boundary must carry weight: "Having seen the gate model — autonomy in
|
||||
operations, human in accountability — here is how Nova instruments itself
|
||||
to prove that model at scale." **Confidence: 0.85.**
|
||||
|
||||
### G-Q14 — Weakest slide (audience-loss point)
|
||||
**Finding:** Slide 9 (Telemetry Architecture) is the audience-loss slide.
|
||||
It's the 4th consecutive architecture slide (6,7,8,9), the most abstract
|
||||
(CloudEvents, SQLite, PowerBI), its Benefit is about data plumbing not
|
||||
business value, and it sits between the attestation matrix (slide 8,
|
||||
emotionally resonant) and the Proof act (slide 10, the numbers) — between
|
||||
the two things the audience came for.
|
||||
**Verdict: BIND.** Compress slide 9 into slide 10 OR reframe its Benefit
|
||||
from data plumbing to trust: "Benefit: you now know the proof you're about
|
||||
to see isn't fabricated — every number traces to a file you can audit."
|
||||
**Confidence: 0.78.**
|
||||
|
||||
### G-Q15 — Proof act citation specificity
|
||||
**Finding:** 5 of 6 Proof citations are specific (file paths + real numbers).
|
||||
Gap: slide 12's derived metrics (FTE, ROI) cite "derived" without showing
|
||||
the formula or the input count.
|
||||
**Verdict: BIND (minor).** Show the ROI formula inline on slide 12 + the
|
||||
N=0 production-runs caveat. **Confidence: 0.80.**
|
||||
|
||||
### G-Q16 — Closing slide (15) — does the ask land?
|
||||
**Finding:** THE ask is present but framed as insider language ("fund the
|
||||
hot-path activation (post-D-096) + the tamper-evident ledger build-out
|
||||
(D-083 lift)"). A leadership audience doesn't know what "hot-path
|
||||
activation" means. The ask is a technical request, not a business decision
|
||||
a leader can make in the room.
|
||||
**Verdict: BIND.** Reframe slide 15's ask as a business decision: "The
|
||||
ask: (1) approve a pilot estate to activate production-estate metrics
|
||||
(unblocks D-096), and (2) approve the tamper-evident ledger build-out
|
||||
(lifts D-083) — turning grounded claims into complete proof." Make it a
|
||||
yes/no a leader can give. **Confidence: 0.82.**
|
||||
|
||||
## Binding decisions (must resolve before SHIP)
|
||||
|
||||
| G-ID | Axis | Verdict | What must change | Conf |
|
||||
|---|---|---|---|---|
|
||||
| G-Q1 | 1 | BIND | Add 9th placeholder view for AI-Agent Intent Share OR move to "Future Horizons"; correct NORTH_STAR:128 | 0.90 |
|
||||
| G-Q4 | 1 | BIND | Add D-122 honesty sentence to slide 7 *Delivers* | 0.85 |
|
||||
| G-Q5 | 1 | BIND | Annotate derived metrics on slide 12 with zero-run caveat | 0.82 |
|
||||
| G-Q6 | 1 | BIND | Rewrite NORTH_STAR:111; reclassify 3 targets to `partial`; regroup deck slide 5. NORTH_STAR-CHANGE trailer required | 0.80 |
|
||||
| G-Q8 | 2 | BIND (minor) | Add stake line with real number to slide 1 *Delivers* | 0.70 |
|
||||
| G-Q9 | 2 | BIND | Rewrite 4 filler closes (slides 1, 4, 12, 15); slide 12 must give ROI formula + caveat | 0.78 |
|
||||
| G-Q10 | 2 | BIND (minor) | Split slide 12 into two (Efficiency + Cost/ROI); deck → 18 slides | 0.68 |
|
||||
| G-Q11 | 2 | BIND | Add preempt to slide 13 (deferrals are measurement infra, not whether platform runs without humans) | 0.75 |
|
||||
| G-Q13 | 3 | BIND | Rewrite 3 hand-waved transitions (esp. Act 3→4 boundary 8→9) | 0.85 |
|
||||
| G-Q14 | 3 | BIND | Compress slide 9 into slide 10 OR reframe its Benefit to trust | 0.78 |
|
||||
| G-Q15 | 3 | BIND (minor) | Show ROI formula inline + N=0 caveat on slide 12 | 0.80 |
|
||||
| G-Q16 | 3 | BIND | Reframe slide 15 ask as a business decision (pilot estate + ledger build-out) | 0.82 |
|
||||
|
||||
**PASS (no change):** G-Q2 (anti-goals, conditional on slide 3), G-Q3
|
||||
(attestation consistency — excellent), G-Q7 (arc order — marginal),
|
||||
G-Q12 (slide openings — formulaic but substantive).
|
||||
|
||||
## Escalations (only the PO can decide)
|
||||
|
||||
| E-ID | Question | Confidence |
|
||||
|---|---|---|
|
||||
| E-003 | Should the 3 "grounded (after P1)" targets with "production estates" scope be reclassified to `partial` (Cloud Spend precedent), or should "grounded" be redefined to mean "measurement pipeline grounded"? Changes a committed NORTH_STAR target's grounding label; requires NORTH_STAR-CHANGE trailer (REQ-204). | <0.60 |
|
||||
| E-004 | Should AI-Agent Intent Share (≥40%) remain a "12–18mo Target" with no backing REQ/placeholder, or move to a "Future Horizons" section? Strategic-scope question (is agentic consumption a 12–18mo commitment or a longer horizon?). | <0.60 |
|
||||
|
||||
## Overall verdict
|
||||
|
||||
**🟡 REDUCE SCOPE / BINDING FIXES REQUIRED — not ready to ship as-is.**
|
||||
|
||||
The plan is architecturally sound (metrics pipeline, Decision Ledger,
|
||||
PowerBI export, x3 deck structure are well-designed and grounded). The
|
||||
attestation clarification (G-Q3) is the best-propagated concept in the
|
||||
plan. The regression-capability gate (CAP-023/024) is a credible safeguard.
|
||||
|
||||
But the plan has one structural contradiction (NORTH_STAR:111 vs PO intent
|
||||
vs grounding column) that infects 4 other findings (G-Q1, G-Q5, G-Q6,
|
||||
G-Q9/slide 12). This is the v1.10 decay pattern (PRE_MORTEM FM-3)
|
||||
repeating in the document meant to prevent it. The "no fabrication" hard
|
||||
constraint is self-violated in two places (AI-Agent Intent Share placeholder
|
||||
claim, derived-metrics-without-caveat) before a single slide is rendered.
|
||||
|
||||
The deck plan is story-competent but not story-excellent. 4 benefit
|
||||
callouts are filler, 3 transitions are hand-waved (incl. the critical
|
||||
Act 3→4 boundary), slide 9 is the audience-loss slide, and the closing
|
||||
ask is insider language.
|
||||
|
||||
**12 binding decisions, 2 escalations.** None require re-architecting the
|
||||
plan; all are edits to NORTH_STAR (2 rows + 1 line, with commit trailer),
|
||||
the deck slide plan (4 slide rewrites, 1 split, 3 transition rewrites),
|
||||
and one placeholder-view addition. Estimate: 1–2 phases of rework, not a
|
||||
milestone restart. The plan does NOT need a revision loop — it needs
|
||||
these 12 fixes applied in P0 (NORTH_STAR) and P5 (deck) before the
|
||||
respective phases ship. Critical path unchanged.
|
||||
|
||||
**Can the milestone proceed?**
|
||||
|
||||
YES, once the 12 BIND decisions are incorporated (G-Q1/Q4/Q5/Q6 into P0
|
||||
NORTH_STAR + P5 deck plan; G-Q8/Q9/Q10/Q11/Q13/Q14/Q15/Q16 into P5 deck
|
||||
plan). E-003/E-004 require PO decisions on NORTH_STAR target framing.
|
||||
Confidence 0.80.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova — IAM Policy Baseline (v1.11, REQ-116)
|
||||
# ACDL — IAM Policy Baseline (v1.11, REQ-116)
|
||||
|
||||
> Source of truth: `terraform/bootstrap/spike_runner_policy.json`.
|
||||
> Applied as: customer-managed policy `acdl-spike-runner-policy`
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
# NORTH_STAR — Nova
|
||||
|
||||
> **Status:** Draft (pending interactive GRILL → final)
|
||||
> **Milestone:** v1.21 — Nova Deck Refinement & Pipeline Hardening
|
||||
> **Owner:** Product Owner
|
||||
> **Purpose:** Durable strategic intent. Read by CIAgent in every future
|
||||
> `/ci-run` so the platform's direction survives across milestones. This
|
||||
> is NOT a status document (that's PROJECT.md) and NOT an engineering
|
||||
> architecture (that's the telemetry reference in RESEARCH.md/
|
||||
> ARCHITECTURE.md). It is the PO's committed direction: what we're
|
||||
> building toward, what we refuse to build, and how we'll know we won.
|
||||
|
||||
---
|
||||
|
||||
## Vision
|
||||
|
||||
> **Infrastructure operations become visible. Every environment
|
||||
> provisioned, every incident healed, every risk remediated — by an
|
||||
> autonomous system whose trustworthiness is provable, not promised.
|
||||
> Human attestation remains required at stage gates — QA signs off for
|
||||
> production, SRE greenlights based on operational readiness — but the
|
||||
> operator is never in the loop of normal operations.**
|
||||
|
||||
Nova is the autonomous infrastructure layer that lets product teams ship
|
||||
without engaging an operator, and lets executives trust the platform not
|
||||
because it never fails but because every decision is captured, scored,
|
||||
and accountable. The recurring theme across the platform is that
|
||||
**infrastructure operations become visible** — security posture,
|
||||
remediation velocity, reliability, and lead time are surfaced as
|
||||
queryable signals rather than hidden in tribal knowledge.
|
||||
|
||||
---
|
||||
|
||||
## Strategic Objectives (4)
|
||||
|
||||
**1. Demonstrate production-grade zero-touch operations.**
|
||||
Nova must run real customer estates with no human in the loop of normal
|
||||
operations — autonomy as the default, not the demo. Stage-gate
|
||||
attestation (QA for production, SRE for operational readiness) remains
|
||||
human by design; operational escalations (AI confidence too low to
|
||||
proceed) are the failure mode we drive toward zero. Everything else
|
||||
collapses if autonomy isn't real.
|
||||
|
||||
**2. Establish provable trust in automated decisions.**
|
||||
Trust is established by deterministic scripts that calculate a score and
|
||||
a band outcome that gates the action — the platform functions without AI.
|
||||
"AI decisions" are really automated decisions. The audit substrate —
|
||||
Decision Ledger, confidence scoring, circuit breakers, blast-radius
|
||||
controls — turns "autonomous" from a marketing claim into a defensible
|
||||
one. Trust is the moat. Features can be copied; an immutable, queryable
|
||||
decision history cannot.
|
||||
|
||||
**3. Deliver compounding, quantifiable ROI for customers.**
|
||||
Each quarter on Nova must show measurable improvement on four CTO-grade
|
||||
metrics, all of which flow into PowerBI views and are captured by the
|
||||
telemetry pipeline:
|
||||
|
||||
- **Lead Time** — from PR merge to production deployment (downward trend).
|
||||
- **Infrastructure Vulnerability Count** — open findings on deployed
|
||||
resources (downward trend, demonstrating that proactive scanning +
|
||||
remediation keeps up with the AI-era 0-day pace).
|
||||
- **MTTR** — for platform-detected and platform-remediated incidents.
|
||||
- **Cloud Spend Reduction** — on pilot estates vs. the pre-Nova
|
||||
baseline.
|
||||
|
||||
If leadership cannot point to a number that improves quarter-over-quarter
|
||||
on these four axes, Nova fails its commercial test, regardless of how
|
||||
clever the automation is.
|
||||
|
||||
**4. Integrate with externally owned development platforms — regardless of source.**
|
||||
Nova integrates with externally owned PDLC, SDLC, Agentic, and Citizen
|
||||
Developer platforms with no regard for the source of the intent. Nova
|
||||
provides a set of skills and MCP endpoints that help the developer or AI
|
||||
agent make their application production-grade. Regardless of the source,
|
||||
all intents to deploy to production go through the same rigorous
|
||||
controls, quality gates, attestation, and evidence stream. Nova is the
|
||||
layer any of those platforms reach for first when an agent needs to
|
||||
deploy — not a vendor arriving late to that market.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Goals (4 — what Nova is fundamentally NOT)
|
||||
|
||||
1. **Not a general-purpose AI agent platform.** We are purpose-built for
|
||||
infrastructure operations. Breadth here produces shallow tools; depth
|
||||
here wins the category.
|
||||
2. **Not a system that removes humans from accountability.** Only from
|
||||
normal operations. Every automated decision lands in an immutable
|
||||
ledger. Every stage-gate promotion (qa/prod/dr) requires a human
|
||||
attestation recorded with approver identity, separation-of-duties
|
||||
check, and the evidence matrix. The absence of an operator in the
|
||||
loop is never the absence of a record.
|
||||
3. **Not an upstream development platform.** Nova does not own the
|
||||
product backlog, IDE workflows, code authorship, or application
|
||||
business logic. The PDLC is upstream; Nova integrates with it through
|
||||
a validated contract boundary — Nova never penetrates 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.
|
||||
+117
-102
@@ -1,31 +1,36 @@
|
||||
---
|
||||
project: acdl
|
||||
milestone: v1.18
|
||||
generated_at: 2026-08-06
|
||||
milestone: v1.14
|
||||
generated_at: 2026-07-29
|
||||
generator: lead-developer
|
||||
verification_toolchain:
|
||||
typecheck: "python3 -m py_compile core/submission_readiness.py mcp/atelier/server.py && python3 -m jsonschema schemas/submission-readiness.schema.json"
|
||||
test: "pytest tests/test_submission_readiness.py tests/test_atelier_mcp.py # REQ-220 + REQ-225"
|
||||
build: "bash scripts/render_deck.sh docs/presentations/nova-no-humans-platform-marp.md # HTML + PPTX (D-142)"
|
||||
typecheck: "terraform validate && python3 -m py_compile core/**/*.py && python3 -m jsonschema schemas/*.schema.json"
|
||||
test: "bash scripts/run_primitive_plan.sh --check-only <primitive> # pipeline-driven (D-102); no per-module pytest"
|
||||
build: "terraform init && terraform plan"
|
||||
note: |
|
||||
v1.18 adds the Citizen Developer & Production-Grade Guidance surface:
|
||||
submission-readiness gate, Atelier-derived skills, the Atelier MCP server
|
||||
(plugin-registry, stdio), and PPTX-as-first-class-artifact deck automation.
|
||||
Three active personas: lead-developer (coordination + decks + RACI/scope
|
||||
docs), backend-engineer (MCP server + submission-readiness validator +
|
||||
render/attach scripts), data-engineer (submission-readiness schema if it
|
||||
touches contract storage / DynamoDB shape). frontend-engineer stays
|
||||
deactivated (v1.18 has no frontend; decks are markdown = lead-developer
|
||||
territory). The MCP plugin-registry is a backend pattern, so a separate
|
||||
mcp-engineer persona is NOT added — it folds into backend-engineer.
|
||||
ACDL has no package.json. The execute/verify/ship workflows substitute
|
||||
`terraform validate` + `python -m py_compile` + JSON Schema validation
|
||||
for npm run typecheck, a per-phase verify script (or the
|
||||
modules-lifecycle pipeline cell) for npm test, and `terraform init` +
|
||||
`terraform plan` for npm run build. v1.11 testing is pipeline-driven
|
||||
(D-102): the modules-lifecycle pipeline matrix-runs each L1 module's
|
||||
examples/{simple,complex}.yml contracts through apply→modify→destroy
|
||||
against live AWS. No per-module Python/pytest. This override is
|
||||
documented here as the single source of truth; the ci-* agents read
|
||||
PERSONAS.md before running verification commands.
|
||||
v1.14 note: NFR-only milestone (bug fixes, security, tests, docs).
|
||||
Roster carries forward from v1.11 unchanged. frontend-engineer stays
|
||||
inactive (no frontend; decks are markdown = lead-developer
|
||||
territory). No custom personas needed (no new domains).
|
||||
---
|
||||
|
||||
# ACDL — Persona Roster (v1.18 Citizen Developer & Production-Grade Guidance)
|
||||
# ACDL — Persona Roster (project-level, v1.11 RESTART)
|
||||
|
||||
> v1.18 roster. Three active personas + one deactivated. The MCP server
|
||||
> plugin-registry (D-140) is a backend pattern, not a new persona — it
|
||||
> folds into backend-engineer. v1.17 precedent (frontend-engineer
|
||||
> deactivated, decks are markdown = lead-developer territory) is upheld.
|
||||
> v1.11 is a restart (D-097). The v1.9 roster is superseded. Three
|
||||
> structural corrections: (1) stateless adapter (D-098), (2) terraform
|
||||
> owns lifecycle (D-101), (3) pipeline-driven testing (D-102). The roster
|
||||
> is simplified to the three active domains: data (terraform foundation),
|
||||
> backend (adapter/resolver), general (pipelines/workflows).
|
||||
|
||||
## Active personas
|
||||
|
||||
@@ -33,104 +38,114 @@ verification_toolchain:
|
||||
- **Domain:** coordination
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **Frameworks:** [] (no framework — owns process + narrative, not code)
|
||||
- **Constraints:** ["pragmatic", "battle-tested defaults", "no fabrication (NORTH_STAR honesty model)"]
|
||||
- **Territory:**
|
||||
- `docs/presentations/**` (Step 1/2/4 markdown + the deck automation trigger)
|
||||
- `.ciagent/**` (PROJECT, ROADMAP, REQUIREMENTS, RESEARCH, PLAN, GRILL, PERSONAS, REVIEW, CHECKPOINT)
|
||||
- `PROJECT.md` (RACI matrix + PDLC-scope statement, REQ-215/216)
|
||||
- `ROADMAP.md`
|
||||
- `REQUIREMENTS.md`
|
||||
- `docs/raci.md` (REQ-215)
|
||||
- `docs/scope.md` (REQ-216)
|
||||
- `docs/skills.md` (REQ-222 — the index page, not the skill files themselves)
|
||||
- `docs/submission-readiness.md` (REQ-219 — citizen-developer-facing copy; co-owned with backend-engineer for the reason-code catalog)
|
||||
- **Reason:** Owns CIAgent metadata, the milestone narrative, the RACI +
|
||||
PDLC-scope statements (REQ-215/216), the deck (21 slides, S&P theme
|
||||
regression check vs P1, CAP-024), the skills index page (REQ-222), and
|
||||
the citizen-developer-facing submission-readiness doc (REQ-219). Is
|
||||
the only persona that touches `.ciagent/**` and the deck markdown.
|
||||
- **Phase-specific flag:** none (active for all of P0–P7).
|
||||
- **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).
|
||||
|
||||
### backend-engineer
|
||||
- **Domain:** backend
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **Frameworks:** ["mcp (Python SDK v2)", "pydantic", "jsonschema", "urllib"]
|
||||
- **Constraints:** ["api-first", "strict-typing", "plugin-registry extensible (D-140)", "stdio now / HTTP-ready (D-135)", "no stack traces to citizen developers (REQ-218)"]
|
||||
- **Territory:**
|
||||
- `mcp/atelier/server.py` (REQ-223)
|
||||
- `mcp/atelier/plugins/**/*.py` (REQ-223 — principles.py, validation.py)
|
||||
- `mcp/atelier/vendor/**` (REQ-224 — vendored Atelier snapshot)
|
||||
- `mcp/atelier/VERSION.md` + `mcp/atelier/README.md` (REQ-224)
|
||||
- `scripts/update_atelier_vendor.sh` (REQ-224)
|
||||
- `core/submission_readiness.py` (REQ-218 — the validator, invoked as `contract_ingestor.py --check-readiness`)
|
||||
- `scripts/render_deck.sh` (REQ-228 — HTML + PPTX render)
|
||||
- `scripts/attach_release_asset.py` (REQ-228 — Gitea release asset upload)
|
||||
- `tests/test_atelier_mcp.py` (REQ-225)
|
||||
- `tests/test_submission_readiness.py` (REQ-220)
|
||||
- `docs/submission-readiness.md` (REQ-219 — reason-code catalog section; co-owned with lead-developer for the narrative)
|
||||
- **Reason:** Owns the MCP server (plugin-registry, stdio, vendored
|
||||
Atelier), the submission-readiness validator (extends
|
||||
`contract_ingestor.py --check-readiness`, D-133), the render/attach
|
||||
scripts (D-142 trigger), and the two new test files. The MCP
|
||||
plugin-registry (D-140) is a backend pattern — no separate
|
||||
mcp-engineer persona is created; backend-engineer owns it.
|
||||
- **Phase-specific flag:** none (active for P1 deck-render, P3 validator,
|
||||
P5 MCP server, P6 scripts).
|
||||
- **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:** `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).
|
||||
|
||||
### data-engineer
|
||||
- **Domain:** data
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **Frameworks:** ["jsonschema", "dynamodb (item shape)"]
|
||||
- **Constraints:** ["schema-first", "superset-gate NOT duplicate (PROJECT.md hard constraint)", "W3.E per-env mandatory table is the source of truth"]
|
||||
- **Territory:**
|
||||
- `schemas/**` (REQ-217 — `submission-readiness.schema.json` is the new schema; existing schemas untouched)
|
||||
- `core/lambda/contract_ingestor.py` (the `--check-readiness` subcommand wiring, D-133 — the validator is in `core/submission_readiness.py` but the ingestor dispatches to it; co-owned with backend-engineer)
|
||||
- **Reason:** Owns the submission-readiness JSON Schema (REQ-217) — it
|
||||
is a schema artifact, data-engineer territory. The schema is a
|
||||
*superset gate above* `contract.schema.json`, not a duplicate (it
|
||||
references contract fields, does not redefine them). The
|
||||
per-env-mandatory table comes from W3.E (the locked decision). The
|
||||
ingestor wiring is co-owned with backend-engineer (the dispatch point
|
||||
is backend; the schema it validates against is data).
|
||||
- **Phase-specific flag:** none (active for P3 schema + ingestor wiring).
|
||||
- **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:** `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).
|
||||
|
||||
### general (lead-developer + backend-engineer pipeline work)
|
||||
- **Domain:** coordination + pipelines
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **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).
|
||||
- **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).
|
||||
|
||||
## Deactivated personas
|
||||
|
||||
### frontend-engineer
|
||||
### 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
|
||||
- **Domain:** frontend
|
||||
- **Frameworks:** ["react", "next.js"] (inert — no territory)
|
||||
- **Constraints:** ["component-first", "server-components", "minimal-client-js"] (inert)
|
||||
- **Territory:** [] (no territory in v1.18)
|
||||
- **Reason:** v1.18 has no frontend; decks are markdown (lead-developer
|
||||
territory); deactivated per PERSONAS.md v1.17 precedent. v1.18's
|
||||
observability stays PowerBI / external (Out of Scope: "A Nova-built
|
||||
frontend / dashboard"). The MCP server exposes tools to an AI agent,
|
||||
not a web UI. No reactivation trigger in this milestone.
|
||||
- **Active:** false
|
||||
- **Phase-specific:** false
|
||||
- **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.
|
||||
|
||||
## Roster decisions
|
||||
### 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.
|
||||
|
||||
### D-143 (0.90): Fold mcp-engineer into backend-engineer
|
||||
The MCP plugin-registry (D-140: `plugins/<name>.py register(mcp)`) is a
|
||||
backend code pattern — Python modules, type hints, stdio transport,
|
||||
urllib for the Gitea asset API. It shares nothing with the data domain
|
||||
(schemas/DynamoDB) and is not a new engineering discipline. Creating a
|
||||
separate `mcp-engineer` persona would fragment ownership of the server +
|
||||
its tests + the render/attach scripts (all backend). **Decision:** fold
|
||||
into backend-engineer. backend-engineer's `frameworks` list gains
|
||||
`mcp (Python SDK v2)`. Confidence 0.90 — the only counter-argument is
|
||||
that MCP is a distinct protocol skill, but the SDK v2 API surface
|
||||
(`@mcp.tool()` + type hints) is small and well within backend-engineer's
|
||||
range (it's the same Pydantic/FastAPI-style pattern the persona already
|
||||
knows).
|
||||
### 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.
|
||||
|
||||
### Territory-overlap resolution (co-ownership)
|
||||
## Phase-specific overrides
|
||||
|
||||
| Path | Primary | Co-owner | Why |
|
||||
|------|---------|----------|-----|
|
||||
| `docs/submission-readiness.md` | lead-developer (narrative + examples) | backend-engineer (reason-code catalog, REQ-218 codes) | The doc is citizen-developer-facing copy (lead) but the reason-code catalog (MISSING_TAGS, ENV_MISSING_MANDATORY, AGENTIC_MISSING_INTENT, MISSING_APP_SOURCE, POLICY_PRECONDITION_MISSING) is backend (it mirrors the validator's return codes). |
|
||||
| `core/lambda/contract_ingestor.py` | backend-engineer (dispatch wiring) | data-engineer (the schema it validates against) | D-133 places the `--check-readiness` subcommand on the ingestor (backend dispatch), but the readiness schema it loads is data-engineer territory. |
|
||||
| `schemas/submission-readiness.schema.json` | data-engineer (schema artifact) | backend-engineer (the validator must match it) | The schema is data-engineer's; the validator (REQ-218) is backend-engineer's and must stay in sync with it. |
|
||||
| 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.
|
||||
+350
-1059
File diff suppressed because it is too large
Load Diff
+5
-512
@@ -1,12 +1,4 @@
|
||||
# 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.
|
||||
# ACDL — Agentic Cloud Delivery Platform
|
||||
|
||||
## Vision / Core Value
|
||||
|
||||
@@ -58,103 +50,6 @@ traceable to a human attestation and an immutable evidence stream.
|
||||
boundary. The platform validates, enriches with operational standards,
|
||||
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 penetrates 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)
|
||||
|
||||
> Source of truth: `.ciagent/CAPABILITY_INVENTORY.md` (Phase 54, D-093).
|
||||
@@ -689,97 +584,12 @@ DX: 16 total). Key changes:
|
||||
10. Old two-surfaces diagram replaced by scope boundary diagram.
|
||||
|
||||
Source markdown, talking points, and README all updated to mirror the new
|
||||
structure. Also includes scripts/sync_to_nova.sh (manual-only "2nd release"
|
||||
into ~/nova — a separate GitLab consumer-facing repo with its own history;
|
||||
domain-based conventional commits, never triggered by CI; REQ-229).
|
||||
structure. Also includes scripts/sync_to_gl.sh (GitLab mirror sync
|
||||
utility, unrelated to presentations).
|
||||
|
||||
No code changes; 494 tests pass; `run_ci.sh` + `run_platform.sh --check-only`
|
||||
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 penetrate 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
|
||||
|
||||
### v1.0 (Prior milestone — the demo)
|
||||
@@ -981,7 +791,7 @@ or user-directed scope). New v1.7 decisions:
|
||||
| W1.A | AI-refinement trigger | **Accept recommendation.** Joint condition: N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in last 6 months AND Infra & Ops unilateral override. |
|
||||
| W1.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. |
|
||||
| BA.A | Initial L3B skill catalog | **Accept recommendation.** 5 skills: web API, worker, scheduled job, static asset, basic observability bootstrap. Addition criteria: (a) reviewable for sensitive data, (b) expressible as a single contract submission, (c) documented use case. **Extended v1.18 (REQ-221/222):** the BA.A 5-skill catalog is extended with 9 Atelier-derived production-grade engineering skills under `skills/` (api, security, data, testing, observability, errors, devops, infrastructure-as-code, compliance), indexed by `docs/skills.md`. The Atelier skills extend, not replace, the BA.A catalog. |
|
||||
| BA.A | Initial L3B skill catalog | **Accept recommendation.** 5 skills: web API, worker, scheduled job, static asset, basic observability bootstrap. Addition criteria: (a) reviewable for sensitive data, (b) expressible as a single contract submission, (c) documented use case. |
|
||||
| W3.D | L1/L2 standard versioning | **Decided.** Semver: interface → MAJOR, behavior → MINOR, lifecycle → PATCH (same as the v1.0 demo D-rule, lifted to the real platform). Pin model: L2 contracts pin L1 by `name@semver`; the resolver picks the highest compatible. Evolution: MAJOR bumps require a new registry entry (immutable publication); old entry enters a 12-month deprecation window. |
|
||||
| W3.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. |
|
||||
@@ -1101,321 +911,4 @@ 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-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-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. |
|
||||
| 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. |
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"run_id": "regr-1785591207",
|
||||
"run_at_utc": "2026-08-01T13:33:27Z",
|
||||
"run_id": "regr-1785329757",
|
||||
"run_at_utc": "2026-07-29T12:55:57Z",
|
||||
"milestone": "v1.10",
|
||||
"phase": 52,
|
||||
"summary": {
|
||||
"Verified": 18,
|
||||
"Verified": 22,
|
||||
"Decayed": 0,
|
||||
"Broken": 0,
|
||||
"Skipped": 4
|
||||
"Broken": 0
|
||||
},
|
||||
"passed": true,
|
||||
"results": [
|
||||
@@ -17,7 +16,7 @@
|
||||
"status": "Verified",
|
||||
"detail": "exit 0; 2 sample contracts validate",
|
||||
"tier": "local",
|
||||
"duration_ms": 235
|
||||
"duration_ms": 252
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-002",
|
||||
@@ -25,7 +24,7 @@
|
||||
"status": "Verified",
|
||||
"detail": "exit 0; env schema validates",
|
||||
"tier": "local",
|
||||
"duration_ms": 201
|
||||
"duration_ms": 196
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-003",
|
||||
@@ -33,7 +32,7 @@
|
||||
"status": "Verified",
|
||||
"detail": "exit 0; ",
|
||||
"tier": "local",
|
||||
"duration_ms": 261
|
||||
"duration_ms": 258
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-004",
|
||||
@@ -41,7 +40,7 @@
|
||||
"status": "Verified",
|
||||
"detail": "exit 0; ",
|
||||
"tier": "local",
|
||||
"duration_ms": 259
|
||||
"duration_ms": 264
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-005",
|
||||
@@ -49,7 +48,7 @@
|
||||
"status": "Verified",
|
||||
"detail": "exit 0; ",
|
||||
"tier": "local",
|
||||
"duration_ms": 337
|
||||
"duration_ms": 314
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-006",
|
||||
@@ -57,7 +56,7 @@
|
||||
"status": "Verified",
|
||||
"detail": "exit 0; interpolation ok",
|
||||
"tier": "local",
|
||||
"duration_ms": 242
|
||||
"duration_ms": 223
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-007",
|
||||
@@ -65,7 +64,7 @@
|
||||
"status": "Verified",
|
||||
"detail": "exit 0; confidence band=pass",
|
||||
"tier": "local",
|
||||
"duration_ms": 91
|
||||
"duration_ms": 80
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-008",
|
||||
@@ -73,15 +72,15 @@
|
||||
"status": "Verified",
|
||||
"detail": "exit 0; outbox hash chain ok",
|
||||
"tier": "local",
|
||||
"duration_ms": 456
|
||||
"duration_ms": 358
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-009",
|
||||
"name": "offline pytest suite passes",
|
||||
"status": "Verified",
|
||||
"detail": "exit 0; [ 98%]\ntests/test_wiz_adapter_real_client.py ......... [100%]\n\n================= 586 passed, 2 deselected in 71.63s (0:01:11) =================",
|
||||
"detail": "exit 0; [ 98%]\ntests/test_wiz_adapter_real_client.py ......... [100%]\n\n====================== 462 passed, 2 deselected in 34.63s ======================",
|
||||
"tier": "local",
|
||||
"duration_ms": 72988
|
||||
"duration_ms": 36065
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-010",
|
||||
@@ -89,63 +88,63 @@
|
||||
"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",
|
||||
"tier": "local",
|
||||
"duration_ms": 73275
|
||||
"duration_ms": 40668
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-011",
|
||||
"name": "headline E2E runs against the local emulating tier (microservice)",
|
||||
"status": "Verified",
|
||||
"detail": "exit 0; al-emulator\",\n \"desired_count\": 1,\n \"running_count\": 1\n },\n \"outbox_dir\": \"/tmp/nova_local_e2e_6vnrnin1/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}",
|
||||
"detail": "exit 0; al-emulator\",\n \"desired_count\": 1,\n \"running_count\": 1\n },\n \"outbox_dir\": \"/tmp/acdl_local_e2e_416d0fmr/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}",
|
||||
"tier": "local",
|
||||
"duration_ms": 634
|
||||
"duration_ms": 583
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-012",
|
||||
"name": "local E2E on the static-assets stack (no ECS)",
|
||||
"status": "Verified",
|
||||
"detail": "exit 0; nova_local_e2e_uq4kkhze/tf\",\n \"backend\": \"local\",\n \"ecs\": null,\n \"outbox_dir\": \"/tmp/nova_local_e2e_uq4kkhze/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}",
|
||||
"detail": "exit 0; acdl_local_e2e_ijhcj1z8/tf\",\n \"backend\": \"local\",\n \"ecs\": null,\n \"outbox_dir\": \"/tmp/acdl_local_e2e_ijhcj1z8/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}",
|
||||
"tier": "local",
|
||||
"duration_ms": 584
|
||||
"duration_ms": 489
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-013",
|
||||
"name": "terraform init+validate+plan live AWS (microservice)",
|
||||
"status": "Skipped",
|
||||
"detail": "terraform init: state bucket absent (post-v1.11-teardown, D-096) [microservice]",
|
||||
"status": "Verified",
|
||||
"detail": "terraform init+validate+plan OK (live AWS, microservice)",
|
||||
"tier": "live-aws",
|
||||
"duration_ms": 737
|
||||
"duration_ms": 28811
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-014",
|
||||
"name": "terraform init+validate+plan live AWS (static-assets)",
|
||||
"status": "Skipped",
|
||||
"detail": "terraform init: state bucket absent (post-v1.11-teardown, D-096) [static-assets]",
|
||||
"status": "Verified",
|
||||
"detail": "terraform init+validate+plan OK (live AWS, static-assets)",
|
||||
"tier": "live-aws",
|
||||
"duration_ms": 676
|
||||
"duration_ms": 31772
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-015",
|
||||
"name": "DynamoDB outbox table exists (live AWS)",
|
||||
"status": "Skipped",
|
||||
"detail": "nova-outbox absent (post-v1.11-teardown steady state, D-096)",
|
||||
"status": "Verified",
|
||||
"detail": "acdl-outbox exists, item_count=9",
|
||||
"tier": "live-aws",
|
||||
"duration_ms": 664
|
||||
"duration_ms": 477
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-016",
|
||||
"name": "S3 state bucket exists + readable (live AWS)",
|
||||
"status": "Skipped",
|
||||
"detail": "state bucket nova-tfstate-581513795199-us-east-1 absent (post-v1.11-teardown, D-096)",
|
||||
"status": "Verified",
|
||||
"detail": "state bucket exists, keys=['platform/terraform.tfstate', 'spike/alb/dev/terraform.tfstate', 'spike/cdn/dev/terraform.tfstate', 'spike/ci-vpc/terraform.tfstate', 'spike/clus/dev/terraform.tfstate']",
|
||||
"tier": "live-aws",
|
||||
"duration_ms": 245
|
||||
"duration_ms": 324
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-017",
|
||||
"name": "DynamoDB nova-contracts table (lifecycle pipeline evidence)",
|
||||
"name": "DynamoDB acdl-contracts table (lifecycle pipeline evidence)",
|
||||
"status": "Verified",
|
||||
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve",
|
||||
"detail": "terraform files present + simple/complex contracts resolve",
|
||||
"tier": "lifecycle-pipeline",
|
||||
"duration_ms": 586
|
||||
"duration_ms": 520
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-018",
|
||||
@@ -153,39 +152,39 @@
|
||||
"status": "Verified",
|
||||
"detail": "LocalLambdaStub instantiates (local tier evidence)",
|
||||
"tier": "lifecycle-pipeline",
|
||||
"duration_ms": 138
|
||||
"duration_ms": 137
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-019",
|
||||
"name": "ECS cluster + service (L2 microservice lifecycle evidence)",
|
||||
"status": "Verified",
|
||||
"detail": "L2 composition resolves (simple + complex contracts; offline proxy)",
|
||||
"detail": "L2 composition resolves (simple + complex contracts)",
|
||||
"tier": "lifecycle-pipeline",
|
||||
"duration_ms": 519
|
||||
"duration_ms": 534
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-020",
|
||||
"name": "CloudFront + WAF (L2 static-assets lifecycle evidence)",
|
||||
"status": "Verified",
|
||||
"detail": "L2 composition resolves (simple + complex contracts; offline proxy)",
|
||||
"detail": "L2 composition resolves (simple + complex contracts)",
|
||||
"tier": "lifecycle-pipeline",
|
||||
"duration_ms": 521
|
||||
"duration_ms": 567
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-021",
|
||||
"name": "uptime-kuma (L1 uptime lifecycle evidence)",
|
||||
"status": "Verified",
|
||||
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve",
|
||||
"detail": "terraform files present + simple/complex contracts resolve",
|
||||
"tier": "lifecycle-pipeline",
|
||||
"duration_ms": 562
|
||||
"duration_ms": 606
|
||||
},
|
||||
{
|
||||
"capability_id": "CAP-022",
|
||||
"name": "OIDC role (L1 iam-role lifecycle evidence)",
|
||||
"status": "Verified",
|
||||
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve",
|
||||
"detail": "terraform files present + simple/complex contracts resolve",
|
||||
"tier": "lifecycle-pipeline",
|
||||
"duration_ms": 611
|
||||
"duration_ms": 529
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,51 +1,51 @@
|
||||
# Regression Report — v1.10 Phase 52
|
||||
|
||||
- **Run ID:** `regr-1785591207`
|
||||
- **Run at (UTC):** 2026-08-01T13:33:27Z
|
||||
- **Summary:** {'Verified': 18, 'Decayed': 0, 'Broken': 0, 'Skipped': 4}
|
||||
- **Run ID:** `regr-1785329757`
|
||||
- **Run at (UTC):** 2026-07-29T12:55:57Z
|
||||
- **Summary:** {'Verified': 22, 'Decayed': 0, 'Broken': 0}
|
||||
- **Passed (milestone gate):** True
|
||||
|
||||
| Capability | Name | Tier | Status | Duration (ms) | Detail |
|
||||
|-----------|------|------|--------|--------------|--------|
|
||||
| CAP-001 | contract.schema.json validates sample contracts | local | **Verified** | 235 | exit 0; 2 sample contracts validate |
|
||||
| CAP-002 | environment.schema.json validates env files | local | **Verified** | 201 | exit 0; env schema validates |
|
||||
| CAP-003 | contract_resolver resolves static-assets | local | **Verified** | 261 | exit 0; |
|
||||
| CAP-004 | contract_resolver resolves microservice | local | **Verified** | 259 | exit 0; |
|
||||
| CAP-005 | terraform adapter emits .tf files | local | **Verified** | 337 | exit 0; |
|
||||
| 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** | 91 | exit 0; confidence band=pass |
|
||||
| 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** | 72988 | exit 0; [ 98%]
|
||||
| CAP-001 | contract.schema.json validates sample contracts | local | **Verified** | 252 | exit 0; 2 sample contracts validate |
|
||||
| CAP-002 | environment.schema.json validates env files | local | **Verified** | 196 | exit 0; env schema validates |
|
||||
| CAP-003 | contract_resolver resolves static-assets | local | **Verified** | 258 | exit 0; |
|
||||
| CAP-004 | contract_resolver resolves microservice | local | **Verified** | 264 | exit 0; |
|
||||
| CAP-005 | terraform adapter emits .tf files | local | **Verified** | 314 | exit 0; |
|
||||
| CAP-006 | contract interpolation expands env/contract tokens | local | **Verified** | 223 | exit 0; interpolation ok |
|
||||
| CAP-007 | confidence_signal.compute returns a band | local | **Verified** | 80 | exit 0; confidence band=pass |
|
||||
| CAP-008 | outbox_writer builds a hash-chained item | local | **Verified** | 358 | exit 0; outbox hash chain ok |
|
||||
| CAP-009 | offline pytest suite passes | local | **Verified** | 36065 | exit 0; [ 98%]
|
||||
tests/test_wiz_adapter_real_client.py ......... [100%]
|
||||
|
||||
================= 586 passed, 2 |
|
||||
| CAP-010 | run_ci.sh reproduces CI pipeline locally | local | **Verified** | 73275 | exit 0; resource(s))
|
||||
====================== 462 passe |
|
||||
| CAP-010 | run_ci.sh reproduces CI pipeline locally | local | **Verified** | 40668 | exit 0; resource(s))
|
||||
|
||||
=== PLATFORM CHECK OK ===
|
||||
contract -> resolver -> stack -> adapter -> structure validated (offline, no AWS)
|
||||
check-only: OK
|
||||
|
||||
=== CI PIPELIN |
|
||||
| CAP-011 | headline E2E runs against the local emulating tier (microservice) | local | **Verified** | 634 | exit 0; al-emulator",
|
||||
| CAP-011 | headline E2E runs against the local emulating tier (microservice) | local | **Verified** | 583 | exit 0; al-emulator",
|
||||
"desired_count": 1,
|
||||
"running_count": 1
|
||||
},
|
||||
"outbox_dir": "/tmp/nova_local_e2e_6vnrnin1/outbox",
|
||||
"outbox_dir": "/tmp/acdl_local_e2e_416d0fmr/outbox",
|
||||
"outbox_events": 2,
|
||||
"outbox |
|
||||
| CAP-012 | local E2E on the static-assets stack (no ECS) | local | **Verified** | 584 | exit 0; nova_local_e2e_uq4kkhze/tf",
|
||||
| CAP-012 | local E2E on the static-assets stack (no ECS) | local | **Verified** | 489 | exit 0; acdl_local_e2e_ijhcj1z8/tf",
|
||||
"backend": "local",
|
||||
"ecs": null,
|
||||
"outbox_dir": "/tmp/nova_local_e2e_uq4kkhze/outbox",
|
||||
"outbox_dir": "/tmp/acdl_local_e2e_ijhcj1z8/outbox",
|
||||
"outbox_events": 2,
|
||||
"outbox |
|
||||
| CAP-013 | terraform init+validate+plan live AWS (microservice) | live-aws | **Skipped** | 737 | terraform init: state bucket absent (post-v1.11-teardown, D-096) [microservice] |
|
||||
| CAP-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 | **Skipped** | 664 | nova-outbox absent (post-v1.11-teardown steady state, D-096) |
|
||||
| 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 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** | 138 | LocalLambdaStub instantiates (local tier evidence) |
|
||||
| CAP-019 | ECS cluster + service (L2 microservice lifecycle evidence) | lifecycle-pipeline | **Verified** | 519 | L2 composition resolves (simple + complex contracts; offline proxy) |
|
||||
| CAP-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** | 562 | terraform files present + fmt -check passes + simple/complex contracts resolve |
|
||||
| CAP-022 | OIDC role (L1 iam-role lifecycle evidence) | lifecycle-pipeline | **Verified** | 611 | terraform files present + fmt -check passes + simple/complex contracts resolve |
|
||||
| CAP-013 | terraform init+validate+plan live AWS (microservice) | live-aws | **Verified** | 28811 | terraform init+validate+plan OK (live AWS, microservice) |
|
||||
| CAP-014 | terraform init+validate+plan live AWS (static-assets) | live-aws | **Verified** | 31772 | terraform init+validate+plan OK (live AWS, static-assets) |
|
||||
| CAP-015 | DynamoDB outbox table exists (live AWS) | live-aws | **Verified** | 477 | acdl-outbox exists, item_count=9 |
|
||||
| CAP-016 | S3 state bucket exists + readable (live AWS) | live-aws | **Verified** | 324 | state bucket exists, keys=['platform/terraform.tfstate', 'spike/alb/dev/terraform.tfstate', 'spike/cdn/dev/terraform.tfstate', 'spike/ci-vpc/terraform.tfstate', |
|
||||
| CAP-017 | DynamoDB acdl-contracts table (lifecycle pipeline evidence) | lifecycle-pipeline | **Verified** | 520 | terraform files present + simple/complex contracts resolve |
|
||||
| CAP-018 | Lambda contract-ingestor (local stub + lifecycle evidence) | lifecycle-pipeline | **Verified** | 137 | 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-020 | CloudFront + WAF (L2 static-assets lifecycle evidence) | lifecycle-pipeline | **Verified** | 567 | L2 composition resolves (simple + complex contracts) |
|
||||
| CAP-021 | uptime-kuma (L1 uptime lifecycle evidence) | lifecycle-pipeline | **Verified** | 606 | terraform files present + simple/complex contracts resolve |
|
||||
| CAP-022 | OIDC role (L1 iam-role lifecycle evidence) | lifecycle-pipeline | **Verified** | 529 | terraform files present + simple/complex contracts resolve |
|
||||
|
||||
+21
-1024
File diff suppressed because it is too large
Load Diff
+1
-1478
File diff suppressed because it is too large
Load Diff
+302
-90
@@ -1,112 +1,324 @@
|
||||
# Nova v1.16 — Multi-Persona Code Review (final phase P21)
|
||||
# ACDL v1.11 — Multi-Persona Code Review (P60–P65 retrofit + new work)
|
||||
|
||||
**Reviewer:** lead-developer (model: glm-5.2)
|
||||
**Scope:** v1.16 milestone — 22 tags (v1.15.5..v1.15.26), 20 execution
|
||||
phases + final. Squash-merged to main via `milestone/v1.16-nova-simplification`.
|
||||
**Date:** 2026-07-30
|
||||
**Reviewer:** ci-code-reviewer (model: glm-5.2)
|
||||
**Scope:** v1.11 milestone, branch `milestone/v1.11-restart` — 22 commits
|
||||
(e1bb214..8c09580), 25 files, +790/-142 lines
|
||||
**Date:** 2026-07-29
|
||||
|
||||
> **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.
|
||||
## Commits reviewed
|
||||
|
||||
## Review approach
|
||||
|
||||
The v1.16 milestone is an NFR sweep (no new features). Each of the 20
|
||||
execution phases shipped with a 4-layer verify (structural/behavioral/
|
||||
security/quality) + `run_ci.sh` 3-stage PASS at every phase boundary.
|
||||
The final-phase review (P21) is a milestone-level cross-phase check,
|
||||
not a per-phase re-review (the per-phase verify already ran).
|
||||
| Commit | Phase | Type | Summary |
|
||||
|--------|-------|------|---------|
|
||||
| e1bb214 | 60 | docs | retrofit plan — L1 lifecycle pipeline live-run |
|
||||
| bc9058f | 60 | feat | L1 module lifecycle live run — module fixes (retrofit) |
|
||||
| bb3ac7c | 60 | fix | WAF scope case + VPC modify DependencyViolation |
|
||||
| 0c5c4d1 | 61 | docs | create phase plan — L2 lifecycle pipeline author |
|
||||
| 361fe60 | 61 | feat | L2 lifecycle pipeline — extend matrix + workflows + tests |
|
||||
| 9ac5720 | 61 | verify | 4-layer gate — PASS |
|
||||
| 6441633 | 62 | docs | create phase plan — L2 lifecycle pipeline live run |
|
||||
| 4dad967 | 60 | fix | ALB target group name_prefix — avoid orphaned conflicts |
|
||||
| adfcf86 | 63 | docs | create phase plan — regression registry + cost docs |
|
||||
| b71e63c | 63 | feat | CAP-017..022 regression registry + COST.md |
|
||||
| beac2ef | 63 | verify | 4-layer gate — PASS |
|
||||
| 06f4fc7 | 60 | fix | free disk space in lifecycle jobs |
|
||||
| 92bb03e | 64 | docs | create phase plan — pre-mortem + teardown |
|
||||
| 186cdde | 64 | feat | pre-mortem — v1.10 post-mortem + forward pre-mortem |
|
||||
| 4102950 | 64 | feat | pre-mortem + teardown plan — HITL escalation CHG0680001 |
|
||||
| 7c4fc1f | 64 | feat | teardown complete — zero live ACDL resources remain |
|
||||
| a52f8a5 | 64 | verify | 4-layer gate — PASS |
|
||||
| a03c019 | 60/62 | fix | ALB name_prefix + adapter dedup + L2 composition wiring |
|
||||
| 93a6598 | 65 | docs | create phase plan — rewrite caps + decks |
|
||||
| 6394801 | 65 | feat | rewrite caps — CAP-017..022 Verified via lifecycle pipeline |
|
||||
| fc91f24 | 65 | verify | 4-layer gate — PASS |
|
||||
| 8c09580 | 65 | docs | update v1.11 status — all phases complete |
|
||||
|
||||
## P0 issues (0)
|
||||
|
||||
No blocking issues found. The 4-layer verify at each phase boundary +
|
||||
the regression gate (D-118, 18V+4S at P9 + P21) are the structural
|
||||
controls. No P0 was auto-applied at P21.
|
||||
No blocking issues found. The targeted fixes are correct for their stated
|
||||
purposes. The 447 fast offline tests pass (485/490 collected; 5 slow
|
||||
deselected, including 2 slow regression-integration tests that exercise the
|
||||
CAPABILITY_REGISTRY against the live codebase).
|
||||
|
||||
## P1 issues (0)
|
||||
## P1 issues (5 — should fix)
|
||||
|
||||
No P1 issues flagged. The grill binding decisions (G-111..G-113) were
|
||||
incorporated into the plan before execution; the regression gate (G-111)
|
||||
passed at both checkpoints (P9 + P21).
|
||||
### P1-1: Adapter dedup silently drops resources whose module is not in the registry
|
||||
[correctness] `adapters/terraform/adapter.py:159-170`
|
||||
|
||||
## P2 issues (2 — post-hoc, non-blocking)
|
||||
The new dedup loop only adds resources to `seen` when `tf_dir` is truthy
|
||||
(in the registry). A resource whose module is missing from the registry is
|
||||
**silently dropped** from `merged` — it never reaches `_emit_module_block`,
|
||||
so no error is raised. The pre-dedup code (`parts.extend(... for r in
|
||||
resources)`) would have raised `ValueError("no terraform_dir in registry
|
||||
for module ...")` via `_emit_module_block`, surfacing the misconfiguration.
|
||||
|
||||
### P2-1: Onboarding framing (E-002, deferred from grill)
|
||||
[scope] `.ciagent/PROJECT.md`, `.ciagent/ROADMAP.md`
|
||||
Confirmed by simulation: two resources, one with `module: nonexistent@1.0.0`,
|
||||
produces a `merged` list of length 1 — the unknown-module resource vanishes
|
||||
without diagnostic.
|
||||
|
||||
The grill escalation E-002 (confidence 0.55) flagged that the PROJECT.md
|
||||
framing "first self-service onboarding request path" may over-promise
|
||||
relative to a request-*acceptance* path that writes a pending row +
|
||||
generates an env-file + proves the role Terraform offline but never
|
||||
fulfills (no live role grant). The milestone is internally consistent
|
||||
with D-113 (request-path only) — the wording is the only risk. The
|
||||
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).
|
||||
**Recommendation:** in the dedup loop, when `tf_dir` is `None`, either
|
||||
(a) raise immediately (preserving the prior contract), or (b) append the
|
||||
resource to a separate `unknown` list and extend `parts` with it so
|
||||
`_emit_module_block` raises the descriptive error. As written, a typo in
|
||||
a composition's `module` field (e.g. `iam-role@1.0.0` vs `iam_roles@1.0.0`)
|
||||
will silently omit a resource from the emitted terraform — a class of
|
||||
defect the v1.10 sweep was specifically created to catch.
|
||||
|
||||
### P2-2: REVIEW.md + AUDIT.md not updated during the run
|
||||
[maintainability] `.ciagent/REVIEW.md`, `.ciagent/AUDIT.md`
|
||||
### P1-2: L2 static-assets "modify" example is a no-op — complex ≡ simple
|
||||
[correctness] `modules/l2/static-assets/examples/complex.yml`,
|
||||
`modules/l2/static-assets/composition.json`
|
||||
|
||||
REVIEW.md still held v1.11 content during the v1.16 run (the per-phase
|
||||
verify ran but wasn't persisted to REVIEW.md until P21). AUDIT.md held
|
||||
v1.15 content. Both are reconstructed at P21 (this review + the audit
|
||||
running now). This matches the established convention (REVIEW.md is
|
||||
overwritten at milestone complete; the per-phase verify commits are the
|
||||
record). Not a defect.
|
||||
The complex.yml comment claims "Modify variant: same bucket_name as simple
|
||||
(in-place modify, adds CDN + WAF)". But resolving both examples yields
|
||||
**identical** resource sets: `['s3','cloudfront-distribution',
|
||||
'cloudfront-originaccesscontrol','waf','kms']`. The CDN and WAF are
|
||||
**always present** in the static-assets composition (they are unconditional
|
||||
children + wires); the `waf_enabled`, `default_ttl`, `max_ttl`,
|
||||
`price_class`, `viewer_protocol_policy` inputs in complex.yml have **no
|
||||
corresponding wires** in composition.json and are silently dropped at
|
||||
resolve time. So the L2 static-assets lifecycle cell's "modify" step
|
||||
applies a contract that produces the same terraform as "simple" — it
|
||||
exercises `terraform apply` twice with no change, not a true modify.
|
||||
|
||||
This is not a regression (the inputs were never wired), but the
|
||||
CAPABILITY_INVENTORY claim "CAP-020 Verified live-aws via L2 static-assets
|
||||
lifecycle pipeline (apply/modify/destroy exit 0)" overstates what the
|
||||
modify step proves: it proves idempotent re-apply, not in-place modify.
|
||||
|
||||
**Recommendation:** either (a) wire `waf_enabled`/`default_ttl`/etc. in
|
||||
composition.json so the complex contract genuinely differs, or (b) correct
|
||||
the comment + CAPABILITY_INVENTORY wording to "apply + idempotent re-apply
|
||||
+ destroy" rather than "apply/modify/destroy". The microservice complex
|
||||
example, by contrast, is a real modify (desired_count 1→2) — that one is
|
||||
fine.
|
||||
|
||||
### P1-3: L2 lifecycle scripts ignore the ci-vpc-outputs.json argument
|
||||
[correctness] `scripts/run_l2_lifecycle_test.sh:14`,
|
||||
`scripts/run_l2_lifecycle_destroy.sh:12`
|
||||
|
||||
Both L2 scripts declare `Usage: ... <module> <example> [ci-vpc-outputs.json]`
|
||||
but neither reads `$3`/`$2`. The microservice composition references the
|
||||
platform VPC via `terraform_remote_state` (data source), and the script
|
||||
sets `ACDL_REMOTE_STATE_KEY=spike/ci-vpc/terraform.tfstate` so the data
|
||||
source reads from the CI VPC state — that part is correct. But the
|
||||
`ci-vpc-outputs.json` argument is positional noise: the workflow passes
|
||||
it (`run_l2_lifecycle_test.sh ${{ matrix.module }} simple
|
||||
/tmp/ci-vpc-outputs.json`) and it is silently ignored. The L1 scripts
|
||||
(`run_lifecycle_test.sh`) inject VPC outputs by rewriting the contract in
|
||||
Python; the L2 path takes a different approach (remote state) and does not
|
||||
need the file, so the argument is vestigial, not a bug — but the usage
|
||||
string advertises a feature the script does not provide, which will
|
||||
confuse a future maintainer who assumes parity with the L1 scripts.
|
||||
|
||||
**Recommendation:** remove the `[ci-vpc-outputs.json]` token from the
|
||||
usage strings (or add a comment explaining the L2 path uses remote state
|
||||
and the arg is accepted-but-ignored for workflow-argument parity).
|
||||
|
||||
### P1-4: CAPABILITY_INVENTORY summary table is stale (says 16, body lists 22)
|
||||
[maintainability] `.ciagent/CAPABILITY_INVENTORY.md:9-16`
|
||||
|
||||
The Summary table still reads "Verified 16 / Decayed 0 / Broken 0 / Total
|
||||
16" — the v1.10 sweep count. The body (lines 93-110) now lists CAP-017..022
|
||||
as **Verified** via the lifecycle pipeline, bringing the real total to 22.
|
||||
The two counts disagree: a reader scanning the summary sees 16 Verified; a
|
||||
reader scanning the inventory body sees 22 Verified. The PRE_MORTEM
|
||||
(lines 82-83) and CAPABILITY_INVENTORY prose both assert all 22 are
|
||||
Verified, but the headline table was not updated in the P65 rewrite.
|
||||
|
||||
**Recommendation:** update the Summary table to "Verified 22 / Decayed 0
|
||||
/ Broken 0 / Total 22" and add CAP-017..022 rows to the Inventory table
|
||||
(the body section "Cloud capabilities NOT re-verified..." is now
|
||||
mis-titled — they ARE verified, just via the lifecycle-pipeline tier).
|
||||
|
||||
### P1-5: CAP-017..022 regression checks are offline proxies, not pipeline evidence
|
||||
[adversarial] `core/regression_verify.py:432-519`,
|
||||
`.ciagent/CAPABILITY_INVENTORY.md:93-110`
|
||||
|
||||
The CAP-017..022 checks (`_check_cap_017_dynamodb` etc.) call
|
||||
`_check_lifecycle_module_terraform` / `_check_lifecycle_l2_module`, which
|
||||
verify only that (a) the terraform dir + required files exist and (b) the
|
||||
example contracts **resolve** (resolver exit 0). They do **not** run
|
||||
`terraform validate`, do not run apply/modify/destroy, and do not query
|
||||
the pipeline's actual green/red status. The CAPABILITY_INVENTORY claims
|
||||
"Evidence = L1 rds module lifecycle pipeline green (terraform validate +
|
||||
contracts resolve)" — but the check does not run terraform validate, and
|
||||
"lifecycle pipeline green" is asserted, not verified by the regression
|
||||
gate.
|
||||
|
||||
This means the lifecycle-pipeline evidence CAN be faked at the regression
|
||||
tier: a module whose terraform is syntactically broken (e.g.
|
||||
`scope = upper(var.scope)` removed, or a missing required variable) would
|
||||
still pass `_check_lifecycle_module_terraform` as long as the files exist
|
||||
and the resolver runs. The real green/red evidence lives only in the
|
||||
workflow run history (Gitea/GitHub Actions), which the regression gate does
|
||||
not read.
|
||||
|
||||
**Mitigation context:** the modules-lifecycle workflow IS the live
|
||||
evidence — when it runs on a PR, the cells genuinely apply/modify/destroy
|
||||
against live AWS. The gap is that the *regression gate* (which gates
|
||||
milestone COMPLETE) trusts the workflow will be run, rather than proving it
|
||||
was run and passed. A milestone could in principle be marked COMPLETE with
|
||||
CAP-017..022 "Verified" if the regression gate runs but the workflow was
|
||||
never executed (e.g. workflow_dispatch never triggered, or the PR was
|
||||
merged without the workflow running).
|
||||
|
||||
**Recommendation:** (a) tighten the CAP-017..022 check docstrings + the
|
||||
CAPABILITY_INVENTORY wording to "terraform files present + contracts
|
||||
resolve (offline proxy; live apply/modify/destroy verified by the
|
||||
modules-lifecycle workflow run, not by this gate)"; and/or (b) add a
|
||||
`terraform validate` step to `_check_lifecycle_module_terraform` (slow but
|
||||
cheap relative to init+apply) so at least HCL syntax is verified at the
|
||||
gate. The teardown trustworthiness (P64) is good — `ci-vpc-destroy` runs
|
||||
`if: always()` and the decommission `---ci---` block is the audit trail.
|
||||
|
||||
## P2 issues (4 — post-hoc)
|
||||
|
||||
### P2-1: ALB `name_prefix = "tg-ci-"` discards `var.name` entirely
|
||||
[maintainability] `modules/l1/alb/terraform/main.tf:9`
|
||||
|
||||
The fix replaces `name = var.name` with `name_prefix = "tg-ci-"` (a
|
||||
hardcoded literal). This is the correct terraform pattern for
|
||||
create_before_destroy resources with name-uniqueness constraints, and the
|
||||
commit message explains the orphaned-resource motivation well. However
|
||||
the target group name is now non-configurable (always `tg-ci-<random>`),
|
||||
and the `var.name` variable is no longer used by the target group at all
|
||||
(it is still used by `aws_lb.this.name`). A consumer who sets `name:
|
||||
my-app` gets an LB named `my-app` but a target group named `tg-ci-...` —
|
||||
inconsistent tagging. Consider `name_prefix = "${var.name}-"` to keep the
|
||||
consumer's name as a prefix while preserving uniqueness. Post-hoc: not
|
||||
blocking; the lifecycle pipeline is the only current consumer and `tg-ci-`
|
||||
is fine for CI.
|
||||
|
||||
### P2-2: No test covers the new dedup merge behavior or `ACDL_REMOTE_STATE_KEY`
|
||||
[testing] `tests/test_adapter.py`, `tests/test_pipeline_contract.py`
|
||||
|
||||
The adapter gained (a) a dedup-merge loop for multi-resource L1s sharing a
|
||||
terraform dir and (b) `ACDL_REMOTE_STATE_KEY` env override for the remote
|
||||
state data block. Neither has a unit test:
|
||||
- No test asserts that two resources with the same `module` collapse to one
|
||||
`module "<first_id>" { ... }` block with merged inputs.
|
||||
- No test asserts that `ACDL_REMOTE_STATE_KEY` overrides the default
|
||||
`platform/terraform.tfstate` key in the emitted `data
|
||||
terraform_remote_state` block.
|
||||
- No test covers the L2 lifecycle scripts (`run_l2_lifecycle_test.sh` /
|
||||
`run_l2_lifecycle_destroy.sh`) — the L1 equivalents are also untested at
|
||||
the script level, so this is consistent with existing practice, but the
|
||||
L2 scripts are new in this session and the `ACDL_REMOTE_STATE_KEY` wiring
|
||||
is the load-bearing correctness mechanism for the microservice lifecycle.
|
||||
|
||||
The 485 offline tests adequately cover the *contract* (pipeline schema,
|
||||
byte-identical workflows, matrix membership, job needs) — the
|
||||
`TestModulesLifecyclePipeline` class is solid (89 tests pass). The gap is
|
||||
adapter *behavior* at the unit level.
|
||||
|
||||
**Recommendation:** add a `test_adapter_dedup_merges_same_module` and a
|
||||
`test_adapter_remote_state_key_override` to `tests/test_adapter.py`.
|
||||
|
||||
### P2-3: `waf` complex example uses `scope: CLOUDFRONT` but WAF scope is now `upper()`'d
|
||||
[correctness] `modules/l1/waf/examples/complex.yml:8`,
|
||||
`modules/l1/waf/terraform/locals.tf:3`
|
||||
|
||||
The `locals.tf` change `scope = upper(var.scope)` is the correct defensive
|
||||
fix (the AWS provider requires `CLOUDFRONT`/`REGIONAL` regardless of input
|
||||
case). The complex.yml was simultaneously changed from `scope: cloudfront`
|
||||
to `scope: CLOUDFRONT`. Both are now correct, but the example's uppercase
|
||||
value is now redundant with the `upper()` — a future reader may wonder
|
||||
which is authoritative. Minor; the defensive `upper()` is the right call
|
||||
and the example matching it is fine. Post-hoc only.
|
||||
|
||||
### P2-4: COST.md reproducibility snippet could leak the account ID via CloudTrail
|
||||
[security] `.ciagent/COST.md:106`
|
||||
|
||||
COST.md contains the AWS account ID `581513795199` in multiple places
|
||||
(summary, S3 bucket name, methodology). This is consistent with the rest of
|
||||
the repo (the bucket name `acdl-tfstate-581513795199-us-east-1` is hardcoded
|
||||
in `adapter.py:130` and `adapter.py:146`), so it is not new leakage and not
|
||||
a regression. No actual secret material (access keys, secret access keys)
|
||||
appears in COST.md, PRE_MORTEM.md, CAPABILITY_INVENTORY.md, or the workflow
|
||||
files — all credential references use `${{ secrets.ACDL_AWS_* }}` or env
|
||||
var names only. The `.ciagent/PROJECT.md:731` reference to a deactivated
|
||||
root key is redacted (`AKIA…ROOT-DEACTIVATED`). **No credential leakage
|
||||
found.** The P2 is only that the account ID is published; if the account
|
||||
is meant to be opaque, this is an accepted exposure (the bucket name
|
||||
already requires it).
|
||||
|
||||
## What is correct
|
||||
|
||||
- **State-bucket drift fix (P1):** `adapter.py:117` now emits
|
||||
`nova-tfstate-*` (matching the live bucket renamed in v1.15 P4). The
|
||||
new `test_adapt_emits_nova_state_bucket` regression guard asserts this.
|
||||
- **Kyverno label fix (P1):** `require-resource-labels.yml` enforces
|
||||
`nova:*` labels (consistent with `nova_tagging.py` hard-fail on
|
||||
`acdl:*`). No policy contradiction.
|
||||
- **Ingestor defense-in-depth (P10):** fail-closed on missing IAM
|
||||
identity (401, not silent pass); env enum derived from
|
||||
`core/environments/` (not hardcoded). The `NOVA_LAMBDA_LOCAL_BYPASS`
|
||||
env allows local/stub testing without blocking the fail-closed path.
|
||||
- **Payload validation (P11):** 256 KB size cap + contract.schema.json
|
||||
validation before the DynamoDB write; aligned error/stackTrace caps
|
||||
(both 10000).
|
||||
- **Regression gate (G-111):** CAP-013..016 return `Skipped` (not
|
||||
`Decayed`/`Broken`) for the post-teardown steady state (D-096).
|
||||
`passed` accepts Skipped. Gate passes at 18V+4S.
|
||||
- **Workflow generator (P8):** `sync_workflows.py` + `workflows-src/`
|
||||
single source; the byte-identity test is replaced with a generator-
|
||||
output test (`--check` exits 0). The 3 pairs are no longer hand-synced.
|
||||
- **Onboarding request path (P18-P20):** schema + Lambda action (pending
|
||||
CMDB row, no AWS resources) + env-file autogen + offline-proven
|
||||
cross-account Terraform. Self-service message (no "contact the platform
|
||||
team"). Real AWS provisioning explicitly deferred (D-113/D-114).
|
||||
- **Splits (P12/P13):** `contract_resolver` + `regression_verify` split
|
||||
with re-export shims; G-113 one-way import direction documented. All
|
||||
tests pass without modification (backwards compat preserved).
|
||||
- **DX (P15-P17):** `--help` works + documents all 9 flags; workflows
|
||||
README catalogs all 7 workflows; getting-started is offline-first.
|
||||
- **Regression gate:** 18 Verified + 4 Skipped at P9 + P21 (0 Decayed/
|
||||
Broken). The 4 Skipped are the post-v1.11-teardown live-AWS caps.
|
||||
- **WAF scope fix (`upper(var.scope)`):** correct and defensive; AWS
|
||||
provider v5 requires uppercase. The `local.scope` indirection is clean.
|
||||
- **VPC `create_before_destroy` + same-CIDR complex example:** correct
|
||||
fix for the DependencyViolation on modify. Using the same CIDR means
|
||||
terraform modifies in-place rather than replacing the VPC (which would
|
||||
cascade-fail on dependent subnets/IGW). The `create_before_destroy`
|
||||
lifecycle is the right guard.
|
||||
- **ALB `name_prefix`:** correct terraform pattern for
|
||||
create_before_destroy + name-uniqueness; well-documented commit message.
|
||||
- **Adapter dedup (for the registered-module case):** correct —
|
||||
multi-resource L1s like cloudfront (distribution + OAC) correctly merge
|
||||
into one `module "cloudfront-distribution" { ... }` block. The merge
|
||||
preserves first-resource inputs and union of outputs. (The
|
||||
unregistered-module drop is P1-1, a separate concern.)
|
||||
- **L2 composition wiring (`ecr.inputs.name`, `roles.inputs.role_name`):**
|
||||
correct. Resolving microservice complex now shows `ecr.inputs.name =
|
||||
"app-repo"` and `roles.inputs.role_name = "app-role"` (defaults applied
|
||||
since the contract doesn't set `name`). Previously these would have hit
|
||||
the "missing required arg" defect class from the v1.10 sweep.
|
||||
- **Microservice complex = real modify:** `desired_count: 2` (vs simple's
|
||||
default 1) is a genuine in-place modify — confirmed by resolving both
|
||||
and diffing `service-service.inputs.desired_count`.
|
||||
- **`ACDL_REMOTE_STATE_KEY` plumbing:** correct end-to-end — the L2 scripts
|
||||
export it, the adapter reads it with a sensible default, and the
|
||||
microservice composition's `terraform_remote_state` data block picks it
|
||||
up. This cleanly separates the short-lived CI VPC state from the
|
||||
long-lived platform VPC state.
|
||||
- **Workflow structure:** `l2-lifecycle` correctly `needs: ci-vpc-apply`;
|
||||
`ci-vpc-destroy` correctly `needs: [lifecycle, l2-lifecycle]` and
|
||||
`if: always()`. The 7 new L2 pipeline-contract tests assert all of this.
|
||||
- **Byte-identical workflows:** `.gitea` and `.github` modules-lifecycle.yml
|
||||
are byte-identical (test asserts this); the `test_workflow_has_four_jobs`
|
||||
rename from three→four is correct.
|
||||
- **Adapter line count:** 194 lines — under the 200-line ceiling, still a
|
||||
clean stateless assembler. The dedup logic added ~16 lines without
|
||||
bloating.
|
||||
- **Teardown verification (P64):** trustworthy in structure — the
|
||||
`ci-vpc-destroy` job runs unconditionally and the decommission
|
||||
`---ci---` block is the audit trail. The adversarial concern (P1-5) is
|
||||
about the regression gate trusting the workflow ran, not about the
|
||||
teardown itself being fakeable.
|
||||
- **Security:** no credential leakage in any reviewed file. All AWS auth
|
||||
in workflows uses `${{ secrets.* }}`; COST.md references only env var
|
||||
names and a redacted/deactivated root key ID.
|
||||
|
||||
## Test coverage assessment
|
||||
## Test coverage assessment (485 offline tests)
|
||||
|
||||
~635 tests pass (was ~620 at v1.15.4). New test files:
|
||||
- `tests/test_onboarding.py` (3 tests — env-file generation)
|
||||
- `tests/test_onboarding_terraform.py` (3 tests — terraform validate + tags)
|
||||
- `tests/test_docs_coverage.py` (expanded — workflows README catalog)
|
||||
- **Adequate:** pipeline contract (89 tests), schema validation, contract
|
||||
resolution, adapter emission (basic), confidence signal, outbox,
|
||||
interpolation, local emulators, module-standards file presence, design-doc
|
||||
currency.
|
||||
- **Gaps (post-hoc):**
|
||||
1. Adapter dedup merge behavior (P2-2) — no unit test.
|
||||
2. `ACDL_REMOTE_STATE_KEY` override (P2-2) — no unit test.
|
||||
3. CAP-017..022 regression checks (P1-5) — not exercised at the unit
|
||||
level; the 2 slow tests in `test_verify_regression_mode.py` run the
|
||||
full registry but are `@pytest.mark.slow` and deselected from the
|
||||
fast suite, so a CI run of the 485 fast tests does not verify
|
||||
CAP-017..022 even at the offline-proxy level.
|
||||
4. WAF `upper()` scope — no test asserts the locals transform; relies
|
||||
on the lifecycle pipeline cell to catch a regression.
|
||||
5. ALB `name_prefix` — no test asserts the target group uses
|
||||
`name_prefix` (P2-1 context).
|
||||
|
||||
New tests in existing files: `test_adapt_emits_nova_state_bucket`,
|
||||
`test_onboarding_message_says_nova_not_acdl`, `test_no_identity_fails_closed`,
|
||||
`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`.
|
||||
The 485 count is honest (447 pass fast, 5 deselected slow, 485/490
|
||||
collected). The gap is behavioral coverage of the new adapter + module
|
||||
logic, not contract/schema coverage.
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS — 0 P0, 0 P1, 2 P2 (post-hoc, accepted).** The v1.16 NFR milestone
|
||||
is complete. All 20 requirements (REQ-165..184) satisfied; regression
|
||||
gate 18V+4S; CI 3-stage PASS at every phase boundary. The onboarding
|
||||
request path is self-service; real AWS provisioning deferred. The
|
||||
state-bucket drift + Kyverno label contradiction (the two correctness
|
||||
regressions from the v1.15 rebrand) are fixed with regression guards.
|
||||
**PASS with P1 flags for post-hoc review.** No P0 fixes applied. The
|
||||
milestone's structural controls (regression gate, mandatory teardown,
|
||||
byte-identical workflows, byte-identical contract↔workflow tests) are
|
||||
sound. The most material finding is P1-5 (the regression gate's
|
||||
CAP-017..022 evidence is an offline proxy, not live pipeline evidence) —
|
||||
this is a repeat of the v1.10 "VERIFY was diff-scoped" structural defect
|
||||
in a milder form: the gate trusts the workflow was run rather than proving
|
||||
it. The mitigations in PRE_MORTEM (FM-1..FM-4) acknowledge related risks;
|
||||
P1-5 is the specific instance for the lifecycle-pipeline tier.
|
||||
+2
-462
@@ -1,4 +1,4 @@
|
||||
# Nova — Roadmap
|
||||
# ACDL — Roadmap
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1063,7 +1063,7 @@ Docs-only NFR patch (no code changes).
|
||||
|
||||
---
|
||||
|
||||
## v1.14 (complete — NFR Refinement: bug fixes, security, stubs, tests, docs, tag `v1.13.24`)
|
||||
## v1.14 (active — NFR Refinement: bug fixes, security, stubs, tests, docs)
|
||||
|
||||
The v1.14 milestone is a 20-phase NFR sweep — no new features. It clears
|
||||
the open P1/P2 backlog from the v1.11 review, hardens the security
|
||||
@@ -1432,463 +1432,3 @@ 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.
|
||||
|
||||
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.
|
||||
|
||||
+9
-13
@@ -2,13 +2,13 @@
|
||||
"projects": [
|
||||
{
|
||||
"slug": "acdl",
|
||||
"name": "Nova — The New Dawn of DevSecOps",
|
||||
"name": "Agentic Cloud Delivery Platform",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"active_project": "acdl",
|
||||
"active_projects": ["acdl"],
|
||||
"active_milestone": "v1.21",
|
||||
"active_milestone": "v1.14",
|
||||
"autonomy": {
|
||||
"level": "full",
|
||||
"escalation_hooks": ["deploy", "delete_data", "merge_to_main"],
|
||||
@@ -37,13 +37,14 @@
|
||||
"escalate_high_severity": true,
|
||||
"bash_allowlist": {
|
||||
"allowed_commands": [
|
||||
"npm", "node", "npx", "pnpm", "yarn",
|
||||
"git", "ls", "cat", "head", "tail", "wc",
|
||||
"echo", "mkdir", "cp", "mv", "rm", "touch",
|
||||
"pwd", "which", "env", "printenv",
|
||||
"python3", "pytest", "pip",
|
||||
"terraform", "checkov",
|
||||
"jest", "eslint", "tsc", "prettier",
|
||||
"curl", "wget",
|
||||
"docker", "docker-compose"
|
||||
"docker", "docker-compose",
|
||||
"ts-node", "tsx"
|
||||
],
|
||||
"max_output_bytes": 1048576,
|
||||
"timeout_ms": 30000,
|
||||
@@ -58,8 +59,7 @@
|
||||
}
|
||||
},
|
||||
"git": {
|
||||
"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.",
|
||||
"branching_strategy": "phase",
|
||||
"auto_commit": true,
|
||||
"auto_push": true
|
||||
},
|
||||
@@ -125,7 +125,6 @@
|
||||
},
|
||||
"ollama-cloud": {
|
||||
"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",
|
||||
"model_profile": "quality",
|
||||
"timeout_ms": 60000
|
||||
@@ -192,11 +191,9 @@
|
||||
{
|
||||
"name": "frontend-engineer",
|
||||
"domain": "frontend",
|
||||
"active": false,
|
||||
"frameworks": ["react", "next.js"],
|
||||
"constraints": ["component-first", "server-components", "minimal-client-js"],
|
||||
"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."
|
||||
"territory": ["**/components/**", "**/pages/**", "**/hooks/**", "**/styles/**", "**/*.tsx", "**/*.css", "**/*.vue"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -208,6 +205,5 @@
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"persist": true
|
||||
},
|
||||
"strategic_direction_file": ".ciagent/NORTH_STAR.md"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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 @@
|
||||
# Nova CI Pipeline (dev environment)
|
||||
# ACDL CI Pipeline — Gitea Actions (dev environment)
|
||||
#
|
||||
# This workflow implements the central pipeline contract:
|
||||
# pipelines/ci.yml (validated against schemas/pipeline.schema.json)
|
||||
|
||||
+14
-15
@@ -1,4 +1,4 @@
|
||||
# Nova Reusable Deploy Workflow (dev environment)
|
||||
# ACDL Reusable Deploy Workflow — Gitea Actions (dev environment)
|
||||
#
|
||||
# This reusable workflow implements the central deployment pipeline contract:
|
||||
# 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.
|
||||
#
|
||||
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
|
||||
# uses: nova/.github/workflows/deploy.yml@v1.19
|
||||
# uses: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea)
|
||||
# uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
|
||||
#
|
||||
# Unversioned references (@main, bare) are discouraged — the consumer's setup
|
||||
@@ -26,7 +26,7 @@
|
||||
# platform log) for auditability.
|
||||
#
|
||||
# Inputs:
|
||||
# contract — path to the consumer's contract YAML (default .nova/contract.yml)
|
||||
# contract — path to the consumer's contract YAML (default .acdl/contract.yml)
|
||||
# mode — full | plan-only | check-only (default full; dev = full apply,
|
||||
# higher environments hold for HITL — the calling repo or the
|
||||
# forge environment gate enforces that)
|
||||
@@ -38,12 +38,12 @@
|
||||
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
|
||||
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
|
||||
#
|
||||
# Override (where OIDC is unavailable, e.g. pending
|
||||
# upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY
|
||||
# Override (where OIDC is unavailable, e.g. Gitea pending
|
||||
# go-gitea/gitea#36988): set ACDL_AWS_ACCESS_KEY_ID + ACDL_AWS_SECRET_ACCESS_KEY
|
||||
# as repository secrets. The platform-managed scheduled pipeline rotates
|
||||
# the key on a daily cadence. When .env.secrets is used locally instead,
|
||||
# rotating the key out of band is the consumer's responsibility.
|
||||
name: nova-deploy
|
||||
name: acdl-deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -51,7 +51,7 @@ on:
|
||||
contract:
|
||||
description: Path to the consumer contract YAML (in the consumer repo)
|
||||
type: string
|
||||
default: .nova/contract.yml
|
||||
default: .acdl/contract.yml
|
||||
mode:
|
||||
description: Pipeline mode — full (apply), plan-only, check-only, or decommission
|
||||
type: string
|
||||
@@ -102,11 +102,10 @@ jobs:
|
||||
- name: Configure AWS credentials (OIDC default + static-key override)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
# 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) || '' }}
|
||||
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) || '' }}
|
||||
aws-region: us-east-1
|
||||
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- name: Run the platform pipeline
|
||||
working-directory: ${{ github.workspace }}
|
||||
@@ -146,7 +145,7 @@ jobs:
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
aws lambda invoke-function-url \
|
||||
--function-url "${{ secrets.NOVA_LAMBDA_URL }}" \
|
||||
--function-url "${{ secrets.ACDL_LAMBDA_URL }}" \
|
||||
--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'}))")" \
|
||||
/dev/null || true
|
||||
@@ -154,13 +153,13 @@ jobs:
|
||||
- name: Upload emitted Terraform
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nova-terraform
|
||||
path: /tmp/nova_platform_run/tf/*.tf
|
||||
name: acdl-terraform
|
||||
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload platform log
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nova-platform-log
|
||||
name: acdl-platform-log
|
||||
path: platform/logs/
|
||||
if-no-files-found: warn
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova Modules Lifecycle Pipeline (dev environment)
|
||||
# ACDL Modules Lifecycle Pipeline — Gitea Actions (dev environment)
|
||||
#
|
||||
# Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through
|
||||
# apply→modify→destroy against live AWS. No per-module Python. The "test" =
|
||||
@@ -9,13 +9,13 @@
|
||||
# terraform files); the composition must be deterministic.
|
||||
#
|
||||
# This workflow implements pipelines/modules-lifecycle.yml (byte-identical
|
||||
# in .github/workflows/).
|
||||
# in .gitea/workflows/ and .github/workflows/).
|
||||
#
|
||||
# Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to
|
||||
# "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast,
|
||||
# no AWS mutation, validates the contract->resolver->adapter->plan chain
|
||||
# for every module on every PR, with no AWS credentials or cost). Set to
|
||||
# "full" via workflow_dispatch (or the NOVA_LIFECYCLE_MODE repo variable)
|
||||
# "full" via workflow_dispatch (or the ACDL_LIFECYCLE_MODE repo variable)
|
||||
# to run the real apply→modify→destroy against live AWS. In plan mode the
|
||||
# short-lived CI VPC apply/destroy jobs are skipped (nothing is applied).
|
||||
#
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
ci-vpc-apply:
|
||||
name: CI VPC apply
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }}
|
||||
if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Terraform 1.9.*
|
||||
@@ -60,8 +60,8 @@ jobs:
|
||||
- name: Apply CI VPC
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
terraform init -input=false -lock=false
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
matrix:
|
||||
module: [s3, kms-key, ecr, ecs-cluster, iam-role, cloudfront, waf, vpc, alb, ecs-service, rds, uptime]
|
||||
env:
|
||||
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }}
|
||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||
- name: Read CI VPC outputs
|
||||
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
|
||||
if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }}
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
terraform init -input=false -lock=false
|
||||
terraform output -json > /tmp/ci-vpc-outputs.json
|
||||
- name: Apply (simple)
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
|
||||
- name: Modify (complex)
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
|
||||
- name: Destroy
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
|
||||
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
matrix:
|
||||
module: [static-assets, microservice]
|
||||
env:
|
||||
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }}
|
||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||
- name: Read CI VPC outputs
|
||||
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
|
||||
if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }}
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
terraform init -input=false -lock=false
|
||||
terraform output -json > /tmp/ci-vpc-outputs.json
|
||||
- name: Apply (simple)
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
|
||||
- name: Modify (complex)
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
|
||||
- name: Destroy
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_l2_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
|
||||
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
name: CI VPC destroy
|
||||
needs: [lifecycle, l2-lifecycle]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }}
|
||||
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Terraform 1.9.*
|
||||
@@ -199,8 +199,8 @@ jobs:
|
||||
- name: Destroy CI VPC
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
terraform init -input=false -lock=false
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# Nova Slides Render — re-renders presentation deck when source files change.
|
||||
name: Nova Slides Render
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'docs/presentations/**'
|
||||
- 'scripts/render_slides.sh'
|
||||
- 'assets/nova-sp-theme.css'
|
||||
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' }
|
||||
- name: Install Chrome
|
||||
run: |
|
||||
npx --yes @marp-team/marp-cli@latest --version
|
||||
npx --yes @mermaid-js/mermaid-cli --version
|
||||
- name: Render slides
|
||||
run: bash scripts/render_slides.sh
|
||||
- name: Commit rendered artifacts
|
||||
run: |
|
||||
git config user.name "nova-slides-bot"
|
||||
git config user.email "bot@nova.local"
|
||||
git add docs/presentations/*.html docs/presentations/*.pptx docs/presentations/assets/png/*.png
|
||||
git diff --cached --quiet || git commit -m "chore(slides): re-render deck [skip ci]"
|
||||
git push
|
||||
@@ -1,45 +0,0 @@
|
||||
# GitHub Workflows — Nova Platform CI/CD Catalog
|
||||
|
||||
This directory contains the GitHub Actions workflows for the Nova
|
||||
platform. 3 are generated from `workflows-src/<name>`; 4 are GitHub-only.
|
||||
|
||||
## Shared workflows (generated from source)
|
||||
|
||||
These 3 are generated from `workflows-src/<name>`. Run `python3 scripts/sync_workflows.py --check` to verify
|
||||
no drift.
|
||||
|
||||
| Workflow | Trigger | Inputs | Required Secrets | Purpose |
|
||||
|----------|---------|--------|------------------|---------|
|
||||
| `ci.yml` | `pull_request: [main]` | — | — | Lint + test + check-only (runs on every PR) |
|
||||
| `deploy.yml` | `workflow_call` (reusable) + `push: [main]` | `contract` (string, required), `mode` (string, default `deploy`), `changeRequestId` (string), `environment` (string) | `NOVA_AWS_ACCESS_KEY_ID`, `NOVA_AWS_SECRET_ACCESS_KEY`, `NOVA_AWS_DEFAULT_REGION`, `NOVA_KMS_KEY_ID`, `NOVA_LAMBDA_URL` | Reusable deploy workflow (invoked by consumer repos via `uses: nova/.github/workflows/deploy.yml@v1.19`) |
|
||||
| `modules-lifecycle.yml` | `pull_request: [main]` + `workflow_dispatch` | `lifecycle_mode` (string, default `plan` — `plan` or `full`) | `NOVA_AWS_ACCESS_KEY_ID`, `NOVA_AWS_SECRET_ACCESS_KEY`, `NOVA_AWS_DEFAULT_REGION`, `NOVA_AWS_ACCOUNT_ID` | L1 + L2 module lifecycle pipeline (plan-only default; full apply/modify/destroy on override) |
|
||||
|
||||
## GitHub-only workflows
|
||||
|
||||
These 4 have no counterpart (the dev forge lacks the features
|
||||
they require — reusable workflows, matrix `needs`, release API).
|
||||
|
||||
| Workflow | Trigger | Inputs | Required Secrets | Purpose |
|
||||
|----------|---------|--------|------------------|---------|
|
||||
| `platform-test.yml` | `pull_request: [main]` | — | — | Lint + unit + integration + schema-validation (replaces `ci.yml` for PRs) |
|
||||
| `primitives-plan.yml` | `pull_request: [main]` | — | `NOVA_AWS_*` | Plan-only for all L1 primitives (matrix) |
|
||||
| `patterns-plan.yml` | `pull_request: [main]` | — | `NOVA_AWS_*` | Plan-only for all L2 modules (matrix) |
|
||||
| `release.yml` | `push: [main]` | — | `NOVA_RELEASE_TOKEN` | Semver tag + MAJOR.MINOR/MAJOR floating-tag maintenance + release creation on merge to main |
|
||||
|
||||
## Reusable deploy workflow (`deploy.yml`)
|
||||
|
||||
Consumer repos invoke the deploy workflow via a versioned tag:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
deploy:
|
||||
uses: nova/.github/workflows/deploy.yml@v1.19
|
||||
with:
|
||||
contract: .nova/contract.yml
|
||||
environment: dev
|
||||
secrets: inherit
|
||||
```
|
||||
|
||||
The workflow checks out the consumer repo + the Nova platform repo, runs
|
||||
`scripts/run_platform.sh`, and posts deploy outputs as a PR comment +
|
||||
to SSM Parameter Store.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova CI Pipeline (dev environment)
|
||||
# ACDL CI Pipeline — Gitea Actions (dev environment)
|
||||
#
|
||||
# This workflow implements the central pipeline contract:
|
||||
# pipelines/ci.yml (validated against schemas/pipeline.schema.json)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova Reusable Deploy Workflow (dev environment)
|
||||
# ACDL Reusable Deploy Workflow — Gitea Actions (dev environment)
|
||||
#
|
||||
# This reusable workflow implements the central deployment pipeline contract:
|
||||
# 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.
|
||||
#
|
||||
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
|
||||
# uses: nova/.github/workflows/deploy.yml@v1.19
|
||||
# uses: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea)
|
||||
# uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
|
||||
#
|
||||
# Unversioned references (@main, bare) are discouraged — the consumer's setup
|
||||
@@ -26,7 +26,7 @@
|
||||
# platform log) for auditability.
|
||||
#
|
||||
# Inputs:
|
||||
# contract — path to the consumer's contract YAML (default .nova/contract.yml)
|
||||
# contract — path to the consumer's contract YAML (default .acdl/contract.yml)
|
||||
# mode — full | plan-only | check-only (default full; dev = full apply,
|
||||
# higher environments hold for HITL — the calling repo or the
|
||||
# forge environment gate enforces that)
|
||||
@@ -38,12 +38,12 @@
|
||||
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
|
||||
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
|
||||
#
|
||||
# Override (where OIDC is unavailable, e.g. pending
|
||||
# upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY
|
||||
# Override (where OIDC is unavailable, e.g. Gitea pending
|
||||
# go-gitea/gitea#36988): set ACDL_AWS_ACCESS_KEY_ID + ACDL_AWS_SECRET_ACCESS_KEY
|
||||
# as repository secrets. The platform-managed scheduled pipeline rotates
|
||||
# the key on a daily cadence. When .env.secrets is used locally instead,
|
||||
# rotating the key out of band is the consumer's responsibility.
|
||||
name: nova-deploy
|
||||
name: acdl-deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -51,7 +51,7 @@ on:
|
||||
contract:
|
||||
description: Path to the consumer contract YAML (in the consumer repo)
|
||||
type: string
|
||||
default: .nova/contract.yml
|
||||
default: .acdl/contract.yml
|
||||
mode:
|
||||
description: Pipeline mode — full (apply), plan-only, check-only, or decommission
|
||||
type: string
|
||||
@@ -102,11 +102,10 @@ jobs:
|
||||
- name: Configure AWS credentials (OIDC default + static-key override)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
# 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) || '' }}
|
||||
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) || '' }}
|
||||
aws-region: us-east-1
|
||||
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- name: Run the platform pipeline
|
||||
working-directory: ${{ github.workspace }}
|
||||
@@ -146,7 +145,7 @@ jobs:
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
aws lambda invoke-function-url \
|
||||
--function-url "${{ secrets.NOVA_LAMBDA_URL }}" \
|
||||
--function-url "${{ secrets.ACDL_LAMBDA_URL }}" \
|
||||
--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'}))")" \
|
||||
/dev/null || true
|
||||
@@ -154,13 +153,13 @@ jobs:
|
||||
- name: Upload emitted Terraform
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nova-terraform
|
||||
path: /tmp/nova_platform_run/tf/*.tf
|
||||
name: acdl-terraform
|
||||
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload platform log
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nova-platform-log
|
||||
name: acdl-platform-log
|
||||
path: platform/logs/
|
||||
if-no-files-found: warn
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova Modules Lifecycle Pipeline (dev environment)
|
||||
# ACDL Modules Lifecycle Pipeline — Gitea Actions (dev environment)
|
||||
#
|
||||
# Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through
|
||||
# apply→modify→destroy against live AWS. No per-module Python. The "test" =
|
||||
@@ -9,13 +9,13 @@
|
||||
# terraform files); the composition must be deterministic.
|
||||
#
|
||||
# This workflow implements pipelines/modules-lifecycle.yml (byte-identical
|
||||
# in .github/workflows/).
|
||||
# in .gitea/workflows/ and .github/workflows/).
|
||||
#
|
||||
# Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to
|
||||
# "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast,
|
||||
# no AWS mutation, validates the contract->resolver->adapter->plan chain
|
||||
# for every module on every PR, with no AWS credentials or cost). Set to
|
||||
# "full" via workflow_dispatch (or the NOVA_LIFECYCLE_MODE repo variable)
|
||||
# "full" via workflow_dispatch (or the ACDL_LIFECYCLE_MODE repo variable)
|
||||
# to run the real apply→modify→destroy against live AWS. In plan mode the
|
||||
# short-lived CI VPC apply/destroy jobs are skipped (nothing is applied).
|
||||
#
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
ci-vpc-apply:
|
||||
name: CI VPC apply
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }}
|
||||
if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Terraform 1.9.*
|
||||
@@ -60,8 +60,8 @@ jobs:
|
||||
- name: Apply CI VPC
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
terraform init -input=false -lock=false
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
matrix:
|
||||
module: [s3, kms-key, ecr, ecs-cluster, iam-role, cloudfront, waf, vpc, alb, ecs-service, rds, uptime]
|
||||
env:
|
||||
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }}
|
||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||
- name: Read CI VPC outputs
|
||||
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
|
||||
if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }}
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
terraform init -input=false -lock=false
|
||||
terraform output -json > /tmp/ci-vpc-outputs.json
|
||||
- name: Apply (simple)
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
|
||||
- name: Modify (complex)
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
|
||||
- name: Destroy
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
|
||||
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
matrix:
|
||||
module: [static-assets, microservice]
|
||||
env:
|
||||
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }}
|
||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||
- name: Read CI VPC outputs
|
||||
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
|
||||
if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }}
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
terraform init -input=false -lock=false
|
||||
terraform output -json > /tmp/ci-vpc-outputs.json
|
||||
- name: Apply (simple)
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
|
||||
- name: Modify (complex)
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
|
||||
- name: Destroy
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: bash scripts/run_l2_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
|
||||
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
name: CI VPC destroy
|
||||
needs: [lifecycle, l2-lifecycle]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }}
|
||||
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Terraform 1.9.*
|
||||
@@ -199,8 +199,8 @@ jobs:
|
||||
- name: Destroy CI VPC
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
terraform init -input=false -lock=false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova Release Pipeline — GitHub Actions (production)
|
||||
# ACDL Release Pipeline — GitHub Actions (production)
|
||||
#
|
||||
# Runs on push to main. Computes the next semver tag from the latest tag +
|
||||
# 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)
|
||||
# - Milestone completion ("docs(milestone): complete") -> bump MINOR (v1.6.1 -> v1.7.0)
|
||||
# - Major bumps are manual (not implemented here).
|
||||
name: nova-release
|
||||
name: acdl-release
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -87,6 +87,6 @@ jobs:
|
||||
BODY=$(git log --format='- %s' HEAD)
|
||||
fi
|
||||
gh release create ${{ steps.version.outputs.new_tag }} \
|
||||
--title "Nova ${{ steps.version.outputs.new_tag }}" \
|
||||
--title "ACDL ${{ steps.version.outputs.new_tag }}" \
|
||||
--notes "$BODY" \
|
||||
--generate-notes || true
|
||||
@@ -1,31 +0,0 @@
|
||||
# Nova Slides Render — re-renders presentation deck when source files change.
|
||||
name: Nova Slides Render
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'docs/presentations/**'
|
||||
- 'scripts/render_slides.sh'
|
||||
- 'assets/nova-sp-theme.css'
|
||||
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' }
|
||||
- name: Install Chrome
|
||||
run: |
|
||||
npx --yes @marp-team/marp-cli@latest --version
|
||||
npx --yes @mermaid-js/mermaid-cli --version
|
||||
- name: Render slides
|
||||
run: bash scripts/render_slides.sh
|
||||
- name: Commit rendered artifacts
|
||||
run: |
|
||||
git config user.name "nova-slides-bot"
|
||||
git config user.email "bot@nova.local"
|
||||
git add docs/presentations/*.html docs/presentations/*.pptx docs/presentations/assets/png/*.png
|
||||
git diff --cached --quiet || git commit -m "chore(slides): re-render deck [skip ci]"
|
||||
git push
|
||||
+1
-24
@@ -14,31 +14,8 @@ terraform/bootstrap/.bootstrap_state.json
|
||||
# CIAgent runtime artifacts
|
||||
.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/
|
||||
**/.terraform.lock.hcl
|
||||
**/tfplan
|
||||
**/*.tfstate*
|
||||
|
||||
# Credential patterns (v1.14, REQ-146)
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
*.cer
|
||||
*.crt
|
||||
*.jks
|
||||
*.keystore.coverage
|
||||
.coverage
|
||||
**/*.tfstate*
|
||||
@@ -1,6 +1,4 @@
|
||||
# Nova
|
||||
|
||||
> **Nova — The New Dawn of DevSecOps.** Security as a seamless enabler of fast deployments — not a bottleneck, not a "no" department.
|
||||
# ACDL — Agentic Cloud Delivery Platform
|
||||
|
||||
Consumers declare intent; the platform delivers safe production deployment
|
||||
through an agentic stack — automatically, safely, and with a complete audit
|
||||
@@ -20,7 +18,7 @@ a configuration file, or an infrastructure module.
|
||||
|
||||
## Repository roles
|
||||
|
||||
There are two kinds of repository in the Nova model:
|
||||
There are two kinds of repository in the ACDL model:
|
||||
|
||||
- **Platform repo (this one).** This is the **source code of the platform**.
|
||||
It owns `modules/`, `adapters/`, `core/`, `schemas/`, `pipelines/`,
|
||||
@@ -28,7 +26,7 @@ There are two kinds of repository in the Nova model:
|
||||
A **consumer never clones it.**
|
||||
- **Consumer repo (yours).** A consumer repo contains only:
|
||||
1. **Its application code** — the service or site being deployed.
|
||||
2. **One or more contracts** — small YAML files at `.nova/contract.yml`
|
||||
2. **One or more contracts** — small YAML files at `.acdl/contract.yml`
|
||||
that declare infrastructure (one or more modules by name + version),
|
||||
select an environment, and supply module-specific inputs.
|
||||
3. **One or more CI definitions** — thin `.github/workflows/*.yml` files
|
||||
@@ -95,7 +93,7 @@ intent via a contract; the platform delivers the deployment through the
|
||||
same contract schema, the same policy envelope, and the same evidence
|
||||
stream.
|
||||
|
||||
Consumers have their own repos and consume Nova by writing a contract that
|
||||
Consumers have their own repos and consume ACDL by writing a contract that
|
||||
declares infrastructure. A consumer declares a contract (id + name +
|
||||
environment + infrastructure); the platform resolves it to a stack instance,
|
||||
compiles it, runs security + policy checks, computes a confidence signal,
|
||||
@@ -126,51 +124,25 @@ engine-specific code. `modules/`, `schemas/`, `contracts/`,
|
||||
|
||||
## How to run
|
||||
|
||||
### Quick start (offline, no AWS required)
|
||||
### Prerequisites
|
||||
|
||||
The fastest way to verify the platform works — no AWS credentials, no
|
||||
bootstrap, no cost. See the [Consumer guide](docs/consumer-guide.md)
|
||||
for the consumer happy path (a consumer owns only a contract + app code).
|
||||
> These prerequisites are for running the **platform repo** locally. A
|
||||
> consumer does not need any of these — see the
|
||||
> [Consumer guide](docs/consumer-guide.md) for the consumer happy path.
|
||||
|
||||
```bash
|
||||
# Install test dependencies
|
||||
pip install -r requirements-test.txt
|
||||
- A platform-managed environment (see [docs/environments/](docs/environments/)).
|
||||
For local testing, `core/environments/dev.json` is provided as the sample.
|
||||
- AWS credentials for the dev environment (in `.env.secrets`, gitignored;
|
||||
see [Credentials & zero-trust](#credentials--zero-trust)).
|
||||
- `terraform` (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3`
|
||||
+ `jsonschema`.
|
||||
|
||||
# 1. Run the test suite (all offline — uses moto for DynamoDB mocking)
|
||||
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`.
|
||||
### Run the platform pipeline end-to-end
|
||||
|
||||
```bash
|
||||
# 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent)
|
||||
# (requires the bootstrap root key in env — skip if the state bucket +
|
||||
# nova-spike-runner already exist)
|
||||
# acdl-spike-runner already exist)
|
||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
||||
python3 terraform/bootstrap/create_state_backend.py
|
||||
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
|
||||
@@ -194,6 +166,26 @@ bash scripts/run_platform.sh --plan-only contracts/static-assets.yaml
|
||||
bash scripts/run_platform.sh --quiet contracts/static-assets.yaml
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
The CI/CD pipeline is defined by a **central pipeline contract** — a
|
||||
@@ -219,9 +211,23 @@ bash scripts/run_ci.sh --quiet # suppress per-stage banners
|
||||
|
||||
### Reusable deploy workflow
|
||||
|
||||
Consumer repos invoke the deploy pipeline via `.github/workflows/deploy.yml`
|
||||
(a reusable GitHub Actions workflow, versioned tag `nova/.github/workflows/deploy.yml@v1.19`).
|
||||
See the [Consumer guide](docs/consumer-guide.md) for the end-to-end happy path.
|
||||
The deployment pipeline is defined by a **central deployment pipeline
|
||||
contract** (`pipelines/contract.yml`, validated against
|
||||
`schemas/deploy-pipeline.schema.json`) and exposed to consumer repos as a
|
||||
**reusable workflow**:
|
||||
|
||||
- `.github/workflows/deploy.yml` — GitHub Actions (production)
|
||||
|
||||
The workflow implements the same stages as `pipelines/contract.yml`
|
||||
(validate-contract → resolve-stack → security checks → infrastructure plan
|
||||
→ policy checks → confidence → evidence event → apply). A consumer repo
|
||||
invokes the reusable workflow via a **versioned tag** (floating MAJOR +
|
||||
MINOR, e.g. `acdl/.github/workflows/deploy.yml@v1.6`). The workflow checks
|
||||
out the consumer repo, then checks out the ACDL platform repo into the
|
||||
runner workspace, and runs `scripts/run_platform.sh` against the consumer's
|
||||
contract — the consumer never clones the platform repo or invokes its
|
||||
scripts locally. See the [Consumer guide](docs/consumer-guide.md) for the
|
||||
end-to-end happy path.
|
||||
|
||||
### Output streaming (run_platform.sh)
|
||||
|
||||
@@ -241,7 +247,7 @@ backwards-compatible log-only mode.
|
||||
## Consumer guide
|
||||
|
||||
A step-by-step guide for a consumer to create their pipeline and define a
|
||||
contract that deploys any Nova module to AWS is at
|
||||
contract that deploys any ACDL module to AWS is at
|
||||
[`docs/consumer-guide.md`](docs/consumer-guide.md). The guide is generic
|
||||
across all modules; `static-assets` is the worked example.
|
||||
|
||||
@@ -277,8 +283,8 @@ no static credentials in repo secrets.
|
||||
`repo:org/consumer-repo:ref:refs/heads/main`) binds the role's trust
|
||||
policy to the exact consumer repo + branch that invoked the workflow.
|
||||
- **Resource-creation attributes** — every resource the pipeline creates
|
||||
is tagged with `nova:owner=<consumer-repo>` and
|
||||
`nova:contract=<contract-id>`. The session policy grants
|
||||
is tagged with `acdl:owner=<consumer-repo>` and
|
||||
`acdl:contract=<contract-id>`. The session policy grants
|
||||
view/update/delete **only on resources whose tags match the calling
|
||||
repo**.
|
||||
|
||||
@@ -296,6 +302,12 @@ documented alternative:
|
||||
runs, or in **`.env.secrets`** (gitignored, chmod 600) for local testing.
|
||||
- The platform rotates platform-runner keys on a **daily cadence** —
|
||||
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
|
||||
key's useful lifetime is one workflow run, and the local alternative is
|
||||
rotated at least daily (platform-runner) or out of band (local).
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Nova Adapters
|
||||
# ACDL Adapters
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Kyverno Adapter
|
||||
|
||||
The Kyverno adapter translates Kyverno `PolicyReport` results to the
|
||||
normalized Nova
|
||||
normalized ACDL
|
||||
[`PolicyCheckResult`](../../schemas/policy_check_result.schema.json) schema
|
||||
(engine: `"kyverno"`), mirroring the Checkov/Wiz adapter pattern.
|
||||
|
||||
@@ -15,7 +15,7 @@ publishes results to `PolicyReport` resources.
|
||||
## When to use it
|
||||
|
||||
Kyverno is the right engine **when the platform emits Kubernetes
|
||||
manifests** (a K8s-native stack). The Nova platform today emits Terraform
|
||||
manifests** (a K8s-native stack). The ACDL platform today emits Terraform
|
||||
only (D-053), so this adapter is **ready but inactive**: it ships now so
|
||||
the schema path, severity/result mapping and sample policies are in place
|
||||
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
|
||||
`securityContext.privileged: true`.
|
||||
- `require-resource-labels.yml` — require `nova:owner` and
|
||||
`nova:environment` labels on all pods (mirrors the Nova tagging standard
|
||||
- `require-resource-labels.yml` — require `acdl:owner` and
|
||||
`acdl:environment` labels on all pods (mirrors the ACDL tagging standard
|
||||
in [`schemas/tagging-standard.json`](../../schemas/tagging-standard.json)).
|
||||
- `require-image-digests.yml` — require container images to reference a
|
||||
digest (`image@sha256:...`), not a mutable tag.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Kyverno adapter — translate Kyverno PolicyReport results to Nova PolicyCheckResult records.
|
||||
"""Kyverno adapter — translate Kyverno PolicyReport results to ACDL PolicyCheckResult records.
|
||||
|
||||
Kyverno is a Kubernetes-native policy engine. It evaluates K8s manifests
|
||||
and produces PolicyReport resources. This adapter translates those results
|
||||
@@ -8,16 +8,13 @@ v1.9 (REQ-111): the translator is fleshed out — full PolicyReport →
|
||||
PolicyCheckResult mapping with severity + skip-with-reason handling. It
|
||||
remains inactive for Terraform-only stacks (guard preserved — emits a
|
||||
single SKIPPED `KYVERNO_INACTIVE_TF_STACK` record when no K8s manifests).
|
||||
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.
|
||||
A `--kube-version` stub is parsed but not yet used (for future GitOps).
|
||||
|
||||
D-053: the platform emits Terraform, not K8s manifests. This adapter
|
||||
activates when the GitOps reconciler (roadmap) emits K8s manifests.
|
||||
Sample policies are included as documentation at adapters/kyverno/policies/.
|
||||
|
||||
CLI: kyverno_adapter.py <policyreport.json> <contract-id>
|
||||
CLI: kyverno_adapter.py <policyreport.json> <contract-id> [--kube-version <ver>]
|
||||
"""
|
||||
|
||||
import datetime
|
||||
@@ -103,7 +100,7 @@ def _emit_inactive_tf(contract_id):
|
||||
}
|
||||
|
||||
|
||||
def adapt(policyreport_json_path, contract_id):
|
||||
def adapt(policyreport_json_path, contract_id, kube_version=None):
|
||||
with open(policyreport_json_path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
out = []
|
||||
@@ -115,6 +112,8 @@ def adapt(policyreport_json_path, contract_id):
|
||||
out.append(_to_pcr(entry, contract_id))
|
||||
if not out:
|
||||
out.append(_emit_inactive_tf(contract_id))
|
||||
# kube_version is parsed but not yet used (future GitOps reconciler).
|
||||
_ = kube_version
|
||||
return out
|
||||
|
||||
|
||||
@@ -124,8 +123,14 @@ def adapt_inactive(contract_id):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kube_ver = None
|
||||
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:
|
||||
print("usage: kyverno_adapter.py <policyreport.json> <contract-id>", file=sys.stderr)
|
||||
print("usage: kyverno_adapter.py <policyreport.json> <contract-id> [--kube-version <ver>]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
print(json.dumps(adapt(args[0], args[1]), indent=2))
|
||||
print(json.dumps(adapt(args[0], args[1], kube_version=kube_ver), indent=2))
|
||||
@@ -3,7 +3,7 @@ kind: ClusterPolicy
|
||||
metadata:
|
||||
name: require-resource-labels
|
||||
annotations:
|
||||
policies.kyverno.io/title: Require Nova Resource Labels
|
||||
policies.kyverno.io/title: Require ACDL Resource Labels
|
||||
policies.kyverno.io/category: Governance
|
||||
policies.kyverno.io/severity: medium
|
||||
policies.kyverno.io/subject: Pod
|
||||
@@ -11,27 +11,27 @@ spec:
|
||||
validationFailureAction: audit
|
||||
background: true
|
||||
rules:
|
||||
- name: require-nova-owner-label
|
||||
- name: require-acdl-owner-label
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "Pods must carry the nova:owner label (Nova tagging standard)."
|
||||
message: "Pods must carry the acdl:owner label (ACDL tagging standard)."
|
||||
pattern:
|
||||
metadata:
|
||||
labels:
|
||||
nova:owner: "?*"
|
||||
- name: require-nova-environment-label
|
||||
acdl:owner: "?*"
|
||||
- name: require-acdl-environment-label
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "Pods must carry the nova:environment label (Nova tagging standard)."
|
||||
message: "Pods must carry the acdl:environment label (ACDL tagging standard)."
|
||||
pattern:
|
||||
metadata:
|
||||
labels:
|
||||
nova:environment: "?*"
|
||||
acdl:environment: "?*"
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Nova Terraform adapter — stateless assembler (v1.11 RESTART, P56a).
|
||||
"""ACDL Terraform adapter — stateless assembler (v1.11 RESTART, P56a).
|
||||
|
||||
A STATELESS ASSEMBLER. It owns no module content — no resource shape, no
|
||||
nested HCL blocks, no defaults, no type-specific logic. It reads the
|
||||
@@ -10,10 +10,9 @@ lives in the per-module terraform/ subdir, NOT in this file.
|
||||
CLI: adapter.py <instance.json> <out_dir>
|
||||
"""
|
||||
|
||||
import json, os, sys
|
||||
_R = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, _R) if _R not in sys.path else None
|
||||
from core import env
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def _load_registry(repo_root):
|
||||
@@ -113,8 +112,6 @@ def adapt(stack_instance, out_dir):
|
||||
|
||||
stack_name = stack.get("name", "spike")
|
||||
environment = stack.get("environment", "dev")
|
||||
account_id = env.get_env("AWS_ACCOUNT_ID", "581513795199")
|
||||
state_bucket = f"nova-tfstate-{account_id}-us-east-1"
|
||||
terraform_tf = (
|
||||
'terraform {\n'
|
||||
' required_version = ">= 1.9, < 1.10"\n'
|
||||
@@ -125,7 +122,7 @@ def adapt(stack_instance, out_dir):
|
||||
' }\n'
|
||||
' }\n'
|
||||
' backend "s3" {\n'
|
||||
f' bucket = "{state_bucket}"\n'
|
||||
' bucket = "acdl-tfstate-581513795199-us-east-1"\n'
|
||||
f' key = "spike/{stack_name}/{environment}/terraform.tfstate"\n'
|
||||
' region = "us-east-1"\n'
|
||||
' }\n'
|
||||
@@ -135,12 +132,12 @@ def adapt(stack_instance, out_dir):
|
||||
data_source_names = stack_instance.get("data_sources", [])
|
||||
parts = []
|
||||
if data_source_names:
|
||||
remote_state_key = env.get_env("REMOTE_STATE_KEY", "platform/terraform.tfstate")
|
||||
remote_state_key = os.environ.get("ACDL_REMOTE_STATE_KEY", "platform/terraform.tfstate")
|
||||
parts.append(
|
||||
'data "terraform_remote_state" "platform" {\n'
|
||||
' backend = "s3"\n'
|
||||
' config = {\n'
|
||||
f' bucket = "{state_bucket}"\n'
|
||||
' bucket = "acdl-tfstate-581513795199-us-east-1"\n'
|
||||
f' key = "{remote_state_key}"\n'
|
||||
' region = "us-east-1"\n'
|
||||
' }\n'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Translate Checkov JSON output to Nova PolicyCheckResult records.
|
||||
"""Translate Checkov JSON output to ACDL PolicyCheckResult records.
|
||||
|
||||
Reads Checkov's JSON output (one framework key, e.g. terraform_plan),
|
||||
emits a list of PolicyCheckResult dicts conforming to
|
||||
@@ -6,23 +6,16 @@ schemas/policy_check_result.schema.json. Run Checkov with --soft-fail so
|
||||
Checkov never exits non-zero; the confidence signal decides the gate, not
|
||||
Checkov's exit code.
|
||||
|
||||
The Nova tagging standard (D-054, D-043 closure, D-109 hard mode in P3)
|
||||
is enforced by a custom Checkov rule at
|
||||
adapters/terraform/policy/custom_rules/nova_tagging.py, loaded via
|
||||
--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).
|
||||
The ACDL tagging standard (D-054, D-043 closure) is enforced by a custom
|
||||
Checkov rule at adapters/terraform/policy/custom_rules/acdl_tagging.py,
|
||||
loaded via --external-checks-dir. The adapter therefore maps
|
||||
ACDL_TAG_NAMING as a real rule (no synthetic SKIPPED record is emitted).
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
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 = {
|
||||
"CKV_AWS_41": ("secrets-in-plaintext", "high"),
|
||||
@@ -36,12 +29,10 @@ RULE_MAP = {
|
||||
"CKV_AWS_40": ("iam-wildcard", "medium"),
|
||||
"CKV_AWS_7": ("kms-key-reference", "medium"),
|
||||
"CKV_AWS_33": ("kms-key-reference", "medium"),
|
||||
# D-054 / D-043 closure, D-109 hard mode (P3): NOVA_TAG_NAMING is a real
|
||||
# custom Checkov rule (adapters/terraform/policy/custom_rules/nova_tagging.py),
|
||||
# loaded via --external-checks-dir. No synthetic SKIPPED record is emitted.
|
||||
# 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"),
|
||||
# D-054 / D-043 closure: ACDL_TAG_NAMING is now a real custom Checkov
|
||||
# rule (adapters/terraform/policy/custom_rules/acdl_tagging.py), loaded
|
||||
# via --external-checks-dir. No synthetic SKIPPED record is emitted.
|
||||
"ACDL_TAG_NAMING": ("tagging-standard", "medium"),
|
||||
}
|
||||
|
||||
_RESULT_MAP = {"PASSED": "pass", "FAILED": "fail", "SKIPPED": "skipped"}
|
||||
@@ -75,7 +66,7 @@ def _to_pcr(checkov_record, contract_id, result_str):
|
||||
}
|
||||
|
||||
|
||||
def adapt(checkov_json_path, contract_id, run_id=None, environment="dev"):
|
||||
def adapt(checkov_json_path, contract_id):
|
||||
with open(checkov_json_path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
out = []
|
||||
@@ -89,25 +80,6 @@ def adapt(checkov_json_path, contract_id, run_id=None, environment="dev"):
|
||||
out.append(_to_pcr(rec, contract_id, "FAILED"))
|
||||
for rec in results.get("skipped_checks", []):
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
# Nova Custom Checkov Rules
|
||||
# ACDL Custom Checkov Rules
|
||||
|
||||
This directory holds Nova-authored Checkov custom rules, written in the
|
||||
This directory holds ACDL-authored Checkov custom rules, written in the
|
||||
[Checkov Python custom-rule framework](https://www.checkov.io/4.Contributing/Custom%20Policies.html).
|
||||
|
||||
## Files
|
||||
|
||||
- `nova_tagging.py` — `NOVA_TAG_NAMING` (D-054, D-109 warn mode in P2):
|
||||
ensures every taggable AWS resource carries the four required Nova tags
|
||||
(`nova:owner`, `nova:contract`, `nova:environment`, `nova:cost-center`).
|
||||
This rule replaces the synthetic SKIPPED `NOVA_TAG_NAMING` record that the
|
||||
Checkov adapter previously emitted (D-043 closure). Renamed from
|
||||
`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.
|
||||
- `acdl_tagging.py` — `ACDL_TAG_NAMING` (D-054): ensures every taggable AWS
|
||||
resource carries the four required ACDL tags
|
||||
(`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`).
|
||||
This rule replaces the synthetic SKIPPED `ACDL_TAG_NAMING` record that the
|
||||
Checkov adapter previously emitted (D-043 closure). The canonical tag set
|
||||
is declared in [`schemas/tagging-standard.json`](../../../schemas/tagging-standard.json).
|
||||
|
||||
## How Checkov loads them
|
||||
|
||||
@@ -31,12 +23,12 @@ checkov -f terraform/spike/main.tf --framework terraform -o json --soft-fail \
|
||||
```
|
||||
|
||||
Checkov imports each `*.py` file in the directory and instantiates the
|
||||
module-level `check` object (see the `check = NovaTaggingStandard()` line at
|
||||
the bottom of `nova_tagging.py`).
|
||||
module-level `check` object (see the `check = AcdlTaggingStandard()` line at
|
||||
the bottom of `acdl_tagging.py`).
|
||||
|
||||
## Severity / result mapping
|
||||
|
||||
The Checkov adapter (`adapters/terraform/policy/checkov_adapter.py`)
|
||||
maps `NOVA_TAG_NAMING` to `(tagging-standard, medium)` in `RULE_MAP`. The
|
||||
maps `ACDL_TAG_NAMING` to `(tagging-standard, medium)` in `RULE_MAP`. The
|
||||
custom rule therefore produces real `PASS`/`FAIL` PolicyCheckResult records,
|
||||
feeding the confidence signal instead of the old SKIPPED placeholder.
|
||||
@@ -0,0 +1,54 @@
|
||||
"""ACDL tagging standard custom Checkov rule (D-054).
|
||||
|
||||
Checks that all taggable AWS resources have the required ACDL tags:
|
||||
acdl:owner, acdl:contract, acdl:environment, acdl:cost-center
|
||||
|
||||
Fails (severity medium) when any required tag is missing.
|
||||
Closes the D-043 deferral (the SKIPPED ACDL_TAG_NAMING placeholder
|
||||
becomes a real check).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
|
||||
from checkov.common.models.enums import CheckResult, CheckCategories
|
||||
|
||||
REQUIRED_TAGS = ("acdl:owner", "acdl:contract", "acdl:environment", "acdl:cost-center")
|
||||
|
||||
# Resources that support tags (exclude resources that have no tags attribute)
|
||||
NON_TAGGABLE_TYPES = (
|
||||
"aws_cloudfront_origin_access_control",
|
||||
"aws_lambda_function_url",
|
||||
"aws_route_table_association",
|
||||
"aws_internet_gateway",
|
||||
)
|
||||
|
||||
class AcdlTaggingStandard(BaseResourceCheck):
|
||||
def __init__(self):
|
||||
name = "Ensure all taggable AWS resources have required ACDL tags"
|
||||
check_id = "ACDL_TAG_NAMING"
|
||||
supported_resources = ["*"] # all resources
|
||||
categories = [CheckCategories.GENERAL_SECURITY]
|
||||
super().__init__(name=name, check_id=check_id, categories=categories, supported_resources=supported_resources)
|
||||
|
||||
def scan_resource_conf(self, conf, entity_type):
|
||||
# Skip non-taggable resources
|
||||
if entity_type in NON_TAGGABLE_TYPES:
|
||||
return CheckResult.PASSED
|
||||
# Check for a tags block
|
||||
tags = conf.get("tags")
|
||||
if not tags:
|
||||
return CheckResult.FAILED
|
||||
tag_keys = set()
|
||||
if isinstance(tags, list) and tags:
|
||||
tag_block = tags[0]
|
||||
if isinstance(tag_block, dict):
|
||||
tag_keys = set(tag_block.keys())
|
||||
elif isinstance(tags, dict):
|
||||
tag_keys = set(tags.keys())
|
||||
missing = [t for t in REQUIRED_TAGS if t not in tag_keys]
|
||||
if missing:
|
||||
return CheckResult.FAILED
|
||||
return CheckResult.PASSED
|
||||
|
||||
check = AcdlTaggingStandard()
|
||||
@@ -1,82 +0,0 @@
|
||||
"""Nova tagging standard custom Checkov rule (D-054, D-109 hard mode).
|
||||
|
||||
Checks that all taggable AWS resources have the required Nova tags:
|
||||
nova:owner, nova:contract, nova:environment, nova:cost-center
|
||||
|
||||
In **hard mode** (P3, REQ-162): the rule hard-fails when a taggable resource
|
||||
is missing any required `nova:*` tag, OR when a resource carries only the
|
||||
legacy `acdl:*` tag keys (and no `nova:*` keys). P2 shipped warn mode
|
||||
(`_WARN_MODE = True`) so the regression gate stayed green during the
|
||||
parallel-tag transition window; P3 flips to hard-fail (`_WARN_MODE = False`)
|
||||
once `nova:*` tags are emitted in terraform and the ABAC policy is swapped
|
||||
to match `nova:*`. P5 keeps hard mode and additionally hard-fails on any
|
||||
`acdl:*` tag key present at all (no legacy tolerated post-cutoff).
|
||||
|
||||
Closes the D-043 deferral (the SKIPPED NOVA_TAG_NAMING placeholder
|
||||
becomes a real check). Renamed from acdl_tagging.py in P2 (REQ-158);
|
||||
the Checkov rule ID ACDL_TAG_NAMING → NOVA_TAG_NAMING.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
|
||||
from checkov.common.models.enums import CheckResult, CheckCategories
|
||||
|
||||
REQUIRED_TAGS = ("nova:owner", "nova:contract", "nova:environment", "nova:cost-center")
|
||||
|
||||
# Legacy acdl:* tag keys — the parallel-tag period (P3) emits both nova:*
|
||||
# and acdl:*; P2 warn mode treats acdl:*-only tags as a warning, not a
|
||||
# failure. The acdl:* VALUES in tagging-standard.json are left for P3.
|
||||
LEGACY_TAGS = ("acdl:owner", "acdl:contract", "acdl:environment", "acdl:cost-center")
|
||||
|
||||
# Resources that support tags (exclude resources that have no tags attribute)
|
||||
NON_TAGGABLE_TYPES = (
|
||||
"aws_cloudfront_origin_access_control",
|
||||
"aws_lambda_function_url",
|
||||
"aws_route_table_association",
|
||||
"aws_internet_gateway",
|
||||
)
|
||||
|
||||
# P5 hard mode (D-109, REQ-164): `_WARN_MODE = False` (set in P3) AND
|
||||
# any `acdl:*` tag key present at all is a hard FAIL (P5 tightens from
|
||||
# P3's "acdl:*-only fails" to "any acdl:* key fails"). The legacy tag
|
||||
# keys are fully removed from terraform (P3); any remaining `acdl:*` key
|
||||
# is a rebrand regression.
|
||||
_WARN_MODE = False
|
||||
|
||||
|
||||
class NovaTaggingStandard(BaseResourceCheck):
|
||||
def __init__(self):
|
||||
name = "Ensure all taggable AWS resources have required Nova tags"
|
||||
check_id = "NOVA_TAG_NAMING"
|
||||
supported_resources = ["*"] # all resources
|
||||
categories = [CheckCategories.GENERAL_SECURITY]
|
||||
super().__init__(name=name, check_id=check_id, categories=categories, supported_resources=supported_resources)
|
||||
|
||||
def scan_resource_conf(self, conf, entity_type):
|
||||
# Skip non-taggable resources
|
||||
if entity_type in NON_TAGGABLE_TYPES:
|
||||
return CheckResult.PASSED
|
||||
# Check for a tags block
|
||||
tags = conf.get("tags")
|
||||
if not tags:
|
||||
return CheckResult.FAILED
|
||||
tag_keys = set()
|
||||
if isinstance(tags, list) and tags:
|
||||
tag_block = tags[0]
|
||||
if isinstance(tag_block, dict):
|
||||
tag_keys = set(tag_block.keys())
|
||||
elif isinstance(tags, dict):
|
||||
tag_keys = set(tags.keys())
|
||||
# P5 (REQ-164): any legacy acdl:* tag key present = hard FAIL.
|
||||
legacy_present = tag_keys & set(LEGACY_TAGS)
|
||||
if legacy_present:
|
||||
return CheckResult.FAILED
|
||||
missing = [t for t in REQUIRED_TAGS if t not in tag_keys]
|
||||
if not missing:
|
||||
return CheckResult.PASSED
|
||||
return CheckResult.FAILED
|
||||
|
||||
check = NovaTaggingStandard()
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Wiz adapter — translate Wiz API results to Nova PolicyCheckResult records.
|
||||
"""Wiz adapter — translate Wiz API results to ACDL PolicyCheckResult records.
|
||||
|
||||
Wiz is a SaaS security platform with a GraphQL API. This adapter
|
||||
translates Wiz issue records to the normalized PolicyCheckResult schema
|
||||
@@ -186,37 +186,8 @@ def is_configured():
|
||||
return bool(os.environ.get("WIZ_API_TOKEN") and os.environ.get("WIZ_API_URL"))
|
||||
|
||||
|
||||
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__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Wiz adapter (REQ-250: plan-mode supported)")
|
||||
parser.add_argument("wiz_json", nargs="?", help="wiz_issues.json (legacy positional mode)")
|
||||
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>")
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: wiz_adapter.py <wiz_issues.json> <contract-id>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
print(json.dumps(adapt(sys.argv[1], sys.argv[2]), indent=2))
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova sample consumer contract — microservice module (dev)
|
||||
# ACDL sample consumer contract — microservice module (dev)
|
||||
# Per-environment contract (REQ-105). Promotion = running the dev job;
|
||||
# no environment field editing. Interpolation resolves against dev.json.
|
||||
id: msvc
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova sample consumer contract — microservice module (dr)
|
||||
# ACDL sample consumer contract — microservice module (dr)
|
||||
# Per-environment contract (REQ-105). Promotion = running the dr job;
|
||||
# no environment field editing. Interpolation resolves against dr.json.
|
||||
id: msvc
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova sample consumer contract — microservice module (prod)
|
||||
# ACDL sample consumer contract — microservice module (prod)
|
||||
# Per-environment contract (REQ-105). Promotion = running the prod job;
|
||||
# no environment field editing. Interpolation resolves against prod.json.
|
||||
id: msvc
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova sample consumer contract — microservice module (qa)
|
||||
# ACDL sample consumer contract — microservice module (qa)
|
||||
# Per-environment contract (REQ-105). Promotion = running the qa job;
|
||||
# no environment field editing. Interpolation resolves against qa.json.
|
||||
id: msvc
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova sample consumer contract — microservice module (dev)
|
||||
# ACDL sample consumer contract — microservice module (dev)
|
||||
#
|
||||
# Reference example for an ECS Fargate microservice deployment.
|
||||
# Interpolation (D-081): bucket_name uses the naming pattern that includes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova sample consumer contract — static-assets module (dev)
|
||||
# ACDL sample consumer contract — static-assets module (dev)
|
||||
# Per-environment contract (REQ-105). The dev default
|
||||
# (contracts/static-assets.yml) remains for backwards compat; this file
|
||||
# is the explicit per-env dev contract. Interpolation resolves against dev.json.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova sample consumer contract — static-assets module (dr)
|
||||
# ACDL sample consumer contract — static-assets module (dr)
|
||||
# Per-environment contract (REQ-105). Promotion = running the dr job;
|
||||
# no environment field editing. Interpolation resolves against dr.json.
|
||||
id: assets
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova sample consumer contract — static-assets module (prod)
|
||||
# ACDL sample consumer contract — static-assets module (prod)
|
||||
# Per-environment contract (REQ-105). Promotion = running the prod job;
|
||||
# no environment field editing. Interpolation resolves against prod.json.
|
||||
id: assets
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova sample consumer contract — static-assets module (qa)
|
||||
# ACDL sample consumer contract — static-assets module (qa)
|
||||
# Per-environment contract (REQ-105). Promotion = running the qa job;
|
||||
# no environment field editing. Interpolation resolves against qa.json.
|
||||
id: assets
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Nova sample consumer contract — static-assets module (dev)
|
||||
# ACDL sample consumer contract — static-assets module (dev)
|
||||
#
|
||||
# This is the reference example for a consumer contract. It declares:
|
||||
# id: short operational acronym (becomes stack.name for state, tags, evidence)
|
||||
|
||||
@@ -14,8 +14,7 @@ concerns split into two tiers:
|
||||
The operator-supplied evidence artifact is a JSON blob with `timestamp`,
|
||||
`type`, `payload`, and an optional `signature` (JWS detached). Freshness
|
||||
is validated against the window from §10.4. Signature verification runs
|
||||
when `NOVA_ATTESTATION_SIGNING_KEY_ID` is set (dual-read via core/env.py:
|
||||
NOVA_* preferred, ACDL_* fallback until P5); it is skipped + logged
|
||||
when `ACDL_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged
|
||||
when unset (dev/CI — D-089). The matrix fails loud if an operator-supplied
|
||||
concern is missing or expired for prod/dr.
|
||||
"""
|
||||
@@ -25,14 +24,6 @@ import os
|
||||
import sys
|
||||
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_DAYS = {
|
||||
@@ -90,15 +81,14 @@ def _is_fresh(artifact: dict, concern: str) -> bool:
|
||||
|
||||
|
||||
def _verify_signature(artifact: dict) -> bool:
|
||||
"""Verify the JWS detached signature when NOVA_ATTESTATION_SIGNING_KEY_ID is set.
|
||||
"""Verify the JWS detached signature when ACDL_ATTESTATION_SIGNING_KEY_ID is set.
|
||||
|
||||
When unset (dev/CI — D-089), signature verification is skipped + logged.
|
||||
Dual-read via core/env.py: NOVA_* preferred, ACDL_* fallback until P5.
|
||||
"""
|
||||
key_id = env.get_env("ATTESTATION_SIGNING_KEY_ID", "") or ""
|
||||
key_id = os.environ.get("ACDL_ATTESTATION_SIGNING_KEY_ID", "")
|
||||
if not key_id:
|
||||
sys.stderr.write(
|
||||
"[attestation] NOVA_ATTESTATION_SIGNING_KEY_ID unset — "
|
||||
"[attestation] ACDL_ATTESTATION_SIGNING_KEY_ID unset — "
|
||||
"signature verification skipped (dev/CI, D-089)\n"
|
||||
)
|
||||
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
|
||||
explode the key-management surface), rotated **quarterly**. The `jws`
|
||||
field is added to the event shape when this ships.
|
||||
- **Async worker + DLQ:** a Lambda (or a forge Actions scheduled workflow)
|
||||
- **Async worker + DLQ:** a Lambda (or a Gitea Actions scheduled workflow)
|
||||
reads the outbox, writes to S3 Object Lock, signs with KMS. DLQ = an
|
||||
SQS dead-letter queue for failed writes. RTO = DLQ replay.
|
||||
- **Daily checkpoints (§9):** a daily job reads the last event hash and
|
||||
@@ -86,7 +86,7 @@ log" anti-goal requires.
|
||||
D-083 ships).
|
||||
- `prev_event_hash` (chain link; `GENESIS` for the first event).
|
||||
- `hash` (this event's SHA-256 over canonical JSON).
|
||||
- `approver_qa` (CI username of the QA approver; populated on
|
||||
- `approver_qa` (Gitea/GitHub username of the QA approver; populated on
|
||||
qa-promotion by v1.9's `hitl_gates.attest` — D-042).
|
||||
- `approver_prod` (SRE username; populated on prod-promotion by v1.9's
|
||||
`hitl_gates.attest`).
|
||||
@@ -112,7 +112,7 @@ log" anti-goal requires.
|
||||
- **D-042** — approver identities (`approver_qa`, `approver_prod`,
|
||||
`approver_dr`) live in the outbox; the separation-of-duties check
|
||||
(`core/separation_of_duties.py`) reads `approver_qa` and compares
|
||||
to the prod-dispatch CI actor. v1.9's
|
||||
to the prod-dispatch `gitea.actor` / `github.actor`. v1.9's
|
||||
`hitl_gates.attest` populates these attributes.
|
||||
- **D-083** (v1.9) — S3 Object Lock + JWS + async worker + DLQ + daily
|
||||
checkpoints deferred to a future milestone. Requires non-offline-
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Nova Confidence Signal (REQ-19).
|
||||
"""ACDL Confidence Signal (REQ-19).
|
||||
|
||||
The platform's certified answer to "is this safe to proceed?" (vision
|
||||
tenet: "Safety is Computed, Not Assumed"). Every delivery action produces
|
||||
@@ -34,13 +34,8 @@ per-input scores.
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import List, Literal, Optional, Dict, Any
|
||||
import json
|
||||
import os
|
||||
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 = {
|
||||
"policy": 0.30,
|
||||
@@ -166,33 +161,7 @@ def compute(contract_id: str, environment: str,
|
||||
band = "warn"
|
||||
if environment == "dev" and band == "warn":
|
||||
band = "block"
|
||||
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
|
||||
return Signal(score, band, per_input, reasons)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+62
-49
@@ -1,4 +1,4 @@
|
||||
"""Nova Contract Resolver — resolve a consumer contract to a Target Stack instance.
|
||||
"""ACDL Contract Resolver — resolve a consumer contract to a Target Stack instance.
|
||||
|
||||
The contract resolver is the bridge between the consumer's declared intent
|
||||
(a contract YAML) and the platform's executable representation (a Target
|
||||
@@ -36,27 +36,26 @@ import sys
|
||||
import yaml
|
||||
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):
|
||||
"""Load the environment onboarding JSON for env_name.
|
||||
|
||||
P7 (REQ-171): delegates to core.environment_check.load() (dedup —
|
||||
the two were verbatim duplicates). The environment_check module is
|
||||
in the same core/ package, so the import works both as a package
|
||||
import and as a script (`python3 core/contract_resolver.py`).
|
||||
Mirrors core.environment_check.load() but is self-contained so the
|
||||
resolver works both as a package import (`from core.contract_resolver
|
||||
import resolve`) and as a script (`python3 core/contract_resolver.py`).
|
||||
Emits a stderr warning when account_id is the placeholder and env != dev.
|
||||
"""
|
||||
from core import environment_check
|
||||
return environment_check.load(env_name, root=repo_root)
|
||||
env_file = os.path.join(repo_root, "core", "environments", f"{env_name}.json")
|
||||
if not os.path.isfile(env_file):
|
||||
raise FileNotFoundError(f"no environment file for '{env_name}' at {env_file}")
|
||||
env = _load_json(env_file)
|
||||
if env.get("account_id") == "000000000000" and env_name != "dev":
|
||||
sys.stderr.write(
|
||||
f"WARNING: environment '{env_name}' has the placeholder account_id "
|
||||
f"000000000000 — replace it with the real {env_name} account id "
|
||||
f"before deploying (onboarding scaffold).\n"
|
||||
)
|
||||
return env
|
||||
|
||||
|
||||
def _load_json(path):
|
||||
@@ -64,21 +63,6 @@ def _load_json(path):
|
||||
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):
|
||||
with open(path, "r") as fh:
|
||||
return yaml.safe_load(fh)
|
||||
@@ -452,9 +436,24 @@ def _namespace_resources(resources, module_name):
|
||||
|
||||
|
||||
def decommission_transform(stack_instance):
|
||||
"""REQ-92: re-export from core.decommission_transform (P12, REQ-176)."""
|
||||
from core.decommission_transform import decommission_transform as _dt
|
||||
return _dt(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
|
||||
|
||||
|
||||
def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
@@ -462,7 +461,7 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
|
||||
Args:
|
||||
contract_path: Path to the contract YAML file.
|
||||
repo_root: Root of the Nova repo (defaults to two levels up from this file).
|
||||
repo_root: Root of the ACDL repo (defaults to two levels up from this file).
|
||||
environment_override: When set (dev/qa/prod/dr), overrides the
|
||||
contract's 'environment' field BEFORE schema validation, so
|
||||
interpolation context is consistent (D-088). Used by
|
||||
@@ -483,7 +482,7 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
contract["environment"] = environment_override
|
||||
|
||||
# Load schemas
|
||||
contract_schema = _load_schema(os.path.join(repo_root, "schemas", "contract.schema.json"))
|
||||
contract_schema = _load_json(os.path.join(repo_root, "schemas", "contract.schema.json"))
|
||||
|
||||
# Validate contract against schema
|
||||
jsonschema.validate(contract, contract_schema)
|
||||
@@ -525,14 +524,10 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
f"module '{module_name}' version '{version}' not found in registry")
|
||||
module_inputs = module_entry.get("inputs", {})
|
||||
|
||||
# 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.
|
||||
# Determine if L1 or L2
|
||||
entry = registry[module_name][version]
|
||||
interface_path = entry["interface"]
|
||||
is_l2 = entry.get("kind") == "l2" or (
|
||||
"kind" not in entry and ("l2" in interface_path or "composition" in interface_path)
|
||||
)
|
||||
is_l2 = "l2" in interface_path or "composition" in interface_path
|
||||
|
||||
if is_l2:
|
||||
fragment = _resolve_l2(module_name, version, module_inputs,
|
||||
@@ -575,8 +570,13 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
merged_outputs.update(fragment.get("outputs", {}))
|
||||
all_resources.extend(fragment["resources"])
|
||||
|
||||
# Determine stack kind: L2 if any module is L2 or if multi-module (P7)
|
||||
kind = "l2" if (multi_module or any_l2) else "l1"
|
||||
# Determine stack kind: L2 if any module is L2 or if multi-module
|
||||
if multi_module:
|
||||
kind = "l2"
|
||||
elif any_l2:
|
||||
kind = "l2"
|
||||
else:
|
||||
kind = "l1"
|
||||
|
||||
stack_instance = {
|
||||
"version": "1.0.0",
|
||||
@@ -603,13 +603,26 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
stack_instance["outputs"] = merged_outputs
|
||||
|
||||
# Validate against stack schema
|
||||
stack_schema = _load_schema(os.path.join(repo_root, "schemas", "stack.schema.json"))
|
||||
stack_schema = _load_json(os.path.join(repo_root, "schemas", "stack.schema.json"))
|
||||
jsonschema.validate(stack_instance, stack_schema)
|
||||
|
||||
return stack_instance
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# P12 (REQ-176): CLI extracted to core/contract_resolver_cli.py.
|
||||
from core.contract_resolver_cli import main
|
||||
sys.exit(main())
|
||||
if len(sys.argv) < 3:
|
||||
print("usage: contract_resolver.py <contract.yml> <out.json> [--environment <name>]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
contract_path = sys.argv[1]
|
||||
out_path = sys.argv[2]
|
||||
env_override = None
|
||||
if "--environment" in sys.argv:
|
||||
idx = sys.argv.index("--environment")
|
||||
if idx + 1 < len(sys.argv):
|
||||
env_override = sys.argv[idx + 1]
|
||||
# Also honor the ACDL_ENVIRONMENT_OVERRIDE env var (used by run_platform.sh).
|
||||
if env_override is None and os.environ.get("ACDL_ENVIRONMENT_OVERRIDE"):
|
||||
env_override = os.environ["ACDL_ENVIRONMENT_OVERRIDE"]
|
||||
result = resolve(contract_path, environment_override=env_override)
|
||||
with open(out_path, "w") as fh:
|
||||
json.dump(result, fh, indent=2)
|
||||
@@ -1,41 +0,0 @@
|
||||
"""Nova Contract Resolver CLI — command-line entry point.
|
||||
|
||||
Extracted from core/contract_resolver.py (P12, REQ-176).
|
||||
|
||||
G-113 import direction: this module imports core.contract_resolver (the
|
||||
re-export shim) for the resolve function. The shim imports the split
|
||||
modules. Nothing imports this CLI module except direct invocation.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
from core.contract_resolver import resolve
|
||||
from core import env
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
"""CLI: resolve a contract YAML to a Target Stack JSON."""
|
||||
argv = argv if argv is not None else sys.argv[1:]
|
||||
if len(argv) < 2:
|
||||
print("usage: contract_resolver.py <contract.yml> <out.json> [--environment <name>", file=sys.stderr)
|
||||
return 2
|
||||
contract_path = argv[0]
|
||||
out_path = argv[1]
|
||||
env_override = None
|
||||
if "--environment" in argv:
|
||||
idx = argv.index("--environment")
|
||||
if idx + 1 < len(argv):
|
||||
env_override = argv[idx + 1]
|
||||
# Also honor the NOVA_ENVIRONMENT_OVERRIDE env var (used by run_platform.sh).
|
||||
if env_override is None and env.get_env("ENVIRONMENT_OVERRIDE"):
|
||||
env_override = env.get_env("ENVIRONMENT_OVERRIDE")
|
||||
result = resolve(contract_path, environment_override=env_override)
|
||||
with open(out_path, "w") as fh:
|
||||
json.dump(result, fh, indent=2)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,31 +0,0 @@
|
||||
"""Nova Decommission Transform — zero counts + disable deletion protection (REQ-92).
|
||||
|
||||
Extracted from core/contract_resolver.py (P12, REQ-176).
|
||||
|
||||
G-113 import direction: this module imports only stdlib. The re-export
|
||||
shim core/contract_resolver.py imports this module. Nothing imports the
|
||||
shim except external callers.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def decommission_transform(stack_instance):
|
||||
"""REQ-92: Transform a resolved stack instance for decommission.
|
||||
|
||||
Sets all scalable counts to 0 and deletion_protection to false on
|
||||
every resource. Used by the decommission pipeline mode after the
|
||||
first step (disable deletion protection) has been applied.
|
||||
"""
|
||||
for res in stack_instance.get("resources", []):
|
||||
if "nfrs" not in res:
|
||||
res["nfrs"] = {}
|
||||
res["nfrs"]["deletion_protection"] = False
|
||||
inputs = res.get("inputs", {})
|
||||
if "desired_count" in inputs:
|
||||
inputs["desired_count"] = 0
|
||||
if "min_capacity" in inputs:
|
||||
inputs["min_capacity"] = 0
|
||||
if "max_capacity" in inputs:
|
||||
inputs["max_capacity"] = 0
|
||||
return stack_instance
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
"""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
|
||||
@@ -55,12 +55,10 @@ def load(env_name, root=None):
|
||||
|
||||
|
||||
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 (
|
||||
"=== Nova Environment Onboarding ===\n"
|
||||
"=== ACDL 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"
|
||||
"ACDL 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"
|
||||
@@ -68,15 +66,13 @@ def _onboarding_message(env_name):
|
||||
" - 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"
|
||||
"To request an environment:\n"
|
||||
" 1. Contact the platform team 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"
|
||||
" 2. The platform team provisions the account/network/state/role\n"
|
||||
" and binds the environment to your repo.\n"
|
||||
" 3. Your next pipeline run will proceed normally.\n\n"
|
||||
"Expected turnaround: contact the platform team for current SLA.\n"
|
||||
"===================================\n"
|
||||
)
|
||||
|
||||
|
||||
@@ -33,13 +33,5 @@ halting the pipeline before any work is done.
|
||||
|
||||
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
|
||||
it to the consumer repo.
|
||||
|
||||
**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).
|
||||
it to the consumer repo. Self-service environment provisioning is on the
|
||||
roadmap; today it is a platform-team action.
|
||||
+4
-26
@@ -1,6 +1,6 @@
|
||||
"""HITL pre-execution attestation gates (REQ-108, D-084).
|
||||
|
||||
Records the approver identity (the CI actor (GITHUB_ACTOR or FORGE_ACTOR)) to the
|
||||
Records the approver identity (`gitea.actor` / `github.actor`) to the
|
||||
DynamoDB outbox for the contractId (attribute `approver_qa` /
|
||||
`approver_prod` / `approver_dr`), runs the separation-of-duties check on
|
||||
prod, invokes the 8-concern attestation matrix for the target env, and
|
||||
@@ -12,10 +12,6 @@ import os
|
||||
import sys
|
||||
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:
|
||||
return {"qa": "approver_qa", "prod": "approver_prod", "dr": "approver_dr"}.get(env, "")
|
||||
@@ -29,7 +25,7 @@ def attest(contract_id: str, env: str, approver: str,
|
||||
Args:
|
||||
contract_id: the contract UUID.
|
||||
env: dev/qa/prod/dr.
|
||||
approver: the approver's username (the CI actor (GITHUB_ACTOR or FORGE_ACTOR)).
|
||||
approver: the approver's username (`gitea.actor` / `github.actor`).
|
||||
evidence: optional operator-supplied evidence artifacts (for the
|
||||
attestation matrix operator-supplied concerns).
|
||||
outbox_client: optional moto-mocked DynamoDB outbox client for tests.
|
||||
@@ -41,7 +37,7 @@ def attest(contract_id: str, env: str, approver: str,
|
||||
return (True, "dev autonomous (no HITL gate)")
|
||||
|
||||
if not approver:
|
||||
return (False, f"no approver identity for {env} (GITHUB_ACTOR/FORGE_ACTOR unset)")
|
||||
return (False, f"no approver identity for {env} (GITHUB_ACTOR/GITEA_ACTOR unset)")
|
||||
|
||||
attr = _approver_attr(env)
|
||||
if not attr:
|
||||
@@ -65,30 +61,12 @@ def attest(contract_id: str, env: str, approver: str,
|
||||
if not ok:
|
||||
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}")
|
||||
|
||||
|
||||
def approver_from_env() -> Optional[str]:
|
||||
"""Read the approver identity from the environment."""
|
||||
return os.environ.get("GITHUB_ACTOR") or os.environ.get("FORGE_ACTOR")
|
||||
return os.environ.get("GITHUB_ACTOR") or os.environ.get("GITEA_ACTOR")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+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
|
||||
canary/deployment-rollback model is explicitly not in scope for v1.
|
||||
|
||||
## Forge-specific gate mechanics (D-042)
|
||||
## Gitea-specific gate mechanics (D-042)
|
||||
|
||||
The dev forge has **no Environments API** and ignores `environment:` blocks
|
||||
Gitea has **no Environments API** and ignores `environment:` blocks
|
||||
(v1.0 D-013; re-confirmed in RESEARCH TARGET 1). The pre-execution gate
|
||||
is modeled as a `workflow_dispatch` with approval inputs:
|
||||
|
||||
- **qa gate:** `workflow_dispatch` with `approve_qa: true`; the dispatch
|
||||
run's `CI actor` is the QA approver.
|
||||
run's `gitea.actor` is the QA approver.
|
||||
- **prod gate:** `workflow_dispatch` with `approve_prod: true`;
|
||||
`CI actor` is the SRE approver.
|
||||
`gitea.actor` is the SRE approver.
|
||||
- **dr gate:** `workflow_dispatch` with `approve_dr: true`; same.
|
||||
|
||||
The approver identity of record = `CI actor` of the dispatch run
|
||||
(D-042). There is no other approval-identity signal in the dev forge. The real
|
||||
OIDC path (blocked on upstream forge OIDC support) does not change this —
|
||||
The approver identity of record = `gitea.actor` of the dispatch run
|
||||
(D-042). There is no other approval-identity signal in Gitea. The real
|
||||
OIDC path (blocked on go-gitea/gitea#36988) does not change this —
|
||||
OIDC authorizes the *runner* to AWS, it does not change how the platform
|
||||
records the *human* approver.
|
||||
|
||||
On GitHub, the equivalent is `CI actor` of the `workflow_dispatch`
|
||||
On GitHub, the equivalent is `github.actor` of the `workflow_dispatch`
|
||||
run; GitHub Environments with required reviewers are the native gate,
|
||||
but the `workflow_dispatch` approval-input fallback is used for
|
||||
byte-identical across forges.
|
||||
byte-identical Gitea + GitHub workflows.
|
||||
|
||||
## Reviewer routing (ARCHITECTURE.md §10.2)
|
||||
|
||||
CODEOWNERS routes the right reviewer to the right gate:
|
||||
Gitea CODEOWNERS routes the right reviewer to the right gate:
|
||||
|
||||
- qa → QA 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`,
|
||||
`type`, `payload`, and an optional `signature` (JWS detached). Freshness
|
||||
is validated against the window above. Signature verification runs when
|
||||
`NOVA_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged when
|
||||
`ACDL_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged when
|
||||
unset (dev/CI — D-089). The matrix fails loud if an operator-supplied
|
||||
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 |
|
||||
| 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit `PENDING_ATTESTATION_AUTO_FREEZE` event; new submission linked via `supersedes` |
|
||||
|
||||
**Implementation:** an `on: schedule` workflow (runs hourly) that
|
||||
**Implementation:** a Gitea `on: schedule` workflow (runs hourly) that
|
||||
scans the DynamoDB outbox for `PENDING_ATTESTATION` events with `ts`
|
||||
older than 1/2 business days and emits the warn/freeze events. Not
|
||||
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:
|
||||
|
||||
1. On promotion dev → qa, the platform reads the QA approver's identity
|
||||
from the `workflow_dispatch` run's `CI actor`
|
||||
from the `workflow_dispatch` run's `gitea.actor` (or `github.actor`)
|
||||
and writes it to the DynamoDB outbox keyed by `contractId` (attribute
|
||||
`approver_qa`).
|
||||
2. On promotion qa → prod, the platform reads the stored `approver_qa`
|
||||
from the outbox and the new SRE approver identity from the
|
||||
from the outbox and the new SRE approver's `gitea.actor` from the
|
||||
prod-dispatch run.
|
||||
3. If `approver_qa == approver_prod`, the platform blocks the prod
|
||||
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.
|
||||
|
||||
v1.9 implements `route_halt_artifact` as a real SNS publish (topic
|
||||
`acdl-sod-halt`, ARN from `NOVA_SOD_HALT_TOPIC_ARN`) with an outbox-event
|
||||
`acdl-sod-halt`, ARN from `ACDL_SOD_HALT_TOPIC_ARN`) with an outbox-event
|
||||
fallback when the topic ARN is unset (REQ-107). The attestation gate
|
||||
itself is `core/hitl_gates.py` (`attest(contract_id, env, approver,
|
||||
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
|
||||
|
||||
- **D-042** — approver identity = `CI actor` of the `workflow_dispatch`
|
||||
run; no Environments API in the dev forge.
|
||||
- **D-042** — approver identity = `gitea.actor` of the `workflow_dispatch`
|
||||
run; no Environments API in Gitea. On GitHub, `github.actor`.
|
||||
- **D-013** (v1.0) — the `workflow_dispatch` approval-input fallback,
|
||||
re-used for the real platform's pre-execution gate model.
|
||||
- **D-084** (v1.9) — 8-concern attestation matrix: offline-testable
|
||||
concerns run for real; operator-supplied concerns accept signed
|
||||
evidence artifacts validated for freshness + schema.
|
||||
- **D-089** (v1.9) — attestation artifact signature verification is
|
||||
skipped when `NOVA_ATTESTATION_SIGNING_KEY_ID` is unset (dev/CI);
|
||||
skipped when `ACDL_ATTESTATION_SIGNING_KEY_ID` is unset (dev/CI);
|
||||
required for prod/dr.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Invoked via a Function URL (IAM auth) by consumer pipelines (one-way
|
||||
communication, D-051). Accepts { consumerRepo, contractId, contract,
|
||||
environment, action } and writes contracts to DynamoDB table nova-contracts
|
||||
environment, action } and writes contracts to DynamoDB table acdl-contracts
|
||||
(PK consumerRepo, SK contractId#submittedAt).
|
||||
|
||||
The report_error action (D-055) creates a GitHub issue on the platform repo
|
||||
@@ -17,66 +17,22 @@ requests. The invoke policy is scoped via ABAC (consumer repo identity).
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
|
||||
import boto3
|
||||
|
||||
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "nova-contracts")
|
||||
CHANGE_REQUESTS_TABLE = os.environ.get("CHANGE_REQUESTS_TABLE", "nova-change-requests")
|
||||
GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "nova/github-token")
|
||||
PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "nova/acdl")
|
||||
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "acdl-contracts")
|
||||
CHANGE_REQUESTS_TABLE = os.environ.get("CHANGE_REQUESTS_TABLE", "acdl-change-requests")
|
||||
GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "acdl/github-token")
|
||||
PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "acdl/acdl")
|
||||
# P1-9: Forge-agnostic API base URL. Defaults to GitHub; set GITHUB_API_BASE
|
||||
# to a compatible forge API root (e.g. https://forge.example.com/api/v1).
|
||||
# to a Gitea API root (e.g. https://git.cloudinit.dev/api/v1) for Gitea.
|
||||
GITHUB_API_BASE = os.environ.get("GITHUB_API_BASE", "https://api.github.com")
|
||||
|
||||
# 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
|
||||
_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():
|
||||
global _dynamodb
|
||||
if _dynamodb is None:
|
||||
@@ -96,22 +52,22 @@ def _iso8601_now():
|
||||
|
||||
|
||||
def _forge_type():
|
||||
"""Detect whether the API base is GitHub or a compatible forge.
|
||||
"""P1-9: Detect whether the API base is GitHub or Gitea.
|
||||
|
||||
Compatible forge API roots contain '/api/v1'; GitHub's is 'api.github.com'.
|
||||
Gitea API roots contain '/api/v1'; GitHub's is 'api.github.com'.
|
||||
"""
|
||||
if "/api/v1" in GITHUB_API_BASE:
|
||||
return "generic_forge"
|
||||
return "gitea"
|
||||
return "github"
|
||||
|
||||
|
||||
def _issues_search_url(owner, repo, encoded_query):
|
||||
"""Build the issue search URL based on forge type.
|
||||
"""P1-9: Build the issue search URL based on forge type.
|
||||
|
||||
GitHub uses /search/issues?q=...; compatible forges use /repos/{owner}/{repo}/issues?...
|
||||
GitHub uses /search/issues?q=...; Gitea uses /repos/{owner}/{repo}/issues?...
|
||||
with query params (no /search/issues endpoint).
|
||||
"""
|
||||
if _forge_type() == "generic_forge":
|
||||
if _forge_type() == "gitea":
|
||||
return (
|
||||
f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
|
||||
f"?state=open&type=issues&q={encoded_query}"
|
||||
@@ -123,7 +79,7 @@ def _issues_search_url(owner, repo, encoded_query):
|
||||
|
||||
|
||||
def _issues_create_url(owner, repo):
|
||||
"""URL for creating an issue (same pattern across forges)."""
|
||||
"""URL for creating an issue (same pattern for both GitHub + Gitea)."""
|
||||
return f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
|
||||
|
||||
|
||||
@@ -137,25 +93,6 @@ def _submit_contract(payload):
|
||||
contract_id = payload["contractId"]
|
||||
contract = payload["contract"]
|
||||
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()
|
||||
table = _get_dynamodb().Table(TABLE_NAME)
|
||||
item = {
|
||||
@@ -193,7 +130,7 @@ def _report_error(payload):
|
||||
contract_id = payload["contractId"]
|
||||
error = payload.get("error", "unknown error")
|
||||
run_url = payload.get("runUrl", "")
|
||||
stack_trace = payload.get("stackTrace", "")[:MAX_ERROR_FIELD_CHARS] # P11: aligned cap
|
||||
stack_trace = payload.get("stackTrace", "")[:2000] # truncate
|
||||
|
||||
# Get the GitHub token from Secrets Manager
|
||||
secrets = _get_secrets_client()
|
||||
@@ -204,7 +141,7 @@ def _report_error(payload):
|
||||
raise RuntimeError(f"failed to read GitHub token from Secrets Manager: {e}")
|
||||
|
||||
owner, repo = PLATFORM_REPO.split("/")
|
||||
title = f"[NOVA-ALERT] Deploy failure: {consumer_repo} / {contract_id}"
|
||||
title = f"[ACDL-ALERT] Deploy failure: {consumer_repo} / {contract_id}"
|
||||
|
||||
# Check for an existing open issue with the same title (idempotency)
|
||||
# URL-encode the contract_id to prevent search-query injection (P1-1).
|
||||
@@ -217,16 +154,7 @@ def _report_error(payload):
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
search_result = json.loads(resp.read())
|
||||
existing = search_result.get("items", [])
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
existing = []
|
||||
else:
|
||||
import sys
|
||||
print(f"WARNING: GitHub issue search failed (HTTP {e.code}): {e}", file=sys.stderr)
|
||||
existing = []
|
||||
except urllib.error.URLError as e:
|
||||
import sys
|
||||
print(f"WARNING: GitHub issue search network error: {e}", file=sys.stderr)
|
||||
except Exception:
|
||||
existing = []
|
||||
|
||||
body = f"""## Deploy Failure Report
|
||||
@@ -250,7 +178,7 @@ def _report_error(payload):
|
||||
{stack_trace}
|
||||
```
|
||||
|
||||
_This issue was auto-created by the Nova platform Lambda (D-055). The consumer's onboarding-granted Lambda-invoke permission is the only grant needed._
|
||||
_This issue was auto-created by the ACDL platform Lambda (D-055). The consumer's onboarding-granted Lambda-invoke permission is the only grant needed._
|
||||
"""
|
||||
|
||||
if existing:
|
||||
@@ -298,64 +226,29 @@ def _validate_caller_identity(event, payload):
|
||||
in the payload matches the principal's ARN-derived source identity, preventing
|
||||
one consumer from impersonating another.
|
||||
|
||||
P10 (REQ-174): if the IAM identity is absent (no callerArn), the function
|
||||
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.
|
||||
If the identity is not available (e.g. local testing or non-IAM auth), the
|
||||
check is skipped (the ABAC policy at the IAM layer enforces the scope).
|
||||
"""
|
||||
identity = event.get("requestContext", {}).get("identity", {})
|
||||
caller_arn = identity.get("userArn", "")
|
||||
if not caller_arn:
|
||||
# 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"
|
||||
)
|
||||
return # no identity available — rely on IAM ABAC enforcement
|
||||
payload_repo = payload.get("consumerRepo", "")
|
||||
if payload_repo:
|
||||
# consumerRepo must be org/repo format, <=128 chars
|
||||
if "/" not in payload_repo or len(payload_repo) > 128:
|
||||
raise ValueError(f"invalid consumerRepo format: {payload_repo!r}")
|
||||
|
||||
# v1.14 (REQ-144): contractId format validation
|
||||
contract_id = payload.get("contractId", "")
|
||||
if contract_id:
|
||||
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]
|
||||
if not payload_repo:
|
||||
return
|
||||
# Extract the session name or principal tag from the ARN. The ABAC policy
|
||||
# scopes via aws:PrincipalTag/acdl:owner = <consumerRepo>. The Function URL
|
||||
# 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
|
||||
# repo identifier (org/repo format). Full enforcement is at the IAM layer.
|
||||
if "/" not in payload_repo or len(payload_repo) > 128:
|
||||
raise ValueError(f"invalid consumerRepo format: {payload_repo!r}")
|
||||
|
||||
|
||||
def _validate_change_request(payload):
|
||||
"""REQ-93: Validate a change request ID against the CMDB (DynamoDB).
|
||||
|
||||
Queries the nova-change-requests table for the given changeRequestId.
|
||||
Queries the acdl-change-requests table for the given changeRequestId.
|
||||
Returns the CR details if status is 'approved' and the consumerRepo matches.
|
||||
Raises ValueError if the CR is not found, not approved, or the repo doesn't match.
|
||||
"""
|
||||
@@ -398,65 +291,6 @@ def _validate_change_request(payload):
|
||||
}
|
||||
|
||||
|
||||
def _onboard_consumer(payload):
|
||||
"""P18 (REQ-182): accept a self-service onboarding request.
|
||||
|
||||
Validates the payload against schemas/onboarding.schema.json, then
|
||||
writes a 'pending' row to nova-contracts (D-119). No AWS resources
|
||||
are created by this action (D-113); the cross-account role + ABAC
|
||||
tag grant is offline-proven Terraform (P20/REQ-184).
|
||||
"""
|
||||
import jsonschema
|
||||
schema_path = os.path.join(os.path.dirname(os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__)))),
|
||||
"schemas", "onboarding.schema.json")
|
||||
try:
|
||||
with open(schema_path) as f:
|
||||
schema = json.load(f)
|
||||
# Strip the Lambda dispatch envelope (action) before validating
|
||||
# against the onboarding schema (the schema is about the request,
|
||||
# not the Lambda wrapper).
|
||||
onboarding_payload = {k: v for k, v in payload.items() if k != "action"}
|
||||
jsonschema.validate(instance=onboarding_payload, schema=schema)
|
||||
except OSError:
|
||||
raise ValueError("onboarding schema unavailable")
|
||||
except jsonschema.ValidationError as e:
|
||||
raise ValueError(f"onboarding payload invalid: {e.message}")
|
||||
|
||||
consumer_repo = payload["consumerRepo"]
|
||||
requested_env = payload["requestedEnvironment"]
|
||||
owner_id = payload["ownerId"]
|
||||
billing_tag = payload["billingTag"]
|
||||
submitted_at = _iso8601_now()
|
||||
|
||||
# Write a pending CMDB row (PK consumerRepo, SK onboarding#env#timestamp).
|
||||
table = _get_dynamodb().Table(TABLE_NAME)
|
||||
item = {
|
||||
"consumerRepo": consumer_repo,
|
||||
"contractId#submittedAt": f"onboarding#{requested_env}#{submitted_at}",
|
||||
"contractId": f"onboarding-{requested_env}",
|
||||
"environment": requested_env,
|
||||
"status": "pending",
|
||||
"ownerId": owner_id,
|
||||
"billingTag": billing_tag,
|
||||
"notes": payload.get("notes", ""),
|
||||
"submittedAt": submitted_at,
|
||||
}
|
||||
table.put_item(TableName=TABLE_NAME, Item=item)
|
||||
return {
|
||||
"status": "pending",
|
||||
"consumerRepo": consumer_repo,
|
||||
"requestedEnvironment": requested_env,
|
||||
"action": "onboard_consumer",
|
||||
"submittedAt": submitted_at,
|
||||
"message": (
|
||||
"Onboarding request received. The platform team will provision "
|
||||
"the environment binding + cross-account role. Track the status "
|
||||
"via the nova-contracts table (status=pending → granted)."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def lambda_handler(event, context):
|
||||
"""AWS Lambda handler entry point.
|
||||
|
||||
@@ -485,8 +319,6 @@ def lambda_handler(event, context):
|
||||
result = _report_error(payload)
|
||||
elif action == "validate_change_request":
|
||||
result = _validate_change_request(payload)
|
||||
elif action == "onboard_consumer":
|
||||
result = _onboard_consumer(payload)
|
||||
else:
|
||||
return {
|
||||
"statusCode": 400,
|
||||
@@ -494,28 +326,6 @@ def lambda_handler(event, context):
|
||||
}
|
||||
return {"statusCode": 200, "body": json.dumps(result)}
|
||||
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)})}
|
||||
except Exception as e: # pragma: no cover - defensive top-level guard
|
||||
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>")
|
||||
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
|
||||
+10
-35
@@ -12,9 +12,7 @@ evidence event) runs end-to-end against the local tier with no AWS:
|
||||
|
||||
Each adapter exposes the same interface as the live counterpart so the
|
||||
caller code path is unchanged; only the I/O target swaps. Selection is
|
||||
gated on the NOVA_LOCAL_TIER env var (set by run_platform.sh --local).
|
||||
Env vars read via core/env.py (NOVA_* only; the ACDL_* fallback was
|
||||
removed in v1.15 P5, REQ-164).
|
||||
gated on the ACDL_LOCAL_TIER env var (set by run_platform.sh --local).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -34,20 +32,12 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
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
|
||||
|
||||
|
||||
def is_local_tier() -> bool:
|
||||
"""True when the local emulating tier is active."""
|
||||
return env.get_env("LOCAL_TIER", "") == "1"
|
||||
return os.environ.get("ACDL_LOCAL_TIER", "") == "1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -69,7 +59,7 @@ class FlatFileOutbox:
|
||||
|
||||
@classmethod
|
||||
def create(cls, dir: Optional[Path] = None) -> "FlatFileOutbox":
|
||||
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="nova_outbox_"))
|
||||
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="acdl_outbox_"))
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
out = cls(dir=d)
|
||||
# Re-read the chain tail if the file already exists.
|
||||
@@ -88,7 +78,7 @@ class FlatFileOutbox:
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
def write_event(self, event: Dict[str, Any],
|
||||
outbox_table: str = "nova-outbox-local",
|
||||
outbox_table: str = "acdl-outbox-local",
|
||||
region: str = "local") -> Dict[str, Any]:
|
||||
"""Write an evidence event to the flat-file outbox.
|
||||
|
||||
@@ -250,7 +240,7 @@ class LocalS3StateBackend:
|
||||
|
||||
@classmethod
|
||||
def create(cls, dir: Optional[Path] = None) -> "LocalS3StateBackend":
|
||||
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="nova_tfstate_"))
|
||||
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="acdl_tfstate_"))
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return cls(state_dir=d)
|
||||
|
||||
@@ -296,7 +286,7 @@ class LocalLambdaStub:
|
||||
|
||||
Returns the handler's response dict
|
||||
({statusCode, body}). The handler's DynamoDB calls are
|
||||
intercepted via the NOVA_LOCAL_TIER env var (the handler checks
|
||||
intercepted via the ACDL_LOCAL_TIER env var (the handler checks
|
||||
_get_dynamodb(); under local tier it would need patching - we
|
||||
patch the module's _get_dynamodb to return a local stub)."""
|
||||
# Import the handler module (the dir is named `lambda`, a Python
|
||||
@@ -381,9 +371,8 @@ class LocalLambdaStub:
|
||||
return _FakeResponse(
|
||||
json.dumps([{"number": 1, "title": "stub"}]).encode())
|
||||
urllib.request.urlopen = _fake_urlopen
|
||||
except (AttributeError, TypeError) as e:
|
||||
import sys
|
||||
print(f"WARNING: could not patch urlopen for local Lambda stub: {e}", file=sys.stderr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
event = {
|
||||
@@ -392,24 +381,12 @@ class LocalLambdaStub:
|
||||
"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)
|
||||
finally:
|
||||
ci._get_dynamodb = original_get
|
||||
if original_urlopen is not None:
|
||||
import urllib.request
|
||||
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
|
||||
|
||||
|
||||
@@ -441,7 +418,7 @@ def run_local_e2e(contract_path: str, repo_root: Optional[Path] = None) -> Dict[
|
||||
|
||||
stack = resolve(contract_path, str(root))
|
||||
stack_name = stack["stack"]["name"]
|
||||
work = Path(tempfile.mkdtemp(prefix="nova_local_e2e_"))
|
||||
work = Path(tempfile.mkdtemp(prefix="acdl_local_e2e_"))
|
||||
tf_dir = work / "tf"
|
||||
tf_dir.mkdir(exist_ok=True)
|
||||
adapter.adapt(stack, str(tf_dir))
|
||||
@@ -512,8 +489,6 @@ def run_local_e2e(contract_path: str, repo_root: Optional[Path] = None) -> Dict[
|
||||
|
||||
if __name__ == "__main__":
|
||||
contract = sys.argv[1] if len(sys.argv) > 1 else "contracts/microservice.yml"
|
||||
# Set so is_local_tier() finds NOVA_LOCAL_TIER (NOVA_* only; the
|
||||
# ACDL_* alias was removed in v1.15 P5, REQ-164).
|
||||
os.environ["NOVA_LOCAL_TIER"] = "1"
|
||||
os.environ["ACDL_LOCAL_TIER"] = "1"
|
||||
result = run_local_e2e(contract)
|
||||
print(json.dumps(result, indent=2))
|
||||
@@ -1,364 +0,0 @@
|
||||
"""Nova Metrics Collector (REQ-189, P2).
|
||||
|
||||
Reads all grounded signals (REGRESSION_REPORT.json, per-run manifests,
|
||||
junit XML, pcr.json, signal.json, COST.md, decision ledger, coverage.json)
|
||||
and normalizes them into a SQLite cold store at metrics/nova_metrics.db.
|
||||
|
||||
D-120: Nova-native (SQLite, no ClickHouse/BigQuery).
|
||||
D-125: hybrid model — reads files + events → SQLite.
|
||||
D-126: cold-only (no hot path; hot path deferred D-096).
|
||||
D-128: metrics/ at repo root.
|
||||
|
||||
Idempotent: re-running the collector against the same inputs produces
|
||||
identical row counts (REQ-200). The collector uses INSERT OR REPLACE
|
||||
on fact tables keyed by natural keys.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
_METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
|
||||
_STORE_PATH = os.path.join(_METRICS_DIR, "nova_metrics.db")
|
||||
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
_REGRESSION_REPORT = os.path.join(_REPO_ROOT, ".ciagent", "REGRESSION_REPORT.json")
|
||||
_RUNS_DIR = os.path.join(_METRICS_DIR, "runs")
|
||||
_LEDGER_DB = os.path.join(_METRICS_DIR, "decision_ledger.db")
|
||||
_COVERAGE_JSON = os.path.join(_METRICS_DIR, "coverage.json")
|
||||
_TEST_RESULTS_XML = os.path.join(_METRICS_DIR, "test-results.xml")
|
||||
|
||||
|
||||
def _iso8601_now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _init_store(db_path=None):
|
||||
"""Create the fact/dim tables in the SQLite cold store."""
|
||||
if db_path is None:
|
||||
db_path = _STORE_PATH
|
||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS fact_run (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
contract_id TEXT,
|
||||
environment TEXT,
|
||||
started_at TEXT,
|
||||
completed_at TEXT,
|
||||
exit_code INTEGER,
|
||||
outcome TEXT,
|
||||
confidence_score REAL,
|
||||
confidence_band TEXT,
|
||||
hitl_block INTEGER,
|
||||
cost_estimate_usd REAL,
|
||||
decision_id TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fact_capability (
|
||||
capability_id TEXT,
|
||||
run_id TEXT,
|
||||
name TEXT,
|
||||
status TEXT,
|
||||
tier TEXT,
|
||||
duration_ms REAL,
|
||||
detail TEXT,
|
||||
run_at_utc TEXT,
|
||||
PRIMARY KEY (capability_id, run_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fact_policy_check (
|
||||
run_id TEXT,
|
||||
rule_id TEXT,
|
||||
severity TEXT,
|
||||
result TEXT,
|
||||
resource_ref TEXT,
|
||||
evaluated_at TEXT,
|
||||
PRIMARY KEY (run_id, rule_id, resource_ref)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fact_confidence (
|
||||
run_id TEXT,
|
||||
score REAL,
|
||||
band TEXT,
|
||||
per_input TEXT,
|
||||
reason_codes TEXT,
|
||||
environment TEXT,
|
||||
computed_at TEXT,
|
||||
PRIMARY KEY (run_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fact_test (
|
||||
run_id TEXT,
|
||||
total_tests INTEGER,
|
||||
passed INTEGER,
|
||||
failed INTEGER,
|
||||
errors INTEGER,
|
||||
skipped INTEGER,
|
||||
duration_s REAL,
|
||||
coverage_pct REAL,
|
||||
collected_at TEXT,
|
||||
PRIMARY KEY (run_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fact_decision (
|
||||
decision_id TEXT,
|
||||
run_id TEXT,
|
||||
chosen_action TEXT,
|
||||
confidence REAL,
|
||||
alternatives TEXT,
|
||||
human_override INTEGER,
|
||||
outcome TEXT,
|
||||
event_time TEXT,
|
||||
PRIMARY KEY (decision_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fact_cost_estimate (
|
||||
run_id TEXT,
|
||||
delta_usd REAL,
|
||||
total_monthly_usd REAL,
|
||||
available INTEGER,
|
||||
estimated_at TEXT,
|
||||
PRIMARY KEY (run_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fact_lifecycle (
|
||||
module TEXT,
|
||||
environment TEXT,
|
||||
phase TEXT,
|
||||
result TEXT,
|
||||
duration_ms REAL,
|
||||
run_at TEXT,
|
||||
PRIMARY KEY (module, environment, phase, run_at)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dim_capability (
|
||||
capability_id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
tier TEXT,
|
||||
source_milestone TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dim_milestone (
|
||||
milestone TEXT PRIMARY KEY,
|
||||
phase INTEGER,
|
||||
tag TEXT,
|
||||
completed_at TEXT
|
||||
);
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def collect_regression_report(db_path=None, report_path=None):
|
||||
"""Read REGRESSION_REPORT.json → fact_capability + dim_capability."""
|
||||
if db_path is None:
|
||||
db_path = _STORE_PATH
|
||||
if report_path is None:
|
||||
report_path = _REGRESSION_REPORT
|
||||
if not os.path.isfile(report_path):
|
||||
return 0
|
||||
_init_store(db_path)
|
||||
with open(report_path) as f:
|
||||
report = json.load(f)
|
||||
run_id = report.get("run_id", f"regr-{report.get('run_at_utc','')}")
|
||||
run_at = report.get("run_at_utc", _iso8601_now())
|
||||
milestone = report.get("milestone", "")
|
||||
conn = sqlite3.connect(db_path)
|
||||
for result in report.get("results", []):
|
||||
cap_id = result.get("capability_id", "")
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO fact_capability
|
||||
(capability_id, run_id, name, status, tier, duration_ms, detail, run_at_utc)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (cap_id, run_id, result.get("name", ""), result.get("status", ""),
|
||||
result.get("tier", ""), result.get("duration_ms", 0),
|
||||
result.get("detail", ""), run_at))
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO dim_capability
|
||||
(capability_id, name, tier, source_milestone)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (cap_id, result.get("name", ""), result.get("tier", ""), milestone))
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO dim_milestone
|
||||
(milestone, phase, tag, completed_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (milestone, report.get("phase", 0), "", run_at))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return len(report.get("results", []))
|
||||
|
||||
|
||||
def collect_run_manifests(db_path=None, runs_dir=None):
|
||||
"""Read per-run manifests from metrics/runs/*.json → fact_run."""
|
||||
if db_path is None:
|
||||
db_path = _STORE_PATH
|
||||
if runs_dir is None:
|
||||
runs_dir = _RUNS_DIR
|
||||
if not os.path.isdir(runs_dir):
|
||||
return 0
|
||||
_init_store(db_path)
|
||||
count = 0
|
||||
conn = sqlite3.connect(db_path)
|
||||
for fname in sorted(os.listdir(runs_dir)):
|
||||
if not fname.endswith(".json"):
|
||||
continue
|
||||
fpath = os.path.join(runs_dir, fname)
|
||||
if os.path.isdir(fpath):
|
||||
continue
|
||||
with open(fpath) as f:
|
||||
manifest = json.load(f)
|
||||
run_id = manifest.get("run_id", fname.replace(".json", ""))
|
||||
conf = manifest.get("confidence", {})
|
||||
hitl = manifest.get("hitl", {})
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO fact_run
|
||||
(run_id, contract_id, environment, started_at, completed_at,
|
||||
exit_code, outcome, confidence_score, confidence_band,
|
||||
hitl_block, cost_estimate_usd, decision_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (run_id, manifest.get("contract_id", ""), manifest.get("environment", ""),
|
||||
manifest.get("started_at", ""), manifest.get("completed_at", ""),
|
||||
manifest.get("exit_code", 0), manifest.get("outcome", ""),
|
||||
conf.get("score", 0), conf.get("band", ""),
|
||||
1 if hitl.get("block") else 0,
|
||||
manifest.get("cost_estimate_usd", 0), manifest.get("decision_id", "")))
|
||||
count += 1
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return count
|
||||
|
||||
|
||||
def collect_decision_ledger(db_path=None, ledger_db=None):
|
||||
"""Read the Decision Ledger SQLite → fact_decision."""
|
||||
if db_path is None:
|
||||
db_path = _STORE_PATH
|
||||
if ledger_db is None:
|
||||
ledger_db = _LEDGER_DB
|
||||
if not os.path.isfile(ledger_db):
|
||||
return 0
|
||||
_init_store(db_path)
|
||||
ledger_conn = sqlite3.connect(ledger_db)
|
||||
rows = ledger_conn.execute(
|
||||
"SELECT event_type, run_id, event_time, payload FROM decision_ledger WHERE event_type = 'nova.ai.decision.made' ORDER BY seq"
|
||||
).fetchall()
|
||||
ledger_conn.close()
|
||||
conn = sqlite3.connect(db_path)
|
||||
count = 0
|
||||
for etype, run_id, event_time, payload_json in rows:
|
||||
payload = json.loads(payload_json)
|
||||
data = payload.get("data", {})
|
||||
decision_id = data.get("decision_id", run_id)
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO fact_decision
|
||||
(decision_id, run_id, chosen_action, confidence, alternatives,
|
||||
human_override, outcome, event_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (decision_id, run_id, data.get("chosen_action", ""),
|
||||
data.get("confidence", 0), json.dumps(data.get("alternatives", {})),
|
||||
1 if data.get("human_override") else 0,
|
||||
data.get("outcome", "pending"), event_time))
|
||||
count += 1
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return count
|
||||
|
||||
|
||||
def collect_test_results(db_path=None, junit_path=None, coverage_path=None):
|
||||
"""Read junit XML + coverage.json → fact_test."""
|
||||
if db_path is None:
|
||||
db_path = _STORE_PATH
|
||||
if junit_path is None:
|
||||
junit_path = _TEST_RESULTS_XML
|
||||
if coverage_path is None:
|
||||
coverage_path = _COVERAGE_JSON
|
||||
if not os.path.isfile(junit_path):
|
||||
return 0
|
||||
_init_store(db_path)
|
||||
run_id = f"test-{_iso8601_now()}"
|
||||
total = passed = failed = errors = skipped = 0
|
||||
duration = 0.0
|
||||
try:
|
||||
tree = ET.parse(junit_path)
|
||||
root = tree.getroot()
|
||||
for suite in root.iter("testsuite"):
|
||||
total += int(suite.get("tests", 0))
|
||||
failed += int(suite.get("failures", 0))
|
||||
errors += int(suite.get("errors", 0))
|
||||
skipped += int(suite.get("skipped", 0))
|
||||
duration += float(suite.get("time", 0))
|
||||
passed = total - failed - errors - skipped
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
coverage_pct = 0.0
|
||||
if os.path.isfile(coverage_path):
|
||||
try:
|
||||
with open(coverage_path) as f:
|
||||
cov = json.load(f)
|
||||
coverage_pct = cov.get("totals", {}).get("percent_covered", 0.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO fact_test
|
||||
(run_id, total_tests, passed, failed, errors, skipped, duration_s, coverage_pct, collected_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (run_id, total, passed, failed, errors, skipped, duration, coverage_pct, _iso8601_now()))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return 1
|
||||
|
||||
|
||||
def collect_lifecycle_reports(db_path=None, lifecycle_dir=None):
|
||||
"""Read metrics/lifecycle/*.json → fact_lifecycle."""
|
||||
if db_path is None:
|
||||
db_path = _STORE_PATH
|
||||
if lifecycle_dir is None:
|
||||
lifecycle_dir = os.path.join(_METRICS_DIR, "lifecycle")
|
||||
if not os.path.isdir(lifecycle_dir):
|
||||
return 0
|
||||
_init_store(db_path)
|
||||
count = 0
|
||||
conn = sqlite3.connect(db_path)
|
||||
for fname in sorted(os.listdir(lifecycle_dir)):
|
||||
if not fname.endswith(".json"):
|
||||
continue
|
||||
fpath = os.path.join(lifecycle_dir, fname)
|
||||
with open(fpath) as f:
|
||||
report = json.load(f)
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO fact_lifecycle
|
||||
(module, environment, phase, result, duration_ms, run_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (report.get("module", ""), report.get("environment", ""),
|
||||
report.get("phase", ""), report.get("result", ""),
|
||||
report.get("duration_ms", 0), report.get("run_at", _iso8601_now())))
|
||||
count += 1
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return count
|
||||
|
||||
|
||||
def collect_all(db_path=None):
|
||||
"""Run all collectors. Returns a summary dict."""
|
||||
if db_path is None:
|
||||
db_path = _STORE_PATH
|
||||
_init_store(db_path)
|
||||
summary = {
|
||||
"capabilities": collect_regression_report(db_path),
|
||||
"runs": collect_run_manifests(db_path),
|
||||
"decisions": collect_decision_ledger(db_path),
|
||||
"tests": collect_test_results(db_path),
|
||||
"lifecycle": collect_lifecycle_reports(db_path),
|
||||
"collected_at": _iso8601_now(),
|
||||
}
|
||||
return summary
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = collect_all()
|
||||
print(json.dumps(result, indent=2))
|
||||
@@ -1,257 +0,0 @@
|
||||
"""Nova Decision Ledger — SQLite append-only hash-chain (REQ-188, D-121).
|
||||
|
||||
Extends outbox_writer.py to emit to a SQLite append-only table with a hash
|
||||
chain (prev_hash + own hash, SHA-256). Stores ai.decision.made events
|
||||
(decision_id=run_id, chosen_action=band, confidence=score,
|
||||
alternatives=perInput, human_override=HITL block) with outcome backfill
|
||||
from apply.completed. Also stores attestation.recorded events (D-132).
|
||||
|
||||
Honors D-083 (no S3 Object Lock/JWS — local SQLite hash-chain only).
|
||||
D-120: Nova-native (SQLite, no QLDB).
|
||||
D-128: metrics/ at repo root.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
_LEDGER_PATH = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
|
||||
"metrics", "decision_ledger.db",
|
||||
)
|
||||
|
||||
_GENESIS_HASH = "GENESIS"
|
||||
|
||||
|
||||
def _iso8601_now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _canonical_hash(event):
|
||||
"""SHA-256 over canonical JSON (sort_keys, compact separators)."""
|
||||
canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _init_db(db_path=None):
|
||||
"""Create the ledger table if it doesn't exist."""
|
||||
if db_path is None:
|
||||
db_path = _LEDGER_PATH
|
||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS decision_ledger (
|
||||
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_id TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
contract_id TEXT,
|
||||
environment TEXT,
|
||||
event_time TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
prev_hash TEXT NOT NULL,
|
||||
hash TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_run_id ON decision_ledger(run_id)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_event_type ON decision_ledger(event_type)")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def _get_last_hash(db_path=None):
|
||||
"""Get the hash of the last row in the ledger (or GENESIS if empty)."""
|
||||
if db_path is None:
|
||||
db_path = _LEDGER_PATH
|
||||
conn = sqlite3.connect(db_path)
|
||||
row = conn.execute("SELECT hash FROM decision_ledger ORDER BY seq DESC LIMIT 1").fetchone()
|
||||
conn.close()
|
||||
return row[0] if row else _GENESIS_HASH
|
||||
|
||||
|
||||
def append(event, db_path=None):
|
||||
"""Append an event to the Decision Ledger with hash-chain integrity.
|
||||
|
||||
Args:
|
||||
event: a CloudEvents 1.0 envelope dict (from event_envelope.make_event)
|
||||
db_path: path to the SQLite ledger
|
||||
|
||||
Returns:
|
||||
The row dict (seq, event_id, event_type, run_id, hash, prev_hash).
|
||||
"""
|
||||
if db_path is None:
|
||||
db_path = _LEDGER_PATH
|
||||
_init_db(db_path)
|
||||
prev_hash = _get_last_hash(db_path)
|
||||
event_hash = _canonical_hash(event)
|
||||
platform = event.get("platform", {})
|
||||
data = event.get("data", {})
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
cursor = conn.execute(
|
||||
"""INSERT INTO decision_ledger
|
||||
(event_id, event_type, run_id, contract_id, environment, event_time, payload, prev_hash, hash)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
event.get("id", ""),
|
||||
event.get("type", ""),
|
||||
platform.get("run_id", ""),
|
||||
platform.get("contract_id", ""),
|
||||
platform.get("environment", ""),
|
||||
event.get("time", _iso8601_now()),
|
||||
json.dumps(event, sort_keys=True),
|
||||
prev_hash,
|
||||
event_hash,
|
||||
),
|
||||
)
|
||||
seq = cursor.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"seq": seq, "event_id": event.get("id", ""), "event_type": event.get("type", ""),
|
||||
"run_id": platform.get("run_id", ""), "hash": event_hash, "prev_hash": prev_hash}
|
||||
|
||||
|
||||
def verify_chain(db_path=None):
|
||||
"""Verify the hash chain integrity. Returns (ok, broken_count, details).
|
||||
|
||||
Recomputes each row's hash from its payload and checks:
|
||||
1. The stored hash matches the recomputed hash.
|
||||
2. The prev_hash matches the previous row's hash.
|
||||
"""
|
||||
if db_path is None:
|
||||
db_path = _LEDGER_PATH
|
||||
_init_db(db_path)
|
||||
conn = sqlite3.connect(db_path)
|
||||
rows = conn.execute("SELECT seq, hash, prev_hash, payload FROM decision_ledger ORDER BY seq").fetchall()
|
||||
conn.close()
|
||||
if not rows:
|
||||
return True, 0, "empty ledger"
|
||||
|
||||
broken = 0
|
||||
details = []
|
||||
prev_hash = _GENESIS_HASH
|
||||
for seq, stored_hash, stored_prev, payload_json in rows:
|
||||
event = json.loads(payload_json)
|
||||
recomputed = _canonical_hash(event)
|
||||
if recomputed != stored_hash:
|
||||
broken += 1
|
||||
details.append(f"seq={seq}: hash mismatch (stored={stored_hash[:12]}... recomputed={recomputed[:12]}...)")
|
||||
if stored_prev != prev_hash:
|
||||
broken += 1
|
||||
details.append(f"seq={seq}: prev_hash mismatch (expected={prev_hash[:12]}... got={stored_prev[:12]}...)")
|
||||
prev_hash = stored_hash
|
||||
return broken == 0, broken, "; ".join(details) if details else "chain intact"
|
||||
|
||||
|
||||
def query_by_run(run_id, db_path=None):
|
||||
"""Query all ledger entries for a given run_id."""
|
||||
if db_path is None:
|
||||
db_path = _LEDGER_PATH
|
||||
_init_db(db_path)
|
||||
conn = sqlite3.connect(db_path)
|
||||
rows = conn.execute(
|
||||
"SELECT seq, event_type, event_time, payload FROM decision_ledger WHERE run_id = ? ORDER BY seq",
|
||||
(run_id,),
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [{"seq": r[0], "event_type": r[1], "event_time": r[2], "payload": json.loads(r[3])} for r in rows]
|
||||
|
||||
|
||||
def stats(db_path=None):
|
||||
"""Return ledger statistics."""
|
||||
if db_path is None:
|
||||
db_path = _LEDGER_PATH
|
||||
_init_db(db_path)
|
||||
conn = sqlite3.connect(db_path)
|
||||
total = conn.execute("SELECT COUNT(*) FROM decision_ledger").fetchone()[0]
|
||||
by_type = conn.execute("SELECT event_type, COUNT(*) FROM decision_ledger GROUP BY event_type").fetchall()
|
||||
by_env = conn.execute("SELECT environment, COUNT(*) FROM decision_ledger GROUP BY environment").fetchall()
|
||||
conn.close()
|
||||
return {
|
||||
"total": total,
|
||||
"by_event_type": dict(by_type),
|
||||
"by_environment": dict(by_env),
|
||||
}
|
||||
|
||||
|
||||
def export_since(since_iso, fmt="json", db_path=None):
|
||||
"""Export ledger entries since a given ISO8601 timestamp."""
|
||||
if db_path is None:
|
||||
db_path = _LEDGER_PATH
|
||||
_init_db(db_path)
|
||||
conn = sqlite3.connect(db_path)
|
||||
rows = conn.execute(
|
||||
"SELECT seq, event_type, run_id, event_time, payload FROM decision_ledger WHERE event_time >= ? ORDER BY seq",
|
||||
(since_iso,),
|
||||
).fetchall()
|
||||
conn.close()
|
||||
entries = [{"seq": r[0], "event_type": r[1], "run_id": r[2], "event_time": r[3], "payload": json.loads(r[4])} for r in rows]
|
||||
if fmt == "csv":
|
||||
import csv
|
||||
import io
|
||||
buf = io.StringIO()
|
||||
writer = csv.DictWriter(buf, fieldnames=["seq", "event_type", "run_id", "event_time", "payload"])
|
||||
writer.writeheader()
|
||||
for e in entries:
|
||||
e["payload"] = json.dumps(e["payload"])
|
||||
writer.writerow(e)
|
||||
return buf.getvalue()
|
||||
return json.dumps(entries, indent=2)
|
||||
|
||||
|
||||
def replay_run(run_id, db_path=None):
|
||||
"""Reconstruct a run's full event sequence from the ledger.
|
||||
|
||||
Prints the ordered event sequence (run.started -> policy.evaluated ->
|
||||
confidence.computed -> ai.decision.made -> attestation.recorded ->
|
||||
run.completed/failed) with the decision's confidence, alternatives,
|
||||
and outcome.
|
||||
"""
|
||||
if db_path is None:
|
||||
db_path = _LEDGER_PATH
|
||||
entries = query_by_run(run_id, db_path)
|
||||
if not entries:
|
||||
return f"no events found for run_id={run_id}"
|
||||
lines = [f"=== Replay: run_id={run_id} ({len(entries)} events) ==="]
|
||||
for e in entries:
|
||||
payload = e["payload"]
|
||||
data = payload.get("data", {})
|
||||
etype = e["event_type"]
|
||||
line = f" [{e['seq']}] {e['event_time']} {etype}"
|
||||
if etype == "nova.ai.decision.made":
|
||||
line += f" confidence={data.get('confidence', '?')} band={data.get('chosen_action', '?')} override={data.get('human_override', '?')}"
|
||||
elif etype == "nova.attestation.recorded":
|
||||
line += f" env={data.get('environment', '?')} approver={data.get('approver', '?')} result={data.get('result', '?')}"
|
||||
elif etype == "nova.run.completed":
|
||||
line += f" exit={data.get('exit_code', '?')} outcome={data.get('outcome', '?')}"
|
||||
elif etype == "nova.run.failed":
|
||||
line += f" exit={data.get('exit_code', '?')} outcome=failed"
|
||||
lines.append(line)
|
||||
lines.append("=== End replay ===")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: decision_ledger.py <verify-chain|stats|query|export|replay> [args]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "verify-chain":
|
||||
ok, broken, details = verify_chain()
|
||||
print(f"chain_ok={ok} broken={broken} details={details}")
|
||||
sys.exit(0 if ok else 1)
|
||||
elif cmd == "stats":
|
||||
print(json.dumps(stats(), indent=2))
|
||||
elif cmd == "query" and len(sys.argv) >= 3:
|
||||
print(json.dumps(query_by_run(sys.argv[2]), indent=2))
|
||||
elif cmd == "export" and len(sys.argv) >= 3:
|
||||
print(export_since(sys.argv[2]))
|
||||
elif cmd == "replay" and len(sys.argv) >= 3:
|
||||
print(replay_run(sys.argv[2]))
|
||||
else:
|
||||
print(f"unknown command: {cmd}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
@@ -1,39 +0,0 @@
|
||||
"""Nova Decision Ledger CLI (REQ-207).
|
||||
|
||||
Subcommands: query, verify-chain, stats, export, replay.
|
||||
Read-only CLI for the Decision Ledger SQLite hash-chain.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from core.metrics.decision_ledger import query_by_run, verify_chain, stats, export_since, replay_run
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: decision_ledger_cli.py <query|verify-chain|stats|export|replay> [args]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "query" and len(sys.argv) >= 3:
|
||||
print(json.dumps(query_by_run(sys.argv[2]), indent=2))
|
||||
elif cmd == "verify-chain":
|
||||
ok, broken, details = verify_chain()
|
||||
print(f"chain_ok={ok} broken={broken} details={details}")
|
||||
sys.exit(0 if ok else 1)
|
||||
elif cmd == "stats":
|
||||
print(json.dumps(stats(), indent=2))
|
||||
elif cmd == "export" and len(sys.argv) >= 3:
|
||||
fmt = sys.argv[3] if len(sys.argv) >= 4 else "json"
|
||||
print(export_since(sys.argv[2], fmt=fmt))
|
||||
elif cmd == "replay" and len(sys.argv) >= 3:
|
||||
print(replay_run(sys.argv[2]))
|
||||
else:
|
||||
print(f"unknown command: {cmd}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,98 +0,0 @@
|
||||
"""Nova CloudEvents 1.0 envelope + platform.* semantic conventions (REQ-187).
|
||||
|
||||
Defines the standard event envelope for all Nova metrics events. Every
|
||||
emitter (run_manifest, decision_ledger, confidence_signal, checkov_adapter,
|
||||
hitl_gates, regression_verify) uses `make_event()` to produce a valid
|
||||
CloudEvents 1.0 envelope. Events are appended to `metrics/events.jsonl`.
|
||||
|
||||
D-120: Nova-native minimal tech (no Kafka/OTel SDK — JSONL + SQLite).
|
||||
D-125: hybrid model — existing file signals stay as files; the collector
|
||||
reads them and emits normalized CloudEvents. New emitters emit directly.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
|
||||
EVENTS_LOG = os.path.join(METRICS_DIR, "events.jsonl")
|
||||
|
||||
|
||||
def _iso8601_now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def make_event(event_type, run_id, environment, data, contract_id="", source="nova.platform", subject="", actor_type="confidence-gate", actor_id="confidence_signal"):
|
||||
"""Build a CloudEvents 1.0 envelope with Nova platform.* conventions.
|
||||
|
||||
Args:
|
||||
event_type: e.g. "nova.run.completed", "nova.ai.decision.made"
|
||||
run_id: the run identifier (e.g. "run-<epoch>")
|
||||
environment: dev|qa|prod|dr
|
||||
data: the event payload dict
|
||||
contract_id: the contract UUID (optional)
|
||||
source: the event source (default "nova.platform")
|
||||
subject: the event subject (default "<contract_id>/<env>")
|
||||
actor_type: the actor type (default "confidence-gate")
|
||||
actor_id: the actor id (default "confidence_signal")
|
||||
|
||||
Returns:
|
||||
A CloudEvents 1.0 envelope dict.
|
||||
"""
|
||||
if not subject:
|
||||
subject = f"{contract_id}/{environment}" if contract_id else environment
|
||||
return {
|
||||
"specversion": "1.0",
|
||||
"id": str(uuid.uuid4()),
|
||||
"source": source,
|
||||
"type": event_type,
|
||||
"time": _iso8601_now(),
|
||||
"subject": subject,
|
||||
"datacontenttype": "application/json",
|
||||
"platform": {
|
||||
"tenant_id": "acdl",
|
||||
"run_id": run_id,
|
||||
"contract_id": contract_id,
|
||||
"environment": environment,
|
||||
"actor": {"type": actor_type, "id": actor_id},
|
||||
"trace_id": run_id,
|
||||
},
|
||||
"data": data,
|
||||
}
|
||||
|
||||
|
||||
def append_event(event, events_log=None):
|
||||
"""Append a CloudEvents envelope to the JSONL event log.
|
||||
|
||||
Creates the metrics/ directory if it doesn't exist.
|
||||
"""
|
||||
if events_log is None:
|
||||
events_log = EVENTS_LOG
|
||||
os.makedirs(os.path.dirname(events_log), exist_ok=True)
|
||||
with open(events_log, "a", encoding="utf-8") as fh:
|
||||
fh.write(json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n")
|
||||
|
||||
|
||||
def emit(event_type, run_id, environment, data, **kwargs):
|
||||
"""Make an event + append it to the JSONL log. Convenience wrapper."""
|
||||
event = make_event(event_type, run_id, environment, data, **kwargs)
|
||||
append_event(event)
|
||||
return event
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 4:
|
||||
print("usage: event_envelope.py <event_type> <run_id> <environment> [data.json]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
_type = sys.argv[1]
|
||||
_run_id = sys.argv[2]
|
||||
_env = sys.argv[3]
|
||||
_data = {}
|
||||
if len(sys.argv) >= 5 and os.path.isfile(sys.argv[4]):
|
||||
with open(sys.argv[4]) as f:
|
||||
_data = json.load(f)
|
||||
ev = emit(_type, _run_id, _env, _data)
|
||||
print(json.dumps(ev, indent=2))
|
||||
@@ -1,73 +0,0 @@
|
||||
"""Nova Infracost Post-Processor (REQ-187, D-120).
|
||||
|
||||
Runs Infracost on `terraform show -json plan.tfplan` (offline, reads plan
|
||||
JSON, no live AWS). Emits nova.cost.estimated{delta_usd} events. Degrades
|
||||
gracefully (omits the event, logs a warning) when Infracost CLI is absent
|
||||
(assumption A6).
|
||||
|
||||
run_platform.sh invokes it after the plan stage.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from core.metrics.event_envelope import emit
|
||||
|
||||
|
||||
def _is_infracost_available():
|
||||
"""Check if the Infracost CLI is on PATH."""
|
||||
return shutil.which("infracost") is not None
|
||||
|
||||
|
||||
def estimate(plan_json_path, run_id, contract_id, environment):
|
||||
"""Run Infracost on a terraform plan JSON. Returns the cost estimate dict.
|
||||
|
||||
Args:
|
||||
plan_json_path: path to `terraform show -json plan.tfplan` output
|
||||
run_id: the run identifier
|
||||
contract_id: the contract UUID
|
||||
environment: dev|qa|prod|dr
|
||||
|
||||
Returns:
|
||||
{"delta_usd": float, "total_monthly_usd": float, "available": bool}
|
||||
or {"available": False} if Infracost is not installed.
|
||||
"""
|
||||
if not _is_infracost_available():
|
||||
sys.stderr.write("[infracost] CLI not found — cost.estimated event omitted (A6 degraded mode)\n")
|
||||
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
|
||||
|
||||
if not os.path.isfile(plan_json_path):
|
||||
sys.stderr.write(f"[infracost] plan JSON not found: {plan_json_path}\n")
|
||||
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["infracost", "breakdown", "--path", plan_json_path, "--format", "json"],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(f"[infracost] CLI failed: {result.stderr[:200]}\n")
|
||||
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
|
||||
|
||||
breakdown = json.loads(result.stdout)
|
||||
delta = float(breakdown.get("diffTotalMonthlyCost", 0.0))
|
||||
total = float(breakdown.get("totalMonthlyCost", 0.0))
|
||||
estimate_data = {"available": True, "delta_usd": delta, "total_monthly_usd": total}
|
||||
|
||||
emit("nova.cost.estimated", run_id, environment, estimate_data, contract_id=contract_id)
|
||||
return estimate_data
|
||||
except Exception as exc:
|
||||
sys.stderr.write(f"[infracost] error: {exc}\n")
|
||||
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 5:
|
||||
print("usage: infracost_adapter.py <plan_json_path> <run_id> <contract_id> <environment>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
est = estimate(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
|
||||
print(json.dumps(est, indent=2))
|
||||
@@ -1,198 +0,0 @@
|
||||
"""Nova PowerBI Export (REQ-190, P3).
|
||||
|
||||
Emits CSV/JSON views to metrics/powerbi/ from the SQLite cold store.
|
||||
Fact + dimension tables + 8 empty placeholder views for deferred metrics
|
||||
(with documented schemas ready to fill when their blocking decisions lift).
|
||||
|
||||
D-120: Nova-native (CSV/JSON files, no live connector)
|
||||
D-129: PowerBI ingests via the folder connector
|
||||
D-128: metrics/ at repo root
|
||||
"""
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
_METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
|
||||
_STORE_PATH = os.path.join(_METRICS_DIR, "nova_metrics.db")
|
||||
_EXPORT_DIR = os.path.join(_METRICS_DIR, "powerbi")
|
||||
|
||||
FACT_VIEWS = [
|
||||
"fact_run",
|
||||
"fact_capability",
|
||||
"fact_policy_check",
|
||||
"fact_confidence",
|
||||
"fact_test",
|
||||
"fact_decision",
|
||||
"fact_cost_estimate",
|
||||
"fact_lifecycle",
|
||||
]
|
||||
|
||||
DIM_VIEWS = [
|
||||
"dim_capability",
|
||||
"dim_milestone",
|
||||
]
|
||||
|
||||
PLACEHOLDER_VIEWS = {
|
||||
"placeholder_live_infra_health": {
|
||||
"columns": ["timestamp", "resource_id", "resource_type", "running_count", "healthy", "downtime_seconds"],
|
||||
"blocking_decision": "D-096",
|
||||
"description": "Live infrastructure health (ECS running count, ALB 5xx, RPS). Blocked: live AWS torn down.",
|
||||
},
|
||||
"placeholder_live_outbox_rate": {
|
||||
"columns": ["timestamp", "contract_id", "write_latency_ms", "append_count"],
|
||||
"blocking_decision": "D-096",
|
||||
"description": "Live outbox write rate / ledger append latency. Blocked: DynamoDB outbox table absent.",
|
||||
},
|
||||
"placeholder_tamper_evident_checkpoints": {
|
||||
"columns": ["timestamp", "checkpoint_id", "jws_signed", "object_lock_enabled"],
|
||||
"blocking_decision": "D-083",
|
||||
"description": "Tamper-evident ledger checkpoints / JWS signature rate. Blocked: S3 Object Lock + JWS deferred.",
|
||||
},
|
||||
"placeholder_onboarding_funnel": {
|
||||
"columns": ["timestamp", "consumer_repo", "requested_environment", "status", "granted_at"],
|
||||
"blocking_decision": "D-113/D-114/D-119",
|
||||
"description": "Onboarding funnel: requested → granted conversion. Blocked: no auto-grant event.",
|
||||
},
|
||||
"placeholder_drift_detection": {
|
||||
"columns": ["timestamp", "workspace_id", "drift_count", "auto_reverted", "detection_cycle"],
|
||||
"blocking_decision": "D-096 + no scheduler",
|
||||
"description": "Drift detection (scheduled terraform plan -detailed-exitcode). Blocked: live AWS + scheduler.",
|
||||
},
|
||||
"placeholder_live_cur_reconciliation": {
|
||||
"columns": ["timestamp", "resource_address", "actual_usd", "baseline_usd", "saved_usd"],
|
||||
"blocking_decision": "D-096",
|
||||
"description": "Live cost CUR reconciliation. Blocked: live AWS billing. Infracost pre-apply estimates are in fact_cost_estimate.",
|
||||
},
|
||||
"placeholder_sla_downtime": {
|
||||
"columns": ["timestamp", "service", "uptime_pct", "downtime_minutes", "slo_target"],
|
||||
"blocking_decision": "D-096",
|
||||
"description": "SLA / unplanned downtime. Blocked: needs live service uptime monitoring.",
|
||||
},
|
||||
"placeholder_predictive_reactive": {
|
||||
"columns": ["timestamp", "action_id", "label", "trigger", "count"],
|
||||
"blocking_decision": "future emitter",
|
||||
"description": "Predictive vs Reactive ratio. Blocked: requires ML anomaly-forecasting service.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _iso8601_now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _export_table_csv(conn, table_name, export_dir):
|
||||
"""Export a SQLite table to a CSV file."""
|
||||
rows = conn.execute(f"SELECT * FROM {table_name}").fetchall()
|
||||
if not rows:
|
||||
return 0
|
||||
columns = [desc[0] for desc in conn.execute(f"SELECT * FROM {table_name} LIMIT 0").description]
|
||||
csv_path = os.path.join(export_dir, f"{table_name}.csv")
|
||||
with open(csv_path, "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(columns)
|
||||
writer.writerows(rows)
|
||||
return len(rows)
|
||||
|
||||
|
||||
def _export_table_json(conn, table_name, export_dir):
|
||||
"""Export a SQLite table to a JSON file."""
|
||||
rows = conn.execute(f"SELECT * FROM {table_name}").fetchall()
|
||||
if not rows:
|
||||
return 0
|
||||
columns = [desc[0] for desc in conn.execute(f"SELECT * FROM {table_name} LIMIT 0").description]
|
||||
records = [dict(zip(columns, row)) for row in rows]
|
||||
json_path = os.path.join(export_dir, f"{table_name}.json")
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(records, f, indent=2, default=str)
|
||||
return len(rows)
|
||||
|
||||
|
||||
def _export_placeholder_csv(view_name, schema, export_dir):
|
||||
"""Export a placeholder CSV with headers only (no data rows)."""
|
||||
csv_path = os.path.join(export_dir, f"{view_name}.csv")
|
||||
with open(csv_path, "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(schema["columns"])
|
||||
return 0
|
||||
|
||||
|
||||
def _export_placeholder_json(view_name, schema, export_dir):
|
||||
"""Export a placeholder JSON with schema metadata (no data rows)."""
|
||||
json_path = os.path.join(export_dir, f"{view_name}.json")
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump({"schema": schema, "data": []}, f, indent=2)
|
||||
return 0
|
||||
|
||||
|
||||
def export_all(store_path=None, export_dir=None, fmt="both"):
|
||||
"""Export all fact/dim tables + placeholder views to CSV and/or JSON.
|
||||
|
||||
Args:
|
||||
store_path: path to the SQLite cold store
|
||||
export_dir: directory for exported files
|
||||
fmt: "csv", "json", or "both"
|
||||
|
||||
Returns:
|
||||
Summary dict with export counts.
|
||||
"""
|
||||
if store_path is None:
|
||||
store_path = _STORE_PATH
|
||||
if export_dir is None:
|
||||
export_dir = _EXPORT_DIR
|
||||
os.makedirs(export_dir, exist_ok=True)
|
||||
|
||||
summary = {"exported_at": _iso8601_now(), "fact_tables": {}, "dim_tables": {}, "placeholder_views": {}}
|
||||
|
||||
if not os.path.isfile(store_path):
|
||||
summary["error"] = f"SQLite store not found: {store_path}"
|
||||
for view_name, schema in PLACEHOLDER_VIEWS.items():
|
||||
if fmt in ("csv", "both"):
|
||||
_export_placeholder_csv(view_name, schema, export_dir)
|
||||
if fmt in ("json", "both"):
|
||||
_export_placeholder_json(view_name, schema, export_dir)
|
||||
summary["placeholder_views"][view_name] = 0
|
||||
return summary
|
||||
|
||||
conn = sqlite3.connect(store_path)
|
||||
|
||||
for table in FACT_VIEWS:
|
||||
count = 0
|
||||
try:
|
||||
if fmt in ("csv", "both"):
|
||||
count = _export_table_csv(conn, table, export_dir)
|
||||
if fmt in ("json", "both"):
|
||||
count = _export_table_json(conn, table, export_dir)
|
||||
except sqlite3.OperationalError:
|
||||
count = 0
|
||||
summary["fact_tables"][table] = count
|
||||
|
||||
for table in DIM_VIEWS:
|
||||
count = 0
|
||||
try:
|
||||
if fmt in ("csv", "both"):
|
||||
count = _export_table_csv(conn, table, export_dir)
|
||||
if fmt in ("json", "both"):
|
||||
count = _export_table_json(conn, table, export_dir)
|
||||
except sqlite3.OperationalError:
|
||||
count = 0
|
||||
summary["dim_tables"][table] = count
|
||||
|
||||
conn.close()
|
||||
|
||||
for view_name, schema in PLACEHOLDER_VIEWS.items():
|
||||
if fmt in ("csv", "both"):
|
||||
_export_placeholder_csv(view_name, schema, export_dir)
|
||||
if fmt in ("json", "both"):
|
||||
_export_placeholder_json(view_name, schema, export_dir)
|
||||
summary["placeholder_views"][view_name] = 0
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = export_all()
|
||||
print(json.dumps(result, indent=2))
|
||||
@@ -1,137 +0,0 @@
|
||||
"""Nova Per-Run Manifest Writer (REQ-187).
|
||||
|
||||
Emits nova.run.started, nova.run.completed, nova.run.failed events with
|
||||
(run_id, contractId, env, stages x durations, exit, confidence, HITL block
|
||||
count). Writes metrics/runs/<run_id>.json. scripts/run_platform.sh invokes
|
||||
the writer at run start + run end.
|
||||
|
||||
D-120: Nova-native (JSONL events + JSON manifest file, no Kafka).
|
||||
D-128: metrics/ at repo root.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
|
||||
_METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
|
||||
_RUNS_DIR = os.path.join(_METRICS_DIR, "runs")
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from core.metrics.event_envelope import emit, make_event, append_event
|
||||
|
||||
|
||||
def _iso8601_now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _run_id():
|
||||
return f"run-{int(time.time())}-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
|
||||
def start_run(contract_id, environment, stages=None):
|
||||
"""Emit nova.run.started + return the run_id."""
|
||||
run_id = _run_id()
|
||||
data = {
|
||||
"contract_id": contract_id,
|
||||
"environment": environment,
|
||||
"started_at": _iso8601_now(),
|
||||
"stages": stages or [],
|
||||
}
|
||||
emit("nova.run.started", run_id, environment, data, contract_id=contract_id)
|
||||
return run_id
|
||||
|
||||
|
||||
def complete_run(run_id, contract_id, environment, stages, exit_code, confidence=None, hitl=None, policy=None, cost_estimate_usd=None, decision_id=None):
|
||||
"""Emit nova.run.completed + write the per-run manifest JSON.
|
||||
|
||||
Args:
|
||||
run_id: the run identifier from start_run()
|
||||
contract_id: the contract UUID
|
||||
environment: dev|qa|prod|dr
|
||||
stages: list of {name, duration_ms, exit_code, error?}
|
||||
exit_code: the overall run exit code
|
||||
confidence: optional {score, band, perInput}
|
||||
hitl: optional {gate, result, block}
|
||||
policy: optional {passed, failed, skipped}
|
||||
cost_estimate_usd: optional float
|
||||
decision_id: optional string (links to the Decision Ledger)
|
||||
"""
|
||||
started_at = stages[0].get("started_at", _iso8601_now()) if stages else _iso8601_now()
|
||||
completed_at = _iso8601_now()
|
||||
outcome = "succeeded" if exit_code == 0 else "failed"
|
||||
|
||||
manifest = {
|
||||
"run_id": run_id,
|
||||
"contract_id": contract_id,
|
||||
"environment": environment,
|
||||
"started_at": started_at,
|
||||
"completed_at": completed_at,
|
||||
"exit_code": exit_code,
|
||||
"stages": stages,
|
||||
"outcome": outcome,
|
||||
}
|
||||
if confidence:
|
||||
manifest["confidence"] = confidence
|
||||
if hitl:
|
||||
manifest["hitl"] = hitl
|
||||
if policy:
|
||||
manifest["policy"] = policy
|
||||
if cost_estimate_usd is not None:
|
||||
manifest["cost_estimate_usd"] = cost_estimate_usd
|
||||
if decision_id:
|
||||
manifest["decision_id"] = decision_id
|
||||
|
||||
os.makedirs(_RUNS_DIR, exist_ok=True)
|
||||
manifest_path = os.path.join(_RUNS_DIR, f"{run_id}.json")
|
||||
with open(manifest_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(manifest, fh, indent=2, sort_keys=True)
|
||||
|
||||
event_type = "nova.run.completed" if exit_code == 0 else "nova.run.failed"
|
||||
emit(event_type, run_id, environment, manifest, contract_id=contract_id)
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def persist_run_artifacts(run_id, work_dir):
|
||||
"""Copy ephemeral $WORK/*.json to metrics/runs/<run_id>/ as durable artifacts.
|
||||
|
||||
Args:
|
||||
run_id: the run identifier
|
||||
work_dir: the $WORK directory (e.g. /tmp/nova_platform_run)
|
||||
"""
|
||||
if not work_dir or not os.path.isdir(work_dir):
|
||||
return []
|
||||
dest = os.path.join(_RUNS_DIR, run_id)
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
copied = []
|
||||
for fname in ("pcr.json", "signal.json", "event.json", "outbox_item.json", "stack.json", "checkov.json"):
|
||||
src = os.path.join(work_dir, fname)
|
||||
if os.path.isfile(src):
|
||||
import shutil
|
||||
shutil.copy2(src, os.path.join(dest, fname))
|
||||
copied.append(fname)
|
||||
return copied
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 4:
|
||||
print("usage: run_manifest.py <start|complete|persist> <contract_id> <environment> [run_id] [work_dir]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
action = sys.argv[1]
|
||||
cid = sys.argv[2]
|
||||
env = sys.argv[3]
|
||||
if action == "start":
|
||||
rid = start_run(cid, env)
|
||||
print(rid)
|
||||
elif action == "complete":
|
||||
rid = sys.argv[4] if len(sys.argv) >= 5 else _run_id()
|
||||
m = complete_run(rid, cid, env, [], 0)
|
||||
print(json.dumps(m, indent=2))
|
||||
elif action == "persist":
|
||||
rid = sys.argv[4] if len(sys.argv) >= 5 else ""
|
||||
wd = sys.argv[5] if len(sys.argv) >= 6 else ""
|
||||
copied = persist_run_artifacts(rid, wd)
|
||||
print(json.dumps({"copied": copied}))
|
||||
@@ -1,167 +0,0 @@
|
||||
"""Nova Trust Snapshot Report (REQ-211, P4).
|
||||
|
||||
Emits metrics/TRUST_SNAPSHOT.md — a dated one-pager with 5 trust metrics
|
||||
+ chain-integrity verdict + snapshot hash. Runnable on demand or at
|
||||
milestone complete.
|
||||
|
||||
Reads from: metrics/decision_ledger.db, metrics/nova_metrics.db,
|
||||
.ciagent/REGRESSION_REPORT.json.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
_METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
|
||||
_LEDGER_DB = os.path.join(_METRICS_DIR, "decision_ledger.db")
|
||||
_STORE_DB = os.path.join(_METRICS_DIR, "nova_metrics.db")
|
||||
_REGRESSION_REPORT = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), ".ciagent", "REGRESSION_REPORT.json")
|
||||
_SNAPSHOT_PATH = os.path.join(_METRICS_DIR, "TRUST_SNAPSHOT.md")
|
||||
|
||||
|
||||
def _iso8601_now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _get_decision_ledger_coverage(ledger_db=None):
|
||||
"""Decision Ledger Coverage: rows with outcome ≠ 'pending' ÷ total."""
|
||||
if ledger_db is None:
|
||||
ledger_db = _LEDGER_DB
|
||||
if not os.path.isfile(ledger_db):
|
||||
return 0.0, 0, 0
|
||||
from core.metrics.decision_ledger import stats, verify_chain
|
||||
s = stats(ledger_db)
|
||||
total = s.get("total", 0)
|
||||
if total == 0:
|
||||
return 0.0, 0, 0
|
||||
ok, broken, _ = verify_chain(ledger_db)
|
||||
coverage = (total - broken) / total if total > 0 else 0.0
|
||||
return coverage, total, broken
|
||||
|
||||
|
||||
def _get_attestation_coverage(ledger_db=None):
|
||||
"""Attestation Coverage: prod/dr attestation.recorded events ÷ total prod/dr runs."""
|
||||
if ledger_db is None:
|
||||
ledger_db = _LEDGER_DB
|
||||
if not os.path.isfile(ledger_db):
|
||||
return 0.0, 0, 0
|
||||
conn = sqlite3.connect(ledger_db)
|
||||
attestations = conn.execute(
|
||||
"SELECT COUNT(*) FROM decision_ledger WHERE event_type = 'nova.attestation.recorded'"
|
||||
).fetchone()[0]
|
||||
conn.close()
|
||||
return 1.0 if attestations > 0 else 0.0, attestations, 0
|
||||
|
||||
|
||||
def _get_capability_health(report_path=None):
|
||||
"""Capability Health: Verified/Skipped/Broken/Decayed counts."""
|
||||
if report_path is None:
|
||||
report_path = _REGRESSION_REPORT
|
||||
if not os.path.isfile(report_path):
|
||||
return {"Verified": 0, "Skipped": 0, "Broken": 0, "Decayed": 0}
|
||||
with open(report_path) as f:
|
||||
report = json.load(f)
|
||||
return report.get("summary", {"Verified": 0, "Skipped": 0, "Broken": 0, "Decayed": 0})
|
||||
|
||||
|
||||
def _get_ai_decision_accuracy(store_db=None):
|
||||
"""AI Decision Accuracy: decisions with outcome='succeeded' ÷ total."""
|
||||
if store_db is None:
|
||||
store_db = _STORE_DB
|
||||
if not os.path.isfile(store_db):
|
||||
return 0.0, 0, 0
|
||||
conn = sqlite3.connect(store_db)
|
||||
try:
|
||||
total = conn.execute("SELECT COUNT(*) FROM fact_decision").fetchone()[0]
|
||||
succeeded = conn.execute("SELECT COUNT(*) FROM fact_decision WHERE outcome = 'succeeded'").fetchone()[0]
|
||||
except sqlite3.OperationalError:
|
||||
conn.close()
|
||||
return 0.0, 0, 0
|
||||
conn.close()
|
||||
accuracy = succeeded / total if total > 0 else 0.0
|
||||
return accuracy, succeeded, total
|
||||
|
||||
|
||||
def _get_confidence_gate_halt_rate(store_db=None):
|
||||
"""Confidence-Gate Halt Rate: runs with band='block' ÷ total."""
|
||||
if store_db is None:
|
||||
store_db = _STORE_DB
|
||||
if not os.path.isfile(store_db):
|
||||
return 0.0, 0, 0
|
||||
conn = sqlite3.connect(store_db)
|
||||
try:
|
||||
total = conn.execute("SELECT COUNT(*) FROM fact_confidence").fetchone()[0]
|
||||
halted = conn.execute("SELECT COUNT(*) FROM fact_confidence WHERE band = 'block'").fetchone()[0]
|
||||
except sqlite3.OperationalError:
|
||||
conn.close()
|
||||
return 0.0, 0, 0
|
||||
conn.close()
|
||||
rate = halted / total if total > 0 else 0.0
|
||||
return rate, halted, total
|
||||
|
||||
|
||||
def generate_snapshot(ledger_db=None, store_db=None, report_path=None, snapshot_path=None):
|
||||
"""Generate the trust snapshot report."""
|
||||
if ledger_db is None:
|
||||
ledger_db = _LEDGER_DB
|
||||
if store_db is None:
|
||||
store_db = _STORE_DB
|
||||
if report_path is None:
|
||||
report_path = _REGRESSION_REPORT
|
||||
if snapshot_path is None:
|
||||
snapshot_path = _SNAPSHOT_PATH
|
||||
|
||||
dl_coverage, dl_total, dl_broken = _get_decision_ledger_coverage(ledger_db)
|
||||
att_coverage, att_count, _ = _get_attestation_coverage(ledger_db)
|
||||
cap_health = _get_capability_health(report_path)
|
||||
ai_accuracy, ai_succeeded, ai_total = _get_ai_decision_accuracy(store_db)
|
||||
halt_rate, halted, total_runs = _get_confidence_gate_halt_rate(store_db)
|
||||
|
||||
chain_ok = dl_broken == 0
|
||||
|
||||
timestamp = _iso8601_now()
|
||||
lines = [
|
||||
f"# Nova Trust Snapshot — {timestamp}",
|
||||
"",
|
||||
"> v1.17 — Strategic Direction, Leadership Metrics & Unified Story (REQ-211)",
|
||||
"> This snapshot is a dated one-pager with 5 trust metrics + chain-integrity verdict.",
|
||||
"",
|
||||
"## Trust Metrics",
|
||||
"",
|
||||
f"| Metric | Value | Details |",
|
||||
f"|--------|-------|---------|",
|
||||
f"| **Decision Ledger Coverage** | {dl_coverage*100:.1f}% | {dl_total} entries, {dl_broken} broken |",
|
||||
f"| **Attestation Coverage** | {att_coverage*100:.1f}% | {att_count} attestation events |",
|
||||
f"| **Capability Health** | {cap_health.get('Verified',0)}V / {cap_health.get('Skipped',0)}S / {cap_health.get('Broken',0)}B / {cap_health.get('Decayed',0)}D | from REGRESSION_REPORT.json |",
|
||||
f"| **AI Decision Accuracy** | {ai_accuracy*100:.1f}% | {ai_succeeded}/{ai_total} succeeded |",
|
||||
f"| **Confidence-Gate Halt Rate** | {halt_rate*100:.1f}% | {halted}/{total_runs} halted |",
|
||||
"",
|
||||
"## Chain Integrity",
|
||||
"",
|
||||
f"- **Verdict:** {'INTACT' if chain_ok else 'BROKEN'}",
|
||||
f"- **Broken entries:** {dl_broken}",
|
||||
"",
|
||||
"## Snapshot Hash",
|
||||
"",
|
||||
]
|
||||
|
||||
content = "\n".join(lines)
|
||||
snapshot_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()[:16]
|
||||
lines.append(f"`{snapshot_hash}`")
|
||||
content = "\n".join(lines)
|
||||
|
||||
os.makedirs(os.path.dirname(snapshot_path), exist_ok=True)
|
||||
with open(snapshot_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
return {"snapshot_path": snapshot_path, "hash": snapshot_hash, "chain_ok": chain_ok,
|
||||
"dl_coverage": dl_coverage, "att_coverage": att_coverage,
|
||||
"cap_health": cap_health, "ai_accuracy": ai_accuracy, "halt_rate": halt_rate}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = generate_snapshot()
|
||||
print(json.dumps(result, indent=2))
|
||||
@@ -1,131 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Nova Onboarding — auto-generate an environment binding file (P19, REQ-183).
|
||||
|
||||
Given a consumer onboarding request (validated against
|
||||
schemas/onboarding.schema.json), generate a ``<env>.json`` environment
|
||||
binding file from the dev template, filling in the consumer's ownerId +
|
||||
billingTag. The generated file is a starting point for the platform team
|
||||
(or a future automation) to bind to a real AWS account.
|
||||
|
||||
This is the "request path" half of the no-humans onboarding flow (D-113).
|
||||
Real AWS account/network/state provisioning is a future feature milestone;
|
||||
this module removes the human handoff from the *request* step by
|
||||
generating the binding file + emitting a git patch / PR-branch instruction.
|
||||
|
||||
Usage:
|
||||
python3 core/onboarding.py <request.json> [--out <env.json>]
|
||||
python3 core/onboarding.py --request '{"consumerRepo":"acdl/c","requestedEnvironment":"qa","ownerId":"team-a","billingTag":"cc-a"}'
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _load_template_env(template_env: str = "dev", root: Path | None = None) -> Dict[str, Any]:
|
||||
"""Load the template environment JSON (defaults to dev.json)."""
|
||||
root = root or _repo_root()
|
||||
env_path = root / "core" / "environments" / f"{template_env}.json"
|
||||
if not env_path.is_file():
|
||||
raise FileNotFoundError(f"template environment {env_path} not found")
|
||||
return json.loads(env_path.read_text())
|
||||
|
||||
|
||||
def generate_env_file(
|
||||
request: Dict[str, Any],
|
||||
template_env: str = "dev",
|
||||
root: Path | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate an environment binding dict from a consumer onboarding request.
|
||||
|
||||
The generated dict is a copy of the template env with:
|
||||
- ``name`` → the requested environment
|
||||
- ``description`` → notes the consumer + owner
|
||||
- ``account_id`` → placeholder (000000000000) for the platform team
|
||||
to fill with the real account
|
||||
- ``ownerId`` + ``billingTag`` → from the request (for ABAC + cost)
|
||||
|
||||
The dict validates against schemas/environment.schema.json.
|
||||
|
||||
Returns the generated env dict.
|
||||
"""
|
||||
template = _load_template_env(template_env, root)
|
||||
requested = request["requestedEnvironment"]
|
||||
owner = request["ownerId"]
|
||||
billing = request["billingTag"]
|
||||
consumer = request["consumerRepo"]
|
||||
|
||||
env = dict(template)
|
||||
env["name"] = requested
|
||||
env["description"] = (
|
||||
f"Auto-generated binding for {consumer} (owner={owner}, "
|
||||
f"billing={billing}). Replace account_id with the real "
|
||||
f"{requested} account before deploying."
|
||||
)
|
||||
env["account_id"] = "000000000000" # placeholder — platform team fills
|
||||
env["ownerId"] = owner
|
||||
env["billingTag"] = billing
|
||||
return env
|
||||
|
||||
|
||||
def _onboarding_request_message(env_name: str) -> str:
|
||||
"""P19 (REQ-183): the rebranded Nova onboarding message — self-service
|
||||
request path, no longer routes to 'contact the platform team'."""
|
||||
return (
|
||||
"=== Nova Environment Onboarding ===\n"
|
||||
f"No environment named '{env_name}' is bound to this repository.\n\n"
|
||||
"Nova environments are platform-managed. The platform provisions on\n"
|
||||
"your behalf:\n"
|
||||
" - an AWS account (or a scoped partition of one)\n"
|
||||
" - a network (VPC + subnets)\n"
|
||||
" - a state backend (an S3 bucket + DynamoDB lock table)\n"
|
||||
" - an IAM role surfaced to your repo via attribute-based\n"
|
||||
" authorization (ABAC)\n\n"
|
||||
"You do not provide an AWS account, VPC, subnet, or state bucket.\n\n"
|
||||
"To request an environment (self-service):\n"
|
||||
" 1. Submit an onboarding request to the Nova Lambda\n"
|
||||
" (action: onboard_consumer) with your repo name + the\n"
|
||||
" environment name you need (e.g. 'dev').\n"
|
||||
" 2. The platform generates an environment binding + opens a PR.\n"
|
||||
" 3. The platform provisions the account/network/state/role and\n"
|
||||
" grants the ABAC role. Your next pipeline run proceeds.\n\n"
|
||||
"Run: python3 core/onboarding.py --request '{...}' to generate a\n"
|
||||
"binding file locally, or POST to the Lambda onboard_consumer action.\n"
|
||||
"===================================\n"
|
||||
)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Generate an env binding from an onboarding request.")
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("request_file", nargs="?", help="path to a request JSON file")
|
||||
group.add_argument("--request", help="inline request JSON string")
|
||||
parser.add_argument("--out", help="output path for the generated env JSON (default: stdout)")
|
||||
parser.add_argument("--template-env", default="dev", help="template environment (default: dev)")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.request:
|
||||
request = json.loads(args.request)
|
||||
else:
|
||||
request = json.loads(Path(args.request_file).read_text())
|
||||
|
||||
env = generate_env_file(request, template_env=args.template_env)
|
||||
env_json = json.dumps(env, indent=2) + "\n"
|
||||
if args.out:
|
||||
Path(args.out).write_text(env_json)
|
||||
print(f"wrote: {args.out}")
|
||||
else:
|
||||
print(env_json)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Nova Outbox Writer — write an evidence event to the DynamoDB outbox.
|
||||
"""ACDL Outbox Writer — write an evidence event to the DynamoDB outbox.
|
||||
|
||||
ARCHITECTURE.md §9: DynamoDB outbox, RPO=0 (synchronous write before
|
||||
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
|
||||
CONFIDENCE_COMPUTED event.
|
||||
|
||||
The outbox table (Phase 08): nova-outbox, PAY_PER_REQUEST, PK contractId,
|
||||
The outbox table (Phase 08): acdl-outbox, PAY_PER_REQUEST, PK contractId,
|
||||
SK eventType#eventTs, TTL expire_at = now + 365d (D-044).
|
||||
|
||||
CLI: outbox_writer.py <event.json> (uses AWS creds from env)
|
||||
@@ -20,7 +20,7 @@ import sys
|
||||
import boto3
|
||||
|
||||
|
||||
OUTBOX_TABLE = "nova-outbox"
|
||||
OUTBOX_TABLE = "acdl-outbox"
|
||||
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||
|
||||
|
||||
|
||||
+18
-75
@@ -8,40 +8,24 @@ Two canonical mechanisms:
|
||||
strings, ALB DNS, S3 bucket URL, CloudFront domain). No raw secrets in
|
||||
the comment — only non-sensitive outputs (DNS names, ARNs, bucket names).
|
||||
|
||||
The namespace is /nova/{environment}/{contractId}/{output_name} so consumers
|
||||
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.)
|
||||
The namespace is /acdl/{environment}/{contractId}/{output_name} so consumers
|
||||
can query their own outputs via aws ssm get-parameter --name /acdl/dev/<id>/...
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
try:
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
except ImportError:
|
||||
boto3 = None
|
||||
ClientError = Exception # type: ignore[assignment,misc]
|
||||
|
||||
# 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)
|
||||
SSM_PREFIX = "/acdl"
|
||||
KMS_KEY_ID_ENV = "ACDL_KMS_KEY_ID"
|
||||
|
||||
from core import env as _envhelper
|
||||
|
||||
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 = {
|
||||
# Outputs that are safe to display in a PR comment (no secrets).
|
||||
SAFE_OUTPUT_NAMES = {
|
||||
"distribution_domain_name",
|
||||
"bucket_arn",
|
||||
"bucket_name",
|
||||
@@ -61,37 +45,6 @@ _HARDCODED_SAFE_OUTPUTS = {
|
||||
}
|
||||
|
||||
|
||||
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():
|
||||
if boto3 is None:
|
||||
raise RuntimeError("boto3 is required for SSM publishing")
|
||||
@@ -101,22 +54,20 @@ def _ssm_client():
|
||||
def _kms_key_id():
|
||||
"""Return the KMS key ID for SSM SecureString encryption.
|
||||
|
||||
P1-3: Fail loud when NOVA_KMS_KEY_ID is not set — silently falling back
|
||||
P1-3: Fail loud when ACDL_KMS_KEY_ID is not set — silently falling back
|
||||
to the AWS-managed key (`alias/aws/ssm`) was a security gap. The platform
|
||||
CMK must be explicitly configured. Set NOVA_ALLOW_DEFAULT_KMS=1 to use
|
||||
the AWS-managed key as an escape hatch for local testing. (Dual-read
|
||||
via core/env.py: NOVA_* preferred, ACDL_* fallback until P5.)
|
||||
CMK must be explicitly configured. Set ACDL_ALLOW_DEFAULT_KMS=1 to use
|
||||
the AWS-managed key as an escape hatch for local testing.
|
||||
"""
|
||||
key_id = _envhelper.get_env("KMS_KEY_ID")
|
||||
key_id = os.environ.get(KMS_KEY_ID_ENV)
|
||||
if key_id:
|
||||
return key_id
|
||||
if _envhelper.get_env("ALLOW_DEFAULT_KMS") == "1":
|
||||
if os.environ.get("ACDL_ALLOW_DEFAULT_KMS") == "1":
|
||||
return "alias/aws/ssm"
|
||||
raise RuntimeError(
|
||||
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"NOVA_ALLOW_DEFAULT_KMS=1 (ACDL_ALLOW_DEFAULT_KMS=1 fallback) to use "
|
||||
f"alias/aws/ssm (escape hatch for local testing)."
|
||||
f"ACDL_ALLOW_DEFAULT_KMS=1 to use alias/aws/ssm (escape hatch for local testing)."
|
||||
)
|
||||
|
||||
|
||||
@@ -146,12 +97,8 @@ def publish_to_ssm(outputs, environment, contract_id):
|
||||
Overwrite=True,
|
||||
)
|
||||
results[name] = param_name
|
||||
except (ClientError, OSError) as e:
|
||||
# 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
|
||||
print(f"WARNING: SSM put_parameter failed for {name}: {type(e).__name__}: {e}", file=sys.stderr)
|
||||
except Exception:
|
||||
# Don't fail the pipeline if one output fails to publish
|
||||
results[name] = None
|
||||
return results
|
||||
|
||||
@@ -163,7 +110,7 @@ def format_comment(outputs, environment, contract_id, ssm_results=None):
|
||||
outputs are noted as 'published to SSM' without their values.
|
||||
"""
|
||||
lines = [
|
||||
f"### Nova Deploy Outputs ({environment})",
|
||||
f"### ACDL Deploy Outputs ({environment})",
|
||||
"",
|
||||
f"**Contract:** `{contract_id}`",
|
||||
f"**Environment:** `{environment}`",
|
||||
@@ -185,7 +132,7 @@ def format_comment(outputs, environment, contract_id, ssm_results=None):
|
||||
ssm_path = "—"
|
||||
lines.append(f"| `{name}` | {display} | {ssm_path} |")
|
||||
lines.append("")
|
||||
lines.append("> Sensitive outputs are available via `aws ssm get-parameter --name /nova/" + environment + "/" + contract_id + "/<output_name>` (KMS-encrypted SecureString).")
|
||||
lines.append("> Sensitive outputs are available via `aws ssm get-parameter --name /acdl/" + environment + "/" + contract_id + "/<output_name>` (KMS-encrypted SecureString).")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -210,6 +157,7 @@ def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
|
||||
if not token or not repo or not pr_number:
|
||||
return False # not in a PR context or no token
|
||||
try:
|
||||
import urllib.request
|
||||
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
|
||||
data = json.dumps({"body": comment_text}).encode()
|
||||
req = urllib.request.Request(url, data=data, method="POST")
|
||||
@@ -217,12 +165,7 @@ def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
|
||||
req.add_header("Accept", "application/vnd.github+json")
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
return True
|
||||
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
|
||||
print(f"WARNING: GitHub PR comment failed: {type(e).__name__}: {e}", file=sys.stderr)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
+97
-173
@@ -32,15 +32,6 @@ from dataclasses import dataclass, field, asdict
|
||||
from pathlib import Path
|
||||
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
|
||||
CIAgent = ROOT / ".ciagent"
|
||||
|
||||
@@ -74,10 +65,7 @@ class RegressionReport:
|
||||
|
||||
@property
|
||||
def passed(self) -> bool:
|
||||
# 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)
|
||||
return all(r.status == "Verified" for r in self.results)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
@@ -149,18 +137,14 @@ def _check_environment_schema_validation() -> Tuple[Status, str]:
|
||||
])
|
||||
|
||||
|
||||
def _check_resolver(contract_path: str) -> Tuple[Status, str]:
|
||||
"""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).
|
||||
"""
|
||||
def _check_resolver_static_assets() -> Tuple[Status, str]:
|
||||
"""CAP-003: contract_resolver resolves static-assets to a Target Stack."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as t:
|
||||
out = t.name
|
||||
try:
|
||||
return _check_subprocess([
|
||||
"python3", "core/contract_resolver.py",
|
||||
contract_path, out,
|
||||
"contracts/static-assets.yml", out,
|
||||
])
|
||||
finally:
|
||||
try:
|
||||
@@ -169,19 +153,25 @@ def _check_resolver(contract_path: str) -> Tuple[Status, str]:
|
||||
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]:
|
||||
"""CAP-004: contract_resolver resolves the microservice contract."""
|
||||
return _check_resolver("contracts/microservice.yml")
|
||||
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as t:
|
||||
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]:
|
||||
"""CAP-005: terraform adapter compiles a resolved stack to .tf files."""
|
||||
work = tempfile.mkdtemp(prefix="nova_regr_")
|
||||
work = tempfile.mkdtemp(prefix="acdl_regr_")
|
||||
stack_path = os.path.join(work, "stack.json")
|
||||
tf_dir = os.path.join(work, "tf")
|
||||
os.makedirs(tf_dir, exist_ok=True)
|
||||
@@ -212,7 +202,7 @@ def _check_interpolation() -> Tuple[Status, str]:
|
||||
"import sys; sys.path.insert(0,'.'); "
|
||||
"from core.contract_resolver import _expand_vars; "
|
||||
"ctx={'env':{'environment':'qa','account_id':'123'},'contract':{'id':'assets'}}; "
|
||||
"assert _expand_vars('nova-${env.environment}-${contract.id}', ctx)=='nova-qa-assets'; "
|
||||
"assert _expand_vars('acdl-${env.environment}-${contract.id}', ctx)=='acdl-qa-assets'; "
|
||||
"print('interpolation ok')",
|
||||
])
|
||||
|
||||
@@ -232,7 +222,7 @@ def _check_confidence_signal() -> Tuple[Status, str]:
|
||||
|
||||
def _check_outbox_writer() -> Tuple[Status, str]:
|
||||
"""CAP-008: outbox_writer writes a hash-chained event to a temp file."""
|
||||
work = tempfile.mkdtemp(prefix="nova_outbox_")
|
||||
work = tempfile.mkdtemp(prefix="acdl_outbox_")
|
||||
event_path = os.path.join(work, "event.json")
|
||||
event = {
|
||||
"contractId": "regression-test", "eventType": "CONFIDENCE_COMPUTED",
|
||||
@@ -316,34 +306,29 @@ def _load_aws_env() -> Dict[str, str]:
|
||||
continue
|
||||
if "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
# NOVA_* only (ACDL_* fallback removed in v1.15 P5, REQ-164).
|
||||
if k == "NOVA_AWS_ACCESS_KEY_ID":
|
||||
if k == "ACDL_AWS_ACCESS_KEY_ID":
|
||||
env["AWS_ACCESS_KEY_ID"] = v
|
||||
elif k == "NOVA_AWS_SECRET_ACCESS_KEY":
|
||||
elif k == "ACDL_AWS_SECRET_ACCESS_KEY":
|
||||
env["AWS_SECRET_ACCESS_KEY"] = v
|
||||
elif k == "AWS_DEFAULT_REGION":
|
||||
env["AWS_DEFAULT_REGION"] = v
|
||||
return env
|
||||
|
||||
|
||||
def _check_live_terraform_plan(contract_path: str, label: str) -> Tuple[Status, str]:
|
||||
"""Shared helper: terraform init+validate+plan against live AWS for a
|
||||
contract (D-093 live-AWS tier of the headline E2E).
|
||||
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).
|
||||
|
||||
Used by CAP-013 (microservice) and CAP-014 (static-assets) — the two
|
||||
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).
|
||||
"""
|
||||
Requires AWS credentials (ACDL_AWS_ACCESS_KEY_ID etc. in .env.secrets).
|
||||
Runs in a temp dir; does NOT apply (plan only)."""
|
||||
import tempfile, os
|
||||
work = tempfile.mkdtemp(prefix=f"nova_regr_live_{label}_")
|
||||
work = tempfile.mkdtemp(prefix="acdl_regr_live_")
|
||||
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",
|
||||
contract_path, stack_path,
|
||||
"contracts/microservice.yml", stack_path,
|
||||
])
|
||||
if rc != 0:
|
||||
return "Broken", f"resolver failed: {err.strip()[-200:]}"
|
||||
@@ -358,11 +343,6 @@ def _check_live_terraform_plan(contract_path: str, label: str) -> Tuple[Status,
|
||||
cwd=tf_dir, timeout=120, env=env,
|
||||
)
|
||||
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:]}"
|
||||
rc, out, err = _run_subprocess(
|
||||
["terraform", "validate"], cwd=tf_dir, timeout=60, env=env,
|
||||
@@ -375,74 +355,76 @@ def _check_live_terraform_plan(contract_path: str, label: str) -> Tuple[Status,
|
||||
)
|
||||
if rc != 0:
|
||||
return "Decayed", f"terraform plan failed: {err.strip()[-200:]}"
|
||||
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")
|
||||
return "Verified", "terraform init+validate+plan OK (live AWS, microservice)"
|
||||
|
||||
|
||||
def _check_live_terraform_plan_static_assets() -> Tuple[Status, str]:
|
||||
"""CAP-014: terraform init+validate+plan against live AWS for the
|
||||
static-assets stack (CloudFront + WAF + S3)."""
|
||||
return _check_live_terraform_plan("contracts/static-assets.yml", "static-assets")
|
||||
import tempfile, os
|
||||
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]:
|
||||
"""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.
|
||||
"""
|
||||
"""CAP-015: DynamoDB outbox table exists + is describable (live AWS)."""
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
env = _load_aws_env()
|
||||
try:
|
||||
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_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
||||
r = dyn.describe_table(TableName="nova-outbox")
|
||||
r = dyn.describe_table(TableName="acdl-outbox")
|
||||
count = r["Table"].get("ItemCount", "unknown")
|
||||
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]}"
|
||||
return "Verified", f"acdl-outbox exists, item_count={count}"
|
||||
except Exception as e:
|
||||
return "Decayed", f"describe_table failed: {type(e).__name__}: {str(e)[:150]}"
|
||||
|
||||
|
||||
def _check_s3_state_bucket() -> Tuple[Status, str]:
|
||||
"""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.
|
||||
"""
|
||||
"""CAP-016: S3 state bucket exists + readable (live AWS)."""
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
env = _load_aws_env()
|
||||
account_id = _envhelper.get_env("AWS_ACCOUNT_ID", "581513795199")
|
||||
state_bucket = f"nova-tfstate-{account_id}-us-east-1"
|
||||
try:
|
||||
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_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
||||
s3.head_bucket(Bucket=state_bucket)
|
||||
r = s3.list_objects_v2(Bucket=state_bucket, MaxKeys=5)
|
||||
s3.head_bucket(Bucket="acdl-tfstate-581513795199-us-east-1")
|
||||
r = s3.list_objects_v2(Bucket="acdl-tfstate-581513795199-us-east-1", MaxKeys=5)
|
||||
keys = [o["Key"] for o in r.get("Contents", [])]
|
||||
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:
|
||||
return "Decayed", f"head_bucket failed: {type(e).__name__}: {str(e)[:150]}"
|
||||
|
||||
@@ -479,30 +461,16 @@ def _check_lifecycle_module_terraform(module: str) -> Tuple[Status, str]:
|
||||
["terraform", "fmt", "-check", "-diff", str(tf_dir)], timeout=30)
|
||||
if rc != 0:
|
||||
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"]:
|
||||
contract = module_dir / "examples" / f"{ex}.yml"
|
||||
contract = ROOT / "modules" / "l1" / module / "examples" / f"{ex}.yml"
|
||||
if not contract.is_file():
|
||||
return "Broken", f"{module_dir.relative_to(ROOT)}/examples/{ex}.yml missing"
|
||||
return "Broken", f"modules/l1/{module}/examples/{ex}.yml missing"
|
||||
rc, out, err = _run_subprocess([
|
||||
"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", ""
|
||||
return "Verified", f"terraform files present + fmt -check passes + simple/complex contracts resolve"
|
||||
|
||||
|
||||
def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
|
||||
@@ -511,15 +479,20 @@ def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
|
||||
This is an offline proxy, not live pipeline evidence; the live
|
||||
apply/modify/destroy is verified by the modules-lifecycle workflow
|
||||
run, not by this gate."""
|
||||
module_dir = ROOT / "modules" / "l2" / module
|
||||
status, detail = _assert_contracts_resolve(module_dir, "l2")
|
||||
if status != "Verified":
|
||||
return status, detail
|
||||
return "Verified", "L2 composition resolves (simple + complex contracts; offline proxy)"
|
||||
for ex in ["simple", "complex"]:
|
||||
contract = ROOT / "modules" / "l2" / module / "examples" / f"{ex}.yml"
|
||||
if not contract.is_file():
|
||||
return "Broken", f"modules/l2/{module}/examples/{ex}.yml missing"
|
||||
rc, out, err = _run_subprocess([
|
||||
"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]:
|
||||
"""CAP-017: DynamoDB nova-contracts table. Evidence = L1 rds module
|
||||
"""CAP-017: DynamoDB acdl-contracts table. Evidence = L1 rds module
|
||||
lifecycle pipeline green (terraform validate + contracts resolve).
|
||||
The DynamoDB table is created via the microservice stack (L2 lifecycle).
|
||||
"""
|
||||
@@ -534,7 +507,7 @@ def _check_cap_018_lambda() -> Tuple[Status, str]:
|
||||
"python3", "-c",
|
||||
"from core.local_emulators import LocalLambdaStub, FlatFileOutbox; "
|
||||
"import tempfile; "
|
||||
"stub = LocalLambdaStub(outbox=FlatFileOutbox(tempfile.mkdtemp(prefix='nova_stub_'))); "
|
||||
"stub = LocalLambdaStub(outbox=FlatFileOutbox(tempfile.mkdtemp(prefix='acdl_stub_'))); "
|
||||
"print('LocalLambdaStub instantiates OK')",
|
||||
])
|
||||
if rc != 0:
|
||||
@@ -566,59 +539,6 @@ def _check_cap_022_oidc_role() -> Tuple[Status, str]:
|
||||
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).
|
||||
# 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
|
||||
@@ -656,7 +576,7 @@ CAPABILITY_REGISTRY: List[Tuple[str, str, str, Callable[[], Tuple[Status, str]]]
|
||||
_check_dynamodb_outbox_table),
|
||||
("CAP-016", "S3 state bucket exists + readable (live AWS)", "live-aws",
|
||||
_check_s3_state_bucket),
|
||||
("CAP-017", "DynamoDB nova-contracts table (lifecycle pipeline evidence)", "lifecycle-pipeline",
|
||||
("CAP-017", "DynamoDB acdl-contracts table (lifecycle pipeline evidence)", "lifecycle-pipeline",
|
||||
_check_cap_017_dynamodb),
|
||||
("CAP-018", "Lambda contract-ingestor (local stub + lifecycle evidence)", "lifecycle-pipeline",
|
||||
_check_cap_018_lambda),
|
||||
@@ -668,10 +588,6 @@ CAPABILITY_REGISTRY: List[Tuple[str, str, str, Callable[[], Tuple[Status, str]]]
|
||||
_check_cap_021_uptime),
|
||||
("CAP-022", "OIDC role (L1 iam-role lifecycle evidence)", "lifecycle-pipeline",
|
||||
_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),
|
||||
]
|
||||
|
||||
|
||||
@@ -725,9 +641,17 @@ def write_report(report: RegressionReport,
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""P13 (REQ-177): re-export from core.regression_verify_cli."""
|
||||
from core.regression_verify_cli import main as _cli_main
|
||||
return _cli_main()
|
||||
milestone = os.environ.get("ACDL_REGRESSION_MILESTONE", "v1.10")
|
||||
phase = int(os.environ.get("ACDL_REGRESSION_PHASE", "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 capabilities "
|
||||
"(milestone gate blocks)", file=sys.stderr)
|
||||
return 1
|
||||
print("regression: all capabilities Verified (milestone gate passes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"""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,31 +1,20 @@
|
||||
"""Check that qaApprover != prodApprover for a contract (ARCHITECTURE.md
|
||||
§10.3, D-042). Reads `approver_qa` from the DynamoDB outbox for the
|
||||
contractId, compares to the prod-dispatch the CI actor.
|
||||
contractId, compares to the prod-dispatch `gitea.actor` / `github.actor`.
|
||||
Blocks on equality, emits `SEPARATION_OF_DUTIES_VIOLATION`, routes a halt
|
||||
artifact to SRE on-call.
|
||||
|
||||
v1.9 (REQ-107, D-085): route_halt_artifact is a real implementation —
|
||||
publishes to SNS topic `acdl-sod-halt` (ARN from NOVA_SOD_HALT_TOPIC_ARN)
|
||||
publishes to SNS topic `acdl-sod-halt` (ARN from ACDL_SOD_HALT_TOPIC_ARN)
|
||||
when set; falls back to a structured stderr emission + a
|
||||
SEPARATION_OF_DUTIES_VIOLATION event write to the DynamoDB outbox when
|
||||
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.)
|
||||
unset. No silent print-only stub.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
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,
|
||||
current_prod_approver: Optional[str]) -> Tuple[bool, str]:
|
||||
@@ -51,13 +40,13 @@ def route_halt_artifact(contract_id: str, violation_reason: str,
|
||||
oncall_client=None) -> None:
|
||||
"""Route a halt artifact to SRE on-call (REQ-107, D-085).
|
||||
|
||||
When NOVA_SOD_HALT_TOPIC_ARN is set, publish to the SNS topic via
|
||||
When ACDL_SOD_HALT_TOPIC_ARN is set, publish to the SNS topic via
|
||||
boto3. When unset (dev/CI), fall back to a structured stderr emission
|
||||
+ a SEPARATION_OF_DUTIES_VIOLATION event write to the DynamoDB outbox
|
||||
via outbox_writer.write_event (so the halt is in the audit chain).
|
||||
The oncall_client, when provided, is the SNS client (test injection).
|
||||
"""
|
||||
topic_arn = env.get_env("SOD_HALT_TOPIC_ARN", "") or ""
|
||||
topic_arn = os.environ.get("ACDL_SOD_HALT_TOPIC_ARN", "")
|
||||
halt_payload = {
|
||||
"contractId": contract_id,
|
||||
"reason": violation_reason,
|
||||
@@ -74,7 +63,7 @@ def route_halt_artifact(contract_id: str, violation_reason: str,
|
||||
sns.publish(
|
||||
TopicArn=topic_arn,
|
||||
Message=json.dumps(halt_payload),
|
||||
Subject="Nova SoD halt",
|
||||
Subject="ACDL SoD halt",
|
||||
)
|
||||
print(f"[halt-artifact] SNS published contract={contract_id} "
|
||||
f"topic={topic_arn}", flush=True)
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
"""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
@@ -1,177 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,68 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,141 +0,0 @@
|
||||
# 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
|
||||
@@ -1,87 +0,0 @@
|
||||
# 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: Nova
|
||||
description: Consumer + platform-engineer documentation for the Nova platform (formerly ACDL — Agentic Cloud Delivery Platform).
|
||||
title: ACDL — Agentic Cloud Delivery Platform
|
||||
description: Consumer + platform-engineer documentation for the ACDL platform.
|
||||
remote_theme: mmistakes/minimal-mistakes@9.0.4
|
||||
|
||||
exclude:
|
||||
|
||||
@@ -230,7 +230,7 @@ change to the modules/stack/confidence/audit.
|
||||
- A MAJOR bump requires a new registry entry (immutable publication); the
|
||||
old entry enters a 12-month deprecation window.
|
||||
- The central deploy pipeline is referenced by a floating MAJOR + MINOR tag
|
||||
(e.g. `@v1.19`); patch fixes flow within the tag, breaking changes land
|
||||
(e.g. `@v1.6`); patch fixes flow within the tag, breaking changes land
|
||||
under the next MINOR tag.
|
||||
|
||||
See [Versioning](pipeline/versioning) for the consumer-facing details.
|
||||
|
||||
+36
-36
@@ -1,15 +1,15 @@
|
||||
# Consumer Guide — Declare intent, deploy to AWS
|
||||
|
||||
This guide walks a consumer through creating their pipeline and defining a
|
||||
contract that deploys any Nova module to AWS. It is **generic** across all
|
||||
contract that deploys any ACDL module to AWS. It is **generic** across all
|
||||
modules in the registry; `static-assets` is the worked example, but every
|
||||
step applies to `microservice` and any future module.
|
||||
|
||||
## The model
|
||||
|
||||
Consumers have their own repos and consume Nova by writing a contract
|
||||
Consumers have their own repos and consume ACDL by writing a contract
|
||||
that declares infrastructure (one or more modules), an environment, and inputs. The consumer declares a **contract** (which infrastructure, which
|
||||
environment, which inputs); the Nova platform owns the pipelines, modules,
|
||||
environment, which inputs); the ACDL platform owns the pipelines, modules,
|
||||
engine adapter, and evidence stream.
|
||||
|
||||
You do not write infrastructure modules, workflow YAML, or adapter code.
|
||||
@@ -19,7 +19,7 @@ definitions.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["your repo<br/>(app code + contracts + CI definitions)"] -->|uses: nova/.github/workflows/deploy.yml@v1.19| B
|
||||
A["your repo<br/>(app code + contracts + CI definitions)"] -->|uses: acdl/.github/workflows/deploy.yml@v1.9| 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
|
||||
C["your resources in AWS"]
|
||||
```
|
||||
@@ -27,13 +27,13 @@ flowchart LR
|
||||
## Versioning the `uses:` reference
|
||||
|
||||
The central deployment pipeline is **always versioned with floating MAJOR
|
||||
and MINOR tags** (e.g. `nova/pipelines/contract.yml@v1.19`). Version
|
||||
and MINOR tags** (e.g. `acdl/pipelines/contract.yml@v1.9`). Version
|
||||
constraints cannot be expressed inside the contract, so the tag in
|
||||
`uses:` is the only immutability lever a consumer has. See
|
||||
[Versioning](pipeline/versioning) for the full rationale.
|
||||
|
||||
**Unversioned references are discouraged.** Do not use `@main` or a bare
|
||||
`nova/pipelines/contract.yml`.
|
||||
`acdl/pipelines/contract.yml`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -47,13 +47,13 @@ platform-managed. See [Environments](environments/).
|
||||
environment is bound, your first pipeline run emits a friendly onboarding
|
||||
prompt. See [Environments](environments/).
|
||||
- **Authorization to reference the central pipeline.** Onboarding grants
|
||||
your repo the right to `uses: nova/.github/workflows/deploy.yml@v1.19`.
|
||||
your repo the right to `uses: acdl/.github/workflows/deploy.yml@v1.9`.
|
||||
Contact the platform team if you have not been onboarded.
|
||||
|
||||
## Step 1 — Create a consumer repo
|
||||
|
||||
Create a repository for your application. The top level holds your app
|
||||
code; your contract lives at `.nova/contract.yml`. Example for a static
|
||||
code; your contract lives at `.acdl/contract.yml`. Example for a static
|
||||
site:
|
||||
|
||||
```
|
||||
@@ -62,7 +62,7 @@ my-static-site/
|
||||
assets/
|
||||
style.css
|
||||
logo.png
|
||||
.nova/
|
||||
.acdl/
|
||||
contract.yaml
|
||||
.github/
|
||||
workflows/
|
||||
@@ -75,7 +75,7 @@ Example for a microservice:
|
||||
my-microservice/
|
||||
app.py
|
||||
Dockerfile
|
||||
.nova/
|
||||
.acdl/
|
||||
contract.yaml
|
||||
.github/
|
||||
workflows/
|
||||
@@ -83,20 +83,20 @@ my-microservice/
|
||||
```
|
||||
|
||||
Your app code lives at the top level. Your contract lives at
|
||||
`.nova/contract.yml` regardless of the module you deploy. Your CI
|
||||
`.acdl/contract.yml` regardless of the module you deploy. Your CI
|
||||
definition lives at `.github/workflows/deploy.yml`.
|
||||
|
||||
## Step 2 — Reference the central pipeline
|
||||
|
||||
In your CI workflow (`.github/workflows/deploy.yml`), reference the central
|
||||
Nova deployment workflow with a **versioned tag** (floating MAJOR + MINOR):
|
||||
ACDL deployment workflow with a **versioned tag** (floating MAJOR + MINOR):
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
deploy:
|
||||
uses: nova/.github/workflows/deploy.yml@v1.19
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.9
|
||||
with:
|
||||
contract: .nova/contract.yml
|
||||
contract: .acdl/contract.yml
|
||||
environment: dev
|
||||
```
|
||||
|
||||
@@ -106,7 +106,7 @@ field; the version pin lives in the CI workflow reference.
|
||||
|
||||
## Step 3 — Define the contract
|
||||
|
||||
Write `.nova/contract.yml`. The `static-assets` example:
|
||||
Write `.acdl/contract.yml`. The `static-assets` example:
|
||||
|
||||
```yaml
|
||||
environment: dev
|
||||
@@ -140,7 +140,7 @@ name: microservice
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `uses` | string | yes | Reference to the central deployment pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `nova/pipelines/contract.yml@v1.19`). Bare or `@main` references are discouraged. See [Versioning](pipeline/versioning). |
|
||||
| `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). |
|
||||
| `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-assets`, `microservice`, `s3`). See the [module catalog](modules/). |
|
||||
| `environment` | string | yes | The platform-managed environment to deploy to (e.g. `dev`). See [Environments](environments/). |
|
||||
| `inputs` | object | yes | Module-specific inputs (see the module's README). |
|
||||
@@ -167,7 +167,7 @@ and execute for you.
|
||||
|
||||
### The consumer CI definition
|
||||
|
||||
Add a thin workflow file to **your** repo that invokes the reusable Nova
|
||||
Add a thin workflow file to **your** repo that invokes the reusable ACDL
|
||||
deploy workflow with a **versioned tag** (`.github/workflows/deploy.yml`):
|
||||
|
||||
```yaml
|
||||
@@ -177,22 +177,22 @@ on:
|
||||
branches: [main]
|
||||
jobs:
|
||||
deploy:
|
||||
uses: nova/.github/workflows/deploy.yml@v1.19
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.9
|
||||
with:
|
||||
contract: .nova/contract.yml
|
||||
contract: .acdl/contract.yml
|
||||
```
|
||||
|
||||
That is the entire consumer-side workflow. When you push to `main`:
|
||||
|
||||
1. The platform runner resolves `uses: nova/.github/workflows/deploy.yml@v1.19`
|
||||
1. The platform runner resolves `uses: acdl/.github/workflows/deploy.yml@v1.9`
|
||||
to the reusable workflow **at the pinned tag**.
|
||||
2. A **platform-provided runner** checks out **your** repo.
|
||||
3. The runner checks out the **Nova platform repo** into the workspace —
|
||||
3. The runner checks out the **ACDL platform repo** into the workspace —
|
||||
this is how the pipeline fetches the platform code at run time. You
|
||||
never clone the platform repo yourself.
|
||||
4. The runner installs the runtime dependencies the platform requires.
|
||||
5. The runner invokes `scripts/run_platform.sh` against your
|
||||
`.nova/contract.yml`.
|
||||
`.acdl/contract.yml`.
|
||||
|
||||
You see the streamed output (infrastructure plan, policy-check results,
|
||||
confidence signal) in your run logs. The `--check-only` and `--plan-only`
|
||||
@@ -203,7 +203,7 @@ hold for attestation).
|
||||
|
||||
### Local validation (optional)
|
||||
|
||||
A consumer *may* clone the Nova platform repo to run `--check-only` against
|
||||
A consumer *may* clone the ACDL platform repo to run `--check-only` against
|
||||
their contract before pushing — this is optional and not required for the
|
||||
happy path. If you do this, the runtime dependencies must be installed
|
||||
locally, and any AWS credentials follow the
|
||||
@@ -213,7 +213,7 @@ static key in `.env.secrets` (gitignored) is rotated **out of band by you**
|
||||
locally-held copies.
|
||||
|
||||
```bash
|
||||
bash scripts/run_platform.sh --check-only path/to/your/.nova/contract.yml
|
||||
bash scripts/run_platform.sh --check-only path/to/your/.acdl/contract.yml
|
||||
```
|
||||
|
||||
## Step 5 — What the pipeline does
|
||||
@@ -326,8 +326,8 @@ per-module extension points. Common examples:
|
||||
| Contract schema | `schemas/contract.schema.json` | JSON Schema for consumer contracts. |
|
||||
| Stack schema | `schemas/stack.schema.json` | JSON Schema for the resolved stack instance. |
|
||||
| Module catalog | [modules/](modules/) | All primitives and modules. |
|
||||
| Sample contract | `contracts/static-assets.yaml` | The reference example contract (uses `@v1.19`). |
|
||||
| Sample contract | `contracts/microservice.yaml` | The microservice example contract (uses `@v1.19`). |
|
||||
| Sample contract | `contracts/static-assets.yaml` | The reference example contract (uses `@v1.9`). |
|
||||
| Sample contract | `contracts/microservice.yaml` | The microservice example contract (uses `@v1.9`). |
|
||||
| Module examples | `modules/<name>/examples/` | Validated per-module example contracts (`simple.yaml` + `complex.yaml`). |
|
||||
| Contract resolver | `core/contract_resolver.py` | Resolves contracts to stack instances. |
|
||||
| Angine adapter | `adapters/terraform/adapter.py` | Compiles stack instances to infrastructure. |
|
||||
@@ -345,7 +345,7 @@ process is a 2-step pipeline with **HITL SRE gates** to prevent accidental
|
||||
destruction:
|
||||
|
||||
1. **Request a change request (CR):** Contact the platform team to create a
|
||||
change request in the platform CMDB (DynamoDB `nova-change-requests`
|
||||
change request in the platform CMDB (DynamoDB `acdl-change-requests`
|
||||
table). The CR must be approved before decommission can proceed. The CR
|
||||
includes the consumer repo, contract ID, and the reason for decommission.
|
||||
|
||||
@@ -353,9 +353,9 @@ destruction:
|
||||
use `mode: decommission` with the `changeRequestId` input:
|
||||
|
||||
```yaml
|
||||
uses: nova/.github/workflows/deploy.yml@v1.19
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.8
|
||||
with:
|
||||
contract: .nova/contract.yml
|
||||
contract: .acdl/contract.yml
|
||||
mode: decommission
|
||||
changeRequestId: "CHG0678912"
|
||||
```
|
||||
@@ -395,7 +395,7 @@ separately (or left running to monitor the decommissioned stack's
|
||||
endpoints going dark).
|
||||
## Per-environment deployment
|
||||
|
||||
Nova supports a **promotion-without-editing** model: you do not edit the
|
||||
ACDL supports a **promotion-without-editing** model: you do not edit the
|
||||
`environment:` field in a contract to promote dev → qa → prod → dr.
|
||||
Instead, there is **one CI job per environment**, each pointing at its
|
||||
respective contract (or the same contract + the `environment` workflow
|
||||
@@ -404,8 +404,8 @@ input). Promotion = running the matching job.
|
||||
### Two shapes (both supported)
|
||||
|
||||
**Shape 1 — per-environment contract files:** a consumer repo has one
|
||||
contract per environment (e.g. `.nova/static-assets.dev.yml`,
|
||||
`.nova/static-assets.qa.yml`, …). Each sets `environment:` to its own
|
||||
contract per environment (e.g. `.acdl/static-assets.dev.yml`,
|
||||
`.acdl/static-assets.qa.yml`, …). Each sets `environment:` to its own
|
||||
name and uses interpolation so env-specific values differ automatically:
|
||||
|
||||
```yaml
|
||||
@@ -421,7 +421,7 @@ name: static-assets
|
||||
```
|
||||
|
||||
**Shape 2 — single contract + `environment` workflow input:** the
|
||||
reusable deploy workflow (`nova/.github/workflows/deploy.yml@v1.19`)
|
||||
reusable deploy workflow (`acdl/.github/workflows/deploy.yml@v1.9`)
|
||||
declares an `environment` input. When non-empty, it overrides the
|
||||
contract's `environment` field at load time (before interpolation), so
|
||||
the same contract can be promoted by passing a different environment:
|
||||
@@ -436,10 +436,10 @@ on: workflow_dispatch:
|
||||
required: true
|
||||
jobs:
|
||||
deploy-qa:
|
||||
uses: nova/.github/workflows/deploy.yml@v1.19
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.9
|
||||
with:
|
||||
environment: qa
|
||||
contract: .nova/contract.yml
|
||||
contract: .acdl/contract.yml
|
||||
```
|
||||
|
||||
### One job per environment
|
||||
@@ -467,7 +467,7 @@ duties check blocks a prod promotion when `approver_qa == approver_prod`
|
||||
| `${env.environment}` | the environment name (dev/qa/prod/dr) | `qa` |
|
||||
| `${env.region}` | the environment's AWS region | `us-east-1` |
|
||||
| `${env.account_id}` | the environment's AWS account id | `123456789012` |
|
||||
| `${env.state_backend.bucket}` | the environment's state bucket | `nova-qa-state` |
|
||||
| `${env.state_backend.bucket}` | the environment's state bucket | `acdl-qa-state` |
|
||||
| `${env.network.vpc_cidr}` | the environment's VPC CIDR | `10.1.0.0/16` |
|
||||
| `${contract.id}` | the contract's operational acronym | `assets` |
|
||||
| `${contract.environment}` | the contract's environment field | `qa` |
|
||||
|
||||
@@ -7,7 +7,7 @@ deploys it.
|
||||
|
||||
## The contract file
|
||||
|
||||
A consumer repo keeps its contract at `.nova/contract.yml`. A minimal
|
||||
A consumer repo keeps its contract at `.acdl/contract.yml`. A minimal
|
||||
example (the `static-assets` module):
|
||||
|
||||
```yaml
|
||||
@@ -57,7 +57,7 @@ infrastructure:
|
||||
## Validation
|
||||
|
||||
The contract is validated against
|
||||
[`schemas/contract.schema.json`](https://github.com/nova/nova/blob/main/schemas/contract.schema.json).
|
||||
[`schemas/contract.schema.json`](https://github.com/acdl/acdl/blob/main/schemas/contract.schema.json).
|
||||
An invalid contract (missing field, unknown module, wrong type) fails at the
|
||||
validate-contract stage with a clear error.
|
||||
|
||||
@@ -65,9 +65,9 @@ validate-contract stage with a clear error.
|
||||
|
||||
Two reference examples exist in `contracts/`:
|
||||
|
||||
- [`contracts/static-assets.yml`](https://github.com/nova/nova/blob/main/contracts/static-assets.yml)
|
||||
- [`contracts/static-assets.yml`](https://github.com/acdl/acdl/blob/main/contracts/static-assets.yml)
|
||||
— the `static-assets` module.
|
||||
- [`contracts/microservice.yml`](https://github.com/nova/nova/blob/main/contracts/microservice.yml)
|
||||
- [`contracts/microservice.yml`](https://github.com/acdl/acdl/blob/main/contracts/microservice.yml)
|
||||
— the `microservice` module.
|
||||
|
||||
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)
|
||||
|
||||
Onboarding now also grants the consumer repo's deploy role permission to
|
||||
invoke the **platform Lambda** — `nova-contract-ingestor` — across
|
||||
invoke the **platform Lambda** — `acdl-contract-ingestor` — across
|
||||
accounts. The Lambda is invoked via a Function URL with IAM auth, so the
|
||||
grant is an inline IAM policy applied to the consumer's deploy role. The
|
||||
policy template lives at
|
||||
[`terraform/platform/consumer_invoke_policy.json`](https://github.com/nova/nova/blob/main/terraform/platform/consumer_invoke_policy.json)
|
||||
[`terraform/platform/consumer_invoke_policy.json`](https://github.com/acdl/acdl/blob/main/terraform/platform/consumer_invoke_policy.json)
|
||||
and is scoped via **ABAC**: the condition
|
||||
`aws:PrincipalTag/nova:owner == ${consumerRepo}` ensures a repo can only
|
||||
`aws:PrincipalTag/acdl:owner == ${consumerRepo}` ensures a repo can only
|
||||
invoke the Lambda when its principal tag matches its claimed identity.
|
||||
|
||||
The consumer's deploy workflow signs the Function URL request with
|
||||
@@ -75,7 +75,7 @@ is used for two purposes:
|
||||
|
||||
1. **Contract ingestion** — the consumer submits its resolved deployment
|
||||
contract (`action: "submit_contract"`) so the platform has a durable
|
||||
record in the `nova-contracts` DynamoDB table (PK `consumerRepo`, SK
|
||||
record in the `acdl-contracts` DynamoDB table (PK `consumerRepo`, SK
|
||||
`contractId#submittedAt`).
|
||||
2. **Error reporting** (D-055) — the consumer reports a deployment error
|
||||
(`action: "report_error"`) which the platform turns into a GitHub
|
||||
@@ -83,16 +83,16 @@ is used for two purposes:
|
||||
prepared-status stub until then).
|
||||
|
||||
The Lambda handler and the Terraform that deploys it live in
|
||||
[`core/lambda/contract_ingestor.py`](https://github.com/nova/nova/blob/main/core/lambda/contract_ingestor.py)
|
||||
[`core/lambda/contract_ingestor.py`](https://github.com/acdl/acdl/blob/main/core/lambda/contract_ingestor.py)
|
||||
and
|
||||
[`terraform/platform/main.tf`](https://github.com/nova/nova/blob/main/terraform/platform/main.tf)
|
||||
[`terraform/platform/main.tf`](https://github.com/acdl/acdl/blob/main/terraform/platform/main.tf)
|
||||
respectively.
|
||||
|
||||
## Onboarding scaffold (current state)
|
||||
|
||||
The platform repo ships a minimal onboarding scaffold:
|
||||
|
||||
- [`core/environments/`](https://github.com/nova/nova/blob/main/core/environments/)
|
||||
- [`core/environments/`](https://github.com/acdl/acdl/blob/main/core/environments/)
|
||||
— environment definitions (a sample `dev.json`).
|
||||
- `core/environment_check.py` — checks whether an environment is defined for
|
||||
a given contract's repo + environment name; prints the friendly onboarding
|
||||
|
||||
+4
-6
@@ -1,6 +1,4 @@
|
||||
# Nova
|
||||
|
||||
> **Nova — The New Dawn of DevSecOps.** Security as a seamless enabler of fast deployments — not a bottleneck, not a "no" department.
|
||||
# ACDL — Agentic Cloud Delivery Platform
|
||||
|
||||
Consumers declare intent; the platform delivers safe production deployment
|
||||
through an agentic stack — automatically, safely, and with a complete audit
|
||||
@@ -11,14 +9,14 @@ a configuration file, or an infrastructure module.
|
||||
|
||||
## Two repositories
|
||||
|
||||
There are two kinds of repository in the Nova model:
|
||||
There are two kinds of repository in the ACDL model:
|
||||
|
||||
- **Platform repo (this one).** The source code of the platform. It owns
|
||||
`modules/`, `adapters/`, `core/`, `schemas/`, `pipelines/`, `scripts/`,
|
||||
and the reusable workflow files. Platform engineers work here. A consumer
|
||||
never clones it.
|
||||
- **Consumer repo (yours).** A consumer repo contains only its application
|
||||
code, one or more contracts (`.nova/contract.yml`), and one or more CI
|
||||
code, one or more contracts (`.acdl/contract.yml`), and one or more CI
|
||||
definitions (a thin `.github/workflows/deploy.yml` that `uses:` the central
|
||||
reusable workflow, pointing at the appropriate environment + contract).
|
||||
The consumer does not write infrastructure modules, workflow YAML, or
|
||||
@@ -77,4 +75,4 @@ Planned future features (no dates; tracked in the internal roadmap):
|
||||
|
||||
- [Consumer Guide](consumer-guide) — start here if you are a consumer.
|
||||
- [Architecture](architecture) — start here if you are a platform engineer.
|
||||
- The [README](https://github.com/nova/nova) describes the platform repo.
|
||||
- The [README](https://github.com/acdl/acdl) describes the platform repo.
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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."
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,16 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Decision Ledger Coverage — Definition of Success
|
||||
|
||||
> KPI: Decision Ledger Coverage
|
||||
> Target: 100% of AI actions with backfilled outcome
|
||||
|
||||
**What this number means:** every AI decision (confidence-gated policy
|
||||
engine outcome) is captured in the Decision Ledger with its outcome
|
||||
backfilled from the subsequent apply.completed/failed event.
|
||||
|
||||
**How it's computed:** `count(decision_ledger rows WHERE outcome ≠
|
||||
'pending') ÷ count(decision_ledger rows)`. Sourced from
|
||||
`metrics/decision_ledger.db`.
|
||||
|
||||
**What "good" looks like:** 100% means no AI decision is ever lost or
|
||||
left without an outcome. The ledger is the trust substrate (NORTH_STAR
|
||||
Objective #2).
|
||||
@@ -1,13 +0,0 @@
|
||||
# Deployment Frequency — Definition of Success
|
||||
|
||||
> KPI: Deployment Frequency
|
||||
> Target: not a committed target (operational signal)
|
||||
|
||||
**What this number means:** the rate of infrastructure state updates
|
||||
deployed safely per day. A DORA-adjacent metric for infrastructure.
|
||||
|
||||
**How it's computed:** `count(run.completed WHERE exit_code = 0)` per
|
||||
day.
|
||||
|
||||
**What "good" looks like:** multiple deploys per day (vs. weekly/monthly
|
||||
for human ops teams).
|
||||
@@ -1,17 +0,0 @@
|
||||
# FTE Hours Saved (Toil Reallocation Value) — Definition of Success
|
||||
|
||||
> KPI: FTE Hours Saved
|
||||
> Target: ≥ 70% of pre-Nova FTE allocation (derived)
|
||||
|
||||
**What this number means:** the engineering hours saved by automated
|
||||
operations, valued at the blended engineering rate. This is what those
|
||||
hours were spent on instead (the "toil reallocation" — capital freed
|
||||
up from ops to feature development).
|
||||
|
||||
**How it's computed:** `run count × manual baseline minutes per run ÷ 60
|
||||
× blended hourly rate`. The manual baseline is the estimated time a
|
||||
human team would take for the same operation (e.g., 30 min/ticket).
|
||||
|
||||
**Honesty caveat:** computed on N internal runs today; the production-
|
||||
denominator activates post-pilot. The formula is grounded; the
|
||||
production numbers are not yet.
|
||||
@@ -1,18 +0,0 @@
|
||||
# Human Escalation Frequency — Definition of Success
|
||||
|
||||
> KPI: Human Escalation Frequency
|
||||
> Target: < 0.1% of platform actions (Post-Pilot)
|
||||
|
||||
**What this number means:** how often the AI platform was forced to fall
|
||||
back or escalate to a human operator due to low confidence. This is the
|
||||
inverse of Touchless Resolution Rate, scoped to operational escalations
|
||||
only.
|
||||
|
||||
**How it's computed:** `count(runs WHERE hitl_block = 1 AND reason =
|
||||
'confidence')` ÷ `total runs`. Attestation sign-offs are excluded.
|
||||
|
||||
**What "good" looks like:** < 0.1% means fewer than 1 in 1000 runs
|
||||
require human intervention. Near-zero is the goal.
|
||||
|
||||
**What would be "gamer metrics":** counting attestation sign-offs as
|
||||
escalations (they're not — they're designed controls).
|
||||
@@ -1,19 +0,0 @@
|
||||
# MTTR (Platform-Run) — Definition of Success
|
||||
|
||||
> KPI: MTTR (p95)
|
||||
> Target: < 60 seconds
|
||||
|
||||
**What this number means:** the time from a platform-run failure
|
||||
(apply.failed) to a successful retry. This is platform-run MTTR, not
|
||||
infra-incident MTTR (which requires an incident detection system that
|
||||
Nova doesn't have yet — deferred).
|
||||
|
||||
**How it's computed:** p95 of `successful_retry.time − failed_run.time`
|
||||
across all runs that failed then succeeded.
|
||||
|
||||
**What "good" looks like:** < 60 seconds means the platform recovers
|
||||
from a failed run in under a minute, 95% of the time.
|
||||
|
||||
**What's deferred:** infra-incident MTTR (anomaly detected → healed)
|
||||
requires an incident detection/remediation system (self-healing
|
||||
velocity). That's a future emitter.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user