---ci--- project: acdl phase: 7 milestone: v1.1 status: plan plan: waves: 5 tasks: 10 requirements: [REQ-16, REQ-17, REQ-18, REQ-19, REQ-20, REQ-21, REQ-22] ---/ci--- Phase 07 plan authored by ci-planner. 5 waves (strictly ordered): - Wave 1 (lead-developer): REQ-16 architecture-v1.0.md snapshot (umbrella) - Wave 2 (platform + backend parallel): REQ-17 ir.schema.json || REQ-22 contract.schema.json - Wave 3 (security-engineer, 5 files): REQ-18 PolicyCheckResult schema + checkov_adapter.py, REQ-20 audit_ledger_design.md, REQ-21 hitl_matrix_design.md + separation_of_duties.py - Wave 4 (backend + security co-authored): REQ-19 confidence_signal.py (depends on Wave 3) - Wave 5 (lead-developer): verify_phase07.sh + traceability 9 deliverable files + 1 verify script. Cross-persona co-authorship on confidence_signal.py (backend weighted-sum + security penalty mapping) mitigated by the PLAN content checklist fixing the critical-override sentinel + short-circuit location.
76 KiB
phase, name, milestone, milestone_type, status, requirements, must_haves, verification
| phase | name | milestone | milestone_type | status | requirements | must_haves | verification | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 7 | architecture-v1-finalization | v1.1 | feature | planned |
|
|
|
Phase 07 — architecture-v1-finalization PLAN
Goal
Finalize the ACDL architecture to v1.0 by authoring the 6 locked
schema/design files + 1 architecture snapshot that resolve all 11 open
decisions recorded in PROJECT.md (D-038). This phase is
design-authoring, not runtime code: the files are the contracts the
v1.1 spike (Phases 08-10) implements against, not the spike itself.
The deliverables, mapped to requirements:
| REQ | File(s) | Owner persona (PERSONAS.md territory) |
|---|---|---|
| REQ-16 | docs/architecture-v1.0.md (snapshot of docs/architecture.md with 11 resolutions applied + status bumped to v1.0) |
lead-developer (owns docs/** meta) |
| REQ-17 | schemas/ir.schema.json (Target Stack IR) |
platform-engineer (owns the IR; schemas/ co-owned with backend-engineer) |
| REQ-18 | schemas/policy_check_result.schema.json + adapters/terraform/policy/checkov_adapter.py |
security-engineer (owns adapters/terraform/policy/**); schema co-authored |
| REQ-19 | platform/confidence_signal.py (6-input weighted signal) |
backend-engineer (6-input weighted sum + thresholds) + security-engineer (severity→penalty + critical-override) — co-owned per PERSONAS.md conflict resolutions |
| REQ-20 | platform/audit_ledger_design.md (tiered ledger) |
security-engineer |
| REQ-21 | platform/hitl_matrix_design.md + platform/separation_of_duties.py |
security-engineer |
| REQ-22 | schemas/contract.schema.json (contract schema) |
backend-engineer (owns schemas/** contract + IR + PolicyCheckResult) |
| — | scripts/verify_phase07.sh + traceability update |
lead-developer |
The spike scope (D-041, D-043) means the designs author the full
target but clearly mark what is spike-scope vs v1.2 build-out. The spike
(Phases 08-10) only runs terraform plan + Checkov + confidence + outbox
write; Object Lock / JWS / HITL-wiring / L3B-skill-catalog are design-only
in Phase 07.
Requirements covered
- REQ-16 → T-7.1 (
docs/architecture-v1.0.mdsnapshot + status bump). - REQ-17 → T-7.2 (
schemas/ir.schema.json). - REQ-22 → T-7.3 (
schemas/contract.schema.json). - REQ-18 → T-7.4 (
schemas/policy_check_result.schema.json) + T-7.5 (adapters/terraform/policy/checkov_adapter.py). - REQ-20 → T-7.6 (
platform/audit_ledger_design.md). - REQ-21 → T-7.7 (
platform/hitl_matrix_design.md) + T-7.8 (platform/separation_of_duties.py). - REQ-19 → T-7.9 (
platform/confidence_signal.py, co-authored). - All → T-7.10 (
scripts/verify_phase07.sh+ traceability update).
All 7 requirements (REQ-16..REQ-22) are Phase 07 per REQUIREMENTS.md
traceability table. Phase 07 depends on Phase 06 (complete, v1.1.1):
the new repo layout (schemas/, platform/, adapters/) is scaffolded
empty with .gitkeep and ready to receive these files.
Waves
Domain priority order from PERSONAS.md:
coordination → security → platform → backend → frontend.
Phase 07 active personas (PERSONAS.md "Phase-specific overrides"): lead-developer (coordination — the snapshot + verify script), backend-engineer (schemas: contract + co-IR + co-PolicyCheckResult + co-confidence), security-engineer (HITL/ledger/SoD/Checkov-adapter/ co-confidence), platform-engineer (IR). frontend-engineer idle.
The files are mostly independent (no runtime dependencies between schemas), but there are logical dependencies:
- REQ-16 (architecture snapshot) is the umbrella — it references all the others.
- The 3 JSON Schemas (REQ-17, REQ-18-partial, REQ-22) are independent.
- REQ-19 (confidence_signal.py) consumes PolicyCheckResult records (REQ-18) — depends on T-7.4.
- REQ-21 (separation_of_duties.py) reads the outbox schema (REQ-20) — depends on T-7.6.
- REQ-20 + REQ-21 are both security-engineer; one wave, sequential within.
Five waves, strictly ordered (each wave depends on the prior wave's commits being present so the verify script and snapshot can reference the authored files):
Wave 1 (lead-developer) — T-7.1 architecture-v1.0.md snapshot (umbrella)
Wave 2 (platform + backend) — T-7.2 ir.schema.json
T-7.3 contract.schema.json (parallel)
Wave 3 (security-engineer) — T-7.4 policy_check_result.schema.json
T-7.5 checkov_adapter.py
T-7.6 audit_ledger_design.md
T-7.7 hitl_matrix_design.md
T-7.8 separation_of_duties.py (sequential)
Wave 4 (backend + security) — T-7.9 confidence_signal.py (co-authored; depends on T-7.4)
Wave 5 (lead-developer) — T-7.10 verify_phase07.sh + traceability
Dependency graph (explicit):
- Wave 4 → Wave 3 (confidence_signal consumes PolicyCheckResult, T-7.4).
- Wave 4 → Wave 3 (separation_of_duties reads the outbox schema documented in audit_ledger_design.md, T-7.6 — the .py reads the outbox shape defined in the .md).
- Wave 5 → all prior waves (verify script asserts all 6 files exist + validate).
- Wave 1 (snapshot) is umbrella — it references the decisions (already in PROJECT.md), not the authored files, so it can land first. It notes that the 6 files are authored in Phases 07 Waves 2-4.
Parallelizable: Wave 2's two tasks (T-7.2 IR, T-7.3 contract) are independent JSON Schemas owned by different personas — run in parallel. All other waves are sequential within the wave.
Wave 1 — lead-developer: REQ-16 architecture-v1.0.md snapshot
The umbrella doc. References the 11 resolutions already recorded in PROJECT.md (does not duplicate them — points at PROJECT.md's "Open-decision resolutions" table + decisions D-034..D-046). Bumps the status note from v0.2 → v1.0. Notes the v1.1 spike (Phases 08-10) + v1.2 build-out boundary.
T-7.1 — Author docs/architecture-v1.0.md
- Owner: lead-developer (territory:
docs/**meta only — not architecture authoring; this is a snapshot of the upstreamdocs/architecture.md, not a re-authoring). - Files owned:
docs/architecture-v1.0.md(new). - What the file must contain (cite sources):
- Header — copy the upstream
docs/architecture.mdheader verbatim, then change the status line fromStatus: Draft v0.2. Section 13 lists 11 open decisions. ...toStatus: v1.0 (snapshot taken in ACDL Phase 07, milestone v1.1). All 11 open decisions in §13 are resolved — see PROJECT.md "Open-decision resolutions" table + decisions D-034..D-046. - Resolution session log table — replace the upstream §0
resolution-session log with a v1.0 resolution log: for each of the
11 open IDs (W1.A, W1.B, W2.A, W3.D, W3.E, BA.A, BA.B, BA.C, BA.D,
BA.E, BA.F, Q1.3-OpenTofu), a row with
ID | Resolution (one-line) | See PROJECT.md. Do not duplicate the full resolution text — point at PROJECT.md. - §13 Consolidated Open Design Decisions — replace the upstream "🟡 OPEN" markers with "✅ RESOLVED (see PROJECT.md)" for each of the 11 items, preserving the upstream section structure.
- §14 Document Status — rewrite to:
Status: v1.0. All 11 open items in §13 are resolved. The architecture is internally consistent; the v1.1 implementation spike (ACDL Phases 08-10) validates the locked substrate abstraction + contract→IR→adapter path. The v1.2 build-out (Object Lock, JWS, HITL wiring, L3B skill catalog, Kyverno/OPA, OIDC federation) is design-authored in Phase 07 and implemented post-spike. - New §15 — Phase 07 authored artifacts — a table mapping each
of the 6 schema/design files to its REQ + owner persona:
REQ-17 → schemas/ir.schema.json (platform-engineer),REQ-18 → schemas/policy_check_result.schema.json + adapters/terraform/policy/checkov_adapter.py (security-engineer),REQ-19 → platform/confidence_signal.py (backend + security co-authored),REQ-20 → platform/audit_ledger_design.md (security-engineer),REQ-21 → platform/hitl_matrix_design.md + platform/separation_of_duties.py (security-engineer),REQ-22 → schemas/contract.schema.json (backend-engineer). Note that these files land in Waves 2-4 of Phase 07. - gitea-runner rename (D-046) — update any
act_runnerreference in the snapshot togitea-runner(formerlyact_runner, renamed 2026-04 in gitea/runner#850) with a one-line note. - Body — the rest of the upstream
docs/architecture.mdbody (§§1-12) is copied verbatim; the §12.6 PolicyCheckResult shape, §10.4 HITL matrix, §8 confidence signal, §9 audit stream, §7 contract schema are all locked — the snapshot preserves them as the v1.0 record. The authored schema files (Waves 2-4) formalize these sections; the snapshot is the prose, the schemas are the contract.
- Header — copy the upstream
- Source citations:
docs/architecture.md(upstream v0.2 — the source of truth for how).PROJECT.md"Open-decision resolutions" table (the 11 resolutions).PROJECT.mddecisions D-034..D-046.ARCHITECTURE.md§status (the ACDL-repo operating copy, already notes "finalizes it to v1.0 in Phase 07").
- Commit message:
phase: 7, status: plan-as-execute, persona: lead-developer, task: T-7.1(followed by a docs commit:docs(P07): snapshot architecture to v1.0 (REQ-16); 11 decisions resolved) - Self-verify:
docs/architecture-v1.0.mdexists.- First status line says
v1.0(notv0.2). - All 11 open-decision IDs appear with
✅ RESOLVED(grep each:W1.A,W1.B,W2.A,W3.D,W3.E,BA.A,BA.B,BA.C,BA.D,BA.E,BA.F,Q1.3). gitea-runnerappears (D-046 applied);act_runneronly in a "formerly" note.- §15 table lists all 6 authored files.
Wave 2 — platform-engineer + backend-engineer (parallel): REQ-17 IR + REQ-22 contract schema
Two independent JSON Schemas, different owners, parallelizable. Both
are JSON Schema draft 2020-12, both live under schemas/, both must
python3 -m jsonschema validate.
T-7.2 — Author schemas/ir.schema.json (Target Stack IR)
- Owner: platform-engineer (territory:
modules-ir/**,adapters/terraform/**, the IR;schemas/co-owned with backend-engineer — platform-engineer is the IR author per PERSONAS.md conflict resolution). - Files owned:
schemas/ir.schema.json(new). - What the file must contain (cite sources):
- JSON Schema draft 2020-12 —
$schema: "https://json-schema.org/draft/2020-12/schema",$id: "https://acdl.cloudinit.dev/schemas/ir.schema.json",title: "ACDL Target Stack IR". - Top-level shape —
required: [version, stack, resources],properties:version(semver string),stack(name/kind/depth),resources(array of resource, minItems 1),relationships(array of relationship, optional). stack—namepattern^l[12]-[a-z][a-z0-9-]*$,kindenum[l1, l2],depthinteger min 1 max 5 (ARCHITECTURE.md §3 "max depth 5").resource($defs/resource) — required[id, type, module, inputs];idpattern^[a-z][a-z0-9-]*$;typestring (substrate-agnostic, e.g.aws:s3:bucket— the IR type, not a Terraform address);modulepattern^l1-[a-z][a-z0-9-]*@\d+\.\d+\.\d+$(L1 registry referencename@semverper W3.D);parent(string, absent for root — single parent per child per ARCHITECTURE.md §12.1);inputs(object, additionalProperties free-form — typed by the L1 module's declared inputs, validated at contract→IR resolution);outputs(object of outputSpec — typed output contract; the adapter translates to Terraform outputs);nfrs(object, opaque to the adapter, consumed by the confidence signal's NFR input).outputSpec($defs/outputSpec) — required[type];typestring (IR type, e.g.string,arn,ref:<resourceId>.<outputName>);descriptionoptional.relationship($defs/relationship) — required[from, to, kind];from/toresource ids;kindenum[parent, depends_on, uses_output]with a description noting v1 usesparent+uses_outputonly (ARCHITECTURE.md §12.1);shared_keyword(string, reserved for v2 multi-relationship, unused in v1 — A-3.2).- Substrate-agnostic invariant — the schema body must contain
no Terraform-specific terms (
variable,output,module,resourceas Terraform blocks,aws_provider prefixes in the schema keywords). Thetypefield values are IR types (aws:s3:bucket), not Terraform resource types (aws_s3_bucket). The adapter (Phase 09) does the translation; the IR is neutral. - Why this round-trips to Terraform (v1) — include a
$commentat the top: "v1 ships one adapter (Terraform). The IR is nearly isomorphic to Terraform in v1 (ARCHITECTURE.md §12.1); the adapter compiles resource.module → module block, resource.inputs → variable + arg, resource.outputs → output, relationship.kind= uses_output → interpolation, relationship.kind=parent → composition ordering hint. As more adapters appear (v2+), the IR gains expressiveness; the L1 content + contract YML + thin-composition tree do not change."
- JSON Schema draft 2020-12 —
- Source citations:
ARCHITECTURE.md§12.1 (substrate abstraction — locked commitments: resources typed inputs/outputs/NFRs, relationships single parent per child, composition tree max-depth-5, policy hooks).RESEARCH.mdTARGET 3 (v1 IR shape sketch — the executor uses this as the basis; the sketch is a recommendation, the authored schema must match ARCHITECTURE.md's locked commitments and be self-contained).PROJECT.mdW3.D (L1/L2 standard versioning —name@semverpin model, MAJOR bump = new registry entry, 12-month deprecation window).
- Commit message:
phase: 7, status: plan-as-execute, persona: platform-engineer, task: T-7.2(thenfeat(P07): author schemas/ir.schema.json (REQ-17)) - Self-verify:
python3 -m jsonschema schemas/ir.schema.jsonexits 0 (schema is itself valid JSON Schema).- A minimal valid IR instance validates:
{version:"1.0.0", stack:{name:"l2-static-asset",kind:"l2",depth:1}, resources:[{id:"s3",type:"aws:s3:bucket", module:"l1-s3@1.0.0",inputs:{bucket_name:"x",region:"us-east-1"}}]}— write this to a temp file and validate against the schema. - No
aws_s3_bucket/ Terraform-block keywords in the schema body. depthmax is 5;modulepattern enforcesname@semver;relationship.kindenum includesparent+uses_output.shared_keywordis reserved (present, unused in v1).
T-7.3 — Author schemas/contract.schema.json
- Owner: backend-engineer (territory:
schemas/**contract + IR + PolicyCheckResult per PERSONAS.md; owns the contract schema + the contract→IR resolution). - Files owned:
schemas/contract.schema.json(new). - What the file must contain (cite sources):
- JSON Schema draft 2020-12 —
$schema: "https://json-schema.org/draft/2020-12/schema",$id: "https://acdl.cloudinit.dev/schemas/contract.schema.json",title: "ACDL Contract". - Top-level required —
[stack, environment](dev's minimum per W3.E).stackpattern^l2-[a-z][a-z0-9-]*$(L2 reference);environmentenum[dev, qa, prod, dr](nostaging— Path A locked, ARCHITECTURE.md §5). inputs— object,additionalPropertiestype[string, number, boolean], always optional (W3.E: "inputsalways optional"). Description: "L2-level parameter map; free-form in v1, typed per-L1 in v1.2" (A-9.2).validation— object withe2eSuite(string, ref to the e2e suite) +loadTest(string, ref to the load test). Mandatory in qa via theallOfconditional below.runbook,dashboard,oncall— strings, mandatory in prod.drDrillRef— string, mandatory in dr.profile— enum[developer, agentic], defaultdeveloper. The L3B marker (ARCHITECTURE.md §5 "profile: agentic unlocks L3B fields").naturalLanguageIntent,confidenceAtSubmission,agentTrace— the L3B fields, optional everywhere (W3.E). Whenprofile: agentic,naturalLanguageIntentbecomes required (viaallOf).confidenceAtSubmissionis a number in[0, 1].supersedes— string,format: uuid, optional. Description: "prior contractId this re-submission replaces (after rejection — ARCHITECTURE.md §10.6)".allOfconditionals (W3.E per-env mandatory table):if environment == qa → then required: [validation](and withinvalidation, requiree2eSuite+loadTest).if environment == prod → then required: [runbook, dashboard, oncall].if environment == dr → then required: [drDrillRef].if profile == agentic → then required: [naturalLanguageIntent].
$comment— "Per-env mandatory inputs per W3.E (PROJECT.md). dev requires stack+environment; qa adds validation.e2eSuite + validation.loadTest; prod adds runbook+dashboard+oncall; dr adds drDrillRef. inputs always optional. profile: agentic fields optional everywhere (naturalLanguageIntent required when profile is agentic)."
- JSON Schema draft 2020-12 —
- Source citations:
ARCHITECTURE.md§7 (contract schema — W3.E per-env mandatory table).RESEARCH.mdTARGET 9 (contract schema sketch — the executor uses this as the basis; must be self-contained + match ARCHITECTURE.md).PROJECT.mdW3.E (the locked per-env mandatory/optional table).PROJECT.mdW2.A (tag for dev/qa, SHA for prod — not a schema field but informs thestackreference resolution in the CLI, noted in the$comment).
- Commit message:
phase: 7, status: plan-as-execute, persona: backend-engineer, task: T-7.3(thenfeat(P07): author schemas/contract.schema.json (REQ-22)) - Self-verify:
python3 -m jsonschema schemas/contract.schema.jsonexits 0.- The spike contract
{stack:"l2-static-asset",environment:"dev",inputs:{bucket_name:"acdl-spike-bucket",region:"us-east-1"}}validates (dev requires only stack+environment). - A qa contract missing
validationfails validation. - A prod contract missing
runbookfails. - A dr contract missing
drDrillReffails. - An agentic contract missing
naturalLanguageIntentfails; an agentic contract with it passes. environmentenum does NOT includestaging.
Wave 3 — security-engineer: REQ-18 + REQ-20 + REQ-21
One persona, five files, one wave (sequential within the wave). The Checkov adapter (T-7.5) depends on the PolicyCheckResult schema (T-7.4); the SoD module (T-7.8) depends on the outbox shape in the ledger design (T-7.6). Order within the wave: T-7.4 → T-7.5 → T-7.6 → T-7.7 → T-7.8.
T-7.4 — Author schemas/policy_check_result.schema.json
- Owner: security-engineer (owns
adapters/terraform/policy/**; the schema is co-authored with backend-engineer who ownsschemas/**contract + IR + PolicyCheckResult — security-engineer owns the adapter, the schema is the contract both share. security-engineer authors it here because the adapter's shape is the security concern- the severity enum is the security-engineer's territory per the conflict-resolution note on confidence_signal co-ownership).
- Files owned:
schemas/policy_check_result.schema.json(new). - What the file must contain (cite sources):
- JSON Schema draft 2020-12 —
$schema,$id: "https://acdl.cloudinit.dev/schemas/policy_check_result.schema.json",title: "ACDL PolicyCheckResult". - Canonical shape — exactly the JSON shape from
ARCHITECTURE.md§12.6:contractId(string, format uuid).evaluatedAt(string, format date-time — ISO-8601).engine(enum[checkov, kyverno, opa]).ruleId(string — e.g.CKV_AWS_24,KYVERNO_NO_PRIVILEGED,ACDL_TAG_NAMING).severity(enum[critical, high, medium, low, info]).result(enum[pass, fail, skipped, error]).message(string, human-readable).evidence(object, additionalProperties true — engine-specific payload, opaque to the confidence signal per §12.6).resourceRef(string — IR-typed resource identifier).
- Required — all of:
contractId, evaluatedAt, engine, ruleId, severity, result, message, resourceRef(evidence is optional — some engines may omit; the confidence signal does not read it). $comment— "Canonical PolicyCheckResult (ARCHITECTURE.md §12.6). The confidence signal (platform/confidence_signal.py) consumes a list of these as itspolicyinput; it is engine-agnostic. The severity enum drives the severity→penalty mapping (critical hard-override, high -0.2, medium -0.05, low -0.01, info 0.0). Adapters (checkov_adapter.py, future kyverno_adapter) translate native engine output to this shape."
- JSON Schema draft 2020-12 —
- Source citations:
ARCHITECTURE.md§12.6 (the canonical shape — copy verbatim into the schema).RESEARCH.mdTARGET 4 (PolicyCheckResult + Checkov adapter).
- Commit message:
phase: 7, status: plan-as-execute, persona: security-engineer, task: T-7.4(thenfeat(P07): author schemas/policy_check_result.schema.json (REQ-18 schema half)) - Self-verify:
python3 -m jsonschema schemas/policy_check_result.schema.jsonexits 0.- A valid instance validates:
{contractId:"<uuid>",evaluatedAt:"2026-07-21T12:00:00Z",engine:"checkov",ruleId:"CKV_AWS_24",severity:"high",result:"fail",message:"SG 0.0.0.0/0 on 22",evidence:{resource:"aws_security_group.r1"},resourceRef:"r1"}. severityenum has exactly[critical, high, medium, low, info].resultenum has exactly[pass, fail, skipped, error].engineenum has exactly[checkov, kyverno, opa].
T-7.5 — Author adapters/terraform/policy/checkov_adapter.py
- Owner: security-engineer (territory:
adapters/terraform/policy/**). - Files owned:
adapters/terraform/policy/checkov_adapter.py(new); createadapters/terraform/policy/__init__.pyif needed for import. - What the file must contain (cite sources):
- Module docstring — "Translate Checkov JSON output to ACDL
PolicyCheckResult records (schemas/policy_check_result.schema.json).
Reads Checkov's JSON output (one framework key, e.g.
terraform_plan), emits a list of PolicyCheckResult dicts. Run Checkov with--soft-failso Checkov never exits non-zero; the confidence signal decides the gate, not Checkov's exit code." RULE_MAP— dict mapping Checkov rule IDs to(ACDL L2 check name, default severity when Checkov omits one), per RESEARCH.md TARGET 4 table:CKV_AWS_41, CKV_AWS_45, CKV_AWS_46→("secrets-in-plaintext", "high").CKV_AWS_20, CKV_AWS_57→("public-ingress", "high")(S3 public ACL).CKV_AWS_24, CKV_AWS_25→("public-ingress", "medium")(SG 0.0.0.0/0).CKV_AWS_1→("iam-wildcard", "high").CKV_AWS_40→("iam-wildcard", "medium").CKV_AWS_7, CKV_AWS_33→("kms-key-reference", "medium"). Unknown Checkov rule IDs →(rule_id, "info")fallback.
_iso8601_now()— UTC now in ISO-8601Zsuffix._to_pcr(checkov_record, contract_id, result_str)— map a Checkov check record to a PolicyCheckResult dict:contractId= the contract_id arg.evaluatedAt=_iso8601_now().engine="checkov".ruleId=checkov_record["check_id"].severity=checkov_record.get("severity", default_sev).lower()(prefer Checkov's severity if present; fall back to RULE_MAP default — A-4.1: spike runs without Prisma Cloud API key).result={"PASSED":"pass","FAILED":"fail", "SKIPPED":"skipped"}.get(result_str, "error").message=checkov_record.get("check_name", "").evidence={"file_path":..., "resource":..., "resource_address":..., "code_block":...}(engine-specific, opaque to the signal).resourceRef=checkov_record.get("resource_address") or checkov_record.get("resource", "")(defensive — A-4.1 0.15 residual for field-name drift).
adapt(checkov_json_path, contract_id)— read the Checkov JSON, iterate frameworks, for each framework'sresultsbody iteratepassed_checks(→ "PASSED"),failed_checks(→ "FAILED"),skipped_checks(→ "SKIPPED"), build the list. Tolerate both{framework: {results: {...}}}and{framework: {...}}shapes (Checkov minor-version drift)._emit_tag_naming_skipped(contract_id)— per D-043, emit a singlePolicyCheckResultwithruleId: "ACDL_TAG_NAMING",result: "skipped",severity: "info",engine: "checkov",message: "tag/naming check deferred to v1.2 (D-043)". This guarantees the confidence signal'spolicyinput is non-empty even if Checkov finds nothing (the "all six inputs present" dev gate).adapt()appends the tag/naming skipped record to the output list after the Checkov pass.__main__—checkov_path, contract_id = sys.argv[1], sys.argv[2];print(json.dumps(adapt(...), indent=2)). CLI:python3 adapters/terraform/policy/checkov_adapter.py checkov.json <contract-uuid>.- No external deps — stdlib only (
json,sys,datetime). Checkov is invoked by the workflow, not by this module; this module reads Checkov's JSON output file. - Spike-scope note — a module-level comment: "Spike scope (D-043): tag/naming is a single SKIPPED record. A custom Checkov YAML rule for tag presence lands in v1.2."
- Module docstring — "Translate Checkov JSON output to ACDL
PolicyCheckResult records (schemas/policy_check_result.schema.json).
Reads Checkov's JSON output (one framework key, e.g.
- Source citations:
ARCHITECTURE.md§12.4 (policy toolchain — Checkov for the L2 checks + tag/naming) + §12.6 (PolicyCheckResult shape).RESEARCH.mdTARGET 4 (Checkov adapter sketch + rule→severity map — the executor uses this as the basis; must be self-contained).PROJECT.mdD-043 (tag/naming deferred — emit SKIPPED).
- Commit message:
phase: 7, status: plan-as-execute, persona: security-engineer, task: T-7.5(thenfeat(P07): author checkov_adapter.py (REQ-18 adapter half; D-043 tag/naming skipped)) - Self-verify:
python3 -m py_compile adapters/terraform/policy/checkov_adapter.pyexits 0.- A synthetic Checkov JSON fixture (a single failed CKV_AWS_24)
produces a PolicyCheckResult with
severity:"medium",result:"fail",ruleId:"CKV_AWS_24"+ the appendedACDL_TAG_NAMINGskipped record. - Output validates against
schemas/policy_check_result.schema.json(each record). RULE_MAPhas all 11 Checkov rule IDs from RESEARCH.md TARGET 4.- No non-stdlib imports.
T-7.6 — Author platform/audit_ledger_design.md
- Owner: security-engineer (territory:
platform/audit_ledger_design.md). - Files owned:
platform/audit_ledger_design.md(new). - What the file must contain (cite sources):
- Header — "ACDL Tiered Audit Ledger Design (REQ-20). Status: design authored in Phase 07; spike implements the v1.0 hash chain + outbox write (D-041); v1.2 builds out Object Lock + JWS + async worker + DLQ."
- Tiered ledger overview — the three tiers:
- Cold tier (source of truth): S3 with Object Lock in compliance mode, 7-year retention (ARCHITECTURE.md §9, RESEARCH TARGET 7). No one — including root — can delete/overwrite until retention expires.
- Hot tier (query index):
acdl-evidenceaudit repo (the v1.0 demo's path; unchanged). Not part of the chain; a queryable mirror. The evidence UI (evidence-ui/index.html) reads it. - Outbox (write path): DynamoDB, RPO=0 (synchronous write before submission ack). Single-region in v1 (us-east-1).
- Spike scope (D-041) — what the spike implements:
- DynamoDB outbox: table
acdl-outbox,PAY_PER_REQUEST(D-044), PKcontractId, SKeventType#eventTs, TTLexpire_at= now + 365d (1-year storage per §8). prev_event_hashchain: SHA-256 over canonical JSON (json.dumps(event, sort_keys=True, separators=(",",":"))), lifted from the v1.0 demo'sevidence_writer.py. Auto-genesis: first event hasprev_hash="GENESIS".- Synchronous write via boto3
put_item(strong-consistent by default). No separate async worker / DLQ in the spike (RTO = workflow re-run; A-5.2). - Mirror to
acdl-evidence(unchanged from v1.0 — the finalize step commitsaudit.json). - Spike evidence event shape (RESEARCH TARGET 7):
{seq, ts, stage, event, prev_hash, hash, contractId, environment, stack, score, band}.
- DynamoDB outbox: table
- v1.2 build-out — what Phase 07 designs but the spike defers:
- S3 Object Lock: bucket
acdl-evidence-lock-<account-id>, Object Lock enabled at creation, compliance mode, 7-yr retention (RetainUntilDate= now + 7y). The outbox→S3 path is an async worker that reads from the outbox and writes to Object Lock. - JWS detached signature (RFC 7515): the event payload is
canonical-JSON-serialized, SHA-256 hashed, signed with a private
key; the signature is stored detached alongside the payload.
Signing key = platform-level KMS key (not per-contract —
RESEARCH A-7.2), rotated quarterly. The
jwsfield is added to the event shape in v1.2. - Async worker + DLQ: a Lambda (or Gitea Actions scheduled workflow) reads the outbox, writes to S3 Object Lock, signs with KMS. DLQ = an SQS dead-letter queue for failed writes. RTO = DLQ replay.
- Daily checkpoints (§9): a daily job reads the last event hash and writes a "checkpoint" event to the ledger (+ optionally to a public notarization service). The spike runs in minutes, not days — no checkpoint in spike.
- S3 Object Lock: bucket
- JWS vs chain — orthogonality note: the
prev_event_hashchain gives ordering/tamper-evidence within the log; JWS gives authenticity per event. The chain is spike-scope; JWS is v1.2. - Outbox item shape (full, spike+v1.2) — the DynamoDB item:
- PK
contractId(UUID). - SK
eventType#eventTs(e.g.POLICY_CHECKED#2026-07-21T12:00:00Z). payload(the event body — hash-chained in spike, JWS-signed in v1.2).prev_event_hash(chain link).hash(this event's SHA-256).approver_qa(Gitea username of the QA approver; empty in dev-only spike; populated on qa-promotion — D-042).approver_prod(SRE username; empty in spike).environment,stack,score,band.expire_at(TTL = now + 365d).- v1.2 only:
jws(detached signature),checkpoint_ref.
- PK
- RPO/RTO table:
- Spike: RPO=0 (sync outbox write), RTO=workflow re-run.
- v1.2: RPO=0 (sync outbox), RTO=async worker DLQ replay.
- Decision trail — cite D-041 (spike scope), D-044 (outbox mode + TTL), D-042 (approver identities in outbox).
- Source citations:
ARCHITECTURE.md§9 (audit + evidence stream — locked: S3 Object Lock compliance 7-yr + DynamoDB outbox RPO=0 + JWS + chain + daily checkpoints).RESEARCH.mdTARGET 7 (tiered audit ledger — S3 Object Lock + JWS + chain; spike minimum).RESEARCH.mdTARGET 5 (DynamoDB outbox pattern — RPO=0, on-demand, PK/SK, TTL).PROJECT.mdD-041 (spike scope = hash chain + outbox), D-044 (outbox mode + TTL), D-042 (approver identities in outbox).
- Commit message:
phase: 7, status: plan-as-execute, persona: security-engineer, task: T-7.6(thendocs(P07): author platform/audit_ledger_design.md (REQ-20; D-041 spike scope)) - Self-verify:
- File exists, non-empty markdown.
- "compliance mode" + "7-year" appear (S3 Object Lock locked).
- "RPO = 0" / "RPO=0" appears.
- "JWS" + "detached" + "KMS" + "quarterly" appear (v1.2 design).
- "prev_event_hash" + "canonical JSON" + "SHA-256" appear (chain).
- "daily checkpoint" appears.
- "D-041", "D-044", "D-042" appear (decision trail).
- A clear "Spike scope" vs "v1.2 build-out" section boundary.
T-7.7 — Author platform/hitl_matrix_design.md
- Owner: security-engineer (territory:
platform/hitl_matrix_design.md). - Files owned:
platform/hitl_matrix_design.md(new). - What the file must contain (cite sources):
- Header — "ACDL Human-in-the-Loop Matrix + Separation-of-Duties Design (REQ-21). Status: design authored in Phase 07; v1.2 wires the gates. Spike (Phases 08-10) is dev-only; HITL is not exercised."
- Gate model (§10.1) — pre-execution gates. The contract is held in "validated but not applied" until the human attests. qa, prod, dr are attestation gates. No partial deployment to roll back on rejection (qa, prod); dr is a separate deployment against a separate cluster/region.
- Gitea-specific gate mechanics (D-042) — Gitea has no
Environments API and ignores
environment:blocks (v1.0 D-013; re-confirmed RESEARCH TARGET 8). The pre-execution gate is modeled as aworkflow_dispatchwith approval inputs:- qa gate:
workflow_dispatchwithapprove_qa: true; the dispatch run'sgitea.actoris the QA approver. - prod gate:
workflow_dispatchwithapprove_prod: true;gitea.actoris the SRE approver. - dr gate:
workflow_dispatchwithapprove_dr: true; same. The approver identity of record =gitea.actorof the dispatch run (D-042). There is no other approval-identity signal in Gitea.
- qa gate:
- Reviewer routing (§10.2) — Gitea CODEOWNERS routes the right reviewer to the right gate (qa → QA team; prod → SRE team; dr → SRE team). CODEOWNERS routes; it does not enforce identity distinctness (that's the outbox check in T-7.8).
- Full 8-concern attestation matrix (§10.4) — the complete table
from
docs/architecture.md§10.4, lifted verbatim: | Env | Concern | Evidence artifact | Freshness | Source | Attester | | qa | Functional correctness | Last successful run of contract-declared validation.e2eSuite with pass rate ≥ 99% | Last 24h | Test runner declared in contract | QA | | qa | Performance baseline | Load test report (k6/Gatling/Locust) showing p99 latency < declared NFR and throughput > declared minimum | Last 7d | Load test runner declared in contract | QA | | qa | Security posture | Vulnerability scan (Trivy/Snyk/contract-declared) with no criticals/highs, signed by Security on-call | Last 24h | Security scanner + Security team signature | QA | | qa | Contract NFRs | Platform-generated report: schema valid, NFR assertions within declared bounds | At submission | Platform contract validator | QA | | prod | Operational readiness | Runbook published, dashboard exists, on-call rotation assigned, alerts configured | At submission, validated against last 30d history | Platform + SRE | SRE | | prod | Incident response | Sev-1 runbook tabletop or live drill completed | Last 90d | SRE drill record | SRE | | prod | Capacity / cost | FinOps forecast for next 30d within budget envelope, cost anomaly baseline stored, budget alert configured | Forecast valid for next 30d | FinOps + SRE | SRE | | prod | Resilience | DR drill, chaos engineering report, backup verified | DR: 180d; chaos: 90d; backup: 30d | SRE + Platform | SRE | | dr | dr-region deploy with the most recent prod-bound dr drill as canary evidence | dr drill report | Last 180d | SRE | SRE | - Timeout behavior (§10.5) — the table:
| Time | State | Action |
| Submission | PENDING_ATTESTATION | Notify responsible team |
| 1 business day | PENDING_ATTESTATION_WARNING | Notify team + platform on-call (elevated path); emit PENDING_ATTESTATION_TIMEOUT_WARNING event |
| 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit PENDING_ATTESTATION_AUTO_FREEZE event; new submission linked via
supersedes| Implementation: a Giteaon: scheduleworkflow (runs hourly) that scans the outbox forPENDING_ATTESTATIONevents withtsolder than 1/2 business days and emits the warn/freeze events. Not implemented in the spike (A-8.3). - Rejection and rollback (§10.6) — rejection returns the contract
to
HELDstate with the rejection reason captured as aPROMOTION_REJECTEDevent. The consumer fixes the cause and re-submits; the new submission is linked to the rejected one viasupersedes(a contract-schema field — T-7.3). The audit chain is extended, not torn up (the "Not a mutable audit log" anti-goal). No partial deployment to roll back at any v1 gate. - Separation of duties (§10.3) — pointer to the .py — a section
that describes the check sequence (dev→qa writes approver_qa to
outbox; qa→prod reads approver_qa + new gitea.actor; if equal →
block + emit SEPARATION_OF_DUTIES_VIOLATION + route halt artifact
to SRE on-call) and points at
platform/separation_of_duties.py(T-7.8) for the implementation. - Spike scope note — the spike is dev-only (REQ-27 contract has
environment: dev), so HITL is not exercised. Phase 07 authors the design; Phase 10'sverify_phase10.shdoes not assert HITL behavior. v1.2 wires the gates. - Decision trail — cite D-042 (approver identity = gitea.actor), D-013 (v1.0 workflow_dispatch fallback, re-used).
- Source citations:
docs/architecture.md§10 (HITL mechanics — §10.1 gate model, §10.2 routing, §10.3 SoD, §10.4 full matrix, §10.5 timeout, §10.6 rejection).ARCHITECTURE.md§10 (ACDL-repo operating copy — same content, refined).RESEARCH.mdTARGET 8 (HITL matrix + SoD — Gitea adaptation, design doc sketch).PROJECT.mdD-042 (approver identity = gitea.actor).
- Commit message:
phase: 7, status: plan-as-execute, persona: security-engineer, task: T-7.7(thendocs(P07): author platform/hitl_matrix_design.md (REQ-21; D-042 gitea.actor)) - Self-verify:
- File exists, non-empty markdown.
- All 8 concerns from §10.4 appear (functional, performance, security, NFRs, op-readiness, incident response, capacity/cost, resilience).
- "gitea.actor" appears (D-042).
- "workflow_dispatch" + "approve_qa" + "approve_prod" + "approve_dr" appear.
- "1 business day" + "2 business days" + "PENDING_ATTESTATION" appear.
- "supersedes" + "HELD" + "PROMOTION_REJECTED" appear.
- "CODEOWNERS" appears.
- "no Environments API" / "no Environments API" appears.
T-7.8 — Author platform/separation_of_duties.py
- Owner: security-engineer (territory:
platform/separation_of_duties.py). - Files owned:
platform/separation_of_duties.py(new). - What the file must contain (cite sources):
- Module docstring — "Check that qaApprover != prodApprover for a
contract (ARCHITECTURE.md §10.3, D-042). Reads
approver_qafrom the DynamoDB outbox for the contractId, compares to the prod-dispatchgitea.actor. Blocks on equality, emitsSEPARATION_OF_DUTIES_VIOLATION, routes a halt artifact to SRE on-call. Spike: dev-only — the check is a no-op stub that returns (True, 'dev-only') because the spike does not exercise HITL (A-8.1)." check(outbox_client, contract_id, current_prod_approver)function:item = outbox_client.get(contract_id)— reads the outbox item for the contractId (the outbox shape is defined inplatform/audit_ledger_design.mdT-7.6: PKcontractId, SKeventType#eventTs, attributes includeapprover_qa).- If
item is None: return(True, "no prior approver (first promotion)")— dev→qa has no SoD check (the first promotion has no prior approver to compare against). qa_approver = item.get("approver_qa"). Ifnot qa_approver: return(True, "no QA approver recorded (dev-only spike)")— the spike's dev-only path.- If
qa_approver == current_prod_approver: return(False, f"SEPARATION_OF_DUTIES_VIOLATION: qaApprover==prodApprover=={qa_approver}")— the platform blocks prod promotion, writes aSEPARATION_OF_DUTIES_VIOLATIONevent to the evidence stream (the pipeline step does the write; this function returns the verdict), and routes a halt artifact to SRE on-call. - Else: return
(True, "distinct").
route_halt_artifact(contract_id, violation_reason, oncall_client)— a helper that routes the halt artifact to SRE on-call (spike: stub that logs the artifact; v1.2 wires a real pager). Not invoked in the dev-only spike.- Spike-scope note — a module-level comment: "Spike scope (A-8.1): the spike is dev-only (REQ-27 contract has environment: dev); HITL is not exercised. This module is authored to its full v1.2 shape but the spike calls it with current_prod_approver=None and a None outbox_client — the check returns (True, 'dev-only')."
- No external deps — stdlib only (
typingfor the signature).outbox_clientis a duck-typed boto3-like client (has.get(pk)); the spike passes a stub. Noimport boto3(the pipeline step owns the boto3 client; this function receives it).
- Module docstring — "Check that qaApprover != prodApprover for a
contract (ARCHITECTURE.md §10.3, D-042). Reads
- Source citations:
ARCHITECTURE.md§10.3 (separation of duties — identity distinctness sequence).RESEARCH.mdTARGET 8 (SoD sketch — the executor uses this as the basis).PROJECT.mdD-042 (approver identity = gitea.actor; the check reads approver_qa from the outbox, compares to the prod-dispatch gitea.actor).platform/audit_ledger_design.mdT-7.6 (the outbox item shape — PK, SK,approver_qaattribute).
- Commit message:
phase: 7, status: plan-as-execute, persona: security-engineer, task: T-7.8(thenfeat(P07): author platform/separation_of_duties.py (REQ-21; D-042; reads outbox approver_qa)) - Self-verify:
python3 -m py_compile platform/separation_of_duties.pyexits 0.check(None, "any", None)returns(True, "no prior approver...").check(stub_with_approver_qa("alice"), "cid", "alice")returns(False, "...SEPARATION_OF_DUTIES_VIOLATION...").check(stub_with_approver_qa("alice"), "cid", "bob")returns(True, "distinct").- No
import boto3(duck-typed client). - "SEPARATION_OF_DUTIES_VIOLATION" string is present.
Wave 4 — backend-engineer + security-engineer co-authored: REQ-19 confidence_signal.py
One file, one commit, co-authored (PERSONAS.md conflict resolution:
backend owns the 6-input weighted sum + per-env thresholds; security
owns the severity→penalty + critical-override). Depends on T-7.4
(PolicyCheckResult schema — the confidence signal consumes a list of
PolicyCheckResult records as its policy input).
T-7.9 — Author platform/confidence_signal.py
- Owners: backend-engineer (6-input weighted sum + per-env
thresholds +
compute()orchestration +Signaldataclass) + security-engineer (severity→penalty mapping + critical-override semantics + the policy-input penalty loop). Co-authored in one commit. lead-developer arbitrates if the two disagree (PERSONAS.md conflict resolution). - Files owned:
platform/confidence_signal.py(new). - What the file must contain (cite sources):
- Module docstring — the 6 inputs + weights + severity→penalty,
verbatim from D-040 + ARCHITECTURE.md §8:
Inputs (weights sum to 1.0, D-040): 1. policy_results (0.30) — list[PolicyCheckResult] (schemas/policy_check_result.schema.json) 2. validation (0.25) — {schema: bool, ir_resolved: bool, tf_validated: bool, tf_planned: bool} 3. freshness (0.10) — {age_days: float, max_age_days: float} 4. source (0.15) — {submitter: str, commit_sha: str, signed: bool} 5. history (0.10) — {prior_rollbacks: int, prior_policy_fails: int} 6. nfrs (0.10) — {declared: list[str], conformance: float|None} Severity → penalty (locked, §8): critical → hard override (score = 0, block) high → -0.20 medium → -0.05 low → -0.01 info → 0.00 Per-env thresholds (locked, §8): dev 0.50, qa 0.75, prod 0.90, dr 0.95. Output: {score, band, perInput, reasonCodes}. Halt with explicit reason on missing input (§8). WEIGHTSdict —{"policy":0.30, "validation":0.25, "freshness":0.10, "source":0.15, "history":0.10, "nfrs":0.10}(D-040). Sum = 1.0.PENALTYdict —{"critical":None, "high":0.20, "medium":0.05, "low":0.01, "info":0.0}(None= hard override — security-engineer territory).THRESHOLDSdict —{"dev":0.50, "qa":0.75, "prod":0.90, "dr":0.95}(ARCHITECTURE.md §8).Signaldataclass —score: float,band: Literal["pass", "warn", "block"],perInput: dict,reasonCodes: list[str]._per_input_score(name, raw)— normalize a raw input to[0,1]. Spike cold-start: unknown/missing inputs →0.5(A-6.2) with aINPUT_MISSING:<name>reason code. The per-input scoring rules (documented inline):policy: weighted by severity — pass=1.0, skipped=1.0 (treat as pass — the ACDL_TAG_NAMING skipped record is neutral), fail=0.0 per finding, averaged. (The severity penalty is applied separately at the score level — seecompute().)validation:1.0if all four bools true, else proportional.freshness:1.0 - (age_days / max_age_days)clamped to[0,1].source:1.0ifsubmitter+commit_shapresent (dev gate is presence, not identity — RESEARCH A-6.1).history:1.0 - (prior_rollbacks * 0.2 + prior_policy_fails * 0.1)clamped to[0,1]. Spike cold-start (first submission) =0.5(no history — A-6.2).nfrs:1.0ifconformancepresent, else0.5(no NFRs declared — spike cold-start A-6.2).
compute(contract_id, environment, inputs)— the orchestrator (backend-engineer):- If
set(inputs.keys()) != set(WEIGHTS.keys()): returnSignal(0.0, "block", {}, [f"INPUT_MISSING:{m}" for m in missing])— halt with explicit reason on missing input (§8). - Build
per_input,reasons,base(weighted sum) via_per_input_scorefor each input. - Severity penalty loop (security-engineer): for each
pcrininputs.get("policy_results", [])wherepcr["result"] == "fail":- If
PENALTY[pcr["severity"]] is None(critical): returnSignal(0.0, "block", per_input, reasons + [f"CRITICAL_OVERRIDE:{pcr['ruleId']}"])— hard override, one critical finding blocks regardless of all other inputs (§8). - Else:
penalty += PENALTY[pcr["severity"]].
- If
score = max(0.0, min(1.0, base - penalty))(clamp to[0,1]).threshold = THRESHOLDS[environment].band = "pass" if score >= threshold else ("block" if score < threshold - 0.10 else "warn")(the warn band is 0.10 wide — A-6.3).- Dev override:
if environment == "dev" and band == "warn": band = "block"— dev is autonomous, no reviewer to handle a warn (A-6.3). - Return
Signal(score, band, per_input, reasons).
- If
__main__— a tiny CLI for spike invocation: read a JSON file of inputs + environment, print the Signal as JSON. Spike workflow callspython3 platform/confidence_signal.py <inputs.json> dev.- No external deps — stdlib only (
dataclasses,typing,json,sys). The PolicyCheckResult records arrive as plain dicts (the Checkov adapter emits dicts, not a typed class). - Spike cold-start note — a module-level comment: "Spike cold- start (A-6.2): inputs 3 (freshness), 5 (history), 6 (nfrs) are 'present + neutral 0.5' because the spike is the first submission with no history and no declared NFRs. The gate is presence, not conformance — the 'all six inputs present' dev gate (§5) is satisfied by non-null per-input scores."
- Module docstring — the 6 inputs + weights + severity→penalty,
verbatim from D-040 + ARCHITECTURE.md §8:
- Source citations:
ARCHITECTURE.md§8 (confidence signal — 6 inputs, weighted sum, per-env thresholds, severity→penalty, output shape, halt-on-missing).RESEARCH.mdTARGET 6 (6-input confidence signal sketch — the executor uses this as the basis; must be self-contained + match ARCHITECTURE.md).PROJECT.mdD-040 (the 6 inputs + weights frozen for v1).schemas/policy_check_result.schema.jsonT-7.4 (thepolicy_resultsinput is a list of these records).
- Commit message:
phase: 7, status: plan-as-execute, persona: backend-engineer+security-engineer, task: T-7.9(thenfeat(P07): author platform/confidence_signal.py (REQ-19; D-040 6 inputs; co-authored backend+security)) - Self-verify:
python3 -m py_compile platform/confidence_signal.pyexits 0.WEIGHTSsums to 1.0; values match D-040 exactly.PENALTY["critical"] is None(hard override sentinel).THRESHOLDSmatches dev 0.50 / qa 0.75 / prod 0.90 / dr 0.95.- A missing-input call returns
Signal(0.0, "block", ..., ["INPUT_MISSING:..."]). - A critical-fail PolicyCheckResult hard-overrides to
0.0, blockregardless of other inputs. - A dev
warnband is flipped toblock(autonomous, no reviewer). - A cold-start spike input set (all 6 present, policy_results =
[the ACDL_TAG_NAMING skipped record], validation all true,
freshness/source/history/nfrs neutral 0.5) produces a
passfor dev (score ≥ 0.50). - No non-stdlib imports.
Wave 5 — lead-developer: verify_phase07.sh + traceability
The phase gate. Depends on all prior waves (the verify script asserts all 6 files exist + validate). Updates the traceability tables in REQUIREMENTS.md + ROADMAP.md.
T-7.10 — Author scripts/verify_phase07.sh + traceability update
- Owner: lead-developer (territory:
scripts/verify_phase*.sh,.ciagent/**traceability). - Files owned:
scripts/verify_phase07.sh(new) +.ciagent/REQUIREMENTS.md(traceability table edit) +.ciagent/ROADMAP.md(Phase 07 status edit). - What the verify script must assert (cite the must_haves in this
PLAN.md frontmatter):
- All 6 schema/design files exist:
schemas/ir.schema.jsonschemas/policy_check_result.schema.jsonschemas/contract.schema.jsonplatform/confidence_signal.pyplatform/audit_ledger_design.mdplatform/hitl_matrix_design.mdadapters/terraform/policy/checkov_adapter.pyplatform/separation_of_duties.pydocs/architecture-v1.0.md(9 files — the 6 REQ-mapped files + checkov_adapter + SoD + the snapshot; count carefully: REQ-17→1, REQ-18→2, REQ-19→1, REQ-20→1, REQ-21→2, REQ-22→1, REQ-16→1 = 9 files total.)
- The 3 JSON Schemas validate as JSON Schema draft 2020-12:
(If
python3 -m jsonschema schemas/ir.schema.json && \ python3 -m jsonschema schemas/policy_check_result.schema.json && \ python3 -m jsonschema schemas/contract.schema.jsonpython3 -m jsonschemais unavailable, fall back toajv compile -s schemas/<file>— note this in the script.) - The 3 .py files py_compile:
python3 -m py_compile platform/confidence_signal.py \ platform/separation_of_duties.py \ adapters/terraform/policy/checkov_adapter.py - The 3 .md design files are non-empty (markdown presence):
test -s platform/audit_ledger_design.md && \ test -s platform/hitl_matrix_design.md && \ test -s docs/architecture-v1.0.md - All 11 decision IDs + OpenTofu are present in PROJECT.md:
for id in W1.A W1.B W2.A W3.D W3.E BA.A BA.B BA.C BA.D BA.E BA.F; do grep -q "$id" .ciagent/PROJECT.md || FAIL "missing $id" done grep -qi "opentofu" .ciagent/PROJECT.md || FAIL "missing OpenTofu" docs/architecture-v1.0.mdexists + status is v1.0:test -f docs/architecture-v1.0.md grep -q "v1.0" docs/architecture-v1.0.md | head -1 # status line- D-040, D-041, D-042, D-043, D-044 present in PROJECT.md (the
Phase 07-relevant decisions):
for d in D-040 D-041 D-042 D-043 D-044; do grep -q "$d" .ciagent/PROJECT.md || FAIL "missing $d" done - The spike contract validates against the contract schema (a
cross-file consistency check — the spike contract is the one
REQ-27 will use):
# Write a minimal dev contract to a temp file, validate against # schemas/contract.schema.json. (The real contracts/spike.yaml # lands in Phase 10.) echo '{"stack":"l2-static-asset","environment":"dev","inputs":{"bucket_name":"x","region":"us-east-1"}}' \ > /tmp/spike-contract.json python3 -c "import jsonschema, json; \ jsonschema.validate(json.load(open('/tmp/spike-contract.json')), \ json.load(open('schemas/contract.schema.json')))" - A minimal IR instance validates against the IR schema:
echo '{"version":"1.0.0","stack":{"name":"l2-static-asset","kind":"l2","depth":1},"resources":[{"id":"s3","type":"aws:s3:bucket","module":"l1-s3@1.0.0","inputs":{"bucket_name":"x","region":"us-east-1"}}]}' \ > /tmp/spike-ir.json python3 -c "import jsonschema, json; \ jsonschema.validate(json.load(open('/tmp/spike-ir.json')), \ json.load(open('schemas/ir.schema.json')))" - Exit 0 on success, non-zero on any failure (with a clear
FAIL: <reason>message per assertion). PrintVERIFIED — Phase 07: architecture v1.0 finalized; 6 files authored + 11 decisions resolvedon success.
- All 6 schema/design files exist:
- Traceability update (in the same commit or a follow-up):
.ciagent/REQUIREMENTS.mdtraceability table: REQ-16..22 →complete (v1.1.2)(the Phase 07 ship tag — Phase 06 was v1.1.1)..ciagent/ROADMAP.mdPhase 07 status:pending→complete (v1.1.2).
- Source citations:
- This PLAN.md's must_haves (the assertions enumerate them).
PERSONAS.mdverification_toolchain (typecheck/test/build substitutes —python3 -m jsonschema,py_compile,scripts/verify_phaseNN.sh).
- Commit message:
phase: 7, status: plan-as-execute, persona: lead-developer, task: T-7.10(thenverify(P07): VERIFIED — 6 files authored, 11 decisions resolved, architecture v1.0) (thendocs(P07): post-ship traceability + roadmap update (v1.1.2)) - Self-verify:
scripts/verify_phase07.shexits 0 when run from repo root.- The script fails (non-zero) if any of the 9 files is missing.
- The script fails if any of the 3 JSON Schemas is invalid JSON Schema.
- The script fails if any of the 3 .py files fails py_compile.
- The script fails if any of the 11 decision IDs is missing from PROJECT.md.
- The script fails if
docs/architecture-v1.0.mddoes not sayv1.0in its status line. - REQUIREMENTS.md traceability shows REQ-16..22 complete.
- ROADMAP.md Phase 07 status is complete.
Wave ordering + dependencies (summary)
Wave 1 (lead-developer) T-7.1 architecture-v1.0.md (umbrella; references decisions)
↓
Wave 2 (platform + backend) T-7.2 ir.schema.json (platform-engineer)
T-7.3 contract.schema.json (backend-engineer) [parallel with T-7.2]
↓
Wave 3 (security-engineer) T-7.4 policy_check_result.schema.json
T-7.5 checkov_adapter.py (depends T-7.4)
T-7.6 audit_ledger_design.md
T-7.7 hitl_matrix_design.md
T-7.8 separation_of_duties.py (depends T-7.6 outbox shape)
↓
Wave 4 (backend + security) T-7.9 confidence_signal.py (depends T-7.4 PolicyCheckResult; co-authored)
↓
Wave 5 (lead-developer) T-7.10 verify_phase07.sh + traceability (depends all prior)
Parallelizable within a wave:
- Wave 2: T-7.2 (IR, platform-engineer) || T-7.3 (contract, backend-engineer) — independent schemas, different owners, no shared files.
Sequential within a wave (same persona or file dependency):
- Wave 3: T-7.4 → T-7.5 (adapter consumes the schema it emits) → T-7.6 → T-7.7 → T-7.8 (SoD reads the outbox shape T-7.6 documents). All security-engineer; one persona, sequential to avoid territory friction.
Cross-wave dependencies:
- Wave 4 → Wave 3 T-7.4 (confidence_signal consumes PolicyCheckResult).
- Wave 4 → Wave 3 T-7.6 (confidence_signal's policy input shape is informed by the outbox/ledger design — not a hard dependency, but consistency).
- Wave 5 → all prior waves (verify script asserts all files).
Per-file content checklist (executor self-verify)
schemas/ir.schema.json (REQ-17, T-7.2)
- JSON Schema draft 2020-12 (
$schema+$id+title). - Top-level
required: [version, stack, resources]. stack.depthinteger min 1 max 5.stack.namepattern^l[12]-[a-z][a-z0-9-]*$.stack.kindenum[l1, l2].resourcerequired[id, type, module, inputs].resource.modulepattern^l1-[a-z][a-z0-9-]*@\d+\.\d+\.\d+$(name@semver per W3.D).resource.parent(single parent per child, ARCHITECTURE.md §12.1).resource.outputs(typed outputSpec).resource.nfrs(opaque to adapter, consumed by confidence NFR input).relationship.kindenum[parent, depends_on, uses_output].relationship.shared_keywordreserved for v2 (present, unused).- Substrate-agnostic invariant: no Terraform-block keywords
(
variable,outputas block,aws_provider prefixes) in the schema body;typevalues are IR types (aws:s3:bucket), not Terraform types (aws_s3_bucket). $commentexplains the v1 round-trip to Terraform.python3 -m jsonschemavalidates the schema itself.- A minimal valid IR instance validates against it.
schemas/policy_check_result.schema.json (REQ-18, T-7.4)
- JSON Schema draft 2020-12.
- Canonical shape from ARCHITECTURE.md §12.6:
contractId,evaluatedAt,engine,ruleId,severity,result,message,evidence,resourceRef. engineenum[checkov, kyverno, opa].severityenum[critical, high, medium, low, info].resultenum[pass, fail, skipped, error].evidenceobject, additionalProperties true (opaque to signal).python3 -m jsonschemavalidates; a valid instance passes.
adapters/terraform/policy/checkov_adapter.py (REQ-18, T-7.5)
- Module docstring (Checkov JSON → PolicyCheckResult).
RULE_MAPwith all 11 Checkov rule IDs (CKV_AWS_41/45/46/20/57/ 24/25/1/40/7/33)._to_pcr()maps severity (prefer Checkov's, fall back to RULE_MAP default)._emit_tag_naming_skipped()emits the ACDL_TAG_NAMING SKIPPED record (D-043).adapt()tolerates both Checkov JSON shapes ({framework:{results:{...}}}and{framework:{...}}).__main__CLI:checkov_adapter.py <checkov.json> <contract-id>.- stdlib only (no
import boto3/ no third-party). python3 -m py_compileexits 0.- Output records validate against policy_check_result.schema.json.
schemas/contract.schema.json (REQ-22, T-7.3)
- JSON Schema draft 2020-12.
- Top-level
required: [stack, environment](dev minimum). stackpattern^l2-[a-z][a-z0-9-]*$.environmentenum[dev, qa, prod, dr](nostaging).inputsobject, additionalProperties[string, number, boolean], always optional.validation(e2eSuite + loadTest),runbook,dashboard,oncall,drDrillRefall present.profileenum[developer, agentic], defaultdeveloper.- L3B fields
naturalLanguageIntent,confidenceAtSubmission,agentTracepresent, optional everywhere. supersedes(uuid, optional).allOfconditionals: qa→validation required, prod→runbook+ dashboard+oncall required, dr→drDrillRef required, agentic→naturalLanguageIntent required.- Spike contract validates; qa/prod/dr/agentic negative cases fail.
platform/confidence_signal.py (REQ-19, T-7.9)
- Module docstring with the 6 inputs + weights + severity→penalty.
WEIGHTS={policy:0.30, validation:0.25, freshness:0.10, source:0.15, history:0.10, nfrs:0.10}(D-040; sums to 1.0).PENALTY={critical:None, high:0.20, medium:0.05, low:0.01, info:0.0}(security-engineer; None = hard override).THRESHOLDS={dev:0.50, qa:0.75, prod:0.90, dr:0.95}(§8).Signaldataclass:score, band, perInput, reasonCodes._per_input_score()per-input rules (policy severity-weighted, validation all-true, freshness age ratio, source presence, history rollback/fail, nfrs conformance or neutral 0.5).compute()halts withINPUT_MISSING:<name>on missing input.compute()critical-override: anyfail+critical→Signal(0.0, "block", ..., ["CRITICAL_OVERRIDE:<ruleId>"]).compute()devwarnflipped toblock(autonomous, no reviewer).- Spike cold-start (all 6 present, ACDL_TAG_NAMING skipped,
validation true, others neutral 0.5) → dev
pass. - stdlib only;
python3 -m py_compileexits 0.
platform/audit_ledger_design.md (REQ-20, T-7.6)
- Header notes Phase 07 design + spike scope (D-041) + v1.2 build-out.
- Three tiers: S3 Object Lock (cold/source of truth), acdl-evidence (hot/query index), DynamoDB outbox (write path).
- S3 Object Lock: compliance mode, 7-year retention.
- DynamoDB outbox: RPO=0,
PAY_PER_REQUEST(D-044), PK/SK, TTLexpire_at= now + 365d. prev_event_hashchain: SHA-256 over canonical JSON, genesisprev_hash="GENESIS".- JWS: detached signature, platform KMS key, quarterly rotation (v1.2).
- Daily checkpoints (v1.2; spike runs in minutes).
- Async worker + DLQ (v1.2; spike RTO = workflow re-run).
- Outbox item shape (PK, SK, payload, prev_event_hash, hash, approver_qa, approver_prod, environment, stack, score, band, expire_at; v1.2: jws, checkpoint_ref).
- RPO/RTO table (spike vs v1.2).
- Decision trail: D-041, D-044, D-042 cited.
- Clear "Spike scope" vs "v1.2 build-out" section boundary.
- File non-empty.
platform/hitl_matrix_design.md (REQ-21, T-7.7)
- Header notes Phase 07 design + v1.2 wiring + spike dev-only.
- Pre-execution gate model (§10.1).
- Gitea-specific gate mechanics:
workflow_dispatch+approve_qa/approve_prod/approve_dr,gitea.actoras approver identity (D-042). - "no Environments API" note (Gitea constraint).
- CODEOWNERS routing (§10.2).
- Full 8-concern matrix from §10.4 (verbatim table).
- Timeout 1d warn / 2d freeze (§10.5) +
PENDING_ATTESTATION*states. - Rejection → HELD +
PROMOTION_REJECTED+supersedes(§10.6). - SoD section pointing at
platform/separation_of_duties.py. - Spike-scope note (dev-only, HITL not exercised).
- D-042 cited.
- File non-empty.
platform/separation_of_duties.py (REQ-21, T-7.8)
- Module docstring (qaApprover != prodApprover, D-042, reads outbox).
check(outbox_client, contract_id, current_prod_approver)→(ok: bool, reason: str).Noneoutbox item →(True, "no prior approver...").- Empty
approver_qa→(True, "no QA approver... (dev-only)"). qa_approver == current_prod_approver→(False, "SEPARATION_OF_DUTIES_VIOLATION: ...").- Distinct →
(True, "distinct"). route_halt_artifact()helper (spike stub).- stdlib only (duck-typed outbox_client; no
import boto3). python3 -m py_compileexits 0.
docs/architecture-v1.0.md (REQ-16, T-7.1)
- Status line says
v1.0(notv0.2). - Resolution log table with all 11 IDs (W1.A, W1.B, W2.A, W3.D, W3.E, BA.A, BA.B, BA.C, BA.D, BA.E, BA.F, Q1.3).
- §13 markers changed from
🟡 OPENto✅ RESOLVED (see PROJECT.md). - §14 status rewritten to v1.0 + spike/v1.2 note.
- §15 new table mapping the 6 files → REQ + owner persona.
gitea-runnerrename applied (D-046);act_runneronly in a "formerly" note.- Body §§1-12 copied from upstream
docs/architecture.md.
scripts/verify_phase07.sh (T-7.10)
- Asserts all 9 files exist (6 REQ-mapped + checkov_adapter + SoD + snapshot).
- Validates the 3 JSON Schemas (
python3 -m jsonschemaorajv). - py_compiles the 3 .py files.
- Markdown presence for the 3 .md files.
- Greps PROJECT.md for all 11 decision IDs + OpenTofu.
- Confirms
docs/architecture-v1.0.mdexists + status is v1.0. - Confirms D-040..D-044 present in PROJECT.md.
- Cross-check: spike contract validates against contract schema; minimal IR validates against IR schema.
- Exits 0 on success, non-zero with
FAIL:on any failure. - Prints
VERIFIED — Phase 07: ...on success. - REQUIREMENTS.md REQ-16..22 → complete.
- ROADMAP.md Phase 07 → complete.
Spike scope vs v1.2 build-out (design-authoring boundary)
The Phase 07 designs author the full target but clearly mark the spike scope (D-041, D-043) vs the v1.2 build-out. This boundary is binding for the executor:
| Concern | Spike scope (Phases 08-10) | v1.2 build-out (design only in Phase 07) |
|---|---|---|
| Audit ledger | hash chain + DynamoDB outbox write + acdl-evidence mirror | S3 Object Lock + JWS + KMS key + async worker + DLQ + daily checkpoints |
| HITL | dev-only (not exercised) | full 8-concern matrix wiring (qa/prod/dr gates, timeout, rejection) |
| SoD | dev-only stub (returns "distinct") | full outbox read + gitea.actor compare + halt artifact routing |
| Policy | Checkov + ACDL_TAG_NAMING SKIPPED (D-043) | custom Checkov YAML rule for tag/naming + Kyverno + OPA |
| L3B | single stub contract submission | 5-skill catalog + real agent |
| AWS auth | per-run-rotated long-lived key (D-039 waiver) | real OIDC federation (blocked on go-gitea/gitea#36988) |
| Terraform | plan only |
apply (HITL-gated) |
| Regions | single-region (us-east-1) | multi-region |
The designs must not collapse this boundary — the spike scope is the minimum that proves the IR commitments hold (REQ-28); the v1.2 build-out is the production platform. Each design doc has a clear "Spike scope" section + a "v1.2 build-out" section.
Decisions made during planning
| ID | Decision | Rationale | Confidence | Alternatives |
|---|---|---|---|---|
| D-P07-1 | 5 waves, not 4 | Wave 1 (snapshot) is umbrella — it references the decisions (already in PROJECT.md), not the authored files, so it can land first and does not need to wait for Waves 2-4. Splitting the security-engineer's 5 files into one wave (Wave 3) keeps one persona's territory coherent. Wave 4 (co-authored confidence_signal) is a distinct co-authorship event, separate from Wave 3. Wave 5 (verify) is the phase gate. | 0.85 | 4 waves (merge Wave 1 into Wave 5; but then the snapshot is authored last, after the files it references — worse); 6 waves (split Wave 3 into two — unnecessary, same persona). |
| D-P07-2 | Wave 2 parallel (IR | contract) | The two JSON Schemas are independent (no shared $ref, no shared file), owned by different personas (platform-engineer + backend-engineer), and both live under schemas/. Parallelizing them uses the parallelization.enabled: true config + max_concurrent_agents: 5. |
|
| D-P07-3 | Wave 3 sequential (one persona, 5 files) | All 5 files are security-engineer territory. Running them sequentially within the wave avoids territory friction (PERSONAS.md territory enforcement mode = warn) and respects the intra-wave file dependencies (T-7.5 → T-7.4; T-7.8 → T-7.6). |
0.85 | Parallel (territory overlap risk; the co-owned schemas/policy_check_result.schema.json is authored by security-engineer here but backend-engineer co-owns schemas/** — sequential avoids a race). |
| D-P07-4 | Wave 4 co-authored in one commit | platform/confidence_signal.py is co-owned (backend: 6-input weighted sum + thresholds; security: severity→penalty + critical-override). One file, one commit, co-authored. lead-developer arbitrates if the two disagree (PERSONAS.md conflict resolution). |
0.80 | Two commits (backend first, security second) — but that splits a single file across two commits, which is worse for review. |
| D-P07-5 | verify_phase07.sh cross-checks spike contract + minimal IR against schemas | The verify script does not just assert file presence — it cross-checks that the spike contract (REQ-27's contracts/spike.yaml shape) validates against the contract schema, and a minimal IR validates against the IR schema. This catches schema/instance drift early (Phase 10 would otherwise discover it). |
0.85 | Presence-only assertions (weaker; Phase 10 discovers drift later). |
Cross-persona co-authorship risk
Two files are co-owned per PERSONAS.md conflict resolutions:
-
platform/confidence_signal.py(Wave 4, T-7.9) — backend-engineer (6-input weighted sum + per-env thresholds +compute()orchestration)- security-engineer (severity→penalty + critical-override). Risk:
the two authors must agree on the
PENALTYdict's sentinel for critical (Nonevs a special float vs a separatehard_overrideflag) + thecompute()control flow (where the critical-override short-circuit lives). The PLAN.md spec fixes this:PENALTY["critical"] is Noneis the sentinel; the critical-override is a short-circuitreturnincompute()before the score clamp. lead-developer arbitrates if the authors disagree (PERSONAS.md). Mitigation: one commit, co-authored; the PLAN.md content checklist is the contract between the two authors.
- security-engineer (severity→penalty + critical-override). Risk:
the two authors must agree on the
-
schemas/policy_check_result.schema.json(Wave 3, T-7.4) — security-engineer authors it (owns the adapter); backend-engineer co-ownsschemas/**(contract + IR + PolicyCheckResult). Risk: low — the shape is locked verbatim from ARCHITECTURE.md §12.6; there is no design freedom to conflict over. security-engineer authors; backend-engineer reviews. Mitigation: the §12.6 canonical shape is the contract; no co-authoring needed, just review.
No other co-authorship risk in Phase 07. The IR schema
(schemas/ir.schema.json) is platform-engineer's (PERSONAS.md conflict
resolution: platform-engineer owns the IR; backend-engineer owns the
contract schema + contract→IR resolution — different files). The
contract schema is backend-engineer's alone. The security-engineer's
five files (Wave 3) are all security-engineer territory with no overlap.
End of Phase 07 PLAN. Path: /root/acdl/.ciagent/PLAN.md.