Task — engineering-spec@1
"REW byte-identical payslip PDF render — Tectonic + pinned fonts produces deterministic PDF bytes for verification"
draftTASK-REW-006
§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.
- MUST validate
pdf_render_statusagainst closed enum per DEC-2201.
- MUST render at
tectonic_renderer.rs::render(payslip_row, template)per DEC-2200:
- Use Tectonic (deterministic LaTeX compiler)
- Fonts pinned in template via explicit version
- Output PDF bytes
- MUST verify at
verifier.rs::verify(pdf_bytes)per DEC-2203:
- SHA256 the PDF
- Compare to expected (from deterministic replay or prior render)
- Mismatch → sev-1 audit + status=verification_failed
- MUST store in TASK-DOC-001 per DEC-2202 with
sha256metadata.
- 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;``
- 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)``
- MUST emit 3 memory audit kinds per DEC-2204. PII per TASK-MEMORY-111: sha256 in chain (not PII); content never in chain.
- MUST thread trace_id from render → verify → audit.
- MUST NOT use non-pinned fonts per DEC-2200.
- 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
- 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
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| Tectonic fail | catch | status=failed; sev-2 | retry |
| Font missing | tex error | status=failed; sev-1 | install font |
| SHA256 mismatch | verifier | status=verification_failed; sev-1 | investigate |
| TASK-DOC-001 store fail | catch | sev-2 | retry |
| Cross-tenant render | RLS | 0 rows | inherent |
| Large bulk render | parallelize | inherent | inherent |
| Template error | TeX validation | sev-1 | fix template |
| Locale not supported | fallback to en | sev-3 | add locale |
| Concurrent render | UNIQUE on payslip | second skipped | inherent |
| Disk fill | catch | sev-2 | cleanup |
§11 — Implementation notes
- §11.1 Tectonic Rust crate
tectonic = "0.15"; compiles deterministically when fonts + packages pinned. - §11.2 Fonts: pin to system path or bundle in image.
- §11.3 memory audit body: payslip_id, sha256, status; content never in chain.
- §11.4 Bulk render: tokio parallel with bounded concurrency (4 at a time).
- §11.5 Member portal access via TASK-PORTAL-001 scoped view.
End of TASK-REW-006 spec.