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
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Suma
4
+ module SchemaTemplate
5
+ # Emits an AsciiDoc body with cross-reference anchors for every
6
+ # schema element so other documents can deep-link into the compiled
7
+ # HTML. Only XML is produced — the anchors only resolve against the
8
+ # XML output, not the HTML rendering.
9
+ class Document
10
+ EXTENSIONS = "xml"
11
+
12
+ attr_reader :schema_id
13
+
14
+ def initialize(schema_id)
15
+ @schema_id = schema_id
16
+ end
17
+
18
+ def extensions
19
+ EXTENSIONS
20
+ end
21
+
22
+ def render(path_to_schema_yaml)
23
+ <<~ADOC
24
+ = #{schema_id}
25
+ :lutaml-express-index: schemas; #{path_to_schema_yaml};
26
+ :bare: true
27
+ :mn-document-class: iso
28
+ :mn-output-extensions: #{extensions}
29
+
30
+ [lutaml_express_liquid,schemas,context]
31
+ ----
32
+ {% for schema in context.schemas %}
33
+
34
+ [[#{schema_id}]]
35
+ [%unnumbered,type=express]
36
+ == #{schema_id} #{rendered_anchors}
37
+
38
+ [source%unnumbered]
39
+ --
40
+ {{ schema.formatted }}
41
+ --
42
+ {% endfor %}
43
+ ----
44
+
45
+ ADOC
46
+ end
47
+
48
+ private
49
+
50
+ # The raw anchor block contains Liquid comments and newlines that
51
+ # are illegal on the section header line; strip them before
52
+ # interpolating.
53
+ def rendered_anchors
54
+ schema_anchors.gsub(%r{//[^\r\n]+}, "").gsub(/[\n\r]+/, "").gsub(
55
+ /^[\n\r]/, ""
56
+ )
57
+ end
58
+
59
+ def schema_anchors
60
+ <<~HEREDOC
61
+ // _fund_cons.liquid
62
+ [[#{schema_id}_funds]]
63
+
64
+ // _constants.liquid
65
+ {% if schema.constants.size > 0 %}
66
+ #{bookmark('constants')}
67
+ {% for thing in schema.constants %}
68
+ #{bookmark('{{thing.id}}')}
69
+ {% endfor %}
70
+ {% endif %}
71
+
72
+ // _types.liquid
73
+ {% if schema.types.size > 0 %}
74
+ #{bookmark('types')}
75
+ // _type.liquid
76
+ {% for thing in schema.types %}
77
+ #{bookmark('{{thing.id}}')}
78
+ {% if thing.items.size > 0 %}
79
+ // _type_items.liquid
80
+ #{bookmark('{{thing.id}}.items')}
81
+ {% for item in thing.items %}
82
+ #{bookmark('{{thing.id}}.items.{{item.id}}')}
83
+ {% endfor %}
84
+ {% endif %}
85
+ {% endfor %}
86
+ {% endif %}
87
+
88
+ // _entities.liquid
89
+ {% if schema.entities.size > 0 %}
90
+ #{bookmark('entities')}
91
+ {% for thing in schema.entities %}
92
+ // _entity.liquid
93
+ #{bookmark('{{thing.id}}')}
94
+ {% endfor %}
95
+ {% endif %}
96
+
97
+ // _subtype_constraints.liquid
98
+ {% if schema.subtype_constraints.size > 0 %}
99
+ #{bookmark('subtype_constraints')}
100
+ // _subtype_constraint.liquid
101
+ {% for thing in schema.subtype_constraints %}
102
+ #{bookmark('{{thing.id}}')}
103
+ {% endfor %}
104
+ {% endif %}
105
+
106
+ // _functions.liquid
107
+ {% if schema.functions.size > 0 %}
108
+ #{bookmark('functions')}
109
+ // _function.liquid
110
+ {% for thing in schema.functions %}
111
+ #{bookmark('{{thing.id}}')}
112
+ {% endfor %}
113
+ {% endif %}
114
+
115
+ // _procedures.liquid
116
+ {% if schema.procedures.size > 0 %}
117
+ #{bookmark('procedures')}
118
+ // _procedure.liquid
119
+ {% for thing in schema.procedures %}
120
+ #{bookmark('{{thing.id}}')}
121
+ {% endfor %}
122
+ {% endif %}
123
+
124
+ // _rules.liquid
125
+ {% if schema.rules.size > 0 %}
126
+ #{bookmark('rules')}
127
+ // _rule.liquid
128
+ {% for thing in schema.rules %}
129
+ #{bookmark('{{thing.id}}')}
130
+ {% endfor %}
131
+ {% endif %}
132
+ HEREDOC
133
+ end
134
+
135
+ def bookmark(anchor)
136
+ mangled = anchor.gsub("}}", ' | replace: "\", "-", "-}}')
137
+ "[[#{schema_id}.#{mangled}]]"
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Suma
4
+ module SchemaTemplate
5
+ # Emits a plain AsciiDoc body for a single EXPRESS schema, producing
6
+ # both HTML and XML outputs via Metanorma.
7
+ class Plain
8
+ EXTENSIONS = "xml,html"
9
+
10
+ attr_reader :schema_id
11
+
12
+ def initialize(schema_id)
13
+ @schema_id = schema_id
14
+ end
15
+
16
+ def extensions
17
+ EXTENSIONS
18
+ end
19
+
20
+ def render(path_to_schema_yaml)
21
+ <<~ADOC
22
+ = #{schema_id}
23
+ :lutaml-express-index: schemas; #{path_to_schema_yaml};
24
+ :bare: true
25
+ :mn-document-class: iso
26
+ :mn-output-extensions: #{extensions}
27
+
28
+ [lutaml_express_liquid,schemas,context]
29
+ ----
30
+ {% for schema in context.schemas %}
31
+
32
+ [%unnumbered]
33
+ == #{schema_id}
34
+
35
+ [source%unnumbered]
36
+ --
37
+ {{ schema.formatted }}
38
+ --
39
+ {% endfor %}
40
+ ----
41
+
42
+ ADOC
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Suma
4
+ # Pure renderers for the AsciiDoc source fed to Metanorma when compiling
5
+ # an EXPRESS schema to HTML/XML.
6
+ #
7
+ # Each template knows how to produce the adoc body for one compilation
8
+ # flavour (plain HTML, or HTML with cross-reference anchors). Templates
9
+ # have no I/O and no knowledge of the underlying ExpressSchema — they
10
+ # only need the schema id, because the rendered adoc is consumed by
11
+ # Liquid inside Metanorma, which fetches the schema by id from the
12
+ # surrounding lutaml-express-index.
13
+ #
14
+ # Composition with the compiler lives in SchemaCompiler.
15
+ module SchemaTemplate
16
+ autoload :Plain, "suma/schema_template/plain"
17
+ autoload :Document, "suma/schema_template/document"
18
+ end
19
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "report"
4
-
5
3
  module Suma
6
4
  module SvgQuality
7
5
  # Batch report wrapping multiple SVG quality reports
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Suma
4
+ module SvgQuality
5
+ module Formatters
6
+ autoload :TerminalFormatter,
7
+ "suma/svg_quality/formatters/terminal_formatter"
8
+ autoload :JsonFormatter, "suma/svg_quality/formatters/json_formatter"
9
+ autoload :YamlFormatter, "suma/svg_quality/formatters/yaml_formatter"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "svg_conform"
4
+
5
+ module Suma
6
+ module SvgQuality
7
+ # Deep module behind the SVG-quality seam: takes paths in, returns
8
+ # +Report+ / +BatchReport+ out. Owns validator construction and
9
+ # per-file result capture. Does not own file discovery, sorting,
10
+ # filtering, or presentation — those stay in the CLI adapter.
11
+ #
12
+ # The progress adapter is injected: pass any object responding to
13
+ # +#call(index, total, report)+. +NullProgress+ is the default
14
+ # no-op; the CLI passes a lambda that writes to +$stderr+.
15
+ class Scanner
16
+ DEFAULT_PROFILE = :metanorma
17
+
18
+ attr_reader :profile, :progress
19
+
20
+ def initialize(profile: DEFAULT_PROFILE,
21
+ progress: NullProgress.new)
22
+ @profile = profile
23
+ @progress = progress
24
+ end
25
+
26
+ def scan(paths)
27
+ validator = build_validator
28
+ reports = paths.each_with_index.map do |path, index|
29
+ scan_one(validator, path, index, paths.size)
30
+ end
31
+ BatchReport.new(reports)
32
+ end
33
+
34
+ def scan_file(path)
35
+ Report.new(path.to_s, build_validator.validate_file(path.to_s,
36
+ profile: profile))
37
+ end
38
+
39
+ # Default no-op progress adapter. Real progress reporters are
40
+ # passed by the caller; this satisfies the same interface so the
41
+ # scanner can be invoked from specs without forcing the
42
+ # dependency.
43
+ class NullProgress
44
+ def call(_index, _total, _report); end
45
+ end
46
+
47
+ private
48
+
49
+ def build_validator
50
+ SvgConform::Validator.new
51
+ end
52
+
53
+ def scan_one(validator, path, index, total)
54
+ result = validator.validate_file(path.to_s, profile: profile)
55
+ report = Report.new(path.to_s, result)
56
+ progress.call(index, total, report)
57
+ report
58
+ end
59
+ end
60
+ end
61
+ end
@@ -4,7 +4,10 @@ require "svg_conform"
4
4
 
5
5
  module Suma
6
6
  module SvgQuality
7
- # Quality levels computed from error count
7
+ autoload :Report, "suma/svg_quality/report"
8
+ autoload :BatchReport, "suma/svg_quality/batch_report"
9
+ autoload :Scanner, "suma/svg_quality/scanner"
10
+
8
11
  module QualityTiers
9
12
  CRITICAL = { name: :critical, min_errors: 200, emoji: "💥" }.freeze
10
13
  HIGH = { name: :high, min_errors: 100, emoji: "🔴" }.freeze
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Suma
4
+ # Term-extractor-specific classification of an EXPRESS schema.
5
+ #
6
+ # Bridges ExpressSchema::Type (the canonical classification shared
7
+ # across the codebase) and the Glossarist-specific labels
8
+ # TermExtractor emits: the domain string ("application module" /
9
+ # "resource") that goes into a concept's +domain+ field, and the
10
+ # entity-type URN term used in generated concept definitions.
11
+ #
12
+ # The mapping is data — a frozen Hash keyed by ExpressSchema::Type
13
+ # symbol — so adding a new schema type is a one-line addition to
14
+ # BY_TYPE (open/closed principle). The previous implementation
15
+ # switched on string keys in three separate places; this consolidates
16
+ # them into one source of truth.
17
+ class TermClassification
18
+ attr_reader :type, :domain_label, :entity_term, :entity_display
19
+
20
+ def initialize(type:, domain_label:, entity_term:, entity_display:)
21
+ @type = type
22
+ @domain_label = domain_label
23
+ @entity_term = entity_term
24
+ @entity_display = entity_display
25
+ freeze
26
+ end
27
+
28
+ def domain_for(schema_id)
29
+ "#{domain_label}: #{schema_id}"
30
+ end
31
+
32
+ BY_TYPE = {
33
+ ExpressSchema::Type::RESOURCE => new(
34
+ type: ExpressSchema::Type::RESOURCE,
35
+ domain_label: "resource",
36
+ entity_term: "express-language.entity_data_type",
37
+ entity_display: "entity data type",
38
+ ),
39
+ ExpressSchema::Type::MODULE_ARM => new(
40
+ type: ExpressSchema::Type::MODULE_ARM,
41
+ domain_label: "application module",
42
+ entity_term: "general.application_object",
43
+ entity_display: "application object",
44
+ ),
45
+ ExpressSchema::Type::MODULE_MIM => new(
46
+ type: ExpressSchema::Type::MODULE_MIM,
47
+ domain_label: "application module",
48
+ entity_term: "express-language.entity_data_type",
49
+ entity_display: "entity data type",
50
+ ),
51
+ ExpressSchema::Type::BUSINESS_OBJECT_MODEL => new(
52
+ type: ExpressSchema::Type::BUSINESS_OBJECT_MODEL,
53
+ domain_label: "resource",
54
+ entity_term: "express-language.entity_data_type",
55
+ entity_display: "entity data type",
56
+ ),
57
+ ExpressSchema::Type::CORE_MODEL => new(
58
+ type: ExpressSchema::Type::CORE_MODEL,
59
+ domain_label: "resource",
60
+ entity_term: "express-language.entity_data_type",
61
+ entity_display: "entity data type",
62
+ ),
63
+ ExpressSchema::Type::STANDALONE => new(
64
+ type: ExpressSchema::Type::STANDALONE,
65
+ domain_label: "resource",
66
+ entity_term: "express-language.entity_data_type",
67
+ entity_display: "entity data type",
68
+ ),
69
+ }.freeze
70
+
71
+ def self.for_schema(id:, path:)
72
+ type = ExpressSchema::Type.classify(id: id, path: path)
73
+ BY_TYPE.fetch(type) do |t|
74
+ raise Error, "[suma] no term classification for type #{t.inspect}"
75
+ end
76
+ end
77
+ end
78
+ end