# src/core/util — small shared pure utilities

## Scope

Tiny, dependency-free pure helpers linked by both artifacts: whole-file byte
loading, unit-interval clamping, the absolute-path rejection test, and the
per-segment envelope curve law.

## Modules

- `file_bytes` (`core/util`) — the ONE whole-file byte loader (Q-W1), linked by both artifacts; blocking I/O, off-audio-thread only.
- `clamp01` (`core/util`, header-only) — the ONE unit-interval clamp (Q-W1), replacing four per-module static copies; NaN passes through unchanged rather than collapsing to a bound.
- `curve_law` (`core/util`, header-only) — the ONE per-segment envelope curve law: the
  exponent domain (0.1..10, neutral 1.0), the normalized-position -> normalized-level map, the
  mid-segment inverse an overlay knot drags through, and the inner dial's own norm<->exponent
  travel. Header-only and dependency-free so the engine's evaluator, the overlay's forward map,
  its inverse, and the deck's inner dial all read one law instead of four copies.
  **The neutral exponent is the IDENTITY, bit for bit** — that is what makes an instance saved
  before curves existed play unchanged, and what the knob law's centre detent exists to keep
  reachable from the dial.
- `relative_path` (`core/util`, header-only) — the ONE absolute-path rejection test behind the relative-paths-only invariant, shared by `bank_model` (`Sample.relativePath`) and `core/tracking/origin_ledger` (`OriginRecord.relativePath`). The two must reject identically or a path one accepts could be smuggled past the other; that is why it is one function and not two.

## Gotchas

- `clamp01` lets NaN pass through unchanged rather than collapsing it to a bound —
  this is deliberate (it replaced four per-module static copies that already
  behaved this way); don't "fix" it to clamp NaN to 0 or 1.
- `file_bytes` does blocking I/O — off-audio-thread only, never call it from
  `process()`.
