ship: phase-03 l2-modules-and-core-scripts (v1.0.3)
Squash merge of phase/03-l2-modules-and-core-scripts; 4 L2s + 5 core scripts; verify_phase03.sh green.
This commit was merged in pull request #3.
This commit is contained in:
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""confidence_signal.py — REQ-08 / D-024
|
||||
|
||||
Reads a contract.yaml, invokes policy_checker.py as a subprocess, and emits
|
||||
a deterministic JSON confidence score.
|
||||
|
||||
policy pass -> {"score": 0.90, "reason": "POLICY_PASS"}
|
||||
policy fail -> {"score": 0.40, "reason": "<violation code>"}
|
||||
|
||||
Exit 0 ALWAYS (per D-024): the pipeline decides the gate, not this script's
|
||||
exit code.
|
||||
|
||||
Input: argv[1] = path to a contract.yaml file.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: confidence_signal.py <contract.yaml>", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
contract_path = sys.argv[1]
|
||||
|
||||
# Resolve policy_checker.py relative to this script so it works regardless
|
||||
# of cwd. Use python3 + script path (not ./) per the contract.
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
policy_checker = os.path.join(here, "policy_checker.py")
|
||||
|
||||
proc = subprocess.run(
|
||||
["python3", policy_checker, contract_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if proc.returncode == 0:
|
||||
score = "0.90"
|
||||
# POLICY_PASS is the expected stdout; strip any trailing whitespace.
|
||||
reason = proc.stdout.strip() or "POLICY_PASS"
|
||||
else:
|
||||
score = "0.40"
|
||||
# The violation code (e.g. "POLICY_VIOLATION:PUBLIC_INGRESS") is on stdout.
|
||||
reason = proc.stdout.strip() or "POLICY_VIOLATION:UNKNOWN"
|
||||
|
||||
# Emit with literal score (two-decimal form per the contract) and a quoted
|
||||
# reason. Constructed manually so json.dumps does not collapse 0.90 -> 0.9.
|
||||
print('{"score": ' + score + ', "reason": ' + json.dumps(reason) + '}')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""evidence_writer.py — REQ-11 / D-023 / D-005
|
||||
|
||||
Appends a hash-chained event to audit.json.
|
||||
|
||||
Each event: {"seq": N, "ts": <iso8601 UTC>, "stage": "...", "event": "...",
|
||||
"prev_hash": "<sha256 or GENESIS>", "hash": "<sha256 of canonical json of this event with hash empty>"}
|
||||
|
||||
Hash chain (D-023):
|
||||
1. Build event dict with hash = "" (empty string).
|
||||
2. canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
|
||||
3. hash = sha256(canonical.encode("utf-8")).hexdigest()
|
||||
4. event["hash"] = hash
|
||||
5. append to audit.json
|
||||
|
||||
Auto-genesis: if audit.json is empty/missing and --stage is not "genesis",
|
||||
a genesis event (seq 0, prev_hash "GENESIS") is inserted first.
|
||||
|
||||
Input:
|
||||
--stage <dev|qa|prod|finalize|genesis> (required)
|
||||
--event "<text>" (required)
|
||||
--audit <path> (optional, default ./audit.json)
|
||||
Output: stdout {"seq": N, "hash": "..."}
|
||||
Exit: 0 on success, 1 on I/O error.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
GENESIS_EVENT_TEXT = "audit log initialized"
|
||||
|
||||
|
||||
def now_iso8601_utc() -> str:
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def compute_hash(event: dict) -> str:
|
||||
"""Compute the sha256 hash of an event using canonical JSON (D-023)."""
|
||||
tmp = dict(event)
|
||||
tmp["hash"] = ""
|
||||
canonical = json.dumps(tmp, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def make_event(seq: int, stage: str, event_text: str, prev_hash: str) -> dict:
|
||||
event = {
|
||||
"seq": seq,
|
||||
"ts": now_iso8601_utc(),
|
||||
"stage": stage,
|
||||
"event": event_text,
|
||||
"prev_hash": prev_hash,
|
||||
"hash": "",
|
||||
}
|
||||
event["hash"] = compute_hash(event)
|
||||
return event
|
||||
|
||||
|
||||
def load_audit(audit_path: str) -> list:
|
||||
if not os.path.exists(audit_path):
|
||||
return []
|
||||
try:
|
||||
with open(audit_path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return []
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
return data
|
||||
|
||||
|
||||
def atomic_write(audit_path: str, data: list) -> None:
|
||||
tmp_path = audit_path + ".tmp"
|
||||
with open(tmp_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(data, fh, indent=2)
|
||||
fh.write("\n")
|
||||
os.replace(tmp_path, audit_path)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Append a hash-chained event to audit.json")
|
||||
parser.add_argument("--stage", required=True,
|
||||
choices=["dev", "qa", "prod", "finalize", "genesis"])
|
||||
parser.add_argument("--event", required=True)
|
||||
parser.add_argument("--audit", default="./audit.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
events = load_audit(args.audit)
|
||||
|
||||
# Auto-genesis: if the log is empty and the caller did not ask for a
|
||||
# genesis event, seed one first.
|
||||
if len(events) == 0 and args.stage != "genesis":
|
||||
genesis = make_event(seq=0, stage="genesis", event_text=GENESIS_EVENT_TEXT,
|
||||
prev_hash="GENESIS")
|
||||
events.append(genesis)
|
||||
|
||||
# Determine the new seq + prev_hash.
|
||||
if events:
|
||||
last = events[-1]
|
||||
seq = last["seq"] + 1
|
||||
prev_hash = last["hash"]
|
||||
else:
|
||||
seq = 0
|
||||
prev_hash = "GENESIS"
|
||||
|
||||
new_event = make_event(seq=seq, stage=args.stage, event_text=args.event,
|
||||
prev_hash=prev_hash)
|
||||
events.append(new_event)
|
||||
|
||||
try:
|
||||
atomic_write(args.audit, events)
|
||||
except OSError as exc:
|
||||
print(f"evidence_writer: I/O error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(json.dumps({"seq": new_event["seq"], "hash": new_event["hash"]}))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""l3b_agent_stub.py — D-008 / D-026 / D-021
|
||||
|
||||
Parses a GitHub/Gitea Issue body by keywords and emits a contract.yaml that
|
||||
selects an L2 stack. This is the agentic (L3B) entry surface: deterministic
|
||||
keyword matching, no external AI APIs.
|
||||
|
||||
D-008 keyword map (priority order — first match wins):
|
||||
gas, price, ingest, data-lake -> l2-commodity-price-feed
|
||||
invoice, billing -> l2-invoice-service
|
||||
analytics, historical, query -> l2-energy-analytics-api
|
||||
regulatory, compliance, reporting, trading
|
||||
-> l2-regulatory-reporting
|
||||
(no match) -> l2-invoice-service (fallback)
|
||||
|
||||
Output contract.yaml (D-021 schema):
|
||||
stack: <mapped L2 name>
|
||||
inputs:
|
||||
environment: dev
|
||||
owner: citizen-developer
|
||||
source: l3b-agent-stub
|
||||
public-ingress: false
|
||||
|
||||
Input:
|
||||
argv[1] = issue body text (or stdin if argv[1] absent/empty)
|
||||
-o <path> = write the contract to a file (default: stdout)
|
||||
Exit:
|
||||
0 on success, 1 on empty input
|
||||
"""
|
||||
import sys
|
||||
|
||||
|
||||
# Ordered keyword groups -> L2 stack mapping (D-008). First match wins.
|
||||
KEYWORD_MAP = [
|
||||
(("gas", "price", "ingest", "data-lake"), "l2-commodity-price-feed"),
|
||||
(("invoice", "billing"), "l2-invoice-service"),
|
||||
(("analytics", "historical", "query"), "l2-energy-analytics-api"),
|
||||
(("regulatory", "compliance", "reporting", "trading"), "l2-regulatory-reporting"),
|
||||
]
|
||||
|
||||
FALLBACK_STACK = "l2-invoice-service"
|
||||
|
||||
|
||||
def map_issue_to_stack(text: str) -> str:
|
||||
lowered = text.lower()
|
||||
for keywords, stack in KEYWORD_MAP:
|
||||
for kw in keywords:
|
||||
if kw in lowered:
|
||||
return stack
|
||||
return FALLBACK_STACK
|
||||
|
||||
|
||||
def render_contract(stack: str) -> str:
|
||||
# Fixed-schema YAML (D-021). Emitted as text (no yaml dependency needed).
|
||||
return (
|
||||
f"stack: {stack}\n"
|
||||
"inputs:\n"
|
||||
" environment: dev\n"
|
||||
" owner: citizen-developer\n"
|
||||
" source: l3b-agent-stub\n"
|
||||
"public-ingress: false\n"
|
||||
)
|
||||
|
||||
|
||||
def read_issue_body(args: list) -> str:
|
||||
"""Read issue body from args[0] (already-stripped argv, no script name)
|
||||
or stdin. Empty -> error."""
|
||||
if len(args) >= 1 and args[0].strip():
|
||||
return args[0]
|
||||
# Fall back to stdin if argv body is absent or empty.
|
||||
if not sys.stdin.isatty():
|
||||
data = sys.stdin.read()
|
||||
if data.strip():
|
||||
return data
|
||||
return ""
|
||||
|
||||
|
||||
def parse_output_flag(argv: list):
|
||||
"""Extract -o <path> from argv (returns (rest, output_path))."""
|
||||
output_path = None
|
||||
rest = []
|
||||
i = 1
|
||||
while i < len(argv):
|
||||
arg = argv[i]
|
||||
if arg == "-o":
|
||||
if i + 1 < len(argv):
|
||||
output_path = argv[i + 1]
|
||||
i += 2
|
||||
continue
|
||||
else:
|
||||
print("l3b_agent_stub: -o requires a path argument", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
rest.append(arg)
|
||||
i += 1
|
||||
return rest, output_path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rest, output_path = parse_output_flag(sys.argv)
|
||||
body = read_issue_body(rest)
|
||||
if not body.strip():
|
||||
print("l3b_agent_stub: empty issue body (no argv[1] and no stdin)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
stack = map_issue_to_stack(body)
|
||||
contract = render_contract(stack)
|
||||
|
||||
if output_path:
|
||||
with open(output_path, "w", encoding="utf-8") as fh:
|
||||
fh.write(contract)
|
||||
else:
|
||||
sys.stdout.write(contract)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
# mock_executor.sh — REQ-06 / D-022
|
||||
#
|
||||
# Reads a contract.yaml, resolves the L2 composition, invokes each L1's
|
||||
# mock_apply.sh in order, and writes state.json to the current working
|
||||
# directory.
|
||||
#
|
||||
# Input: argv[1] = path to a contract.yaml file.
|
||||
# Output:
|
||||
# - stdout: per-L1 progress (echoed from each mock_apply.sh)
|
||||
# - state.json in cwd: {"l2": "...", "l1s": [...], "contract": {...}}
|
||||
# Exit:
|
||||
# 0 if all L1s exit 0; 1 if any L1 exited non-zero (state.json is still
|
||||
# written with the recorded exit codes).
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "usage: mock_executor.sh <contract.yaml>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONTRACT_PATH="$1"
|
||||
|
||||
if [[ ! -f "$CONTRACT_PATH" ]]; then
|
||||
echo "contract not found: $CONTRACT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Parse the contract (stack + full contract dict) via python3 + yaml. ---
|
||||
# Emit stack on line 1 and the full contract JSON on line 2, then read both
|
||||
# lines into separate bash variables (so the JSON's internal spaces survive).
|
||||
CONTRACT_PARSED=$(python3 - "$CONTRACT_PATH" <<'PY'
|
||||
import sys, json, yaml
|
||||
path = sys.argv[1]
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
contract = yaml.safe_load(fh)
|
||||
if not isinstance(contract, dict):
|
||||
sys.stderr.write("contract is not a mapping\n")
|
||||
sys.exit(2)
|
||||
stack = contract.get("stack", "")
|
||||
# Use a compact JSON (no spaces) so the single-line contract survives bash
|
||||
# variable capture cleanly.
|
||||
print(stack)
|
||||
print(json.dumps(contract, sort_keys=True, separators=(",", ":")))
|
||||
PY
|
||||
)
|
||||
|
||||
STACK=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '1p')
|
||||
CONTRACT_JSON=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '2p')
|
||||
|
||||
if [[ -z "$STACK" ]]; then
|
||||
echo "contract missing 'stack' key" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Resolve the L2 manifest. ---
|
||||
L2_MANIFEST="modules/l2/${STACK}/manifest.yaml"
|
||||
if [[ ! -f "$L2_MANIFEST" ]]; then
|
||||
echo "L2_NOT_FOUND: ${STACK}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Read the L2's l1s: list (ordered names) via python. ---
|
||||
L1_NAMES_JSON=$(python3 - "$L2_MANIFEST" <<'PY'
|
||||
import sys, json, yaml
|
||||
path = sys.argv[1]
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
manifest = yaml.safe_load(fh)
|
||||
l1s = manifest.get("l1s", []) if isinstance(manifest, dict) else []
|
||||
names = [entry.get("name", "") for entry in l1s if isinstance(entry, dict)]
|
||||
print(json.dumps(names))
|
||||
PY
|
||||
)
|
||||
|
||||
# --- Invoke each L1's mock_apply.sh in order, recording exit codes. ---
|
||||
# Build the l1s results array in JSON via python, appending as we go.
|
||||
RESULTS_JSON="[]"
|
||||
|
||||
ALL_OK=0
|
||||
while IFS= read -r L1_NAME; do
|
||||
L1_SCRIPT="modules/l1/${L1_NAME}/mock_apply.sh"
|
||||
if [[ ! -f "$L1_SCRIPT" ]]; then
|
||||
echo "L1_NOT_FOUND: ${L1_NAME}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Capture stdout + exit code. stderr passes through.
|
||||
L1_OUT=$(bash "$L1_SCRIPT")
|
||||
L1_RC=$?
|
||||
|
||||
# Echo the L1's stdout so the pipeline sees the progress lines.
|
||||
printf '%s\n' "$L1_OUT"
|
||||
|
||||
# Record {"name": ..., "applied": true, "exit_code": ...}.
|
||||
RESULTS_JSON=$(python3 - "$RESULTS_JSON" "$L1_NAME" "$L1_RC" <<'PY'
|
||||
import sys, json
|
||||
results = json.loads(sys.argv[1])
|
||||
name = sys.argv[2]
|
||||
rc = int(sys.argv[3])
|
||||
results.append({"name": name, "applied": True, "exit_code": rc})
|
||||
print(json.dumps(results))
|
||||
PY
|
||||
)
|
||||
|
||||
if [[ $L1_RC -ne 0 ]]; then
|
||||
ALL_OK=1
|
||||
fi
|
||||
done < <(python3 -c "import sys, json; print('\n'.join(json.loads(sys.argv[1])))" "$L1_NAMES_JSON")
|
||||
|
||||
# --- Write state.json to the current working directory (D-022). ---
|
||||
python3 - "$RESULTS_JSON" "$STACK" "$CONTRACT_JSON" <<'PY'
|
||||
import sys, json
|
||||
results = json.loads(sys.argv[1])
|
||||
stack = sys.argv[2]
|
||||
contract = json.loads(sys.argv[3])
|
||||
state = {
|
||||
"l2": stack,
|
||||
"l1s": results,
|
||||
"contract": contract,
|
||||
}
|
||||
with open("state.json", "w", encoding="utf-8") as fh:
|
||||
json.dump(state, fh, indent=2)
|
||||
fh.write("\n")
|
||||
PY
|
||||
|
||||
exit "$ALL_OK"
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""policy_checker.py — REQ-07 / D-025
|
||||
|
||||
Reads a contract.yaml and enforces the single Phase-03 policy rule:
|
||||
`public-ingress: true` is forbidden.
|
||||
|
||||
Input: argv[1] = path to a contract.yaml file.
|
||||
Output: stdout "POLICY_PASS" or "POLICY_VIOLATION:PUBLIC_INGRESS"
|
||||
Exit: 0 on pass, 1 on violation.
|
||||
|
||||
Idempotent, no side effects (no file writes). Treats an absent or falsy
|
||||
`public-ingress` key as a pass.
|
||||
"""
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: policy_checker.py <contract.yaml>", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
contract_path = sys.argv[1]
|
||||
|
||||
try:
|
||||
with open(contract_path, "r", encoding="utf-8") as fh:
|
||||
contract = yaml.safe_load(fh)
|
||||
except FileNotFoundError:
|
||||
print(f"contract not found: {contract_path}", file=sys.stderr)
|
||||
return 2
|
||||
except yaml.YAMLError as exc:
|
||||
print(f"invalid yaml: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
# Treat missing/non-mapping as no policy violation.
|
||||
if not isinstance(contract, dict):
|
||||
print("POLICY_PASS")
|
||||
return 0
|
||||
|
||||
public_ingress = contract.get("public-ingress", False)
|
||||
|
||||
if public_ingress is True:
|
||||
print("POLICY_VIOLATION:PUBLIC_INGRESS")
|
||||
return 1
|
||||
|
||||
print("POLICY_PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+240
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 03 verification script.
|
||||
# Confirms the 4 L2 modules and the 5 core scripts conform to their contracts.
|
||||
#
|
||||
# Usage: scripts/verify_phase03.sh
|
||||
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
fail_count=0
|
||||
pass() { printf ' [PASS] %s\n' "$1"; }
|
||||
fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); }
|
||||
|
||||
# Expected L2 names per REQ-04.
|
||||
EXPECTED_L2S=(
|
||||
l2-invoice-service
|
||||
l2-commodity-price-feed
|
||||
l2-energy-analytics-api
|
||||
l2-regulatory-reporting
|
||||
)
|
||||
|
||||
echo "== Phase 03 verification =="
|
||||
echo "Root: ${ROOT}"
|
||||
echo
|
||||
|
||||
# --- Check 1: exactly 4 L2 folders with the expected names ---
|
||||
echo "-- Check 1: 4 L2 folders with expected names --"
|
||||
actual=$(ls modules/l2/ 2>/dev/null | sort | tr '\n' ' ')
|
||||
expected=$(printf '%s\n' "${EXPECTED_L2S[@]}" | sort | tr '\n' ' ')
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
pass "exactly 4 L2 folders present and named correctly"
|
||||
else
|
||||
fail "L2 folder list mismatch"
|
||||
echo " expected: $expected"
|
||||
echo " actual: $actual"
|
||||
fi
|
||||
|
||||
# --- Check 2: each L2 manifest.yaml validates + references 5 existing L1s ---
|
||||
echo "-- Check 2: L2 manifests reference 5 existing L1s --"
|
||||
l2_validate=$(python3 << 'PYEOF' || true
|
||||
import yaml, glob, os, sys
|
||||
ok = True
|
||||
l1s = set(os.listdir('modules/l1'))
|
||||
for f in sorted(glob.glob('modules/l2/*/manifest.yaml')):
|
||||
d = yaml.safe_load(open(f))
|
||||
folder = os.path.basename(os.path.dirname(f))
|
||||
problems = []
|
||||
if d.get('name') != folder: problems.append(f"name != {folder}")
|
||||
if d.get('kind') != 'l2': problems.append("kind != l2")
|
||||
refs = [x.get('name') for x in d.get('l1s', [])]
|
||||
if len(refs) != 5: problems.append(f"expected 5 l1s, got {len(refs)}")
|
||||
unknown = [r for r in refs if r not in l1s]
|
||||
if unknown: problems.append(f"unknown L1 refs: {unknown}")
|
||||
# each l1 entry must have an inputs: map
|
||||
for x in d.get('l1s', []):
|
||||
if not isinstance(x.get('inputs'), dict): problems.append(f"l1 {x.get('name')} missing inputs map")
|
||||
status = 'OK' if not problems else 'FAIL: ' + '; '.join(problems)
|
||||
print(f' [{status}] {f}')
|
||||
if problems: ok = False
|
||||
sys.exit(0 if ok else 1)
|
||||
PYEOF
|
||||
)
|
||||
echo "$l2_validate"
|
||||
if [ "$l2_validate" = "" ] || echo "$l2_validate" | grep -q FAIL; then
|
||||
if ! echo "$l2_validate" | grep -q PASS; then
|
||||
fail "one or more L2 manifests invalid (see above)"
|
||||
fi
|
||||
else
|
||||
pass "all 4 L2 manifests valid"
|
||||
fi
|
||||
# Re-run for the explicit pass/fail count
|
||||
python3 << 'PYEOF' > /tmp/l2_check.txt 2>&1 || true
|
||||
import yaml, glob, os, sys
|
||||
ok = True
|
||||
l1s = set(os.listdir('modules/l1'))
|
||||
for f in sorted(glob.glob('modules/l2/*/manifest.yaml')):
|
||||
d = yaml.safe_load(open(f))
|
||||
folder = os.path.basename(os.path.dirname(f))
|
||||
if d.get('name') != folder: ok = False
|
||||
if d.get('kind') != 'l2': ok = False
|
||||
refs = [x.get('name') for x in d.get('l1s', [])]
|
||||
if len(refs) != 5: ok = False
|
||||
if any(r not in l1s for r in refs): ok = False
|
||||
for x in d.get('l1s', []):
|
||||
if not isinstance(x.get('inputs'), dict): ok = False
|
||||
sys.exit(0 if ok else 1)
|
||||
PYEOF
|
||||
if [ $? -eq 0 ]; then pass "all 4 L2 manifests pass structural + reference checks"; else fail "L2 manifest structural check"; fi
|
||||
|
||||
# --- Check 3: typecheck (bash -n + py_compile + yaml load) ---
|
||||
echo "-- Check 3: typecheck --"
|
||||
if bash -n scripts/mock_executor.sh; then pass "bash -n mock_executor.sh"; else fail "bash -n mock_executor.sh"; fi
|
||||
if python3 -m py_compile scripts/policy_checker.py scripts/confidence_signal.py scripts/evidence_writer.py scripts/l3b_agent_stub.py 2>/dev/null; then
|
||||
pass "py_compile all 4 python scripts"
|
||||
else
|
||||
fail "py_compile"
|
||||
fi
|
||||
if python3 -c "import yaml, glob; [yaml.safe_load(open(f)) for f in glob.glob('modules/l2/*/manifest.yaml')]" 2>/dev/null; then
|
||||
pass "yaml load all L2 manifests"
|
||||
else
|
||||
fail "yaml load L2 manifests"
|
||||
fi
|
||||
|
||||
# --- Check 4: policy_checker (D-025) ---
|
||||
echo "-- Check 4: policy_checker behavior (D-025) --"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK" "$ROOT/tmp_pass_contract.yaml" "$ROOT/tmp_fail_contract.yaml" "$ROOT/state.json" 2>/dev/null || true' EXIT
|
||||
printf 'stack: l2-commodity-price-feed\npublic-ingress: false\n' > "$WORK/pass.yaml"
|
||||
printf 'stack: l2-regulatory-reporting\npublic-ingress: true\n' > "$WORK/fail.yaml"
|
||||
out=$(python3 scripts/policy_checker.py "$WORK/pass.yaml" 2>&1); rc=$?
|
||||
if [ "$out" = "POLICY_PASS" ] && [ "$rc" = "0" ]; then
|
||||
pass "policy_checker pass contract -> POLICY_PASS exit 0"
|
||||
else
|
||||
fail "policy_checker pass contract: got '$out' exit=$rc"
|
||||
fi
|
||||
out=$(python3 scripts/policy_checker.py "$WORK/fail.yaml" 2>&1); rc=$?
|
||||
if [ "$out" = "POLICY_VIOLATION:PUBLIC_INGRESS" ] && [ "$rc" = "1" ]; then
|
||||
pass "policy_checker fail contract -> POLICY_VIOLATION:PUBLIC_INGRESS exit 1"
|
||||
else
|
||||
fail "policy_checker fail contract: got '$out' exit=$rc"
|
||||
fi
|
||||
|
||||
# --- Check 5: confidence_signal (D-024) ---
|
||||
echo "-- Check 5: confidence_signal behavior (D-024) --"
|
||||
out=$(python3 scripts/confidence_signal.py "$WORK/pass.yaml" 2>&1); rc=$?
|
||||
if echo "$out" | grep -q '"score": 0.90' && [ "$rc" = "0" ]; then
|
||||
pass "confidence_signal pass -> score 0.90 exit 0"
|
||||
else
|
||||
fail "confidence_signal pass: got '$out' exit=$rc"
|
||||
fi
|
||||
out=$(python3 scripts/confidence_signal.py "$WORK/fail.yaml" 2>&1); rc=$?
|
||||
if echo "$out" | grep -q '"score": 0.40' && [ "$rc" = "0" ]; then
|
||||
pass "confidence_signal fail -> score 0.40 exit 0"
|
||||
else
|
||||
fail "confidence_signal fail: got '$out' exit=$rc"
|
||||
fi
|
||||
|
||||
# --- Check 6: evidence_writer hash chain (D-023) ---
|
||||
echo "-- Check 6: evidence_writer hash chain (D-023) --"
|
||||
rm -f "$WORK/audit.json"
|
||||
python3 scripts/evidence_writer.py --stage dev --event "dev start" --audit "$WORK/audit.json" > /dev/null
|
||||
python3 scripts/evidence_writer.py --stage qa --event "qa approved" --audit "$WORK/audit.json" > /dev/null
|
||||
python3 scripts/evidence_writer.py --stage prod --event "prod approved" --audit "$WORK/audit.json" > /dev/null
|
||||
chain_ok=$(python3 << PYEOF
|
||||
import json, hashlib, sys
|
||||
try:
|
||||
events = json.load(open("$WORK/audit.json"))
|
||||
assert len(events) == 4, f"expected 4 (genesis + 3), got {len(events)}"
|
||||
assert events[0]['prev_hash'] == 'GENESIS', "genesis prev_hash"
|
||||
for i in range(1, len(events)):
|
||||
assert events[i]['prev_hash'] == events[i-1]['hash'], f"chain break at {i}"
|
||||
e = dict(events[i]); h = e.pop('hash'); e['hash'] = ''
|
||||
canon = json.dumps(e, sort_keys=True, separators=(',',':'))
|
||||
assert hashlib.sha256(canon.encode()).hexdigest() == h, f"hash mismatch at {i}"
|
||||
print("OK")
|
||||
except AssertionError as ex:
|
||||
print(f"FAIL: {ex}")
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
)
|
||||
if [ "$chain_ok" = "OK" ]; then
|
||||
pass "evidence_writer: 4 events, GENESIS + 3, chain links + hashes valid"
|
||||
else
|
||||
fail "evidence_writer chain: $chain_ok"
|
||||
fi
|
||||
|
||||
# --- Check 7: mock_executor (D-022) ---
|
||||
echo "-- Check 7: mock_executor writes state.json (D-022) --"
|
||||
rm -f "$ROOT/state.json"
|
||||
out=$(bash scripts/mock_executor.sh "$WORK/pass.yaml" 2>&1); rc=$?
|
||||
if [ "$rc" != "0" ]; then
|
||||
fail "mock_executor exit $rc (expected 0)"
|
||||
else
|
||||
me_ok=$(python3 << PYEOF
|
||||
import json, sys
|
||||
try:
|
||||
s = json.load(open("$ROOT/state.json"))
|
||||
assert s['l2'] == 'l2-commodity-price-feed', f"l2 mismatch: {s.get('l2')}"
|
||||
assert 'l1s' in s and len(s['l1s']) == 5, f"expected 5 l1s, got {len(s.get('l1s', []))}"
|
||||
assert all(x['applied'] is True and x['exit_code'] == 0 for x in s['l1s']), "l1 not all applied+0"
|
||||
assert 'contract' in s, "missing contract field"
|
||||
print("OK")
|
||||
except Exception as ex:
|
||||
print(f"FAIL: {ex}")
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
)
|
||||
if [ "$me_ok" = "OK" ]; then
|
||||
pass "mock_executor: state.json with l2 + 5 l1s (all exit 0) + contract"
|
||||
else
|
||||
fail "mock_executor state.json: $me_ok"
|
||||
fi
|
||||
fi
|
||||
rm -f "$ROOT/state.json"
|
||||
|
||||
# --- Check 8: l3b_agent_stub D-008 keyword map ---
|
||||
echo "-- Check 8: l3b_agent_stub keyword map (D-008) --"
|
||||
act3=$(python3 scripts/l3b_agent_stub.py "We need to ingest natural gas prices from Platts and report on compliance." 2>&1)
|
||||
if echo "$act3" | grep -q 'stack: l2-commodity-price-feed'; then
|
||||
pass "l3b Act 3 example -> l2-commodity-price-feed"
|
||||
else
|
||||
fail "l3b Act 3 example: got '$act3'"
|
||||
fi
|
||||
fallback=$(python3 scripts/l3b_agent_stub.py "please deploy something" 2>&1)
|
||||
if echo "$fallback" | grep -q 'stack: l2-invoice-service'; then
|
||||
pass "l3b fallback (no keywords) -> l2-invoice-service"
|
||||
else
|
||||
fail "l3b fallback: got '$fallback'"
|
||||
fi
|
||||
regulatory=$(python3 scripts/l3b_agent_stub.py "regulatory compliance reporting for trading desk" 2>&1)
|
||||
if echo "$regulatory" | grep -q 'stack: l2-regulatory-reporting'; then
|
||||
pass "l3b regulatory keywords -> l2-regulatory-reporting"
|
||||
else
|
||||
fail "l3b regulatory: got '$regulatory'"
|
||||
fi
|
||||
invoice=$(python3 scripts/l3b_agent_stub.py "monthly invoice and billing reconciliation" 2>&1)
|
||||
if echo "$invoice" | grep -q 'stack: l2-invoice-service'; then
|
||||
pass "l3b invoice keywords -> l2-invoice-service"
|
||||
else
|
||||
fail "l3b invoice: got '$invoice'"
|
||||
fi
|
||||
analytics=$(python3 scripts/l3b_agent_stub.py "historical analytics and query API" 2>&1)
|
||||
if echo "$analytics" | grep -q 'stack: l2-energy-analytics-api'; then
|
||||
pass "l3b analytics keywords -> l2-energy-analytics-api"
|
||||
else
|
||||
fail "l3b analytics: got '$analytics'"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== Summary =="
|
||||
if [ "$fail_count" -eq 0 ]; then
|
||||
echo "Phase 03 verification PASSED (4 L2s + 5 core scripts, all checks ok)"
|
||||
exit 0
|
||||
else
|
||||
echo "Phase 03 verification FAILED (${fail_count} check(s) failed)"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user