|
| 1 | +## 2026-09-22 — D-LGJ-FOLD-5: the consumer stops asking sixteen times, and a red pin on `main` gets root-caused |
| 2 | + |
| 3 | +The consumer half of minor 12. `BricksQuery.sumBy()` was sixteen queries — |
| 4 | +one `sumOf` over `where(REGION.eq(v))` per region, each paying plan evaluation |
| 5 | +into a selection mask plus the reduction over it — and is now one fused |
| 6 | +`sumByGroup`. No selection mask is built at any point. |
| 7 | + |
| 8 | +- **Measured through the consumer, not inherited from the core suite:** 1 |
| 9 | + crossing at 1,000 and 64,000 rows, and — new — at 1, 16 and 64 groups. |
| 10 | + The old path was already invariant in ROWS, which is why its literal was |
| 11 | + asserted at both row counts; this one is invariant in GROUPS as well, a |
| 12 | + strictly stronger claim, so it gets its own arm instead of riding on the |
| 13 | + row-count one. |
| 14 | +- **The group-count arm needed a seam, and the seam is the honest kind.** The |
| 15 | + public `sumBy()` always asks for exactly `Orders.REGIONS` groups, so nothing |
| 16 | + the test could previously reach varied the group count and *"one crossing |
| 17 | + whatever the group count"* was a doc comment no input could falsify. A |
| 18 | + package-private `sumByGroupCount` serves that arm alone; `getMethods()` does |
| 19 | + not see it, so the aggregate-only-egress guard still audits exactly the |
| 20 | + public surface the guarantee is about. |
| 21 | +- **Disable arm A recovered the old cost formula exactly:** reverting to the |
| 22 | + per-group loop measured **2 / 32 / 128 at 1 / 16 / 64 groups**, identical at |
| 23 | + both row counts — `2 × groups` — with **parity GREEN throughout**. The two |
| 24 | + paths agree on the answer and differ only in cost, which is what makes this a |
| 25 | + migration rather than a behaviour change. Note even the degenerate case |
| 26 | + discriminates: 1 group costs 2 on the old path, 1 on the new. |
| 27 | +- **`Orders.REGIONS` is a mirror, so it is pinned to the fixture and not to a |
| 28 | + copied number.** It replaces three literal 16s with one named source for the |
| 29 | + native generator's classid cardinality, which the ABI manifest does not |
| 30 | + report. A comparison against a second Java literal would drift in lockstep |
| 31 | + with whatever edited it and prove nothing; instead every id below REGIONS |
| 32 | + must carry rows and id REGIONS itself must carry none. Two-sided for a |
| 33 | + reason: arm B (REGIONS = 20) fires the first half only, arm C (REGIONS = 8) |
| 34 | + the second only — at 3,960 rows on id 8 — so neither half alone catches |
| 35 | + both directions. |
| 36 | +- **Return type stays `Map<Integer, Long>`:** sized by the question, one entry |
| 37 | + per group, never by the data. Handing back `GroupTotals` would leak a facade |
| 38 | + type into a consumer's public surface for nothing. |
| 39 | + |
| 40 | +**And a separate finding, which is the part worth remembering: |
| 41 | +`GraphHopTest` was RED on `main`.** 1 FAILED / 65 passed at `bb81d80`, |
| 42 | +established by running it from a clean worktree rather than inferred from the |
| 43 | +diff's shape. Consumers have **no CI line** — the only workflow gates |
| 44 | +`lgj-abi` (fmt, clippy, test) — so nothing reported it, the same |
| 45 | +no-CI-line shape that hid the r2il probe step upstream. Filed as |
| 46 | +`ISS-LGJ-CONSUMERS-HAVE-NO-CI-LINE`. |
| 47 | + |
| 48 | +Root cause, and it is the design rather than a regression: `Mask.words()` no |
| 49 | +longer returns its cached window directly — it re-describes through |
| 50 | +`lgj_mask_describe` and compares the returned epoch against its stamp, so a |
| 51 | +cached address is never read after the substrate moved under it. Root |
| 52 | +`CLAUDE.md` names that as shipped. The pin asserting a second |
| 53 | +`materializeRows()` costs **zero** was written against the pure-cache |
| 54 | +behaviour that preceded it, so the test moved, not the code; reverting the |
| 55 | +number would mean deleting the re-validation. The plan's §3.5 *"describe |
| 56 | +cached per Mask; zero crossings steady-state"* is superseded for `Mask`: the |
| 57 | +words are still not re-fetched and the population is still never re-scanned, |
| 58 | +but the describe is no longer free. Steady-state is **constant per call**. |
| 59 | + |
| 60 | +The re-pin asserts that rather than flipping a literal: a THIRD call is |
| 61 | +measured and required to equal the second. A one-off extra describe would |
| 62 | +satisfy a bare `== 1` on the second call; only a per-call cost satisfies |
| 63 | +second == third. **Disable arm D proves the two arms are independent** — |
| 64 | +restoring the pure cache kills the magnitude arm (expected 1, was 0) and |
| 65 | +leaves the constancy arm GREEN at 0, because it tests a different property. |
| 66 | + |
| 67 | +- **Gates:** core **612/612**, bricks **70/70** (was 62; the six group-count |
| 68 | + arms and two cardinality checks are the +8), graph **68/68** (was 65 + 1 |
| 69 | + failed; the third-call cost and its content check are the +2), trades |
| 70 | + **12/12 + 3/3** — **765 checks**, JDK 28 `--release 28 --enable-preview` |
| 71 | + against a freshly built `abi 0.12, ndarray::simd avx512, release`. Four |
| 72 | + disable arms, each red-then-green. |
| 73 | + |
1 | 74 | ## 2026-09-22 — minor 12: `lgj_plan_group_sum_i32`, the grouped sum that never builds a selection (fold-distillation wave 4) |
2 | 75 |
|
3 | 76 | Upstream first (lance-graph #1256 merged `99cdca38`: the tiled executor, |
@@ -26,9 +99,12 @@ selection exists at any point. |
26 | 99 | - **The eighth named materialisation site:** `Engine.groupSumI32`'s |
27 | 100 | `toArray`, sized by `groups` (the question), pinned in `DoctrineFenceTest` |
28 | 101 | and listed in root `CLAUDE.md`. `GroupTotals` exposes no array. |
29 | | -- **Still owed:** the `bricks` consumer's `sumBy()` still runs the 32-crossing |
30 | | - path; migrating it to `sumByGroup` is a consumer-wave change, not this |
31 | | - one. `lgj_hop` still holds mask-sized Vecs (pre-existing, unrelated). |
| 102 | +- **Still owed:** ⊘ the first clause is DISCHARGED (2026-09-22, D-LGJ-FOLD-5, |
| 103 | + see the section above) — struck in place, not deleted: it was true when |
| 104 | + written. It read *"the `bricks` consumer's `sumBy()` still runs the |
| 105 | + 32-crossing path; migrating it to `sumByGroup` is a consumer-wave change, |
| 106 | + not this one."* That consumer wave has now run. `lgj_hop` still holds |
| 107 | + mask-sized Vecs (pre-existing, unrelated) and remains owed. |
32 | 108 |
|
33 | 109 | ## 2026-09-19 — PR #81 merged (`07aa441`): production IS the Valhalla arm; Panama × Valhalla is one membrane |
34 | 110 |
|
|
0 commit comments