Files
acdl/modules/STANDARDS.md
T
Jon Chery 031887ec56 refactor(P57): contract surface redesign + rename + .yml repo-wide
Contract surface redesign:
- New top-level fields: id (3-6 char acronym → stack.name), name (full → stack.title),
  infrastructure (map keyed by module name, replaces module:)
- Drop uses: field (dead reference; version pin lives in CI workflow uses: line)
- Drop top-level module/inputs (now nested under infrastructure map)
- Per-module optional version (defaults to latest published from registry)
- Multi-module contracts: one file deploys N modules in one pipeline run,
  resource IDs namespaced with module name to avoid collisions
- stack.schema.json: add optional title field for display name

Rename:
- pipelines/deploy.yaml → pipelines/contract.yml (declarative spec, not a pipeline)
- pipelines/ci.yaml → pipelines/ci.yml
- All 44 .yaml files → .yml repo-wide (contracts, module examples, kyverno policies)
- .acdl/contract.yaml → .acdl/contract.yml

Resolver (core/contract_resolver.py):
- Rewrite resolve() to loop infrastructure map, default version to latest,
  merge module fragments into one stack with namespaced resource IDs
- _latest_version() picks highest non-deprecated from registry
- _namespace_resources() prefixes IDs + rewrites ref: expressions for multi-module
- Single-module path: unprefixed IDs (backward compatible)

Verification:
- 494 tests pass (0 contract-shape failures)
- Local E2E passes (contract → resolver → adapter → local ECS HTTP 200 → outbox)

---ci---
project: acdl
phase: 57
milestone: v1.10.2
status: execute
---/ci---
2026-07-27 21:37:40 +00:00

588 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ACDL Module Engineering Standards
Standards for authoring and reviewing ACDL modules. These standards
govern the two module tiers — **L1 primitives** (single cloud resource
or small group of related resources) and **L2 modules** (compositions
that reference L1 primitives to deploy a complete stack) — and the
engine adapter that compiles them to Terraform. They are written for
**platform engineers** and **AI agents** that author or review new
modules against the existing corpus (12 L1 primitives and 2 L2 modules
shipped in v1.8).
A module that fails any section below is not ready to publish.
## 1. Overview
These standards codify the conventions already established by the
shipped modules (`s3`, `vpc`, `ecs-cluster`, `ecs-service`, `iam-role`,
`alb`, `ecr`, `cloudfront`, `waf`, `rds`, `kms-key`, `uptime`; the L2
modules `static-assets` and `microservice`). They exist so that:
- platform engineers can review a new module against a fixed checklist;
- AI agents authoring modules produce code that passes review without
iteration; and
- the engine adapter (`adapters/terraform/adapter.py`) can compile a
module instance with no module-specific code in the adapter beyond the
three tables in §8.
When this document and an existing module disagree, the existing module
is the authority for v1.x. A change to this document is a MINOR version
bump of the standards; a change that breaks shipped modules is a MAJOR
bump and requires a migration plan.
## 2. L1 Primitive Standards
An L1 primitive is a single cloud resource or a small group of related
resources (e.g. a VPC with subnets and a route table). It is declared by
an `interface.json` and realized by the engine adapter; it does not
own Terraform code.
### 2.1 Required files
Every L1 primitive MUST contain, at minimum:
| File | Purpose |
|------|---------|
| `interface.json` | Angine-agnostic declaration: inputs, outputs, NFRs, optional multi-resource graph. |
| `instance.json` | A concrete instance used as the adapter regression baseline. |
| `README.md` | Plain-language documentation following `README-TEMPLATE.md` (see §7). |
| `examples/simple.yml` | A minimal contract that uses the primitive with required inputs only. |
| `examples/complex.yml` | A contract that exercises optional inputs, NFRs, and (if applicable) the multi-resource graph. |
Directory layout:
```
modules/l1/<name>/
interface.json
instance.json
README.md
examples/
simple.yml
complex.yml
```
### 2.2 interface.json schema
`interface.json` MUST be a JSON object with the following required
fields:
| Field | Type | Constraint |
|-------|------|------------|
| `name` | string | `^[a-z][a-z0-9-]*$`; MUST match the module folder name. |
| `version` | string | Semver (`^\d+\d+\.\d+$`); MUST match the registry entry semver. |
| `kind` | string | Literal `"l1"`. |
| `type` | string | Stack type in `aws:<service>:<kind>` format (see §2.7). |
| `description` | string | One or two sentences in plain language; no Terraform jargon. |
| `inputs` | object | Keyed by input name; each value is an input declaration (§2.3). MAY be empty. |
| `outputs` | object | Keyed by output name; each value is an output declaration (§2.4). MAY be empty. |
| `nfrs` | object | Keyed by NFR name; each value is an NFR declaration (§2.5). MUST include `deletion_protection` and `encryption_enabled`. |
Optional fields for multi-resource primitives:
| Field | Type | Constraint |
|-------|------|------------|
| `resources` | array | One entry per distinct cloud resource; see §2.6. |
| `intra_refs` | array | Internal wiring between resources; see §2.6. |
A primitive that creates a single resource (e.g. `s3`, `iam-role`,
`rds`, `kms-key`) omits `resources` and `intra_refs`; its `type` field
is the single resource's stack type. A primitive that creates a small
group of related resources (e.g. `vpc`, `alb`, `cloudfront`) declares
`resources[]` with one entry per resource and `intra_refs[]` for the
internal wiring; its `type` field is the *primary* resource's stack
type.
### 2.3 Input declaration
Each entry in `inputs` is an object:
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `type` | string | yes | One of: `string`, `number`, `boolean`, `array`, `object`. |
| `description` | string | yes | Plain language; no Terraform jargon. |
| `required` | boolean | yes | `true` if the consumer MUST supply this input. |
| `default` | (any) | no | Present only when `required` is `false`. MUST match the declared `type`. |
| `enum` | array | no | Allowed values for `string`/`number` inputs (e.g. RDS `engine`). |
`region` is a required `string` input on every primitive that creates a
regional resource. Global resources (e.g. CloudFront) still declare
`region` because the provider region is used for child resources (the
OAC in the `cloudfront` case).
Every primitive that holds at-rest data MUST declare an optional
`kms_key_arn` input (`string`, `required: false`); see §4.
### 2.4 Output declaration
Each entry in `outputs` is an object:
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `type` | string | yes | `arn` for ARN outputs; `string` for all others. |
| `description` | string | yes | Plain language. |
Use `arn` (not `string`) for any output that returns an AWS ARN — the
adapter and policy engine key off the `arn` type to apply ARN-scoped
rules.
### 2.5 NFR declaration
Each entry in `nfrs` is an object:
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `type` | string | yes | One of: `string`, `number`, `boolean`. |
| `description` | string | yes | Plain language. |
| `default` | (any) | yes | MUST match the declared `type`. NFRs always have a default. |
Mandatory NFRs on every L1:
| NFR | Type | Default | Notes |
|-----|------|---------|-------|
| `deletion_protection` | boolean | `true` | See §5. |
| `encryption_enabled` | boolean | `true` | See §4. |
A primitive for which an NFR does not conceptually apply (e.g. an IAM
role has no at-rest data) still declares it with `default: true` and a
description noting the non-applicability, so the standards check and the
adapter emit logic stay uniform. The shipped `iam-role` primitive is the
reference for this case.
Additional NFRs are encouraged where they carry operational meaning
(e.g. `s3.versioning`, `rds.backup_retention_period`,
`kms-key.enable_rotation`, `vpc.flow_logs_encrypted`). Name them in
lowercase snake_case.
### 2.6 Multi-resource pattern
A primitive that creates more than one cloud resource (e.g. `vpc`
creates `aws_vpc` + `aws_subnet` + `aws_route_table`; `alb` creates
`aws_lb` + `aws_lb_target_group` + `aws_lb_listener`; `cloudfront`
creates `aws_cloudfront_distribution` +
`aws_cloudfront_origin_access_control`) declares a `resources` array.
Each `resources[]` entry:
| Field | Type | Notes |
|-------|------|-------|
| `type` | string | The resource's stack type (`aws:<service>:<kind>`). |
| `description` | string | Plain language. |
| `inputs` | array | Names (strings) of inputs from the top-level `inputs` object that this resource consumes. |
| `outputs` | array | Names (strings) of outputs from the top-level `outputs` object that this resource produces. |
The top-level `inputs`/`outputs` objects remain the single source of
truth; `resources[].inputs` and `resources[].outputs` are arrays of
*names* referencing those objects, not re-declarations.
`intra_refs[]` wires outputs of one resource to inputs of another
within the same primitive. Each entry:
| Field | Type | Notes |
|-------|------|-------|
| `from` | string | `<resource-type>.<output-name>` — the producing side. |
| `to` | string | `<resource-type>.<input-name>` — the consuming side. |
Reference: `cloudfront/interface.json` declares an intra-ref from
`aws:cloudfront:distribution.oac_id` to
`aws:cloudfront:originaccesscontrol.oac_id`; `vpc/interface.json`
declares intra-refs from the subnet and route table to the VPC's
`vpc_id`.
### 2.7 Naming and stack types
- Module folder names and `interface.json` `name` values MUST match
`^[a-z][a-z0-9-]*$` (lowercase, hyphenated, leading letter). Examples:
`s3`, `ecs-cluster`, `kms-key`, `iam-role`, `uptime`.
- Input and output names are lowercase snake_case.
- Stack types follow `aws:<service>:<kind>`:
- `aws:s3:bucket`
- `aws:ec2:vpc`, `aws:ec2:subnet`, `aws:ec2:routetable`
- `aws:ecs:cluster`, `aws:ecs:task_definition`, `aws:ecs:service`,
`aws:ecs:uptime-service`
- `aws:iam:role`
- `aws:elbv2:loadbalancer`, `aws:elbv2:listener`,
`aws:elbv2:targetgroup`
- `aws:ecr:repository`
- `aws:cloudfront:distribution`, `aws:cloudfront:originaccesscontrol`
- `aws:wafv2:webacl`
- `aws:rds:instance`
- `aws:kms:key`, `aws:kms:alias`
- The engine adapter's `TYPE_MAP` is the registry of stack types the
adapter can compile (see §8). A new stack type requires a `TYPE_MAP`
entry before the primitive can be deployed.
## 3. L2 Module Standards
An L2 module is a composition that references one or more L1 primitives
to deploy a complete stack (e.g. an ECS Fargate microservice, a static
asset site behind CloudFront + WAF). It is declared by a
`composition.json`; it does not own Terraform code and does not have an
`instance.json`.
### 3.1 Required files
| File | Purpose |
|------|---------|
| `composition.json` | The composition tree: children, wires, outputs, optional features. |
| `README.md` | Plain-language documentation following `README-TEMPLATE.md` (see §7). |
| `examples/simple.yml` | A minimal contract that uses the module with required inputs only. |
| `examples/complex.yml` | A contract that exercises optional inputs and feature flags. |
Directory layout:
```
modules/l2/<name>/
composition.json
README.md
examples/
simple.yml
complex.yml
```
There is no `instance.json` for an L2 module — the L2 is deployed by
resolving the composition tree to L1 instances at compile time, not by
loading a pre-baked instance.
### 3.2 composition.json schema
`composition.json` MUST be a JSON object with the following fields:
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `name` | string | yes | `^[a-z][a-z0-9-]*$`; matches the module folder name. |
| `version` | string | yes | Semver; matches the registry entry. |
| `kind` | string | yes | Literal `"l2"`. |
| `depth` | integer | yes | Literal `1` in v1 (see §3.5). |
| `description` | string | yes | Plain language. |
| `children` | array | yes | One entry per referenced L1 module (§3.3). |
| `wires` | array | yes | Wires from contract inputs / child outputs to child inputs / stack outputs (§3.4). |
| `outputs` | array | yes | Wires from child outputs to stack outputs (§3.4). |
| `features` | object | no | Feature flags propagated to children by the resolver (§3.6). |
### 3.3 Children
Each `children[]` entry:
| Field | Type | Notes |
|-------|------|-------|
| `id` | string | The child id, unique within the composition. `^[a-z][a-z0-9-]*$`. The id is the local name used in wires (e.g. `vpc`, `cluster`, `kms`). |
| `module` | string | `<name>@<semver>` referencing a registered L1 module. |
Children MUST reference L1 modules registered in `registry.json` (see
§6). The referenced semver MUST exist in the registry. An L2 MUST NOT
reference another L2 (no L3 in v1; see §3.5).
Reference: `microservice/composition.json` declares seven children
(`vpc`, `cluster`, `ecr`, `roles`, `alb`, `service`, `kms`), each
referencing an L1 at `@1.0.0`.
### 3.4 Wire format
A wire is a JSON object `{"from": "<source>", "to": "<target>"}` with an
optional `default` field for contract-input wires.
Sources (the `from` side):
| Source form | Meaning |
|-------------|---------|
| `contract.inputs.<name>` | A value supplied by the consumer's contract YAML. |
| `<childId>.outputs.<name>` | An output produced by a child L1 module. |
Targets (the `to` side):
| Target form | Meaning |
|-------------|---------|
| `<childId>.inputs.<name>` | An input on a child L1 module. |
| `stack.outputs.<name>` | A value the L2 exposes as a stack output. |
Wires that source from `contract.inputs.<name>` MAY carry a `default`
value used when the consumer omits the input. Reference:
`microservice/composition.json` wires `contract.inputs.bucket_name` to
`vpc.inputs.cidr` with `default: "10.0.0.0/16"` (a historical quirk
preserved for regression).
The `outputs[]` array uses the same wire shape but its `to` is always
`stack.outputs.<name>` and its `from` is always
`<childId>.outputs.<name>`.
### 3.5 Maximum depth
`depth` is `1` for every L2 in v1. The composition tree is strictly L2
→ L1: an L2 may reference only L1 primitives, never another L2. There
is no L3 in v1. The stack schema permits `depth` up to 5 for forward
compatibility, but the v1 resolver and adapter only handle depth 1.
### 3.6 Feature flags
An L2 MAY declare a `features` object. Two flags are defined in v1:
| Flag | Type | Default | Effect |
|------|------|---------|--------|
| `deletion_protection` | boolean | `true` | When `true`, the resolver propagates `deletion_protection: true` to every child's NFRs. When `false`, children are deployed with `deletion_protection: false` (used by decommission; see §5). |
| `uptime_enabled` | boolean | `true` | When `true`, the uptime monitoring L1 is deployed after the L2 module in a separate terraform state. When `false`, the uptime deployment is skipped. |
Feature flags are propagated to children by the resolver; the L2
`composition.json` does not need to wire them explicitly as inputs. The
resolver reads `features` and injects the corresponding NFR/input on
each child.
## 4. Encryption by Default
Encryption is mandatory and on by default across the platform.
1. Every L1 MUST declare an `encryption_enabled` NFR (boolean, default
`true`) in `interface.json`. See §2.5.
2. Every L1 that holds at-rest data (S3, RDS, ECR, ECS task
definition env, VPC flow logs, CloudWatch log groups) MUST declare an
optional `kms_key_arn` input (`string`, `required: false`). When
supplied, the adapter wires it to the resource's KMS encryption
argument.
3. L2 modules MUST wire a per-stack customer-managed KMS key to all
children that accept `kms_key_arn`. The KMS key is a `kms-key` child
of the L2 — one key per L2 deployment, no shared keys. Reference:
both `static-assets` and `microservice` declare a `kms` child
(`kms-key@1.0.0`) and wire `kms.outputs.kms_key_arn` to every child
that accepts a CMK.
4. For a standalone L1 deployment (an L1 used outside an L2), if the
consumer does not supply `kms_key_arn`, the adapter falls back to the
AWS-managed default key for that service and emits a warning to
stderr. The primitive is still encrypted; only the key manager
differs.
5. The `kms-key` primitive enables key rotation by default
(`enable_rotation` NFR, default `true`), and the adapter emits
`enable_key_rotation = true` on the `aws_kms_key` resource.
A primitive that does not hold at-rest data (e.g. `iam-role`,
`ecs-cluster`, `alb`) still declares `encryption_enabled` for standards
uniformity (see §2.5) but does not declare `kms_key_arn`.
## 5. Deletion Protection by Default
Deletion protection is mandatory and on by default to prevent
accidental teardown of production infrastructure.
1. Every L1 MUST declare a `deletion_protection` NFR (boolean, default
`true`) in `interface.json`. See §2.5.
2. When `deletion_protection` is `true`, the engine adapter emits a
`lifecycle { prevent_destroy = true }` block on the corresponding
Terraform resource. A `terraform destroy` against a protected
resource fails with an error naming the resource.
3. L2 modules expose `features.deletion_protection` (default `true`).
The resolver propagates the flag to every child's NFRs (see §3.6).
4. **Decommission mode.** To tear down a stack that was deployed with
deletion protection, the consumer sets `inputs.deletion_protection:
false` on the contract (or `features.deletion_protection: false` on
an L2) and re-applies. The decommission transform
(`decommission_transform`) zeroes capacity counts (e.g. ECS desired
count to 0, RDS allocated storage to the minimum) so that the
subsequent `destroy` applies against a quiesced stack. The transform
is applied by the resolver before the adapter emits resources.
## 6. Registry
Every module — L1 and L2 — MUST be registered in
`modules/registry.json` at its semver. The registry is the source of
truth for what is published; the adapter and resolver refuse to compile
a module that is not registered.
Registry entry shape:
```json
{
"<module-name>": {
"<semver>": {
"interface": "modules/<l1|l2>/<module-name>/<interface.json|composition.json>",
"published_at": "<ISO 8601 timestamp>",
"deprecated": false
}
}
}
```
- `interface` is the path (relative to the repo root) to the module's
interface file — `interface.json` for an L1, `composition.json` for
an L2.
- `published_at` is an ISO 8601 timestamp. Use a full
`YYYY-MM-DDTHH:MM:SSZ` form; do not omit the seconds or the timezone
designator.
- `deprecated` is `false` for a live module. A MAJOR version bump does
not delete the old entry; it flips `deprecated` to `true` and starts a
12-month deprecation window (see §7 Versioning).
A new semver of an existing module is a new key under the module's
object; old semvers are retained. The registry is append-only for
published semvers — a published semver is never edited or deleted.
## 7. README Standards
Every module README MUST follow the structure of
`modules/README-TEMPLATE.md`. Required sections, in order:
1. `# <name> — <plain-language description>` — title with the module
name and a one-line description.
2. `## Overview` — one or two sentences in plain language.
3. `## Resources` — a table of the Terraform resources the module
creates (L1) or the primitives it references (L2).
4. `## Inputs` — a table: `| Name | Type | Required | Default | Description |`.
5. `## Outputs` — a table: `| Name | Type | Description |`.
6. `## NFRs` — a table: `| Name | Type | Default | Description |`.
`deletion_protection` and `encryption_enabled` are mandatory NFRs
for every L1; they MUST appear in this table.
7. `## Usage` — a concrete snippet showing how a consumer references
the module in a contract.
8. `## Compliance extension points` — resources or behaviors that could
be added for the future compliance milestone (GDPR, SOX, SOC2,
DORA). Not implemented yet; listed so the redesign can plan for them.
9. `## Examples` — links to `examples/simple.yml` and
`examples/complex.yml` with a one-line description of each.
10. `## Versioning` — the module's semver policy: interface MAJOR,
behavior MINOR, lifecycle PATCH. MAJOR bumps require a new
`registry.json` entry (immutable publication); old entries enter a
12-month deprecation window.
An L2 README's `## Resources` section lists the referenced L1 children
rather than Terraform resources, and its `## Inputs`/`## Outputs`
sections reflect the contract inputs and stack outputs of the
composition.
## 8. Adapter Extension 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.
### 8.1 The three tables
| 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. |
Reference: `adapter.py:26` (`TYPE_MAP`), `adapter.py:51` (`INPUT_MAP`),
`adapter.py:75` (`OUTPUT_MAP`).
### 8.2 Specialized `_emit_resource` branches
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:
- `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`.
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`.
### 8.3 Adding a new L1 to the adapter
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.
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.
## 9. Code Review Checklist
Use this checklist when reviewing a new module (L1 or L2). Every box
must be checked before the module is registered and published.
### 9.1 Files and structure
- [ ] All required files present:
- L1: `interface.json`, `instance.json`, `README.md`,
`examples/simple.yml`, `examples/complex.yml`.
- L2: `composition.json`, `README.md`, `examples/simple.yml`,
`examples/complex.yml` (no `instance.json`).
- [ ] `interface.json` (L1) / `composition.json` (L2) validates against
`schemas/stack.schema.json`.
- [ ] `examples/simple.yml` and `examples/complex.yml` validate
against `schemas/contract.schema.json`.
- [ ] Module registered in `modules/registry.json` at its semver with a
full ISO 8601 `published_at` and `deprecated: false`.
### 9.2 Interface (L1)
- [ ] `name` matches the folder name and `^[a-z][a-z0-9-]*$`.
- [ ] `version` is semver and matches the registry entry.
- [ ] `kind` is `"l1"`.
- [ ] `type` follows `aws:<service>:<kind>`.
- [ ] Every input has `type`, `description`, `required`; optional inputs
carry a `default` of the correct type; `enum` present where the value
set is constrained.
- [ ] Every output has `type` (`arn` for ARNs, `string` otherwise) and
`description`.
- [ ] `nfrs` includes `deletion_protection` (boolean, default `true`)
and `encryption_enabled` (boolean, default `true`).
- [ ] `kms_key_arn` input present if the primitive holds at-rest data.
- [ ] Multi-resource primitives declare `resources[]` (with `inputs`/
`outputs` as arrays of names) and `intra_refs[]` with `{from, to}`.
### 9.3 Composition (L2)
- [ ] `kind` is `"l2"` and `depth` is `1`.
- [ ] Every `children[]` entry is `{id, module}` with `module` in
`<name>@<semver>` form referencing a registered L1.
- [ ] No child references an L2 (no L3 in v1).
- [ ] `wires[]` use the `contract.inputs.<name>` /
`<childId>.outputs.<name>``<childId>.inputs.<name>` /
`stack.outputs.<name>` forms.
- [ ] `outputs[]` use `<childId>.outputs.<name>`
`stack.outputs.<name>`.
- [ ] A `kms` child (`kms-key@<semver>`) is present and its
`kms_key_arn` output is wired to every child that accepts a CMK.
- [ ] `features` (if present) only uses defined flags
(`deletion_protection`, `uptime_enabled`).
### 9.4 Adapter
- [ ] `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 `instance.json` round-trips through the
adapter without error (regression baseline).
### 9.5 README and docs
- [ ] README follows `README-TEMPLATE.md` with all required sections in
order (§7).
- [ ] `## NFRs` table lists `deletion_protection` and
`encryption_enabled` for an L1.
- [ ] `## Compliance extension points` lists at least one plausible
future extension.
### 9.6 Tests
- [ ] A test is added for the new primitive covering adapter emission
(the Terraform output for `instance.json` matches the expected
fixture) and interface validation (`interface.json` validates against
`stack.schema.json`).
- [ ] For an L2, a test is added that the composition resolves to the
expected set of L1 instances and that the adapter emits a root module
calling the L1 modules.