suma 0.5.2 → 0.5.4
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 +4 -4
- data/.github/workflows/release.yml +6 -10
- data/Gemfile +31 -5
- data/lib/suma/cli/convert_jsdai.rb +2 -4
- data/lib/suma/cli/validate.rb +49 -0
- data/lib/suma/express_schema.rb +2 -2
- data/lib/suma/jsdai/figure.rb +5 -10
- data/lib/suma/jsdai/figure_image.rb +1 -2
- data/lib/suma/link_validation.rb +18 -3
- data/lib/suma/mapping_drift.rb +167 -0
- data/lib/suma/null_cache.rb +11 -0
- data/lib/suma/schema_cache.rb +66 -0
- data/lib/suma/schema_collection.rb +5 -3
- data/lib/suma/schema_exporter.rb +38 -6
- data/lib/suma/svg_quality/formatters.rb +2 -2
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +3 -0
- data/suma.gemspec +2 -2
- metadata +11 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f6a70a78b69206429eb11f3798c6c7f6e0fcfb2a3c46fd2dcea9e605b9ed230f
|
|
4
|
+
data.tar.gz: 8ba4c119da7747d212a4e1e85d13a4cbf0191cc780566f4f8a0fbca0529eb91a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b695befb4db91a60adbb4b43739367e5187d9e07b4c97610b0843886cfbed4bc53ce6ac7ce43764fa27e9f6d9c46db9b57dfc2e298944429524b9fe7abbdd4b
|
|
7
|
+
data.tar.gz: d8c3bc865349f54b50425b130f527b863442d43a50cdafe820a0fa37dad103300b8cfd2e0c3fbf5304039e1af592db2b8d1cd03f3e0bbe8db7ccce1f55f9f9d4
|
|
@@ -14,21 +14,17 @@ on:
|
|
|
14
14
|
repository_dispatch:
|
|
15
15
|
types: [ do-release ]
|
|
16
16
|
|
|
17
|
+
# Least-privilege ceiling for the called rubygems-release.yml: its release job
|
|
18
|
+
# needs contents:write (git tag push) and id-token:write (OIDC Trusted Publishing).
|
|
19
|
+
permissions:
|
|
20
|
+
contents: write
|
|
21
|
+
id-token: write
|
|
22
|
+
|
|
17
23
|
jobs:
|
|
18
24
|
release:
|
|
19
25
|
uses: metanorma/ci/.github/workflows/rubygems-release.yml@main
|
|
20
26
|
with:
|
|
21
27
|
next_version: ${{ github.event.inputs.next_version }}
|
|
22
|
-
# Explicit release_command needed since metanorma/ci#314 deprecated
|
|
23
|
-
# `bundler_cache` (hardcoded to false) — the reusable's release job no
|
|
24
|
-
# longer runs `bundle install` implicitly via ruby/setup-ruby, so the
|
|
25
|
-
# default `bundle exec rake release` fires against uninstalled gems.
|
|
26
|
-
# Surfaced by kwkwan on metanorma-plugin-lutaml#285. Deeper fix
|
|
27
|
-
# (bundle install inside rubygems-release.yml's release job) tracked
|
|
28
|
-
# separately.
|
|
29
|
-
release_command: |
|
|
30
|
-
bundle install
|
|
31
|
-
bundle exec rake release
|
|
32
28
|
secrets:
|
|
33
29
|
rubygems-api-key: ${{ secrets.METANORMA_CI_RUBYGEMS_API_KEY }}
|
|
34
30
|
pat_token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}
|
data/Gemfile
CHANGED
|
@@ -6,13 +6,17 @@ source "https://rubygems.org"
|
|
|
6
6
|
gemspec
|
|
7
7
|
|
|
8
8
|
gem "canon"
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
# metanorma resolves from the gemspec constraint (~> 2.3) to the released gem.
|
|
10
|
+
# The staged-build render options (preserve_unresolved:/artifact_store_dir:/
|
|
11
|
+
# reinflate:, metanorma#578) landed in metanorma 2.5.0, now on rubygems, so the
|
|
12
|
+
# former feature-branch override (feature/collection-incremental-resumable) is
|
|
13
|
+
# dropped: CI builds against a stable released metanorma, not a floating branch.
|
|
14
|
+
# Flavor gem needed to compile the dummy-collection integration fixture
|
|
15
|
+
# (spec/fixtures/dummy_collection, suma#107).
|
|
16
|
+
gem "metanorma-iso"
|
|
14
17
|
# gem "metanorma-plugin-lutaml", github: "metanorma/metanorma-plugin-lutaml", branch: "main"
|
|
15
18
|
# gem "metanorma-standoc", github: "metanorma/metanorma-standoc", branch: "main"
|
|
19
|
+
# gem "expressir", github: "lutaml/expressir", branch: "main"
|
|
16
20
|
gem "nokogiri"
|
|
17
21
|
gem "openssl", "~> 3.0"
|
|
18
22
|
gem "rake"
|
|
@@ -21,3 +25,25 @@ gem "rubocop"
|
|
|
21
25
|
gem "rubocop-performance"
|
|
22
26
|
gem "rubocop-rake"
|
|
23
27
|
gem "rubocop-rspec"
|
|
28
|
+
# Expressir >= 2.4.4 passes the lutaml-model `serialize:` mapping
|
|
29
|
+
# kwarg, which needs lutaml-model >= 0.8.50, which needs rubyzip ~> 3,
|
|
30
|
+
# which needs glossarist >= 2.14, which needs relaton ~> 3.0.0.pre.alpha
|
|
31
|
+
# — a chain no RELEASED metanorma accepts. Left unpinned, the resolver
|
|
32
|
+
# lands on expressir 2.4.5 (whose gemspec under-declares its
|
|
33
|
+
# lutaml-model floor) + lutaml-model 0.8.29, a pair that crashes at
|
|
34
|
+
# load with `unknown keyword: :serialize`. Pin the last released
|
|
35
|
+
# expressir that works on this chain; lift once a released metanorma
|
|
36
|
+
# accepts relaton 3.
|
|
37
|
+
gem "expressir", "2.4.3"
|
|
38
|
+
|
|
39
|
+
# Metanorma 2.5.5 was released referencing Metanorma::Core::Flavors,
|
|
40
|
+
# which only exists in unreleased metanorma-core; its gemspec
|
|
41
|
+
# under-declares the metanorma-core floor, so fresh resolves crash at
|
|
42
|
+
# Metanorma::Collection.parse. Stay on the last consistent release
|
|
43
|
+
# until metanorma-core ships.
|
|
44
|
+
gem "metanorma", "< 2.5.5"
|
|
45
|
+
|
|
46
|
+
# metanorma-plugin-lutaml 0.7.52+ calls Expressir::Express::LazyRepository
|
|
47
|
+
# (expressir >= 2.4.20) but its gemspec still allows expressir >= 2.3.5,
|
|
48
|
+
# breaking builds on the pinned expressir line above.
|
|
49
|
+
gem "metanorma-plugin-lutaml", "< 0.7.52"
|
|
@@ -10,7 +10,7 @@ module Suma
|
|
|
10
10
|
desc "convert_jsdai XML_FILE IMAGE_FILE OUTPUT_DIR",
|
|
11
11
|
"Convert JSDAI XML and image files to SVG and EXP files"
|
|
12
12
|
|
|
13
|
-
# rubocop:disable Metrics/MethodLength
|
|
13
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
14
14
|
def convert_jsdai(xml_file, image_file, output_dir)
|
|
15
15
|
xml_file = File.expand_path(xml_file)
|
|
16
16
|
image_file = File.expand_path(image_file)
|
|
@@ -34,11 +34,10 @@ module Suma
|
|
|
34
34
|
|
|
35
35
|
run(xml_file, image_file, output_dir)
|
|
36
36
|
end
|
|
37
|
-
# rubocop:enable Metrics/MethodLength
|
|
38
37
|
|
|
39
38
|
private
|
|
40
39
|
|
|
41
|
-
# rubocop:disable Metrics/MethodLength
|
|
40
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
42
41
|
def run(xml_file, image_file, output_dir)
|
|
43
42
|
FileUtils.mkdir_p(output_dir)
|
|
44
43
|
|
|
@@ -57,7 +56,6 @@ module Suma
|
|
|
57
56
|
|
|
58
57
|
puts "Conversion complete."
|
|
59
58
|
end
|
|
60
|
-
# rubocop:enable Metrics/MethodLength
|
|
61
59
|
end
|
|
62
60
|
end
|
|
63
61
|
end
|
data/lib/suma/cli/validate.rb
CHANGED
|
@@ -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
|
data/lib/suma/express_schema.rb
CHANGED
|
@@ -64,7 +64,7 @@ module Suma
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def to_plain
|
|
67
|
-
parsed.to_s
|
|
67
|
+
parsed.to_s
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def filename_plain
|
|
@@ -92,7 +92,7 @@ module Suma
|
|
|
92
92
|
|
|
93
93
|
FileUtils.mkdir_p(File.dirname(filename_plain))
|
|
94
94
|
|
|
95
|
-
content = with_annotations ? parsed.
|
|
95
|
+
content = with_annotations ? parsed.format(no_remarks: false) : to_plain
|
|
96
96
|
File.write(filename_plain, content)
|
|
97
97
|
end
|
|
98
98
|
|
data/lib/suma/jsdai/figure.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Suma
|
|
|
12
12
|
@xml_file = xml_file
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
# rubocop:disable Metrics/MethodLength
|
|
15
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
16
16
|
def to_svg
|
|
17
17
|
width, height = @image.dimensions
|
|
18
18
|
svg_parts = []
|
|
@@ -46,9 +46,8 @@ module Suma
|
|
|
46
46
|
svg_parts << "\n\t\t</svg>"
|
|
47
47
|
svg_parts.join
|
|
48
48
|
end
|
|
49
|
-
# rubocop:enable Metrics/MethodLength
|
|
50
49
|
|
|
51
|
-
# rubocop:disable Metrics/MethodLength
|
|
50
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
52
51
|
def to_exp
|
|
53
52
|
basename = File.basename(@xml_file, ".xml")
|
|
54
53
|
schema_name = extract_schema_name(basename)
|
|
@@ -73,11 +72,10 @@ module Suma
|
|
|
73
72
|
exp_parts << "\n*)\n"
|
|
74
73
|
exp_parts.join
|
|
75
74
|
end
|
|
76
|
-
# rubocop:enable Metrics/MethodLength
|
|
77
75
|
|
|
78
76
|
private
|
|
79
77
|
|
|
80
|
-
# rubocop:disable Metrics/MethodLength
|
|
78
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
81
79
|
def generate_shape_element(area)
|
|
82
80
|
shape_attrs = []
|
|
83
81
|
shape_attrs << 'onmouseout="this.style.opacity=0" '
|
|
@@ -108,7 +106,6 @@ module Suma
|
|
|
108
106
|
"<rect #{shape_attrs.join}"
|
|
109
107
|
end
|
|
110
108
|
end
|
|
111
|
-
# rubocop:enable Metrics/MethodLength
|
|
112
109
|
|
|
113
110
|
def parse_rect_coords(coords_str)
|
|
114
111
|
parts = coords_str.split(",").map(&:to_i)
|
|
@@ -140,7 +137,7 @@ module Suma
|
|
|
140
137
|
end
|
|
141
138
|
end
|
|
142
139
|
|
|
143
|
-
# rubocop:disable Metrics/MethodLength
|
|
140
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
144
141
|
def extract_schema_name(basename)
|
|
145
142
|
# For module schemas like "armexpg1" with module="activity"
|
|
146
143
|
# Result should be "Activity_arm"
|
|
@@ -159,9 +156,8 @@ module Suma
|
|
|
159
156
|
basename.sub(/expg\d+$/, "")
|
|
160
157
|
end
|
|
161
158
|
end
|
|
162
|
-
# rubocop:enable Metrics/MethodLength
|
|
163
159
|
|
|
164
|
-
# rubocop:disable Metrics/MethodLength
|
|
160
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
165
161
|
def extract_target_from_href(href)
|
|
166
162
|
# Extract the target from href
|
|
167
163
|
# Type 1: "../../resources/action_schema/action_schema.xml#action_schema.as_name_attribute_select"
|
|
@@ -201,7 +197,6 @@ module Suma
|
|
|
201
197
|
href
|
|
202
198
|
end
|
|
203
199
|
end
|
|
204
|
-
# rubocop:enable Metrics/MethodLength
|
|
205
200
|
end
|
|
206
201
|
end
|
|
207
202
|
end
|
|
@@ -58,7 +58,7 @@ module Suma
|
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
# rubocop:disable Metrics/MethodLength
|
|
61
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
62
62
|
def extract_jpeg_dimensions
|
|
63
63
|
# Read JPEG file to extract dimensions
|
|
64
64
|
# JPEG uses markers, we look for SOF (Start of Frame) markers
|
|
@@ -90,7 +90,6 @@ module Suma
|
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
|
-
# rubocop:enable Metrics/MethodLength
|
|
94
93
|
end
|
|
95
94
|
end
|
|
96
95
|
end
|
data/lib/suma/link_validation.rb
CHANGED
|
@@ -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 =
|
|
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
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
# Null-object cache used when schema caching is disabled: every fetch misses
|
|
5
|
+
# and nothing is stored, so callers need no conditional guards around a cache.
|
|
6
|
+
class NullCache
|
|
7
|
+
def fetch(*, **) = nil
|
|
8
|
+
|
|
9
|
+
def store(*, **) = nil
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Suma
|
|
7
|
+
# Content-addressed, on-disk cache of *generated* EXPRESS schema output --
|
|
8
|
+
# the plain (annotations stripped, ISO copyright kept) or annotated `.exp`
|
|
9
|
+
# text produced from a source schema.
|
|
10
|
+
#
|
|
11
|
+
# The cache key composes the source bytes, the Expressir version, and the
|
|
12
|
+
# annotation mode. Those three fully determine the output (Expressir's
|
|
13
|
+
# serialiser is deterministic), so a hit is guaranteed byte-identical to a
|
|
14
|
+
# fresh generation -- and the store can be shared across builds and git
|
|
15
|
+
# worktrees: identical source content hits regardless of which build wrote it.
|
|
16
|
+
#
|
|
17
|
+
# Supersedes the parsed-AST `Marshal` approach of metanorma/suma#35, which was
|
|
18
|
+
# brittle across Ruby/Expressir versions and cached the wrong artifact (the
|
|
19
|
+
# parse, not the output).
|
|
20
|
+
class SchemaCache
|
|
21
|
+
def initialize(directory)
|
|
22
|
+
@directory = directory.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The cached output for +source+, or +nil+ on a miss.
|
|
26
|
+
def fetch(source, annotations:)
|
|
27
|
+
path = entry_path(source, annotations: annotations)
|
|
28
|
+
return unless File.exist?(path)
|
|
29
|
+
|
|
30
|
+
File.read(path, encoding: "UTF-8")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Cache +content+ as the output for +source+. Returns the entry path.
|
|
34
|
+
def store(source, annotations:, content:)
|
|
35
|
+
path = entry_path(source, annotations: annotations)
|
|
36
|
+
FileUtils.mkdir_p(@directory)
|
|
37
|
+
write_atomically(path, content)
|
|
38
|
+
path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def entry_path(source, annotations:)
|
|
44
|
+
File.join(@directory, key(source, annotations: annotations))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def key(source, annotations:)
|
|
48
|
+
mode = annotations ? "annotated" : "plain"
|
|
49
|
+
"#{Digest::SHA256.hexdigest(source)}-#{expressir_version}-#{mode}.exp"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def expressir_version
|
|
53
|
+
@expressir_version ||= Expressir::VERSION
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Write through a sibling temp file and rename, so a concurrent reader never
|
|
57
|
+
# observes a half-written entry.
|
|
58
|
+
def write_atomically(path, content)
|
|
59
|
+
temp = "#{path}.#{Process.pid}.tmp"
|
|
60
|
+
File.write(temp, content, encoding: "UTF-8")
|
|
61
|
+
File.rename(temp, path)
|
|
62
|
+
ensure
|
|
63
|
+
File.delete(temp) if temp && File.exist?(temp)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -7,8 +7,9 @@ module Suma
|
|
|
7
7
|
attr_accessor :config, :schemas, :docs, :output_path_docs,
|
|
8
8
|
:output_path_schemas, :manifest
|
|
9
9
|
|
|
10
|
-
def initialize(config: nil, config_yaml: nil, output_path_docs: nil,
|
|
11
|
-
output_path_schemas: nil, manifest: nil
|
|
10
|
+
def initialize(config: nil, config_yaml: nil, output_path_docs: nil, # rubocop:disable Metrics/ParameterLists
|
|
11
|
+
output_path_schemas: nil, manifest: nil,
|
|
12
|
+
options: { annotations: true })
|
|
12
13
|
@schemas = {}
|
|
13
14
|
@docs = {}
|
|
14
15
|
@schema_name_to_docs = {}
|
|
@@ -19,6 +20,7 @@ module Suma
|
|
|
19
20
|
@config = config
|
|
20
21
|
@config ||= config_yaml && Expressir::SchemaManifest.from_file(config_yaml)
|
|
21
22
|
@manifest = manifest
|
|
23
|
+
@options = options
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def doc_from_schema_name(schema_name)
|
|
@@ -42,7 +44,7 @@ module Suma
|
|
|
42
44
|
exporter = SchemaExporter.new(
|
|
43
45
|
schemas: schemas.values,
|
|
44
46
|
output_path: @output_path_schemas,
|
|
45
|
-
options:
|
|
47
|
+
options: @options,
|
|
46
48
|
)
|
|
47
49
|
exporter.export
|
|
48
50
|
|
data/lib/suma/schema_exporter.rb
CHANGED
|
@@ -23,6 +23,7 @@ module Suma
|
|
|
23
23
|
@schemas = schemas
|
|
24
24
|
@output_path = Pathname.new(output_path).expand_path
|
|
25
25
|
@options = default_options.merge(options)
|
|
26
|
+
@cache = build_cache
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def export
|
|
@@ -45,19 +46,51 @@ module Suma
|
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def export_to_directory(schemas)
|
|
48
|
-
schemas.each
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
schemas.each { |schema| export_single_schema(schema) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Reuse cached plain/annotated output when the schema source is unchanged,
|
|
53
|
+
# otherwise generate it via +save_exp+ and cache the result. The Expressir
|
|
54
|
+
# parse (the cost) happens only on a cache miss. Caching is an orchestration
|
|
55
|
+
# concern, so it lives here in the service rather than in the ExpressSchema
|
|
56
|
+
# data model.
|
|
57
|
+
def export_single_schema(schema)
|
|
58
|
+
source = File.read(schema.path.to_s, encoding: "UTF-8")
|
|
59
|
+
cached = @cache.fetch(source, annotations: options[:annotations])
|
|
60
|
+
cached ? write_cached(schema, cached) : generate_and_cache(schema, source)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def generate_and_cache(schema, source)
|
|
64
|
+
schema.save_exp(with_annotations: options[:annotations])
|
|
65
|
+
content = File.read(schema.filename_plain)
|
|
66
|
+
@cache.store(source, annotations: options[:annotations], content: content)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def write_cached(schema, content)
|
|
70
|
+
relative = Pathname.new(schema.filename_plain).relative_path_from(Dir.pwd)
|
|
71
|
+
Utils.log "Save schema (cached): #{relative}"
|
|
72
|
+
FileUtils.mkdir_p(File.dirname(schema.filename_plain))
|
|
73
|
+
File.write(schema.filename_plain, content)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A shared, content-addressed cache when a cache directory is configured
|
|
77
|
+
# (via the +:cache_dir+ option or the SUMA_SCHEMA_CACHE_DIR environment
|
|
78
|
+
# variable), otherwise a null cache (caching disabled).
|
|
79
|
+
def build_cache
|
|
80
|
+
directory = options[:cache_dir] || ENV.fetch("SUMA_SCHEMA_CACHE_DIR", nil)
|
|
81
|
+
return NullCache.new if directory.nil? || directory.empty?
|
|
82
|
+
|
|
83
|
+
SchemaCache.new(directory)
|
|
51
84
|
end
|
|
52
85
|
|
|
53
|
-
# rubocop:disable Metrics/MethodLength
|
|
86
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
54
87
|
def create_zip_archive
|
|
55
88
|
require "zip"
|
|
56
89
|
|
|
57
90
|
zip_path = "#{output_path}.zip"
|
|
58
91
|
Utils.log "Creating ZIP archive: #{zip_path}"
|
|
59
92
|
|
|
60
|
-
Zip::File.open(zip_path,
|
|
93
|
+
Zip::File.open(zip_path, create: true) do |zipfile|
|
|
61
94
|
Dir.glob("#{output_path}/**/*").each do |file|
|
|
62
95
|
next if File.directory?(file)
|
|
63
96
|
|
|
@@ -68,6 +101,5 @@ module Suma
|
|
|
68
101
|
|
|
69
102
|
Utils.log "ZIP archive created: #{zip_path}"
|
|
70
103
|
end
|
|
71
|
-
# rubocop:enable Metrics/MethodLength
|
|
72
104
|
end
|
|
73
105
|
end
|
|
@@ -5,8 +5,8 @@ module Suma
|
|
|
5
5
|
module Formatters
|
|
6
6
|
autoload :TerminalFormatter,
|
|
7
7
|
"suma/svg_quality/formatters/terminal_formatter"
|
|
8
|
-
autoload :JsonFormatter,
|
|
9
|
-
autoload :YamlFormatter,
|
|
8
|
+
autoload :JsonFormatter, "suma/svg_quality/formatters/json_formatter"
|
|
9
|
+
autoload :YamlFormatter, "suma/svg_quality/formatters/yaml_formatter"
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
end
|
data/lib/suma/version.rb
CHANGED
data/lib/suma.rb
CHANGED
|
@@ -16,8 +16,11 @@ 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"
|
|
21
|
+
autoload :NullCache, "suma/null_cache"
|
|
20
22
|
autoload :RegisterManifestGenerator, "suma/register_manifest_generator"
|
|
23
|
+
autoload :SchemaCache, "suma/schema_cache"
|
|
21
24
|
autoload :SchemaCategory, "suma/schema_category"
|
|
22
25
|
autoload :SchemaCollection, "suma/schema_collection"
|
|
23
26
|
autoload :SchemaComparer, "suma/schema_comparer"
|
data/suma.gemspec
CHANGED
|
@@ -33,13 +33,13 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
|
|
|
33
33
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
34
34
|
spec.require_paths = ["lib"]
|
|
35
35
|
|
|
36
|
-
spec.add_dependency "expressir"
|
|
36
|
+
spec.add_dependency "expressir"
|
|
37
37
|
spec.add_dependency "glossarist", "~> 2.8", ">= 2.8.7"
|
|
38
38
|
spec.add_dependency "lutaml-model", "~> 0.8.0"
|
|
39
39
|
spec.add_dependency "metanorma", "~> 2.3"
|
|
40
40
|
spec.add_dependency "plurimath"
|
|
41
41
|
spec.add_dependency "ruby-progressbar"
|
|
42
|
-
spec.add_dependency "rubyzip"
|
|
42
|
+
spec.add_dependency "rubyzip"
|
|
43
43
|
spec.add_dependency "svg_conform", "~> 0.1.0"
|
|
44
44
|
spec.add_dependency "table_tennis"
|
|
45
45
|
spec.add_dependency "thor", ">= 0.20"
|
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.
|
|
4
|
+
version: 0.5.4
|
|
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-
|
|
11
|
+
date: 2026-09-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: expressir
|
|
@@ -16,20 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
20
|
-
- - "~>"
|
|
21
|
-
- !ruby/object:Gem::Version
|
|
22
|
-
version: '2.1'
|
|
19
|
+
version: '0'
|
|
23
20
|
type: :runtime
|
|
24
21
|
prerelease: false
|
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
26
23
|
requirements:
|
|
27
24
|
- - ">="
|
|
28
25
|
- !ruby/object:Gem::Version
|
|
29
|
-
version:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '2.1'
|
|
26
|
+
version: '0'
|
|
33
27
|
- !ruby/object:Gem::Dependency
|
|
34
28
|
name: glossarist
|
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -110,16 +104,16 @@ dependencies:
|
|
|
110
104
|
name: rubyzip
|
|
111
105
|
requirement: !ruby/object:Gem::Requirement
|
|
112
106
|
requirements:
|
|
113
|
-
- - "
|
|
107
|
+
- - ">="
|
|
114
108
|
- !ruby/object:Gem::Version
|
|
115
|
-
version: '
|
|
109
|
+
version: '0'
|
|
116
110
|
type: :runtime
|
|
117
111
|
prerelease: false
|
|
118
112
|
version_requirements: !ruby/object:Gem::Requirement
|
|
119
113
|
requirements:
|
|
120
|
-
- - "
|
|
114
|
+
- - ">="
|
|
121
115
|
- !ruby/object:Gem::Version
|
|
122
|
-
version: '
|
|
116
|
+
version: '0'
|
|
123
117
|
- !ruby/object:Gem::Dependency
|
|
124
118
|
name: svg_conform
|
|
125
119
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -216,9 +210,12 @@ files:
|
|
|
216
210
|
- lib/suma/link_validator.rb
|
|
217
211
|
- lib/suma/link_validator/step.rb
|
|
218
212
|
- lib/suma/manifest_traverser.rb
|
|
213
|
+
- lib/suma/mapping_drift.rb
|
|
219
214
|
- lib/suma/note_processor.rb
|
|
215
|
+
- lib/suma/null_cache.rb
|
|
220
216
|
- lib/suma/processor.rb
|
|
221
217
|
- lib/suma/register_manifest_generator.rb
|
|
218
|
+
- lib/suma/schema_cache.rb
|
|
222
219
|
- lib/suma/schema_category.rb
|
|
223
220
|
- lib/suma/schema_collection.rb
|
|
224
221
|
- lib/suma/schema_comparer.rb
|