Merge PR #250 (3e417729) into the 2026-09-24 integration wave

Conflicts with #294 in render.rs and project.rs resolved as tested:
#250's side of every hunk (levelled GroupEnd { level }, groups()/group_of,
axis_frame's carried headers, the try_from level check), then #294's shape
(Item { tuple, .. }, FrameEntry::item(..), the DataItem { tuple, collapsed }
arm, and GroupEnd { .. } in folds). Evidence EOF appends keep both sides.
This commit is contained in:
Edward Langley
2026-09-24 02:49:32 -07:00
15 changed files with 1143 additions and 130 deletions
+24
View File
@@ -1210,3 +1210,27 @@ improvise-bqm0. #231's rule, `position(regular).unwrap_or(0)`, fell back to tile
bug #231 fixed. The fixture is `_Index`, `_Dim`, a Label and `_Measure`. The rule
answers 3 (`_Measure`), then 2 (the Label) with `_Measure` removed, and 0 for an
empty bar. Injected: #231's rule answers 0 and fails. The Row rival fails too.
### a_frame_whose_group_level_names_no_category_does_not_deserialize
improvise-e265, ChatGPT Codex's P2 on #250. A `FrameGroup.level` arrives over
the wire, and `AxisFrame::groups` sizes its open-run stack with
`open.resize(g.level, None)`. A frame carrying `level: 4294967295` therefore
drove a multi-gigabyte allocation in the client instead of being refused. At
`e44b1491` the frame deserialized without complaint. The fix checks every level
against `cats.len()` as the frame arrives: `context/code/protocol.md` §3b.
The test serializes a well-formed two-category frame and checks that it reads
back equal. That is the control. It then injects two faults into the JSON, one
at a time: an oversized group level on the **second** marker, and a `GroupEnd`
one level past `cats`. For each fault it asserts two things. The mutated text is
still well-formed JSON. And the refusal names the level ("names no category"),
so the frame is refused for its level and not for its shape. The collapsed,
item-free frame keeps the fixture independent of `FrameEntry::Item`'s shape,
which #275 changes.
Rivals, each run:
- The `try_from` attribute removed: `unwrap_err` panics, because the frame
parses.
- Only the first marker checked (`.next().filter(..)` instead of `.find(..)`):
it fails the same way, because the first marker is in range.