Known debt items consolidated from CLAUDE.md files and codebase analysis. Each entry has a description, component, severity, and measurement command.
- Component: simlin-engine
- Severity: medium
- Description:
unwrap_or_default()can mask unexpected conditions by silently substituting default values. Many uses are idiomatic (e.g.map.get().unwrap_or_default()for missing keys), but others should be replaced with explicit error handling orOption/Resultpropagation. Tracked as a measurement, not enforced by ratchet -- automated grep-based enforcement cannot distinguish idiomatic from problematic uses and incentivizes worse code. - Measure:
rg 'unwrap_or_default\(\)' --type rust -c src/simlin-engine/ - Count: 99 occurrences across 17 files (as of 2026-02-15)
- Owner: unassigned
- Last reviewed: 2026-02-15
- Component: simlin-engine, libsimlin
- Severity: low
- Description:
println!calls in library code (outside CLI and test code) should use proper logging or be removed. They can cause issues in WASM builds and pollute output for library consumers. - Measure:
rg 'println!' --type rust src/simlin-engine/src/ src/libsimlin/src/ -c - Count: 55 in simlin-engine/src/, 6 in libsimlin/src/ (as of 2026-02-15)
- Owner: unassigned
- Last reviewed: 2026-02-15
- Component: core, engine
- Severity: low
- Description:
@simlin/coredepends on@simlin/engine. This means the "shared data models" package depends on the WASM engine wrapper. Evaluate whether to invert this relationship or restructure so core is truly a leaf package. - Measure: Check
src/core/package.jsondependencies - Owner: unassigned
- Last reviewed: 2026-02-15
- Component: simlin-engine
- Severity: medium
- Description: 1,276
.unwrap()calls across 59 files. Many are in test code (parser/tests.rs: 100, json_proptest.rs: 59, json_sdai_proptest.rs: 62) but a significant number are in production code paths. The highest concentrations are in vm.rs (316), mdl/parser.rs (55), json.rs (41), and mdl/convert/variables.rs (39). VM unwraps are largely on view_stack operations where emptiness would indicate a compiler bug, but other call sites could benefit from proper error propagation. - Measure:
rg '\.unwrap\(\)' --type rust -c src/simlin-engine/src/ - Count: 1,276 occurrences across 59 files (as of 2026-02-15)
- Owner: unassigned
- Last reviewed: 2026-02-15
- Component: libsimlin
- Severity: medium
- Description: 102
.unwrap()calls in production FFI code (excluding tests_remaining.rs). Panicking across an FFI boundary is undefined behavior. lib.rs has 55 unwraps, simulation.rs has 14, model.rs has 8. These should be converted to return error codes through the FFI error mechanism. - Measure:
rg '\.unwrap\(\)' --type rust src/libsimlin/src/ --glob '!tests_remaining.rs' -c - Count: 102 occurrences across 8 files (as of 2026-02-15)
- Owner: unassigned
- Last reviewed: 2026-02-15
- Component: TypeScript packages (diagram, server, app, core)
- Severity: medium
- Description: 42 explicit
: anytype annotations and 24as anytype assertions across the TypeScript codebase. Heaviest concentrations: Editor.tsx (14: any), server/authn.ts (7: any), server/models/table-firestore.ts (5: any), Canvas.tsx (5as any), VariableDetails.tsx (4as any). These bypass type safety and should be replaced with proper types. - Measure:
rg ': any\b' --glob '*.{ts,tsx}' src/ -candrg 'as any\b' --glob '*.{ts,tsx}' src/ -c - Count: 42
: any+ 24as any= 66 total (as of 2026-02-15) - Owner: unassigned
- Last reviewed: 2026-02-15
- Component: TypeScript packages (diagram, app, engine, server)
- Severity: low
- Description: 50
console.*calls in production TypeScript code (non-test). Breakdown: diagram (37 across 8 files, mostly VariableDetails.tsx with 19), app (5), engine (4), server (4). These should be replaced with structured logging or removed. - Measure:
rg 'console\.(log|warn|error)\(' --glob '*.{ts,tsx}' src/diagram/ src/app/ src/engine/src/ src/server/ -c - Count: 50 occurrences across 18 files (as of 2026-02-15)
- Owner: unassigned
- Last reviewed: 2026-02-15
- Component: all
- Severity: low
- Description: 81 TODO/FIXME/HACK/XXX comments across the codebase (Rust and TypeScript). Highest concentrations: simlin-engine/model.rs (10), xmile/mod.rs (6), array_tests.rs (5), xmile/variables.rs (4). These represent acknowledged but unresolved work items that should be triaged into tracked issues or resolved.
- Measure:
rg 'TODO|FIXME|HACK|XXX' --glob '*.{rs,ts,tsx}' src/ -c - Count: 81 occurrences across 36 files (as of 2026-02-15)
- Owner: unassigned
- Last reviewed: 2026-02-15
- Component: simlin-engine
- Severity: low
- Description:
#[allow(dead_code)]attributes scattered through simlin-engine: retained-for-discoverability registry fields inwasmgen/lower.rs(7), VDF record decoders (vdf/record_results.rs), test-only constructors (test_common.rs), and assorted AST, bytecode and importer helpers.SymbolicOpcodeandOpcodecarry none: every variant is oneCompilerconstructs, and the dead-code lint (denied under clippy) is what keeps it that way. - Measure:
rg '#\[allow\(dead_code\)\]' --type rust src/simlin-engine/src/ -c - Count: 40 occurrences across 22 files (as of 2026-09-03)
- Owner: unassigned
- Last reviewed: 2026-09-03
- Component: app, core, server, engine, diagram
- Severity: medium
- Description: Large portions of TypeScript code lack corresponding test files. The app package has zero tests. The core package has 1 test file (datamodel.test.ts). The server package has 7 test files but no coverage for database models, auth helpers, route handlers, or rendering pipeline. The engine package has 8 test files covering the public API but no unit tests for internal modules (dispose, memory, import-export, error handling). The diagram package has 24 test files but none for the 23 component library files (Paper, Tabs, Card, etc.) or major UI modules (VariableDetails, ModelPropertiesDrawer, HostedWebEditor).
- Measure:
find src/{app,core,diagram,engine,server} -name '*.test.ts' -o -name '*.test.tsx' | grep -v node_modules | wc -l - Count: 40 test files total: diagram (24), engine (8), server (7), core (1), app (0) (as of 2026-02-15)
- Owner: unassigned
- Last reviewed: 2026-02-15
- Component: simlin-engine
- Severity: low
- Description: 707
.clone()calls across 50 files. Most clones are in non-hot-path code (serde.rs: 49, ltm.rs: 49, model.rs: 47, units_infer.rs: 38, compiler/context.rs: 30). The VM hot path has only 33 clones across 5,513 lines, which is well-controlled. Many clones in compiler/ and model.rs are for building intermediate data structures during compilation, where ownership transfer is impractical. Worth monitoring but not actionable today. - Measure:
rg '\.clone\(\)' --type rust src/simlin-engine/src/ -c - Count: 707 occurrences across 50 files (as of 2026-02-15)
- Owner: unassigned
- Last reviewed: 2026-02-15
- Component: simlin-engine, libsimlin
- Severity: low
- Description:
eprintln!calls in simlin-engine library code -- runtime warnings in results.rs (unsupported sim methods), model.rs (compilation errors), and variable.rs. These should use proper error types or conditional logging rather than printing to stderr. libsimlin has none. - Measure:
rg 'eprintln!' --type rust src/simlin-engine/src/ src/libsimlin/src/ -c - Count: 18 in simlin-engine, 0 in libsimlin (as of 2026-08-08)
- Owner: unassigned
- Last reviewed: 2026-08-08
-
Component: simlin-engine
-
Severity: RESOLVED (2026-07-26) -- the original premise was half wrong; do not act on it
-
Description: This entry used to claim that the
errors/unit_errorsfields onVariableand theerrorsfield onModelStage0/ModelStage1were all "redundant with the salsa incremental compilation pipeline ... dead weight carried through the monolithic compilation path", and that removing them would simplify the data model. Every read site was classified before acting, and the four fields fall into two very different groups.Variable::diagnosticsis a LIVE error channel, not residue. It is how parsing and lowering report a failure on a variable (as context-freeDiagnosticErrors), and the salsa diagnostics are downstream of it:db::var_fragment::explicit_fragment_inputreads the parsed variable'sUnitentries (the malformed-<units>-string rows), its fatal entries (parse errors, where the conveyor/queue driven-flowEmptyEquationsuppression applies) and the lowered variable's fatal entries (MismatchedDimensionsand everything elselower_astraises), and turns each into aDiagnostic. Emptying the lowered read makes every dimension-mismatch diagnostic vanish; emptying the parsed read turns a spec-sanctioned empty equation on a conveyor-driven flow into a phantom error. Deleting the field without first replacing the channel would silently drop diagnostics. The claim is pinned as a test rather than left as prose:db::diagnostic_tests::variable_error_fields_are_the_lowering_channelasserts both that the stage's value carries the error in the field and that the matching diagnostic reachescollect_all_diagnostics.No whole-model lowered copy carries an error list. The duplicate-canonical-ident collision (GH #891) is derived once per model by
db::diagnostic::model_duplicate_variablesand reported byemit_duplicate_variable_diagnostics(and as the hard error incompile_project_incremental); there is no whole-model simulatability gate reading a stage-level error list:compile_project_incrementalis the only compiler, and its cycle gate (db/dep_graph.rs) decides whether a resolved recurrence SCC compiles.Test helpers report
collect_all_diagnostics(test_common::TestProject::error_diagnosticsfor the(location, code)view): the production source of truth, which carries the unknown-dependency, bare-lookup-table and assembly diagnostics and a source location on every row, and has no model-levelVariablesHaveErrorsroll-up (the code stays in the numbered FFI enum with no producer). -
Owner: unassigned
-
Last reviewed: 2026-08-26
- Component: simlin-engine (src/simlin-engine/src/db/fragment_compile.rs, src/simlin-engine/src/db/var_fragment.rs)
- Severity: low
- Description: The parse side is dimension-granular:
parse_source_variablereads only the dimensions its equation names (db::query::variable_relevant_dimensions) and, for a qualified snapshot index, one element (project_has_qualified_element), so a dimension edit re-parses only the variables that read it (db::dimension_invalidation_tests). The fragment side is not:lowered_source_variable,compile_var_fragmentandexplicit_fragment_inputreadproject_dimensions_contextwhole, so a dimension edit re-lowers and recompiles every fragment of the project. AC1.5 (dimension changes propagate correctly) holds through salsa's backdating -- this is a pure performance item, and it matters for projects with many dimensions and thousands of variables. - Measure:
rg -n 'project_dimensions_context\(db, project\)' src/simlin-engine/src/db/fragment_compile.rs src/simlin-engine/src/db/var_fragment.rs(the whole-context reads a per-variable dimension projection would replace);db::exec_probe::ProbedDbover a dimension edit counts the fragments that recompile. - Tracked: GH #1045 (the work item; this entry holds the measure).
- Owner: unassigned
- Last reviewed: 2026-09-03
- Component: simlin-engine (src/simlin-engine/src/vdf.rs)
- Severity: RESOLVED (2026-07-05)
- Description: (Resolved on branch
vdf-parity-fixes.)tools/vdf_xray.pyhandled several VDF boundary cases the Rust parser had not adopted. The two decode rules were ported to Rustextract_data: (1) per-block bitmap width --VdfFile::parsenow reads header0x7C(block time-point grid count) alongside0x78(saved count), andVdfFile::block_bitmap_layoutdiscriminates each block by "u16 count == bitmap popcount" (preferring the compact saved-grid width when both match), with full-grid blocks decoded over the whole grid and sampled at the saved Time positions (block_positions_for_time_values, origin from the last saved time) -- fixing the garbage decode of the class-0x05 input blocks on the saved-suffixrisk.vdf/risk2.vdffixtures; (2) raw-zero OT entries with dynamic class0x11and a nonzero section-6 final value now decode as all-NaN missing data (inline_ot_entry_is_missing; 455 such slots onRef.vdf) instead of constant 0.0, and the C-LEARN comparator skips such reference-missing columns (vdf_reference_is_missingintests/integration/simulate.rs). The other two boundary cases the entry listed were already moot in Rust: the slot table and section-6 arrays are decoded from exact header/section-header pointers (slot_table_from_header, header0x58/0x5C/0x60), so theSCEN01.VDFcandidate-scan preference and the empty-ref-stream overrun do not arise. Covered bytest_saved_suffix_blocks_use_wider_bitmap_grid,test_ref_raw_zero_dynamic_ot_entries_decode_as_missing, and therisk.vdf/risk2.vdfextension oftest_section6_final_values_match_extracted_last_values.
- Component: simlin-engine (src/simlin-engine/src/db/analysis.rs)
- Severity: RESOLVED (2026-04-25)
- Description: (Resolved during the per-reference element graph refactor; see commit ff3f1afe and
docs/design-plans/2026-04-25-ltm-per-ref-elem-graph.md.)classify_element_dependencylumped both wildcard reducers (population[*]) and fixed-index references (population[NYC]) underCrossElement, whichexpand_edge_to_elementsthen expanded to the full N-by-N element cross-product. For a pattern likerelative_pop[R] = population / population[NYC]the true element structure is two N-edge patterns (same-element and broadcast from NYC), not N-squared. On arrays with tens of elements the spurious edges triggered combinatorial loop-enumeration blow-ups even though their runtime link scores were effectively zero. The fix replaces the per-(from, to)classifier with an AST-walking per-reference emitter (nowcollect_all_reference_sitesindb/ltm_ir.rs+emit_edges_for_referenceindb/analysis.rs) that classifies each reference byRefShapeand emitssource[element] -> target[d]forFixedIndex(element)references rather than the all-to-all expansion. The companionWildcard/DynamicIndex(inlined-reducer) cross-product was then also eliminated via aggregate-node hoisting -- a maximal inlined reducer is rerouted through a synthetic$⁚ltm⁚agg⁚{n}node (only the rows the reducer'sread_slicereads, O(N+M)); seedocs/design-plans/2026-05-09-ltm-503-cross-element-agg.mdand commits 44527d17 / 3eca55fb. - Note (2026-05-13): The conservative-slice carve-out (a reducer over an explicit slice used as a sub-expression,
x[r] = ... + SUM(pop[NYC, *])) was resolved by #514 (docs/design-plans/2026-05-11-ltm-arrays-hardening.md, Phase 4 -- commits ec018d0b / bfb61d08 / 7f837928):AggNode.read_slice(AxisRead ∈ {Pinned, Iterated, Reduced}) makes a sliced reducer hoistable and routes only the read rows through the agg; an arrayed reducer over an iterated dim (SUM(matrix[D1, *])) mints an arrayed agg with one slot per element. The only thing that still takes the full cross-product from a reducer is the bare dynamic index --SUM(pop[idx, *])with a non-literalidx, orarr[i+1]-- which isn't statically describable; that is a deliberate narrow carve-out (the IR reclassifies it asDynamicIndex). Mapped-dimension sliced reducers are hoisted (GH #534) except where the correspondence declines them (no declared correspondence, a many-to-one map, or aMappedReadaxis, GH #997), which stay conservative. - Measure: Build a test model with explicit subscript references and count element-level edges vs.
N + Nexpected. - Owner: unassigned
- Last reviewed: 2026-05-13
- Component: simlin-engine (src/simlin-engine/src/ltm.rs
analyze_expr_polarity_with_context) - Severity: RESOLVED (2026-06-09)
- Description: (Resolved: every half is closed, see the notes below;
ltm/polarity.rspasses SUM/MEAN through.) Only the scalar two-arg forms ofMax(a, Some(b))/Min(a, Some(b))are handled; the array reducer forms (Sum,Mean,Max(_, None),Min(_, None),Stddev,Rank) fall through toApp(_, _, _) => Unknown. Any variable computed viaSUM(x[*])orMEAN(x[*])therefore contributesUnknownpolarity, and every loop through it is classifiedUndetermined. ForSumandMeanpolarity is trivially the argument's polarity (monotone in every element). Graphical-function monotonicity also uses a strict EPSILON=1e-10 check that flags numeric import noise asUnknown. Fix: add reducer cases (pass through for SUM/MEAN, Unknown for STDDEV/RANK) and consider a plateau-tolerant GF monotonicity check. - Related (different code site): #516 (LTM tracking epic: #488) -- cross-element loops recovered through a synthetic
$⁚ltm⁚aggnode always classifyUndeterminedbecauserecover_cross_agg_loops(src/simlin-engine/src/db/ltm.rs) builds the agg-hop links off the variable-level graph, which has no agg node, sopolarity(X→agg)/polarity(agg→Y)areUnknown. Same SUM/MEAN-is-monotone fact as this item, applied to the synthetic aggregate's incoming/outgoing edges rather than to aSUM(x[*])user-variable RHS; the fix may share a "polarity of a monotone reducer's incoming edge" helper. - Note (2026-05-13): The graphical-function-monotonicity half is resolved -- #492 (
docs/design-plans/2026-05-11-ltm-arrays-hardening.md, Phase 7 -- commit ee07c935) replaces the strictEPSILON=1e-10test with a y-range-relative epsilon (max(EPSILON, range_rel * (y_max - y_min))), so near-flat lookup arms with imported numeric noise no longer flip a monotone curve toUnknown. Phase 7 also added per-element graphical-function static polarity (#502, commit 4991cd6c). The reducer-blind-spot half was handled by #480 (closed); the residual nuance that the monotonicity check compares the y-deltadyrather than the slopedy/dx(non-uniform x-spacing) is tracked as GH #536. The agg-hop-polarity half (recover_agg_hop_polarities) is tracked as #516. - Note (2026-06-09): The y-delta-vs-slope residual (GH #536) is fully resolved --
analyze_graphical_function_polarityclassifies each segment by its slope against a tolerance of1e-6 * (y_max - y_min) / avg_dxwhereavg_dxis the average x-spacing. On uniformly-spaced tables everydx == avg_dxso the per-segment dy threshold reduces EXACTLY to1e-6 * (y_max - y_min)-- the same value #492 used, preserving import-noise tolerance for finely-sampled tables. For non-uniform tables the threshold scales proportionally with segment width so narrow steep changes are still caught (the original #536 motivation). A degeneratex[i] == x[i-1]segment is skipped (duplicate point) or bails toUnknown(genuine vertical step). - Tracked in: #480 (LTM tracking epic: #488)
- Owner: unassigned
- Last reviewed: 2026-05-13
- Component: simlin-engine (src/simlin-engine/src/ltm.rs
IndexedGraph::dedup_circuits,CausalGraph::deduplicate_loops) - Severity: RESOLVED
- Description: (Resolved by #308: dedup keys on the canonical edge-sequence rotation,
ltm::canonical_rotation.) Dedup hashed the sorted node-index vector. In a multidigraph a cycle A->B->C->A and the distinct cycle A->C->B->A share a node set and are silently merged into a singleLoop.test_layout_arms_raceexercises this today. For scalar SD models with asymmetric dependency structure the merge happens to be benign, but the semantics are wrong: the two cycles have distinct edge sequences and potentially distinct polarity products. Fix: key dedup by a canonical edge-sequence rotation (rotate so the lex-smallest node starts the cycle, then compare the ordered edge list) instead of the sorted node set. - Tracked in: #308 (LTM tracking epic: #488)
- Owner: unassigned
- Last reviewed: 2026-04-29
- Component: simlin-engine (src/simlin-engine/src/ltm.rs
IndexedGraph::enumerate_circuits_in_scc) - Severity: RESOLVED
- Description: (Resolved in commit aa56c5a1 on the reduce-ltm-mem branch.) Production code now implements Johnson 1975 with the blocked-set + B[w] unblock-list mechanism; the misnamed "Johnson-style" Tiernan variant is retained only under
#[cfg(test)]as a test oracle for a Johnson-vs-Tiernan equivalence proptest. Keeping the entry as a historical pointer to the commit that fixed it. - Owner: unassigned
- Last reviewed: 2026-04-18
- Component: simlin-engine (src/simlin-engine/src/ltm_finding.rs)
- Severity: RESOLVED
- Description: (Resolved; the code this names no longer exists. See
docs/design-plans/2026-08-17-ltm-discovery-exact.md.) The per-timestep DFS keyed itsbest_scoreandvisitingstate onIdent<Canonical>(String-backed), cloning identifiers into hash maps on every recursive call. Discovery now runs entirely on integer-indexed structures:IndexedSearchinterns each node once and both candidate generators address nodes, edges, activity bitsets, and score rows byu32index. Kept as a historical pointer to the indexing lesson -- anIdent-keyed hot path in a per-step search is the shape to look for. - Tracked in: #481 (LTM tracking epic: #488)
- Owner: unassigned
- Last reviewed: 2026-08-18
- Component: simlin-engine (src/simlin-engine/src/db/analysis.rs
model_loop_circuits_tiered) - Severity: RESOLVED (2026-05-08, PR #496)
- Description: (Resolved by the variable-level loop enumeration refactor; see
docs/design-plans/2026-05-06-ltm-482-variable-level-loop-enumeration.md.)model_element_loop_circuitsenumerated circuits on the full element graph; for a pure-A2A model with V variables over an N-element dimension, every variable-level cycle inflated to N element-level circuits thatbuild_element_level_loopscollapsed back to one A2A loop. The fix ismodel_loop_circuits_tiered: it runs Johnson on the variable graph first, classifies each cycle by itsRefShapecomposition viamodel_edge_shapes, and emits pure-scalar / pure-A2A cycles as Loops directly. Only cross-element / mixed cycles flow through element-level Johnson, and only on the subgraph induced by their nodes. The auto-flip gate now keys on the variable-level SCC (cheap pre-Johnson check) and the slow-path subgraph SCC (computed inside the tiered enumerator) rather than the full N-times-inflated element-graph SCC. - Note (2026-04-25): The per-reference element graph refactor (
docs/design-plans/2026-04-25-ltm-per-ref-elem-graph.md) had earlier eliminated the spurious NxN edge density onFixedIndex-using models. After that refactor pure-A2A models already had small element-graph SCCs; the remaining win for tiered enumeration is the elimination of redundant per-element circuit enumeration (O(N) circuits per pure-A2A cycle that were collapsed back to one Loop).MAX_LTM_SCC_NODES = 50is retained: WRLD3 still trips it because its variable-level SCC is 166 (population, capital, agriculture, persistent-pollution, non-renewable resources) and the tiered enumerator gates on that. - Tracked in: #482 (LTM tracking epic: #488)
- Owner: unassigned
- Last reviewed: 2026-05-06
- Component: simlin-engine (src/simlin-engine/src/ltm_augment.rs
build_partial_equation) - Severity: RESOLVED (2026-04-25; reducer half superseded 2026-05-09)
- Description: (Resolved alongside #20 via per-shape partial equations; see commit a3f595ac and
docs/design-plans/2026-04-25-ltm-per-ref-elem-graph.md.) For a target likeshare[R] = population / SUM(population[*])the sourcepopulationappears both as a bare (same-element) reference and inside a wildcard reducer. The old A2A ceteris-paribus wrapper left allpopulationreferences unchanged, so whenshare[nyc]was evaluated in the partial,SUM(population[*])used CURRENT populations for every element instead of PREV for the non-target elements. The partial equalled the full expression, link-score magnitude was always 1, and dominance was misattributed. The fix splits the link score perRefShape:model_ltm_variablesemitted oneLtmSyntheticVarper(from, to, shape)triple, andbuild_partial_equation_shaped(inltm_augment.rs) holds the matching-shape references live while wrapping the rest inPREVIOUS(). Soshare = pop / SUM(pop[*])produced both a Bare link score (pop / PREVIOUS(SUM(pop[*]))) and a Wildcard link score (PREVIOUS(pop) / SUM(pop[*])). Superseded-by: the per-shape Wildcard link score described here (the…⁚wildcardvariant) was retired in commit 49be92dc; reducer references are now hoisted into a$⁚ltm⁚agg⁚{n}aggregate node and the lumped reducer link score is decomposed into the chainpop[d] → agg → share[r]-- thepop[d] → agghalf recovers each source element's fractional contribution to the aggregate's velocity (the…⁚wildcardvariant's variable-level Δpop denominator never did). The Bare-vs-FixedIndex per-shape split survives. Seedocs/design-plans/2026-05-09-ltm-503-cross-element-agg.md. - Owner: unassigned
- Last reviewed: 2026-05-09
- Component: simlin-engine (src/simlin-engine/src/ltm_augment.rs
generate_nonlinear_partial) - Severity: RESOLVED (2026-05-13; STDDEV) / WONTFIX-by-design (RANK)
- Description: For STDDEV and RANK the "nonlinear" reducer path returned the target variable itself, yielding a delta-ratio
(target - PREV(target)) / (source[d] - PREV(source[d]))instead of a ceteris-paribus partial. Under uniform scaling of all elements, STDDEV does not change, but the delta-ratio still reported nonzero per-element attributions. The fix unrolls STDDEV element-by-element with the standard formulasqrt(((s[d] - mean_p)^2 + sum_{i!=d}(PREV(s[i]) - mean_p)^2) / N)wheremean_p = (s[d] + sum_{i!=d}PREV(s[i]))/N. - Note (2026-05-13): MIN and MAX (the scalar two-arg forms) were already handled correctly via explicit nested binary calls with selective
PREVIOUS()wrapping. STDDEV is now resolved -- #483 (docs/design-plans/2026-05-11-ltm-arrays-hardening.md, Phase 6 -- commits 26ed48d3 / dd838dde) builds the analytic population-variancesqrtpartial ingenerate_nonlinear_partial(divisorN, matchingvm.rs::Opcode::ArrayStddev; the mean string-inlined; single-element variance is identically 0). RANK keeps the delta-ratio by design: RANK is an order statistic (non-differentiable) and returns an array, so it can never be a real scalar/A2A reducer RHS -- the delta-ratio is the documented conservative stand-in, pinned bytest_generate_rank_keeps_delta_ratioso the choice is explicit, not a silent fallback. (The pre-#483ltm_augment.rs:1374-1381line ref is stale; the RANK arm is the"RANK"case ingenerate_nonlinear_partial.) - Tracked in: #483 (LTM tracking epic: #488)
- Owner: unassigned
- Last reviewed: 2026-05-13
- Component: simlin-engine (src/simlin-engine/src/ltm_finding.rs
rank_and_filter) - Severity: RESOLVED
- Description: (Resolved by GH #310; the ordering is now filter-then-cap, and the cap itself is coverage-aware -- see
rank_and_filter's rustdoc anddocs/design/ltm--loops-that-matter.md, "Ranking and Filtering".)rank_and_filterused to sort by average absolute score, truncate toMAX_LOOPS= 200, and only then apply per-partitionMIN_CONTRIBUTIONfiltering, so a loop dominant in a small partition but globally ranked below 200 was lost before the partition scope saw it. Retention now runs over the whole discovered set before any cap, andselect_reportedguarantees every step's dominant loop within a competing partition a slot even under cap pressure. - Tracked in: #310 (LTM tracking epic: #488)
- Owner: unassigned
- Last reviewed: 2026-08-18
- Component: simlin-engine (LTM augmentation layer)
- Severity: low
- Description: The reference treats
LOOPSCORE(path...)andPATHSCORE(path...)as equation-level primitives. Simlin does not implement either as a builtin, but the capability LOOPSCORE exists for -- naming a loop and scoring it every run regardless of what discovery reported -- is implemented as pinned loops (SetLoopNamepatch primitive ->LoopMetadata->db/ltm/pinned.rs::model_pinned_loops, which always emits that loop'sloop_scorein both modes; LTM ref section 10). What is missing is the equation-level syntax: a modeler cannot writeLOOPSCORE(a, b, c)inside another variable's equation and use the value. Fix: parse the builtins and lower them onto the existing pinned-loop machinery, so the two spellings share one implementation. - Tracked in: #484 (LTM tracking epic: #488)
- Owner: unassigned
- Last reviewed: 2026-08-18
- Component: simlin-engine (src/simlin-engine/src/ltm.rs
LoopPolarity::from_runtime_scores) - Severity: RESOLVED (2026-05-08, PR #490)
- Description: (Resolved.) The paper's polarity-confidence metric
|r - |b|| / (r + |b|)classifies loops as Rux/Bux when mostly one polarity.LoopPolarity::from_runtime_scorescomputes the ratio, classifies a mostly-one-sign loop asMostlyReinforcing/MostlyBalancing, and every loop surface carries it aspolarity_confidence. - Tracked in: #485 (LTM tracking epic: #488)
- Owner: unassigned
- Last reviewed: 2026-05-06
- Component: simlin-engine (src/simlin-engine/src/ltm_augment.rs flow-to-stock path)
- Severity: RESOLVED (2026-09-07)
- Description: (Resolved.) The flow-to-stock link score is
|Δflow / Δnet|over the stock's synthetic net-flow aux, a ratio of integration-step (dt) deltas with no stock history behind it, and the VM and wasm both re-evaluate the flows at the restored end-of-step state before snapshotting it under RK2/RK4, so LTM runs under every integration method and needs no guard (tests/integration/ltm_integration_method.rspins Euler, RK2 and RK4 identical on an isolated loop and on the births/deaths model, whose flows are proportional to the stock, and pins a nonlinear flow following each method's own trajectory). Keeping the entry as a pointer to the design doc's "Integration Methods and Save Step" note. - Tracked in: #486 (LTM tracking epic: #488)
- Owner: unassigned
- Last reviewed: 2026-04-29
- Component: simlin-engine (src/simlin-engine/src/ltm_augment.rs:374)
- Severity: RESOLVED (2026-04-29)
- Description: (Resolved in commit f408528b "engine: drop unused
_is_affecting_stockflag in stock-to-flow link score".)generate_stock_to_flow_equationno longer computes the unused flag; structural routing now happens ingenerate_link_score_equation(ltm_augment.rs:607-637) before the three per-shape helpers (generate_flow_to_stock_equation,generate_stock_to_flow_equation,generate_auxiliary_to_auxiliary_equation) are invoked. Keeping the entry as a historical pointer to the commit that removed it. - Owner: unassigned
- Last reviewed: 2026-04-29
- Component: VDF tooling / simlin-engine
- Severity: medium
- Description: Local
third_party/uib_sd/zambaquifiles with magic7f f7 17 53parse like ordinary eight-section simulation-result VDFs for the primary run, but header word0x68points past the normal sparse-block run into an additional sensitivity/optimization-style payload.tools/vdf_xray.pycan 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
- 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; cross-element assertions tightened 2026-05-09)
- 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 atcompile_ltm_equation_fragment's LTM-var dep fallback (formerlydb/ltm.rs:798-816): when an LTM equation depends on another LTM variable, the dep stub was hardcoded tosize: 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'sLtmSyntheticVar.dimensionsvia salsa-cachedmodel_ltm_variables, build anAst::ApplyToAll(canonical_dims, dummy_const)stub when dimensions is non-empty, and use the rightproduct(dim_lengths)size. Nowloop_score⁚<id>slots correctly evaluate per-element. Verified bytest_a2a_loop_score_has_distinct_per_element_valuesintests/simulate_ltm.rsand the layout bite checktest_arrayed_loop_importance_matches_argmax_abs_aggregationintests/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; those were initially relaxed to "at least one slot non-zero". Subsequently tightened: once cross-element loops became scored on the element-level path (the per-reference element graph + the aggregate-node work; see commit d14c978b anddocs/design-plans/2026-05-09-ltm-503-cross-element-agg.md),test_cross_element_ltm_exhaustivere-asserts a slot-by-slot non-zero check on the A2A reinforcing births loop and pins the exact element-path factor set of the cross-element migration loop's loop-score equation. The migration loops still legitimately zero out one slot due toMAX(...)semantics inmigration_in/migration_out, which is fixture behavior, so the "at least one slot non-zero" form persists for those. - Owner: unassigned
- Last reviewed: 2026-05-09
- Component: simlin-engine (
src/simlin-engine/src/ltm.rs::CyclePartitions::partition_for_loop+db/ltm.rs::build_element_level_loops) - Severity: RESOLVED (2026-05-13)
- Description: The LTM partition map keyed on element-level stock names (e.g.,
population[nyc]) because it is built frommodel_element_cycle_partitions. For pure-dimension A2A loops, however,build_element_level_loopscalledvar_graph.find_stocks_in_loop(&var_level_nodes)and stored variable-level stock names (e.g.,population) inLoop::stocks.partition_for_loopthen didfind_map(|s| stock_partition.get(s))and the lookup missed, so every A2A loop returnedNone. Result: the LTMloop_partitionsmap systematically reportedNonefor arrayed loops, regardless of which element-level SCC they actually belonged to. - 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 was wrong for any model that has multiple independent A2A loops (their rel-scores got cross-normalised against unrelated partitions). - Note (2026-05-13): Resolved by #487 (
docs/design-plans/2026-05-11-ltm-arrays-hardening.md, Phase 2 -- commits 11eb1af1 / 40bf7d51 / f26330f6, plus c3318a2c relaxing thesim_newloop-partition assert in libsimlin). Approach (a): an A2ALoop'sstocksnow carry element-level stock names (the element-subscripted stocks the loop actually traverses), andloop_partitionsis keyed per slot of the A2A loop --LtmVariablesResult::loop_partitions: HashMap<String, Vec<Option<usize>>>-- since two elements of the same A2A loop can land in different cycle partitions.compute_rel_loop_scoresnormalises each(partition, slot)loop score against the per-(partition, slot) sum, so an independent A2A loop no longer cross-pollutes a sibling. libsimlin consumes the per-slot data via the subscripted loop-id accessor (commit 40bf7d51); no new C accessor was needed. - Tracked in: #487 (LTM tracking epic: #488)
- Owner: unassigned
- Last reviewed: 2026-05-13
- Component: simlin-mcp, simlin-serve (
.github/workflows/serve-release.yml,mcp-release.yml) - Severity: low
- Description: The npm release workflows for
@simlin/mcpand@simlin/servedo not produce a macOS Intel (darwin-x64) binary. Onlydarwin-arm64(Apple Silicon) and Linux targets are built. Intel Mac users cannot install these packages via npm optionalDependencies. The fix is to addcargo-zigbuild --target x86_64-apple-darwinsteps to both release workflows and add"@simlin/mcp-darwin-x64"/"@simlin/serve-darwin-x64"entries to the respectivepackage.jsonoptionalDependencies. - Owner: unassigned
- Last reviewed: 2026-04-25
- Component: simlin-serve (
src/watcher.rs,tests/watcher_merge.rs) - Severity: medium
- Description: Three watcher behaviours are lost on macOS for files that existed before the watcher's
FSEventStreamCreatesubscription:- Rename source side: an external
mv a.sd.json b.sd.jsondoesn't produce a pairedModify(Name(Both))event. FSEvents reports each side as a single-pathModify(Name(Any)), andnotify-debouncer-fullonly pairs them when its file-id cache already knows the source — which only happens for files that arrived via aCreateevent after the watcher started. The destination side merges viahandle_model_change; the source side stays in the registry as a phantom entry. - Rename-collision dual-removal:
mv a bwhere both are tracked never broadcasts the secondProjectRemovedbecause the destination side surfaces as a content-modify into the existing entry rather than aModify(Name(Both))that would route to theAlreadyExistsarm ofhandle_model_rename. - Plain
unlink()of a pre-existing file:external_remove_drops_registry_entry_and_broadcasts_removedconsistently times out on macOS-latest waiting forProjectRemoved. The classify branch added in commit 7faf89d4 (treatingModify(Name(_))on a missing leaf asRemoved) covers the rename-flagged-unlink case but does not move the test, so the underlying event must be arriving as something else (or not at all). Sister tests that create a file inside the watch window and then mutate or remove it pass on macOS, suggesting the file-id cache miss is the common factor.
- Rename source side: an external
- The Linux-equivalent flows work because inotify's
MOVED_FROM/MOVED_TOcookies always arrive together regardless of cache state, andIN_DELETEis a first-class event with no FSEvents-style flag coalescing. - Symptoms: external rename of a tracked file: SPA's editor for the old path may not migrate cleanly to the new path; rename-collision over a tracked destination merges contents instead of dropping both stale Loro states;
ProjectRenamedis never broadcast on macOS; an externalrmof a tracked file may leave a phantom registry entry. - Test impact: three integration tests are gated with
#[cfg_attr(target_os = "macos", ignore = ...)]:external_remove_drops_registry_entry_and_broadcasts_removedexternal_rename_re_keys_registry_and_emits_project_renamedrename_over_tracked_destination_removes_both_and_rehydrates
- Possible fixes (need design discussion, not papered over):
- Hydrate the file-id cache at watcher start by recursively scanning the root and registering each existing file with the debouncer's cache (e.g. via
notify_debouncer_full::FileIdMap::add_path). Closes both the rename-source-side miss and the unlink miss for pre-existing files at the cost of a one-shot scan. - Heuristic post-hoc pairing / inference in our actor: keep a short-lived (<=200ms) "recently disappeared registry entries" buffer; pair
Modify(Name(_))events with a recent removal whose content hash matches. - Switch to
notify::PollWatcheron macOS for the smoke / CI surface and accept higher latency in exchange for deterministic event delivery. - Accept the macOS UX gap as documented and update the SPA client to handle the unpaired event sequence (a removal followed by a separate hydrate of the destination) without losing in-flight Loro edits — likely requires Loro doc state to migrate via content rather than via path-key.
- Hydrate the file-id cache at watcher start by recursively scanning the root and registering each existing file with the debouncer's cache (e.g. via
- Investigation log (2026-04-26): classify-side fix routes missing-leaf
Modify(Name(_))→Removedwas insufficient; path-resolution fix usingresolve_canonical_path(canonicalize the deepest existing ancestor) made the Linux semantics platform-correct but did not move the macOS test 1 either, which strongly suggests the underlying event simply isn't reaching the actor. Without a macOS box to instrument the FSEvents stream directly, the next investigative step is to spawn a debug binary on a macOS runner that subscribes to FSEvents directly and prints the raw flags it receives for the test scenario. - Owner: unassigned
- Last reviewed: 2026-04-26
- Component: simlin-serve (
src/handlers.rs::save_project,simlin-engine/src/io.rs::atomic_write,tests/smoke.rs) - Severity: medium
- Description: On windows-latest the smoke test's
POST /api/projects/teacup.xmilesave returns500 Internal Server Errorwith the genericSaveError::Internalbody. The Linux and macOS smoke jobs both pass through the same end-to-end code path, so the failure is Windows-specific. The most likely root cause is the Windows-only branch insimlin-engine::io::atomic_writethat callsfs::remove_file(target)beforefs::rename(tmp, target)— std'srenamedoes not overwrite on Windows, so the target must be unlinked first, but if the in-process watcher'snotify-debouncer-fullis holding a directory handle (viaReadDirectoryChangesW) at the same path, the unlink can be deferred or the rename can lose to a kernel-level open-handle race. - Symptoms: Linux + macOS smoke pass; Windows smoke fails on the first save assertion at
tests/smoke.rs:316:5. The save POST receives a 500 within seconds, with the response body{"error":"internal server error"}. - Test impact:
tests/integration/main.rscompilesmod smokeout on Windows, soci.yaml'sserve-smokematrix runs the smoke test on ubuntu and macOS only, and Windows coverage of simlin-serve is theserve-windows-checkjob'scargo check -p simlin-serve --all-targets: a regression that breaks the Windows compile still trips CI, a Windows runtime regression does not. Resolving this item means dropping the cfg gate and adding windows-latest back to theserve-smokematrix. - Investigation hints:
- The harness already captures the spawned binary's stdout (where
tracing_subscriber::fmt()writes by default) and stderr, andChildGuard::dropdumps both on test panic. So the next failing CI run on Windows will surface the underlyingtracing::error!fromhandlers.rs:708in the job log, naming the specific err value behindSaveError::Internal. Read that first. - Once the err is visible, the most likely paths are (a)
commit_write->atomic_writereturning a Windows I/O error fromremove_fileorrename; (b)serialize_projectfailing on Windows-style line endings if Git's autocrlf checked outteacup.xmilewith\r\nand the XMILE writer emits something incompatible; (c) registryensure_or_getracing with the watcher. - A fix worth trying as a hypothesis test: switch
atomic_writeon Windows to useMoveFileExWwithMOVEFILE_REPLACE_EXISTINGviawindows-sysinstead of theremove-then-renametwo-step. That eliminates the open-handle window the watcher might be sitting in.
- The harness already captures the spawned binary's stdout (where
- Owner: unassigned
- Last reviewed: 2026-04-27
- Component: deploy (
scripts/deploy-web.sh,app.yaml,.gcloudignore,src/server/package.json) - Severity: medium
- Description:
pnpm deploy:webrunsgcloud app deployfrom the repo root, which uploads the entire workspace (minus.gcloudignore). On the App Engine instance, GAE's Node buildpack runspnpm installagainst the rootpackage.json+pnpm-workspace.yaml, which installs dependencies for every workspace package -- including@rsbuild/*,slate,radix-ui, rspress, and so on. None of this is needed at runtime: the server's actual prod-dep closure is ~10 third-party packages plus@simlin/coreand@simlin/engine. TheNODE_ENV=productionenv var that should skip devDependencies is broken under pnpm v10 (upstream bug GoogleCloudPlatform/buildpacks#591, still open as of 2026-05-08). - Symptom: GAE deploy is slow and the instance image is fat.
.npmrc'sstrict-peer-dependencies=truemakes any unmet transitive peer in the unrelated workspace packages abort the whole deploy. A new devDep added to e.g.src/diagramfor purely local tooling can break production deploys. - Status (2026-06-27): implemented as
pnpm deploy:web:staged, locally proven, pending a realgcloud --no-promotetest before it replacesdeploy:web. The staged path (scripts/deploy-web-staged.sh->scripts/build-deploy-staging.mjs+ the pure transforms inscripts/deploy-staging-manifests.mjs) assembles a self-containeddeploy-staging/whosepackage.jsonis exactly the server's prod closure, and deploysgcloud app deploy deploy-staging/app.yaml. The workspace-ref blocker is resolved by vendoring@simlin/coreand@simlin/engineintodeploy-staging/vendor/and rewriting them (and core's transitive@simlin/engineref) tofile:deps; apnpm-lock.yamlis generated for the minimal manifest. Confirmed the buildpack lever from Google's docs: App Engine standard ALWAYS reinstalls from the deployedpackage.json+ lockfile and has no vendored-node_modulesescape hatch, so the only thing that shrinks the install is the deployed manifest. Note the pnpm10 devDep bug is a side issue -- the dominant bloat is installing the whole workspace (app/diagram/website/serve-web prod deps: React, slate, radix, rspress), which a--prodinstall at the root would still pull. - Locally verified (2026-06-27): builder runs; an isolated
pnpm install --frozen-lockfile --prodOUTSIDE the repo (no workspace/.npmrcancestor, the GAE-faithful case) succeeds from the generated lockfile; the full server-sidelibsimlin.wasm(withsimlin_project_render_png) materializes at the__dirnamepath;node lib/index.jsboots ("WASM module initialized"), serves/->200,/api/user->401,/static/js/sd-component.js->200,/robots.txt->200. Result: 590 MB / 1171 pkgs -> 80 MB / 230 pkgs (~86% smaller), 28.7 MB upload payload. Unit tests inscripts/tests/deploy-staging-manifests.test.mjs. - Remaining before retiring
deploy:web: a realgcloud app deploy --no-promoteagainst a staging GAE version to confirm the nodejs24 buildpack honorspackageManager: pnpm@10.6.0(corepack) and accepts the frozenlockfileVersion. Until thendeploy:webstays as the proven fallback. This also incidentally fixes #695 (the staged upload is ~clean, well under the 10k file cap) and narrows #40 (only repopublic/is still transiently mutated by the reuseddeploy:assemble). - Owner: unassigned
- Component: deploy (
src/app/package.jsondeploy:assemble/deploy:clean,scripts/deploy-web.sh,public/) - Severity: low
- Description: The deploy step copies the rsbuild output (
src/app/build/*,src/app/build-component/static/...) directly into the trackedpublic/directory and removes the tracked symlinks (src/app/public,src/server/public,src/server/default_projects) sogclouddoesn't traverse the same content twice. The cleanup step (deploy:clean) restores them withgit checkout HEAD --andrm -rf public/asset-manifest.json public/static/{js,wasm,css,media}. This works -- a bashtrapinscripts/deploy-web.shnow guarantees the cleanup runs even on Ctrl-C/error -- but it's structurally fragile: any new build output category that lands underpublic/static/needs to be added to therm -rflist (we missedstatic/cssandstatic/mediauntil 2026-05-08), and a partial-cleanup recovery still requires a manualgit checkout. - Why it's not fixed yet: The clean fix overlaps with item 39 (staging-dir deploy): assemble the SPA into
<dir>/public/rather than the workspacepublic/, andgcloud app deployfrom<dir>. With nothing written into the tracked tree, no cleanup is needed --<dir>is just removed when done. Same blockers as 39. - Mitigation in place:
scripts/verify-deploy-build.shand agit status --porcelain-must-be-empty check in CI'sfrontendjob catch the "missed a path in deploy:clean" regression class on every PR. - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: server (
src/server/api.ts, the save handler) - Severity: low
- Description: The handler writes the File before the version-conditional
project.update, because theTableinterface has no cross-table transaction. A stale version fails fast before anything is written, the loser of a version race (409) and a transport failure (500) delete the File they wrote (cleanupUnreferencedFile), and the preview is invalidated only after a successful update. Two windows remain: a crash between the two writes leaks one File, and a winner saving byte-identical content in the same millisecond (same file id) that commits between the cleanup's re-read and its delete could lose its File. - Suspected fix: Move file create + project update into a single Firestore transaction, which needs a transaction primitive on the
Tableinterface. - Owner: unassigned
- Last reviewed: 2026-10-11
- Component: server (
src/server/api.ts:309-370,src/server/models/table-firestore.ts:38-40) - Severity: medium
- Description: There is no allowlist regex on usernames or project names at the API layer.
A username containing
/registers fine, but Firestore then rejects subsequent${userId}/${projectSlug}writes as INVALID_ARGUMENT, bricking the account.FirestoreTable.filterIdusesid.replace('/', '|')(single replace), so an id with multiple/is partially escaped, and a username containing a literal|could collide with the escaped form of another user's id. Pre-existing. - Suspected fix: Apply an allowlist regex such as
/^[a-z0-9][a-z0-9-]{0,38}$/to usernames (and a similar one to project names) at the API boundary; switchfilterIdtoreplaceAll('/', '|'). - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: server (
src/server/route-handlers.ts:50-73) - Severity: medium
- Description:
createProjectRouteHandlerreturns 404 for projects that don't exist, but 302 (redirect to/) for private projects that the requester doesn't own. The two responses are trivially distinguishable by an unauthenticated client, allowing enumeration of private project names belonging to a known username. Mild risk for an SD-modeling tool but a real information leak. Introduced in commit d44d3aea (#210). - Suspected fix: Standardise on the same response (e.g. always 404) for "doesn't exist" and "exists privately, not yours".
- Owner: unassigned
- Last reviewed: 2026-05-08
- Component: server (
src/server/new-user.ts:58-67) - Severity: medium
- Description:
files.filter(async (file) => { ... return stats.isDirectory(); })returns a Promise from every callback, and Promises are always truthy, sofilteris a no-op: every directory entry (including non-directories) flows into the subsequentpopulateExampleloop. The bug is masked today only because eachpopulateExamplecall is wrapped in try/catch and logs failures silently. Pre-existing. - Suspected fix:
for awaitaccumulating into a new array, orawait Promise.all(files.map(...))followed by a synchronous.filter. - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: server (
src/server/authn.ts:128-176) - Severity: medium
- Description: The first-sign-in path is check-then-insert:
findOneByScan({email})-> if no user, createtemp-<uuidV4()>. Two simultaneous first sign-ins for the same email both insert distincttemp-rows, after which every subsequentfindOneByScan({email})throwsexpected single result document, not 2. The recovery branch only handles the temp+real case (deletes temps when a real user exists); two-temps never converge. Pre-existing. - Suspected fix: Wrap the check-and-insert in a Firestore transaction that re-reads the email index, or hash the email to a deterministic temp ID so concurrent inserts collide on the primary key.
- Owner: unassigned
- Last reviewed: 2026-05-08
- Component: server (
src/server/headers.ts:9-37) - Severity: low
- Description: The replacement
res.writeHeadis an arrow function that ends withreturn this as ServerResponse;. In strict CJS module scopethisisundefined, so the patched method returnsundefinedinstead of theResponse. The only current caller (request-logger.ts:42) usesres.writeHead(500)and discards the return value, so the bug is latent -- but any future caller that chainsres.writeHead(...).end(...)or relies on the documentedServerResponsereturn type will crash silently. Pre-existing. - Suspected fix: Convert the arrow to a regular function (so
thisbinds to the response) or captureresin the closure andreturn res;explicitly. - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: server (
src/server/api.ts:254-265) - Severity: low
- Description:
if (!req.body.currVersion)is falsy for0(unreachable today since versions start at 1) but truthy for-1. Theas numbercast is type-only, with no runtime conversion: a client sendingcurrVersion: "1"producesnewVersion = projectVersion + 1 = "11"(string concat), and the new version is stored as a string. Pre-existing. - Suspected fix: Replace with
Number.isInteger(req.body.currVersion) && req.body.currVersion > 0(or a parsed-then-validatedNumber(...)step) before using the value. - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: app (
src/app/App.tsx:228-231) - Severity: medium
- Description:
urlParams.get('project')is passed directly to<Redirect to={projectParam}>with no shape validation. Same-origin browser policy prevents cross-origin redirects, but a value like?project=//evil.com/page(or?project=/foo/../bar) is a valid same-origin pushState path that fools the subsequent/\/.*\/.*/path-shape check atApp.tsx:235and routes the user to a confusing or attacker-controlled path. Introduced in commit 48a1e10a (#107). - Suspected fix: Validate against an allowlist before redirecting, e.g.
if (projectParam && /^\/[^/][^/]*\/[^/]+$/.test(projectParam)). - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: app (
src/app/config/env.js:25-35) - Severity: low
- Description:
require('dotenv-expand')(require('dotenv').config(...))always throwsMODULE_NOT_FOUNDbecause neither package is installed. The catch swallows it silently, and the comment misleadingly suggests the loading is conditional rather than universally broken. Result: any.env*files insrc/app/are ignored. Introduced in commit d0bc3e37 (#214). - Suspected fix: Add
dotenvanddotenv-expandtosrc/app/package.jsonif.envloading is actually wanted, or delete the block (and thedotenvexport frompaths.js) if not. - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: app (
src/app/config/paths.js:16-90) - Severity: low
- Description: A large fraction of
paths.jspredates the rsbuild migration and is no longer consumed:getPublicUrlOrPath,publicUrlOrPath, thehomepagelookup,appPublic,yarnLockFile,appNodeModules,appPackageJson,appTsConfig,moduleFileExtensions, andresolveModule. The actually-used exports areappPath,appBuild,componentBuild,appHtml,appIndexJs,componentIndexJs, and (conditionally)dotenv. Introduced in commit d0bc3e37 (#214). - Suspected fix: Trim the file to the consumed exports; delete the helpers that backed the dead exports.
- Owner: unassigned
- Last reviewed: 2026-05-08
- Component: app (
src/app/config/rsbuild/shared.config.js:96-110) - Severity: low
- Description: The dev-server proxy matches paths starting with
/api/,/auth/,/oauth/,/logout,/render/,/session, and/download/, but only/api/*and/sessionactually exist on the backend (seesrc/server/app.ts:252,260,src/server/authn.ts:234,238). The dead matchers are noise; worse, several usestartsWithand so prefix-collide:/sessionAlphawould be proxied even though only/sessionis a real route. Introduced in commit 9c7403e6 (rsbuild migration). - Suspected fix: Reduce the matcher to the actual endpoints; switch
/sessionfromstartsWithto equality (pathname === '/session'). - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: app (
src/app/NewProject.tsx:132-140,src/app/NewUser.tsx:90-98) - Severity: medium
- Description: Both
handleClosemethods schedule the create-project / create-user POST viasetTimeoutwithout any in-flight guard. A second click before the first POST resolves enqueues a duplicate request. The server does not enforce uniqueness, so the user can wind up with duplicate projects (or trigger a 409 cascade) on a slow network. Pre-existing. - Suspected fix: Add a
submittingboolean to component state, set it on the first click, disable the submit button while it is true, and reset it in the request's success/error handler. - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: app (
src/app/App.tsx:97-115) - Severity: low
- Description:
connectAuthEmulator,onAuthStateChanged, and the initialsetTimeout(this.getUserInfo)all run in theInnerAppconstructor. Under React 18+ StrictMode in dev these double-fire, and the unsubscribe handle returned byonAuthStateChangedis discarded so the listener leaks on unmount. In productionInnerAppis the root component and never unmounts, so the leak is cosmetic; the dev double-fire is the more visible symptom. The pattern predates the React 18->19 upgrade and was ratified by commit 846c69eb (#162) without revisiting it. - Suspected fix: Move the side effects into
componentDidMount; store theonAuthStateChangedunsubscribe handle and call it fromcomponentWillUnmount. - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: app (
src/app/config/rsbuild/shared.config.js:91-93) - Severity: low
- Description:
url.replace('/index', '')strips the first occurrence of/indexanywhere in the URL. A URL likehttp://localhost:3000/index/foo/indexwould becomehttp://localhost:3000/foo/index, and any project path containing the literal substring/index(e.g. a hypotheticalusername/index-models) would have it stripped from the printed URL. Introduced in commit 9c7403e6 (rsbuild migration). - Suspected fix:
url.replace(/\/index$/, '')so only a trailing/indexis removed. - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: app (
src/app/build-comparison-results.json,src/app/config/build-utils.js:28) - Severity: low
- Description: Two artifacts of the rsbuild migration are still present and harmless but misleading: (a)
build-comparison-results.jsonis a one-shot rsbuild-vs-webpack size comparison snapshot from the migration, no longer consumed by anything; (b)canReadAssetinbuild-utils.jsstill excludesservice-worker.jsfrom the asset-readability check, but no service worker exists in the project. Introduced in commit 9c7403e6 (rsbuild migration) / d0bc3e37 (#214). - Suspected fix: Delete both. If a service worker is reintroduced later, the exclusion can come back with it.
- Owner: unassigned
- Last reviewed: 2026-05-08
- Component: core (
src/core/datamodel.ts:1336-1356) - Severity: low
- Description: The TS
Dimensioninterface only carriesnameandsubscripts, while the Rust JSONDimensionhassize,mapsTo,mappings, andparent. Today the deployed save paths bypass the TSProjectfor serialisation so the loss is invisible -- butprojectAttachData()consultsdim.subscriptsto splay arrayed-variable Series data, and for indexed subdimensions the engine emits emptyelements. So TS sees an emptysubscriptsarray and silently fails to attach data for any variable arrayed over an indexed subdimension; the user would observe missing sparkline series. Pre-existing. - Suspected fix: Extend the TS
Dimensiontype withparent/mappings(and optionallysize) and use them inprojectAttachData; alternatively add a code comment documenting the limitation and the safe paths if a full fix is out of scope. - Owner: unassigned
- Last reviewed: 2026-05-08
- Component: engine (
src/engine/src/worker-server.ts:454-473) - Severity: low
- Description:
disposeProjectiteratesfor (const childHandle of children)whiledisposeModel/disposeSim(called from inside the loop) mutate the sameSetviathis.projectChildrenbookkeeping. ECMAScript'sSetiterator handles deletion of the current element well, so the code happens to work today, but a future change that deletes a not-yet-visited entry would silently skip handles and leak resources. The post-loopthis.projectChildren.delete(workerHandle)is also redundant once iteration finishes. Introduced in commit 1b516b63 (#238). - Suspected fix: Snapshot the set with
[...children]before iterating; remove the redundant post-loopdelete. - Owner: unassigned
- Last reviewed: 2026-05-08
-
Component: build / CI (
scripts/pre-commit,.github/workflows/,src/simlin-engine/Cargo.toml) -
Severity: low (was medium; the dev-target half of this is now structurally impossible — see below)
-
Description: Neither the pre-commit hook nor CI exercises a Cargo feature matrix for
simlin-engine, so feature-gating bugs in the library are invisible to the canonical gate. simlin-engine features:file_io,schema,ai_info,debug-derive(default =schema, ai_info, debug-derive), plusext_data,png_render,ltm_bench.Historical failure (no longer reachable). During Phase 7c of the Vensim macro epic a subagent added
src/simlin-engine/tests/metasd_macros.rs(which calls thefile_io-gatedload_dat/load_csv) WITHOUT the mirroringrequired-features = ["file_io"]entry. That brokecargo build -p simlin-engine --all-targetswheneverfile_iowas not enabled (error[E0425],load_dat/load_csvconfigured out), and survived multiple commits because the workspace build pullsfile_iointo scope via cross-crate feature unification (simlin-clirequestssimlin-engine/file_io). The Cargo.toml fix landed in7cba4ad2. GH #925 then removed the underlying hazard: the crate takes a self dev-dependency (simlin-engine = { path = ".", default-features = false, features = ["file_io"] }), sofile_iois unconditionally on for every test/bench/example target. A dev target can no longer break under a missingfile_io— that entire failure class is gone, and the#[cfg]/required-featuresbookkeeping it demanded is gone with it.What remains. The gap narrows to lib-only configurations: nothing gates
cargo check -p simlin-engine --lib --no-default-features(or other non-default lib feature subsets), where a#[cfg(feature = ...)]mistake insrc/can still break the build. In practice this is caught indirectly by the pre-commit WASM build (src/engine/build.sh'scargo rustc -p simlin --lib --release --target wasm32-unknown-unknown --crate-type cdylib --no-default-features, which compiles the engine withfile_io/schema/png_renderoff), but that is one point sample, not a matrix, and it is a release build of a different crate. Note also that the self dev-dep means--all-targetsno longer type-checks the lib withfile_iooff — so a matrix step must be--lib-scoped to test anything. -
Suspected fix: Add a lib-scoped feature-matrix check to the pre-commit hook and/or CI: at minimum
cargo check -p simlin-engine --lib --no-default-featuresandcargo check -p simlin-engine --lib --all-features. Optionallycargo hack --feature-powerset --lib(bounded). Do not use--all-targets --no-default-features: the crate's own unit tests requiredebug-derive(thousands ofassert_eq!/{:?}sites) and have never compiled without it, so that invocation fails for reasons unrelated to feature-gating hygiene. Keep within the documented test-time budget (docs/dev/rust.md#test-time-budgets) — a couple of representative lib configurations, not the full powerset, is the pragmatic choice given the 3-minute workspace cap. -
Secondary note (workflow, not a separate item): the original break propagated because multi-agent plan execution structurally trusts the inherited baseline — each subagent verified only its own delta, and several built with
file_ioand concluded the base was clean. Subagents should verify the exact configuration they will later claim green BEFORE starting, and STOP if the inherited baseline is already broken. -
Owner: unassigned
-
Last reviewed: 2026-07-10
- Component: docs / process (
src/simlin-engine/CLAUDE.md; theproject-claude-librarian/maintaining-project-contextskill) - Severity: RESOLVED (the documentation commit that closes the
compiler-unification-v2branch,doc: engine map, ledger and evergreen design docs) - Description:
src/simlin-engine/CLAUDE.mdis a scannable map -- one bullet per pipeline stage and perdb/file, each under about 120 words, with the standing invariants and the test that pins each -- and carries no "Last updated" stamp (scripts/check-docs.pyrefuses one) and no history (the rootCLAUDE.md's comment standards: documentation is evergreen, never a changelog). The standing rule that keeps it so: a per-delta update to a CLAUDE.md rewrites the surrounding bullet as if the code had always been this way; it never appends a paragraph narrating the change, and a bullet that grows past a screen is split, not extended. An epic's history lives in its design plan (docs/design-plans/), which is where the compiler-unification plan's ledger and divergence lists are. - Owner: unassigned
- Last reviewed: 2026-09-03
- Component: process / workflow (the
writing-implementation-plans/execute-implementation-planskills;docs/implementation-plans/) - Severity: medium
- Description: Implementation plans tend to defer all real-corpus numeric/structural validation to the last phase, which hides latent pre-existing blockers until most dependent work is already layered on top.
Concretely, in the Vensim macro epic two latent defects — GH #554 (false
init->initmacro recursion) and a #554-classDELAY N->delaynvariant — blocked acceptance criteria but only surfaced in Phase 7, the first phase to exercise the renamed-builtin / stdlib-module-collision path against real models (C-LEARN, metasd), after five phases of dependent work. A cheap corpus smoke run against the Phase 1-2 datamodel would have surfaced #554 far earlier and reduced rework risk. A second symptom: discovering a known bug-class of latent blockers mid-plan triggered serial stop-and-ask round-trips even though both #554-class decisions were the same class with the same answer. - Suspected fix: Future implementation plans should interleave an early, cheap real-corpus smoke validation (run against the earliest phase that produces a runnable artifact) rather than back-loading all corpus validation into the last phase; and plans should pre-authorize fixing a known bug-class of latent blockers discovered during corpus validation, to avoid serial stop-and-ask round-trips for what is provably one decision. This is guidance for the plan-authoring skills, not a code change.
- Owner: unassigned
- Last reviewed: 2026-05-15
- Component: process / workflow (the
writing-implementation-plans/execute-implementation-plan/finishing-a-development-branchskills;docs/implementation-plans/) - Severity: low
- Description: For the Vensim macro epic, the design plan was committed at the branch's merge-base (
86cc7fcb), but the 8-filedocs/implementation-plans/2026-05-13-macros/directory (phase_01..07 + test-requirements.md) that drove 40+ commits stayed untracked in git for the entire execution and was only committed at the very end (d44d32fa, "doc: add Vensim macro support implementation plan") during the finishing step. Consequences: reviewers of intermediate commits had no in-repo plan to check the work against, and a lost or compacted session could have lost the plan entirely (it existed only in the working tree). Distinct from #407 (which is about pruning stale plan content post-merge, not about when the plan enters version control). - Suspected fix: Commit the implementation plan at the start of execution — the
starting-an-implementation-plan/executing-an-implementation-planstep should commitdocs/implementation-plans/<plan>/before the first task subagent runs — rather than rescuing it at thefinishing-a-development-branchstep. Optionally add a guard in the execute step that refuses to start if the plan directory is untracked. - Owner: unassigned
- Last reviewed: 2026-05-15
- Component: diagram (
src/diagram/drawing/Canvas.tsx,src/diagram/drawing/canvas-interaction.ts) - Severity: medium
- Description: (Resolved on the canvas-interaction-migration branch; see
docs/design-plans/2026-06-07-canvas-interaction-migration.md.) The gating prerequisite was satisfied first: a reconciler-level gesture-sequence suite (tests/canvas-gesture-harness.tsx+canvas-gestures-*.test.tsx, commit 0852bde0) drives the real<Canvas>through PointerEvent down/move/up/cancel sequences asserting only on prop callbacks and rendered DOM. The migration (commit c2a391a5, fixed by 061320f2) then replaced the boolean modes and loose instance fields with a singleinteraction: InteractionStatefield. One deliberate deviation from the suspected fix below, adjudicated in adversarial review: element/arrowhead/source press resolution (handleSetSelection) and pointer-up resolution stay in the shell because they are geometry-dominated; they compose the module's pure helpers and construct union variants directly. The reducer's never-shell-drivenelementPointerDownbranch was deleted rather than kept as a parallel model (it was this entry's dual-source-of-truth complaint).reduceInteractionowns the empty-canvas, creation-tool, pinch enter/exit, and label-drag-start mode transitions. Canvas was subsequently converted to a function component (commit fe392c1c) with the same suite as the gate. - Suspected fix: Complete the migration: replace the boolean
CanvasStatemodes with a singleInteractionStatefield, drivehandlePointerDown/handleSetSelection/handlePointerCancelthroughreduceInteraction(applying its effects), and read the union in render instead of the booleans. This was deliberately deferred because doing it with behavior-preservation confidence needs end-to-end gesture-sequence tests that drive the Canvas through the React reconciler (pointer down -> move -> up), which do not yet exist; those tests should be written first. Continuous pinch/pan/momentum physics stay shell-internal by design. - Measure:
grep -c 'isMovingArrow\|isMovingSource\|isMovingCanvas\|isDragSelecting\|isPinching\|isMovingLabel' src/diagram/drawing/Canvas.tsx(drops to ~0 reads of the booleans when migrated). - Owner: unassigned
- Discovered: Phase 3 Canvas refactor (diagram-react-refactor branch)
- Last reviewed: 2026-06-07
- Component: diagram (
src/diagram/drawing/Canvas.tsx) - Severity: low
- Description: (Resolved in commit dd624051 on the canvas-interaction-migration branch.) The three methods and the
UpdateCloudAndFlow/UpdateFlow/UpdateStockAndFlowsimports only they used were deleted ahead of the tagged-union migration (#65) so the migration diff stayed small. They predated the unifiedapplyGroupMovement(group-movement.ts), which owns live-preview movement geometry. - Suspected fix: Delete the three methods (and any now-unused helper imports they alone pull in). Left in place during Phase 3 to keep that commit scoped to render purity + the interaction model.
- Measure:
grep -rn 'constrainFlowMovement\|constrainCloudMovement\|constrainStockMovement' src/diagram --include='*.ts' --include='*.tsx' | grep -v '/lib'returns nothing. - Owner: unassigned
- Discovered: Phase 3 Canvas refactor (diagram-react-refactor branch)
- Last reviewed: 2026-06-07
- Component: diagram (
src/diagram/project-controller.ts) - Severity: low
- Description: (Resolved by the controller's executor model: a viewport persist refreshes no annotations, connector dependencies are fetched by a coalesced
connectorsmaintenance item requested only after landed edits, the open, undo/redo and navigation, and drift is computed at render time from the cached dependencies. Kept as a historical pointer.) A sketch-connector/equation consistency check (ProjectController.attachConnectorErrors) runs fromupdateVariableErrors, which executes on EVERY project rebuild inupdateProject. It fetches per-variable equation dependencies via the engine'sModel.getIncomingLinks(varName)-- one async engine call per on-view aux/flow/stock variable (batched withPromise.all). Because viewport-only settles (pan/zoom) route throughqueueViewUpdate->updateProject->updateVariableErrors, this recompute also fires when connectors cannot possibly have changed. For large models (e.g. World3, hundreds of variables) on the Web Worker backend that is NpostMessageround-trips per pan/zoom settle, adding avoidable latency to the settle's save. The feature is correct as-is; this is purely an efficiency refinement. - Suggested fix: (a) Skip the connector recompute on non-content updates by distinguishing viewport-only rebuilds from content/structure edits, or (b) memoize per-variable incoming-links keyed on the serialized model content so pan/zoom settles reuse the prior result.
- Owner: unassigned
- Discovered: sketch-connector/equation consistency-check work on branch
editor-bug-burndown-2026-07 - Last reviewed: 2026-07-02
- Component: pysimlin (src/pysimlin/tests/test_json_types.py)
- Severity: medium
- Description: (Resolved by the 2026-08-01 variable-type unification, which rewrote these strategies against the unified
simlin.typesclasses -- option 2 below:graphical_function_strategyno longer draws variable-length point lists (x points are derived, at most 6 y draws),stock_strategy/flow_strategy/aux_strategydraw flat fields with no nested compat draws, and thesuppress_health_check=[HealthCheck.too_slow]guard is retained as belt-and-suspenders. Kept as a historical pointer.) Several Hypothesis property-based tests intermittently fail withFailedHealthCheckbecause input generation is too slow. The affected tests areTestJsonRoundtrip::test_stock_roundtrip,TestSchemaCompliance::test_flow_validates_against_schema, andTestPatchRoundtrip::test_upsert_stock_roundtrip. The root cause is deeply nested composite strategies:flow_strategyandauxiliary_strategyconditionally invokegraphical_function_strategy, which itself draws from twographical_function_scale_strategyinstances plus variable-length point lists with constrained floats. Thestock_strategydraws multipleident_strategylists. When Hypothesis explores complex branches (e.g., graphical functions with many points and both scales), generation time can exceed the default health check deadline, causing intermittent failures that are environment-dependent (slower in CI, under load, or in sandboxed environments). Possible fixes include: (1) addingsuppress_health_check=[HealthCheck.too_slow]to the@settingsdecorator on affected tests, (2) simplifying strategies by reducingmax_sizeparameters or usingst.buildsinstead of@st.compositewhere possible, (3) caching or flattening nested composite strategies to reduce draw overhead, or (4) increasing thedeadlinesetting. Option 2 is preferred as it addresses the root cause rather than suppressing the symptom. - Measure: Run
cd src/pysimlin && uv run pytest tests/test_json_types.py -x --count=10(with pytest-repeat) to observe intermittent failures - Count: 3 affected tests (as of 2026-02-24)
- Owner: unassigned
- Last reviewed: 2026-02-24
- Note: Originally filed as a duplicate "### 19"; renumbered to 68 when the other 19 (Rust VDF Parser Boundary Parity) was resolved. Entry numbers 20-67 are cross-referenced elsewhere and were left untouched.