suma 0.5.8 → 0.5.10

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release-provenance.yml +5 -2
  3. data/.rubocop_todo.yml +27 -0
  4. data/CLAUDE.md +17 -5
  5. data/README.adoc +237 -11
  6. data/lib/suma/anchor_validator.rb +85 -0
  7. data/lib/suma/artifact_server.rb +102 -0
  8. data/lib/suma/cli/artifact_server_cli.rb +22 -0
  9. data/lib/suma/cli/build.rb +4 -0
  10. data/lib/suma/cli/core.rb +50 -4
  11. data/lib/suma/cli/export.rb +1 -1
  12. data/lib/suma/cli/extract_terms.rb +1 -1
  13. data/lib/suma/cli/generate_register.rb +1 -1
  14. data/lib/suma/cli/ingest_translations.rb +36 -0
  15. data/lib/suma/cli/package_gcr.rb +51 -0
  16. data/lib/suma/cli/validate.rb +16 -0
  17. data/lib/suma/cli.rb +4 -1
  18. data/lib/suma/closure_validator.rb +124 -20
  19. data/lib/suma/corpus_graph.rb +79 -16
  20. data/lib/suma/gcr_package.rb +230 -0
  21. data/lib/suma/link_validation_result.rb +8 -0
  22. data/lib/suma/link_validator.rb +0 -3
  23. data/lib/suma/output_canonicalizer.rb +107 -0
  24. data/lib/suma/processor.rb +94 -20
  25. data/lib/suma/provenance.rb +92 -12
  26. data/lib/suma/schema_collection.rb +1 -0
  27. data/lib/suma/schema_compiler.rb +79 -13
  28. data/lib/suma/schema_manifest_generator.rb +10 -3
  29. data/lib/suma/schema_profile.rb +12 -0
  30. data/lib/suma/schema_template/anchors.rb +154 -0
  31. data/lib/suma/schema_template/document.rb +2 -93
  32. data/lib/suma/schema_template/plain.rb +10 -6
  33. data/lib/suma/schema_template.rb +1 -0
  34. data/lib/suma/site_build.rb +153 -0
  35. data/lib/suma/staged_collection_builder.rb +11 -3
  36. data/lib/suma/translation_ingester.rb +169 -0
  37. data/lib/suma/urn_resolver.rb +51 -1
  38. data/lib/suma/version.rb +1 -1
  39. data/lib/suma.rb +7 -0
  40. metadata +13 -2
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+ require "fileutils"
6
+ require "pathname"
7
+
8
+ module Suma
9
+ # Freshness decision for the rendered collection site.
10
+ #
11
+ # The collection render is expensive: Metanorma compiles every member
12
+ # document. Its output is a pure function of the emitted collection manifest,
13
+ # the source tree it references, and the rendering toolchain, so this class
14
+ # keys those inputs, remembers the key and the produced files in the output
15
+ # directory, and lets the processor skip the render when nothing changed.
16
+ class SiteBuild
17
+ STATE_FILE = ".suma-site-build.json"
18
+
19
+ # Emitted manifests that the build itself regenerates deterministically
20
+ # from the sources above; whether they exist yet must not change the key.
21
+ GENERATED_FILES = %w[collection-output.yaml schemas.yml].freeze
22
+
23
+ def initialize(root:, output_directory:, inputs: [])
24
+ @root = File.expand_path(root)
25
+ @output_directory = File.expand_path(output_directory)
26
+ @extra_inputs = inputs.map { |path| File.expand_path(path) }
27
+ end
28
+
29
+ # @return [Array<String>] source files determining the rendered site
30
+ def source_files
31
+ @source_files ||= begin
32
+ files = @extra_inputs.select { |file| File.file?(file) }
33
+ (files + tree_files).uniq.sort
34
+ end
35
+ end
36
+
37
+ # @return [String] digest of every source file and the render toolchain
38
+ def key
39
+ @key ||= begin
40
+ digest = Digest::SHA256.new
41
+ source_files.each do |file|
42
+ digest << relative(file) << "\0"
43
+ digest << Digest::SHA256.file(file).hexdigest << "\0"
44
+ end
45
+ digest << JSON.generate(Provenance.render_toolchain)
46
+ digest.hexdigest
47
+ end
48
+ end
49
+
50
+ # True when the previous render used the same inputs and every file it
51
+ # produced is still present.
52
+ def current?
53
+ state = read_state
54
+ return false unless state && state["key"] == key
55
+
56
+ outputs = Array(state["outputs"])
57
+ !outputs.empty? &&
58
+ outputs.all? { |file| File.file?(File.join(@output_directory, file)) }
59
+ end
60
+
61
+ # Persist the key and the produced files for the next build.
62
+ def record
63
+ write_state("key" => key, "outputs" => output_files)
64
+ self
65
+ end
66
+
67
+ def state_path
68
+ File.join(@output_directory, STATE_FILE)
69
+ end
70
+
71
+ private
72
+
73
+ def tree_files
74
+ excluded = excluded_paths
75
+ Dir.glob(File.join(@root, "**", "*")).select do |file|
76
+ File.file?(file) &&
77
+ excluded.none? { |path| under?(file, path) } &&
78
+ !generated?(relative(file).tr("\\", "/"))
79
+ end
80
+ end
81
+
82
+ # Files the collection render writes next to its sources. They are
83
+ # outputs of the previous build, never inputs, so including them would
84
+ # change the key on every run.
85
+ def generated?(relative)
86
+ base = File.basename(relative)
87
+ GENERATED_FILES.include?(relative) ||
88
+ relative.start_with?("relaton/") ||
89
+ base.start_with?("tmp_document") ||
90
+ base.end_with?("_index.html", ".err.html", ".log.txt") ||
91
+ document_artifact?(relative, base)
92
+ end
93
+
94
+ def document_artifact?(relative, base)
95
+ return false unless relative.start_with?("documents/")
96
+
97
+ base == "cover.html" || base == "document.xml" ||
98
+ (base.start_with?("document.") && base.include?(".xml"))
99
+ end
100
+
101
+ # Generated trees are derived from the sources above and never feed the
102
+ # site render as inputs of their own. Bundler's install directory is a
103
+ # dependency artifact: CI conventionally places it at vendor/bundle
104
+ # inside the project, where it must never count as site source.
105
+ def excluded_paths
106
+ fixed = %w[schema_docs plain_schemas .git .bundle vendor/bundle]
107
+ .map { |dir| File.join(@root, dir) }
108
+ env = [ENV.fetch("SUMA_SCHEMA_CACHE_DIR", nil),
109
+ ENV.fetch("BUNDLE_PATH", nil)]
110
+ .compact
111
+ .map { |dir| File.expand_path(dir, @root) }
112
+ [@output_directory] + fixed + env
113
+ end
114
+
115
+ def under?(file, path)
116
+ file == path || file.start_with?("#{path}#{File::SEPARATOR}")
117
+ end
118
+
119
+ def read_state
120
+ return unless File.file?(state_path)
121
+
122
+ JSON.parse(File.read(state_path))
123
+ rescue JSON::ParserError
124
+ nil
125
+ end
126
+
127
+ def write_state(state)
128
+ FileUtils.mkdir_p(@output_directory)
129
+ temp = "#{state_path}.#{Process.pid}.tmp"
130
+ File.write(temp, "#{JSON.pretty_generate(state)}\n")
131
+ File.rename(temp, state_path)
132
+ ensure
133
+ File.delete(temp) if temp && File.exist?(temp)
134
+ end
135
+
136
+ def output_files
137
+ Dir.glob(File.join(@output_directory, "**", "*"))
138
+ .select do |file|
139
+ File.file?(file) && File.basename(file) != STATE_FILE
140
+ end
141
+ .map { |file| relative_to(file, @output_directory) }
142
+ .sort
143
+ end
144
+
145
+ def relative(path)
146
+ relative_to(path, @root)
147
+ end
148
+
149
+ def relative_to(path, base)
150
+ Pathname.new(path).relative_path_from(Pathname.new(base)).to_s
151
+ end
152
+ end
153
+ end
@@ -37,14 +37,16 @@ module Suma
37
37
  # +collection_config_path+ is the emitted collection manifest (the same
38
38
  # +collection-output.yaml+ the normal build renders). Member filerefs are
39
39
  # relative to its directory, so all per-member manifests are written there.
40
+ # (a data-declaring initializer)
40
41
  def initialize(collection_config_path:, output_directory:, coverpage: nil,
41
- store_dir: nil, formats: %i[xml html])
42
+ store_dir: nil, formats: %i[xml html], no_valid: false) # rubocop:disable Metrics/ParameterLists
42
43
  @config_path = collection_config_path
43
44
  @config_dir = File.dirname(File.expand_path(collection_config_path))
44
45
  @output_directory = output_directory
45
46
  @coverpage = coverpage
46
47
  @store_dir = store_dir || File.join(output_directory, STORE_DIRNAME)
47
48
  @formats = formats
49
+ @no_valid = no_valid
48
50
  end
49
51
 
50
52
  def build
@@ -97,7 +99,7 @@ module Suma
97
99
  manifest_path: one_path,
98
100
  opts: { format: [:xml], output_folder: out,
99
101
  preserve_unresolved: true, artifact_store_dir: @store_dir,
100
- compile: { install_fonts: false } },
102
+ compile: compile_flags },
101
103
  )
102
104
  ensure
103
105
  FileUtils.rm_f(one_path)
@@ -118,10 +120,16 @@ module Suma
118
120
  manifest_path: reinf_path,
119
121
  opts: { format: @formats, output_folder: @output_directory,
120
122
  reinflate: true, coverpage: @coverpage,
121
- compile: { install_fonts: false } }.compact,
123
+ compile: compile_flags }.compact,
122
124
  )
123
125
  end
124
126
 
127
+ def compile_flags
128
+ { install_fonts: false }.tap do |flags|
129
+ flags[:novalid] = true if @no_valid
130
+ end
131
+ end
132
+
125
133
  # Copy each member's stored stub into +dir+ and return docref entries pointing
126
134
  # at the copies, keyed by real docid (as the store keys them).
127
135
  def stored_docrefs(members, dir)
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "glossarist"
5
+ require "yaml"
6
+
7
+ module Suma
8
+ # Overlays translated content onto extracted Glossarist concepts (T5).
9
+ #
10
+ # A translations file is a YAML sequence of localized-concept documents,
11
+ # each carrying an identity key plus the content to apply:
12
+ #
13
+ # - concept_id: example_schema.bolt # managed id (data.identifier)
14
+ # # or the managed concept's uuid
15
+ # - id: <localized uuid> # alternative identity key
16
+ # data:
17
+ # language_code: fra
18
+ # terms: [...]
19
+ # definition: [...]
20
+ #
21
+ # Every field the entry provides replaces the localization's field; every
22
+ # field it omits falls back, explicitly, to the localization's current
23
+ # content (for a new language: the source-language localization's). All
24
+ # (de)serialization goes through the Glossarist/lutaml-model framework;
25
+ # entries are merged as serialized hashes and reified with of_yaml.
26
+ class TranslationIngester
27
+ def initialize(concepts_dir:, translations:, output_dir: nil)
28
+ @concepts_dir = concepts_dir
29
+ @translations = translations
30
+ @output_dir = output_dir || concepts_dir
31
+ end
32
+
33
+ # @return [Hash{String=>Array<String>}] concept file → applied entry
34
+ # identities
35
+ def ingest
36
+ entries = load_entries
37
+ applied = {}
38
+ each_concept_document do |file, doc|
39
+ matched = entries_for(doc, entries)
40
+ next if matched.empty?
41
+
42
+ applied[file] = matched.map { |entry| apply_entry(doc, entry) }
43
+ write_document(file, doc)
44
+ end
45
+ assert_no_unmatched!(entries, applied)
46
+ applied
47
+ end
48
+
49
+ private
50
+
51
+ def load_entries
52
+ content = YAML.safe_load_file(@translations, permitted_classes: [],
53
+ aliases: true)
54
+ entries = content.is_a?(Hash) ? content["translations"] : content
55
+ unless entries.is_a?(Array)
56
+ raise Error,
57
+ "translations file must be a YAML sequence of localized " \
58
+ "concepts: #{@translations}"
59
+ end
60
+
61
+ entries
62
+ end
63
+
64
+ def each_concept_document
65
+ Dir.glob(File.join(@concepts_dir, "*.yaml")).each do |file|
66
+ doc = Glossarist::V3::ConceptDocument.from_yamls(File.read(file))
67
+ yield file, doc
68
+ end
69
+ end
70
+
71
+ def entries_for(doc, entries)
72
+ entries.select { |entry| entry_matches?(doc, entry) }
73
+ end
74
+
75
+ def entry_matches?(doc, entry)
76
+ if entry["concept_id"]
77
+ [doc.concept.data.id, doc.concept.uuid].include?(entry["concept_id"])
78
+ else
79
+ localized_uuids(doc).include?(entry["id"])
80
+ end
81
+ end
82
+
83
+ def localized_uuids(doc)
84
+ doc.localizations.map(&:id) +
85
+ doc.concept.data.localized_concepts.values
86
+ end
87
+
88
+ def apply_entry(doc, entry)
89
+ incoming = Glossarist::LocalizedConcept
90
+ .of_yaml(entry.except("concept_id"))
91
+ localized = merge_localization(doc, entry, incoming)
92
+ replace_localization(doc, localized)
93
+ entry["concept_id"] || entry["id"]
94
+ end
95
+
96
+ def replace_localization(doc, localized)
97
+ language = localized.data.language_code
98
+ doc.localizations.reject! { |l10n| l10n.data.language_code == language }
99
+ doc.localizations << localized
100
+ doc.concept.data.localized_concepts[language] = localized.uuid
101
+ end
102
+
103
+ # Entry fields win; absent fields keep the localization's current
104
+ # content (or the source-language content for a new language). The
105
+ # merge happens on the raw entry data — a parsed model would
106
+ # materialize empty collections and wipe the fields it omits.
107
+ #
108
+ # Glossarist parses the serialized "id" key into `#id` (not `#uuid`),
109
+ # so the persisted id is preserved and stamped back onto the merged
110
+ # localization explicitly; a new language gets the same v5 id
111
+ # TermExtractor's linked localizations use.
112
+ def merge_localization(doc, entry, incoming)
113
+ current = current_localization(doc, entry, incoming)
114
+ base = current || doc.localizations.first
115
+ return incoming unless base
116
+
117
+ identity = entry["id"] || current&.id ||
118
+ new_localization_uuid(doc, incoming.data.language_code)
119
+ localized = Glossarist::LocalizedConcept
120
+ .of_yaml(merged_hash(base, entry, identity))
121
+ localized.uuid = identity
122
+ localized
123
+ end
124
+
125
+ def merged_hash(base, entry, identity)
126
+ merged = base.to_yaml_hash
127
+ merged["data"] = (merged["data"] || {})
128
+ .merge(entry["data"] || {})
129
+ merged["id"] = identity
130
+ merged
131
+ end
132
+
133
+ def current_localization(doc, entry, incoming)
134
+ if entry["id"]
135
+ doc.localizations.find { |l10n| l10n.id == entry["id"] }
136
+ else
137
+ doc.localizations.find do |l10n|
138
+ l10n.data.language_code == incoming.data.language_code
139
+ end
140
+ end
141
+ end
142
+
143
+ # Same convention as TermExtractor's linked localizations.
144
+ def new_localization_uuid(doc, language)
145
+ Glossarist::Utilities::UUID.uuid_v5(
146
+ Glossarist::Utilities::UUID::OID_NAMESPACE,
147
+ "#{doc.concept.data.id}-#{language}",
148
+ )
149
+ end
150
+
151
+ def write_document(file, doc)
152
+ output = File.join(@output_dir, File.basename(file))
153
+ FileUtils.mkdir_p(@output_dir) if output != file
154
+ File.write(output, doc.to_yamls, encoding: "utf-8")
155
+ end
156
+
157
+ def assert_no_unmatched!(entries, applied)
158
+ matched = applied.values.flatten
159
+ unmatched = entries.reject do |entry|
160
+ matched.include?(entry["concept_id"] || entry["id"])
161
+ end
162
+ return if unmatched.empty?
163
+
164
+ raise Error,
165
+ "unmatched translation entries: " \
166
+ "#{unmatched.map { |e| e['concept_id'] || e['id'] }.join(', ')}"
167
+ end
168
+ end
169
+ end
@@ -53,9 +53,12 @@ module Suma
53
53
  )
54
54
  end
55
55
 
56
- # Write JSON and HTML for every artifact.
56
+ # Write JSON and HTML for every artifact, plus an index of the set.
57
57
  def write(output_directory)
58
58
  artifacts.each_key { |key| write_urn(key, output_directory) }
59
+ FileUtils.mkdir_p(output_directory)
60
+ File.write(File.join(output_directory, "index.html"),
61
+ index_document, encoding: "utf-8")
59
62
  Utils.log "Wrote #{artifacts.length} URN artifacts to #{output_directory}"
60
63
  end
61
64
 
@@ -97,11 +100,38 @@ module Suma
97
100
  payload = element_payload(schema, element)
98
101
  artifacts[compose(:tech, "#{schema.id}.#{element.id}")] = payload if
99
102
  payload
103
+ add_attribute_artifacts(artifacts, schema, element)
100
104
  end
101
105
  end
102
106
  artifacts
103
107
  end
104
108
 
109
+ # Attribute property IRIs appear in the OWL projection as
110
+ # <schema>.<entity>.<attribute>; make them dereferenceable too.
111
+ def add_attribute_artifacts(artifacts, schema, element)
112
+ return unless element.is_a?(Expressir::Model::Declarations::Entity)
113
+
114
+ element.attributes.each do |attribute|
115
+ key = compose(:tech,
116
+ "#{schema.id}.#{element.id}.#{attribute.id}")
117
+ artifacts[key] = attribute_artifact(schema, element, attribute)
118
+ end
119
+ end
120
+
121
+ def attribute_artifact(schema, entity, attribute)
122
+ {
123
+ "urn" => compose(:tech, "#{schema.id}.#{entity.id}.#{attribute.id}"),
124
+ "kind" => "attribute",
125
+ "schema" => schema.id,
126
+ "entity" => entity.id,
127
+ "id" => attribute.id,
128
+ "attribute_kind" => attribute.kind,
129
+ "optional" => attribute.optional == true,
130
+ "type" => attribute.type&.source,
131
+ "source" => relative_source(schema),
132
+ }
133
+ end
134
+
105
135
  def schemas
106
136
  @schemas ||= begin
107
137
  manifest = Expressir::SchemaManifest.from_file(@manifest_file)
@@ -310,6 +340,26 @@ module Suma
310
340
  HTML
311
341
  end
312
342
 
343
+ # A deterministic index of every resolved URN, linked to its document.
344
+ def index_document
345
+ items = artifacts.keys.sort.map do |urn|
346
+ href = artifact_path(urn, "html")
347
+ %( <li><a href="#{escape_html(href)}">#{escape_html(urn)}</a></li>)
348
+ end.join("\n")
349
+ <<~HTML
350
+ <!doctype html>
351
+ <html lang="en">
352
+ <head><meta charset="utf-8"><title>Resolved URNs</title></head>
353
+ <body>
354
+ <h1>Resolved URNs</h1>
355
+ <ul>
356
+ #{items}
357
+ </ul>
358
+ </body>
359
+ </html>
360
+ HTML
361
+ end
362
+
313
363
  def escape_html(value)
314
364
  value.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
315
365
  .gsub('"', "&quot;")
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.8"
4
+ VERSION = "0.5.10"
5
5
  end
data/lib/suma.rb CHANGED
@@ -7,6 +7,8 @@ require "fileutils"
7
7
  module Suma
8
8
  autoload :VERSION, "suma/version"
9
9
 
10
+ autoload :AnchorValidator, "suma/anchor_validator"
11
+ autoload :ArtifactServer, "suma/artifact_server"
10
12
  autoload :SchemaProfile, "suma/schema_profile"
11
13
  autoload :BuildPlan, "suma/build_plan"
12
14
  autoload :Processor, "suma/processor"
@@ -20,14 +22,17 @@ module Suma
20
22
  autoload :CorpusGraph, "suma/corpus_graph"
21
23
  autoload :CollectionManifest, "suma/collection_manifest"
22
24
  autoload :EengineConverter, "suma/eengine_converter"
25
+ autoload :GcrPackage, "suma/gcr_package"
23
26
  autoload :ExpressReformatter, "suma/express_reformatter"
24
27
  autoload :ExpressSchema, "suma/express_schema"
25
28
  autoload :LinkValidation, "suma/link_validation"
29
+ autoload :LinkValidationResult, "suma/link_validation_result"
26
30
  autoload :LinkValidator, "suma/link_validator"
27
31
  autoload :ManifestTraverser, "suma/manifest_traverser"
28
32
  autoload :MappingDrift, "suma/mapping_drift"
29
33
  autoload :NoteProcessor, "suma/note_processor"
30
34
  autoload :NullCache, "suma/null_cache"
35
+ autoload :OutputCanonicalizer, "suma/output_canonicalizer"
31
36
  autoload :OwlProjection, "suma/owl_projection"
32
37
  autoload :RegisterManifestGenerator, "suma/register_manifest_generator"
33
38
  autoload :SchemaCache, "suma/schema_cache"
@@ -42,11 +47,13 @@ module Suma
42
47
  autoload :SchemaNaming, "suma/schema_naming"
43
48
  autoload :SchemaTemplate, "suma/schema_template"
44
49
  autoload :SiteConfig, "suma/site_config"
50
+ autoload :SiteBuild, "suma/site_build"
45
51
  autoload :StagedCollectionBuilder, "suma/staged_collection_builder"
46
52
  autoload :SvgQuality, "suma/svg_quality"
47
53
  autoload :TermClassification, "suma/term_classification"
48
54
  autoload :TermExtractor, "suma/term_extractor"
49
55
  autoload :ThorExt, "suma/thor_ext"
56
+ autoload :TranslationIngester, "suma/translation_ingester"
50
57
  autoload :Urn, "suma/urn"
51
58
  autoload :UrnResolver, "suma/urn_resolver"
52
59
  autoload :Utils, "suma/utils"
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.8
4
+ version: 0.5.10
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-10-20 00:00:00.000000000 Z
11
+ date: 2026-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: expressir
@@ -185,11 +185,14 @@ files:
185
185
  - bin/setup
186
186
  - exe/suma
187
187
  - lib/suma.rb
188
+ - lib/suma/anchor_validator.rb
189
+ - lib/suma/artifact_server.rb
188
190
  - lib/suma/build_plan.rb
189
191
  - lib/suma/change_classification.rb
190
192
  - lib/suma/change_impact.rb
191
193
  - lib/suma/change_manifest.rb
192
194
  - lib/suma/cli.rb
195
+ - lib/suma/cli/artifact_server_cli.rb
193
196
  - lib/suma/cli/build.rb
194
197
  - lib/suma/cli/check_svg_quality.rb
195
198
  - lib/suma/cli/compare.rb
@@ -199,6 +202,8 @@ files:
199
202
  - lib/suma/cli/extract_terms.rb
200
203
  - lib/suma/cli/generate_register.rb
201
204
  - lib/suma/cli/generate_schemas.rb
205
+ - lib/suma/cli/ingest_translations.rb
206
+ - lib/suma/cli/package_gcr.rb
202
207
  - lib/suma/cli/reformat.rb
203
208
  - lib/suma/cli/resolve.rb
204
209
  - lib/suma/cli/validate.rb
@@ -213,17 +218,20 @@ files:
213
218
  - lib/suma/eengine_converter.rb
214
219
  - lib/suma/express_reformatter.rb
215
220
  - lib/suma/express_schema.rb
221
+ - lib/suma/gcr_package.rb
216
222
  - lib/suma/jsdai.rb
217
223
  - lib/suma/jsdai/figure.rb
218
224
  - lib/suma/jsdai/figure_image.rb
219
225
  - lib/suma/jsdai/figure_xml.rb
220
226
  - lib/suma/link_validation.rb
227
+ - lib/suma/link_validation_result.rb
221
228
  - lib/suma/link_validator.rb
222
229
  - lib/suma/link_validator/step.rb
223
230
  - lib/suma/manifest_traverser.rb
224
231
  - lib/suma/mapping_drift.rb
225
232
  - lib/suma/note_processor.rb
226
233
  - lib/suma/null_cache.rb
234
+ - lib/suma/output_canonicalizer.rb
227
235
  - lib/suma/owl_projection.rb
228
236
  - lib/suma/processor.rb
229
237
  - lib/suma/provenance.rb
@@ -241,8 +249,10 @@ files:
241
249
  - lib/suma/schema_naming.rb
242
250
  - lib/suma/schema_profile.rb
243
251
  - lib/suma/schema_template.rb
252
+ - lib/suma/schema_template/anchors.rb
244
253
  - lib/suma/schema_template/document.rb
245
254
  - lib/suma/schema_template/plain.rb
255
+ - lib/suma/site_build.rb
246
256
  - lib/suma/site_config.rb
247
257
  - lib/suma/staged_collection_builder.rb
248
258
  - lib/suma/svg_quality.rb
@@ -256,6 +266,7 @@ files:
256
266
  - lib/suma/term_classification.rb
257
267
  - lib/suma/term_extractor.rb
258
268
  - lib/suma/thor_ext.rb
269
+ - lib/suma/translation_ingester.rb
259
270
  - lib/suma/urn.rb
260
271
  - lib/suma/urn_resolver.rb
261
272
  - lib/suma/utils.rb