From 7310da224ee572d300ea944359a8898ba36a9590 Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Tue, 21 Jul 2026 13:20:13 +0000 Subject: [PATCH] =?UTF-8?q?docs(P03):=20research=20findings=20=E2=80=94=20?= =?UTF-8?q?L2/contract/state/audit/core-script=20schemas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ---ci--- phase: 3 milestone: v1.0 status: research research: l2_schema: l1s list of {name, inputs: map}; 4 L2s each referencing 5 L1s contract_schema: stack + inputs + optional public-ingress: bool state_json_shape: l2, l1s array (name+applied+exit_code), contract audit_json: JSON array; canonical-JSON SHA-256 hash chain; GENESIS prev_hash core_script_io: 5 scripts with explicit input/output/exit contracts personas: no change; backend-engineer owns core scripts + L2 manifests; infra-stub-engineer owns L1 manifests only ---/ci--- ARCHITECTURE.md gains the L2 manifest schema, the L2 list with per-L2 L1 references (5 each, within max-depth-5), the contract.yaml schema, the state.json shape, the audit.json event + canonical-JSON hash chain, and a core-script I/O contract table. PERSONAS.md is unchanged for Phase 03 (backend-engineer and infra-stub-engineer were both already active in the project-level roster). --- .ciagent/ARCHITECTURE.md | 110 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/.ciagent/ARCHITECTURE.md b/.ciagent/ARCHITECTURE.md index c475734..d6696f4 100644 --- a/.ciagent/ARCHITECTURE.md +++ b/.ciagent/ARCHITECTURE.md @@ -143,4 +143,112 @@ Each L1 module lives at `modules/l1//` with exactly two files: | `l1-cloudwatch` | Observability primitive | L1 modules are single-purpose, substrate-agnostic, max-depth-1 (per -PROJECT.md Constraints). They do not compose with other L1s. \ No newline at end of file +PROJECT.md Constraints). They do not compose with other L1s. + +## L2 module schema + core scripts (Phase 03 research) + +### L2 manifest.yaml schema (D-020) + +```yaml +name: l2-commodity-price-feed # matches the folder name +kind: l2 # literal "l2" +description: +l1s: # ordered list of L1 references + - name: l1-eks-fargate # MUST match an existing L1 folder name + inputs: + cluster_name: price-feed-cluster + region: us-east-1 + cpu_arch: arm64 + - name: l1-lambda + inputs: + function_name: price-ingest + runtime: python3.11 + handler: index.handler + # ... up to 5 L1 references per L2 (max-depth-5 per REQ-05; L2->L1 is depth 1) +``` + +L2s reference L1s **by name only** (no path); `mock_executor.sh` resolves +the name to `modules/l1//`. + +### L2 list (fixed per REQ-04) + +| Folder | Description | L1s (per S&P Global Energy / Platts use cases) | +|--------|-------------|------------------------------------------------| +| `l2-invoice-service` | Billing + invoicing microservice | `l1-eks-fargate`, `l1-iam-role`, `l1-lambda`, `l1-sqs`, `l1-s3` | +| `l2-commodity-price-feed` | Real-time price ingestion | `l1-eks-fargate`, `l1-lambda`, `l1-api-gateway`, `l1-eventbridge`, `l1-s3` | +| `l2-energy-analytics-api` | Historical query API | `l1-eks-fargate`, `l1-api-gateway`, `l1-lambda`, `l1-s3`, `l1-cloudwatch` | +| `l2-regulatory-reporting` | Compliance + reporting | `l1-eks-fargate`, `l1-iam-role`, `l1-lambda`, `l1-sqs`, `l1-s3` | + +Each L2 references exactly 5 L1s (within the max-depth-5 constraint; L2→L1 +is depth 1, so depth-5 is generous but the spec caps composition depth at +5 — the count is 5 to demonstrate a realistic composed stack). + +### contract.yaml schema (D-021) + +```yaml +stack: l2-commodity-price-feed # MUST match an existing L2 folder name +inputs: # top-level params for the L2 (optional) + environment: dev + owner: platform-team +public-ingress: false # bool; true triggers POLICY_VIOLATION:PUBLIC_INGRESS +``` + +The `public-ingress` key is the only policy-enforced field in Phase 03. +Phase 04's pipeline reads `contract.yaml`, runs `policy_checker.py`, then +`mock_executor.sh` to apply the L2. + +### state.json shape (D-022) + +`mock_executor.sh` writes `state.json` to its working directory: + +```json +{ + "l2": "l2-commodity-price-feed", + "l1s": [ + {"name": "l1-eks-fargate", "applied": true, "exit_code": 0}, + {"name": "l1-lambda", "applied": true, "exit_code": 0}, + ... + ], + "contract": { + "stack": "l2-commodity-price-feed", + "inputs": {...}, + "public-ingress": false + } +} +``` + +### audit.json event + hash chain (D-023) + +`audit.json` is a JSON array of event objects. `evidence_writer.py` +appends one event per call. Hash chain: + +1. Construct the event dict with `hash` set to empty string. +2. Serialize via `json.dumps(event, sort_keys=True, separators=(",", ":"))` — canonical JSON (deterministic key order, no whitespace). +3. Compute `hash = sha256(canonical_json.encode("utf-8")).hexdigest()`. +4. Set `event["hash"] = hash`. +5. Append to `audit.json`. + +Genesis event (when `audit.json` is empty or missing): + +```json +{ + "seq": 0, + "ts": "2026-07-21T13:00:00Z", + "stage": "genesis", + "event": "audit log initialized", + "prev_hash": "GENESIS", + "hash": "" +} +``` + +Subsequent events: `seq = prev.seq + 1`, `prev_hash = prev.hash`. + +### Core script I/O contracts + +| Script | Input | Output | Exit | +|--------|-------|--------|------| +| `mock_executor.sh` | `` (argv[1]); reads L2 manifest from `modules/l2//manifest.yaml` | writes `state.json` to cwd; prints per-L1 progress | 0 on all-L1s-pass; non-zero on any L1 failure | +| `policy_checker.py` | `` (argv[1]) | stdout: `POLICY_PASS` or `POLICY_VIOLATION:PUBLIC_INGRESS` | 0 on pass; 1 on violation | +| `confidence_signal.py` | `` (argv[1]); calls policy_checker | stdout: `{"score": 0.90|0.40, "reason": "..."}` | 0 always (per D-024; pipeline decides gate) | +| `evidence_writer.py` | argv: `--stage ` `--event ""` `--audit ` (default `./audit.json`) | appends event to audit.json; prints the new event's hash + seq | 0 on success; 1 on I/O error | +| `l3b_agent_stub.py` | argv[1] = issue body text (or stdin if no argv); optional `-o ` (default stdout) | writes a `contract.yaml` (D-021 schema) with `stack` set by the D-008 keyword map | 0 on success; 1 on empty input | \ No newline at end of file