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.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user