diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index d663e6591..f7bb719bf 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,11 @@ +## 2026-07-16 — E-H268-PROBE-WAVE-1-RESULTS — three H.268 conjectures measured: WH-tile NEUTRAL/not-transferring, signature-checksum PASS (depth-2 blind spot), walk-spectrum KILL of §10(g)'s "decorrelated" half + +**Status:** FINDING (three probes run against shipped code, deterministic seeds, Opus-adjudicated; grades E-WH-TWO-SIDES-SIG-CHECKSUM-1 legs 1+2 and §10(g); plan `h268-probe-wave-v1.md`). No canon changed; one claim-leg CLOSED NOT-TRANSFERRING, one KILLED, one PASSes with a bounded caveat. +**P1 PROBE-WH-MAG → NEUTRAL (negative-leaning); the [H] bare-tile leg CLOSED NOT-TRANSFERRING.** Per-class MSE ratio B/A (WH-rotate-then-cascade ÷ direct i4+i2 — the probe isolates WH pre-rotation as the ONLY variable, a uniform cascade on both paths, not the shipped I4I2 branch), 256 tiles/class, 16-cell tiles: gradient+spike **0.929**, heavy-tailed **1.317**, uniform-noise **1.869**. Misses the <0.9 PASS bar on every class and regresses past 1.1 on two — adjudicated NEUTRAL (strict KILL bar of >1.1 on ALL unmet). Mechanism: WHT₁₆ spreads a tile's outlier energy across all 16 cells (≈1/√16 each), inflating the per-cell quantization floor where the direct cascade quantized 13-14 near-zero cells almost for free. WH helps only where energy is already spread (smooth gradient, 0.929) or where the outlier escapes to a passthrough tier first. The 16-cell probe has NO passthrough escape and NO centroid residual — exactly the two things the shipped row codec (E-PALETTE-RESIDUAL-LADDER-1) pairs WH with — so the row-level win does not transfer to bare per-tile granularity, and the shipped row codec is untouched. Follow-up PROBE-WH-MAG-2 (WH + per-tile passthrough escape + centroid residual) NAMED but DEFERRED — worth running only if a per-tile adaptive-codec consumer materializes (e.g. the geo-elevation continuous-field exit at 4×4 granularity, where 0.929 is a real if sub-threshold thread). +**P2 PROBE-SIG-CHECKSUM → PASS, with a depth-2 blind-spot bound in the verdict.** All three green at Pillar-11's own thresholds: replay-identity bit-exact (Frobenius 0.0), tree-like A→B→A mid-path excursion invisible (0.0 < 1e-9), non-tree interior displacement caught (11.31 > 0.05; level-1 delta 0.0 — level-2 signed area does the discriminating). Caveat (exact identity, not approximation): at DEPTH=2 the digest's null space is strictly LARGER than tree-like equivalence — a non-backtracking interior displacement by an offset PARALLEL to the neighbor chord `path[i+1]−path[i−1]` produces EXACTLY zero signature change (½(off⊗chord − chord⊗off) vanishes when off ∥ chord; level-1 is endpoint-invariant). The probe's `displace_interior_point` picks a perpendicular offset precisely to dodge this null space. Consequence: do NOT claim the depth-2 digest catches all non-tree edits — it catches all with non-zero signed-area component. Mitigable by depth 3 or a paired independently-projected second digest. +**P3 PROBE-WALK-SPECTRUM → KILL of §10(g)'s "decorrelated from the palette lattice by construction" half; the "known period-17 structure" half CONFIRMED.** Walk period-17 harmonics |R| = 0.998/0.996/0.994 (closed form R(17m)=1−8m/N, N=4096) — the KNOWN structure exactly as predicted. But walk |R| at palette-lattice periods peaks at 0.875 (τ=64) vs the LCG baseline's lattice max 0.0205 → 42.7×, far past the 3× KILL bar. Verified by hand: the parity of a full-period-17 coprime-walk permutation is an EXACT period-17 ±1 waveform, so R(τ) is governed by C(τ mod 17) at every lag; τ=64 → 64 mod 17 = 13 → C(13)=C(4)=−15 → −15/17 ≈ −0.882 ≈ measured −0.875. `gcd(17,τ)=1` guarantees a full-period permutation, NOT low autocorrelation (C(4)=−15 is near the ±16 maximum). Corrected claim: the walk gives KNOWN/replayable/self-testable structure (auditability + the period-permutation conformance self-test) — NOT low lattice correlation; the "anti-moiré in concept space / friendlier to I-NOISE-FLOOR-JIRAK" consequence is STRUCK (strong periodic autocorrelation is if anything worse for weak-dependence rates than a PRNG's ≈−1/N floor). Genuine decorrelation, if ever needed, requires a DESIGNED low-autocorrelation sequence (Legendre/quadratic-residue Paley over a prime p≫17, or a maximal-length LFSR m-sequence). Unaffected: the walk's bijectivity/quantization role and OGAR D-QUANTGATE stay canon (the coprime walk QUANTIZES by construction — bijective closure + WGSL-portable integer stride — which this probe neither tests nor challenges); the KILL is scoped strictly to the anti-moiré decorrelation consequence as originally worded. +**Cross-ref:** E-WH-TWO-SIDES-SIG-CHECKSUM-1 (leg 1 CLOSED not-transferring for bare tiles, leg 2 PASS with caveat), E-PALETTE-RESIDUAL-LADDER-1 (shipped row codec, untouched), E-H268-REPLAYABLE-TILE-1 §(g) (KILLED as worded), I-NOISE-FLOOR-JIRAK, ndarray `pr-x12-h268-morton-wgpu-synergies.md` §5 results sub-table + §10 re-grades + §10(g) correction, plan `h268-probe-wave-v1.md` Results. Probes: `crates/bgz-tensor/src/adaptive_codec.rs` mod probe_wh_mag, `crates/jc/src/sig_checksum.rs`, `crates/helix/src/walk_spectrum.rs` — deterministic; graded probes (P1/P3) assert only structural sanity, P2 asserts a deterministic identity by design. + ## 2026-07-16 — E-WH-TWO-SIDES-SIG-CHECKSUM-1 — one Walsh–Hadamard family for both pyramid sides; Hambly–Lyons signature as the replayable-trajectory checksum (probe-gated forward synthesis) **Status:** CONJECTURE (operator-proposed synthesis; [H] and [S] respectively; two named probes; nothing shipped, no canon changed). diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index 20a4a2ba1..8ff173535 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -1,3 +1,18 @@ +## 2026-07-16 — h268-probe-wave v1 — ACTIVE + +**Plan:** `.claude/plans/h268-probe-wave-v1.md` +**Scope:** convert three cheap conjectures from the merged H.268 arc +(ndarray #242-#245 / lance-graph #695-#701) into measured verdicts, +CPU-only, no new deps: PROBE-WH-MAG (WHT₁₆ magnitude preconditioner vs +direct i4+i2, in bgz-tensor — grades E-WH-TWO-SIDES-SIG-CHECKSUM-1 leg 1), +PROBE-SIG-CHECKSUM (depth-2 signature as replayable trajectory digest, in +jc behind `hambly-lyons` — leg 2), PROBE-WALK-SPECTRUM (stride-4-mod-17 +autocorrelation vs PRNG baseline at palette-lattice periods, in helix — +grades §10(g) anti-confabulation). Explicit non-scope: PROBE-GPU-LUT +(wgpu), OGAR PHASE-1/PERT-RHO/PYR-1, A8. Sonnet drafters + Opus +adjudication; autonomous merge after clean CI/review (operator-granted +this goal). + ## 2026-07-10 — temporal-markov-and-style-classes v1 — ACTIVE **Plan:** `.claude/plans/temporal-markov-and-style-classes-v1.md` diff --git a/.claude/plans/h268-probe-wave-v1.md b/.claude/plans/h268-probe-wave-v1.md new file mode 100644 index 000000000..52bad9e00 --- /dev/null +++ b/.claude/plans/h268-probe-wave-v1.md @@ -0,0 +1,184 @@ +# H.268 Probe Wave v1 — WH-MAG · SIG-CHECKSUM · WALK-SPECTRUM + +> Date: 2026-07-16. Status: ACTIVE (this session, autoattended). +> Parent arc: the H.268 documentation arc (ndarray #242-#245, lance-graph +> #695-#701, all MERGED). This wave converts three of its cheapest +> conjectures into measured verdicts. Everything runs CPU-only against +> shipped code; the heavier gates (PROBE-GPU-LUT wgpu harness, OGAR +> PHASE-1/PERT-RHO/PYR-1, A8) are explicitly OUT of this wave. +> Execution model per operator directive: Sonnet drafters (grindwork, +> edit-only + targeted own-crate `cargo test`), Opus filigree reviewer +> (adjudication vs pass/kill), main thread = spec, central gates, +> commits, PR, autonomous resolve + merge after clean CI/reviews. + +## P1 — PROBE-WH-MAG (grades E-WH-TWO-SIDES-SIG-CHECKSUM-1 leg 1, [H]) + +**Question:** does Hadamard-rotating a 4×4 tile's 16-cell magnitude +vector before i4+i2 quantization reduce reconstruction error vs direct +i4+i2, as bgz-tensor's row-level result suggests? + +- **Home:** `crates/bgz-tensor/src/adaptive_codec.rs` — new `#[cfg(test)] + mod probe_wh_mag` (same crate so the private `hadamard_rotate` and the + `quantize_f32_to_i4/i2` + dequant helpers are reachable; follow the + file's existing test style). +- **Method:** deterministic synthetic tile fields (seeded SplitMix64-style + LCG, NO `rand` dep), 16 cells each, three classes × ≥256 tiles: + (a) smooth gradient + rare spike (elevation-like: linear ramp with one + cell perturbed 8-32× the ramp step); (b) heavy-tailed spiky (2-3 outlier + cells at 10-100× the base scale); (c) uniform noise (control — WH should + neither help nor hurt much). For each tile: path A = i4-quantize → + dequant → residual → i2-quantize → dequant (the shipped cascade, direct); + path B = `hadamard_rotate`(16) first, same cascade, inverse-rotate after + (WH is self-inverse up to 1/16 scale — get the scaling right; verify + round-trip on an unquantized tile first as a self-check assertion). + Metric: per-class mean squared reconstruction error ratio B/A. +- **Verdict bands (explicit precedence — amended per review, mixed + outcomes were uncategorized as first written):** **PASS** if ANY of + class (a)/(b) has B/A < 0.9 (WH materially helps where outliers + exist); else **KILL** if B/A > 1.1 on ALL classes (WH hurts at 16-cell + granularity); else **NEUTRAL** (every remaining combination, including + mixed results — record, conjecture stays open, tile-transfer + unproven). +- Print the three ratios in the test output (`--nocapture`-friendly + `eprintln!`) AND assert the self-check; the verdict assertion itself is + informational (assert only the round-trip identity, not the pass/kill — + the Opus reviewer adjudicates the printed numbers so a NEUTRAL result + does not red the suite). + +## P2 — PROBE-SIG-CHECKSUM (grades leg 2, [S]) + +**Question:** does the depth-2 truncated signature work as a replayable +trajectory digest — replay-identical, tree-like edits invisible, non-tree +perturbations caught? + +- **Home:** `crates/jc/src/sig_checksum.rs`, NEW module behind the + existing `hambly-lyons` feature (mirror `hambly_lyons.rs`'s structure, + thresholds, and `sigker::signature_truncated` usage — READ that file + first; reuse its `signature_distance` idea, its DIM/DEPTH constants + style, and register nothing in the pillar `prove` list — this is a + probe module, not a 12th pillar). +- **Method:** a deterministic 2-D "trajectory" path of ~64 points (seeded + integer walk mapped to f64). Three tests: + (a) `replay_identity` — same path built twice → byte-identical + signature levels (exact f64 equality is expected — same ops, same + order; if that proves flaky use distance < 1e-12); + (b) `tree_like_edit_invisible` — insert an A→B→A excursion mid-path → + signature distance to the original < the module's zero-threshold + (reuse hambly_lyons.rs's forward-leg tolerance); + (c) `non_tree_edit_caught` — displace one interior point off the path + (a triangle-style detour that does NOT backtrack) → distance > the + converse-leg threshold. +- **PASS:** all three green. **KILL:** (b) or (c) fails at the module's + own thresholds (then the digest idea needs depth > 2 or is wrong). +- Wire `pub mod sig_checksum;` into `crates/jc/src/lib.rs` under the same + feature gate style as `hambly_lyons`. + +## P3 — PROBE-WALK-SPECTRUM (grades §10(g) anti-confabulation, [H]) + +**Question:** is the stride-4-mod-17 walk's dependence structure actually +"known and concentrated" (period-17 harmonics only, no energy at +palette-lattice periods) vs a PRNG baseline — the measured basis the +CodeRabbit round demanded for the I-NOISE-FLOOR-JIRAK-friendliness claim? + +- **Home:** `crates/helix/src/walk_spectrum.rs`, NEW module (test-only is + fine: `#![cfg(test)]`-style or a `#[cfg(test)] mod` in a thin file), + using `constants::{MODULUS, STRIDE}`. +- **Method:** build the ±1 sign sequence s[k] = ±1 from the walk (e.g. + parity of the residue `(start + STRIDE·k) mod MODULUS`, N = 4096 = + 16 × 256 full periods) and a seeded LCG ±1 baseline of equal length. + Compute plain circular autocorrelation R(τ) for τ ∈ 1..=512 (O(N·τmax) + is fine at this size; NO fft dep). Report: (i) the walk's |R| at + multiples of 17 (expected ≈ 1, the known structure); (ii) the walk's + |R| at τ ∈ {256, 128, 64, 32, 16} (the palette-lattice periods; note + gcd(17, 256) = 1 so expected ≈ noise floor); (iii) the LCG baseline's + max |R| over the same τ set. +- **PASS:** walk lattice-period |R| ≤ 2× the LCG baseline's max |R| AND + walk period-17 |R| ≥ 0.9 (dependence is exactly where predicted, + nowhere else). **KILL:** walk |R| at any lattice period > 3× baseline + (the walk aliases against the palette lattice — §10(g) dies as stated). +- Same output discipline as P1: print the table, assert only structural + sanity (sequence length, ±1 values), reviewer adjudicates. + +## Iron rules for this wave + +- Sonnet drafters: edit-only + AT MOST their own targeted + `cargo test --manifest-path crates//Cargo.toml [--features ...]` + (bgz-tensor / jc+`--features hambly-lyons` / helix). NO workspace + builds, NO worktrees, NO commits, NO new dependencies (no `rand`, no + `rustfft` — seeded integer generators and O(N·τ) loops only). +- No model identifier in file content. `// SAFETY:` n/a (no unsafe — + unsafe is BANNED in this wave; if a drafter thinks it needs unsafe, + STOP and report). +- Probes must be deterministic (fixed seeds) and leave the existing test + suites green. +- Main thread runs the central gates: targeted `cargo test` for the three + crates + `cargo fmt --check`-equivalent on touched files. +- Opus filigree reviewer adjudicates printed numbers vs PASS/NEUTRAL/KILL + above; verdicts land in EPIPHANIES (prepend) + this plan file's results + section + the ndarray matrix-doc §5 probe queue (companion PR). +- Autonomous merge authority (operator, 2026-07-16 this goal): after CI + green + external review comments resolved, the PR is merged by the + session (squash/merge per repo default), then the wave continues. + +**Reproducibility note (amended per review):** every generator constant +is pinned IN the probe code, which is the normative source — WH-MAG: +per-class SplitMix64 seeds in `probe_wh_mag` (`adaptive_codec.rs`); +SIG-CHECKSUM: the seeded integer walk in `sig_checksum.rs`; +WALK-SPECTRUM: `WALK_START = 0`, `LCG_SEED = 0x9E37_79B9_7F4A_7C15`, +PCG multiplier/increment pair, N = 4096, τ_max = 512 +(`walk_spectrum.rs`). The plan intentionally does not duplicate the +constants; reproduce by running the named test modules. + +**LCG-baseline scope note (amended per review):** the P3 "vs baseline" +comparison is DESCRIPTIVE — the binding evidence for the verdict is the +closed-form C(τ mod 17) structure (exact integer arithmetic), not a +statistical significance test; any future significance phrasing must +derive its threshold via Jirak 2016 weak-dependence rates per +I-NOISE-FLOOR-JIRAK. + +## Results (filled post-run) + +Run 2026-07-16 against shipped code (deterministic seeds; Opus reviewer +adjudicated the printed numbers vs the PASS/NEUTRAL/KILL bands above). +All three test modules green; the graded probes (P1, P3) assert only +structural sanity, so a NEUTRAL/KILL verdict does not red the suite +(P2's asserts are a deterministic identity, correct to assert). + +- **PROBE-WH-MAG → NEUTRAL (negative-leaning); [H] bare-tile leg CLOSED + NOT-TRANSFERRING.** B/A = gradient+spike 0.929, heavy-tailed 1.317, + uniform-noise 1.869 (the probe isolates WH pre-rotation as the only + variable — a uniform cascade on both paths, not the shipped I4I2 + branch). Misses the <0.9 PASS bar everywhere; regresses past 1.1 on + (b)+(c); (a) not >1.1 so not a strict KILL → NEUTRAL. WHT₁₆ spreads + outlier energy tile-wide, inflating the per-cell quantization floor + where the direct cascade got 13-14 near-zero cells for free. The + row-level win (E-PALETTE-RESIDUAL-LADDER-1) does not transfer to bare + 16-cell tiles because the probe has neither the passthrough escape + tier nor the centroid residual the shipped row codec pairs WH with; + the shipped codec is untouched. Follow-up PROBE-WH-MAG-2 (WH + escape + tier + centroid residual) named, deferred to a per-tile-codec + consumer. +- **PROBE-SIG-CHECKSUM → PASS (with a depth-2 blind-spot bound).** + replay bit-exact (0.0); tree-like mid-path excursion invisible + (0.0 < 1e-9); non-tree interior displacement caught (11.31 > 0.05, + level-1 delta 0.0). Caveat: at DEPTH=2 an interior displacement + PARALLEL to the neighbor chord is EXACTLY signature-invisible (the + signed-area term vanishes) — the digest's null space exceeds + tree-like equivalence, so it does NOT catch all non-tree edits. The + probe uses a perpendicular offset by design. Mitigable by depth 3 or + a paired second digest. +- **PROBE-WALK-SPECTRUM → KILL (of §10(g)'s "decorrelated by + construction" half; period-17-structure half CONFIRMED).** Period-17 + harmonics |R| = 0.998/0.996/0.994 (R(17m)=1−8m/N). Lattice-period + walk |R| max 0.875 (τ=64) vs LCG 0.0205 → 42.7× ≫ 3× KILL bar. + R(64) verified: 64 mod 17 = 13, C(13)=C(4)=−15, −15/17 ≈ −0.882 ≈ + −0.875 — an intrinsic period-17 sidelobe, not lattice aliasing. + Coprimality ≠ decorrelation. Corrected claim: the walk gives + KNOWN/replayable structure (auditability), NOT low lattice + correlation; decorrelation would need a designed low-autocorrelation + sequence (Legendre/Paley over p≫17, or an m-sequence). D-QUANTGATE / + bijectivity unaffected — only the anti-moiré-in-concept-space + consequence dies. + +Verdicts: EPIPHANIES `E-H268-PROBE-WAVE-1-RESULTS`; ndarray matrix doc +§5 results sub-table + §10 re-grades + §10(g) correction (companion PR). diff --git a/crates/bgz-tensor/src/adaptive_codec.rs b/crates/bgz-tensor/src/adaptive_codec.rs index 09404043c..29125f995 100644 --- a/crates/bgz-tensor/src/adaptive_codec.rs +++ b/crates/bgz-tensor/src/adaptive_codec.rs @@ -436,3 +436,236 @@ mod tests { ); } } + +/// PROBE-WH-MAG (h268-probe-wave-v1.md P1) — does Hadamard-rotating a 16-cell +/// tile before i4+i2 quantization reduce reconstruction error vs the direct +/// cascade, as bgz-tensor's row-level result (see `encode()` above, which +/// always rotates) suggests? Prints per-class MSE ratios; the PASS/NEUTRAL/ +/// KILL verdict is adjudicated by the reviewer against the printed numbers, +/// not asserted here (plan iron rule: only structural sanity + the WH +/// round-trip identity are asserted). +#[cfg(test)] +mod probe_wh_mag { + use super::*; + + const TILE_DIM: usize = 16; + const TILES_PER_CLASS: usize = 256; + + /// Deterministic SplitMix64 generator — no `rand` dependency (iron rule: + /// no new deps for this wave). + struct SplitMix64 { + state: u64, + } + + impl SplitMix64 { + fn new(seed: u64) -> Self { + SplitMix64 { state: seed } + } + + fn next_u64(&mut self) -> u64 { + self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.state; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + + /// Uniform f64 in [0, 1). + fn next_f64(&mut self) -> f64 { + (self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64) + } + + /// Uniform f64 in [lo, hi). + fn next_range(&mut self, lo: f64, hi: f64) -> f64 { + lo + self.next_f64() * (hi - lo) + } + + fn next_index(&mut self, n: usize) -> usize { + (self.next_u64() % n as u64) as usize + } + } + + /// Class (a): smooth gradient with one cell perturbed 8-32x the ramp step + /// (elevation-like: mostly-smooth field, rare spike). + fn gen_gradient_spike(rng: &mut SplitMix64) -> [f32; TILE_DIM] { + let step = rng.next_range(0.1, 1.0); + let mut cell = [0.0f32; TILE_DIM]; + for (i, v) in cell.iter_mut().enumerate() { + *v = (i as f64 * step) as f32; + } + let spike_idx = rng.next_index(TILE_DIM); + let factor = rng.next_range(8.0, 32.0); + cell[spike_idx] += (step * factor) as f32; + cell + } + + /// Class (b): heavy-tailed spiky — small base noise plus 2-3 outlier + /// cells at 10-100x the base scale. + fn gen_heavy_tailed(rng: &mut SplitMix64) -> [f32; TILE_DIM] { + let base_scale = rng.next_range(0.01, 0.1); + let mut cell = [0.0f32; TILE_DIM]; + for v in cell.iter_mut() { + *v = rng.next_range(-base_scale, base_scale) as f32; + } + let n_outliers = 2 + (rng.next_u64() % 2) as usize; // 2 or 3 + + // Distinct indices: resample collisions so the class always carries + // its documented 2-3 unique outlier cells (a collision would silently + // degrade a tile to a single outlier and bias the class MSE). + let mut indices: Vec = Vec::with_capacity(n_outliers); + while indices.len() < n_outliers { + let idx = rng.next_index(TILE_DIM); + if !indices.contains(&idx) { + indices.push(idx); + } + } + for idx in indices { + let factor = rng.next_range(10.0, 100.0); + let sign = if rng.next_f64() < 0.5 { -1.0 } else { 1.0 }; + cell[idx] = (sign * base_scale * factor) as f32; + } + cell + } + + /// Class (c): uniform noise, no structure — the control (WH should + /// neither help nor hurt much here). + fn gen_uniform_noise(rng: &mut SplitMix64) -> [f32; TILE_DIM] { + let mut cell = [0.0f32; TILE_DIM]; + for v in cell.iter_mut() { + *v = rng.next_range(-1.0, 1.0) as f32; + } + cell + } + + /// Path A: the shipped i4->residual->i2 cascade, applied directly (no + /// Hadamard rotation). + fn cascade_direct(v: &[f32]) -> Vec { + let n = v.len(); + let (i4_codes, i4_params) = quantize_f32_to_i4(v); + let dequant1 = dequantize_i4_to_f32(&i4_codes, &i4_params, n); + let res2: Vec = v.iter().zip(dequant1.iter()).map(|(a, b)| a - b).collect(); + let (i2_codes, i2_params) = quantize_f32_to_i2(&res2); + let dequant2 = dequantize_i2_to_f32(&i2_codes, &i2_params, n); + dequant1 + .iter() + .zip(dequant2.iter()) + .map(|(a, b)| a + b) + .collect() + } + + /// Path B: `hadamard_rotate` first, same cascade, inverse-rotate after. + /// `wht_f32` (which `hadamard_rotate` wraps) already normalizes by + /// `1/sqrt(dim)` and is exactly self-inverse (see its own doc-comment + /// example), so no extra 1/16 scale factor is needed at dim=16 — + /// `hadamard_rotate(hadamard_rotate(v))` reproduces `v` directly. This + /// is confirmed by `wh_round_trip_identity` below. + fn cascade_wh(v: &[f32]) -> Vec { + let n = v.len(); + let rotated = hadamard_rotate(v, n); + let recon_rotated = cascade_direct(&rotated); + hadamard_rotate(&recon_rotated, n) + } + + fn mse(a: &[f32], b: &[f32]) -> f64 { + a.iter() + .zip(b.iter()) + .map(|(x, y)| { + let d = (*x - *y) as f64; + d * d + }) + .sum::() + / a.len() as f64 + } + + /// Self-check (mandatory per the plan): `hadamard_rotate` round-trips to + /// identity on an unquantized 16-vector. Run BEFORE trusting path B's + /// inverse-rotate step below. + /// + /// Tolerance note (spec deviation): the plan specifies 1e-6. `wht_f32`'s + /// own doc-comment example asserts the SECOND (round-trip) application + /// at `1e-5`, not `1e-6` — the single-pass forward transform hits 1e-6, + /// but two passes of the SIMD butterfly + `1/sqrt(dim)` normalization + /// accumulate f32 rounding error that measurably exceeds 1e-6 at + /// dim=16 (observed worst case ~1.1e-6 on values up to magnitude 10). + /// Matching the library's own established round-trip tolerance (1e-5) + /// here rather than the plan's number. + #[test] + fn wh_round_trip_identity() { + let mut rng = SplitMix64::new(0xABCD_1234_5678_9E01); + for _ in 0..64 { + let mut v = [0.0f32; TILE_DIM]; + for x in v.iter_mut() { + *x = rng.next_range(-10.0, 10.0) as f32; + } + let rotated = hadamard_rotate(&v, TILE_DIM); + let back = hadamard_rotate(&rotated, TILE_DIM); + for i in 0..TILE_DIM { + assert!( + (v[i] - back[i]).abs() < 1e-5, + "WH round-trip mismatch at cell {}: {} vs {}", + i, + v[i], + back[i] + ); + } + } + } + + #[test] + fn probe_wh_mag_class_ratios() { + type TileGen = fn(&mut SplitMix64) -> [f32; TILE_DIM]; + let classes: [(&str, u64, TileGen); 3] = [ + ("gradient+spike", 0x1111_0000_AAAA_0001, gen_gradient_spike), + ("heavy-tailed", 0x2222_0000_BBBB_0002, gen_heavy_tailed), + ("uniform-noise", 0x3333_0000_CCCC_0003, gen_uniform_noise), + ]; + + eprintln!( + "\nPROBE-WH-MAG: {} tiles/class, dim={} (informational — verdict adjudicated externally)", + TILES_PER_CLASS, TILE_DIM + ); + eprintln!( + "{:<16} {:>16} {:>16} {:>10}", + "class", "mse_A(direct)", "mse_B(WH)", "B/A" + ); + + let mut ratios: Vec<(String, f64)> = Vec::with_capacity(classes.len()); + for (name, seed, gen) in classes.iter() { + let mut rng = SplitMix64::new(*seed); + let mut sum_a = 0.0f64; + let mut sum_b = 0.0f64; + for _ in 0..TILES_PER_CLASS { + let tile = gen(&mut rng); + let recon_a = cascade_direct(&tile); + let recon_b = cascade_wh(&tile); + sum_a += mse(&tile, &recon_a); + sum_b += mse(&tile, &recon_b); + } + let mean_a = sum_a / TILES_PER_CLASS as f64; + let mean_b = sum_b / TILES_PER_CLASS as f64; + assert!( + mean_a.is_finite() && mean_a > 0.0 && mean_b.is_finite(), + "class {name} produced invalid MSE values: direct={mean_a}, WH={mean_b}" + ); + let ratio = mean_b / mean_a; + eprintln!( + "{:<16} {:>16.8} {:>16.8} {:>10.4}", + name, mean_a, mean_b, ratio + ); + ratios.push((name.to_string(), ratio)); + } + + // Structural sanity only. The verdict (PASS/NEUTRAL/KILL per the + // plan's thresholds) is adjudicated by the reviewer against the + // printed table above — asserting it here would red the suite on a + // legitimate NEUTRAL result. A non-finite ratio, however, is an + // unusable probe run and MUST fail. + assert_eq!(ratios.len(), 3, "expected exactly 3 tile classes"); + for (name, ratio) in &ratios { + assert!( + ratio.is_finite(), + "class {name} produced an invalid ratio: {ratio}" + ); + } + } +} diff --git a/crates/helix/src/lib.rs b/crates/helix/src/lib.rs index 159f2c04f..202c73822 100644 --- a/crates/helix/src/lib.rs +++ b/crates/helix/src/lib.rs @@ -66,6 +66,8 @@ pub mod prove; pub mod quantize; pub mod residue; pub mod simd; +#[cfg(test)] +mod walk_spectrum; pub use constants::{ E, EULER_GAMMA, GOLDEN_ANGLE, GOLDEN_RATIO, GOLDEN_RATIO_INV, LN_17, MODULUS, PALETTE_SIZE, diff --git a/crates/helix/src/walk_spectrum.rs b/crates/helix/src/walk_spectrum.rs new file mode 100644 index 000000000..62abe18d0 --- /dev/null +++ b/crates/helix/src/walk_spectrum.rs @@ -0,0 +1,194 @@ +//! PROBE-WALK-SPECTRUM — measures the stride-4-mod-17 walk's autocorrelation +//! spectrum against a seeded-LCG ±1 baseline. +//! +//! Grades the §10(g) anti-confabulation claim from the H.268 probe wave: is +//! the walk's dependence structure actually "known and concentrated" (energy +//! only at period-17 harmonics, none at the palette-lattice periods 16/32/ +//! 64/128/256), rather than a PRNG-shaped baseline would show? Cross-ref +//! `E-WH-TWO-SIDES-SIG-CHECKSUM-1`. +//! +//! **The LCG baseline is DESCRIPTIVE ONLY — not a noise floor, not a +//! significance threshold.** The probe's lattice-period conclusion rests on +//! the closed-form structure worked below (the walk's autocorrelation at lag +//! τ is governed by the exact one-period value `C(τ mod 17)/17` — integer +//! arithmetic, not an estimate), so no statistical test is invoked and none +//! is needed at this effect size. The single seeded-LCG sequence is printed +//! purely as a familiar reference magnitude. Per `I-NOISE-FLOOR-JIRAK`, any +//! FUTURE claim that phrases this as a significance/noise-floor statement +//! must derive its threshold from Jirak 2016 weak-dependence rates — this +//! probe deliberately makes no such claim. +//! +//! **Test-only probe.** This module is `#![cfg(test)]`-gated end to end and +//! is not part of the crate's default build surface, and is not registered +//! in any pillar/`prove` list — it is a one-shot measurement, not a proof +//! obligation. +//! +//! ## Derivation note (worked by hand, then verified by the printed table) +//! +//! `s[k] = ±1` from the parity of the walk residue +//! `r(k) = (start + STRIDE·k) mod MODULUS`. Because `gcd(STRIDE, MODULUS) = +//! gcd(4, 17) = 1`, `r(k)` is an exact permutation of `0..17` with period +//! **exactly 17 in `k`** (unconditionally — this periodicity does not depend +//! on the sequence length `N`). Walking the permutation for `start = 0` +//! gives the residue order `0,4,8,12,16,3,7,11,15,2,6,10,14,1,5,9,13`, whose +//! parities are 9 even / 8 odd — **not balanced**. So `s` is exactly +//! period-17 but its one-period mean is `1/17 ≠ 0` (a small DC bias, not +//! flagged by this probe since only the AC structure at specific lags is +//! measured). +//! +//! `N = 4096` is **not** a multiple of 17 (`4096 mod 17 = 16`), so the +//! *circular* autocorrelation (which wraps at `N`, not at 17) is not an +//! exact period-17 identity — the wraparound compares `s[k]` against a +//! residue shifted by one extra step for a `τ`-sized tail of indices. Working +//! it through exactly: for `τ = 17·m`, `R(τ) = [(N − 17m)·1 + m·Σ_adj] / N` +//! where `Σ_adj = 9` is the fixed sum of adjacent-residue parity products +//! around one full period (computed directly from the residue-parity +//! sequence above). That simplifies to `R(17·m) = 1 − 8m/N`, i.e. `R(17) ≈ +//! 0.998047`, `R(34) ≈ 0.996094`, `R(51) ≈ 0.994141` — all comfortably above +//! the 0.9 floor asserted below, confirming the "known, concentrated" +//! period-17 structure (not exactly 1, because `N` is not a clean multiple +//! of 17, but the deviation is a bounded `O(τ/N)` boundary effect, not +//! aliasing). For `τ` at the palette-lattice periods (16/32/64/128/256), +//! `gcd(17, τ) = 1` for all of them (17 is prime and none is a multiple of +//! 17), so no such exact-alignment mechanism exists — the printed table is +//! the actual measurement, not re-derived by hand here. + +#![cfg(test)] + +use crate::constants::{MODULUS, STRIDE}; +use crate::curve_ruler::CurveRuler; + +/// Sequence length: `16 × 256` — full periods of the palette lattice. +const N: usize = 4096; +/// Max lag examined for the LCG baseline's overall-max context figure. +const TAU_MAX: usize = 512; +/// Deterministic walk start offset. The derivation above does not depend on +/// this choice: any start is a cyclic relabelling of the same 9-even/8-odd +/// residue-parity pattern, so the period-17 structure is unaffected. +const WALK_START: u8 = 0; +/// Deterministic LCG seed (SplitMix64's golden-ratio odd constant — used +/// here only as a fixed seed literal, not as a dependency). +const LCG_SEED: u64 = 0x9E37_79B9_7F4A_7C15; + +/// `±1` sign sequence from the parity of the walk residue, generated by the +/// CANONICAL carrier (`CurveRuler::from_place(start).index(k)`) so the +/// measurement stays aligned with the shipped walk rather than a local +/// reimplementation of the stride/modulus arithmetic. +fn walk_signs(start: u8, n: usize) -> Vec { + let ruler = CurveRuler::from_place(start as u64); + (0..n) + .map(|k| { + if ruler.index(k as u32) % 2 == 0 { + 1 + } else { + -1 + } + }) + .collect() +} + +/// One step of a 64-bit LCG (the PCG multiplier/increment pair — a fixed, +/// well-known constant choice, not a new crate dependency). +fn lcg_next(state: &mut u64) -> u64 { + *state = state + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + *state +} + +/// `±1` baseline sequence from a seeded LCG (top bit of each draw — the low +/// bits of a 64-bit LCG have short periods, so the top bit is used). +fn lcg_signs(seed: u64, n: usize) -> Vec { + let mut state = seed; + (0..n) + .map(|_| { + if (lcg_next(&mut state) >> 63) & 1 == 1 { + 1 + } else { + -1 + } + }) + .collect() +} + +/// Plain circular autocorrelation `R(τ) = (1/N) · Σ_k s[k]·s[(k+τ) mod N]`. +/// `O(N)` per call; the probe calls this `O(τ_max)` times, `O(N·τ_max)` +/// total — fine at this size (no FFT dependency). +fn autocorrelation(s: &[i32], tau: usize) -> f64 { + let n = s.len(); + let mut acc: i64 = 0; + for (k, &sk) in s.iter().enumerate() { + let j = (k + tau) % n; + acc += (sk * s[j]) as i64; + } + acc as f64 / n as f64 +} + +#[test] +fn probe_walk_spectrum() { + let walk = walk_signs(WALK_START, N); + let lcg = lcg_signs(LCG_SEED, N); + + // Structural sanity only — the printed table below is what the reviewer + // adjudicates against the PASS/NEUTRAL/KILL bands, not these asserts. + assert_eq!(walk.len(), N); + assert_eq!(lcg.len(), N); + assert!(walk.iter().all(|&v| v == 1 || v == -1)); + assert!(lcg.iter().all(|&v| v == 1 || v == -1)); + + let harmonics = [17usize, 34, 51]; + let lattice = [16usize, 32, 64, 128, 256]; + + eprintln!( + "=== PROBE-WALK-SPECTRUM (N={N}, MODULUS={MODULUS}, STRIDE={STRIDE}, start={WALK_START}) ===" + ); + + eprintln!("-- walk |R(tau)| at period-17 harmonics (expected ~= 1) --"); + let mut harmonic_rs = Vec::with_capacity(harmonics.len()); + for &tau in &harmonics { + let r = autocorrelation(&walk, tau); + harmonic_rs.push(r); + eprintln!(" tau={tau:4} R={r:+.6} |R|={:.6}", r.abs()); + } + + eprintln!( + "-- walk |R(tau)| at palette-lattice periods (gcd(17,tau)=1, expected ~ noise floor) --" + ); + let mut walk_lattice_max: f64 = 0.0; + for &tau in &lattice { + let r = autocorrelation(&walk, tau); + walk_lattice_max = walk_lattice_max.max(r.abs()); + eprintln!(" tau={tau:4} R={r:+.6} |R|={:.6}", r.abs()); + } + + eprintln!("-- LCG baseline |R(tau)| at the same palette-lattice periods --"); + let mut lcg_lattice_max: f64 = 0.0; + for &tau in &lattice { + let r = autocorrelation(&lcg, tau); + lcg_lattice_max = lcg_lattice_max.max(r.abs()); + eprintln!(" tau={tau:4} R={r:+.6} |R|={:.6}", r.abs()); + } + + let mut lcg_overall_max: f64 = 0.0; + for tau in 1..=TAU_MAX { + lcg_overall_max = lcg_overall_max.max(autocorrelation(&lcg, tau).abs()); + } + + eprintln!("-- context: LCG baseline max|R| over tau in 1..={TAU_MAX} = {lcg_overall_max:.6}"); + eprintln!( + "walk lattice max|R| = {walk_lattice_max:.6}, LCG lattice max|R| = {lcg_lattice_max:.6}, ratio = {:.3}", + walk_lattice_max / lcg_lattice_max.max(1e-12) + ); + + // Derivation-confirmed structural assertion only (see module doc): the + // hand-worked R(17*m) = 1 - 8m/N formula puts all three harmonics above + // 0.994 for N=4096. This is NOT the PASS/NEUTRAL/KILL verdict from the + // plan — the reviewer adjudicates the full printed table above, + // including whether the lattice-period ratio clears the 2x/3x bands. + for (tau, &r) in harmonics.iter().zip(harmonic_rs.iter()) { + assert!( + r.abs() > 0.9, + "period-17 harmonic tau={tau} |R|={r} unexpectedly low (derivation predicts ~0.99+)" + ); + } +} diff --git a/crates/jc/src/lib.rs b/crates/jc/src/lib.rs index 6687ea495..5f7b840ca 100644 --- a/crates/jc/src/lib.rs +++ b/crates/jc/src/lib.rs @@ -43,6 +43,14 @@ pub mod ewa_sandwich_3d; pub mod pflug; pub mod hambly_lyons; +// PROBE-SIG-CHECKSUM — depth-2 truncated signature as a replayable +// trajectory digest (H.268 probe wave, grades E-WH-TWO-SIDES-SIG-CHECKSUM-1 +// leg 2). A probe, not a 12th pillar: intentionally NOT added to the +// `run_all_pillars` registry below. Gated by the same `hambly-lyons` +// feature as Pillar 11; see `src/sig_checksum.rs` for the probe design. +#[cfg(feature = "hambly-lyons")] +pub mod sig_checksum; + // Diagnostic probe (not a theorem proof). Run via: // cargo run --manifest-path crates/jc/Cargo.toml --release --example sigma_probe pub mod sigma_codebook_probe; diff --git a/crates/jc/src/sig_checksum.rs b/crates/jc/src/sig_checksum.rs new file mode 100644 index 000000000..add7361d5 --- /dev/null +++ b/crates/jc/src/sig_checksum.rs @@ -0,0 +1,343 @@ +//! PROBE-SIG-CHECKSUM — depth-2 truncated signature as a replayable +//! trajectory digest (grades E-WH-TWO-SIDES-SIG-CHECKSUM-1 leg 2). +//! +//! Citation: B. Hambly & T. Lyons, "Uniqueness for the signature of a path +//! of bounded variation and the reduced path group", Annals of Mathematics, +//! Vol. 171, No. 1 (2010), 109-167. See `hambly_lyons.rs` (Pillar 11) for +//! the theorem statement this probe reuses operationally. +//! +//! # What this probe checks +//! +//! Hambly-Lyons Theorem 4 says the signature is a lossless digest *modulo +//! tree-like equivalence*: `S(X) = S(Y) ⟺ X ~ Y` (tree-quotient). This +//! probe asks whether that theorem behaves the way a "trajectory checksum" +//! needs it to behave in practice, on a single realistic path rather than +//! on synthetic out-and-back / triangle pairs in isolation (as Pillar 11 +//! does): +//! +//! 1. **Replay identity** — hashing (signing) the same trajectory twice +//! must give the same digest. Trivial in principle, but worth pinning +//! down because `signature_truncated` accumulates via `tensor_multiply` +//! (see `sigker::signature::tensor_multiply`), and floating-point +//! accumulation order must be deterministic across two independent +//! builds of an identical path for the digest to be replayable at all. +//! 2. **Tree-like edit invisibility** — inserting an out-and-back +//! excursion (`A → B → A`) *in the middle* of an otherwise-normal path +//! must leave the digest unchanged (within float tolerance). This is +//! the operationally interesting case: Pillar 11 tests out-and-back as +//! the *entire* path; this probe tests it as a *sub-path* spliced into +//! a longer trajectory, relying on `tensor_multiply`'s associativity +//! (Chen's identity) for the excursion's near-identity contribution to +//! the middle of the product to vanish rather than to propagate. +//! 3. **Non-tree edit is caught** — permanently displacing one interior +//! point of the path (a kink that does *not* backtrack — no A→B→A +//! structure) changes the digest measurably. Level-1 (total +//! displacement, endpoint − startpoint) is invariant under an interior +//! displacement, so this specifically tests that level-2 (signed-area / +//! curvature information) is doing the discriminating work. +//! +//! # Probe design +//! +//! - **Home:** `crates/jc/src/sig_checksum.rs`, gated by the same +//! `hambly-lyons` feature as Pillar 11 (`sigker` workspace sibling). +//! This module is registered in `lib.rs` but is **not** added to the +//! pillar `prove()` registry — it is a probe, not a 12th pillar. +//! - **Path:** a single deterministic 2-D "trajectory" of `PATH_LEN = 64` +//! points, built via a seeded SplitMix64 integer walk (steps in +//! `{-1, 0, 1}²` per axis, cumulatively summed) mapped to `f64` +//! coordinates — mirrors `hambly_lyons.rs`'s `splitmix64`/`rand_in` +//! generator style, but accumulates a walk instead of drawing +//! independent points. +//! - **Signature:** `sigker::signature_truncated` at `DEPTH = 2` in +//! `DIM = 2` (the tensor-algebra path, not `signature_kernel_pde` — +//! same rationale as Pillar 11: this probe is independent of the +//! PR #350 PDE-form correction). +//! - **Distance:** reuses the Frobenius-across-all-levels +//! `signature_distance` helper from `hambly_lyons.rs` (duplicated here +//! since the original is private to that module). +//! - **Thresholds:** reused verbatim from `hambly_lyons.rs`'s own +//! forward/converse legs — `FORWARD_TOLERANCE = 1e-9` (the zero-edit +//! threshold) and `CONVERSE_THRESHOLD = 0.05` (the caught-edit +//! threshold) — so this probe is graded on the exact same numeric bar +//! Pillar 11 already certifies, not a bar invented for this probe. +//! +//! # Pass / kill +//! +//! - **PASS:** all three tests green (replay-identical, tree-edit +//! invisible under `FORWARD_TOLERANCE`, non-tree edit caught above +//! `CONVERSE_THRESHOLD`). +//! - **KILL:** test (b) or (c) fails at these thresholds — depth-2 +//! truncation is not enough for the checksum idea as stated (would need +//! depth > 2, or the digest framing is wrong). + +#[cfg(feature = "hambly-lyons")] +mod active { + use sigker::signature::Signature; + use sigker::signature_truncated; + + /// Path dimension (a 2-D trajectory). + const DIM: usize = 2; + /// Truncation depth — matches Pillar 11's `hambly_lyons::DEPTH`. + const DEPTH: usize = 2; + /// Number of points in the deterministic trajectory. + const PATH_LEN: usize = 64; + + /// Reused verbatim from `hambly_lyons.rs`'s forward-leg tolerance — + /// the zero-threshold below which two signatures are "the same digest". + const FORWARD_TOLERANCE: f64 = 1e-9; + /// Reused verbatim from `hambly_lyons.rs`'s converse-leg threshold — + /// the bar a non-tree edit's digest distance must clear to be "caught". + const CONVERSE_THRESHOLD: f64 = 0.05; + + fn splitmix64(state: &mut u64) -> u64 { + *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = *state; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + + /// Frobenius distance across all signature levels. Duplicated from + /// `hambly_lyons.rs::active::signature_distance` (private there) — + /// identical implementation, kept local per the plan's edit-only + /// constraint (do not modify `hambly_lyons.rs`). + fn signature_distance(a: &Signature, b: &Signature) -> f64 { + assert_eq!(a.dim, b.dim); + assert_eq!(a.depth, b.depth); + let mut acc = 0.0_f64; + for (la, lb) in a.levels.iter().zip(b.levels.iter()) { + for (xa, xb) in la.iter().zip(lb.iter()) { + let d = xa - xb; + acc += d * d; + } + } + acc.sqrt() + } + + /// Build the deterministic `PATH_LEN`-point 2-D trajectory: a seeded + /// SplitMix64 integer walk (each axis steps by -1, 0, or +1 per point), + /// cumulatively summed into `f64` coordinates. Same seed always + /// reproduces the same path — that determinism is exactly what test + /// (a) below is checking end-to-end. + fn build_path(seed: u64) -> Vec> { + let mut state = seed; + let mut x = 0.0_f64; + let mut y = 0.0_f64; + let mut path = Vec::with_capacity(PATH_LEN); + path.push(vec![x, y]); + for _ in 1..PATH_LEN { + let r = splitmix64(&mut state); + let dx = ((r % 3) as i64 - 1) as f64; + let dy = (((r >> 2) % 3) as i64 - 1) as f64; + x += dx; + y += dy; + path.push(vec![x, y]); + } + path + } + + /// Fixed seed for a reproducible probe trajectory. + const PATH_SEED: u64 = 0xD161_1D3D_0A7A_7EC7u64; + + /// Insert an `A → B → A` excursion mid-path at index `i`: splice + /// `[B, path[i]]` right after `path[i]`. `B` is an arbitrary point + /// offset from `path[i]` — the excursion goes out to `B` and + /// immediately returns, which is the canonical tree-like-equivalence + /// generator (Hambly-Lyons: identifying a sub-path with its + /// concatenated reverse collapses it to its start point). + fn insert_tree_like_excursion(path: &[Vec], i: usize) -> Vec> { + let anchor = path[i].clone(); + let b = vec![anchor[0] + 5.0, anchor[1] + 3.0]; + let mut edited = Vec::with_capacity(path.len() + 2); + edited.extend_from_slice(&path[..=i]); + edited.push(b); + edited.push(anchor); + edited.extend_from_slice(&path[i + 1..]); + edited + } + + /// Find the first interior index `i` (starting at `from`, scanning + /// forward) whose neighbors `path[i-1]`, `path[i+1]` are not + /// coincident — i.e. `path[i+1] - path[i-1] != (0, 0)`. + /// + /// Why this matters: at `DEPTH = 2` truncation, displacing only + /// `path[i]` (endpoints of the whole trajectory untouched) changes the + /// *total* signature by *exactly* the antisymmetric (signed-area) term + /// `½·(off ⊗ combined − combined ⊗ off)` where + /// `combined = path[i+1] − path[i-1]` — this is an algebraic identity + /// of `tensor_multiply`'s associativity/bilinearity at this truncation + /// depth, not an approximation, and it holds independent of where `i` + /// sits in the path (the prefix/suffix segments contribute nothing at + /// depth 2, since the edit's own level-0/level-1 contributions are + /// exactly zero). That signed-area term is zero whenever `off` is + /// parallel to `combined` — which includes the degenerate case + /// `combined == (0, 0)`, where *no* offset produces a visible edit. + /// Scanning for a non-degenerate `combined` makes the probe robust to + /// which interior point of the fixed seeded path is chosen. + fn find_displaceable_index(path: &[Vec], from: usize) -> usize { + let n = path.len(); + for i in from..n - 1 { + let dx = path[i + 1][0] - path[i - 1][0]; + let dy = path[i + 1][1] - path[i - 1][1]; + if dx != 0.0 || dy != 0.0 { + return i; + } + } + panic!("no displaceable interior index found in path of length {n}"); + } + + /// Permanently displace the interior point at index `i` to a new, + /// non-collinear location — a kink that does *not* backtrack (no + /// A→B→A structure: the path never returns to the original `path[i]`). + /// Endpoints are untouched, so level-1 (total displacement) is + /// invariant; only the level-2 (area/curvature) signature component + /// should move. The offset is chosen **perpendicular** to + /// `path[i+1] - path[i-1]` (see `find_displaceable_index`) so the + /// depth-2 signed-area term is maximal rather than accidentally + /// vanishing for a fixed-direction offset. + fn displace_interior_point(path: &[Vec], i: usize) -> Vec> { + let combined_x = path[i + 1][0] - path[i - 1][0]; + let combined_y = path[i + 1][1] - path[i - 1][1]; + debug_assert!( + combined_x != 0.0 || combined_y != 0.0, + "index {i} is degenerate for displacement — use find_displaceable_index" + ); + // Rotate 90°: (x, y) -> (-y, x). Guaranteed non-parallel to + // (combined_x, combined_y) whenever that vector is nonzero. + const SCALE: f64 = 8.0; + let offset_x = -combined_y * SCALE; + let offset_y = combined_x * SCALE; + + let mut edited = path.to_vec(); + edited[i] = vec![path[i][0] + offset_x, path[i][1] + offset_y]; + edited + } + + #[cfg(test)] + mod tests { + use super::*; + + /// (a) replay_identity — building the same deterministic path twice + /// and signing both must produce identical digests. Exact f64 + /// equality is expected (same ops, same order, same seed); if this + /// ever proves flaky across toolchains, the fallback is + /// `signature_distance(...) < 1e-12`, tighter than the module's own + /// `FORWARD_TOLERANCE` — noted here rather than used, since exact + /// equality held on this run (see the assert below). + #[test] + fn replay_identity() { + let path_a = build_path(PATH_SEED); + let path_b = build_path(PATH_SEED); + assert_eq!(path_a, path_b, "path replay must be byte-identical"); + + let sig_a = signature_truncated(&path_a, DEPTH); + let sig_b = signature_truncated(&path_b, DEPTH); + + assert_eq!(sig_a.dim, sig_b.dim); + assert_eq!(sig_a.depth, sig_b.depth); + + let mut max_abs_diff = 0.0_f64; + for (la, lb) in sig_a.levels.iter().zip(sig_b.levels.iter()) { + assert_eq!(la.len(), lb.len()); + for (&xa, &xb) in la.iter().zip(lb.iter()) { + // Exact f64 equality: same seed, same accumulation + // order, same floating-point operations both times. + assert_eq!( + xa.to_bits(), + xb.to_bits(), + "replay must be bit-identical per coefficient" + ); + max_abs_diff = max_abs_diff.max((xa - xb).abs()); + } + } + + let dist = signature_distance(&sig_a, &sig_b); + eprintln!( + "[PROBE-SIG-CHECKSUM] (a) replay_identity: max |Δcoeff| = {:.3e}, \ + Frobenius distance = {:.3e} (exact bit-equality asserted above; \ + fallback tolerance would have been 1e-12)", + max_abs_diff, dist + ); + assert_eq!(dist, 0.0, "replayed digests must be exactly identical"); + } + + /// (b) tree_like_edit_invisible — an out-and-back excursion spliced + /// mid-path must leave the digest within `FORWARD_TOLERANCE` of the + /// original (the module's own zero-threshold, reused from + /// `hambly_lyons.rs`'s forward leg). + #[test] + fn tree_like_edit_invisible() { + let path = build_path(PATH_SEED); + let mid = PATH_LEN / 2; + let edited = insert_tree_like_excursion(&path, mid); + assert_eq!(edited.len(), path.len() + 2); + + let sig_orig = signature_truncated(&path, DEPTH); + let sig_edited = signature_truncated(&edited, DEPTH); + let dist = signature_distance(&sig_orig, &sig_edited); + + eprintln!( + "[PROBE-SIG-CHECKSUM] (b) tree_like_edit_invisible: \ + excursion at index {mid}, distance = {:.3e} (pass if < {:.0e})", + dist, FORWARD_TOLERANCE + ); + assert!( + dist < FORWARD_TOLERANCE, + "tree-like A→B→A excursion must be invisible to the digest: \ + distance {dist:e} >= FORWARD_TOLERANCE {FORWARD_TOLERANCE:e}" + ); + } + + /// (c) non_tree_edit_caught — permanently displacing one interior + /// point (no backtrack, endpoints unchanged) must move the digest + /// past `CONVERSE_THRESHOLD` (the module's own converse-leg + /// threshold, reused from `hambly_lyons.rs`). + #[test] + fn non_tree_edit_caught() { + let path = build_path(PATH_SEED); + let mid = find_displaceable_index(&path, PATH_LEN / 2); + let edited = displace_interior_point(&path, mid); + assert_eq!(edited.len(), path.len()); + // Endpoints untouched — level-1 (total displacement) must be + // invariant; the digest still has to move via level-2. + assert_eq!(edited[0], path[0]); + assert_eq!(edited[path.len() - 1], path[path.len() - 1]); + + let sig_orig = signature_truncated(&path, DEPTH); + let sig_edited = signature_truncated(&edited, DEPTH); + assert_eq!(sig_orig.dim, DIM); + let dist = signature_distance(&sig_orig, &sig_edited); + + // Level-1 alone (total displacement, endpoint − startpoint) is + // mathematically invariant under an interior-only displacement; + // computed directly (not via `signature_distance`, which + // requires matching `depth`) purely as a diagnostic confirming + // the discriminating signal comes from level-2, not level-1. + let level1_delta: f64 = sig_orig.levels[1] + .iter() + .zip(sig_edited.levels[1].iter()) + .map(|(a, b)| (a - b) * (a - b)) + .sum::() + .sqrt(); + assert!( + level1_delta < FORWARD_TOLERANCE, + "interior-only displacement unexpectedly changed level 1 \ + ({level1_delta:e}) — the probe's level-2-discrimination \ + conclusion would be unsound" + ); + + eprintln!( + "[PROBE-SIG-CHECKSUM] (c) non_tree_edit_caught: \ + displaced index {mid}, distance = {:.4} (pass if > {:.2}); \ + level-1-only delta = {:.3e} (expected ~0, invariant under \ + an interior-only displacement)", + dist, CONVERSE_THRESHOLD, level1_delta + ); + assert!( + dist > CONVERSE_THRESHOLD, + "non-tree interior displacement must be caught by the digest: \ + distance {dist} <= CONVERSE_THRESHOLD {CONVERSE_THRESHOLD}" + ); + } + } +}