---ci--- project: acdl phase: 0 milestone: v1.17 status: research ---/ci---
86 KiB
Nova — v1.11 RESTART Research Findings
Phase: research (pre-Phase 56). Milestone: v1.11 (RESTART). Status: research. Researcher: ci-researcher. Autonomy: full (CLARIFY auto-resolved; all binding decisions D-097..D-107 are committed in the CLARIFY stage). Branch:
milestone/v1.11-restart(branched off tagv1.10.2, per D-097). Sources: ACDL codebase (v1.10.2 tree) + git history (failed first attempt onphase/56-iam-re-bootstrap+phase/57-live-deploy-microservice) + the CLARIFY commit (80b7286).This file overwrites the prior v1.1 research artifact. v1.11 is a fresh milestone; the v1.1 research (Gitea OIDC, Checkov, IR shape, outbox) is historical and preserved in git history. This file documents the technical findings that ground the v1.11 restart plan.
Background — why v1.11 is a restart
v1.11 is a restart, not a continuation. The first attempt (phase/56 +
phase/57, abandoned per D-097) made five defects worse, not better. The
restart branches off the clean v1.10.2 tag and corrects three structural
defects that the CLARIFY stage locked as binding decisions:
- Stateless adapter (D-098, D-099, D-100). The current adapter is a
750-line monolith with 3 constant tables and 39 type-specific branches
that duplicate what
interface.jsonalready declares and hardcode defaults that belong in the module. v1.11 makes it a ~80-line stateless assembler; each L1 ships a realterraform/module dir that owns its resource shape, nested blocks, and defaults. - Terraform owns lifecycle (D-101). The first attempt added a Python
script (
verify_deploy_microservice.py) that ranterraform init -reconfigurein a fresh temp dir each time, which contributed to the 4-VPC bug. v1.11 deletes that script;run_platform.shgains--applyand--destroymodes; Python never runs terraform. - Pipeline-driven testing (D-102, D-103, D-104). No per-module
Python/pytest. A modules-lifecycle pipeline matrix-runs each L1
module's
examples/{simple,complex}.ymlcontracts through apply→modify→destroy against live AWS. The "test" = the pipeline cell going green.
FINDING 1 — Adapter monolith audit
1.1 The three constant tables
adapters/terraform/adapter.py (750 lines on the v1.10.2 tree) is built
around three constant tables:
| Table | Line | What it encodes | Entries |
|---|---|---|---|
TYPE_MAP |
26 | Stack type (aws:<service>:<kind>) → Terraform resource type (aws_s3_bucket, aws_vpc, …). |
19 |
INPUT_MAP |
51 | Stack input name → Terraform arg name, per stack type. Only non-identity mappings are listed; an input not present uses the stack name as the Terraform arg (identity). | 19 (one per stack type) |
OUTPUT_MAP |
75 | Stack output name → Terraform attribute name, per stack type. Only non-identity mappings. | 19 (one per stack type) |
Why they duplicate interface.json. Each L1 module already declares
its inputs, outputs, and stack type in interface.json (engine-agnostic).
The three tables are the engine binding — the Terraform-specific name
mappings that interface.json deliberately omits (it is engine-agnostic
per ARCHITECTURE.md §12). The duplication is therefore intentional in
the original design: the adapter was meant to be a thin translator that
holds the engine binding in three tables, and the L1 holds the
engine-agnostic content.
The drift. What was not intended is that the tables grew into 39
type-specific branches (§1.2) that hardcode resource shapes, nested HCL
blocks, and defaults (§1.3) — content that belongs in the module, not the
adapter. The adapter stopped being a thin translator and became a
per-resource-type code generator. D-098 corrects this: the engine binding
moves into a per-module terraform/ subdir (the real Terraform module),
and the adapter becomes a stateless assembler that emits
module "x" { source = "..." ... } blocks. The three tables are deleted.
1.2 The 39 type-specific branches across 18 stack types
_emit_resource (line 156) is a generic loop that, for each input, looks
up the Terraform arg in INPUT_MAP, renders the value, and appends
arg = value. But 18 of the 19 stack types have a specialized branch
inside _emit_resource that runs after the generic loop and emits nested
HCL blocks, hardcoded defaults, or resource-specific wiring. The count of
39 branches is the sum of the per-type specializations (some types have
2–3 branches). The full inventory:
| # | Stack type | Terraform type | Specialized logic (what the branch does) |
|---|---|---|---|
| 1 | aws:s3:bucket |
aws_s3_bucket |
versioning {} block (default true); server_side_encryption_configuration {} block (SSE-KMS, CMK ref or managed-key fallback with stderr warning); kms_key_arn is not a bare arg — emitted as the SSE block. |
| 2 | aws:ec2:vpc |
aws_vpc |
tags { Name = ... } from the name input; hardcoded cidr_block = "10.0.0.0/16" default when the L2 doesn't supply a CIDR (line 288). |
| 3 | aws:ec2:subnet |
aws_subnet |
vpc_id = aws_vpc.vpc-vpc.id hardcoded ref when not in inputs; hardcoded cidr_block = "10.0.1.0/24" default (line 296); tags { Name = ... }. |
| 4 | aws:ec2:routetable |
aws_route_table |
vpc_id = aws_vpc.vpc-vpc.id hardcoded ref; route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.vpc-igw.id } hardcoded default route; tags { Name = "<name>-rt" }. |
| 5 | aws:ecs:cluster |
aws_ecs_cluster |
Hardcoded name = "acdl-microservice" default when not in inputs (line 300). |
| 6 | aws:ecs:task_definition |
aws_ecs_task_definition |
_container_definitions() helper: jsonencodes image/port/env into a container_definitions block; hardcoded family = "app" default (line 279). |
| 7 | aws:ecs:service |
aws_ecs_service |
network_configuration {} block (subnets + security_groups wrapped in list brackets); load_balancer {} block from lb_target_group_arn with hardcoded container_name = "app" + container_port = 8080; hardcoded desired_count = 1, launch_type = "FARGATE", task_definition = aws_ecs_task_definition.service-task-definition.arn, name = "acdl-microservice". |
| 8 | aws:iam:role |
aws_iam_role |
managed_policy_arns = [...] from comma-separated string; hardcoded ECS task execution assume_role_policy JSON when not supplied (line 326–331); hardcoded name = "acdl-microservice-role" default. |
| 9 | aws:elbv2:loadbalancer |
aws_lb |
subnets/security_group wrapped in list brackets; hardcoded load_balancer_type = "application" default. |
| 10 | aws:elbv2:listener |
aws_lb_listener |
default_action { type = "forward" target_group_arn = aws_lb_target_group.alb-targetgroup.arn } hardcoded; load_balancer_arn = aws_lb.alb-loadbalancer.id hardcoded ref. |
| 11 | aws:elbv2:targetgroup |
aws_lb_target_group |
Hardcoded target_type = "ip", vpc_id = aws_vpc.vpc-vpc.id, protocol = "HTTP", port = 8080. |
| 12 | aws:ecr:repository |
aws_ecr_repository |
Hardcoded name = "acdl-microservice" default; encryption_configuration {} block (not a bare kms_key_arn arg). |
| 13 | aws:cloudfront:distribution |
aws_cloudfront_distribution |
origin {} block (origin_id, domain_name, origin_access_control_id, s3_origin_config {}); default_cache_behavior {} block (viewer_protocol_policy, target_origin_id, ttls, allowed/cached methods); enabled = true; price_class; restrictions { geo_restriction {} }; viewer_certificate { cloudfront_default_certificate = true }; web_acl_id from WAF ref. ~8 nested blocks. |
| 14 | aws:cloudfront:originaccesscontrol |
aws_cloudfront_origin_access_control |
name; hardcoded origin_access_control_origin_type = "s3", signing_behavior = "always", signing_protocol = "sigv4". |
| 15 | aws:wafv2:webacl |
aws_wafv2_web_acl |
name; hardcoded scope = "CLOUDFRONT"; default_action {} (allow/block from input, default allow); visibility_config {}; custom rule {} blocks as nested HCL (P1-4 fix) or default AWS-managed-rules block. ~5 nested blocks. |
| 16 | aws:rds:instance |
aws_db_instance |
NFR-derived backup_retention_period (default 7), deletion_protection (default true); storage_encrypted = true default; skip_final_snapshot = true (dev safety). |
| 17 | aws:kms:key |
aws_kms_key |
NFR-derived enable_key_rotation = true default. |
| 18 | aws:ecs:uptime-service |
aws_ecs_service |
Feature-flag gate (returns "" when disabled); container_definitions jsonencode for uptime-kuma; hardcoded subnets = ["subnet-uptime"], security_groups = ["sg-uptime"], assign_public_ip = true; hardcoded desired_count = 1, launch_type = "FARGATE". |
Plus a global prevent_destroy lifecycle block emitted for every resource
when nfrs.deletion_protection is true (line 576–581), and the
_emit_igw() helper that synthesizes an internet gateway + route table
association from the VPC resource (line 585).
1.3 Hardcoded defaults that belong in the module
The defaults below are emitted by the adapter when the L2 composition does
not supply the input. They are resource shape decisions — CIDR ranges,
trust policies, network config — that belong in the module's locals.tf
(D-100), not in the adapter. The adapter should pass only resolved contract
inputs; if a default is wrong, fix the module, not the adapter.
| Default | Adapter line | What it is | Where it belongs |
|---|---|---|---|
cidr_block = "10.0.0.0/16" |
288 | VPC CIDR default | modules/l1/vpc/terraform/locals.tf |
cidr_block = "10.0.1.0/24" |
296 | Subnet CIDR default | modules/l1/vpc/terraform/locals.tf |
ECS task execution assume_role_policy JSON |
326–331 | Trust policy for the IAM role | modules/l1/iam-role/terraform/main.tf (or locals.tf) |
ECR/logs inline policy / encryption_configuration {} |
304–315, 380 | ECR KMS encryption block | modules/l1/ecr/terraform/main.tf |
Fargate requires_compatibilities / launch_type = "FARGATE" |
261–263 | ECS launch config | modules/l1/ecs-service/terraform/locals.tf |
assign_public_ip (uptime) |
565 | ECS network config | modules/l1/uptime/terraform/main.tf |
Listener/target ports (port = 8080, container_port = 8080) |
201, 348 | ALB + ECS container ports | modules/l1/alb/terraform/locals.tf + modules/l1/ecs-service/terraform/locals.tf |
Security group emission (security_groups = [...]) |
255–258, 564 | ECS network config | modules/l1/ecs-service/terraform/main.tf |
name = "acdl-microservice" (cluster, ECR, service) |
265, 300, 303 | Resource name defaults | modules/l1/*/terraform/locals.tf |
family = "app" |
279 | Task definition family | modules/l1/ecs-service/terraform/locals.tf |
target_type = "ip", protocol = "HTTP" |
345, 347 | ALB target group defaults | modules/l1/alb/terraform/locals.tf |
load_balancer_type = "application" |
342 | ALB type default | modules/l1/alb/terraform/locals.tf |
desired_count = 1 |
260 | ECS desired count | modules/l1/ecs-service/terraform/locals.tf |
WAF scope = "CLOUDFRONT", managed-rules default block |
421, 472–490 | WAF defaults | modules/l1/waf/terraform/main.tf |
CloudFront signing_behavior = "always", signing_protocol = "sigv4", origin_type = "s3" |
365–367 | OAC defaults | modules/l1/cloudfront/terraform/main.tf |
CloudFront viewer_certificate { cloudfront_default_certificate = true }, restrictions {} |
403–410 | Distribution defaults | modules/l1/cloudfront/terraform/main.tf |
RDS backup_retention_period = 7, skip_final_snapshot = true |
497, 506 | RDS defaults | modules/l1/rds/terraform/locals.tf |
KMS enable_key_rotation = true |
510 | KMS rotation default | modules/l1/kms-key/terraform/main.tf |
prevent_destroy = true lifecycle (global) |
576–581 | Deletion protection | Each module's main.tf (or a shared lifecycle.tf) |
1.4 Why this is a drift from the original vision
ARCHITECTURE.md §12.2 states: "The adapter is a thin layer; it does not
own L1/L2 content — it only translates." STANDARDS.md §8 (line 448–506)
documents the intended design: "a thin translator with 3 tables +
specialized branches." The drift was baked into the standards doc
itself — §8.2 explicitly blesses "specialized _emit_resource branches"
for "resources with nested HCL blocks" and §8.3 step 4 instructs module
authors to "add a specialized branch in _emit_resource keyed on that
stack type" when a new L1 needs nested blocks.
The result: every new L1 with a nested block (CloudFront, WAF, ECS,
uptime) added 30–80 lines of resource-shape code to the adapter. The
adapter grew from a spike-era ~150 lines to 750 lines, with the resource
shape (CIDR ranges, trust policies, container ports, managed-rule sets)
encoded as Python string concatenation rather than Terraform HCL. D-098
corrects the drift: the standards doc §8 must be rewritten to document the
new pattern (per-module terraform/ subdir + stateless assembler), and
the "specialized branch" guidance is removed.
Confidence: 0.95. The audit is a direct line-by-line read of the
v1.10.2 adapter.py; the drift is structural and unambiguous.
FINDING 2 — State-key root cause of the 4-VPC bug
2.1 The state key
adapter.py line 664 + 676:
stack_name = stack.get("name", "spike")
terraform_tf = (
...
f' key = "spike/{stack_name}/terraform.tfstate"\n'
...
)
core/contract_resolver.py line 569:
"stack": {
"name": contract["id"],
...
}
So stack_name = contract["id"] and the state key is
spike/{contract.id}/terraform.tfstate.
2.2 The 5 microservice contracts
All five microservice contracts share id: msvc and differ only in
environment:
| Contract file | id |
environment |
|---|---|---|
contracts/microservice.yml |
msvc |
dev |
contracts/microservice.dev.yml |
msvc |
dev |
contracts/microservice.qa.yml |
msvc |
qa |
contracts/microservice.prod.yml |
msvc |
prod |
contracts/microservice.dr.yml |
msvc |
dr |
The state key does not include the environment. So all four
environment contracts (dev/qa/prod/dr) collide on the same state key:
spike/msvc/terraform.tfstate.
2.3 The two root causes
Root cause 1 — the adapter emits per-contract state keys with no VPC
sharing. The microservice composition (modules/l2/microservice/ composition.json) includes a vpc child (vpc@1.0.0). Every contract
that resolves through this composition emits its own VPC resource. There
is no platform VPC to share; each contract deploys its own VPC. D-105
corrects this: terraform/platform owns ONE VPC; the microservice
composition drops its vpc child and references the platform VPC via a
data source. The standalone vpc L1 module stays (consumers deploy their
own VPCs). No per-contract VPC ever again.
Root cause 2 — the state key does not distinguish environments. Because
the state key is spike/{contract.id}/terraform.tfstate and all four env
contracts share id: msvc, every environment's terraform apply writes to
the same remote state key. Combined with the first attempt's
verify_deploy_microservice.py running terraform init -reconfigure in a
fresh temp dir each time, each run created a fresh local state that
diverged from the remote key. The first run (dev) created VPC #1 and
pushed it to spike/msvc/terraform.tfstate. The second run (qa) ran
-reconfigure in a fresh temp dir, pulled the remote state (which had
dev's VPC), but because the local state was fresh and the composition
emitted a new VPC resource address, terraform saw the VPC as "to add"
again — creating VPC #2 and overwriting the remote state. Repeating for
prod and dr created VPCs #3 and #4. Four VPCs, one state key, no
environment discrimination.
D-106 corrects this: the composition must be deterministic — same contract
→ same resolved stack → same state key, every time. State keys become
env-aware and stable across apply/modify/destroy:
spike/{id}/{env}/terraform.tfstate. The environment is part of the key,
so dev/qa/prod/dr never collide.
2.4 Why -reconfigure in a fresh temp dir made it worse
terraform init -reconfigure forces terraform to re-read the backend
config and pull remote state into the local working directory. When the
working directory is a fresh temp dir (as verify_deploy_microservice.py
did), there is no local .terraform/ state cache — terraform must pull
the remote state fresh. If the remote state key is shared across
environments (root cause 2) and the composition emits a new VPC each time
(root cause 1), the -reconfigure pull merges the prior environment's
state with the new resource addresses, and the subsequent apply creates a
new VPC because the resource address in the new composition run differs
from the one in the remote state (the L2 namespacing or the fresh temp dir
caused terraform to treat the VPC as a new resource). D-101 deletes
verify_deploy_microservice.py entirely; run_platform.sh gains
--apply and --destroy modes that run terraform in a stable working
directory (not a fresh temp dir per run), and Python never runs terraform.
Confidence: 0.90. The state-key derivation is a direct code read (adapter.py:664,676 + contract_resolver.py:569). The 5 contracts are read verbatim. The 4-VPC mechanism is the only consistent explanation for the observed symptom (4 VPCs in the account after 4 env runs). The 0.10 residual is for the possibility that the resource-address divergence was caused by a separate composition-namespacing bug rather than the fresh temp dir alone — but either way, the two root causes (shared state key + per-contract VPC) are confirmed and D-105/D-106 correct both.
FINDING 3 — Per-module terraform module design
3.1 What the per-module terraform/ subdir should contain
D-098/D-099: each L1 module ships a real terraform/ module dir. The
canonical layout for a multi-resource module:
modules/l1/<name>/
interface.json # engine-agnostic (unchanged)
instance.json # regression baseline (unchanged)
README.md
examples/
simple.yml
complex.yml
terraform/ # NEW — the engine binding
versions.tf # required_version + required_providers
variables.tf # from interface.json inputs
locals.tf # default interpolation (heavy use, D-099)
main.tf # resource blocks (resource shape + nested blocks)
outputs.tf # from interface.json outputs
Trivial single-resource modules (e.g. s3) may inline locals in
main.tf (D-099). Multi-resource modules (vpc, ecs-service, alb,
microservice-shaped) get the full split.
3.2 The three reference modules (from interface.json)
s3 (modules/l1/s3/interface.json):
variables.tf:bucket_name(string, required),region(string, required),kms_key_arn(string, optional).locals.tf:sse_algorithm = "aws:kms", versioning defaulttrue, managed-key fallback (alias/aws/s3whenkms_key_arnis null), theprevent_destroylifecycle.main.tf:resource "aws_s3_bucket" "this" { bucket = var.bucket_name ... }+versioning {}block +server_side_encryption_configuration {}block (CMK ref or managed fallback).outputs.tf:bucket_arn(→aws_s3_bucket.this.arn),bucket_name(→aws_s3_bucket.this.id),bucket_regional_domain_name(→aws_s3_bucket.this.bucket_regional_domain_name).versions.tf:terraform { required_version = ">= 1.9, < 1.10" required_providers { aws = { source = "hashicorp/aws", version = "~> 5.0" } } }.
vpc (modules/l1/vpc/interface.json — multi-resource: vpc + subnet +
routetable):
variables.tf:cidr(string, required),azs(string, required),name(string, required),region(string, required).locals.tf:cidr_block = coalesce(var.cidr, "10.0.0.0/16"), subnet CIDR derivation (cidrsubnets(local.cidr_block, 8, 8, ...)per AZ),nametag interpolation, the IGW + route table association.main.tf:aws_vpc,aws_subnet(count/for_each overazssplit),aws_route_table,aws_internet_gateway,aws_route_table_association— all the resources that the adapter's_emit_igw()helper synthesized dynamically now live here as real HCL.outputs.tf:vpc_id,subnet_ids(join the subnet ids).versions.tf: same provider block.
ecs-service (modules/l1/ecs-service/interface.json — multi-resource:
task_definition + service):
variables.tf:image,port,cpu(default 256),memory(default 512),env(optional),cluster_arn,subnets,security_group,lb_target_group_arn(optional),region,kms_key_arn(optional),desired_count(default 1),launch_type(default "FARGATE"),family(default "app").locals.tf:container_definitionsjsonencode (image/port/env/cpu/ memory),requires_compatibilities = ["FARGATE"]when launch_type is FARGATE, log group name + KMS ref, theprevent_destroylifecycle.main.tf:aws_ecs_task_definition(family, container_definitions, requires_compatibilities, execution_role_arn) +aws_ecs_service(name, cluster, task_definition, desired_count, launch_type, network_configuration {}, load_balancer {} block).outputs.tf:service_arn,task_def_arn.versions.tf: same provider block.
3.3 How the stateless adapter assembles them
The new adapter (D-098) is a ~80-line stateless assembler. It:
- Reads
modules/registry.json→ for each resource in the resolved stack instance, looks up the L1 module bymodulefield (<name>@<semver>). - Gets the
terraform_dirfrom the registry entry (or derives it asmodules/l1/<name>/terraform/). - Emits a root
main.tfwith onemodule "x" { source = "<terraform_dir>" ... }block per resource, passing the resolved contract inputs as module arguments. - Wires refs via
module "x".<output>interpolations: aref:<id>.<out>input value becomesmodule.<id>.<out>in the consuming module block. - Emits the stack-level
output {}blocks (passthrough from the producing module's outputs). - Emits
terraform.tf(backend config with the env-aware state key, D-106) +providers.tf(aws provider, region from the first resource).
The adapter holds no TYPE_MAP, INPUT_MAP, OUTPUT_MAP, and no
type-specific branches. The engine binding (stack type → Terraform resource
type, input → arg name, output → attribute name, nested blocks, defaults)
lives entirely in the per-module terraform/ subdir. interface.json
stays engine-agnostic.
Confidence: 0.90. The module layout is grounded in the existing
interface.json files (read verbatim) and the Terraform module convention
(versions/variables/locals/main/outputs split). The assembler design is
D-098/D-099 (user-confirmed). The 0.10 residual is for the exact
terraform_dir registry field shape (not yet implemented) and the
ref-wiring syntax (module.<id>.<out> vs a locals alias).
FINDING 4 — Existing pipeline architecture
4.1 The central pipeline contract
pipelines/contract.yml is the declarative deployment pipeline spec (a
contract, not an executable workflow). It declares 9 stages:
validate-contract → resolve-stack → terraform-plan → checkov →
confidence → apply (dev only) → publish-outputs → deploy-uptime →
comment-outputs. Each stage has name, command, required (bool),
and optional description. The executable workflow
(.github/workflows/deploy.yml + .gitea/workflows/deploy.yml,
byte-identical) implements these stages by invoking
scripts/run_platform.sh. Validated against
schemas/deploy-pipeline.schema.json.
4.2 The plan-only pipelines (existing, run on every PR)
Two platform pipelines run on every PR to main (offline, free):
| Pipeline | File | Matrix | What it does |
|---|---|---|---|
| Primitives plan | .github/workflows/primitives-plan.yml (+ .gitea/ byte-identical) |
s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf, rds (10 primitives) |
For each L1 primitive, runs bash scripts/run_primitive_plan.sh --check-only <primitive> — resolves the primitive's instance.json, runs the adapter, validates the emitted Terraform structure (offline, no AWS). |
| Patterns plan | .github/workflows/patterns-plan.yml (+ .gitea/ byte-identical) |
static-assets, microservice (2 modules) |
For each L2 module, runs bash scripts/run_pattern_plan.sh --check-only <module> — resolves the sample contract, runs the adapter, validates the emitted Terraform (offline). |
Both trigger on pull_request: branches: [main], run on ubuntu-latest,
install jsonschema pyyaml boto3. The --check-only mode is offline (no
AWS, no Checkov, no DynamoDB) — it resolves the contract/instance, runs
the adapter, and validates the emitted Terraform file structure. This is
what makes the pipelines free.
4.3 run_platform.sh — plan only, never apply/destroy
scripts/run_platform.sh (521 lines) has three modes today:
--check-only(offline, no AWS): contract → resolver → adapter → stream TF → validate → exit 0.--plan-only(requires AWS): contract → resolver → adapter →terraform init -reconfigure -lock=false→terraform validate→terraform plan -lock=false -out=tfplan→ exit 0 (line 274–297).- default (requires AWS + Checkov + DynamoDB): contract → resolver →
adapter →
terraform plan→ Checkov → confidence → outbox.
Critically, line 287 runs terraform plan only. There is no
terraform apply and no terraform destroy in run_platform.sh today.
The apply stage in pipelines/contract.yml (line 54–57) declares
command: bash scripts/run_platform.sh --plan-only — a misnomer; it runs
plan, not apply. The lifecycle modes (--apply, --destroy) must be
added (D-101). Python never runs terraform; run_platform.sh is the
only shell entry point.
4.4 run_primitive_plan.sh
scripts/run_primitive_plan.sh (65 lines) runs the platform pipeline for
a single primitive. --check-only mode: resolves instance.json, runs
the adapter, validates the emitted {main.tf,terraform.tf,providers.tf}
exist and main.tf is non-empty. Default mode (requires AWS): terraform init -backend=false → terraform validate → terraform plan. This is
the per-primitive plan check that the primitives-plan pipeline matrix
invokes.
4.5 The byte-identical Gitea+GitHub convention
pipelines/README.md:22 documents the convention: "Create byte-identical
workflow YAMLs in .gitea/workflows/<name>.yml and
.github/workflows/<name>.yml." Both workflows must implement the same
stages, commands, triggers, and runner declared in the contract.
tests/test_pipeline_contract.py validates that the Gitea and GitHub
workflow YAMLs are byte-identical and conform to the schema. The only
difference is the forge runtime (Gitea Actions vs GitHub Actions). The
new modules-lifecycle pipeline (D-102) must follow this convention:
byte-identical .gitea/workflows/modules-lifecycle.yml +
.github/workflows/modules-lifecycle.yml.
Confidence: 0.95. All pipeline files are read verbatim from the
v1.10.2 tree. The "plan only, never apply/destroy" finding is a direct
read of run_platform.sh line 287 + the --plan-only exit at line 293.
FINDING 5 — PERSONAS.md update for v1.11
The existing PERSONAS.md (v1.9) has 6 active personas:
lead-developer, backend-engineer, platform-engineer (custom),
security-engineer (custom), lambda-engineer (custom, v1.9),
frontend-engineer. v1.11 changes the roster:
- Deactivate
lambda-engineer— no per-module Python this milestone (D-102: testing is pipeline-driven, not pytest). The v1.9 Lambda (core/lambda/contract_ingestor.py) persists but is not touched in v1.11. - Deactivate
cost-engineer— not in the v1.9 roster (the v1.9data-engineeris already deactivated). v1.11 has no cost-engineer work; cost is documented inCOST.md(REQ-119) by the lead-developer. - Keep
backend-engineer— owns the adapter rewrite (stateless assembler) +core/contract_resolver.py(env-aware state keys, D-106). - Keep
data-engineer(reactivated) — ownsterraform/(platform VPC, D-105) + the per-moduleterraform/subdirs (the engine binding, D-098/D-099/D-100). This is the heaviest territory in v1.11: 12 L1 modules each get a realterraform/module dir. - Keep
general(thelead-developer+backend-engineerpipeline work) — ownspipelines/+.gitea/workflows/+.github/workflows/(the modules-lifecycle pipeline, D-102) +scripts/run_platform.sh(--apply/--destroymodes, D-101).
Territory alignment (v1.11)
| Persona | Territory | Domain |
|---|---|---|
| backend-engineer | adapters/terraform/adapter.py (rewrite to stateless assembler), core/contract_resolver.py (env-aware state keys), schemas/stack.schema.json (if touched) |
backend |
| data-engineer | terraform/ (platform VPC, D-105), modules/l1/*/terraform/ (per-module terraform subdirs — the engine binding), modules/l1/*/interface.json (defaults move from adapter to interface), modules/registry.json (terraform_dir field) |
data |
| general (lead-developer + backend-engineer) | pipelines/modules-lifecycle.yml, .gitea/workflows/modules-lifecycle.yml + .github/workflows/modules-lifecycle.yml (byte-identical), scripts/run_platform.sh (--apply/--destroy), scripts/run_primitive_plan.sh (if extended), modules/STANDARDS.md §8 rewrite |
coordination + pipelines |
Territory enforcement: warn
Co-authoring is expected on the adapter + run_platform.sh boundary
(backend-engineer rewrites the adapter; general adds the lifecycle modes
to run_platform.sh that invoke it). warn keeps it frictionless —
cross-territory edits are logged in the commit message but do not fail
the task.
Domain priority (v1.11)
data → backend → general
Rationale: the terraform foundation (per-module terraform/ subdirs +
platform VPC) is the binding constraint — the stateless adapter cannot be
written until the reference s3 module exists (D-107: P56a proves the
design with s3 first). Backend (adapter/resolver) follows once the module
shape is proven. General (pipelines/workflows) wires the lifecycle modes
last, once the adapter + modules produce valid terraform.
The updated PERSONAS.md is written to /root/acdl/.ciagent/PERSONAS.md
(see that file). YAML frontmatter with active, phase_specific, and
reason fields per persona.
Confidence: 0.90. The persona changes are grounded in the CLARIFY decisions (D-098..D-107) and the v1.11 scope (no per-module Python → lambda-engineer deactivated; terraform module authoring is the heaviest work → data-engineer reactivated).
Assumptions logged
| ID | Assumption | Confidence | Rationale |
|---|---|---|---|
| A-1.1 | The terraform_dir field will be added to modules/registry.json entries (or derived as modules/l1/<name>/terraform/) so the stateless adapter can locate each module's terraform subdir. |
0.85 | D-098 says the adapter reads registry.json → gets terraform_dir. The exact field name is not yet locked; the derivation path is the obvious fallback. |
| A-1.2 | The ref-wiring syntax in the root main.tf will be module.<id>.<output> (standard Terraform module output interpolation), not a locals alias. |
0.85 | The existing _ref_expr already produces <tf_type>.<id>.<attr>; the module equivalent is module.<id>.<output>. Standard Terraform convention. |
| A-2.1 | The 4-VPC bug's resource-address divergence was caused by the fresh temp dir + -reconfigure pull merging remote state with new composition runs, not a separate composition-namespacing bug. |
0.80 | The two confirmed root causes (shared state key + per-contract VPC) are sufficient to explain 4 VPCs. The exact terraform-state mechanics of the divergence are inferred, not observed in a debug log. |
| A-3.1 | Trivial single-resource modules (s3) may inline locals in main.tf; multi-resource modules (vpc, ecs-service, alb) get the full 5-file split. |
0.90 | D-099 states this explicitly. |
| A-4.1 | The modules-lifecycle pipeline will matrix-run each L1 module's examples/{simple,complex}.yml contracts (the modify variants), not new contract files. |
0.90 | D-103: "Uses the module's own existing example contracts as the modify variants. No extra contract files needed." |
| A-5.1 | platform-engineer and security-engineer from the v1.9 roster are folded into data-engineer and backend-engineer for v1.11 (the v1.11 scope is terraform + adapter + pipelines, not security adapters or HITL gates). |
0.75 | The v1.11 scope (D-097..D-107) does not touch Wiz/Kyverno/Checkov/HITL. The persona roster is simplified to the three active domains. |
Decisions surfaced (research → already bound in CLARIFY)
All v1.11 binding decisions (D-097..D-107) were committed in the CLARIFY
stage (80b7286) before this research ran. This research grounds those
decisions with codebase evidence; it does not surface new binding
decisions. The decisions are summarized in §Background above and
documented in full in the CLARIFY commit.
v1.12 Addendum — Presentation Refinement Research
Generated: 2026-07-29. Phase 66. Milestone v1.12. Mode: docs-only NFR milestone focused on the leadership decks. Surface:
docs/presentations/(PW + DX, all four layers) + one real adapter fix + two probe fixes required to make deck claims true.
Background — why v1.12 exists
v1.11 (P56a–P65) landed the stateless adapter, pipeline-driven
lifecycle testing, single platform VPC, COST.md, PRE_MORTEM.md, and
a teardown to zero-cost. P65's plan (REQ-118) required the decks to be
rewritten to "Verified live-aws via lifecycle pipeline; torn down to
zero-cost." That rewrite did not fully land on the deck artifacts. This
research is a drift audit: a systematic comparison of the deck artifacts
against the v1.11-verified reality.
FINDING 1 — Drift audit (9 items)
Systematic comparison of docs/presentations/* against
.ciagent/CAPABILITY_INVENTORY.md, .ciagent/COST.md,
.ciagent/PRE_MORTEM.md, .ciagent/ROADMAP.md, and git log.
- Wrong verification status. Both rendered HTML decks still say
"6 cloud capabilities are design-verified + locally emulated,
deploy-unverified (IAM drift)" (PW "Testing vs. Planned" slide;
DX slide A6).
CAPABILITY_INVENTORY.mdsays 22/22 Verified and the IAM-drift framing was removed in P65. The decks contradict the inventory. Verified:grep -c "deploy-unverified\|IAM drift\|design-verified" docs/presentations/*.html→ 3 hits per deck. - Re-verification header stale. Both source
.mdheaders say "Re-verification (2026-07-27)… v1.10 Phase 54… 16/16… 6 IAM-gated escalated." Should reflect v1.11: 22/22 Verified, torn down. - Road to the North Star diagram stale.
docs/presentations/assets/mmd/road-to-north-star.mmdshows v1.10 as "NEXT" with "HITL wiring / all-runner OIDC / regulatory ledger". v1.11 is complete; the diagram must advance. - Rendered HTML not re-rendered.
git logshows the HTML was last touched at10b87a6(P57), before v1.11. P65's "re-render HTML" task did not reach the rendered artifacts. - Version refs stale. Decks reference
@v1.10in deploy.ymluses:snippets (Safe Promotion Path, Safe Decommission). Ship tag is nowv1.11.0; will bev1.12.0at Phase 70 complete. - Cost story has no real numbers.
COST.mdexists ($0.001883 over 8 days, ~$0.007/mo, S3-dominated, zero BAU compute) but the decks' A6 "Operating Model & Cost" slide is generic prose with no figures. - Pre-mortem unreferenced. P65 planned to add a pre-mortem
reference;
PRE_MORTEM.mdexists (v1.10 decay root cause + four forward failure modes) but no deck slide references it. - Two v1.11 stories absent. (a) Architectural simplicity: adapter
918→~80 lines, defaults centralized in per-module
terraform/dirs. (b) Verifiable deploys: amodules-lifecyclepipeline matrix-runs each module apply→modify→destroy against live AWS. Neither is in the decks. - Duplicated story-beat lines.
how-the-platform-works.mdslides 3–10 each repeat their intro line twice (a copy-paste artifact).
FINDING 2 — Regression gate surfaces real decay (D-091)
The v1.12 regression gate run (Phase 66) re-ran the D-091 regression
gate to back every deck claim. It found 3 Broken capabilities:
{'Verified': 19, 'Decayed': 0, 'Broken': 3}.
CAP-013 — live-aws — REAL platform defect (Class A)
adapters/terraform/adapter.py:159-172 (the seen dedup loop)
collapses the two ecs-service sub-resources (service-task-definition
service-service, both moduleecs-service@1.0.0) into ONEmodule "service-task-definition"block. But the stack outputservice_arn(resolverfrom: "service-service") is emitted asvalue = module.service-service.service_arn— referencing a module call that was never emitted.terraform validatefails: "No module call name." The same defect silently breaks thealbL1 too. Static- assets (CAP-014) doesn't hit it because its L1s are single-resource. Classification A — real platform defect. The adapter produces invalid Terraform for any multi-resource L1 with stack-level outputs. Fix required before decks can claim 22/22 Verified.
CAP-017 — lifecycle-pipeline — regression-probe bug (Class B/C)
core/regression_verify.py:444 hardcodes
required = ["versions.tf", "variables.tf", "locals.tf", "main.tf", "outputs.tf"]. The CAP-017 probe targets the rds L1 module, whose
main.tf uses only var.* and aws_db_subnet_group.this — no local.*
references, so locals.tf is legitimately absent. The probe is over-
strict. The rds module is correctly structured; the capability works.
Classification B/C — trivial probe fix. Drop locals.tf from the
required list, or make it conditional on local. usage.
CAP-018 — lifecycle-pipeline — regression-probe bug (Class B/C)
core/local_emulators.py:273 defines LocalLambdaStub as a dataclass
with one required field outbox: FlatFileOutbox. Every real caller
passes it (core/local_emulators.py:464, the tests). The CAP-018 probe
at core/regression_verify.py:486-491 is the only caller that
instantiates it bare: LocalLambdaStub() → TypeError. The probe was
added in P63 and never aligned with the real signature. The capability
is exercised green by CAP-011. Classification B/C — trivial probe
fix. Pass an outbox to the constructor.
Implication for the decks
CAPABILITY_INVENTORY.md claims 22/22 Verified, but the regression
gate (D-091 — the exact mechanism PRE_MORTEM.md FM-3 says backs every
deck claim) shows CAP-013 is genuinely broken. The inventory
overstates. v1.12 cannot ship decks claiming 22/22 until CAP-013 is
fixed and the gate re-runs clean. This is the structural mitigation the
pre-mortem requires (verified-only claims; decks unfrozen only after
re-verification). The user decision: fix the defect inside v1.12
(Phase 67), then the decks can honestly claim 22/22.
FINDING 3 — Talking points structure gap
Both talking-points files have only 5 appendix sections (A1–A5) while the Marp decks have 6 (A6 = "Operating Model & Cost"). The A6 content exists in the Marp deck and source markdown but was never distilled into the talking points. The re-distill step (Phase 69) must add the A6 section to both talking-points files.
FINDING 4 — Versioning facts
- Current ship tag:
v1.11.0(v1.11 complete). deploy.ymlstill referencesv1.9in comments +ref: v1.9— v1.11 apparently did not bump the deploy workflowuses:tag (the bump is a separate concern; decks use the current ship tag).- Decks should show
@v1.11in examples (current state); Phase 70 bumps to@v1.12after the tag exists.
Assumptions logged
- No automated
ci-doc-verifierscript exists in the repo. The plan's "ci-doc-verifier confirms" is satisfied by a manual grep-based verification recorded in the Phase 70 VERIFY step (consistent with how prior NFR-patch phases handled it). Confidence 0.90 — verified byls scripts/ | grep docandgrep -rl deck tests/. - PPTX export requires Chromium + Marp CLI; the environment has it
(
/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome). PPTX is uploaded to the Gitea release, not committed. Confidence 0.85 — README documents the path; the chromium binary exists.
Decisions surfaced (research → bound in CLARIFY-equivalent)
- D-108 — v1.12 includes one real adapter fix (CAP-013) and two probe fixes (CAP-017, CAP-018) as Phase 67 prerequisites, so the decks can honestly claim 22/22 Verified. The milestone is "presentation refinement" but the verified-only-claims pre-mortem mitigation makes the fixes mandatory. The user confirmed this scope (interactive decision, 2026-07-29).
- D-109 — Decks use
@v1.11in examples during Phase 68 (current state), bumped to@v1.12at Phase 70 complete after the tag exists. Avoids a dangling reference to a tag that doesn't exist yet.
v1.14 Research Addendum — NFR Refinement scope audit (2026-07-29)
Phase 0 RESEARCH for milestone v1.14 (NFR Refinement). A full codebase survey (8 categories, file:line evidence) was conducted to populate the 20-phase scope. This addendum records the findings; the phase list is in ROADMAP.md §v1.14; the requirements are in REQUIREMENTS.md §v1.14.
Survey method
Read-only survey of /root/acdl at v1.13.2 (HEAD 139224ff, 533 tests
collected). 8 categories: stubs, P1/P2 backlog, security, docs drift,
test gaps, terraform gaps, workflow gaps, config hygiene. All file:line
references verified against the live codebase.
Finding 1 — Open P1/P2 backlog (REVIEW.md v1.11)
5 P1 + 4 P2 findings from the v1.11 multi-persona review remain open:
| ID | File:Line | Status | v1.14 phase |
|---|---|---|---|
| P1-1 | adapter.py:159-170 (silent drop of unregistered-module resources) |
open | P1 |
| P1-2 | static-assets/composition.json (unwired cloudfront inputs; WAF unconditional) |
open | P2 |
| P1-3 | run_l2_lifecycle_*.sh (vestigial [ci-vpc-outputs.json] arg) |
open | P3 |
| P1-4 | CAPABILITY_INVENTORY.md:9-16 (summary table stale) |
fixed (now 22/22) | — |
| P1-5 | regression_verify.py:432-519 (CAP-017..022 offline proxy, no terraform validate) |
open | P4 |
| P2-1 | alb/main.tf:9 (name_prefix="tg-ci-" discards var.name) |
open | P6 |
| P2-2 | test_adapter.py (no dedup-merge or remote-state-key test) |
open | P5 |
| P2-3 | waf/complex.yml + locals.tf (redundant upper() + uppercase example) |
open (post-hoc) | folded into P2 |
| P2-4 | COST.md:106 (account ID published; accepted exposure) |
open (post-hoc) | folded into P8 (centralize code-side) |
Finding 2 — Security posture gaps
Swallowed errors (6 sites):
core/local_emulators.py:374—except Exception: passin_fake_urlopen; if patching fails, urlopen stays real → network egress. [SEC] → P7.core/lambda/contract_ingestor.py:157— GitHub search failure →existing = []→ duplicate issues. → P7.terraform/bootstrap/create_state_backend.py:51— over-broadexcept Exception:onhead_bucket→ spuriouscreate_bucketon permissions/network errors. → P7.core/output_publisher.py:100,168— SSM/GitHub failure → silentNone/False. → P7.terraform/bootstrap/apply_iam_baseline.py:78— over-broad on old-version delete. → P7.
Hardcoded account ID 581513795199 (15+ sites):
adapter.py:125,140, apply_iam_baseline.py:33,
create_state_backend.py:33,35, push_consumer_image.py:32, terraform
state-bucket names, ECR image ref. → P8 (externalize to
ACDL_AWS_ACCOUNT_ID / data.aws_caller_identity).
IAM policy wildcards (6 Resource: "*" statements):
spike_runner_policy.json — cloudfront (line 117), wafv2 (129), kms
(218), iam (236). KMS allows key creation/deletion on ANY key; IAM
allows role creation on ANY role. → P9 (scope to acdl-* ARNs).
Contract-ingestor identity validation gap:
contract_ingestor.py:221-245 — _validate_caller_identity validates
consumerRepo format only; doesn't verify caller owns the repo (ABAC
reliance). No contractId/environment/error validation. → P10.
Schema validation gaps:
contract.schema.json + environment.schema.json — no
additionalProperties: false (undocumented fields pass silently); no
format validation for bucket/ARN/CIDR. → P11.
Credential hygiene:
.gitignore covers .env*/*.tfstate* but no credential-pattern
catch-all (*.pem/*.key/*.p12). → P12.
Audit ledger integrity (D-083):
audit_ledger_design.md:47-70 — JWS + Object Lock + DLQ deferred. Per
D-096, stays deferred; documented in P19. The hash-chain + DynamoDB
outbox is the v1.14 audit record.
Finding 3 — Stubs / missing functionality
adapters/kyverno/kyverno_adapter.py:11,115-116—--kube-versionparsed then discarded (_ = kube_version). → P13 (implement or remove + document).scripts/__pycache__/verify_deploy_microservice.cpython-312.pyc— orphan bytecode for a deleted source file. → P14.core/regression_verify.py:237— DynamoDB write deferred to Phase 54 (outbox hash-chain verified, no real DynamoDB write). Accepted deferral.adapters/wiz/wiz_adapter.py— real GraphQL client (not a stub); degrades gracefully. OK.core/separation_of_duties.py—route_halt_artifactis real (SNS + outbox fallback). OK.core/lambda/contract_ingestor.py—report_erroris real (GitHub issues via Secrets Manager). OK.
Finding 4 — Documentation drift
ARCHITECTURE.md— no v1.11/v1.12/v1.13/v1.14 addendum; line 500-506 still describes the old parameterized adapter (pre-stateless rewrite). → P19.- Stale
@v1.6–@v1.9workflow refs inREADME.md:225,docs/consumer-guide.md(12 sites),docs/architecture.md:233,docs/pipeline/versioning.md:29,docs/pipeline/index.md:42. → P19. modules/STANDARDS.md§8 referencesTYPE_MAP(deleted in v1.11); §9.4 requires 5-file split but §489-492 allows inlining — inconsistent. → P18.COST.mdwindow stops at v1.10; no v1.11–v1.13 spend. → P19.GRILL.mdG-005/G-008 escalations — CAP-017..022 now Verified via lifecycle pipeline; COST.md now exists. → P19 (mark resolved).IAM_POLICY.md— reflects v1.11 re-bootstrap but not v1.12/v1.13. → P19.- Decks reference "v1.12" verification status; not re-synced for v1.13.2. → P19.
Finding 5 — Test coverage gaps
- 533 tests collected; 5
@pytest.mark.slow(deselected from fast suite). 7 scripts with no test:seed_uptime_monitors.py,push_consumer_image.py,sync_to_gl.sh,post_stage_comment.sh,rotate_spike_key.sh,create_state_backend.py,create_iam_user.py. → P15. - Adapter dedup-merge +
ACDL_REMOTE_STATE_KEYoverride — no unit test (P2-2). → P5.
Finding 6 — Terraform gaps
- 3 L1 modules lack
locals.tf(ecr,ecs-cluster,rds). → P18. static-assets/composition.jsonunwired inputs (P1-2). → P2.terraform/platform/main.tf:255— hardcoded CIDR;count=2subnets not data-driven. → P20.terraform/bootstrap/create_state_backend.py:51— over-broad except (Finding 2). → P7.
Finding 7 — Workflow / pipeline gaps
- 4 GitHub-only workflows (patterns-plan, platform-test, primitives-plan, release) — no Gitea mirror. → P16.
rotate_spike_key.sh(onlyset -u),sync_to_gl.sh(nosetflags). → P16.- 3 shared workflows (ci, deploy, modules-lifecycle) byte-identical (verified). OK.
- modules-lifecycle matrix covers all 12 L1 + 2 L2. OK.
Finding 8 — Config / project hygiene
config.jsonbash_allowlist has dead JS entries (npm/node/jest/eslint /tsc — no package.json). → P14/P17.config.jsonbranching_strategy: "phase"mismatched with flat-workflow practice. → P17.config.jsonollama-cloud.base_url: ""(empty; noglmmodel configured). → P17.config.jsonfrontend-engineerpersona still inpersonas[](PERSONAS.md:80 says inactive). → P17.pyproject.tomlversion1.3.0(stale); coverage sourceacdl_platform(renamed tocorein v1.6). → P14.
Persona assessment (v1.14)
The v1.14 milestone is NFR-only (bug fixes, security, tests, docs). The active persona roster from v1.11 (PERSONAS.md) carries forward unchanged:
- lead-developer (active) — coordination; owns the wave ordering + cross-phase dependencies.
- backend-engineer (active) — owns
adapters/,core/(adapter dedup, contract ingestor, regression gate, output publisher). - data-engineer (active) — owns
terraform/,modules/(ALB fix, static-assets wiring, platform VPC, IAM policy, STANDARDS). - frontend-engineer (inactive) — no frontend; decks are markdown (lead-developer territory). Stays deactivated per PERSONAS.md:80.
No custom personas needed for v1.14 (no new domains). Territory
enforcement = warn (config.json:167). The v1.14 work is concentrated
in adapters/, core/, terraform/, scripts/, tests/, docs/,
.ciagent/ — all within existing persona territories.
v1.15 Research Addendum — Nova Rebrand scope audit (2026-07-30)
Survey method
A thorough, exhaustive codebase survey (via the explore subagent) plus
targeted grep -rni counts. The survey covered 346 tracked files,
reporting occurrence counts and the mechanical-vs-judgment split per
category. The full survey is recorded in the planning conversation
transcript; the binding conclusions are summarized here.
Finding 1 — Brand string surface area
- 1,465 total
ACDL/acdloccurrences across 205 files. - 17 occurrences of the full "Agentic Cloud Delivery Platform" phrase (all prominent titles/headers: README, docs/index, vision, pyproject, decks, .ciagent/*.md).
- 0 existing references to "nova" (case-insensitive) — no collision risk.
- User-facing (docs/, README, decks, contracts, schemas, module READMEs): high-priority for rebrand.
- Internal (.ciagent/*.md, tests/, terraform/, scripts/, workflows): mechanical but voluminous.
Finding 2 — Code identifiers (judgment category)
- Python package name:
pyproject.tomlname = "acdl"(noacdl/package dir exists — source lives incore/,adapters/; the name is a metadata label). Mechanical rename. - Python file:
adapters/terraform/policy/custom_rules/acdl_tagging.py(+ Checkov registration inschemas/tagging-standard.jsonline 5 + adapter config). Rename file + update registration. - Env var prefixes: 21 distinct
ACDL_*prefixes (ACDL_LIFECYCLE_MODE66×,ACDL_AWS_ACCESS_KEY_ID40×,ACDL_AWS_SECRET_ACCESS_KEY37×,ACDL_REMOTE_STATE_KEY22×,ACDL_AWS_ACCOUNT_ID21×,ACDL_TAG_NAMING20×,ACDL_KMS_KEY_ID16×,ACDL_BOOTSTRAP_AWS_*15× each,ACDL_SOD_HALT_TOPIC_ARN14×,ACDL_LOCAL_TIER13×, etc.). No centralized env loader exists today (scatteredos.environ.get). D-108: a newcore/env.pyget_env()helper centralizes the dual-read fallback. - Workflow
name::.github/workflows/release.ymlline 11name: acdl-release— mechanical.
Finding 3 — AWS resource names (high-risk migration)
Terraform creates real AWS resources with acdl- prefixes. Renaming
forces destroy+recreate (downtime, data loss for DynamoDB/state bucket).
D-102: full rename with migration (user-directed).
| Resource | Type | Migration |
|---|---|---|
acdl-contracts / acdl-change-requests |
DynamoDB | scan+copy data, verify row counts |
acdl/github-token |
Secrets Manager | recreate secret, repoint Lambda |
acdl-contract-ingestor (role/policy/Lambda) |
IAM+Lambda | recreate role/Lambda, update trigger |
acdl-sod-halt |
SNS | recreate topic, repoint publisher |
acdl-ecs-sg |
SG | recreate (brief ECS disruption) |
alias/acdl-platform |
KMS alias | repoint alias (cheap) |
acdl-microservice (cluster/ECR/service/task/role) |
ECS+ECR | re-push images, recreate service |
acdl-spike-runner (user/policy) |
IAM | re-bootstrap with new key |
acdl-tfstate-581513795199-us-east-1 |
S3 state bucket | terraform init -migrate-state, back up state JSON |
acdl-alb (name prefix) |
ALB | recreate (brief downtime) |
Finding 4 — Consumer/infra conventions (judgment category, D-104)
- AWS tag keys
acdl:owner|environment|contract|cost-center|ref(5 keys, ~109 tag assignments) — matched by ABAC session policies. Parallel-tag period (addnova:*, swap policy, removeacdl:*). - SSM path
/acdl/{env}/{contractId}/{output}(67 refs) — deploy outputs stored here. Migration script copies params, readers updated, old deleted. - Consumer path
.acdl/contract.yml(23 refs) — consumer repos depend on this. Renamed.nova/contract.yml+ migration guide.
Finding 5 — Docs & decks (mechanical)
- README.md (16), docs/index.md, docs/vision.md, docs/architecture.md, docs/consumer-guide.md (35), docs/modules/index.md (28), all .ciagent/.md, modules/STANDARDS.md, schemas/README.md, pipelines/README.md, adapters/README.md, terraform//README.md.
- Deck markdown + mermaid
.mmdsources (5 files) + rendered HTML. PNGs re-exported from edited.mmdsources. - S&P visual theme (
sp-theme.json, deck CSS) is client branding — D-107: untouched. Only product-brand text (ACDL→Nova) changes. - Schema
$idURLs (https://acdl.cloudinit.dev/schemas/...) →https://nova.cloudinit.dev/schemas/...(D-110: illustrative, no DNS resolution needed for validation).
Finding 6 — CI / pipeline / release
- Workflow files mirrored in
.gitea/workflows/+.github/workflows/(modules-lifecycle 31×, deploy 22×, ci 2×, release 3×). release.ymlrelease titleACDL vX.Y.Z→Nova vX.Y.Z(forward only; past releases keep names).- Git branches/tags use
milestone/v*/phase/*/v*— no brand name present, no change needed (D-112: flat-branch convention preserved). - config.json
release.gitea.repostaysacdl(D-105: real repo name unchanged; doc URLs illustrative only).
Finding 7 — External / URLs
github.com/acdl/...(~20 refs in docs + module READMEs + reusable-workflowuses:refs) — D-105: illustrative, updated tonovafor prose. Real GitHub org/repo rename is out of scope.git.cloudinit.dev/continuous-intelligence/acdl*(incl. sister reposacdl-contracts,acdl-evidence) — updated in prose tonova*.- README has no badges (no shields.io, no img src).
Finding 8 — Nomenclature / tagline
- "DevSecOps", "New Dawn", "enabler" appear nowhere in the repo today — clean insertion, no collisions to reconcile (D-106).
- Current tagline ("North Star" / "consumers declare intent") is retained; Nova tagline added alongside.
- "bottleneck" (6 occurrences in deck talking points) — compatible with the Nova "no bottleneck" messaging; left in place.
Persona assessment (v1.15)
No new personas needed for v1.15 — the rebrand touches existing
territories (docs, code, terraform, CI, tests). The active roster:
lead-developer (docs/decks/.ciagent meta + verification + migration
runbooks), backend-engineer (core/env.py dual-read helper, contract
resolver path, Lambda, output_publisher, regression_verify),
data-engineer (terraform resource names/tagging, state bucket
migration, DynamoDB data migration, ECR re-push, schemas/tagging-standard).
The frontend-engineer remains deactivated (no UI; decks are
markdown = lead-developer territory). A security-engineer persona is
not activated — the ABAC session-policy + tag-key migration (REQ-162) is
data-engineer territory (terraform IAM) with lead-developer review.
Territory enforcement = warn (co-authoring expected at the
core/env.py + terraform boundary, and the contract-resolver +
deploy-workflow boundary).
Assumptions logged
- A1 (confidence 0.9): No live AWS access is available during P0–P4
execution (the
acdl-spike-runnerIAM user's creds are in.env.secretsbut live apply/modify/destroy is gated byNOVA_LIFECYCLE_MODEdefaulting to plan-only). The terraform changes are validated viaterraform validate; live apply is exercised by the modules-lifecycle workflow when explicitly set to full. This matches the v1.11–v1.14 established pattern. - A2 (confidence 0.85):
.env.secretscontains live rotated AWS credentials keyed byACDL_AWS_*. P2 renames the KEYS only (values stay). The runtime reads via the newcore/env.pydual-read helper (NOVA_AWS_ACCESS_KEY_IDpreferred,ACDL_AWS_ACCESS_KEY_IDfallback), so no re-rotation is needed until P5 removes the fallback. - A3 (confidence 0.8): The Gitea release API (
POST .../releases) is reachable forv1.14.xtags (the v1.14 milestone shipped releases throughv1.13.24/ release id 285). P0 ship targetsv1.14.0.
v1.16 NFR Simplification — Research Addendum (2026-07-30)
Milestone: v1.16-Nova-Simplification (NFR). Research is codebase- grounded (not domain/ecosystem) — the two explore passes identified concrete, file:line-verified residual debt the v1.15 rebrand left, plus genuine simplification and the onboarding request-path scaffolding.
R1. v1.14 NFR categories already closed (do NOT re-propose)
v1.14 (REQ-135..154) swept: over-broad excepts (REQ-141), hardcoded
account-ID externalization (REQ-142, ACDL_AWS_ACCOUNT_ID env + live
fallback G-102), IAM Resource:"*" scoping to nova-* ARNs (REQ-143,
G-104), contractId/env/error validation (REQ-144),
additionalProperties:false schemas (REQ-145), .gitignore credential
catch-all (REQ-146), Kyverno --kube-version removal + deferral doc
(REQ-147, G-103), orphan bytecode/dead-config cleanup (REQ-148), 7
untested-script test coverage (REQ-149), set -euo pipefail parity +
Gitea workflow parity (REQ-150), config/persona/backend hygiene (REQ-151),
STANDARDS.md TYPE_MAP consistency (REQ-152), ARCHITECTURE/COST/GRILL doc
sync (REQ-153), platform-VPC CIDR/subnet parameterization (REQ-154).
The v1.16 grill (G-101..G-106) and escalation E-001 are all CLOSED. v1.16 finds NEW residual signals (D-117).
R2. Fresh debt the v1.15 rebrand left (verified file:line)
High-severity correctness regressions (P1):
adapters/terraform/adapter.py:117— emitsstate_bucket = f"acdl-tfstate-{account_id}-us-east-1". The live state bucket was renamed tonova-tfstate-*in v1.15 P4 (REQ-163), but the adapter's emitted terraform backend still referencesacdl-tfstate-*. In plan-only mode this is latent (no real init against the bucket), but a full-mode lifecycle run would point at a non-existent bucket.adapters/kyverno/policies/require-resource-labels.yml:6,21,25,33,37— enforcesacdl:owner/acdl:environmentlabels.nova_tagging.pyhard-fails on anyacdl:*key post-P5 (REQ-164). The Kyverno policy contradicts the Nova tagging standard.
User-facing brand misses (P2):
core/environment_check.py:59,61— onboarding message header/body say "ACDL Environment Onboarding" / "ACDL environments are platform- managed" (user-facing).core/lambda/contract_ingestor.py:145,191— GitHub issue alert title[ACDL-ALERT]+ body "auto-created by the ACDL platform Lambda" (user-facing artifact).scripts/post_stage_comment.sh:39,46— PR comment header "ACDL Stage"- footer "ACDL deploy pipeline" (user-facing).
scripts/run_ci.sh:39— CI banner "ACL CI Pipeline".- Module docstrings:
core/contract_resolver.py:1,474,core/confidence_signal.py:1,adapters/terraform/adapter.py:1,adapters/kyverno/kyverno_adapter.py:1,adapters/wiz/wiz_adapter.py:1,adapters/README.md:1,adapters/kyverno/README.md:4,18.
Dead code + stale comments (P3):
scripts/run_platform.sh:153—export ACDL_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE" # legacy fallback, removed in P5— comment says "removed in P5" but the line is STILL present (dead code, P5 already shipped).- Stale dual-read comments across
core/local_emulators.py:15-16,503,505,core/regression_verify.py:318-319,333,scripts/run_regression.sh,scripts/run_lifecycle_*.sh(reference the retired G-106 fallback). acdl_*temp-dir prefixes:core/local_emulators.py:71,252,core/regression_verify.py:183,234,scripts/run_pattern_plan.sh:29,scripts/run_primitive_plan.sh:29,scripts/run_lifecycle_*.sh:36,41.
R3. Simplification opportunities (verified)
core/regression_verify.py:328-409—_check_live_terraform_plan_ microservice+_check_live_terraform_plan_static_assetsare ~95% identical (resolve → adapt → init → validate → plan). Extract_check_live_terraform_plan(contract, label)(~35 lines saved).core/regression_verify.py:149-178—_check_resolver_static_assets+_check_resolver_microserviceidentical except contract path. Extract_check_resolver(contract).core/regression_verify.py:477-503— duplicated lifecycle-contract- resolve block. Extract_assert_contracts_resolve(module_dir).scripts/run_platform.sh:336-350+:452-466— duplicated HITL attestation block. Extractrun_hitl_gate()shell fn (~14 lines).core/contract_resolver.py:50-68duplicatescore/environment_check.py: 36-54env loader verbatim. Import instead.scripts/run_platform.sh:145-146— hardcodedCONTRACT_IDUUID +WORK="/tmp/acdl_platform_run_v18"(v18stale). Make config/env- derived..gitea/workflows/↔.github/workflows/— 3 byte-identical pairs (ci.yml,deploy.yml,modules-lifecycle.yml, ~20 KB) maintained by hand + a test asserting identity. Generator (D-115) eliminates manual-sync risk.core/contract_resolver.py:540—is_l2 = "l2" in interface_path or "composition" in interface_pathfragile string heuristic. Addkindto registry entries (P7).scripts/run_platform.sh(610 lines) — decommission block (:180-237)- uptime block (
:520-606) are self-contained. Extract toscripts/run_decommission.sh+scripts/run_uptime.sh(P9).
- uptime block (
R4. Security gaps (verified, NEW — not v1.14 duplicates)
core/lambda/contract_ingestor.py:251-252—if not caller_arn: passsilently skips identity validation when IAM identity absent; relies on ABAC layer only (no defense-in-depth). Fail closed instead (P10).core/lambda/contract_ingestor.py:269—valid_envs = {"dev","qa", "prod","dr"}hardcoded; thecore/environments/dir is the source of truth. Derive from the directory (P10).core/lambda/contract_ingestor.py—submit_contractchecks thecontractkey exists but never validates the blob's size or schema. Unbounded payload → DynamoDB write amplification. Size cap + schema validation (P11).scripts/migrate_ssm_paths.py:113—except Exception: pass(claimsParameterNotFoundbut catches all). Last true broad-swallow. Narrow toParameterNotFound(P4).core/output_publisher.py:112,182—except Exceptioninpublish_to_ssm+post_github_commentswallow all (not narrowed by REQ-141 which targeted 6 other sites). Narrow to specific exceptions.
R5. Onboarding request-path scaffolding (already present)
The infrastructure for a zero-human request path already exists:
terraform/platform/main.tf:153-183deployscontract_ingestorLambda- Function URL (IAM auth).
core/lambda/contract_ingestor.py:325-362dispatchessubmit_contract | report_error | validate_change_request. Addingonboard_consumeris a small extension (P18, D-119: writes apendingCMDB row, no provisioning).terraform/platform/consumer_invoke_policy.jsonis the ABAC policy template (aws:PrincipalTag/nova:owner == ${consumerRepo}scopedlambda:InvokeFunctionUrl).core/environments/*.jsonare static JSON templates with placeholderaccount_id: "000000000000"— auto-generation from a request is straightforward (P19).
Missing for "no humans": (a) onboard_consumer action + onboarding
schema (P18), (b) core/onboarding.py to auto-generate <env>.json +
emit a PR (P19), (c) cross-account deploy-role + ABAC tag Terraform,
offline-proven (P20, D-114). Real AWS account/network/state creation
stays a future feature (D-113).
R6. Developer experience gaps
scripts/run_platform.shhas no--help(:82rejects--*flags).--deploy-uptime(:532) is undocumented in the header.--localis absent from the README (P15).- No
.github/workflows/README.mdcataloging the 7 workflows' inputs/ secrets/triggers (P16). - No single getting-started path; README "How to run" lists 3 manual
bootstrap steps. The offline happy path (
run_ci.sh+run_platform.sh --check-only/--local) is not surfaced first (P17).
Assumptions logged (v1.16)
- A1 (0.9): No live AWS access during execution (consistent with
v1.11–v1.15).
NOVA_LIFECYCLE_MODEdefaults to plan-only; terraform changes validated viaterraform validate. The state-bucket drift (P1) is latent in plan-only mode but must still be fixed for correctness. - A2 (0.85): The
onboard_consumerLambda action (P18) is offline- testable viamoto/ the local Lambda stub (D-092), consistent with the existingsubmit_contract/report_errortest pattern. - A3 (0.8): The workflow generator (P8, D-115) must preserve the byte-identity property as a test assertion (generated outputs match committed files), not lose it — the dedup is mechanical, not a semantic change to the workflows.
- A4 (0.85): The regression gate (D-091, D-118) at P9 and P21 confirms "simplify without regressions" — 22/22 capabilities must stay Verified. The gate is the credible control for the simplification wave.
v1.17 Research — Strategic Direction, Leadership Metrics & Unified Story
Phase: research (P0). Milestone: v1.17. Status: research. Researcher: ci-researcher + explore agent (signal inventory). Autonomy: full. Decisions D-120..D-132 locked in the planning conversation (PROJECT.md). NORTH_STAR.md drafted (pending GRILL).
1. Telemetry Signal Inventory (grounding audit)
Methodology: every claim below is grounded in a concrete file path +
line number in /root/acdl. No speculation. The explore agent performed
a full sweep of the repo. The finding: Nova has no metrics/telemetry/
dashboard aggregation layer today. What exists is a set of discrete,
structured, file-based signal artifacts (JSON reports, JSONL logs,
hash-chained outbox events, PR comments, Checkov JSON) plus unstructured
stdout logs. A metrics milestone must aggregate these existing signals
— it must not invent new ones without first adding emitters.
(a) Signals that EXIST TODAY and are STRUCTURED (groundable)
| Signal | File / Emitter | Schema | Persistent? |
|---|---|---|---|
| Regression report (22 caps, status, duration_ms, gate) | .ciagent/REGRESSION_REPORT.json ← core/regression_verify.py:643-667 |
regression_verify.py:82-91 |
Yes (committed file) |
| Regression report (markdown mirror) | .ciagent/REGRESSION_REPORT.md |
same | Yes |
| Checkpoint (milestone/phase/tag/regression summary) | .ciagent/CHECKPOINT.json (CIAgent-managed) |
ad-hoc | Yes |
| PolicyCheckResult list (per-rule pass/fail/severity/resourceRef) | $WORK/pcr.json ← run_platform.sh:395 + checkov_adapter.py:50-71 |
schemas/policy_check_result.schema.json |
No (ephemeral /tmp/) |
| Confidence signal (score, band, perInput, reasonCodes) | $WORK/signal.json ← run_platform.sh:412-426 + confidence_signal.py:60-65 |
confidence_signal.py:60-65 |
No (ephemeral) |
| Outbox event (hash-chained, CONFIDENCE_COMPUTED) | $WORK/event.json + $WORK/outbox_item.json ← run_platform.sh:444-459 + outbox_writer.py:44-56 |
audit_ledger_design.md:44-45,81-97 |
No (ephemeral; live DynamoDB torn down D-096) |
| Resolved Target Stack | $WORK/stack.json ← contract_resolver.py:581-603 |
schemas/stack.schema.json |
No (ephemeral) |
| Lambda return bodies (submit/report_error/validate_cr/onboard) | core/lambda/contract_ingestor.py:171,265,284,392,446 |
ad-hoc JSON | No (Lambda not live; local stub only) |
| DynamoDB CMDB rows (submitted/pending contracts) | nova-contracts table ← contract_ingestor.py:160-170,433-445 |
ad-hoc | No (table torn down D-096) |
| SSM parameters (deploy outputs) | /nova/<env>/<contractId>/<name> ← output_publisher.py:123-156 |
ad-hoc | No (live AWS, torn down) |
| PR stage comment (mode, runId) | GitHub PR API ← post_stage_comment.sh:34-48 + deploy.yml:141 |
markdown table | Yes (GitHub) |
| PR deploy-outputs comment | GitHub PR API ← output_publisher.py:159-189 |
markdown table | Yes (GitHub) |
| GitHub issue (deploy failure alert) | GitHub API ← contract_ingestor.py:179-290 + deploy.yml:143-152 |
issue body | Yes (GitHub) |
| Local E2E result (stack_name, tier, outbox_events, chain_verified, lambda_status) | stdout JSON ← core/local_emulators.py:498-508,519 |
ad-hoc | No (stdout) |
| HITL gate result | core/hitl_gates.py:87,90 + run_platform.sh:179-185 |
stdout HITL PASS/BLOCK |
No (stdout) |
| Attestation matrix result | core/attestation_matrix.py:184,187 |
stdout ATTESTATION PASS/BLOCK |
No (stdout) |
| Cost figures | .ciagent/COST.md (manual Cost Explorer query) |
markdown table | Yes (manual, not automated) |
(b) Signals that EXIST but are UNSTRUCTURED (log-only)
| Signal | Source | Format |
|---|---|---|
| CI pipeline result | scripts/run_ci.sh:70-71 |
stdout banner === CI PIPELINE OK === |
| Platform stage banners + summaries | scripts/run_platform.sh:222,241,258,263,315,383,411,442,463,490,496 |
stdout === Step N: ... === + summary lines |
| Terraform init/validate/plan/apply/destroy logs | $WORK/tf-*.log ← run_platform.sh:320,324,328,352,375 |
raw terraform stdout (via tee) |
| Lifecycle test results | scripts/run_lifecycle_test.sh etc. |
exit code only (no report file) |
| Decommission step counts | scripts/run_decommission.sh:40,54 |
stdout decommission step N: M resources... |
| Uptime endpoint count | scripts/run_uptime.sh:72,87 |
stdout uptime: N endpoint(s) to monitor |
| Onboarding prompt | core/environment_check.py:57-81 |
stdout text block |
| Pytest results | pyproject.toml:25 (-v --tb=short) |
stdout only (no junit/json) |
| sync_workflows result | scripts/sync_workflows.py:56,53 |
stdout OK: 3 workflow pairs match / DRIFT: ... |
(c) Proposed executive metrics with NO grounding today (DEFERRED)
| Proposed metric | Why no grounding | Controlling decision |
|---|---|---|
| Live infrastructure health (ECS running count, ALB 5xx, RPS) | Live AWS torn down; CAP-013..016 Skipped | D-096 |
| Live outbox write rate / ledger append latency | DynamoDB outbox table absent | D-096 |
| Tamper-evident ledger checkpoint count / JWS signature rate | S3 Object Lock + JWS + async worker deferred | D-083 |
| Onboarding funnel: requested → granted conversion | Only "requested" (pending row) is emitted; no grant event | D-113, D-114, D-119 |
| Time-to-provision (onboarding SLA) | Real AWS provisioning deferred | D-113 |
| Cross-account role grant count | Offline-proven only, no live apply | D-114 |
| Drift detection (scheduled terraform plan -detailed-exitcode) | Needs live AWS workspaces + a scheduler Nova doesn't have | D-096 + no scheduler |
| GreenOps / carbon (WattTime/Electricity Maps API) | No grounding; new external API | future emitter |
| Predictive vs Reactive ratio | Requires an ML anomaly-forecasting service | future emitter |
| Multi-cloud normalization (Azure/GCP/K8s, FOCUS spec) | Nova is AWS-only | future |
| Red Team MTTR | No red-team program exists | future |
| Self-healing velocity | Nova has no auto-remediator | future emitter |
| SLA / unplanned downtime | Needs live service uptime monitoring against SLOs | D-096 |
| Per-module lifecycle success rate over time | No structured report file written; only exit code | gap (no decision) |
| Test pass rate / test count time-series | No junit/json reporter configured | gap (add --junitxml to addopts) |
| Code coverage trend | pytest-cov installed but not in addopts |
gap |
| Deploy frequency / lead time / MTTR (DORA) | No deploy-event emitter; pipeline runs not counted | gap |
| Policy pass rate time-series | pcr.json emitted but ephemeral; not persisted |
gap (D-096 blocks live persistence) |
| Confidence score distribution over time | signal.json emitted but ephemeral |
gap |
| Consumer adoption count / active consumers | PROJECT.md:487 explicitly states "0 consumer adoption today" |
honest scope |
| Cost time-series (automated) | COST.md is a one-shot manual query; no automated emitter |
gap |
Bottom line: the single richest existing structured signal is
.ciagent/REGRESSION_REPORT.json (22 capabilities × {status, tier,
duration_ms, detail} + summary counts + boolean gate). The next richest
is the per-run $WORK/*.json family (pcr.json, signal.json, event.json,
stack.json) — but these are ephemeral and not persisted in CI.
The lowest-friction grounding for a "no-humans" dashboard is therefore:
(1) regression report → capability health, (2) PR comments + GitHub
issues → deploy/failure activity, (3) add --junitxml to pytest → test
trend, (4) persist $WORK/*.json → policy/confidence/outbox time-series,
(5) extend outbox_writer → Decision Ledger, (6) add Infracost →
pre-apply cost estimates.
2. Telemetry Reference Architecture (Nova-native adaptation)
The PO provided a full distributed-system telemetry reference architecture (CloudEvents 1.0 envelope, OpenTelemetry SDK, Kafka/NATS event bus, Prometheus hot path, ClickHouse warehouse, QLDB decision ledger, Infracost, drift detection, ML anomaly forecasting). Per D-120, we adopt the principles but implement with Nova-native minimal tech. The mapping:
| Direction's principle | Nova-native implementation (v1.17) |
|---|---|
| Events are the source of truth; dashboards are projections | Hybrid (D-125): existing file signals stay as files; collector reads them and emits normalized CloudEvents into metrics/events.jsonl + SQLite. New emitters emit CloudEvents directly. |
| Every AI action is logged with confidence + alternatives | Decision Ledger (D-121): outbox_writer.py extended → SQLite append-only hash-chain table. ai.decision.made modeled from confidence_signal (D-122): decision_id=run_id, chosen_action=band, confidence=score, alternatives=perInput, human_override=HITL block. |
| Hot/cold storage split | Cold-only SQLite (D-126): metrics/nova_metrics.db. Hot path deferred (no live ops, D-096). |
| Read-only external integrators | Infracost (pre-apply, offline, reads plan JSON). Cloud billing CUR deferred (D-096). Carbon APIs deferred (future). |
| CloudEvents 1.0 envelope | Adopted. core/metrics/event_envelope.py defines the envelope + platform.* semantic conventions. |
| Decision Ledger = append-only with hash chain + outcome backfill | SQLite append-only table with hash chain (D-121). Outcome backfilled from apply.completed via decision_id → request_id correlation. Honors D-083 (no S3 Object Lock/JWS). |
| Cost governance: mandatory tags + Infracost pre-apply | Nova already enforces nova:* tags (nova_tagging.py, hard mode). Infracost added as plan post-processor (D-120). Post-apply CUR deferred (D-096). |
| Definition-of-success docs for every KPI | Per-KPI docs in docs/metrics/ (D-127). |
| Replay-ability | SQLite store + JSONL event log are replayable by design. |
CloudEvents envelope (Nova-native)
{
"specversion": "1.0",
"id": "<uuid>",
"source": "nova.platform",
"type": "nova.run.completed",
"time": "<ISO8601>",
"subject": "<contractId>/<env>",
"datacontenttype": "application/json",
"platform": {
"tenant_id": "acdl",
"run_id": "run-<epoch>",
"contract_id": "<uuid>",
"environment": "dev|qa|prod|dr",
"actor": {"type": "confidence-gate", "id": "confidence_signal"},
"trace_id": "<run_id>"
},
"data": {
"duration_ms": 4800,
"stages": ["resolve", "adapt", "validate", "plan", "apply"],
"exit_code": 0,
"confidence": {"score": 0.94, "band": "pass", "perInput": {...}},
"policy": {"passed": 12, "failed": 0, "skipped": 0},
"hitl": {"gate": "dev", "result": "autonomous", "block": false},
"cost_estimate_usd": -12.40,
"decision_id": "run-<epoch>",
"outcome": "succeeded"
}
}
Core event types (Nova-native minimum viable set)
| Event type | Emitted by | Purpose | Grounding |
|---|---|---|---|
nova.run.started |
run_platform.sh | Measures demand; provisioning lead time start | new emitter (P1) |
nova.run.completed |
run_platform.sh | Run count, stage durations, exit, MTTR | new emitter (P1) |
nova.run.failed |
run_platform.sh | Failure count, MTTR numerator | new emitter (P1) |
nova.policy.evaluated |
checkov_adapter.py | Policy pass rate, compliance KPIs | grounded (pcr.json → P1 persists) |
nova.confidence.computed |
confidence_signal.py | Confidence distribution, decision accuracy | grounded (signal.json → P1 persists) |
nova.ai.decision.made |
outbox_writer.py (extended) | Decision Ledger entry | grounded (D-121, D-122) |
nova.attestation.recorded |
hitl_gates.py | Attestation Coverage, human-in-the-loop audit | grounded (D-132) |
nova.cost.estimated |
Infracost post-processor | Pre-apply cost estimate | new emitter (P1, Infracost) |
nova.capability.verified |
regression_verify.py | Capability health, regression gate | grounded (REGRESSION_REPORT.json) |
nova.test.completed |
pytest (junit XML) | Test count, pass rate | new (P1 adds --junitxml) |
3. Metric-to-Signal Scorecard (the "no fabrication" contract)
| Executive metric (NORTH_STAR target) | Status | Source / formula | Decision |
|---|---|---|---|
| Touchless Resolution Rate ≥99% | grounded (after P1) | runs without operational HITL block ÷ total runs (attestation gates excluded) | D-122, D-132 |
| Human Escalation Frequency <0.1% | grounded (after P1) | operational HITL blocks ÷ total runs (attestation sign-offs excluded) | D-122, D-132 |
| MTTR (p95) <60s | grounded (platform-run) | apply.failed.time → successful retry.time | D-131 |
| Predictive vs Reactive ≥3:1 | deferred | requires ML forecasting (future emitter) | future |
| AI Decision Accuracy ≥99.5% | grounded (after decision ledger) | decisions not followed by apply.failed/incident within 5min | D-121, D-122 |
| Drift Auto-Reversal ≥95% | deferred | requires drift detection (D-096 + scheduler) | D-096 |
| Cloud Spend Reduction ≥25% | partial | pre-apply estimate grounded (Infracost); actuals deferred (D-096 CUR) | D-120 |
| L1/L2 Ops Hours Avoided ≥70% | derived | formula: run count × manual baseline minutes × blended rate | D-127 |
| Platform ROI ≥250% | derived | formula: (labor savings + cloud savings + avoided downtime) ÷ platform op cost | D-127 |
| Decision Ledger Coverage 100% | grounded (this milestone) | outbox_writer.py → SQLite hash-chain | D-121 |
| Attestation Coverage 100% | grounded | hitl_gates.py + outbox approver_* attributes; prod/dr | D-132 |
| AI-Agent Intent Share ≥40% | future | no AI-agent consumers today; placeholder view | future |
| Capability health (18V+4S) | grounded | REGRESSION_REPORT.json | existing |
| Confidence score distribution | grounded (after P1) | signal.json → decision ledger | D-121 |
| Policy pass rate | grounded (after P1) | pcr.json → persisted | D-120 |
| Test count / pass rate | grounded (after P1) | pytest --junitxml | D-120 |
| Provisioning Lead Time | grounded (after P1) | run.started → run.completed | D-120 |
| Deployment Frequency | grounded (after P1) | count(run.completed) per day | D-120 |
| Deploy-failure alert count | grounded | GitHub issues via Lambda report_error (D-055) | existing |
| Cost figures (actuals) | manual one-shot | COST.md (Cost Explorer query) | existing |
| FTE Hours Saved / TRV | derived | formula over run count + COST.md | D-127 |
| Self-healing velocity | deferred | no auto-remediator | future |
| SLA / unplanned downtime | deferred | needs live service uptime (D-096) | D-096 |
| GreenOps / carbon | deferred | WattTime/Electricity Maps API (future) | future |
| Red Team MTTR | deferred | no red-team program | future |
| Multi-cloud normalization | deferred | Nova is AWS-only | future |
| Live CUR reconciliation | deferred | needs live AWS billing (D-096) | D-096 |
4. Deferred-Decision Ledger (constraints on this milestone)
| Decision | Scope | Grounding impact |
|---|---|---|
| D-096 | Live AWS torn down post-v1.11 | BLOCKS all live-AWS metrics (CAP-013..016 Skipped; live outbox; live state bucket; live CUR) |
| D-083 | S3 Object Lock + JWS + async worker deferred | BLOCKS tamper-evident ledger; v1.17 uses SQLite hash-chain instead |
| D-113/D-114/D-119 | Onboarding = request-path only; no auto-grant | BLOCKS onboarding funnel "granted" half |
| D-091/D-118 | Regression gate (D-091) gates milestone completion | ENABLES the strongest metric signal (REGRESSION_REPORT.json) |
| D-092 | Local emulating adapters | ENABLES offline E2E metrics (CAP-011/012) |
| D-055 | report_error Lambda action creates GitHub issues | ENABLES deploy-failure alert metric |
| D-050 | Publish deploy outputs to SSM + GitHub PR comment | ENABLES outputs-published metric |
| D-054/D-043/D-109 | Nova tagging standard (hard mode) | ENABLES tagging-compliance metric |
| D-084 | 8-concern attestation matrix | ENABLES attestation metrics (operator-supplied evidence artifacts) |
| D-089 | Signature verification skipped when signing key unset (dev/CI) | Signature metrics are no-ops in dev |
5. Deck-Storytelling Research (x3 arc + per-slide benefit)
The "tell them x3" structure
The PO's direction: "Tell them what you're going to tell them, then tell them, then tell them what you told them." Applied at two levels:
Deck level (the 5-act arc):
- Opening slide = "what I'm going to tell you" — the full arc preview: Problem → Vision → How → Proof → Roadmap.
- Body (acts 1–5) = "tell them" — each act delivers its content.
- Closing slide = "what I told you" — recap of the 5 acts + the ask.
Per slide:
- Slide opens with what it'll cover (1 line: "This slide shows X").
- Slide delivers the content (bullets, diagram, or table).
- Slide closes with an explicit "benefit of this stage" callout (1 line: "Benefit: you now know Y" or "Why this matters: Z").
Fluidity conventions
- Transitions are written, not hand-waved. Each slide's opening line references the previous slide's close ("Having seen X, now consider Y").
- No disjointed jumps. If a topic shift is needed, a bridge slide or a transition sentence carries the audience across.
- The arc is visible. A small "act indicator" in the Marp footer
(e.g.,
Act 3/5: How it works) keeps the audience oriented.
Existing deck inventory (to be retired)
Two decks exist today in docs/presentations/:
how-the-platform-works.md(32,916 bytes) → marp → html → talking-pointsthe-developer-experience.md(27,509 bytes) → marp → html → talking-points
Both follow a 4-step process (source .md → Marp → HTML → talking-points)
documented in docs/presentations/README.md. Per D-130, both are merged
into one unified narrative deck and retired.
Grounded metrics already cited in existing decks
- "22/22 auto-verifiable capabilities Verified" — stale vs current REGRESSION_REPORT.json (18V+4S post-D-096). The unified deck must derive this from the report, not copy the stale claim.
- Confidence thresholds: dev ≥0.50, qa ≥0.75, prod ≥0.90, dr ≥0.95 —
grounded in
core/confidence_signal.py:57(THRESHOLDS). - RPO = 0 (evidence write synchronous) — grounded in
core/audit_ledger_design.md:27,103. - Cost figures —
how-the-platform-works.md:461; cites COST.md. - Confidence signal 6 inputs + weights — grounded in
core/confidence_signal.py:40-47. - "~80-line stateless adapter" vs "918-line monolith" — grounded in ROADMAP/RESEARCH prose.
Planned deck structure (for PLAN to detail)
The unified deck "Nova — The No-Humans Infrastructure Platform":
| Act | Slides | Content | Proof source |
|---|---|---|---|
| 1. Problem | 2–3 | The no-humans imperative; why operators are the bottleneck; the trust gap | NORTH_STAR vision |
| 2. Vision/Direction | 2–3 | Nova's vision; 4 strategic objectives; anti-goals; the attestation model (autonomy in operations, human at stage gates) | NORTH_STAR |
| 3. How it works | 3–4 | Contract → resolver → adapter → confidence → HITL gate; the Decision Ledger; the 8-concern attestation matrix | code grounding |
| 4. Proof (metrics) | 3–4 | Capability health (18V+4S); confidence distribution; policy pass rate; Decision Ledger coverage; Attestation Coverage; cost estimates; the grounded/derived/deferred honesty model | metrics export |
| 5. Roadmap/Ask | 2 | 12–18mo targets (committed); deferred metrics (honest); the ask | NORTH_STAR targets |
Total: ~12–16 slides. Opening = arc preview; closing = recap + ask.
6. Assumptions logged (v1.17)
- A1 (0.9): No live AWS access during execution (consistent with v1.11–v1.16). All metrics that require live AWS ship as placeholder views. The Infracost integration runs offline (reads plan JSON).
- A2 (0.85): The Decision Ledger SQLite hash-chain is sufficient for v1.17's audit needs. The full tamper-evident ledger (S3 Object Lock + JWS, D-083) is a future milestone. The hash-chain provides append-only + integrity verification locally.
- A3 (0.8): The "AI decision" framing (D-122) is honest: Nova's "AI" is the confidence-gated policy engine (confidence_signal + HITL gate), not an LLM planner. The deck and METRICS.md must frame this accurately — overclaiming "AI" would violate the "no fabrication" constraint.
- A4 (0.85): The unified deck's "Proof" section cites only grounded
metrics with real numbers. Deferred metrics are shown as "Planned"
with the
<span class="badge planned">Planned</span>badge. No fabricated numbers in any slide. - A5 (0.8):
--junitxml+--json-reportadded to pytest addopts does not break the existing test suite (the flags are additive; pytest continues to run normally). CAP-009 (offline pytest suite passes) must remain Verified after the change. - A6 (0.75): Infracost is available as a CLI tool that can be installed
in the CI environment and run locally. It reads
terraform plan -out=plan.tfplan+terraform show -json plan.tfplanto produce a cost estimate. No live AWS access required. If Infracost is not available, thecost.estimatedevent is omitted (degraded mode, not a failure).