Task — engineering-spec@1

"REW byte-identical payslip PDF render — Tectonic + pinned fonts produces deterministic PDF bytes for verification"

draftTASK-REW-006
module rew · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-REW-005 · blocks none

§1 — Description (BCP-14 normative)

The REW service MUST ship payslip PDF render at services/rew/src/pdf/ deterministic via Tectonic + pinned fonts + sha256 verification, 3 memory audit kinds.

  1. MUST validate pdf_render_status against closed enum per DEC-2201.
  1. MUST render at tectonic_renderer.rs::render(payslip_row, template) per DEC-2200:
  1. MUST verify at verifier.rs::verify(pdf_bytes) per DEC-2203:
  1. MUST store in TASK-DOC-001 per DEC-2202 with sha256 metadata.
  1. MUST define table at migration 0006: ``sql CREATE TABLE rew_payslip_pdfs ( pdf_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, payslip_id UUID NOT NULL REFERENCES rew_payslip_rows(payslip_id), doc_id UUID NOT NULL, -- TASK-DOC-001 ref sha256 CHAR(64) NOT NULL, status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued','rendering','rendered','verification_failed','failed')), rendered_at TIMESTAMPTZ, verified_at TIMESTAMPTZ, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (payslip_id) ); ALTER TABLE rew_payslip_pdfs ENABLE ROW LEVEL SECURITY; CREATE POLICY pdfs_rls ON rew_payslip_pdfs USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON rew_payslip_pdfs FROM cyberos_app; GRANT UPDATE (status, doc_id, sha256, rendered_at, verified_at) ON rew_payslip_pdfs TO cyberos_app; ``
  1. MUST expose endpoints: ``text POST /v1/rew/payslips/{id}/render (CFO trigger) GET /v1/rew/payslips/{id}/pdf (download for member) POST /v1/rew/payslips/{id}/verify (CFO re-verify) ``
  1. MUST emit 3 memory audit kinds per DEC-2204. PII per TASK-MEMORY-111: sha256 in chain (not PII); content never in chain.
  1. MUST thread trace_id from render → verify → audit.
  1. MUST NOT use non-pinned fonts per DEC-2200.
  1. MUST NOT bypass verification per DEC-2203.

§2 — Why this design

Why Tectonic (DEC-2200)? LaTeX deterministic when fonts pinned; Tectonic specifically removes non-deterministic timestamps from PDF.

Why pinned fonts (DEC-2200)? Font rendering varies by version; different bytes = different sha256 = audit fail.

Why sha256 verification (DEC-2203)? Member challenges payslip → CFO can prove same input produces same PDF.


§3 — API contract

POST /v1/rew/payslips/{id}/render
GET  /v1/rew/payslips/{id}/pdf

Sample status:

{
  "pdf_id": "uuid",
  "payslip_id": "uuid",
  "sha256": "abc123...",
  "status": "rendered",
  "doc_id": "uuid-doc-ref"
}

§4 — Acceptance criteria

  1. pdf_render_status enum cardinality 5. 2. Tectonic deterministic. 3. Fonts pinned in template. 4. SHA256 verified post-render. 5. Mismatch → sev-1 + status=verification_failed. 6. TASK-DOC-001 storage. 7. 3 memory audit kinds emitted. 8. PII: content never in chain; sha256 ok. 9. RLS denies cross-tenant. 10. CFO-only render trigger. 11. Member can download own payslip. 12. Trace_id preserved. 13. UNIQUE(payslip_id). 14. Append-only via REVOKE except status cols. 15. Replay produces byte-identical. 16. Template versioned. 17. Render perf < 5s per payslip. 18. Bulk render parallel. 19. Render failure → status=failed + sev-2. 20. Multilingual support (vi/en).

§5 — Verification

#[tokio::test]
async fn byte_identical_replay() {
    let ctx = TestContext::with_payslip().await;
    let r1 = ctx.render_pdf(ctx.payslip_id).await;
    let r2 = ctx.render_pdf(ctx.payslip_id).await;
    assert_eq!(r1.sha256, r2.sha256);
}

#[tokio::test]
async fn verification_catches_drift() {
    let ctx = TestContext::with_rendered_pdf().await;
    ctx.simulate_pdf_corruption(ctx.pdf_id).await;
    let r = ctx.verify(ctx.pdf_id).await;
    assert_eq!(r.status, "verification_failed");
}

#[tokio::test]
async fn pinned_fonts_in_template() {
    let template = std::fs::read_to_string("templates/payslip.tex").unwrap();
    assert!(template.contains("\\usepackage{fontspec}"));
    assert!(template.contains("\\setmainfont{"));  // pinned font
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-REW-005. Cross-module: TASK-DOC-001 (storage), TASK-AUTH-101 (CFO role), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Tectonic failcatchstatus=failed; sev-2retry
Font missingtex errorstatus=failed; sev-1install font
SHA256 mismatchverifierstatus=verification_failed; sev-1investigate
TASK-DOC-001 store failcatchsev-2retry
Cross-tenant renderRLS0 rowsinherent
Large bulk renderparallelizeinherentinherent
Template errorTeX validationsev-1fix template
Locale not supportedfallback to ensev-3add locale
Concurrent renderUNIQUE on payslipsecond skippedinherent
Disk fillcatchsev-2cleanup

§11 — Implementation notes


End of TASK-REW-006 spec.