scripts/source_ledger.py — pure deterministic projection:
- build_records: accepted OUTWARD artifact + SOURCE_READ/TOOL_FAILED receipts
→ ledger records with receipt_id (deterministic hash of stage+tool+
canonical_url+timestamp+seq), source_id, requested_url+resolved_url,
domain, status, tool, timestamp, source_type, claim_ids,
contradiction_ids, blocked_reason
- THE safety invariant: READ impossible without a successful machine
receipt; artifact-claimed-read with no matching receipt downgrades to
FAILED honestly; BLOCKED/FAILED/NOT_SELECTED back ZERO accepted claims
- NOT_SELECTED distinct from BLOCKED (never attempted ≠ attempted-refused)
- explicit contradicts_source_ids used when present; legacy nesting labeled
in render; plain-string contradictions not linked (labeled honestly)
- render_markdown: fixed template; runner/model from persisted factual
fields only, UNKNOWN if absent — never reconstructed
scripts/publish_source_ledger.py — idempotent Gitea publication:
- comment identified by marker containing artifact hash; same hash UPDATES,
new hash creates; publication failure raises VISIBLY (never silently
falsifies the ledger) and does not alter stage outcomes
research_stage_executor.py: model + runner persisted into the accepted
OUTWARD artifact (smallest authority point; PROV-1 metadata fields)
Real acceptance on #161: 16 records (4 READ with machine receipts replayed
from the genuine events file, 12 NOT_SELECTED backing zero claims), ledger
live on Gitea issue 161 (comment 3510), idempotency proven (second publish
updates same comment). Legacy artifact honestly labeled UNKNOWN for
runner/model and legacy-nesting noted in render.
14 ledger tests; full suite 1151 passed; ruff clean.