diff --git a/.github/workflows/pr-production-ledger.yml b/.github/workflows/pr-production-ledger.yml index b2603dab5..92b110dac 100644 --- a/.github/workflows/pr-production-ledger.yml +++ b/.github/workflows/pr-production-ledger.yml @@ -103,6 +103,45 @@ jobs: `checks_pending: ${pendingChecks.length}`, `checks_failed: ${failedChecks.length}`, ].join('\n'); + const epsEvent = { + schema_version: 'eps.v1', + event_type: 'pr.ledger_observed', + occurred_at: new Date().toISOString(), + source: 'github.actions.pr-production-ledger', + repo: `${context.repo.owner}/${context.repo.repo}`, + git_sha: pr.head.sha, + run_id: context.runId, + run_attempt: Number(process.env.GITHUB_RUN_ATTEMPT || 1), + entity_type: 'pull_request', + entity_id: `pr:${prNumber}`, + status: 'observed', + provenance: { source_ref: pr.html_url, source_kind: 'github_api', attribution_confidence: 1 }, + attributes: { + base_sha: pr.base.sha, commits: commits.length, changed_files: changedFiles, + additions: pr.additions, deletions: pr.deletions, ahead_of_base: ahead, + behind_base: behind, checks_total: checks.length, + checks_pending: pendingChecks.length, checks_failed: failedChecks.length, + branch_alignment: behind > 0 ? 'BEHIND_MASTER' : 'ALIGNED_OR_AHEAD' + } + }; + const stable = JSON.stringify([ + epsEvent.event_type, epsEvent.source, epsEvent.repo, + epsEvent.git_sha, epsEvent.run_id, epsEvent.run_attempt, + epsEvent.entity_type, epsEvent.entity_id, epsEvent.status + ]); + epsEvent.event_id = `eps-${require('crypto').createHash('sha256').update(stable).digest('hex').slice(0, 32)}`; + require('fs').writeFileSync( + `${process.env.GITHUB_WORKSPACE}/eps-events.ndjson`, + JSON.stringify(epsEvent) + '\n', + { encoding: 'utf8', flag: 'w' } + ); + // Validate the emitted envelope before artifact publication. This job intentionally + // does not checkout PR code, so validation stays inside the trusted ledger step. + if (epsEvent.schema_version !== 'eps.v1' || !epsEvent.event_id || !epsEvent.provenance?.source_ref || !epsEvent.entity_id || !epsEvent.entity_type) { + core.setFailed('EPS envelope validation failed'); + return; + } + await core.summary .addHeading(`PR #${prNumber} production ledger`) .addRaw(ledger + '\n\n') @@ -116,3 +155,13 @@ jobs: .addRaw(failedChecks.length ? `❌ ${failedChecks.length} failed check(s).\n` : '✅ No completed failing checks observed.\n') .addRaw(pendingChecks.length ? `⏳ ${pendingChecks.length} check(s) still pending.\n` : '✅ No pending checks observed.\n') .write(); + + + - name: Upload EPS telemetry artifact + if: ${{ always() && hashFiles('eps-events.ndjson') != '' }} + uses: actions/upload-artifact@v4.6.2 + with: + name: eps-events-${{ github.run_id }}-${{ github.run_attempt }} + path: eps-events.ndjson + if-no-files-found: error + retention-days: 14 diff --git a/docs/ops/EPS-CONCEPT-TEMPLATES.md b/docs/ops/EPS-CONCEPT-TEMPLATES.md new file mode 100644 index 000000000..8bfc9ff19 --- /dev/null +++ b/docs/ops/EPS-CONCEPT-TEMPLATES.md @@ -0,0 +1,55 @@ +# EPS Concept Templates + +These templates are intentionally implementation-neutral. They describe future surfaces without making them authoritative. + +## Concept: Evidence Timeline + +**Inputs:** EPS events +**Interaction:** time window, entity, source, status +**Output:** linked event sequence +**Authority:** source evidence +**Validation:** every node resolves to event_id + source reference + +## Concept: Agent Work Graph + +**Inputs:** agent/task/attempt/provider/manager EPS events +**Interaction:** actor, task, handoff, retry, intervention +**Output:** relationship projection +**Authority:** EPS + source evidence +**Validation:** unknown attribution remains unknown + +## Concept: Repository Activity Projection + +**Inputs:** Git/PR/Actions EPS events +**Renderer:** Gource or future equivalent +**Interaction:** temporal playback, path/actor filters +**Authority:** EPS/source evidence +**Validation:** rendered event count reconciles to projection input + +## Concept: Operations Observatory + +**Inputs:** validated EPS + derived metrics +**Renderer:** Grafana / SHE / Vercel +**Interaction:** time-series, drill-down, alert → evidence +**Authority:** EPS/source evidence +**Validation:** freshness + provenance + completeness gates + +## Concept: Research Observatory + +**Inputs:** repository observations, starred/forked/added seeds, provider/model experiments +**Renderer:** Hex / future interactive surface +**Interaction:** cohort, similarity, disposition, experiment comparison +**Authority:** source evidence + frozen snapshots +**Validation:** reproducible snapshot and schema version + +## Concept: ML Experiment Loop + +**Inputs:** frozen EPS snapshot +**Process:** feature extraction → model → evaluation → derived evidence +**Output:** model/version/result record +**Authority:** source EPS snapshot +**Validation:** deterministic fixture + uncertainty + source IDs + +## Design rule + +A custom interactive surface is a **projection of EPS**, never an alternative telemetry authority. diff --git a/docs/ops/EPS-DASHBOARD-CONTRACT.md b/docs/ops/EPS-DASHBOARD-CONTRACT.md new file mode 100644 index 000000000..b1db1a0cc --- /dev/null +++ b/docs/ops/EPS-DASHBOARD-CONTRACT.md @@ -0,0 +1,26 @@ +# EPS Dashboard Contract + +The dashboard is a consumer of EPS, never its producer. + +## Required primitives + +- freshness and last observed event +- ingestion and validation rate +- volume by source/type/status +- provenance drill-down +- run and attempt timeline +- attribution-confidence distribution +- stale/partial/unverified evidence +- replay/duplicate rejection +- ML/Hex/Grafana derived links +- projection health including Gource/render adapters + +Every panel declares: metric, EPS event types, reducer, freshness, provenance drill-down, and failure semantics. + +Missing evidence must never silently become zero. + +Gource, SHE/Vercel and future WebGL/WASM interaction consume the same EPS projections. A visual interaction may link to event IDs/source refs but cannot author telemetry. + +## Acceptance + +A dashboard feature is accepted only when its metric has an EPS source/reducer, missing/stale/partial evidence is visible, source evidence is reachable, replay does not inflate it, and a non-visual evidence path reproduces the finding. diff --git a/docs/ops/EPS-EVENT.schema.json b/docs/ops/EPS-EVENT.schema.json new file mode 100644 index 000000000..47a331741 --- /dev/null +++ b/docs/ops/EPS-EVENT.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/timerloggedout-spec/termux-monorepo/blob/master/docs/ops/EPS-EVENT.schema.json", + "title": "Evidence Projection Surface event", + "type": "object", + "additionalProperties": false, + "required": ["schema_version","event_id","event_type","occurred_at","source","repo","entity_type","entity_id","status","provenance","attributes"], + "properties": { + "schema_version": {"const": "eps.v1"}, + "event_id": {"type": "string","minLength": 8,"maxLength": 256}, + "event_type": {"type": "string","pattern": "^[a-z][a-z0-9_.-]{2,127}$"}, + "occurred_at": {"type": "string","format": "date-time"}, + "source": {"type": "string","minLength": 1,"maxLength": 256}, + "repo": {"type": "string","pattern": "^[^/\\s]+/[^/\\s]+$"}, + "git_sha": {"type": ["string","null"],"pattern": "^[0-9a-f]{40}$"}, + "run_id": {"type": ["integer","null"],"minimum": 1}, + "run_attempt": {"type": ["integer","null"],"minimum": 1}, + "entity_type": {"type": "string","minLength": 1,"maxLength": 128}, + "entity_id": {"type": "string","minLength": 1,"maxLength": 512}, + "status": {"type": "string","enum": ["observed","queued","running","completed","failed","cancelled","partial","unverified"]}, + "provenance": { + "type": "object", + "additionalProperties": false, + "required": ["source_ref"], + "properties": { + "source_ref": {"type": "string","minLength": 1,"maxLength": 2048}, + "source_kind": {"type": "string","minLength": 1,"maxLength": 128}, + "attribution_confidence": {"type": ["number","null"],"minimum": 0,"maximum": 1} + } + }, + "attributes": { + "type": "object", + "additionalProperties": true, + "propertyNames": {"maxLength": 128} + } + } +} \ No newline at end of file diff --git a/docs/ops/EPS-IMPLEMENTATION-MATRIX.md b/docs/ops/EPS-IMPLEMENTATION-MATRIX.md new file mode 100644 index 000000000..17279b255 --- /dev/null +++ b/docs/ops/EPS-IMPLEMENTATION-MATRIX.md @@ -0,0 +1,70 @@ +# EPS Implementation Matrix + +| Initiative | Current disposition | EPS relationship | Next useful increment | +|---|---|---|---| +| #701 | LANDED | SSOT/session decisions are evidence metadata | ingest lane disposition + source SHA/run refs | +| #682 | EXTRACT / WAIT | ML DAG is a major EPS consumer/producer candidate | emit run/attempt/decision events only after dual-gate evidence | +| #432 / #549 / #601 | EXTRACT | ML wholesale family is historical evidence and reusable component source | extract stable schemas/reducers, not branches wholesale | +| #630 | EXTRACT | Jules provenance + dashboard behavior can become agent-event fixtures | capture agent/task/attempt provenance | +| #684 | HOLD / observe | cadence, concurrency, taint boundaries are telemetry dimensions | emit schedule/concurrency/provider-state events | +| #685 / #695 / #543 / #680 / #702 | OBSERVE | proposals, optimization, skill quality, catalog changes, and research seeds are heterogeneous evidence sources | normalize only metadata needed for reproducible evaluation | +| #48 / #69 / #73 | HOLD | connector/routing/debate history supplies provenance and boundary cases | preserve as historical fixtures; do not revive wrong-base code | + +## Gravitee + +#702 is relevant to EPS, but as a **research-seed / integration-observatory producer**, not as a telemetry dependency. + +Its current proposal registers the Gravitee upstream research seed, preserves the local fork as a comparison surface, and explicitly avoids vendoring/submodules. That makes it useful for: + +- API-management capability classification; +- gateway/plugin/policy/MCP capability observations; +- upstream-versus-fork provenance; +- Kubernetes/platform integration evidence; +- future API gateway telemetry experiments. + +The useful extraction is the **observation contract**, not the upstream implementation wholesale. + +### Gravitee + EPS boundary + +`Repository Observatory → EPS research_observation → ML/Hex/Grafana → optional visualization` + +Do not let Gravitee become a new execution plane or require its runtime before the observation contract is proven. + +## Grafana + +Grafana is complementary when EPS is projected into operational time series. Prefer derived metrics such as: + +- event ingestion rate; +- validation failures; +- stale-event age; +- run/attempt completion latency; +- provider-state counts; +- action/effect yield; +- attribution-confidence distributions. + +Grafana should alert on validated telemetry and link back to evidence identifiers. + +## Hex + +The existing `3l0.moneyball.v1` contract already has compatible grains: experiment run, agent task attempt, provider call, outcome score, and manager decision. EPS should provide the provenance envelope around those records rather than create a competing Moneyball schema. + +Hex remains analytical/evidence presentation, with durable retention outside Hex where required. + +## ML + +ML should consume frozen EPS snapshots, never mutate the evidence plane. Candidate work includes: + +- anomaly detection; +- cohort/regression analysis; +- provider/model routing evaluation; +- attribution-confidence calibration; +- graph/community analysis; +- cost/performance frontiers. + +Any learned result is derived evidence and must retain source event IDs, source SHA/ref, model/version, timestamp, and uncertainty. + +## Gource / visualization + +Gource is a secondary renderer. Its compact custom-log format cannot carry EPS provenance, so EPS must remain canonical and a projection adapter should map EPS events into Gource events. + +Future interactive surfaces may reuse the same adapter model without coupling telemetry collection to a particular renderer. diff --git a/docs/ops/EPS-KNOWLEDGE-EVALUATION.md b/docs/ops/EPS-KNOWLEDGE-EVALUATION.md new file mode 100644 index 000000000..bb5dac565 --- /dev/null +++ b/docs/ops/EPS-KNOWLEDGE-EVALUATION.md @@ -0,0 +1,48 @@ +# EPS Knowledge Evaluation + +## Goal + +Measure whether telemetry improves repository knowledge work before investing heavily in custom visualization. + +## Baselines + +1. GitHub/Git-only source inspection. +2. Existing ledger/evidence artifacts. +3. EPS normalized events. +4. EPS + ML/Hex/Grafana derived views. +5. EPS + visualization (secondary experiment). + +## Tasks + +Use reproducible repository questions: + +- What happened to a specific PR and why? +- Which agent/provider/action produced an observed effect? +- Can the event be traced to an exact SHA/run/attempt? +- Where did retries, stalls, conflicts, or provider-state interruptions occur? +- Is a metric complete, partial, stale, or unverified? +- Which observations can be safely joined without multiplying denominators? +- Can an anomaly discovered visually be recovered from source evidence? + +## Measures + +Do not collapse these into a single opaque score. + +- evidence retrieval accuracy; +- provenance reconstruction accuracy; +- time-to-answer; +- missing-evidence detection; +- false attribution rate; +- stale-data detection; +- duplicate/replay resistance; +- context consumed; +- human intervention; +- downstream reducer agreement. + +An optional experimental **Knowledge Yield** can be reported as useful, evidence-backed discoveries per unit of analyst time/context. It is a research metric, not a governance score. + +## Acceptance + +EPS is useful when it measurably reduces evidence-retrieval work without increasing false attribution or hiding incomplete coverage. + +A visualization experiment only advances when the same evidence tasks remain reproducible without the visualization. diff --git a/docs/ops/EVIDENCE-PROJECTION-SURFACE.md b/docs/ops/EVIDENCE-PROJECTION-SURFACE.md new file mode 100644 index 000000000..564796c1c --- /dev/null +++ b/docs/ops/EVIDENCE-PROJECTION-SURFACE.md @@ -0,0 +1,99 @@ +# Evidence Projection Surface (EPS) + +**Status:** telemetry-first implementation contract +**Priority:** P0 telemetry / P2 visualization +**Scope:** repository-wide evidence production, normalization, projection, and downstream analysis + +## Purpose + +EPS is the repository's **telemetry-first projection contract**. It turns independently verifiable repository events into small, immutable, provenance-bound records that can be consumed by analytics, ML, Hex, Grafana, agentic orchestration, and later visualization surfaces. + +EPS is **not** a second source of truth, database, scheduler, provider router, or merge authority. + +The authority chain remains: + +`GitHub / Git / Actions / artifacts → EPS event → validated evidence bundle → reducers / ML / Hex / Grafana / visualization` + +If a projection conflicts with source evidence, regenerate the projection. + +## First implementation gap + +The existing production ledger is a useful read-only evidence producer, but the current implementation writes a human-readable job summary and does not emit the previously planned agent-throughput JSONL receipt. The repository's own audit identified this gap: the throughput schema was a contract rather than a runtime emitter. + +EPS closes that gap incrementally without requiring every agent workflow to be rewritten at once. + +### Phase 1 producer + +`.github/workflows/pr-production-ledger.yml` emits an EPS NDJSON artifact for each applicable PR event. + +### Phase 2 producers + +Eligible agent workflows add agent/task/provider/manager events using the same envelope. The existing Hex Moneyball grain contracts become EPS-compatible consumers rather than a parallel telemetry format. + +## Envelope + +Every EPS record should carry: + +- `schema_version` +- `event_id` — deterministic/idempotent identity +- `event_type` +- `occurred_at` +- `source` +- `repo` +- `git_sha` when known +- `run_id` / `run_attempt` when Actions-backed +- `entity_id` and `entity_type` +- `status` +- `provenance` +- bounded `attributes` + +No prompts, completions, tool payloads, credentials, arbitrary comments, or repository contents belong in EPS. + +## Provenance rule + +An EPS record is useful only if a reviewer can navigate back to source evidence. Prefer: + +`run → job → step → event → PR/issue → SHA → artifact` + +Unknown values remain unknown. Do not manufacture timestamps, attribution, success, causality, or zero-valued metrics. + +## Downstream lanes + +| Consumer | Role | EPS boundary | +|---|---|---| +| Agentic orchestration | routing/manager evidence | consumes validated records; does not rewrite source evidence | +| ML / Moneyball | cohorting, regression, anomaly detection | derived metrics only | +| Hex | analytical/evidence presentation | read-only consumer; not system of record | +| Grafana | operational telemetry/alerts | operational projection; no write authority | +| Context Relationship Graph | relationship/provenance projection | metadata-only; no causal inference | +| Gource | temporal repository visualization | renderer/projection consumer, never canonical telemetry | +| Vercel/SHE | interactive evidence surface | presentation/query layer | +| future Web/WASM visualization | custom interaction | secondary lane after telemetry contracts stabilize | + +## Required downstream distinction + +**Telemetry first:** capture and validate evidence before optimizing dashboards or interaction design. + +**Visualization second:** visualization may reveal anomalies and aid exploration, but findings must resolve to EPS/source evidence. + +## Idempotency + +Consumers must deduplicate by `event_id`. Replays of the same source observation must not inflate counts. + +Actions with the same GitHub `run_id` but different `run_attempt` are distinct execution attempts. + +## Evaluation + +EPS success is evaluated by: + +1. source-to-event coverage; +2. provenance completeness; +3. idempotent replay behavior; +4. freshness; +5. schema validity; +6. attribution-confidence correctness; +7. reducer reproducibility; +8. downstream agreement across Hex/Grafana/ML; +9. ability to reconstruct a source event from its evidence identifiers. + +Visualization utility is evaluated separately and never substitutes for telemetry completeness. diff --git a/scripts/telemetry/emit_eps_event.py b/scripts/telemetry/emit_eps_event.py new file mode 100644 index 000000000..f4b9750e5 --- /dev/null +++ b/scripts/telemetry/emit_eps_event.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Emit one deterministic EPS v1 event from a bounded JSON payload. + +The emitter is metadata-only. It intentionally rejects free-form prompt, +completion, tool-payload, credential, and repository-content fields. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import sys +from datetime import datetime, timezone + +FORBIDDEN = { + "prompt", "completion", "messages", "message", "tool_payload", + "tool_call", "credential", "token", "secret", "password", "repository_content", +} + +def event_id(payload: dict) -> str: + stable = {k: payload.get(k) for k in ( + "event_type", "occurred_at", "source", "repo", "git_sha", + "run_id", "run_attempt", "entity_type", "entity_id", "status", + )} + return "eps-" + hashlib.sha256( + json.dumps(stable, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest()[:32] + +def validate(payload: dict) -> None: + required = { + "schema_version", "event_type", "occurred_at", "source", "repo", + "entity_type", "entity_id", "status", "provenance", "attributes", + } + missing = sorted(required - payload.keys()) + if missing: + raise ValueError("missing required fields: " + ", ".join(missing)) + if payload["schema_version"] != "eps.v1": + raise ValueError("unsupported schema_version") + if not re.fullmatch(r"^[^/\s]+/[^/\s]+$", payload["repo"]): + raise ValueError("repo must be owner/name") + if payload.get("git_sha") is not None and not re.fullmatch(r"[0-9a-f]{40}", payload["git_sha"]): + raise ValueError("git_sha must be a full lowercase SHA") + if not isinstance(payload["attributes"], dict): + raise ValueError("attributes must be an object") + leaked = sorted(FORBIDDEN.intersection(payload["attributes"])) + if leaked: + raise ValueError("forbidden attributes: " + ", ".join(leaked)) + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("input", nargs="?", default="-") + args = parser.parse_args() + raw = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read() + payload = json.loads(raw) + payload.setdefault("schema_version", "eps.v1") + payload.setdefault("event_id", event_id(payload)) + payload.setdefault("run_id", None) + payload.setdefault("run_attempt", None) + payload.setdefault("git_sha", None) + validate(payload) + try: + dt = datetime.fromisoformat(payload["occurred_at"].replace("Z", "+00:00")) + if dt.tzinfo is None: + raise ValueError + except ValueError as exc: + raise ValueError("occurred_at must be timezone-aware ISO-8601") from exc + print(json.dumps(payload, sort_keys=True, separators=(",", ":"))) + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/telemetry/validate_eps.py b/scripts/telemetry/validate_eps.py new file mode 100644 index 000000000..1a5a04dd1 --- /dev/null +++ b/scripts/telemetry/validate_eps.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +import argparse,hashlib,json,re,sys +from datetime import datetime +FORBIDDEN={"prompt","completion","messages","message","tool_payload","tool_call","credential","token","secret","password","repository_content"} +STATUSES={"observed","queued","running","completed","failed","cancelled","partial","unverified"} +def expected_id(e): + s=[e.get(k) for k in ("event_type","source","repo","git_sha","run_id","run_attempt","entity_type","entity_id","status")] + return "eps-"+hashlib.sha256(json.dumps(s,separators=(",",":"),ensure_ascii=False).encode()).hexdigest()[:32] +def validate(e): + req={"schema_version","event_id","event_type","occurred_at","source","repo","entity_type","entity_id","status","provenance","attributes"} + if req-set(e): raise ValueError("missing:"+",".join(sorted(req-set(e)))) + if e["schema_version"]!="eps.v1": raise ValueError("schema_version") + if not re.fullmatch(r"^[^/\\s]+/[^/\\s]+$",e["repo"]): raise ValueError("repo") + if e.get("git_sha") is not None and not re.fullmatch(r"[0-9a-f]{40}",e["git_sha"]): raise ValueError("git_sha") + if e["status"] not in STATUSES: raise ValueError("status") + if not isinstance(e["attributes"],dict): raise ValueError("attributes") + if FORBIDDEN & set(e["attributes"]): raise ValueError("forbidden") + datetime.fromisoformat(e["occurred_at"].replace("Z","+00:00")) + if not isinstance(e["provenance"],dict) or not e["provenance"].get("source_ref"): raise ValueError("provenance") + c=e["provenance"].get("attribution_confidence") + if c is not None and (not isinstance(c,(int,float)) or not 0<=c<=1): raise ValueError("confidence") + if e["event_id"]!=expected_id(e): raise ValueError("event_id") +def main(): + ap=argparse.ArgumentParser(); ap.add_argument("input",nargs="?",default="-"); ap.add_argument("--unique",action="store_true"); a=ap.parse_args() + fh=sys.stdin if a.input=="-" else open(a.input,encoding="utf-8"); seen=set() + for n,line in enumerate(fh,1): + if not line.strip(): continue + try: + e=json.loads(line); validate(e) + if a.unique and e["event_id"] in seen: raise ValueError("duplicate event_id") + seen.add(e["event_id"]) + except Exception as exc: print(f"line {n}: {exc}",file=sys.stderr); return 1 + return 0 +if __name__=="__main__": raise SystemExit(main()) diff --git a/scripts/visualization/evidence_to_gource.py b/scripts/visualization/evidence_to_gource.py new file mode 100644 index 000000000..182244f31 --- /dev/null +++ b/scripts/visualization/evidence_to_gource.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import argparse,json,sys +from datetime import datetime +ACTION={"file.added":"A","file.modified":"M","file.deleted":"D","repository.file_added":"A","repository.file_modified":"M","repository.file_deleted":"D"} +def main(): + ap=argparse.ArgumentParser(); ap.add_argument("input",nargs="?",default="-"); ap.add_argument("--actor-attribute",default="actor"); a=ap.parse_args() + fh=sys.stdin if a.input=="-" else open(a.input,encoding="utf-8") + for line in fh: + if not line.strip(): continue + e=json.loads(line) + if e.get("schema_version")!="eps.v1" or not e.get("event_id") or not e.get("provenance",{}).get("source_ref"): raise ValueError("invalid EPS provenance") + attrs=e.get("attributes",{}); action=ACTION.get(e.get("event_type"),attrs.get("gource_action")); path=attrs.get("path") or attrs.get("file_path"); actor=attrs.get(a.actor_attribute) or e.get("entity_id") + if action not in "AMD" or not path or not actor: continue + ts=int(datetime.fromisoformat(e["occurred_at"].replace("Z","+00:00")).timestamp()) + print(f"{ts}|{str(actor).replace('|','/')}|{action}|{str(path).replace('|','/')}") +if __name__=="__main__": main() diff --git a/tests/test_emit_eps_event.py b/tests/test_emit_eps_event.py new file mode 100644 index 000000000..1277373b7 --- /dev/null +++ b/tests/test_emit_eps_event.py @@ -0,0 +1,57 @@ +import json +import subprocess +import sys +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parents[1] / "scripts/telemetry/emit_eps_event.py" + +class TestEPSEmitter(unittest.TestCase): + def run_emitter(self, payload): + proc = subprocess.run( + [sys.executable, str(SCRIPT)], + input=json.dumps(payload), + text=True, + capture_output=True, + ) + return proc + + def base(self): + return { + "schema_version": "eps.v1", + "event_type": "pr.ledger_observed", + "occurred_at": "2026-09-21T00:00:00Z", + "source": "github.actions.pr-production-ledger", + "repo": "timerloggedout-spec/termux-monorepo", + "git_sha": "a" * 40, + "run_id": 123, + "run_attempt": 2, + "entity_type": "pull_request", + "entity_id": "pr:701", + "status": "observed", + "provenance": {"source_ref": "https://github.com/timerloggedout-spec/termux-monorepo/pull/701", "attribution_confidence": 1.0}, + "attributes": {"checks_total": 3, "checks_pending": 1}, + } + + def test_emits_deterministic_id(self): + a = self.run_emitter(self.base()) + b = self.run_emitter(self.base()) + self.assertEqual(a.returncode, 0) + self.assertEqual(a.stdout, b.stdout) + self.assertIn('"event_id":"eps-', a.stdout) + + def test_rejects_sensitive_attributes(self): + payload = self.base() + payload["attributes"]["prompt"] = "must not leave telemetry" + proc = self.run_emitter(payload) + self.assertNotEqual(proc.returncode, 0) + self.assertIn("forbidden", proc.stderr) + + def test_rejects_short_sha(self): + payload = self.base() + payload["git_sha"] = "abc" + proc = self.run_emitter(payload) + self.assertNotEqual(proc.returncode, 0) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_validate_eps.py b/tests/test_validate_eps.py new file mode 100644 index 000000000..ac52b8f30 --- /dev/null +++ b/tests/test_validate_eps.py @@ -0,0 +1,13 @@ +import hashlib,json,subprocess,sys,unittest +from pathlib import Path +ROOT=Path(__file__).resolve().parents[1] +class TestValidateEPS(unittest.TestCase): + def event(self): + e={"schema_version":"eps.v1","event_type":"file.modified","occurred_at":"2026-09-21T00:00:00Z","source":"fixture","repo":"o/r","git_sha":"a"*40,"run_id":1,"run_attempt":1,"entity_type":"commit","entity_id":"c:1","status":"observed","provenance":{"source_ref":"fixture","attribution_confidence":1},"attributes":{"actor":"agent","path":"src/a.py"}} + s=[e.get(k) for k in ("event_type","source","repo","git_sha","run_id","run_attempt","entity_type","entity_id","status")] + e["event_id"]="eps-"+hashlib.sha256(json.dumps(s,separators=(",",":"),ensure_ascii=False).encode()).hexdigest()[:32]; return e + def test_duplicate_rejected(self): + e=self.event(); p=subprocess.run([sys.executable,str(ROOT/"scripts/telemetry/validate_eps.py"),"--unique"],input=json.dumps(e)+"\n"+json.dumps(e)+"\n",text=True,capture_output=True); self.assertNotEqual(p.returncode,0) + def test_gource_projection(self): + e=self.event(); p=subprocess.run([sys.executable,str(ROOT/"scripts/visualization/evidence_to_gource.py")],input=json.dumps(e)+"\n",text=True,capture_output=True); self.assertEqual(p.returncode,0); self.assertIn("|M|src/a.py",p.stdout) +if __name__=="__main__": unittest.main()