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 togetherThe 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#
| Field | Why it matters |
|---|---|
video.sha256 | Identifies exactly which file was judged. |
manifestHash | The compose manifest the checks ran against. |
contactSheetSha256 | The sheet cannot be swapped after the fact. |
bundleHashes | Hashes of events.json and the frame index. |
provenance.specHash | Which spec produced it. |
provenance.appUrl | What was filmed. |
capturedAt / verifiedAt | When, in UTC. |
checkSuiteVersion | Which suite judged it, e.g. playhead/checks@4 — so an older verdict is not silently compared against newer rules. |
playheadVersion / host | Which build, on what platform. |
coverage | How much was actually verified, not just whether it passed. |
Signing#
Ed25519 — third-party verifiable#
playhead keygen -o playhead-signing.pemVerdicts 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 outSimpler, 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/captureRecomputes 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.
--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 outAttaches 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.