Files
acdl/.ciagent/RESEARCH.md
T
Jon Chery ee5c372e65 docs(P00): complete pre-execution phase — v1.16 NFR scope
Phase 0: SPECIFY → CLARIFY → RESEARCH → IDEATE → PLAN → GRILL.
NFR milestone v1.16 (Nova Simplification), 20 execution phases + final.
Tags on v1.15.x line: v1.15.5 (this phase) → v1.15.6..v1.15.25 (P1-P20) →
v1.15.26 (P21 final = release).

Scope (D-113..D-119): Simplify without regressions, Security,
Maintainability, User/Developer Experience, No Humans Onboarding Flow
(request-path only; real AWS provisioning deferred). Regression gate
(D-118, G-111) gates P9 + P21 at 20/22 Verified + 2 Skipped.

Grill: PASS-with-binding (G-111..G-113, E-002 deferred to P21).

---ci---
project: acdl
phase: 0
milestone: v1.16
status: complete
phase_role: pre_execution
requirements:
  covered: [REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-170, REQ-171, REQ-172, REQ-173, REQ-174, REQ-175, REQ-176, REQ-177, REQ-178, REQ-179, REQ-180, REQ-181, REQ-182, REQ-183, REQ-184]
  partial: []
---/ci---
2026-07-30 15:12:32 +00:00

65 KiB
Raw Blame History

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:<service>:<kind>) → 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 23 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 = "<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 326331); 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 576581), 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 326331 Trust policy for the IAM role modules/l1/iam-role/terraform/main.tf (or locals.tf)
ECR/logs inline policy / encryption_configuration {} 304315, 380 ECR KMS encryption block modules/l1/ecr/terraform/main.tf
Fargate requires_compatibilities / launch_type = "FARGATE" 261263 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 = [...]) 255258, 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, 472490 WAF defaults modules/l1/waf/terraform/main.tf
CloudFront signing_behavior = "always", signing_protocol = "sigv4", origin_type = "s3" 365367 OAC defaults modules/l1/cloudfront/terraform/main.tf
CloudFront viewer_certificate { cloudfront_default_certificate = true }, restrictions {} 403410 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) 576581 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 448506) 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 3080 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:

stack_name = stack.get("name", "spike")
terraform_tf = (
    ...
    f'    key    = "spike/{stack_name}/terraform.tfstate"\n'
    ...
)

core/contract_resolver.py line 569:

"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/<name>/
  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 (<name>@<semver>).
  2. Gets the terraform_dir from the registry entry (or derives it as modules/l1/<name>/terraform/).
  3. Emits a root main.tf with one module "x" { source = "<terraform_dir>" ... } block per resource, passing the resolved contract inputs as module arguments.
  4. Wires refs via module "x".<output> interpolations: a ref:<id>.<out> input value becomes module.<id>.<out> 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.<id>.<out> 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-contractresolve-stackterraform-plancheckovconfidenceapply (dev only) → publish-outputsdeploy-uptimecomment-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 <primitive> — 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 <module> — 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=falseterraform validateterraform plan -lock=false -out=tfplan → exit 0 (line 274297).
  • 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 5457) 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=falseterraform validateterraform 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/<name>.yml and .github/workflows/<name>.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/<name>/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.<id>.<output> (standard Terraform module output interpolation), not a locals alias. 0.85 The existing _ref_expr already produces <tf_type>.<id>.<attr>; the module equivalent is module.<id>.<output>. 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 (P56aP65) 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 310 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 (A1A5) 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:374except 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.pyroute_halt_artifact is real (SNS + outbox fallback). OK.
  • core/lambda/contract_ingestor.pyreport_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.11v1.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.ZNova 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 P0P4 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.11v1.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:153export 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:540is_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-252if 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:269valid_envs = {"dev","qa", "prod","dr"} hardcoded; the core/environments/ dir is the source of truth. Derive from the directory (P10).
  • core/lambda/contract_ingestor.pysubmit_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:113except Exception: pass (claims ParameterNotFound but catches all). Last true broad-swallow. Narrow to ParameterNotFound (P4).
  • core/output_publisher.py:112,182except 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 <env>.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.11v1.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.