diff --git a/.claude/board/entries/2026-10-25-drf-w1-writeback.md b/.claude/board/entries/2026-10-25-drf-w1-writeback.md index 04549e092..ac5752422 100644 --- a/.claude/board/entries/2026-10-25-drf-w1-writeback.md +++ b/.claude/board/entries/2026-10-25-drf-w1-writeback.md @@ -1,4 +1,4 @@ -# D-DRF W1–W3 — staged writeback coalesced per canonical row (lab) +# D-DRF-W1 — W1–W3: staged writeback coalesced per canonical row (lab) **Status:** TEST-PINNED (lab). **Code:** `crates/drf-focus-probe/src/lib.rs` (`Focus::writeback`, `Writeback::{stage, dirty_rows, flush}`, `RowImage`). **Tests:** `w1_*`, `w2_*`, `w3_*` in diff --git a/.claude/board/entries/2026-10-25-drf-w3-ordered-apply.md b/.claude/board/entries/2026-10-25-drf-w3-ordered-apply.md new file mode 100644 index 000000000..575b1f0ef --- /dev/null +++ b/.claude/board/entries/2026-10-25-drf-w3-ordered-apply.md @@ -0,0 +1,19 @@ +# D-DRF-W3 — ordered same-facet writes apply by declared stream position (lab) + +**Status:** TEST-PINNED (lab). **Code:** `crates/drf-focus-probe/src/lib.rs` +(`Writeback::stage_at`, `flush`). **Tests:** `tests/integration/focus.rs` +`w3_ordered_*`, `w3_equal_positions_or_mixed_ordering_*`. Plan: +`.claude/plans/d-dr64-focus-cache-falsifiers-v1.md` §4 W3. + +- **W3 PASS (ordered arm):** three noncommuting overwrites of one facet (positions 1, 2, 3) + through three aliasing slots give the position-3 value in all 6 staging orders. 64 writes + over 4 rows in a scrambled staging order match a scalar oracle (highest position per row). +- **W3 PASS (refusal arm):** different values at one declared position, or an unordered + `stage` next to a positioned one, refuse with `Conflict`. Equal values coalesce in both. +- **Mechanism:** patches sort by `(ord, offset, pos)` and apply in that order, so the + sort is the order of application; no second pass. +- **Disables, each red on its own test:** sort without `pos` (staging order wins); treat + equal positions as ordered; treat an unordered write as ordered. + +OPEN: positions are caller-declared `u64`s. Nothing yet ties them to a real stream +(CycleWriter sequence, WAL LSN); that is W4–W6. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index db1c96dfe..b12cec492 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,11 +25,12 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -467 entries, 2026-08-06 .. 2026-10-25. +468 entries, 2026-08-06 .. 2026-10-25. | date | entry id | finding | file | |---|---|---|---| -| 2026-10-25 | `D-DRF` | | [2026-10-25-drf-w1-writeback.md](2026-10-25-drf-w1-writeback.md) | +| 2026-10-25 | `D-DRF-W3` | | [2026-10-25-drf-w3-ordered-apply.md](2026-10-25-drf-w3-ordered-apply.md) | +| 2026-10-25 | `D-DRF-W1` | | [2026-10-25-drf-w1-writeback.md](2026-10-25-drf-w1-writeback.md) | | 2026-10-24 | `tanh-precision-symmetric-i8` | | [2026-10-24-tanh-precision-symmetric-i8.md](2026-10-24-tanh-precision-symmetric-i8.md) | | 2026-10-24 | `D-DRF-P2` | | [2026-10-24-drf-p2-breakeven.md](2026-10-24-drf-p2-breakeven.md) | | 2026-10-24 | `D-DRF-P1` | | [2026-10-24-drf-p1-focus-lease-probe.md](2026-10-24-drf-p1-focus-lease-probe.md) | diff --git a/crates/drf-focus-probe/src/lib.rs b/crates/drf-focus-probe/src/lib.rs index 124153213..d704240bf 100644 --- a/crates/drf-focus-probe/src/lib.rs +++ b/crates/drf-focus-probe/src/lib.rs @@ -294,6 +294,10 @@ struct Patch { ord: u16, offset: usize, value: i32, + /// Declared stream position. `None` sorts before every `Some`, so an + /// unordered write is applied first and can only coexist with ordered + /// writes of the same value (checked in `flush`). + pos: Option, } /// The final image of one dirty canonical row, owned for handoff to a writer. @@ -319,8 +323,32 @@ impl Focus { } impl Writeback<'_> { - /// Stage `value` into `facet` of the row behind `slot`. + /// Stage `value` into `facet` of the row behind `slot`, with no declared + /// order: a second write to the same facet must carry the same value. pub fn stage(&mut self, slot: usize, facet: Facet, value: i32) -> Result<(), Refusal> { + self.push(slot, facet, value, None) + } + + /// Stage `value` at declared stream position `pos`. Writes to one facet + /// apply in position order whatever order they were staged in; two + /// different values at one position refuse (W3). + pub fn stage_at( + &mut self, + slot: usize, + facet: Facet, + value: i32, + pos: u64, + ) -> Result<(), Refusal> { + self.push(slot, facet, value, Some(pos)) + } + + fn push( + &mut self, + slot: usize, + facet: Facet, + value: i32, + pos: Option, + ) -> Result<(), Refusal> { let ord = *self .focus .map @@ -331,6 +359,7 @@ impl Writeback<'_> { ord, offset: facet.offset(), value, + pos, }); Ok(()) } @@ -345,8 +374,9 @@ impl Writeback<'_> { /// One image per distinct dirty row, in ordinal order: the sealed source row /// with every staged facet applied. Writes to different facets of one row - /// all survive (W2); two different values for the same facet, or two - /// facets whose byte ranges overlap, refuse (W3). + /// all survive (W2). Writes to one facet apply in declared position + /// order; different values with no order between them (equal positions, + /// or an unordered write), or facets whose byte ranges overlap, refuse (W3). pub fn flush(mut self, soa: &Soa<'_>) -> Result, Refusal> { check_seal(soa, &self.focus.map)?; if self.focus.key.source != soa.id { @@ -355,16 +385,17 @@ impl Writeback<'_> { source_seal: soa.id.index_seal, }); } - self.patches.sort_by_key(|p| (p.ord, p.offset)); + self.patches.sort_by_key(|p| (p.ord, p.offset, p.pos)); for w in self.patches.windows(2) { if w[0].ord != w[1].ord { continue; } - // Same facet with a different value, or two facets whose byte - // ranges overlap: either would silently overwrite a staged write. - // Sorted by offset, so checking neighbours finds every overlap. + // Same facet: different values need two distinct declared + // positions, else nothing says which wins. Different facets must + // not overlap. Sorted by offset, so neighbours find every overlap. let same = w[0].offset == w[1].offset; - if (same && w[0].value != w[1].value) + let ordered = matches!((w[0].pos, w[1].pos), (Some(a), Some(b)) if a != b); + if (same && w[0].value != w[1].value && !ordered) || (!same && w[1].offset < w[0].offset + Facet::WIDTH) { return Err(Refusal::Conflict { @@ -373,6 +404,7 @@ impl Writeback<'_> { }); } } + // Applying in sorted order leaves each facet at its highest position. let mut out: Vec = Vec::new(); for p in &self.patches { if out.last().is_none_or(|img| img.ord != p.ord) { diff --git a/crates/drf-focus-probe/tests/integration/focus.rs b/crates/drf-focus-probe/tests/integration/focus.rs index 5f887ffbf..40294daba 100644 --- a/crates/drf-focus-probe/tests/integration/focus.rs +++ b/crates/drf-focus-probe/tests/integration/focus.rs @@ -490,3 +490,99 @@ fn w3_overlapping_facets_at_different_offsets_refuse_and_adjacent_facets_both_su assert_eq!(row[OFF..OFF + 4], 1i32.to_le_bytes()); assert_eq!(row[OFF + 4..OFF + 8], 2i32.to_le_bytes()); } + +#[test] +fn w3_ordered_same_facet_writes_apply_by_declared_position_in_every_staging_order() { + // Two slots alias row 500. The writes do not commute (overwrite), so the + // result must follow the declared stream position, not the staging order. + let r = rows(); + let soa = Soa { + rows: &r, + id: sid(1, 1, 1, 9), + }; + let m = map_from(304, 9, |s| if s < 3 { 500 } else { (s % NROWS) as u16 }); + let mgr = FocusManager::new(); + let f = mgr.load(&soa, &m, Scope(1), facet()).unwrap(); + let writes = [(0usize, 30i32, 3u64), (1, 10, 1), (2, 20, 2)]; + let perms = [ + [0, 1, 2], + [0, 2, 1], + [1, 0, 2], + [1, 2, 0], + [2, 0, 1], + [2, 1, 0], + ]; + for perm in perms { + let mut wb = f.writeback(); + for i in perm { + let (slot, value, pos) = writes[i]; + wb.stage_at(slot, facet(), value, pos).unwrap(); + } + let imgs = wb.flush(&soa).unwrap(); + assert_eq!(imgs.len(), 1); + assert_eq!( + imgs[0].row[OFF..OFF + 4], + 30i32.to_le_bytes(), + "order {perm:?}" + ); + } + // 64 positioned writes over 4 rows in a scrambled staging order: each row + // ends with the value of its highest declared position (scalar oracle). + let m = map_from(305, 9, |s| [10u16, 20, 30, 40][s % 4]); + let f = mgr.load(&soa, &m, Scope(1), facet()).unwrap(); + let mut wb = f.writeback(); + let mut want = [(0u64, 0i32); 4]; + for k in 0..64usize { + let slot = (k * 37) % 64; + let pos = ((k * 23) % 64) as u64; + let value = (k as i32) * 7 - 100; + wb.stage_at(slot, facet(), value, pos).unwrap(); + if pos >= want[slot % 4].0 { + want[slot % 4] = (pos, value); + } + } + let imgs = wb.flush(&soa).unwrap(); + assert_eq!(imgs.len(), 4); + for (i, img) in imgs.iter().enumerate() { + assert_eq!(img.ord, [10u16, 20, 30, 40][i]); + assert_eq!(img.row[OFF..OFF + 4], want[i].1.to_le_bytes()); + } +} + +#[test] +fn w3_equal_positions_or_mixed_ordering_with_different_values_refuse() { + let r = rows(); + let soa = Soa { + rows: &r, + id: sid(1, 1, 1, 9), + }; + let m = map_from(306, 9, |s| if s < 2 { 500 } else { (s % NROWS) as u16 }); + let mgr = FocusManager::new(); + let f = mgr.load(&soa, &m, Scope(1), facet()).unwrap(); + let conflict = Some(Refusal::Conflict { + ord: 500, + offset: OFF, + }); + // Same declared position, different values: no order to apply them in. + let mut wb = f.writeback(); + wb.stage_at(0, facet(), 1, 5).unwrap(); + wb.stage_at(1, facet(), 2, 5).unwrap(); + assert_eq!(wb.flush(&soa).err(), conflict); + // An unordered write next to an ordered one, different values. + let mut wb = f.writeback(); + wb.stage(0, facet(), 1).unwrap(); + wb.stage_at(1, facet(), 2, 5).unwrap(); + assert_eq!(wb.flush(&soa).err(), conflict); + // Silence twins: equal values coalesce in both cases. + let mut wb = f.writeback(); + wb.stage_at(0, facet(), 4, 5).unwrap(); + wb.stage_at(1, facet(), 4, 5).unwrap(); + assert_eq!(wb.flush(&soa).unwrap().len(), 1); + let mut wb = f.writeback(); + wb.stage(0, facet(), 4).unwrap(); + wb.stage_at(1, facet(), 4, 9).unwrap(); + assert_eq!( + wb.flush(&soa).unwrap()[0].row[OFF..OFF + 4], + 4i32.to_le_bytes() + ); +}