# Engineering practices

These practices are project-agnostic. They travel with me between repositories;
project-specific notes live in the final section, which you can delete when
copying this file elsewhere.

## Design philosophy

1. **Prefer the simplest design that exploits the problem's invariants.**
   Before designing for the general case, identify the invariant that makes the
   common case cheap (e.g. "the routing graph is fixed while a gesture is in
   progress — cache the router per gesture" beats an incremental graph
   migration). When a plan is getting large, stop and check whether a simpler
   observation collapses it.
2. **"Simple" means no over-engineering — it does not mean avoiding
   complexity or abstraction.** Sharp tools (higher-ranked trait bounds,
   advanced generics) are permitted, but they are sharp: each use needs a
   justification. Speculative machinery for problems we don't have yet is
   over-engineering; a well-chosen abstraction for a problem we do have is not.
3. **Fix the API, don't work around it.** If a new feature is awkward because
   of the current API's shape, spike and rework the API first. Work-arounds are
   fragile, accumulate as rot, and the rework is an opportunity to reconsider
   the structure. (A per-feature fallback that patched around a missing input
   model lived exactly one day before the model grew the missing concept and
   the fallback was deleted — build the concept first.)
4. **Tech debt is a real concern.** A forked or parallel implementation of
   anything *will* diverge from its sibling and generate issues downstream.
   Treat duplication and drift as costs on par with bugs.

## DRY and shared functionality

- **Copy-paste is to be avoided wherever possible.** Parallel implementations
  that must be updated in lockstep are silent-drift hazards.
- The preference ladder for sharing functionality:
  **pure functions → helper functions → methods on structs/enums → macros
  (last resort).** All of these beat pasting the logic again.
- Policy that several call sites must agree on (a hit-test priority order, a
  z-order, a dispatch sequence) should be **encoded once** — as data or a
  single resolver — and consumed everywhere, so the sites cannot disagree.
- **External crates are preferred to reproducing their functionality in-app.**
  If a library we already depend on computes the thing (e.g. the UI toolkit's
  text-layout engine), consume its output rather than approximating it —
  even at modest integration cost — because the in-house approximation will
  drift.

## The type system encodes invariants

- **Illegal states should be unrepresentable.** Mutually exclusive application
  modes are one enum, not N `Option`/`bool` fields. A press-with-origin is
  `Option<Press { origin }>`, never `pointer_down: bool` + `origin:
  Option<_>` which can disagree.
- **No naked `bool` arguments.** Use a two-variant enum or a typed wrapper
  that names the meaning at the call site.
- **No naked `f32`/`f64` arguments.** A bare float in a signature carries no
  meaning. Wrap it in a newtype naming the semantic (`Zoom`, `Progress`,
  `WorldPx`): constructor to wrap, `impl From<Newtype> for f32` so callees
  extract via `Into`. If the value is *constrained* (non-negative, unit
  interval), it stays wrapped everywhere and the newtype enforces the
  constraint — clamping constructors, saturating ops implemented once on
  `std::ops` impls (never re-clamped at call sites), NaN handled so ordering
  is total.
- **Durations are `core::time::Duration`. Always.** Convert to float seconds
  only at foreign API boundaries. This makes negative durations and unit
  confusion unrepresentable, and deletes their guard code.
- Prefer typed enums with `Into` impls over threading primitives through
  layers.

## Comments

- Follow the clean-code principle: **comment only surprising or unexpected
  behavior.** Never narrate what the code does, where a change came from, or
  why a change is correct — that is reviewer-talk, noise once merged.
- If a function needs a comment to be understood, that is refactoring
  pressure, not documentation debt. **Heavily commented code is a warning to
  the reader that it requires extra thought** — restructure until the comment
  is unnecessary.
- Prefer names and types that make intent clear; comments drift, names and
  types do not.
- Data structures are commented only when part of a public API or when their
  intent is non-obvious.

## Dependencies

- Before building a feature on a deprecated or outdated API, **migrate the
  dependency to its latest version first**, then build on the current API.
  Building on the old surface doubles the eventual migration.

## Testing and verification

- A test must **prove** what it claims: if it depends on a geometric or
  structural precondition (two rects overlap, a target exists), assert the
  precondition inside the test so it can't silently stop testing anything.
- Verify UI behavior **through the full real path** (dispatch, tool switching,
  event flow), not only through isolated component tests — isolated tests can
  pass while the integrated path misbehaves.
- Refactors declared behavior-preserving must not change any test outcome.
  Behavior *changes* are enumerated explicitly (what, where, why) at review
  time, never discovered later.
- Golden/regression tests guard end-to-end behavior; regenerating a golden is
  an acceptance step whose diff gets reviewed, not a chore to silence.
- Zero clippy warnings; new code lands with its tests.

## Developer-facing tooling

- For author/dev tooling, prefer the console and an edit-and-re-run loop over
  in-app UI (transport controls, guards, on-screen error panes). Print errors
  where the developer already is; keep flags few and composable.
- Errors that point at a file should carry spans and render with the source
  attached (e.g. miette-style reports), not stringly byte offsets.

## Workflow conventions

- Commit messages explain the *why* and the shape of the change; intentional
  behavior changes are listed in the message.
- Breaking changes are acceptable while a project is undeployed, in the name
  of simplification — note them clearly.

---

# Project notes: blockworx

A block-diagram editor that runs in the browser: blocks with pins/ports,
auto-routed wires, and SVG symbols. The front end is the Dioxus shell in
`web/`; nothing below it names a host.

## The crate graph

Dependencies run strictly downward; `blockworx-canvas2d` is the one backend,
and the only crate below the shell that paints to a host.

| Crate | Owns | Depends on |
|---|---|---|
| `blockworx-doc` | entities, ids, opcodes, commits, the fold | — |
| `blockworx-geom` | world geometry, the grid metric and its bridge to the document grid | doc |
| `blockworx-paint` | `Color`, `Font`, `Renderer`/`Canvas`/`Animator`/`TextLayout`, palette, theme, `Style`, `Vantage`, the easing table, the recorder | doc, geom |
| `blockworx-text` | the `Shaper` — `harfrust` shaping, epaint's line-breaking rules, a layout cache — and the glyph `Outlines` | paint |
| `blockworx-store` | the `.bwx` container: manifest, revs, assets, `Doc`, atomic writes | doc |
| `blockworx-router` | the wire router and its lattice | doc, geom |
| `blockworx-editor` | path, state, presentation, op emitters, gesture, `Drawing`, shapes, the render path | doc, geom, paint, router, store |
| `blockworx-tools` | the tools, commands and bindings, the undo stack, the spotlight | + editor |
| `blockworx-export` | SVG, PNG and PDF, generic over `TextLayout` | + editor, text |
| `blockworx-kernel` | `Session`, the chrome model, the pointer resolver, the camera, the navigator's flattening (`nav`), the selection bar's order and placement (`bar`), the palette's rows (`palette`), and `kernel(&mut Session, events, &impl TextLayout) -> View` | + editor, tools |
| `blockworx-canvas2d` | the web backend: `replay` onto a `CanvasRenderingContext2d`, `Glyphs` (the `Shaper` the kernel is called with and the `Path2D` its outlines trace to), `Images` over `Blob` URLs, the ground, `fit`, `css_cursor`, the DOM input path as pure samples plus a `Reader` (the pan/pinch latches, and two fingers as a `Span`), and the download/clipboard hand-offs | doc, geom, paint, text |
| `blockworx-opfs` | the browser's own storage: a `Storage` over a `FileSystemDirectoryHandle`, the origin `Root` the web library lists containers from, and the Web Lock a container is held by | store |
| `blockworx-web` (`web/`) | the Dioxus shell: `Shell` (the `Session`, the batch, the frame loop's `pacing`), the `Chrome` model the components bind, the canvas element and its DOM handlers, the in-place editor, the light/dark `mode`, and the regions — top bar, toolbar, sidebar (the activity bar, and the one panel it opens: Diagrams, Parts, History, Settings), status line, notices, selection overlay and its pickers, ⌘K palette, file input; a package of its own, built by `dx` | kernel, canvas2d, tools, editor, export, store, geom, paint, doc |
| `blockworx-bench` | criterion benches over the kernel: the scenarios a user waits for (open, frame, frame at fit, select, nudge, drag frame, drop) on the settled 50×50, and the `spans` example that tallies where each one's time goes; nothing depends on it | kernel (`test-support`), text, tools, editor |
| `blockworx` (`src/`) | the command line over a container on disk: `log`, `verify`, `migrate`, and the `RUST_LOG` subscriber they report through; it names no host and no editor | store |

`Shell` is the frame and nothing else: a DOM handler only pushes onto the
batch, and one booked frame is one `blockworx_kernel::kernel` call and one
replay — the ground and the display list onto the canvas, the hand-offs
delivered, and the chrome model into its signal when it moved, which is what
the Dioxus components bind to. The batch lives in a cell of its own, so a
handler firing mid-frame adds to the next frame rather than re-entering the
session. `Session` is named only by that call and the host facts told ahead of
it (the sheet, the identity); the theme reaches the kernel only as
`Action::SetPalette`. What is still the shell's own — the file doors, export
delivery, the pickers, the clipboard, persistence, the theme — is listed under
"Kernel: what still lives in the shell" in `docs/exit-egui-playbook.md`; the
frame's decisions are in `docs/shell-on-kernel-playbook.md`, and the shell's
own shape in `docs/dioxus-web-shell-playbook.md`.

## Conventions

- `cargo xtask ci` (format, lints, tests, docs, gates, wasm) must pass before
  code is committed. Four of its steps are greps and dependency gates:
  **headless** proves the ten core crates carry no host — no toolkit, no
  windowing, no GPU API, no async runtime — over their dev-dependencies as well
  as their normal ones, **backend** proves the backend knows nothing above
  `paint` (and `text`, which a browser has no layout engine to replace),
  **shell** proves the browser is reached only through `blockworx-canvas2d`,
  `blockworx-opfs` and `blockworx-web`, and **palette** proves nothing outside
  the palette names a color of its own. The wasm step also runs the
  `wasm-bindgen-test` suites that need a browser — the web backend's and the
  origin storage's — in headless Chrome through `wasm-pack`, skipped with a
  message when `wasm-pack` is absent; set `CHROMEDRIVER` when the driver
  `wasm-pack` fetches does not match the installed Chrome.
- `cargo xtask web build` bundles the Dioxus shell with `dx` and reports what
  it weighs, gzipped as well as raw; `cargo xtask web serve` runs it with live
  reload, `cargo xtask web setup` installs what a fresh checkout is missing.
  `cargo xtask web walk` builds, serves and drives a real browser through
  `web/walk/`'s walk-throughs — Chrome for the editor, the chrome, storage and
  touch, Firefox for a second engine's answer — and is deliberately *not* part
  of `ci`: it is a thing to run and read, and it skips with a message where
  there is no browser or driver. There is one front end and one build of it;
  `cargo xtask native` builds the command line, which opens no window.
- Fixtures and drivers a crate exposes to suites above it go behind a
  `test-support` feature (`blockworx-doc` calls its own `fixtures`), never
  behind `#[cfg(test)]`, which stops at the crate line.
- Stage and commit `todo.md` alongside code changes; it records the running
  worklog.
- The document format is JSON, documented in `docs/json-format.md` — the
  `manifest.jsonl` rows, the `revs/` snapshots, the stamp an export carries,
  and the document model. Read it before generating or editing
  a `.json` document or a container.
- Performance findings live in `TUNING.md`. **Timings come from criterion**:
  `cargo bench -p blockworx-bench` (`-- --save-baseline <name>` before a
  change, `-- --baseline <name>` after), never from an `Instant` around one
  call. Where the time goes comes from
  `cargo run --release -p blockworx-bench --example spans`, which is
  attribution, not timing; in the browser, from the tracing-web spans in a
  CPU profile. A new hot path gets a scenario in `crates/bench` before it
  gets a number.
- Theme colors: use base colors (B07 brightest) for UI/guide roles, not
  accents; introduce new roles instead of numerically manipulating the
  palette.
- Prefer a menu that opens in place — the toolbar's folded sheet tools, the
  selection overlay's pickers, the ⌘K palette — over a modal dialog.
