quality() is documented and exported as a general N-group match-quality function (src/lib.rs:199), but it only works for exactly two groups. Verified on main @ 2b5d3b1, --release:
quality(&[&[a],&[b]],beta)// ok -> 0.8115...
quality(&[&[a],&[b],&[c]],beta)// PANIC: index out of bounds: the len is 3 but the index is 3
quality(&[&[a]],beta)// PANIC: "eh, okey"
History::predict_quality calls straight through (src/history.rs:405), so it panics identically:
h.predict_quality(&[&[&"a"],&[&"b"],&[&"c"]])// PANIC at src/lib.rs:240
Three separate defects.
1. The x column counter is wrong (src/lib.rs:224-244)
letmutt=0;letmutx=0;for(row,group)inrating_groups.windows(2).enumerate(){fornint..t+current.len(){rotated_a_matrix[(row,n)]=flatten_weights[n];x+=1;// <-- x tracks the +1 block...
}t+=current.len();forninx..x+next.len(){// <-- ...but is then reused as the -1 block start
rotated_a_matrix[(row,n)]=-flatten_weights[n];}x+=next.len();}
x accumulates across rows while t restarts the window each iteration, so from row 1 onward the two counters disagree. With three singleton groups: row 1 writes +1 at column 1 (correct) then attempts -1 at column 3 of a 2×3 matrix → out of bounds. For two groups the loop body runs once and x never diverges from t, which is why the only existing test (test_quality, src/lib.rs:273) passes.
The -1 block should start at t + current.len() — the column right after the +1 block — making the separate x counter unnecessary.
2. Matrix::inverse is unimplemented for n > 1 (src/matrix.rs:112-122)
quality() calls middle.inverse() where middle is (k-1)×(k-1) for k groups. Only k == 2 yields a 1×1 matrix. For k >= 3 this panic is reached the moment defect 1 is fixed, and for k == 1 it is reached immediately (0×0 matrix).
Matrix::adjugate (src/matrix.rs:89) computes exactly what a cofactor inverse needs and is currently dead code — nothing calls it. Wiring inverse to adjugate / determinant would work for small n, though see defect 3.
3. det() is O(n!) (src/matrix.rs:3-25)
Recursive cofactor expansion, allocating a fresh Vec per minor per level. Fine at n=1..2; unusable beyond ~n=8. quality() on a 10-team match would need a 9×9 determinant — 362,880 recursive terms with an allocation each.
If N-group quality is worth supporting, Matrix needs an LU (or Cholesky, since the matrices here are symmetric positive-definite by construction) factorization for both determinant and inverse. That also removes the adjugate/minor cofactor path entirely.
Also worth fixing while here
predict_quality silently drops unknown keys. Both filter_maps in src/history.rs:393-400 discard keys that aren't interned and competitors with no posterior. Querying a team of entirely-unknown players yields an empty group and a plausible-looking number rather than an error — measured: h.predict_quality(&[&[&"a"], &[&"nobody"]]) returns 0.1585… instead of signalling that half the query was meaningless.
Matrix has no dimension checks.Mul/Add (src/matrix.rs:155-213) assume conformable shapes and will silently read wrong elements or panic on OOB.
Over-constrained lifetimes.impl<'a> ops::Mul<&'a Matrix> for &'a Matrix (src/matrix.rs:177) forces both operands to share one lifetime; they should be independent.
Scope
Fix the column indexing in quality().
Implement Matrix::inverse for n > 1, or replace the hand-rolled Matrix with a factorization-based implementation.
Define and enforce the behaviour for degenerate inputs: 0 groups, 1 group, empty groups.
Make predict_quality surface unknown keys rather than silently dropping them.
Acceptance
quality() returns finite, correct values for 2, 3, 5, and 10 groups, cross-checked against sublee/trueskill (this is the open README Todo item — see #7).
Degenerate inputs return an error or a documented value; no panics.
predict_quality covered by tests at 3+ teams.
`quality()` is documented and exported as a general N-group match-quality function (`src/lib.rs:199`), but it only works for exactly two groups. Verified on `main` @ 2b5d3b1, `--release`:
```rust
quality(&[&[a], &[b]], beta) // ok -> 0.8115...
quality(&[&[a], &[b], &[c]], beta) // PANIC: index out of bounds: the len is 3 but the index is 3
quality(&[&[a]], beta) // PANIC: "eh, okey"
```
`History::predict_quality` calls straight through (`src/history.rs:405`), so it panics identically:
```rust
h.predict_quality(&[&[&"a"], &[&"b"], &[&"c"]]) // PANIC at src/lib.rs:240
```
Three separate defects.
## 1. The `x` column counter is wrong (`src/lib.rs:224-244`)
```rust
let mut t = 0;
let mut x = 0;
for (row, group) in rating_groups.windows(2).enumerate() {
for n in t..t + current.len() {
rotated_a_matrix[(row, n)] = flatten_weights[n];
x += 1; // <-- x tracks the +1 block...
}
t += current.len();
for n in x..x + next.len() { // <-- ...but is then reused as the -1 block start
rotated_a_matrix[(row, n)] = -flatten_weights[n];
}
x += next.len();
}
```
`x` accumulates across rows while `t` restarts the window each iteration, so from row 1 onward the two counters disagree. With three singleton groups: row 1 writes `+1` at column 1 (correct) then attempts `-1` at column 3 of a 2×3 matrix → out of bounds. For two groups the loop body runs once and `x` never diverges from `t`, which is why the only existing test (`test_quality`, `src/lib.rs:273`) passes.
The `-1` block should start at `t + current.len()` — the column right after the `+1` block — making the separate `x` counter unnecessary.
## 2. `Matrix::inverse` is unimplemented for n > 1 (`src/matrix.rs:112-122`)
```rust
pub fn inverse(&self) -> Matrix {
let mut matrix = Matrix::new(self.width, self.height);
if self.height == self.width && self.height == 1 {
matrix[(0, 0)] = 1.0 / self[(0, 0)];
} else {
panic!("eh, okey")
}
matrix
}
```
`quality()` calls `middle.inverse()` where `middle` is `(k-1)×(k-1)` for `k` groups. Only `k == 2` yields a 1×1 matrix. For `k >= 3` this panic is reached the moment defect 1 is fixed, and for `k == 1` it is reached immediately (0×0 matrix).
`Matrix::adjugate` (`src/matrix.rs:89`) computes exactly what a cofactor inverse needs and is currently **dead code** — nothing calls it. Wiring `inverse` to `adjugate / determinant` would work for small n, though see defect 3.
## 3. `det()` is O(n!) (`src/matrix.rs:3-25`)
Recursive cofactor expansion, allocating a fresh `Vec` per minor per level. Fine at n=1..2; unusable beyond ~n=8. `quality()` on a 10-team match would need a 9×9 determinant — 362,880 recursive terms with an allocation each.
If N-group quality is worth supporting, `Matrix` needs an LU (or Cholesky, since the matrices here are symmetric positive-definite by construction) factorization for both `determinant` and `inverse`. That also removes the `adjugate`/`minor` cofactor path entirely.
## Also worth fixing while here
- **`predict_quality` silently drops unknown keys.** Both `filter_map`s in `src/history.rs:393-400` discard keys that aren't interned and competitors with no posterior. Querying a team of entirely-unknown players yields an empty group and a plausible-looking number rather than an error — measured: `h.predict_quality(&[&[&"a"], &[&"nobody"]])` returns `0.1585…` instead of signalling that half the query was meaningless.
- **`Matrix` has no dimension checks.** `Mul`/`Add` (`src/matrix.rs:155-213`) assume conformable shapes and will silently read wrong elements or panic on OOB.
- **Over-constrained lifetimes.** `impl<'a> ops::Mul<&'a Matrix> for &'a Matrix` (`src/matrix.rs:177`) forces both operands to share one lifetime; they should be independent.
## Scope
- Fix the column indexing in `quality()`.
- Implement `Matrix::inverse` for n > 1, or replace the hand-rolled `Matrix` with a factorization-based implementation.
- Define and enforce the behaviour for degenerate inputs: 0 groups, 1 group, empty groups.
- Make `predict_quality` surface unknown keys rather than silently dropping them.
## Acceptance
- `quality()` returns finite, correct values for 2, 3, 5, and 10 groups, cross-checked against [sublee/trueskill](https://github.com/sublee/trueskill) (this is the open README Todo item — see #7).
- Degenerate inputs return an error or a documented value; no panics.
- `predict_quality` covered by tests at 3+ teams.
The contrast-matrix column counter is gone — the negative block always starts immediately after the positive one, so the second counter was never needed.
Matrix::determinant and Matrix::inverse now share one LU decomposition with partial pivoting, replacing both the O(n!) cofactor expansion and panic!("eh, okey"). The dead adjugate/minor path is removed, and multiply/add gained dimension checks.
Degenerate inputs assert with a message naming the requirement rather than underflowing or producing NaN.
The two-group golden is unchanged. N-group behaviour is pinned by invariants in tests/quality.rs — permutation invariance, quality falling as a skill gap widens, finite and in-range for 2..=8 groups, uneven group sizes, and predict_quality at three teams.
Not done: the sublee/trueskill reference cross-check from the acceptance criteria. No reference values were available to compare against, so the N-group results are verified structurally rather than numerically. That remains the one open item in the README Todo list, and is the right place to track it.
predict_quality now documents that it panics on unknown keys rather than silently dropping them into an empty group — a loud failure instead of a confident wrong number, though returning Result would be better still (see #21).
Fixed in 0d32690.
All three defects addressed:
1. The contrast-matrix column counter is gone — the negative block always starts immediately after the positive one, so the second counter was never needed.
2. `Matrix::determinant` and `Matrix::inverse` now share one LU decomposition with partial pivoting, replacing both the O(n!) cofactor expansion and `panic!("eh, okey")`. The dead `adjugate`/`minor` path is removed, and multiply/add gained dimension checks.
3. Degenerate inputs assert with a message naming the requirement rather than underflowing or producing NaN.
The two-group golden is unchanged. N-group behaviour is pinned by invariants in `tests/quality.rs` — permutation invariance, quality falling as a skill gap widens, finite and in-range for 2..=8 groups, uneven group sizes, and `predict_quality` at three teams.
**Not done:** the sublee/trueskill reference cross-check from the acceptance criteria. No reference values were available to compare against, so the N-group results are verified structurally rather than numerically. That remains the one open item in the README Todo list, and is the right place to track it.
`predict_quality` now documents that it panics on unknown keys rather than silently dropping them into an empty group — a loud failure instead of a confident wrong number, though returning `Result` would be better still (see #21).
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.
quality()is documented and exported as a general N-group match-quality function (src/lib.rs:199), but it only works for exactly two groups. Verified onmain@2b5d3b1,--release:History::predict_qualitycalls straight through (src/history.rs:405), so it panics identically:Three separate defects.
1. The
xcolumn counter is wrong (src/lib.rs:224-244)xaccumulates across rows whiletrestarts the window each iteration, so from row 1 onward the two counters disagree. With three singleton groups: row 1 writes+1at column 1 (correct) then attempts-1at column 3 of a 2×3 matrix → out of bounds. For two groups the loop body runs once andxnever diverges fromt, which is why the only existing test (test_quality,src/lib.rs:273) passes.The
-1block should start att + current.len()— the column right after the+1block — making the separatexcounter unnecessary.2.
Matrix::inverseis unimplemented for n > 1 (src/matrix.rs:112-122)quality()callsmiddle.inverse()wheremiddleis(k-1)×(k-1)forkgroups. Onlyk == 2yields a 1×1 matrix. Fork >= 3this panic is reached the moment defect 1 is fixed, and fork == 1it is reached immediately (0×0 matrix).Matrix::adjugate(src/matrix.rs:89) computes exactly what a cofactor inverse needs and is currently dead code — nothing calls it. Wiringinversetoadjugate / determinantwould work for small n, though see defect 3.3.
det()is O(n!) (src/matrix.rs:3-25)Recursive cofactor expansion, allocating a fresh
Vecper minor per level. Fine at n=1..2; unusable beyond ~n=8.quality()on a 10-team match would need a 9×9 determinant — 362,880 recursive terms with an allocation each.If N-group quality is worth supporting,
Matrixneeds an LU (or Cholesky, since the matrices here are symmetric positive-definite by construction) factorization for bothdeterminantandinverse. That also removes theadjugate/minorcofactor path entirely.Also worth fixing while here
predict_qualitysilently drops unknown keys. Bothfilter_maps insrc/history.rs:393-400discard keys that aren't interned and competitors with no posterior. Querying a team of entirely-unknown players yields an empty group and a plausible-looking number rather than an error — measured:h.predict_quality(&[&[&"a"], &[&"nobody"]])returns0.1585…instead of signalling that half the query was meaningless.Matrixhas no dimension checks.Mul/Add(src/matrix.rs:155-213) assume conformable shapes and will silently read wrong elements or panic on OOB.impl<'a> ops::Mul<&'a Matrix> for &'a Matrix(src/matrix.rs:177) forces both operands to share one lifetime; they should be independent.Scope
quality().Matrix::inversefor n > 1, or replace the hand-rolledMatrixwith a factorization-based implementation.predict_qualitysurface unknown keys rather than silently dropping them.Acceptance
quality()returns finite, correct values for 2, 3, 5, and 10 groups, cross-checked against sublee/trueskill (this is the open README Todo item — see #7).predict_qualitycovered by tests at 3+ teams.Fixed in
0d32690.All three defects addressed:
Matrix::determinantandMatrix::inversenow share one LU decomposition with partial pivoting, replacing both the O(n!) cofactor expansion andpanic!("eh, okey"). The deadadjugate/minorpath is removed, and multiply/add gained dimension checks.The two-group golden is unchanged. N-group behaviour is pinned by invariants in
tests/quality.rs— permutation invariance, quality falling as a skill gap widens, finite and in-range for 2..=8 groups, uneven group sizes, andpredict_qualityat three teams.Not done: the sublee/trueskill reference cross-check from the acceptance criteria. No reference values were available to compare against, so the N-group results are verified structurally rather than numerically. That remains the one open item in the README Todo list, and is the right place to track it.
predict_qualitynow documents that it panics on unknown keys rather than silently dropping them into an empty group — a loud failure instead of a confident wrong number, though returningResultwould be better still (see #21).