Repository navigation
ltm: scalar module output feeding an arrayed reader emits a single scalar constant-0 link score, dropping all arrayed loops through modules #716
Description
Activity
- addedltmLoops that Matter (LTM) analysis subsystemLoops that Matter (LTM) analysis subsystem
on Jun 4, 2026 - added a commit that references this issue
on Jun 4, 2026 Mechanism note from the GH #758 work (branch
ltm-fix-batch-2, commit5c9c2d41): the constant-0 here is plausibly the same broken-by-construction species #758 fixed for non-module edges.module_link_score_equation(src/simlin-engine/src/db.rs) always returnsEquation::Scalar; for an arrayed reader, both the ceteris-paribus path (viascalarize_ltm_equation) and theblack_box_unit_transfer_equationfallback produce a scalar equation referencing the arrayed target bare -- which never compiles in scalar context, so the fragment fail-warns and is stubbed to 0 (or, if it compiles, is dimensionally wrong). The #758 gate deliberately excludes module endpoints (arrayed_non_modulein src/simlin-engine/src/db/ltm/link_scores.rs), so module->arrayed-reader edges remain on this path; this issue's per-target-element emission fix is what covers them.- added a commit that references this issue
on Jun 11, 2026 Mechanism and a measured count for this, from the LTM fragment-compile investigation in #994.
This is the largest remaining class of LTM fragment-compile failures: 340 generating roots on C-LEARN, and it is the top bucket left after that PR (which takes the corpus from 1,603 failures to 449; 333 of the 449 are this).
Two code sites produce the symptom described here:
-
link_score_equation_text_shaped(src/simlin-engine/src/db/ltm/compile.rs:254) short-circuits onfrom_is_module || to_is_moduleintomodule_link_score_equation, which ends inLtmEquation::scalarize. That call is a shape relabel only —src/simlin-engine/src/db/ltm/equation.rs:203drops the dimensions and keeps arm 0's text verbatim. So an arrayed-target body whose references are in apply-to-all form gets relabelledScalarand then cannot compile, which is why the emitted variable is a single scalar rather than a per-element family. -
Independently,
try_scalar_to_arrayed_link_scores— the machinery that already emits one scalar score per target element for a scalar source — bails on a module source atsrc/simlin-engine/src/db/ltm/link_scores.rs:1149. A module output is a scalar node feeding an arrayed target, so the one mechanism built for exactly this shape excludes it.
So the fix has a shape: route a module-to-arrayed-target edge through the existing per-target-element path instead of returning
Noneat the module guard, resolving the module's output reference viamodule_output_ref_in_document_order(which exists) as the live source. That reuses existing machinery and adds no new abstraction.The cost worth knowing before starting: the module guard exists in three places —
link_score_dimensions,try_scalar_to_arrayed_link_scores, and thecompile.rsshort-circuit — and they have to agree.One approach that looks attractive and is not viable: emitting the module link score as apply-to-all over the target's dimensions instead of scalarizing.
link_scores.rs:1090documents why a scalarfrommust not get an A2A score —expand_a2a_link_offsetsinvents a phantomfrom[elem]node, and loops throughfrombecome undiscoverable. The currentvec![]is deliberately avoiding that.Diagnosing this is now much cheaper than it was:
cargo run --release -p simlin-engine --example ltm_fragment_failures(added in #994) buckets every failure by compiler error and cross-tabulates against endpoint shape, and runs on any model viaLTM_FAIL_MODEL.-
Summary
A SCALAR module output feeding an ARRAYED reader gets its
input -> module(reallymodule-output -> arrayed-reader) link score emitted as a single scalar$⁚ltm⁚link_score⁚{m}→{growth}variable that evaluates to a constant 0. There are NO per-target-element$⁚ltm⁚link_score⁚{m}→{growth}[{elem}]variables. As a result, every arrayed feedback loop that runs through a module is dropped by LTM discovery (the loop product is 0, so the loop never forms) and mis-scored in exhaustive mode. This is an arrayed-link-score emission gap for the module-output-as-source case, not a settled equilibrium.Empirical evidence
Probed on an arrayed fixture: arrayed stock
s[Region]; a scalar multi-output modulem(pos/neg producer); per-element readergrowth[Region] = f(m·pos); loop closess[e] -> m -> growth[e] -> s[e].s[nyc] -> m,s[boston] -> m; exit readergrowth[nyc]).m -> growthlink score is emitted as a single scalar$⁚ltm⁚link_score⁚m→growthwhose value is constant 0. There are NO$⁚ltm⁚link_score⁚m→growth[nyc]per-element variables.m·$⁚ltm⁚composite⁚input_val = -1, internalinput_val→pos = +1,input_val→neg = -1), so the0is an emission gap, not an equilibrium.loops: []for the arrayed loops[e] -> m -> growth[e] -> s[e]. The per-exit-port recompute (the GH ltm: discovery mode can report the wrong loop polarity (sign error) through a multi-output module #698 family, PR ltm: discovery-mode correctness fixes from deep review #705) can never apply to arrayed loops through a module because they never form.Root cause direction
Ordinary scalar-source -> arrayed-target edges get per-target-element link scores via
try_scalar_to_arrayed_link_scores(src/simlin-engine/src/db/ltm/link_scores.rs), named$⁚ltm⁚link_score⁚{from}→{to}[{elem}]. But the module-output-as-source case takes a different path: module-involved links are built bymodule_link_score_equation(src/simlin-engine/src/db.rs~line 580 / the dispatch at ~756-778) and its per-shape twin insrc/simlin-engine/src/db/ltm/compile.rs, which produce a single scalarLtmSyntheticVarwithdimensions: vec![]. So a module-output -> arrayed-reader edge never reaches the per-target-element emission that an ordinary scalar -> arrayed edge does.Likely fix: route module-output -> arrayed-reader links through the per-target-element emission (mirroring
try_scalar_to_arrayed_link_scores), emitting one$⁚ltm⁚link_score⁚{m}→{to}[{elem}]per reader element, so the arrayed loop product is per-element and the loops form. The discovery parser specifically needs the element on thetoside (a single Bare-A2A var would be undiscoverable, the same reasoning that motivatedtry_scalar_to_arrayed_link_scoresputting the element onto).Why it matters
Correctness/completeness. Any model with an arrayed reader of a module output (a common shape -- a scalar policy/macro feeding per-region dynamics) silently loses its entire arrayed feedback-loop class through the module, with no diagnostic. The modeler sees
loops: [](or a loop list missing the module loops) and is taught the wrong mental model ("no feedback here"). Discovery is also the regime large/auto-flipped models fall into, so this bites exactly where it is least visible.Distinct from existing tracking
$⁚ltm⁚agg⁚{n}node and the drop is a DFSvisiting-set reachability problem; link scores are present and non-zero. Here the source is a module, the link score is absent per-element / constant-0, and the drop is an emission gap. Different node kind, different root cause.get_linksis variable-level / slot-0 only for arrayed models): an FFI surfacing limitation on top of correctly-computed per-element scores. Here the per-element scores are never emitted in the first place.Components affected
src/simlin-engine/src/db.rs(module_link_score_equation, the module-involved link dispatch at ~756-778)src/simlin-engine/src/db/ltm/compile.rs(link_score_equation_text_shaped, the per-shape module twin)src/simlin-engine/src/db/ltm/link_scores.rs(try_scalar_to_arrayed_link_scores-- the per-target-element emission the module path should mirror;emit_link_scores_for_edgedispatch)src/simlin-engine/src/ltm_finding.rs(discovery, the arrayed loop never forms), exhaustive loop-score builder (mis-scored loop product)Secondary observation (possibly distinct)
Feeding a scalar module input from an arrayed source (
src "s") or a single element (src "s[nyc]") fails to compile withfailed to compile fragments for variables: m, m, m. This is the mirror direction (arrayed/element-as-source into a scalar module input) and may be a separate validation/compile gap rather than an LTM-emission gap; noting it here for context. If it proves orthogonal during the fix, it warrants its own issue.Discovery context
Empirically discovered during PR #705 review follow-up (LTM implementer), 2026-06-03, via probes on the arrayed pos/neg-module fixture described above. Part of LTM tracking epic #488. Related discussion: PR #705 review thread r3353758167.