# Working on smart-package-tracker
Orientation for developers and AI agents. Read this before changing code.
`CLAUDE.md` points here; this file is the single source of truth.
## What this crate is
A `no_std` + `alloc` library that mints package tracking IDs (`PKG-9ED9285C`),
renders them as Code 128 or QR barcodes to PNG or SVG, and reads Code 128 back
out of an image. No network I/O, no async, no carrier integrations — those are
deliberately out of scope (see "Boundaries").
## The one diagram that explains the layout
```text
TrackingId ──▶ Symbology::encode ──▶ Symbol ──▶ Renderer::render ──▶ bytes
src/id/ src/symbology/ (BitMatrix) src/render/
payload ◀── Decoder::decode ──── BitMatrix ◀── binarize ◀── GrayImage ◀── image
src/symbology/ src/scan/
```
The `BitMatrix` in the middle is the seam, and both directions cross it.
Encoders never know about pixels; renderers never know about symbologies; the
scanner never knows which symbology it is reading. A linear barcode is a 1-row
matrix, a QR code is a square one — that is the *only* difference the renderers
see.
**If a change makes a renderer or the scanner match on `SymbologyKind`, the
seam has been broken. Push the difference into `Symbol`/`SymbologyKind`
metadata instead.** `required_quiet_zone()`, `is_linear()` and
`linear_character()` are what that looks like in practice.
## Repo map
| `src/lib.rs` | Crate docs, feature table, re-exports. Public API surface starts here. |
| `src/error.rs` | The single `Error` enum. `#[non_exhaustive]` — add variants, never repurpose them. |
| `src/id/mod.rs` | `TrackingId` newtype + parsing/validation. `MAX_LEN`, the charset rule. |
| `src/id/generator.rs` | `IdGenerator`/builder: prefix, entropy width, checksum policy. |
| `src/id/checksum.rs` | ISO/IEC 7064 MOD 37,36 check character. |
| `src/symbology/mod.rs` | `BitMatrix`, `Symbol`, the `Symbology` and `Decoder` traits, `SymbologyKind`/`LinearCharacter` metadata. |
| `src/symbology/code128.rs` | Code 128 encode + decode, wrapping the `code128` crate. |
| `src/symbology/qr.rs` | QR encode, wrapping the `qrcode` crate. |
| `src/render/mod.rs` | `RenderOptions`, `Length`, `Color`, `QuietZone`, and **`Layout`** — the shared geometry calculation. |
| `src/render/png.rs` | Rasterises `Layout` into RGBA, wraps it in a PNG with a `pHYs` chunk. |
| `src/render/svg.rs` | Emits the same `Layout` as `<rect>` runs. |
| `src/render/hri.rs` | Embedded 5x7 bitmap font for the human-readable text line. |
| `src/scan/mod.rs` | `GrayImage`, `Scanner`, `Scan`. Image input, scan-line selection, and the row/column sweep. |
| `src/scan/binarize.rs` | Local adaptive threshold: pixels to dark/light modules. |
| `src/scan/linear.rs` | Pixel runs to modules, one character at a time. Symbology-agnostic. |
| `tests/integration.rs` | Consumer-level end-to-end tests (Code 128). |
| `tests/qr.rs` | QR output decoded back with `rqrr`, an independent decoder. |
| `tests/scan.rs` | Render, degrade the image the way reality does, scan it back. |
| `examples/label.rs` | Runnable end-to-end demo: `cargo run --example label`. |
## Invariants — do not break these silently
1. **Quiet zones are conformant by default.** Code 128 needs 10 modules per
side, QR needs 4 on all four sides (`SymbologyKind::required_quiet_zone`).
Nothing — including the human-readable text line — may be drawn inside them.
This is the number one cause of "looks fine, will not scan".
2. **Modules snap to whole pixels.** A bar straddling a pixel boundary renders
as a grey edge and costs the contrast a scanner needs. `Layout` is integer
pixels throughout.
3. **PNG and SVG describe identical geometry.** Both consume the same `Layout`.
Any geometry change belongs in `Layout::compute`, never in one renderer.
4. **Fallible operations return `Result`, they do not panic.** The README
promises this. Accessors tolerate out-of-range input rather than panicking,
and the handful of constructors that do assert say so in a `# Panics`
section. Keep it that way.
5. **Output is byte-for-byte reproducible.** No timestamps, no hash-map
iteration order, no system fonts. That is why the HRI font is embedded.
6. **`no_std` + `alloc` core.** Only `std`, `png`, and `qr` may assume `std`.
Use `alloc::` imports, not `std::`, outside `#[cfg(feature = "std")]`.
7. **`#![forbid(unsafe_code)]`.** Non-negotiable.
8. **A scan never returns a guess.** Every payload the scanner returns has
passed the symbology's own check character. A wrong answer routes a parcel
to the wrong depot and looks correct doing it, which is worse than a miss —
so widening what the scanner will accept is a decision to make deliberately,
not a way to raise a hit rate.
## Commands
```sh
cargo test --all-features # everything, incl. doctests
cargo clippy --all-features --all-targets -- -D warnings
cargo fmt --all --check
cargo run --example label # writes label*.png / label*.svg
# no_std still builds
cargo build --target thumbv7em-none-eabihf --no-default-features --features code128,svg,scan
# feature combinations compile in isolation (CI covers a subset)
for f in "" std serde os-rng code128 svg qr png scan; do
```
MSRV is **1.85** (set by `getrandom` 0.4 / edition 2024) and is tested in CI.
Raising it is a breaking change.
## How to add things
* **A new symbology** — implement `Symbology` in `src/symbology/`, add a
`SymbologyKind` variant with its `required_quiet_zone()` and `is_linear()`,
gate it behind a feature. Touch no rendering code. QR was added this way.
* **A new renderer** — implement `Renderer` in `src/render/`, consume `Layout`,
compute no geometry of your own.
* **Making a symbology scannable** — implement `Decoder` for it and return a
`LinearCharacter` from `SymbologyKind::linear_character()`. `src/scan/` needs
no change; if it does, the metadata is not saying enough. A matrix symbology
cannot be scanned this way at all — a scan line through a QR grid is not a
code — and would need its own locator.
* **A new error case** — add a variant to `src/error.rs` with a lowercase,
actionable `#[error(...)]` message. The enum is `#[non_exhaustive]`, so this
is not a breaking change.
* **A new public item** — it needs a doc comment (`missing_docs` is warned on),
an `# Errors` section if fallible, and a `# Panics` section if it can panic.
## Test conventions
* Unit tests live in a `#[cfg(test)] mod tests` at the bottom of their file and
gate on features (`#[cfg(all(test, feature = "code128"))]`).
* Integration tests assert on *decoded output*, not on internals — PNG bytes are
decoded back to pixels, QR output is decoded by `rqrr`.
* Prefer a test that would fail for the real-world reason (a scanner cannot read
this) over one that asserts an implementation detail. Note that a decoder
handed a perfectly cropped image is far more forgiving than a scanner pointed
at a parcel — see the comment in `tests/qr.rs`.
* Scanning tests start from the crate's own renderer and degrade the image the
way reality does: rotated a quarter turn, inverted, dropped into a larger
scene, speckled, blurred. A scanner tested only on its own pristine output
has not been tested. Noise must come from a fixed seed — see `Lcg` in
`tests/scan.rs` — or the suite passes on the desk and fails in CI.
## Boundaries
Out of scope on purpose: shipment events, carrier APIs, GS1 identifier
issuance. They bring network I/O, async runtimes, and vendor licence terms into
a dependency graph that is permissively licensed with no copyleft (`cargo deny
check` enforces this).
Scanning is in scope only as far as it stays arithmetic. It reads rendered
labels, flatbed scans and screenshots — axis-aligned images — and adds no
dependencies. Camera-grade decoding (perspective correction, symbol
localisation, curved surfaces) is a scanning engine, and rebuilding one inside
a label-generation crate is the wrong shape; depend on `rxing` or similar.
QR scanning is out for the same reason plus a concrete cost: the only viable
decoder, `rqrr`, adds seven crates to the published graph. It stays a
dev-dependency, where it verifies our QR *output* without shipping to
consumers.
## Release — irreversible
Pushing a `v*.*.*` tag runs `.github/workflows/release.yml`, which publishes to
crates.io behind a `crates-io` environment approval. **A published version can
be yanked but never removed or replaced.** Do not create version tags unless
publishing is the explicit intent. `workflow_dispatch` with `dry_run: true`
verifies and packages without publishing.
## Where the bugs have actually come from
Every defect found in this crate so far has been one of four shapes. They are
worth checking for by name in any change:
1. **A rule applied to one symbology but not the other.** The human-readable
text line was placed a fixed one module below the symbol — correct for
Code 128, which has no vertical quiet zone, and a quiet-zone violation for
QR, which needs four clear modules underneath.
2. **Two limits chosen independently that have to agree.** `MAX_ENTROPY_BITS`
allowed a body longer than `MAX_LEN` permitted, so the generator could mint
IDs its own parser refused. Tie limits like these together with a
`const _: () = assert!(...)` so they cannot drift.
3. **A validated type reachable through a back door.** Deriving
`Deserialize` on `TrackingId` let any string in, bypassing `parse`
completely. Anything that constructs a validated type must go through its
validator.
4. **A guard that does not guard what its comment claims.** The image size
limit was per-axis while the allocation is the product of both.
And one about tests: a test that passes because a decoder is lenient proves
nothing about a scanner. Assert on the rendered pixels.
Fixed instances of all four are described in
[CHANGELOG.md](CHANGELOG.md#unreleased).