Files
acdl/.ciagent/RESEARCH.md
T
Jon Chery 5dc97673e5 docs(P00): research findings — v1.24 env-transition detect-and-destroy
---ci---
project: acdl
phase: 0
milestone: v1.24
status: research
---/ci---
2026-08-12 14:22:37 +00:00

9.4 KiB

Nova — v1.24 Research Findings

Phase: research (pre-execution). Milestone: v1.24 (Consumer Guide Accuracy & Env-Promotion Lifecycle Enforcement). Status: research. Researcher: ci-researcher. Autonomy: full.

1. Problem domain

Two distinct problem spaces in one milestone:

1a. Consumer guide accuracy (docs)

The consumer guide (docs/consumer-guide.md, 477 lines) has 5 accuracy defects identified in review:

  1. Step 3 contract fields table (lines 141-147) lists uses, module, environment, inputs. The actual schema (schemas/contract.schema.json:7) requires id, name, environment, infrastructure. The uses field was dropped in v1.10.2 (REQ-50 superseded) and module was replaced by the infrastructure map key. The worked examples (lines 111-137) use the correct fields.

  2. Step 4 caller (lines 173-183) omits environment: in with:, while Step 2 (lines 94-101) shows environment: dev. The two canonical caller snippets disagree.

  3. Step 5 stage 8 (lines 232, 253) says "(dev only)" for the apply stage. Higher environments do apply — they apply after HITL attestation per docs/environments/index.md:44-54.

  4. Step 8 (lines 290-306) says "Change environment in your contract" to promote, which contradicts the same doc's "Per-environment deployment" section (lines 398-402): "you do not edit the environment: field… Promotion = running the matching job." The test test_consumer_guide_states_no_field_editing asserts the no-editing model.

  5. Reference table (lines 329-330) says sample contracts "use @v1.19" but the sample contracts (contracts/static-assets.yml, contracts/microservice.yml) don't carry uses: — the version pin lives in the caller workflow.

1b. Environment-promotion lifecycle enforcement (feat)

Root cause confirmed by code inspection:

  • adapters/terraform/adapter.py:129 sets the Terraform state key to: spike/{stack_name}/{environment}/terraform.tfstate
  • stack_name = contract["id"] (stable across env changes, per core/contract_resolver.py:584).
  • When a consumer edits environment: from devqa on the same contract id, the state key changes from spike/assets/dev/ to spike/assets/qa/. Terraform initializes a fresh state file in the new env's state path. The prior env's resources remain live in AWS with their state file untouched. No destroy ever runs. This orphans resources.

The user's binding directive: Editing environment: on a stable contract.id is a valid promotion path (Shape A). The platform must destroy the prior env's resources before building the new env. There must be no path that orphans resources — fail closed if the destroy fails.

2. Existing codebase structure (integration points)

DynamoDB nova-contracts table (the prior-env source of truth)

  • Table: nova-contracts (env var CONTRACTS_TABLE, default nova-contracts). Defined in core/lambda/contract_ingestor.py:25.
  • Schema: PK consumerRepo (S), SK contractId#submittedAt (S). Attributes: contractId, contract, environment, status, submittedAt.
  • Written by: _submit_contract() at core/lambda/contract_ingestor.py:135-176. The consumer's deploy workflow submits the contract via the Lambda Function URL.
  • Read pattern for env-transition: Query by PK consumerRepo + SK begins_with contractId# + FilterExpression status = "submitted" → sort by submittedAt desc → take the latest → read its environment. This is the last-submitted env. For the last-applied env, a new #LAST_APPLIED SK suffix is added (REQ-283).
  • Test pattern: tests/test_contract_ingestor.py:74-110 (moto_contracts_table fixture) uses moto mock_aws to create the table. The env-transition tests will mirror this pattern.

Outbox writer (evidence events)

  • core/outbox_writer.py writes hash-chained evidence events to nova-outbox table. PK contractId, SK eventType#eventTs.
  • The env-transition destroy step emits a nova.env.destroyed event via this writer (REQ-284c). Pattern: build an event dict with contractId, eventType: "ENV_DESTROYED", environment: <prior_env>, ts, then call write_event().

Contract resolver (environment override)

  • core/contract_resolver.py:460-483 resolve() accepts environment_override — when set, it overrides the contract's environment field before schema validation and interpolation (D-088). This is the mechanism the destroy step uses to re-resolve the contract against the prior env: resolve(contract, env_override=prior_env).
  • Already tested in tests/test_deploy_workflow_env_input.py:35-52.

run_platform.sh (where Step 0b goes)

  • scripts/run_platform.sh is the platform pipeline. Step 0 (lines 222-239) is the environment onboarding check. Step 1 (lines 241-249) is contract validation. Step 0b goes between them (after onboarding, before validation).
  • The destroy step mirrors the existing --destroy mode (lines 376-394): terraform init -reconfigure + terraform destroy -auto-approve. The difference: it runs against the prior env's state key, not the current one.
  • CONTRACT_ID is already set at line 217 (NOVA_CONTRACT_ID with a default). CONSUMER_REPO needs to be derived from GITHUB_REPOSITORY or a new NOVA_CONSUMER_REPO env var (REQ-286).

deploy.yml (consumer repo → platform)

  • .github/workflows/deploy.yml:132 runs bash platform/scripts/run_platform.sh $MODE_FLAG $ENV_FLAG "${{ inputs.contract }}".
  • To pass NOVA_CONSUMER_REPO, add NOVA_CONSUMER_REPO=${{ github.repository }} as an env var on the "Run the platform pipeline" step (REQ-286).

Test patterns

  • Shell script assertions: tests/test_pipeline.py:79-95 reads the script text and asserts substrings. The env-transition tests follow this pattern for run_platform.sh.
  • DynamoDB mocking: tests/test_contract_ingestor.py:74-110 uses moto mock_aws + boto3.client + create_table. The env-transition tests follow this pattern.
  • Consumer guide assertions: tests/test_consumer_guide_per_env_section.py reads docs/consumer-guide.md text and asserts substrings. The updated tests follow this pattern.

3. Persona assessment (v1.24)

This milestone has two distinct work territories:

  1. Docs (P1): docs/consumer-guide.md edits + test updates. This is lead-developer territory (narrative/docs + test assertions).
  2. Platform code (P2): core/env_transition.py (new), scripts/run_platform.sh edits, .github/workflows/deploy.yml edit, adapters/terraform/adapter.py doc comment. This is backend-engineer territory (Python + bash + YAML).
  3. Tests (P3): tests/test_env_transition.py (new), tests/test_run_platform_env_transition.py (new), tests/test_consumer_guide_per_env_section.py updates. Split: backend-engineer for the env_transition + pipeline tests; lead-developer for the consumer guide test updates.

Roster: lead-developer (docs + guide tests) + backend-engineer (Python + bash + YAML + pipeline tests). frontend-engineer stays deactivated (no UI). data-engineer not needed (no schema changes — the nova-contracts table already exists with the right shape; we only add a new SK suffix). No new personas.

4. Key decisions logged

ID Decision Confidence Source
D-201 Both promotion shapes supported (A: edit+destroy, B: per-env callers) 0.95 User directive
D-202 Prior-env source of truth = nova-contracts DynamoDB table 0.85 User directive + code inspection
D-203 Detect-and-destroy at pipeline start (Step 0b) 0.85 User directive
D-204 Fail closed on destroy failure (no orphan path) 0.95 User directive
D-205 Cross-account destroy out of scope (same-account only) 0.80 CLARIFY A3
D-206 Env-transition destroy is NOT the HITL decommission pipeline 0.85 CLARIFY A5
D-207 Last-applied env recorded via #LAST_APPLIED SK in nova-contracts 0.85 RESEARCH §2
D-208 State key spike/{id}/{env}/ stays as-is (correct for both shapes) 0.90 RESEARCH §1b

5. Pitfalls

  1. Destroy needs the prior env's Terraform config, not the new env's. The destroy step must re-resolve the contract with environment_override=prior_env so the emitted TF matches the prior env's resources. If we resolve with the new env, the destroy plan won't match the prior state → terraform tries to create, not destroy.
  2. terraform init -reconfigure is required when switching state backends between envs (if envs use different state buckets). The -reconfigure flag tells Terraform to forget the previous backend config.
  3. The deletion_protection NFR (REQ-86) blocks destroy. The destroy step must resolve with deletion_protection: false injected (same as decommission Step 2 in scripts/run_decommission.sh:34-37). Without this, terraform destroy fails on prevent_destroy lifecycle blocks.
  4. DynamoDB may not be reachable in local/CI mode. The detect step must handle ClientError / EndpointNotFound gracefully → log warning
    • return None (conservative). This is documented in REQ-282.
  5. The consumer guide test test_consumer_guide_states_no_field_editing will fail after the Step 8 rewrite. It must be updated in the same phase as the guide edit (P1) or the test suite breaks.