Viewer Evidence Recording

Copy Markdown View Source

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 statusMeaningEvidence file
unverifiedRecording obligation not satisfiedForbidden
supportedProof-backed support; promotion keys required on the matrix rowRequired at evidence: path
explicit_deferralHonest "no" with named reasonForbidden — 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:

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:

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:

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

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:

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):

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

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

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

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 IDPass criteria (Apple Preview + forms fixture)
openPDF opens without error dialog
default_state_visibleEmail shows prefilled value; terms checked; contact email radio selected
edit_or_toggleChange email text; toggle terms; switch radio to phone
saveSave 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:

SurfaceFixtureRegeneration
embedded_files / linkstest/fixtures/embedded_artifact_support_fixture.pdfMIX_ENV=test mix run -e 'Rendro.Test.EmbeddedArtifactSupportFixture.write_fixture("test/fixtures/embedded_artifact_support_fixture.pdf")'
protectiontest/fixtures/protection_support_fixture.pdfmix 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).

"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.

"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.

"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.

"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:

"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

StatusMatrix keysEvidence file
supportedevidence, recorded_at, viewer_kind, proof[]Required
explicit_deferralevidence_deferred onlyForbidden
unverifiedproof[] optionalForbidden

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

SubcommandExit codePurpose
list0Summary counts + table (surface, viewer, status, notes)
missing1 if any unverified; 0 if noneBacklog filter
validate1 on Tier-A errors; 0 with legacy/staleness warnings onlySchema + evidence files + orphan scan
validate --strict1 on Tier-A errors or stale recorded_at (>180 days); 0 when currentSame 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

SymptomLikely causeAction
mix docs.contract fails lane 8Orphan evidence, schema error, bad frontmatterRun mix rendro.viewer_evidence validate; fix paths and lint
Promotion-complete test fails in fixtures onlyTier-B fixture matrix missing evidenceProduction tier-A still passes until promotion; add keys when recording
forms_claims_test fails after api_stability editBroke Adobe unverified wording or refute guardsPreserve 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.