Metadata-Version: 2.4
Name: incident-ledger
Version: 1.1.0
Summary: An append-only, hash-chained SQLite evidence log for LLM systems, with deterministic replay.
Project-URL: Homepage, https://github.com/MaqAnquor/incident-ledger
Project-URL: Repository, https://github.com/MaqAnquor/incident-ledger
Author: Abhijeet Verma
License: MIT
License-File: LICENSE
Keywords: agentic-ai,ai-accountability,audit-log,deterministic-replay,hash-chain,provenance,sqlite,tamper-evident
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown

<div align="center">

# 🔗 incident-ledger

**Turn an AI system's logs into _evidence_.**

An append-only, hash-chained SQLite evidence log for LLM systems — with deterministic replay.
Pure Python standard library · zero dependencies · runs anywhere.

[![PyPI](https://img.shields.io/pypi/v/incident-ledger.svg)](https://pypi.org/project/incident-ledger/)
[![Python](https://img.shields.io/pypi/pyversions/incident-ledger.svg)](https://pypi.org/project/incident-ledger/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21387489.svg)](https://doi.org/10.5281/zenodo.21387489)

</div>

---

When an AI system takes an action — issues a refund, moves money, files a ticket — *"we have logs"* is not the same as having **evidence**. `incident-ledger` turns an inference log into evidence by enforcing three properties in **code, not prose**:

| | Property | Guarantee |
|---|---|---|
| 🕒 | **Contemporaneity** | Every row is written *at inference time*, stamped with the moment it happened. The only mutation is `append` — no `update`, no `delete`. |
| 🔒 | **Tamper-evidence** | Each row's `entry_hash` is a SHA-256 over its content **and the previous row's hash**. Alter or delete any earlier row — even by editing the SQLite file directly — and the chain breaks; `verify()` reports the exact `seq`. |
| 🔁 | **Reproducibility** | A sealed decision reconstructs into a replay bundle (pinned model, seed, context) and reruns — with an honest boundary: exact match at temperature 0, a *pre-registered* tolerance above it, and an explicit `not-replayable` verdict when no seed was captured. |

> This is the minimal reference implementation accompanying the paper *["Implementing Agentic-AI Accountability"](https://doi.org/10.5281/zenodo.21387487)*. It implements the logging / audit-trail control common to the IMDA MGF, EU AI Act, NIST AI RMF, and ISO/IEC 42001 — and it is deliberately small: **every line is meant to be read.**

## How the chain works

```mermaid
graph LR
    G["GENESIS<br/>(0 × 64)"] --> R1["seq 1<br/>H(content₁ + GENESIS)"]
    R1 --> R2["seq 2<br/>H(content₂ + hash₁)"]
    R2 --> R3["seq 3<br/>H(content₃ + hash₂)"]
    R3 --> D["…"]
    style G fill:#e8e8e8,stroke:#888,color:#000
    style R1 fill:#d7ebff,stroke:#3b82f6,color:#000
    style R2 fill:#d7ebff,stroke:#3b82f6,color:#000
    style R3 fill:#d7ebff,stroke:#3b82f6,color:#000
```

Each seal depends on the one before it, so history can only be *appended* — never quietly rewritten.

## Install

```bash
pip install incident-ledger
```

<details>
<summary>Or from source</summary>

```bash
git clone https://github.com/MaqAnquor/incident-ledger
cd incident-ledger
pip install -e .
```
</details>

Requires Python ≥ 3.10. No third-party dependencies.

## Quickstart

```python
from incident_ledger import IncidentLedger

# Open on a file for a durable ledger (":memory:" for tests).
ledger = IncidentLedger("evidence.db")

entry = ledger.append(
    query="Is order #9871244 refundable?",
    answer="Yes — $487 refund issued.",
    confidence=0.91,
    model_version="refund-clf-v3@sha-9c1a2f",   # pin the exact model; never "latest"
    context_snapshot="Policy R-12: refunds within 30 days of delivery.",
    sources=("policy://refunds/R-12", "order://9871244"),
    disclosed_limits=("partial-refund case untested",),
    downstream_action="refund.issue",
    trace={"sampling": {"seed": 482991, "temperature": 0.0}},
)

# The chain is intact...
assert ledger.verify().ok

# ...and tampering is detectable, even done directly against the file.
import sqlite3
raw = sqlite3.connect("evidence.db")
raw.execute("UPDATE ledger SET answer = 'No refund' WHERE seq = 1")
raw.commit(); raw.close()

result = IncidentLedger("evidence.db").verify()
assert not result.ok
print(result.broken_seq, result.reason)   # 1  "altered content at seq 1: seal does not recompute"
```

### Replay a sealed decision

```python
from incident_ledger import replay_from_ledger

# Reconstruct the bundle from a sealed row and rerun it. The attempt itself
# is sealed back into the ledger as a new contemporaneous entry.
outcome = replay_from_ledger(ledger, seq=1)
print(outcome.mode, outcome.reproduced)   # "deterministic" True
```

A row sealed with no captured seed is reported `not-replayable` rather than silently treated as deterministic — a gap you disclose, not one you paper over.

### Independent verification: checkpoints

A hash chain the deployer controls end-to-end is still only tamper-*evident*: an owner who edits one row can recompute every later seal and present a clean chain. A **checkpoint** — a single [RFC 6962](https://www.rfc-editor.org/rfc/rfc6962) Merkle root over the sealed entries — is one short value you can hand to a counterparty or auditor. Once that root is *witnessed outside your control*, you can no longer rewrite any history it covers.

```python
from incident_ledger import verify_inclusion

cp = ledger.checkpoint()          # one root over all entries — publish/witness this
proof = ledger.prove_inclusion(seq=1)   # O(log n) path, not the whole ledger

# A third party checks one entry against the root they witnessed — no ledger copy,
# no cooperation from the deployer at check time.
assert verify_inclusion(ledger.get(1), proof, cp.root)
```

The point, in one line: an owner can rewrite the whole chain so that `verify()` still passes — but the rewritten ledger's checkpoint root will **not** match a root witnessed beforehand.

> **What this does and does not buy.** A root you compute *locally* is still owner-recomputable — by itself that's still tamper-evidence, not independence. This module supplies the *machinery* (a compact digest and O(log n) proofs); reaching true independent verifiability requires a party who is **not** the deployer to hold the checkpoint. The code makes witnessing cheap; it cannot make it unnecessary. No external service, no blockchain, no network — stdlib-only.

## API

| Symbol | Purpose |
|---|---|
| `IncidentLedger(path=":memory:", *, now=time.time)` | The store. `append(...)`, `verify()`, `get(seq)`, `entries()`, `head_hash`, `len()`. |
| `LedgerEntry` | One immutable sealed row; `payload()`, `recompute_hash()`. |
| `VerificationResult` | Verdict of `verify()`: `ok`, `entries_checked`, `broken_seq`, `reason`. |
| `ReplayBundle` | Everything needed to rerun one decision: model version, seed, context, temperature, prompt. |
| `replay(bundle, ...)` / `replay_from_ledger(ledger, seq, ...)` | Rerun a bundle, or reconstruct one from a sealed row and rerun it. |
| `ReplayResult` | `mode` (`deterministic` \| `bounded-variance` \| `pin-mismatch` \| `not-replayable`), `reproduced`, `agreement`. |
| `ledger.checkpoint()` → `Checkpoint` | A witnessable Merkle root over all entries: `n`, `root`, `head_hash`, `created_at`. |
| `ledger.prove_inclusion(seq)` → `InclusionProof` | An O(log n) proof that one entry sits under the checkpoint root. |
| `verify_inclusion(entry, proof, root)` | A third party confirms one entry belongs under a witnessed root — no ledger copy needed. |
| `merkle_root(entry_hashes)` | The RFC 6962 root over a list of seals, for building/checking checkpoints directly. |

## Measured overhead

Tamper-evidence is cheap. On a modern laptop (file-backed SQLite, one synchronous commit per append), over 1,000 appends:

| Metric | Value |
|---|---|
| Mean append latency | **~0.47 ms** (p95 ~0.60 ms) |
| Storage per 1,000 inferences | **~4.0 MB** (payload-dominated) |
| Full-chain `verify()` over 1,000 rows | **~31 ms** |
| Hash-chain overhead vs. a plain-insert baseline | **under 0.1 ms** per append (within run-to-run variance) |

The hash chain adds far less than the SQLite commit already costs. Regenerate the numbers on your own machine:

```bash
python benchmarks/ledger_overhead.py --n 1000 --reps 3
```

> Figures are measured on a single machine, single thread, one payload shape and one durability setting; they do not extrapolate to production write-concurrency or a different retention policy.

## Tests

```bash
pip install pytest
pytest -q
```

## Citing

If you use this software, please cite both the software and the paper — see [CITATION.cff](CITATION.cff).

- **Software:** [`10.5281/zenodo.21387489`](https://doi.org/10.5281/zenodo.21387489)
- **Paper:** [`10.5281/zenodo.21387487`](https://doi.org/10.5281/zenodo.21387487) — *Implementing Agentic-AI Accountability: A Minimal, From-Scratch Reference Stack Mapped to the IMDA MGF, EU AI Act, NIST, and ISO/IEC 42001*

> Both the software and the paper are permanently archived on Zenodo under the DOIs above.

## License

[MIT](LICENSE) © Abhijeet Verma

<div align="center">
<sub>Built to be read, not just run.</sub>
</div>
