---
phase: P0
name: pre-execution
milestone: v1.14
requirements: [REQ-135, REQ-136, REQ-137, REQ-138, REQ-139, REQ-140, REQ-141, REQ-142, REQ-143, REQ-144, REQ-145, REQ-146, REQ-147, REQ-148, REQ-149, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154]
wave: 0
depends_on: []
---

# v1.14 — NFR Refinement Plan (20 execution phases + 1 final)

**Milestone:** v1.14 (NFR — bug fixes, security, stubs, tests, docs)
**Type:** NFR (all phases fix/test/docs/chore/refactor). Final patch IS
the release. Tags: `v1.13.3` (P0) → `v1.13.4..v1.13.23` (P1–P20) →
`v1.13.24` (P21 = milestone release).
**Branch:** `milestone/v1.14-refinement` → `phase/NN-<slug>`

## Wave ordering (D-098)

- **Wave 1 (P1–P6):** bug fixes. P1→P2 sequential (composition depends
  on dedup correctness); P3–P6 independent. **G-105: full regression
  gate run after P4** (validates the hardened gate before W2).
- **Wave 2 (P7–P12):** security. P8→P9 sequential (IAM ARNs reference
  externalized account ID); rest independent. **G-106: mid-milestone
  regression-gate checkpoint after P12** (offline gate run; non-Verified
  halts W3 until fixed).
- **Wave 3 (P13–P17):** stub/test/CI/hygiene. P15 depends on P7
  (hardened errors before script tests); P17 depends on P14 (both touch
  config.json); P13 independent.
- **Wave 4 (P18–P20):** standards/docs/VPC. P19 depends on P1–P18
  (reflects all prior phases); P18 + P20 independent.

## Execution approach

Each phase: EXECUTE (persona-assigned task groups) → VERIFY (4 layers +
regression gate at milestone complete) → SHIP (patch tag). Phase
boundary checkpoint resets context. The execute workflow reads this
PLAN.md + ROADMAP.md §v1.14 + PERSONAS.md for task decomposition.

---

## Wave 1 — Bug Fixes (P1–P6)

### P1 — adapter-dedup-diagnostic (REQ-135)
**Persona:** backend-engineer
**Territory:** `adapters/terraform/adapter.py`
**Tasks:**
1. In the dedup loop (`adapter.py:159-170`), when `tf_dir` is `None`,
   raise `ValueError(f"no terraform_dir in registry for module
   {module}")` instead of silently skipping.
2. Verify registered-module dedup behavior preserved (multi-resource L1s
   still merge into one `module "x" { ... }` block).
3. Run `pytest tests/test_adapter.py` + `run_ci.sh`.

### P2 — static-assets-wiring-fix (REQ-136)
**Persona:** data-engineer
**Territory:** `modules/l2/static-assets/`
**Tasks:**
1. Wire `default_ttl`/`max_ttl`/`price_class`/`viewer_protocol_policy`
   in `composition.json` to the cloudfront child's inputs.
2. Add a `waf_enabled` feature flag (default true) to the
   static-assets composition; make the WAF child conditional on it.
3. Update `examples/complex.yml` to set `waf_enabled: true` + non-default
   TTLs so it resolves to a different resource set than `simple.yml`.
4. Run `pytest` + `run_ci.sh`.

### P3 — lifecycle-script-arg-cleanup (REQ-137)
**Persona:** backend-engineer
**Territory:** `scripts/run_l2_lifecycle_*.sh`
**Tasks:**
1. Remove the `[ci-vpc-outputs.json]` token from the usage strings of
   `run_l2_lifecycle_test.sh` + `run_l2_lifecycle_destroy.sh`, OR add a
   comment documenting the L2-uses-remote-state design + parity reason.
2. Run `pytest` + `run_ci.sh`.

### P4 — regression-gate-evidence-hardening (REQ-138)
**Persona:** backend-engineer
**Territory:** `core/regression_verify.py`, `.ciagent/CAPABILITY_INVENTORY.md`
**Binding decisions:** G-105 (gate must pass clean post-P4 before W2)
**Tasks:**
1. Add a `terraform validate` step to
   `_check_lifecycle_module_terraform` (or document why it's too slow +
   fall back to a `terraform fmt -check` syntax probe).
2. Tighten CAPABILITY_INVENTORY + docstrings to "offline proxy; live
   apply/modify/destroy verified by the modules-lifecycle workflow run,
   not by this gate."
3. **Run the full regression gate immediately after P4 lands** (G-105).
   Gate must pass clean before W2 begins.
4. Run `pytest` + `run_ci.sh`.

### P5 — adapter-behavior-tests (REQ-139)
**Persona:** backend-engineer
**Territory:** `tests/test_adapter.py`
**Tasks:**
1. Add `test_adapter_dedup_merges_same_module` — two resources with the
   same `module` collapse to one `module "<first_id>" { ... }` block with
   merged inputs.
2. Add `test_adapter_remote_state_key_override` — `ACDL_REMOTE_STATE_KEY`
   overrides the default `platform/terraform.tfstate` key in the emitted
   `data terraform_remote_state` block.
3. Run `pytest` + `run_ci.sh`.

### P6 — alb-name-prefix-fix (REQ-140)
**Persona:** data-engineer
**Territory:** `modules/l1/alb/terraform/main.tf`
**Tasks:**
1. Change `name_prefix = "tg-ci-"` to `name_prefix = "${var.name}-"` so
   the consumer's name prefixes the target group.
2. Run `terraform validate` in the alb module dir standalone.
3. Run `pytest` + `run_ci.sh`.

---

## Wave 2 — Security (P7–P12)

### P7 — swallowed-error-hardening (REQ-141)
**Persona:** backend-engineer
**Territory:** `core/local_emulators.py`, `core/lambda/contract_ingestor.py`,
  `terraform/bootstrap/create_state_backend.py`, `core/output_publisher.py`,
  `terraform/bootstrap/apply_iam_baseline.py`
**Tasks:**
1. `local_emulators.py:374` — narrow `except Exception: pass` to catch
   `AttributeError`/`TypeError` (monkeypatch setup); log + re-raise if
   patching fails (prevents network egress).
2. `contract_ingestor.py:157` — catch `urllib.error.URLError`/
   `HTTPError` specifically; log the search failure; keep `existing = []`
   only on `404`/network, re-raise on auth errors.
3. `create_state_backend.py:51` — catch `ClientError` with
   `NoSuchBucket`/`404` error code; re-raise on permissions/network.
4. `output_publisher.py:100,168` — catch `ClientError`/`HTTPError`
   specifically; log with context.
5. `apply_iam_baseline.py:78` — catch `NoSuchEntityException` on
   old-version delete; re-raise on other errors.
6. Run `pytest` + `run_ci.sh`.

### P8 — account-id-externalization (REQ-142)
**Persona:** backend-engineer + data-engineer
**Territory:** `adapters/terraform/adapter.py`, `terraform/bootstrap/`,
  `scripts/push_consumer_image.py`, terraform resource ARNs
**Binding decisions:** G-101 (grep excludes backend blocks), G-102
  (fallback bound to live account ID + workflow env wiring)
**Tasks:**
1. `adapter.py:125,140` — read `ACDL_AWS_ACCOUNT_ID` env; build the
   state-bucket name dynamically. **Fallback constant = `581513795199`**
   (the live account ID, NOT a placeholder — G-102). Documented for
   offline tests.
2. `apply_iam_baseline.py:33`, `create_state_backend.py:33,35` — read
   from env (same fallback).
3. `push_consumer_image.py:32` — read from env.
4. Terraform: use `data.aws_caller_identity.current.account_id` for
   **resource ARNs** in `spike_runner_policy.json` + resource names.
   **Exclude terraform `backend "s3"` blocks** (`terraform/*/terraform.tf`,
   `terraform/ci-vpc/main.tf`, `terraform/platform/main.tf`,
   `terraform/microservice/terraform.tf`) — backend `bucket` args are
   static-config-only, evaluated pre-init (G-101). Leave backend blocks
   literal or move to `terraform init -backend-config` (separate change,
   not in P8 scope).
5. **Lifecycle workflow env wiring (G-102):** the `modules-lifecycle.yml`
   full-mode jobs must set `ACDL_AWS_ACCOUNT_ID` from
   `aws sts get-caller-identity --query Account --output text` before
   any `run_platform.sh`/lifecycle invocation. No full-mode run proceeds
   with the env unset.
6. Run `pytest` + `run_ci.sh`; verify
   `grep -rn "581513795199" adapters/ scripts/ terraform/bootstrap/ core/`
   returns 0 hits (excluding tests + docs + terraform backend blocks).

### P9 — iam-policy-least-privilege (REQ-143)
**Persona:** data-engineer
**Territory:** `terraform/bootstrap/spike_runner_policy.json`,
  `tests/test_iam_policy_baseline.py`, `modules/l1/*/terraform/main.tf`,
  `modules/l2/*/composition.json`
**Binding decisions:** G-104 (verify acdl-* naming before merge)
**Tasks:**
1. Scope `iam:CreateRole` etc. (line 236) to
   `arn:aws:iam::*:role/acdl-*`.
2. Scope KMS (line 218) to `arn:aws:kms::*:key/acdl-*` (or
   `alias/acdl-*`).
3. CloudFront (line 117) + WAFv2 (line 129) remain `Resource: "*"` with
   a documented global-ARN constraint (CloudFront ARNs are global;
   cannot be account-scoped — G-104).
4. **Verify acdl-* naming (G-104):** grep/audit
   `modules/l1/*/terraform/main.tf` + `modules/l2/*/composition.json`
   for every IAM role + KMS key name created by the lifecycle pipeline.
   If any non-`acdl-*` name is found, rename the resource or widen that
   one statement (documented).
5. Add a regression test in `test_iam_policy_baseline.py` asserting no
   new `Resource: "*"` on non-global actions.
6. Run `pytest` + `run_ci.sh`.

### P10 — contract-ingestor-identity-validation (REQ-144)
**Persona:** backend-engineer
**Territory:** `core/lambda/contract_ingestor.py`, `tests/test_contract_ingestor.py`
**Tasks:**
1. Add `contractId` format validation (regex, ≤64 chars).
2. Add `environment` enum validation (dev/qa/prod/dr).
3. Add `error` length cap (truncate `stackTrace` at a reasonable limit).
4. Document the ABAC reliance in the `_validate_caller_identity`
   docstring + add a note to ARCHITECTURE.md (P19 will land it).
5. Add a spoofing-resistance test (caller submits a `consumerRepo` they
   don't own → rejected if ABAC misconfigured; documented best-effort).
6. Run `pytest` + `run_ci.sh`.

### P11 — schema-input-validation-hardening (REQ-145)
**Persona:** backend-engineer
**Territory:** `schemas/contract.schema.json`, `schemas/environment.schema.json`,
  `tests/test_environment_schema.py`, `tests/test_contract_schema.py`
**Tasks:**
1. Add `"additionalProperties": false` to both schemas' top-level
   objects.
2. Add `maxItems`/`maxProperties` bounds to `infrastructure` map +
   `monitored_endpoints` array.
3. Add `pattern` validation for `state_backend.bucket` (S3 naming
   rules: lowercase, 3-63 chars, no underscores).
4. Add `pattern` validation for `runner_role_arn` (ARN format).
5. Add `pattern` validation for `vpc_cidr` (CIDR format).
6. Add tests asserting rejection of undocumented fields + malformed
   values.
7. Run `pytest` + `run_ci.sh`.

### P12 — gitignore-credential-hygiene (REQ-146)
**Persona:** lead-developer
**Territory:** `.gitignore`, `tests/test_no_secrets_tracked.py`
**Tasks:**
1. Add credential-pattern catch-all to `.gitignore`:
   `*.pem`, `*.key`, `*.p12`, `*.pfx`, `*.cer`, `*.crt`, `*.jks`.
2. Create `tests/test_no_secrets_tracked.py` — runs
   `git ls-files | grep -E '\.(pem|key|p12|pfx|cer|crt|jks)$'` and
   asserts 0 hits.
3. Run `pytest` + `run_ci.sh`.

---

## Wave 3 — Stub / Test / CI / Hygiene (P13–P17)

### P13 — kyverno-kube-version-resolution (REQ-147)
**Persona:** backend-engineer
**Territory:** `adapters/kyverno/kyverno_adapter.py`, `tests/test_kyverno_adapter.py`
**Binding decisions:** G-103 (removal+documentation path, NOT implementation)
**Tasks:**
1. **Remove the `--kube-version` flag** from
   `kyverno_adapter.py:11,115-116` (G-103 — implementing version-aware
   policy selection would be a new feature, violating D-095).
2. Add a docstring documenting the deferral to the GitOps reconciler
   roadmap (D-053): the Kyverno adapter is inactive for Terraform-only
   stacks; `--kube-version` will be relevant when the GitOps reconciler
   emits K8s manifests.
3. Update `test_kyverno_adapter.py` to remove the `--kube-version` test
   cases + assert the flag is absent.
4. Run `pytest` + `run_ci.sh`.

### P14 — orphan-artifact-and-dead-config-cleanup (REQ-148)
**Persona:** lead-developer
**Territory:** `scripts/__pycache__/`, `pyproject.toml`, `.ciagent/config.json`
**Tasks:**
1. Delete the orphan
   `scripts/__pycache__/verify_deploy_microservice.cpython-312.pyc`.
2. Fix `pyproject.toml` coverage source: `acdl_platform` → `core`.
3. Bump `pyproject.toml` version `1.3.0` → current (v1.14).
4. Remove dead JS allowlist entries from `config.json`
   `bash_allowlist.allowed_commands` (npm/node/npx/pnpm/yarn/jest/eslint/
   tsc/prettier — no package.json).
5. Run `pytest` + `run_ci.sh`.

### P15 — untested-scripts-coverage (REQ-149)
**Persona:** backend-engineer
**Territory:** `tests/` (new test files for 7 scripts)
**Tasks:**
1. `tests/test_seed_uptime_monitors.py` — mock the uptime-kuma API;
   assert monitor creation from a JSON file.
2. `tests/test_push_consumer_image.py` — mock `subprocess.run` (docker
   login/build/push) + boto3 ECR; assert the flow.
3. `tests/test_sync_to_gl.sh` (shell test) — dry-run mode; assert the
   copy + push commands are constructed correctly.
4. `tests/test_post_stage_comment.sh` (shell test) — no-op when not in
   a PR context; assert the `gh api` call structure when in PR.
5. `tests/test_rotate_spike_key.sh` (shell test) — mock `aws iam`;
   assert deactivate/create/update-secret flow.
6. `tests/test_create_state_backend.py` — mock boto3 S3/DynamoDB;
   assert idempotent creation.
7. `tests/test_create_iam_user.py` — mock boto3 IAM; assert idempotent
   user/policy/key creation.
8. Run `pytest` + `run_ci.sh`.

### P16 — workflow-parity-and-script-flags (REQ-150)
**Persona:** backend-engineer
**Territory:** `.gitea/workflows/`, `scripts/rotate_spike_key.sh`,
  `scripts/sync_to_gl.sh`
**Tasks:**
1. Either mirror the 4 GitHub-only workflows (patterns-plan,
   platform-test, primitives-plan, release) to `.gitea/workflows/`, or
   add a README documenting the Gitea limitation (Gitea runners don't
   use release/primitives-plan/patterns-plan; release is GitHub-only by
   design).
2. Add `set -euo pipefail` to `rotate_spike_key.sh` (currently only
   `set -u`).
3. Add `set -euo pipefail` to `sync_to_gl.sh` (currently no `set`
   flags).
4. Run `pytest` + `run_ci.sh`.

### P17 — config-and-persona-hygiene (REQ-151)
**Persona:** lead-developer
**Territory:** `.ciagent/config.json`, `.ciagent/PERSONAS.md`
**Tasks:**
1. Mark `frontend-engineer` persona `active: false` in `config.json`
   `personas.personas[]` (PERSONAS.md:80 already says inactive).
2. Fix `branching_strategy: "phase"` — either change to `"flat"` or
   document that the field is advisory + the project uses flat workflow
   (committed directly to main per established convention).
3. Configure `ollama-cloud` backend: set `base_url` to the actual
   endpoint OR add a comment documenting why it's intentionally unset
   (the runtime uses the `glm-5.2` model via the opencode backend, not
   the `llm_backends` config).
4. Run `pytest` + `run_ci.sh`.

---

## Wave 4 — Standards / Docs / VPC (P18–P20)

### P18 — module-standards-consistency (REQ-152)
**Persona:** data-engineer
**Territory:** `modules/STANDARDS.md`, `modules/l1/{ecr,ecs-cluster,rds}/terraform/`
**Tasks:**
1. Either add `locals.tf` to `ecr`, `ecs-cluster`, `rds` (extract
   inlined locals from `main.tf`), OR reconcile STANDARDS §9.4 to
   explicitly allow inlining for trivial single-resource modules.
2. Remove the stale `TYPE_MAP` reference in STANDARDS §8 (deleted in
   the v1.11 stateless rewrite).
3. Run `pytest` + `run_ci.sh`.

### P19 — documentation-sync-v1.14 (REQ-153)
**Persona:** lead-developer
**Territory:** `.ciagent/ARCHITECTURE.md`, `docs/`, `README.md`,
  `.ciagent/COST.md`, `.ciagent/GRILL.md`, `.ciagent/IAM_POLICY.md`,
  `docs/presentations/`
**Tasks:**
1. ARCHITECTURE.md: add v1.11 addendum (stateless adapter, platform VPC,
   ACDL_LIFECYCLE_MODE), v1.12 addendum (CAP-013 fix, plan-only
   default), v1.13 addendum (config.json schema migration, badge
   cleanup, platform-architecture diagram), v1.14 addendum (all 20
   phases). Record D-083 deferral explicitly.
2. Bump stale `@v1.6`–`@v1.9` → `@v1.13` across `README.md:225`,
   `docs/consumer-guide.md` (12 sites), `docs/architecture.md:233`,
   `docs/pipeline/versioning.md:29`, `docs/pipeline/index.md:42`.
3. Sync decks to v1.13.2 reality (version refs, capability claims).
4. Update COST.md window to v1.11–v1.14 (lifecycle pipeline live-runs +
   teardown).
5. Resolve G-005/G-008 in GRILL.md (CAP-017..022 now Verified via
   lifecycle pipeline; COST.md now exists + covers v1.11+).
6. Update IAM_POLICY.md for v1.12/v1.13/v1.14 (plan-only default,
   config.json schema, v1.14 IAM scoping from P9).
7. Run `pytest` + `run_ci.sh`; verify
   `grep -rn "@v1\.[6-9]" docs/ README.md` returns 0 hits.

### P20 — platform-vpc-parameterization (REQ-154)
**Persona:** data-engineer
**Territory:** `terraform/platform/main.tf`
**Tasks:**
1. Add a `vpc_cidr` variable (default `10.0.0.0/16`); replace the
   hardcoded `cidr_block`.
2. Replace `count = 2` subnets with
   `count = length(data.aws_availability_zones.available.names)`.
3. Add a `data "aws_availability_zones" "available" {}` block.
4. Document the `0.0.0.0/0` ingress on port 80 (ALB-fronted, acceptable
   for a public-facing service; add a comment).
5. Run `terraform validate` + `pytest` + `run_ci.sh`.

---

## Final Phase — P21 (review + audit + ship)

**Persona:** lead-developer (review coordination) + ci-code-reviewer +
  ci-debugger (audit)
**Tasks:**
1. Multi-persona code review across all v1.14 phases (P1–P20). Auto-apply
   P0 fixes; flag P1+ for post-hoc review. If P1+ found, fix in-phase.
2. Audit: reconstruction test (git log vs `.ciagent/` files), file
   discipline, branch hygiene, commit discipline. Fix critical issues
   in-phase.
3. Complete: update REQUIREMENTS.md (REQ-135..154 → complete),
   ROADMAP.md (v1.14 complete), PROJECT.md.
4. Tag `v1.13.24` (IS the milestone release). Merge
   `milestone/v1.14-refinement` → `main`. Create Gitea release with full
   milestone summary.

## Success Criteria (milestone gate)

1. All 20 REQ-135..REQ-154 marked complete in REQUIREMENTS.md.
2. Review: 0 new P0; all P1-1..P1-5 + P2-1..P2-4 resolved.
3. Audit: clean; reconstruction test passes.
4. Regression gate (D-091) clean against the v1.14 state.
5. `pytest` passes; `run_ci.sh` exits 0; `run_platform.sh --check-only`
   exits 0.
6. Tag `v1.13.24` created; milestone merged to main.