smart-package-tracker 0.4.0

Generate package tracking IDs, render them as Code 128 barcodes (PNG and SVG), and read them back out of images
Documentation
# 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

| Path | What lives there |
|------|------------------|
| `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
  cargo check --no-default-features --features "$f" --quiet || echo "FAIL [$f]"
done
```

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).