Skip to content

refactor: extract shared types and utility functions to mdl/types#235

Merged
ako merged 9 commits intomendixlabs:mainfrom
retran:feature/extract-shared-types
Apr 20, 2026
Merged

refactor: extract shared types and utility functions to mdl/types#235
ako merged 9 commits intomendixlabs:mainfrom
retran:feature/extract-shared-types

Conversation

@retran
Copy link
Copy Markdown
Contributor

@retran retran commented Apr 19, 2026

Why

The executor layer (mdl/) imports sdk/mpr for shared type definitions — EdmxDocument, JavaAction, NavigationProfile, JsonStructure, and dozens more. This coupling means any package that needs these types must pull in the entire MPR reader/writer stack, preventing alternative backend implementations and making the mdl/ subtree unsuitable for lightweight compilation targets.

This PR extracts all shared types into a new mdl/types package that has no dependency on sdk/mpr.

What changed

New packages:

  • mdl/types/ — 9 files containing all shared domain types previously defined in sdk/mpr/reader_types.go, sdk/mpr/edmx.go, sdk/mpr/asyncapi.go, sdk/mpr/utils.go, and sdk/mpr/writer_jsonstructure.go
  • mdl/backend/mpr/convert.go — bidirectional conversion layer between sdk/mpr reader/writer types and mdl/types

Extracted types and functions:

  • id.goGenerateID, GenerateDeterministicID, BlobToUUID, UUIDToBlob, ValidateID, Hash
  • infrastructure.goMPRVersion, ProjectVersion, FolderInfo, UnitInfo, RenameHit, RawUnit, RawUnitInfo, RawCustomWidgetType, EntityMemberAccess, EntityAccessRevocation
  • java.goJavaAction, JavaScriptAction
  • mapping.goJsonStructure, JsonElement, ImageCollection, Image
  • navigation.goNavigationDocument, NavigationProfile, NavHomePage, NavRoleBasedHome, NavMenuItem, NavOfflineEntity, plus spec types
  • edmx.go — Full EDMX/CSDL parser with all Edm* types and ParseEdmx
  • asyncapi.go — Full AsyncAPI parser with all AsyncAPI* types and ParseAsyncAPI
  • json_utils.goPrettyPrintJSON, BuildJsonElementsFromSnippet, datetime normalization

Migration (65 files): All executor handlers, catalog builders, backend interfaces, and mock implementations updated from mpr.* to types.* — purely mechanical import swaps with no logic changes.

Backward compatibility: sdk/mpr retains thin delegation wrappers and returns types.* directly, so external callers are unaffected.

Stack

PR 1/5 in the shared-types refactoring stack.
Merge order: #232this#236#237#238#239

retran added 2 commits April 19, 2026 15:06
Phase 1-8: 85 read/write handler tests with mock backend infrastructure
Error paths: 49 tests covering backend error propagation for all handler groups
Not-connected: 29 tests verifying Connected/ConnectedForWrite guards
JSON format: 26 tests validating JSON output for show/list handlers

Production code changes:
- Added Func fields to MockBackend for 8 agent-editor write methods
- Fixed ContainerID parameter semantics in withContainer and all call sites
Create mdl/types/ package with WASM-safe shared types extracted from
sdk/mpr: domain model types, ID utilities, EDMX/AsyncAPI parsing,
JSON formatting helpers. Migrate all executor handlers to use
mdl/types directly, removing type aliases from sdk/mpr/reader_types.go.

- Extract 16+ domain types to mdl/types/ (infrastructure, java,
  navigation, mapping)
- Extract GenerateID, BlobToUUID, ValidateID to mdl/types/id.go
- Extract ParseEdmx, ParseAsyncAPI to mdl/types/edmx.go, asyncapi.go
- Extract PrettyPrintJSON, BuildJsonElementsFromSnippet to json_utils.go
- Migrate 30+ executor handler files off sdk/mpr type references
- sdk/mpr retains thin delegation wrappers for backward compatibility
@github-actions
Copy link
Copy Markdown

AI Code Review

What Looks Good

This PR successfully executes a shared types refactoring to decouple the executor layer from sdk/mpr, which aligns with the project's architectural goals. The changes are:

  • Consistently applied: All references to extracted types are updated across backend interfaces, mock implementations, and catalog builders
  • Backward compatible: Type aliases in sdk/mpr maintain compatibility for existing code
  • Well-structured: Conversion functions properly map between sdk/mpr and mdl/types with correct field translations
  • Test-conscious: Includes new mock tests for affected executor commands
  • Scoped appropriately: Focused solely on the refactor goal without mixing in unrelated changes

The conversion layer in mdl/backend/mpr/convert.go correctly handles the bidirectional translation needed for the MPR backend to work with both type systems.

Recommendation

Approve - This PR is a clean, well-executed refactor that achieves its stated goal of reducing coupling between the executor layer and sdk/mpr while maintaining backward compatibility. The changes are mechanical and consistent, with no alterations to MDL syntax or behavior. All checklist items either pass or are not applicable (since no MDL syntax is modified).


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR starts the “shared-types” refactor by moving a broad set of shared MDL/MPR-facing value types and utility helpers out of sdk/mpr into a new mdl/types package, then updating mdl/ (executor, catalog, backend interfaces, linter, CLI) to depend on mdl/types rather than sdk/mpr for those definitions—reducing coupling and enabling alternative backends.

Changes:

  • Introduces mdl/types with extracted shared document/value types (navigation, mapping/json structures, images, infra metadata, IDs, AsyncAPI, EDMX, JSON helpers).
  • Updates mdl/backend interfaces and mdl/backend/mpr + mdl/backend/mock implementations to use mdl/types types.
  • Migrates many executor/catalog call sites and adds/updates mock-based tests around the updated interfaces.

Reviewed changes

Copilot reviewed 108 out of 111 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
sdk/mpr/writer_imagecollection_test.go Updates tests to use types.ImageCollection.
sdk/mpr/writer_imagecollection.go Switches writer APIs/serialization to types.ImageCollection.
sdk/mpr/writer_core.go Delegates UUID/blob conversion and ID generation to types.
sdk/mpr/utils.go Delegates shared ID/UUID/hash utilities to types and reworks BSON ID conversion.
sdk/mpr/reader.go Delegates blob→UUID conversion to types.
sdk/mpr/parser_misc.go Updates parsed unit return types to mdl/types (Java/JS actions, navigation, images, JSON structure).
sdk/mpr/asyncapi.go Replaces AsyncAPI structs with type aliases to mdl/types and delegates parsing.
mdl/visitor/visitor_agenteditor.go Removes stray blank line.
mdl/types/navigation.go Adds navigation document and related types/specs.
mdl/types/mapping.go Adds JsonStructure/JsonElement/ImageCollection/Image types.
mdl/types/java.go Adds lightweight JavaAction/JavaScriptAction descriptor types.
mdl/types/infrastructure.go Adds shared infra metadata types (versions, units, folders, raw units, rename hits, access types).
mdl/types/id.go Adds shared ID generation, UUID/blob conversions, validation, and hashing.
mdl/types/doc.go Package documentation for mdl/types.
mdl/types/asyncapi.go Adds AsyncAPI parsing/types in mdl/types.
mdl/linter/rules/page_navigation_security.go Switches nav menu item traversal to types.NavMenuItem.
mdl/executor/widget_templates.go Uses types.GenerateID for widget template ID generation.
mdl/executor/widget_operations.go Uses types.GenerateID when creating translation IDs.
mdl/executor/widget_engine.go Uses types.GenerateID for widget template loading and widget IDs.
mdl/executor/mock_test_helpers_test.go Adds shared mock ExecContext/model factories/assertion helpers for executor tests.
mdl/executor/helpers.go Switches folder cache element type + uses types.GenerateID for new folders.
mdl/executor/executor.go Updates executor cache folder/unit info types to mdl/types.
mdl/executor/cmd_write_handlers_mock_test.go Adds mock tests for write handlers using updated backend interfaces/types.
mdl/executor/cmd_workflows_write.go Uses types.GenerateID for workflow element IDs.
mdl/executor/cmd_workflows_mock_test.go Adds workflow show/describe mock tests.
mdl/executor/cmd_settings_mock_test.go Adds settings show/describe mock tests.
mdl/executor/cmd_security_write.go Switches member access/revocation structs to mdl/types.
mdl/executor/cmd_security_mock_test.go Adds mock tests for security show/describe flows.
mdl/executor/cmd_rest_clients_mock_test.go Adds mock tests for REST client show/describe.
mdl/executor/cmd_rename.go Switches rename reporting types from mpr.RenameHit to types.RenameHit.
mdl/executor/cmd_published_rest_mock_test.go Adds mock tests for published REST show/describe.
mdl/executor/cmd_pages_mock_test.go Adds mock tests for pages/snippets/layouts listing behaviors.
mdl/executor/cmd_pages_builder_v3_layout.go Uses types.GenerateID throughout page widget building.
mdl/executor/cmd_pages_builder.go Switches folder cache types to mdl/types.
mdl/executor/cmd_odata_mock_test.go Adds mock tests for OData client/service show/describe.
mdl/executor/cmd_odata.go Uses types.GenerateID for created objects and types.ParseEdmx for metadata parsing.
mdl/executor/cmd_notconnected_mock_test.go Adds not-connected guard tests across handlers.
mdl/executor/cmd_navigation_mock_test.go Adds navigation show/describe mock tests using mdl/types.
mdl/executor/cmd_navigation.go Switches navigation spec/profile/menu types to mdl/types.
mdl/executor/cmd_modules_mock_test.go Adds modules listing mock test using types.UnitInfo.
mdl/executor/cmd_misc_mock_test.go Adds version show mock test using types.ProjectVersion.
mdl/executor/cmd_microflows_mock_test.go Adds microflows/nanoflows show/describe mock tests.
mdl/executor/cmd_microflows_create.go Uses types.GenerateID for created microflow IDs/parameters.
mdl/executor/cmd_microflows_builder_workflow.go Uses types.GenerateID when constructing workflow-related microflow actions.
mdl/executor/cmd_microflows_builder_graph.go Uses types.GenerateID for graph object IDs.
mdl/executor/cmd_microflows_builder_flows.go Uses types.GenerateID for flow/merge/event IDs.
mdl/executor/cmd_microflows_builder_control.go Uses types.GenerateID for control-flow node IDs.
mdl/executor/cmd_microflows_builder_annotations.go Uses types.GenerateID for annotation/event IDs.
mdl/executor/cmd_mermaid_mock_test.go Adds mermaid describe mock test for domain model graphs.
mdl/executor/cmd_jsonstructures_mock_test.go Adds JSON structure show mock test using types.JsonStructure.
mdl/executor/cmd_jsonstructures.go Switches JSON structure operations/helpers to mdl/types utilities/types.
mdl/executor/cmd_javascript_actions_mock_test.go Adds JS action show/describe mock tests using types.JavaScriptAction.
mdl/executor/cmd_javaactions_mock_test.go Adds Java action show/describe mock tests.
mdl/executor/cmd_javaactions.go Uses types.GenerateID for Java action construction.
mdl/executor/cmd_import_mappings_mock_test.go Adds import mappings show mock test.
mdl/executor/cmd_import_mappings.go Switches JSON-structure element map types to types.JsonElement.
mdl/executor/cmd_imagecollections_mock_test.go Adds image collection show/describe mock tests using types.ImageCollection.
mdl/executor/cmd_imagecollections.go Switches image collection construction/list lookup to mdl/types.
mdl/executor/cmd_fragments_mock_test.go Adds fragment show mock tests.
mdl/executor/cmd_folders.go Switches folder info parameter types to types.FolderInfo.
mdl/executor/cmd_export_mappings_mock_test.go Adds export mappings show mock test.
mdl/executor/cmd_export_mappings.go Switches JSON-structure element map types to types.JsonElement.
mdl/executor/cmd_enumerations_mock_test.go Refactors enumeration tests to use shared mock helpers + adds describe/not-found coverage.
mdl/executor/cmd_entities_mock_test.go Adds entity show mock tests.
mdl/executor/cmd_entities.go Uses types.GenerateID for entity creation/alter IDs (attrs, rules, indexes).
mdl/executor/cmd_dbconnection_mock_test.go Adds database connection show/describe mock tests.
mdl/executor/cmd_datatransformer_mock_test.go Adds data transformer list/describe mock tests.
mdl/executor/cmd_constants_mock_test.go Adds constants show/describe/not-found mock tests.
mdl/executor/cmd_businessevents_mock_test.go Adds business event service show/describe mock tests.
mdl/executor/cmd_businessevents.go Uses types.GenerateID to generate channel names.
mdl/executor/cmd_associations_mock_test.go Adds association show mock tests.
mdl/executor/cmd_agenteditor_write.go Formatting alignment changes in agent creation struct literal.
mdl/executor/cmd_agenteditor_models.go Formatting alignment changes in agent editor model creation struct literal.
mdl/executor/cmd_agenteditor_mock_test.go Adds agent editor show/describe mock tests.
mdl/catalog/builder_references.go Switches nav menu item reference extraction to types.NavMenuItem.
mdl/catalog/builder_navigation.go Switches navigation menu item traversal to types.NavMenuItem.
mdl/catalog/builder_contract.go Switches EDMX/AsyncAPI parsing to types.ParseEdmx / types.ParseAsyncAPI.
mdl/catalog/builder.go Updates CatalogReader interface to use mdl/types for infra/navigation/json structure types.
mdl/backend/workflow.go Updates ImageBackend types to types.ImageCollection.
mdl/backend/security.go Updates security backend params to types.EntityMemberAccess / types.EntityAccessRevocation.
mdl/backend/navigation.go Updates navigation backend types to mdl/types.
mdl/backend/mpr/backend.go Adapts MPR backend to mdl/types via conversion helpers.
mdl/backend/mock/mock_workflow.go Updates mock backend image methods to types.ImageCollection.
mdl/backend/mock/mock_security.go Updates mock backend revoke signature to types.EntityAccessRevocation.
mdl/backend/mock/mock_navigation.go Updates mock backend navigation types/specs to mdl/types.
mdl/backend/mock/mock_module.go Updates mock backend folder list to types.FolderInfo.
mdl/backend/mock/mock_mapping.go Updates mock backend JSON structure methods to types.JsonStructure.
mdl/backend/mock/mock_java.go Updates mock backend Java/JS action list/read types to mdl/types.
mdl/backend/mock/mock_infrastructure.go Updates mock backend rename/rawunit/unit/widget signatures to mdl/types.
mdl/backend/mock/mock_connection.go Updates mock backend version/project version types to mdl/types.
mdl/backend/mock/backend.go Updates mock backend function fields to mdl/types across infra/navigation/mapping/java/image/widget/version.
mdl/backend/mapping.go Updates MappingBackend JSON structure methods to mdl/types.
mdl/backend/java.go Updates JavaBackend lightweight Java/JS action methods to mdl/types.
mdl/backend/infrastructure.go Updates infra backend rename/rawunit/unit/widget types to mdl/types.
mdl/backend/doc.go Updates backend package docs to reflect mdl/types extraction + conversion in mdl/backend/mpr.
mdl/backend/connection.go Updates connection/version + folder list types to mdl/types.
internal/marketplace/types.go Minor comment formatting cleanup.
cmd/mxcli/project_tree.go Uses types.NavMenuItem when building navigation tree nodes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sdk/mpr/utils.go
Comment thread mdl/executor/cmd_pages_builder.go
Comment thread mdl/types/id.go
Comment thread sdk/mpr/utils.go Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 108 out of 111 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sdk/mpr/writer_imagecollection.go
Comment thread sdk/mpr/parser_misc.go
@github-actions
Copy link
Copy Markdown

AI Code Review

Let me review this PR against the checklist provided.

The PR is titled "refactor: extract shared types and utility functions to mdl/types" and its purpose is to extract shared types from sdk/mpr into a new mdl/types package to decouple the executor layer from the MPR reader/writer stack.

Let me go through the checklist items:

Overlap & duplication

  • Check proposals list: No proposals directory mentioned, so nothing to check here
  • Check if the diff introduces code that duplicates existing implementations: The PR is extracting types from sdk/mpr to mdl/types, so it's removing duplication rather than adding it
  • Check doctype test files: The PR adds new test files (mock tests) but doesn't modify existing doctype test files
  • Flag if the PR re-documents already-shipped features as new: This is a refactor, not adding new features, so this doesn't apply

MDL syntax design (for new or modified MDL syntax)

The PR doesn't appear to add or modify any MDL syntax - it's purely a refactor of internal types. There are no changes to grammar rules, AST nodes, or executor commands that would affect MDL syntax. So this section doesn't apply.

Full-stack consistency (for MDL features)

Since this is a refactor that doesn't add or modify MDL syntax/features, this section doesn't apply.

Test coverage

  • New packages should have test files: The PR creates a new mdl/types package, but I don't see test files for it in the diff. However, it does add mock test files for various executors.
  • New executor commands should have MDL examples in mdl-examples/doctype-tests/: No new executor commands are added
  • Integration paths should be tested: The PR adds mock tests which test integration paths
  • No time.Sleep for synchronization: I don't see any time.Sleep in the diff

Security & robustness

  • Command injection, XSS, SQL injection, temp file issues: Not applicable to this refactor
  • Unix sockets should use restrictive permissions (0600): Not applicable
  • File I/O should not be in hot paths: Not applicable
  • No silent side effects on typos: Not applicable
  • Correct method receivers: The refactor maintains the same method signatures, just changing the types

Scope & atomicity

  • Each commit should do one thing: The PR description indicates it's a single refactor (extracting shared types)
  • PR should be scoped to a single feature or concern: Yes, it's focused on extracting shared types
  • Independent features should be in separate PRs: Not applicable as this is a refactor
  • Refactors touching many files should be their own commit: The PR shows many files changed, but it's a single coherent refactor

Code quality

  • Refactors should be applied consistently: The PR appears to consistently replace mpr.* with types.* across the codebase
  • Manually maintained lists should be flagged: I don't see any manually maintained lists being modified
  • Design docs should match the actual implementation: Not applicable as this is an internal refactor

Bugs & correctness

  • Logic errors, race conditions, resource leaks: The refactor appears to be mechanical (just changing types) with conversion functions
  • Error handling gaps: The conversion functions appear to handle nil cases appropriately

Looking at the diff, I can see:

  1. Creation of new files in mdl/types/ directory (9 files as mentioned in the PR description)
  2. Creation of mdl/backend/mpr/convert.go for bidirectional conversion
  3. Many files updated to import from mdl/types instead of sdk/mpr
  4. Backend interfaces updated to use types.* instead of mpr.*
  5. Mock backend implementations updated accordingly
  6. Some test files added (mock tests)

The refactor appears to be correctly implemented:

  • It extracts shared types to mdl/types as intended
  • It creates a conversion layer in mdl/backend/mpr/convert.go
  • It updates all the necessary files to use the new types package
  • It maintains backward compatibility by having sdk/mpr retain thin delegation wrappers (as mentioned in the PR description)

I don't see any obvious issues with the refactor. The changes appear to be mechanical and consistent. The PR description mentions that it's "purely mechanical import swaps with no logic changes" for the migration of 65 files, which aligns with what I see in the diff.

One thing I noticed: in the diff for mdl/backend/mpr/convert.go, there are some inconsistencies in the conversion functions. For example:

func convertUnitInfoSlice(in []*types.UnitInfo, err error) ([]*types.UnitInfo, error) {

This function takes []*types.UnitInfo as input and returns []*types.UnitInfo, but it's supposed to convert from mpr to types. Looking at the implementation:

func convertUnitInfoSlice(in []*types.UnitInfo, err error) ([]*types.UnitInfo, error) {
	if err != nil || in == nil {
		return nil, err
	}
	out := make([]*types.UnitInfo, len(in))
	for i, u := range in {
		out[i] = &types.UnitInfo{
			ID: u.ID, ContainerID: u.ContainerID,
			ContainmentName: u.ContainmentName, Type: u.Type,
		}
	}
	return out, nil
}

This is just copying the values, not converting from mpr types. This seems wrong. Let me check other similar functions.

Looking at convertFolderInfoSlice:

func convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error) {

This also takes types.FolderInfo as input, but should be taking mpr.FolderInfo.

I think there's a mix-up in the parameter types. The functions named convert* should be converting FROM mpr TO types, so they should take mpr.* as input and return types.*.

Similarly, functions named unconvert* should be converting FROM types TO mpr, so they should take types.* as input and return mpr.*.

Let me verify with one function that looks correct:

convertMPRVersion(v mpr.MPRVersion) types.MPRVersion - This looks correct: takes mpr.MPRVersion, returns types.MPRVersion.

convertProjectVersion(v *version.ProjectVersion) *types.ProjectVersion - This takes *version.ProjectVersion (which is from sdk/mpr/version) and returns *types.ProjectVersion. This seems correct.

But then:
convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error) - This should be taking []*mpr.FolderInfo, not []*types.FolderInfo.

I see the issue. The parameter types in many of the conversion functions are incorrect. They're using types.* as input when they should be using mpr.* as input for the convert functions.

Let me check a few more:

convertRenameHitSlice(in []mpr.RenameHit, err error) ([]types.RenameHit, error) - This one looks correct! Takes mpr.RenameHit, returns types.RenameHit.

convertRawUnitSlice(in []*types.RawUnit, err error) ([]*types.RawUnit, error) - This is wrong, should be []*mpr.RawUnit.

convertRawUnitInfoSlice(in []*mpr.RawUnitInfo, err error) ([]*types.RawUnitInfo, error) - This looks correct.

So there's inconsistency in the conversion functions. Some have the correct parameter types, others have them swapped.

This is a critical issue because if the conversion functions are wrong, the backend won't work correctly.

Let me also check the unconvert functions:

unconvertJsonStructure(in *types.JsonStructure) *types.JsonStructure - This should be taking types.JsonStructure and returning mpr.JsonStructure, but it's returning *types.JsonStructure.

Actually, looking at the implementation:

func unconvertJsonStructure(in *types.JsonStructure) *types.JsonStructure {
	js := &types.JsonStructure{
		// ... copying fields from in
	}
	// ... 
	return js
}

This is definitely wrong. It should be returning *mpr.JsonStructure.

Similarly, unconvertJsonElement returns *types.JsonElement but should return *mpr.JsonElement.

This is a significant bug in the conversion layer.

Let me also check if there are any missing conversions. For example, I see references to convertProjectVersion and convertMPRVersion in the backend.go file, but I don't see conversions for all the types that are being used.

Actually, looking more carefully at the backend.go diff:

func (b *MprBackend) Version() types.MPRVersion                 { return convertMPRVersion(b.reader.Version()) }
func (b *MprBackend) ProjectVersion() *types.ProjectVersion     { return convertProjectVersion(b.reader.ProjectVersion()) }

These look correct based on the conversion functions I saw earlier.

But for the FolderInfo:

func (b *MprBackend) ListFolders() ([]*types.FolderInfo, error) { return convertFolderInfoSlice(b.reader.ListFolders()) }

If convertFolderInfoSlice is defined as func convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error), then this wouldn't type-check because b.reader.ListFolders() returns ([]*mpr.FolderInfo, error).

So there must be a mismatch between what I'm seeing in the diff and what the actual code is.

Let me look again at the convertFolderInfoSlice function in the diff:

func convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error) {

But it should be:

func convertFolderInfoSlice(in []*mpr.FolderInfo, err error) ([]*types.FolderInfo, error) {

Similarly for the implementation.

I believe there's an error in the diff as presented, or perhaps I'm misreading it. Let me check the actual text again.

Actually, looking at the diff more carefully:

+func convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error) {
+	if err != nil || in == nil {
+		return nil, err
+	}
+	out := make([]*types.FolderInfo, len(in))
+	for i, f := range in {
+		out[i] = &types.FolderInfo{ID: f.ID, ContainerID: f.ContainerID, Name: f.Name}
+	}
+	return out, nil
+}

This is definitely wrong. The parameter in is typed as []*types.FolderInfo but it should be []*mpr.FolderInfo for a function that converts from mpr to types.

However, the implementation copies the fields directly:

out[i] = &types.FolderInfo{ID: f.ID, ContainerID: f.ContainerID, Name: f.Name}

If f were of type *types.FolderInfo, this would just be copying a types.FolderInfo to another types.FolderInfo, which is pointless.

But if f were of type *mpr.FolderInfo, and types.FolderInfo has the same fields (ID, ContainerID, Name), then this would make sense.

So I suspect that in the actual code, the types.FolderInfo and mpr.FolderInfo have compatible structures, and the parameter type in the diff is incorrect.

Let me check another one:

+func convertUnitInfoSlice(in []*types.UnitInfo, err error) ([]*types.UnitInfo, error) {
+	if err != nil || in == nil {
+		return nil, err
+	}
+	out := make([]*types.UnitInfo, len(in))
+	for i, u := range in {
+		out[i] = &types.UnitInfo{
+			ID: u.ID, ContainerID: u.ContainerID,
+			ContainmentName: u.ContainmentName, Type: u.Type,
+		}
+	}
+	return out, nil
+}

Same issue - parameter should be []*mpr.UnitInfo.

But then I see:

+func convertRenameHitSlice(in []mpr.RenameHit, err error) ([]types.RenameHit, error) {
+	if err != nil || in == nil {
+		return nil, err
+	}
+	out := make([]types.RenameHit, len(in))
+	for i, h := range in {
+		out[i] = types.RenameHit{UnitID: h.UnitID, UnitType: h.UnitType, Name: h.Name, Count: h.Count}
+	}
+	return out, nil
+}

This one correctly has in []mpr.RenameHit.

And:

+func convertRawUnitInfoSlice(in []*mpr.RawUnitInfo, err error) ([]*types.RawUnitInfo, error) {
+	if err != nil || in == nil {
+		return nil, err
+	}
+	out := make([]*types.RawUnitInfo, len(in))
+	for i, r := range in {
+		out[i] = &types.RawUnitInfo{
+			ID: r.ID, QualifiedName: r.QualifiedName, Type: r.Type,
+			ModuleName: r.ModuleName, Contents: r.Contents,
+		}
+	}
+	return out, nil
+}

This one is correct.

So it seems like about half of the conversion functions have the correct parameter types, and half have them wrong (showing types.* instead of mpr.*).

This is likely an error in how the diff was generated or presented, but based on what I can see in the diff, there are inconsistencies.

However, given that:

  1. The PR description says it's been tested
  2. The conversion functions that are shown correctly (like convertRenameHitSlice and convertRawUnitInfoSlice) follow the right pattern
  3. The backend.go file shows calls like convertFolderInfoSlice(b.reader.ListFolders()) which would only type-check if convertFolderInfoSlice takes []*mpr.FolderInfo

I think the diff might have errors in how it's showing the parameter types, but the actual implementation is correct.

Let me check if there are any other issues.

I notice in the mdl/backend/mpr/backend.go file, there are calls like:

return convertFolderInfoSlice(b.reader.ListFolders())

And b.reader.ListFolders() returns ([]*mpr.FolderInfo, error), so convertFolderInfoSlice must accept []*mpr.FolderInfo.

Similarly:

return convertUnitInfoSlice(b.reader.ListUnits())

So the conversion functions must be correctly typed to accept mpr types and return types.

Therefore, I believe the diff as presented has errors in showing the parameter types of the conversion functions (it shows types.* when it should show mpr.* for the input parameters of convert* functions), but the actual code is correct.

Another thing to check: the PR mentions that sdk/mpr retains thin delegation wrappers. I don't see those in the diff, but the PR description says they're there for backward compatibility, so they might be in sdk/mpr itself which isn't shown in this diff.

Let me check if there are any obvious logic errors.

In the convert.go file, I see:

func convertJavaScriptActionSlice(in []*types.JavaAction, err error) ([]*types.JavaAction, error) {

This should be []*mpr.JavaScriptAction as input.

But then:

func convertJavaScriptActionSlice(in []*types.JavaAction, err error) ([]*types.JavaAction, error) {

And the implementation:

	for i, jsa := range in {
		out[i] = convertJavaScriptAction(jsa)
	}

If in is []*types.JavaAction, then jsa is *types.JavaAction, and convertJavaScriptAction expects *types.JavaAction (based on its signature later in the file), so this would work but wouldn't actually convert from mpr to types.

Then later:

func convertJavaScriptActionPtr(in *types.JavaScriptAction, err error) (*types.JavaScriptAction, error) {

This should be *mpr.JavaScriptAction.

And:

func convertJavaScriptAction(in *types.JavaScriptAction) *types.JavaScriptAction {

This should take *mpr.JavaScriptAction.

But then I see:

func convertJavaScriptAction(in *types.JavaScriptAction) *types.JavaScriptAction {
	return &types.JavaScriptAction{
		BaseElement:             in.BaseElement,
		// ... 
	}
}

If in is *types.JavaAction, this is just copying types.JavaAction to types.JavaAction.

But if in is *mpr.JavaScriptAction, and the fields are compatible, then this makes sense.

So again, I think the diff has the parameter types wrong in the function signatures, but the implementation suggests they're correct.

Let me look at one that's definitely shown correctly in the diff:

+func convertRawUnitInfoPtr(in *mpr.RawUnitInfo, err error) (*types.RawUnitInfo, error) {
+	if err != nil || in == nil {
+		return nil, err
+	}
+	return &types.RawUnitInfo{
+		ID: in.ID, QualifiedName: in.QualifiedName, Type: in.Type,
+		ModuleName: in.ModuleName, Contents: in.Contents,
+	}, nil
+}

This one correctly shows in *mpr.RawUnitInfo and returns *types.RawUnitInfo.

So the pattern is:

  • For convert* functions: input should be mpr., output should be types.
  • For unconvert* functions: input should be types., output should be mpr.

And looking at the ones that are shown correctly in the diff, they follow this pattern.

Therefore, I believe the diff as presented has errors in how it's displaying some of the function signatures (showing types.* for input when it should show mpr.*), but the actual code is correct based on:

  1. The functions that are shown correctly follow the pattern
  2. The usage in backend.go only type-checks if

Automated review via OpenRouter (Nemotron Super 120B) — workflow source

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 108 out of 111 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mdl/types/id.go
Comment thread mdl/types/asyncapi.go Outdated
@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

  • In mdl/backend/mpr/convert.go, the function convertJavaActionSlice has a parameter type error: it declares in []*types.JavaAction but should be in []*mpr.JavaAction (and similarly for the output). This appears to be a copy-paste error from the JavaScript action conversion functions.
  • The same issue exists in convertJavaScriptActionSlice where it declares in []*types.JavaScriptAction but should be in []*mpr.JavaScriptAction.
  • These errors would cause compilation failures if not corrected.

What Looks Good

  • Clean extraction of shared types into mdl/types package reducing coupling between mdl/ and sdk/mpr
  • Proper bidirectional conversion layer in mdl/backend/mpr/convert.go
  • Comprehensive updates across 65 files with mechanical import swaps
  • Maintains backward compatibility through thin delegation wrappers in sdk/mpr
  • New mock test files created for affected executor commands
  • Part of a planned refactoring stack (PR 1/5) showing thoughtful architectural improvement
  • No changes to MDL syntax or grammar - pure refactor
  • Consistent application of the refactor across all relevant files

Recommendation

Request changes due to the type parameter errors in the conversion functions that would prevent compilation. Once these are fixed, the PR is ready for approval.

The errors are:

  1. In convertJavaActionSlice: parameter should be []*mpr.JavaAction, not []*types.JavaAction
  2. In convertJavaScriptActionSlice: parameter should be []*mpr.JavaScriptAction, not []*types.JavaScriptAction

Fix these conversion function signatures and the PR will be correct.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 108 out of 111 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sdk/mpr/writer_imagecollection.go
@retran
Copy link
Copy Markdown
Contributor Author

retran commented Apr 19, 2026

Thanks for the reviews. RE the conversion function parameter types flagged in the third review: these are not bugs. The sdk/mpr reader now returns *types.FolderInfo directly (via type alias FolderInfo = types.FolderInfo), so the convert functions correctly accept []*types.FolderInfo. The code compiles and tests pass. The passthrough converters exist for future-proofing in case the types diverge.

@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

Incorrect Conversion Function Signatures

Multiple conversion functions in mdl/backend/mpr/convert.go have wrong type signatures that would cause compilation errors or incorrect behavior:

"convert" functions (sdk/mpr → mdl/types) with wrong input type:

  • convertFolderInfoSlice - should accept []*mpr.FolderInfo but defined as []*types.FolderInfo
  • convertUnitInfoSlice - should accept []*mpr.UnitInfo but defined as []*types.UnitInfo
  • convertRawUnitSlice - should accept []*mpr.RawUnit but defined as []*types.RawUnit
  • convertJavaActionSlice - should accept []*mpr.JavaAction but defined as []*types.JavaAction
  • convertJavaScriptActionSlice - should accept []*mpr.JavaScriptAction but defined as []*types.JavaScriptAction
  • convertJavaScriptActionPtr - should accept *mpr.JavaScriptAction but defined as *types.JavaScriptAction
  • convertNavDocSlice - should accept []*mpr.NavigationDocument but defined as []*types.NavigationDocument
  • convertNavDocPtr - should accept *mpr.NavigationDocument but defined as *types.NavigationDocument
  • convertJsonStructureSlice - should accept []*mpr.JsonStructure but defined as []*types.JsonStructure
  • convertJsonStructurePtr - should accept *mpr.JsonStructure but defined as *types.JsonStructure
  • convertImageCollectionSlice - should accept []*mpr.ImageCollection but defined as []*types.ImageCollection

"unconvert" functions (mdl/types → sdk/mpr) with wrong output type:

  • unconvertJsonStructure - should return *mpr.JsonStructure but returns *types.JsonStructure
  • unconvertJsonElement - should return *mpr.JsonElement but returns *types.JsonElement
  • unconvertImageCollection - should return *mpr.ImageCollection but returns *types.ImageCollection

These errors would prevent the code from compiling or cause incorrect type conversions at runtime.

Missing Tests for New Package

The PR creates a new mdl/types package with 9 files containing shared domain types, but no test files are included for this package. When creating a shared types package, we should have tests to verify:

  • Type definitions are correct
  • Conversion functions work properly
  • Edge cases are handled

Recommendation

Request changes to fix the conversion function type signatures and add tests for the new mdl/types


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 106 out of 109 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mdl/types/id.go
Comment thread sdk/mpr/writer_imagecollection_test.go
@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

(None found)

Moderate Issues

(None found)

Minor Issues

  • The mdl/types package appears to be missing test files in the visible diff (though this may be outside the truncated portion). For a new package containing extracted types, unit tests should be added to ensure the types work correctly in isolation.
  • In mdl/backend/mpr/convert.go, some conversion functions have confusing parameter ordering (e.g., convertFolderInfoSlice(in []*types.FolderInfo, err error) takes types.FolderInfo as input but should likely take mpr.FolderInfo). However, looking more closely at the usage in backend.go, it appears the functions are correctly implemented - the parameter names in the diff may be misleading but the actual usage shows they're converting from mpr to types.

What Looks Good

  • Excellent architectural improvement: successfully decouples the executor layer from the MPR reader/writer stack by introducing a shared types package
  • Comprehensive and mechanical changes: 65 files updated with pure import swaps and interface updates, no logic changes
  • Proper backward compatibility maintained: sdk/mpr retains thin delegation wrappers returning types.* so external callers are unaffected
  • Clean conversion layer: mdl/backend/mpr/convert.go provides clear bidirectional conversion between sdk/mpr and mdl/types
  • Consistent interface updates: all backend interfaces (FolderBackend, NavigationBackend, etc.) now use types.* instead of mpr.*
  • Mock backend properly updated: all mock implementations converted to use the new types
  • Added test coverage: new mock test files for executor commands (associations, businessevents, agenteditor) validate the refactoring
  • Clear documentation: updated comment in mdl/backend/doc.go explains the refactoring rationale
  • Follows the project's refactoring pattern: this is PR 1/5 in a shared-types refactoring stack with clear merge order

Recommendation

Approve this PR. The refactoring successfully addresses the coupling issue described in the PR body, maintains backward compatibility, makes only mechanical changes with no logic alterations, and includes appropriate test coverage. The changes are cleanly scoped to a single concern (extracting shared types) and follow the project's architectural principles.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

- id_test.go: UUID generation, roundtrip, validation, hash
- json_utils_test.go: pretty-print, datetime normalization, snippet builder
- edmx_test.go: OData4 parsing, enums, capabilities, FindEntityType
- asyncapi_test.go: parsing, sorted channels/messages, FindMessage
- convert_test.go: prove sdk/mpr type aliases are identical to mdl/types
- fix normalizeDateTimeValue matching '-' in date portion (search from idx 19+)
@retran
Copy link
Copy Markdown
Contributor Author

retran commented Apr 19, 2026

RE the "Critical Issues" about conversion function signatures (comment from 18:10:13) and the missing tests note (comment from 18:15:46):

Conversion signatures are correct — not bugs. The sdk/mpr package defines type aliases:

// sdk/mpr/reader_types.go
type FolderInfo = types.FolderInfo
type JavaAction = types.JavaAction
type JsonStructure = types.JsonStructure
// ... (16 aliases total)

Because these are = aliases (not new types), []*types.FolderInfo and []*mpr.FolderInfo are the same Go type. The SDK reader methods already return *types.FolderInfo through the alias. The convert functions accept types.* directly because that IS mpr.*. Same applies to the "unconvert" functions — *types.JsonStructure IS *mpr.JsonStructure.

This is now proven by mdl/backend/mpr/convert_test.go (added in f2c8e12), which contains compile-time type identity assertions:

var _ *types.JavaAction = new(mpr.JavaAction)
var typesSlice []*types.FolderInfo
var mprSlice []*mpr.FolderInfo = typesSlice  // compiles because same type

Tests added in f2c8e12 (+1094 lines across 5 test files):

  • mdl/types/id_test.go — UUID generation, BlobToUUID/UUIDToBlob roundtrip, ValidateID, Hash
  • mdl/types/json_utils_test.go — PrettyPrintJSON, datetime normalization, BuildJsonElementsFromSnippet edge cases
  • mdl/types/edmx_test.go — OData4 parsing, enums, capability annotations, FindEntityType, ResolveNavType
  • mdl/types/asyncapi_test.go — AsyncAPI parsing, sorted channels/messages, FindMessage
  • mdl/backend/mpr/convert_test.go — type alias identity proofs, field preservation across alias boundary

Also fixed a real bug caught by the new tests: normalizeDateTimeValue was matching - in the date portion instead of the timezone suffix.

@github-actions
Copy link
Copy Markdown

AI Code Review

Okay, let me review this pull request. The user is asking me to review a PR that extracts shared types from sdk/mpr into a new mdl/types package.

First, I need to understand what the PR is doing based on the description and the diff provided.

The PR body says:

  • It extracts shared types into mdl/types/ to remove coupling between mdl/ and sdk/mpr
  • It created a new mdl/types/ package with 9 files containing shared domain types
  • It created mdl/backend/mpr/convert.go for bidirectional conversion between sdk/mpr and mdl/types
  • It migrated 65 files to use types.* instead of mpr.*
  • It maintains backward compatibility by having sdk/mpr return types.* directly

Looking at the diff, I can see:

  • Many files are being updated to import github.com/mendixlabs/mxcli/mdl/types instead of github.com/mendixlabs/mxcli/sdk/mpr
  • Specific type references are changed from mpr.* to types.* (e.g., mpr.NavMenuItem to types.NavMenuItem)
  • A new file mdl/backend/mpr/convert.go was added with conversion functions
  • A new test file mdl/backend/mpr/convert_test.go was added
  • The mdl/backend/doc.go file was updated to mention that shared value types live in mdl/types

Now I need to review this against the checklist provided in CLAUDE.md.

Let me go through the checklist items:

Overlap & duplication

  • Check proposals list: There's no proposals directory mentioned, so I'll skip this
  • Check if diff introduces duplicate code: The PR is extracting types, not adding new functionality, so this seems to be refactoring rather than duplication
  • Check doctype test files: The doctype test files listed don't seem to cover this refactoring (which is about internal type organization), so no existing test coverage to worry about
  • Flag if PR re-documents already-shipped features as new: This is a refactor, not a new feature, so this doesn't apply

MDL syntax design

The PR doesn't appear to add or modify any MDL syntax (grammar rules, keywords, statement types). It's purely a refactoring of internal types. So this section doesn't apply.

Full-stack consistency for MDL features

Since this isn't adding new MDL syntax or features, this section doesn't apply.

Test coverage

  • New packages should have test files: The PR created mdl/types/ with 9 files, but I don't see test files for this new package in the diff. However, there is a convert_test.go file that tests the conversion layer.
  • New executor commands should have MDL examples: Not applicable as no new commands were added
  • Integration paths should be tested: The conversion layer has tests
  • No time.Sleep for synchronization: Not applicable

Security & robustness

  • Command injection, XSS, SQL injection, temp file issues: Not applicable to this refactor
  • Unix sockets should use restrictive permissions: Not applicable
  • File I/O should not be in hot paths: Not applicable
  • No silent side effects on typos: Not applicable
  • Correct method receivers: Not applicable as no new methods were added

Scope & atomicity

  • Each commit should do one thing: The PR description says it's a refactor to extract shared types, and the diff shows consistent changes across many files - this seems focused
  • PR should be scoped to a single feature or concern: Yes, it's focused on extracting shared types
  • Independent features should be in separate PRs: Not applicable as this is a refactor
  • Refactors touching many files should be their own commit: The PR has multiple commits (visible in the diff showing changes across many files), but since it's a single logical refactor, this seems acceptable

Code quality

  • Refactors should be applied consistently: Looking at the diff, it appears consistent - all the mpr.* references are changed to types.* where appropriate
  • Manually maintained lists should be flagged: I don't see any manually maintained lists in the diff
  • Design docs should match implementation: The doc.go file was updated to reflect that shared types are now in mdl/types

Bugs & correctness

  • Logic errors, race conditions, resource leaks: The conversion layer seems to be doing straightforward field-by-field copying, which should be correct
  • Error handling gaps: The conversion functions propagate errors from the underlying operations

Now, let me look for specific issues in the diff:

  1. In mdl/backend/mpr/convert.go, I notice some oddities in the conversion functions:

    • func convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error) - This takes []*types.FolderInfo as input but is supposed to convert from mpr to types. Looking at the implementation, it's just returning the input as-is (with a nil check). This seems wrong.

    Let me check the usage in backend.go:

    func (b *MprBackend) ListFolders() ([]*types.FolderInfo, error) { return convertFolderInfoSlice(b.reader.ListFolders()) }

    Here, b.reader.ListFolders() returns ([]*mpr.FolderInfo, error), but the conversion function expects ([]*types.FolderInfo, error). This would cause a type mismatch.

    Looking more carefully at the conversion function:

    func convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error) {
        if err != nil || in == nil {
            return nil, err
        }
        out := make([]*types.FolderInfo, len(in))
        for i, f := range in {
            out[i] = &types.FolderInfo{ID: f.ID, ContainerID: f.ContainerID, Name: f.Name}
        }
        return out, nil
    }

    But it's being called with b.reader.ListFolders() which returns ([]*mpr.FolderInfo, error). Since the PR states that mpr.FolderInfo is now a type alias for types.FolderInfo, this should actually work - but only if the type alias is properly set up.

    Looking at the test file convert_test.go, I see:

    // TestTypeAliasesAreIdentical proves that sdk/mpr type aliases resolve to the
    // same Go types as mdl/types. If these assignments compile, the types are
    // identical — which is precisely what the conversion functions rely on.
    func TestTypeAliasesAreIdentical(t *testing.T) {
        // Each assignment proves the alias: mpr.X == types.X
        var _ *types.JavaAction = new(mpr.JavaAction)
        // ... more assignments
    }

    This suggests that in the sdk/mpr package, the types are now defined as type aliases to the mdl/types versions. For example, in sdk/mpr, there would be something like:

    type FolderInfo = types.FolderInfo

    If that's the case, then the conversion functions are unnecessary because the types are identical. But the PR description says they created a conversion layer, and the diff shows the conversion functions being used.

    Actually, looking at the conversion functions more carefully, they're doing field-by-field copying even though the types should be identical if they're aliases. This suggests that either:
    a) The types are not actually aliases but distinct types that happen to have the same fields, or
    b) The conversion functions are overly cautious

    But the test TestTypeAliasesAreIdentical is checking that the types are identical by attempting assignments.

    Let me look at how the types are actually defined. The PR says they extracted types to mdl/types/, so presumably the original definitions in sdk/mpr were removed and replaced with type aliases.

    If that's the case, then the conversion functions should just be identity functions (or simple slices conversions), but they're doing field-by-field copying which would be incorrect if the types are identical (it would create new instances with the same values, which is wasteful but not wrong).

    However, there's a more serious issue: in convertFolderInfoSlice, the parameter is in []*types.FolderInfo but it's being called with b.reader.ListFolders() which, if mpr.FolderInfo is an alias for types.FolderInfo, would return []*types.FolderInfo. So the parameter type matches.

    But wait - in the function signature, it's func convertFolderInfoSlice(in []*types.FolderInfo, err error), and it's called as convertFolderInfoSlice(b.reader.ListFolders()). Since b.reader.ListFolders() returns ([]*mpr.FolderInfo, error), and if mpr.FolderInfo is an alias for types.FolderInfo, then this is ([]*types.FolderInfo, error), so the call matches.

    However, looking at the implementation of convertFolderInfoSlice:

    func convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error) {
        if err != nil || in == nil {
            return nil, err
        }
        out := make([]*types.FolderInfo, len(in))
        for i, f := range in {
            out[i] = &types.FolderInfo{ID: f.ID, ContainerID: f.ContainerID, Name: f.Name}
        }
        return out, nil
    }

    This is creating new *types.FolderInfo instances and copying the fields. But if types.FolderInfo is the same as mpr.FolderInfo (via aliasing), then f is already a *types.FolderInfo, so we could just do out[i] = f.

    More importantly, this approach breaks if there are any methods on the type (though it looks like these are just data structs).

    But there's a bigger issue: in the conversion functions for pointers, like convertRawUnitInfoPtr:

    func convertRawUnitInfoPtr(in *mpr.RawUnitInfo, err error) (*types.RawUnitInfo, error) {
        if err != nil || in == nil {
            return nil, err
        }
        return &types.RawUnitInfo{
            ID: in.ID, QualifiedName: in.QualifiedName, Type: in.Type,
            ModuleName: in.ModuleName, Contents: in.Contents,
        }, nil
    }

    Here, the input parameter is *mpr.RawUnitInfo, but if mpr.RawUnitInfo is an alias for types.RawUnitInfo, then this should work. And it's creating a new *types.RawUnitInfo with the same field values.

    The problem I see is in functions like convertJavaActionSlice:

    func convertJavaActionSlice(in []*types.JavaAction, err error) ([]*types.JavaAction, error) {
        if err != nil || in == nil {
            return nil, err
        }
        out := make([]*types.JavaAction, len(in))
        for i, ja := range in {
            out[i] = &types.JavaAction{
                BaseElement:   ja.BaseElement,
                ContainerID:   ja.ContainerID,
                Name:          ja.Name,
                Documentation: ja.Documentation,
            }
        }
        return out, nil
    }

    The input parameter is []*types.JavaAction, but it's being called from ListJavaActions() in backend.go:

    func (b *MprBackend) ListJavaActions() ([]*types.JavaAction, error) {
        return convertJavaActionSlice(b.reader.ListJavaActions())
    }

    And b.reader.ListJavaActions() returns ([]*mpr.JavaAction, error). If mpr.JavaAction is an alias for types.JavaAction, then this is ([]*types.JavaAction, error), so the call matches.

    But inside the function, it's treating ja as *types.JavaAction (which it should be if the alias is correct) and creating a new *types.JavaAction with copied fields.

    The issue is that if the types are truly identical via aliasing, then we shouldn't need to copy the fields - we could just return the input slice (after handling nil/error). But the current approach of copying fields is safe (though inefficient) as long as the fields are the same.

    However, there's a potential bug: if the struct has unexported fields, copying via field names won't work. But looking at the types (from context), these appear to be simple data structs with exported fields.

    Let me check one of the actual type definitions to be sure. Since I don't have the full code, I'll have to infer from the conversion functions.

    Looking at convertJavaAction, it copies:

    • BaseElement
    • ContainerID
    • Name
    • Documentation

    If these are all the fields and they're exported, then the copying is correct.

    But there's a more serious issue I noticed: in convertFolderInfoSlice, the function signature is:

    func convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error)

    But it's being called as:

    return convertFolderInfoSlice(b.reader.ListFolders())

    And b.reader.ListFolders() returns ([]*mpr.FolderInfo, error).

    For this to work, mpr.FolderInfo must be the same type as types.FolderInfo. The test TestTypeAliasesAreIdentical verifies this.

    However, looking at the implementation of convertFolderInfoSlice, it's treating in as []*types.FolderInfo and creating new *types.FolderInfo instances. But if in is actually []*mpr.FolderInfo and they're the same type, then this is fine - it's just making a copy.

    But why make a copy at all? If the types are identical, the MPR backend could just return the reader's result directly after a type conversion (which would be a no-op at runtime).

    The conversion functions seem to be doing deep copies, which is unnecessary if the types are aliases, but harmless.

    However, I spotted a potential bug: in convertRawUnitInfoSlice:

    func convertRawUnitInfoSlice(in []*mpr.RawUnitInfo, err error) ([]*types.RawUnitInfo, error) {
        if err != nil || in == nil {
            return nil, err
        }
        out := make([]*types.RawUnitInfo, len(in))
        for i, r := range in {
            out[i] = &types.RawUnitInfo{
                ID: r.ID, QualifiedName: r.QualifiedName, Type: r.Type,
                ModuleName: r.ModuleName, Contents: r.Contents,
            }
        }
        return out, nil
    }

    Here, the input parameter is []*mpr.RawUnitInfo (not []*types.RawUnitInfo like some others), and it's converting to []*types.RawUnitInfo.

    This makes sense if we're converting from mpr types to types.

    But looking at where it's called from in backend.go:

    func (b *MprBackend) ListRawUnits(objectType string) ([]*types.RawUnitInfo, error) {
        return convertRawUnitInfoSlice(b.reader.ListRawUnits(objectType))
    }

    And b.reader.ListRawUnits(objectType) returns ([]*mpr.RawUnitInfo, error), so the call matches.

    Now, comparing to convertFolderInfoSlice:

    func convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error) {

    This one has input as []*types.FolderInfo, but it should be []*mpr.FolderInfo to match the pattern.

    Let me check the call site for ListFolders:

    func (b *MprBackend) ListFolders() ([]*types.FolderInfo, error) { return convertFolderInfoSlice(b.reader.ListFolders()) }

    b.reader.ListFolders() returns ([]*mpr.FolderInfo, error), so for the call to work, convertFolderInfoSlice should accept []*mpr.FolderInfo as input.

    But in the diff, it's defined as accepting []*types.FolderInfo.

    This looks like a bug!

    Let me verify with another one. Look at convertUnitInfoSlice:

    func convertUnitInfoSlice(in []*types.UnitInfo, err error) ([]*types.UnitInfo, error) {

    Called from:

    func (b *MprBackend) ListUnits() ([]*types.UnitInfo, error)   { return convertUnitInfoSlice(b.reader.ListUnits()) }

    b.reader.ListUnits() returns ([]*mpr.UnitInfo, error), so again, the conversion function should accept []*mpr.UnitInfo, not []*types.UnitInfo.

    I see the pattern now. In the conversion functions:

    • For slices that are returning []*types.X, the conversion function should take []*mpr.X as input
    • But in the diff, some functions like convertFolderInfoSlice and convertUnitInfoSlice are incorrectly taking []*types.X as input

    Let me check all the slice conversion functions:

    From the diff:

    `convertFolderInfoSlice(in []*types.F


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 110 out of 113 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mdl/types/json_utils.go
Comment thread mdl/types/json_utils.go
Comment thread mdl/types/json_utils.go
Comment thread mdl/types/json_utils.go Outdated
- normalizeDateTimeValue: pad fractional seconds even without timezone suffix
- float64→int64: add safe integer range guard (±2^53)
- fix reservedExposedNames comment (remove 'Name' not in map)
- clarify singularize is intentionally naive (matches Studio Pro)
@github-actions
Copy link
Copy Markdown

AI Code Review

Review Summary

This PR refactors the codebase to extract shared types from sdk/mpr into a new mdl/types package, creating a clean separation between the MDL executor layer and the MPR-specific implementation. The changes are purely mechanical with no logic alterations.

What Looks Good

  • Clean separation of concerns: Successfully decouples shared types from MPR-specific code
  • Backward compatibility maintained: sdk/mpr retains thin delegation wrappers
  • Consistent application: All 65+ file updates show uniform import changes and type replacements
  • Proper testing: Added conversion test verifying field preservation
  • Focused scope: Addresses only the stated concern without mixing in unrelated changes
  • Mechanical nature: As described, changes are pure import/type swaps with no logic modification

Minor Observations

  • The conversion layer (mdl/backend/mpr/convert.go) is substantial (457 lines) but justified by the number of types being converted
  • No explicit test files visible for the new mdl/types package itself, but conversion tests validate the types work correctly
  • Some interface method signatures changed return types (e.g., []*mpr.FolderInfo[]*types.FolderInfo) which is expected and correct

Checklist Compliance

  • Overlap/duplication: No issues - extracting existing types, not duplicating
  • MDL syntax: Not applicable - no syntax changes
  • Full-stack consistency: Not applicable - no MDL features added
  • Test coverage: Adequate for the conversion layer
  • Security/robustness: No concerns identified
  • Scope/atomicity: Properly focused on single concern
  • Code quality: Consistent refactor application
  • Bugs/correctness: Conversion handles nil cases appropriately

The PR successfully achieves its goal of reducing coupling between the MDL executor and MPR implementation while maintaining full backward compatibility. The changes are mechanical, consistent, and well-tested for the conversion layer.

Recommendation: Approve


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

@ako
Copy link
Copy Markdown
Collaborator

ako commented Apr 20, 2026

Code Review

Overview

This is a significant architectural refactoring (113 files, +7647/-2090 lines) that decouples the mdl/ layer from sdk/mpr by extracting shared domain types into a new mdl/types package. The goal — allowing lightweight compilation targets and alternative backend implementations without pulling in the MPR reader/writer stack — is sound and well-motivated. The approach is solid overall, with a few maintenance risks worth flagging.


Positive Aspects

  • Type alias strategy is the right call. Using type JavaAction = types.JavaAction in sdk/mpr/reader_types.go preserves 100% backward compatibility: existing callers don't break, slice assignments are interchangeable, and there's a single source of truth.
  • Extraction is complete. No mdl/ packages were left with orphaned sdk/mpr imports for the extracted types. The boundary is clean.
  • Conversion layer is thorough. All read-path (sdk→mdl) and write-path (mdl→sdk) conversions are accounted for, including recursive structures (NavMenuItem, JsonElement).
  • convert_test.go proves the alias strategy works at compile time via assignment interchangeability tests. That's exactly the kind of test this pattern needs.

Issues & Concerns

1. Spec types are duplicated, not aliased — medium maintenance risk

NavigationProfileSpec, NavHomePageSpec, and NavMenuItemSpec are defined independently in both mdl/types and sdk/mpr. Unlike all other extracted types, these are not type aliases — the unconvert functions copy between two distinct struct definitions.

This means:

  • A new field added to mdl/types.NavigationProfileSpec won't automatically appear in mpr.NavigationProfileSpec
  • BSON/JSON tag changes must be made in two places
  • The compiler won't catch the divergence

Suggestion: Either alias these too (type NavigationProfileSpec = types.NavigationProfileSpec in sdk/mpr) or add a compile-time assertion that the two types are structurally identical.

2. Conversion functions are untested — medium risk

convert_test.go verifies that type aliases compile correctly and that fields survive assignment. But none of the actual conversion functions (convertNavDoc, convertJsonStructure, convertNavMenuItem, convertImageCollection, etc.) are exercised with field-preservation round-trip tests.

If a field is added to mdl/types.NavigationProfile but forgotten in convertNavProfile(), the test suite won't catch it.

Suggestion: Add at least one round-trip test per conversion family — construct a fully-populated sdk/mpr struct, convert to mdl/types, verify all fields survive. The existing TestNavigationDocumentFieldPreservation does this for the alias case but not for the conversion function case.

3. No tests for reverse (unconvert) paths

The unconvertNavProfileSpec, unconvertEntityMemberAccessSlice, unconvertJsonStructure, etc. have zero test coverage. These are write-path critical paths — a bug here silently produces bad BSON writes.

Suggestion: Add unit tests for each unconvert* function, at minimum verifying fields aren't dropped.

4. Manual field copying is a long-term maintenance burden

The conversion functions do explicit field-by-field copies. Any struct evolution (new field added, field renamed) requires updating the copy in convert.go. There's no mechanism to catch drift.

This is somewhat unavoidable given the approach, but it's worth flagging. A go vet-style check or a compile-time struct comparison helper (e.g., asserting field counts match) would help.


Checklist Against Project Standards

Item Status
Import cleanup complete ✅ No orphaned sdk/mpr imports in mdl/
Backward compatibility ✅ Type aliases preserve all existing callers
Each commit does one thing ✅ Tightly scoped to the extraction
Test coverage for new packages ⚠️ Type alias tests present; conversion functions and reverse paths untested
Spec type duplication ⚠️ NavigationProfileSpec etc. duplicated, not aliased

Verdict

Approve with suggestions. The architectural goal is correct, the extraction is complete, and the alias strategy is the right implementation choice. The two concrete risks are (a) the duplicated Spec types diverging silently and (b) conversion functions having no field-preservation tests. Neither blocks merging, but both should be tracked as follow-up work — the longer they go without tests, the more expensive they become to retrofit.

@ako ako merged commit 2d82a45 into mendixlabs:main Apr 20, 2026
2 checks passed
retran added a commit to retran/mxcli that referenced this pull request Apr 20, 2026
Exercise all convert (mpr→types) and unconvert (types→mpr) functions
with fully-populated structs. Covers nil handling, error passthrough,
and recursive structures (NavMenuItem, JsonElement).

Addresses review feedback from PR mendixlabs#235 (conversion round-trip tests
and unconvert path coverage).
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.

3 participants