Skip to content
Back to skills

Playback

ASecurity

Pulp timeline transport, immutable compiled playback programs, bounded arrangement audio rendering, block-level publication latches, stable shells, and ProcessContext projection.

  • 22 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 3, 2026
toolsjavascriptrustgojavashellnodeexpressawstestinggit

Works with

  • cursor
  • terminal
  • cli
  • api

Security analysis

A100/100

Scanned September 20, 2026

npx -y skills add danielraffel/pulp --skill playback --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Playback?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Playback
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/danielraffel-playback/badge)](https://www.skillsdirectory.com/skills/danielraffel-playback)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: playback
description: Pulp timeline transport, immutable compiled playback programs, bounded arrangement audio rendering, block-level publication latches, stable shells, and ProcessContext projection.
---

# Playback

Playback has two independent public surfaces: `MasterTransport` for the block
clock, and immutable compiled playback programs. Build a
`ProgramCompileRequest` from one captured immutable Project snapshot, an
external monotonically increasing document revision, a shared precompiled tempo
map, an explicit sample rate, and an explicit `DirtyTrackSet`. The request rate
is invalid by default; set `ProgramCompileRequest::sample_rate` from the engine
configuration and compile the tempo map at that same exact `RationalRate`.
Submission normalizes both and fails synchronously if the request rate is
invalid, above the compiled-rate ceiling, or differs from the map, preventing
one program from mixing sample domains. Drive it with
`DeferredCompileExecutor::run_for()` on threadless/UI hosts or use
`WorkerCompileExecutor` on native threaded hosts. The compiler is the sole
publisher to its `PlaybackProgramStore`.
Use sparse `TrackCompilePolicy` deltas when a track changes provider selection
or adoption policy. The compiler validates availability, forces that track
through the dirty path, retains omitted published policies, and coalesces
pending deltas with latest-track wins.
For this phase, only `ProviderKind::Arrangement` with the arrangement-only
availability mask is valid; reject launcher or external-input claims until
their provider payloads are compiled.

For MediaRef clips, prepare a `DecodedAudioAssetPool` off the audio thread. Use
`DecodedAudioAssetPool::decode_wav()` for bounded in-memory WAV bytes, then pass
the immutable pool in `ProgramCompileRequest::audio_assets`. The existing
compiler incrementally lowers media clips into each `TrackProgram`; do not build
a second playback-program model. `TimeConform::None` uses the existing bounded
native-rate source mapping after sample-rate conversion. `TimeConform::Resample`
uses bounded stateless varispeed: map each
rendered musical tick to the same fraction of the referenced source range and
derive the effective source step for anti-aliasing. Use the compiled tempo map's
analytic fractional sample-to-tick inverse for ordinary playback and precise
host ticks for host beat mapping; sample-fraction interpolation across a tempo
ramp is not musical phase. `TimeConform::Stretch` is compiled off the audio
thread: slice the source, fixed-SRC it into the compiled timeline-rate domain,
drive the finite stretcher with an analysis-boundary tempo schedule, and publish
only an immutable artifact with exactly the authored timeline frame count.
Use the scalar double finite builder for deterministic offline compilation,
then convert its exact result to the public float artifact in bounded blocks.
Document-tempo playback consumes that artifact 1:1. For live host-tempo
projection, prepare a complete `RealtimeStretchProgramRuntime` off the audio
thread and stream the artifact through its preallocated low-latency processor;
the audio callback may stretch but must never allocate, lock, or prepare DSP.
The runtime publishes one fixed causal latency for all parallel audio and MIDI
paths and resets coherently on transport/program epochs. Keep
source/tempo/algorithm semantic identity in the artifact cache key and document
revision/program generation in separate provenance.
Compiler work-block size is scheduling only and must change neither key nor
output. Never route `Stretch` through `Resample`, pad/trim a length mismatch, or
fall back to `None`.
Gain and anchor-native fade durations live on the immutable Clip. Missing,
mismatched, or over-capacity assets fail compilation instead of creating a
silent placeholder.
When sequence lowering flattens a complete nested media clip, preserve its
authored `TimeConform` value. A window that trims a `Resample` clip is lowered,
not refused: the leaf carries a source RANGE rather than a lone offset, as
`LoweredClip::source_frame_offset` plus `source_frame_phase_end`, which
`AudioClipRendererProgram` carries under the same names and
`musical_phase_source_position` reads as the two ends of its phase map. Both
ends come from the retained window's own fractions of the clip's authored tick
span, because that is what the conform function says; an elapsed-samples offset
agrees only where source frames and timeline frames happen to advance together,
which is exactly the case a real conform is not. A zero `phase_end` means the
media reference's own end, so an untrimmed leaf lowers to the program it always
did.
`Stretch` is lowered too, and the range is not what does it. Its audio is a
rendered artifact keyed to an authored tick range, so the artifact stays keyed
to that range — `LoweredClip::authored_window_start` and `authored_duration`,
the pair a trim already recorded for generated content, say where the range is —
and the leaf reads the frame span of the render that belongs to its window, as
`AudioClipRendererProgram::source_start` plus `source_frame_count` over an
artifact longer than the clip. `offline_stretch_artifact_window` in
`audio_renderer_internal.hpp` is the one place that arithmetic lives, and both
the artifact compiler and the program compiler go through it. Two consequences
worth knowing: a trimmed leaf and its untrimmed twin produce the same artifact
key, so they share one cached render and the trimmed leaf is bit-identical to
the untrimmed one over the same ticks; and the authored range can begin before
tick zero when a placement sits earlier than the offset it reads, which the
tempo map extrapolates and no document clip can express.
A stretched leaf keeps its whole media reference — narrowing it would change
what gets stretched rather than which part of the result is heard — so the
lowerer's elapsed-samples rebase is scoped to `TimeConform::None`. Anything
reading an artifact directly must offset by `source_start`: the realtime
stretch lane's `artifact_sample` does, and reading from frame zero there is
silently the wrong audio rather than an error.

Each nested refusal names one cause. A child device chain raises
`NestedDeviceChainUnsupported`, and an absolute-anchored leaf inside a nested
sequence raises `NestedAbsoluteChildUnsupported`. A child automation lane
raises one of three, because the constructs that would lift them differ: an
automated pan raises `NestedAutomationPanUnsupported` on entry to the child
track, since no leaf carries a stereo placement at any level. An automated gain
travels to the leaf and is answered by the leaf's own kind — a leaf that reads
no clip gain raises `NestedAutomationGainEventLeafUnsupported` and needs a
renderer that scales it before any envelope would matter, while one that does
read clip gain raises `NestedAutomationGainMediaUnsupported` and needs only
that `ClipPlaybackProperties::gain_linear` stop being a lone scalar. Do not reach for one
code to cover several constructs: the code is what tells an author which
construct is missing, and a generic one hides that. Two guards in
`validate_reference` are deliberately not capability codes — a nesting depth
past `kMaxSequenceNestingDepth` and a non-musical `SequenceRef` placement both
raise `InvalidStructure`, because `sequence_graph_validation` and
`Clip::create_absolute` already reject them at construction, so reaching either
means the document should not exist.

When host beat mapping intentionally makes musical material follow the host
tempo, keep absolute clips, take-comp segments, and frozen artifacts on
`TransportRange::timeline_sample_start`; those sources are sample-domain
content and must not inherit the beat projection. Carry precise fractional host
tick endpoints through every callback and nested `ProcessContext` projection;
integer `timeline_tick_*` fields remain compatibility metadata and must not
drive host-mapped interpolation, loop admission, note scheduling, automation
refinement, MIDI clock, or metronome enumeration. A precise host-mapping
rejection must not fall back to document-tempo placement. For musical audio,
derive the
effective source-position step per output frame. A converter prepared only for
the asset-rate/timeline-rate ratio cannot anti-alias faster host playback, so
prepare and share a per-MediaRef-range multiresolution audio pyramid off the
audio thread. Build it incrementally inside the compiler work budget, count it
against both converter-count and aggregate prepared-byte limits, including
persistent sinc tables and container storage, and seed unchanged programs back
into the cache. Clamp every pyramid level to the exact
referenced source range so neighboring asset frames cannot bleed into a clip.
Fixed-rate and variable-rate kernel construction are part of that same
incremental budget: initializing a converter must not synchronously populate
every sinc phase before yielding.
Use fixed-size, incrementally allocated prepared chunks whose persistent
footprint is computable; implementation-defined container bookkeeping cannot
sit outside the byte cap.
Each 2:1 stage must low-pass before decimation; select the coarsest level that
leaves a bounded residual step, then use its prebuilt reconstruction kernel. Do
not approximate extreme ratios by clamping a tiny cutoff onto a
fixed-width source-rate kernel: once the sinc support contains too few zero
crossings, normalization turns it into a short moving average and aliases
despite the nominal cutoff. The fixed asset-rate/timeline-rate path therefore
fails compilation beyond its honest kernel range, while host-tempo playback
uses the prepared pyramid to retain its wider bounded contract.

An active take lane replaces the track's arrangement source; zero
`active_take_lane_id` selects arrangement clips. The compiler lowers each
canonical comp selection to an `AudioClipRendererProgram` with
`SourceKind::TakeCompSegment` and a one-based lane ordinal. The typed origin
keeps repeated selections from one take distinct without inventing project
identities. Lower one selection per compile work unit, count arrangement
regions and comp selections against the same whole-program `max_clips`, and
require the take rate, asset metadata, and decoded audio rate to agree.
Inactive lanes remain document data and contribute no playback regions.

A selected `TrackFreeze` supersedes both the arrangement and active take comp
with one `AudioClipRendererProgram` whose `SourceKind` is `FrozenTrack` and
whose stable identity is the owning track. It is a sealed post-device artifact:
the compiler emits no authored clip/note events, ordered device placements, or
automation program for that track, while leaving all authored document state
intact for unfreeze. Count the artifact against the same whole-program
`max_clips`, validate its project asset, decoded audio, media range, and sample
rate exactly, and reject coordinate/SRC overflow before publication. A dirty
freeze/unfreeze edit must rebuild the track program; replay selects the sealed
asset and never re-renders it. Desktop graph binding therefore accepts no
device routes for a frozen track and rejects stale routes as unexpected,
preventing a post-device freeze from traversing the authored chain twice.

On the audio thread, call `PlaybackProgramBlockLatch::begin_block()` exactly
once per callback and pass that pin to every `StableRendererShell`. Never cache
a `TrackProgram*` past the pin. Adoption accepts skipped generations
(`candidate > active`) for the same ItemId and proves carry-state ownership
against the shell's `RendererCarryState` SeqLock snapshot.
The host's `TimelineGraphBinding` is the deliberate exception to independently
latching `PlaybackProgramStore`: its enclosing immutable binding generation
already owns the exact `PlaybackProgram` together with the exact graph snapshot
and renderer set. It constructs a non-owning `PlaybackProgramBlock` only while
that generation is pinned, so program destruction/refcount traffic still never
runs on the audio thread. Content adoption republishes the whole binding
generation; do not reintroduce a separate store latch there.

For arrangement note playback, construct one `ArrangementNoteRenderer` per
track, call `prepare(maximum_events_per_block)` off the audio thread, then pass
the shared block pin and the current `TransportSnapshot` to `process()`. The
renderer owns a bounded realtime-limited MIDI buffer; inspect `events()` only
for the current block. The buffer carries a full-resolution native MIDI-2 UMP
sidecar alongside its MIDI-1 compatibility mirror; treat the two lanes as one
atomic event block and propagate either lane's overflow. It consumes both
transport ranges in order, releases
active notes before the second range on a loop wrap, and intentionally resets
without note chase on seek/adoption in Phase 1. `TransportSnapshot` carries the
non-owning identity of the exact compiled tempo map that resolved its ranges;
the renderer rejects a program compiled against another map. Overlapping
logical notes on one MIDI key are reference-counted into one physical note-on
and one final note-off.

Event-stream delay compensation rides on that same `process()`. The three-argument
overload takes an `EventCompensationShift` (samples, `std::int64_t`) and reads
each transport range from `range.timeline_sample_start + shift` instead of the
range's own origin, so a chain that delays the events themselves still lands
them on the sample the document authored. Non-obvious parts, in the order they
bite:

- **Shift the window, never the event data.** `PlaybackProgram` is immutable and
  shared by the offline and realtime paths. Folding a host latency into
  `NoteProgramEvent::sample` makes the program a function of the host graph and
  forces a recompile on every device swap. The addend belongs on `range_start`.
- **Per range, never per block.** A block straddling a loop wrap carries two
  monotonic ranges; shifting the block would read the second from the first's
  origin and replay the pre-wrap window. The no-chase-on-seek rule applies to the
  *shifted* range for the same reason.
- **A changed shift is held until the transport stops.** The renderer latches the
  first value it is given and only adopts a different one while
  `is_playing` is false, reporting `shift_relatch_pending` until then. Adopting
  mid-playback would displace every later event by the delta.
- **A host-beat-mapped range refuses a compensating shift**
  (`NoteRenderCode::CompensationUnsupported`). That range locates events by
  authored tick against the host's beat window, so a document-sample shift has
  nowhere to land, and converting it to ticks is what the unit rule forbids.
- **Reading ahead past an enabled loop's end folds back to the post-wrap
  content.** What belongs in that window is what the musician hears when those
  frames reach the device, which is the content after the wrap and not the
  document positions past the loop point. `plan_compensated_read()` splits the
  window at the loop point into at most two runs — the loop length is never
  shorter than the maximum block, so one window crosses at most once — and each
  run carries the loop pass it belongs to, so note modifiers resolve against the
  right pass. The renderer releases what was sounding at the *stream's* wrap,
  which arrives a shift before the transport's, and then suppresses the
  transport's own discontinuity for a wrap the read-ahead already served:
  serving it twice would cut the post-wrap notes read-ahead had already started.
  The suppression is scoped to the compensated, looping, non-scrubbing case, so
  a scrub-window restart still releases. The whole path is skipped — including
  the loop's tempo-map conversion — when nothing compensates.
- **An event-to-audio device contributes nothing to the shift.** The graph's own
  delay compensation already aligns its audio output against every sibling
  branch; adding it again pulls the stream early by exactly the amount the graph
  handled. Only event-domain latency moves the window. Accumulate it with
  `accumulate_event_chain_shift()`, which range-checks against the ceiling the
  caller supplies rather than declaring a second latency constant.

The full contract, including what is still open, is
`docs/policies/event-stream-pdc.md`.

Compile an unattached `AutomationLane` with `AutomationProgram::compile()` on
the control/worker thread. The immutable program owns its exact tempo map and
retains tick-domain segment semantics. Each compile also receives a nonzero
instance token; generation orders adoption, while the token prevents an equal-
generation replacement from masquerading as the active immutable program.
`AutomationCursor::process()` consumes
the shared transport snapshot and writes plain-domain control points into a
caller-owned span. Each point says whether it seeds a range, steps immediately,
or ramps linearly from the preceding emitted point. Span capacity is the
explicit per-lane budget: range seeds and unique in-range authored knots are
mandatory, remaining capacity refines continuous spans deterministically, and
output never overflows. Keep device-wide budgeting, lane aggregation, parameter
metadata, normalization, and the SignalGraph mailbox write in the host binding;
playback must not depend on `pulp::state` merely to mirror
`ParameterEventQueue`.

Group already-compiled lane owners with `TrackAutomationProgram::create()` on
the compiler thread. The aggregate validates a compiler-supplied track ID,
requires exact tempo-map owner identity, rejects duplicate lane IDs and
device-parameter targets, and stores programs in lane-ItemId order. Preserve
unchanged program owners when rebuilding it: mixed child generations are
intentional because each cursor adopts by its lane program's generation and
instance token.

`ProgramCompiler` is the attachment boundary for authored automation. It walks
each track's ordered device placements, compiles only lanes owned by that track,
and publishes the resulting `TrackAutomationProgram` inside the immutable
`TrackProgram`. Use `AutomationPlaybackLimits` on every compile request: reject
over-limit device, lane, and point counts before reserving proportional storage,
and use `platform_defaults()` so wasm/threadless builds receive their lower
budgets. Incremental compilation retains unchanged lane owners; attachment,
target, or point edits dirty only the affected track/lane.

On the audio thread, give one `TrackAutomationRenderer` the exact immutable
track automation program and the shared transport snapshot. It emits bounded
per-device `ParameterEvent` batches in device-placement order: seeds become
zero-duration endpoints, linear points preserve their ramp duration, and
immediate points step at their sample offset. Candidate traversal and emitted
events have separate limits. A mandatory event that cannot fit fails the whole
block without exposing partial device batches; optional refinement points may
coalesce deterministically. The renderer owns all scratch storage after
`prepare()` and performs no allocation in `process()`.

Use this skill when changing `core/playback`, the master timeline transport, or
the format-layer projection from playback snapshots to `ProcessContext`.

## Contracts

- Playback owns integer `TickPosition`, `SamplePosition`, and `MonotonicBeat`
  state. Floating-point beat values exist only in the one-way format projection.
- A block has one range normally and at most two ranges when it crosses one loop
  boundary. `prepare()` rejects a loop shorter than `max_buffer_size`, which is
  what makes the fixed two-range representation complete.
- Timeline ticks wrap at the loop boundary. `MonotonicBeat` never wraps or
  reanchors on a seek; only a new prepare/reset lifecycle starts a new clock.
- Scrubbing is a transport mode, not a renderer feature. `begin_scrub()` /
  `scrub_to()` / `end_scrub()` make the transport emit repeated windows that
  restart on the latest posted anchor, so a dragged playhead is audible without
  a single line of scrub-aware code in any renderer: a window restart is
  structurally a loop wrap (reposition + `discontinuity` + block split), which
  the note and automation renderers already handle. Do not add a scrub branch to
  a renderer; make the transport produce the right ranges instead.
- The scrub anchor is **latched, not immediate**: a newly posted position takes
  effect at the next window boundary. That makes the grain rate the window
  length rather than the UI event rate — posting at 60 Hz against an immediate
  anchor would machine-gun sub-grain restarts. The window must be at least
  `max_buffer_size` (`begin_scrub` rejects shorter, and `begin_block` clamps
  anyway) so a block still spans at most two windows and the fixed two-range
  representation stays complete.
- **Scrubbing suspends loop wrapping.** A drag states a position directly, so
  the transport must not pull the window back to the loop start or make
  positions outside the loop unreachable. The loop is still reported in the
  snapshot (a UI keeps drawing it) and wrapping resumes on the first block after
  `end_scrub()`, which parks the playhead on the anchor the drag released on.
  This also keeps a loop wrap and a window restart from ever needing a third
  range in one block.
- While scrubbing, `is_playing` is true even when the musical transport is
  stopped — consumers that only care whether the playhead moves need no scrub
  branch — and `scrubbing` distinguishes the mode. Entering and leaving scrub
  set `reset_requested`; the window restarts in between deliberately do not,
  because they recur many times a second and `discontinuity` already describes
  them.
- Two existing discontinuity consumers inherit scrub behavior on purpose, and
  both are correct as-is: `CaptureEngine` cancels active takes on a
  non-loop-wrap jump, so scrubbing aborts a recording rather than splicing it,
  and `ExternalSyncOutput` emits a song-position/MTC update per window restart,
  so slaved gear chases the drag. A scrub block carries at most two ranges, the
  same as a loop wrap, so neither exceeds `max_messages_per_block`. Do not add a
  scrub branch to either; if the behavior needs to change, change what the
  transport publishes.
- `TempoSyncSource` is the backend-neutral session-tempo boundary. Its only
  virtual operation is the realtime `capture_audio_block()` mapping/command
  exchange. Backend enablement, peer discovery, and start/stop-sync policy do
  not belong on the interface; the desktop `AbletonLinkTempoSync` adapter owns
  those Link-specific controls.
- A configured `TempoSyncSource*` is non-owning and must outlive
  `MasterTransport`. It switches callers to the host-time `begin_block`
  overload. Its opaque `TempoSyncHostTime` is created by the source and tagged
  with that source's clock domain; a default token or a token from another
  source fails before capture. The timestamp names the first sample at the
  output boundary, so the audio-device layer must add output latency before
  entering playback. A missing host time, disabled backend, backend failure, or
  invalid mapping fails closed and never advances on the document clock.
- Joining an external tempo session is passive. `prepare()` does not broadcast
  `initial_position` or `initially_playing`; only later explicit `seek()`,
  `set_playing()`, or `set_tempo_sync_tempo()` calls become one-shot commands
  on the next audio block. Applied generations advance only after a valid
  capture, so a failed block retries the command rather than losing it.
- Session-tempo projection still obeys the fixed one-or-two-range contract,
  including one loop wrap and precise fractional host ticks. `begin_scrub()`
  rejects an active sync source: scrubbing owns a private repeated-window clock
  and cannot share authority with a network beat mapping. The audio-thread guard
  rejects any impossible mixed state defensively as well.
- Both document-tempo and session-tempo blocks publish through the same
  canonical block/range projection pipeline. Keep flags, meter anchoring,
  monotonic ticks, host mapping, and previous-state publication there; source
  paths should only derive their mode-specific projections.
- A tempo source must preserve the host-clock time at which its reported
  `is_playing` state becomes effective. `project_tempo_sync_playing()` applies a
  transition at or before the first sample and defers one inside or beyond the
  half-open block, because `TransportSnapshot::is_playing` is block-wide. Keep
  this quantization explicit; silently discarding the timestamp makes remote
  starts and stops early, while pretending to split them would contradict the
  snapshot consumed by renderers.
- Keep `tempo_sync.cpp` in `PulpPlaybackSources.cmake`, which mirrors it into
  native, threadless, WAM, and WebCLAP builds. Keep SDK-backed adapters such as
  `adapters/ableton_link.cpp` outside `core/playback/src/` and in a separate
  non-installed target; the source-closure gate treats every `src/*.cpp` as
  portable, so an SDK-backed translation unit there would be pulled toward the
  wasm lanes.
- A stopped block still emits one range covering all callback frames, but both
  musical clock intervals have zero duration.
- The control thread is the sole writer of the complete desired-state `SeqLock`.
  `begin_block()` is the sole audio-thread consumer and must remain allocation-
  and lock-free. It is declared `AudioCallbackSafeAfterPrepare`, wraps itself
  in `ScopedNoAlloc`, and its test uses `ScopedRtProcessProbe` so Unix CI traps
  both allocations and pthread locks.
- Starting playback is not a seek or DSP reset. Explicit seeks request a reset;
  range discontinuities project to `ProcessContext::transport_jump`.
- Arrangement note events are compiled against the owning program's exact
  tempo map and ordered by sample, note-off before note-on, then clip/note ID.
  A renderer uses half-open sample ranges and never latches a callback size.
- Automation values are evaluated at the tempo map's canonical tick for each
  selected sample. Do not interpolate by sample fraction across tempo ramps.
  Each loop/seek/adoption range is reseeded, stopped blocks emit only when
  reseeding, and same-lane adoption requires a strictly newer generation.
- Attached automation compilation and rendering remain portable playback code.
  Mirror every new playback translation unit into the native target, the
  no-exceptions target, and both WAM/WebCLAP curated source lists; keep
  `web-timeline-source-closure` green. This proves wasm compilation only, not a
  JavaScript timeline API or host parameter delivery.
- Audio and note renderers must consume the same `TransportSnapshot` for a
  callback. The replay golden uses a varying schedule up to the transport's
  prepared `max_buffer_size`; never cache the first callback size in either
  renderer or bypass `MasterTransport`'s upper-bound rejection.
- `StableRendererShell`, `ArrangementAudioTrackRenderer`, and
  `ArrangementNoteRenderer` expose control-thread `reset()` for a successful
  quiesced sample-rate or maximum-block-size lifecycle change. Reset every
  bound renderer together after graph reprepare; note reset also clears active
  counts, pending flush/overflow state, current event buffers, and block index.
- Note rendering is a transport-tick MIDI lane. Do not lower it to an audio
  `CustomNodeType`; the host/embedded adapter routes its bounded MIDI output.
- `core/playback` must not include `pulp/format`, `pulp/host`, or `pulp/view`.
  `<pulp/format/playback_context_projection.hpp>` owns the one-way adapter.
  Keep `timeline-engine-dependency-floor` green; it allowlists source includes
  and CMake links for timebase, timeline (when present), and playback. The link
  check reads `target_link_libraries` dependencies and skips the configured
  target plus target-defining commands, so a subsystem-local helper executable
  whose own name shares the module prefix (e.g. `pulp-timeline-schema-emit`) may
  link `pulp::timeline` without tripping the floor.
- A follow action's period is anchored to `LaunchHandle::last_start()` — the
  monotonic beat the launch RESOLVED to — never to the monotonic origin and
  never to the block that carried the Start. `FollowActionTimer` builds a
  `LaunchQuantize` whose phase is that beat and walks it with the same
  `next_launch_boundary()` / `resolve_launch_sample()` pair a launch uses, so
  the fire inherits the launch's sample accuracy across a loop wrap for free.
  Recovering the launch beat from a Start event's sample offset instead would
  round through the tempo map and lose that exactness.
- A test whose launch lands on a multiple of the follow period CANNOT tell a
  launch-anchored grid from an origin-anchored one — both produce the same
  boundaries, so re-anchoring to phase 0 keeps such a test green. Prove the
  anchoring with an OFF-grid launch (an immediate launch from a non-beat
  `initial_position`); only then does the fire sample separate the two.
- The compiler asks `clip_content_role()` what a clip contributes before it
  compiles anything, and that classifier visits `timeline::ClipContent` through
  `ClipContentCases` — an overload set with no generic fallback. Do not go back
  to testing alternatives inline with `holds_alternative` / `get_if`. A clip
  whose content kind the compiler does not recognize produces no audio program
  and no notes, and nothing anywhere reports it: the document is intact, the
  compile succeeds, and the track is silent. Routing every content decision
  through one exhaustive classifier turns that into a build failure at the point
  where somebody has to decide whether the new kind renders. `audio_renderer.cpp`
  carries the matching `static_assert` on the alternative count, because its
  "not a `MediaRef` means not audio" assumption lives there too.
- `ArrangementAudioRenderer::process()` clears output, validates the complete
  zero/one-wrap snapshot, and mixes arrangement-selected tracks in stable
  PlaybackProgram order. It is immutable-input RT safe, wraps `ScopedNoAlloc`,
  and must remain covered by `rt_allocation_probe`. Mono duplicates on wider
  output, multichannel-to-mono averages, wider sources map by channel, and the
  engine does not clip or normalize deterministic float sums.

### A per-pass decision splits across compile and render — put each half where its inputs are

Per-note playback modifiers (probability, pass condition, ratchet) are the
worked example. The split is not a style choice; each half sits where its inputs
exist:

- **Authored, pass-independent → compile time.** A ratchet count is a pure
  function of the content, so `program_compiler.cpp` lowers a ratcheted note into
  N on/off pairs that tile the authored span, with the last subdivision landing
  on the note's own end so repeats never drift. A subdivision that collapses to
  zero samples at the compiled tempo fails the compile rather than emitting an
  on with no off.
- **Pass-dependent → the renderer.** Probability and the pass condition cannot be
  decided at compile time without freezing every pass to one answer, so they are
  evaluated in `ArrangementNoteRenderer::process()` against a pass index.

The pass index is **transport-owned, never renderer-local**. Each
`TransportRange` carries `loop_pass_index`; the master transport and host
projector advance it at a wrap and re-anchor it on start, seek/jump, or loop
identity changes (including precise fractional host bounds). A renderer may be
created mid-playback, skip a callback, or fail a bounded output flush and still
observes the authoritative pass on its next range. Do not reconstruct the pass
from `MonotonicBeat`: its signed tick storage intentionally saturates at the
domain boundary.

Two properties make the gate safe to apply per event. The pass index is constant
across a range, because a wrap always starts a new range — so a note's on and its
off resolve against the same pass and the gate can never admit one without the
other. And the decision is a pure function of `(draw key, pass index)`, so no
draw state crosses blocks and evaluation order cannot change a result. Anything
seeded on the audio thread must have this shape: fold the seed and the identity
into one key at compile time, then mix it with the pass index in `process()`.

Side data a renderer needs per event goes in a **sparse table on `TrackProgram`
looked up by item id**, not a field on `NoteProgramEvent`. That struct is 40
bytes and the scale suite compiles ten million of them; a `std::uint32_t` index
would not fit the existing padding and would grow every event by eight bytes to
carry data almost no note has. An empty-span check makes the common case free.
Sorting such a table is real work, so it gets its own budgeted
`BudgetedStableMergeState` stage rather than a bare `std::sort` inside a compile
slice.

### Clip fade evaluation lives in one header, and `AudioClipRendererProgram` is built positionally

The clip envelope (gain, fade in, fade out, fade shape) is evaluated in
`core/playback/src/clip_fade_envelope.hpp` and nowhere else. Before it existed
the same arithmetic had three homes — a whole-frame and a fractional overload in
`audio_renderer_render.cpp`, plus a byte-identical fractional copy in
`realtime_stretch_renderer.cpp` — so a fade behavior added to the normal render
path silently did not apply under live stretch. The two overloads that survive
are split on numerics, not on contract: the whole-frame one computes the
remaining-frame count as exact integer arithmetic, the fractional one clamps a
`long double` that can land past the last frame. Anything that reads the
authored shape belongs in `fade_gain`, which both call.

The fractional overload narrows progress to `float` before it calls `fade_gain`,
and that narrowing is load-bearing rather than cosmetic. `fade_gain` is a
template that deduces its type from the argument, so handing it the `long double`
position instantiates a double-width sin for `EqualPower` — once per output
sample on the realtime stretch path — while the gain is narrowed to float on
return regardless, so the width buys nothing. Nothing guards this: the RT probes
look for allocation, and a wider sin does not allocate. It is also easy to
under-read on a Mac, because the lowering is arch-dependent — `long double` is
`double` on arm64, so the wide call shows up there as `_sin`, where x86_64 gets
the 80-bit `_sinl`. Verify on the emitted object rather than at the source level,
since a cast that deduction discards still compiles:
`nm -u build/core/playback/CMakeFiles/pulp-playback.dir/src/realtime_stretch_renderer.cpp.o | grep -i sin`
should report `_sinf` and nothing wider.

Fade **progress is measured in frames**, in every shape. The compiler converts
authored fade endpoints from ticks to frames
(`audio_renderer.cpp`, the musical branch of the clip lowering); the renderer
then normalizes position against that frame count. So a nonlinear shape needs no
tempo mapping of its own — it is a reparameterization of a progress value that
is already in the time domain, and it inherits exactly the tempo behavior the
linear ramp always had. This is also the acoustically correct answer: a
constant-power crossfade is a statement about power against *time*, not against
beats, so measuring progress in ticks would make the same authored fade dip
differently on either side of a tempo change.

`AudioClipRendererProgram` is brace-initialized **positionally** in four places
in `audio_renderer.cpp` (the offline-stretch, native/resample, take-comp, and
frozen-track paths). Inserting a field mid-struct shifts every later initializer.
It fails closed only when the adjacent types differ — two neighbouring
`std::uint64_t` fields would swap silently and compile. Grep every
`AudioClipRendererProgram{` when the struct grows, and prefer adding to the end
of a run of same-typed fields.

### Track mixer

- **Track mixer.** `TrackProgram::mixer()` carries the track's own
  `gain_linear`/`pan` with any lanes that automate them already resolved to
  borrowed `AutomationProgram` pointers. It is applied inside the clip
  accumulate in `audio_renderer_render.cpp`, so the whole-program mixdown and
  the per-track graph renderer stay in agreement — applying it in only one would
  break offline/live parity. A lane **supersedes** the authored constant rather
  than multiplying with it, and `TrackMixerProgram::transparent()` short-circuits
  an untouched track back onto the exact pre-mixer code path. Pan is a balance:
  it attenuates the opposite side, never boosts, is inert below two channels, and
  is exactly unity at centre.
- **Mixer lanes never reach device delivery.** `TrackAutomationRenderer` skips
  any lane whose `device_target()` is null, and so does the admission scan in
  `core/host/src/timeline_automation_delivery.cpp`. A mixer lane still lives in
  the track's `TrackAutomationProgram`; it just has no device to address.
- **One curve evaluator.** `select_automation_segment` and
  `evaluate_automation_segment` in `automation_program.cpp` are shared by the
  device-delivery cursor and `TrackMixerControlCursor`, so an automated fader and
  an automated plugin parameter cannot read the same curve differently.
  `TrackMixerControlCursor` is forward-only — `restart()` before revisiting an
  earlier position, which the render loop does per channel and per transport
  range.

## A clip carrying MIDI expression lanes is refused, never compiled without them

`MidiContent` carries controller/expression lanes beside its notes, and nothing
downstream of the compiler reads them: `program_compiler.cpp` builds a track's note
program out of `notes()` alone. Compiling a lane-bearing clip would publish a
program that plays the notes with every authored controller point gone and nothing
to read the loss from — the document keeps the lanes, so authoring, saving,
reloading, and copying all behave while playback quietly ignores them.

Two refusals prevent that, and they are **not** the same statement:

- `CompileErrorCode::MidiExpressionLaneUnsupported` — raised in
  `program_compiler.cpp` when a clip is first seen in `Stage::CompileTracks` with a
  non-empty `lanes()`. This is the general gate. Every clip a program is built from
  reaches that point, whether authored on the track or generated by lowering a
  nested sequence, so it covers the whole surface rather than one path. A renderer
  that chases and emits lane values is what removes it.
- `CompileErrorCode::TrimmedMidiLaneUnsupported` — raised earlier, in
  `sequence_content_lowerer.cpp`, when a nested clip's content is rebuilt for the
  retained window. A **controller lane has no correct trim**: a point *outside* the
  window can be the value sounding *inside* it, so dropping it changes what the
  controllers say and keeping it puts a point outside the clip. That question
  survives the renderer landing, so this refusal outlives the one above.

**If you are implementing controller chase or expression semantics, both refusals
are your markers.** Deleting `MidiExpressionLaneUnsupported` is correct once the
note program carries lanes; deleting `TrimmedMidiLaneUnsupported` is not — it needs
a decided boundary value, most likely a point synthesised at the window edge from
the last value at or before it. That is why they are separate codes: collapsing
them into one would delete the trim guard by accident when the renderer lands.

The pair is proved in `test_timeline_nesting_playback.cpp` (target
`pulp-test-timeline-nesting`): a flat lane-bearing clip is refused, the same clip
without lanes still compiles — so the guard is not "refuse every MIDI clip" — and a
trimmed nested lane-bearing clip still reports the trim code.

## Validation

Configuring a fresh build dir for these suites needs
`-DPULP_ENABLE_DESIGN_IMPORT=ON` **passed explicitly** whenever the cache has
ever held OFF: `PULP_BUILD_TESTS=ON` hard-requires it, and a cached OFF survives
a reconfigure that does not name the option, so the configure fails on an option
combination unrelated to anything you changed. Passing it every reconfigure is
cheaper than recognising the error a second time. (The `ci` skill covers the
other half of this option — the OFF-side link break the release lane guards.)

Build and run `pulp-test-playback-automation-cursor`,
`pulp-test-playback-track-automation-program`,
`pulp-test-playback-track-automation-renderer`, `pulp-test-playback-program`,
`pulp-test-playback-transport`, `pulp-test-timebase`, and
`pulp-test-transport-quantizer`, plus `pulp-test-playback-audio-renderer`
(which carries the track-mixer cases, including the proof that a gain lane moves
the rendered samples rather than merely existing in the document). Keep loop-boundary, variable-block, ramp,
negative-preroll, extreme-position, SeqLock hammer, and RT-allocation cases.
Track-freeze changes also require `pulp-test-timeline-graph-binding`: prove the
artifact routes directly after the authored chain, a stale device mapping is
rejected, and a dirty thaw restores arrangement/device compilation.

`pulp-test-playback-note-renderer` also fuzzes the no-stuck-notes property:
fixed-seed randomized seek/loop/play sequences over overlapping notes assert
the physical MIDI stream is a per-key on/off toggle (a note-on only for an idle
key, a note-off only for a sounding key), and a terminal stop-flush must leave
`has_active_notes()` false with every note-on matched by a note-off. Seeds are
hardcoded so a red is a real defect, not a flake; keep the non-vacuity witnesses
(notes held live across seeks and loop wraps) asserting above zero so the
all-clear cannot go vacuous. The toggle invariant and terminal balance are NOT
enough on their own — they are both structurally guaranteed regardless of the
seek/loop flush: `emit()` folds logical overlaps so the physical stream is a
clean per-key toggle even when a discontinuity strands a note, and the terminal
stop-flush always rebalances the counts. A stranded note is only observable
against an independent coverage oracle: a key may sound only while the playhead
sits inside the union of that key's compiled note extents, so a still-sounding
key whose playhead has moved past every extent is the stuck note. Keep that
oracle (checked at each playing block's last played sample, stuck-direction
only — a note whose onset precedes the new range is deliberately not chased, so
covered-but-silent is legal) when touching this proof; without it, deleting the
`range.discontinuity` flush in `note_renderer.cpp` leaves the fuzz green.

The same file carries the scrub counterpart, which reuses that oracle over
randomized `begin_scrub`/`scrub_to`/`end_scrub`/seek/play/loop sequences. Its
non-vacuity witnesses are scrub-specific — window restarts that happened while
notes were sounding, and restarts that split a block — because a scrub fuzz that
never rewinds the playhead under a live note proves nothing. Deleting the
`pending_discontinuity_` assignment in `start_scrub_window()` (transport.cpp)
must red both that fuzz and the deterministic
`a scrub window restart releases the notes it strands` case; if it does not, the
scrub coverage has gone vacuous.

### A playhead-coherence test needs a cross-field invariant, not a changing value

`concurrent playhead readings are never internally inconsistent` runs the writer
and the reader on separate threads and asserts that every reading it observes is
internally coherent. Asserting only that the value changes would pass on a torn
implementation, which also changes. The invariant comes from the fixture: a
**step** tempo map makes `tempo_bpm` a pure function of `position`, so a reading
assembled from two different publishes pairs a position on one side of the step
with the tempo from the other, and no legal reading does that.

Two controls keep the test from going vacuous, and both belong in any test of
this shape. The same predicate runs single-threaded first, which proves the
invariant holds of a coherent reading before it is trusted to detect an
incoherent one. And the test asserts it observed at least one publish — without
that, a reader that never caught the writer would pass everything.

### The RT probe's wiring fails closed — keep it that way

`ScopedRtProcessProbe` has two backends. In the counting backend
`allocation_count()` reports what the harness `operator new` override saw. In
the trap backend — `PULP_NATIVE_CORE_PROCESS_RT_TRAP_TESTS=1`, the one every
playback RT suite uses on Unix — it **unconditionally returns 0**, because a
violation aborts the process before the assertion runs. So in a trap build the
`REQUIRE(allocations == 0)` line carries no information: the abort is the
signal, and the assertion is only there to keep both backends writing the same
test.

That looks like it should be silently vacuous whenever the trap translation
unit is not linked, since it is a strong override of a **weak no-op default** in
`core/native-components/src/native_core.cpp`. It is not, and the reason is worth
protecting. `RtNoAllocScope`'s constructor and destructor are declared in
`rt_test_scope.hpp` but **defined out of line** in
`test/native_components/rt_intercept_test_support.cpp`, so a registration that
sets the define while omitting the source fails at link:

```
Undefined symbols for architecture arm64:
  "pulp::native_components::test::RtNoAllocScope::RtNoAllocScope()", referenced from:
      CATCH2_INTERNAL_TEST_20() in test_playback_program.cpp.o
```

The counting backend fails closed the same way — `RtAllocationProbe`'s
constructor and the `operator new` override that feeds it live in the same TU
(`harness/rt_allocation_probe.cpp`), so they can never be split.

**Do not inline those constructors into the headers.** They look like trivial
one-liners begging to be moved, and moving them would convert a hard link error
into a probe that returns a hardcoded 0 forever. The out-of-line definition is
the guard.

What the link check cannot catch is a probe scope that does not actually
enclose the RT call, or an allocation the optimizer elides because nothing
escapes. Those need a control: put a `new` inside the scope whose result
escapes through a `volatile` sink, rebuild, confirm the binary aborts with
`[pulp-rt-trap]`, then remove it. Worth doing whenever you add a probe or doubt
an existing one — cheap, and it is the only way to tell a scope that proves
something from one that merely runs.

Copy the registration shape from `pulp-test-playback-program` in
`test/cmake/timeline_tests.cmake`: the `$<BOOL:${UNIX}>` source split,
`pulp::native-components`, `${CMAKE_DL_LIBS}` (the pthread interposers use
`dlsym`), and the generator-expression define.

Two things that waste time here. The trap message names the violation kind, so
`blocking lock inside no-alloc scope` means a lock, not a hidden `new` — do not
go hunting for an allocation. And restoring a patched test file with `mv` gives
it an mtime *older* than the object built from the patched copy, so `make` skips
the rebuild and you re-run the control binary believing you reverted; `touch`
after every revert.

When export/install wiring changes, also run the installed SDK consumer smoke.
Also build `timeline-program-threadless-no-exceptions-check`; it compiles the
program/compiler/executor/shell lane with `-fno-exceptions -fno-rtti` and the
threadless executor stub. Run the WASI SDK build when `/opt/wasi-sdk` is
available; the native compile-only gate remains mandatory when it is not.
Keep `pulp-test-timeline-replay-golden` green: it applies journaled gain, fade,
and note edits, replays from the checkpoint, and compares the audio/MIDI byte
stream with both the committed snapshot and the pinned fixture.
`web-timeline-source-closure` compares the native timebase, timeline, and
playback source lists with both curated production web ABI lists. Add a portable
engine translation unit to native, WAM, and WebCLAP ownership together.

`test/cmake/sampler_runtime_tests.cmake` also registers sampler Heritage
runtime tests. Those tests exercise `pulp::audio` profile/runtime behavior and
do not make Heritage profiles part of the immutable playback-program model;
keep that ownership boundary when extending the shared test inventory.

## Compile-context subscriptions and the exact dirty set

`compile_context_registry.hpp` is the invalidation half of the
compile-context subscription contract (the document/read half lives in
`core/timeline` — see the timeline skill). It exists because the compiler's
dirty set is exact rather than diffed: a renderer that reads a sequence-owned
context lane while compiling has no dirty item of its own when that lane
changes, so without a declaration it would render stale forever.

Three pieces, and the boundaries between them matter:

- `CompileContextRegistry` maps a content **schema type name** (the identity a
  `RegisteredContent` clip actually carries) to declared subscriptions. It
  refuses a duplicate type rather than overwriting — two renderers disagreeing
  about what a content kind reads would make invalidation depend on registration
  order. An unregistered type reads nothing, which is correct: no renderer
  compiles it, so there is no program that could go stale. Built-in MIDI is the
  deliberate exception: the program compiler reads its owning sequence groove,
  so `MidiContent` always subscribes to `Groove` without plugin registration.
  Media and empty content read none.
- `CompileInvalidationIndex::build()` is the kind → reader-track reverse index.
  Rebuild it when the document's **structure** changes; a context edit alone does
  not invalidate it, because editing a lane's contents does not change who reads
  it. It walks clips through the exhaustive `ClipContentCases` visitor, so a new
  `ClipContent` alternative stops the build here until someone decides whether it
  can subscribe.
- `resolve_dirty_tracks()` is the production translation from
  `timeline::DirtySet` to `DirtyTrackSet` (tests used to hand-build the latter).
  Its precision is documented per dirty-item shape in the header. Two shapes are
  deliberately conservative and should stay that way: an item with no owning
  sequence is project-scoped (tempo, meter, assets) and sets `all`, and a
  trackless item in this sequence that is not `DirtyFlags::Context`-flagged is a
  structural sequence edit and also sets `all`.

Production callers construct `ProgramCompileRequest::invalidation` from the
shared registry and exact `CommitResult`. Its constructor binds the dirty set to
that result's target snapshot, revision, exact predecessor snapshot, and an
immutable registry copy. Sparse reuse is allowed only when the predecessor is
the currently published project; a restored or forked lineage rebuilds in full.
`submit()` resolves that pinned input and remembers the generation that reached
publication. A different registry generation forces a full compile, including
at the same document revision. Do not resolve
outside the request and then drop the registry generation before submission.
Completion is keyed by `CompileTicket::submission_epoch` and
`CompilerStatus::latest_published_epoch`; revision equality is insufficient for
a same-document registry refresh. Treat the latter as a successful-publication
watermark: `latest_published_epoch >= submission_epoch` is terminal for a ticket,
meaning its request published or was superseded by a later successful
publication. Callers requiring exact-current document identity must also
require epoch equality and compare the published program identity/revision.
Epochs are scoped to one compiler instance: destroying the facade forfeits
completion observation, and a replacement compiler starts a new epoch domain.
If `busy` is false, an error with the watermark still below the ticket is
terminal failure.

### Trusted registered-content compilers

`CompileContextRegistry` owns the lowering declaration as well as its
invalidation subscriptions. A `ContentRendererRegistration` binds an exact
registered-content type, schema version, codec provenance, output kind,
fragment-note ceiling, state policy, production declaration, and an
off-realtime `noexcept` compile hook. Always call `declare(registration,
schemas)` with the immutable `SchemaRegistry` used to create or load the
content; the declaration records that registry identity and rejects mismatched
or duplicate provenance rather than accepting an order-dependent renderer.
The admitted surface is deliberately narrower than the enums suggest: notes
only, `RegisteredRendererStatePolicy::Reset` only, and at most 4096 fragment
notes per clip. `CarryByItemId` is refused until carried state has an exact
identity and lifecycle contract.

The hook receives `RegisteredContentCompileInput` with relative clip duration,
the validated payload, a `CompileContextView` narrowed to the declared
subscriptions, and the compiler's bounded note quota. Return an immutable
`ContentProgramFragment`; do not emit absolute arrangement positions or retain
the view. Missing exact resolution is
`CompileErrorCode::UnresolvedRegisteredContent`. A hook failure is
`RegisteredContentCompileFailed`. Output beyond the effective bound is
`RegisteredContentFragmentQuotaExceeded`, and its `CompileError` must carry the
offending clip plus exact `actual` and `limit`. Never degrade any of these to
silence.

Renderer production declarations participate in the compiled track and program
claims. Aggregate heterogeneous tracks with `timeline::weakest`; never preserve
the strongest claim just because it compiled first. The installed consumer
`examples/timeline-sdk-consumer/registered_chord_renderer.cpp` is the canonical
proof: schema and renderer registration, exact deterministic baseline and
semantic hash, exact `CommitResult` invalidation, epoch completion,
generated-track replacement with ordinary MIDI owner reuse, unresolved and
quota diagnostics, and weakest production aggregation. Keep it running in the
installed SDK smoke whenever this contract changes. Registrations are
process-local; they are not persisted in the Timeline document.
Nondefault renderer production declarations are also process-local:
`ProgramWire` refuses to serialize a program that carries one. This prevents a
remote process from inheriting a reproducibility claim without the hook that
justified it. A nested reference that trims registered content compiles by
window-after-generate: the hook sees the authored clip duration and an origin
tick rebased to the authored start, so a stateful pattern keeps its phase, and
the compiler windows the returned fragment to the retained span with the same
clamp-and-drop rule a trimmed note leaf uses. The fragment quota is charged
against what the hook generates, which is the authored extent.

Built-in note compilation applies the owning sequence groove at the original
owner-sequence onset. Move note-on/off by one shared displacement, intersect the
pair with the owning clip's half-open window, scale velocity half-up with
saturation, then subdivide the retained span for ratchets. Nested leaves carry
their owner sequence and source onset through lowering; never compose parent and
child groove. A trimmed nested MIDI leaf with authored groove selects over a window widened
by `groove_timing_reach`, so a note just outside a retained edge is still
available to be chased back in at its full authored length; whether it actually
sounds is decided afterwards by the clamp to the retained window.

**Adding a `CompileContextKind` is a data change, with one trap.** Both
`CompileInvalidationIndex::build()` and the `CompileContextSubscriptions` bitset
loop over `[0, kCompileContextKindCount)`, so a new kind needs no new case in
either — but it does need `kCompileContextKindCount` bumped in lockstep with the
enum. Forget that and the new kind is never indexed, never dirtied, and every
test that only checks "my subscriber recompiled" still passes because the
subscriber recompiles for some other reason. The `static_assert` on the bitset
width catches only the ninth kind, not a stale count. Write the exactness test
so it names the readers of *each* kind separately: a per-sequence index and a
per-kind index are indistinguishable until two kinds have disjoint readers.

**Proving invalidation exactness.** `PlaybackProgram::find_track()` returns the
compiled `TrackProgram` the published program holds. The compiler reuses an
untouched track's program object outright, so an unchanged **pointer** is a
direct observation that a track was not recompiled, and a changed pointer that it
was. Assert on that, not on a proxy like a compile counter — and assert the
program generation actually advanced in the same test, or "unchanged pointer"
could just mean no compile happened at all. A dirty-set test that still passes
when the subscription is ignored and everything recompiles is vacuous; break the
resolution both ways (over-dirty and under-dirty) and confirm it goes red.

## Production mode and replay honesty

- `provider_production_declaration` / `track_production_declaration` /
  `program_reproducibility` (`production_class.hpp`) derive what a compiled
  program may claim about being replayed, rather than storing it on the program,
  so the claim cannot drift from what the compiler actually lowered. A render
  spanning several classes aggregates with `timeline::weakest`, never with the
  first or the strongest.
- **You cannot compile a `Launcher` or `ExternalInput` track today.**
  `plan_compile` rejects any `TrackCompilePolicy` whose provider is not exactly
  `Arrangement` with `available_mask == 1`, so a `PlaybackProgram` can only ever
  carry arrangement tracks even though `ProviderSelectorProgram` models three
  kinds and really does gate rendering. Unit-test per-provider behavior against a
  hand-built `ProviderSelectorProgram`; a test that tries to compile one gets
  `CompileErrorCode::InvalidRequest` and proves nothing.
- `BufferedContentSource` composes `audio::StreamingSampleSource` with a zero
  preload window, so every frame travels through the ring where it can be
  counted. Deadline mode treats a zero producer return as “not ready yet,” not
  permanent EOF: later pumps retry at the same frame or seek to a playhead that
  already counted the interval as starved. Count starvation against the
  *declared* frame count. Size the implicit ring for the declared wall-clock
  lookahead, the largest audio callback, and any declared preroll, while
  `StreamingSampleSource` independently caps producer read-ahead at the larger
  of the lookahead and the preroll. A declared preroll is enforced
  synchronously inside prepare/seek — a producer that cannot fill it fails the
  call rather than silently under-delivering.
- Repositioning a `BufferedContentSource` is epoch-scoped, the
  `GeneratedEventSource::begin_playback_epoch` shape: `seek()` and
  `loop_wrap()` are control-thread operations that must begin a nonzero,
  strictly newer epoch, and they tolerate an in-flight producer call — it is
  stopped through its token within one chunk and its frames are discarded with
  the old ring before the rebuilt ring is primed at the new position.
  `cancel_production()` interrupts the in-flight chunk without repositioning
  and without latching; the next pump retries the same position. The source
  never wraps on its own: the transport owns looping and calls `loop_wrap()`
  at the declared boundary.
- `GeneratedEventSource` is a bounded push handoff for producer-generated MIDI:
  keep revisable staging separate from immutable committed SPSC slots, begin a
  nonzero strictly newer playback epoch quiescently on seek/restart, and commit
  complete half-open monotonic-tick batches only at the declared quantization
  grid. Validate each UMP word count from its message type. Audio pulls never
  regress the permanent elapsed frontier; a missing or discarded span reports
  exact lag, emits no generated events, and requests active-note flush. A
  deadline miss selects only the producer-declared fallback policy.
- A new `core/playback/src/*.cpp` is compiled by
  `timeline-program-threadless-no-exceptions-check` with `-fno-exceptions
  -fno-rtti` and `PULP_COMPILE_EXECUTOR_DISABLE_THREADS=1`, and swept into both
  wasm lanes by the closure gate. Anything that owns a `std::thread` or throws
  belongs in a header or a sibling module, not in `src/`.

## Publishing a program across a realm boundary

`PlaybackProgram` is `shared_ptr`-woven and cannot leave the process that built
it. `pulp/playback/program_wire.hpp` is the crossing form: one contiguous,
self-describing byte range that carries indices where the program carries
pointers. Reach for it whenever a consumer does not share the producer's heap —
a Worker publishing to an AudioWorklet, or a helper process — and never try to
hand the program itself over some serialization of pointers.

Things worth knowing before changing it:

- **Consume pinned bytes, not a retained source program.**
  `ProgramWireAutomationConsumer` takes a move-only `ProgramWireBytePin`,
  validates the exact prepared `CompiledTempoMap` object and its source tempo
  points, and returns the candidate pin on reject/unchanged or the retired pin
  on adoption. The caller supplies fixed lane state and explicit track/byte
  capacity; the consumer also enforces the separate
  `kProgramWireMaximumAutomationLanes` whole-publication ceiling. A rejection
  changes neither the active pin nor cursor state, so rejected and retired
  buffers may be poisoned immediately after their pins return.
- **Wire automation uses the production cursor and device boundary.** The
  consumer adapts borrowed segment records to `AutomationProgramView`, then
  runs the same `AutomationCursor` algorithm as an in-process
  `AutomationProgram`. Adoption precomputes fixed-capacity lane/group topology;
  block rendering uses the production mandatory-knot, optional coalescing,
  per-device event, and aggregate work ceilings without allocation or locks.
  This is automation parity only, not whole note/audio render parity.
- **Publication identity is global per lane even when attachment moves.** The
  active key includes producer epoch, track/lane identities, lane generation,
  and instance token. A lane generation may not regress under the same producer
  merely because the lane moved tracks; track identity controls cursor
  continuity, not stale-publication detection. Empty automation lanes remain
  valid and render no events.
- **Decode allocates nothing.** Records are native-layout, eight-byte-multiple,
  eight-byte-aligned structs, so `decode_program_wire` hands back typed spans
  borrowed straight out of the buffer. That is why the format asserts
  little-endian at compile time and rejects a misaligned base address instead of
  falling back to a copy. Adding a field that is not a fixed-size scalar — a
  string, a variable-length blob — breaks that property; give it its own
  section with its own `(first, count)` ranges instead.
- **The encoder refuses rather than drops.** A track with an audio renderer
  program, or a mixer control pointing at a lane the track does not own, is a
  typed error and not a silently thinner payload. Preserve that when widening
  what the wire covers: a lossy encode is indistinguishable downstream from a
  program that was authored that way.
- **Deliberate exclusions, and why.** Decoded audio (bulk, already content-hash
  addressed — a generation wire that inlined it would republish gigabytes per
  edit), the audio clip programs (derived, and carrying derived-cache pointers),
  `AudioRendererLimits` (mostly offline-stretch and converter budgets governing
  the compiler's host). The instance token is **not** excluded — see below.
- **Lane identity on the wire is `(producer_epoch, lane_id, generation,
  instance_token)`, and no proper subset works.** The token's in-process job is
  to stop an equal-generation replacement from masquerading as the active
  program — `AutomationCursor` decides `Unchanged` on the lane key *and* the
  token together — so `ProgramWireAutomationLaneRecord` carries it and a
  consumer gets to reach the same answer the cursor does. `producer_epoch`
  covers a different case and only that case: `generation` is minted per store
  and **restarts at 1**, so a producer torn down and recreated looks
  non-monotonic to a surviving consumer and would be refused forever on
  generation alone, and two producers of one document both minting generation 1
  would look like one publication without the epoch. Zero is refused for both
  rather than acting as a wildcard — the epoch at encode and decode, the token
  at decode only, since the compiler owns `AutomationProgram`'s constructor and
  always mints a nonzero one, so an encoder-side check would be unreachable.
- **A foreign token is comparable — within one epoch.** The objection to
  carrying it was that a process-local counter names nothing a consuming realm
  can look up. True and beside the point: a consumer never compares a foreign
  token to one of its own, only two foreign tokens to each other under one
  `producer_epoch`, where they came from one counter. Across epochs they are
  incomparable, and across epochs the epoch has already decided. Equality only —
  a larger token does not mean newer, since ordering is
  `(producer_epoch, generation)`'s job.
- **Per lane, not per publication.** The incremental compiler reuses a lane's
  program when that lane did not change, so its token is stable across a publish
  that touched only its neighbours. That is what lets a consumer re-adopt the
  lanes that moved and keep cursor state for the rest, instead of re-seeding
  everything on every publish.
- **Version growth is additive by section, not by version bump.** An unknown
  section marked `kProgramWireSectionOptional` is skipped; an unknown section
  without it is rejected. Bump `min_reader_version` only when an older reader
  would *misread* the bytes, not when it would merely miss data — and decide
  "merely" per payload, not per format: the controller sections are optional
  on a payload that carries none and required, with the floor raised, on one
  that carries any, because missing expression is a musical loss and not a
  cosmetic one. Do not solve a new section by widening an existing record; a
  wider record moves every older reader's stride and forces the floor up for
  every payload, controller-free ones included.
- **The byte golden is the guard that matters.** An encoder and a decoder that
  are wrong in the same direction still round-trip; only the pinned digest in
  `test/test_playback_program_wire.cpp` catches a reordered field. If you change
  the layout on purpose, re-pin it in the same change and say so. The digest is
  taken over a payload whose lane instance tokens have been normalised to their
  ordinals, because the token is minted per compile and would otherwise make the
  digest depend on how many programs the process built first. Normalise any
  future per-publication field the same way — write a fixed value into it rather
  than skipping the bytes, so its offset and width stay covered.
- The tempo map travels as its editable `TempoPoint`s, because
  `CompiledTempoMap`'s segments are private and derived. `encode_program_wire`
  therefore takes the points and refuses any that did not compile the program's
  map — `CompiledTempoMap::matches()` is what keeps the two honest.

### Check that a replacement identifier answers the same question, not a nearby one

The wire shipped without `instance_token` on the reasoning that `producer_epoch`
"replaces that guard across a realm." It did not, and the way it failed is worth
keeping.

`producer_epoch` answers *is this a different producer?* `instance_token`
answered *is this a different program from the same producer?* Adjacent
questions, and the substitution is sound for the case it was written against — a
producer torn down and recreated. It silently dropped the more common one: a
single worker recompiling. `generation` is **caller-supplied**, not minted per
compile, so two compiles of one document by one producer at one epoch agreed on
every field the wire carried and encoded to byte-identical payloads. A consumer
computing `Unchanged` from them reached the opposite answer to the in-process
`AutomationCursor` — a silently wrong render, not a decode error, which is the
class of bug a validating decoder cannot catch for you because nothing is
malformed.

Two habits come out of it:

- Before excluding a field from a wire, write down the question it answers and
  the question its stand-in answers. If the sentences differ, the exclusion is
  dropping a case, and the case it drops is the one nobody listed.
- Distrust a canonicality argument that is doing double duty. "Omitting it is
  also what makes one document encode to one byte range" was true and was a
  reason to want the exclusion; it was not evidence the exclusion was safe.

## A refusal of something authorable costs a written reason

`tools/scripts/negative_capability_check.py` (ctest
`playback-negative-capability`, selftest
`playback-negative-capability-selftest`) reads the refusal-shaped members of
`CompileErrorCode` **and of `TimelineGraphAdmissionCode`** — anything spelled
`Unsupported`, `NotSupported`, `Rejected`, `Refused`, or `Disallowed` — finds
every site that raises one, and decides whether the refused construct is
reachable from the timeline authoring surface. Both enums are read because a
document is refused in two places, not one: the playback compiler refuses what
it cannot lower, and graph admission refuses a device chain shape it will not
build. A checker pointed at one enum reports a clean registry while the other
enum's refusals accumulate unowned, which is the failure this gate exists to
prevent. An authorable refusal needs an entry in
`tools/scripts/negative_capability_allowlist.json` carrying an owner, a
`status` of `live-defect` or `intended`, and a reason.

The class it guards is worth naming: a construct a user **can** author and the
compiler then refuses is worse than the construct not existing. The document
saves, reloads, copies and round-trips, and only playback says no — with
nothing at authoring time to warn anyone. The gate does not forbid these; it
forbids adding one for free.

A refusal reads as authorable when the source above the raise reads a symbol
declared in `core/timeline/include/pulp/timeline/**` or named by
`core/timeline/schema/timeline_schema.json` — a model accessor, a model type,
an enum constant, a schema field. A refusal that only inspects internal
lowering state passes without an entry.

**Naming a code is not raising it.** Two mentions are excluded on purpose: a
field whose declared default happens to be a code, and a `case` label. A table
that maps every `CompileErrorCode` member to a wire name — the shape any
projection of the codes over a wire needs — mentions all of them at once, and
each label reads its own enumerator, so without the exclusion the table reports
as a raise of every refusal it can spell while raising none. Only the label
text is dropped, never the line, so a raise sharing a line with a label is
still found; the selftest holds both halves of that boundary. If you are adding
such a table, expect the gate to stay quiet about it and keep the real raise
sites in `core/playback/src/` as the thing it is watching.

**Three things it cannot see, so do not read a pass as "the compiler accepts
everything authorable":** a refusal expressed by dropping, clamping, or
substituting rather than by naming a code; a refusal raised through a different
error enum, such as an importer's or a renderer's; and an authored read that
sits further than `AUTHORING_LOOKBACK_LINES` above the raise or arrives through
an internal struct field that no longer names its model origin.

**A `case` label is a destination, not a raise.** The check consumes
`case Scope::CompileErrorCode::Enumerator:` before it looks for raises, because a
switch that maps every enumerator to its own name for a diagnostic otherwise
reports one refusal per arm — and the enumerator sitting in a neighbouring arm
lends its name to the lookback window, so each of those phantom refusals also
reads as authorable on evidence it never touched. Only the label text is
consumed, never the whole line: a refusal constructed in the arm's body is still
a raise, including on the same line as the label. A label on some other enum is
left alone, because it names no code for the raise pattern to find. One shape it
still reads as a raise, deliberately, because over-flagging asks for an entry
someone must answer rather than dropping one that is owed: a
`code == CompileErrorCode::X` comparison, which names a code without
constructing one.

Every entry carries a `status`: `live-defect` is tracked, not resolved, and
`intended` says the refusal is the answer. Retiring a refusal is removing its
raise site and its entry in the same change; the gate fails an entry whose
raise site no longer exists, so a reason cannot outlive its code. The selftest
takes the entries it drops from the allowlist document itself rather than from
a list of its own, so a retirement cannot fail it — a gate that goes red when
the code improves teaches people to edit the gate. Its synthetic raise and
`case`-label fixtures do still name enumerators (`MidiExpressionLaneUnsupported`,
`TrimmedGrooveUnsupported`, `NestedMixerPanUnsupported`) and the header
fixture splices after `MidiExpressionLaneUnsupported,`; deleting one of those
members from `CompileErrorCode` fails the selftest loudly and means re-pointing
the fixture, not weakening it.

## Dependency floor

The inbound sequencer tiers in `tools/cmake/PulpLinkFloor.cmake` include the
dependency-free `music` rung. `pulp::signal` uses that canonical pitch/scale
contract, so a sequencer plugin reaches `music` through its ordinary audio
closure even when its own sources do not include a music header. Keep `music`
in `sequencer-editor`, `sequencer-plugin`, and `sequencer-plugin-editor`
together: `pulp::timeline` also consumes the same theory owner, and the two
positive link-floor fixtures exercise the plugin and plugin-editor tiers. This
is a shared public-contract dependency, not per-target link debt.

`playback`'s floor is declared in `MODULE_FLOORS` in
`tools/scripts/timeline_engine_dependency_floor_check.py`, which scans both
`#include <pulp/<module>/...>` in every source file under `core/playback/` and
`target_link_libraries` in its `CMakeLists.txt`. Both axes must stay inside the
declared set, so reaching for a format, host, or view type fails the gate even
when the build would have linked.

`project_package` has its own floor above Timeline: it may reach timeline,
timebase, platform, and runtime, but it must not reach playback. Package
publication or recovery must not widen playback's row.

**The link axis is transitive, and playback is the module that shows why.** The
check follows what a linked library itself links, to a fixed point, so a row
cannot stay green by depending on a module that breaches it. `core/playback`
links `pulp::audio`, which links `pulp::state`, `pulp::signal` and
`pulp::sample-bank-manifest` PUBLIC and, through `pulp::state`, `pulp::events`
PRIVATE — four modules the row never named. Those are recorded in
`LINK_CLOSURE_DEBT`, deliberately *not* folded into `MODULE_FLOORS`: a floor row
also governs which headers the module's sources may include, so widening the row
would have granted `core/playback` the right to `#include <pulp/state/...>` as a
side effect of writing down a link fact. An entry there is a debt, not a
permission — cut the underlying link and delete the entry, and the gate tightens
with no other edit.

The PUBLIC/PRIVATE split survives the trip and matters for a
pay-for-what-you-use claim: `state`, `signal` and `sample-bank-manifest` arrive
PUBLIC, so their include directories propagate and a playback consumer genuinely
can reach `<pulp/state/...>` today; `events` is PRIVATE and link-only; and
`signal` is an INTERFACE library, so paying for it costs headers rather than
object code. Whether playback *should* reach the state store is an open design
question the debt list does not answer — it exists so the gate can police
whatever answer is reached.

The table holds every engine-adjacent module, not just playback, and the selftest
is generic over it. Adding a module there is how a new `core/` target gets the
same enforcement; it does not widen anyone else's floor.

**The rows are not independent, and "cannot reach X" is usually the wrong half of
the argument.** `timeline_editor`'s row is a strict superset of `timeline`'s, so a
claim of the form "this type must live in `core/timeline` because that module
cannot link `view`" is true and *proves nothing* — it is equally true one rung up.
When a floor row is offered as the reason for placing something, check which row
**excludes** which: that is the only asymmetry between two rungs in a chain, and
it is what the gate can actually act on. The worked example is the edit vocabulary
(`EditIntent`), which sits at the editor rung precisely because `timeline`'s row
excludes `timeline_editor` and can therefore reject a reducer or serializer that
reaches for a gesture verb.

### An editor view never links playback

`core/timeline_editor` carries a floor that deliberately excludes `playback`, and
the selftest asserts that pair by name in both the include and the link
direction. An editor learns where the playhead is through
`timeline_editor::SequencerUiHost`, whose implementation lives with whoever owns
audio — so a plugin that draws a piano roll over its own engine consumes the
editor without acquiring a transport.

**The module does not acquire a transport; the plugin binary does.** That row
governs `core/timeline_editor`'s own includes and links, and it holds. It says
nothing about what the plugin packaging adds around it, and measuring the other
direction shows the difference. `tools/cmake/PulpLinkFloor.cmake` walks CMake's
resolved link graph for a consumer; run over `StepSequencer_CLAP` it reports:

```
playback: StepSequencer_CLAP -> pulp-view -> pulp-view-script
            -> pulp-view-core -> pulp-host -> pulp-playback
```

VST3, CLAP and AU each link `${_PULP_VIEW_TARGET}` unconditionally in
`PulpPluginFormats.cmake` — drawing or not — and the view stack reaches the
plugin host and, through it, this module. So every Pulp plugin links `playback`
today, and the outbound gate is right to stay green about it: nothing in
`core/playback` or `core/timeline_editor` reached upward to cause it. Cite the
editor row for what a *module* costs, and a link-floor report for what a
*binary* costs; they are different claims and only one of them is about the
artifact a host loads. The inbound side is documented in the `timeline` skill.

**"Every Pulp plugin links `playback`" is true of a desktop configure only.**
The chain runs through `pulp-host`, and `core/host` is behind `NOT IOS` — iOS
disallows dlopen of third-party plugins, so hosting is not built there and the
`pulp-view-core -> pulp::host` edge is dropped too. One guard therefore removes
`host`, `playback` *and* `timeline` from an iOS closure, because that edge is the
plugin's only route to all three. Anything asserting `playback` is present in a
plugin binary must say which configure it means; entries a guard can remove
must be appended to `PULP_LINK_FLOOR_DEBT_<target>` under that same condition
rather than declared unconditionally, which reads their absence as rot. See the
`timeline` skill for the full rule.

Read that report as an upper bound and nothing more. `TIER` proves only that
nothing outside it is reached, so a tier can name `playback` — or the editor
rung — while the binary links neither, and still pass. If what you need to show
is that a module *is* in the artifact, say so with `pulp_assert_link_floor`'s
`REQUIRE` list, which fails naming any module that is absent from the measured
closure. `StepSequencer_CLAP` does link `playback`, by the chain above and only
by it; it does not link `timeline_editor` at all. The positive inbound proof is
`TimelinePluginProof_CLAP`: it requires `format timeline timeline_editor
timeline_view` under the `sequencer-plugin-editor` tier. That tier includes the
base `view` and `canvas` rungs intentionally reached by the concrete piano-roll
view, while packaging-driven `playback` remains per-target debt. Its processor
implements the host seam and embeds the real piano-roll view, while the editor
and view modules remain independent of playback.

That interface hands out `UiPlayhead` **by value**, and the reason is specific to
this module: `TransportSnapshot` borrows `const CompiledTempoMap*` from the
compiled program. That is correct for a block renderer, which consumes the
snapshot inside the callback that produced it, and unsafe for a view, which keeps
its copy across frames while the engine may adopt a different program underneath.
Never widen the UI-facing seam by passing a `TransportSnapshot` — project the
fields a view needs into values, as `UiPlayhead` does. `UiPlayhead::program_generation`
is what lets a view tell a stale reading from a live one without holding anything
a program swap can invalidate.

A value type both rungs genuinely need goes in `core/timebase`, never duplicated
into each. `timebase` is the whole of what the two floors have in common beyond
`platform`/`runtime`, so it is the only home that does not require widening a
row. `LoopRegion` is the worked example: `playback::LoopRegion` is an alias of
`timebase::LoopRegion` beside the existing `MeterSignature` one, and
`UiPlayhead::loop` names the same type — a loop set on the transport reaches an
editor reading with nothing to convert. Do not read this as licence to share the
*readings* themselves: `TransportPlayhead` and `UiPlayhead` stay separate
because their fields differ in kind, not merely in spelling.

### Position leaves the transport in two directions, one SeqLock each

`MasterTransport` publishes `desired_` toward the audio thread and
`TransportPlayhead` back toward everyone else. The audio thread writes the
second one for every block it *accepts* — a block whose ranges failed validation
is one the caller was told not to render, so it must not become the position a
view draws either — and `playhead()` reads it from a view, a meter, or a test,
allocating nothing and taking no lock. `prepare()` and `reset()` publish too, so
a reader between a lifecycle change and the first callback sees the transport's
starting state rather than the previous program's position or a default one.

Four properties of it are decisions rather than accidents:

- **A reading names the block's FIRST frame** (`ranges[0].timeline_tick_start`),
  not its last. That frame has not left the device yet, so it is the
  least-ahead-of-audible position the transport can honestly state; publishing
  the block's end would put every reading a whole buffer into the future.
- **The type is playback's own, not `timeline_editor::UiPlayhead`.** The floor
  above forbids that include outright, and the split is right independently of
  the gate: `UiPlayhead::program_generation` names a compiled program, which a
  transport does not know about. Whoever implements `SequencerUiHost` owns the
  projection and supplies the generation from the program it adopted.
- **`sequence` survives `reset()`.** Every other field of the reading is cleared
  there and the counter is deliberately excluded, because a reader tells
  readings apart by sequence and a restarted counter would let a fresh reading
  impersonate one the reader already drew. `reset()` publishes a retired reading
  rather than leaving the previous lifecycle's position readable until the next
  block, which is exactly the moment a view would otherwise draw a playhead
  belonging to a program that is gone.
- **Every publication is stamped in one place.** `publish_playhead()` takes the
  reading, assigns the sequence, and writes; no call site assigns the counter
  itself. A site that forgot to would publish a reading a reader treats as one
  it already handled — a silent stall rather than a build failure, which is why
  the stamp is structural rather than a convention.

`SeqLock` is the primitive because the payload is a trivially-copyable
multi-field struct that a reader wants the newest of, whole. `TripleBuffer`
would also work and costs 3x the storage for nothing at this size; `SpscQueue`
is wrong in kind — a view wants the latest reading, never every reading.

Publishing from `reset()` makes the control thread a second writer of a lock the
audio thread otherwise owns. That is the shape `reset()` already has for
`desired_`, whose ordinary writer is the control thread, and it is bounded the
same way: a caller that reset a transport concurrently with `begin_block()`
would be racing the plain assignments in `reset()` long before it raced this one.

### A view rung does not reach `playback`, and that absence is the contract

`MODULE_FLOORS` carries a `timeline_view` row above the editor kernel. It admits
`timeline_editor`, `timeline`, `timebase`, `view`, `canvas`, `platform`, `runtime` — and
**deliberately omits `playback`**.

That omission is the load-bearing part, not an oversight: it keeps a view's only coupling toward
audio the `SequencerUiHost` interface, so **an arranger drawn over somebody else's engine acquires
no transport.** If you find yourself wanting to widen that row to reach `playback`, the thing you
actually want is a host implementing `SequencerUiHost` — the row is what stops a view reaching past
the seam and binding to this engine specifically.

(It also omits `project_package`, keeping storage a sibling rung rather than a base: an editor is
proven against a `serialize_project` round trip, and re-hosting it on a package protocol later is
adapter work above the row rather than a change to it.)

## `kCompileContextKindCount` is an array dimension, so changing it is a struct-layout change

The invalidation index stores `std::array<std::vector<ItemId>,
kCompileContextKindCount>` in two structs in `compile_invalidation_internal.hpp`,
and the subscriber walk loops to the same constant. That is convenient — adding
a context kind needs no new reverse-index case, and no exhaustive switch to
extend — but it means bumping the count silently resizes those structs.

Treat it as a struct-layout change: build **all** targets, not just the timeline
and playback ones. Stray positional initializers fail closed at compile time, so
they are safe, but only a full build surfaces them, and a partial build pushes
that discovery to CI.

Before assuming a new kind needs reverse-index work, check for an exhaustive
`switch` over `CompileContextKind` — at time of writing there is none, and the
count-parameterised arrays are why.

## A floor row admits a module for includes AND links; `FORBIDDEN_LINKS` withdraws the link half

`MODULE_FLOORS` in `tools/scripts/timeline_engine_dependency_floor_check.py`
governs both what a module's sources may `#include` and what its build file may
link, from one set. That conflation is fine until a module legitimately needs a
header from a module it must not link.

`FORBIDDEN_LINKS` in the same file is the escape hatch: it names, per module, a
dependency the floor admits as an include and rejects as a link. `timebase`
keeps `runtime` in its floor so `pulp/runtime/result.hpp` stays reachable, while
the link is withdrawn so `libpulp-runtime.a` (and the mbedTLS archives its
PRIVATE link items drag along) stay off a consumer's link line.

Two consequences when editing that script. The selftest's fixture generator has
to agree with `verify()` about which half each name carries, which is what
`linkable_floor_names()` is for. And an entry reports a missing build file
rather than passing quietly, so it cannot outlive its subject.

## Nested-trim fixtures: check which edge you are actually trimming

`nested_clip(id, sequence_id, start, duration, source_start = 0)` defaults
`source_start` to **0**, which produces a *right* trim: the reference admits
the child's first `duration` ticks. `test_timeline_nesting_playback.cpp`'s
long-standing `trimmed_nested_lane_project` uses that default, so `left_trim`
is always zero there.

This matters because controller **chase** is a left-trim concern: it only runs
when the retained window starts inside the child. A chase implementation
verified only against that fixture has never executed — the tests pass while
the code is unreached. Use `source_start > 0` (see
`left_trimmed_nested_lane_project`) to exercise chase, and keep a right-trim
case too, since dropping points past the window end is the mirror failure.

The retained window is half-open in child-local ticks: points before it set
what sounds on entry, points at or after `left_trim + target_duration` are
never reached and must not be emitted.

### A trim can be real in ticks and absent in frames

`kTicksPerQuarter` is 705'600, so one tick is about 0.034 frames at 120 BPM and
48 kHz: roughly 29.4 ticks to a frame. `ticks_to_samples()` rounds to nearest,
so a trim of up to fourteen ticks moves no frame edge at all. The window a
sub-frame trim produces correctly spans the whole artifact while the clip
covers less than the whole authored tick range.

So a sample-domain trim predicate and a tick-domain one are **not** equivalent,
and a validator must never assert that they are. `validate_clip_program()` did,
and refused a correct clip: `link_audio_track_program()` answered `InvalidAsset`
for a nested stretched clip nudged by a single tick. Assert the implication that
survives the resolution gap instead -- covering the whole authored range means
reading the whole artifact -- and leave the converse alone, because it is false.

Two consequences for fixtures. A trim written as `kTicksPerQuarter / 4` is
exactly 6'000 frames at that tempo and rate, so a table built only from quarter
fractions is frame-aligned throughout and cannot see any of this; spell an
awkward tick count when the frame grid is what is under test. And
`link_audio_track_program()` is the only caller of `validate_clip_program()` --
`ProgramCompilerTask` constructs an `AudioTrackRendererProgram` directly as a
friend -- so a test that only compiles a program never reaches that validator
and cannot fail on anything it holds.

## A per-lane feel is one sequence per lane, because groove is sequence-owned

A groove belongs to a `Sequence`, not a `Track`: `Sequence::groove()` exists, `Track`
has none, and the compiler reads `context_sequence->groove()` when it lowers notes. So
the musically obvious request — "straight hats, snare a little early, bass a little
late" — is authored as **one nested sequence per lane**, each carrying its own
`timeline::GrooveTemplate`, referenced from an arrangement track by a `SequenceRef`
clip. There is no per-track feel knob, and looking for one wastes time.

That shape works exactly. At 120 BPM / 48 kHz a quarter is 705'600 ticks and 24'000
samples, so one tick is 5/147 of a sample — exact for any tick that is a multiple of
147. An authored -7'350 ticks lands a note 250 samples early and +11'025 lands it 375
late, at both the compiled `NoteProgramEvent.sample` and the position the renderer
emits. When asserting this, derive the expected samples by hand rather than by calling
the same conversion under test, and keep a no-groove render as the control: without it
a lowering that silently dropped the groove would pass.

**The order-preserving refusal does not run here.** `timebase::OrderPreservingGrooveKernel`
rejects a reordering table with `GrooveKernelError::ReordersEvents`, but it has no
consumer on the compile path. `timeline::GrooveTemplate::create` validates only that
each offset is smaller than a step, plus velocity and strength bounds — no monotonicity
check, and the model says the omission is deliberate. So within a lane a two-entry table
leaning opposite ways can push step N past step N+1 with nothing refusing it, and across
lanes independent grooves are unconstrained by construction. Treat "the kernel guarantees
order" as true only of the kernel, never of an authored document groove.

## A feel-free groove pads nothing observable, so do not test the reach short-circuit

`groove_timing_reach()` returns a supremum, not an estimate: swing's
displacement map is piecewise linear with its extremum exactly at the pair
midpoint, a step table adds its widest authored offset, and the two compose
additively. It short-circuits to zero when a groove `states_no_feel()` or its
`timing_strength()` is zero.

That short-circuit is a **cost guard, not a behavioural one, and no black-box
fixture can make it fail** — a `confirm_failure.sh` cycle over it correctly
returns NOT CONFIRMED. The reason is the sounding clamp in
`program_compiler.cpp`: a note the pad newly admits lies entirely outside
`clip.start()`, and a groove that displaces nothing has nothing to carry it back
inside, so `sounding_end <= sounding_start` and the note is dropped as
zero-length. The right edge behaves the same way. Padding a window whose groove
moves nothing therefore changes compiled output by exactly nothing, by
construction.

That was measured, not argued: with the strength-zero clause deleted and a probe
note planted squarely in the pad region, every feel-free assertion still passed,
while the same note was plainly visible under an authored groove. Keep the
clause — padding a window that provably needs none is waste — but do not claim
it is covered, and do not add a fixture that appears to cover it. Demanding
coverage for a branch with no observable behaviour is a category error, and the
usual way it gets "satisfied" is by quietly weakening a neighbouring assertion.

The pad is invisible to *most* notes for a second reason worth knowing: the
lowerer measures `clipped_note.start` from the **padded** window and the
compiler subtracts `pad_left` back off, so the arithmetic cancels. Only a note
the pad newly admits reads differently.

## The program wire refuses what it cannot represent

`program_wire_encoded_size` rejects programs it has no section for — an audio
program (`AudioProgramUnsupported`), a non-default production declaration. A
value that is not safe to ignore fails closed rather than returning a copy
that plays thinner than the program meant. Note the tempo-point check fires
*before* the per-track loop, so a fixture passing no tempo points is refused
for that reason first — an encode-refusal test in a suite without a tempo
fixture will pass for the wrong reason.

Controller events are no longer on that list. Wire version 3 carries them in
two appended sections (`ControllerRanges`, one `(first, count)` per track, and
`ControllerEvents`, one `ProgramWireControllerEventRecord` per
`ControllerProgramEvent`), and `ControllerEventsUnsupported` is retired
because the case it refused works — a retirement earned by the round trip, not
by moving an assertion. Things to know before touching it:

- **Order is carried verbatim, not re-derived.** The encoder copies
  `arrangement_controller_events()` in the program's sequence and the decoder
  preserves it; `program_wire_matches` compares position for position, so a
  swapped tied pair is a mismatch. The wire does not enforce
  `controller_program_event_less` order on decode — see the finding below.
- **The reader floor is per payload, not per build.** `kProgramWireVersion` is
  3 for every payload; `min_reader_version` is 2 when the program carries no
  controller events (both appended sections flagged optional, so a version 2
  reader skips them and renders exactly the program) and 3 when it carries any
  (sections required, so a version 2 reader refuses at the header). Both are
  functions of the counts, which is what keeps one program at one encoding.
  The one outcome the format never produces is a floor of 2 over a non-empty
  controller section — that is an older reader silently dropping expression,
  the loss the old refusal existed to prevent. In the other direction a
  version 3 reader accepts a version 2 payload with the sections absent, gated
  on `header.version < 3`; the same bytes stamped 3 are a non-canonical payload
  and refuse with `MissingSection`.
- **No version 2 record changed.** The ranges live in their own section
  rather than as two more fields on `ProgramWireTrackRecord`, precisely so a
  version 2 reader's stride over every section it knows is what it was. The
  `sizeof` asserts in `program_wire.hpp` are the honest diff: two new lines,
  every existing value unchanged.
- **Decode bounds and refusals.** A range's `count` is judged against
  `kProgramWireMaximumControllerEventsPerTrack` — the compiler's own
  `kMaximumControllerEventsPerTrack` in `program.hpp`, shared so the two cannot
  drift — *before* its range check, so a corrupted count is `InvalidLimits`
  rather than a walk. An address wider than four bits or a zero clip, lane or
  point identity is `MalformedControllerEvent`, judged by
  `timeline::midi_lane_address_well_formed` rather than a second rule. An
  origin outside the enum is `InvalidEnum`. Each is covered by a resealed
  corrupt-and-restore case in `test_playback_program_wire.cpp`.
- **Finding, not fixed here: the compiler does not sort controller events.**
  `program.hpp` documents `arrangement_controller_events()` as being in
  `controller_program_event_less` order, but `program_compiler.cpp` has no
  controller sort stage (notes have `SortTrackNotes`; controllers are pushed
  clip by clip, lane by lane, point by point) and nothing in `core/` calls the
  comparator. Two lanes on one clip therefore emit lane-major, not time-major.
  The order *is* total over distinct events — `MidiContent::create` makes point
  ids unique within a content and addresses unique across its lanes — so the
  wire has one sequence to preserve and preserves it; a consumer that needs
  time order must sort, and a decoder that enforced sorted order would refuse
  every multi-lane program the compiler emits today.

## Nested gain composes by multiplying; a placement fade rides beside the leaf; pan does neither

`sequence_content_lowerer.cpp` flattens a `SequenceRef` into leaf clips on the
referring track, so anything the child track owned has to find a home on a leaf
or be refused. Gain has one: nesting stacks gain stages in series, and the
flattened leaf carries their product —
`placement.gain * child_track.gain * leaf.gain`, composing again at each extra
level. Unity is exactly the identity, so a transparent nesting returns the
float the leaf authored rather than a rounded near-miss, and an equality
assertion on the composed value is measuring composition rather than rounding
when the fixture uses powers of two.

A **placement fade** composes too, but it cannot fold, and the difference is
the whole design. A product of gains is a gain; a ramp is time-varying, and two
ramps of different shapes reduce to no third shape. So the placement's envelope
travels *beside* the leaf instead of inside it: `LoweredClip::placement_fades`
carries every enclosing ramp in owner-timeline ticks, `audio_renderer.cpp`
converts them to clip-relative frames as
`AudioClipRendererProgram::placement_fade`, and `clip_fade_envelope.hpp`
multiplies them into the leaf's own envelope. Nesting a level deeper appends;
nothing collapses.

Four things about that carrier are load-bearing, and each is a way to get it
subtly wrong:

- **It stores a window of fade PROGRESS, not a pair of endpoint gains.** A
  shape is a pure reparameterization of progress, so the slice of a ramp one
  leaf covers must be read by applying the shape to the progress that leaf
  actually spans. Storing the gain at each end and ramping linearly between
  them lands both edges exactly and bends the wrong way everywhere in between —
  right for `Linear`, wrong for every `EqualPower` fade. That error measures
  close and sounds wrong.
- **A segment names its ends by what they read, not which way it points.**
  `silent_frame` is where the ramp is zero, `open_frame` where it is unity, so
  a fade-out is a fade-in with the ends exchanged and there is no direction
  flag to get backwards. Past the open end the segment is skipped; past the
  silent end it returns zero.
- **A ramp end routinely falls outside the leaf that carries it.** Flattening
  cuts a nested window at clip boundaries and never at a ramp edge, so one leaf
  can begin inside the placement's fade-in and end outside it, and a single
  leaf can sit under four multiplicative ramps across two shapes: its own fade
  in and out, plus the placement's head and tail. Keeping the whole ramp and
  evaluating the position — rather than renormalizing per leaf — is what makes
  two neighbouring leaves read the same gain at the frame they share.
- **The second envelope is guarded on presence.** `detail::clip_envelope` has a
  call site in `realtime_stretch_renderer.cpp` that runs *once per output
  sample*, which is why progress is narrowed to `float` before the shape lookup
  there (pinning `EqualPower` to `sinf`). `placement_fade` is null for every
  clip that was not nested under a faded placement, so the ordinary clip pays
  one predictable branch and no extra transcendental. Do not add an unguarded
  second `fade_gain` call.

An inner placement's own fades are **lifted out of the clip** at
`sequence_content_lowerer.cpp`'s nested-`SequenceRef` branch: the re-placed
`nested_clip` is built with its fade durations zeroed and the authored,
untrimmed ramp recorded instead. That is not an optimization. The clip carries
the *trimmed* window, `Clip::create` runs `valid_playback_properties`, and a
fade wider than its clip is rejected — so leaving the fades on the clip turns a
trimmed faded nesting into `InvalidStructure` before the walk ever reaches it.
Reading the ramp off the untrimmed extent is also the musically correct answer:
a trimmed placement enters its fade part way up.

Two neighbouring cases keep their own refusal rather than being folded away —
the code names which obstacle it hit:

- `NestedMixerPanUnsupported` — a clip carries no stereo placement at all, and
  the parent track's single pan also serves everything else on that track.
  Unlike gain there is no sink for *any* content kind.
- `NestedGainSinkUnsupported` — a composed gain lands on the leaf's clip gain,
  and **clip gain only reaches a renderer for media content**. Note, registered
  and opaque leaves compile to events, and nothing scales an event by the gain
  of the clip that carried it, so folding a child fader into one would discard
  it silently. This is the easy thing to get wrong: the composition looks
  correct in the lowerer and is simply never read.

`NestedPlacementFadeUnsupported` still exists and asks that **same sink
question** about the envelope. A fade is a time-varying gain, so a placement
fade over a note, registered or opaque leaf has nowhere to land either, and it
refuses rather than playing that leaf at full level through an envelope the
author wrote. It is scoped to the leaves a ramp actually *reaches*: a note leaf
lying wholly past the ramp reads unity and compiles fine. When that refusal
fires it names the leaf, not the placement — which is the tell that it is the
sink case and not the old whole-envelope refusal it replaced.

A leaf's **own** fade is the case this is all easiest to confuse with, and it
behaves differently on purpose. A leaf fade is measured from the clip's edge
and a trim moves that edge, so the retained fade is the authored one minus the
trim, clamped to what is left of the clip — edge-anchored, not the same ramp
entered part-way through. A placement ramp does the opposite: it keeps its
authored edges and the leaf is read at whatever progress its position implies.
Both are right; they answer different questions.

So a nested child holding notes still refuses a fader, and the fixture that
proves it must use media content to see composition at all. Read the composed
value through `TrackProgram::audio_program()->clips()[n].gain_linear`, and the
ramps through the same clip's `placement_fade`.

## A nested child-track state refuses only if it *substitutes* content

Four track states look alike in the document and split cleanly once you ask
what reads them. `begin_track` handles a **top-level** track and substitutes:
`freeze()` calls `output.clear()` and returns `Freeze`; a valid
`active_take_lane_id()` clears and returns `ActiveTake`. Both discard the
arrangement and stand something else in its place. The **nested** walk does no
such thing — `step_reference` descends straight into `track.clips()` and never
calls `begin_track` at all.

That asymmetry is the whole rule. A nested frozen or comped track would play
precisely the arrangement its author replaced, so each refuses under its own
name: `NestedFrozenTrackUnsupported` and `NestedActiveTakeUnsupported`. They
do not share a code, because the construct that would lift them is the same
one but the reason a reader hits them is not — and a shared code sends you to
the wrong half of the document.

### Freeze nests only where the nesting transforms nothing

A freeze is a rendered artifact anchored in **absolute samples**, and nothing
about it can be re-derived: it either lands where it was rendered to land or it
is a stale render playing at the wrong time or level. So the question the
lowerer asks is not "can the artifact be mapped through this nesting?" but
"does this nesting transform its child at all?". Where the answer is no, the
artifact is already in the right place and lowers as an absolute `MediaRef`
leaf carrying the same media over the same samples; everywhere else the
refusal stands, exactly as before.

`nesting_is_transparent` is the whole of that judgement, and it is built to
fail closed: a `NestingTransformation` enumerator with no case in
`nesting_imposes` reaches a trailing `return true` and is reported as
*imposed*, so a transformation nobody has reasoned about refuses rather than
permits. Adding an enumerator without answering for it cannot widen the
permit.

Three things about the enumeration are worth knowing before you edit it:

- **It is wider than what the walk applies today.** A placement's
  `time_conform` and a child track's `modulators` / `macros` /
  `modulation_routes` are read by neither this walk nor `begin_track`, so a
  child carrying one is neither honoured nor refused anywhere else. Without an
  entry here, a modulated fader under a sealed artifact would be silently
  permitted.
- **The artifact's sample rate is in the list and is not a transformation the
  owner applies.** An unnested artifact compiles through
  `compile_track_freeze_program` or `compile_take_comp_segment_program`, both of
  which take the projected timeline span as the renderable length; a lowered
  leaf compiles through the generic absolute-clip path, which takes the source
  length scaled and rounded up. Those agree only when no rate conversion
  happens. Do not delete the check as redundant with either compiler's own rate
  validation — that validation runs on a path the lowered leaf never takes.
- **Some entries are unreachable backstops.** A `SequenceRef` clip cannot carry
  a conform or an absolute anchor (`Clip::create` and `create_absolute` reject
  both), so no refusal test can exercise those entries and none pretends to.

### An active take comp nests by the same predicate, N leaves instead of one

`NestedActiveTakeUnsupported` is narrowed by the *same* `nesting_is_transparent`
call — one construct, two payloads, and the eighteen nesting observations are
the same eighteen questions for both. What differs is what gets emitted and
what `ArtifactRate` has to look at.

`SealedArtifact` is how the predicate carries the difference: exactly one of
`freeze` / `active_take` is set, and neither being set returns *imposed*, in the
same fail-closed direction as the switch's trailing `return true`. For a comp,
`artifact_rate_differs` asks the question once **per take a segment draws from**
— not per take in the lane, because a lane may hold takes the comp never
selects and a rate those carry is not a rate anything would convert.

`emit_sealed_active_take` is the payload. Per comp segment: resolve the take the
segment names, read the source offset as the distance from that take's
`placement_start` to the segment's `range.start`, and emit an absolute leaf over
`MediaRef{take.media.asset_id, take.media.source_start + offset,
segment.range.sample_count}` at `segment.range.start`. That is the arithmetic
`compile_take_comp_segment_program` performs, re-derived on the document side
rather than shared, because the two build different things — a document clip
and a renderer program — and what they owe each other is the rendered samples.
A test asserts that identity; no comment should be trusted to.

Two consequences worth knowing:

- **An empty comp is transparent and lowers to nothing.** That is correct, not
  a hole: `begin_track` renders an empty comp as no clips too, so the nested and
  unnested documents agree on silence.
- **The `TakeCompSegment` ordinal collision is out of reach from this lane.**
  `link_audio_track_program` identifies a comp-segment program by a bare ordinal
  (`segment_index + 1`), so two copies of one comp on one track collide. A
  *lowered* comp never produces that program kind — it produces `ArrangementClip`
  leaves carrying generated document identities — and the pair that would have
  collided cannot be authored anyway: transparency pins a placement to its
  child's origin, so a second transparent placement on the same track would have
  to overlap the first, and `Track::create` rejects the overlap. A second
  placement on a *different* track compiles and sounds its own copy, which is
  what a placement means for ordinary child content too.

`record_armed()` and the bare `take_lanes()` list are read by **neither** path,
and refusing them rejected documents that already compiled correctly. Three
independent places corroborate this before you trust it:

- each appears exactly once in all of `core/playback` — in the guard that used
  to refuse it, and nowhere else;
- `sequence_preflight.cpp` resolves media for `freeze->media` and
  `active_take_lane->comp_segments()` only, so a dormant lane is never even
  resolved to an asset;
- the CLI's own duration walk (`timeline_playback.cpp`) skips a track for
  `freeze() || active_take_lane_id().valid()` and consults neither of the
  other two.

So when you are deciding whether a new child-track state may nest, do not ask
whether it is "set". Ask whether anything substitutes on it. If the state only
records intent — arm, an unselected lane — the nested walk lowers the same
clips it would have lowered without it, and the test that proves so should
assert **whole-event identity** (`NoteProgramEvent`'s `operator<=>` is
defaulted, so `std::equal` over the spans compares every field) rather than
merely that no error came back.

One fixture trap: prove a dormant lane inert with a lane holding a **real**
take against a declared project asset. An empty lane is trivially inert and
proves nothing, and `TakeLane::create` imposes no non-empty requirement, so
the weak fixture compiles and looks like evidence.

## `CompilerStatus` says how much of a compile was incremental, so do not time it

`CompilerStatus::active_tracks_completed` is partitioned by
`active_tracks_recompiled` and `active_tracks_reused`. A track lands in
`reused` when the dirty set spared it and its `TrackProgram` was carried over
from the live program untouched; it lands in `recompiled` when a fresh one was
built. The two always sum to `active_tracks_completed`, and `take_pending`
clears all three together through `clear_active_track_counts()` so the
partition can never describe a previous request.

Read those counters when you need to know an edit stayed incremental. The
tempting alternative — assert the compile finished quickly — measures the host
as much as the compiler, and a one-track edit that silently started rebuilding
the whole sequence still fits inside a generous millisecond ceiling on a fast
machine while blowing a tight one on a busy machine. The counters are exact
everywhere.

Two things force a *reused*-looking track back onto the recompile side, so
expect them rather than treating them as a lost optimisation:
`requires_generation_refresh` (offline Stretch artifacts whose publication
provenance is generation-specific) and any capacity refusal, which fails the
whole request instead of completing the track.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…