Skip to content

engine: replace protobuf patch types with native Rust types, add module support#240

Merged
bpowers merged 2 commits intomainfrom
claude/add-module-support-QdPsa
Feb 7, 2026
Merged

engine: replace protobuf patch types with native Rust types, add module support#240
bpowers merged 2 commits intomainfrom
claude/add-module-support-QdPsa

Conversation

@bpowers
Copy link
Owner

@bpowers bpowers commented Feb 7, 2026

The apply_patch API previously converted JSON input to protobuf
(project_io::ProjectPatch) as an intermediate format before applying
operations to the datamodel. This was a leftover from when the API was
protobuf-based.

This change defines native Rust patch types (ProjectPatch, ModelPatch,
ProjectOperation, ModelOperation) that use datamodel types directly,
eliminating the protobuf round-trip. The JSON conversion in libsimlin
now goes directly from JSON -> native patch types -> datamodel, which
is simpler and more maintainable.

Additionally, this adds:

  • AddModel project operation for creating new models within a patch
    transaction, enabling atomic "add model + add module referencing it"
  • Comprehensive test coverage for module upsert, delete, replace,
    module references, and combined AddModel+UpsertModule operations
  • Test for patch rollback behavior on error
  • Fix pre-existing TS type error in core/datamodel.ts (List)

https://claude.ai/code/session_011dCiV2VEYEPkUekXvBayyh

…le support

The apply_patch API previously converted JSON input to protobuf
(project_io::ProjectPatch) as an intermediate format before applying
operations to the datamodel. This was a leftover from when the API was
protobuf-based.

This change defines native Rust patch types (ProjectPatch, ModelPatch,
ProjectOperation, ModelOperation) that use datamodel types directly,
eliminating the protobuf round-trip. The JSON conversion in libsimlin
now goes directly from JSON -> native patch types -> datamodel, which
is simpler and more maintainable.

Additionally, this adds:
- AddModel project operation for creating new models within a patch
  transaction, enabling atomic "add model + add module referencing it"
- Comprehensive test coverage for module upsert, delete, replace,
  module references, and combined AddModel+UpsertModule operations
- Test for patch rollback behavior on error
- Fix pre-existing TS type error in core/datamodel.ts (List<string>)

https://claude.ai/code/session_011dCiV2VEYEPkUekXvBayyh
@claude
Copy link

claude bot commented Feb 7, 2026

Code Review - PR #240

I've reviewed the changes in this PR that replaces protobuf patch types with native Rust types and adds module support. Overall, this is a solid refactoring that simplifies the codebase. Here are my findings:

Positive Aspects

Architecture Improvements:

  • Excellent elimination of the unnecessary protobuf round-trip in the patch application flow. The direct conversion from JSON → native Rust types → datamodel is much cleaner.
  • The new native ProjectPatch, ModelPatch, ProjectOperation, and ModelOperation types are well-designed and more idiomatic Rust.
  • The addition of AddModel operation enables atomic multi-model operations, which is important for maintaining consistency.

Code Quality:

  • Comprehensive test coverage added (465 new lines of tests) covering module operations, AddModel, and error rollback scenarios.
  • Good use of Rust's type system with proper enums instead of protobuf's Option-wrapped enum variants.
  • Cleaner, more maintainable code with significantly reduced complexity in libsimlin/src/lib.rs (77 lines removed).

Testing:

  • Tests cover edge cases like duplicate model names, module references, rollback on error, and combined operations.
  • Good test naming convention (descriptive function names).

Issues and Concerns

1. Missing Clone Derivation Context (Minor)

In src/simlin-engine/src/patch.rs:307-359, all the new types derive Clone, but this might have performance implications for large patches since they contain Vec and String fields. Consider:

  • Is cloning necessary throughout the codebase? If these are only used transiently, perhaps references could be used more.
  • The current implementation clones data in several places (lines 418-420, 422-424, 426-428, 432-434), which seems unavoidable given the current API design.

2. Error Handling Simplification Loses Information

In src/libsimlin/src/lib.rs:3384-3396, the error handling for convert_json_project_patch was simplified:

// Before
let patch_proto = match convert_json_project_patch(json_patch) {
    Ok(patch) => patch,
    Err(err) => {
        store_ffi_error(out_error, err);
        return;
    }
};

// After
let patch = convert_json_project_patch(json_patch);

The new signature returns engine::ProjectPatch directly instead of Result<...>. This assumes the conversion can never fail, which may be true now but could be fragile if validation is added later. Consider whether infallible conversion is the right long-term design.

3. Canonicalization Happens at Application Time

Lines 418-435 in patch.rs show canonicalization happening during patch application. This means:

  • Invalid identifiers won't be caught until application
  • The same canonicalization work is repeated if patches are retried
  • The patch types don't enforce canonical form

Consider: Should canonicalization happen during JSON→Rust conversion instead?

4. Test Coverage Gap

While test coverage is excellent, I notice:

  • No tests for the SetSource operation (though it's a simple assignment)
  • No tests for malformed module references (referencing non-existent variables)
  • No tests for cyclic module dependencies

5. Documentation

The new public types (ProjectPatch, ProjectOperation, etc.) added to the public API (line 278 in lib.rs) should have doc comments explaining:

  • When to use each operation type
  • What happens with invalid operations
  • Examples of common patch patterns

6. TypeScript Type Fix is Good

The fix in src/core/datamodel.ts (lines 10, 18) properly adds the generic type parameter List<string>. This resolves a pre-existing type error.

Minor Issues

Code Style:

  • Line 494-505: apply_add_model uses as_str() followed by .to_string(), which could potentially be simplified depending on the canonicalize function's signature.

Consistency:

  • The Clone requirement on all patch types means every application creates a full copy of the datamodel. While this enables rollback (which is tested at line 1567), it could be expensive for large projects. Document this trade-off or consider CoW approaches if it becomes a bottleneck.

Security Considerations

No security concerns identified. The patch application properly:

  • Validates model existence before operations
  • Checks for duplicate model names
  • Rolls back on errors (preventing partial application)
  • Canonicalizes identifiers to prevent bypass via alternate names

Performance Considerations

  • The staged clone-and-apply pattern (line 361) is good for atomicity but doubles memory usage during patch application
  • Multiple clones of Stock, Flow, Aux, Module during operations could add up for large patches
  • Consider: Are there opportunities to use Cow or other zero-copy techniques?

Recommendations

  1. High Priority: Add doc comments to the newly public API types.
  2. Medium Priority: Add tests for invalid module references and cyclic dependencies.
  3. Low Priority: Consider whether canonicalization should happen earlier in the pipeline.
  4. Low Priority: Profile memory usage with large patches to validate the clone-based approach.

Summary

This is a well-executed refactoring that meaningfully improves the codebase. The elimination of protobuf as an intermediate format is the right choice, and the new native types are cleaner and more maintainable. The test coverage is excellent, and the atomic patch application with rollback is properly implemented.

The concerns raised are minor and mostly about future maintainability rather than current correctness. I recommend merging this PR with consideration for adding documentation to the new public APIs.

Verdict: Approve with minor suggestions

@codecov
Copy link

codecov bot commented Feb 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.47%. Comparing base (1b516b6) to head (0ebd249).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #240      +/-   ##
==========================================
- Coverage   76.53%   76.47%   -0.06%     
==========================================
  Files          80       80              
  Lines       21828    21763      -65     
==========================================
- Hits        16705    16643      -62     
+ Misses       5123     5120       -3     

☔ 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.

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: 9265e9bb26

ℹ️ 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 193 to 195
fn apply_add_model(project: &mut datamodel::Project, name: &str) -> Result<()> {
let canonical_name = canonicalize(name);
// Check if a model with this name already exists

Choose a reason for hiding this comment

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

P2 Badge Preserve display model names when adding models

apply_add_model canonicalizes the provided model name and stores the canonicalized string, but Project::get_model(_mut) matches model names literally (no canonicalization). This means a patch that adds a model like "Customer Growth" will create a model named customer_growth, and any subsequent operations (including in the same patch) that refer to the original display name will fail with BadModelName. It also silently changes the user-visible model name. Consider storing the name as provided (like other code paths do) or canonicalizing lookups consistently.

Useful? React with 👍 / 👎.

…e conversion

Three issues addressed:

1. apply_add_model was the only code path that canonicalized model
   names before storing. XMILE import, JSON import, and the C FFI
   simlin_project_add_model all store names as-provided. Since
   get_model/get_model_mut use literal string equality, canonicalizing
   on add broke same-patch references using the original display name
   (e.g. "Customer Growth" would become "customer_growth" and then a
   ModelPatch targeting "Customer Growth" would fail with BadModelName).
   Now stores names as-provided, matching all other code paths.

2. apply_patch now takes ProjectPatch by value instead of by reference.
   This eliminates all .clone() calls on the operation payloads (Stock,
   Flow, Aux, Module, View, SimSpecs, Source) since we can move data
   directly out of the consumed patch. The Clone derive is removed from
   the patch types.

3. convert_json_project_patch in libsimlin returns Result again. While
   the current conversion arms are all infallible, returning Result
   preserves the ability to add validation without changing the call
   site, and matches the established pattern in the FFI layer.

https://claude.ai/code/session_011dCiV2VEYEPkUekXvBayyh
@claude
Copy link

claude bot commented Feb 7, 2026

Code Review

This PR refactors the patch API to use native Rust types instead of protobuf intermediates, and adds module support including an AddModel operation. The changes are well-structured and improve code maintainability. Here's my detailed review:

✅ Strengths

1. Architecture Improvement

  • Eliminating the protobuf round-trip (JSON → protobuf → datamodel) in favor of direct conversion (JSON → native types → datamodel) significantly simplifies the code flow
  • The new ProjectPatch, ModelPatch, ProjectOperation, and ModelOperation types are clean, idiomatic Rust enums that are easier to work with
  • Moving from apply_patch(&patch) to apply_patch(patch) (taking by value) eliminates unnecessary clones - nice optimization

2. Test Coverage

  • Excellent comprehensive test coverage for the new functionality:
    • add_model_creates_empty_model
    • add_model_duplicate_returns_error
    • upsert_module_adds_module_variable
    • upsert_module_with_references
    • upsert_module_replaces_existing
    • delete_module_variable
    • add_model_and_module_in_same_patch
    • rename_variable_updates_module_references_in_same_model
    • patch_rollback_on_error
    • add_model_preserves_display_name
    • add_model_and_operate_on_it_in_same_patch_with_display_name
  • The rollback test is particularly valuable for ensuring transactional integrity

3. Code Simplification

  • Removed the require_field! macro and complex protobuf unwrapping logic
  • The convert_json_* functions in libsimlin/src/lib.rs are now much cleaner (lines 3481-3534 vs the old version)
  • Eliminated ~100 lines of boilerplate conversion code

4. Fixes Critical Issues

  • Model name canonicalization issue: correctly preserves display names ("Customer Growth" vs "customer_growth") which is essential for same-patch references
  • TypeScript type error fix in datamodel.ts (List<string>)

🔍 Observations & Minor Concerns

1. API Signature Change
In libsimlin/src/lib.rs, the comment mentions that convert_json_project_patch returns Result to preserve extensibility for future validation:

fn convert_json_project_patch(
    patch: JsonProjectPatch,
) -> std::result::Result<engine::ProjectPatch, FfiError>

However, all current conversion arms are infallible. This is fine as stated in the commit message, but consider:

  • Is there any validation we should add now? (e.g., checking model name format, preventing empty strings)
  • The Into conversions (sim_specs.into(), stock.into(), etc.) are all infallible - are we confident these won't need error handling?

2. Missing SetSource in JsonProjectOperation
I notice that ProjectOperation has three variants:

pub enum ProjectOperation {
    SetSimSpecs(datamodel::SimSpecs),
    SetSource(datamodel::Source),  // <-- This one
    AddModel { name: String },
}

But JsonProjectOperation only has two:

enum JsonProjectOperation {
    SetSimSpecs { sim_specs: ... },
    AddModel { name: String },
    // Missing SetSource
}

Is this intentional? If SetSource is deprecated or unused via the JSON API, that's fine, but it should be documented or removed from ProjectOperation entirely for consistency.

3. Error Messages
The error messages are generally good, but in apply_add_model:

Some(format!("model '{}' already exists", name))

Consider including the project name in error messages to help with debugging in multi-project scenarios.

4. Test Pattern Consistency
Most tests use .unwrap() which is appropriate for tests. However, some tests like patch_rollback_on_error check for specific error conditions - excellent! Consider adding a few more negative test cases:

  • Attempting to add a module referencing a non-existent model
  • Attempting to upsert a module with circular references
  • Edge cases with empty model names or special characters

📋 Code Quality Details

Type Safety: ✅ Excellent use of Rust's type system to ensure correctness

Memory Management: ✅ The move semantics (apply_patch(patch) consuming the patch) is optimal

Error Handling: ✅ Proper use of Result types with meaningful error codes

Documentation: ⚠️ The new types are documented, but consider adding examples showing how to construct a patch programmatically

🔐 Security Considerations

✅ No security concerns identified:

  • Input validation is appropriate (model existence checks)
  • Rollback mechanism prevents partial application on errors
  • No unsafe code introduced

🎯 Performance Considerations

✅ Performance improved:

  • Eliminated protobuf serialization/deserialization overhead
  • Using move semantics instead of cloning patch data
  • Capacity pre-allocation in conversion functions (with_capacity)

🧪 Testing Recommendations

While test coverage is excellent, consider adding:

  1. Integration test showing real-world workflow: add model → populate with variables → add module reference
  2. Benchmark comparing old vs new patch application performance (could demonstrate the value of removing protobuf)
  3. Fuzz testing for the JSON → native type conversion

📝 Documentation Suggestions

  1. Add doc comment examples to ProjectPatch, ProjectOperation, and ModelOperation showing typical usage
  2. Document the transactional behavior (all-or-nothing semantics with rollback on error)
  3. Clarify whether SetSource is meant to be available via JSON API

Summary

Overall Assessment: ✅ Approve with minor suggestions

This is a high-quality refactoring that:

  • Improves maintainability by eliminating unnecessary intermediate representations
  • Adds valuable new functionality (AddModel, module support)
  • Includes comprehensive test coverage
  • Fixes pre-existing issues (model name handling, TS types)

The code follows Rust best practices and the project's conventions. The minor observations above are suggestions for improvement rather than blockers.

Recommended Action: Merge after addressing the SetSource JSON API gap (either add it or document why it's intentionally omitted).

Great work! 🚀

@bpowers
Copy link
Owner Author

bpowers commented Feb 7, 2026

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Another round soon, please!

ℹ️ 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".

@bpowers bpowers merged commit ab8e69c into main Feb 7, 2026
10 checks passed
@bpowers bpowers deleted the claude/add-module-support-QdPsa branch February 7, 2026 04:02
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.

2 participants