Evidence & signing

A video is a claim about a product. These are the artifacts that let someone else check the claim without taking your word for it.

What a render leaves behind#

out/
├── capture/
│   ├── frames/            2× frames on a timestamp index
│   ├── events.json        every action: kind, label, bounding box, timings
│   ├── manifest.json      frame index, viewport, durations
│   └── spec.resolved.json the spec as actually executed
├── narration/             synthesized lines, cached by content
├── out.mp4                the finished video
├── bug-report.md          bug-repro only: pasteable write-up of the issue
├── compose-manifest.json  everything the compositor rendered
├── verify/
│   ├── verdict.json       the verdict, coverage, provenance and hashes
│   └── contact-sheet.png  key moments as an annotated grid
└── evidence/index.html    a relocatable page tying it all together

The evidence page#

playhead evidence out/

A single self-contained HTML page with the video, the step list and the verdict. render writes one automatically; this command rebuilds it. It is relocatable, so it works from a ticket attachment, an S3 bucket or a gh-pages branch — with host-pages: true the GitHub Action publishes it so the pull-request comment is a playable URL rather than a zip. CI setup →

Determinism#

Compose is a pure function of the bundle and the spec: no network, no browser. Same inputs, same bytes — and you can assert it:

playhead compose out/capture --expect-sha 4623a8d724593b5d…

Capture is the stage that can vary, because your app can. Pin it down with app.environment (timezone, locale, fixedTime, colorScheme, reducedMotion) and app.network.block for third-party noise. Spec reference →

What the verdict records#

FieldWhy it matters
video.sha256Identifies exactly which file was judged.
manifestHashThe compose manifest the checks ran against.
contactSheetSha256The sheet cannot be swapped after the fact.
bundleHashesHashes of events.json and the frame index.
provenance.specHashWhich spec produced it.
provenance.appUrlWhat was filmed.
capturedAt / verifiedAtWhen, in UTC.
checkSuiteVersionWhich suite judged it, e.g. playhead/checks@4 — so an older verdict is not silently compared against newer rules.
playheadVersion / hostWhich build, on what platform.
coverageHow much was actually verified, not just whether it passed.

Signing#

Ed25519 — third-party verifiable#

playhead keygen -o playhead-signing.pem

Verdicts are then signed with the private key, and consumers verify against the public key id they have pinned. This is the stronger option: someone who does not trust your CI can still check the verdict.

HMAC — a shared secret#

PLAYHEAD_SIGNING_KEY=… playhead render specs/tour.yaml -o out

Simpler, symmetric, and enough when producer and consumer are the same organisation. The GitHub Action exposes it as the signing-key input.

Attesting#

playhead attest out/verify/verdict.json --bundle out/capture

Recomputes every hash the verdict claims — video, manifest, contact sheet, and with --bundle, the bytes of every frame — then checks the signature. This is what makes a verdict evidence rather than a log line: if a frame were edited after the fact, the recomputed bundle hash would not match.

Unsigned verdicts fail attestation by default. Use --allow-unsigned when you only want the hash checks — for instance while developing locally, before you have set up a key.

Reporting into a tracker#

playhead jira report PROJ-482 -o out

Attaches the video (or the failure clip), the verdict, the contact sheet and the console log to an existing issue, then posts a verdict comment. Playhead never creates issues, and credentials come from your environment.

Privacy#

Frames, event logs, narration audio and verdicts are written to your disk and are never uploaded. The optional --vision review sends the contact sheet to a local Ollama model. The only component that can reach a network is playhead author, when you give it a GROQ_API_KEY — and it falls back to local Ollama when you do not.