Markdown for LLMs
Save and replay: archive a capture, reproduce the answer
The source Markdown for this article. Copy it into your assistant or download it as a text file.
# Save and replay: archive a capture, reproduce the answer
## At a glance
- **Goal:** freeze a computed answer into a file that can be stored,
handed to a reviewer, and replayed later — and tell replaying the
capture apart from merely recomputing the case, including exactly
what the checksum covers and what it does not.
- **You need:** [Show grounds](/build/show-grounds/): the capture
freezes the answer whose grounds that chapter presents.
- **Run:**
```bash
node --import tsx --test test/capture-replay.test.ts
```
- **Files:**
```text
src/capture.ts CAPTURE_FORMAT deadline-app.capture/1
src/replay.ts replayCapture, verifyCaptureIntegrity
src/server.ts POST /api/deadline/replay
src/cli.ts replay command
```
## Save a calculation
Calculate the worked case and write a capture file. The CLI writes a
capture only when you pass `--out`:
```bash
node --import tsx src/cli.ts evaluate --event 2026-03-06 --days 14 --out /tmp/case.capture.json
```
```text
The period ends on 2026-03-20
Computed from the event date and the duration by the pinned canon.
rules: TagesfristEnde
sources: No sources anchored in this canon build — the rule names above carry the provenance.
program: sha256:468e3fe17c6a28371c6ce8258ae7928c496593673d4b73a52c2a9c40cb50ee33
semantic: sha256:90e53b453107dfee9422a5fe24774823430b5c82c3577b9321a0838a6e54c695
result: sha256:ae9a7b19361154c868eb9b28d5ac6395b1d2844053259a1abfd6f75e300de812
capture: /tmp/case.capture.json
```
## Replay the saved case
Hand the file to `replay`:
```bash
node --import tsx src/cli.ts replay /tmp/case.capture.json
```
```text
integrity: ok — stored document bytes match the recorded document sha256 (input, query, versions, and other metadata are not covered)
replay: match — replayed answer matches the captured result hash
expected: sha256:ae9a7b19361154c868eb9b28d5ac6395b1d2844053259a1abfd6f75e300de812
actual: sha256:ae9a7b19361154c868eb9b28d5ac6395b1d2844053259a1abfd6f75e300de812
```
The command exits 0 only when both checks pass.
## Understand the two checks
The two messages report two different checks:
- **`integrity`** checks the stored bytes. It decodes the stored
answer document, hashes it, and compares the hash with the checksum
recorded next to it. It says nothing about the input, the query, or
the versions in the file.
- **`replay`** checks the result. It runs the frozen query against the
frozen case again and compares the fresh `result` hash with the
recorded one. It never reads the stored document bytes.
Replay is not recompute. Recomputing runs the current case through the
current canon and shows today's answer. Replaying takes a frozen
capture, checks the stored document bytes, re-evaluates its frozen
case input, and compares the fresh answer with the frozen result
hash. The comparison is the point: it tells the reviewer whether the
same case still answers the same way.
## What the capture stores
A capture is the whole evaluated case plus the answer document, under
a versioned format name so a future application never misreads an old
file:
```ts
// src/capture.ts
export interface CapturedEvaluation {
format: string;
capturedAt: string;
model: string;
adapter: string;
input: FormInput;
caseInput: CaseInput;
query: AppQuery;
evaluationStatus: string;
truthStatus?: string;
hashes: Record<string, string>;
// sha256 over the raw evaluation-document bytes.
documentSha256: string | null;
// The real document bytes, base64; null when the answer came from a host.
documentBase64: string | null;
documentNote: string | null;
versions: CaptureVersions;
}
```
Two groups of fields make the two checks possible. `documentBase64`
and `documentSha256` feed integrity: `documentSha256` covers the
stored document bytes — the same bytes `documentBase64` carries — and
nothing else. `caseInput`, `query`, and `hashes.result` feed replay:
the frozen case, the frozen question, and the result to compare with.
```ts
// src/replay.ts
// Recompute the sha256 over the stored document bytes and compare it
// with the recorded checksum. This checks the document bytes alone:
// it says nothing about the input, query, versions, or metadata, and
// nothing about who wrote the file (bytes and checksum travel together).
export function verifyCaptureIntegrity(capture: CapturedEvaluation): IntegrityReport {
```
The application keeps the two reports separate:
```ts
// src/replay.ts
export interface IntegrityReport {
ok: boolean;
expectedSha256: string | null;
actualSha256: string | null;
detail: string;
}
export interface ReplayReport {
match: boolean;
expectedHash: string | null;
actualHash: string | null;
evaluationStatus: string;
detail: string;
}
```
## What each check detects
Because `replayCapture` never reads the document bytes, the two checks
fail differently, and the tests pin each cell:
| Edit | Integrity | Replay |
|---|---|---|
| One document byte flipped | fails | skipped |
| Display input only | passes | matches — the edit is not detected |
| Stored case facts | passes | mismatches |
| Stored query | passes | mismatches |
| Recorded result hash | passes | mismatches |
| Version strings | passes | matches — the edit is not detected |
| Document plus its checksum, replaced together | passes | matches — no forgery protection is claimed |
| No document bytes (host answer) | fails | skipped |
"Passes undetected" is not a bug in the table — it is the documented
boundary. The checksum proves the stored bytes match their recorded
checksum; it does not prove who wrote the file, because bytes and
checksum travel together. Version strings are advisory metadata with
no external record to check them against. A reviewer who needs
provenance must compare against a digest kept elsewhere.
## Replay over HTTP and on another machine
The server exposes the same flow over HTTP: `POST /api/deadline/replay`
takes a capture id, verifies the stored file, replays it, and returns
the comparison. Export is an explicit user action in the form too: it
downloads only on its buttons — Save summary for the
`{view, captureId}` answer JSON, Download capture
(`GET /api/deadline/capture?captureId=…`) for the full stored
archive. Only the full archive is replayable.
Calculate in the form (or by POST), download the full archive, then
carry the file to another machine:
```bash
curl -s -X POST localhost:8787/api/deadline/evaluate \
-H 'content-type: application/json' \
-d '{"eventDate":"2026-03-06","durationDays":14,"legalTime":"2026-09-17"}' \
-o /tmp/answer.json
ID=$(node -e "console.log(JSON.parse(require('fs').readFileSync('/tmp/answer.json','utf8')).captureId)")
curl -s "localhost:8787/api/deadline/capture?captureId=$ID" -o /tmp/capture.json
```
Stop the first server. In a second checkout with the pinned
dependencies installed — no access to the first server's store —
replay the carried file:
```bash
node --import tsx src/cli.ts replay /tmp/capture.json
```
```text
integrity: ok — stored document bytes match the recorded document sha256 (input, query, versions, and other metadata are not covered)
replay: match — replayed answer matches the captured result hash
```
The suite pins this portability: the downloaded bytes are copied
to a directory outside the server store and replayed from there,
with integrity ok and replay match.
## Limits and errors
- Comparison stays within one SDK. The TypeScript and Python SDKs
return the same statuses and values, but their canonical bytes and
`result` hashes differ. A capture saved by one SDK is replayed and
compared by the same SDK only; cross-SDK comparison would report a
mismatch on identical outcomes.
- A document-checksum failure rejects the file before any evaluation:
a capture whose bytes do not hash to its `documentSha256` is not
replayed, and no answer is shown from it.
- The format string is checked first. A capture naming another format
is refused with the expected name, not parsed hopefully.
- Captures of host answers carry no document bytes and always fail
integrity with the recorded note. Only local answers are replayable.
## Check your understanding
Run the matrix:
```bash
node --import tsx --test test/capture-replay.test.ts
```
```text
ok 1 - capture round-trips: replay matches the recorded result hash
ok 2 - tampered document bytes fail integrity, replay is skipped
ok 3 - an input-only edit passes undetected
ok 4 - a case edit passes integrity and mismatches replay
ok 5 - a query edit passes integrity and mismatches replay
ok 6 - a versions edit passes undetected
ok 7 - an edited result hash mismatches replay
ok 8 - consistent document-plus-checksum replacement passes both checks
ok 9 - a foreign-model capture is refused, not mixed
ok 10 - a host-style capture without bytes fails integrity, replay skipped
# tests 10
# pass 10
# fail 0
```
Predict before you open each answer.
<details>
<summary>You change the duration inside the stored case facts and replay. What do the two lines say?</summary>
`integrity: ok` — the stored document bytes are untouched. `replay:
MISMATCH` — the edited case computes a different answer, so its
`result` hash no longer matches the recorded one.
</details>
<details>
<summary>You change only the display `input` block. Does either check notice?</summary>
No. Integrity covers only the document bytes, and replay runs the
stored `caseInput`, not the display input. Both pass; this edit is a
documented blind spot.
</details>
<details>
<summary>Which download do you keep to replay the calculation later?</summary>
Download capture — the full stored archive. Save summary holds only
the `{view, captureId}` answer JSON, which has no frozen case and no
document bytes, so it cannot be replayed.
</details>
## Next
- [Change the model](/build/change-model/)
- [Quickstart](/guide/quickstart/) for the single-file starting point
- [Versions and capability matrix](/build/sdk/compatibility/) and [upgrade without losing reproducibility](/build/application/versioning/)