# 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; // 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.
You change the duration inside the stored case facts and replay. What do the two lines say? `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.
You change only the display `input` block. Does either check notice? 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.
Which download do you keep to replay the calculation later? 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.
## 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/)