# Nova Module Engineering Standards Standards for authoring and reviewing Nova 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// 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::` 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::`). | | `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 | `.` — the producing side. | | `to` | string | `.` — 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::`: - `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 is a **stateless assembler** (v1.11, D-098): it reads the registry, emits a root `main.tf` instantiating each L1 as `module "x" { source = "..." }` with resolved inputs and wired refs. There is no `TYPE_MAP` (deleted in the v1.11 stateless rewrite). A new stack type requires a `terraform/` dir in the L1 module + a registry entry with a `terraform_dir` field. ## 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// 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 | `@` 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": "", "to": ""}` with an optional `default` field for contract-input wires. Sources (the `from` side): | Source form | Meaning | |-------------|---------| | `contract.inputs.` | A value supplied by the consumer's contract YAML. | | `.outputs.` | An output produced by a child L1 module. | Targets (the `to` side): | Target form | Meaning | |-------------|---------| | `.inputs.` | An input on a child L1 module. | | `stack.outputs.` | A value the L2 exposes as a stack output. | Wires that source from `contract.inputs.` 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.` and its `from` is always `.outputs.`. ### 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 { "": { "": { "interface": "modules///", "published_at": "", "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. `# ` — 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. Stateless Assembler Pattern 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 "" { source = ... }` block with resolved inputs and wired refs. 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`. ### 8.1 Per-module terraform dir Each L1 module ships a `terraform/` subdir: ``` modules/l1//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 ``` **`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: ```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({ ... }) } ``` `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.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 "" { source = "" ... }` block. 3. Passes each input (except `region`, which is provider-level) as a module argument. For `ref:.` values, emits `module..` 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. 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`. **No adapter code changes.** The adapter is generic; it assembles any module that has a `terraform_dir` in the registry. ## 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`, `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`, no `terraform/`). - [ ] `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::`. - [ ] 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 `@` form referencing a registered L1. - [ ] No child references an L2 (no L3 in v1). - [ ] `wires[]` use the `contract.inputs.` / `.outputs.` → `.inputs.` / `stack.outputs.` forms. - [ ] `outputs[]` use `.outputs.` → `stack.outputs.`. - [ ] A `kms` child (`kms-key@`) 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 (stateless assembler) - [ ] The new primitive's `terraform/` subdir exists with `versions.tf`/`variables.tf`/`main.tf`/`outputs.tf` and passes `terraform init + validate` standalone. `locals.tf` is required for multi-resource modules; trivial single-resource modules (e.g. `kms-key`, `ecr`, `ecs-cluster`) may inline locals in `main.tf`. - [ ] `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 — the adapter emits a root `main.tf` with a `module "" { source = ... }` block). ### 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. --- ## 10. Policy Authoring Standard (v1.25) Module owners may ship per-module kyverno-json policies in `modules//policies/` (future convention; v1.25 policies live under `adapters/kyverno-json/policies/`). A policy file is a `ValidatingPolicy` resource (YAML or JSON). ### 10.1 Required fields - `apiVersion: json.kyverno.io/v1alpha1` - `kind: ValidatingPolicy` - `metadata.name` — matches the filename (e.g. `require-tags.json` → `name: require-tags`). This becomes the `ruleId` prefix `KJ_`. - `metadata.annotations["nova.cloudinit.dev/severity"]` — one of `critical`, `high`, `medium`, `low`, `info`. Drives the confidence signal's penalty mapping. - `spec.rules[].validate.assert` — an `all` or `any` list of assertion trees with JMESPath expressions. **No `forEach`, pattern operators, anchors, or wildcards** — use the `~` projection modifier to iterate. ### 10.2 Severity guidance | Severity | When to use | Confidence penalty | | --- | --- | --- | | `critical` | a violation makes the deploy unsafe (e.g. public ingress on a prod DB) | hard override (score = 0, block) | | `high` | a violation is a security or compliance gap (e.g. plaintext secrets) | -0.20 | | `medium` | a violation is a best-practice miss (e.g. missing tags) | -0.05 | | `low` | a violation is a style or convention issue | -0.01 | | `info` | a non-blocking observation (default) | 0.0 | ### 10.3 Assertion-tree patterns - **Iterate an array:** use the `~` modifier on the array key: ```yaml check: ~.resources: (@ < `5`): true ``` - **Match a resource type:** use the `match.any` block: ```yaml match: any: - type: aws:s3:bucket ``` - **Binding for descendant access:** use `->name`: ```yaml (bar + bat)->sum: ($sum): 10 ``` ### 10.4 Testing - Ship a fixture pair (`passing.json` + `failing.json`) under `tests/fixtures//`. - Add a test file `tests/test__policies.py` using the `KyvernoJsonEngine` (skip-without-kj pattern). - The regression gate (`pytest tests/`) must remain green.