Files
acdl/.ciagent/RESEARCH.md
T
Jon Chery aebc63127d docs(P66): v1.12 init + research — presentation refinement drift audit
---
ci---
project: acdl
phase: 66
milestone: v1.12
status: specify
---
/ci---

---
ci---
project: acdl
phase: 66
milestone: v1.12
status: research
---
/ci---

Spec: validate v1.12 specification (presentation refinement, decks-only
surface + one adapter fix + two probe fixes). REQUIREMENTS.md gains
REQ-129..REQ-133. config.json milestone v1.11 -> v1.12, branch
milestone/v1.12-presentation.

Research: drift audit (9 items) comparing docs/presentations/* against
v1.11-verified reality. Regression gate (D-091) re-run surfaced 3 Broken
capabilities: CAP-013 (real adapter dedup defect, Class A), CAP-017
(probe over-strict re locals.tf, Class B/C), CAP-018 (probe stale
LocalLambdaStub signature, Class B/C). PRE_MORTEM.md FM-3 requires decks
to match verified reality; the inventory's 22/22 claim is overstated
until CAP-013 is fixed. Decisions D-108 (fix defect inside v1.12),
D-109 (deck version refs @v1.11 -> @v1.12 at Phase 70).
2026-07-29 12:51:13 +00:00

695 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ACDL — 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:
```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/<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-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 <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=false` → `terraform validate` →
`terraform 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=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/<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.