suma 0.5.9 → 0.5.11
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/.rubocop_todo.yml +27 -0
- data/CLAUDE.md +17 -5
- data/README.adoc +101 -0
- data/lib/suma/anchor_validator.rb +85 -0
- data/lib/suma/artifact_server.rb +102 -0
- data/lib/suma/cli/artifact_server_cli.rb +22 -0
- data/lib/suma/cli/build.rb +20 -3
- data/lib/suma/cli/core.rb +50 -4
- data/lib/suma/cli/export.rb +1 -1
- data/lib/suma/cli/extract_terms.rb +1 -1
- data/lib/suma/cli/generate_register.rb +1 -1
- data/lib/suma/cli/ingest_translations.rb +36 -0
- data/lib/suma/cli/package_gcr.rb +51 -0
- data/lib/suma/cli/validate.rb +16 -0
- data/lib/suma/cli.rb +4 -1
- data/lib/suma/closure_validator.rb +141 -37
- data/lib/suma/corpus_graph.rb +80 -17
- data/lib/suma/gcr_package.rb +230 -0
- data/lib/suma/link_validation_result.rb +8 -0
- data/lib/suma/link_validator.rb +0 -3
- data/lib/suma/output_canonicalizer.rb +107 -0
- data/lib/suma/owl_projection.rb +3 -3
- data/lib/suma/processor.rb +100 -20
- data/lib/suma/provenance.rb +92 -12
- data/lib/suma/schema_collection.rb +1 -0
- data/lib/suma/schema_compiler.rb +79 -13
- data/lib/suma/schema_exporter.rb +12 -0
- data/lib/suma/schema_index.rb +5 -5
- data/lib/suma/schema_manifest_generator.rb +10 -3
- data/lib/suma/schema_profile.rb +12 -0
- data/lib/suma/schema_template/anchors.rb +154 -0
- data/lib/suma/schema_template/document.rb +2 -93
- data/lib/suma/schema_template/plain.rb +10 -6
- data/lib/suma/schema_template.rb +1 -0
- data/lib/suma/site_build.rb +153 -0
- data/lib/suma/staged_collection_builder.rb +146 -10
- data/lib/suma/term_extractor.rb +10 -4
- data/lib/suma/translation_ingester.rb +169 -0
- data/lib/suma/urn_resolver.rb +3 -3
- data/lib/suma/version.rb +1 -1
- data/lib/suma/worker_pool.rb +5 -1
- data/lib/suma.rb +7 -0
- metadata +13 -2
data/lib/suma/processor.rb
CHANGED
|
@@ -13,8 +13,10 @@ module Suma
|
|
|
13
13
|
|
|
14
14
|
def initialize(metanorma_yaml_path:, schemas_all_path:, compile: true,
|
|
15
15
|
output_directory: "_site", staged: false,
|
|
16
|
+
staged_workers: 1, staged_batch_size: nil,
|
|
16
17
|
incremental: false, state_file: nil, jobs: 1,
|
|
17
|
-
provenance_file: nil, sign_method: nil, sign_key: nil
|
|
18
|
+
provenance_file: nil, sign_method: nil, sign_key: nil,
|
|
19
|
+
no_valid: false)
|
|
18
20
|
@metanorma_yaml_path = metanorma_yaml_path
|
|
19
21
|
@schemas_all_path = schemas_all_path
|
|
20
22
|
@compile_flag = compile
|
|
@@ -22,12 +24,17 @@ module Suma
|
|
|
22
24
|
# Opt-in memory-bounded staged build (metanorma/suma#94); the default
|
|
23
25
|
# single-process build below is unchanged when false.
|
|
24
26
|
@staged = staged
|
|
27
|
+
@staged_workers = staged_workers
|
|
28
|
+
@staged_batch_size = staged_batch_size
|
|
25
29
|
@incremental = incremental
|
|
26
30
|
@state_file = state_file
|
|
27
31
|
@jobs = jobs
|
|
28
32
|
@provenance_file = provenance_file
|
|
29
33
|
@sign_method = sign_method
|
|
30
34
|
@sign_key = sign_key
|
|
35
|
+
# Skip Metanorma document validation (metanorma `--no-valid`). Opt-in:
|
|
36
|
+
# for collections whose flavor's validators are still in flight.
|
|
37
|
+
@no_valid = no_valid
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
def run
|
|
@@ -39,7 +46,6 @@ module Suma
|
|
|
39
46
|
Utils.log "Compiling schema collection..."
|
|
40
47
|
compile_schema(schemas_all_path, collection_config)
|
|
41
48
|
|
|
42
|
-
Utils.log "Compiling complete collection..."
|
|
43
49
|
compile_collection(collection_config, output_directory)
|
|
44
50
|
end
|
|
45
51
|
|
|
@@ -56,6 +62,7 @@ module Suma
|
|
|
56
62
|
@provenance_file,
|
|
57
63
|
inputs: provenance_inputs,
|
|
58
64
|
outputs: provenance_outputs,
|
|
65
|
+
exclude: provenance_excludes,
|
|
59
66
|
)
|
|
60
67
|
if @sign_method
|
|
61
68
|
signature = Provenance.sign(
|
|
@@ -80,6 +87,31 @@ module Suma
|
|
|
80
87
|
@output_directory]
|
|
81
88
|
end
|
|
82
89
|
|
|
90
|
+
# Build metadata that is not site content: the site freshness state, the
|
|
91
|
+
# staged-build store, and a provenance file written inside the output tree.
|
|
92
|
+
def provenance_excludes
|
|
93
|
+
excludes = [File.join(@output_directory, SiteBuild::STATE_FILE)]
|
|
94
|
+
if @staged
|
|
95
|
+
excludes << File.join(@output_directory,
|
|
96
|
+
StagedCollectionBuilder::STORE_DIRNAME)
|
|
97
|
+
excludes << File.join(@output_directory, "_staged_reinflate")
|
|
98
|
+
end
|
|
99
|
+
excludes + provenance_artifacts.select { |path| within_output?(path) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def provenance_artifacts
|
|
103
|
+
return [] unless @provenance_file
|
|
104
|
+
|
|
105
|
+
base = File.expand_path(@provenance_file)
|
|
106
|
+
[base, "#{base}.minisig", "#{base}.bundle"]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def within_output?(path)
|
|
110
|
+
full = File.expand_path(path)
|
|
111
|
+
output = File.expand_path(@output_directory)
|
|
112
|
+
full == output || full.start_with?("#{output}#{File::SEPARATOR}")
|
|
113
|
+
end
|
|
114
|
+
|
|
83
115
|
def export_schema_config
|
|
84
116
|
site_config = Suma::SiteConfig::Config.from_file(metanorma_yaml_path)
|
|
85
117
|
|
|
@@ -108,6 +140,7 @@ module Suma
|
|
|
108
140
|
incremental: @incremental,
|
|
109
141
|
state_file: @state_file,
|
|
110
142
|
jobs: @jobs,
|
|
143
|
+
no_valid: @no_valid,
|
|
111
144
|
},
|
|
112
145
|
)
|
|
113
146
|
|
|
@@ -115,36 +148,83 @@ module Suma
|
|
|
115
148
|
end
|
|
116
149
|
|
|
117
150
|
def compile_collection(collection_config, output_directory)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
151
|
+
prepare_collection_manifest(collection_config)
|
|
152
|
+
write_collection_manifest(collection_config)
|
|
153
|
+
|
|
154
|
+
if site_build.current?
|
|
155
|
+
Utils.log "Collection outputs are up to date; 0 documents rendered."
|
|
156
|
+
return
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
Utils.log "Rendering collection site..."
|
|
160
|
+
render_collection(collection_config, output_directory)
|
|
161
|
+
canonicalizer.canonicalize_documents
|
|
162
|
+
site_build.record
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Generated manifest identifiers are canonicalized in the model, before
|
|
166
|
+
# serialization: patching the dumped YAML would preserve Psych's
|
|
167
|
+
# value-dependent (random) quoting decision.
|
|
168
|
+
def prepare_collection_manifest(collection_config)
|
|
169
|
+
ManifestTraverser.new(collection_config.manifest)
|
|
170
|
+
.remove_schemas_only_sources
|
|
171
|
+
canonicalizer.canonicalize_manifest(collection_config.manifest)
|
|
172
|
+
end
|
|
121
173
|
|
|
174
|
+
def write_collection_manifest(collection_config)
|
|
175
|
+
collection_config.to_file(COLLECTION_OUTPUT_PATH)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def render_collection(collection_config, output_directory)
|
|
122
179
|
if @staged
|
|
123
|
-
#
|
|
124
|
-
# its own process
|
|
180
|
+
# The emitted manifest is the staging input; each member is compiled
|
|
181
|
+
# in its own process and then reinflated into +output_directory+.
|
|
125
182
|
StagedCollectionBuilder.new(
|
|
126
183
|
collection_config_path: COLLECTION_OUTPUT_PATH,
|
|
127
184
|
output_directory: output_directory,
|
|
128
|
-
coverpage:
|
|
185
|
+
coverpage: collection_coverpage(collection_config),
|
|
186
|
+
no_valid: @no_valid,
|
|
187
|
+
workers: @staged_workers,
|
|
188
|
+
batch_size: @staged_batch_size ||
|
|
189
|
+
StagedCollectionBuilder::DEFAULT_BATCH_SIZE,
|
|
129
190
|
).build
|
|
130
191
|
else
|
|
131
|
-
|
|
192
|
+
Metanorma::Collection.parse(COLLECTION_OUTPUT_PATH)
|
|
193
|
+
.render(collection_options(collection_config, output_directory))
|
|
132
194
|
end
|
|
133
195
|
end
|
|
134
196
|
|
|
135
|
-
def
|
|
136
|
-
|
|
137
|
-
ManifestTraverser.new(collection_config.manifest).remove_schemas_only_sources
|
|
138
|
-
collection_config.to_file(new_collection_config_path)
|
|
139
|
-
|
|
140
|
-
collection = Metanorma::Collection.parse(new_collection_config_path)
|
|
141
|
-
|
|
142
|
-
collection_opts = {
|
|
197
|
+
def collection_options(collection_config, output_directory)
|
|
198
|
+
{
|
|
143
199
|
output_folder: output_directory,
|
|
144
|
-
compile:
|
|
145
|
-
coverpage: collection_config
|
|
200
|
+
compile: compile_flags,
|
|
201
|
+
coverpage: collection_coverpage(collection_config),
|
|
146
202
|
}
|
|
147
|
-
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def compile_flags
|
|
206
|
+
{ install_fonts: false }.tap do |flags|
|
|
207
|
+
flags[:novalid] = true if @no_valid
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def collection_coverpage(collection_config)
|
|
212
|
+
collection_config.coverpage || "cover.html"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def site_build
|
|
216
|
+
@site_build ||= SiteBuild.new(
|
|
217
|
+
root: File.dirname(File.expand_path(metanorma_yaml_path)),
|
|
218
|
+
output_directory: output_directory,
|
|
219
|
+
inputs: [schemas_all_path],
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def canonicalizer
|
|
224
|
+
@canonicalizer ||= OutputCanonicalizer.new(
|
|
225
|
+
output_directory: output_directory,
|
|
226
|
+
sources: site_build.source_files,
|
|
227
|
+
)
|
|
148
228
|
end
|
|
149
229
|
end
|
|
150
230
|
end
|
data/lib/suma/provenance.rb
CHANGED
|
@@ -83,6 +83,9 @@ module Suma
|
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
# Gems whose versions determine rendered document bytes.
|
|
87
|
+
RENDER_GEM_PREFIXES = %w[metanorma- isodoc].freeze
|
|
88
|
+
|
|
86
89
|
# @return [Hash] toolchain versions that determine output bytes
|
|
87
90
|
def self.toolchain
|
|
88
91
|
{
|
|
@@ -98,30 +101,63 @@ module Suma
|
|
|
98
101
|
versions
|
|
99
102
|
end
|
|
100
103
|
|
|
104
|
+
# Toolchain identity for cached rendered documents: the provenance
|
|
105
|
+
# toolchain plus every installed Metanorma/Isodoc gem, so a renderer or
|
|
106
|
+
# flavor update invalidates cached renders even when source is unchanged.
|
|
107
|
+
def self.render_toolchain
|
|
108
|
+
@render_toolchain ||= toolchain.merge("render" => render_gems)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.render_gems
|
|
112
|
+
gem_specs.select { |spec| render_gem?(spec.name) }
|
|
113
|
+
.sort_by { |spec| [spec.name, spec.version.to_s] }
|
|
114
|
+
.map { |spec| "#{spec.name}-#{spec.version}" }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.render_gem?(name)
|
|
118
|
+
name == "metanorma" || RENDER_GEM_PREFIXES.any? do |prefix|
|
|
119
|
+
name.start_with?(prefix)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.gem_specs
|
|
124
|
+
Gem::Specification.select { |spec| spec.name.match?(/\A(?:metanorma|isodoc)/) }
|
|
125
|
+
end
|
|
126
|
+
|
|
101
127
|
# @param inputs [Array<String>] files or directories
|
|
102
128
|
# @param outputs [Array<String>] files or directories
|
|
129
|
+
# @param exclude [Array<String>] files or directories omitted from the
|
|
130
|
+
# manifest and from the unrecorded-output check
|
|
103
131
|
# @return [Hash] manifest
|
|
104
|
-
def self.record(inputs:, outputs:)
|
|
132
|
+
def self.record(inputs:, outputs:, exclude: [])
|
|
133
|
+
excluded = exclude.map { |path| File.expand_path(path) }
|
|
105
134
|
{
|
|
106
135
|
"toolchain" => toolchain,
|
|
107
|
-
"inputs" => digests(inputs),
|
|
108
|
-
"outputs" => digests(outputs),
|
|
136
|
+
"inputs" => digests(inputs, exclude: excluded),
|
|
137
|
+
"outputs" => digests(outputs, exclude: excluded),
|
|
138
|
+
"output_roots" => output_roots(outputs, exclude: excluded),
|
|
139
|
+
"excluded" => excluded.map { |path| relative(path) }.sort,
|
|
109
140
|
}
|
|
110
141
|
end
|
|
111
142
|
|
|
112
143
|
# Write a provenance manifest as canonical JSON.
|
|
113
|
-
def self.write(path, inputs:, outputs:)
|
|
144
|
+
def self.write(path, inputs:, outputs:, exclude: [])
|
|
114
145
|
FileUtils.mkdir_p(File.dirname(path))
|
|
115
|
-
manifest = record(inputs: inputs, outputs: outputs)
|
|
146
|
+
manifest = record(inputs: inputs, outputs: outputs, exclude: exclude)
|
|
116
147
|
File.write(path, "#{JSON.pretty_generate(manifest)}\n")
|
|
117
148
|
path
|
|
118
149
|
end
|
|
119
150
|
|
|
120
|
-
# Independently recompute every digest in the manifest
|
|
151
|
+
# Independently recompute every digest in the manifest and flag files
|
|
152
|
+
# that appeared under a recorded output root but are not in the manifest.
|
|
121
153
|
#
|
|
122
154
|
# @return [Array<String>] problems (empty = verified)
|
|
123
155
|
def self.verify(path)
|
|
124
156
|
manifest = JSON.parse(File.read(path))
|
|
157
|
+
digest_problems(manifest) + unrecorded_outputs(manifest)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def self.digest_problems(manifest)
|
|
125
161
|
SECTIONS.flat_map do |section|
|
|
126
162
|
manifest.fetch(section, {}).filter_map do |relative, digest|
|
|
127
163
|
full = File.expand_path(relative)
|
|
@@ -134,18 +170,59 @@ module Suma
|
|
|
134
170
|
end
|
|
135
171
|
end
|
|
136
172
|
|
|
173
|
+
def self.unrecorded_outputs(manifest)
|
|
174
|
+
recorded = manifest.fetch("outputs", {})
|
|
175
|
+
excluded = expanded(manifest["excluded"])
|
|
176
|
+
Array(manifest["output_roots"]).flat_map do |root|
|
|
177
|
+
output_files(root)
|
|
178
|
+
.reject { |file| recorded.key?(file) || excluded?(file, excluded) }
|
|
179
|
+
.map { |file| "outputs: unrecorded #{file}" }
|
|
180
|
+
end.sort
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def self.output_files(root)
|
|
184
|
+
full_root = File.expand_path(root)
|
|
185
|
+
return [] unless File.directory?(full_root)
|
|
186
|
+
|
|
187
|
+
files([full_root]).map { |file| relative(file) }.uniq
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.expanded(paths)
|
|
191
|
+
Array(paths).map { |path| File.expand_path(path) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def self.excluded?(relative_path, excluded)
|
|
195
|
+
full = File.expand_path(relative_path)
|
|
196
|
+
excluded.any? do |path|
|
|
197
|
+
full == path || full.start_with?("#{path}#{File::SEPARATOR}")
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
137
201
|
# @param paths [Array<String>] files or directories
|
|
202
|
+
# @param exclude [Array<String>] expanded paths to skip
|
|
138
203
|
# @return [Hash{String=>String}] relative path → sha256
|
|
139
|
-
def self.digests(paths)
|
|
140
|
-
files
|
|
141
|
-
|
|
142
|
-
end
|
|
143
|
-
files.select { |file| File.file?(file) }
|
|
204
|
+
def self.digests(paths, exclude: [])
|
|
205
|
+
files(paths)
|
|
206
|
+
.reject { |file| excluded?(file, exclude) }
|
|
144
207
|
.map { |file| relative(file) }
|
|
145
208
|
.uniq.sort
|
|
146
209
|
.to_h { |file| [file, Digest::SHA256.file(file).hexdigest] }
|
|
147
210
|
end
|
|
148
211
|
|
|
212
|
+
def self.files(paths)
|
|
213
|
+
paths.flat_map do |path|
|
|
214
|
+
File.directory?(path) ? Dir.glob(File.join(path, "**/*")) : [path]
|
|
215
|
+
end.select { |file| File.file?(file) }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Relative paths of the directory entries among +outputs+.
|
|
219
|
+
def self.output_roots(outputs, exclude: [])
|
|
220
|
+
outputs.select { |path| File.directory?(path) }
|
|
221
|
+
.map { |path| relative(path) }
|
|
222
|
+
.reject { |path| excluded?(path, exclude) }
|
|
223
|
+
.uniq.sort
|
|
224
|
+
end
|
|
225
|
+
|
|
149
226
|
def self.relative(path)
|
|
150
227
|
Pathname.new(File.expand_path(path))
|
|
151
228
|
.relative_path_from(Pathname.getwd)
|
|
@@ -154,6 +231,9 @@ module Suma
|
|
|
154
231
|
path.to_s
|
|
155
232
|
end
|
|
156
233
|
|
|
157
|
-
private_class_method :optional_versions, :sign_with, :executable
|
|
234
|
+
private_class_method :optional_versions, :sign_with, :executable?,
|
|
235
|
+
:render_gems, :render_gem?, :gem_specs,
|
|
236
|
+
:digest_problems, :unrecorded_outputs, :output_files,
|
|
237
|
+
:expanded, :excluded?, :files
|
|
158
238
|
end
|
|
159
239
|
end
|
data/lib/suma/schema_compiler.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
3
4
|
require "fileutils"
|
|
5
|
+
require "json"
|
|
4
6
|
require "pathname"
|
|
5
7
|
|
|
6
8
|
module Suma
|
|
@@ -12,20 +14,32 @@ module Suma
|
|
|
12
14
|
# that the caller injects. This split means templates can be tested as pure
|
|
13
15
|
# functions and the compiler can be tested with a real adoc fixture, without
|
|
14
16
|
# either depending on the other.
|
|
17
|
+
#
|
|
18
|
+
# Rendering is skipped when the adoc, the schema config, the schema source,
|
|
19
|
+
# and the rendering toolchain are unchanged since the outputs were written:
|
|
20
|
+
# the render key is stored beside the outputs.
|
|
15
21
|
class SchemaCompiler
|
|
22
|
+
RENDER_KEY_FILE = ".suma-render-key"
|
|
23
|
+
|
|
16
24
|
attr_reader :schema, :id, :output_path, :template
|
|
17
25
|
|
|
18
|
-
def initialize(schema:, output_path:, template:)
|
|
26
|
+
def initialize(schema:, output_path:, template:, no_valid: false)
|
|
19
27
|
@schema = schema
|
|
20
28
|
@id = schema.id
|
|
21
29
|
@output_path = output_path
|
|
22
30
|
@template = template
|
|
31
|
+
@no_valid = no_valid
|
|
23
32
|
end
|
|
24
33
|
|
|
25
34
|
def compile
|
|
26
35
|
save_config
|
|
27
|
-
|
|
36
|
+
adoc = template.render(config_relative_path)
|
|
37
|
+
save_adoc(adoc)
|
|
38
|
+
key = render_key(adoc)
|
|
39
|
+
return reuse_render if rendered_current?(key)
|
|
40
|
+
|
|
28
41
|
invoke_metanorma
|
|
42
|
+
write_render_key(key)
|
|
29
43
|
self
|
|
30
44
|
end
|
|
31
45
|
|
|
@@ -37,6 +51,13 @@ module Suma
|
|
|
37
51
|
template.extensions
|
|
38
52
|
end
|
|
39
53
|
|
|
54
|
+
# Options handed to Metanorma::Compile. Exposed for specs.
|
|
55
|
+
def compile_options
|
|
56
|
+
{ agree_to_terms: true, install_fonts: false }.tap do |options|
|
|
57
|
+
options[:novalid] = true if @no_valid
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
40
61
|
private
|
|
41
62
|
|
|
42
63
|
def filename_adoc(ext = "adoc")
|
|
@@ -47,15 +68,20 @@ module Suma
|
|
|
47
68
|
File.join(output_path, "schema_#{id}.yaml")
|
|
48
69
|
end
|
|
49
70
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
FileUtils.mkdir_p(File.dirname(filename_adoc))
|
|
71
|
+
def render_key_path
|
|
72
|
+
File.join(output_path, RENDER_KEY_FILE)
|
|
73
|
+
end
|
|
54
74
|
|
|
55
|
-
|
|
75
|
+
def config_relative_path
|
|
76
|
+
Pathname.new(filename_config)
|
|
56
77
|
.relative_path_from(Pathname.new(File.dirname(filename_adoc)))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def save_adoc(content)
|
|
81
|
+
log_relative "Save EXPRESS adoc", filename_adoc
|
|
57
82
|
|
|
58
|
-
File.
|
|
83
|
+
FileUtils.mkdir_p(File.dirname(filename_adoc))
|
|
84
|
+
File.write(filename_adoc, content)
|
|
59
85
|
end
|
|
60
86
|
|
|
61
87
|
def save_config
|
|
@@ -68,16 +94,56 @@ module Suma
|
|
|
68
94
|
config.save_to_path(filename_config)
|
|
69
95
|
end
|
|
70
96
|
|
|
97
|
+
# Everything the rendered document depends on. The schema source is an
|
|
98
|
+
# input even though the adoc references it only by path.
|
|
99
|
+
def render_key(adoc)
|
|
100
|
+
Digest::SHA256.hexdigest(
|
|
101
|
+
[adoc, File.read(filename_config), schema_source_digest,
|
|
102
|
+
JSON.generate(Provenance.render_toolchain)].join("\0"),
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def schema_source_digest
|
|
107
|
+
path = schema.path.to_s
|
|
108
|
+
File.file?(path) ? Digest::SHA256.file(path).hexdigest : "missing:#{path}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def rendered_current?(key)
|
|
112
|
+
File.file?(render_key_path) && File.read(render_key_path) == key &&
|
|
113
|
+
expected_output_paths.all? { |path| File.file?(path) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def expected_output_paths
|
|
117
|
+
extensions.split(",").map { |ext| filename_adoc(ext.strip) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def write_render_key(key)
|
|
121
|
+
File.write(render_key_path, key)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def reuse_render
|
|
125
|
+
log_relative "Reusing rendered schema doc (id: #{id})", filename_adoc
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
71
129
|
def invoke_metanorma
|
|
72
130
|
log_relative "Compiling schema (id: #{id})", filename_adoc
|
|
73
|
-
Metanorma::Compile.new.compile(
|
|
74
|
-
|
|
75
|
-
agree_to_terms: true,
|
|
76
|
-
install_fonts: false,
|
|
77
|
-
)
|
|
131
|
+
Metanorma::Compile.new.compile(filename_adoc, **compile_options)
|
|
132
|
+
verify_outputs!
|
|
78
133
|
log_relative "Compiling schema (id: #{id}) ... done!", filename_adoc
|
|
79
134
|
end
|
|
80
135
|
|
|
136
|
+
# Metanorma::Compile#compile can return nil for a handled failure instead
|
|
137
|
+
# of raising. Never cache a render that produced no output.
|
|
138
|
+
def verify_outputs!
|
|
139
|
+
missing = expected_output_paths.reject { |path| File.file?(path) }
|
|
140
|
+
return if missing.empty?
|
|
141
|
+
|
|
142
|
+
names = missing.map { |path| File.basename(path) }.join(", ")
|
|
143
|
+
raise CompilationError,
|
|
144
|
+
"Metanorma produced no #{names} for schema #{id}"
|
|
145
|
+
end
|
|
146
|
+
|
|
81
147
|
def log_relative(prefix, path)
|
|
82
148
|
relative = Pathname.new(path).relative_path_from(Dir.pwd)
|
|
83
149
|
Utils.log "#{prefix}: #{relative}"
|
data/lib/suma/schema_exporter.rb
CHANGED
|
@@ -47,6 +47,18 @@ module Suma
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def export_to_directory(schemas)
|
|
50
|
+
run_pool_export(schemas)
|
|
51
|
+
rescue SystemStackError
|
|
52
|
+
# One pathological schema (a very deep AST) can overflow the
|
|
53
|
+
# worker threads' ~1 MB stack. The main thread has the full OS
|
|
54
|
+
# stack and every completed export is cached, so the retry only
|
|
55
|
+
# pays for the schemas that did not finish.
|
|
56
|
+
Utils.log "[export] worker thread stack exhausted; " \
|
|
57
|
+
"retrying the remaining schemas on the main thread"
|
|
58
|
+
schemas.each { |schema| export_single_schema(schema) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def run_pool_export(schemas)
|
|
50
62
|
WorkerPool.new(size: options[:jobs]).map(schemas) do |schema|
|
|
51
63
|
export_single_schema(schema)
|
|
52
64
|
end
|
data/lib/suma/schema_index.rb
CHANGED
|
@@ -40,11 +40,11 @@ module Suma
|
|
|
40
40
|
|
|
41
41
|
def element_collections(schema)
|
|
42
42
|
[
|
|
43
|
-
schema.entities,
|
|
44
|
-
schema.types,
|
|
45
|
-
schema.constants,
|
|
46
|
-
schema.functions,
|
|
47
|
-
schema.rules,
|
|
43
|
+
Array(schema.entities),
|
|
44
|
+
Array(schema.types),
|
|
45
|
+
Array(schema.constants),
|
|
46
|
+
Array(schema.functions),
|
|
47
|
+
Array(schema.rules),
|
|
48
48
|
schema.procedures,
|
|
49
49
|
schema.subtype_constraints,
|
|
50
50
|
]
|
|
@@ -48,11 +48,18 @@ exclude_paths: nil)
|
|
|
48
48
|
YAML.safe_load(File.read(file_path, encoding: "UTF-8"), aliases: true)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# Collection manifest files referenced by the site manifest. Both
|
|
52
|
+
# manifest shapes are handled: legacy `docref` entries and the
|
|
53
|
+
# nested `documents` entries (metanorma 2.x collection manifests).
|
|
54
|
+
# Only `file` references count — `fileref` entries are attachments
|
|
55
|
+
# (documents, schema exports), not collection manifests.
|
|
51
56
|
def load_manifest_files(collection_files)
|
|
52
|
-
collection_files.
|
|
57
|
+
collection_files.flat_map do |c|
|
|
53
58
|
collection_data = load_yaml(c)
|
|
54
|
-
collection_data["manifest"]
|
|
55
|
-
|
|
59
|
+
manifest = collection_data["manifest"] || {}
|
|
60
|
+
entries = Array(manifest["docref"]) + Array(manifest["documents"])
|
|
61
|
+
entries.filter_map { |entry| entry["file"] }
|
|
62
|
+
end
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
def load_project_schemas(manifest_files)
|
data/lib/suma/schema_profile.rb
CHANGED
|
@@ -126,9 +126,21 @@ module Suma
|
|
|
126
126
|
category_ids: %w[other],
|
|
127
127
|
)
|
|
128
128
|
|
|
129
|
+
# buildingSMART IFC: one standalone schema per edition (IFC4, IFC4X3,
|
|
130
|
+
# ...), entities prefixed Ifc, no ARM/MIM-style decomposition.
|
|
131
|
+
IFC = new(
|
|
132
|
+
id: "ifc",
|
|
133
|
+
rules: [],
|
|
134
|
+
suffix_labels: {},
|
|
135
|
+
acronyms: %w[bim ifc idf 2d 3d 4d 5d],
|
|
136
|
+
acronym_patterns: [/\Aifc\d+(x\d+)*\z/],
|
|
137
|
+
category_ids: %w[other],
|
|
138
|
+
)
|
|
139
|
+
|
|
129
140
|
REGISTRY = {
|
|
130
141
|
"iso10303" => ISO_10303,
|
|
131
142
|
"generic" => GENERIC,
|
|
143
|
+
"ifc" => IFC,
|
|
132
144
|
}.freeze
|
|
133
145
|
end
|
|
134
146
|
end
|