Audit of v1.15-Nova milestone: reconstruction PASS (27 commits, 20 with ci blocks, 7 merges per convention, state matches checkpoint). File discipline: 10 auto-fixed rebrand-completeness gaps — 9 .ciagent/*.md headers still said ACDL (PROJECT/REQUIREMENTS/ROADMAP/RESEARCH/AUDIT/ REVIEW/COST/IAM_POLICY/CAPABILITY_INVENTORY → Nova) + config.json projects[].name → Nova. Active v1.15 sections already Nova; historical v1.1-v1.14 narratives left as-is. Branch hygiene PASS (6 phases + 1 milestone all merged, 0 orphans). Commit discipline PASS (0 non-merge commits without ci blocks, 0 unresolved escalations). 6 audit checks all PASS. Verdict: PASS. ---ci--- project: acdl phase: 5 milestone: v1.15 status: complete phase_role: final audit: pass ---/ci---
18 KiB
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
modulecollapse to onemodule "<first_id>" { ... }block with merged inputs. - No test asserts that
ACDL_REMOTE_STATE_KEYoverrides the defaultplatform/terraform.tfstatekey in the emitteddata terraform_remote_stateblock. - 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 theACDL_REMOTE_STATE_KEYwiring 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. Thelocal.scopeindirection 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). Thecreate_before_destroylifecycle 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 showsecr.inputs.name = "app-repo"androles.inputs.role_name = "app-role"(defaults applied since the contract doesn't setname). 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 diffingservice-service.inputs.desired_count. ACDL_REMOTE_STATE_KEYplumbing: correct end-to-end — the L2 scripts export it, the adapter reads it with a sensible default, and the microservice composition'sterraform_remote_statedata block picks it up. This cleanly separates the short-lived CI VPC state from the long-lived platform VPC state.- Workflow structure:
l2-lifecyclecorrectlyneeds: ci-vpc-apply;ci-vpc-destroycorrectlyneeds: [lifecycle, l2-lifecycle]andif: always(). The 7 new L2 pipeline-contract tests assert all of this. - Byte-identical workflows:
.giteaand.githubmodules-lifecycle.yml are byte-identical (test asserts this); thetest_workflow_has_four_jobsrename 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-destroyjob 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):
- Adapter dedup merge behavior (P2-2) — no unit test.
ACDL_REMOTE_STATE_KEYoverride (P2-2) — no unit test.- CAP-017..022 regression checks (P1-5) — not exercised at the unit
level; the 2 slow tests in
test_verify_regression_mode.pyrun the full registry but are@pytest.mark.slowand 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. - WAF
upper()scope — no test asserts the locals transform; relies on the lifecycle pipeline cell to catch a regression. - ALB
name_prefix— no test asserts the target group usesname_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.