suma 0.5.3 → 0.5.5

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: bd20e6823c86b989284cda93b4edf57c6ff7d16520e4a35fb111ee7df94c45e2
4
- data.tar.gz: cd8763148b716e7ee6b226f4306fb4f33c825b9d6139978fb8f633496164b319
3
+ metadata.gz: 3924d8ea8d4f6e7dd9d94226840b4d6135606edfeffca7198d71d26b59f53208
4
+ data.tar.gz: adce8f15063a18626c2b7828e746078f2e1e588594e393aaa122e81144fd3ea0
5
5
  SHA512:
6
- metadata.gz: ceb4f07c65cf174b04c0511d0f5cf9c0b753de5d1e55cf14c87d98276f4c11518d6c9d3367c4804eb7a7c0368f08ece16f36dc26fe07e6492ec24d07fc20ea85
7
- data.tar.gz: d8f38fa09779fb06f5c8d6d0da109e6f3edf11be8dfb00b382bb0aba5a1c5e03cb4da981e8ea45a06cd6bebec093a6e50fa6acdb10d19013c94d517e5a286e1b
6
+ metadata.gz: f602953242571ffe779c059b4f5e16120fe08e6fa196dad10e7f095369ebe642afd7bc91d5f404ebadb4b8e48378bfca366c3f62697c001c2e2b0823ba81f1b8
7
+ data.tar.gz: 16b6596fe1658ccbb7ffa6b4d08969bf1ae9442835a498f99bb2bf756f713576b547dfb422741306753facd7a01fa3d790ff137f6b03f8a79b3a761427eddb09
data/Gemfile CHANGED
@@ -13,10 +13,25 @@ gem "canon"
13
13
  # dropped: CI builds against a stable released metanorma, not a floating branch.
14
14
  # Flavor gem needed to compile the dummy-collection integration fixture
15
15
  # (spec/fixtures/dummy_collection, suma#107).
16
- gem "metanorma-iso"
16
+ # metanorma-iso releases (<= 3.5.0) require "pubid-iso"/"pubid-cen"/
17
+ # "pubid-iec" (pubid 1.15 split gems) that do not exist on the relaton-3
18
+ # chain, and whose lib/pubid/*.rb collide with the pubid 2 monolith's.
19
+ # The pubid-2 migration is on metanorma-iso main; it needs the unreleased
20
+ # metanorma-standoc "Standoc::Document" split, so pin that from main too.
21
+ # Flip both to released gems once metanorma-iso releases the migration.
22
+ gem "metanorma-iso", github: "metanorma/metanorma-iso", branch: "main"
23
+ gem "metanorma-standoc", github: "metanorma/metanorma-standoc", branch: "main"
24
+ # metanorma#607: Collection#bibitem must drop localized <edition> variants
25
+ # (metanorma-iso main emits them) before the relaton parse.
26
+ gem "metanorma", github: "metanorma/metanorma",
27
+ branch: "fix/collection-bibdata-localized-edition"
28
+ # iso/standoc main also use unreleased metanorma-document 0.5.x model
29
+ # classes (e.g. Components::Inline::TermrefElement).
30
+ gem "metanorma-document", github: "metanorma/metanorma-document", branch: "main"
17
31
  # gem "metanorma-plugin-lutaml", github: "metanorma/metanorma-plugin-lutaml", branch: "main"
18
32
  # gem "metanorma-standoc", github: "metanorma/metanorma-standoc", branch: "main"
19
33
  # gem "expressir", github: "lutaml/expressir", branch: "main"
34
+ gem "expressir", "2.4.27"
20
35
  gem "nokogiri"
21
36
  gem "openssl", "~> 3.0"
22
37
  gem "rake"
@@ -25,3 +40,11 @@ gem "rubocop"
25
40
  gem "rubocop-performance"
26
41
  gem "rubocop-rake"
27
42
  gem "rubocop-rspec"
43
+ # isodoc 3.7.3 `require`s "sassc-embedded" while rendering HTML but does
44
+ # not declare it.
45
+ gem "sassc-embedded", "~> 1.0"
46
+ # relaton-render's floor (>= 2.0.0.pre.alpha.6) admits the stale June
47
+ # 2.2.0.pre prerelease, which predates lutaml-model 0.8.78's strict
48
+ # collection checks (duplicate <edition> raise). 2.1.9 is the current
49
+ # lutaml-model-0.8 line.
50
+ gem "relaton-bib", "2.1.9"
@@ -10,6 +10,9 @@ module Suma
10
10
  # indexing, validation) lives in the deep module and is reachable
11
11
  # from specs without invoking Thor.
12
12
  class Validate < Thor
13
+ def self.exit_on_failure?
14
+ true
15
+ end
13
16
  desc "links SCHEMAS_FILE DOCUMENTS_PATH [OUTPUT_FILE]",
14
17
  "Extract and validate express links without creating intermediate file"
15
18
  def links(schemas_file = "schemas-srl.yml",
@@ -22,6 +25,52 @@ module Suma
22
25
  ).call
23
26
  puts LinkValidation.generate_summary(result)
24
27
  end
28
+
29
+ desc "mappings DOCUMENTS_PATH",
30
+ "Validate module mapping.yaml links and reference paths " \
31
+ "against the parsed module corpus; only NEW drift " \
32
+ "(not in the baseline) fails"
33
+ option :baseline, type: :string, default: nil,
34
+ desc: "Baseline YAML path (default: " \
35
+ "mapping-drift-baseline.yml under DOCUMENTS_PATH)"
36
+ option :write_baseline, type: :boolean, default: false,
37
+ desc: "Write the scanned drift as the new " \
38
+ "baseline instead of checking"
39
+ def mappings(documents_path = "documents")
40
+ drift = MappingDrift.new(documents_path)
41
+ path = options[:baseline] || default_baseline_path(documents_path)
42
+
43
+ return write_baseline(drift, path) if options[:write_baseline]
44
+
45
+ assert_baseline!(path)
46
+ fail_on_new_drift(*drift.check(path), path)
47
+ end
48
+
49
+ private
50
+
51
+ def default_baseline_path(documents_path)
52
+ File.join(documents_path, "mapping-drift-baseline.yml")
53
+ end
54
+
55
+ def assert_baseline!(path)
56
+ return if File.file?(path)
57
+
58
+ raise Thor::Error, "no baseline at #{path} — generate one with " \
59
+ "--write-baseline and commit it"
60
+ end
61
+
62
+ def fail_on_new_drift(report, fresh, path)
63
+ say "#{report.entries.size} drift entr(ies), #{fresh.size} new vs #{path}"
64
+ return if fresh.empty?
65
+
66
+ raise Thor::Error, "#{fresh.size} new mapping drift:\n#{fresh.join("\n")}"
67
+ end
68
+
69
+ def write_baseline(drift, path)
70
+ report = drift.scan
71
+ File.write(path, report.to_yaml)
72
+ say "baseline written: #{path} (#{report.entries.size} entries)"
73
+ end
25
74
  end
26
75
  end
27
76
  end
@@ -127,13 +127,28 @@ module Suma
127
127
  def validate_links(schemas_config, links_by_file)
128
128
  paths_by_id = schemas_config.schemas.to_h { |s| [s.id, s.path] }
129
129
  progress.start("Loading schemas", paths_by_id.size)
130
- repo = Expressir::Express::Parser.from_files(paths_by_id.values) do |*_args|
131
- progress.increment
132
- end
130
+ repo = load_repository(paths_by_id.values)
133
131
  index = SchemaIndex.new(repo)
134
132
  LinkValidator.new(index).validate(links_by_file)
135
133
  end
136
134
 
135
+ # Compiled-set warm start (expressir TODO.suma-improvements/04),
136
+ # opt-in via SUMA_COMPILED_SET=1: parse the closure once into an
137
+ # EXSCS1 artifact beside the output and hydrate from it on
138
+ # subsequent runs.
139
+ def load_repository(paths)
140
+ return Expressir::Express::Parser.from_files(paths) unless ENV["SUMA_COMPILED_SET"] == "1"
141
+
142
+ dir = output_file ? output_file.dirname : Pathname.new(Dir.pwd)
143
+ set_path = File.join(dir, "schema-closure.exscs")
144
+ if File.file?(set_path)
145
+ return Expressir::Express::LazyRepository.new(set_path).to_eager
146
+ end
147
+
148
+ FileUtils.mkdir_p(dir)
149
+ Expressir::Express::Parser.from_files(paths, compiled_set: set_path)
150
+ end
151
+
137
152
  def write_summary(result)
138
153
  FileUtils.mkdir_p(output_file.dirname)
139
154
  File.write(output_file, self.class.generate_summary(result))
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "expressir"
5
+
6
+ module Suma
7
+ # Mapping validation surfaced in builds (expressir
8
+ # TODO.suma-improvements/05): validate every module mapping.yaml —
9
+ # its <<express:...>> links and reference paths — against the parsed
10
+ # module corpus, and compare the drift against a committed baseline
11
+ # so only NEW drift fails a build.
12
+ #
13
+ # Owns: module discovery, the shared repository parse, per-module
14
+ # validation, and the baseline comparison. Does not own: presentation
15
+ # and exit codes (the CLI adapter), or the validation rules
16
+ # (Expressir::Mapping does).
17
+ class MappingDrift
18
+ # One drift finding; +key+ is the stable identity compared across
19
+ # runs: "<module path>|<kind>|<detail>".
20
+ class Entry < Lutaml::Model::Serializable
21
+ attribute :key, :string
22
+
23
+ key_value do
24
+ map "key", to: :key
25
+ end
26
+ end
27
+
28
+ # Serializable drift report / baseline (same wire shape).
29
+ class Report < Lutaml::Model::Serializable
30
+ attribute :entries, Entry, collection: true
31
+
32
+ key_value do
33
+ map "entries", to: :entries
34
+ end
35
+
36
+ def keys
37
+ entries.map(&:key)
38
+ end
39
+ end
40
+
41
+ MAPPING_GLOB = "**/mapping.yaml"
42
+ MODULE_SCHEMAS = %w[arm.exp mim.exp].freeze
43
+ private_constant :MAPPING_GLOB, :MODULE_SCHEMAS
44
+
45
+ attr_reader :documents_path, :stepmod
46
+
47
+ # +stepmod+ overrides where closure schema resolution looks up
48
+ # USE/REFERENCE dependencies (STEPmod `schemas/**/<name>.exp`
49
+ # layout); it defaults to +documents_path+ itself.
50
+ def initialize(documents_path, stepmod: nil)
51
+ @documents_path = Pathname.new(documents_path).expand_path
52
+ @stepmod = stepmod
53
+ end
54
+
55
+ # Every module mapping under documents_path that has arm/mim
56
+ # schemas next to it.
57
+ def discover
58
+ Dir[documents_path.join(MAPPING_GLOB).to_s].filter_map do |mapping|
59
+ schemas = module_schemas(File.dirname(mapping))
60
+ { mapping: mapping, schemas: schemas } if schemas.any?
61
+ end
62
+ end
63
+
64
+ # Validate every discovered module against one shared repository.
65
+ # @return [Report]
66
+ def scan
67
+ modules = discover
68
+ return Report.new(entries: []) if modules.empty?
69
+
70
+ paths, gaps = closure(modules)
71
+ repository = Expressir::Express::Parser.from_files(paths)
72
+ Report.new(entries: gaps + modules.flat_map do |mod|
73
+ drift_for(mod, repository)
74
+ end)
75
+ end
76
+
77
+ # Scan and compare against the baseline file.
78
+ # @return [[Report, Array<String>]] the full report and the keys
79
+ # not present in the baseline (the NEW drift).
80
+ def check(baseline_path)
81
+ known = Report.from_yaml(File.read(baseline_path)).keys
82
+ report = scan
83
+ [report, report.keys - known]
84
+ end
85
+
86
+ private
87
+
88
+ def module_schemas(dir)
89
+ MODULE_SCHEMAS
90
+ .map { |name| File.join(dir, name) }
91
+ .select { |path| File.file?(path) }
92
+ end
93
+
94
+ # The repository covers every module schema plus its transitive
95
+ # USE/REFERENCE closure — reference paths name resource-schema
96
+ # types the modules themselves never declare. A schema the corpus
97
+ # does not carry (e.g. external PLIB templates) becomes a closure
98
+ # gap entry instead of aborting the whole scan; the affected
99
+ # module still validates against its own schemas.
100
+ def closure(modules)
101
+ paths = []
102
+ gaps = []
103
+ modules.each do |mod|
104
+ mod[:schemas].each do |root|
105
+ paths.concat(closure_of(root, mod[:mapping], gaps))
106
+ end
107
+ end
108
+ [paths.compact.uniq, gaps]
109
+ end
110
+
111
+ def closure_of(root, mapping_path, gaps)
112
+ Expressir::Commands::ParityInputs.closure_paths(root,
113
+ stepmod: stepmod_root)
114
+ rescue StandardError => e
115
+ gaps << Entry.new(key: entry_key(mapping_path, "closure", e.message))
116
+ [root]
117
+ end
118
+
119
+ def stepmod_root
120
+ stepmod || documents_path.to_s
121
+ end
122
+
123
+ def drift_for(mod, repository)
124
+ mapping_path = mod[:mapping]
125
+ begin
126
+ document = Expressir::Mapping.load_file(mapping_path)
127
+ rescue StandardError => e
128
+ return [Entry.new(key: entry_key(mapping_path, "load", e.message))]
129
+ end
130
+
131
+ unknown = Expressir::Mapping.unknown_links(document, repository)
132
+ unknown.map do |link|
133
+ Entry.new(key: entry_key(mapping_path, "unknown_link", link.text))
134
+ end + refpath_issues(mapping_path, document, repository)
135
+ end
136
+
137
+ def refpath_issues(mapping_path, document, repository)
138
+ Expressir::Mapping.refpaths(document).flat_map do |location, content|
139
+ parsed = Expressir::Mapping::RefPath.parse(content)
140
+ parse_error_entries(mapping_path, location, parsed) +
141
+ validate_entries(mapping_path, location, parsed, repository)
142
+ end
143
+ end
144
+
145
+ def parse_error_entries(mapping_path, location, parsed)
146
+ parsed.parse_errors.map do |error|
147
+ Entry.new(key: entry_key(mapping_path, "refpath",
148
+ "#{location}: #{error}"))
149
+ end
150
+ end
151
+
152
+ def validate_entries(mapping_path, location, parsed, repository)
153
+ Expressir::Mapping::RefPath.validate(parsed, repository).map do |issue|
154
+ detail = "#{location} [step #{issue.step}]: #{issue.message}"
155
+ Entry.new(key: entry_key(mapping_path, "refpath", detail))
156
+ end
157
+ end
158
+
159
+ def entry_key(mapping_path, kind, detail)
160
+ "#{relative(mapping_path)}|#{kind}|#{detail}"
161
+ end
162
+
163
+ def relative(path)
164
+ Pathname.new(path).expand_path.relative_path_from(documents_path).to_s
165
+ end
166
+ end
167
+ end
@@ -35,9 +35,9 @@ module Suma
35
35
  [%unnumbered,type=express]
36
36
  == #{schema_id} #{rendered_anchors}
37
37
 
38
- [source%unnumbered]
38
+ [source%unnumbered,subs="+macros"]
39
39
  --
40
- {{ schema.formatted }}
40
+ {{ schema.formatted_hyperlinked_adoc }}
41
41
  --
42
42
  {% endfor %}
43
43
  ----
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.5.3"
4
+ VERSION = "0.5.5"
5
5
  end
data/lib/suma.rb CHANGED
@@ -16,6 +16,7 @@ module Suma
16
16
  autoload :LinkValidation, "suma/link_validation"
17
17
  autoload :LinkValidator, "suma/link_validator"
18
18
  autoload :ManifestTraverser, "suma/manifest_traverser"
19
+ autoload :MappingDrift, "suma/mapping_drift"
19
20
  autoload :NoteProcessor, "suma/note_processor"
20
21
  autoload :NullCache, "suma/null_cache"
21
22
  autoload :RegisterManifestGenerator, "suma/register_manifest_generator"
data/suma.gemspec CHANGED
@@ -40,7 +40,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
40
40
  spec.add_dependency "plurimath"
41
41
  spec.add_dependency "ruby-progressbar"
42
42
  spec.add_dependency "rubyzip"
43
- spec.add_dependency "svg_conform", "~> 0.1.0"
43
+ spec.add_dependency "svg_conform", ">= 0.1.0"
44
44
  spec.add_dependency "table_tennis"
45
45
  spec.add_dependency "thor", ">= 0.20"
46
46
 
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.5.3
4
+ version: 0.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-18 00:00:00.000000000 Z
11
+ date: 2026-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: expressir
@@ -118,14 +118,14 @@ dependencies:
118
118
  name: svg_conform
119
119
  requirement: !ruby/object:Gem::Requirement
120
120
  requirements:
121
- - - "~>"
121
+ - - ">="
122
122
  - !ruby/object:Gem::Version
123
123
  version: 0.1.0
124
124
  type: :runtime
125
125
  prerelease: false
126
126
  version_requirements: !ruby/object:Gem::Requirement
127
127
  requirements:
128
- - - "~>"
128
+ - - ">="
129
129
  - !ruby/object:Gem::Version
130
130
  version: 0.1.0
131
131
  - !ruby/object:Gem::Dependency
@@ -210,6 +210,7 @@ files:
210
210
  - lib/suma/link_validator.rb
211
211
  - lib/suma/link_validator/step.rb
212
212
  - lib/suma/manifest_traverser.rb
213
+ - lib/suma/mapping_drift.rb
213
214
  - lib/suma/note_processor.rb
214
215
  - lib/suma/null_cache.rb
215
216
  - lib/suma/processor.rb