Summary
Phase 2 Task 5 of the @simlin/engine wasm backend work gated Sim.getRun()'s link-fetching on ltmEnabled && _engine !== 'wasm' (src/engine/src/sim.ts, ~line 234), replacing what had been an unconditional getLinks() call. As a result, a default (LTM-off) VM Sim.getRun() now returns Run.links = [], whereas before this change it returned populated-but-scoreless links.
This is a deviation from acceptance-criterion AC1.3's literal "default unchanged" wording for the Run.links field. It is presently consumer-invisible and matches the Phase 2 plan's prescribed code and design intent (LTM-off runs carry empty links). This issue exists to document the contract (LTM-off Run.links is [] by design) so a future consumer relying on populated default-path links is not surprised -- or to restore populated-scoreless links for the LTM-off VM path if that contract is preferred.
Why the plan's premise was off
The Phase 2 plan's Background assumed "LTM-off VM runs already carry links: []." That is not how the underlying FFI behaves: simlin_analyze_get_links (src/libsimlin/src/analysis.rs:227-258) returns causal links from model_causal_edges regardless of LTM -- enable_ltm only gates the link score field. So:
- Before this change: a default (LTM-off) VM
Sim.getRun() returned populated links (the causal edges) with no/zero scores.
- After this change: it returns
[].
The change is correct relative to the Phase 2 plan's prescribed code and its design intent (LTM-off runs should carry empty links), but it does change the observable default-path Run.links value, which is what AC1.3 nominally pinned as "unchanged."
Why the practical impact is currently zero (verified)
A full repo consumer audit (src/diagram, src/app, src/core, src/server, src/engine) confirmed that no consumer currently reads a Run's .links for an LTM-off run. The only Model.run caller outside the engine package -- src/diagram/Editor.tsx:487/:493 -- reads only varNames and getSeries, never .links. So the change is invisible to every present consumer and does not regress any current behavior.
Why it matters
It is a latent contract change. The guarantee a future consumer might reasonably assume ("a default run's .links carries the model's causal edges, just without scores") no longer holds: a default run's .links is now empty. With no documented contract, a future feature that wants the causal edge list from a cheap LTM-off run would silently get [] and might conclude the model has no causal structure.
Component(s) affected
src/engine/src/sim.ts (~line 234, the ltmEnabled && _engine !== 'wasm' gate on getLinks())
src/libsimlin/src/analysis.rs:227-258 (simlin_analyze_get_links -- returns causal edges regardless of LTM; enable_ltm gates only the score)
- Consumers of
Run.links (audited: only src/diagram/Editor.tsx calls Model.run, and it does not read .links)
Possible approaches for resolution
Pick one:
- Document the contract. State (in the
@simlin/engine CLAUDE.md and/or a doc comment on Sim.getRun() / the Run.links field) that an LTM-off Run.links is [] by design -- causal links and their scores are produced only when LTM is enabled -- so a future consumer does not assume populated default-path links.
- Restore populated-scoreless links for the LTM-off VM path if the "default
.links carries causal edges without scores" contract is the one we want to keep. Note simlin_analyze_get_links already returns these regardless of LTM, so this is a matter of relaxing the ltmEnabled gate for the VM path (the _engine !== 'wasm' portion would still need its own resolution -- see below).
Note the gate has two conditions; the _engine !== 'wasm' half (wasm runs never fetch links) is a separate question about whether the wasm backend will ever surface links and is out of scope for this item, which is specifically about the LTM-off VM default-path .links value vs AC1.3.
How it was discovered
Identified during the Phase 2 code review of the @simlin/engine wasm simulation backend integration (docs/implementation-plans/2026-05-22-engine-wasm-sim/, branch engine-wasm-sim). The reviewer traced the actual FFI behavior in analysis.rs, confirmed the plan's "LTM-off runs already carry links: []" Background premise was incorrect, performed the cross-package consumer audit confirming no current consumer reads LTM-off .links, and concluded the change matches the plan's prescribed code and design intent but deviates from AC1.3's literal "default unchanged" wording. Filed as the tracking item for that contract deviation.
Summary
Phase 2 Task 5 of the
@simlin/enginewasm backend work gatedSim.getRun()'s link-fetching onltmEnabled && _engine !== 'wasm'(src/engine/src/sim.ts, ~line 234), replacing what had been an unconditionalgetLinks()call. As a result, a default (LTM-off) VMSim.getRun()now returnsRun.links = [], whereas before this change it returned populated-but-scoreless links.This is a deviation from acceptance-criterion AC1.3's literal "default unchanged" wording for the
Run.linksfield. It is presently consumer-invisible and matches the Phase 2 plan's prescribed code and design intent (LTM-off runs carry empty links). This issue exists to document the contract (LTM-offRun.linksis[]by design) so a future consumer relying on populated default-path links is not surprised -- or to restore populated-scoreless links for the LTM-off VM path if that contract is preferred.Why the plan's premise was off
The Phase 2 plan's Background assumed "LTM-off VM runs already carry
links: []." That is not how the underlying FFI behaves:simlin_analyze_get_links(src/libsimlin/src/analysis.rs:227-258) returns causal links frommodel_causal_edgesregardless of LTM --enable_ltmonly gates the link score field. So:Sim.getRun()returned populated links (the causal edges) with no/zero scores.[].The change is correct relative to the Phase 2 plan's prescribed code and its design intent (LTM-off runs should carry empty links), but it does change the observable default-path
Run.linksvalue, which is what AC1.3 nominally pinned as "unchanged."Why the practical impact is currently zero (verified)
A full repo consumer audit (
src/diagram,src/app,src/core,src/server,src/engine) confirmed that no consumer currently reads aRun's.linksfor an LTM-off run. The onlyModel.runcaller outside the engine package --src/diagram/Editor.tsx:487/:493-- reads onlyvarNamesandgetSeries, never.links. So the change is invisible to every present consumer and does not regress any current behavior.Why it matters
It is a latent contract change. The guarantee a future consumer might reasonably assume ("a default run's
.linkscarries the model's causal edges, just without scores") no longer holds: a default run's.linksis now empty. With no documented contract, a future feature that wants the causal edge list from a cheap LTM-off run would silently get[]and might conclude the model has no causal structure.Component(s) affected
src/engine/src/sim.ts(~line 234, theltmEnabled && _engine !== 'wasm'gate ongetLinks())src/libsimlin/src/analysis.rs:227-258(simlin_analyze_get_links-- returns causal edges regardless of LTM;enable_ltmgates only the score)Run.links(audited: onlysrc/diagram/Editor.tsxcallsModel.run, and it does not read.links)Possible approaches for resolution
Pick one:
@simlin/engineCLAUDE.mdand/or a doc comment onSim.getRun()/ theRun.linksfield) that an LTM-offRun.linksis[]by design -- causal links and their scores are produced only when LTM is enabled -- so a future consumer does not assume populated default-path links..linkscarries causal edges without scores" contract is the one we want to keep. Notesimlin_analyze_get_linksalready returns these regardless of LTM, so this is a matter of relaxing theltmEnabledgate for the VM path (the_engine !== 'wasm'portion would still need its own resolution -- see below).Note the gate has two conditions; the
_engine !== 'wasm'half (wasm runs never fetch links) is a separate question about whether the wasm backend will ever surface links and is out of scope for this item, which is specifically about the LTM-off VM default-path.linksvalue vs AC1.3.How it was discovered
Identified during the Phase 2 code review of the
@simlin/enginewasm simulation backend integration (docs/implementation-plans/2026-05-22-engine-wasm-sim/, branchengine-wasm-sim). The reviewer traced the actual FFI behavior inanalysis.rs, confirmed the plan's "LTM-off runs already carrylinks: []" Background premise was incorrect, performed the cross-package consumer audit confirming no current consumer reads LTM-off.links, and concluded the change matches the plan's prescribed code and design intent but deviates from AC1.3's literal "default unchanged" wording. Filed as the tracking item for that contract deviation.