Compare commits

...
Author SHA1 Message Date
Nick e7d4c2ad9b chore(ci): resync dependency lock — pyright 1.1.411 → 1.1.412 (upstream drift)
CI Pipeline / best-practices (push) Successful in 10s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / lint (push) Successful in 38s
CI Pipeline / canary (push) Successful in 39s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 1m4s
CI Pipeline / best-practices (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Successful in 36s
CI Pipeline / canary (pull_request) Successful in 30s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / unit-tests (pull_request) Successful in 1m7s
CI Pipeline / documentation (pull_request) Successful in 3s
Spec gate (headless) / spec-gate (pull_request) Successful in 8s
CI Pipeline / gitea-offline (pull_request) Successful in 16s
CI Pipeline / type-check (pull_request) Successful in 30s
CI Pipeline / required-checks (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Successful in 40s
2026-09-09 19:01:09 +01:00
Nick 2da0afb324 feat(PROV-1): deterministic Source Ledger — renderer, persisted metadata, idempotent Gitea publication (closes #282)
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
Auto-label / priority-gate (pull_request) Skipped
CI Pipeline / lint (push) Failing after 28s
Auto-label / auto-label (pull_request) Successful in 2s
CI Pipeline / canary (push) Successful in 33s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 57s
CI Pipeline / best-practices (pull_request) Successful in 4s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Failing after 26s
CI Pipeline / canary (pull_request) Successful in 31s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / gitea-offline (pull_request) Successful in 16s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / type-check (pull_request) Successful in 29s
CI Pipeline / unit-tests (pull_request) Successful in 57s
CI Pipeline / required-checks (pull_request) Successful in 1s
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.
2026-09-09 18:56:53 +01:00
conductor aa8ca45302 chore(release): auto-bump version to 0.59.5 — earned by #307 (INFRA-1 acceptance: native merge API after 1.27.3 upgrade)
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / unit-tests (push) Successful in 58s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #307
2026-09-09 17:53:58 +01:00
conductor 058c183a89 chore(release): auto-bump version to 0.59.5 — earned by #307 (INFRA-1 acceptance: native merge API after 1.27.3 upgrade)
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #307
2026-09-09 17:53:56 +01:00
conductor 62cb334c95 chore(release): auto-bump version to 0.59.5 — earned by #307 (INFRA-1 acceptance: native merge API after 1.27.3 upgrade)
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / lint (push) Successful in 39s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 59s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #307
2026-09-09 17:53:53 +01:00
conductor 2379ba2a70 chore(release): auto-bump version to 0.59.5 — earned by #307 (INFRA-1 acceptance: native merge API after 1.27.3 upgrade)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / canary (push) Failing after 36s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / unit-tests (push) Successful in 53s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #307
2026-09-09 17:53:50 +01:00
conductor a7215d96be chore(release): auto-bump version to 0.59.5 — earned by #307 (INFRA-1 acceptance: native merge API after 1.27.3 upgrade)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / canary (push) Failing after 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Failing after 48s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #307
2026-09-09 17:53:48 +01:00
conductor e7b7aa2b10 Merge pull request 'INFRA-1 acceptance: native merge API after 1.27.3 upgrade' (#307) from infra1-accept into master
CI Pipeline / best-practices (push) Successful in 6s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / canary (push) Successful in 34s
CI Pipeline / lint (push) Successful in 39s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 58s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-09 17:53:32 +01:00
Nick 897fd1d74f chore(infra1): acceptance PR — native merge API after Gitea 1.27.3 upgrade
Auto-label / priority-gate (pull_request) Skipped
Auto-label / auto-label (pull_request) Successful in 1s
CI Pipeline / best-practices (pull_request) Successful in 2s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Successful in 35s
CI Pipeline / canary (pull_request) Successful in 33s
CI Pipeline / unit-tests (pull_request) Successful in 53s
CI Pipeline / gitea-offline (pull_request) Successful in 15s
CI Pipeline / type-check (pull_request) Successful in 29s
CI Pipeline / required-checks (pull_request) Successful in 1s
Spec gate (headless) / spec-gate (pull_request) Successful in 7s
Auto-version and Release / release (pull_request) Successful in 34s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / lint (push) Failing after 37s
CI Pipeline / canary (push) Failing after 47s
CI Pipeline / type-check (push) Successful in 37s
CI Pipeline / unit-tests (push) Failing after 1m14s
CI Pipeline / required-checks (push) Successful in 6s
2026-09-09 17:51:19 +01:00
conductor 1b224dabf1 chore(release): auto-bump version to 0.59.4 — earned by #306 (INFRA-1 probe 5b: full merge response capture)
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / unit-tests (push) Successful in 50s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 17s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #306
2026-09-09 17:42:17 +01:00
conductor a9866db411 chore(release): auto-bump version to 0.59.4 — earned by #306 (INFRA-1 probe 5b: full merge response capture)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #306
2026-09-09 17:42:13 +01:00
conductor 003b6942e7 chore(release): auto-bump version to 0.59.4 — earned by #306 (INFRA-1 probe 5b: full merge response capture)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / unit-tests (push) Successful in 53s
CI Pipeline / required-checks (push) Successful in 0s
Promotion record: #306
2026-09-09 17:42:09 +01:00
conductor 6e4d9d9b47 chore(release): auto-bump version to 0.59.4 — earned by #306 (INFRA-1 probe 5b: full merge response capture)
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Failing after 29s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 54s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #306
2026-09-09 17:42:07 +01:00
conductor caa94095bf chore(release): auto-bump version to 0.59.4 — earned by #306 (INFRA-1 probe 5b: full merge response capture)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Failing after 29s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #306
2026-09-09 17:42:05 +01:00
conductor b1491ceef2 Merge pull request 'INFRA-1 probe 5b: full merge response capture' (#306) from infra1-probe5 into master
CI Pipeline / best-practices (push) Successful in 5s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / unit-tests (push) Successful in 56s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-09 17:41:47 +01:00
Nick c4c73ca037 chore(infra1): probe 5 — full native merge response capture (robot disabled)
CI Pipeline / best-practices (pull_request) Successful in 3s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Successful in 35s
CI Pipeline / canary (pull_request) Successful in 31s
CI Pipeline / gitea-offline (pull_request) Successful in 15s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / unit-tests (pull_request) Successful in 51s
CI Pipeline / type-check (pull_request) Successful in 29s
CI Pipeline / required-checks (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Successful in 47s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / lint (push) Failing after 39s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / canary (push) Failing after 38s
CI Pipeline / type-check (push) Successful in 41s
CI Pipeline / unit-tests (push) Failing after 1m9s
CI Pipeline / required-checks (push) Successful in 4s
2026-09-09 17:39:43 +01:00
conductor b5827a3cd0 chore(release): auto-bump version to 0.59.3 — earned by #305 (INFRA-1 probe 3: direct native merge proof)
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / required-checks (push) Successful in 2s
Promotion record: #305
2026-09-09 17:27:59 +01:00
conductor 4d79f7c230 chore(release): auto-bump version to 0.59.3 — earned by #305 (INFRA-1 probe 3: direct native merge proof)
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 33s
CI Pipeline / lint (push) Successful in 39s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / unit-tests (push) Successful in 56s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #305
2026-09-09 17:27:57 +01:00
conductor 467be81b47 chore(release): auto-bump version to 0.59.3 — earned by #305 (INFRA-1 probe 3: direct native merge proof)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 53s
CI Pipeline / required-checks (push) Successful in 0s
Promotion record: #305
2026-09-09 17:27:51 +01:00
conductor e3cec58917 chore(release): auto-bump version to 0.59.3 — earned by #305 (INFRA-1 probe 3: direct native merge proof)
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / canary (push) Successful in 29s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #305
2026-09-09 17:27:49 +01:00
conductor bb25b0a48a chore(release): auto-bump version to 0.59.3 — earned by #305 (INFRA-1 probe 3: direct native merge proof)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Failing after 31s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Failing after 46s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #305
2026-09-09 17:27:47 +01:00
conductor 5643b6c3b5 Merge pull request 'INFRA-1 probe 3: direct native merge proof' (#305) from infra1-probe3 into master
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 56s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-09 17:27:34 +01:00
conductor 5cfe2bf5f3 Merge branch 'master' into infra1-probe3
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / best-practices (pull_request) Successful in 3s
CI Pipeline / unit-tests (push) Successful in 56s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Successful in 35s
CI Pipeline / gitea-offline (pull_request) Successful in 15s
Spec gate (headless) / spec-gate (pull_request) Successful in 7s
CI Pipeline / canary (pull_request) Successful in 32s
CI Pipeline / required-checks (push) Successful in 0s
CI Pipeline / type-check (pull_request) Successful in 29s
CI Pipeline / unit-tests (pull_request) Successful in 51s
CI Pipeline / required-checks (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Successful in 36s
2026-09-09 17:25:07 +01:00
Nick 5252ffcfc3 chore(infra1): probe 3 — native merge by direct API call
Auto-label / auto-label (pull_request) Successful in 2s
Auto-label / priority-gate (pull_request) Skipped
CI Pipeline / lint (pull_request) Successful in 40s
CI Pipeline / best-practices (pull_request) Successful in 4s
CI Pipeline / documentation (pull_request) Successful in 4s
CI Pipeline / gitea-offline (pull_request) Successful in 16s
CI Pipeline / type-check (pull_request) Successful in 29s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / canary (pull_request) Successful in 31s
CI Pipeline / unit-tests (pull_request) Successful in 52s
CI Pipeline / required-checks (pull_request) Successful in 0s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / lint (push) Failing after 40s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / canary (push) Failing after 43s
CI Pipeline / type-check (push) Successful in 38s
CI Pipeline / unit-tests (push) Failing after 1m18s
CI Pipeline / required-checks (push) Successful in 8s
2026-09-09 17:18:43 +01:00
conductor 3498b8590c chore(release): auto-bump version to 0.59.2 — earned by #304 (INFRA-1 probe 2: merge-during-pending reproduction)
CI Pipeline / required-checks (push) Successful in 2s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
Promotion record: #304
2026-09-09 17:17:29 +01:00
conductor 11e417bc3d chore(release): auto-bump version to 0.59.2 — earned by #304 (INFRA-1 probe 2: merge-during-pending reproduction)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 50s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #304
2026-09-09 17:17:26 +01:00
conductor cec2214151 chore(release): auto-bump version to 0.59.2 — earned by #304 (INFRA-1 probe 2: merge-during-pending reproduction)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #304
2026-09-09 17:17:20 +01:00
conductor bbf90e21b2 chore(release): auto-bump version to 0.59.2 — earned by #304 (INFRA-1 probe 2: merge-during-pending reproduction)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 49s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / canary (push) Failing after 31s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #304
2026-09-09 17:17:16 +01:00
conductor 29aaaea132 chore(release): auto-bump version to 0.59.2 — earned by #304 (INFRA-1 probe 2: merge-during-pending reproduction)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / canary (push) Failing after 29s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 39s
CI Pipeline / required-checks (push) Successful in 2s
Promotion record: #304
2026-09-09 17:17:15 +01:00
conductor d0afb38279 Merge pull request 'INFRA-1 probe 2: merge-during-pending reproduction' (#304) from infra1-probe2 into master
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 52s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-09 17:16:56 +01:00
Nick 91fc479e0d chore(infra1): probe 2 — native merge while CI pending
Auto-label / priority-gate (pull_request) Skipped
Auto-label / auto-label (pull_request) Successful in 1s
CI Pipeline / best-practices (pull_request) Successful in 3s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / lint (pull_request) Successful in 35s
CI Pipeline / canary (pull_request) Successful in 30s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / gitea-offline (pull_request) Successful in 15s
CI Pipeline / unit-tests (pull_request) Successful in 52s
CI Pipeline / type-check (pull_request) Successful in 28s
CI Pipeline / required-checks (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Successful in 41s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / lint (push) Failing after 41s
CI Pipeline / canary (push) Failing after 45s
CI Pipeline / type-check (push) Successful in 40s
CI Pipeline / unit-tests (push) Failing after 1m21s
CI Pipeline / required-checks (push) Successful in 6s
2026-09-09 17:11:36 +01:00
conductor a72bb5f089 chore(release): auto-bump version to 0.59.1 — earned by #303 (INFRA-1 probe: native merge API reproduction (disposable))
CI Pipeline / required-checks (push) Successful in 2s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / unit-tests (push) Successful in 55s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 14s
Promotion record: #303
2026-09-09 17:09:33 +01:00
conductor 5cba27e32f chore(release): auto-bump version to 0.59.1 — earned by #303 (INFRA-1 probe: native merge API reproduction (disposable))
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 50s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #303
2026-09-09 17:09:27 +01:00
conductor 7882e5ff5f chore(release): auto-bump version to 0.59.1 — earned by #303 (INFRA-1 probe: native merge API reproduction (disposable))
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 55s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: #303
2026-09-09 17:09:24 +01:00
conductor 4de17ed567 chore(release): auto-bump version to 0.59.1 — earned by #303 (INFRA-1 probe: native merge API reproduction (disposable))
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / canary (push) Successful in 36s
CI Pipeline / unit-tests (push) Successful in 1m0s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / required-checks (push) Successful in 2s
Promotion record: #303
2026-09-09 17:09:22 +01:00
conductor 45a6771788 chore(release): auto-bump version to 0.59.1 — earned by #303 (INFRA-1 probe: native merge API reproduction (disposable))
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / canary (push) Failing after 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 51s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / required-checks (push) Successful in 2s
Promotion record: #303
2026-09-09 17:09:20 +01:00
conductor 447e1219fa Merge pull request 'INFRA-1 probe: native merge API reproduction (disposable)' (#303) from infra1-probe into master
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / lint (push) Successful in 38s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / unit-tests (push) Successful in 57s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-09 17:09:03 +01:00
10 changed files with 531 additions and 7 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.59.0**
**Current release: v0.59.5**
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.59.0"
version = "0.59.5"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+1 -1
View File
@@ -82,7 +82,7 @@ pygments==2.21.0
# pytest
# rich
# textual
pyright==1.1.411
pyright==1.1.412
# via rune (pyproject.toml)
pytest==9.1.1
# via
+103
View File
@@ -0,0 +1,103 @@
#!/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
MARKER_PREFIX = "<!-- source-ledger"
def _api(method: str, path: str, payload: dict | None = None) -> tuple[int, dict | str]:
token = os.environ.get("GITEA_TOKEN", "")
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))
+9
View File
@@ -344,6 +344,12 @@ class StageExecutor:
"error": "no text output", "stderr": error_detail}
artifact = self._normalize_outward_artifact(
worker_artifact, relay_result, event)
# PROV-1: persisted factual metadata for the Source Ledger
artifact["model"] = os.environ.get("MIMO_MODEL", "UNKNOWN")
from runner_evidence import capture_execution_evidence, summarize_for_artifact
artifact["runner"] = summarize_for_artifact(capture_execution_evidence(
stage="OUTWARD", issue=self.issue,
process_id=f"research-{self.issue}-r{self.round_no}"))
gate_errors = self._validate_outward(artifact)
if gate_errors:
self.tree.fail("; ".join(gate_errors))
@@ -448,6 +454,8 @@ class StageExecutor:
"text": artifact_text,
"event_counts": relay_result["event_counts"],
"elapsed": relay_result["elapsed"],
# PROV-1 (#282): persisted factual metadata for the Source Ledger.
"model": os.environ.get("MIMO_MODEL", "UNKNOWN"),
}
gate_errors = self._validate_outward(artifact)
@@ -1395,6 +1403,7 @@ class StageExecutor:
"worker_artifact_file": True,
"event_counts": relay_result["event_counts"],
"elapsed": relay_result["elapsed"],
"model": os.environ.get("MIMO_MODEL", "UNKNOWN"),
}
def _validate_outward(self, artifact: dict) -> list:
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""source_ledger.py — deterministic Source Ledger renderer (PROV-1, #282).
A thin, boring projection of already-persisted state:
accepted OUTWARD artifact + SOURCE_READ/TOOL_FAILED receipts
+ persisted run metadata (runner, model)
→ pure deterministic render → fixed Markdown template
→ idempotent Gitea issue comment (caller's responsibility)
Hard invariants:
- READ is impossible without a successful machine receipt (SOURCE_READ with
matching URL). BLOCKED/FAILED sources may appear in the ledger but can
never support accepted evidence.
- runner/model come from persisted factual fields only; UNKNOWN if absent.
Never reconstructed from session memory or timing records.
- requested_url and resolved_url both retained.
- claim relationships use explicit IDs when present; for pre-#282 artifacts
the nesting of claims under sources_read is used and is labeled
"derived (legacy nesting)" in the rendered output.
- renderer failure raises visibly — provenance publication failing loudly
must not silently falsify the ledger.
- No trust scores. Factual source classification only.
"""
from __future__ import annotations
import hashlib
import json
from urllib.parse import urlparse
STATUS_READ = "READ"
STATUS_BLOCKED = "BLOCKED"
STATUS_FAILED = "FAILED"
STATUS_NOT_SELECTED = "NOT_SELECTED"
def _domain(url: str) -> str:
try:
return urlparse(url).netloc or "unknown"
except (ValueError, AttributeError):
# Malformed URL — domain is unknown, the row still renders
return "unknown"
def _receipt_id(stage: str, tool: str, url: str, ts, seq: int) -> str:
"""Deterministic receipt identity: hash of stage + tool + canonical URL +
timestamp + event sequence (NOT ts+url alone)."""
canonical = url.rstrip("/")
raw = f"{stage}|{tool}|{canonical}|{ts}|{seq}"
return "R-" + hashlib.sha256(raw.encode()).hexdigest()[:12]
def _norm(url: str) -> str:
return (url or "").rstrip("/").lower()
def build_records(outward: dict, stage: str = "OUTWARD") -> list[dict]:
"""Pure projection: accepted OUTWARD artifact + receipts → ledger records.
Raises on structurally invalid input (renderer failure is visible)."""
if not outward or not isinstance(outward, dict):
raise ValueError("SourceLedger: no accepted OUTWARD artifact")
discovery = outward.get("discovery", {})
sources = discovery.get("sources_discovered", [])
reading = outward.get("reading", {}).get("sources_read", [])
receipts = outward.get("tool_receipts", []) or outward.get("reading", {}).get("tool_receipts", [])
# Receipt join keys: successful reads by normalized URL
read_urls: dict[str, dict] = {}
failed_urls: dict[str, dict] = {}
for i, r in enumerate(receipts):
url = _norm(r.get("url", ""))
if not url:
continue
rid = _receipt_id(stage, r.get("tool", "?"), r.get("url", ""),
r.get("ts", 0), i)
if r.get("kind") == "SOURCE_READ":
read_urls[url] = {"receipt_id": rid, "tool": r.get("tool", "?"), "ts": r.get("ts")}
elif r.get("kind") == "TOOL_FAILED":
failed_urls[url] = {"receipt_id": rid, "tool": r.get("tool", "?"), "ts": r.get("ts")}
# Claim → source: explicit if artifact carries claim source_ids, else
# legacy nesting (claims are children of sources_read entries).
claims_by_source: dict[str, list[str]] = {}
legacy = False
for s in reading:
sid = s.get("id")
for c in s.get("claims", []):
claims_by_source.setdefault(sid, []).append(c["id"])
legacy = True
# Contradiction links: explicit contradicts_source_ids if present;
# otherwise no deterministic derivation (labeled honestly).
contradictions_by_source: dict[str, list[str]] = {}
for i, c in enumerate(outward.get("contradictory_evidence", outward.get("contradictions", []))):
if isinstance(c, dict):
for sid in c.get("contradicts_source_ids", []):
contradictions_by_source.setdefault(sid, []).append(c.get("id", f"C{i+1}"))
# plain-string contradictions are not linked (labeled in render)
records = []
for s in sources:
sid = s.get("id", "?")
url = s.get("url", "")
nurl = _norm(url)
# Status precedence: machine receipt is authoritative. A source the
# artifact CLAIMS was read but with no SOURCE_READ receipt cannot
# render as READ.
if nurl in read_urls:
status, receipt = STATUS_READ, read_urls[nurl]
elif nurl in failed_urls:
status, receipt = STATUS_FAILED, failed_urls[nurl]
elif s.get("read_receipt"):
# Artifact asserts read but no machine receipt matched — honest
# downgrade with provenance note rather than silent READ.
status, receipt = STATUS_FAILED, None
else:
# Never attempted: not selected for reading (distinct from BLOCKED,
# which implies an attempted fetch that was refused)
status, receipt = STATUS_NOT_SELECTED, None
# Upgrade precedence fix is above; this block is intentionally after
# status resolution so the downgrade applies regardless of note text.
resolved = s.get("resolved_url", url) # explicit field if present
records.append({
"receipt_id": (receipt or {}).get("receipt_id", ""),
"source_id": sid,
"requested_url": url,
"resolved_url": resolved,
"domain": _domain(resolved),
"status": status,
"tool": (receipt or {}).get("tool", s.get("tool", "UNKNOWN_NOT_EXPOSED")),
"timestamp": (receipt or {}).get("ts", "UNKNOWN_NOT_EXPOSED"),
"source_type": s.get("class", s.get("source_class", "UNKNOWN_NOT_EXPOSED")),
"claim_ids": claims_by_source.get(sid, []),
"contradiction_ids": contradictions_by_source.get(sid, []),
"blocked_reason": (s.get("note", "") if status == STATUS_BLOCKED else
(receipt.get("kind") if status == STATUS_FAILED and receipt else
("no machine SOURCE_READ receipt matched (legacy artifact)" if status == STATUS_FAILED else ""))),
"supports_evidence": status == STATUS_READ,
})
return records
def render_markdown(records: list[dict], outward: dict, runner: dict | None,
model: str, artifact_hash: str, legacy_claims: bool = False) -> str:
"""Fixed Markdown template. Pure function of records + metadata."""
d = outward.get("discovery", {})
n_read = sum(1 for r in records if r["status"] == STATUS_READ)
n_blocked = sum(1 for r in records if r["status"] == STATUS_BLOCKED)
n_failed = sum(1 for r in records if r["status"] == STATUS_FAILED)
n_claims = sum(len(r["claim_ids"]) for r in records)
n_contra = len(outward.get("contradictory_evidence", outward.get("contradictions", [])))
runner_name = (runner or {}).get("runner_name", "UNKNOWN")
runner_host = (runner or {}).get("runner_hostname", "UNKNOWN")
prov_note = ""
if legacy_claims:
prov_note = ("\n*Claim↔source links for this legacy artifact are derived from "
"claim nesting (pre-#282 format); future artifacts carry explicit "
"supports_source_ids/contradicts_source_ids.*")
lines = [
"## OUTWARD — ACCEPTED",
f"- artifact hash: `{artifact_hash}`",
f"- runner: {runner_name} ({runner_host})",
f"- model: {model}",
f"- queries: {len(d.get('queries', []))}",
f"- sources discovered: {len(records)}",
f"- sources actually read (machine receipt): {n_read}",
f"- blocked: {n_blocked} | failed fetch: {n_failed}",
f"- evidence claims: {n_claims} | contradictions: {n_contra}",
"",
"## SOURCE LEDGER",
"",
"| ID | source/domain | type | status | E-claims | contradictions | caveat |",
"|---|---|---|---|---|---|---|",
]
for r in records:
caveat = r["blocked_reason"] or ""
if r["status"] != STATUS_READ:
caveat = (caveat + " — backs ZERO accepted claims").strip(" —")
link = f"[{r['domain']}]({r['requested_url']})" if r["requested_url"].startswith("http") else r["domain"]
lines.append(
f"| {r['source_id']} | {link} | {r['source_type']} | {r['status']} "
f"| {len(r['claim_ids'])} | {len(r['contradiction_ids'])} | {caveat[:60]} |")
lines.append(prov_note)
return "\n".join(lines)
+1 -1
View File
@@ -3,4 +3,4 @@
This is the main entry point for the Rune program.
"""
__version__ = "0.59.0"
__version__ = "0.59.5"
+1 -1
View File
@@ -252,7 +252,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.59.0"' in rune_version,
'__version__ = "0.59.5"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+2 -2
View File
@@ -7,7 +7,7 @@ mode system, subject header, shared memory and identity.
This is not an interface. This is how I see, speak, and interact with the world.
Version: v0.59.0
Version: v0.59.5
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1923,7 +1923,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.59.0 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.59.5 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+228
View File
@@ -0,0 +1,228 @@
"""Tests: Source Ledger deterministic renderer (#282 / PROV-1).
Invariants:
- READ impossible without a successful machine receipt
- BLOCKED/FAILED appear but back zero accepted claims
- deterministic receipt_id (same inputs → same ID)
- runner/model from persisted fields only; UNKNOWN if absent
- requested_url + resolved_url both retained
- legacy nesting labeled; explicit IDs used when present
- render is a pure function (same input → same output)
"""
from __future__ import annotations
import importlib.util
import json
import sys
import pytest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
spec = importlib.util.spec_from_file_location("source_ledger", ROOT / "scripts" / "source_ledger.py")
sl = importlib.util.module_from_spec(spec)
sys.modules["source_ledger"] = sl
spec.loader.exec_module(sl)
def _artifact(**overrides):
a = {
"discovery": {
"queries": [{"id": "Q1", "query": "q", "receipt": "ok"}],
"sources_discovered": [
{"id": "O1", "url": "https://example.org/read-me", "class": "official docs"},
{"id": "O2", "url": "https://blocked.example/x", "class": "forum",
"note": "BLOCKED: bot challenge"},
{"id": "O3", "url": "https://failed.example/y", "class": "blog"},
],
"selected_for_reading": ["O1"],
},
"reading": {"sources_read": [{"id": "O1", "url": "https://example.org/read-me",
"claims": [{"id": "E1", "claim": "x"}]}]},
"tool_receipts": [
{"kind": "SOURCE_READ", "tool": "webfetch", "url": "https://example.org/read-me", "ts": 100},
{"kind": "TOOL_FAILED", "tool": "webfetch", "url": "https://failed.example/y", "ts": 101},
],
"contradictory_evidence": [
{"id": "C1", "topic": "t", "resolution": "r",
"contradicts_source_ids": ["O1"]},
],
}
a.update(overrides)
return a
def test_read_requires_machine_receipt():
"""THE safety invariant: READ only with a SOURCE_READ receipt."""
records = sl.build_records(_artifact())
by_id = {r["source_id"]: r for r in records}
assert by_id["O1"]["status"] == "READ"
assert by_id["O1"]["receipt_id"].startswith("R-")
assert by_id["O1"]["supports_evidence"] is True
def test_artifact_claimed_read_without_receipt_downgrades():
"""A source claiming read with no matching machine receipt → FAILED,
never silently READ."""
a = _artifact(tool_receipts=[]) # no receipts at all
# Artifact-asserts-read lives on the discovery source entry (real shape)
a["discovery"]["sources_discovered"][0]["read_receipt"] = True
records = sl.build_records(a)
by_id = {r["source_id"]: r for r in records}
assert by_id["O1"]["status"] == "FAILED"
assert by_id["O1"]["supports_evidence"] is False
def test_blocked_and_failed_back_zero_claims():
records = sl.build_records(_artifact())
by_id = {r["source_id"]: r for r in records}
assert by_id["O2"]["status"] == "NOT_SELECTED"
assert by_id["O3"]["status"] == "FAILED"
for r in records:
if r["status"] != "READ":
assert r["claim_ids"] == []
assert r["supports_evidence"] is False
def test_receipt_id_deterministic():
r1 = sl._receipt_id("OUTWARD", "webfetch", "https://x.example/", 100, 0)
r2 = sl._receipt_id("OUTWARD", "webfetch", "https://x.example/", 100, 0)
r3 = sl._receipt_id("OUTWARD", "webfetch", "https://x.example/", 100, 1)
assert r1 == r2 and r1 != r3
def test_requested_and_resolved_urls_retained():
a = _artifact()
a["discovery"]["sources_discovered"][0]["resolved_url"] = "https://resolved.example/read-me"
records = sl.build_records(a)
by_id = {r["source_id"]: r for r in records}
assert by_id["O1"]["requested_url"] == "https://example.org/read-me"
assert by_id["O1"]["resolved_url"] == "https://resolved.example/read-me"
assert by_id["O1"]["domain"] == "resolved.example"
def test_claim_and_contradiction_ids():
records = sl.build_records(_artifact())
by_id = {r["source_id"]: r for r in records}
assert by_id["O1"]["claim_ids"] == ["E1"]
assert by_id["O1"]["contradiction_ids"] == ["C1"]
def test_legacy_nesting_flagged_in_render():
md = sl.render_markdown(sl.build_records(_artifact()), _artifact(),
{"runner_name": "r", "runner_hostname": "h"},
"test-model", "abc123", legacy_claims=True)
assert "legacy artifact" in md or "pre-#282" in md
def test_render_pure_function():
a = _artifact()
recs = sl.build_records(a)
md1 = sl.render_markdown(recs, a, {"runner_name": "rune-desktop",
"runner_hostname": "nick-desktop"},
"glm-5.3-flash", "hash1")
md2 = sl.render_markdown(recs, a, {"runner_name": "rune-desktop",
"runner_hostname": "nick-desktop"},
"glm-5.3-flash", "hash1")
assert md1 == md2
def test_unknown_metadata_not_reconstructed():
md = sl.render_markdown(sl.build_records(_artifact()), _artifact(),
None, "UNKNOWN", "hash1")
assert "UNKNOWN" in md
# runner None → UNKNOWN, never guessed
assert "rune-desktop" not in md
def test_empty_artifact_raises_visibly():
try:
sl.build_records({})
raise AssertionError("should have raised")
except ValueError:
pass
def test_full_render_smoke():
md = sl.render_markdown(sl.build_records(_artifact()), _artifact(),
{"runner_name": "rune-desktop",
"runner_hostname": "nick-desktop"},
"glm-5.3-flash", "abc123")
assert "OUTWARD — ACCEPTED" in md
assert "SOURCE LEDGER" in md
assert "READ" in md and "NOT_SELECTED" in md and "FAILED" in md
assert "backs ZERO accepted claims" in md
# ── Idempotent publication (#282 constraint) ────────────────────────────────
def _publish_mod():
spec = importlib.util.spec_from_file_location(
"publish_source_ledger", ROOT / "scripts" / "publish_source_ledger.py")
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
return m
def test_idempotent_publish_updates_not_duplicates(tmp_path, monkeypatch):
"""Same artifact hash → second publish UPDATES the existing comment."""
plm = _publish_mod()
a = _artifact()
(tmp_path / "outward.json").write_text(json.dumps(a))
calls = {"posted": [], "patched": []}
class FakeResp:
def __init__(self, payload, status=200):
self._p, self.status = payload, status
def read(self):
return json.dumps(self._p).encode()
def fake_api(method, path, payload=None):
if path.endswith("/comments?limit=50"):
return 200, [{"id": 1, "body": "<!-- source-ledger hash=abc123 -->\nold body"}]
if method == "PATCH" and "/comments/" in path:
calls["patched"].append((path, payload["body"][:40]))
return 200, {"id": 1}
if method == "POST" and path.endswith("/comments"):
calls["posted"].append(payload["body"][:40])
return 201, {"id": 99}
return 200, {}
monkeypatch.setattr(plm, "_api", fake_api)
r1 = plm.publish(999, str(tmp_path), "abc123")
assert r1["action"] == "updated" and r1["comment_id"] == 1
assert not calls["posted"], "must UPDATE, not create a duplicate"
r2 = plm.publish(999, str(tmp_path), "abc123")
assert r2["action"] == "updated"
assert len(calls["posted"]) == 0
def test_new_hash_creates_new_comment(tmp_path, monkeypatch):
plm = _publish_mod()
(tmp_path / "outward.json").write_text(json.dumps(_artifact()))
posted = []
def fake_api(method, path, payload=None):
if path.endswith("/comments?limit=50"):
return 200, []
if method == "POST" and path.endswith("/comments"):
posted.append(payload["body"][:50])
return 201, {"id": 7}
return 200, {}
import unittest.mock as mock
monkeypatch.setattr(plm, "_api", fake_api)
r = plm.publish(999, str(tmp_path), "newhash")
assert r["action"] == "created" and len(posted) == 1
def test_publish_failure_raises_visibly(tmp_path, monkeypatch):
"""Provenance publication failure must be VISIBLE, not swallowed."""
plm = _publish_mod()
(tmp_path / "outward.json").write_text(json.dumps(_artifact()))
def fake_api(method, path, payload=None):
if path.endswith("/comments?limit=50"):
return 500, "server error"
return 200, {}
import unittest.mock as mock
monkeypatch.setattr(plm, "_api", fake_api)
with pytest.raises(RuntimeError, match="cannot list comments"):
plm.publish(999, str(tmp_path), "hash1")