Files
Voiced/CLAUDE.md
T
xavierk f13665922c Fix glyph cache poisoning across renders; add CLAUDE.md and handoff.md
fontkit caches glyphs per font for the whole process and pdfkit embeds composite
components with no code points, so setting ':' before '.' in Poppins corrupted every
later render in the same process (text layer showed '1,200;00', line breaks shifted).
primeFontGlyphs() now creates the component glyphs with their cmap code point at font
registration, awaited by renderCore and browser init. Regression test and Quirk 13 added;
the warm-up workarounds in the template tests are removed.
2026-10-04 13:11:40 +05:30

7.1 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Voiced is an offline-first GST invoicing desktop app (Tauri v2, React 19, IBM Carbon, @react-pdf/renderer 4.9.0, SQLite via rusqlite) built by Bongbetic for vendor Test Vendor. Targets: Void Linux (xbps, WebKitGTK 2.50) and Windows (NSIS, WebView2). handoff.md holds the current project state and next steps; the approved plan is /home/soubarna/.claude/plans/take-a-look-at-logical-sunbeam.md. The README's "Project layout" section is out of date; trust this file and the code.

Commands

npm run build                      # tsc --noEmit && vite build (the type-check gate)
npm test                           # vitest run, ~7 min; run on a quiet machine (load starves the sweep tests)
npx vitest run src/pdf/templates/monolith            # one folder
npx vitest run path/to/file.test.tsx -t "name"       # one test
npm run fonts:verify               # bundled fonts match public/fonts/manifest.json (regenerate with fonts:manifest)
npm run logo:measure               # regenerate src/pdf/templates/slots.generated.ts from the Canva PDFs; must be idempotent
npm run thumbnails                 # regenerate public/templates/*.png (unchanged templates must stay byte-identical)
npm run templates:compare          # Canva vs Voiced side-by-side PNGs (scripts/templates/compare.mjs, output under /tmp)
cargo test --manifest-path src-tauri/Cargo.toml       # Rust unit tests (gst, db migrations, logo, commands)
npm run app:dev                    # tauri dev (served from localhost:1420; does NOT exercise the tauri:// protocol)
npm run app:build                  # production build, custom-protocol
npm run app:build:windows          # NSIS via cargo-xwin cross-compile (see README)
./packaging/void/build-xbps.sh     # Void .xbps into build/void/repo

Real-webview self-test (the only check that runs the production build): build with npm run tauri -- build --debug --no-bundle --features custom-protocol, then bash /tmp/run-selftest.sh. The app is inert unless VOICED_SELFTEST_OUT is set; the script runs the debug binary with temp XDG dirs and the report lands in /tmp/voiced-e2e/report.json (ok: true expected). /tmp is not durable: if the script is gone, recreate it with that env var, XDG_DATA_HOME/XDG_CONFIG_HOME temp dirs and a timeout 180.

Architecture

Rust backend (src-tauri/src)

  • db.rs owns the schema as numbered migrations (rusqlite_migration, M1–M5); it copies the DB aside before migrating. Add schema changes as new migrations, never edit old ones. Issued invoices carry snapshots (vendor, client, GST, layout fingerprint) and are immutable.
  • gst.rs does all money and tax maths in integer paise. Tax heads (CGST+SGST vs IGST vs UTGST) derive from (supplier state, place of supply); a mismatch with the chosen head is rejected. An unregistered vendor gets a plain "Invoice" with no tax. GSTIN is checked with the mod-36 checksum.
  • logo.rs trims, quantises and measures logos (aspect, density, wordmark/mark/tall, knockout variant for dark surfaces).
  • commands/*.rs are the Tauri commands; raw.rs carries binary payloads over raw-body IPC (ASCII headers, ArrayBuffer response). archive.rs stores each issued PDF content-addressed under app_local_data_dir/archive/<sha256>.pdf.
  • selftest.rs is the unattended end-to-end hook described above.

Frontend (src)

views/ and components/ are the Carbon UI; lib/api.ts is the typed wrapper over the Tauri commands; lib/exportFlow.ts and hooks/useInvoiceExport drive export (searchable or flattened) and archiving.

PDF engine (src/pdf) — the part that needs several files to understand

Pipeline: model/build (buildRenderModel) → engine/geometry (computeFrame: A4/Letter plus margin presets) → a template Layout → InvoicePdf/InvoicePage → render/core (renderCore: bytes, normalized layout tree, auditLayout issues, layout fingerprint). Rendering runs in a module Web Worker through a latest-wins render/client (FIFO assemble lane, watchdog, main-thread fallback). Preview uses pdf.js; flatten rasterises pages at 300 DPI and re-assembles them as an image-only PDF.

  • Templates live in templates/<family>/ and are registered in templates/registry.ts (picker order) and catalog.ts (labels, thumbnails). Each has tokens.ts, plan.ts, Layout.tsx and a test. They implement templates/contract.ts. 14 exist: Classic plus the 13 Canva templates, specified in docs/templates/*.md (measured from the Canva PDFs in templates/, which is untracked on purpose: never stage templates/ or .commandcode/).
  • Logo placement is slot-based (engine/logo.ts, templates/slots.*): the user's logo fills the Canva placeholder box, contained, with a shrink-only weight factor. Template placeholder logos are never reproduced. Some templates use opt-in slots (slots.optin.ts), default off.
  • Tests that guard layout: template harness and sweep tests (harness*.ts, harness.sweep*.test.tsx: 1–45 rows, long text, A4/Letter, no overlap), layout lint, fingerprint goldens (goldens.test.ts) and quirks.test.tsx. The Node goldens must equal the fingerprints the real webview reports.
  • Glyph priming: fontkit caches glyphs process-wide and pdfkit embeds composite components with no code points, which corrupted later renders (":" before "." gave "₹1,200;00"). primeFontGlyphs() in fonts/register.ts fixes it and runs in renderCore and browser init. Do not widen it to every cmap glyph (breaks Inter's contextual hyphen); it uses fontkit private APIs, so re-check on fontkit upgrades.
  • Fonts: TTF only, bundled in public/fonts/<family>/ with a generated manifest; every family needs normal and italic sources. WOFF2 embeds blank glyphs. Roles/type tokens are in fonts/roles.ts.

react-pdf 4.9.0 rules (all pinned by quirks.test.tsx; break them and layouts hang or overlap)

  • Text lines are cached at first measure, so give flex children pinned widths (width=minWidth=maxWidth) with a {flexGrow:1, flexBasis:0} fill child.
  • No lineHeight on Page/View/render-prop ancestors; set an absolute 'Npt' lineHeight on each static Text. Hyphenation is off.
  • fixed, wrap, minPresenceAhead are key-presence checks: spread them only when meaningful (blocks/pdfProps).
  • A non-fixed page-level absolute Svg/Image hangs pagination. Render props must return Text/View only.
  • maxLines does not clip a single over-long word (use FitText). Trailing letterSpacing needs Tracked compensation.
  • A fixed table header as the first child of the table View repeats on each page; use TableGuard/BottomSpacer with minPresenceAhead.

Conventions

  • Commit only after the gates pass (npm run build, full npm test, fonts:verify, and the self-test for engine/template changes). Work is on stacked phase branches (phase-0-hotfix → phase-a-foundation → phase-b-engine → phase-c-templates); create the next phase branch from the previous one.
  • Windows/WebView2 has never been run on a real machine; the app window UI has only been exercised headless.