aebc63127d
--- 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).
695 lines
41 KiB
Markdown
695 lines
41 KiB
Markdown
# 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
|
||
2–3 branches). The full inventory:
|
||
|
||
| # | Stack type | Terraform type | Specialized logic (what the branch does) |
|
||
|---|-----------|----------------|------------------------------------------|
|
||
| 1 | `aws:s3:bucket` | `aws_s3_bucket` | `versioning {}` block (default true); `server_side_encryption_configuration {}` block (SSE-KMS, CMK ref or managed-key fallback with stderr warning); `kms_key_arn` is not a bare arg — emitted as the SSE block. |
|
||
| 2 | `aws:ec2:vpc` | `aws_vpc` | `tags { Name = ... }` from the `name` input; hardcoded `cidr_block = "10.0.0.0/16"` default when the L2 doesn't supply a CIDR (line 288). |
|
||
| 3 | `aws:ec2:subnet` | `aws_subnet` | `vpc_id = aws_vpc.vpc-vpc.id` hardcoded ref when not in inputs; hardcoded `cidr_block = "10.0.1.0/24"` default (line 296); `tags { Name = ... }`. |
|
||
| 4 | `aws:ec2:routetable` | `aws_route_table` | `vpc_id = aws_vpc.vpc-vpc.id` hardcoded ref; `route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.vpc-igw.id }` hardcoded default route; `tags { Name = "<name>-rt" }`. |
|
||
| 5 | `aws:ecs:cluster` | `aws_ecs_cluster` | Hardcoded `name = "acdl-microservice"` default when not in inputs (line 300). |
|
||
| 6 | `aws:ecs:task_definition` | `aws_ecs_task_definition` | `_container_definitions()` helper: jsonencodes `image`/`port`/`env` into a `container_definitions` block; hardcoded `family = "app"` default (line 279). |
|
||
| 7 | `aws:ecs:service` | `aws_ecs_service` | `network_configuration {}` block (subnets + security_groups wrapped in list brackets); `load_balancer {}` block from `lb_target_group_arn` with hardcoded `container_name = "app"` + `container_port = 8080`; hardcoded `desired_count = 1`, `launch_type = "FARGATE"`, `task_definition = aws_ecs_task_definition.service-task-definition.arn`, `name = "acdl-microservice"`. |
|
||
| 8 | `aws:iam:role` | `aws_iam_role` | `managed_policy_arns = [...]` from comma-separated string; hardcoded ECS task execution `assume_role_policy` JSON when not supplied (line 326–331); hardcoded `name = "acdl-microservice-role"` default. |
|
||
| 9 | `aws:elbv2:loadbalancer` | `aws_lb` | `subnets`/`security_group` wrapped in list brackets; hardcoded `load_balancer_type = "application"` default. |
|
||
| 10 | `aws:elbv2:listener` | `aws_lb_listener` | `default_action { type = "forward" target_group_arn = aws_lb_target_group.alb-targetgroup.arn }` hardcoded; `load_balancer_arn = aws_lb.alb-loadbalancer.id` hardcoded ref. |
|
||
| 11 | `aws:elbv2:targetgroup` | `aws_lb_target_group` | Hardcoded `target_type = "ip"`, `vpc_id = aws_vpc.vpc-vpc.id`, `protocol = "HTTP"`, `port = 8080`. |
|
||
| 12 | `aws:ecr:repository` | `aws_ecr_repository` | Hardcoded `name = "acdl-microservice"` default; `encryption_configuration {}` block (not a bare `kms_key_arn` arg). |
|
||
| 13 | `aws:cloudfront:distribution` | `aws_cloudfront_distribution` | `origin {}` block (origin_id, domain_name, origin_access_control_id, `s3_origin_config {}`); `default_cache_behavior {}` block (viewer_protocol_policy, target_origin_id, ttls, allowed/cached methods); `enabled = true`; `price_class`; `restrictions { geo_restriction {} }`; `viewer_certificate { cloudfront_default_certificate = true }`; `web_acl_id` from WAF ref. ~8 nested blocks. |
|
||
| 14 | `aws:cloudfront:originaccesscontrol` | `aws_cloudfront_origin_access_control` | `name`; hardcoded `origin_access_control_origin_type = "s3"`, `signing_behavior = "always"`, `signing_protocol = "sigv4"`. |
|
||
| 15 | `aws:wafv2:webacl` | `aws_wafv2_web_acl` | `name`; hardcoded `scope = "CLOUDFRONT"`; `default_action {}` (allow/block from input, default allow); `visibility_config {}`; custom `rule {}` blocks as nested HCL (P1-4 fix) or default AWS-managed-rules block. ~5 nested blocks. |
|
||
| 16 | `aws:rds:instance` | `aws_db_instance` | NFR-derived `backup_retention_period` (default 7), `deletion_protection` (default true); `storage_encrypted = true` default; `skip_final_snapshot = true` (dev safety). |
|
||
| 17 | `aws:kms:key` | `aws_kms_key` | NFR-derived `enable_key_rotation = true` default. |
|
||
| 18 | `aws:ecs:uptime-service` | `aws_ecs_service` | Feature-flag gate (returns `""` when disabled); `container_definitions` jsonencode for uptime-kuma; hardcoded `subnets = ["subnet-uptime"]`, `security_groups = ["sg-uptime"]`, `assign_public_ip = true`; hardcoded `desired_count = 1`, `launch_type = "FARGATE"`. |
|
||
|
||
Plus a global `prevent_destroy` lifecycle block emitted for every resource
|
||
when `nfrs.deletion_protection` is true (line 576–581), and the
|
||
`_emit_igw()` helper that synthesizes an internet gateway + route table
|
||
association from the VPC resource (line 585).
|
||
|
||
### 1.3 Hardcoded defaults that belong in the module
|
||
|
||
The defaults below are emitted by the adapter when the L2 composition does
|
||
not supply the input. They are *resource shape* decisions — CIDR ranges,
|
||
trust policies, network config — that belong in the module's `locals.tf`
|
||
(D-100), not in the adapter. The adapter should pass only resolved contract
|
||
inputs; if a default is wrong, fix the module, not the adapter.
|
||
|
||
| Default | Adapter line | What it is | Where it belongs |
|
||
|---------|-------------|------------|------------------|
|
||
| `cidr_block = "10.0.0.0/16"` | 288 | VPC CIDR default | `modules/l1/vpc/terraform/locals.tf` |
|
||
| `cidr_block = "10.0.1.0/24"` | 296 | Subnet CIDR default | `modules/l1/vpc/terraform/locals.tf` |
|
||
| ECS task execution `assume_role_policy` JSON | 326–331 | Trust policy for the IAM role | `modules/l1/iam-role/terraform/main.tf` (or `locals.tf`) |
|
||
| ECR/logs inline policy / `encryption_configuration {}` | 304–315, 380 | ECR KMS encryption block | `modules/l1/ecr/terraform/main.tf` |
|
||
| Fargate `requires_compatibilities` / `launch_type = "FARGATE"` | 261–263 | ECS launch config | `modules/l1/ecs-service/terraform/locals.tf` |
|
||
| `assign_public_ip` (uptime) | 565 | ECS network config | `modules/l1/uptime/terraform/main.tf` |
|
||
| Listener/target ports (`port = 8080`, `container_port = 8080`) | 201, 348 | ALB + ECS container ports | `modules/l1/alb/terraform/locals.tf` + `modules/l1/ecs-service/terraform/locals.tf` |
|
||
| Security group emission (`security_groups = [...]`) | 255–258, 564 | ECS network config | `modules/l1/ecs-service/terraform/main.tf` |
|
||
| `name = "acdl-microservice"` (cluster, ECR, service) | 265, 300, 303 | Resource name defaults | `modules/l1/*/terraform/locals.tf` |
|
||
| `family = "app"` | 279 | Task definition family | `modules/l1/ecs-service/terraform/locals.tf` |
|
||
| `target_type = "ip"`, `protocol = "HTTP"` | 345, 347 | ALB target group defaults | `modules/l1/alb/terraform/locals.tf` |
|
||
| `load_balancer_type = "application"` | 342 | ALB type default | `modules/l1/alb/terraform/locals.tf` |
|
||
| `desired_count = 1` | 260 | ECS desired count | `modules/l1/ecs-service/terraform/locals.tf` |
|
||
| WAF `scope = "CLOUDFRONT"`, managed-rules default block | 421, 472–490 | WAF defaults | `modules/l1/waf/terraform/main.tf` |
|
||
| CloudFront `signing_behavior = "always"`, `signing_protocol = "sigv4"`, `origin_type = "s3"` | 365–367 | OAC defaults | `modules/l1/cloudfront/terraform/main.tf` |
|
||
| CloudFront `viewer_certificate { cloudfront_default_certificate = true }`, `restrictions {}` | 403–410 | Distribution defaults | `modules/l1/cloudfront/terraform/main.tf` |
|
||
| RDS `backup_retention_period = 7`, `skip_final_snapshot = true` | 497, 506 | RDS defaults | `modules/l1/rds/terraform/locals.tf` |
|
||
| KMS `enable_key_rotation = true` | 510 | KMS rotation default | `modules/l1/kms-key/terraform/main.tf` |
|
||
| `prevent_destroy = true` lifecycle (global) | 576–581 | Deletion protection | Each module's `main.tf` (or a shared `lifecycle.tf`) |
|
||
|
||
### 1.4 Why this is a drift from the original vision
|
||
|
||
ARCHITECTURE.md §12.2 states: *"The adapter is a thin layer; it does not
|
||
own L1/L2 content — it only translates."* STANDARDS.md §8 (line 448–506)
|
||
documents the intended design: "a thin translator with 3 tables +
|
||
specialized branches." The drift was **baked into the standards doc
|
||
itself** — §8.2 explicitly blesses "specialized `_emit_resource` branches"
|
||
for "resources with nested HCL blocks" and §8.3 step 4 instructs module
|
||
authors to "add a specialized branch in `_emit_resource` keyed on that
|
||
stack type" when a new L1 needs nested blocks.
|
||
|
||
The result: every new L1 with a nested block (CloudFront, WAF, ECS,
|
||
uptime) added 30–80 lines of resource-shape code to the adapter. The
|
||
adapter grew from a spike-era ~150 lines to 750 lines, with the resource
|
||
shape (CIDR ranges, trust policies, container ports, managed-rule sets)
|
||
encoded as Python string concatenation rather than Terraform HCL. D-098
|
||
corrects the drift: the standards doc §8 must be rewritten to document the
|
||
new pattern (per-module `terraform/` subdir + stateless assembler), and
|
||
the "specialized branch" guidance is removed.
|
||
|
||
**Confidence: 0.95.** The audit is a direct line-by-line read of the
|
||
v1.10.2 `adapter.py`; the drift is structural and unambiguous.
|
||
|
||
---
|
||
|
||
## FINDING 2 — State-key root cause of the 4-VPC bug
|
||
|
||
### 2.1 The state key
|
||
|
||
`adapter.py` line 664 + 676:
|
||
|
||
```python
|
||
stack_name = stack.get("name", "spike")
|
||
terraform_tf = (
|
||
...
|
||
f' key = "spike/{stack_name}/terraform.tfstate"\n'
|
||
...
|
||
)
|
||
```
|
||
|
||
`core/contract_resolver.py` line 569:
|
||
|
||
```python
|
||
"stack": {
|
||
"name": contract["id"],
|
||
...
|
||
}
|
||
```
|
||
|
||
So `stack_name = contract["id"]` and the state key is
|
||
`spike/{contract.id}/terraform.tfstate`.
|
||
|
||
### 2.2 The 5 microservice contracts
|
||
|
||
All five microservice contracts share `id: msvc` and differ only in
|
||
`environment`:
|
||
|
||
| Contract file | `id` | `environment` |
|
||
|---------------|------|----------------|
|
||
| `contracts/microservice.yml` | `msvc` | `dev` |
|
||
| `contracts/microservice.dev.yml` | `msvc` | `dev` |
|
||
| `contracts/microservice.qa.yml` | `msvc` | `qa` |
|
||
| `contracts/microservice.prod.yml` | `msvc` | `prod` |
|
||
| `contracts/microservice.dr.yml` | `msvc` | `dr` |
|
||
|
||
The state key does **not** include the environment. So all four
|
||
environment contracts (dev/qa/prod/dr) collide on the same state key:
|
||
`spike/msvc/terraform.tfstate`.
|
||
|
||
### 2.3 The two root causes
|
||
|
||
**Root cause 1 — the adapter emits per-contract state keys with no VPC
|
||
sharing.** The `microservice` composition (`modules/l2/microservice/
|
||
composition.json`) includes a `vpc` child (`vpc@1.0.0`). Every contract
|
||
that resolves through this composition emits its own VPC resource. There
|
||
is no platform VPC to share; each contract deploys its own VPC. D-105
|
||
corrects this: `terraform/platform` owns ONE VPC; the microservice
|
||
composition drops its `vpc` child and references the platform VPC via a
|
||
data source. The standalone `vpc` L1 module stays (consumers deploy their
|
||
own VPCs). No per-contract VPC ever again.
|
||
|
||
**Root cause 2 — the state key does not distinguish environments.** Because
|
||
the state key is `spike/{contract.id}/terraform.tfstate` and all four env
|
||
contracts share `id: msvc`, every environment's `terraform apply` writes to
|
||
the same remote state key. Combined with the first attempt's
|
||
`verify_deploy_microservice.py` running `terraform init -reconfigure` in a
|
||
**fresh temp dir each time**, each run created a fresh local state that
|
||
diverged from the remote key. The first run (dev) created VPC #1 and
|
||
pushed it to `spike/msvc/terraform.tfstate`. The second run (qa) ran
|
||
`-reconfigure` in a fresh temp dir, pulled the remote state (which had
|
||
dev's VPC), but because the local state was fresh and the composition
|
||
emitted a *new* VPC resource address, terraform saw the VPC as "to add"
|
||
again — creating VPC #2 and overwriting the remote state. Repeating for
|
||
prod and dr created VPCs #3 and #4. Four VPCs, one state key, no
|
||
environment discrimination.
|
||
|
||
D-106 corrects this: the composition must be deterministic — same contract
|
||
→ same resolved stack → same state key, every time. State keys become
|
||
**env-aware and stable** across apply/modify/destroy:
|
||
`spike/{id}/{env}/terraform.tfstate`. The environment is part of the key,
|
||
so dev/qa/prod/dr never collide.
|
||
|
||
### 2.4 Why `-reconfigure` in a fresh temp dir made it worse
|
||
|
||
`terraform init -reconfigure` forces terraform to re-read the backend
|
||
config and pull remote state into the local working directory. When the
|
||
working directory is a fresh temp dir (as `verify_deploy_microservice.py`
|
||
did), there is no local `.terraform/` state cache — terraform must pull
|
||
the remote state fresh. If the remote state key is shared across
|
||
environments (root cause 2) and the composition emits a new VPC each time
|
||
(root cause 1), the `-reconfigure` pull merges the prior environment's
|
||
state with the new resource addresses, and the subsequent `apply` creates a
|
||
new VPC because the resource address in the *new* composition run differs
|
||
from the one in the remote state (the L2 namespacing or the fresh temp dir
|
||
caused terraform to treat the VPC as a new resource). D-101 deletes
|
||
`verify_deploy_microservice.py` entirely; `run_platform.sh` gains
|
||
`--apply` and `--destroy` modes that run terraform in a stable working
|
||
directory (not a fresh temp dir per run), and Python never runs terraform.
|
||
|
||
**Confidence: 0.90.** The state-key derivation is a direct code read
|
||
(adapter.py:664,676 + contract_resolver.py:569). The 5 contracts are read
|
||
verbatim. The 4-VPC mechanism is the only consistent explanation for the
|
||
observed symptom (4 VPCs in the account after 4 env runs). The 0.10
|
||
residual is for the possibility that the resource-address divergence was
|
||
caused by a separate composition-namespacing bug rather than the fresh
|
||
temp dir alone — but either way, the two root causes (shared state key +
|
||
per-contract VPC) are confirmed and D-105/D-106 correct both.
|
||
|
||
---
|
||
|
||
## FINDING 3 — Per-module terraform module design
|
||
|
||
### 3.1 What the per-module `terraform/` subdir should contain
|
||
|
||
D-098/D-099: each L1 module ships a real `terraform/` module dir. The
|
||
canonical layout for a multi-resource module:
|
||
|
||
```
|
||
modules/l1/<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 274–297).
|
||
- default (requires AWS + Checkov + DynamoDB): contract → resolver →
|
||
adapter → `terraform plan` → Checkov → confidence → outbox.
|
||
|
||
**Critically, line 287 runs `terraform plan` only.** There is no
|
||
`terraform apply` and no `terraform destroy` in `run_platform.sh` today.
|
||
The `apply` stage in `pipelines/contract.yml` (line 54–57) declares
|
||
`command: bash scripts/run_platform.sh --plan-only` — a misnomer; it runs
|
||
plan, not apply. The lifecycle modes (`--apply`, `--destroy`) **must be
|
||
added** (D-101). Python never runs terraform; `run_platform.sh` is the
|
||
only shell entry point.
|
||
|
||
### 4.4 `run_primitive_plan.sh`
|
||
|
||
`scripts/run_primitive_plan.sh` (65 lines) runs the platform pipeline for
|
||
a single primitive. `--check-only` mode: resolves `instance.json`, runs
|
||
the adapter, validates the emitted `{main.tf,terraform.tf,providers.tf}`
|
||
exist and `main.tf` is non-empty. Default mode (requires AWS): `terraform
|
||
init -backend=false` → `terraform validate` → `terraform plan`. This is
|
||
the per-primitive plan check that the primitives-plan pipeline matrix
|
||
invokes.
|
||
|
||
### 4.5 The byte-identical Gitea+GitHub convention
|
||
|
||
`pipelines/README.md:22` documents the convention: "Create byte-identical
|
||
workflow YAMLs in `.gitea/workflows/<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 (P56a–P65) landed the stateless adapter, pipeline-driven
|
||
lifecycle testing, single platform VPC, `COST.md`, `PRE_MORTEM.md`, and
|
||
a teardown to zero-cost. P65's plan (REQ-118) required the decks to be
|
||
rewritten to "Verified live-aws via lifecycle pipeline; torn down to
|
||
zero-cost." That rewrite did not fully land on the deck artifacts. This
|
||
research is a drift audit: a systematic comparison of the deck artifacts
|
||
against the v1.11-verified reality.
|
||
|
||
## FINDING 1 — Drift audit (9 items)
|
||
|
||
Systematic comparison of `docs/presentations/*` against
|
||
`.ciagent/CAPABILITY_INVENTORY.md`, `.ciagent/COST.md`,
|
||
`.ciagent/PRE_MORTEM.md`, `.ciagent/ROADMAP.md`, and `git log`.
|
||
|
||
1. **Wrong verification status.** Both rendered HTML decks still say
|
||
"6 cloud capabilities are design-verified + locally emulated,
|
||
deploy-unverified (IAM drift)" (PW "Testing vs. Planned" slide;
|
||
DX slide A6). `CAPABILITY_INVENTORY.md` says 22/22 Verified and the
|
||
IAM-drift framing was *removed* in P65. The decks contradict the
|
||
inventory. Verified: `grep -c "deploy-unverified\|IAM drift\|design-verified"
|
||
docs/presentations/*.html` → 3 hits per deck.
|
||
2. **Re-verification header stale.** Both source `.md` headers say
|
||
"Re-verification (2026-07-27)… v1.10 Phase 54… 16/16… 6 IAM-gated
|
||
escalated." Should reflect v1.11: 22/22 Verified, torn down.
|
||
3. **Road to the North Star diagram stale.**
|
||
`docs/presentations/assets/mmd/road-to-north-star.mmd` shows v1.10 as
|
||
"NEXT" with "HITL wiring / all-runner OIDC / regulatory ledger". v1.11
|
||
is complete; the diagram must advance.
|
||
4. **Rendered HTML not re-rendered.** `git log` shows the HTML was last
|
||
touched at `10b87a6` (P57), *before* v1.11. P65's "re-render HTML"
|
||
task did not reach the rendered artifacts.
|
||
5. **Version refs stale.** Decks reference `@v1.10` in deploy.yml `uses:`
|
||
snippets (Safe Promotion Path, Safe Decommission). Ship tag is now
|
||
`v1.11.0`; will be `v1.12.0` at Phase 70 complete.
|
||
6. **Cost story has no real numbers.** `COST.md` exists ($0.001883 over
|
||
8 days, ~$0.007/mo, S3-dominated, zero BAU compute) but the decks' A6
|
||
"Operating Model & Cost" slide is generic prose with no figures.
|
||
7. **Pre-mortem unreferenced.** P65 planned to add a pre-mortem
|
||
reference; `PRE_MORTEM.md` exists (v1.10 decay root cause + four
|
||
forward failure modes) but no deck slide references it.
|
||
8. **Two v1.11 stories absent.** (a) Architectural simplicity: adapter
|
||
918→~80 lines, defaults centralized in per-module `terraform/` dirs.
|
||
(b) Verifiable deploys: a `modules-lifecycle` pipeline matrix-runs
|
||
each module apply→modify→destroy against live AWS. Neither is in the
|
||
decks.
|
||
9. **Duplicated story-beat lines.** `how-the-platform-works.md` slides
|
||
3–10 each repeat their intro line twice (a copy-paste artifact).
|
||
|
||
## FINDING 2 — Regression gate surfaces real decay (D-091)
|
||
|
||
The v1.12 regression gate run (Phase 66) re-ran the D-091 regression
|
||
gate to back every deck claim. It found **3 Broken capabilities**:
|
||
`{'Verified': 19, 'Decayed': 0, 'Broken': 3}`.
|
||
|
||
### CAP-013 — live-aws — REAL platform defect (Class A)
|
||
|
||
`adapters/terraform/adapter.py:159-172` (the `seen` dedup loop)
|
||
collapses the two `ecs-service` sub-resources (`service-task-definition`
|
||
+ `service-service`, both module `ecs-service@1.0.0`) into ONE
|
||
`module "service-task-definition"` block. But the stack output
|
||
`service_arn` (resolver `from: "service-service"`) is emitted as
|
||
`value = module.service-service.service_arn` — referencing a module
|
||
call that was never emitted. `terraform validate` fails: "No module
|
||
call name." The same defect silently breaks the `alb` L1 too. Static-
|
||
assets (CAP-014) doesn't hit it because its L1s are single-resource.
|
||
**Classification A — real platform defect.** The adapter produces
|
||
invalid Terraform for any multi-resource L1 with stack-level outputs.
|
||
**Fix required before decks can claim 22/22 Verified.**
|
||
|
||
### CAP-017 — lifecycle-pipeline — regression-probe bug (Class B/C)
|
||
|
||
`core/regression_verify.py:444` hardcodes
|
||
`required = ["versions.tf", "variables.tf", "locals.tf", "main.tf",
|
||
"outputs.tf"]`. The CAP-017 probe targets the `rds` L1 module, whose
|
||
`main.tf` uses only `var.*` and `aws_db_subnet_group.this` — no `local.*`
|
||
references, so `locals.tf` is legitimately absent. The probe is over-
|
||
strict. The rds module is correctly structured; the capability works.
|
||
**Classification B/C — trivial probe fix.** Drop `locals.tf` from the
|
||
required list, or make it conditional on `local.` usage.
|
||
|
||
### CAP-018 — lifecycle-pipeline — regression-probe bug (Class B/C)
|
||
|
||
`core/local_emulators.py:273` defines `LocalLambdaStub` as a dataclass
|
||
with one required field `outbox: FlatFileOutbox`. Every real caller
|
||
passes it (`core/local_emulators.py:464`, the tests). The CAP-018 probe
|
||
at `core/regression_verify.py:486-491` is the *only* caller that
|
||
instantiates it bare: `LocalLambdaStub()` → `TypeError`. The probe was
|
||
added in P63 and never aligned with the real signature. The capability
|
||
is exercised green by CAP-011. **Classification B/C — trivial probe
|
||
fix.** Pass an `outbox` to the constructor.
|
||
|
||
### Implication for the decks
|
||
|
||
`CAPABILITY_INVENTORY.md` claims 22/22 Verified, but the regression
|
||
gate (D-091 — the exact mechanism PRE_MORTEM.md FM-3 says backs every
|
||
deck claim) shows CAP-013 is genuinely broken. **The inventory
|
||
overstates.** v1.12 cannot ship decks claiming 22/22 until CAP-013 is
|
||
fixed and the gate re-runs clean. This is the structural mitigation the
|
||
pre-mortem requires (verified-only claims; decks unfrozen only after
|
||
re-verification). The user decision: fix the defect inside v1.12
|
||
(Phase 67), then the decks can honestly claim 22/22.
|
||
|
||
## FINDING 3 — Talking points structure gap
|
||
|
||
Both talking-points files have only 5 appendix sections (A1–A5) while
|
||
the Marp decks have 6 (A6 = "Operating Model & Cost"). The A6 content
|
||
exists in the Marp deck and source markdown but was never distilled
|
||
into the talking points. The re-distill step (Phase 69) must add the
|
||
A6 section to both talking-points files.
|
||
|
||
## FINDING 4 — Versioning facts
|
||
|
||
- Current ship tag: `v1.11.0` (v1.11 complete).
|
||
- `deploy.yml` still references `v1.9` in comments + `ref: v1.9` —
|
||
v1.11 apparently did not bump the deploy workflow `uses:` tag (the
|
||
bump is a separate concern; decks use the current ship tag).
|
||
- Decks should show `@v1.11` in examples (current state); Phase 70
|
||
bumps to `@v1.12` after the tag exists.
|
||
|
||
## Assumptions logged
|
||
|
||
- No automated `ci-doc-verifier` script exists in the repo. The plan's
|
||
"ci-doc-verifier confirms" is satisfied by a manual grep-based
|
||
verification recorded in the Phase 70 VERIFY step (consistent with how
|
||
prior NFR-patch phases handled it). Confidence 0.90 — verified by
|
||
`ls scripts/ | grep doc` and `grep -rl deck tests/`.
|
||
- PPTX export requires Chromium + Marp CLI; the environment has it
|
||
(`/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome`).
|
||
PPTX is uploaded to the Gitea release, not committed. Confidence
|
||
0.85 — README documents the path; the chromium binary exists.
|
||
|
||
## Decisions surfaced (research → bound in CLARIFY-equivalent)
|
||
|
||
- **D-108** — v1.12 includes one real adapter fix (CAP-013) and two
|
||
probe fixes (CAP-017, CAP-018) as Phase 67 prerequisites, so the decks
|
||
can honestly claim 22/22 Verified. The milestone is "presentation
|
||
refinement" but the verified-only-claims pre-mortem mitigation makes
|
||
the fixes mandatory. The user confirmed this scope (interactive
|
||
decision, 2026-07-29).
|
||
- **D-109** — Decks use `@v1.11` in examples during Phase 68 (current
|
||
state), bumped to `@v1.12` at Phase 70 complete after the tag exists.
|
||
Avoids a dangling reference to a tag that doesn't exist yet. |