Files
wanderbooks/crates/ledger-import/templates/workpaper.typ
T
swaitsandClaude Fable 5.1 d45e842873 feat(workpaper): the audit workpaper as a deterministic PDF (P8)
One page set a CPA can read: what was checked, through which day and state
of the books, every finding with its code and severity. No clock and no
random id, so the same findings give the same bytes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
2026-10-29 15:54:08 -06:00

78 lines
2.4 KiB
Typst

// An audit workpaper: what was checked, through which date and which
// books' state, and every finding with its code and severity.
//
// Everything arrives as data in sys.inputs.data (JSON), built by
// ledger-import::workpaper. The template only typesets: no arithmetic, and
// no text is ever parsed as markup. It holds no clock: the same data gives
// the same bytes.
//
// Shape: { company, title, scope, through, through_seq, program_version,
// tb_hash, findings_sha256, summary: str,
// findings: [{ severity, code, subject, message }],
// checks: [{ code, severity, predicate }] }
#let d = json(bytes(sys.inputs.data))
#let ink = luma(25)
#let quiet = luma(110)
#let hair = 0.5pt + luma(160)
#set document(title: d.company + " — " + d.title, author: d.company)
#set text(font: ("Inter", "Noto Sans CJK SC", "Noto Sans Arabic UI", "Noto Sans Hebrew", "Noto Sans Thai", "Noto Sans Devanagari"), size: 9pt, fill: ink, number-width: "tabular", hyphenate: false)
#set page(
paper: "us-letter",
margin: (x: 0.8in, top: 0.8in, bottom: 0.8in),
footer: context {
set text(7.5pt, fill: quiet)
grid(
columns: (1fr, auto),
[#d.program_version · head #d.through_seq],
align(right)[Page #counter(page).display() of #counter(page).final().first()],
)
},
)
#align(center)[
#text(15pt, fill: luma(45), d.company) \
#v(4pt)
#text(10.5pt, weight: "bold", d.title) \
#v(2pt)
Through #d.through · scope #d.scope
]
#v(12pt)
#grid(
columns: (auto, 1fr),
row-gutter: 5pt,
column-gutter: 12pt,
text(fill: quiet)[Books' figure head], [#d.through_seq],
text(fill: quiet)[Trial balance hash], text(size: 7.5pt)[#d.tb_hash],
text(fill: quiet)[Findings hash], text(size: 7.5pt)[#d.findings_sha256],
text(fill: quiet)[Result], [#d.summary],
)
#v(12pt)
#text(10pt, weight: "bold")[Findings]
#v(4pt)
#if d.findings.len() == 0 [
No findings.
] else [
#table(
columns: (auto, auto, auto, 1fr),
stroke: (x: none, y: hair),
inset: (x: 4pt, y: 4pt),
table.header([*Severity*], [*Code*], [*About*], [*Finding*]),
..d.findings.map(f => (f.severity, f.code, f.subject, f.message)).flatten(),
)
]
#v(14pt)
#text(10pt, weight: "bold")[What was checked]
#v(4pt)
#table(
columns: (auto, auto, 1fr),
stroke: (x: none, y: hair),
inset: (x: 4pt, y: 4pt),
table.header([*Code*], [*Severity*], [*True when it is reported*]),
..d.checks.map(c => (c.code, c.severity, c.predicate)).flatten(),
)