Files
Voiced/CLAUDE.md
T
xavierk 63854dd960 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

59 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
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 reference PDFs; must be idempotent
npm run thumbnails # regenerate public/templates/*.png (unchanged templates must stay byte-identical)
npm run templates:compare # reference 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 reference templates, specified in `docs/templates/*.md` (measured from the reference 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 reference 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.