# 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 `dev` → `qa` 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: `, `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.