# Audit Row Reader

Source: https://dinand.com/experiments/audit-row-reader/app/
Author: Dinand Tinholt

> Reads decision-head audit logs in the browser, checks their hash chain across rotated files and turns them into a readable trail.

Lab tool · reads logs in your browser

# Audit Row Reader

Drop one or more decision-head audit logs here and read them as a trail. Each line shows the answer to one question and how sure the reading was. Rotated files verify as one series, and the page names the first row, or the first file boundary, that stops matching its seal.

Runs in this browser tab. The page has no server, and the logs you drop or paste never leave this machine. The fonts are its one network request. Your screen settings stay in this browser.

## The log

nothing loaded

Drop audit.jsonl files here with their .chain seals. Several rotated files read as one series, ordered by their first timestamp.

Choose logs Choose seals

A generated sample log; no deployed system wrote it. tools/make_sample.py writes it in the documented row format.

Or paste JSONL, one row per line

## Hash chain

### File boundaries

How the chain is checked

decision-head writes no chain today. Its audit-log page says rows are not chained or signed, and chained row hashes sit on the README's planned list. This reader checks a chain in either of two forms.

A seal file holds a running hash for every row: h[i] = sha256(h[i-1] + "\n" + row i). A first file starts from 64 zeros. A rotated file starts from the previous file's last hash, written on the seal's # start: line, so the files check as one series and a missing or reordered file shows at its boundary. This page or tools/seal.py writes seals.

If the rows carry a prev_hash field, the form the 2026-10-06 security review proposed (SHA-256 of the previous line), the reader checks every link, across files too. An edit to row k shows up at row k+1. An edit to the very last row cannot show up at all.

With neither, the reader checks that each line parses as a JSON object and says so.

## Look at these

Band low Band high Identical run, rows Same means Shift screen line Answer-mix line

## Cases

rows grouped by request_hash

A case is every answered row that shares a request_hash: the same state and questions sent to the same model. A trailing -deliberate is set aside so fast and deliberate answers to one case sit together. Refused requests hash without the model id, so they stay out of this view.

Show

## Calibration by question

yes/no questions, fast and learned rows

The log holds no outcomes. This table shows how a question's answers spread across probability bins, so it cannot tell whether a 0.7 comes true seven times in ten. Read it as a distribution, and use the before and after columns to see what a calibration change did to that spread.

Question

## Charts

hover a mark for its numbers

Question Rolling window, rows

*

### Calibration drift

rolling mean of the top probability, and the share of answers flagged low_confidence, over the last N answered rows

### Answer mix by day

share of each answer per day, for the chosen question*

### Probability by question

yes/no questions plot P(yes); choice and score questions plot the top option's probability. Count of answered rows per bin of 0.1.

## Trail

Question Band Mode Go to row

The table scrolls down and sideways. Only the rows in view are drawn.

| Row | Time (UTC) | Question | Answer | Probability | Band | Mode | backend · calibration | Chain |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |

```

```

## Export

files are written in this tab and saved by your browser

### Redaction check

Before anything leaves the page, the reader scans every text value and every key in the loaded rows for email addresses, phone numbers, IBANs that pass their checksum, and runs of capitalised words that look like names. It keeps counts and nothing else. The 2026-10-06 security review notes that free text placed in question_id or in option names lands in the log, so those fields are scanned too. The name pattern also catches product and place names.

### Flagged rows as CSV

one line per entry under "Look at these"

### The whole trail as sanitised JSONL

keeps only the fields you tick; unreadable lines are left out; the result no longer matches any seal

Fields to keep

check The export may carry personal data

## Keeping the seal

where the .chain file should live

A seal helps only if the account that writes the log cannot also rewrite the seal. decision-head's 2026-10-06 security review lists the audit log as an asset that "must be complete, attributable and free of secrets and documents". It also says the files the service writes "are operator-controlled but not integrity-checked", and that without a chain "a tampered or truncated log is also undetectable".

Anything running under the service's account can edit both files when they sit side by side. That covers the service, a script, and an agent with shell access. So the seal goes somewhere that account cannot write.

- Another machine or account. A one-way job copies each .chain file there. The log host gets no write access back.
- Append-only or retention-locked storage. decision-head's audit-log page already sends the log there: "ship it to append-only storage if you need that guarantee". Seals can share that store.
- The last hash, written down. A seal's final line commits to every row before it. Paste it into a ticket, an email or a signed note and you can check the whole file later, even if the seal file is lost.
- Rotation. Close a file with tools/seal.py rotate. The new seal starts from the previous file's last hash, so a dropped or reordered file breaks at its boundary.
- Re-seal on purpose only. A fresh seal over an edited log verifies cleanly. A seal written after an edit vouches for the edit.

Quotes from the decision-head repository: docs/security-review-2026-10-06.md (threat model, asset A4 and boundary TB4; finding 8); docs/audit-log.md ("What the audit log does not record"). The storage advice is this build's.

---
Site guide for AI agents: https://dinand.com/llms.txt
