thread panicked at src/history.rs:226:42:
index out of bounds: the len is 0 but the index is 18446744073709551615
Cause
History::iteration opens with (src/history.rs:225):
forjin(0..self.time_slices.len()-1).rev(){
With no slices, len() - 1 underflows to usize::MAX. In release the range is astronomically large and the first self.time_slices[j + 1] indexes far out of bounds; in debug the subtraction itself panics with an overflow message. Either way, converging a history that has had no events added is a hard crash.
Why it matters
History::converge returns Result<ConvergenceReport, InferenceError> — a caller reasonably assumes bad states come back as Err, not as a panic. An empty history is a completely ordinary state: it is what History::default() gives you, and it is what a caller has after ingesting an empty event stream (add_events returns Ok(()) early for an empty iterator, src/history.rs:689, so "ingest then converge" panics if the input source happened to be empty).
Fix
Guard the sweep: with 0 slices there is nothing to converge, so return a zero-iteration ConvergenceReport with converged: true (or an explicit error, if converging nothing should be a caller mistake — but Ok matches the early-return already in add_events).
Use a saturating/checked form of the bound rather than len() - 1, and check the 1-slice path too: the existing if self.time_slices.len() == 1 branch (src/history.rs:261) suggests the loops are already known not to handle small inputs uniformly.
Acceptance
History::default().converge() returns Ok with iterations: 0 and does not panic.
Same for log_evidence() and learning_curves() on an empty history — audit them for the same pattern.
```rust
let mut h = History::default();
h.converge().unwrap();
```
```
thread panicked at src/history.rs:226:42:
index out of bounds: the len is 0 but the index is 18446744073709551615
```
## Cause
`History::iteration` opens with (`src/history.rs:225`):
```rust
for j in (0..self.time_slices.len() - 1).rev() {
```
With no slices, `len() - 1` underflows to `usize::MAX`. In release the range is astronomically large and the first `self.time_slices[j + 1]` indexes far out of bounds; in debug the subtraction itself panics with an overflow message. Either way, converging a history that has had no events added is a hard crash.
## Why it matters
`History::converge` returns `Result<ConvergenceReport, InferenceError>` — a caller reasonably assumes bad states come back as `Err`, not as a panic. An empty history is a completely ordinary state: it is what `History::default()` gives you, and it is what a caller has after ingesting an empty event stream (`add_events` returns `Ok(())` early for an empty iterator, `src/history.rs:689`, so "ingest then converge" panics if the input source happened to be empty).
## Fix
Guard the sweep: with 0 slices there is nothing to converge, so return a zero-iteration `ConvergenceReport` with `converged: true` (or an explicit error, if converging nothing should be a caller mistake — but `Ok` matches the early-return already in `add_events`).
Use a saturating/checked form of the bound rather than `len() - 1`, and check the 1-slice path too: the existing `if self.time_slices.len() == 1` branch (`src/history.rs:261`) suggests the loops are already known not to handle small inputs uniformly.
## Acceptance
- `History::default().converge()` returns `Ok` with `iterations: 0` and does not panic.
- `h.add_events(std::iter::empty()).and_then(|_| h.converge())` likewise.
- Same for `log_evidence()` and `learning_curves()` on an empty history — audit them for the same pattern.
Fixed in f4e2922. History::iteration returns early when there are no slices, and converge() short-circuits to a zero-iteration converged: true report.
log_evidence(), learning_curves(), learning_curve() and current_skill() were audited for the same pattern — all iterate rather than index, so none had it. Covered by empty_history_converges_trivially, empty_event_stream_then_converge and empty_history_queries_do_not_panic in tests/degenerate_inputs.rs.
Fixed in f4e2922. `History::iteration` returns early when there are no slices, and `converge()` short-circuits to a zero-iteration `converged: true` report.
`log_evidence()`, `learning_curves()`, `learning_curve()` and `current_skill()` were audited for the same pattern — all iterate rather than index, so none had it. Covered by `empty_history_converges_trivially`, `empty_event_stream_then_converge` and `empty_history_queries_do_not_panic` in `tests/degenerate_inputs.rs`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Cause
History::iterationopens with (src/history.rs:225):With no slices,
len() - 1underflows tousize::MAX. In release the range is astronomically large and the firstself.time_slices[j + 1]indexes far out of bounds; in debug the subtraction itself panics with an overflow message. Either way, converging a history that has had no events added is a hard crash.Why it matters
History::convergereturnsResult<ConvergenceReport, InferenceError>— a caller reasonably assumes bad states come back asErr, not as a panic. An empty history is a completely ordinary state: it is whatHistory::default()gives you, and it is what a caller has after ingesting an empty event stream (add_eventsreturnsOk(())early for an empty iterator,src/history.rs:689, so "ingest then converge" panics if the input source happened to be empty).Fix
Guard the sweep: with 0 slices there is nothing to converge, so return a zero-iteration
ConvergenceReportwithconverged: true(or an explicit error, if converging nothing should be a caller mistake — butOkmatches the early-return already inadd_events).Use a saturating/checked form of the bound rather than
len() - 1, and check the 1-slice path too: the existingif self.time_slices.len() == 1branch (src/history.rs:261) suggests the loops are already known not to handle small inputs uniformly.Acceptance
History::default().converge()returnsOkwithiterations: 0and does not panic.h.add_events(std::iter::empty()).and_then(|_| h.converge())likewise.log_evidence()andlearning_curves()on an empty history — audit them for the same pattern.Fixed in
f4e2922.History::iterationreturns early when there are no slices, andconverge()short-circuits to a zero-iterationconverged: truereport.log_evidence(),learning_curves(),learning_curve()andcurrent_skill()were audited for the same pattern — all iterate rather than index, so none had it. Covered byempty_history_converges_trivially,empty_event_stream_then_convergeandempty_history_queries_do_not_panicintests/degenerate_inputs.rs.