Skip to content

engine: improve MDL roundtrip fidelity#397

Open
bpowers wants to merge 27 commits intomainfrom
mdl-roundtrip-fidelity
Open

engine: improve MDL roundtrip fidelity#397
bpowers wants to merge 27 commits intomainfrom
mdl-roundtrip-fidelity

Conversation

@bpowers
Copy link
Owner

@bpowers bpowers commented Mar 19, 2026

Summary

  • Add ViewElementCompat struct to preserve Vensim-specific element dimensions (width/height/bits) through parse-write roundtrips
  • Split merged multi-view MDL files back into separate named views at Group boundaries
  • Preserve font specification, lookup range bounds, native lookup call syntax, variable name casing, and equation formatting conventions
  • Sort ungrouped variables alphabetically; use inline format for short equations with backslash continuations for long ones
  • Comprehensive integration test roundtripping mark2.mdl with per-view element matching

Acceptance Criteria (18/18 verified)

  • AC1 Multi-view output: 2 views with correct names, element partitioning, single-view compat, font preservation
  • AC2 Element metadata: Stock/Aux/Flow/Cloud/Alias preserve original dimensions; graceful fallback for non-MDL sources
  • AC3 Lookup fidelity: Native table ( input ) syntax, explicit range bounds preserved, computed bounds when no explicit range
  • AC4 Equation formatting: Inline format for short equations, backslash continuations, original variable casing, alphabetical ungrouped ordering
  • AC5 Test coverage: Format roundtrip test for mark2.mdl, all existing tests pass

Test plan

  • All 2763 engine tests pass (cargo test -p simlin-engine)
  • MDL roundtrip integration tests pass (cargo test -p simlin-engine --test mdl_roundtrip)
  • Human test plan at docs/test-plans/2026-03-18-mdl-roundtrip-fidelity.md for Vensim visual verification

bpowers added 22 commits March 18, 2026 08:20
Variables with both an equation and a graphical function now emit
WITH LOOKUP(input, (table_data)) instead of bare lookup data.
Standalone lookup definitions (no input equation) use the correct
name(\n\tbody) syntax instead of name=\n\t(body).

Sketch fixes: valve type-11 field 3 set to 0, cloud type-12 field 3
set to 48 (ASCII '0'), flow pipe connectors include direction flag
(4=stock, 100=cloud) and pipe type (22), influence connectors include
field 9=64. Flow pipe direction flags are now based on endpoint type
(stock vs cloud) rather than point order.
Vensim requires a specific element ordering within flow blocks: pipe
connectors (type 1, flag 22) must precede the valve (type 11) and flow
label (type 10). Cloud elements (type 12) must appear before the pipe
connectors that reference them. The previous ordering (valve, label,
pipes; clouds at end) caused Vensim to not display most sketch elements.

Reorder write_flow_element to emit pipes first, then valve, then label.
Move cloud emission from the main element loop to just before their
associated flow, using the cloud's flow_uid field for grouping.

Add mark2 sketch ordering test that validates pipe-before-valve ordering,
cloud-before-pipe ordering, and correct direction/influence flags.
Update mark2 test fixture with Vensim-reformatted version. Add water
and lookup_ex models to the sketch roundtrip test suite.
Real newline characters in XMILE view element names (e.g. "Effect of
fish density\non catch per ship") were emitted as actual line breaks in
MDL sketch output, breaking Vensim's line-by-line parser. Add
format_sketch_name() that escapes actual newlines to literal \n and
use it for all sketch element name writers.

Also emit the .Control group header before sim spec variables, and
emit per-view sketch section separators for multi-view models.
Vensim requires CRLF (\r\n) line endings. Our writer was outputting
LF (\n) which caused Vensim to report "syntax error" and fail to
render most sketch elements. Convert all newlines to CRLF in the
final output step.
The settings marker requires a DEL byte (0x7F) between :L and <%^E!@
to match what Vensim produces and expects: :L\x7F<%^E!@. Without this
byte Vensim reports "syntax error" on the file.

Also fix type 26 (display range end) to use FINAL TIME instead of
TIME STEP, matching all reference MDL files.
Design for improving MDL writer output fidelity so roundtripped files
preserve multi-view structure, element metadata, lookup syntax, variable
name casing, and equation formatting. Six implementation phases covering
datamodel extensions, parser metadata capture, writer multi-view split,
lookup/casing fixes, equation formatting, and integration testing.
Infrastructure for preserving Vensim-specific view element metadata
during MDL roundtripping. Adds ViewElementCompat (width, height, bits)
to Aux, Stock, Flow, Cloud, and Alias view elements, and a font field
to StockFlow. Flow gets both compat (valve dimensions) and label_compat
(attached label dimensions).

Updates protobuf schema, serde conversions, and all construction sites
across the codebase (json, xmile, mdl, layout, diagram, stdlib).
…rsing

Add raw bits field to VensimVariable, VensimValve, and VensimComment
parsed types so the original MDL sketch metadata is preserved through
the conversion pipeline. Populate ViewElementCompat on all element types
during view conversion: stocks, auxes, aliases, flows (valve compat +
label compat), and clouds.

This is the parse-side of AC2.1/AC2.2 -- the writer side will consume
these compat values to reproduce original sketch dimensions instead of
hardcoded defaults.
Add font field to ViewHeader and capture the $-prefixed font line
during MDL sketch parsing instead of discarding it. The font string
(stored without the leading $) flows through convert_view into
StockFlow.font for roundtrip fidelity. In multi-view merge, the
font from the first view is preserved.
When MDL specifies explicit y_range via [(xmin,ymin)-(xmax,ymax)], preserve
those bounds for roundtrip fidelity instead of always recomputing from data
points. This mirrors the existing x_range handling. When no explicit bounds
are present (e.g. WITH LOOKUP without range prefix), y_scale continues to be
computed from actual data points.
When the MDL parser merges multiple named views into a single StockFlow,
it inserts Group elements as view boundary markers. The writer now detects
these Group boundaries and splits the merged elements back into separate
MDL views, each with its own V300 header, font line, and view title.

Shared maps (valve UIDs, element positions, names) are built from all
elements before splitting to ensure cross-view link and alias references
resolve correctly. The preserved font from the StockFlow is emitted when
present, falling back to the default Times New Roman spec.

Includes 11 unit tests covering: split logic with/without Groups,
element partitioning, Module filtering, font preservation, multi-view
MDL output headers, and V300 separator counts.
Each sketch element write function (stock, aux, flow, cloud, alias)
now uses the preserved compat dimensions when present, falling back
to hardcoded defaults for elements from non-MDL sources (XMILE imports).
Flow elements use both compat (valve) and label_compat (attached label).
When the AST contains a lookup(table, input) call where the first
argument is a bare variable, emit it as `table name ( input )` -- the
native Vensim syntax -- instead of `LOOKUP(table name, input)`.

Falls back to the generic LOOKUP(...) form when the first argument is
not a simple variable reference (e.g. a subscripted expression).
Build a display name map from view elements (Aux, Stock, Flow) that
preserves original casing from the MDL source. Variable ident names are
canonical (lowercase, underscored), but the MDL format expects the
original casing on equation LHS lines. The map is built at the start of
write_equations_section and threaded through write_variable_entry and
all downstream functions.

When no view element matches a variable ident, the fallback
format_mdl_ident produces the standard lowercase space-separated form.
Short equations (name + equation <= 80 chars, single line) now use
Vensim's inline format with spaces around the operator:
  average repayment rate = 0.03
Longer or multiline equations still use the traditional format:
  name=
  	equation

Lookup and graphical function entries always use multiline format
regardless of length.
Add tokenize_for_wrapping() to split MDL equation text at natural break
points (after commas, before binary operators, around parens/brackets)
and wrap_equation_with_continuations() to insert Vensim-style backslash
continuation sequences (\<newline><tab><tab>) when lines exceed 80
characters.  Applied in both the scalar multiline path and the arrayed
element entry path.
Ungrouped variables in MDL output now appear in alphabetical order by
canonical ident, matching Vensim's deterministic ordering convention.
Grouped variables continue to follow their sector-based ordering from
the model groups.
The comment claimed the operator and its surrounding spaces were
collected into one token, which misrepresented what the code does.
The spaces stay with the adjacent operand tokens; only the bare
operator character is emitted as its own token.
Adds mdl_format_roundtrip test that roundtrips mark2.mdl through
parse/write/re-parse and verifies:
- AC1.1: 2 views with names "1 housing" and "2 investments"
- AC1.2: Per-view named elements match; valve/cloud counts match;
  connector counts within tolerance of documented normalizations
  (Time shadow elements dropped, init-only links not preserved)
- AC1.4: Font specification "Verdana|10" preserved per view
- AC3.1: Lookup calls use table(input) syntax, not LOOKUP()
- AC3.2: Explicit lookup range bounds preserved
- AC4.1: Short equations use inline format
- AC4.3: Variable name casing preserved on LHS
Document the new ViewElementCompat struct and font field added to
datamodel view element types for Vensim MDL roundtrip fidelity.
Expand the mdl/ module description to cover writer.rs and the view/
subdir's convert.rs module.
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0de40e6961

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1834 to +1837
let has_groups = sf
.elements
.iter()
.any(|e| matches!(e, ViewElement::Group(_)));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid splitting every visual group into separate MDL views

split_view_on_groups now treats the presence of any ViewElement::Group as a signal that the sketch was a merged multi-view MDL and splits at each group boundary, but Group is also the normal visual container type (see datamodel.rs docs for view_element::Group). In a single-view model that legitimately uses diagram groups (for example XMILE-imported groups), this will emit multiple *<view> sections and repartition elements purely by ordering, changing layout structure on export. The split should be gated by explicit MDL-merge metadata instead of all group elements.

Useful? React with 👍 / 👎.

@claude
Copy link

claude bot commented Mar 19, 2026

Code Review

I've reviewed this PR thoroughly. The changes are well-structured, well-tested, and accomplish the stated goal of improving MDL roundtrip fidelity.

Architecture

The approach of threading compat metadata through the parse → datamodel → write pipeline is clean and follows the existing Compat pattern. The multi-view split via Group boundaries cleanly reverses the merge performed at parse time. Protobuf additions are all optional fields, maintaining backwards compatibility.

Key changes verified

  • ViewElementCompat: Correctly stores width/height/bits through the roundtrip
  • Font preservation: Captured at parse, stored on StockFlow, emitted per-view segment
  • Lookup syntax: LOOKUP(tbl, x)tbl ( x ) conversion in MdlPrintVisitor is correct; standalone vs WITH LOOKUP distinction is properly handled
  • Inline/multiline formatting: Length check and backslash continuation logic are sound
  • Multi-view split: split_view_on_groups correctly handles the Group boundary detection with proper edge cases (no groups, empty pre-Group segments)
  • Pipe connector ordering: Moved before valve/label to match Vensim's expected ordering
  • Cloud/link field corrections: field 3 → 48 for clouds, field 9 → 64 for influence connectors are intentional fidelity fixes
  • CRLF conversion: Applied at the end of write_project, safe since all content is built with \n internally
  • Settings marker: \x7F insertion between :L and <%^E!@ is a fidelity fix based on reference files
  • y-scale preservation: Using explicit range when available, falling back to data-computed bounds — correct and well-tested

No blocking issues found

The patch is correct. Tests are comprehensive and cover the key scenarios including edge cases (no groups, no font, short/long equations, standalone vs embedded lookups, compat fallbacks for non-MDL sources). The behavioral changes (type 26 → stop instead of dt, cloud field 3, link field 9, settings marker) are all intentional fidelity corrections documented with comments.

Overall correctness: correct

@codecov
Copy link

codecov bot commented Mar 19, 2026

Codecov Report

❌ Patch coverage is 82.59587% with 59 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.85%. Comparing base (a3fbf46) to head (975baf0).

Files with missing lines Patch % Lines
src/simlin-engine/tests/mdl_roundtrip.rs 82.44% 46 Missing ⚠️
src/simlin-engine/src/serde.rs 67.85% 9 Missing ⚠️
src/simlin-engine/src/mdl/convert/variables.rs 75.00% 2 Missing ⚠️
src/simlin-engine/src/json.rs 85.71% 1 Missing ⚠️
src/simlin-engine/src/mdl/view/convert.rs 95.45% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #397      +/-   ##
==========================================
+ Coverage   76.72%   76.85%   +0.12%     
==========================================
  Files         144      144              
  Lines       37162    37700     +538     
==========================================
+ Hits        28513    28973     +460     
- Misses       8649     8727      +78     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@claude
Copy link

claude bot commented Mar 19, 2026

Code Review

I've reviewed the full PR diff across all 34 changed files, focusing on correctness of the core implementation in writer.rs, convert/variables.rs, view/convert.rs, the datamodel extensions, protobuf schema changes, and serialization layers.

Findings

No bugs found. The implementation is clean and well-tested.

Notable observations (not bugs)

  • Protobuf versioning: New fields (ViewElementCompat sub-messages, font on View) use correct sequential field numbers and are all optional/message types, so existing serialized instances will deserialize correctly with None/default values. This follows the backwards-compatibility requirement from CLAUDE.md.

  • View splitting: split_view_on_groups correctly handles the case where a Group element is always the first element of each view segment (as produced by merge_views during parsing). Elements before the first Group would form a separate segment with the StockFlow's own name — a correct defensive behavior, though not exercised in normal MDL roundtrips.

  • Equation formatting: The inline vs. multiline threshold (80 chars) and tokenizer for backslash continuations correctly handle quoted strings (single and double), unary operators at line boundaries, and the LF→CRLF conversion in write_project.

  • Y-range preservation: The change to build_graphical_function correctly gates on table.y_range being Some before using explicit bounds, with a clean fallback to data-point-computed bounds when no explicit range was specified. The four new unit tests cover all combinations (explicit/computed × normal/degenerate).

Verdict: Correct

The patch should not break existing code or tests. All new fields have proper fallback defaults, the writer handles both MDL-sourced (with compat metadata) and XMILE-sourced (without) models gracefully, and the comprehensive roundtrip test suite validates end-to-end correctness.

🤖 Generated with Claude Code

Fix escape_mdl_quoted_ident to handle newline characters in variable
names. Literal newlines now become the two-character \n escape, and
existing \n sequences (backslash + n from XMILE name attributes) pass
through without double-escaping. This fixes the "Missing matching
quote" error when converting XMILE models with multi-line variable
names (e.g. fishbanks).

Fix split_view_on_groups to emit empty segments for consecutive Group
markers, preventing silent loss of empty views in multi-view MDL files.

Add clarifying comment on is_lookup_only_equation explaining the
sentinel values and their MDL parser origin.

Strengthen the mdl_format_roundtrip test with per-element field-level
comparison (AC1.3): for each type-10 element matched by name, compare
width, height, and bits fields between original and roundtripped output.
Shape comparison is skipped because Vensim allows displaying any
variable type with any shape, and cross-view duplicate shapes are
excluded since they become aliases during view composition.
@claude
Copy link

claude bot commented Mar 19, 2026

Code Review: engine: improve MDL roundtrip fidelity

I reviewed the full diff (~4500 lines of changes across 34 files). This PR adds Vensim-specific metadata preservation through parse-write roundtrips: element dimensions, font specs, lookup syntax, variable casing, multi-view splitting, and equation formatting conventions.

No blocking bugs found

The code is well-structured with comprehensive unit and integration tests covering each new behavior. The changes to protobuf schema follow additive-only conventions (new optional fields). All the MDL format fixes (CRLF line endings, 0x7F settings marker, cloud field 3 = 48, valve field 3 = 0, link influence flag = 64, type 26 = stop time) are consistent with Vensim reference files and validated by the roundtrip tests.

Minor observations (non-blocking)

  1. escape_mdl_quoted_ident dual-path for \n (src/simlin-engine/src/mdl/writer.rs:69-78): The function treats \ followed by n as "already escaped" and passes it through without double-escaping. This is correct for the MDL roundtrip use case but means a variable name with a literal backslash before the letter 'n' (e.g. hypothetical "item\number") would be silently misinterpreted as containing a newline. Extremely unlikely in practice given Vensim naming conventions.

  2. Control group case-sensitivity (src/simlin-engine/src/mdl/writer.rs, lines around the group.name == "Control" checks): The exclusion checks for exact strings "Control" and "control" but not other casings. Since Vensim is case-insensitive, an MDL file with .CONTROL would not be matched. Very unlikely in practice.

  3. project_io.gen.rs side-effects: The protobuf regeneration removed Eq and Hash from Dimension and reordered DimensionElements/DimensionSize. These are prost-build artifacts from regeneration and are fine if tests pass, but worth noting the derive changes are intentional.

Overall correctness verdict: Correct

The patch is free of bugs and blocking issues. Existing code and tests will not break. The new functionality is thoroughly tested including a comprehensive mark2.mdl roundtrip integration test that validates per-view element matching, font preservation, lookup syntax, and equation formatting.

Remove "0" from is_lookup_only_equation sentinels. The MDL parser
produces "0+0" for standalone lookups and the actual input expression
for WITH LOOKUP. A real equation like WITH LOOKUP(0, body) would
produce equation="0", which was incorrectly treated as a standalone
lookup sentinel and roundtripped as name(body) instead of preserving
the input expression.

Fix view_compat_from_proto to return None when the proto message has
all default field values, preventing zero-dimension sketch elements
when deserializing old protos that lack compat data.

Clarify escape_mdl_quoted_ident comment explaining why backslash+n
sequences from XMILE name attributes are preserved as-is.
@claude
Copy link

claude bot commented Mar 19, 2026

Code Review

I reviewed this PR thoroughly, focusing on the MDL writer changes (view splitting, LOOKUP rewriting, equation formatting), datamodel extensions (ViewElementCompat, font, y-range preservation), serialization (protobuf, serde), and the integration with existing diagram/layout/XMILE code paths.

Areas Examined

  • Multi-view splitting (split_view_on_groups): Correctly handles no-group (single view), elements before first group, consecutive groups (empty segments), and trailing segments. No off-by-one errors.
  • LOOKUP syntax rewriting (MdlPrintVisitor::walk): Correctly pattern-matches LOOKUP(table, input) with 2 args and first-arg-is-Var guard; falls through to generic function call path for other cases.
  • Protobuf schema: All new fields use correct, non-conflicting tag numbers. All are optional, maintaining backwards compatibility.
  • Serde roundtrip: view_compat_from_proto correctly treats all-default proto messages as None. All view element construction sites updated with new compat field.
  • Y-range preservation: Verified no division by (y_scale.max - y_scale.min) exists anywhere in the codebase (Rust, TypeScript, or Python). The TypeScript linearScale in chart-utils.ts explicitly guards domainSpan === 0. Safe.
  • Cloud UIDs per-segment vs global maps: cloud_uids and flow_clouds are scoped per-segment while valve_uids/elem_positions/name_map are global. Since clouds and their associated flows originate from the same original view and are always in the same Group segment, this is correct in practice.
  • CRLF conversion: The final .replace('\n', "\r\n") is safe because the writer internally uses only \n and the parser normalizes \r during input.
  • Equation formatting: Inline vs multiline threshold, backslash continuations via tokenize_for_wrapping and wrap_equation_with_continuations — logic is sound.
  • All construction sites: Verified that every place constructing Aux, Stock, Flow, Cloud, Alias, or StockFlow has been updated with the new compat/font fields (diagram, layout, json, xmile, patch, tests, stdlib generator).

Verdict

Correct. No bugs found that would break existing code or tests. The changes are well-structured with proper fallbacks (compat fields default to None, writer falls back to hardcoded defaults). The integration test provides good structural coverage of the roundtrip path. Existing interfaces are updated consistently across all construction sites.

🤖 Generated with Claude Code

Pass the original MDL view title to create_sector_group instead of the
normalized title. View names go through make_view_names_unique during
parsing (replacing special characters, deduplicating), but Group
elements should carry the original title so the MDL writer can
reproduce it in the sketch section header.
@claude
Copy link

claude bot commented Mar 19, 2026

Code Review

Reviewed the full diff for PR #397 — MDL roundtrip fidelity improvements including ViewElementCompat, view splitting on Group boundaries, font preservation, lookup syntax, variable casing, equation formatting, and the comprehensive integration test suite.

Areas examined:

  • writer.rs (~1900 lines): Equation formatting (inline vs multiline, backslash continuations), sketch element serialization (aux/stock/flow/cloud/alias/link), flow pipe connector direction flags, view splitting via split_view_on_groups, sim spec output, settings section
  • view/convert.rs (~550 lines): merge_views font handling, make_compat i32→u32 cast safety, convert_variable compat propagation, cloud deferred emission, UID reassignment
  • datamodel.rs: ViewElementCompat struct additions across Aux/Stock/Flow/Cloud/Alias
  • project_io.proto: New ViewElementCompat message, font field on View
  • serde.rs: Proto↔datamodel ViewElementCompat roundtrip (correctly handles absent-vs-default-valued messages)
  • json.rs: Correctly sets font: None for non-MDL paths
  • xmile/views.rs: All XMILE paths correctly set compat: None / label_compat: None
  • layout/mod.rs: All generated layouts correctly set compat: None
  • types.rs: VensimVariable/Valve/Comment bits field (i32, safely cast via make_compat)
  • tests/mdl_roundtrip.rs: MDL→MDL, XMILE→MDL, and view element roundtrip test coverage

Findings

No bugs found. The implementation is thorough and handles edge cases well:

  • Flow pipe connectors correctly distinguish stock (flag 4) vs cloud (flag 100) endpoints
  • split_view_on_groups correctly handles the pre-first-Group segment edge case
  • tokenize_for_wrapping correctly identifies unary vs binary operators for line breaking
  • view_compat_from_proto correctly treats all-default proto messages as absent (returns None)
  • Precedence-aware parenthesization in mdl_paren_if_necessary correctly handles non-commutative operators on right children

Overall Correctness Verdict

Correct. The patch is free of bugs and blocking issues. Existing code and tests will not break — all new compat/font fields are Option with None defaults on non-MDL paths, and the protobuf changes are additive (new optional fields only).

The control group skip logic matched only "Control" and "control"
but Vensim allows other casings (e.g. ".CONTROL"). Use
eq_ignore_ascii_case to handle all variants and prevent duplicate
control sections in the output.
@claude
Copy link

claude bot commented Mar 19, 2026

Code Review

I reviewed all code changes in this PR (~3500 lines of non-documentation code across datamodel, serde, protobuf, MDL parser, view conversion, and MDL writer).

Findings

No bugs found that meet the threshold for flagging.

The implementation is well-structured and consistent:

  • Datamodel/protobuf/serde layer: ViewElementCompat is correctly threaded through all serialization paths. All protobuf changes are additive-only with optional fields, maintaining backwards compatibility. The view_compat_from_proto function correctly collapses all-zero compat structs to None to distinguish absent from present metadata.

  • Parser metadata capture: bits fields were already being parsed (used for is_ghost/scratch_name checks) but not stored — now they're preserved in the struct. Font line capture correctly strips the leading $. The valve bits field is parsed from the correct position in the MDL element line format.

  • View splitting: split_view_on_groups correctly reverses the merge operation. Group names carry the original MDL title (e.g., 1 housing) and the writer prefixes with * to reconstruct the original *1 housing line.

  • y-range preservation: The behavior change from "always recompute" to "preserve explicit, compute otherwise" is well-tested with four test cases covering explicit range, degenerate explicit range, computed range, and degenerate computed range.

  • Equation formatting: The inline vs. multiline threshold, backslash continuations, and lookup call syntax detection are all correct. The tokenize_for_wrapping function handles unary operators, quoted strings, and parentheses properly.

  • Cross-layer consistency: All view element constructors in XMILE, JSON, layout, and diagram code correctly set compat: None and font: None — no missing fields.

Overall Correctness Verdict

Correct. The patch is free of bugs and blocking issues. Existing code and tests will not break.

@bpowers
Copy link
Owner Author

bpowers commented Mar 19, 2026

Review cycle summary

Ran 5 iterations of automated code review (Codex + Claude). Addressed the following feedback across 4 fix commits:

Iteration 1 identified a name escaping bug, pipe connector flag concerns, and empty view splitting issues. Fixed escape_mdl_quoted_ident to properly handle newline characters in XMILE-sourced variable names (both literal newlines and the two-character \n escape). This fixed the "Missing matching quote" error when converting models like fishbanks. Also fixed split_view_on_groups to handle consecutive Group markers and added a per-element field-level comparison to the roundtrip test (AC1.3).

Iteration 2 found the is_lookup_only_equation sentinel incorrectly included "0", which could mis-serialize WITH LOOKUP(0, body) as a standalone lookup. Removed "0" from sentinels (only "" and "0+0" are valid). Also fixed view_compat_from_proto to return None for proto messages with all-default fields, preventing zero-dimension sketch elements.

Iteration 3 flagged that Group elements carried normalized view titles instead of originals. Fixed create_sector_group to receive and use the original MDL title.

Iteration 4 identified case-sensitive control group matching. Changed to eq_ignore_ascii_case to handle all Vensim casings and prevent duplicate control sections.

Pre-existing limitations (tracked separately): view coordinate preservation during compose/split, per-view font metadata collapse during merge, sketch shape preservation for custom display shapes, pipe connector flag semantics.

Gate view splitting on is_mdl_view_marker flag so XMILE organizational
groups are not incorrectly split into separate views during MDL export.
Groups created during MDL multi-view merge set this flag; XMILE groups
leave it false.

Emit a minimal valid sketch section when a model has no views, preventing
malformed MDL output for equation-only models.

Extract the lookup sentinel "0+0" into a named constant LOOKUP_SENTINEL
shared between the MDL parser and writer to reduce coupling on a magic
string.

Split writer_tests.rs out of writer.rs to stay under the 6000-line lint
threshold.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant