S06-S08: Python spec, Go in-process leaf, tests and Forgejo CI
Some checks are pending
ci / python (push) Waiting to run
ci / go (push) Waiting to run

This commit is contained in:
George Lambert 2026-09-15 22:17:19 -04:00
commit d67509e470
16 changed files with 726 additions and 0 deletions

View file

@ -0,0 +1,89 @@
"""Append-only admin configuration history (Verae DataCube-shaped JSONL).
Each row stores previous file, new file, unified diff, actor, and dual hashes.
Compatible with pfc.chain field names so a real cube can ingest the log.
"""
from __future__ import annotations # annotations
import difflib # unified diff
import hashlib # sha256
import json # rows
import time # created_ts
from pathlib import Path # file
from typing import Any, Dict, List, Optional # types
def sha256_hex(data: bytes) -> str:
"""Document hash for the history row."""
return hashlib.sha256(data).hexdigest() # hex
def unified_diff(prev: str, new: str, name: str = "config") -> str:
"""Unified diff of previous and new config text."""
return "".join( # join generator
difflib.unified_diff(
prev.splitlines(True), # old lines keepends
new.splitlines(True), # new lines
fromfile=name + ".prev", # label
tofile=name + ".new", # label
)
)
class AdminHistory:
"""JSONL chain under a cube directory: blockchain/chain.jsonl."""
def __init__(self, cube_dir: Path):
self.path = Path(cube_dir) / "blockchain" / "chain.jsonl" # pfc layout
self.path.parent.mkdir(parents=True, exist_ok=True) # ensure dir
if not self.path.exists(): # empty chain
self.path.touch() # create
def _rows(self) -> List[Dict[str, Any]]:
"""Read all rows."""
rows = [] # accumulator
for line in self.path.read_text().splitlines(): # each line
if line.strip(): # skip blanks
rows.append(json.loads(line)) # parse
return rows # list
def tip(self) -> Optional[Dict[str, Any]]:
"""Last row or None."""
rows = self._rows() # load
return rows[-1] if rows else None # tip
def append_change(
self,
*,
actor: str,
prev_text: str,
new_text: str,
name: str = "secure-messaging.json",
) -> Dict[str, Any]:
"""Append prev, new, diff. Empty actor is rejected."""
if not (actor or "").strip(): # required
raise ValueError("actor required")
prev_h = sha256_hex(prev_text.encode()) # hash old
new_h = sha256_hex(new_text.encode()) # hash new
payload = { # chain payload
"type": "admin-config",
"actor": actor,
"prev": prev_text,
"new": new_text,
"diff": unified_diff(prev_text, new_text, name),
"prev_sha256": prev_h,
"new_sha256": new_h,
"created_ts": time.time(),
}
body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() # canonical
prev = self.tip() # previous chain row
row = { # pfc-like envelope
"seq": (prev["seq"] + 1) if prev else 0,
"prev_sha256": prev["sha256"] if prev else "0" * 64,
"payload": payload,
"sha256": sha256_hex(body),
}
with self.path.open("a") as f: # append-only
f.write(json.dumps(row, sort_keys=True) + "\n") # one line
return row # for tests