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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ __pycache__/
# Review loop state (per-working-copy)
.review-timestamp

# Claude Code per-session runtime state
.claude/scheduled_tasks.lock

# simlin-mcp npm packaging artifacts
/src/simlin-mcp/vendor
/src/simlin-mcp/npm
Expand Down
19 changes: 19 additions & 0 deletions docs/tech-debt.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,22 @@ Known debt items consolidated from CLAUDE.md files and codebase analysis. Each e
- **Description**: Local `third_party/uib_sd/zambaqui` files with magic `7f f7 17 53` parse like ordinary eight-section simulation-result VDFs for the primary run, but header word `0x68` points past the normal sparse-block run into an additional sensitivity/optimization-style payload. `tools/vdf_xray.py` can now inspect the ordinary run structures, but the extra tail and production Rust support for this result-family container are not decoded.
- **Owner**: unassigned
- **Last reviewed**: 2026-04-24

### 34. A2A Loop Score Variable Broadcasts Slot 0 Across All Slots

- **Component**: simlin-engine (`src/simlin-engine/src/db_ltm.rs::compile_ltm_equation_fragment`, LTM-var-to-LTM-var dep stub)
- **Severity**: RESOLVED (2026-04-25)
- **Description**: (**Resolved** during issue #463 work.) For an A2A arrayed loop, the loop_score equation `"link_score⁚A→B" * "link_score⁚B→A" * ...` was being compiled with every link_score reference treated as a scalar (slot 0 only) instead of A2A. Root cause was at `compile_ltm_equation_fragment`'s LTM-var dep fallback (formerly `db_ltm.rs:798-816`): when an LTM equation depends on another LTM variable, the dep stub was hardcoded to `size: 1, ast: None`, forcing the compiler to emit slot-0 reads regardless of the dep's actual A2A dimensions. The fix mirrors the working pattern used for explicit model A2A vars (line 740-743 / `build_stub_variable`): look up the dep's `LtmSyntheticVar.dimensions` via salsa-cached `model_ltm_variables`, build an `Ast::ApplyToAll(canonical_dims, dummy_const)` stub when dimensions is non-empty, and use the right `product(dim_lengths)` size. Now `loop_score⁚<id>` slots correctly evaluate per-element. Verified by `test_a2a_loop_score_has_distinct_per_element_values` in `tests/simulate_ltm.rs` and the layout bite check `test_arrayed_loop_importance_matches_argmax_abs_aggregation` in `tests/layout.rs`. Two pre-existing tests (`test_arrayed_population_ltm_exhaustive`, `test_cross_element_ltm_exhaustive`) had assertions that passed pre-fix only because the broadcast bug hid equilibrium elements; relaxed to "at least one slot non-zero" to match real fixture semantics.
- **Owner**: unassigned
- **Last reviewed**: 2026-04-25

### 35. A2A Loops Get `partition = None` in `loop_partitions`

- **Component**: simlin-engine (`src/simlin-engine/src/ltm.rs::CyclePartitions::partition_for_loop` + `db_ltm.rs::build_element_level_loops`)
- **Severity**: medium
- **Description**: The LTM partition map keys on **element-level** stock names (e.g., `population[nyc]`) because it is built from `model_element_cycle_partitions`. For pure-dimension A2A loops, however, `build_element_level_loops` calls `var_graph.find_stocks_in_loop(&var_level_nodes)` and stores **variable-level** stock names (e.g., `population`) in `Loop::stocks`. `partition_for_loop` then does `find_map(|s| stock_partition.get(s))` and the lookup misses, so every A2A loop returns `None`. Result: the LTM `loop_partitions` map systematically reports `None` for arrayed loops, regardless of which element-level SCC they actually belong to. Mixed/scalar loops use element-level stock names and partition correctly.
- **Why this matters**: The `compute_rel_loop_scores*` family normalises against partition denominators. With every A2A loop in the same fictitious "no parent" bucket, partition normalisation is wrong for any model that has multiple independent A2A loops (their rel-scores get cross-normalised against unrelated partitions). It also prevents mixed (A2A + scalar) partitions from arising in practice -- the codex review on PR #472 pointed at a real algorithmic bug in the layout aggregation that, today, can only be triggered through hand-crafted helper inputs because the engine never produces mixed-stride partitions due to this quirk.
- **Suspected fix**: Either (a) make `Loop::stocks` for A2A loops carry element-level names (one per element of the A2A dim, all of which should be in the same SCC by construction), or (b) extend `partition_for_loop` to expand a variable-level stock name to its element-level instances and look those up. (a) is more local but changes the `Loop` struct's semantic; (b) keeps the type unchanged. Either fix needs care that downstream consumers of `Loop::stocks` (e.g., `enumerate_module_pathways`) still get the names they expect.
- **Discovery context**: Found while writing an integration regression test for the codex P1 fix on PR #472. The test fixture deliberately built a model with both A2A and scalar feedback, expecting them to share a partition, and observed the A2A loop systematically getting `partition = None`. Documented in `test_compute_metadata_importance_series_length_matches_step_count` in `tests/layout.rs` so a future engine fix automatically begins exercising the mixed-stride path.
- **Owner**: unassigned
- **Last reviewed**: 2026-04-25
24 changes: 24 additions & 0 deletions src/libsimlin/simlin.h
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,30 @@ void simlin_analyze_get_rel_loop_score(SimlinSim *sim,
uintptr_t *out_written,
SimlinError **out_error);

// Get the number of element slots a loop's `loop_score` series occupies.
//
// For scalar loops this is 1; for arrayed (A2A) loops it equals the
// product of the loop's dimension lengths. Used by callers (pysimlin,
// the TS engine) to detect whether a loop supports subscripted access
// (`r1[Boston]`) or only bare ID access.
//
// Errors with `DoesNotExist` if the loop_id is not present in the
// snapshot captured at `simlin_sim_new` time -- typically because the
// sim was created with `enable_ltm = false`, the loop was added in a
// later patch (the snapshot is bound to compilation-era loops), or
// the LTM pipeline auto-flipped to discovery mode (which doesn't
// emit loop_score variables).
//
// # Safety
// - `sim` must be a valid pointer to a SimlinSim
// - `loop_id` must be a valid null-terminated C string
// - `out_element_count` must be a valid pointer to a usize
// - `out_error` may be null or a valid pointer to a SimlinError pointer
void simlin_analyze_get_loop_element_count(SimlinSim *sim,
const char *loop_id,
uintptr_t *out_element_count,
SimlinError **out_error);

// simlin_error_str returns a string representation of an error code.
// The returned string must not be freed or modified.
//
Expand Down
Loading
Loading