622abe015b
---ci---
project: acdl
phase: 10
milestone: v1.1
status: plan-as-execute
persona: backend-engineer
task: [T-10.4, T-10.5, T-10.7]
requirements.covered: [REQ-27]
---/ci---
Wave 2: contract + resolver + outbox writer.
- T-10.4: contracts/spike.yaml - the spike contract (stack:
l2-static-asset, environment: dev, inputs bucket_name + region). D-P10-2:
YAML consumer surface; the resolver parses YAML -> validates against the
JSON contract schema.
- T-10.5: acdl_platform/contract_resolver.py - resolve(contract_path) ->
IR instance. 7 steps: load YAML, validate contract schema, look up L2 in
registry, load composition.json, map inputs through wires, emit IR
instance, validate IR against ir.schema.json. Verified end-to-end:
spike.yaml -> IR instance with kind=l2, one l1-s3 resource, validates
against ir.schema.json.
- T-10.7: acdl_platform/outbox_writer.py - write_event(event) ->
DynamoDB put_item. SHA-256 over canonical JSON, prev_event_hash=GENESIS
for the first event (D-P10-3), PK contractId, SK eventType#eventTs, TTL
expire_at = now + 365d (D-044). stdlib + boto3.
Also regenerated terraform/spike/{main.tf,terraform.tf} by running the
adapter against the resolved L2 IR (the backend key is now
spike/l2-static-asset/terraform.tfstate, derived from the stack name per
D-P10-1).
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
"""ACDL Outbox Writer — write an evidence event to the DynamoDB outbox.
|
|
|
|
ARCHITECTURE.md §9: DynamoDB outbox, RPO=0 (synchronous write before
|
|
ack). The event is hash-chained (SHA-256 over canonical JSON); the first
|
|
event has prev_event_hash="GENESIS". D-P10-3: the spike writes ONE
|
|
CONFIDENCE_COMPUTED event.
|
|
|
|
The outbox table (Phase 08): acdl-outbox, PAY_PER_REQUEST, PK contractId,
|
|
SK eventType#eventTs, TTL expire_at = now + 365d (D-044).
|
|
|
|
CLI: outbox_writer.py <event.json> (uses AWS creds from env)
|
|
"""
|
|
|
|
import datetime
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import boto3
|
|
|
|
|
|
OUTBOX_TABLE = "acdl-outbox"
|
|
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
|
|
|
|
|
def _canonical_hash(event):
|
|
"""SHA-256 over canonical JSON (sort_keys, compact separators)."""
|
|
canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
|
|
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def write_event(event, outbox_table=OUTBOX_TABLE, region=REGION):
|
|
"""Write an evidence event to the DynamoDB outbox. Returns the item dict."""
|
|
contract_id = event["contractId"]
|
|
event_type = event.get("eventType", "CONFIDENCE_COMPUTED")
|
|
event_ts = event.get("ts") or datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
sk = f"{event_type}#{event_ts}"
|
|
|
|
# Chain: first event = GENESIS (D-P10-3 spike writes one event).
|
|
prev_hash = event.get("prev_event_hash", "GENESIS")
|
|
event_hash = _canonical_hash(event)
|
|
|
|
item = {
|
|
"contractId": {"S": contract_id},
|
|
"eventType#eventTs": {"S": sk},
|
|
"payload": {"S": json.dumps(event, sort_keys=True)},
|
|
"prev_event_hash": {"S": prev_hash},
|
|
"hash": {"S": event_hash},
|
|
"environment": {"S": str(event.get("environment", ""))},
|
|
"stack": {"S": str(event.get("stack", ""))},
|
|
"score": {"N": str(event.get("score", 0))},
|
|
"band": {"S": str(event.get("band", ""))},
|
|
"expire_at": {"N": str(int((datetime.datetime.now(datetime.timezone.utc) +
|
|
datetime.timedelta(days=365)).timestamp()))},
|
|
}
|
|
|
|
session = boto3.Session(region_name=region)
|
|
dyn = session.client("dynamodb")
|
|
dyn.put_item(TableName=outbox_table, Item=item)
|
|
return item
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) != 2:
|
|
print("usage: outbox_writer.py <event.json>", file=sys.stderr)
|
|
sys.exit(2)
|
|
with open(sys.argv[1], "r") as fh:
|
|
event = json.load(fh)
|
|
item = write_event(event)
|
|
print(json.dumps({k: list(v.values())[0] for k, v in item.items()}, indent=2)) |