# Nova — v1.11 RESTART Research Findings > Phase: research (pre-Phase 56). Milestone: v1.11 (RESTART). Status: research. > Researcher: ci-researcher. Autonomy: full (CLARIFY auto-resolved; all > binding decisions D-097..D-107 are committed in the CLARIFY stage). > Branch: `milestone/v1.11-restart` (branched off tag `v1.10.2`, per D-097). > Sources: ACDL codebase (v1.10.2 tree) + git history (failed first attempt > on `phase/56-iam-re-bootstrap` + `phase/57-live-deploy-microservice`) + > the CLARIFY commit (`80b7286`). > > This file overwrites the prior v1.1 research artifact. v1.11 is a fresh > milestone; the v1.1 research (Gitea OIDC, Checkov, IR shape, outbox) is > historical and preserved in git history. This file documents the > technical findings that ground the v1.11 restart plan. --- ## Background — why v1.11 is a restart v1.11 is a **restart**, not a continuation. The first attempt (phase/56 + phase/57, abandoned per D-097) made five defects worse, not better. The restart branches off the clean `v1.10.2` tag and corrects three structural defects that the CLARIFY stage locked as binding decisions: 1. **Stateless adapter** (D-098, D-099, D-100). The current adapter is a 750-line monolith with 3 constant tables and 39 type-specific branches that duplicate what `interface.json` already declares and hardcode defaults that belong in the module. v1.11 makes it a ~80-line stateless assembler; each L1 ships a real `terraform/` module dir that owns its resource shape, nested blocks, and defaults. 2. **Terraform owns lifecycle** (D-101). The first attempt added a Python script (`verify_deploy_microservice.py`) that ran `terraform init -reconfigure` in a fresh temp dir each time, which contributed to the 4-VPC bug. v1.11 deletes that script; `run_platform.sh` gains `--apply` and `--destroy` modes; Python never runs terraform. 3. **Pipeline-driven testing** (D-102, D-103, D-104). No per-module Python/pytest. A modules-lifecycle pipeline matrix-runs each L1 module's `examples/{simple,complex}.yml` contracts through apply→modify→destroy against live AWS. The "test" = the pipeline cell going green. --- ## FINDING 1 — Adapter monolith audit ### 1.1 The three constant tables `adapters/terraform/adapter.py` (750 lines on the v1.10.2 tree) is built around three constant tables: | Table | Line | What it encodes | Entries | |-------|------|-----------------|---------| | `TYPE_MAP` | 26 | Stack type (`aws::`) → Terraform resource type (`aws_s3_bucket`, `aws_vpc`, …). | 19 | | `INPUT_MAP` | 51 | Stack input name → Terraform arg name, per stack type. Only non-identity mappings are listed; an input not present uses the stack name as the Terraform arg (identity). | 19 (one per stack type) | | `OUTPUT_MAP` | 75 | Stack output name → Terraform attribute name, per stack type. Only non-identity mappings. | 19 (one per stack type) | **Why they duplicate `interface.json`.** Each L1 module already declares its inputs, outputs, and stack type in `interface.json` (engine-agnostic). The three tables are the *engine binding* — the Terraform-specific name mappings that `interface.json` deliberately omits (it is engine-agnostic per ARCHITECTURE.md §12). The duplication is therefore *intentional in the original design*: the adapter was meant to be a thin translator that holds the engine binding in three tables, and the L1 holds the engine-agnostic content. **The drift.** What was *not* intended is that the tables grew into 39 type-specific branches (§1.2) that hardcode resource shapes, nested HCL blocks, and defaults (§1.3) — content that belongs in the module, not the adapter. The adapter stopped being a thin translator and became a per-resource-type code generator. D-098 corrects this: the engine binding moves into a per-module `terraform/` subdir (the real Terraform module), and the adapter becomes a stateless assembler that emits `module "x" { source = "..." ... }` blocks. The three tables are deleted. ### 1.2 The 39 type-specific branches across 18 stack types `_emit_resource` (line 156) is a generic loop that, for each input, looks up the Terraform arg in `INPUT_MAP`, renders the value, and appends `arg = value`. But 18 of the 19 stack types have a *specialized branch* inside `_emit_resource` that runs after the generic loop and emits nested HCL blocks, hardcoded defaults, or resource-specific wiring. The count of 39 branches is the sum of the per-type specializations (some types have 2–3 branches). The full inventory: | # | Stack type | Terraform type | Specialized logic (what the branch does) | |---|-----------|----------------|------------------------------------------| | 1 | `aws:s3:bucket` | `aws_s3_bucket` | `versioning {}` block (default true); `server_side_encryption_configuration {}` block (SSE-KMS, CMK ref or managed-key fallback with stderr warning); `kms_key_arn` is not a bare arg — emitted as the SSE block. | | 2 | `aws:ec2:vpc` | `aws_vpc` | `tags { Name = ... }` from the `name` input; hardcoded `cidr_block = "10.0.0.0/16"` default when the L2 doesn't supply a CIDR (line 288). | | 3 | `aws:ec2:subnet` | `aws_subnet` | `vpc_id = aws_vpc.vpc-vpc.id` hardcoded ref when not in inputs; hardcoded `cidr_block = "10.0.1.0/24"` default (line 296); `tags { Name = ... }`. | | 4 | `aws:ec2:routetable` | `aws_route_table` | `vpc_id = aws_vpc.vpc-vpc.id` hardcoded ref; `route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.vpc-igw.id }` hardcoded default route; `tags { Name = "-rt" }`. | | 5 | `aws:ecs:cluster` | `aws_ecs_cluster` | Hardcoded `name = "acdl-microservice"` default when not in inputs (line 300). | | 6 | `aws:ecs:task_definition` | `aws_ecs_task_definition` | `_container_definitions()` helper: jsonencodes `image`/`port`/`env` into a `container_definitions` block; hardcoded `family = "app"` default (line 279). | | 7 | `aws:ecs:service` | `aws_ecs_service` | `network_configuration {}` block (subnets + security_groups wrapped in list brackets); `load_balancer {}` block from `lb_target_group_arn` with hardcoded `container_name = "app"` + `container_port = 8080`; hardcoded `desired_count = 1`, `launch_type = "FARGATE"`, `task_definition = aws_ecs_task_definition.service-task-definition.arn`, `name = "acdl-microservice"`. | | 8 | `aws:iam:role` | `aws_iam_role` | `managed_policy_arns = [...]` from comma-separated string; hardcoded ECS task execution `assume_role_policy` JSON when not supplied (line 326–331); hardcoded `name = "acdl-microservice-role"` default. | | 9 | `aws:elbv2:loadbalancer` | `aws_lb` | `subnets`/`security_group` wrapped in list brackets; hardcoded `load_balancer_type = "application"` default. | | 10 | `aws:elbv2:listener` | `aws_lb_listener` | `default_action { type = "forward" target_group_arn = aws_lb_target_group.alb-targetgroup.arn }` hardcoded; `load_balancer_arn = aws_lb.alb-loadbalancer.id` hardcoded ref. | | 11 | `aws:elbv2:targetgroup` | `aws_lb_target_group` | Hardcoded `target_type = "ip"`, `vpc_id = aws_vpc.vpc-vpc.id`, `protocol = "HTTP"`, `port = 8080`. | | 12 | `aws:ecr:repository` | `aws_ecr_repository` | Hardcoded `name = "acdl-microservice"` default; `encryption_configuration {}` block (not a bare `kms_key_arn` arg). | | 13 | `aws:cloudfront:distribution` | `aws_cloudfront_distribution` | `origin {}` block (origin_id, domain_name, origin_access_control_id, `s3_origin_config {}`); `default_cache_behavior {}` block (viewer_protocol_policy, target_origin_id, ttls, allowed/cached methods); `enabled = true`; `price_class`; `restrictions { geo_restriction {} }`; `viewer_certificate { cloudfront_default_certificate = true }`; `web_acl_id` from WAF ref. ~8 nested blocks. | | 14 | `aws:cloudfront:originaccesscontrol` | `aws_cloudfront_origin_access_control` | `name`; hardcoded `origin_access_control_origin_type = "s3"`, `signing_behavior = "always"`, `signing_protocol = "sigv4"`. | | 15 | `aws:wafv2:webacl` | `aws_wafv2_web_acl` | `name`; hardcoded `scope = "CLOUDFRONT"`; `default_action {}` (allow/block from input, default allow); `visibility_config {}`; custom `rule {}` blocks as nested HCL (P1-4 fix) or default AWS-managed-rules block. ~5 nested blocks. | | 16 | `aws:rds:instance` | `aws_db_instance` | NFR-derived `backup_retention_period` (default 7), `deletion_protection` (default true); `storage_encrypted = true` default; `skip_final_snapshot = true` (dev safety). | | 17 | `aws:kms:key` | `aws_kms_key` | NFR-derived `enable_key_rotation = true` default. | | 18 | `aws:ecs:uptime-service` | `aws_ecs_service` | Feature-flag gate (returns `""` when disabled); `container_definitions` jsonencode for uptime-kuma; hardcoded `subnets = ["subnet-uptime"]`, `security_groups = ["sg-uptime"]`, `assign_public_ip = true`; hardcoded `desired_count = 1`, `launch_type = "FARGATE"`. | Plus a global `prevent_destroy` lifecycle block emitted for every resource when `nfrs.deletion_protection` is true (line 576–581), and the `_emit_igw()` helper that synthesizes an internet gateway + route table association from the VPC resource (line 585). ### 1.3 Hardcoded defaults that belong in the module The defaults below are emitted by the adapter when the L2 composition does not supply the input. They are *resource shape* decisions — CIDR ranges, trust policies, network config — that belong in the module's `locals.tf` (D-100), not in the adapter. The adapter should pass only resolved contract inputs; if a default is wrong, fix the module, not the adapter. | Default | Adapter line | What it is | Where it belongs | |---------|-------------|------------|------------------| | `cidr_block = "10.0.0.0/16"` | 288 | VPC CIDR default | `modules/l1/vpc/terraform/locals.tf` | | `cidr_block = "10.0.1.0/24"` | 296 | Subnet CIDR default | `modules/l1/vpc/terraform/locals.tf` | | ECS task execution `assume_role_policy` JSON | 326–331 | Trust policy for the IAM role | `modules/l1/iam-role/terraform/main.tf` (or `locals.tf`) | | ECR/logs inline policy / `encryption_configuration {}` | 304–315, 380 | ECR KMS encryption block | `modules/l1/ecr/terraform/main.tf` | | Fargate `requires_compatibilities` / `launch_type = "FARGATE"` | 261–263 | ECS launch config | `modules/l1/ecs-service/terraform/locals.tf` | | `assign_public_ip` (uptime) | 565 | ECS network config | `modules/l1/uptime/terraform/main.tf` | | Listener/target ports (`port = 8080`, `container_port = 8080`) | 201, 348 | ALB + ECS container ports | `modules/l1/alb/terraform/locals.tf` + `modules/l1/ecs-service/terraform/locals.tf` | | Security group emission (`security_groups = [...]`) | 255–258, 564 | ECS network config | `modules/l1/ecs-service/terraform/main.tf` | | `name = "acdl-microservice"` (cluster, ECR, service) | 265, 300, 303 | Resource name defaults | `modules/l1/*/terraform/locals.tf` | | `family = "app"` | 279 | Task definition family | `modules/l1/ecs-service/terraform/locals.tf` | | `target_type = "ip"`, `protocol = "HTTP"` | 345, 347 | ALB target group defaults | `modules/l1/alb/terraform/locals.tf` | | `load_balancer_type = "application"` | 342 | ALB type default | `modules/l1/alb/terraform/locals.tf` | | `desired_count = 1` | 260 | ECS desired count | `modules/l1/ecs-service/terraform/locals.tf` | | WAF `scope = "CLOUDFRONT"`, managed-rules default block | 421, 472–490 | WAF defaults | `modules/l1/waf/terraform/main.tf` | | CloudFront `signing_behavior = "always"`, `signing_protocol = "sigv4"`, `origin_type = "s3"` | 365–367 | OAC defaults | `modules/l1/cloudfront/terraform/main.tf` | | CloudFront `viewer_certificate { cloudfront_default_certificate = true }`, `restrictions {}` | 403–410 | Distribution defaults | `modules/l1/cloudfront/terraform/main.tf` | | RDS `backup_retention_period = 7`, `skip_final_snapshot = true` | 497, 506 | RDS defaults | `modules/l1/rds/terraform/locals.tf` | | KMS `enable_key_rotation = true` | 510 | KMS rotation default | `modules/l1/kms-key/terraform/main.tf` | | `prevent_destroy = true` lifecycle (global) | 576–581 | Deletion protection | Each module's `main.tf` (or a shared `lifecycle.tf`) | ### 1.4 Why this is a drift from the original vision ARCHITECTURE.md §12.2 states: *"The adapter is a thin layer; it does not own L1/L2 content — it only translates."* STANDARDS.md §8 (line 448–506) documents the intended design: "a thin translator with 3 tables + specialized branches." The drift was **baked into the standards doc itself** — §8.2 explicitly blesses "specialized `_emit_resource` branches" for "resources with nested HCL blocks" and §8.3 step 4 instructs module authors to "add a specialized branch in `_emit_resource` keyed on that stack type" when a new L1 needs nested blocks. The result: every new L1 with a nested block (CloudFront, WAF, ECS, uptime) added 30–80 lines of resource-shape code to the adapter. The adapter grew from a spike-era ~150 lines to 750 lines, with the resource shape (CIDR ranges, trust policies, container ports, managed-rule sets) encoded as Python string concatenation rather than Terraform HCL. D-098 corrects the drift: the standards doc §8 must be rewritten to document the new pattern (per-module `terraform/` subdir + stateless assembler), and the "specialized branch" guidance is removed. **Confidence: 0.95.** The audit is a direct line-by-line read of the v1.10.2 `adapter.py`; the drift is structural and unambiguous. --- ## FINDING 2 — State-key root cause of the 4-VPC bug ### 2.1 The state key `adapter.py` line 664 + 676: ```python stack_name = stack.get("name", "spike") terraform_tf = ( ... f' key = "spike/{stack_name}/terraform.tfstate"\n' ... ) ``` `core/contract_resolver.py` line 569: ```python "stack": { "name": contract["id"], ... } ``` So `stack_name = contract["id"]` and the state key is `spike/{contract.id}/terraform.tfstate`. ### 2.2 The 5 microservice contracts All five microservice contracts share `id: msvc` and differ only in `environment`: | Contract file | `id` | `environment` | |---------------|------|----------------| | `contracts/microservice.yml` | `msvc` | `dev` | | `contracts/microservice.dev.yml` | `msvc` | `dev` | | `contracts/microservice.qa.yml` | `msvc` | `qa` | | `contracts/microservice.prod.yml` | `msvc` | `prod` | | `contracts/microservice.dr.yml` | `msvc` | `dr` | The state key does **not** include the environment. So all four environment contracts (dev/qa/prod/dr) collide on the same state key: `spike/msvc/terraform.tfstate`. ### 2.3 The two root causes **Root cause 1 — the adapter emits per-contract state keys with no VPC sharing.** The `microservice` composition (`modules/l2/microservice/ composition.json`) includes a `vpc` child (`vpc@1.0.0`). Every contract that resolves through this composition emits its own VPC resource. There is no platform VPC to share; each contract deploys its own VPC. D-105 corrects this: `terraform/platform` owns ONE VPC; the microservice composition drops its `vpc` child and references the platform VPC via a data source. The standalone `vpc` L1 module stays (consumers deploy their own VPCs). No per-contract VPC ever again. **Root cause 2 — the state key does not distinguish environments.** Because the state key is `spike/{contract.id}/terraform.tfstate` and all four env contracts share `id: msvc`, every environment's `terraform apply` writes to the same remote state key. Combined with the first attempt's `verify_deploy_microservice.py` running `terraform init -reconfigure` in a **fresh temp dir each time**, each run created a fresh local state that diverged from the remote key. The first run (dev) created VPC #1 and pushed it to `spike/msvc/terraform.tfstate`. The second run (qa) ran `-reconfigure` in a fresh temp dir, pulled the remote state (which had dev's VPC), but because the local state was fresh and the composition emitted a *new* VPC resource address, terraform saw the VPC as "to add" again — creating VPC #2 and overwriting the remote state. Repeating for prod and dr created VPCs #3 and #4. Four VPCs, one state key, no environment discrimination. D-106 corrects this: the composition must be deterministic — same contract → same resolved stack → same state key, every time. State keys become **env-aware and stable** across apply/modify/destroy: `spike/{id}/{env}/terraform.tfstate`. The environment is part of the key, so dev/qa/prod/dr never collide. ### 2.4 Why `-reconfigure` in a fresh temp dir made it worse `terraform init -reconfigure` forces terraform to re-read the backend config and pull remote state into the local working directory. When the working directory is a fresh temp dir (as `verify_deploy_microservice.py` did), there is no local `.terraform/` state cache — terraform must pull the remote state fresh. If the remote state key is shared across environments (root cause 2) and the composition emits a new VPC each time (root cause 1), the `-reconfigure` pull merges the prior environment's state with the new resource addresses, and the subsequent `apply` creates a new VPC because the resource address in the *new* composition run differs from the one in the remote state (the L2 namespacing or the fresh temp dir caused terraform to treat the VPC as a new resource). D-101 deletes `verify_deploy_microservice.py` entirely; `run_platform.sh` gains `--apply` and `--destroy` modes that run terraform in a stable working directory (not a fresh temp dir per run), and Python never runs terraform. **Confidence: 0.90.** The state-key derivation is a direct code read (adapter.py:664,676 + contract_resolver.py:569). The 5 contracts are read verbatim. The 4-VPC mechanism is the only consistent explanation for the observed symptom (4 VPCs in the account after 4 env runs). The 0.10 residual is for the possibility that the resource-address divergence was caused by a separate composition-namespacing bug rather than the fresh temp dir alone — but either way, the two root causes (shared state key + per-contract VPC) are confirmed and D-105/D-106 correct both. --- ## FINDING 3 — Per-module terraform module design ### 3.1 What the per-module `terraform/` subdir should contain D-098/D-099: each L1 module ships a real `terraform/` module dir. The canonical layout for a multi-resource module: ``` modules/l1// interface.json # engine-agnostic (unchanged) instance.json # regression baseline (unchanged) README.md examples/ simple.yml complex.yml terraform/ # NEW — the engine binding versions.tf # required_version + required_providers variables.tf # from interface.json inputs locals.tf # default interpolation (heavy use, D-099) main.tf # resource blocks (resource shape + nested blocks) outputs.tf # from interface.json outputs ``` Trivial single-resource modules (e.g. `s3`) may inline `locals` in `main.tf` (D-099). Multi-resource modules (`vpc`, `ecs-service`, `alb`, `microservice`-shaped) get the full split. ### 3.2 The three reference modules (from interface.json) **s3** (`modules/l1/s3/interface.json`): - `variables.tf`: `bucket_name` (string, required), `region` (string, required), `kms_key_arn` (string, optional). - `locals.tf`: `sse_algorithm = "aws:kms"`, versioning default `true`, managed-key fallback (`alias/aws/s3` when `kms_key_arn` is null), the `prevent_destroy` lifecycle. - `main.tf`: `resource "aws_s3_bucket" "this" { bucket = var.bucket_name ... }` + `versioning {}` block + `server_side_encryption_configuration {}` block (CMK ref or managed fallback). - `outputs.tf`: `bucket_arn` (→ `aws_s3_bucket.this.arn`), `bucket_name` (→ `aws_s3_bucket.this.id`), `bucket_regional_domain_name` (→ `aws_s3_bucket.this.bucket_regional_domain_name`). - `versions.tf`: `terraform { required_version = ">= 1.9, < 1.10" required_providers { aws = { source = "hashicorp/aws", version = "~> 5.0" } } }`. **vpc** (`modules/l1/vpc/interface.json` — multi-resource: vpc + subnet + routetable): - `variables.tf`: `cidr` (string, required), `azs` (string, required), `name` (string, required), `region` (string, required). - `locals.tf`: `cidr_block = coalesce(var.cidr, "10.0.0.0/16")`, subnet CIDR derivation (`cidrsubnets(local.cidr_block, 8, 8, ...)` per AZ), `name` tag interpolation, the IGW + route table association. - `main.tf`: `aws_vpc`, `aws_subnet` (count/for_each over `azs` split), `aws_route_table`, `aws_internet_gateway`, `aws_route_table_association` — all the resources that the adapter's `_emit_igw()` helper synthesized dynamically now live here as real HCL. - `outputs.tf`: `vpc_id`, `subnet_ids` (join the subnet ids). - `versions.tf`: same provider block. **ecs-service** (`modules/l1/ecs-service/interface.json` — multi-resource: task_definition + service): - `variables.tf`: `image`, `port`, `cpu` (default 256), `memory` (default 512), `env` (optional), `cluster_arn`, `subnets`, `security_group`, `lb_target_group_arn` (optional), `region`, `kms_key_arn` (optional), `desired_count` (default 1), `launch_type` (default "FARGATE"), `family` (default "app"). - `locals.tf`: `container_definitions` jsonencode (image/port/env/cpu/ memory), `requires_compatibilities = ["FARGATE"]` when launch_type is FARGATE, log group name + KMS ref, the `prevent_destroy` lifecycle. - `main.tf`: `aws_ecs_task_definition` (family, container_definitions, requires_compatibilities, execution_role_arn) + `aws_ecs_service` (name, cluster, task_definition, desired_count, launch_type, network_configuration {}, load_balancer {} block). - `outputs.tf`: `service_arn`, `task_def_arn`. - `versions.tf`: same provider block. ### 3.3 How the stateless adapter assembles them The new adapter (D-098) is a ~80-line stateless assembler. It: 1. Reads `modules/registry.json` → for each resource in the resolved stack instance, looks up the L1 module by `module` field (`@`). 2. Gets the `terraform_dir` from the registry entry (or derives it as `modules/l1//terraform/`). 3. Emits a root `main.tf` with one `module "x" { source = "" ... }` block per resource, passing the resolved contract inputs as module arguments. 4. Wires refs via `module "x".` interpolations: a `ref:.` input value becomes `module..` in the consuming module block. 5. Emits the stack-level `output {}` blocks (passthrough from the producing module's outputs). 6. Emits `terraform.tf` (backend config with the env-aware state key, D-106) + `providers.tf` (aws provider, region from the first resource). The adapter holds **no** TYPE_MAP, INPUT_MAP, OUTPUT_MAP, and no type-specific branches. The engine binding (stack type → Terraform resource type, input → arg name, output → attribute name, nested blocks, defaults) lives entirely in the per-module `terraform/` subdir. `interface.json` stays engine-agnostic. **Confidence: 0.90.** The module layout is grounded in the existing `interface.json` files (read verbatim) and the Terraform module convention (versions/variables/locals/main/outputs split). The assembler design is D-098/D-099 (user-confirmed). The 0.10 residual is for the exact `terraform_dir` registry field shape (not yet implemented) and the ref-wiring syntax (`module..` vs a locals alias). --- ## FINDING 4 — Existing pipeline architecture ### 4.1 The central pipeline contract `pipelines/contract.yml` is the declarative deployment pipeline spec (a contract, not an executable workflow). It declares 9 stages: `validate-contract` → `resolve-stack` → `terraform-plan` → `checkov` → `confidence` → `apply` (dev only) → `publish-outputs` → `deploy-uptime` → `comment-outputs`. Each stage has `name`, `command`, `required` (bool), and optional `description`. The executable workflow (`.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml`, byte-identical) implements these stages by invoking `scripts/run_platform.sh`. Validated against `schemas/deploy-pipeline.schema.json`. ### 4.2 The plan-only pipelines (existing, run on every PR) Two platform pipelines run on every PR to main (offline, free): | Pipeline | File | Matrix | What it does | |----------|------|--------|--------------| | Primitives plan | `.github/workflows/primitives-plan.yml` (+ `.gitea/` byte-identical) | `s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf, rds` (10 primitives) | For each L1 primitive, runs `bash scripts/run_primitive_plan.sh --check-only ` — resolves the primitive's `instance.json`, runs the adapter, validates the emitted Terraform structure (offline, no AWS). | | Patterns plan | `.github/workflows/patterns-plan.yml` (+ `.gitea/` byte-identical) | `static-assets, microservice` (2 modules) | For each L2 module, runs `bash scripts/run_pattern_plan.sh --check-only ` — resolves the sample contract, runs the adapter, validates the emitted Terraform (offline). | Both trigger on `pull_request: branches: [main]`, run on `ubuntu-latest`, install `jsonschema pyyaml boto3`. The `--check-only` mode is offline (no AWS, no Checkov, no DynamoDB) — it resolves the contract/instance, runs the adapter, and validates the emitted Terraform file structure. This is what makes the pipelines free. ### 4.3 `run_platform.sh` — plan only, never apply/destroy `scripts/run_platform.sh` (521 lines) has three modes today: - `--check-only` (offline, no AWS): contract → resolver → adapter → stream TF → validate → exit 0. - `--plan-only` (requires AWS): contract → resolver → adapter → `terraform init -reconfigure -lock=false` → `terraform validate` → `terraform plan -lock=false -out=tfplan` → exit 0 (line 274–297). - default (requires AWS + Checkov + DynamoDB): contract → resolver → adapter → `terraform plan` → Checkov → confidence → outbox. **Critically, line 287 runs `terraform plan` only.** There is no `terraform apply` and no `terraform destroy` in `run_platform.sh` today. The `apply` stage in `pipelines/contract.yml` (line 54–57) declares `command: bash scripts/run_platform.sh --plan-only` — a misnomer; it runs plan, not apply. The lifecycle modes (`--apply`, `--destroy`) **must be added** (D-101). Python never runs terraform; `run_platform.sh` is the only shell entry point. ### 4.4 `run_primitive_plan.sh` `scripts/run_primitive_plan.sh` (65 lines) runs the platform pipeline for a single primitive. `--check-only` mode: resolves `instance.json`, runs the adapter, validates the emitted `{main.tf,terraform.tf,providers.tf}` exist and `main.tf` is non-empty. Default mode (requires AWS): `terraform init -backend=false` → `terraform validate` → `terraform plan`. This is the per-primitive plan check that the primitives-plan pipeline matrix invokes. ### 4.5 The byte-identical Gitea+GitHub convention `pipelines/README.md:22` documents the convention: "Create byte-identical workflow YAMLs in `.gitea/workflows/.yml` and `.github/workflows/.yml`." Both workflows must implement the same stages, commands, triggers, and runner declared in the contract. `tests/test_pipeline_contract.py` validates that the Gitea and GitHub workflow YAMLs are byte-identical and conform to the schema. The only difference is the forge runtime (Gitea Actions vs GitHub Actions). The new modules-lifecycle pipeline (D-102) must follow this convention: byte-identical `.gitea/workflows/modules-lifecycle.yml` + `.github/workflows/modules-lifecycle.yml`. **Confidence: 0.95.** All pipeline files are read verbatim from the v1.10.2 tree. The "plan only, never apply/destroy" finding is a direct read of `run_platform.sh` line 287 + the `--plan-only` exit at line 293. --- ## FINDING 5 — PERSONAS.md update for v1.11 The existing `PERSONAS.md` (v1.9) has 6 active personas: `lead-developer`, `backend-engineer`, `platform-engineer` (custom), `security-engineer` (custom), `lambda-engineer` (custom, v1.9), `frontend-engineer`. v1.11 changes the roster: - **Deactivate `lambda-engineer`** — no per-module Python this milestone (D-102: testing is pipeline-driven, not pytest). The v1.9 Lambda (`core/lambda/contract_ingestor.py`) persists but is not touched in v1.11. - **Deactivate `cost-engineer`** — not in the v1.9 roster (the v1.9 `data-engineer` is already deactivated). v1.11 has no cost-engineer work; cost is documented in `COST.md` (REQ-119) by the lead-developer. - **Keep `backend-engineer`** — owns the adapter rewrite (stateless assembler) + `core/contract_resolver.py` (env-aware state keys, D-106). - **Keep `data-engineer`** (reactivated) — owns `terraform/` (platform VPC, D-105) + the per-module `terraform/` subdirs (the engine binding, D-098/D-099/D-100). This is the heaviest territory in v1.11: 12 L1 modules each get a real `terraform/` module dir. - **Keep `general`** (the `lead-developer` + `backend-engineer` pipeline work) — owns `pipelines/` + `.gitea/workflows/` + `.github/workflows/` (the modules-lifecycle pipeline, D-102) + `scripts/run_platform.sh` (`--apply`/`--destroy` modes, D-101). ### Territory alignment (v1.11) | Persona | Territory | Domain | |---------|-----------|--------| | backend-engineer | `adapters/terraform/adapter.py` (rewrite to stateless assembler), `core/contract_resolver.py` (env-aware state keys), `schemas/stack.schema.json` (if touched) | backend | | data-engineer | `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), `modules/registry.json` (terraform_dir field) | data | | general (lead-developer + backend-engineer) | `pipelines/modules-lifecycle.yml`, `.gitea/workflows/modules-lifecycle.yml` + `.github/workflows/modules-lifecycle.yml` (byte-identical), `scripts/run_platform.sh` (`--apply`/`--destroy`), `scripts/run_primitive_plan.sh` (if extended), `modules/STANDARDS.md` §8 rewrite | coordination + pipelines | ### Territory enforcement: `warn` Co-authoring is expected on the adapter + `run_platform.sh` boundary (backend-engineer rewrites the adapter; general adds the lifecycle modes to `run_platform.sh` that invoke it). `warn` keeps it frictionless — cross-territory edits are logged in the commit message but do not fail the task. ### Domain priority (v1.11) `data → backend → general` Rationale: 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. The updated `PERSONAS.md` is written to `/root/acdl/.ciagent/PERSONAS.md` (see that file). YAML frontmatter with `active`, `phase_specific`, and `reason` fields per persona. **Confidence: 0.90.** The persona changes are grounded in the CLARIFY decisions (D-098..D-107) and the v1.11 scope (no per-module Python → lambda-engineer deactivated; terraform module authoring is the heaviest work → data-engineer reactivated). --- ## Assumptions logged | ID | Assumption | Confidence | Rationale | |----|------------|------------|-----------| | A-1.1 | The `terraform_dir` field will be added to `modules/registry.json` entries (or derived as `modules/l1//terraform/`) so the stateless adapter can locate each module's terraform subdir. | 0.85 | D-098 says the adapter reads `registry.json` → gets `terraform_dir`. The exact field name is not yet locked; the derivation path is the obvious fallback. | | A-1.2 | The ref-wiring syntax in the root `main.tf` will be `module..` (standard Terraform module output interpolation), not a locals alias. | 0.85 | The existing `_ref_expr` already produces `..`; the module equivalent is `module..`. Standard Terraform convention. | | A-2.1 | The 4-VPC bug's resource-address divergence was caused by the fresh temp dir + `-reconfigure` pull merging remote state with new composition runs, not a separate composition-namespacing bug. | 0.80 | The two confirmed root causes (shared state key + per-contract VPC) are sufficient to explain 4 VPCs. The exact terraform-state mechanics of the divergence are inferred, not observed in a debug log. | | A-3.1 | Trivial single-resource modules (s3) may inline `locals` in `main.tf`; multi-resource modules (vpc, ecs-service, alb) get the full 5-file split. | 0.90 | D-099 states this explicitly. | | A-4.1 | The modules-lifecycle pipeline will matrix-run each L1 module's `examples/{simple,complex}.yml` contracts (the modify variants), not new contract files. | 0.90 | D-103: "Uses the module's own existing example contracts as the modify variants. No extra contract files needed." | | A-5.1 | `platform-engineer` and `security-engineer` from the v1.9 roster are folded into `data-engineer` and `backend-engineer` for v1.11 (the v1.11 scope is terraform + adapter + pipelines, not security adapters or HITL gates). | 0.75 | The v1.11 scope (D-097..D-107) does not touch Wiz/Kyverno/Checkov/HITL. The persona roster is simplified to the three active domains. | --- ## Decisions surfaced (research → already bound in CLARIFY) All v1.11 binding decisions (D-097..D-107) were committed in the CLARIFY stage (`80b7286`) before this research ran. This research *grounds* those decisions with codebase evidence; it does not surface new binding decisions. The decisions are summarized in §Background above and documented in full in the CLARIFY commit. --- # v1.12 Addendum — Presentation Refinement Research > Generated: 2026-07-29. Phase 66. Milestone v1.12. > Mode: docs-only NFR milestone focused on the leadership decks. > Surface: `docs/presentations/` (PW + DX, all four layers) + one real > adapter fix + two probe fixes required to make deck claims true. ## Background — why v1.12 exists v1.11 (P56a–P65) landed the stateless adapter, pipeline-driven lifecycle testing, single platform VPC, `COST.md`, `PRE_MORTEM.md`, and a teardown to zero-cost. P65's plan (REQ-118) required the decks to be rewritten to "Verified live-aws via lifecycle pipeline; torn down to zero-cost." That rewrite did not fully land on the deck artifacts. This research is a drift audit: a systematic comparison of the deck artifacts against the v1.11-verified reality. ## FINDING 1 — Drift audit (9 items) Systematic comparison of `docs/presentations/*` against `.ciagent/CAPABILITY_INVENTORY.md`, `.ciagent/COST.md`, `.ciagent/PRE_MORTEM.md`, `.ciagent/ROADMAP.md`, and `git log`. 1. **Wrong verification status.** Both rendered HTML decks still say "6 cloud capabilities are design-verified + locally emulated, deploy-unverified (IAM drift)" (PW "Testing vs. Planned" slide; DX slide A6). `CAPABILITY_INVENTORY.md` says 22/22 Verified and the IAM-drift framing was *removed* in P65. The decks contradict the inventory. Verified: `grep -c "deploy-unverified\|IAM drift\|design-verified" docs/presentations/*.html` → 3 hits per deck. 2. **Re-verification header stale.** Both source `.md` headers say "Re-verification (2026-07-27)… v1.10 Phase 54… 16/16… 6 IAM-gated escalated." Should reflect v1.11: 22/22 Verified, torn down. 3. **Road to the North Star diagram stale.** `docs/presentations/assets/mmd/road-to-north-star.mmd` shows v1.10 as "NEXT" with "HITL wiring / all-runner OIDC / regulatory ledger". v1.11 is complete; the diagram must advance. 4. **Rendered HTML not re-rendered.** `git log` shows the HTML was last touched at `10b87a6` (P57), *before* v1.11. P65's "re-render HTML" task did not reach the rendered artifacts. 5. **Version refs stale.** Decks reference `@v1.10` in deploy.yml `uses:` snippets (Safe Promotion Path, Safe Decommission). Ship tag is now `v1.11.0`; will be `v1.12.0` at Phase 70 complete. 6. **Cost story has no real numbers.** `COST.md` exists ($0.001883 over 8 days, ~$0.007/mo, S3-dominated, zero BAU compute) but the decks' A6 "Operating Model & Cost" slide is generic prose with no figures. 7. **Pre-mortem unreferenced.** P65 planned to add a pre-mortem reference; `PRE_MORTEM.md` exists (v1.10 decay root cause + four forward failure modes) but no deck slide references it. 8. **Two v1.11 stories absent.** (a) Architectural simplicity: adapter 918→~80 lines, defaults centralized in per-module `terraform/` dirs. (b) Verifiable deploys: a `modules-lifecycle` pipeline matrix-runs each module apply→modify→destroy against live AWS. Neither is in the decks. 9. **Duplicated story-beat lines.** `how-the-platform-works.md` slides 3–10 each repeat their intro line twice (a copy-paste artifact). ## FINDING 2 — Regression gate surfaces real decay (D-091) The v1.12 regression gate run (Phase 66) re-ran the D-091 regression gate to back every deck claim. It found **3 Broken capabilities**: `{'Verified': 19, 'Decayed': 0, 'Broken': 3}`. ### CAP-013 — live-aws — REAL platform defect (Class A) `adapters/terraform/adapter.py:159-172` (the `seen` dedup loop) collapses the two `ecs-service` sub-resources (`service-task-definition` + `service-service`, both module `ecs-service@1.0.0`) into ONE `module "service-task-definition"` block. But the stack output `service_arn` (resolver `from: "service-service"`) is emitted as `value = module.service-service.service_arn` — referencing a module call that was never emitted. `terraform validate` fails: "No module call name." The same defect silently breaks the `alb` L1 too. Static- assets (CAP-014) doesn't hit it because its L1s are single-resource. **Classification A — real platform defect.** The adapter produces invalid Terraform for any multi-resource L1 with stack-level outputs. **Fix required before decks can claim 22/22 Verified.** ### CAP-017 — lifecycle-pipeline — regression-probe bug (Class B/C) `core/regression_verify.py:444` hardcodes `required = ["versions.tf", "variables.tf", "locals.tf", "main.tf", "outputs.tf"]`. The CAP-017 probe targets the `rds` L1 module, whose `main.tf` uses only `var.*` and `aws_db_subnet_group.this` — no `local.*` references, so `locals.tf` is legitimately absent. The probe is over- strict. The rds module is correctly structured; the capability works. **Classification B/C — trivial probe fix.** Drop `locals.tf` from the required list, or make it conditional on `local.` usage. ### CAP-018 — lifecycle-pipeline — regression-probe bug (Class B/C) `core/local_emulators.py:273` defines `LocalLambdaStub` as a dataclass with one required field `outbox: FlatFileOutbox`. Every real caller passes it (`core/local_emulators.py:464`, the tests). The CAP-018 probe at `core/regression_verify.py:486-491` is the *only* caller that instantiates it bare: `LocalLambdaStub()` → `TypeError`. The probe was added in P63 and never aligned with the real signature. The capability is exercised green by CAP-011. **Classification B/C — trivial probe fix.** Pass an `outbox` to the constructor. ### Implication for the decks `CAPABILITY_INVENTORY.md` claims 22/22 Verified, but the regression gate (D-091 — the exact mechanism PRE_MORTEM.md FM-3 says backs every deck claim) shows CAP-013 is genuinely broken. **The inventory overstates.** v1.12 cannot ship decks claiming 22/22 until CAP-013 is fixed and the gate re-runs clean. This is the structural mitigation the pre-mortem requires (verified-only claims; decks unfrozen only after re-verification). The user decision: fix the defect inside v1.12 (Phase 67), then the decks can honestly claim 22/22. ## FINDING 3 — Talking points structure gap Both talking-points files have only 5 appendix sections (A1–A5) while the Marp decks have 6 (A6 = "Operating Model & Cost"). The A6 content exists in the Marp deck and source markdown but was never distilled into the talking points. The re-distill step (Phase 69) must add the A6 section to both talking-points files. ## FINDING 4 — Versioning facts - Current ship tag: `v1.11.0` (v1.11 complete). - `deploy.yml` still references `v1.9` in comments + `ref: v1.9` — v1.11 apparently did not bump the deploy workflow `uses:` tag (the bump is a separate concern; decks use the current ship tag). - Decks should show `@v1.11` in examples (current state); Phase 70 bumps to `@v1.12` after the tag exists. ## Assumptions logged - No automated `ci-doc-verifier` script exists in the repo. The plan's "ci-doc-verifier confirms" is satisfied by a manual grep-based verification recorded in the Phase 70 VERIFY step (consistent with how prior NFR-patch phases handled it). Confidence 0.90 — verified by `ls scripts/ | grep doc` and `grep -rl deck tests/`. - PPTX export requires Chromium + Marp CLI; the environment has it (`/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome`). PPTX is uploaded to the Gitea release, not committed. Confidence 0.85 — README documents the path; the chromium binary exists. ## Decisions surfaced (research → bound in CLARIFY-equivalent) - **D-108** — v1.12 includes one real adapter fix (CAP-013) and two probe fixes (CAP-017, CAP-018) as Phase 67 prerequisites, so the decks can honestly claim 22/22 Verified. The milestone is "presentation refinement" but the verified-only-claims pre-mortem mitigation makes the fixes mandatory. The user confirmed this scope (interactive decision, 2026-07-29). - **D-109** — Decks use `@v1.11` in examples during Phase 68 (current state), bumped to `@v1.12` at Phase 70 complete after the tag exists. Avoids a dangling reference to a tag that doesn't exist yet. --- ## v1.14 Research Addendum — NFR Refinement scope audit (2026-07-29) > Phase 0 RESEARCH for milestone v1.14 (NFR Refinement). A full codebase > survey (8 categories, file:line evidence) was conducted to populate the > 20-phase scope. This addendum records the findings; the phase list is > in ROADMAP.md §v1.14; the requirements are in REQUIREMENTS.md §v1.14. ### Survey method Read-only survey of `/root/acdl` at v1.13.2 (HEAD `139224ff`, 533 tests collected). 8 categories: stubs, P1/P2 backlog, security, docs drift, test gaps, terraform gaps, workflow gaps, config hygiene. All file:line references verified against the live codebase. ### Finding 1 — Open P1/P2 backlog (REVIEW.md v1.11) 5 P1 + 4 P2 findings from the v1.11 multi-persona review remain open: | ID | File:Line | Status | v1.14 phase | |----|-----------|--------|-------------| | P1-1 | `adapter.py:159-170` (silent drop of unregistered-module resources) | open | P1 | | P1-2 | `static-assets/composition.json` (unwired cloudfront inputs; WAF unconditional) | open | P2 | | P1-3 | `run_l2_lifecycle_*.sh` (vestigial `[ci-vpc-outputs.json]` arg) | open | P3 | | P1-4 | `CAPABILITY_INVENTORY.md:9-16` (summary table stale) | **fixed** (now 22/22) | — | | P1-5 | `regression_verify.py:432-519` (CAP-017..022 offline proxy, no `terraform validate`) | open | P4 | | P2-1 | `alb/main.tf:9` (`name_prefix="tg-ci-"` discards `var.name`) | open | P6 | | P2-2 | `test_adapter.py` (no dedup-merge or remote-state-key test) | open | P5 | | P2-3 | `waf/complex.yml` + `locals.tf` (redundant `upper()` + uppercase example) | open (post-hoc) | folded into P2 | | P2-4 | `COST.md:106` (account ID published; accepted exposure) | open (post-hoc) | folded into P8 (centralize code-side) | ### Finding 2 — Security posture gaps **Swallowed errors (6 sites):** - `core/local_emulators.py:374` — `except Exception: pass` in `_fake_urlopen`; if patching fails, urlopen stays real → network egress. [SEC] → P7. - `core/lambda/contract_ingestor.py:157` — GitHub search failure → `existing = []` → duplicate issues. → P7. - `terraform/bootstrap/create_state_backend.py:51` — over-broad `except Exception:` on `head_bucket` → spurious `create_bucket` on permissions/network errors. → P7. - `core/output_publisher.py:100,168` — SSM/GitHub failure → silent `None`/`False`. → P7. - `terraform/bootstrap/apply_iam_baseline.py:78` — over-broad on old-version delete. → P7. **Hardcoded account ID `581513795199` (15+ sites):** `adapter.py:125,140`, `apply_iam_baseline.py:33`, `create_state_backend.py:33,35`, `push_consumer_image.py:32`, terraform state-bucket names, ECR image ref. → P8 (externalize to `ACDL_AWS_ACCOUNT_ID` / `data.aws_caller_identity`). **IAM policy wildcards (6 `Resource: "*"` statements):** `spike_runner_policy.json` — cloudfront (line 117), wafv2 (129), kms (218), iam (236). KMS allows key creation/deletion on ANY key; IAM allows role creation on ANY role. → P9 (scope to `acdl-*` ARNs). **Contract-ingestor identity validation gap:** `contract_ingestor.py:221-245` — `_validate_caller_identity` validates `consumerRepo` format only; doesn't verify caller owns the repo (ABAC reliance). No `contractId`/`environment`/`error` validation. → P10. **Schema validation gaps:** `contract.schema.json` + `environment.schema.json` — no `additionalProperties: false` (undocumented fields pass silently); no format validation for bucket/ARN/CIDR. → P11. **Credential hygiene:** `.gitignore` covers `.env*`/`*.tfstate*` but no credential-pattern catch-all (`*.pem`/`*.key`/`*.p12`). → P12. **Audit ledger integrity (D-083):** `audit_ledger_design.md:47-70` — JWS + Object Lock + DLQ deferred. Per D-096, stays deferred; documented in P19. The hash-chain + DynamoDB outbox is the v1.14 audit record. ### Finding 3 — Stubs / missing functionality - `adapters/kyverno/kyverno_adapter.py:11,115-116` — `--kube-version` parsed then discarded (`_ = kube_version`). → P13 (implement or remove + document). - `scripts/__pycache__/verify_deploy_microservice.cpython-312.pyc` — orphan bytecode for a deleted source file. → P14. - `core/regression_verify.py:237` — DynamoDB write deferred to Phase 54 (outbox hash-chain verified, no real DynamoDB write). Accepted deferral. - `adapters/wiz/wiz_adapter.py` — real GraphQL client (not a stub); degrades gracefully. OK. - `core/separation_of_duties.py` — `route_halt_artifact` is real (SNS + outbox fallback). OK. - `core/lambda/contract_ingestor.py` — `report_error` is real (GitHub issues via Secrets Manager). OK. ### Finding 4 — Documentation drift - `ARCHITECTURE.md` — no v1.11/v1.12/v1.13/v1.14 addendum; line 500-506 still describes the **old** parameterized adapter (pre-stateless rewrite). → P19. - Stale `@v1.6`–`@v1.9` workflow refs in `README.md:225`, `docs/consumer-guide.md` (12 sites), `docs/architecture.md:233`, `docs/pipeline/versioning.md:29`, `docs/pipeline/index.md:42`. → P19. - `modules/STANDARDS.md` §8 references `TYPE_MAP` (deleted in v1.11); §9.4 requires 5-file split but §489-492 allows inlining — inconsistent. → P18. - `COST.md` window stops at v1.10; no v1.11–v1.13 spend. → P19. - `GRILL.md` G-005/G-008 escalations — CAP-017..022 now Verified via lifecycle pipeline; COST.md now exists. → P19 (mark resolved). - `IAM_POLICY.md` — reflects v1.11 re-bootstrap but not v1.12/v1.13. → P19. - Decks reference "v1.12" verification status; not re-synced for v1.13.2. → P19. ### Finding 5 — Test coverage gaps - 533 tests collected; 5 `@pytest.mark.slow` (deselected from fast suite). 7 scripts with no test: `seed_uptime_monitors.py`, `push_consumer_image.py`, `sync_to_gl.sh`, `post_stage_comment.sh`, `rotate_spike_key.sh`, `create_state_backend.py`, `create_iam_user.py`. → P15. - Adapter dedup-merge + `ACDL_REMOTE_STATE_KEY` override — no unit test (P2-2). → P5. ### Finding 6 — Terraform gaps - 3 L1 modules lack `locals.tf` (`ecr`, `ecs-cluster`, `rds`). → P18. - `static-assets/composition.json` unwired inputs (P1-2). → P2. - `terraform/platform/main.tf:255` — hardcoded CIDR; `count=2` subnets not data-driven. → P20. - `terraform/bootstrap/create_state_backend.py:51` — over-broad except (Finding 2). → P7. ### Finding 7 — Workflow / pipeline gaps - 4 GitHub-only workflows (patterns-plan, platform-test, primitives-plan, release) — no Gitea mirror. → P16. - `rotate_spike_key.sh` (only `set -u`), `sync_to_gl.sh` (no `set` flags). → P16. - 3 shared workflows (ci, deploy, modules-lifecycle) byte-identical (verified). OK. - modules-lifecycle matrix covers all 12 L1 + 2 L2. OK. ### Finding 8 — Config / project hygiene - `config.json` bash_allowlist has dead JS entries (npm/node/jest/eslint /tsc — no package.json). → P14/P17. - `config.json` `branching_strategy: "phase"` mismatched with flat-workflow practice. → P17. - `config.json` `ollama-cloud.base_url: ""` (empty; no `glm` model configured). → P17. - `config.json` `frontend-engineer` persona still in `personas[]` (PERSONAS.md:80 says inactive). → P17. - `pyproject.toml` version `1.3.0` (stale); coverage source `acdl_platform` (renamed to `core` in v1.6). → P14. ### Persona assessment (v1.14) The v1.14 milestone is NFR-only (bug fixes, security, tests, docs). The active persona roster from v1.11 (PERSONAS.md) carries forward unchanged: - **lead-developer** (active) — coordination; owns the wave ordering + cross-phase dependencies. - **backend-engineer** (active) — owns `adapters/`, `core/` (adapter dedup, contract ingestor, regression gate, output publisher). - **data-engineer** (active) — owns `terraform/`, `modules/` (ALB fix, static-assets wiring, platform VPC, IAM policy, STANDARDS). - **frontend-engineer** (inactive) — no frontend; decks are markdown (lead-developer territory). Stays deactivated per PERSONAS.md:80. No custom personas needed for v1.14 (no new domains). Territory enforcement = `warn` (config.json:167). The v1.14 work is concentrated in `adapters/`, `core/`, `terraform/`, `scripts/`, `tests/`, `docs/`, `.ciagent/` — all within existing persona territories. --- ## v1.15 Research Addendum — Nova Rebrand scope audit (2026-07-30) ### Survey method A thorough, exhaustive codebase survey (via the explore subagent) plus targeted `grep -rni` counts. The survey covered 346 tracked files, reporting occurrence counts and the mechanical-vs-judgment split per category. The full survey is recorded in the planning conversation transcript; the binding conclusions are summarized here. ### Finding 1 — Brand string surface area - **1,465** total `ACDL`/`acdl` occurrences across **205** files. - **17** occurrences of the full "Agentic Cloud Delivery Platform" phrase (all prominent titles/headers: README, docs/index, vision, pyproject, decks, .ciagent/*.md). - **0** existing references to "nova" (case-insensitive) — **no collision risk**. - User-facing (docs/, README, decks, contracts, schemas, module READMEs): high-priority for rebrand. - Internal (.ciagent/*.md, tests/, terraform/, scripts/, workflows): mechanical but voluminous. ### Finding 2 — Code identifiers (judgment category) - **Python package name**: `pyproject.toml` `name = "acdl"` (no `acdl/` package dir exists — source lives in `core/`, `adapters/`; the name is a metadata label). Mechanical rename. - **Python file**: `adapters/terraform/policy/custom_rules/acdl_tagging.py` (+ Checkov registration in `schemas/tagging-standard.json` line 5 + adapter config). Rename file + update registration. - **Env var prefixes**: 21 distinct `ACDL_*` prefixes (`ACDL_LIFECYCLE_MODE` 66×, `ACDL_AWS_ACCESS_KEY_ID` 40×, `ACDL_AWS_SECRET_ACCESS_KEY` 37×, `ACDL_REMOTE_STATE_KEY` 22×, `ACDL_AWS_ACCOUNT_ID` 21×, `ACDL_TAG_NAMING` 20×, `ACDL_KMS_KEY_ID` 16×, `ACDL_BOOTSTRAP_AWS_*` 15× each, `ACDL_SOD_HALT_TOPIC_ARN` 14×, `ACDL_LOCAL_TIER` 13×, etc.). No centralized env loader exists today (scattered `os.environ.get`). D-108: a new `core/env.py` `get_env()` helper centralizes the dual-read fallback. - **Workflow `name:`**: `.github/workflows/release.yml` line 11 `name: acdl-release` — mechanical. ### Finding 3 — AWS resource names (high-risk migration) Terraform creates real AWS resources with `acdl-` prefixes. Renaming forces destroy+recreate (downtime, data loss for DynamoDB/state bucket). D-102: full rename with migration (user-directed). | Resource | Type | Migration | |----------|------|-----------| | `acdl-contracts` / `acdl-change-requests` | DynamoDB | scan+copy data, verify row counts | | `acdl/github-token` | Secrets Manager | recreate secret, repoint Lambda | | `acdl-contract-ingestor` (role/policy/Lambda) | IAM+Lambda | recreate role/Lambda, update trigger | | `acdl-sod-halt` | SNS | recreate topic, repoint publisher | | `acdl-ecs-sg` | SG | recreate (brief ECS disruption) | | `alias/acdl-platform` | KMS alias | repoint alias (cheap) | | `acdl-microservice` (cluster/ECR/service/task/role) | ECS+ECR | re-push images, recreate service | | `acdl-spike-runner` (user/policy) | IAM | re-bootstrap with new key | | `acdl-tfstate-581513795199-us-east-1` | S3 state bucket | `terraform init -migrate-state`, back up state JSON | | `acdl-alb` (name prefix) | ALB | recreate (brief downtime) | ### Finding 4 — Consumer/infra conventions (judgment category, D-104) - **AWS tag keys** `acdl:owner|environment|contract|cost-center|ref` (5 keys, ~109 tag assignments) — matched by ABAC session policies. Parallel-tag period (add `nova:*`, swap policy, remove `acdl:*`). - **SSM path** `/acdl/{env}/{contractId}/{output}` (67 refs) — deploy outputs stored here. Migration script copies params, readers updated, old deleted. - **Consumer path** `.acdl/contract.yml` (23 refs) — consumer repos depend on this. Renamed `.nova/contract.yml` + migration guide. ### Finding 5 — Docs & decks (mechanical) - README.md (16), docs/index.md, docs/vision.md, docs/architecture.md, docs/consumer-guide.md (35), docs/modules/index.md (28), all .ciagent/*.md, modules/STANDARDS.md, schemas/README.md, pipelines/README.md, adapters/README.md, terraform/*/README.md. - Deck markdown + mermaid `.mmd` sources (5 files) + rendered HTML. PNGs re-exported from edited `.mmd` sources. - S&P visual theme (`sp-theme.json`, deck CSS) is **client branding** — D-107: untouched. Only product-brand text (ACDL→Nova) changes. - Schema `$id` URLs (`https://acdl.cloudinit.dev/schemas/...`) → `https://nova.cloudinit.dev/schemas/...` (D-110: illustrative, no DNS resolution needed for validation). ### Finding 6 — CI / pipeline / release - Workflow files mirrored in `.gitea/workflows/` + `.github/workflows/` (modules-lifecycle 31×, deploy 22×, ci 2×, release 3×). - `release.yml` release title `ACDL vX.Y.Z` → `Nova vX.Y.Z` (forward only; past releases keep names). - Git branches/tags use `milestone/v*` / `phase/*` / `v*` — **no brand name present**, no change needed (D-112: flat-branch convention preserved). - config.json `release.gitea.repo` stays `acdl` (D-105: real repo name unchanged; doc URLs illustrative only). ### Finding 7 — External / URLs - `github.com/acdl/...` (~20 refs in docs + module READMEs + reusable-workflow `uses:` refs) — D-105: illustrative, updated to `nova` for prose. Real GitHub org/repo rename is out of scope. - `git.cloudinit.dev/continuous-intelligence/acdl*` (incl. sister repos `acdl-contracts`, `acdl-evidence`) — updated in prose to `nova*`. - README has **no badges** (no shields.io, no img src). ### Finding 8 — Nomenclature / tagline - "DevSecOps", "New Dawn", "enabler" appear **nowhere** in the repo today — clean insertion, no collisions to reconcile (D-106). - Current tagline ("North Star" / "consumers declare intent") is retained; Nova tagline added alongside. - "bottleneck" (6 occurrences in deck talking points) — compatible with the Nova "no bottleneck" messaging; left in place. ### Persona assessment (v1.15) No new personas needed for v1.15 — the rebrand touches existing territories (docs, code, terraform, CI, tests). The active roster: **lead-developer** (docs/decks/.ciagent meta + verification + migration runbooks), **backend-engineer** (core/env.py dual-read helper, contract resolver path, Lambda, output_publisher, regression_verify), **data-engineer** (terraform resource names/tagging, state bucket migration, DynamoDB data migration, ECR re-push, schemas/tagging-standard). The **frontend-engineer** remains deactivated (no UI; decks are markdown = lead-developer territory). A **security-engineer** persona is not activated — the ABAC session-policy + tag-key migration (REQ-162) is data-engineer territory (terraform IAM) with lead-developer review. Territory enforcement = `warn` (co-authoring expected at the core/env.py + terraform boundary, and the contract-resolver + deploy-workflow boundary). ### Assumptions logged - A1 (confidence 0.9): No live AWS access is available during P0–P4 execution (the `acdl-spike-runner` IAM user's creds are in `.env.secrets` but live apply/modify/destroy is gated by `NOVA_LIFECYCLE_MODE` defaulting to plan-only). The terraform changes are validated via `terraform validate`; live apply is exercised by the modules-lifecycle workflow when explicitly set to full. This matches the v1.11–v1.14 established pattern. - A2 (confidence 0.85): `.env.secrets` contains live rotated AWS credentials keyed by `ACDL_AWS_*`. P2 renames the KEYS only (values stay). The runtime reads via the new `core/env.py` dual-read helper (`NOVA_AWS_ACCESS_KEY_ID` preferred, `ACDL_AWS_ACCESS_KEY_ID` fallback), so no re-rotation is needed until P5 removes the fallback. - A3 (confidence 0.8): The Gitea release API (`POST .../releases`) is reachable for `v1.14.x` tags (the v1.14 milestone shipped releases through `v1.13.24` / release id 285). P0 ship targets `v1.14.0`. --- ## v1.16 NFR Simplification — Research Addendum (2026-07-30) **Milestone:** v1.16-Nova-Simplification (NFR). Research is codebase- grounded (not domain/ecosystem) — the two explore passes identified concrete, file:line-verified residual debt the v1.15 rebrand left, plus genuine simplification and the onboarding request-path scaffolding. ### R1. v1.14 NFR categories already closed (do NOT re-propose) v1.14 (REQ-135..154) swept: over-broad excepts (REQ-141), hardcoded account-ID externalization (REQ-142, `ACDL_AWS_ACCOUNT_ID` env + live fallback G-102), IAM `Resource:"*"` scoping to `nova-*` ARNs (REQ-143, G-104), contractId/env/error validation (REQ-144), `additionalProperties:false` schemas (REQ-145), `.gitignore` credential catch-all (REQ-146), Kyverno `--kube-version` removal + deferral doc (REQ-147, G-103), orphan bytecode/dead-config cleanup (REQ-148), 7 untested-script test coverage (REQ-149), `set -euo pipefail` parity + Gitea workflow parity (REQ-150), config/persona/backend hygiene (REQ-151), STANDARDS.md TYPE_MAP consistency (REQ-152), ARCHITECTURE/COST/GRILL doc sync (REQ-153), platform-VPC CIDR/subnet parameterization (REQ-154). The v1.16 grill (G-101..G-106) and escalation E-001 are all CLOSED. v1.16 finds NEW residual signals (D-117). ### R2. Fresh debt the v1.15 rebrand left (verified file:line) **High-severity correctness regressions (P1):** - `adapters/terraform/adapter.py:117` — emits `state_bucket = f"acdl-tfstate-{account_id}-us-east-1"`. The live state bucket was renamed to `nova-tfstate-*` in v1.15 P4 (REQ-163), but the adapter's emitted terraform backend still references `acdl-tfstate-*`. In plan-only mode this is latent (no real init against the bucket), but a full-mode lifecycle run would point at a non-existent bucket. - `adapters/kyverno/policies/require-resource-labels.yml:6,21,25,33,37` — enforces `acdl:owner`/`acdl:environment` labels. `nova_tagging.py` hard-fails on any `acdl:*` key post-P5 (REQ-164). The Kyverno policy contradicts the Nova tagging standard. **User-facing brand misses (P2):** - `core/environment_check.py:59,61` — onboarding message header/body say "ACDL Environment Onboarding" / "ACDL environments are platform- managed" (user-facing). - `core/lambda/contract_ingestor.py:145,191` — GitHub issue alert title `[ACDL-ALERT]` + body "auto-created by the ACDL platform Lambda" (user-facing artifact). - `scripts/post_stage_comment.sh:39,46` — PR comment header "ACDL Stage" + footer "ACDL deploy pipeline" (user-facing). - `scripts/run_ci.sh:39` — CI banner "ACL CI Pipeline". - Module docstrings: `core/contract_resolver.py:1,474`, `core/confidence_signal.py:1`, `adapters/terraform/adapter.py:1`, `adapters/kyverno/kyverno_adapter.py:1`, `adapters/wiz/wiz_adapter.py:1`, `adapters/README.md:1`, `adapters/kyverno/README.md:4,18`. **Dead code + stale comments (P3):** - `scripts/run_platform.sh:153` — `export ACDL_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE" # legacy fallback, removed in P5` — comment says "removed in P5" but the line is STILL present (dead code, P5 already shipped). - Stale dual-read comments across `core/local_emulators.py:15-16,503,505`, `core/regression_verify.py:318-319,333`, `scripts/run_regression.sh`, `scripts/run_lifecycle_*.sh` (reference the retired G-106 fallback). - `acdl_*` temp-dir prefixes: `core/local_emulators.py:71,252`, `core/regression_verify.py:183,234`, `scripts/run_pattern_plan.sh:29`, `scripts/run_primitive_plan.sh:29`, `scripts/run_lifecycle_*.sh:36,41`. ### R3. Simplification opportunities (verified) - `core/regression_verify.py:328-409` — `_check_live_terraform_plan_ microservice` + `_check_live_terraform_plan_static_assets` are ~95% identical (resolve → adapt → init → validate → plan). Extract `_check_live_terraform_plan(contract, label)` (~35 lines saved). - `core/regression_verify.py:149-178` — `_check_resolver_static_assets` + `_check_resolver_microservice` identical except contract path. Extract `_check_resolver(contract)`. - `core/regression_verify.py:477-503` — duplicated lifecycle-contract- resolve block. Extract `_assert_contracts_resolve(module_dir)`. - `scripts/run_platform.sh:336-350` + `:452-466` — duplicated HITL attestation block. Extract `run_hitl_gate()` shell fn (~14 lines). - `core/contract_resolver.py:50-68` duplicates `core/environment_check.py: 36-54` env loader verbatim. Import instead. - `scripts/run_platform.sh:145-146` — hardcoded `CONTRACT_ID` UUID + `WORK="/tmp/acdl_platform_run_v18"` (`v18` stale). Make config/env- derived. - `.gitea/workflows/` ↔ `.github/workflows/` — 3 byte-identical pairs (`ci.yml`, `deploy.yml`, `modules-lifecycle.yml`, ~20 KB) maintained by hand + a test asserting identity. Generator (D-115) eliminates manual-sync risk. - `core/contract_resolver.py:540` — `is_l2 = "l2" in interface_path or "composition" in interface_path` fragile string heuristic. Add `kind` to registry entries (P7). - `scripts/run_platform.sh` (610 lines) — decommission block (`:180-237`) + uptime block (`:520-606`) are self-contained. Extract to `scripts/run_decommission.sh` + `scripts/run_uptime.sh` (P9). ### R4. Security gaps (verified, NEW — not v1.14 duplicates) - `core/lambda/contract_ingestor.py:251-252` — `if not caller_arn: pass` silently skips identity validation when IAM identity absent; relies on ABAC layer only (no defense-in-depth). Fail closed instead (P10). - `core/lambda/contract_ingestor.py:269` — `valid_envs = {"dev","qa", "prod","dr"}` hardcoded; the `core/environments/` dir is the source of truth. Derive from the directory (P10). - `core/lambda/contract_ingestor.py` — `submit_contract` checks the `contract` key exists but never validates the blob's size or schema. Unbounded payload → DynamoDB write amplification. Size cap + schema validation (P11). - `scripts/migrate_ssm_paths.py:113` — `except Exception: pass` (claims `ParameterNotFound` but catches all). Last true broad-swallow. Narrow to `ParameterNotFound` (P4). - `core/output_publisher.py:112,182` — `except Exception` in `publish_to_ssm` + `post_github_comment` swallow all (not narrowed by REQ-141 which targeted 6 other sites). Narrow to specific exceptions. ### R5. Onboarding request-path scaffolding (already present) The infrastructure for a zero-human *request* path already exists: - `terraform/platform/main.tf:153-183` deploys `contract_ingestor` Lambda + Function URL (IAM auth). - `core/lambda/contract_ingestor.py:325-362` dispatches `submit_contract | report_error | validate_change_request`. Adding `onboard_consumer` is a small extension (P18, D-119: writes a `pending` CMDB row, no provisioning). - `terraform/platform/consumer_invoke_policy.json` is the ABAC policy template (`aws:PrincipalTag/nova:owner == ${consumerRepo}` scoped `lambda:InvokeFunctionUrl`). - `core/environments/*.json` are static JSON templates with placeholder `account_id: "000000000000"` — auto-generation from a request is straightforward (P19). Missing for "no humans": (a) `onboard_consumer` action + onboarding schema (P18), (b) `core/onboarding.py` to auto-generate `.json` + emit a PR (P19), (c) cross-account deploy-role + ABAC tag Terraform, offline-proven (P20, D-114). Real AWS account/network/state creation stays a future feature (D-113). ### R6. Developer experience gaps - `scripts/run_platform.sh` has no `--help` (`:82` rejects `--*` flags). `--deploy-uptime` (`:532`) is undocumented in the header. `--local` is absent from the README (P15). - No `.github/workflows/README.md` cataloging the 7 workflows' inputs/ secrets/triggers (P16). - No single getting-started path; README "How to run" lists 3 manual bootstrap steps. The offline happy path (`run_ci.sh` + `run_platform.sh --check-only`/`--local`) is not surfaced first (P17). ### Assumptions logged (v1.16) - A1 (0.9): No live AWS access during execution (consistent with v1.11–v1.15). `NOVA_LIFECYCLE_MODE` defaults to plan-only; terraform changes validated via `terraform validate`. The state-bucket drift (P1) is latent in plan-only mode but must still be fixed for correctness. - A2 (0.85): The `onboard_consumer` Lambda action (P18) is offline- testable via `moto` / the local Lambda stub (D-092), consistent with the existing `submit_contract`/`report_error` test pattern. - A3 (0.8): The workflow generator (P8, D-115) must preserve the byte-identity property *as a test assertion* (generated outputs match committed files), not lose it — the dedup is mechanical, not a semantic change to the workflows. - A4 (0.85): The regression gate (D-091, D-118) at P9 and P21 confirms "simplify without regressions" — 22/22 capabilities must stay Verified. The gate is the credible control for the simplification wave. --- # v1.17 Research — Strategic Direction, Leadership Metrics & Unified Story > Phase: research (P0). Milestone: v1.17. Status: research. > Researcher: ci-researcher + explore agent (signal inventory). > Autonomy: full. Decisions D-120..D-132 locked in the planning > conversation (PROJECT.md). NORTH_STAR.md drafted (pending GRILL). ## 1. Telemetry Signal Inventory (grounding audit) **Methodology:** every claim below is grounded in a concrete file path + line number in `/root/acdl`. No speculation. The explore agent performed a full sweep of the repo. The finding: **Nova has no metrics/telemetry/ dashboard aggregation layer today.** What exists is a set of discrete, structured, file-based signal artifacts (JSON reports, JSONL logs, hash-chained outbox events, PR comments, Checkov JSON) plus unstructured stdout logs. A metrics milestone must aggregate these existing signals — it must not invent new ones without first adding emitters. ### (a) Signals that EXIST TODAY and are STRUCTURED (groundable) | Signal | File / Emitter | Schema | Persistent? | |--------|---------------|--------|-------------| | Regression report (22 caps, status, duration_ms, gate) | `.ciagent/REGRESSION_REPORT.json` ← `core/regression_verify.py:643-667` | `regression_verify.py:82-91` | **Yes** (committed file) | | Regression report (markdown mirror) | `.ciagent/REGRESSION_REPORT.md` | same | Yes | | Checkpoint (milestone/phase/tag/regression summary) | `.ciagent/CHECKPOINT.json` (CIAgent-managed) | ad-hoc | Yes | | PolicyCheckResult list (per-rule pass/fail/severity/resourceRef) | `$WORK/pcr.json` ← `run_platform.sh:395` + `checkov_adapter.py:50-71` | `schemas/policy_check_result.schema.json` | **No** (ephemeral `/tmp/`) | | Confidence signal (score, band, perInput, reasonCodes) | `$WORK/signal.json` ← `run_platform.sh:412-426` + `confidence_signal.py:60-65` | `confidence_signal.py:60-65` | No (ephemeral) | | Outbox event (hash-chained, CONFIDENCE_COMPUTED) | `$WORK/event.json` + `$WORK/outbox_item.json` ← `run_platform.sh:444-459` + `outbox_writer.py:44-56` | `audit_ledger_design.md:44-45,81-97` | No (ephemeral; live DynamoDB torn down D-096) | | Resolved Target Stack | `$WORK/stack.json` ← `contract_resolver.py:581-603` | `schemas/stack.schema.json` | No (ephemeral) | | Lambda return bodies (submit/report_error/validate_cr/onboard) | `core/lambda/contract_ingestor.py:171,265,284,392,446` | ad-hoc JSON | No (Lambda not live; local stub only) | | DynamoDB CMDB rows (submitted/pending contracts) | `nova-contracts` table ← `contract_ingestor.py:160-170,433-445` | ad-hoc | **No** (table torn down D-096) | | SSM parameters (deploy outputs) | `/nova///` ← `output_publisher.py:123-156` | ad-hoc | No (live AWS, torn down) | | PR stage comment (mode, runId) | GitHub PR API ← `post_stage_comment.sh:34-48` + `deploy.yml:141` | markdown table | Yes (GitHub) | | PR deploy-outputs comment | GitHub PR API ← `output_publisher.py:159-189` | markdown table | Yes (GitHub) | | GitHub issue (deploy failure alert) | GitHub API ← `contract_ingestor.py:179-290` + `deploy.yml:143-152` | issue body | Yes (GitHub) | | Local E2E result (stack_name, tier, outbox_events, chain_verified, lambda_status) | stdout JSON ← `core/local_emulators.py:498-508,519` | ad-hoc | No (stdout) | | HITL gate result | `core/hitl_gates.py:87,90` + `run_platform.sh:179-185` | stdout `HITL PASS/BLOCK` | No (stdout) | | Attestation matrix result | `core/attestation_matrix.py:184,187` | stdout `ATTESTATION PASS/BLOCK` | No (stdout) | | Cost figures | `.ciagent/COST.md` (manual Cost Explorer query) | markdown table | Yes (manual, not automated) | ### (b) Signals that EXIST but are UNSTRUCTURED (log-only) | Signal | Source | Format | |--------|--------|--------| | CI pipeline result | `scripts/run_ci.sh:70-71` | stdout banner `=== CI PIPELINE OK ===` | | Platform stage banners + summaries | `scripts/run_platform.sh:222,241,258,263,315,383,411,442,463,490,496` | stdout `=== Step N: ... ===` + summary lines | | Terraform init/validate/plan/apply/destroy logs | `$WORK/tf-*.log` ← `run_platform.sh:320,324,328,352,375` | raw terraform stdout (via `tee`) | | Lifecycle test results | `scripts/run_lifecycle_test.sh` etc. | exit code only (no report file) | | Decommission step counts | `scripts/run_decommission.sh:40,54` | stdout `decommission step N: M resources...` | | Uptime endpoint count | `scripts/run_uptime.sh:72,87` | stdout `uptime: N endpoint(s) to monitor` | | Onboarding prompt | `core/environment_check.py:57-81` | stdout text block | | Pytest results | `pyproject.toml:25` (`-v --tb=short`) | stdout only (no junit/json) | | sync_workflows result | `scripts/sync_workflows.py:56,53` | stdout `OK: 3 workflow pairs match` / `DRIFT: ...` | ### (c) Proposed executive metrics with NO grounding today (DEFERRED) | Proposed metric | Why no grounding | Controlling decision | |------------------|------------------|---------------------| | Live infrastructure health (ECS running count, ALB 5xx, RPS) | Live AWS torn down; CAP-013..016 Skipped | **D-096** | | Live outbox write rate / ledger append latency | DynamoDB outbox table absent | **D-096** | | Tamper-evident ledger checkpoint count / JWS signature rate | S3 Object Lock + JWS + async worker deferred | **D-083** | | Onboarding funnel: requested → granted conversion | Only "requested" (pending row) is emitted; no grant event | **D-113, D-114, D-119** | | Time-to-provision (onboarding SLA) | Real AWS provisioning deferred | **D-113** | | Cross-account role grant count | Offline-proven only, no live apply | **D-114** | | Drift detection (scheduled terraform plan -detailed-exitcode) | Needs live AWS workspaces + a scheduler Nova doesn't have | **D-096** + no scheduler | | GreenOps / carbon (WattTime/Electricity Maps API) | No grounding; new external API | future emitter | | Predictive vs Reactive ratio | Requires an ML anomaly-forecasting service | future emitter | | Multi-cloud normalization (Azure/GCP/K8s, FOCUS spec) | Nova is AWS-only | future | | Red Team MTTR | No red-team program exists | future | | Self-healing velocity | Nova has no auto-remediator | future emitter | | SLA / unplanned downtime | Needs live service uptime monitoring against SLOs | **D-096** | | Per-module lifecycle success rate over time | No structured report file written; only exit code | gap (no decision) | | Test pass rate / test count time-series | No junit/json reporter configured | gap (add `--junitxml` to addopts) | | Code coverage trend | `pytest-cov` installed but not in `addopts` | gap | | Deploy frequency / lead time / MTTR (DORA) | No deploy-event emitter; pipeline runs not counted | gap | | Policy pass rate time-series | `pcr.json` emitted but ephemeral; not persisted | gap (D-096 blocks live persistence) | | Confidence score distribution over time | `signal.json` emitted but ephemeral | gap | | Consumer adoption count / active consumers | `PROJECT.md:487` explicitly states "0 consumer adoption today" | honest scope | | Cost time-series (automated) | `COST.md` is a one-shot manual query; no automated emitter | gap | **Bottom line:** the single richest existing structured signal is `.ciagent/REGRESSION_REPORT.json` (22 capabilities × {status, tier, duration_ms, detail} + summary counts + boolean gate). The next richest is the per-run `$WORK/*.json` family (pcr.json, signal.json, event.json, stack.json) — but these are **ephemeral** and **not persisted in CI**. The lowest-friction grounding for a "no-humans" dashboard is therefore: (1) regression report → capability health, (2) PR comments + GitHub issues → deploy/failure activity, (3) add `--junitxml` to pytest → test trend, (4) persist `$WORK/*.json` → policy/confidence/outbox time-series, (5) extend outbox_writer → Decision Ledger, (6) add Infracost → pre-apply cost estimates. ## 2. Telemetry Reference Architecture (Nova-native adaptation) The PO provided a full distributed-system telemetry reference architecture (CloudEvents 1.0 envelope, OpenTelemetry SDK, Kafka/NATS event bus, Prometheus hot path, ClickHouse warehouse, QLDB decision ledger, Infracost, drift detection, ML anomaly forecasting). Per D-120, we adopt the **principles** but implement with **Nova-native minimal tech**. The mapping: | Direction's principle | Nova-native implementation (v1.17) | |---|---| | Events are the source of truth; dashboards are projections | Hybrid (D-125): existing file signals stay as files; collector reads them and emits normalized CloudEvents into `metrics/events.jsonl` + SQLite. New emitters emit CloudEvents directly. | | Every AI action is logged with confidence + alternatives | Decision Ledger (D-121): `outbox_writer.py` extended → SQLite append-only hash-chain table. `ai.decision.made` modeled from confidence_signal (D-122): decision_id=run_id, chosen_action=band, confidence=score, alternatives=perInput, human_override=HITL block. | | Hot/cold storage split | Cold-only SQLite (D-126): `metrics/nova_metrics.db`. Hot path deferred (no live ops, D-096). | | Read-only external integrators | Infracost (pre-apply, offline, reads plan JSON). Cloud billing CUR deferred (D-096). Carbon APIs deferred (future). | | CloudEvents 1.0 envelope | Adopted. `core/metrics/event_envelope.py` defines the envelope + `platform.*` semantic conventions. | | Decision Ledger = append-only with hash chain + outcome backfill | SQLite append-only table with hash chain (D-121). Outcome backfilled from apply.completed via decision_id → request_id correlation. Honors D-083 (no S3 Object Lock/JWS). | | Cost governance: mandatory tags + Infracost pre-apply | Nova already enforces `nova:*` tags (nova_tagging.py, hard mode). Infracost added as plan post-processor (D-120). Post-apply CUR deferred (D-096). | | Definition-of-success docs for every KPI | Per-KPI docs in `docs/metrics/` (D-127). | | Replay-ability | SQLite store + JSONL event log are replayable by design. | ### CloudEvents envelope (Nova-native) ```json { "specversion": "1.0", "id": "", "source": "nova.platform", "type": "nova.run.completed", "time": "", "subject": "/", "datacontenttype": "application/json", "platform": { "tenant_id": "acdl", "run_id": "run-", "contract_id": "", "environment": "dev|qa|prod|dr", "actor": {"type": "confidence-gate", "id": "confidence_signal"}, "trace_id": "" }, "data": { "duration_ms": 4800, "stages": ["resolve", "adapt", "validate", "plan", "apply"], "exit_code": 0, "confidence": {"score": 0.94, "band": "pass", "perInput": {...}}, "policy": {"passed": 12, "failed": 0, "skipped": 0}, "hitl": {"gate": "dev", "result": "autonomous", "block": false}, "cost_estimate_usd": -12.40, "decision_id": "run-", "outcome": "succeeded" } } ``` ### Core event types (Nova-native minimum viable set) | Event type | Emitted by | Purpose | Grounding | |---|---|---|---| | `nova.run.started` | run_platform.sh | Measures demand; provisioning lead time start | new emitter (P1) | | `nova.run.completed` | run_platform.sh | Run count, stage durations, exit, MTTR | new emitter (P1) | | `nova.run.failed` | run_platform.sh | Failure count, MTTR numerator | new emitter (P1) | | `nova.policy.evaluated` | checkov_adapter.py | Policy pass rate, compliance KPIs | grounded (pcr.json → P1 persists) | | `nova.confidence.computed` | confidence_signal.py | Confidence distribution, decision accuracy | grounded (signal.json → P1 persists) | | `nova.ai.decision.made` | outbox_writer.py (extended) | Decision Ledger entry | grounded (D-121, D-122) | | `nova.attestation.recorded` | hitl_gates.py | Attestation Coverage, human-in-the-loop audit | grounded (D-132) | | `nova.cost.estimated` | Infracost post-processor | Pre-apply cost estimate | new emitter (P1, Infracost) | | `nova.capability.verified` | regression_verify.py | Capability health, regression gate | grounded (REGRESSION_REPORT.json) | | `nova.test.completed` | pytest (junit XML) | Test count, pass rate | new (P1 adds --junitxml) | ## 3. Metric-to-Signal Scorecard (the "no fabrication" contract) | Executive metric (NORTH_STAR target) | Status | Source / formula | Decision | |---|---|---|---| | Touchless Resolution Rate ≥99% | grounded (after P1) | runs without operational HITL block ÷ total runs (attestation gates excluded) | D-122, D-132 | | Human Escalation Frequency <0.1% | grounded (after P1) | operational HITL blocks ÷ total runs (attestation sign-offs excluded) | D-122, D-132 | | MTTR (p95) <60s | grounded (platform-run) | apply.failed.time → successful retry.time | D-131 | | Predictive vs Reactive ≥3:1 | **deferred** | requires ML forecasting (future emitter) | future | | AI Decision Accuracy ≥99.5% | grounded (after decision ledger) | decisions not followed by apply.failed/incident within 5min | D-121, D-122 | | Drift Auto-Reversal ≥95% | **deferred** | requires drift detection (D-096 + scheduler) | D-096 | | Cloud Spend Reduction ≥25% | partial | pre-apply estimate grounded (Infracost); actuals deferred (D-096 CUR) | D-120 | | L1/L2 Ops Hours Avoided ≥70% | derived | formula: run count × manual baseline minutes × blended rate | D-127 | | Platform ROI ≥250% | derived | formula: (labor savings + cloud savings + avoided downtime) ÷ platform op cost | D-127 | | Decision Ledger Coverage 100% | grounded (this milestone) | outbox_writer.py → SQLite hash-chain | D-121 | | Attestation Coverage 100% | grounded | hitl_gates.py + outbox approver_* attributes; prod/dr | D-132 | | AI-Agent Intent Share ≥40% | future | no AI-agent consumers today; placeholder view | future | | Capability health (18V+4S) | grounded | REGRESSION_REPORT.json | existing | | Confidence score distribution | grounded (after P1) | signal.json → decision ledger | D-121 | | Policy pass rate | grounded (after P1) | pcr.json → persisted | D-120 | | Test count / pass rate | grounded (after P1) | pytest --junitxml | D-120 | | Provisioning Lead Time | grounded (after P1) | run.started → run.completed | D-120 | | Deployment Frequency | grounded (after P1) | count(run.completed) per day | D-120 | | Deploy-failure alert count | grounded | GitHub issues via Lambda report_error (D-055) | existing | | Cost figures (actuals) | manual one-shot | COST.md (Cost Explorer query) | existing | | FTE Hours Saved / TRV | derived | formula over run count + COST.md | D-127 | | Self-healing velocity | **deferred** | no auto-remediator | future | | SLA / unplanned downtime | **deferred** | needs live service uptime (D-096) | D-096 | | GreenOps / carbon | **deferred** | WattTime/Electricity Maps API (future) | future | | Red Team MTTR | **deferred** | no red-team program | future | | Multi-cloud normalization | **deferred** | Nova is AWS-only | future | | Live CUR reconciliation | **deferred** | needs live AWS billing (D-096) | D-096 | ## 4. Deferred-Decision Ledger (constraints on this milestone) | Decision | Scope | Grounding impact | |----------|-------|------------------| | D-096 | Live AWS torn down post-v1.11 | BLOCKS all live-AWS metrics (CAP-013..016 Skipped; live outbox; live state bucket; live CUR) | | D-083 | S3 Object Lock + JWS + async worker deferred | BLOCKS tamper-evident ledger; v1.17 uses SQLite hash-chain instead | | D-113/D-114/D-119 | Onboarding = request-path only; no auto-grant | BLOCKS onboarding funnel "granted" half | | D-091/D-118 | Regression gate (D-091) gates milestone completion | ENABLES the strongest metric signal (REGRESSION_REPORT.json) | | D-092 | Local emulating adapters | ENABLES offline E2E metrics (CAP-011/012) | | D-055 | report_error Lambda action creates GitHub issues | ENABLES deploy-failure alert metric | | D-050 | Publish deploy outputs to SSM + GitHub PR comment | ENABLES outputs-published metric | | D-054/D-043/D-109 | Nova tagging standard (hard mode) | ENABLES tagging-compliance metric | | D-084 | 8-concern attestation matrix | ENABLES attestation metrics (operator-supplied evidence artifacts) | | D-089 | Signature verification skipped when signing key unset (dev/CI) | Signature metrics are no-ops in dev | ## 5. Deck-Storytelling Research (x3 arc + per-slide benefit) ### The "tell them x3" structure The PO's direction: "Tell them what you're going to tell them, then tell them, then tell them what you told them." Applied at two levels: **Deck level (the 5-act arc):** 1. **Opening slide** = "what I'm going to tell you" — the full arc preview: Problem → Vision → How → Proof → Roadmap. 2. **Body** (acts 1–5) = "tell them" — each act delivers its content. 3. **Closing slide** = "what I told you" — recap of the 5 acts + the ask. **Per slide:** 1. **Slide opens** with what it'll cover (1 line: "This slide shows X"). 2. **Slide delivers** the content (bullets, diagram, or table). 3. **Slide closes** with an explicit **"benefit of this stage" callout** (1 line: "Benefit: you now know Y" or "Why this matters: Z"). ### Fluidity conventions - **Transitions are written, not hand-waved.** Each slide's opening line references the previous slide's close ("Having seen X, now consider Y"). - **No disjointed jumps.** If a topic shift is needed, a bridge slide or a transition sentence carries the audience across. - **The arc is visible.** A small "act indicator" in the Marp footer (e.g., `Act 3/5: How it works`) keeps the audience oriented. ### Existing deck inventory (to be retired) Two decks exist today in `docs/presentations/`: - `how-the-platform-works.md` (32,916 bytes) → marp → html → talking-points - `the-developer-experience.md` (27,509 bytes) → marp → html → talking-points Both follow a 4-step process (source `.md` → Marp → HTML → talking-points) documented in `docs/presentations/README.md`. Per D-130, both are merged into one unified narrative deck and retired. ### Grounded metrics already cited in existing decks - "22/22 auto-verifiable capabilities Verified" — **stale** vs current REGRESSION_REPORT.json (18V+4S post-D-096). The unified deck must derive this from the report, not copy the stale claim. - Confidence thresholds: dev ≥0.50, qa ≥0.75, prod ≥0.90, dr ≥0.95 — grounded in `core/confidence_signal.py:57` (THRESHOLDS). - RPO = 0 (evidence write synchronous) — grounded in `core/audit_ledger_design.md:27,103`. - Cost figures — `how-the-platform-works.md:461`; cites COST.md. - Confidence signal 6 inputs + weights — grounded in `core/confidence_signal.py:40-47`. - "~80-line stateless adapter" vs "918-line monolith" — grounded in ROADMAP/RESEARCH prose. ### Planned deck structure (for PLAN to detail) The unified deck "Nova — The No-Humans Infrastructure Platform": | Act | Slides | Content | Proof source | |---|---|---|---| | 1. Problem | 2–3 | The no-humans imperative; why operators are the bottleneck; the trust gap | NORTH_STAR vision | | 2. Vision/Direction | 2–3 | Nova's vision; 4 strategic objectives; anti-goals; the attestation model (autonomy in operations, human at stage gates) | NORTH_STAR | | 3. How it works | 3–4 | Contract → resolver → adapter → confidence → HITL gate; the Decision Ledger; the 8-concern attestation matrix | code grounding | | 4. Proof (metrics) | 3–4 | Capability health (18V+4S); confidence distribution; policy pass rate; Decision Ledger coverage; Attestation Coverage; cost estimates; the grounded/derived/deferred honesty model | metrics export | | 5. Roadmap/Ask | 2 | 12–18mo targets (committed); deferred metrics (honest); the ask | NORTH_STAR targets | Total: ~12–16 slides. Opening = arc preview; closing = recap + ask. ## 6. Assumptions logged (v1.17) - A1 (0.9): No live AWS access during execution (consistent with v1.11–v1.16). All metrics that require live AWS ship as placeholder views. The Infracost integration runs offline (reads plan JSON). - A2 (0.85): The Decision Ledger SQLite hash-chain is sufficient for v1.17's audit needs. The full tamper-evident ledger (S3 Object Lock + JWS, D-083) is a future milestone. The hash-chain provides append-only + integrity verification locally. - A3 (0.8): The "AI decision" framing (D-122) is honest: Nova's "AI" is the confidence-gated policy engine (confidence_signal + HITL gate), not an LLM planner. The deck and METRICS.md must frame this accurately — overclaiming "AI" would violate the "no fabrication" constraint. - A4 (0.85): The unified deck's "Proof" section cites only grounded metrics with real numbers. Deferred metrics are shown as "Planned" with the `Planned` badge. No fabricated numbers in any slide. - A5 (0.8): `--junitxml` + `--json-report` added to pytest addopts does not break the existing test suite (the flags are additive; pytest continues to run normally). CAP-009 (offline pytest suite passes) must remain Verified after the change. - A6 (0.75): Infracost is available as a CLI tool that can be installed in the CI environment and run locally. It reads `terraform plan -out=plan.tfplan` + `terraform show -json plan.tfplan` to produce a cost estimate. No live AWS access required. If Infracost is not available, the `cost.estimated` event is omitted (degraded mode, not a failure).