"""Nova CloudEvents 1.0 envelope + platform.* semantic conventions (REQ-187). Defines the standard event envelope for all Nova metrics events. Every emitter (run_manifest, decision_ledger, confidence_signal, checkov_adapter, hitl_gates, regression_verify) uses `make_event()` to produce a valid CloudEvents 1.0 envelope. Events are appended to `metrics/events.jsonl`. D-120: Nova-native minimal tech (no Kafka/OTel SDK — JSONL + SQLite). D-125: hybrid model — existing file signals stay as files; the collector reads them and emits normalized CloudEvents. New emitters emit directly. """ import datetime import hashlib import json import os import sys import uuid METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics") EVENTS_LOG = os.path.join(METRICS_DIR, "events.jsonl") def _iso8601_now(): return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") def make_event(event_type, run_id, environment, data, contract_id="", source="nova.platform", subject="", actor_type="confidence-gate", actor_id="confidence_signal"): """Build a CloudEvents 1.0 envelope with Nova platform.* conventions. Args: event_type: e.g. "nova.run.completed", "nova.ai.decision.made" run_id: the run identifier (e.g. "run-") environment: dev|qa|prod|dr data: the event payload dict contract_id: the contract UUID (optional) source: the event source (default "nova.platform") subject: the event subject (default "/") actor_type: the actor type (default "confidence-gate") actor_id: the actor id (default "confidence_signal") Returns: A CloudEvents 1.0 envelope dict. """ if not subject: subject = f"{contract_id}/{environment}" if contract_id else environment return { "specversion": "1.0", "id": str(uuid.uuid4()), "source": source, "type": event_type, "time": _iso8601_now(), "subject": subject, "datacontenttype": "application/json", "platform": { "tenant_id": "acdl", "run_id": run_id, "contract_id": contract_id, "environment": environment, "actor": {"type": actor_type, "id": actor_id}, "trace_id": run_id, }, "data": data, } def append_event(event, events_log=None): """Append a CloudEvents envelope to the JSONL event log. Creates the metrics/ directory if it doesn't exist. """ if events_log is None: events_log = EVENTS_LOG os.makedirs(os.path.dirname(events_log), exist_ok=True) with open(events_log, "a", encoding="utf-8") as fh: fh.write(json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n") def emit(event_type, run_id, environment, data, **kwargs): """Make an event + append it to the JSONL log. Convenience wrapper.""" event = make_event(event_type, run_id, environment, data, **kwargs) append_event(event) return event if __name__ == "__main__": if len(sys.argv) < 4: print("usage: event_envelope.py [data.json]", file=sys.stderr) sys.exit(2) _type = sys.argv[1] _run_id = sys.argv[2] _env = sys.argv[3] _data = {} if len(sys.argv) >= 5 and os.path.isfile(sys.argv[4]): with open(sys.argv[4]) as f: _data = json.load(f) ev = emit(_type, _run_id, _env, _data) print(json.dumps(ev, indent=2))