suma 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2e8f69b5a73e7e808b0dceb8443829f1ee07370f391e29144187eac0870b3ba
4
- data.tar.gz: 912fa2f86f74512f95fb25dbf082de1d65bb65848e832cab66444e1e4e698956
3
+ metadata.gz: 9fda8e972931c092b7c01d4eaafda7ada9d0f29739d584123b527bf2b5a75075
4
+ data.tar.gz: c9efe1f70b118180a770f843a6858b3d73e435349a6148e857516fe603dcb688
5
5
  SHA512:
6
- metadata.gz: f9d9d3a67a2425b3b96d0a8ceddfa964e1d2a7a391fb626201f4ca1be98c23a96eb622d10ee5b7073c2d80be65f9d9a88b9d75996231c02da5035593aa4e7860
7
- data.tar.gz: 79435485f1e2c1389a8054153e1e6abfb145f098038937c3f79d3d7a55e9b6b9ead89b78f6a2f74f462d8d71e7f726ed455e20ffd712357b58455818691eacc1
6
+ metadata.gz: ce79c333f25940d8a0a201a3ff65d40c68d816581c9cfadecf3900684ff2ca043597203e6d5a2fa728dfcdd0bfe5415ce80923ed385273dc0df0518f3f183631
7
+ data.tar.gz: aa335cf0c8669cd27d5d3b7c715c23d16278337b42db554d9971e0fd1b9a3e9d89e056756e1cc5c08d555635059f83a3688691f05763dcc8a1f267425a8f3ddb
data/.rubocop.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  inherit_from:
2
- - .rubocop_todo.yml
3
2
  - https://raw.githubusercontent.com/riboseinc/oss-guides/main/ci/rubocop.yml
3
+ - .rubocop_todo.yml
4
4
 
5
5
  plugins:
6
6
  - rubocop-performance
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2025-10-12 09:15:38 UTC using RuboCop version 1.81.1.
3
+ # on 2025-10-15 10:00:06 UTC using RuboCop version 1.81.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -18,55 +18,12 @@ Gemspec/RequiredRubyVersion:
18
18
  Exclude:
19
19
  - 'suma.gemspec'
20
20
 
21
- # Offense count: 2
22
- # This cop supports safe autocorrection (--autocorrect).
23
- # Configuration parameters: EnforcedStyle, IndentationWidth.
24
- # SupportedStyles: with_first_argument, with_fixed_indentation
25
- Layout/ArgumentAlignment:
26
- Exclude:
27
- - 'spec/suma/cli/export_spec.rb'
28
-
29
- # Offense count: 1
30
- # This cop supports safe autocorrection (--autocorrect).
31
- # Configuration parameters: EnforcedStyleAlignWith.
32
- # SupportedStylesAlignWith: either, start_of_block, start_of_line
33
- Layout/BlockAlignment:
34
- Exclude:
35
- - 'spec/suma/cli/export_spec.rb'
36
-
37
- # Offense count: 1
38
- # This cop supports safe autocorrection (--autocorrect).
39
- Layout/BlockEndNewline:
40
- Exclude:
41
- - 'spec/suma/cli/export_spec.rb'
42
-
43
- # Offense count: 2
44
- # This cop supports safe autocorrection (--autocorrect).
45
- # Configuration parameters: Width, AllowedPatterns.
46
- Layout/IndentationWidth:
47
- Exclude:
48
- - 'spec/suma/cli/export_spec.rb'
49
-
50
- # Offense count: 74
21
+ # Offense count: 84
51
22
  # This cop supports safe autocorrection (--autocorrect).
52
23
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
53
24
  # URISchemes: http, https
54
25
  Layout/LineLength:
55
- Exclude:
56
- - 'lib/suma/cli/build.rb'
57
- - 'lib/suma/cli/validate.rb'
58
- - 'lib/suma/collection_manifest.rb'
59
- - 'lib/suma/jsdai/figure.rb'
60
- - 'lib/suma/jsdai/figure_image.rb'
61
- - 'lib/suma/processor.rb'
62
- - 'lib/suma/schema_attachment.rb'
63
- - 'lib/suma/schema_document.rb'
64
- - 'lib/suma/thor_ext.rb'
65
- - 'spec/suma/cli/export_spec.rb'
66
- - 'spec/suma/cli/extract_terms_spec.rb'
67
- - 'spec/suma/cli/validate_ascii_spec.rb'
68
- - 'spec/suma/jsdai/figure_spec.rb'
69
- - 'suma.gemspec'
26
+ Enabled: false
70
27
 
71
28
  # Offense count: 2
72
29
  # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
@@ -84,30 +41,48 @@ Lint/IneffectiveAccessModifier:
84
41
  Exclude:
85
42
  - 'lib/suma/cli/export.rb'
86
43
 
87
- # Offense count: 9
44
+ # Offense count: 11
88
45
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
89
46
  Metrics/AbcSize:
90
47
  Exclude:
48
+ - 'lib/suma/cli/compare.rb'
91
49
  - 'lib/suma/jsdai/figure.rb'
92
50
  - 'lib/suma/jsdai/figure_image.rb'
93
51
  - 'lib/suma/schema_attachment.rb'
94
52
  - 'lib/suma/schema_document.rb'
95
53
  - 'lib/suma/thor_ext.rb'
96
54
 
97
- # Offense count: 3
55
+ # Offense count: 6
98
56
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
99
57
  Metrics/CyclomaticComplexity:
100
58
  Exclude:
59
+ - 'lib/suma/cli/compare.rb'
60
+ - 'lib/suma/eengine/wrapper.rb'
101
61
  - 'lib/suma/jsdai/figure.rb'
102
62
  - 'lib/suma/jsdai/figure_image.rb'
103
63
  - 'lib/suma/thor_ext.rb'
104
64
 
105
- # Offense count: 1
65
+ # Offense count: 5
66
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
67
+ Metrics/MethodLength:
68
+ Max: 60
69
+
70
+ # Offense count: 4
106
71
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
107
72
  Metrics/PerceivedComplexity:
108
73
  Exclude:
74
+ - 'lib/suma/cli/compare.rb'
75
+ - 'lib/suma/eengine/wrapper.rb'
109
76
  - 'lib/suma/jsdai/figure_image.rb'
110
77
 
78
+ # Offense count: 1
79
+ # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
80
+ # AllowedMethods: call
81
+ # WaywardPredicates: nonzero?
82
+ Naming/PredicateMethod:
83
+ Exclude:
84
+ - 'lib/suma/eengine/wrapper.rb'
85
+
111
86
  # Offense count: 6
112
87
  # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
113
88
  # SupportedStyles: snake_case, normalcase, non_integer
@@ -131,18 +106,32 @@ RSpec/ContextWording:
131
106
  Exclude:
132
107
  - 'spec/suma/cli/export_spec.rb'
133
108
 
134
- # Offense count: 37
109
+ # Offense count: 51
135
110
  # Configuration parameters: CountAsOne.
136
111
  RSpec/ExampleLength:
137
112
  Max: 44
138
113
 
139
- # Offense count: 2
114
+ # Offense count: 4
140
115
  # Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns.
141
116
  RSpec/IndexedLet:
142
117
  Exclude:
118
+ - 'spec/suma/cli/compare_spec.rb'
143
119
  - 'spec/suma/cli/export_spec.rb'
144
120
 
145
- # Offense count: 33
121
+ # Offense count: 22
122
+ # Configuration parameters: AssignmentOnly.
123
+ RSpec/InstanceVariable:
124
+ Exclude:
125
+ - 'spec/suma/cli/compare_spec.rb'
126
+
127
+ # Offense count: 1
128
+ # Configuration parameters: EnforcedStyle.
129
+ # SupportedStyles: have_received, receive
130
+ RSpec/MessageSpies:
131
+ Exclude:
132
+ - 'spec/suma/cli/compare_spec.rb'
133
+
134
+ # Offense count: 46
146
135
  RSpec/MultipleExpectations:
147
136
  Max: 12
148
137
 
@@ -154,12 +143,6 @@ RSpec/SpecFilePathFormat:
154
143
  - 'spec/suma/cli/expressir_spec.rb'
155
144
 
156
145
  # Offense count: 1
157
- # This cop supports safe autocorrection (--autocorrect).
158
- # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
159
- # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
160
- # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
161
- # FunctionalMethods: let, let!, subject, watch
162
- # AllowedMethods: lambda, proc, it
163
- Style/BlockDelimiters:
146
+ RSpec/StubbedMock:
164
147
  Exclude:
165
- - 'spec/suma/cli/export_spec.rb'
148
+ - 'spec/suma/cli/compare_spec.rb'
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in suma.gemspec
6
6
  gemspec
7
7
 
8
+ gem "canon"
8
9
  gem "nokogiri"
9
10
  gem "rake"
10
11
  gem "rspec"
data/README.adoc CHANGED
@@ -698,6 +698,125 @@ express-files/
698
698
  ----
699
699
 
700
700
 
701
+ === Compare schemas command
702
+
703
+ ==== General
704
+
705
+ The `suma compare` command compares two EXPRESS schemas using eengine and
706
+ generates an EXPRESS Changes YAML file tracking the differences.
707
+
708
+ This command is essential for managing schema evolution across versions,
709
+ particularly when working with multiple git branches of the same repository.
710
+
711
+ [source,sh]
712
+ ----
713
+ $ suma compare TRIAL_SCHEMA REFERENCE_SCHEMA --version VERSION [options]
714
+ ----
715
+
716
+ Parameters:
717
+
718
+ `TRIAL_SCHEMA`:: Path to the new/trial EXPRESS schema file
719
+ `REFERENCE_SCHEMA`:: Path to the old/reference EXPRESS schema file
720
+
721
+ Options:
722
+
723
+ `--version=VERSION`, `-v VERSION`:: (required) Version number for this change edition
724
+ `--output=PATH`, `-o PATH`:: Output Change YAML file path (default: `{schema}.changes.yaml` in trial schema directory)
725
+ `--mode=MODE`:: Schema comparison mode: `resource` or `module` (default: `resource`)
726
+ `--trial-stepmod=PATH`:: Override auto-detected trial repository root
727
+ `--reference-stepmod=PATH`:: Override auto-detected reference repository root
728
+ `--verbose`:: Enable verbose output
729
+
730
+ ==== Typical workflow
731
+
732
+ The compare command is designed for a two-repository workflow where you have
733
+ different branches checked out:
734
+
735
+ [source,sh]
736
+ ----
737
+ # 1. Check out old version at /path/to/repo-old
738
+ # 2. Check out new version at /path/to/repo-new
739
+ # 3. Compare schemas:
740
+ $ suma compare \
741
+ /path/to/repo-new/schemas/resources/action_schema/action_schema.exp \
742
+ /path/to/repo-old/schemas/resources/action_schema/action_schema.exp \
743
+ --version 2
744
+ ----
745
+
746
+ ==== Behavior
747
+
748
+ The command:
749
+
750
+ * Auto-detects git repository roots from schema file paths
751
+ * Uses detected roots as stepmod paths for eengine
752
+ * Generates or updates `.changes.yaml` file in the trial schema directory
753
+ * **Replaces** existing change edition if the version already exists
754
+ * **Adds** new change edition if the version is different
755
+
756
+ ==== Requirements
757
+
758
+ This command requires `eengine` to be installed:
759
+
760
+ * macOS: https://github.com/expresslang/homebrew-eengine
761
+ * Linux: https://github.com/expresslang/eengine-releases
762
+
763
+ [example]
764
+ .To compare schemas with auto-detection
765
+ [source,sh]
766
+ ----
767
+ $ suma compare \
768
+ ~/iso-10303-new/schemas/resources/support_resource_schema/support_resource_schema.exp \
769
+ ~/iso-10303-old/schemas/resources/support_resource_schema/support_resource_schema.exp \
770
+ --version 2
771
+ # => generates or updates support_resource_schema.changes.yaml
772
+ ----
773
+
774
+ [example]
775
+ .To compare with verbose output
776
+ [source,sh]
777
+ ----
778
+ $ suma compare schema_new.exp schema_old.exp --version 2 --verbose
779
+ # Shows: eengine version, detected repo roots, XML generation, etc.
780
+ ----
781
+
782
+ [example]
783
+ .To specify custom output location
784
+ [source,sh]
785
+ ----
786
+ $ suma compare schema_new.exp schema_old.exp \
787
+ --version 2 \
788
+ --output /path/to/custom.changes.yaml
789
+ ----
790
+
791
+ [example]
792
+ .To override auto-detected repository roots
793
+ [source,sh]
794
+ ----
795
+ $ suma compare schema_new.exp schema_old.exp \
796
+ --version 2 \
797
+ --trial-stepmod ~/repo-new \
798
+ --reference-stepmod ~/repo-old
799
+ ----
800
+
801
+ ==== Generated Change YAML format
802
+
803
+ The command generates EXPRESS Changes YAML files in the ISO 10303 format:
804
+
805
+ [source,yaml]
806
+ ----
807
+ ---
808
+ schema: support_resource_schema
809
+ editions:
810
+ - version: '2'
811
+ description: 'TYPE text: Underlying Type changed'
812
+ modifications:
813
+ - type: TYPE
814
+ name: text
815
+ ----
816
+
817
+ For complete format documentation, see the
818
+ https://www.expresslang.org/docs/express-changes[EXPRESS Changes specification].
819
+
701
820
  === Expressir command
702
821
 
703
822
  ==== General
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "../eengine/wrapper"
5
+ require_relative "../eengine_converter"
6
+
7
+ module Suma
8
+ module Cli
9
+ # Command to compare EXPRESS schemas using eengine
10
+ class Compare < Thor
11
+ desc "compare TRIAL_SCHEMA REFERENCE_SCHEMA",
12
+ "Compare EXPRESS schemas using eengine and generate Change YAML"
13
+ long_desc <<~DESC
14
+ Compare two EXPRESS schemas from different git branches/checkouts.
15
+
16
+ Typical workflow:
17
+ 1. Check out old version of repo at /path/to/repo-old
18
+ 2. Check out new version of repo at /path/to/repo-new
19
+ 3. Run comparison:
20
+ suma compare \\
21
+ /path/to/repo-new/schemas/.../schema.exp \\
22
+ /path/to/repo-old/schemas/.../schema.exp \\
23
+ --version 2
24
+
25
+ The command will:
26
+ - Auto-detect repository roots from schema paths
27
+ - Use those as stepmod paths for eengine
28
+ - Generate/update the .changes.yaml file in the new repo
29
+ DESC
30
+
31
+ option :output, type: :string, aliases: "-o",
32
+ desc: "Output Change YAML file path " \
33
+ "(default: {schema}.changes.yaml in trial schema directory)"
34
+ option :version, type: :string, aliases: "-v", required: true,
35
+ desc: "Version number for this change edition"
36
+ option :mode, type: :string, default: "resource",
37
+ enum: ["resource", "module"],
38
+ desc: "Schema comparison mode"
39
+ option :trial_stepmod, type: :string,
40
+ desc: "Override auto-detected trial repo root"
41
+ option :reference_stepmod, type: :string,
42
+ desc: "Override auto-detected reference repo root"
43
+ option :verbose, type: :boolean, default: false,
44
+ desc: "Enable verbose output"
45
+
46
+ def compare(trial_schema, reference_schema)
47
+ # Validate schema files exist
48
+ unless File.exist?(trial_schema)
49
+ say "Error: Trial schema not found: #{trial_schema}", :red
50
+ exit 1
51
+ end
52
+
53
+ unless File.exist?(reference_schema)
54
+ say "Error: Reference schema not found: #{reference_schema}", :red
55
+ exit 1
56
+ end
57
+
58
+ # Check eengine availability
59
+ unless Eengine::Wrapper.available?
60
+ say "Error: eengine not found in PATH", :red
61
+ say "Install eengine following instructions at:"
62
+ say " macOS: https://github.com/expresslang/homebrew-eengine"
63
+ say " Linux: https://github.com/expresslang/eengine-releases"
64
+ exit 1
65
+ end
66
+
67
+ # Auto-detect repo roots
68
+ trial_stepmod = options[:trial_stepmod] ||
69
+ detect_repo_root(trial_schema)
70
+ reference_stepmod = options[:reference_stepmod] ||
71
+ detect_repo_root(reference_schema)
72
+
73
+ if options[:verbose]
74
+ say "Using eengine version: #{Eengine::Wrapper.version}", :green
75
+ say "Trial repo root: #{trial_stepmod}", :cyan
76
+ say "Reference repo root: #{reference_stepmod}", :cyan
77
+ end
78
+
79
+ # Create a temporary directory for eengine output
80
+ require "tmpdir"
81
+ out_dir = nil
82
+ out_dir = Dir.mktmpdir("eengine-compare-")
83
+
84
+ # Run comparison
85
+ result = Eengine::Wrapper.compare(
86
+ trial_schema,
87
+ reference_schema,
88
+ mode: options[:mode],
89
+ trial_stepmod: trial_stepmod,
90
+ reference_stepmod: reference_stepmod,
91
+ out_dir: out_dir,
92
+ )
93
+
94
+ unless result[:has_changes]
95
+ say "No changes detected between schemas", :yellow
96
+ # Clean up temp directory
97
+ FileUtils.rm_rf(out_dir) if out_dir && File.directory?(out_dir)
98
+ return
99
+ end
100
+
101
+ unless result[:xml_path]
102
+ say "Error: XML output not found", :red
103
+ exit 1
104
+ end
105
+
106
+ if options[:verbose]
107
+ say "Comparison XML generated: #{result[:xml_path]}", :green
108
+ end
109
+
110
+ # Convert to Change YAML
111
+ convert_to_change_yaml(result[:xml_path], trial_schema, out_dir)
112
+ rescue Eengine::EengineError => e
113
+ # Clean up temp directory
114
+ FileUtils.rm_rf(out_dir) if out_dir && File.directory?(out_dir)
115
+ say "Error: #{e.message}", :red
116
+ say e.stderr if e.respond_to?(:stderr) && options[:verbose]
117
+ exit 1
118
+ end
119
+
120
+ private
121
+
122
+ def detect_repo_root(schema_path)
123
+ # Walk up from schema path to find .git directory
124
+ current = File.expand_path(File.dirname(schema_path))
125
+
126
+ loop do
127
+ if File.directory?(File.join(current, ".git"))
128
+ return current
129
+ end
130
+
131
+ parent = File.dirname(current)
132
+ break if parent == current # reached root
133
+
134
+ current = parent
135
+ end
136
+
137
+ # If no .git found, use the directory containing the schema
138
+ # (for non-git workflows)
139
+ File.dirname(schema_path)
140
+ end
141
+
142
+ def convert_to_change_yaml(xml_path, trial_schema, out_dir)
143
+ schema_name = extract_schema_name(trial_schema)
144
+ output_path = determine_output_path(trial_schema)
145
+
146
+ # Load existing ChangeSchema if it exists
147
+ existing_schema = if File.exist?(output_path)
148
+ if options[:verbose]
149
+ say "Loading existing change schema: " \
150
+ "#{output_path}", :cyan
151
+ end
152
+ require "expressir/changes"
153
+ Expressir::Changes::SchemaChange.from_file(output_path)
154
+ end
155
+
156
+ # Convert using Suma's converter
157
+ converter = EengineConverter.new(xml_path, schema_name)
158
+ change_schema = converter.convert(
159
+ version: options[:version],
160
+ existing_change_schema: existing_schema,
161
+ )
162
+
163
+ # Save using Expressir model
164
+ change_schema.to_file(output_path)
165
+
166
+ # Determine what action was taken
167
+ if existing_schema
168
+ existing_edition = existing_schema.editions.find do |ed|
169
+ ed.version == options[:version]
170
+ end
171
+
172
+ say "Change YAML file updated: #{output_path}", :green
173
+ if existing_edition
174
+ say " Replaced existing version #{options[:version]}", :green
175
+ else
176
+ say " Added version #{options[:version]} to change editions",
177
+ :green
178
+ end
179
+ else
180
+ say "Change YAML file created: #{output_path}", :green
181
+ end
182
+
183
+ if options[:verbose]
184
+ say "\nGenerated change schema content:", :cyan
185
+ say File.read(output_path)
186
+ end
187
+
188
+ # Clean up temp directory and XML file
189
+ FileUtils.rm_rf(out_dir) if out_dir && File.directory?(out_dir)
190
+ end
191
+
192
+ def extract_schema_name(path)
193
+ # Remove version suffix if present (e.g., schema_1.exp -> schema)
194
+ basename = File.basename(path, ".exp")
195
+ basename.sub(/_\d+$/, "")
196
+ end
197
+
198
+ def determine_output_path(trial_schema)
199
+ if options[:output]
200
+ options[:output]
201
+ else
202
+ # Place .changes.yaml next to the trial schema in the NEW repo
203
+ base = extract_schema_name(trial_schema)
204
+ dir = File.dirname(trial_schema)
205
+ File.join(dir, "#{base}.changes.yaml")
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "thor"
4
4
  require_relative "../thor_ext"
5
+ require_relative "../export_standalone_schema"
5
6
 
6
7
  module Suma
7
8
  module Cli
@@ -86,22 +87,12 @@ module Suma
86
87
  def create_schema_from_exp_file(exp_file)
87
88
  # Create a schema object from a standalone EXPRESS file
88
89
  # The id will be determined during parsing
89
- StandaloneSchema.new(
90
+ ExportStandaloneSchema.new(
90
91
  id: nil,
91
92
  path: File.expand_path(exp_file),
92
93
  )
93
94
  end
94
95
 
95
- # Simple schema class for standalone EXPRESS files
96
- class StandaloneSchema
97
- attr_accessor :id, :path
98
-
99
- def initialize(id:, path:)
100
- @id = id
101
- @path = path
102
- end
103
- end
104
-
105
96
  def self.exit_on_failure?
106
97
  true
107
98
  end
data/lib/suma/cli.rb CHANGED
@@ -74,6 +74,25 @@ module Suma
74
74
  Cli::Export.start
75
75
  end
76
76
 
77
+ desc "compare TRIAL_SCHEMA REFERENCE_SCHEMA",
78
+ "Compare EXPRESS schemas using eengine and generate Change YAML"
79
+ option :output, type: :string, aliases: "-o",
80
+ desc: "Output Change YAML file path"
81
+ option :version, type: :string, aliases: "-v", required: true,
82
+ desc: "Version number for this change edition"
83
+ option :mode, type: :string, default: "resource",
84
+ desc: "Schema comparison mode (resource/module)"
85
+ option :trial_stepmod, type: :string,
86
+ desc: "Override auto-detected trial repo root"
87
+ option :reference_stepmod, type: :string,
88
+ desc: "Override auto-detected reference repo root"
89
+ option :verbose, type: :boolean, default: false,
90
+ desc: "Enable verbose output"
91
+ def compare(_trial_schema, _reference_schema)
92
+ require_relative "cli/compare"
93
+ Cli::Compare.start
94
+ end
95
+
77
96
  desc "validate SUBCOMMAND ...ARGS", "Validate express documents"
78
97
  subcommand "validate", Cli::Validate
79
98
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Suma
4
+ module Eengine
5
+ # Base error class for eengine-related errors
6
+ class EengineError < StandardError; end
7
+
8
+ # Raised when eengine binary is not found in PATH
9
+ class EengineNotFoundError < EengineError
10
+ def initialize
11
+ super("eengine not found in PATH. Install eengine:\n " \
12
+ "macOS: https://github.com/expresslang/homebrew-eengine\n " \
13
+ "Linux: https://github.com/expresslang/eengine-releases")
14
+ end
15
+ end
16
+
17
+ # Raised when eengine comparison fails
18
+ class ComparisonError < EengineError
19
+ attr_reader :stderr
20
+
21
+ def initialize(message, stderr = nil)
22
+ super(message)
23
+ @stderr = stderr
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require_relative "errors"
5
+
6
+ module Suma
7
+ module Eengine
8
+ # Wrapper for eengine binary to compare EXPRESS schemas
9
+ class Wrapper
10
+ class << self
11
+ # Compare two EXPRESS schemas using eengine
12
+ #
13
+ # @param trial_schema [String] Path to the new/trial schema
14
+ # @param reference_schema [String] Path to the old/reference schema
15
+ # @param options [Hash] Comparison options
16
+ # @option options [String] :mode Comparison mode (resource/module)
17
+ # @option options [String] :trial_stepmod Path to trial repo root
18
+ # @option options [String] :reference_stepmod Path to reference repo root
19
+ # @return [Hash] Result with :success, :xml_path, :has_changes, :output
20
+ def compare(trial_schema, reference_schema, options = {})
21
+ ensure_eengine_available!
22
+
23
+ cmd = build_command(trial_schema, reference_schema, options)
24
+ output, error, status = Open3.capture3(*cmd)
25
+
26
+ unless status.success?
27
+ error_message = error.empty? ? "Unknown eengine error" : error.strip
28
+ raise ComparisonError.new(error_message, error)
29
+ end
30
+
31
+ parse_output(output, options)
32
+ end
33
+
34
+ # Check if eengine is available on the system
35
+ #
36
+ # @return [Boolean] true if eengine binary is found
37
+ def available?
38
+ return false if ENV["EENGINE_DISABLED"] == "true"
39
+
40
+ eengine_path && eengine_executable?
41
+ end
42
+
43
+ # Get the eengine version
44
+ #
45
+ # @return [String, nil] Version string or nil if unavailable
46
+ def version
47
+ return nil unless available?
48
+
49
+ cmd = [eengine_path, "--version"]
50
+ output, _, status = Open3.capture3(*cmd)
51
+ status.success? ? parse_version(output) : nil
52
+ rescue StandardError
53
+ nil
54
+ end
55
+
56
+ private
57
+
58
+ def eengine_path
59
+ @eengine_path ||= find_eengine_binary
60
+ end
61
+
62
+ def find_eengine_binary
63
+ # Search for eengine or eengine-* in PATH
64
+ ENV["PATH"].split(File::PATH_SEPARATOR).each do |dir|
65
+ # First try plain eengine
66
+ plain_path = File.join(dir, "eengine")
67
+ return plain_path if File.exist?(plain_path) && File.executable?(plain_path)
68
+
69
+ # Then try eengine-* pattern
70
+ Dir.glob(File.join(dir, "eengine-*")).each do |path|
71
+ return path if File.executable?(path)
72
+ end
73
+ end
74
+ nil
75
+ end
76
+
77
+ def eengine_executable?
78
+ eengine_path && File.executable?(eengine_path)
79
+ end
80
+
81
+ def ensure_eengine_available!
82
+ raise EengineNotFoundError unless available?
83
+ end
84
+
85
+ def build_command(trial, reference, options)
86
+ cmd = [
87
+ eengine_path,
88
+ "--compare",
89
+ "-trial_schema", trial,
90
+ "-trial_stepmod", options[:trial_stepmod] || ".",
91
+ "-reference_schema", reference,
92
+ "-reference_stepmod", options[:reference_stepmod] || ".",
93
+ "-mode", options[:mode] || "resource",
94
+ "--xml-output"
95
+ ]
96
+
97
+ # Add output directory if specified
98
+ if options[:out_dir]
99
+ cmd += ["-out-dir", options[:out_dir]]
100
+ end
101
+
102
+ cmd
103
+ end
104
+
105
+ def parse_output(output, _options)
106
+ # Extract XML file path from output
107
+ # eengine prints: "Writing \"path/to/file.xml\""
108
+ xml_match = output.match(/Writing "(.+\.xml)"/)
109
+ xml_path = xml_match ? xml_match[1] : nil
110
+
111
+ # Expand to absolute path if found
112
+ xml_path = File.expand_path(xml_path) if xml_path
113
+
114
+ # Determine if changes were detected
115
+ has_changes = detect_changes(output)
116
+
117
+ {
118
+ success: true,
119
+ xml_path: xml_path,
120
+ has_changes: has_changes,
121
+ output: output,
122
+ }
123
+ end
124
+
125
+ def detect_changes(output)
126
+ # Check for various indicators of changes in the output
127
+ return true if output.include?("Comparing TYPE")
128
+ return true if output.include?("Comparing ENTITY")
129
+ return true if output.include?("Comparing FUNCTION")
130
+ return true if output.include?("Comparing RULE")
131
+ return true if output.include?("Comparing PROCEDURE")
132
+
133
+ # Check for modification indicators
134
+ return true if output.include?("changed")
135
+ return true if output.include?("modified")
136
+ return true if output.include?("added")
137
+ return true if output.include?("removed")
138
+
139
+ false
140
+ end
141
+
142
+ def parse_version(output)
143
+ # Extract version from output like "Express Engine 5.2.7"
144
+ version_match = output.match(/Express Engine ([\d.]+)/)
145
+ version_match ? version_match[1] : nil
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "expressir/commands/changes_import_eengine"
4
+
5
+ module Suma
6
+ # Converts eengine comparison XML to Expressir::Changes::SchemaChange
7
+ # This is a thin wrapper around Expressir's ChangesImportEengine command
8
+ class EengineConverter
9
+ def initialize(xml_path, schema_name)
10
+ @xml_path = xml_path
11
+ @schema_name = schema_name
12
+ @xml_content = File.read(xml_path)
13
+ end
14
+
15
+ # Convert the eengine XML to a ChangeSchema
16
+ #
17
+ # @param version [String] Version number for this change edition
18
+ # @param existing_change_schema [Expressir::Changes::SchemaChange, nil]
19
+ # Existing schema to append to, or nil to create new
20
+ # @return [Expressir::Changes::SchemaChange] The updated change schema
21
+ def convert(version:, existing_change_schema: nil)
22
+ # Use Expressir's built-in conversion which properly handles
23
+ # HTML elements in descriptions
24
+ Expressir::Commands::ChangesImportEengine.from_xml(
25
+ @xml_content,
26
+ @schema_name,
27
+ version,
28
+ existing_schema: existing_change_schema,
29
+ )
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Suma
4
+ # Simple schema class for standalone EXPRESS files
5
+ # Used when exporting individual .exp files that are not part of a manifest
6
+ class ExportStandaloneSchema
7
+ attr_accessor :id, :path
8
+
9
+ def initialize(id:, path:)
10
+ @id = id
11
+ @path = path
12
+ end
13
+ end
14
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "express_schema"
4
4
  require_relative "utils"
5
+ require_relative "export_standalone_schema"
5
6
  require "fileutils"
6
7
 
7
8
  module Suma
@@ -44,7 +45,7 @@ module Suma
44
45
  def export_single_schema(schema)
45
46
  # Check if this is a standalone EXPRESS file
46
47
  # (not from a manifest structure)
47
- is_standalone_file = schema.is_a?(Cli::Export::StandaloneSchema)
48
+ is_standalone_file = schema.is_a?(ExportStandaloneSchema)
48
49
  schema_output_path = determine_output_path(schema, is_standalone_file)
49
50
 
50
51
  express_schema = ExpressSchema.new(
data/lib/suma/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Suma
4
- VERSION = "0.1.24"
4
+ VERSION = "0.1.25"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: suma
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.24
4
+ version: 0.1.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-12 00:00:00.000000000 Z
11
+ date: 2025-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: expressir
@@ -162,6 +162,7 @@ files:
162
162
  - lib/suma.rb
163
163
  - lib/suma/cli.rb
164
164
  - lib/suma/cli/build.rb
165
+ - lib/suma/cli/compare.rb
165
166
  - lib/suma/cli/convert_jsdai.rb
166
167
  - lib/suma/cli/export.rb
167
168
  - lib/suma/cli/extract_terms.rb
@@ -172,6 +173,10 @@ files:
172
173
  - lib/suma/cli/validate_links.rb
173
174
  - lib/suma/collection_config.rb
174
175
  - lib/suma/collection_manifest.rb
176
+ - lib/suma/eengine/errors.rb
177
+ - lib/suma/eengine/wrapper.rb
178
+ - lib/suma/eengine_converter.rb
179
+ - lib/suma/export_standalone_schema.rb
175
180
  - lib/suma/express_schema.rb
176
181
  - lib/suma/jsdai.rb
177
182
  - lib/suma/jsdai/figure.rb