# Viewer Evidence Recording
## Purpose
Rendro's support matrix (`priv/support_matrix.json`) is the public index of viewer compatibility per surface. Each promoted cell points at a structured evidence file under `priv/viewer_evidence/<surface>/<viewer>.md` — observation facts, behavior notes, and reproducibility metadata in frontmatter; promotion state (`evidence`, `recorded_at`, `viewer_kind`) lives on the matrix only.
Treat this like MDN BCD or Can I Use: the matrix answers "what does Rendro claim?"; the evidence file answers "what did we observe, on which fixture, with which viewer version?"
## Prerequisites
Recording requires a **full repo checkout**. The Hex package ships `guides/` but omits `priv/support_matrix.json`, `priv/schemas/`, and `priv/viewer_evidence/`. HexDocs is read-only documentation for this recipe — you cannot record promotions from the published package alone.
You need:
- Elixir/Mix from the repo root
- The target viewer installed on your workstation (macOS Preview, Adobe Acrobat Reader, etc.)
- Permission to edit `priv/support_matrix.json`, evidence files, and (when promoting) `guides/api_stability.md` and `CHANGELOG.md`
## Status vocabulary
| Matrix `status` | Meaning | Evidence file |
|-----------------|---------|---------------|
| `unverified` | Recording obligation not satisfied | Forbidden |
| `supported` | Proof-backed support; promotion keys required on the matrix row | Required at `evidence:` path |
| `explicit_deferral` | Honest "no" with named reason | Forbidden — matrix-only `evidence_deferred` |
Promotion keys on `supported` rows: `evidence`, `recorded_at`, `viewer_kind` (`manual`, `pdfium-cli`, or `pdfjs-dist`). Do not put `status`, `viewer_kind`, or promotion keys in evidence frontmatter.
### Automated path (Linux CI — pdfium-cli, pdfinfo, qpdf)
When `pdfium-cli`, `pdfinfo`, and `qpdf` are on PATH, record legacy rows without GUI viewers:
```bash
mix rendro.viewer_evidence record forms chrome_pdfium \
--fixture test/fixtures/forms_support_fixture.pdf \
--recorded-by ci:viewer-evidence-live-proof
mix rendro.viewer_evidence record forms apple_preview \
--fixture test/fixtures/forms_support_fixture.pdf \
--recorded-by ci:viewer-evidence-live-proof
mix rendro.viewer_evidence record embedded_files adobe_acrobat_reader \
--fixture test/fixtures/embedded_artifact_support_fixture.pdf \
--recorded-by ci:viewer-evidence-live-proof
mix rendro.viewer_evidence record links adobe_acrobat_reader \
--fixture test/fixtures/embedded_artifact_support_fixture.pdf \
--recorded-by ci:viewer-evidence-live-proof
mix rendro.viewer_evidence record links apple_preview \
--fixture test/fixtures/embedded_artifact_support_fixture.pdf \
--recorded-by ci:viewer-evidence-live-proof
mix rendro.viewer_evidence record protection apple_preview \
--fixture test/fixtures/protection_support_fixture.pdf \
--recorded-by ci:viewer-evidence-live-proof
```
Trust-sensitive surfaces (signature widgets, signing preparation, signed artifacts, long-lived signed artifacts) use the same pdfium-cli / pdfsig / pyhanko structural-proxy lane:
```bash
mix rendro.viewer_evidence record signature_widget chrome_pdfium \
--recorded-by ci:viewer-evidence-live-proof
mix rendro.viewer_evidence record signature_widget adobe_acrobat_reader \
--recorded-by ci:viewer-evidence-live-proof
mix rendro.viewer_evidence record signature_widget apple_preview \
--recorded-by ci:viewer-evidence-live-proof
mix rendro.viewer_evidence record signing_preparation adobe_acrobat_reader \
--recorded-by ci:viewer-evidence-live-proof
mix rendro.viewer_evidence record signed_artifact chrome_pdfium \
--recorded-by ci:viewer-evidence-live-proof
mix rendro.viewer_evidence record signed_artifact adobe_acrobat_reader \
--recorded-by ci:viewer-evidence-live-proof
mix rendro.viewer_evidence record long_lived_signed_artifact adobe_acrobat_reader \
--recorded-by ci:viewer-evidence-live-proof
```
Set matrix `viewer_kind` to `"pdfium-cli"`. CI validates committed fixtures via:
```bash
mix test --include live_pdf_tools \
test/rendro/adapters/forms_viewer_evidence_live_test.exs \
test/rendro/adapters/embedded_files_viewer_evidence_live_test.exs \
test/rendro/adapters/links_viewer_evidence_live_test.exs \
test/rendro/adapters/protection_viewer_evidence_live_test.exs \
test/rendro/adapters/signature_widget_viewer_evidence_live_test.exs \
test/rendro/adapters/signed_artifact_viewer_evidence_live_test.exs \
test/rendro/adapters/trust_sensitive_viewer_evidence_live_test.exs
```
`trust_sensitive_viewer_evidence_live_test.exs` records all structural-proxy evidence files in one lane. Structural automation proxies do not validate Apple Preview or Adobe Acrobat GUI behavior.
### Manual path (Preview / Acrobat)
Run these steps in order. Each step ends with an observable check.
### 1. Find backlog cells
```bash
mix rendro.viewer_evidence missing
```
**Check:** Exit code **0** when no unverified cells remain. Exit code **1** when unverified cells exist. Stdout lists `surface`, `viewer`, and `status` for each backlog cell. Pick your target cell from the table.
### 2. Confirm behavior IDs
Open `priv/support_matrix.json` and read the `proof[]` array for your target viewer row (for example `forms.viewers.apple_preview`).
**Check:** You have a fixed list of behavior IDs (`open`, `default_state_visible`, etc.) before opening the viewer.
### 3. Prepare the fixture
For **forms**, from repo root:
```bash
mix run -e 'Rendro.Test.FormSupportFixture.write_fixture("test/fixtures/forms_support_fixture.pdf")'
```
Other surfaces: see Appendix A.
**Check:** The fixture path exists on disk and is committed (or staged) before manual observation.
### 4. Manual checklist in the viewer
Open the fixture in the target viewer. For each `proof[]` behavior ID, record pass/fail and a substantive note (widget names, visible state, what you toggled, save path behavior). Read `viewer_version` from the viewer's About dialog and `platform` from the OS at observation time — never copy from another matrix row.
**Check:** You can answer pass/fail for every `proof[]` ID without guessing.
### 5. Create the evidence file
Copy the template and fill frontmatter (field semantics in the skeleton below — use your observation values, not these placeholders):
```bash
cp priv/viewer_evidence/_template.md priv/viewer_evidence/<surface>/<viewer>.md
```
Example path for forms × Apple Preview: `priv/viewer_evidence/forms/apple_preview.md`.
**Skeleton frontmatter** (field names: `schema_version`, `surface`, `viewer`, `viewer_version`, `platform`, `recorded_at`, `fixture`, `behaviors[]` with `behavior` / `result` / `note` per `proof[]` ID — use observation values in the file, not in this guide).
Add a short body: provenance, fixture regen command, and boundary notes (Appendix F).
**Check:** File path matches `priv/viewer_evidence/<surface>/<viewer>.md` and frontmatter `surface`/`viewer` match the matrix mapping.
### 6. Validate structure
```bash
mix rendro.viewer_evidence validate
```
**Check:** Exit code **0**. Fix any Tier-A errors (schema, lint, orphan scan) before promoting. Tier-B promotion-complete validation passes for all `supported` rows.
### 7. Promote the matrix row
Add to the `supported` viewer object in `priv/support_matrix.json`:
- `"evidence": "priv/viewer_evidence/<surface>/<viewer>.md"`
- `"recorded_at": "YYYY-MM-DD"` — **must equal** `recorded_at` in evidence frontmatter
- `"viewer_kind": "manual"` (or `"pdfium-cli"` / `"pdfjs-dist"` for automated observers)
Do not change `status` or `proof[]` for re-attestation. Update `guides/api_stability.md` and `CHANGELOG.md` when closing the public contract (see plan 69-03).
**Check:** `recorded_at` matches evidence frontmatter.
### 8. Verify listing and docs-contract
```bash
mix rendro.viewer_evidence list
mix test test/docs_contract/viewer_evidence_claims_test.exs
```
**Check:** Your cell appears in the table without a "legacy: missing evidence" note. Docs-contract lane passes.
---
## Worked example — forms × chrome_pdfium
The canonical observations for the first CI-automated promoted cell live only in the repository evidence file:
[priv/viewer_evidence/forms/chrome_pdfium.md](https://github.com/szTheory/rendro/blob/main/priv/viewer_evidence/forms/chrome_pdfium.md)
The guide shows structure and commands; **the canonical file wins** for version strings, platform, behavior notes, and dates.
Apple Preview consolidated evidence (`priv/viewer_evidence/forms/apple_preview.md`) uses the same pdfium-cli structural proxy lane — GUI Preview is not re-run in CI.
Copy source for new cells: `priv/viewer_evidence/_template.md`.
## Appendix A — Per-surface manual checklists
### Forms (`forms`)
Representative fixture: `test/fixtures/forms_support_fixture.pdf` (widgets: `email` prefilled `jon@example.test`, `terms` checkbox, `contact_email` / `contact_phone` radio group).
| Behavior ID | Pass criteria (Apple Preview + forms fixture) |
|-------------|-----------------------------------------------|
| `open` | PDF opens without error dialog |
| `default_state_visible` | Email shows prefilled value; terms checked; contact email radio selected |
| `edit_or_toggle` | Change email text; toggle terms; switch radio to phone |
| `save` | Save As to a new path; reopen; edited state persists |
Other surfaces use the same automated record commands when `pdfium-cli`, `pdfinfo`, and `qpdf` are available:
| Surface | Fixture | Regeneration |
|---------|---------|--------------|
| `embedded_files` / `links` | `test/fixtures/embedded_artifact_support_fixture.pdf` | `MIX_ENV=test mix run -e 'Rendro.Test.EmbeddedArtifactSupportFixture.write_fixture("test/fixtures/embedded_artifact_support_fixture.pdf")'` |
| `protection` | `test/fixtures/protection_support_fixture.pdf` | `mix run scripts/protected_viewer_proof_fixture.exs --output test/fixtures/protection_support_fixture.pdf` |
Protection regen produces **new bytes** — re-run the structural proof lane after regeneration.
## Appendix B — Explicit deferral discipline
Use `explicit_deferral` when a viewer cannot satisfy a behavior and you can name **why** in ≥40 characters. Do not create an evidence file. Do not use vague deferral vocabulary: `TBD`, `not yet`, `deferred for later`, or empty strings — CI lint rejects them.
### Template: UPSTREAM_ISSUE
Use when promotion is blocked by missing upstream viewer capability (not a Rendro authoring gap).
```json
"pdfjs": {
"status": "explicit_deferral",
"evidence_deferred": "PDF.js does not implement AcroForm signature widget editing or unsigned placeholder rendering per mozilla/pdf.js#4202; promotion requires upstream signature-field support."
}
```
Example surfaces: `forms.viewers.pdfjs`, `forms.signature_widget_viewers.pdfjs`, `signing_preparation.viewers.pdfjs`.
### Template: NO_SIG_VALIDATION
Use when the viewer cannot validate `/Sig` digital signatures or signed-artifact integrity UI.
```json
"apple_preview": {
"status": "explicit_deferral",
"evidence_deferred": "Apple Preview does not validate /Sig digital signatures and append-save invalidates signature dictionaries; signed-artifact viewer promotion requires Acrobat or pdfium-cli structural lanes."
}
```
Example surfaces: `signing.viewers.apple_preview`, `signing.viewers.pdfjs`.
### Template: NO_LTV_INDICATORS
Use when long-term-validation timestamp, revocation, or expiry indicators are absent.
```json
"chrome_pdfium": {
"status": "explicit_deferral",
"evidence_deferred": "pdfium-cli structural open and form extraction do not expose long-term-validation timestamp, revocation, or expiry indicators; LTV posture remains Acrobat-only for viewer promotion."
}
```
Example surfaces: `signing.long_lived.viewers.{apple_preview,chrome_pdfium,pdfjs}`.
### Template: SURFACE_EQUIVALENCE (supported inheritance, not deferral)
Use when two surfaces share identical viewer behavior — record once on the primary surface and inherit pointers on the secondary surface.
```json
"apple_preview": {
"status": "supported",
"proof": ["prepared_artifact_opens_cleanly", "widget_renders_as_unsigned_placeholder", "viewer_does_not_silently_re_sign_or_corrupt", "byte_range_layout_intact_after_save_as"],
"evidence": "priv/viewer_evidence/signature_widget/apple_preview.md",
"recorded_at": "2026-05-29",
"viewer_kind": "pdfium-cli"
}
```
Applies to `signing_preparation` non-Acrobat rows inheriting `signature_widget` evidence (D-15). Adobe Acrobat Reader requires independent `signing_preparation` evidence because byte-range layout is viewer-discriminable.
### Hypothetical teaching example (non-production)
*Do not add orphan rows — teaching contrast only.*
`signed_artifact` × `apple_preview` deferral JSON:
```json
"apple_preview": {
"status": "explicit_deferral",
"evidence_deferred": "Apple Preview renders signature appearance but does not implement /Sig cryptographic validation as of Preview 11.0 on macOS 15 — integrity UI absent."
}
```
### Contrast table
| Status | Matrix keys | Evidence file |
|--------|-------------|---------------|
| `supported` | `evidence`, `recorded_at`, `viewer_kind`, `proof[]` | Required |
| `explicit_deferral` | `evidence_deferred` only | Forbidden |
| `unverified` | `proof[]` optional | Forbidden |
## Appendix C — Frontmatter schema guardrails
- **Byte budget:** 65_536 bytes per evidence file (`byte_size/1` on disk).
- **Forbidden frontmatter keys:** `status`, `viewer_kind`, and other promotion fields — matrix only.
- **Fixture:** Provide `fixture` (repo-relative path) **or** `fixture_sha256`; prefer committed path for reproducibility (`test/fixtures/...`).
- **Behaviors:** Each `behaviors[].behavior` must be a valid ID for the surface; include every `proof[]` entry from the matrix row even though the validator currently allows subsets.
- **Lint:** No embedded images, PEM blocks, home-directory paths (`/Users/...`), or secrets in body or notes.
Schema: `priv/schemas/viewer_evidence.schema.json`. Template: `priv/viewer_evidence/_template.md`.
## Appendix D — Mix task reference
Module: `Mix.Tasks.Rendro.ViewerEvidence`
| Subcommand | Exit code | Purpose |
|------------|-----------|---------|
| `list` | 0 | Summary counts + table (`surface`, `viewer`, `status`, `notes`) |
| `missing` | 1 if any `unverified`; 0 if none | Backlog filter |
| `validate` | 1 on Tier-A errors; 0 with legacy/staleness warnings only | Schema + evidence files + orphan scan |
| `validate --strict` | 1 on Tier-A errors **or** stale `recorded_at` (>180 days); 0 when current | Same as `validate` plus staleness gate — **not** merge-blocking CI (D-09/D-10) |
Add `--json` for machine-readable stdout.
Full API and CI notes: see `Mix.Tasks.Rendro.ViewerEvidence` moduledoc (`mix help rendro.viewer_evidence`).
## Appendix E — CI and docs-contract troubleshooting
| Symptom | Likely cause | Action |
|---------|--------------|--------|
| `mix docs.contract` fails lane 8 | Orphan evidence, schema error, bad frontmatter | Run `mix rendro.viewer_evidence validate`; fix paths and lint |
| Promotion-complete test fails in fixtures only | Tier-B fixture matrix missing `evidence` | Production tier-A still passes until promotion; add keys when recording |
| `forms_claims_test` fails after `api_stability` edit | Broke Adobe `unverified` wording or refute guards | Preserve narrow claims; see the implementation notes |
Docs-contract proves **structural** alignment (matrix JSON, evidence schema, path references, lint). The `viewer-evidence-live-proof` GitHub Actions lane runs pdfium-cli, pdfsig, pyhanko, and poppler structural-proxy proofs that regenerate committed evidence files — no GUI viewer sessions required for trust-sensitive closures.
## Appendix F — Overclaim boundaries
- **Poppler / `pdfinfo` structural proof ≠ viewer proof.** Passing structural tests does not promote a viewer row.
- **One cell ≠ other surfaces or viewers.** Promoting `forms × apple_preview` does not promote Acrobat, PDFium, PDF.js, protection, links, or signature surfaces.
- **Re-attestation ≠ net-new support.** Legacy `supported` re-homes keep `status: supported`; refresh `recorded_at` to the spot-check date; cite older attestation dates in body prose only.
- **Signing recipe ≠ viewer interop.** `Rendro.Sign` integrity validation is a separate lane from interactive form evidence.
Do not promote if manual checks failed, notes are template stubs, or `recorded_at` does not match the matrix row.