suma 0.5.19 → 0.5.20

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: df03b41b1c8ad66d68f1f6bb711f9cc49396e1b9217b2819eef5f1a9ae98b997
4
- data.tar.gz: 04f0d313dbe23ce6579b0c2736d264c7934dcc250a84c9e55f8bc39014e50a8e
3
+ metadata.gz: 467b9bcc21844daaac8a68fdb76b15b9e165b6fd2243d469282f44d15f69cb97
4
+ data.tar.gz: 4714dc0a4e2b9b8f63f4cdb5a88db33401662633f15fb7669c3c18908dc77492
5
5
  SHA512:
6
- metadata.gz: 1eb98f1e8d9cd8ead0c40db5867929d74a50d440cd964848a0fbafaac2b23e0ad0d484522cc98a1ea5afc9c4cf88137ed8670af848088059c37c0c718da2dfdc
7
- data.tar.gz: 85bf0f0ea57ac912398b33264550a89f7b0118cac0b76f7a239cc33b49a78d39d9e535abdeb6a801022fb462dabb02727bbc38a055ee99b9f7ac7a88e50b8afe
6
+ metadata.gz: 4a2a42ab57f4d4cec318dd6232e604ae9081402d66372437abd3854efa435ad87fddd43e91fd0da24156584bfc652174b36930cc7d30b00fb727857b9cd3e66a
7
+ data.tar.gz: 69822cae107459997e8ebb80499fc6f8058f8e396949a33e8971b2cc6802ffbabe1f221f51c3b132b5c32170f965920363c5f61da3d6f7d3ae49e27a1a5b0ecb
@@ -3,6 +3,7 @@
3
3
  require "metanorma"
4
4
  require "yaml"
5
5
  require "fileutils"
6
+ require "pathname"
6
7
 
7
8
  module Suma
8
9
  # Memory-bounded, resumable staged collection build (metanorma/suma#94).
@@ -73,7 +74,7 @@ module Suma
73
74
  staged = shard_members(members)
74
75
  Utils.log shard_log(staged.size, members.size)
75
76
  if @workers == 1
76
- staged.each_with_index { |member, i| stage_member(member, i) }
77
+ unstaged_members(staged).each_with_index { |member, i| stage_member(member, i) }
77
78
  else
78
79
  stage_parallel(staged)
79
80
  end
@@ -104,6 +105,19 @@ module Suma
104
105
  "skipping reinflate"
105
106
  end
106
107
 
108
+ # Members already present in the store (their docref-slug artefact
109
+ # exists) are not re-rendered: an interrupted or repeated build over
110
+ # a warm store goes straight to reinflate, so the assembly pass costs
111
+ # minutes instead of re-rendering the whole collection.
112
+ def unstaged_members(members)
113
+ members.reject do |member|
114
+ identifier = member["identifier"]
115
+ stored = identifier && stored_semantic(identifier)
116
+ Utils.log "[staged] stored, skipping #{identifier}" if stored
117
+ stored
118
+ end
119
+ end
120
+
107
121
  # Batch the members and run +@workers+ renderer child processes
108
122
  # concurrently. Each batch file carries fully-rendered single-member
109
123
  # manifests; children claim batches through O_EXCL markers, so a slow
@@ -113,7 +127,7 @@ module Suma
113
127
  # identifier-keyed store entries, so concurrent children cannot race;
114
128
  # sectionsplit only happens at reinflate (a single sequential pass).
115
129
  def stage_parallel(members)
116
- paths, count = write_batch_files(members)
130
+ paths, count = write_batch_files(unstaged_members(members))
117
131
  run_batch_workers(count)
118
132
  ensure
119
133
  remove_batch_files(paths, count)
@@ -201,15 +215,13 @@ module Suma
201
215
  # identifier; members whose document docid differs from the
202
216
  # docref (docrefs carrying metanorma's GUID marker) were
203
217
  # therefore unrecoverable ("no stored artefact"). Alias the
204
- # artefacts this render produced under the docref slug too.
205
- # Shape: single_member_manifest => {"manifest" =>
206
- # {"manifest" => [{"docref" => [dr]}]}}.
218
+ # artefacts keyed by the render's own bibdata keys under the
219
+ # docref slug too. Shape: single_member_manifest =>
220
+ # {"manifest" => {"manifest" => [{"docref" => [dr]}]}}.
207
221
  member_id = manifest.dig("manifest", "manifest", 0, "docref", 0, "identifier")
208
222
  id_slug = member_id.to_s
209
223
  .gsub(/[^A-Za-z0-9._-]+/, "-").gsub(/-{2,}/, "-")
210
224
  .gsub(/\A-|-\z/, "")
211
- semantics_before = Dir.children(store_dir)
212
- .select { |f| f.end_with?(".semantic.xml") }
213
225
  compile = { install_fonts: false }
214
226
  compile[:novalid] = true if no_valid == "1"
215
227
  collection = Metanorma::Collection.parse(one_path)
@@ -219,37 +231,25 @@ module Suma
219
231
  compile: compile,
220
232
  )
221
233
  unless id_slug.empty?
222
- # A warm artefact cache makes the render a full cache hit:
223
- # no new artefact files appear, so a produced-files delta
224
- # never fires. The collection's own keys are what the
225
- # renderer stored under, so alias every semantic artefact
226
- # keyed by them to the docref slug as well.
227
- store_keys = (collection.bibdatas || {}).keys
228
- produced = Dir.children(store_dir).select do |f|
229
- f.end_with?(".semantic.xml")
230
- end - semantics_before
231
- if produced.empty?
232
- store_keys.each do |key|
233
- src_slug = key.to_s
234
- .gsub(/[^A-Za-z0-9._-]+/, "-").gsub(/-{2,}/, "-")
235
- .gsub(/\A-|-\z/, "")
236
- next if src_slug.empty? || src_slug == id_slug
237
-
238
- Dir[File.join(store_dir,
239
- "#{src_slug}.*.semantic.xml")].each do |f|
240
- alias_name = f.sub(
241
- %r{/#{Regexp.escape(src_slug)}.},
242
- "/#{id_slug}.",
243
- )
244
- FileUtils.cp(f, alias_name)
245
- end
246
- end
247
- end
248
- produced.each do |f|
249
- if (m = f.match(/\A(.*).([0-9a-f]+).semantic.xml\z/))
250
- alias_name = "#{id_slug}.#{m[2]}.semantic.xml"
251
- alias_path = File.join(store_dir, alias_name)
252
- FileUtils.cp(File.join(store_dir, f), alias_path)
234
+ # Alias strictly from the collection's own bibdata keys:
235
+ # they are the docids this member's render stored under.
236
+ # A produced-files delta is not safe here - with
237
+ # concurrent workers another child's render lands in the
238
+ # shared store mid-flight, and a delta would alias other
239
+ # members' documents under this docref (suma#167).
240
+ (collection.bibdatas || {}).keys.each do |key|
241
+ src_slug = key.to_s
242
+ .gsub(/[^A-Za-z0-9._-]+/, "-").gsub(/-{2,}/, "-")
243
+ .gsub(/\A-|-\z/, "")
244
+ next if src_slug.empty? || src_slug == id_slug
245
+
246
+ Dir[File.join(store_dir,
247
+ "#{src_slug}.*.semantic.xml")].each do |f|
248
+ alias_name = f.sub(
249
+ %r{/#{Regexp.escape(src_slug)}.},
250
+ "/#{id_slug}.",
251
+ )
252
+ FileUtils.cp(f, alias_name)
253
253
  end
254
254
  end
255
255
  end
@@ -313,21 +313,81 @@ module Suma
313
313
  end
314
314
 
315
315
  # Assemble a manifest of the stored stubs and reinflate them into the site.
316
+ # The original manifest tree is preserved (nested collections,
317
+ # attachments, per-member sectionsplit) with each member entry swapped
318
+ # for its stored stub; the render resolves paths against the
319
+ # reinflate manifest's own directory, so every path the render reads
320
+ # is absolutized against the collection root (suma#168, suma#169).
316
321
  def reinflate(members)
317
322
  reinf_dir = File.join(@output_directory, "_staged_reinflate")
318
323
  FileUtils.mkdir_p(reinf_dir)
319
324
  docrefs = stored_docrefs(members, reinf_dir)
325
+ write_reinflate_manifest(docrefs, reinf_dir)
326
+ run_render(manifest_path: File.join(reinf_dir, "reinflate.yml"),
327
+ opts: reinflate_render_options)
328
+ end
329
+
330
+ def write_reinflate_manifest(docrefs, reinf_dir)
331
+ rebuilt = with_stubbed_members(manifest["manifest"], docrefs, [0])
320
332
  reinf_path = File.join(reinf_dir, "reinflate.yml")
321
333
  File.write(reinf_path,
322
- base_manifest.merge("manifest" => collection_manifest(docrefs)).to_yaml)
334
+ base_manifest.merge("manifest" => rebuilt).to_yaml)
323
335
  Utils.log "[staged] reinflating #{docrefs.size} stored members -> " \
324
336
  "#{@output_directory}"
325
- run_render(
326
- manifest_path: reinf_path,
327
- opts: { format: @formats, output_folder: @output_directory,
328
- reinflate: true, coverpage: @coverpage,
329
- compile: compile_flags }.compact,
330
- )
337
+ end
338
+
339
+ def reinflate_render_options
340
+ {
341
+ format: @formats,
342
+ output_folder: File.expand_path(@output_directory),
343
+ reinflate: true,
344
+ coverpage: @coverpage && File.expand_path(@coverpage, @config_dir),
345
+ compile: compile_flags,
346
+ }.compact
347
+ end
348
+
349
+ # Deep-copy the manifest tree, replacing each member entry (same
350
+ # predicate and pre-order as collection_members) with its stub docref,
351
+ # and absolutizing non-member filerefs (attachments) against the
352
+ # collection root.
353
+ def with_stubbed_members(node, docrefs, counter)
354
+ case node
355
+ when Array
356
+ node.map { |child| with_stubbed_members(child, docrefs, counter) }
357
+ when Hash
358
+ with_stubbed_members_hash(node, docrefs, counter)
359
+ else
360
+ node
361
+ end
362
+ end
363
+
364
+ def with_stubbed_members_hash(node, docrefs, counter)
365
+ rewritten = node.dup
366
+ rewritten.each_key do |key|
367
+ rewritten[key] = rewrite_entry_value(key, rewritten[key], docrefs, counter)
368
+ end
369
+ stub_for(rewritten, docrefs, counter) || rewritten
370
+ end
371
+
372
+ def rewrite_entry_value(key, value, docrefs, counter)
373
+ if %w[file fileref].include?(key) && value &&
374
+ !Pathname.new(value).absolute?
375
+ File.expand_path(value, @config_dir)
376
+ else
377
+ with_stubbed_members(value, docrefs, counter)
378
+ end
379
+ end
380
+
381
+ def stub_for(node, docrefs, counter)
382
+ return unless member_entry?(node)
383
+
384
+ index = counter[0]
385
+ counter[0] += 1
386
+ docrefs[index]
387
+ end
388
+
389
+ def member_entry?(node)
390
+ (node["fileref"] || node["file"]) && !truthy?(node["attachment"])
331
391
  end
332
392
 
333
393
  def compile_flags
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.19"
4
+ VERSION = "0.5.20"
5
5
  end
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.19
4
+ version: 0.5.20
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-01 00:00:00.000000000 Z
11
+ date: 2026-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: expressir