ConveyorState::leak_step (src/simlin-engine/src/conveyor.rs, ~line 721) branches on exponential_leak. The exponential arm (§5.2) computes each flow's continuous shed, subtracts it, and return (leak_vols, slat_vols); (~line 777) before reaching the integer-leakage (§5.4) requantization block that the linear (§5.1) path falls through to.
Consequence: a conveyor with exponential_leak="true" whose leak flow carries <leak_integers/> leaks continuously. leak_carry[k] is never accumulated, floor(leak_carry[k]) whole units are never taken, and the reported leak rate is the raw continuous rate. The integers flag is simply ignored on that path. The model simulates and produces plausible-looking fractional leak rates, so the failure is silent.
Note the leak_step rustdoc already promises otherwise: "slat_vols[k] sums to leak_vols[k] (conservation) after any integer-leak requantization".
Why this looks like a bug rather than intent
docs/design/conveyors.md §5.4 ("Integer leakage") does not restrict integer leakage to the linear model:
With <leak_integers/>, flow k accumulates its computed real leak into leak_carry[k] each DT; it actually removes floor(leak_carry[k]) whole units (distributed from the in-zone slats, exit-most first) and retains the fractional remainder in leak_carry[k]. Reported rate = whole units removed / DT. Linear leak_window is consumed by in-zone travel (§5.1) independent of the quantization, so the carry redistributes timing without changing a cohort's schedule.
Only the trailing sentence about leak_window consumption is scoped to the linear model; the quantization rule itself is stated unconditionally. The conveyor-level exponential_leak flag is documented (§5, line 447) as selecting the leak model for all leak flows, and <leak_integers/> is a per-flow marker (§ table, line 199) that is orthogonal to it. Spec and implementation disagree.
Why it matters
- Correctness, silently: the modeler asked for whole-unit leakage and got fractional. Nothing errors, nothing warns.
- Integer leakage exists precisely for discrete-entity belts (people, vehicles, batches). Fractional leak on such a belt is a wrong answer, not a rounding nit.
Components affected
src/simlin-engine/src/conveyor.rs -- ConveyorState::leak_step (VM/interpreter path, the parity oracle)
src/simlin-engine/src/wasmgen/belt.rs -- wasm backend, which deliberately mirrors the VM here: its shed_words slat-stride helper is gated on !exponential_leak, and this is documented in belt.rs and in the belt.rs bullet of src/simlin-engine/CLAUDE.md
src/simlin-engine/src/conveyor_compile.rs -- expand_conveyors, if the resolution is a validation error
docs/design/conveyors.md §5.4 -- if the resolution is to scope integer leakage to linear
Possible resolutions (decide as part of this issue)
- Define and implement exponential integer semantics. Accumulate
leak_carry[k] from the exponential arm's computed continuous shed, remove floor(carry) whole units distributed exit-most-first from the in-zone slats, and report whole_units / DT. This is the reading the spec text implies. Open sub-questions: interaction with the proportional overdrain scale-down (quantize before or after scaling?), and how whole-unit removal interacts with the "all flows see the same start-of-step content" rate-additivity rule of §5.2.
- Reject the combination loudly. Make
<leak_integers/> + exponential_leak="true" a validation ERROR at expansion time in conveyor_compile::expand_conveyors, and amend docs/design/conveyors.md §5.4 to scope integer leakage to the linear model. Smaller and safer than defining new semantics, and turns a silent wrong answer into a loud one.
Option 2 is likely the better first move unless Stella is known to support the combination -- worth checking what Stella does before committing (per the house rule that Stella behavior wins over the OASIS spec).
Whichever resolution is chosen, both backends must change together, plus a VM/wasm parity test. The wasm backend's current gate is not an oversight -- it mirrors the VM on purpose, because the VM is the parity oracle and silently "fixing" one backend would have made the two disagree. If option 2 is chosen, the wasm gate and its comment stay but the reject moves upstream to expansion, and the belt.rs / CLAUDE.md notes should be updated to cite the validation error rather than "mirrors the VM's silent drop".
Discovery context
Found while lowering conveyor leaks to wasm (GH #922 step 2, src/simlin-engine/src/wasmgen/belt.rs).
Not covered by #938 (mid-pass ConveyorTransitTooLong state), #939 (Vm::into_results row counts), #940 (sub-model conveyor rejection), #941 (sir_social_distancing_mixnot.stmx fixture), or #865 (GF leak fraction / PREVIOUS(driven_flow) / JSON serialization).
ConveyorState::leak_step(src/simlin-engine/src/conveyor.rs, ~line 721) branches onexponential_leak. The exponential arm (§5.2) computes each flow's continuous shed, subtracts it, andreturn (leak_vols, slat_vols);(~line 777) before reaching the integer-leakage (§5.4) requantization block that the linear (§5.1) path falls through to.Consequence: a conveyor with
exponential_leak="true"whose leak flow carries<leak_integers/>leaks continuously.leak_carry[k]is never accumulated,floor(leak_carry[k])whole units are never taken, and the reported leak rate is the raw continuous rate. Theintegersflag is simply ignored on that path. The model simulates and produces plausible-looking fractional leak rates, so the failure is silent.Note the
leak_steprustdoc already promises otherwise: "slat_vols[k]sums toleak_vols[k](conservation) after any integer-leak requantization".Why this looks like a bug rather than intent
docs/design/conveyors.md§5.4 ("Integer leakage") does not restrict integer leakage to the linear model:Only the trailing sentence about
leak_windowconsumption is scoped to the linear model; the quantization rule itself is stated unconditionally. The conveyor-levelexponential_leakflag is documented (§5, line 447) as selecting the leak model for all leak flows, and<leak_integers/>is a per-flow marker (§ table, line 199) that is orthogonal to it. Spec and implementation disagree.Why it matters
Components affected
src/simlin-engine/src/conveyor.rs--ConveyorState::leak_step(VM/interpreter path, the parity oracle)src/simlin-engine/src/wasmgen/belt.rs-- wasm backend, which deliberately mirrors the VM here: itsshed_wordsslat-stride helper is gated on!exponential_leak, and this is documented inbelt.rsand in thebelt.rsbullet ofsrc/simlin-engine/CLAUDE.mdsrc/simlin-engine/src/conveyor_compile.rs--expand_conveyors, if the resolution is a validation errordocs/design/conveyors.md§5.4 -- if the resolution is to scope integer leakage to linearPossible resolutions (decide as part of this issue)
leak_carry[k]from the exponential arm's computed continuous shed, removefloor(carry)whole units distributed exit-most-first from the in-zone slats, and reportwhole_units / DT. This is the reading the spec text implies. Open sub-questions: interaction with the proportional overdrain scale-down (quantize before or after scaling?), and how whole-unit removal interacts with the "all flows see the same start-of-step content" rate-additivity rule of §5.2.<leak_integers/>+exponential_leak="true"a validation ERROR at expansion time inconveyor_compile::expand_conveyors, and amenddocs/design/conveyors.md§5.4 to scope integer leakage to the linear model. Smaller and safer than defining new semantics, and turns a silent wrong answer into a loud one.Option 2 is likely the better first move unless Stella is known to support the combination -- worth checking what Stella does before committing (per the house rule that Stella behavior wins over the OASIS spec).
Whichever resolution is chosen, both backends must change together, plus a VM/wasm parity test. The wasm backend's current gate is not an oversight -- it mirrors the VM on purpose, because the VM is the parity oracle and silently "fixing" one backend would have made the two disagree. If option 2 is chosen, the wasm gate and its comment stay but the reject moves upstream to expansion, and the
belt.rs/CLAUDE.mdnotes should be updated to cite the validation error rather than "mirrors the VM's silent drop".Discovery context
Found while lowering conveyor leaks to wasm (GH #922 step 2,
src/simlin-engine/src/wasmgen/belt.rs).Not covered by #938 (mid-pass
ConveyorTransitTooLongstate), #939 (Vm::into_resultsrow counts), #940 (sub-model conveyor rejection), #941 (sir_social_distancing_mixnot.stmxfixture), or #865 (GF leak fraction /PREVIOUS(driven_flow)/ JSON serialization).