Files
rune/scripts/publish_source_ledger.py
T
Nick 4eefc15837
CI Pipeline / best-practices (push) Successful in 8s
Auto-version and Release / release (push) Failing after 13s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / canary (push) Successful in 33s
CI Pipeline / lint (push) Failing after 41s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / unit-tests (push) Successful in 59s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 37s
fix(lifecycle+auth): canonical gitea auth path, terminalization helper, fault-injection gaps (T15 Parts A-E)
- scripts/gitea_auth.py: single credential ladder (env -> 0600 token file ->
  git credential fill); 6 consumers migrated off direct os.environ reads
- rune_operator: consolidate 5 duplicated failure blocks into
  _terminalize_execution_failure; fix 3 fault-injection gaps:
  (1) dual-module PreparedTool identity split crashed approval-path saves,
  (2) fallback-model failure escaped send_task (created/pending orphan),
  (3) running-transition save uncaught (running-with-no-owner)
- envelope task.status now agrees with canonical ledger (awaiting_approval)
- orphan task_ac35f4e287c8 terminalized failed with durable evidence
- tests: gitea auth ladder (6), lifecycle fault matrix (9 boundaries +
  restart/reload + helper contract); 1280 green in ci-venv; ratchet 352/80
2026-09-11 20:15:57 +01:00

117 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""publish_source_ledger.py — idempotent Gitea publication for the Source Ledger
(PROV-1, #282).
Pure projection publisher: renders the ledger from the accepted OUTWARD
artifact and posts/updates a single OUTWARD SOURCE LEDGER comment on the
issue. Idempotency: the comment is identified by a fixed marker containing
the artifact hash — re-running with the same hash UPDATES the existing
comment instead of creating duplicates.
Publication failure raises visibly (per #282 constraint: provenance
publication fails visibly, must not silently falsify the ledger). It does
NOT alter research cognition or stage outcomes — callers invoke it after
gate acceptance, and its failure does not invalidate the accepted artifact.
"""
from __future__ import annotations
import json
import os
import urllib.request
from source_ledger import build_records, render_markdown
import sys as _sys
_SCRIPT_DIR = str(__import__('pathlib').Path(__file__).resolve().parent)
if _SCRIPT_DIR not in _sys.path:
_sys.path.insert(0, _SCRIPT_DIR)
import gitea_auth
MARKER_PREFIX = "<!-- source-ledger"
def _api(method: str, path: str, payload: dict | None = None) -> tuple[int, dict | str]:
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
server = os.environ.get("GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
req = urllib.request.Request(
server + "/api/v1" + path,
method=method,
data=json.dumps(payload).encode() if payload else None,
headers={"Authorization": f"token {token}", "Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=60) as r:
body = r.read().decode()
return r.status, (json.loads(body) if body.strip() else "")
except urllib.error.HTTPError as e:
return e.code, e.read().decode()[:500]
def _marker(artifact_hash: str) -> str:
return f"{MARKER_PREFIX} hash={artifact_hash} -->"
def publish(
issue: int, artifact_dir: str, artifact_hash: str, runner: dict | None = None, model: str | None = None
) -> dict:
"""Render + post/update the ledger comment. Returns publication result."""
path = os.path.join(artifact_dir, "outward.json")
with open(path) as f:
outward = json.load(f)
# runner/model: persisted factual fields from the artifact; UNKNOWN if absent
runner = runner or outward.get("runner")
model = model or outward.get("model", "UNKNOWN")
records = build_records(outward)
legacy = not any(
c.get("contradicts_source_ids")
for c in outward.get("contradictory_evidence", outward.get("contradictions", []))
if isinstance(c, dict)
)
body = (
_marker(artifact_hash)
+ "\n"
+ render_markdown(records, outward, runner, model, artifact_hash, legacy_claims=legacy)
)
# Find existing ledger comment for this artifact hash (idempotency)
status, comments = _api("GET", f"/repos/conductor/rune/issues/{issue}/comments?limit=50")
if status != 200 or not isinstance(comments, list):
raise RuntimeError(f"SourceLedger publish: cannot list comments (HTTP {status})")
existing = None
for c in comments:
if isinstance(c, dict) and c.get("body", "").startswith(_marker(artifact_hash)):
existing = c
break
if existing:
status, _ = _api("PATCH", f"/repos/conductor/rune/issues/comments/{existing['id']}", {"body": body})
if status not in (200, 201):
raise RuntimeError(f"SourceLedger publish: update failed (HTTP {status})")
return {"action": "updated", "comment_id": existing["id"], "artifact_hash": artifact_hash}
status, created = _api("POST", f"/repos/conductor/rune/issues/{issue}/comments", {"body": body})
if status not in (200, 201):
raise RuntimeError(f"SourceLedger publish: create failed (HTTP {status}): {created}")
return {"action": "created", "comment_id": created.get("id"), "artifact_hash": artifact_hash}
def main_with(argv: list[str]) -> int:
import argparse
parser = argparse.ArgumentParser(description="Publish the OUTWARD Source Ledger")
parser.add_argument("--issue", type=int, required=True)
parser.add_argument("--artifact-dir", required=True)
parser.add_argument("--artifact-hash", required=True)
args = parser.parse_args(argv[1:])
result = publish(args.issue, args.artifact_dir, args.artifact_hash)
print(json.dumps(result))
return 0
if __name__ == "__main__":
import sys
raise SystemExit(main_with(sys.argv))