Files
acdl/modules/STANDARDS.md
T
Jon Chery 7585c828f0
acdl-ci / Lint (push) Successful in 7s
acdl-ci / Test (push) Successful in 23s
acdl-ci / Platform check-only (offline) (push) Successful in 8s
docs(P48): vision gaps + badge system + substrate→engine + CR format + agentic tags
9 requirements implemented across presentation decks and project docs:

1. DX closing slide: added 'Infrastructure as a utility, not a craft' bullet
   to convey the full vision (infrastructure consumed, not maintained;
   platform compounds value over time).
2. PW Problem slide: 'moving a merged change' → 'promoting a change'.
3. PW Problem slide: added 'Red tape' and 'Scalability without increasing
   headcount' bullets (4 frictions, not 2).
4. PW Roadmap slide: redesigned with side-by-side HTML table layout
   (Testing | Planned), 16px font, no overflow.
5. PW deck: added new slide 'What This Platform Is — and Isn't' after North
   Star (sovereign boundary, infrastructure as utility, 4 anti-goals).
   PW deck now 16 slides (was 15).
6. Maturity nomenclature: 'Available today'/'shipped' → 'Testing' across
   both decks + source markdown. New .testing badge (blue/teal #DBEAFE).
   Roadmap title: 'Testing vs. Planned'. The platform has 0 consumer
   adoption — 'shipped' was inaccurate.
7. Global: 'substrate' → 'engine' across entire project (88 matches, 30+
   files including .ciagent/, docs/, modules/, adapters/, schemas/, code).
8. Presentation files only: 'forge' → 'VCS' / 'version control system'
   (6 occurrences in 4 files). 'forge' retained in all technical docs and
   code as the industry-standard term.
9. New .agentic badge (purple/violet #EDE9FE) appended to agentic features
   in both decks: confidence signal, autonomous dev, pattern recognition,
   dynamic module creation, citizen developer surface, auto-promotion.

Also: Change Request ID format changed from 'CR-2026-001' to 'CHG0678912'
across presentation files, consumer guide, and test fixtures.

HTML re-rendered. PPTX rendered for release upload.

---ci---
phase: 48
milestone: v1.9
status: complete
requirements:
  covered: []
  partial: []
---/ci---
2026-07-23 14:58:29 +00:00

25 KiB
Raw Permalink Blame History

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.yaml A minimal contract that uses the primitive with required inputs only.
examples/complex.yaml 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.yaml
    complex.yaml

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.yaml A minimal contract that uses the module with required inputs only.
examples/complex.yaml A contract that exercises optional inputs and feature flags.

Directory layout:

modules/l2/<name>/
  composition.json
  README.md
  examples/
    simple.yaml
    complex.yaml

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:

{
  "<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.yaml and examples/complex.yaml 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.yaml, examples/complex.yaml.
    • L2: composition.json, README.md, examples/simple.yaml, examples/complex.yaml (no instance.json).
  • interface.json (L1) / composition.json (L2) validates against schemas/stack.schema.json.
  • examples/simple.yaml and examples/complex.yaml 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.