# Development tools Not part of the application — nothing in `app/` imports these, and the app does not need them to run. They exist because the things this project treats as rules are tedious enough by hand that they get skipped or got wrong, and most of these were rebuilt from scratch more than once before being kept. Run them from the repository root. ## The rules they enforce | Tool | Rule it serves | | --- | --- | | `verify_migration.py` | "Every migration is verified against a **populated** predecessor, including downgrade" — which the test suite does not cover, because it builds every schema from nothing at head | | `headline_numbers.py` | "Numbers are sacred" — the before/after comparison required of any change touching `queries.py`, `fyreport.py`, `charts_build.py` or `exports.py` | | `screenshot.py` | Rendering the pages and *looking* at them, which is the only thing that catches what a measurement cannot | ```sh uv run python tools/verify_migration.py 0002 # sqlite PG=1 uv run python tools/verify_migration.py 0002 # postgres uv run python tools/screenshot.py # -> shots/ ``` Measuring those pages is no longer a tool: it is `tests/test_visual.py`, which renders them itself and checks both widths. `screenshot.py` stays because looking at a page catches what no assertion was written for. `browser.py` is where both of them find Chrome. **The one that will catch you:** compare numbers with the price feed **off** (`APP_PRICE_FEED__ENABLED=false`) when the "after" side is a test container. Otherwise it fetches the day's closes, every price moves, and it looks exactly like a calculation change. That produced 95 false differences the first time. ## The brand `app/branding.py` holds the drawing; everything else is generated from it by `render_brand.py`, which writes the Jinja partial, the favicon and the README banners. **`tests/test_branding.py` re-runs it in memory and fails if anything on disk has drifted**, so it is the one tool here the test suite depends on — a redraw cannot land in the top bar and miss the favicon. ```sh uv run python tools/render_brand.py # needs headless Chrome for the PNGs ``` The wordmark's letter offsets are the typeface's own advances and kerning, so there is nothing to solve: spacing questions are answered by re-exporting from the font, not by fitting a model to the outlines. See `docs/contributing/style.md` for what earns a comment, and `docs/contributing/decisions.md` for where the long reasoning goes instead.