---ci--- project: acdl phase: 0 milestone: v1.9 status: plan ---/ci--- 5 vertical-slice phases, wave-ordered: - Wave 1 (parallel): Phase 39 (design docs + P1-1, disjoint file sets) - Wave 2: Phase 40 (contract interpolation + env schema) - Wave 3: Phase 41 (per-env CI jobs + env workflow input) - Wave 4: Phase 42 (stubs: SoD, HITL gates, attestation matrix, Wiz, Kyverno) - Wave 5: Phase 43 (verify + review + audit + complete) Plans reference REQ-100..REQ-111. Decisions D-080..D-089 applied. Versioning: feature milestone, patches v1.8.1..v1.8.5, tag v1.9.0 at COMPLETE.
20 KiB
phase, name, milestone, requirements, type
| phase | name | milestone | requirements | type | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 39-43 | v1.9-design-doc-interpolation-per-env-ci-stubs-p1-1 | v1.9 |
|
feat/docs/fix |
ACDL v1.9 — Phase Plans
Milestone v1.9. Generated at PLAN stage. Autonomy: full. Requirements: REQ-100..REQ-111 (see REQUIREMENTS.md). Decisions: D-080..D-089 (see PROJECT.md + RESEARCH.md RA section). Versioning: feature milestone — progressive patch versions per phase (v1.8.1..v1.8.5), tag
v1.9.0at milestone COMPLETE.
Wave ordering
- Wave 1 (parallel, 2 tasks): Phase 39 — design-doc refresh (security-engineer) + P1-1 adapter parameterization (platform-engineer). Disjoint file sets; no merge conflict.
- Wave 2 (sequential): Phase 40 — contract interpolation. Depends on Phase 39's design-doc context (lightweight).
- Wave 3 (sequential): Phase 41 — per-env CI jobs. Depends on Phase 40's interpolation + env schema.
- Wave 4 (sequential): Phase 42 — stub implementation. Depends on Phase 41's HITL job structure.
- Wave 5 (sequential): Phase 43 — verify + review + audit + complete.
Phase 39 — design-doc-refresh-and-p1-1-parameterization
Requirements: REQ-100, REQ-101, REQ-102
Personas: security-engineer (lead: design docs), platform-engineer (lead: P1-1), backend-engineer (review)
Branch: phase/39-design-doc-refresh-and-p1-1
Task 39.1 — Refresh hitl_matrix_design.md (REQ-100, security-engineer)
- Rewrite the status block: "v1.2 wires the gates" → "v1.9 wires the gates (Phase 42)".
- Update "Spike scope note" → "v1.9 scope note": qa/prod/dr now exercised (Phase 41 wires the job structure; Phase 42 wires the attestation gates); dev remains autonomous.
- Update §10.4 matrix: mark the offline-testable concerns (contract NFRs, schema validity, policy pass) as implemented in v1.9 (
core/attestation_matrix.py); mark operator-supplied concerns as accept signed evidence artifacts (D-084). - Add a "v1.9 wiring" section: cross-reference Phase 41's per-env jobs + Phase 42's
hitl_gates.py+attestation_matrix.py+ the outbox-based SoD check. - Preserve D-042 (approver identity =
gitea.actor/github.actor) — still accurate. - Verify:
grep -i "dev-only spike" core/hitl_matrix_design.mdreturns 0 hits;grep -i "v1.2 wires" core/hitl_matrix_design.mdreturns 0 hits.
Task 39.2 — Refresh audit_ledger_design.md (REQ-101, security-engineer)
- Mark the "Spike scope (D-041)" section as shipped + production since v1.8 (hash chain + DynamoDB outbox +
acdl-evidencemirror). - Move the "v1.2 build-out" section (S3 Object Lock + JWS + async worker + DLQ + daily checkpoints) under a clearly-labeled "Deferred to a future milestone (D-083)" heading. Keep the content (it's the design for when it ships) but mark it not-v1.9.
- Update the RPO/RTO table: spike row → "v1.8+ (production): RPO=0 (sync outbox), RTO=workflow re-run"; v1.2 row → "Future milestone (D-083): RPO=0, RTO=DLQ replay".
- Update the outbox item shape: note
approver_qa/approver_prod/approver_drare populated by v1.9'shitl_gates.attest(Phase 42). - Verify:
grep -i "Phases 08-10 implement" core/audit_ledger_design.mdreturns 0 hits; the deferred section is clearly labeled.
Task 39.3 — P1-1 adapter parameterization (REQ-102, platform-engineer)
modules/l1/ecs-service/interface.json: add inputsdesired_count(integer, default 1),launch_type(string, default "FARGATE"),family(string, default "app").modules/l1/alb/interface.json: add inputsload_balancer_type(string, default "application"),target_type(string, default "ip").modules/l1/vpc/interface.json: add inputname(string, default "app") for the VPC/IGW/RTNametag prefix.adapters/terraform/adapter.py: change hardcoded defaults toinputs.get("<name>", "<default>")where the default matches the interface default (safety fallback; the resolver populates from the interface). Remove the hardcodedName = "acdl-microservice-rt"(line 283) → useinputs.get("name", "app")-derived tag.- Preserve the v1.1 S3 regression (S3 has none of these inputs → no change).
- Tests:
tests/test_p1_1_adapter_parameterization.py— (a)desired_count: 3in contract inputs emitsdesired_count = 3; (b) absentdesired_countemitsdesired_count = 1via interface default; (c)target_type: "instance"emitstarget_type = "instance"; (d) v1.1 S3 regression still passes (byte-identicalmain.tf). - Verify:
pytest tests/test_p1_1_adapter_parameterization.pypasses;run_platform.sh --check-onlyexits 0;pytesttotal count increases; v1.1 S3 regression test passes.
Task 39.4 — Design doc test (REQ-100/101, backend-engineer)
tests/test_design_docs_current.py: assert (a) no stale "dev-only spike" / "v1.2 wires the gates" / "Phases 08-10 implement" framing in either design doc; (b)audit_ledger_design.mdhas a "Deferred to a future milestone" section referencing D-083; (c)hitl_matrix_design.mdreferences the v1.9 implementation (attestation_matrix.py,hitl_gates.py).- Verify:
pytest tests/test_design_docs_current.pypasses.
Must-haves (Phase 39)
core/hitl_matrix_design.mdrefreshed (no stale framing).core/audit_ledger_design.mdrefreshed (S3 Object Lock marked deferred D-083).- Adapter has no hardcoded ECS/ALB/VPC defaults (read from inputs).
tests/test_p1_1_adapter_parameterization.py+tests/test_design_docs_current.pypass.run_ci.shexits 0;run_platform.sh --check-onlyexits 0; v1.1 S3 regression passes.
Phase 40 — contract-interpolation
Requirements: REQ-103, REQ-104
Personas: backend-engineer (lead), platform-engineer (review)
Branch: phase/40-contract-interpolation
Task 40.1 — Environment JSON schema (REQ-104, backend-engineer)
schemas/environment.schema.json(draft 2020-12): requiredname(string),account_id(string),region(string),state_backend(object:bucket,lock_table),network(object:vpc_cidr,azsarray),runner_role_arn(string),autonomy(enum: full/attested),confidence_threshold(number).core/environments/dev.jsonvalidates against it.- Add
core/environments/qa.json,prod.json,dr.json:account_id: "000000000000",autonomy: "attested",confidence_threshold0.75/0.90/0.95, regions us-east-1, state_backend bucketsacdl-qa-state/acdl-prod-state/acdl-dr-state. core/environment_check.py: addload(env_name, root=None)returning the parsed env dict;check()stays. Add a stderr warning whenaccount_id == "000000000000"andenv_name != "dev"(prompts real binding).tests/test_environment_schema.py: all 4 env files validate;load("dev")returns the dict; warning emitted for qa/prod/dr placeholders.- Verify:
pytest tests/test_environment_schema.pypasses.
Task 40.2 — Interpolation expansion in the resolver (REQ-103, backend-engineer)
core/contract_resolver.py: add_expand_vars(value, context)— recursively walks dicts/lists/strings; replaces${env.<dotted.path>}and${contract.<dotted.path>}tokens by looking up the dotted path in the context dict. Unknown token →ValueError(f"unresolved interpolation token: {token}").resolve(): after schema validation, load the env viaenvironment_check.load(contract["environment"]), buildcontext = {"env": env, "contract": contract}, expand all string values incontract["inputs"](recursively, per D-087), then proceed to IR resolution.- The expansion is post-schema-validation (schema sees the raw tokens, which are valid strings) and pre-IR-resolution (the resolver sees concrete values).
tests/test_interpolation.py: (a)${env.region}expands tous-east-1; (b)${env.state_backend.bucket}expands toacdl-dev-state; (c)${contract.module}expands tostatic-assets; (d) unknown token raisesValueError; (e) nested map valueenv: { DB_URL: "acdl-${env.environment}-db" }expands recursively; (f)resolve("contracts/static-assets.yaml")succeeds with expanded values.- Verify:
pytest tests/test_interpolation.pypasses.
Task 40.3 — Sample contracts use naming patterns (REQ-103, backend-engineer)
contracts/static-assets.yaml:bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}(the naming pattern the requirement calls out: region + account id + environment).contracts/microservice.yaml: same pattern forbucket_name.- Keep
region: us-east-1as a literal (or${env.region}— both valid; use${env.region}to demonstrate). tests/test_sample_contracts_interpolate.py: resolving the sample contracts produces concrete bucket names likeacdl-dev-static-assets-000000000000-us-east-1.- Verify:
pytest tests/test_sample_contracts_interpolate.pypasses;run_platform.sh --check-onlyexits 0 (resolver expands before adapter).
Must-haves (Phase 40)
schemas/environment.schema.jsonexists; 4 env files validate._expand_varsin resolver; unknown tokens raise.- Sample contracts use
${env.*}+${contract.*}naming patterns. tests/test_environment_schema.py+tests/test_interpolation.py+tests/test_sample_contracts_interpolate.pypass.run_ci.shexits 0;run_platform.sh --check-onlyexits 0.
Phase 41 — per-environment-ci-jobs
Requirements: REQ-105, REQ-106
Personas: backend-engineer (lead), security-engineer (HITL gate review)
Branch: phase/41-per-environment-ci-jobs
Task 41.1 — Per-env contract files (REQ-105, backend-engineer)
contracts/static-assets.dev.yaml,.qa.yaml,.prod.yaml,.dr.yaml— each setsenvironment:to its own name;inputs.bucket_nameuses${env.environment}-${contract.module}-${env.account_id}-${env.region}interpolation (so the file content is near-identical; onlyenvironment:differs).contracts/microservice.{dev,qa,prod,dr}.yaml— same pattern.- Keep
contracts/static-assets.yaml+contracts/microservice.yamlas the dev default (backwards compat). tests/test_per_env_contracts.py: all 8 per-env files validate againstschemas/contract.schema.json; each resolves to a stack with the correct environment.- Verify:
pytest tests/test_per_env_contracts.pypasses.
Task 41.2 — Deploy workflow environment input (REQ-106, backend-engineer)
.github/workflows/deploy.yml+.gitea/workflows/deploy.yml(byte-identical): addenvironmentinput (type: string, default"", description "Target environment override (dev/qa/prod/dr); when empty, the contract's environment field is used").scripts/run_platform.sh: add--environment <name>flag. When set, override the contract'senvironmentfield at load time (before schema validation per D-088, so interpolation context is consistent). Re-run the onboarding check against the supplied env.- The workflow's "Run the platform pipeline" step passes
--environment ${{ inputs.environment }}when non-empty. tests/test_deploy_workflow_env_input.py: both deploy workflows declare theenvironmentinput; byte-identical;run_platform.sh --environment qa contracts/static-assets.yamlproduces a stack whose env is qa (tested via the resolver directly since run_platform.sh needs AWS for full mode — test the override logic in the resolver).core/contract_resolver.pyresolve(): accept optionalenvironment_overridearg; when set, setcontract["environment"] = overridebefore schema validation + interpolation.- Verify:
pytest tests/test_deploy_workflow_env_input.pypasses; both deploy workflows byte-identical.
Task 41.3 — Per-env caller workflow docs + HITL gate structure (REQ-106, security-engineer review)
docs/CONSUMER_GUIDE.md: add a "Per-environment deployment" section with 4 caller-workflow examples (.github/workflows/deploy-dev.yml,deploy-qa.yml,deploy-prod.yml,deploy-dr.yml), eachuses: acdl/.github/workflows/deploy.yml@v1.9withenvironment: <env>+contract: .acdl/<module>.<env>.yaml. Document: "Promotion = running the matching job; noenvironment:field editing."- HITL gate structure (wired in Phase 42, documented here): qa/prod/dr caller workflows use
workflow_dispatchwith approval inputs (approve_qa,approve_prod,approve_dr) perhitl_matrix_design.mdD-042;gitea.actor/github.actoris the approver of record. dev is autonomous (no gate). tests/test_consumer_guide_per_env_section.py: the consumer guide has the per-env section with 4 caller examples.- Verify:
pytest tests/test_consumer_guide_per_env_section.pypasses.
Must-haves (Phase 41)
- 8 per-env contract files exist + validate + resolve.
- Deploy workflow has
environmentinput (byte-identical Gitea + GitHub). run_platform.sh --environment <name>overrides; resolver supportsenvironment_override.- Consumer guide documents per-env caller workflows + promotion-without-editing.
tests/test_per_env_contracts.py+tests/test_deploy_workflow_env_input.py+tests/test_consumer_guide_per_env_section.pypass.run_ci.shexits 0; both deploy workflows byte-identical.
Phase 42 — stub-implementation
Requirements: REQ-107, REQ-108, REQ-109, REQ-110, REQ-111
Personas: security-engineer (lead), backend-engineer (run_platform wiring), lambda-engineer (SNS topic Terraform)
Branch: phase/42-stub-implementation
Task 42.1 — route_halt_artifact real (REQ-107, security-engineer + lambda-engineer)
core/separation_of_duties.pyroute_halt_artifact: whenACDL_SOD_HALT_TOPIC_ARNset, publish to SNS via boto3 (sns.publish(TopicArn=arn, Message=..., Subject="ACDL SoD halt")); when unset, fall back to structured stderr emission + aSEPARATION_OF_DUTIES_VIOLATIONevent write viaoutbox_writer.write_event(so the halt is in the audit chain). No silent print-only stub.terraform/platform/main.tf: addaws_sns_topic.acdl-sod-halt+ a basic access policy (allow the platform Lambda / runner role to publish). Output the topic ARN.tests/test_route_halt_artifact.py: (a) withACDL_SOD_HALT_TOPIC_ARNset, moto-mocked SNS receives the publish; (b) without it, aSEPARATION_OF_DUTIES_VIOLATIONevent is written to the outbox (moto-mocked DynamoDB); (c) stderr emission occurs in both cases.- Verify:
pytest tests/test_route_halt_artifact.pypasses.
Task 42.2 — HITL attestation gates (REQ-108, security-engineer + backend-engineer)
core/hitl_gates.py:attest(contract_id, env, approver, evidence, outbox_client=None)→ recordsapprover_qa/approver_prod/approver_drto the outbox item forcontract_id; runsseparation_of_duties.check(outbox_client, contract_id, approver)on prod; invokes the attestation matrix (Task 42.3) for the target env; returns(ok, reason). Dev skips (returns(True, "dev autonomous")).scripts/run_platform.sh: before apply (for qa/prod/dr), callhitl_gates.attestwith the approver fromGITHUB_ACTOR/GITEA_ACTORenv. Block on(ok=False).tests/test_hitl_gates.py: (a) dev skips; (b) qa recordsapprover_qa(moto outbox); (c) prod recordsapprover_prod+ SoD blocks whenapprover_qa == approver_prod; (d) prod passes when approvers differ.- Verify:
pytest tests/test_hitl_gates.pypasses.
Task 42.3 — 8-concern attestation matrix (REQ-109, security-engineer)
core/attestation_matrix.py:check(env, evidence_bundle)→ runs the 8 concerns. Offline-testable concerns (contract NFRs, schema validity, policy pass) run for real. Operator-supplied concerns accept an uploaded signed evidence artifact (JSON withtimestamp,type,payload, optionalsignature); validate freshness (within the declared window fromhitl_matrix_design.md§10.4) + schema (per-concern). Signature verification via KMS whenACDL_ATTESTATION_SIGNING_KEY_IDset; skipped + logged when unset (D-089). Fail loud if missing/expired for prod/dr.hitl_gates.attestcallsattestation_matrix.check(env, evidence)and blocks on any failing concern.tests/test_attestation_matrix.py: (a) offline concerns pass for a valid contract; (b) operator-supplied concern missing → block for prod; (c) operator-supplied concern present + fresh → pass; (d) expired artifact → block; (e) signature skip when key unset (logged).- Verify:
pytest tests/test_attestation_matrix.pypasses.
Task 42.4 — Wiz real API client (REQ-110, security-engineer)
adapters/wiz/wiz_adapter.py: addWizClientclass —__init__readsWIZ_API_TOKEN+WIZ_API_URL;fetch_issues(filter_by)queries the Wiz GraphQL API (<url>/graphql, Bearer auth,issuesquery). Translate results →PolicyCheckResultrecords (engine: "wiz",ruleId: <control.name>,severity: <lowercased>,status: FAIL,message: <title>,resource: <entity.name>). Graceful degrade: whenWIZ_API_TOKENorWIZ_API_URLunset → emit the existing singleSKIPPEDWIZ_NOT_CONFIGUREDrecord (no network call). Pagination handled viapageInfo.hasNextPage.tests/test_wiz_adapter_real_client.py: (a) with a recorded GraphQL fixture,WizClienttranslates issues →PolicyCheckResultrecords; (b) graceful degrade when env unset; (c) pagination followsendCursor.- Verify:
pytest tests/test_wiz_adapter_real_client.pypasses.
Task 42.5 — Kyverno translator fleshed out (REQ-111, security-engineer)
adapters/kyverno/kyverno_adapter.py: fullPolicyReport→PolicyCheckResultmapping — handlepass/fail/skip/warnresults, severity mapping (critical/high/medium/low/info), resource extraction, skip-with-reason handling. Keep the inactive-for-Terraform guard (emits a singleSKIPPEDKYVERNO_INACTIVE_TF_STACKrecord when no K8s manifests). Add a--kube-versionstub (parsed but not yet used — for future GitOps).tests/test_kyverno_adapter.py: expand — (a)passresult →PolicyCheckResultwithstatus: PASS; (b)failwith severity → correct severity mapping; (c)skipwith reason →SKIPPEDrecord; (d) inactive-for-TF guard emits theKYVERNO_INACTIVE_TF_STACKrecord.- Verify:
pytest tests/test_kyverno_adapter.pypasses.
Must-haves (Phase 42)
route_halt_artifactreal (SNS + outbox fallback); SNS topic in Terraform.hitl_gates.pyattests qa/prod/dr; SoD blocks on identity equality.attestation_matrix.pyimplements 8 concerns (offline-testable + signed artifacts).- Wiz adapter real client + graceful degrade.
- Kyverno translator fleshed out + inactive guard preserved.
- All 5 new test files pass;
run_ci.shexits 0.
Phase 43 — verify-review-audit-complete
Requirements: — (milestone gate)
Personas: lead-developer (lead), all personas (review participation)
Branch: phase/43-verify-review-audit-complete
Task 43.1 — 4-layer verify
- Structural: all new files present (environment.schema.json, 4 env files, 8 per-env contracts, hitl_gates.py, attestation_matrix.py, SNS topic in main.tf, 5+ new test files).
- Behavioral:
pytestpasses (count increases from v1.8's 350 by ~30+ new tests);run_ci.shexits 0;run_platform.sh --check-onlyexits 0. - Security: no hardcoded adapter defaults; HITL gates block on SoD violation; attestation matrix fails loud on missing evidence for prod/dr; Wiz degrades gracefully.
- Quality: each new feature has dedicated tests (interpolation, per-env jobs, SoD, HITL gates, attestation matrix, Wiz, Kyverno).
Task 43.2 — Multi-persona review
ciagent-reviewacross the v1.9 diff (phases 39–42). Auto-apply P0; flag P1+ for post-hoc.- Reconstruct
.ciagent/REVIEW.mdwith v1.9 content (D-086). Note that v1.3–v1.8 reviews were not persisted (no git-history rewrite).
Task 43.3 — Audit
- Reconstruction: git log matches
.ciagent/files. - File discipline: all
.ciagent/files valid. - Branch hygiene: stale branches cleaned.
- Commit discipline: all commits have
---ci---blocks.
Task 43.4 — Complete
- Update
.ciagent/REQUIREMENTS.md: mark REQ-100..REQ-111 complete; add v1.9 traceability table. - Update
.ciagent/ROADMAP.md: add v1.9 milestone section (complete). - Update
.ciagent/PROJECT.md: v1.9 status → complete. - Tag
v1.9.0; update floatingv1.9+v1tags. - Bump
uses:/ref:from@v1.6→@v1.9incontracts/*.yaml,deploy.ymlcheckoutref:,docs/CONSUMER_GUIDE.md(D-071 successor). - Commit:
docs(milestone): complete v1.9.
Must-haves (Phase 43)
- 4-layer verify PASS.
- Review: 0 new P0; P1+ flagged for post-hoc.
- Audit: clean.
- Tag
v1.9.0created; floating tags updated. uses:/ref:bumped to@v1.9.- REQUIREMENTS.md + ROADMAP.md + PROJECT.md updated.
End of PLAN.md.