Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .claude/board/AGENT_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@
**Cross-repo validation (LOCAL ONLY, no odoo-rs commit):** built a slice-2-scoped enriched fixture, ran `od_ontology::RecomputeDag` from a throwaway `origin/main` worktree. **Baseline:** 0 cross-model compute edges, edge `_compute_amount_residual → _compute_amount` ABSENT. **Enriched:** 27 compute edges, that cross-model edge PRESENT — the wishlist's P0 ask (visible cross-model ordering) is delivered. **Correction:** the graph stays acyclic; the MISSED-1 dependency is an *ordering edge*, not a cycle (move depends on line; line does not depend back on move's totals), so odoo-rs's `slice_2_compute_subset_no_cross_model_cycle` no-cycle assertion legitimately still holds. Reported, not faked. Worktree removed.

**Finding:** the corpus's original generator (`emit_ontology2.py` over `methods.parquet`) is absent from the tree; the blueprint extractor emits typed Rust `OdooEntity` consts (separate artifact). Enrichment runs over shipped corpus + present Odoo source — the correct additive stage. See `EPIPHANIES.md` E-ODOO-FK-DEEP-READS.
## 2026-06-17 — perturbation-sim B1: finite-gate the spectral-gap NaN landmine (Davis–Kahan / λ₂→0 blackout precursor)

**Main thread (Opus) — single implementer**, on new branch `claude/perturbation-soa-nan-gate-witness` off `origin/main`. Phase B of the SoA-substrate arc — "one NaN less + something to check against." Ran cargo freely against the shared `target/` (per the Opus-orchestrator hygiene rule).

**B1 (the must-have) SHIPPED — finite-gate every spectral-gap division.** Full audit of all 7 spectral files + the 6 other modules (every `/` by gap/λ₂/eigenvalue-diff/norm). Finding: the crate was already remarkably well-defended (eigen.rs `pseudo_apply`/`pseudo_inverse` use a *relative* `rel_tol·λ_max` cutoff; lodf/kirchhoff/stats/timing/buffer/chaoda/model all guarded), but the Davis–Kahan path used **naked, absolute, inconsistent** ε-guards that (a) caught `0/0` but (b) let a tiny-positive noisy `gap` through as a silently-wrong finite bound. Fix mirrors `ndarray::hpc::entropy_ladder::residue_surprise` (PR #221): floor the denominator AFTER the subtract-and-`min`, relative to `λ_max`, divide unconditionally.

Sites gated (all in `perturbation-sim/src/`):
- `perturbation.rs:141-164` — `davis_kahan_bound`: `gap = min(λ₂−λ₁, λ₃−λ₂)` floored at `SPECTRAL_GAP_FLOOR·λ_max` AFTER the min; `gap ≤ floor` ⇒ `FRAGMENTATION_SENTINEL` (= `+∞`, the documented divergence signal — finiteness-checkable, never NaN). Two new `pub const`s (`SPECTRAL_GAP_FLOOR`, `FRAGMENTATION_SENTINEL`) exported via lib.rs; the small-network arm and the degenerate arm both return the sentinel, never a fabricated finite.
- `perturbation.rs:95-101` — `connectivity_loss`: `λ₂'/λ₂` gated by the floor + result `.clamp(0,1)` (was unclamped → could exceed 1 on noisy `fiedler_before`).
- `rolling_floor.rs:52-65` — `weyl_over_fiedler` (`Δλ·1/λ₂`): floor unified with `SPECTRAL_GAP_FLOOR`, divide by `λ₂.abs()`, numerator `.max(0.0)` (sign-noise can't flip the ratio negative).

**Regression tests (the "check against"):** +4 tests, 91 → **95 green**.
- `blackout_precursor_regime_is_never_nan` (near-zero-bridge grid, all lines): asserts NO output is NaN + the fragmenting-trip DK bound IS the sentinel (not a noisy finite).
- `disconnected_graph_spectral_outputs_are_nan_free` (two independent components, λ₂≈0).
- `weyl_over_fiedler_is_nan_free_at_the_precursor` (λ₂ ∈ {0, ±1e-14, 1e-300, …}).
- `healthy_grid_davis_kahan_is_finite_and_bounds_rotation` (**parity** — asymmetric weighted path with a *separated* λ₂; non-vacuity asserted via `saw_finite`): the gate does NOT perturb healthy numbers, gates ONLY the singular path. (Discovered: a symmetric ring's Fiedler mode is degenerate → sentinel is correct & reachable in normal use; parity must use an asymmetric grid.)

**B2 (the bonus) — STOP-and-report, not forced.** Assessed wiring `witness.rs`'s `particle == wave` oracle as the contract `witness_table` evaluator. **Outcome: the contract `WitnessTable` is an address-resolution primitive** (`(mailbox_ref:u32, spo_fact_ref:Option<u64>)` lookup by 6-bit W-slot; `get`/`set` only — see `lance-graph-contract/src/witness_table.rs`), **NOT an arc evaluator.** There is no evaluator hook to wire the particle/wave identity into; doing so would require inventing a NEW contract trait/method (an arc-evaluation surface), which is *more than additive* and crosses the iron-rule "no new trait/bridge" guidance + I-LEGACY-API-FEATURE-GATED. witness.rs's own doc already flags this as "a separate, gated step." Per the task's STOP-and-report instruction, did NOT force it. **Exact surface needed (if B2 is later greenlit):** a contract trait e.g. `WitnessArcEvaluator { fn evaluate_arc(&self, field: &[f64], arc: &[f64]) -> f64; }` (or an inherent method on a NEW field-carrying type — `WitnessTable` carries no field column), plus the FWHT/Parseval engine behind a feature gate. That is an additive *new type*, not an extension of the existing primitive — operator decision required.

**Gates:** `cargo test --manifest-path crates/perturbation-sim/Cargo.toml` → 95 lib + 0 doctest green; `cargo clippy --manifest-path … --all-targets -- -D warnings` → clean (EXIT 0); `cargo fmt --check` → clean; examples build. No pre-existing `-D warnings` debt in this standalone crate (zero-dep; no causal-edge/p64-bridge transitive deps touched).

**Board hygiene (this commit):** EPIPHANIES `E-SPECTRAL-GAP-NAN-1` prepended (the spectral-gap NaN class + floored-denominator decision + the acflow adjacent-finding note); this AGENT_LOG entry prepended. Additive / behaviour-preserving in the normal regime; gated ONLY the degenerate path. PR NOT opened (orchestrator opens it).

---

Expand Down
12 changes: 12 additions & 0 deletions .claude/board/EPIPHANIES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## 2026-06-17 — E-SPECTRAL-GAP-NAN-1 — the Davis–Kahan `/gap` and `λ₂→0` precursor are the SAME NaN class as entropy_ladder; floor the denominator AFTER the subtract

**Status:** FINDING (shipped on branch `claude/perturbation-soa-nan-gate-witness`, `perturbation-sim` B1). The spectral half of `perturbation-sim` carries an *intrinsic* NaN/divergence landmine that fires **in the exact regime the model exists to capture**: the Davis–Kahan Fiedler rotation bound `sinθ ≤ ‖E‖₂ / gap` and the mode-instability modifier `Δλ × (1/λ₂)` both divide by a **vanishing spectral gap**, and `gap → 0` / `λ₂ → 0` IS the blackout precursor (network fragmentation). This is the identical bug-class to `ndarray::hpc::entropy_ladder::residue_surprise` (PR #221): a denominator built by subtraction that collapses to 0 → `0/0` NaN, or — subtler — a *tiny-but-positive* denominator that slips past a naked `gap > ε` test and yields an astronomically large **finite** number presented as a trustworthy bound.

**The fix is the entropy_ladder pattern: floor the denominator AFTER the subtract-and-`min` that builds it, then divide unconditionally.** `gap = min(λ₂−λ₁, λ₃−λ₂)` is built first, then floored relative to the spectral scale (`SPECTRAL_GAP_FLOOR · λ_max`, mirroring `eigen.rs`'s zero-eigenvalue cutoff convention); a gap at/below the floor is *spectral degeneracy*. The naked-ε guards that were already present (`gap > 1e-12 → INFINITY`, `λ₂.abs() < 1e-12 → INFINITY`) caught the `0/0` case but (a) used an **absolute** ε on a relatively-scaled quantity and (b) let a tiny-positive noisy gap through as a silently-wrong finite bound. The B1 fix makes the floor relative and consistent across all three spectral sites (`perturbation::davis_kahan_bound`, `SpectralPerturbation::connectivity_loss`, `rolling_floor::weyl_over_fiedler`), and clamps `connectivity_loss` to `[0,1]`.

**The mathematical decision — sentinel, not swallow.** `gap = 0` is a *real result*, not an error: the Fiedler vector may rotate arbitrarily, so `sinθ ≤ ‖E‖₂ / 0 = +∞`. The gate surfaces `f64::INFINITY` (the workspace's established divergence sentinel — cf. `weyl_over_fiedler`, `kirchhoff_index`, `collapse_number`), which is **finiteness-checkable** (downstream branches on `.is_finite()`) and **never NaN**. The one invariant the gate guarantees: every spectral output is either a trustworthy finite number or the explicit `+∞` divergence signal — never `NaN`, never a silently-wrong finite. A symmetric ring confirmed the sentinel is genuinely reachable in normal use (its Fiedler mode is doubly-degenerate, `λ₂=λ₃` ⇒ `gap=0` legitimately) — so this is not a corner case, it is the standing behaviour on any symmetric topology.

**Regression discipline (the "one NaN less + something to check against"):** three new tests drive the precursor regime (near-zero bridge, fully disconnected graph, degenerate ring) and assert NaN-freedom on every output + sentinel-correctness on fragmentation; one parity test (asymmetric weighted path, genuinely separated `λ₂`) pins that the gate **does not perturb healthy-grid numbers** — it gates ONLY the singular path. 91 → 95 `perturbation-sim` tests, clippy `-D warnings` clean.

**Adjacent (NOT in B1 scope — a different mechanism):** `acflow.rs` (the AC Newton–Raphson power-flow path) carries its own division landmines — `Cx::recip` divides by `|z|²` (NaN on a zero-impedance line `r=x=0`) and the Jacobian partials `dp_dv`/`dq_dv` divide by bus voltage `v[i]` (a divergent NR iteration could drive `v→0`). These are **voltage-collapse**-regime sites, not spectral-fragmentation, and are gated differently (the `solve_linear` partial-pivot `best < 1e-13 → return false` already protects the linear solve, and a non-converged solve returns `converged: false`). Flagged here for a future AC-flow NaN pass; B1 deliberately scoped to the Laplacian-spectral path the task named.

## 2026-06-17 — E-ODOO-EXTRACT-DEEP-READS — the consumer-side recompute-DAG probe surfaces a corpus enrichment ask, not a ClassView gap

**Status:** FINDING (consumer-side, ratifying gap on producer side). `od-ontology::RecomputeDag` shipped at [`AdaWorldAPI/odoo-rs@d8a270d`](https://github.com/AdaWorldAPI/odoo-rs/commit/d8a270d) (540 LOC, 8 tests, clippy-clean) topologically sorts the `MethodKind::Compute` subset of the slice 1 / slice 2 corpora cleanly. The hand-found `_compute_amount.md` MISSED-1 P0 cycle (`move._compute_amount` → `line.reconciled` → `line._compute_amount_residual`) is **structurally invisible** because the Odoo extractor's `reads_field` for `@api.depends('line_ids.reconciled')` emits only the **first hop** (`account_move.line_ids`) — not the **leaf** (`account_move_line.reconciled`). One sibling triple per `@api.depends` leaf reads, same wire shape, same provenance source, would land the catch.
Expand Down
4 changes: 3 additions & 1 deletion crates/perturbation-sim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ pub use model::{
apply_aging, assess_capability, edge_age_factors, scale_susceptance, with_uniform_derate,
AgeModel, Capability, DataLevel,
};
pub use perturbation::{spectral_perturbation, SpectralPerturbation};
pub use perturbation::{
spectral_perturbation, SpectralPerturbation, FRAGMENTATION_SENTINEL, SPECTRAL_GAP_FLOOR,
};
pub use resilience::{algebraic_connectivity, kirchhoff_index, Resilience};
pub use rolling_floor::{weyl_over_fiedler, FloorBand, RollingFloor, StackResult, TierFloors};
pub use sketch::{fwht, resistance_sketch, walsh_pyramid_energy, ResistanceSketch, WalshEnergy};
Expand Down
Loading
Loading