Contributing to Inkvec
Thanks for taking an interest in Inkvec. This document covers how to build it, what CI checks before a merge, and what a pull request should look like.
Building and testing
Rust 1.89 or newer (the workspace rust-version). From a checkout:
cargo build --release --workspace cargo test --release --workspace
For the browser build (crates/inkvec-wasm), a plain cargo check -p inkvec-wasm --target
wasm32-unknown-unknown on stable is enough to catch compile errors; producing the actual
web/pkg / web/pkg-threads packages needs tools/build_wasm.sh (nightly + rust-src +
wasm-pack), which is only run when you are changing the WASM build itself.
Before sending a pull request, run what CI runs:
cargo fmt --all -- --check cargo clippy --workspace --all-targets -- -D clippy::correctness -D clippy::suspicious cargo test --release --workspace
Before you push
Most red CI runs are not failed tests but things that need no build to find: code left
unformatted in one of the four Cargo workspaces (the root, studio/core, studio/src-tauri,
studio/wasm), a copy of the version number that was not bumped, or a generated file (the
bindings, the Studio's copy of the guide, the Studio's licence notices) left stale.
tools/prepush.py runs exactly those checks, in the order CI does, and stops at the first
failure with a one-line hint saying how to fix it:
python tools/prepush.py # about a minute; most of it is the licence notices python tools/prepush.py --full # also the quality ratchet (bench/quality.py), minutes python tools/prepush.py --list # what it runs python tools/prepush.py --skip tsc # leave one check out
The TypeScript check uses the Studio's own compiler, so run npm ci in studio/ once
first (or skip it with --skip tsc if you did not touch the Studio). Every check runs at
below-normal priority, so it does not starve a build running beside it.
To have git run it on every push:
python tools/install_hooks.py # add --full to include the quality ratchet python tools/install_hooks.py --uninstall
The hook lives where git looks for hooks, so every worktree of the checkout shares it, and
the installer refuses to replace a pre-push hook it did not write (unless --force).
git push --no-verify skips it for one push.
The regression gate
bench/ci_gate.py scores the committed 246-icon screen set (bench/data, kept in the
repository so the gate runs from a bare checkout) under eight conditions: Quality and Fast
mode, each at 128 px, 512 px with transparency, 512 px flattened onto white (an opaque logo)
and web (the same logo resized to 400 px and saved as a quality-80 JPEG: the noise real
inputs carry; bench/build_web_tier.py).
Each condition is compared icon by icon with the per-platform baseline
bench/gate/baselines/<os>-<arch>.json, on four axes:
- dE00 (colour error against the artist's file), margin 1%
- turning_gap (distance from the artist's control-polygon turning, per canvas side), margin 2%
- ratio_gap (distance from the artist's parameter count,
|ln(ratio)|), margin 3%
Turning and parameters are judged as distances from the artist's file, not as raw values: a trace that turns more, or writes more numbers, toward what the artist drew is not a regression. The raw values are still reported. - geom (geometric match to the artist's file: the mean distance between the trace's edges and the artist's, in input pixels, read as the area between them), margin 2%
The verdict is statistical: a paired bootstrap interval for the change of each axis, and a
non-inferiority test that passes when the one-sided 95% upper bound stays below the margin.
Where a change is too broad for the 246 icons to resolve that margin (typically at 512 px),
the margin is floored at the smallest effect the set can detect, and such a pass is
reported as "within-noise"; there is no "inconclusive" verdict. Details and the
literature are in bench/README.md.
Run it against a release build (about 40 min for all eight conditions on a desktop; pass
--conditions for a subset while iterating):
cargo build --release -p inkvec-cli python bench/ci_gate.py --exe target/release/inkvec --workers 4
(needs numpy, scipy, pillow, scikit-image, resvg_py). Icons whose SVG is
byte-identical to the baseline's are traced but not scored again, so a change that moves few
icons costs little more than the traces. The gate also reports, without gating them, how far
each trace is from the artist's file in how it is drawn (segment kinds, smooth joins, nodes
at the extrema, the design grid, ...); to tune a build or a flag against those statistics:
python bench/human_stats.py --exe target/release/inkvec --extra-args "--editability" --base-args ""
python tools/prepush.py --gate
runs it with the hard cases. Only a demonstrable gain moves a baseline. If a change
regresses an axis, the gate fails; it can only be bypassed with
--exe ... --bypass-gate "<justification>", and only with a strong, explicit
justification the maintainers agree with. Do not pass --bypass-gate in a pull request
without discussing it first. If a change is a deliberate, agreed trade, re-baseline with
--write-baseline and include the updated bench/gate/baselines/ files in your diff.
The quality ratchet
bench/quality.py does not analyse anything itself โ cargo clippy, rustc's own lints
and cargo fmt do that, configured under [workspace.lints] in the workspace
Cargo.toml. What it adds is a budget recorded in bench/quality_budget.json that
grandfathers today's existing debt (for example, long functions clippy's
too_many_lines already flags) so the count can fall but never rise: a warning count
going up on a lint that already has debt fails, but existing debt does not block
unrelated work.
python bench/quality.py # check; exit 1 on regression python bench/quality.py --report # every metric, no gate
--update re-baselines after a deliberate decision (for example, intentionally adding a
long function) and prints what it loosened so the change is visible in review โ use it
sparingly and explain why in the pull request.
Third-party licence notices
docs/THIRD_PARTY.md is generated from the resolved dependency graph. If your change
adds, removes or updates a Cargo dependency, regenerate it:
python tools/third_party.py
CI checks it is current with python tools/third_party.py --check. If your change
introduces a dependency that is not under a permissive licence, expect follow-up
questions โ see the licence list embedded in tools/third_party.py.
Commit and pull request expectations
- Keep commits focused; prefer several small commits over one large one when the changes are logically separate.
- Describe why, not just what, in commit messages and PR descriptions.
- If a change is meant to be a no-op on output, say so and how you checked (byte-identical SVG on a sample, or the regression gate showing no movement).
- Match the existing code style;
cargo fmtand the workspace lints are the source of truth, not personal preference. - New public items should be documented (
missing_docsis a workspace lint). - Update
CHANGELOG.mdunderUnreleasedfor user-visible changes.
Licensing
Inkvec is licensed under Apache-2.0 (see LICENSE). By submitting a
contribution, you agree it is provided under the same licence (inbound = outbound). We do
not require a separate contributor licence agreement (CLA).