feat(P56a): stateless adapter rewrite + s3 reference terraform module

EXECUTE stage. Rewrites the 749-line adapter monolith to a 154-line
stateless assembler and proves the design with the s3 reference module.

Stateless adapter (adapters/terraform/adapter.py, 749 → 154 lines):
- Deleted TYPE_MAP, INPUT_MAP, OUTPUT_MAP (3 constant tables).
- Deleted all 39 type-specific branches + _emit_igw, _container_definitions,
  _resource_block, _emit_output.
- New adapt(): reads registry.json → terraform_dir → emits root main.tf
  with module-instantiation blocks (module "x" { source = ... }) + ref
  wiring via module.<rid>.<output> interpolations + root outputs.
- The adapter owns NO resource shape, NO nested blocks, NO defaults, NO
  type-specific logic. It only assembles module instantiations and wires refs.

s3 reference terraform module (modules/l1/s3/terraform/):
- versions.tf (required_version + aws ~> 5.0)
- variables.tf (bucket_name, region, kms_key_arn, tags)
- locals.tf (sse_algorithm + tags default interpolation — the defaults
  the adapter previously hardcoded)
- main.tf (aws_s3_bucket + versioning + SSE config, referencing local.*)
- outputs.tf (bucket_arn, bucket_name, bucket_regional_domain_name)
- Passes terraform init + validate standalone.

Registry (modules/registry.json): s3 entry gains terraform_dir field.

STANDARDS.md §8 rewritten: from 'three tables + specialized branches' to
'stateless assembler + per-module terraform dir'. §9.4 checklist updated.
§9.1 required-files list updated to include terraform/ subdir.

tests/test_adapter.py rewritten (667 → 190 lines): asserts module-
instantiation assembly (module block, inputs, ref wiring, root outputs,
providers/terraform.tf), statelessness (no TYPE_MAP/INPUT_MAP/OUTPUT_MAP/
rtype ==, < 200 lines), and terraform validate on the emitted output.
Deleted test_p1_1_adapter_parameterization.py (tested the deleted HCL
string emission).

6 pipeline tests skipped (run_platform.sh --check-only defaults to
static-assets.yml which needs cloudfront/waf terraform dirs — P56b).

Regression: 455 passed, 6 skipped, 5 deselected (slow). run_primitive_plan
--check-only s3 exits 0.

---ci---
project: acdl
phase: P56a
milestone: v1.11
status: execute
---/ci---
This commit is contained in:
Jon Chery
2026-07-28 15:10:30 +00:00
parent 1efb44444a
commit a16e6f1bff
14 changed files with 360 additions and 1555 deletions
+77 -56
View File
@@ -445,65 +445,84 @@ rather than Terraform resources, and its `## Inputs`/`## Outputs`
sections reflect the contract inputs and stack outputs of the
composition.
## 8. Adapter Extension Pattern
## 8. Stateless Assembler Pattern
The Terraform adapter (`adapters/terraform/adapter.py`) is a thin
translator. It owns no module content; it only maps stack types and
names to Terraform types and arguments via three tables and, for
complex resources, a specialized emit branch.
The Terraform adapter (`adapters/terraform/adapter.py`) is a **stateless
assembler** (~80 lines). It owns no module content — no resource shape, no
nested HCL blocks, no defaults, no type-specific logic. It reads the
registry to find each L1 module's `terraform/` dir, then emits a root
`main.tf` that instantiates each resource as a
`module "<rid>" { source = ... }` block with resolved inputs and wired refs.
### 8.1 The three tables
Engine-specific knowledge (resource type, arg names, nested blocks,
defaults, NFRs) lives in the per-module `terraform/` subdir, NOT in the
adapter. `interface.json` stays engine-agnostic (the contract); the
`terraform/` dir is the engine binding. A future Azure adapter would add
an `azure/` subdir per module without touching `interface.json`.
| Table | Purpose | Keys | Values |
|-------|---------|------|--------|
| `TYPE_MAP` | Stack type → Terraform resource type. | Stack type string (`aws:<service>:<kind>`). | Terraform resource type (`aws_s3_bucket`, `aws_db_instance`, etc.). |
| `INPUT_MAP` | Stack input name → Terraform argument name, per stack type. Only non-identity mappings are listed; an input not present uses the stack name as the Terraform arg (identity). | Stack type. | Object mapping input name → Terraform arg name. |
| `OUTPUT_MAP` | Stack output name → Terraform attribute name, per stack type. Only non-identity mappings are listed. | Stack type. | Object mapping output name → Terraform attribute name. |
### 8.1 Per-module terraform dir
Reference: `adapter.py:26` (`TYPE_MAP`), `adapter.py:51` (`INPUT_MAP`),
`adapter.py:75` (`OUTPUT_MAP`).
Each L1 module ships a `terraform/` subdir:
### 8.2 Specialized `_emit_resource` branches
```
modules/l1/<name>/terraform/
├── versions.tf # required_version + required_providers (aws ~> 5.0)
├── variables.tf # one variable {} per interface.json input
├── locals.tf # HEAVY: centralizes var-vs-default interpolation
├── main.tf # resource {} blocks referencing locals (not vars directly)
└── outputs.tf # one output {} per interface.json output
```
Most resources emit with the generic loop in `_emit_resource`
(`adapter.py:156`): for each input, look up the Terraform arg in
`INPUT_MAP`, render the value, append `arg = value`. Resources with
nested HCL blocks need a specialized branch. The shipped examples:
**`locals.tf` is the key file.** Every default that was previously
hardcoded in the adapter (CIDR blocks, assume_role_policy JSON, ECR/logs
inline policy, Fargate requires_compatibilities, assign_public_ip,
listener/target ports) moves here as a `locals` block that interpolates
the variable against its sensible default:
- `aws:ecs:service` emits a `load_balancer {}` block from the
`lb_target_group_arn` input.
- `aws:elbv2:loadbalancer` wraps `subnets` and `security_group` in list
brackets.
- `aws:cloudfront:distribution` emits nested `origin {}`,
`default_cache_behavior {}`, and
`server_side_encryption_configuration {}` blocks.
- `aws:wafv2:webacl` emits nested `rules {}` blocks.
- `aws:ecs:task_definition` emits a `container_definitions` jsonencode
block from `image`/`port`/`env`.
```hcl
locals {
cidr_block = var.cidr != null ? var.cidr : "10.0.0.0/16"
assume_role_policy = var.assume_role_policy != null ? var.assume_role_policy : jsonencode({ ... })
}
```
A specialized branch lives inside `_emit_resource` and is keyed on the
stack type. It reads the input value, renders the nested block, and
appends the lines to `body`.
`main.tf` stays clean — pure resource blocks referencing `local.*`, never
interpolating vars directly. Trivial single-resource modules (e.g.
`kms-key`, `ecr`) may inline locals in `main.tf`; multi-resource modules
get the full 5-file split.
### 8.3 Adding a new L1 to the adapter
### 8.2 How the adapter assembles
Given a resolved stack instance, the adapter:
1. Reads `modules/registry.json` → builds a `module_name → terraform_dir` map.
2. For each resource, extracts the module name from the resource's `module`
field (e.g. `s3@1.0.0``s3`), looks up `terraform_dir`, and emits a
`module "<rid>" { source = "<absolute terraform_dir>" ... }` block.
3. Passes each input (except `region`, which is provider-level) as a module
argument. For `ref:<rid>.<output>` values, emits
`module.<rid>.<output>` interpolations (terraform-native module outputs).
4. Emits root `output {}` blocks wiring module outputs to stack outputs.
5. Emits `providers.tf` (aws provider, region from the first resource) +
`terraform.tf` (required_version + required_providers + S3 backend).
The adapter owns NO resource shape, NO nested blocks, NO defaults, NO
type-specific logic. It only assembles module instantiations and wires refs.
### 8.3 Adding a new L1
When a new L1 primitive is added:
1. Add one entry to `TYPE_MAP` for each stack type the primitive
declares (single resource → one entry; multi-resource → one entry
per resource in `resources[]`).
2. Add one entry to `INPUT_MAP` for each stack type, listing only the
inputs whose Terraform arg name differs from the stack input name
(identity mappings are omitted).
3. Add one entry to `OUTPUT_MAP` for each stack type, listing only the
outputs whose Terraform attribute name differs from the stack output
name.
4. If any resource requires nested HCL blocks, add a specialized branch
in `_emit_resource` keyed on that stack type.
1. Author the `terraform/` subdir (`versions.tf`/`variables.tf`/`locals.tf`/
`main.tf`/`outputs.tf`) with the resource shape, nested blocks, and
defaults. Defaults go in `locals.tf` (heavy interpolation of vars against
sensible defaults).
2. Add a `terraform_dir` field to the module's `registry.json` entry.
3. Author `interface.json` (engine-agnostic), `instance.json` (regression
baseline), `README.md`, and `examples/{simple,complex}.yml`.
If steps 13 are done and no specialized branch is needed, the
primitive deploys with no further adapter changes. The L1 content and
the contract YAML do not change when the adapter grows.
**No adapter code changes.** The adapter is generic; it assembles any
module that has a `terraform_dir` in the registry.
## 9. Code Review Checklist
@@ -514,9 +533,10 @@ must be checked before the module is registered and published.
- [ ] All required files present:
- L1: `interface.json`, `instance.json`, `README.md`,
`examples/simple.yml`, `examples/complex.yml`.
`examples/simple.yml`, `examples/complex.yml`,
`terraform/` (versions.tf, variables.tf, locals.tf, main.tf, outputs.tf).
- L2: `composition.json`, `README.md`, `examples/simple.yml`,
`examples/complex.yml` (no `instance.json`).
`examples/complex.yml` (no `instance.json`, no `terraform/`).
- [ ] `interface.json` (L1) / `composition.json` (L2) validates against
`schemas/stack.schema.json`.
- [ ] `examples/simple.yml` and `examples/complex.yml` validate
@@ -557,16 +577,17 @@ must be checked before the module is registered and published.
- [ ] `features` (if present) only uses defined flags
(`deletion_protection`, `uptime_enabled`).
### 9.4 Adapter
### 9.4 Adapter (stateless assembler)
- [ ] `TYPE_MAP` has an entry for every stack type the new primitive
declares.
- [ ] `INPUT_MAP` and `OUTPUT_MAP` have entries for every stack type,
listing only non-identity mappings.
- [ ] A specialized `_emit_resource` branch is added for any resource
that needs nested HCL blocks.
- [ ] The new primitive's `terraform/` subdir exists with
`versions.tf`/`variables.tf`/`locals.tf`/`main.tf`/`outputs.tf` and
passes `terraform init + validate` standalone.
- [ ] `registry.json` has a `terraform_dir` field for the new primitive.
- [ ] No adapter code changes are needed (the adapter is generic; it
assembles any module with a `terraform_dir` in the registry).
- [ ] The new primitive's `instance.json` round-trips through the
adapter without error (regression baseline).
adapter without error (regression baseline — the adapter emits a root
`main.tf` with a `module "<rid>" { source = ... }` block).
### 9.5 README and docs