Skip to content

ltm-augment: partial builders PREVIOUS-wrap dimension-name subscript indices (Wildcard and Bare shapes), silently stubbing scores #759

Description

@bpowers

Summary

The ceteris-paribus partial builders in src/simlin-engine/src/ltm_augment.rs (build_partial_equation_shaped -> wrap_non_matching_in_previous -> wrap_index_non_matching_in_previous) PREVIOUS-wrap a bare dimension-name identifier sitting inside a subscript index. Originally filed against the live Wildcard reference shape (SUM(matrix[State, *]) becoming SUM(matrix[PREVIOUS(state), *])), the defect is also reachable through a mundane Bare-shape apply-to-all equation with no reducer at all (see "Demonstrated reachable, fully silent path" below), where the dimension name leaks into the co-source other_deps set and the frozen co-source's subscript index gets wrapped.

PREVIOUS of a dimension name is meaningless: the fragment fails compilation and the synthetic link-score variable is silently stubbed -- the #546/#548/#587 hazard class ("LTM fragment compiler can't handle a shape -> synthetic var stubs to 0 -> degraded score masquerades as inactive"). Correction to the original filing: when the doomed expression routes through a synthesized implicit helper, the failure produces no diagnostic at all -- model_ltm_fragment_diagnostics never covers implicit helpers (the #741 class) -- so the original claim that the defect "is surfaced as an Assembly Warning when reached" is wrong for this route.

Demonstrated reachable, fully silent path (GH #743 review, raises severity to medium)

Verified independently by an implementor (discovery) and an adversarial reviewer (probe) during GH #743 work on branch ltm-fix-batch-2 at commit 7012d9f0:

growth[D1] = matrix[D1, c1] * frac[D1]

A Bare-shape apply-to-all equation with a pinned literal co-source index (c1), no reducer involved. The changed-first partial for the frac -> growth edge freezes the co-source as

PREVIOUS(matrix[PREVIOUS(d1), d2·c1])

-- the iterated-dim name d1 is PREVIOUS-wrapped inside the subscript index.

Mechanism: the A2A/scalar link-score generators build their dep sets via identifier_set(ast, &[], None) -- with empty dims (two call sites, ~2694 and ~2980 at 7012d9f0) -- so dimension names leak into other_deps. By contrast, build_arrayed_link_score_equation deliberately strips the source's dimension and element names from the dep set before building partials. Once d1 is in other_deps, the per-index walk in wrap_index_non_matching_in_previous treats it as a causal dependency (the #587 guards only recognize dimension elements, never dimension names) and wraps it.

Observed failure mode: the doomed expression routes through a synthesized implicit helper; the helper's fragment-compile failure is silent (#741 -- model_ltm_fragment_diagnostics never covers implicit helpers), the helper stubs to 0, and the link score reads a constant wrong value -- -40 for the probe constants (= -1/(5·0.005)) -- with zero diagnostics. Silent wrong numbers on a mundane equation shape.

Failure chain (original Wildcard-shape analysis)

  1. wrap_non_matching_in_previous's live-Subscript arm (ltm_augment.rs ~506-551): the reference matches the live Wildcard shape (classify_expr0_subscript_shape returns Wildcard whenever any index is *), so the outer subscript stays unwrapped and each index gets the per-index treatment.
  2. The iterated-dim index State is not a literal element (is_literal_element_index matches dimension elements, never dimension names), so it is routed into wrap_index_non_matching_in_previous (ltm_augment.rs ~788).
  3. Neither GH LTM: PREVIOUS-in-subscript arg not reduced to a bare var during helper rewriting -> LTM synthetic link/loop score silently drops to 0 #587 guard fires: qualify_element_index (~766) and the is_element_of_any_dimension fallback (~815) both test whether the index ident names a dimension element. A dimension name is neither, so the index falls through to the recursive wrap_non_matching_in_previous call, where the bare Var(state) is indistinguishable from a causal dependency reference and gets wrapped: PREVIOUS(state).
  4. The resulting partial fails fragment compilation; assemble_module gracefully drops the fragment, the synthetic var keeps its layout slot and reads constant 0 for the whole run. On the named-synthetic-var route model_ltm_fragment_diagnostics emits a Warning; on the implicit-helper route (the demonstrated Bare-shape path) nothing is emitted (ltm: failed implicit-helper fragment compile emits no diagnostic (model_ltm_fragment_diagnostics skips model_ltm_implicit_var_info) #741); loop scores through the edge silently read garbage.

Reachability (severity: medium)

Superseded scoping note: this issue originally claimed the defect was latent/unreachable on current paths and surfaced via an Assembly Warning when reached. Both claims were falsified during the GH #743 review rounds (branch ltm-fix-batch-2, commit 7012d9f0): the Bare-shape pinned-index repro above is reachable on a mundane apply-to-all equation and is fully silent. Severity raised from low to medium.

The original Wildcard-path analysis (still accurate for that shape):

Counterfactually verified during the #534 review on branch ltm-core-batch (commit 74f5366): removing the #534 walk_var_equation gate (making the mapped whole-RHS reducer variable-backed again) reproduces sum(matrix[PREVIOUS(state), *]), the fragment-compile failure, and zero-stubbed loop scores.

Components affected

  • src/simlin-engine/src/ltm_augment.rs -- wrap_non_matching_in_previous (live-Subscript per-index path, ~528-546), wrap_index_non_matching_in_previous (~788, the guard gap), qualify_element_index (~766), and the A2A/scalar link-score generators' dep-set construction (identifier_set(ast, &[], None) call sites, ~2694 and ~2980), which let dimension names into other_deps in the first place.

Fix shape

Two complementary directions; the same fix covers both the original Wildcard-path shape and the demonstrated Bare-path shape:

  1. Wrapper-side guard (the original proposal): the wrapper must not wrap identifiers that resolve to dimension names in subscript-index position. The subscript-index context is already known during the walk (wrap_index_non_matching_in_previous is only ever called on indices), and the iterated-dim machinery already distinguishes index idents -- cf. is_live_source_iterated_dim_subscript / classify_expr0_subscript_shape's GH ltm-augment: subscripted-A2A-reference link-score partial fails to compile (PREVIOUS arg must be Var) #511 branch, and the iter_ctx / dims_ctx parameters already threaded through the walk. Extend the ~801/~815 guard pair with a third case: an index Var whose canonical name resolves to a project dimension (via dims_ctx, or matching iter_ctx's iterated dims) is a dimension selector, never a causal reference -- leave it verbatim, mirroring the LTM: PREVIOUS-in-subscript arg not reduced to a bare var during helper rewriting -> LTM synthetic link/loop score silently drops to 0 #587 element-name treatment.
  2. Generator-side dep filtering: pass the target's dims to the A2A/scalar generators' identifier_set calls (or strip dimension names from the resulting dep set, the way build_arrayed_link_score_equation already does) so dimension names never land in other_deps at all.

Regression tests should pin the partial text for (a) a live Wildcard reference with an iterated-dim index (e.g. via the #534 counterfactual shape) and (b) the Bare-shape pinned-index repro above (growth[D1] = matrix[D1, c1] * frac[D1], asserting the frac -> growth link score is correct and no PREVIOUS(d1) appears in the partial), so neither mangle can silently return.

Discovery context

Identified during adversarial review of the GH #534 work on branch ltm-core-batch (commit 74f5366); the #534 commit avoids the Wildcard path (synthetic-agg minting for mapped whole-RHS reducers) rather than fixing the builder.

Rescoped 2026-06: during the GH #743 review rounds on branch ltm-fix-batch-2 (commit 7012d9f0), an implementor and an independent adversarial reviewer both demonstrated the Bare-shape pinned-index path above, falsifying the latency and warning claims and raising severity to medium.

Tracking

Part of LTM tracking epic #488 (Augmentation group). Related but distinct:

Metadata

Metadata

Assignees

No one assigned

    Labels

    ltmLoops that Matter (LTM) analysis subsystem

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions