FlyBrain-Lab / src /research /ledger.py
timfromhcs's picture
V5 final: src/research/ledger.py
3327a12 verified
Raw History Blame Contribute Delete
5.14 kB
"""Research ledger V4 (mission §35): every major event receives a full
provenance record with cryptographic hashes and a chained event stream.
Deterministic: event ids derive from content + sequence, never wall-clock
(timestamps are operational metadata only).
"""
import hashlib
import json
import os
import subprocess
import time
from typing import Any, Dict, List, Optional
LEDGER_VERSION = "research_ledger_v1"
EVENT_TYPES = ("birth", "death", "mutation", "crossover", "learning", "growth",
"pruning", "synapse_creation", "synapse_deletion", "reproduction",
"speciation", "checkpoint", "experiment", "milestone")
def code_sha() -> str:
"""Current commit as code identity; 'uncommitted' when dirty/unavailable."""
try:
commit = subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True,
text=True, timeout=5).stdout.strip()
dirty = subprocess.run(["git", "status", "--porcelain"], capture_output=True,
text=True, timeout=5).stdout.strip()
return commit if (commit and not dirty) else f"{commit or 'unknown'}+dirty"
except Exception:
return "unknown"
def dataset_sha(soma_path: str, connections_path: str) -> str:
from src.connectome.loader import _file_sha256
return hashlib.sha256(
_file_sha256(soma_path).encode()
+ _file_sha256(connections_path).encode()).hexdigest()
def shader_sha(shader_dir: str = "shaders") -> str:
h = hashlib.sha256()
for name in sorted(os.listdir(shader_dir)) if os.path.isdir(shader_dir) else []:
p = os.path.join(shader_dir, name)
if name.endswith(".spv") and os.path.isfile(p):
with open(p, "rb") as f:
h.update(name.encode() + f.read())
return h.hexdigest()
def _record_hash(rec: Dict[str, Any]) -> str:
# Identity covers content + sequence + chain only. Operational metadata
# (ts wall-clock, record_hash itself) is NEVER identity: two identical
# appends must hash identically or deterministic replay breaks.
payload = json.dumps({k: v for k, v in rec.items()
if k not in ("record_hash", "ts")},
sort_keys=True, default=str)
return hashlib.sha256(payload.encode()).hexdigest()
class ResearchLedger:
"""Append-only, hash-chained research event ledger."""
def __init__(self, experiment_id: str, seed: int,
path: Optional[str] = None):
self.experiment_id = experiment_id
self.seed = int(seed)
self.path = path
self.records: List[Dict[str, Any]] = []
self._prev = "0" * 64
self._static = {
"experiment_id": experiment_id,
"seed": self.seed,
"code_sha": code_sha(),
"dataset_sha": "unknown",
"shader_sha": shader_sha(),
"ledger_version": LEDGER_VERSION,
}
def set_dataset_sha(self, sha: str) -> None:
self._static["dataset_sha"] = sha
def append(self, event_type: str, tick: int, generation: int,
payload: Dict[str, Any], genome_sha: str = "", brain_sha: str = "",
world_sha: str = "", result_sha: str = "", parent_event: str = "") \
-> Dict[str, Any]:
if event_type not in EVENT_TYPES:
raise ValueError(f"unknown ledger event type {event_type!r}")
seq = len(self.records)
rec = {
**self._static,
"event_id": hashlib.sha256(
f"{self.experiment_id}|{event_type}|{seq}|{self._prev}"
.encode()).hexdigest()[:16],
"parent_event": parent_event or (self.records[-1]["event_id"]
if self.records else ""),
"event_type": event_type,
"seq": seq,
"tick": int(tick),
"generation": int(generation),
"genome_sha": genome_sha or "none",
"brain_sha": brain_sha or "none",
"world_sha": world_sha or "none",
"result_sha": result_sha or "none",
"payload": payload,
"prev_hash": self._prev,
"ts": time.time(), # operational metadata only, never identity
}
rec["record_hash"] = _record_hash(rec)
self.records.append(rec)
self._prev = rec["record_hash"]
if self.path:
os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True)
with open(self.path, "a", encoding="utf-8") as f:
f.write(json.dumps(rec, sort_keys=True, default=str) + "\n")
return rec
def verify_chain(self) -> bool:
prev = "0" * 64
for rec in self.records:
if rec["prev_hash"] != prev:
return False
if rec["record_hash"] != _record_hash(rec):
return False
prev = rec["record_hash"]
return True
def event_stream_hash(self) -> str:
h = hashlib.sha256()
for rec in self.records:
h.update(rec["record_hash"].encode())
return h.hexdigest()