Repository navigation
ltm: exhaustive-mode loops never get runtime polarity reclassification (Rux/Bux unreachable) #679
Description
Problem
LoopPolarity::from_runtime_scores (src/simlin-engine/src/ltm/types.rs:254) implements the LTM papers' Rux/Bux/U runtime classification with the POLARITY_CONFIDENCE_THRESHOLD (0.99) confidence gate. But its only production caller is the discovery FoundLoop path (src/simlin-engine/src/ltm_finding.rs:2036). The other in-tree caller (src/simlin-engine/src/db/analysis.rs:3074) is inside a #[cfg(test)] test, not a production surface.
In exhaustive mode, model_detected_loops (src/simlin-engine/src/db/analysis.rs) reports the structural label with polarity_confidence hardcoded to 1.0 (every link signed) or 0.0 (any link unknown), and never applies a runtime override. The code comments confirm this is intentional today:
db/analysis.rs:808-814-- "For the structural pipeline (model_detected_loops), this is 1.0 when every link ... 0.0 when any link is unknown."db/analysis.rs:829-831-- "The structuralmodel_detected_loopspipeline never produces these variants -- it has no runtime data."
So MostlyReinforcing/MostlyBalancing (Rux/Bux) are structurally unreachable through the exhaustive-mode detected-loops surface, even though the runtime loop_score series exists post-simulation and from_runtime_scores would yield the honest Rux/Bux/U label with a real confidence value.
Consequence
A loop whose polarity genuinely changes sign during the simulation (e.g. a logistic-style single-flow formulation, or the yeast-alcohol model from the LTM papers) is reported through the exhaustive path either:
- with a confidently-wrong steady
R/Blabel (under the older, looser polarity rules), or - with a blanket
Undetermined(after the tightened Mul/Div polarity rules incc072973),
when the truthful answer post-simulation is Rux/Bux/U with a concrete confidence. The loop_score series that would resolve this is available; it just is never consulted on the exhaustive path. This is a labelling fidelity gap -- loop scores themselves are unaffected.
Why it matters
Correctness/honesty of the polarity label that downstream consumers (UI rendering, pysimlin, MCP analyze) present to modelers. A "mostly reinforcing, confidence 0.97" loop and a "reinforcing, confidence 1.0" loop are materially different stories about the model's behavior, and the engine has the data to tell them apart but doesn't on the exhaustive surface.
Components affected
src/simlin-engine/src/ltm/types.rs(LoopPolarity::from_runtime_scores)src/simlin-engine/src/db/analysis.rs(model_detected_loops,DetectedLoop::polarity_confidence,DetectedLoopPolarity)src/libsimlin/src/analysis.rs(the post-sim analyze path that has theResultsin hand and already coalesces the variants -- see ltm: surface MostlyReinforcing/MostlyBalancing in JSON/TS/WASM layers #495)
Suggested approach
model_detected_loops itself is a pre-simulation salsa query (no runtime series), so the override does not belong there. After simulation, reclassify exhaustive-mode loops from their per-loop loop_score series via from_runtime_scores. The libsimlin analyze path already holds the simulation Results and the loop ids, so the reclassification (structural label + polarity_confidence -> runtime label + real confidence) belongs in that post-sim surface, mirroring what the discovery FoundLoop path already does in-engine. This also unblocks the FFI/TS plumbing tracked in #495 from ever seeing a non-trivial confidence on the exhaustive path.
Related
- ltm: surface MostlyReinforcing/MostlyBalancing in JSON/TS/WASM layers #495 -- surfacing
MostlyReinforcing/MostlyBalancing+polarity_confidencethrough the JSON/TS/WASM/FFI layers (assumes the engine produces the variants; this issue is that exhaustive mode never produces them at all -- they are complementary) - ltm: clarify or eliminate the unreachable MostlyBalancing tie-break in LoopPolarity::from_runtime_scores #506 -- the unreachable
MostlyBalancingtie-break insidefrom_runtime_scores(code clarity; distinct from this caller-coverage gap) - ltm: cross-element loops through aggregate nodes always classify as Undetermined (agg-hop link polarity is always Unknown) #516 -- agg-hop static polarity always
Undetermined(a different code site) - Epic: epic: Loops that Matter (LTM) -- open work #488
Context
Found during a design review on 2026-06-02. Severity: moderate (labels only; loop scores unaffected).
Activity
ltm: add in-engine runtime loop-polarity reclassification primitive
engine: extend positive-value convention to Mul co-factors (#1008)