# Nova v1.11 — Multi-Persona Code Review (P60–P65 retrofit + new work)

**Reviewer:** ci-code-reviewer (model: glm-5.2)
**Scope:** v1.11 milestone, branch `milestone/v1.11-restart` — 22 commits
(e1bb214..8c09580), 25 files, +790/-142 lines
**Date:** 2026-07-29

## Commits reviewed

| Commit | Phase | Type | Summary |
|--------|-------|------|---------|
| e1bb214 | 60 | docs | retrofit plan — L1 lifecycle pipeline live-run |
| bc9058f | 60 | feat | L1 module lifecycle live run — module fixes (retrofit) |
| bb3ac7c | 60 | fix | WAF scope case + VPC modify DependencyViolation |
| 0c5c4d1 | 61 | docs | create phase plan — L2 lifecycle pipeline author |
| 361fe60 | 61 | feat | L2 lifecycle pipeline — extend matrix + workflows + tests |
| 9ac5720 | 61 | verify | 4-layer gate — PASS |
| 6441633 | 62 | docs | create phase plan — L2 lifecycle pipeline live run |
| 4dad967 | 60 | fix | ALB target group name_prefix — avoid orphaned conflicts |
| adfcf86 | 63 | docs | create phase plan — regression registry + cost docs |
| b71e63c | 63 | feat | CAP-017..022 regression registry + COST.md |
| beac2ef | 63 | verify | 4-layer gate — PASS |
| 06f4fc7 | 60 | fix | free disk space in lifecycle jobs |
| 92bb03e | 64 | docs | create phase plan — pre-mortem + teardown |
| 186cdde | 64 | feat | pre-mortem — v1.10 post-mortem + forward pre-mortem |
| 4102950 | 64 | feat | pre-mortem + teardown plan — HITL escalation CHG0680001 |
| 7c4fc1f | 64 | feat | teardown complete — zero live ACDL resources remain |
| a52f8a5 | 64 | verify | 4-layer gate — PASS |
| a03c019 | 60/62 | fix | ALB name_prefix + adapter dedup + L2 composition wiring |
| 93a6598 | 65 | docs | create phase plan — rewrite caps + decks |
| 6394801 | 65 | feat | rewrite caps — CAP-017..022 Verified via lifecycle pipeline |
| fc91f24 | 65 | verify | 4-layer gate — PASS |
| 8c09580 | 65 | docs | update v1.11 status — all phases complete |

## P0 issues (0)

No blocking issues found. The targeted fixes are correct for their stated
purposes. The 447 fast offline tests pass (485/490 collected; 5 slow
deselected, including 2 slow regression-integration tests that exercise the
CAPABILITY_REGISTRY against the live codebase).

## P1 issues (5 — should fix)

### P1-1: Adapter dedup silently drops resources whose module is not in the registry
[correctness] `adapters/terraform/adapter.py:159-170`

The new dedup loop only adds resources to `seen` when `tf_dir` is truthy
(in the registry). A resource whose module is missing from the registry is
**silently dropped** from `merged` — it never reaches `_emit_module_block`,
so no error is raised. The pre-dedup code (`parts.extend(... for r in
resources)`) would have raised `ValueError("no terraform_dir in registry
for module ...")` via `_emit_module_block`, surfacing the misconfiguration.

Confirmed by simulation: two resources, one with `module: nonexistent@1.0.0`,
produces a `merged` list of length 1 — the unknown-module resource vanishes
without diagnostic.

**Recommendation:** in the dedup loop, when `tf_dir` is `None`, either
(a) raise immediately (preserving the prior contract), or (b) append the
resource to a separate `unknown` list and extend `parts` with it so
`_emit_module_block` raises the descriptive error. As written, a typo in
a composition's `module` field (e.g. `iam-role@1.0.0` vs `iam_roles@1.0.0`)
will silently omit a resource from the emitted terraform — a class of
defect the v1.10 sweep was specifically created to catch.

### P1-2: L2 static-assets "modify" example is a no-op — complex ≡ simple
[correctness] `modules/l2/static-assets/examples/complex.yml`,
`modules/l2/static-assets/composition.json`

The complex.yml comment claims "Modify variant: same bucket_name as simple
(in-place modify, adds CDN + WAF)". But resolving both examples yields
**identical** resource sets: `['s3','cloudfront-distribution',
'cloudfront-originaccesscontrol','waf','kms']`. The CDN and WAF are
**always present** in the static-assets composition (they are unconditional
children + wires); the `waf_enabled`, `default_ttl`, `max_ttl`,
`price_class`, `viewer_protocol_policy` inputs in complex.yml have **no
corresponding wires** in composition.json and are silently dropped at
resolve time. So the L2 static-assets lifecycle cell's "modify" step
applies a contract that produces the same terraform as "simple" — it
exercises `terraform apply` twice with no change, not a true modify.

This is not a regression (the inputs were never wired), but the
CAPABILITY_INVENTORY claim "CAP-020 Verified live-aws via L2 static-assets
lifecycle pipeline (apply/modify/destroy exit 0)" overstates what the
modify step proves: it proves idempotent re-apply, not in-place modify.

**Recommendation:** either (a) wire `waf_enabled`/`default_ttl`/etc. in
composition.json so the complex contract genuinely differs, or (b) correct
the comment + CAPABILITY_INVENTORY wording to "apply + idempotent re-apply
+ destroy" rather than "apply/modify/destroy". The microservice complex
example, by contrast, is a real modify (desired_count 1→2) — that one is
fine.

### P1-3: L2 lifecycle scripts ignore the ci-vpc-outputs.json argument
[correctness] `scripts/run_l2_lifecycle_test.sh:14`,
`scripts/run_l2_lifecycle_destroy.sh:12`

Both L2 scripts declare `Usage: ... <module> <example> [ci-vpc-outputs.json]`
but neither reads `$3`/`$2`. The microservice composition references the
platform VPC via `terraform_remote_state` (data source), and the script
sets `ACDL_REMOTE_STATE_KEY=spike/ci-vpc/terraform.tfstate` so the data
source reads from the CI VPC state — that part is correct. But the
`ci-vpc-outputs.json` argument is positional noise: the workflow passes
it (`run_l2_lifecycle_test.sh ${{ matrix.module }} simple
/tmp/ci-vpc-outputs.json`) and it is silently ignored. The L1 scripts
(`run_lifecycle_test.sh`) inject VPC outputs by rewriting the contract in
Python; the L2 path takes a different approach (remote state) and does not
need the file, so the argument is vestigial, not a bug — but the usage
string advertises a feature the script does not provide, which will
confuse a future maintainer who assumes parity with the L1 scripts.

**Recommendation:** remove the `[ci-vpc-outputs.json]` token from the
usage strings (or add a comment explaining the L2 path uses remote state
and the arg is accepted-but-ignored for workflow-argument parity).

### P1-4: CAPABILITY_INVENTORY summary table is stale (says 16, body lists 22)
[maintainability] `.ciagent/CAPABILITY_INVENTORY.md:9-16`

The Summary table still reads "Verified 16 / Decayed 0 / Broken 0 / Total
16" — the v1.10 sweep count. The body (lines 93-110) now lists CAP-017..022
as **Verified** via the lifecycle pipeline, bringing the real total to 22.
The two counts disagree: a reader scanning the summary sees 16 Verified; a
reader scanning the inventory body sees 22 Verified. The PRE_MORTEM
(lines 82-83) and CAPABILITY_INVENTORY prose both assert all 22 are
Verified, but the headline table was not updated in the P65 rewrite.

**Recommendation:** update the Summary table to "Verified 22 / Decayed 0
/ Broken 0 / Total 22" and add CAP-017..022 rows to the Inventory table
(the body section "Cloud capabilities NOT re-verified..." is now
mis-titled — they ARE verified, just via the lifecycle-pipeline tier).

### P1-5: CAP-017..022 regression checks are offline proxies, not pipeline evidence
[adversarial] `core/regression_verify.py:432-519`,
`.ciagent/CAPABILITY_INVENTORY.md:93-110`

The CAP-017..022 checks (`_check_cap_017_dynamodb` etc.) call
`_check_lifecycle_module_terraform` / `_check_lifecycle_l2_module`, which
verify only that (a) the terraform dir + required files exist and (b) the
example contracts **resolve** (resolver exit 0). They do **not** run
`terraform validate`, do not run apply/modify/destroy, and do not query
the pipeline's actual green/red status. The CAPABILITY_INVENTORY claims
"Evidence = L1 rds module lifecycle pipeline green (terraform validate +
contracts resolve)" — but the check does not run terraform validate, and
"lifecycle pipeline green" is asserted, not verified by the regression
gate.

This means the lifecycle-pipeline evidence CAN be faked at the regression
tier: a module whose terraform is syntactically broken (e.g.
`scope = upper(var.scope)` removed, or a missing required variable) would
still pass `_check_lifecycle_module_terraform` as long as the files exist
and the resolver runs. The real green/red evidence lives only in the
workflow run history (Gitea/GitHub Actions), which the regression gate does
not read.

**Mitigation context:** the modules-lifecycle workflow IS the live
evidence — when it runs on a PR, the cells genuinely apply/modify/destroy
against live AWS. The gap is that the *regression gate* (which gates
milestone COMPLETE) trusts the workflow will be run, rather than proving it
was run and passed. A milestone could in principle be marked COMPLETE with
CAP-017..022 "Verified" if the regression gate runs but the workflow was
never executed (e.g. workflow_dispatch never triggered, or the PR was
merged without the workflow running).

**Recommendation:** (a) tighten the CAP-017..022 check docstrings + the
CAPABILITY_INVENTORY wording to "terraform files present + contracts
resolve (offline proxy; live apply/modify/destroy verified by the
modules-lifecycle workflow run, not by this gate)"; and/or (b) add a
`terraform validate` step to `_check_lifecycle_module_terraform` (slow but
cheap relative to init+apply) so at least HCL syntax is verified at the
gate. The teardown trustworthiness (P64) is good — `ci-vpc-destroy` runs
`if: always()` and the decommission `---ci---` block is the audit trail.

## P2 issues (4 — post-hoc)

### P2-1: ALB `name_prefix = "tg-ci-"` discards `var.name` entirely
[maintainability] `modules/l1/alb/terraform/main.tf:9`

The fix replaces `name = var.name` with `name_prefix = "tg-ci-"` (a
hardcoded literal). This is the correct terraform pattern for
create_before_destroy resources with name-uniqueness constraints, and the
commit message explains the orphaned-resource motivation well. However
the target group name is now non-configurable (always `tg-ci-<random>`),
and the `var.name` variable is no longer used by the target group at all
(it is still used by `aws_lb.this.name`). A consumer who sets `name:
my-app` gets an LB named `my-app` but a target group named `tg-ci-...` —
inconsistent tagging. Consider `name_prefix = "${var.name}-"` to keep the
consumer's name as a prefix while preserving uniqueness. Post-hoc: not
blocking; the lifecycle pipeline is the only current consumer and `tg-ci-`
is fine for CI.

### P2-2: No test covers the new dedup merge behavior or `ACDL_REMOTE_STATE_KEY`
[testing] `tests/test_adapter.py`, `tests/test_pipeline_contract.py`

The adapter gained (a) a dedup-merge loop for multi-resource L1s sharing a
terraform dir and (b) `ACDL_REMOTE_STATE_KEY` env override for the remote
state data block. Neither has a unit test:
- No test asserts that two resources with the same `module` collapse to one
  `module "<first_id>" { ... }` block with merged inputs.
- No test asserts that `ACDL_REMOTE_STATE_KEY` overrides the default
  `platform/terraform.tfstate` key in the emitted `data
  terraform_remote_state` block.
- No test covers the L2 lifecycle scripts (`run_l2_lifecycle_test.sh` /
  `run_l2_lifecycle_destroy.sh`) — the L1 equivalents are also untested at
  the script level, so this is consistent with existing practice, but the
  L2 scripts are new in this session and the `ACDL_REMOTE_STATE_KEY` wiring
  is the load-bearing correctness mechanism for the microservice lifecycle.

The 485 offline tests adequately cover the *contract* (pipeline schema,
byte-identical workflows, matrix membership, job needs) — the
`TestModulesLifecyclePipeline` class is solid (89 tests pass). The gap is
adapter *behavior* at the unit level.

**Recommendation:** add a `test_adapter_dedup_merges_same_module` and a
`test_adapter_remote_state_key_override` to `tests/test_adapter.py`.

### P2-3: `waf` complex example uses `scope: CLOUDFRONT` but WAF scope is now `upper()`'d
[correctness] `modules/l1/waf/examples/complex.yml:8`,
`modules/l1/waf/terraform/locals.tf:3`

The `locals.tf` change `scope = upper(var.scope)` is the correct defensive
fix (the AWS provider requires `CLOUDFRONT`/`REGIONAL` regardless of input
case). The complex.yml was simultaneously changed from `scope: cloudfront`
to `scope: CLOUDFRONT`. Both are now correct, but the example's uppercase
value is now redundant with the `upper()` — a future reader may wonder
which is authoritative. Minor; the defensive `upper()` is the right call
and the example matching it is fine. Post-hoc only.

### P2-4: COST.md reproducibility snippet could leak the account ID via CloudTrail
[security] `.ciagent/COST.md:106`

COST.md contains the AWS account ID `581513795199` in multiple places
(summary, S3 bucket name, methodology). This is consistent with the rest of
the repo (the bucket name `acdl-tfstate-581513795199-us-east-1` is hardcoded
in `adapter.py:130` and `adapter.py:146`), so it is not new leakage and not
a regression. No actual secret material (access keys, secret access keys)
appears in COST.md, PRE_MORTEM.md, CAPABILITY_INVENTORY.md, or the workflow
files — all credential references use `${{ secrets.ACDL_AWS_* }}` or env
var names only. The `.ciagent/PROJECT.md:731` reference to a deactivated
root key is redacted (`AKIA…ROOT-DEACTIVATED`). **No credential leakage
found.** The P2 is only that the account ID is published; if the account
is meant to be opaque, this is an accepted exposure (the bucket name
already requires it).

## What is correct

- **WAF scope fix (`upper(var.scope)`):** correct and defensive; AWS
  provider v5 requires uppercase. The `local.scope` indirection is clean.
- **VPC `create_before_destroy` + same-CIDR complex example:** correct
  fix for the DependencyViolation on modify. Using the same CIDR means
  terraform modifies in-place rather than replacing the VPC (which would
  cascade-fail on dependent subnets/IGW). The `create_before_destroy`
  lifecycle is the right guard.
- **ALB `name_prefix`:** correct terraform pattern for
  create_before_destroy + name-uniqueness; well-documented commit message.
- **Adapter dedup (for the registered-module case):** correct —
  multi-resource L1s like cloudfront (distribution + OAC) correctly merge
  into one `module "cloudfront-distribution" { ... }` block. The merge
  preserves first-resource inputs and union of outputs. (The
  unregistered-module drop is P1-1, a separate concern.)
- **L2 composition wiring (`ecr.inputs.name`, `roles.inputs.role_name`):**
  correct. Resolving microservice complex now shows `ecr.inputs.name =
  "app-repo"` and `roles.inputs.role_name = "app-role"` (defaults applied
  since the contract doesn't set `name`). Previously these would have hit
  the "missing required arg" defect class from the v1.10 sweep.
- **Microservice complex = real modify:** `desired_count: 2` (vs simple's
  default 1) is a genuine in-place modify — confirmed by resolving both
  and diffing `service-service.inputs.desired_count`.
- **`ACDL_REMOTE_STATE_KEY` plumbing:** correct end-to-end — the L2 scripts
  export it, the adapter reads it with a sensible default, and the
  microservice composition's `terraform_remote_state` data block picks it
  up. This cleanly separates the short-lived CI VPC state from the
  long-lived platform VPC state.
- **Workflow structure:** `l2-lifecycle` correctly `needs: ci-vpc-apply`;
  `ci-vpc-destroy` correctly `needs: [lifecycle, l2-lifecycle]` and
  `if: always()`. The 7 new L2 pipeline-contract tests assert all of this.
- **Byte-identical workflows:** `.gitea` and `.github` modules-lifecycle.yml
  are byte-identical (test asserts this); the `test_workflow_has_four_jobs`
  rename from three→four is correct.
- **Adapter line count:** 194 lines — under the 200-line ceiling, still a
  clean stateless assembler. The dedup logic added ~16 lines without
  bloating.
- **Teardown verification (P64):** trustworthy in structure — the
  `ci-vpc-destroy` job runs unconditionally and the decommission
  `---ci---` block is the audit trail. The adversarial concern (P1-5) is
  about the regression gate trusting the workflow ran, not about the
  teardown itself being fakeable.
- **Security:** no credential leakage in any reviewed file. All AWS auth
  in workflows uses `${{ secrets.* }}`; COST.md references only env var
  names and a redacted/deactivated root key ID.

## Test coverage assessment (485 offline tests)

- **Adequate:** pipeline contract (89 tests), schema validation, contract
  resolution, adapter emission (basic), confidence signal, outbox,
  interpolation, local emulators, module-standards file presence, design-doc
  currency.
- **Gaps (post-hoc):**
  1. Adapter dedup merge behavior (P2-2) — no unit test.
  2. `ACDL_REMOTE_STATE_KEY` override (P2-2) — no unit test.
  3. CAP-017..022 regression checks (P1-5) — not exercised at the unit
     level; the 2 slow tests in `test_verify_regression_mode.py` run the
     full registry but are `@pytest.mark.slow` and deselected from the
     fast suite, so a CI run of the 485 fast tests does not verify
     CAP-017..022 even at the offline-proxy level.
  4. WAF `upper()` scope — no test asserts the locals transform; relies
     on the lifecycle pipeline cell to catch a regression.
  5. ALB `name_prefix` — no test asserts the target group uses
     `name_prefix` (P2-1 context).

The 485 count is honest (447 pass fast, 5 deselected slow, 485/490
collected). The gap is behavioral coverage of the new adapter + module
logic, not contract/schema coverage.

## Verdict

**PASS with P1 flags for post-hoc review.** No P0 fixes applied. The
milestone's structural controls (regression gate, mandatory teardown,
byte-identical workflows, byte-identical contract↔workflow tests) are
sound. The most material finding is P1-5 (the regression gate's
CAP-017..022 evidence is an offline proxy, not live pipeline evidence) —
this is a repeat of the v1.10 "VERIFY was diff-scoped" structural defect
in a milder form: the gate trusts the workflow was run rather than proving
it. The mitigations in PRE_MORTEM (FM-1..FM-4) acknowledge related risks;
P1-5 is the specific instance for the lifecycle-pipeline tier.