suma 0.2.6 → 0.4.0

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -1
  3. data/.rubocop_todo.yml +170 -13
  4. data/CLAUDE.md +37 -11
  5. data/Gemfile +3 -3
  6. data/README.adoc +127 -13
  7. data/exe/suma +1 -1
  8. data/lib/suma/cli/build.rb +0 -5
  9. data/lib/suma/cli/check_svg_quality.rb +82 -97
  10. data/lib/suma/cli/compare.rb +0 -1
  11. data/lib/suma/cli/convert_jsdai.rb +0 -2
  12. data/lib/suma/cli/core.rb +119 -0
  13. data/lib/suma/cli/export.rb +37 -22
  14. data/lib/suma/cli/extract_terms.rb +5 -8
  15. data/lib/suma/cli/generate_register.rb +34 -0
  16. data/lib/suma/cli/generate_schemas.rb +0 -2
  17. data/lib/suma/cli/reformat.rb +39 -64
  18. data/lib/suma/cli/validate.rb +14 -7
  19. data/lib/suma/cli/validate_links.rb +11 -157
  20. data/lib/suma/cli.rb +12 -141
  21. data/lib/suma/collection_config.rb +0 -2
  22. data/lib/suma/collection_manifest.rb +7 -110
  23. data/lib/suma/eengine/wrapper.rb +0 -1
  24. data/lib/suma/eengine.rb +8 -0
  25. data/lib/suma/express_reformatter.rb +94 -0
  26. data/lib/suma/express_schema.rb +0 -1
  27. data/lib/suma/jsdai/figure.rb +0 -3
  28. data/lib/suma/jsdai.rb +5 -2
  29. data/lib/suma/link_validation.rb +144 -0
  30. data/lib/suma/link_validator.rb +17 -8
  31. data/lib/suma/manifest_traverser.rb +92 -0
  32. data/lib/suma/processor.rb +5 -8
  33. data/lib/suma/register_manifest_generator.rb +163 -0
  34. data/lib/suma/schema_category.rb +83 -0
  35. data/lib/suma/schema_collection.rb +30 -34
  36. data/lib/suma/schema_comparer.rb +4 -3
  37. data/lib/suma/schema_compiler.rb +86 -0
  38. data/lib/suma/schema_discovery.rb +75 -0
  39. data/lib/suma/schema_exporter.rb +14 -42
  40. data/lib/suma/schema_manifest_generator.rb +14 -6
  41. data/lib/suma/schema_naming.rb +111 -0
  42. data/lib/suma/schema_template/document.rb +141 -0
  43. data/lib/suma/schema_template/plain.rb +46 -0
  44. data/lib/suma/schema_template.rb +19 -0
  45. data/lib/suma/svg_quality/batch_report.rb +0 -2
  46. data/lib/suma/svg_quality/formatters.rb +12 -0
  47. data/lib/suma/svg_quality/scanner.rb +61 -0
  48. data/lib/suma/svg_quality.rb +4 -1
  49. data/lib/suma/term_classification.rb +78 -0
  50. data/lib/suma/term_extractor.rb +125 -81
  51. data/lib/suma/urn.rb +61 -0
  52. data/lib/suma/version.rb +1 -1
  53. data/lib/suma.rb +35 -3
  54. data/suma.gemspec +1 -1
  55. metadata +28 -6
  56. data/lib/suma/schema_attachment.rb +0 -103
  57. data/lib/suma/schema_document.rb +0 -118
@@ -1,25 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pathname"
4
- require_relative "../svg_quality"
5
- require_relative "../svg_quality/report"
6
- require_relative "../svg_quality/batch_report"
7
- require_relative "../svg_quality/formatters/terminal_formatter"
8
- require_relative "../svg_quality/formatters/json_formatter"
9
- require_relative "../svg_quality/formatters/yaml_formatter"
10
4
 
11
5
  module Suma
12
6
  module Cli
13
- # Check SVG quality using svg_conform Validator API - thin CLI wrapper
7
+ # Check SVG quality. Thin adapter around +Suma::SvgQuality::Scanner+:
8
+ # argument parsing, file discovery, sorting, filtering, output
9
+ # formatting. The deep scanner module owns validation orchestration
10
+ # and is reachable from specs without invoking Thor.
14
11
  class CheckSvgQuality
15
12
  DATA_PATH = "schemas"
16
13
  DEFAULT_PATTERN = "**/*.svg"
17
14
  DEFAULT_PROFILE = :metanorma
18
15
 
19
16
  def initialize(pattern: DEFAULT_PATTERN, profile: DEFAULT_PROFILE,
20
- format: "terminal", output: nil, min_errors: nil,
21
- summary_only: false, progress: false, limit: nil,
22
- sort: "errors")
17
+ format: "terminal", output: nil, min_errors: nil,
18
+ summary_only: false, progress: false, limit: nil,
19
+ sort: "errors")
23
20
  @options = {
24
21
  pattern: pattern,
25
22
  profile: profile,
@@ -37,71 +34,42 @@ module Suma
37
34
  require "svg_conform"
38
35
 
39
36
  path_obj = Pathname.new(path).expand_path
40
-
41
- # Enable progress by default when outputting to terminal
42
37
  show_progress = options[:progress] || ($stdout.tty? && !options[:output])
43
- if show_progress
44
- $stdout.sync = true
45
- $stderr.sync = true
46
- end
47
-
48
- if path_obj.file?
49
- # Single file mode - show detailed errors
50
- analyze_single_file(path_obj)
51
- else
52
- # Directory mode - show batch report
53
- svg_files = find_svg_files(path_obj)
54
-
55
- if svg_files.empty?
56
- puts "No SVG files found in #{path}"
57
- return
58
- end
38
+ sync_stdio! if show_progress
59
39
 
60
- puts "🔍 Scanning #{svg_files.size} SVG files..."
61
- puts
40
+ files = discover_files(path_obj)
41
+ return if files.empty?
62
42
 
63
- reports = analyze_files_one_by_one(svg_files, show_progress)
64
- batch_report = SvgQuality::BatchReport.new(reports)
65
- sorted_report = sort_report(batch_report)
66
- output_report(sorted_report)
43
+ if single_file?(path_obj, files)
44
+ print_single_report(scan_single(files.first))
45
+ else
46
+ scan_and_output(files, show_progress)
67
47
  end
68
48
  end
69
49
 
70
- def analyze_single_file(path)
71
- validator = SvgConform::Validator.new
72
- result = validator.validate_file(path.to_s, profile: options[:profile])
73
-
74
- puts "📄 SVG Quality Report: #{path}"
75
- puts ""
76
- puts " Valid: #{result.valid? ? 'YES ✅' : 'NO ❌'}"
77
- puts " Errors: #{result.error_count}"
78
- puts ""
50
+ private
79
51
 
80
- if result.errors.any?
81
- puts " 📋 Error Details"
82
- puts ""
52
+ attr_reader :options
83
53
 
84
- # Group errors by requirement_id
85
- by_req = result.errors.group_by(&:requirement_id)
86
-
87
- by_req.each do |req_id, errors|
88
- puts " #{req_id} (#{errors.size} occurrences)"
89
- errors.first(5).each do |e|
90
- puts " - #{e.message}"
91
- end
92
- if errors.size > 5
93
- puts " ... and #{errors.size - 5} more"
94
- end
95
- puts ""
96
- end
97
- end
54
+ def single_file?(path_obj, files)
55
+ path_obj.file? && files.size == 1
98
56
  end
99
57
 
100
- private
58
+ def scan_and_output(files, show_progress)
59
+ puts "🔍 Scanning #{files.size} SVG files..." if show_progress
60
+ batch = SvgQuality::Scanner.new(
61
+ profile: options[:profile],
62
+ progress: progress_adapter(show_progress),
63
+ ).scan(files)
64
+ output_report(sort_report(batch))
65
+ end
101
66
 
102
- attr_reader :options
67
+ def sync_stdio!
68
+ $stdout.sync = true
69
+ $stderr.sync = true
70
+ end
103
71
 
104
- def find_svg_files(path)
72
+ def discover_files(path)
105
73
  if path.directory?
106
74
  Pathname.glob(path.join(options[:pattern])).select(&:file?)
107
75
  elsif path.file? && path.extname == ".svg"
@@ -111,34 +79,47 @@ module Suma
111
79
  end
112
80
  end
113
81
 
114
- def analyze_files_one_by_one(files, show_progress = false)
115
- validator = SvgConform::Validator.new
116
- reports = []
117
-
118
- files.each_with_index do |file, index|
119
- result = validator.validate_file(file.to_s,
120
- profile: options[:profile])
121
- report = SvgQuality::Report.new(file.to_s, result)
122
- reports << report
123
-
124
- if show_progress
125
- tier = report.quality_tier
126
- status = report.valid? ? "✅" : "❌"
127
- msg = " [#{index + 1}/#{files.size}] #{tier[:emoji]} #{report.error_count} errors #{status} #{shorten_path(file)}\n"
128
- $stderr.print msg
129
- $stderr.flush
130
- end
82
+ def scan_single(path)
83
+ SvgQuality::Scanner.new(profile: options[:profile]).scan_file(path)
84
+ end
85
+
86
+ def print_single_report(report)
87
+ result = report
88
+ puts "📄 SVG Quality Report: #{result.file_path}"
89
+ puts ""
90
+ puts " Valid: #{result.valid? ? 'YES ✅' : 'NO ❌'}"
91
+ puts " Errors: #{result.error_count}"
92
+ puts ""
93
+
94
+ return unless result.errors.any?
95
+
96
+ puts " 📋 Error Details"
97
+ puts ""
98
+ by_req = result.errors.group_by(&:requirement_id)
99
+ by_req.each do |req_id, errors|
100
+ puts " #{req_id} (#{errors.size} occurrences)"
101
+ errors.first(5).each { |e| puts " - #{e.message}" }
102
+ puts " ... and #{errors.size - 5} more" if errors.size > 5
103
+ puts ""
131
104
  end
105
+ end
106
+
107
+ def progress_adapter(enabled)
108
+ return SvgQuality::Scanner::NullProgress.new unless enabled
132
109
 
133
- reports
110
+ ->(_index, _total, report) do
111
+ tier = report.quality_tier
112
+ status = report.valid? ? "✅" : "❌"
113
+ $stderr.print " #{tier[:emoji]} #{report.error_count} errors " \
114
+ "#{status} #{shorten_path(report.file_path)}\n"
115
+ $stderr.flush
116
+ end
134
117
  end
135
118
 
136
119
  def sort_report(batch_report)
137
120
  case options[:sort]
138
- when :quality
139
- batch_report.sort_by_quality
140
- else
141
- batch_report.sort_by_errors
121
+ when :quality then batch_report.sort_by_quality
122
+ else batch_report.sort_by_errors
142
123
  end
143
124
  end
144
125
 
@@ -146,21 +127,25 @@ module Suma
146
127
  filtered = batch_report.filter_by_min_errors(options[:min_errors])
147
128
  limited = filtered.limit(options[:limit])
148
129
 
149
- formatter = case options[:format].to_sym
150
- when :json
151
- SvgQuality::Formatters::JsonFormatter.new(limited,
152
- output: options[:output])
153
- when :yaml
154
- SvgQuality::Formatters::YamlFormatter.new(limited,
155
- output: options[:output])
156
- else
157
- SvgQuality::Formatters::TerminalFormatter.new(limited,
158
- output: options[:output], sort: options[:sort])
159
- end
160
-
130
+ formatter = formatter_for(limited)
161
131
  puts formatter.format
162
132
  end
163
133
 
134
+ def formatter_for(batch_report)
135
+ case options[:format].to_sym
136
+ when :json
137
+ SvgQuality::Formatters::JsonFormatter.new(batch_report,
138
+ output: options[:output])
139
+ when :yaml
140
+ SvgQuality::Formatters::YamlFormatter.new(batch_report,
141
+ output: options[:output])
142
+ else
143
+ SvgQuality::Formatters::TerminalFormatter.new(batch_report,
144
+ output: options[:output],
145
+ sort: options[:sort])
146
+ end
147
+ end
148
+
164
149
  def shorten_path(path)
165
150
  p = Pathname.new(path)
166
151
  if p.absolute?
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require_relative "../schema_comparer"
5
4
 
6
5
  module Suma
7
6
  module Cli
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require_relative "../thor_ext"
5
- require_relative "../jsdai"
6
4
  require "fileutils"
7
5
 
8
6
  module Suma
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "expressir"
5
+ require "expressir/cli"
6
+
7
+ module Suma
8
+ module Cli
9
+ # Top-level CLI entrypoint.
10
+ #
11
+ # Each command delegates to a dedicated Thor class under +Suma::Cli::+,
12
+ # which reparses ARGV and therefore owns the canonical +option+
13
+ # declarations. Re-declaring options here would duplicate the inner
14
+ # classes' declarations and cause help text and validation logic to
15
+ # drift. Options are declared in exactly one place (the inner class).
16
+ #
17
+ # The exception is +check_svg_quality+, which constructs its analyzer
18
+ # inline (no inner Thor class), so its options belong here.
19
+ class Core < Thor
20
+ extend ThorExt::Start
21
+
22
+ desc "build METANORMA_SITE_MANIFEST",
23
+ "Build collection specified in site manifest (`metanorma*.yml`)"
24
+ def build(_site_manifest)
25
+ Cli::Build.start
26
+ end
27
+
28
+ desc "generate-schemas METANORMA_MANIFEST_FILE SCHEMA_MANIFEST_FILE",
29
+ "Generate EXPRESS schema manifest file from Metanorma site manifest"
30
+ def generate_schemas(_metanorma_manifest_file, _schema_manifest_file)
31
+ Cli::GenerateSchemas.start
32
+ end
33
+
34
+ desc "reformat EXPRESS_FILE_PATH",
35
+ "Reformat EXPRESS files"
36
+ def reformat(_express_file_path)
37
+ Cli::Reformat.start
38
+ end
39
+
40
+ desc "extract-terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH",
41
+ "Extract terms from SCHEMA_MANIFEST_FILE into " \
42
+ "Glossarist v3 format"
43
+ def extract_terms(_schema_manifest_file, _glossarist_output_path)
44
+ Cli::ExtractTerms.start
45
+ end
46
+
47
+ desc "generate-register SCHEMA_MANIFEST_FILE OUTPUT_PATH",
48
+ "Generate a Glossarist register.yaml with hierarchical sections"
49
+ def generate_register(_schema_manifest_file, _output_path)
50
+ Cli::GenerateRegister.start
51
+ end
52
+
53
+ desc "convert-jsdai XML_FILE IMAGE_FILE OUTPUT_DIR",
54
+ "Convert JSDAI XML and image files to SVG and EXP files"
55
+ def convert_jsdai(_xml_file, _image_file, _output_dir)
56
+ Cli::ConvertJsdai.start
57
+ end
58
+
59
+ desc "export *FILES",
60
+ "Export EXPRESS schemas from manifest files or " \
61
+ "standalone EXPRESS files"
62
+ def export(*_files)
63
+ Cli::Export.start
64
+ end
65
+
66
+ desc "compare TRIAL_SCHEMA REFERENCE_SCHEMA",
67
+ "Compare EXPRESS schemas using eengine and generate Change YAML"
68
+ def compare(_trial_schema, _reference_schema)
69
+ Cli::Compare.start
70
+ end
71
+
72
+ desc "validate SUBCOMMAND ...ARGS", "Validate express documents"
73
+ subcommand "validate", Cli::Validate
74
+
75
+ desc "check_svg_quality [PATH]",
76
+ "Check SVG quality and sort by severity (critical files first)"
77
+ option :pattern, type: :string, default: Cli::CheckSvgQuality::DEFAULT_PATTERN,
78
+ desc: "Glob pattern for finding SVG files"
79
+ option :profile, type: :string,
80
+ default: Cli::CheckSvgQuality::DEFAULT_PROFILE,
81
+ desc: "Validation profile to use (metanorma, svg_1_2_rfc, etc.)"
82
+ option :format, type: :string, default: "terminal",
83
+ desc: "Output format: terminal, yaml, json"
84
+ option :output, type: :string, aliases: "-o",
85
+ desc: "Output file path"
86
+ option :min_errors, type: :numeric,
87
+ desc: "Minimum error count threshold"
88
+ option :limit, type: :numeric, default: nil,
89
+ desc: "Maximum number of files to show (default: unlimited)"
90
+ option :sort, type: :string, default: "errors",
91
+ desc: "Sort by: errors (most errors first) or quality (lowest scores first)"
92
+ option :progress, type: :boolean, default: false,
93
+ desc: "Show progress during processing"
94
+ option :summary_only, type: :boolean, default: false,
95
+ desc: "Show only summary"
96
+ def check_svg_quality(path = Cli::CheckSvgQuality::DATA_PATH)
97
+ analyzer = Cli::CheckSvgQuality.new(
98
+ pattern: options[:pattern],
99
+ profile: options[:profile],
100
+ format: options[:format],
101
+ output: options[:output],
102
+ min_errors: options[:min_errors],
103
+ summary_only: options[:summary_only],
104
+ progress: options[:progress],
105
+ limit: options[:limit],
106
+ sort: options[:sort],
107
+ )
108
+ analyzer.run(path)
109
+ end
110
+
111
+ desc "expressir SUBCOMMAND ...ARGS", "Expressir commands"
112
+ subcommand "expressir", Expressir::Cli
113
+
114
+ def self.exit_on_failure?
115
+ true
116
+ end
117
+ end
118
+ end
119
+ end
@@ -1,11 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require_relative "../thor_ext"
4
+ require "pathname"
5
5
 
6
6
  module Suma
7
7
  module Cli
8
- # Export command for exporting EXPRESS schemas from a manifest
8
+ # Export command. Thin Thor adapter that constructs
9
+ # +Suma::ExpressSchema+ instances from manifest entries or
10
+ # standalone +.exp+ files, then delegates the actual writing to
11
+ # +Suma::SchemaExporter+.
12
+ #
13
+ # The schema-type → output-subdirectory mapping lives in
14
+ # +Suma::SchemaCategory+, the single source of truth. The exporter
15
+ # itself never classifies — it consumes loaded ExpressSchema
16
+ # objects whose output paths were set by this adapter.
9
17
  class Export < Thor
10
18
  desc "export *FILES",
11
19
  "Export EXPRESS schemas from manifest files or " \
@@ -18,8 +26,6 @@ module Suma
18
26
  desc: "Create ZIP archive of exported schemas"
19
27
 
20
28
  def export(*files)
21
- require_relative "../schema_exporter"
22
- require_relative "../utils"
23
29
  require "expressir"
24
30
 
25
31
  validate_files(files)
@@ -41,7 +47,7 @@ module Suma
41
47
  end
42
48
 
43
49
  def run(files, options)
44
- schemas = load_schemas_from_files(files)
50
+ schemas = files.flat_map { |file| build_schemas(file) }
45
51
 
46
52
  exporter = SchemaExporter.new(
47
53
  schemas: schemas,
@@ -55,22 +61,12 @@ module Suma
55
61
  exporter.export
56
62
  end
57
63
 
58
- def load_schemas_from_files(files)
59
- all_schemas = []
60
-
61
- files.each do |file|
62
- all_schemas += process_file(file)
63
- end
64
-
65
- all_schemas
66
- end
67
-
68
- def process_file(file)
64
+ def build_schemas(file)
69
65
  case File.extname(file).downcase
70
66
  when ".yml", ".yaml"
71
- load_manifest_schemas(file)
67
+ build_from_manifest(file)
72
68
  when ".exp"
73
- [create_schema_from_exp_file(file)]
69
+ [build_standalone(file)]
74
70
  else
75
71
  raise ArgumentError, "Unsupported file type: #{file}. " \
76
72
  "Only .yml, .yaml, and .exp files are " \
@@ -78,13 +74,32 @@ module Suma
78
74
  end
79
75
  end
80
76
 
81
- def load_manifest_schemas(file)
77
+ def build_from_manifest(file)
82
78
  manifest = Expressir::SchemaManifest.from_file(file)
83
- manifest.schemas
79
+ manifest.schemas.map { |entry| build_from_manifest_entry(entry) }
80
+ end
81
+
82
+ def build_from_manifest_entry(entry)
83
+ category = SchemaCategory.for_schema(id: entry.id, path: entry.path)
84
+ ExpressSchema.new(
85
+ id: entry.id,
86
+ path: entry.path.to_s,
87
+ output_path: output_root.join(category.directory).to_s,
88
+ is_standalone_file: false,
89
+ )
90
+ end
91
+
92
+ def build_standalone(exp_file)
93
+ ExpressSchema.new(
94
+ id: nil,
95
+ path: File.expand_path(exp_file),
96
+ output_path: output_root.to_s,
97
+ is_standalone_file: true,
98
+ )
84
99
  end
85
100
 
86
- def create_schema_from_exp_file(exp_file)
87
- Struct.new(:id, :path).new(nil, File.expand_path(exp_file))
101
+ def output_root
102
+ Pathname.new(options[:output]).expand_path
88
103
  end
89
104
 
90
105
  def self.exit_on_failure?
@@ -1,28 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require_relative "../thor_ext"
5
- require_relative "../term_extractor"
6
4
 
7
5
  module Suma
8
6
  module Cli
9
7
  class ExtractTerms < Thor
10
8
  desc "extract_terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH",
11
9
  "Extract terms from SCHEMA_MANIFEST_FILE into " \
12
- "Glossarist v2 format"
10
+ "Glossarist v3 format"
13
11
  option :language_code, type: :string, default: "eng", aliases: "-l",
14
12
  desc: "Language code for the Glossarist"
13
+ option :urn, type: :string, required: true, aliases: "-u",
14
+ desc: "URN for the dataset source " \
15
+ "(used for section references)"
15
16
 
16
17
  def extract_terms(schema_manifest_file, output_path)
17
- unless File.exist?(File.expand_path(schema_manifest_file))
18
- raise Errno::ENOENT, "Specified SCHEMA_MANIFEST_FILE " \
19
- "`#{schema_manifest_file}` not found."
20
- end
21
-
22
18
  TermExtractor.new(
23
19
  schema_manifest_file,
24
20
  output_path,
25
21
  language_code: options[:language_code],
22
+ urn: options[:urn],
26
23
  ).call
27
24
  end
28
25
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Suma
6
+ module Cli
7
+ class GenerateRegister < Thor
8
+ desc "generate_register SCHEMA_MANIFEST_FILE OUTPUT_PATH",
9
+ "Generate a Glossarist register.yaml with hierarchical sections"
10
+ option :language_code, type: :string, default: "eng", aliases: "-l",
11
+ desc: "Language code for section names"
12
+ option :urn, type: :string, required: true, aliases: "-u",
13
+ desc: "URN prefix for the dataset"
14
+ option :id, type: :string, required: true,
15
+ desc: "Dataset identifier (e.g. iso10303-2-express)"
16
+ option :ref, type: :string, required: true,
17
+ desc: "Human-readable reference label"
18
+ option :owner, type: :string, default: Suma::RegisterManifestGenerator::DEFAULT_OWNER,
19
+ desc: "Owner of the dataset (e.g. 'ISO/TC 184/SC 4')"
20
+
21
+ def generate_register(schema_manifest_file, output_path)
22
+ RegisterManifestGenerator.new(
23
+ schema_manifest_file,
24
+ output_path,
25
+ urn: options[:urn],
26
+ id: options[:id],
27
+ ref: options[:ref],
28
+ language_code: options[:language_code],
29
+ owner: options[:owner],
30
+ ).generate
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require_relative "../thor_ext"
5
- require_relative "../schema_manifest_generator"
6
4
 
7
5
  module Suma
8
6
  module Cli