relaton 3.0.2 → 3.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f15745a534625b27b3d013d7b3d6edd7e0185f1bcc1c545f7b44dc6059d9d113
4
- data.tar.gz: ac15b5259abb3c980cd6bee9d10c5f2f0ae12fea955478ca28f7da82027ab6f6
3
+ metadata.gz: f6cef1665e29f9d4a6e8ab48528253675e5b901399666138306c2be69c0d1b6e
4
+ data.tar.gz: e4eef10cab8373db1d911a4b68342b35c1bb335c15f986d24697f286e83d9a96
5
5
  SHA512:
6
- metadata.gz: de3f2c7182f6a2b1737a54e9168bcab856dbc81fe169f293b5b932f0224e8cb95187f506829230068ebc5308884e89a918515b03838f969c9c9870020442efff
7
- data.tar.gz: a4bfe1d14667d8907eecd43fe880b02e7d15ceab20d2c6302c8f5c220d6c4398b3adefa056ff8815639686ad85674713c5c3a4759bd5ad57ffdd1e42b9bc39e1
6
+ metadata.gz: 41185be7ff82e3b45667eb33380fb3eee8e548e3ea24039d8743c5ed50b2fcc1b1c252536f87a1d6b0c4821e41e17b08a45e861af1d6886a8abe9df287a800a6
7
+ data.tar.gz: 3235473497b828e0f5d8890a59b6548994b2ab113d665f010d24e1970c6b24eaa398b298d7f83ba3b770153c558c712059ff5147b5f39d86b61ad5e35312d930
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  require "fileutils"
2
3
  require "net/http"
3
4
  require "uri"
@@ -64,7 +65,7 @@ module Relaton
64
65
  # @return [String, nil] file name
65
66
  #
66
67
  def get_file(renewal) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
67
- @current = YAML.load_file CURRENT if File.exist? CURRENT
68
+ @current = Yeptris::Psych.load_file CURRENT if File.exist? CURRENT
68
69
  @current ||= {}
69
70
  uri = URI(CSV_URL)
70
71
  n = 0
@@ -183,7 +184,7 @@ module Relaton
183
184
  # @return [Relaton3gpp::BibliographicItem, nil] merged bibliographic item or nil if no merge has been done
184
185
  #
185
186
  def merge_duplication(bib, file)
186
- hash = YAML.load_file file
187
+ hash = Yeptris::Psych.load_file file
187
188
  existed = Item.from_hash hash
188
189
  changed = update_source? bib, existed
189
190
  bib1, bib2, chng = transposed_relation bib, existed
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  module Relaton::Bipm
2
3
  class DataOutcomesParser
3
4
  SHORTTYPE = {
@@ -130,7 +131,7 @@ module Relaton::Bipm
130
131
  fr_file = en_file.sub "en", "fr"
131
132
  [en_file, fr_file].map do |file|
132
133
  if File.exist? file
133
- data = YAML.safe_load(File.read(file, encoding: "UTF-8"))
134
+ data = Yeptris::Psych.safe_load(File.read(file, encoding: "UTF-8"))
134
135
  path = file
135
136
  end
136
137
  [path, data]
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  require "moxml"
2
3
 
3
4
  module Relaton::Bipm
@@ -60,8 +61,8 @@ module Relaton::Bipm
60
61
  warn_duplicate = false
61
62
  item2 = Item.from_yaml File.read(outfile, encoding: "UTF-8")
62
63
  fix_si_brochure_id item2
63
- hash1 = YAML.safe_load item1.to_yaml
64
- hash2 = YAML.safe_load item2.to_yaml
64
+ hash1 = Yeptris::Psych.safe_load item1.to_yaml
65
+ hash2 = Yeptris::Psych.safe_load item2.to_yaml
65
66
  Item.from_yaml deep_merge(hash1, hash2).to_yaml
66
67
  else
67
68
  warn_duplicate = true
data/lib/relaton/bipm.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require "zip"
2
2
  require "fileutils"
3
- require "parslet"
4
3
  require "relaton/bib"
5
4
  require "relaton/index"
6
5
  require "relaton/core"
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  require "yeptris"
2
3
  require "fileutils"
3
4
 
@@ -121,7 +122,7 @@ module Relaton
121
122
  kept = ledger.merge(ledger_new).select do |_, row|
122
123
  row.is_a?(Hash) && row["file"] && @written.include?(row["file"])
123
124
  end
124
- File.write ledger_path, YAML.dump(kept), encoding: "UTF-8"
125
+ File.write ledger_path, Yeptris::Psych.dump(kept), encoding: "UTF-8"
125
126
  @ledger = kept
126
127
  end
127
128
 
@@ -349,7 +350,7 @@ module Relaton
349
350
 
350
351
  def ledger
351
352
  @ledger ||= if File.exist?(ledger_path)
352
- YAML.safe_load_file(ledger_path) || {}
353
+ Yeptris::Psych.safe_load_file(ledger_path) || {}
353
354
  else
354
355
  {}
355
356
  end
@@ -67,7 +67,7 @@ module Relaton
67
67
  # @return [Pubid::Identifier, nil]
68
68
  def cache_pubid(ref)
69
69
  pubid_class&.parse(ref)
70
- rescue ArgumentError, ::Pubid::Errors::Error, Parslet::ParseFailed
70
+ rescue ArgumentError, ::Pubid::Errors::Error
71
71
  # relaton-bib carries the log facade; core specs load without it.
72
72
  require "relaton/bib"
73
73
  Relaton::Bib::Util.info "does not parse as this flavor's pubid; " \
@@ -134,6 +134,21 @@ module Relaton
134
134
  copy
135
135
  end
136
136
 
137
+ # A copy of `pubid` with each given component the parse leaves nil, for
138
+ # #item_cache_key. `pubid` itself when there is nothing to add.
139
+ #
140
+ # @param pubid [Pubid::Identifier]
141
+ # @param values [Hash{Symbol => String, nil}]
142
+ # @return [Pubid::Identifier]
143
+ def with_components(pubid, **values)
144
+ values = values.select { |attr, value| value && pubid.public_send(attr).nil? }
145
+ return pubid if values.empty?
146
+
147
+ copy = pubid.class.from_hash pubid.to_hash
148
+ values.each { |attr, value| copy.public_send "#{attr}=", value }
149
+ copy
150
+ end
151
+
137
152
  # The cache key for a query: the parsed pubid with the `year` and
138
153
  # `all_parts` options folded in. The publication date range is not part
139
154
  # of the identity, so it never enters the key.
@@ -153,6 +168,20 @@ module Relaton
153
168
  pubid
154
169
  end
155
170
 
171
+ # The cache key of a fetched document: its primary docidentifier as
172
+ # parsed (`pubid`), plus the components that docid omits by design and
173
+ # the item keeps elsewhere. A flavor whose printed docid drops the
174
+ # edition (IHO `B-12`, ECMA `ECMA-262`) overrides this, or every
175
+ # edition gets one key and one cache file (relaton#408). The same set
176
+ # as relaton-cli's `MachineIndex::REFINABLE_KEYS`.
177
+ #
178
+ # @param pubid [Pubid::Identifier] the docid's parse; not mutated
179
+ # @param _item [Relaton::Bib::ItemData] the fetched document
180
+ # @return [Pubid::Identifier]
181
+ def item_cache_key(pubid, _item)
182
+ pubid
183
+ end
184
+
156
185
  def initialize
157
186
  raise "This is an abstract class!"
158
187
  end
@@ -223,6 +223,17 @@ relaton#189 item 2 / relaton#204. Two lutaml-store `FileSystem` stores under
223
223
  both naming the same file. There are no redirect entries. A document is
224
224
  deleted with the last row that points to it; a row whose document is gone is
225
225
  a miss.
226
+ - **The item row's key must tell editions apart (relaton#408).** `Db#item_key`
227
+ parses the primary docidentifier and passes it through
228
+ `Core::Processor#item_cache_key(pubid, item)`. A flavor whose printed docid
229
+ omits the edition by design overrides the hook to add it from the item:
230
+ IHO adds `version` from `edition` (`IHO B-12` → `IHO B-12 3.0.0`), ECMA adds
231
+ `edition` and the `volume` extent (`ECMA-262` → `ECMA-262 ed16`), through
232
+ `#with_components`, which fills only components the parse leaves nil, on a
233
+ copy. Without it every edition got one item key and one file, and a fetch of
234
+ `IHO B-12 2.0.3` overwrote the file the `IHO B-12 3.0.0` query row named. A
235
+ cache with such a file is dropped by the next `Relaton::VERSION` bump
236
+ (`grammar_hash`), so no separate cache-version bump was made.
226
237
 
227
238
  Matching: the **exact** row first (canonical `to_hash`, not pubid `==`, which
228
239
  is false after a `from_hash` round trip for some flavors, e.g. UN). Only a
@@ -233,8 +244,9 @@ still applies and it never answers with another query's year-stripped copy.
233
244
  `ISO 19115:2003 === ISO 19115-1:2003` (and the same for IEC, BSI, JIS, GB).
234
245
  `Cache#subset_of?` therefore also requires that every component the row adds
235
246
  (at any depth) is allowed: only a language for the dated fallback
236
- (`LANGUAGE_COMPONENTS`), a year/date/month or a language for the date-range
237
- `candidates` (`EDITION_COMPONENTS`). A row with no `root.number` (DOI, ISBN)
247
+ (`LANGUAGE_COMPONENTS`), a year/date/month, an edition/version (the
248
+ `item_cache_key` components; not a volume, which names another document) or a
249
+ language for the date-range `candidates` (`EDITION_COMPONENTS`). A row with no `root.number` (DOI, ISBN)
238
250
  goes to one of 256 digest buckets (`<flavor>/~<hex>`).
239
251
 
240
252
  Expiry (`Cache#valid_row?`): an undated row, and **every** `not_found` row,
@@ -36,8 +36,11 @@ module Relaton
36
36
  # reads an omitted `part` as "any part" (`ISO 19115:2003 ===
37
37
  # ISO 19115-1:2003`), which names another document.
38
38
  LANGUAGE_COMPONENTS = %w[language languages].freeze
39
- # What a row may add to a key and still be one of its editions.
40
- EDITION_COMPONENTS = (%w[year date month] + LANGUAGE_COMPONENTS).freeze
39
+ # What a row may add to a key and still be one of its editions. An
40
+ # `edition` or a `version` comes from Core::Processor#item_cache_key
41
+ # (IHO, ECMA; relaton#408). A `volume` names another document.
42
+ EDITION_COMPONENTS =
43
+ (%w[year date month edition version] + LANGUAGE_COMPONENTS).freeze
41
44
 
42
45
  # @return [String]
43
46
  attr_reader :dir
@@ -221,7 +221,7 @@ module Relaton
221
221
  require "pubid"
222
222
  parsed = ::Pubid.parse reference
223
223
  parsed if parsed.to_s == reference
224
- rescue ArgumentError, ::Pubid::Errors::Error, Parslet::ParseFailed
224
+ rescue ArgumentError, ::Pubid::Errors::Error
225
225
  nil
226
226
  end
227
227
 
data/lib/relaton/db.rb CHANGED
@@ -226,7 +226,7 @@ module Relaton
226
226
  # reference they cannot read is not-found, not a failure (relaton#412).
227
227
  def unreadable_reference?(error)
228
228
  (defined?(Pubid::Errors::Error) && error.is_a?(Pubid::Errors::Error)) ||
229
- (defined?(Parslet::ParseFailed) && error.is_a?(Parslet::ParseFailed))
229
+ false
230
230
  end
231
231
 
232
232
  # The flavor routing picks for `fetch_async`'s per-flavor queue; nil
@@ -382,14 +382,18 @@ module Relaton
382
382
  [query, query && processor.cache_key(code, year, opts, query)]
383
383
  end
384
384
 
385
- # The key of a fetched document, from its primary identifier. The
386
- # identifier is data, so an unparseable one gives no key.
385
+ # The key of a fetched document, from its primary identifier and what
386
+ # that identifier omits (Core::Processor#item_cache_key, relaton#408).
387
+ # The identifier is data, so an unparseable one gives no key.
387
388
  def item_key(bib, stdclass)
388
389
  docid = bib.docidentifier.detect(&:primary) || bib.docidentifier.first
389
390
  return unless docid&.content
390
391
 
391
- cache_key docid.content, nil, {}, stdclass
392
- rescue ::Pubid::Errors::Error, Parslet::ParseFailed
392
+ key = cache_key docid.content, nil, {}, stdclass
393
+ return key unless key.is_a? ::Pubid::Identifier
394
+
395
+ @registry[stdclass].item_cache_key key, bib
396
+ rescue ::Pubid::Errors::Error
393
397
  nil
394
398
  end
395
399
 
@@ -456,7 +460,7 @@ module Relaton
456
460
  def copublisher_query(code, opts, stdclass, publisher)
457
461
  query = begin
458
462
  @registry[stdclass].query_pubid(code, opts)
459
- rescue ::Pubid::Errors::Error, Parslet::ParseFailed
463
+ rescue ::Pubid::Errors::Error
460
464
  nil
461
465
  end
462
466
  return query if query
@@ -21,6 +21,14 @@ module Relaton
21
21
  Bibliography.get(code, date, opts)
22
22
  end
23
23
 
24
+ # An ECMA record prints `ECMA-262` with no ` ed<N>`/` vol<N>`, and keeps
25
+ # them in `edition` and in a `volume` extent (relaton#408).
26
+ def item_cache_key(pubid, item)
27
+ volume = item.extent.flat_map(&:locality)
28
+ .detect { |l| l.type == "volume" }&.reference_from
29
+ with_components pubid, edition: item.edition&.content, volume: volume
30
+ end
31
+
24
32
  #
25
33
  # Fetch all the documents from a source
26
34
  #
@@ -1,4 +1,4 @@
1
- require "yaml"
1
+ require "yeptris/psych"
2
2
 
3
3
  module Relaton
4
4
  # Cross-SDO equivalence as data: chain patterns and explicit equivalences
@@ -23,7 +23,7 @@ module Relaton
23
23
  else
24
24
  [path]
25
25
  end
26
- files.each { |f| tables << YAML.safe_load_file(f, aliases: true) }
26
+ files.each { |f| tables << Yeptris::Psych.safe_load_file(f, aliases: true) }
27
27
  tables
28
28
  end
29
29
 
@@ -1,7 +1,7 @@
1
+ require "yeptris/psych"
1
2
  # encoding: UTF-8
2
3
  # frozen_string_literal: true
3
4
 
4
- require "yaml"
5
5
  require "gb_agencies"
6
6
 
7
7
  module Relaton
@@ -132,7 +132,7 @@ module Relaton
132
132
  # @param pref [String]
133
133
  # @return [Hash{String=>String}]
134
134
  def prefix(pref)
135
- @prefixes ||= YAML.load_file File.join(__dir__, "yaml/prefixes.yaml")
135
+ @prefixes ||= Yeptris::Psych.load_file File.join(__dir__, "yaml/prefixes.yaml")
136
136
  @prefixes[pref]
137
137
  end
138
138
 
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  require "etc"
2
3
  require "zip"
3
4
  require_relative "../ieee"
@@ -77,7 +78,7 @@ module Relaton
77
78
  # @param file [String]
78
79
  # @return [String, nil]
79
80
  def primary_docid(file)
80
- yaml = YAML.safe_load(File.read(file, encoding: "UTF-8"),
81
+ yaml = Yeptris::Psych.safe_load(File.read(file, encoding: "UTF-8"),
81
82
  permitted_classes: [Symbol, Date, Time])
82
83
  id = yaml && yaml["docidentifier"]&.find do |i|
83
84
  i["type"] == "IEEE" && i["primary"] == true && i["trademark"].nil?
@@ -25,6 +25,12 @@ module Relaton
25
25
  Bibliography.get(code, date, opts)
26
26
  end
27
27
 
28
+ # An IHO record prints `B-12` and keeps its edition in `edition`, so
29
+ # the edition goes on the key as the version (relaton#408).
30
+ def item_cache_key(pubid, item)
31
+ with_components pubid, version: item.edition&.content
32
+ end
33
+
28
34
  # @param xml [String]
29
35
  # @return [Relaton::Iho::ItemData]
30
36
  def from_xml(xml)
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  module Relaton
2
3
  module Index
3
4
  #
@@ -219,12 +220,12 @@ module Relaton
219
220
  end
220
221
 
221
222
  def load_index(yaml, save = false)
222
- index = YAML.safe_load(yaml, permitted_classes: [Symbol])
223
+ index = Yeptris::Psych.safe_load(yaml, permitted_classes: [Symbol])
223
224
  save index if save
224
225
  return deserialize_pubid(index) if check_format(index)
225
226
 
226
227
  report_invalid_index(save, "Wrong structure of")
227
- rescue Psych::SyntaxError
228
+ rescue Yeptris::Psych::SyntaxError
228
229
  report_invalid_index(save, "YAML parsing error when reading")
229
230
  rescue InvalidIndexError
230
231
  report_invalid_index(save, "Wrong structure of")
@@ -430,7 +431,7 @@ module Relaton
430
431
  end
431
432
  yaml = yaml.read unless yaml.is_a?(String)
432
433
  Util.info "Downloaded index from `#{url}` for sqlite materialization", progname
433
- raw = YAML.safe_load(yaml, permitted_classes: [Symbol])
434
+ raw = Yeptris::Psych.safe_load(yaml, permitted_classes: [Symbol])
434
435
  return nil unless check_format(raw)
435
436
 
436
437
  Enumerator.new do |y|
@@ -439,7 +440,7 @@ module Relaton
439
440
  y << [number, number, r[:id], r[:file]]
440
441
  end
441
442
  end
442
- rescue Psych::SyntaxError, SocketError, OpenURI::HTTPError, Errno::ECONNRESET,
443
+ rescue Yeptris::Psych::SyntaxError, SocketError, OpenURI::HTTPError, Errno::ECONNRESET,
443
444
  OpenSSL::SSL::SSLError => e
444
445
  Util.info "SQLite index build failed (#{e.message}); falling back", progname
445
446
  nil
@@ -512,8 +513,8 @@ module Relaton
512
513
  return unless yaml
513
514
 
514
515
  begin
515
- raw = YAML.safe_load(yaml, permitted_classes: [Symbol])
516
- rescue Psych::SyntaxError
516
+ raw = Yeptris::Psych.safe_load(yaml, permitted_classes: [Symbol])
517
+ rescue Yeptris::Psych::SyntaxError
517
518
  warn_local_index_error("YAML parsing error when reading")
518
519
  delete_sidecar
519
520
  return [[], []]
@@ -613,7 +614,7 @@ module Relaton
613
614
  yaml = zip.entries.first.get_input_stream.read
614
615
  end
615
616
  Util.info "Downloaded index from `#{url}`", progname
616
- raw = YAML.safe_load(yaml, permitted_classes: [Symbol])
617
+ raw = Yeptris::Psych.safe_load(yaml, permitted_classes: [Symbol])
617
618
  if check_format(raw)
618
619
  save raw
619
620
  raw = nil # release the parsed copy; read_raw_file loads the sidecar's
@@ -622,7 +623,7 @@ module Relaton
622
623
  warn_remote_index_error "Wrong structure of"
623
624
  [[], []]
624
625
  end
625
- rescue Psych::SyntaxError
626
+ rescue Yeptris::Psych::SyntaxError
626
627
  warn_remote_index_error "YAML parsing error when reading"
627
628
  [[], []]
628
629
  end
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  require "yeptris"
2
3
  require "json"
3
4
  require "net/http"
@@ -152,11 +153,11 @@ module Relaton
152
153
  yaml = unzip(body)
153
154
  raise Error, "empty archive at `#{url(path)}`" unless yaml
154
155
 
155
- index = YAML.safe_load(yaml, permitted_classes: [Symbol])
156
+ index = Yeptris::Psych.safe_load(yaml, permitted_classes: [Symbol])
156
157
  raise Error, "wrong structure of `#{url(path)}`" unless @file_io.check_basic_format(index)
157
158
 
158
159
  @file_io.deserialize_pubid(index)
159
- rescue FileIO::InvalidIndexError, Psych::SyntaxError, Zip::Error, Zlib::Error => e
160
+ rescue FileIO::InvalidIndexError, Yeptris::Psych::SyntaxError, Zip::Error, Zlib::Error => e
160
161
  raise Error, "cannot read `#{url(path)}`: #{e.message}"
161
162
  end
162
163
  end
@@ -88,13 +88,30 @@ module Relaton
88
88
  end
89
89
 
90
90
  def pubid_match?(pubid)
91
- match_excludings = translate_excludings(excludings) + [:all_parts]
92
- match_excludings << :edition unless pubid.typed_stage&.abbr&.include?("DIR")
91
+ match_excludings = base_match_excludings
92
+ unless pubid.typed_stage&.abbr&.include?("DIR")
93
+ match_excludings = match_excludings + [:edition]
94
+ end
93
95
  # Only the candidate is built via .create (from the index) and so may
94
96
  # carry a compound part; `ref_pubid_no_year` is always a parsed pubid,
95
97
  # already split, so it needs no normalization.
96
98
  cand = normalize_compound_part(exclude_id_attrs(pubid, *match_excludings))
97
- cand == exclude_id_attrs(ref_pubid_no_year, *match_excludings)
99
+ cand == ref_excluded(match_excludings)
100
+ end
101
+
102
+ # The candidate's excludings set varies only by the DIR exception; the
103
+ # reference side is invariant per distinct set. Exclude the ref's
104
+ # (possibly deep) base chain once per set, not once per candidate —
105
+ # on a large vocabulary compile this chain ran per candidate per
106
+ # lookup and dominated the bibitem-resolution phase (relaton#423).
107
+ def base_match_excludings
108
+ @base_match_excludings ||= translate_excludings(excludings) + [:all_parts]
109
+ end
110
+
111
+ def ref_excluded(match_excludings)
112
+ @ref_excluded ||= {}
113
+ @ref_excluded[match_excludings] ||=
114
+ exclude_id_attrs(ref_pubid_no_year, *match_excludings)
98
115
  end
99
116
 
100
117
  # @TODO TEMP WORKAROUND (pubid 2.x migration): the v1-generated index
@@ -1,5 +1,5 @@
1
+ require "yeptris/psych"
1
2
  require "fileutils"
2
- require "yaml"
3
3
  require "date"
4
4
 
5
5
  module Relaton::Jcgm
@@ -65,7 +65,7 @@ module Relaton::Jcgm
65
65
  end
66
66
 
67
67
  def load_yaml(file)
68
- YAML.safe_load File.read(file, encoding: "UTF-8"), permitted_classes: [Date, Time]
68
+ Yeptris::Psych.safe_load File.read(file, encoding: "UTF-8"), permitted_classes: [Date, Time]
69
69
  end
70
70
 
71
71
  # "JCGM 17th Meeting (2012)" — ordinal via pubid's Meeting.ordinal.
@@ -64,7 +64,8 @@ All data models inherit from `Lutaml::Model::Serializable` and use declarative a
64
64
  - **Per-record resilience (mirrors the ISO flavor).** A single bad record must never abort the crawl. `#write_file` skips (never `id.content`-crashes on) a record whose MODS entry yields **no docidentifier** (e.g. a DOI stored only in `<identifier type="doi">`, which `ModsParser#parse_doi` doesn't read), and records ids that `#pubid` can't parse (new series like `NIST RB`/`NIST CHIPS`) as "not indexed" — the YAML is still written, only the index-v2 entry is skipped. Both classes accumulate in `#failures`; the `#report_errors` override emits each at `:error` (via `#log_error` → `Util.error`), which the shared `Core::DataFetcher` gh_issue machinery turns into one "Error fetching documents" GitHub issue in CI (`GITHUB_REPOSITORY` + `GITHUB_TOKEN`). `#pubid` no longer warns per-failure; reporting is centralized in `#report_errors`.
65
65
  - **Draft-stage rows come from CSRC (relaton#313).** The MODS collection has no draft records, so `#fetch_csrc` adds the CSRC pubs-export feed (`PubsExport.instance.data`) after the MODS pass. It builds each row **exactly as the runtime builds a CSRC hit** — `HitCollection#hit_data` (which calls `#pubs_export_id`) → `Scraper.parse_page` — so the crawled id and the live id are one string, with the stage, revision and update from the row's uri and DOI (`NIST FIPS 197/Upd1 ipd`, `NIST SP 800-108r1 ipd`, `NIST SP 1800-12 2pd`). Only a row whose id carries a **stage** is written; finals come from MODS, so a CSRC row never shadows a base document. On the 914-row fixture feed this writes 302 staged records. Don't go back to a hand-built `"NIST #{series} #{docnumber} #{stage}"` id: it dropped the numbered drafts (`2pd`…), the `/UpdN`, and spelled revisions `Rev. 1`. A stage that CSRC does not list (e.g. `NIST FIPS 197 fpd`) still answers through the consumer's #303 family fallback. Three more rules: `#csrc_bib` clears the `fetched` date the runtime stamps, or every draft changes each day and `Core#force_write` rewrites ~300 files per incremental crawl; CSRC lists a republished draft twice (`…/ipd` and `…/ipd-(1)`, one DOI, so one id), so each id is written once and the plain uri wins; and only the build step is rescued (to a `#failures` entry) — a write error stops the crawl, as in the MODS pass. `#pubs_export_id` must not mutate its row (`sub`, not `sub!`): the crawler and the runtime share the `PubsExport` data.
66
66
  - **A CSRC relation names the related document (relaton#314).** `Scraper#related_id` finds the related feed row by its uri and derives its id with `HitCollection.pubs_export_id` (the class method the instance method delegates to). The entry's own `docidentifier` drops the update and the part, and the hit's code is the record's own id, so neither is the related id. `HitCollection#search_filter` ends in an `ensure` that returns `arr`: every branch must set `arr`, because an earlier `return` is overridden there.
67
- - **CSWP comes from CSRC, not the NIST Library (relaton-nist#104).** The NIST Library announced CSWP data, but its MODS release (`allrecords-MODS.xml`, and the MARC `allrecords.xml`) holds **no** CSWP record — 0 on the Sept2026 release. It publishes CSWPs only as RIS/BibTeX files under `bib/` on its `nist-pages` branch. So the index has only the CSRC draft CSWP rows (`#fetch_csrc`), and a final CSWP answers from CSRC at runtime: `HitCollection#search` asks the CSRC feed (`from_json`) and asks the index (`from_ga`) only when CSRC gives no hit. That is the chosen rule — CSRC wins, no merge — because CSRC lists every numbered CSWP with more metadata (affiliations, status, supersedes, comment period). The known gap: the NIST Library also has about 35 **legacy dated ids** (`NIST CSWP 01142020`, the same paper as `CSWP 9`, from before NIST renumbered the series) and translations (`NIST.CSWP.6.spa`). CSRC lists neither, so they answer not-found; a fix needs a mapping built from those RIS files. `hit_collection_spec.rb` pins the CSRC answer.
67
+ - **CSWP comes from CSRC, not the NIST Library (relaton-nist#104).** The NIST Library announced CSWP data, but its MODS release (`allrecords-MODS.xml`, and the MARC `allrecords.xml`) holds **no** CSWP record — 0 on the Sept2026 release. It publishes CSWPs only as RIS/BibTeX files under `bib/` on its `nist-pages` branch. So the index has only the CSRC draft CSWP rows (`#fetch_csrc`), and a final CSWP answers from CSRC at runtime: `HitCollection#search` asks the CSRC feed (`from_json`) and asks the index (`from_ga`) only when CSRC gives no hit. That is the chosen rule — CSRC wins, no merge — because CSRC lists every numbered CSWP with more metadata (affiliations, status, supersedes, comment period). `hit_collection_spec.rb` pins the CSRC answer.
68
+ - **Legacy dated CSWP ids and CSWP translations come from a static table (relaton#406).** CSRC lists neither the ~30 **legacy dated ids** (`NIST CSWP 01142020`, the same paper as `CSWP 9`, from before NIST numbered the series) nor the translations (`NIST.CSWP.6.spa`). `cswp.yaml` holds both, built once from the 84 RIS files (header of the file says how), and `Cswp` reads it. `Bibliography.get` asks `Cswp.lookup` **first** — the parenthetical-date regex after it would strip `(spa)`. A legacy id fetches the numbered id through the normal CSRC path and adds the dated id as a non-primary docidentifier (`06222021-draft` → the CSRC `CSWP 19 ipd`); a translation, dated or numbered, answers an offline record from the table (`NIST CSWP 6(spa)`, the RIS title/authors/year/DOI, `language`/`script` from `Cswp::LANGUAGES`, a `translatedFrom` relation to the original). `normalize` reads `NIST CSWP 6 spa`, `NIST CSWP 6(spa)` and `NIST.CSWP.6.spa` as one key. `Processor#cache_pubid` returns nil (so `Db` hands `get` the String and does not cache) for **every legacy id** and for a translation that pubid does not render back. A legacy answer is the numbered record plus the dated id, and `Db` would store it under the numbered id's row, so a later `NIST CSWP 9` would carry `NIST CSWP 01142020`. pubid reads `04162018ar` as `04162018a`, `6 spa` as `6(spa)`, and fails on `04162018bg`/`02122014pl`. A translation answers only for its own RIS year. Not mapped, on purpose: `02042022-1`…`-5` have no SN and share the title "Cybersecurity White Paper: EO Response", so they still answer not-found. The CSRC `ipd` rows of `27`/`29`/`32` already answer, so the table leaves out their RIS files. A new translation means a new table row.
68
69
  - **`#write_file` serializes before it indexes**, so a record that fails to serialize leaves no index row naming a file that was never written. That is how a `Bib::Date` in `Scraper.fetch_dates` hid: `Nist::ItemData` takes `Nist::Date`, and YAML serialization rejects the parent class (the same strictness as the docidentifier note below).
69
70
  - `ModsParser` — maps MODS XML (via `loc_mods`) to `ItemData`
70
71
  - `Scraper` — fetches items from GitHub YAML or CSRC JSON
@@ -39,6 +39,9 @@ module Relaton
39
39
  # read back as its printed form; the pubid itself is not changed.
40
40
  ref = ref.to_s unless ref.is_a?(String)
41
41
  return fetch_ref_err(ref, year, []) if ref.match?(/\sEP$/)
42
+ # Before the parsing below, which reads "(spa)" as a date.
43
+ cswp = Cswp.lookup(ref)
44
+ return cswp_get(ref, cswp, year, opts) if cswp
42
45
 
43
46
  /^(?<code2>[^(]+)(?:\((?<date2>\w+\s(?:\d{2},\s)?\d{4})\))?\s?\(?(?:(?<=\()(?<stage>(?:I|F|\d)PD))?/ =~ ref
44
47
  stage ||= /(?<=\.)PD-\w+(?=\.)/.match(ref)&.to_s
@@ -62,6 +65,34 @@ module Relaton
62
65
 
63
66
  private
64
67
 
68
+ #
69
+ # Get a CSWP that CSRC does not list (relaton#406). A legacy dated id
70
+ # answers the numbered CSRC record, with the dated id as an extra
71
+ # docidentifier; a translation answers its record from the RIS table.
72
+ #
73
+ # @param ref [String] reference
74
+ # @param cswp [Array] the Cswp.lookup result
75
+ # @param year [String, nil] year
76
+ # @param opts [Hash] options
77
+ #
78
+ # @return [Relaton::Nist::ItemData, nil] bibliographic item
79
+ #
80
+ def cswp_get(ref, cswp, year, opts)
81
+ type, id, dated = cswp
82
+ if type == :translation
83
+ return fetch_ref_err(ref, year, [id[:year]]) if year && year.to_s != id[:year]
84
+
85
+ item = Cswp.translation_item(id)
86
+ Util.info "Found: `#{item.docidentifier.first.content}`", key: ref
87
+ return item
88
+ end
89
+
90
+ Util.info "Legacy dated CSWP id, fetching `#{id}`", key: ref
91
+ item = nistbib_get(id, year, opts)
92
+ item&.docidentifier&.<< Docidentifier.new(content: dated, type: "NIST")
93
+ item
94
+ end
95
+
65
96
  #
66
97
  # Get NIST document by reference
67
98
  #
@@ -0,0 +1,137 @@
1
+ require "yeptris/psych"
2
+
3
+ module Relaton
4
+ module Nist
5
+ #
6
+ # CSWP ids the CSRC pubs-export feed does not list (relaton#406): the
7
+ # legacy dated ids from before the series was numbered, and the
8
+ # translations. The table (`cswp.yaml`) is built from the NIST Library RIS
9
+ # files; see the header of that file.
10
+ #
11
+ # This file loads no flavor model at require time, so the processor can
12
+ # ask #lookup on the cold path. #translation_item needs the models.
13
+ #
14
+ module Cswp
15
+ # ISO 639-2 code of a translation => [ISO 639-1 code, ISO 15924 script]
16
+ LANGUAGES = {
17
+ "ara" => %w[ar Arab], "bul" => %w[bg Cyrl], "fre" => %w[fr Latn],
18
+ "gre" => %w[el Grek], "ind" => %w[id Latn], "jpn" => %w[ja Jpan],
19
+ "kor" => %w[ko Kore], "may" => %w[ms Latn], "nor" => %w[no Latn],
20
+ "pol" => %w[pl Latn], "por" => %w[pt Latn], "spa" => %w[es Latn],
21
+ "ukr" => %w[uk Cyrl]
22
+ }.freeze
23
+
24
+ class << self
25
+ # @return [Hash] the table as written in cswp.yaml
26
+ def table
27
+ @table ||= Yeptris::Psych.load_file File.expand_path("cswp.yaml", __dir__)
28
+ end
29
+
30
+ #
31
+ # The comparable form of a CSWP reference: no publisher, the
32
+ # separators (".", "(...)") read as spaces, upper case.
33
+ # "NIST CSWP 6(spa)", "NIST.CSWP.6.spa" and "NIST CSWP 6 spa" all
34
+ # give "CSWP 6 SPA".
35
+ #
36
+ # @param ref [String]
37
+ # @return [String]
38
+ def normalize(ref)
39
+ ref.to_s.sub(/\ANIST[.\s]+/i, "").gsub(/[().]/, " ").squeeze(" ").strip.upcase
40
+ end
41
+
42
+ #
43
+ # Find a reference in the table.
44
+ #
45
+ # @param ref [String]
46
+ # @return [Array, nil] `[:legacy, numbered id, dated id]` for a
47
+ # legacy id of an English CSWP, `[:translation, entry]` for a
48
+ # translation (dated or numbered), nil otherwise
49
+ def lookup(ref)
50
+ key = normalize ref
51
+ return unless key.start_with? "CSWP "
52
+
53
+ key = key.delete_prefix "CSWP "
54
+ dated, target = legacy[key]
55
+ entry = translations[(target || key).upcase]
56
+ return [:translation, entry] if entry
57
+
58
+ [:legacy, "NIST CSWP #{target}", "NIST CSWP #{dated}"] if target
59
+ end
60
+
61
+ #
62
+ # Build the record of a translation from its RIS fields.
63
+ #
64
+ # @param entry [Hash] a translation entry from #lookup
65
+ # @return [Relaton::Nist::ItemData]
66
+ def translation_item(entry) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
67
+ number, lang_code = entry[:id].split
68
+ language, script = LANGUAGES.fetch lang_code
69
+ id = "NIST CSWP #{number}(#{lang_code})"
70
+ ItemData.new(
71
+ type: "standard",
72
+ docidentifier: [
73
+ Docidentifier.new(content: id, type: "NIST", primary: true),
74
+ Docidentifier.new(content: entry[:doi].split("/").last, type: "DOI"),
75
+ ],
76
+ title: [Bib::Title.new(content: entry[:title], language: "en", script: "Latn")],
77
+ source: [Bib::Uri.new(type: "doi", content: "https://doi.org/#{entry[:doi]}")],
78
+ date: [Date.new(type: "published", at: entry[:year])],
79
+ contributor: entry[:authors].map { |name| author name } << publisher,
80
+ language: [language],
81
+ script: [script],
82
+ relation: [original_relation("NIST CSWP #{number}")],
83
+ place: [Bib::Place.new(city: "Gaithersburg", region: [Bib::Place::RegionType.new(iso: "MD")])],
84
+ ext: Ext.new(doctype: Doctype.new(content: "standard"), flavor: "nist"),
85
+ )
86
+ end
87
+
88
+ private
89
+
90
+ # dated id (upper case) => [dated id, numbered id]
91
+ def legacy
92
+ @legacy ||= table["legacy"].to_h { |dated, numbered| [dated.upcase, [dated, numbered]] }
93
+ end
94
+
95
+ # translation id (upper case) => entry
96
+ def translations
97
+ @translations ||= table["translations"].to_h do |id, fields|
98
+ [id.upcase, fields.transform_keys(&:to_sym).merge(id: id, authors: fields["authors"] || [])]
99
+ end
100
+ end
101
+
102
+ # @param name [String] RIS author, "Surname, Forename"
103
+ # @return [Bib::Contributor]
104
+ def author(name)
105
+ surname, forename = name.split(/,\s*/, 2)
106
+ fullname = Bib::FullName.new(
107
+ surname: Bib::LocalizedString.new(content: surname, language: "en", script: "Latn"),
108
+ forename: [Bib::FullNameType::Forename.new(content: forename, language: "en", script: "Latn")],
109
+ )
110
+ Bib::Contributor.new(
111
+ person: Bib::Person.new(name: fullname),
112
+ role: [Bib::Contributor::Role.new(type: "author")],
113
+ )
114
+ end
115
+
116
+ # @return [Bib::Contributor]
117
+ def publisher
118
+ org = Bib::Organization.new(
119
+ name: [Bib::TypedLocalizedString.new(content: "National Institute of Standards and Technology")],
120
+ abbreviation: Bib::LocalizedString.new(content: "NIST"),
121
+ )
122
+ Bib::Contributor.new(organization: org, role: [Bib::Contributor::Role.new(type: "publisher")])
123
+ end
124
+
125
+ # @param id [String] the original's id
126
+ # @return [Relaton::Nist::Relation]
127
+ def original_relation(id)
128
+ bib = ItemData.new(
129
+ formattedref: Bib::Formattedref.new(content: id),
130
+ docidentifier: [Docidentifier.new(content: id, type: "NIST", primary: true)],
131
+ )
132
+ Relation.new(type: "translatedFrom", bibitem: bib)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,155 @@
1
+ # CSWP ids the CSRC pubs-export feed does not list (relaton#406).
2
+ #
3
+ # Source: the RIS files `bib/NIST.CSWP.*.ris` on the `nist-pages` branch of
4
+ # usnistgov/NIST-Tech-Pubs (the NIST Library MODS/MARC releases hold no CSWP).
5
+ #
6
+ # legacy: a dated id from before the series was numbered -> the numbered id,
7
+ # matched by the exact RIS title (TI). A dated translation maps to the
8
+ # numbered translation. `06222021-draft` has no numbered RIS; its title is
9
+ # the CSRC `CSWP 19` initial public draft.
10
+ # Not mapped: `02042022-1`..`-5` have no SN and share the title
11
+ # "Cybersecurity White Paper: EO Response", so no numbered id matches.
12
+ #
13
+ # translations: numbered id + ISO 639-2 language code -> the RIS fields (TI,
14
+ # PY, DO, AU). The original is the numbered id without the language code.
15
+ legacy:
16
+ "01142020": "9"
17
+ "01162020es": "10 spa"
18
+ "01162020id": "10 ind"
19
+ "01162020pt": "10 por"
20
+ "01262018": "5"
21
+ "02042020-1": "11"
22
+ "02042020-2": "11A"
23
+ "02042020-3": "11B"
24
+ "02042020-4": "11C"
25
+ "02042020-5": "11D"
26
+ "02042020-6": "11E"
27
+ "02042020-7": "11F"
28
+ "02122014": "1"
29
+ "02122014pl": "1 pol"
30
+ "02192014": "2"
31
+ "04162018": "6"
32
+ "04162018ar": "6 ara"
33
+ "04162018bg": "6 bul"
34
+ "04162018es": "6 spa"
35
+ "04162018fr": "6 fre"
36
+ "04162018id": "6 ind"
37
+ "04162018pt": "6 por"
38
+ "04212016": "4"
39
+ "04222019": "8"
40
+ "04232020": "13"
41
+ "04282021": "15"
42
+ "06032014": "3"
43
+ "06222021-draft": "19 ipd"
44
+ "09102018": "7"
45
+ "09292021": "21"
46
+ translations:
47
+ "1 pol":
48
+ title: Framework for Improving Critical Infrastructure Cybersecurity, Version 1.0 (Polish translation)
49
+ year: "2014"
50
+ doi: 10.6028/NIST.CSWP.1.pol
51
+ authors:
52
+ - Stine, Kevin
53
+ "6 ara":
54
+ title: Framework for Improving Critical Infrastructure Cybersecurity, Version 1.1 (Arabic translation)
55
+ year: "2018"
56
+ doi: 10.6028/NIST.CSWP.6.ara
57
+ authors:
58
+ - Stine, Kevin
59
+ "6 bul":
60
+ title: Framework for Improving Critical Infrastructure Cybersecurity, Version 1.1 (Bulgarian translation)
61
+ year: "2018"
62
+ doi: 10.6028/NIST.CSWP.6.bul
63
+ authors:
64
+ - Stine, Kevin
65
+ "6 fre":
66
+ title: Framework for Improving Critical Infrastructure Cybersecurity, Version 1.1 (French Translation)
67
+ year: "2018"
68
+ doi: 10.6028/NIST.CSWP.6.fre
69
+ authors:
70
+ - Stine, Kevin
71
+ "6 gre":
72
+ title: Greek Translation of the Framework for Improving Critical Infrastructure Cybersecurity
73
+ year: "2018"
74
+ doi: 10.6028/NIST.CSWP.6.gre
75
+ authors:
76
+ - Stine, Kevin
77
+ "6 ind":
78
+ title: Framework for Improving Critical Infrastructure Cybersecurity, Version 1.1 (Indonesian Translation)
79
+ year: "2018"
80
+ doi: 10.6028/NIST.CSWP.6.ind
81
+ authors:
82
+ - Stine, Kevin
83
+ "6 jpn":
84
+ title: Japanese Translation of Framework for Improving Critical Infrastructure Cybersecurity Version 1.1 (Cybersecurity Framework)
85
+ year: "2018"
86
+ doi: 10.6028/NIST.CSWP.6.jpn
87
+ authors:
88
+ - Stine, Kevin
89
+ "6 kor":
90
+ title: Korean Translation of the Framework for Improving Critical Infrastructure Cybersecurity Version 1.1
91
+ year: "2024"
92
+ doi: 10.6028/NIST.CSWP.6.kor
93
+ authors:
94
+ - Stine, Kevin
95
+ "6 may":
96
+ title: Malay Translation of the Framework for Improving Critical Infrastructure Cybersecurity V1.1
97
+ year: "2018"
98
+ doi: 10.6028/NIST.CSWP.6.may
99
+ authors:
100
+ - Stine, Kevin
101
+ "6 nor":
102
+ title: Norwegian Translation of the Framework for Improving Critical Infrastructure Cybersecurity Version 1.1
103
+ year: "2018"
104
+ doi: 10.6028/NIST.CSWP.6.nor
105
+ authors:
106
+ - Stine, Kevin
107
+ "6 por":
108
+ title: Portuguese Translation of the Framework for Improving Critical Infrastructure Cybersecurity Version 1.1 (Cybersecurity Framework)
109
+ year: "2018"
110
+ doi: 10.6028/NIST.CSWP.6.por
111
+ authors:
112
+ - Stine, Kevin
113
+ "6 spa":
114
+ title: Framework for Improving Critical Infrastructure Cybersecurity, Version 1.1 (Spanish translation)
115
+ year: "2018"
116
+ doi: 10.6028/NIST.CSWP.6.spa
117
+ authors:
118
+ - Stine, Kevin
119
+ "6 ukr":
120
+ title: Ukranian Translation of the Framework for Improving Critical Infrastructure Cybersecurity Version 1.1 (Cybersecurity Framework)
121
+ year: "2018"
122
+ doi: 10.6028/NIST.CSWP.6.ukr
123
+ authors:
124
+ - Stine, Kevin
125
+ "10 ara":
126
+ title: "NIST Privacy Framework: A Tool for Improving Privacy Through Enterprise Risk Management, Version 1.0 (Arabic translation)"
127
+ year: "2022"
128
+ doi: 10.6028/NIST.CSWP.10.ara
129
+ authors:
130
+ - Lefkovitz, Naomi
131
+ - Boeckl, Katie
132
+ "10 ind":
133
+ title: Indonesian Translation of the NIST Privacy Framework Version 1.0
134
+ year: "2020"
135
+ doi: 10.6028/NIST.CSWP.10.ind
136
+ authors:
137
+ - Lefkovitz, Naomi
138
+ "10 may":
139
+ title: "NIST Privacy Framework: A Tool for Improving Privacy Through Enterprise Risk Management, Version 1.0 (Malay translation)"
140
+ year: "2024"
141
+ doi: 10.6028/NIST.CSWP.10.may
142
+ authors:
143
+ - Lefkovitz, Naomi
144
+ "10 por":
145
+ title: Portuguese Translation of the NIST Privacy Framework Version 1.0
146
+ year: "2021"
147
+ doi: 10.6028/NIST.CSWP.10.por
148
+ authors:
149
+ - Lefkovitz, Naomi
150
+ "10 spa":
151
+ title: Spanish Translation of the NIST Privacy Framework Version 1.0
152
+ year: "2021"
153
+ doi: 10.6028/NIST.CSWP.10.spa
154
+ authors:
155
+ - Lefkovitz, Naomi
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
3
+ require "yeptris/psych"
4
+
4
5
  require "mechanize"
5
6
  require "loc_mods"
6
7
  require_relative "../nist"
@@ -183,7 +184,7 @@ module Relaton
183
184
  end
184
185
 
185
186
  def series
186
- @series ||= YAML.load_file File.expand_path("series.yaml", __dir__)
187
+ @series ||= Yeptris::Psych.load_file File.expand_path("series.yaml", __dir__)
187
188
  end
188
189
  end
189
190
  end
@@ -21,6 +21,26 @@ module Relaton
21
21
  Bibliography.get(code, date, opts)
22
22
  end
23
23
 
24
+ # Two kinds of Cswp table reference reach `get` as written and are not
25
+ # cached (relaton#406). A legacy dated id answers the numbered record
26
+ # with the dated id added, and Db would store that record under the
27
+ # numbered id's row, so a later "NIST CSWP 9" would carry the dated id.
28
+ # A translation that pubid reads in another form ("NIST CSWP
29
+ # 04162018ar" as "...04162018a", "NIST CSWP 6 spa" as "...6(spa)").
30
+ #
31
+ # @param ref [String]
32
+ # @return [Pubid::Nist::Identifier, nil]
33
+ def cache_pubid(ref)
34
+ pubid = super
35
+ return if pubid.nil?
36
+
37
+ require_relative "cswp"
38
+ type, = Cswp.lookup(ref)
39
+ return pubid if type.nil? || (type == :translation && pubid.to_s == ref)
40
+
41
+ nil
42
+ end
43
+
24
44
  #
25
45
  # Fetch all the documents from a source
26
46
  #
data/lib/relaton/nist.rb CHANGED
@@ -12,6 +12,7 @@ require_relative "nist/pubs_export"
12
12
  require_relative "nist/hit"
13
13
  require_relative "nist/hit_collection"
14
14
  require_relative "nist/scraper"
15
+ require_relative "nist/cswp"
15
16
  require_relative "nist/bibliography"
16
17
  # require_relative "nist/tech_pubs_parser"
17
18
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yeptris/psych"
4
+
3
5
  module Relaton
4
6
  module Sdo
5
7
  # Singleton registry of organizations, built lazily from the index manifest
@@ -35,7 +37,7 @@ module Relaton
35
37
 
36
38
  def build_index
37
39
  yaml = Fetcher.fetch_index
38
- data = yaml.nil? || yaml.empty? ? {} : YAML.safe_load(yaml)
40
+ data = yaml.nil? || yaml.empty? ? {} : Yeptris::Psych.safe_load(yaml)
39
41
  # A corrupt/unexpected index (non-mapping YAML) degrades to "no orgs"
40
42
  # rather than crashing every lookup with a TypeError.
41
43
  organizations = data.is_a?(Hash) ? (data["organizations"] || {}) : {}
@@ -1,3 +1,3 @@
1
1
  module Relaton
2
- VERSION = "3.0.2".freeze
2
+ VERSION = "3.0.4".freeze
3
3
  end
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  module Relaton
2
3
  module Xsf
3
4
  class Hit < Relaton::Core::Hit
@@ -6,7 +7,7 @@ module Relaton
6
7
 
7
8
  agent = Mechanize.new
8
9
  resp = agent.get hit[:url]
9
- hash = YAML.safe_load resp.body
10
+ hash = Yeptris::Psych.safe_load resp.body
10
11
  hash["fetched"] = Date.today.to_s
11
12
  @doc = Relaton::Xsf::Item.from_yaml hash.to_yaml
12
13
  rescue StandardError => e
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relaton
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.2
4
+ version: 3.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-30 00:00:00.000000000 Z
11
+ date: 2026-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -42,30 +42,30 @@ dependencies:
42
42
  name: base64
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '0.3'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: '0.3'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: bibtex-ruby
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ">="
59
+ - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: '6.2'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ">="
66
+ - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: '6.2'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: cnccs
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -98,16 +98,16 @@ dependencies:
98
98
  name: sqlite3
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ">="
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '1.7'
103
+ version: '2.10'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ">="
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '1.7'
110
+ version: '2.10'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: csv
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -224,16 +224,16 @@ dependencies:
224
224
  name: iso639
225
225
  requirement: !ruby/object:Gem::Requirement
226
226
  requirements:
227
- - - ">="
227
+ - - "~>"
228
228
  - !ruby/object:Gem::Version
229
- version: '0'
229
+ version: '1.4'
230
230
  type: :runtime
231
231
  prerelease: false
232
232
  version_requirements: !ruby/object:Gem::Requirement
233
233
  requirements:
234
- - - ">="
234
+ - - "~>"
235
235
  - !ruby/object:Gem::Version
236
- version: '0'
236
+ version: '1.4'
237
237
  - !ruby/object:Gem::Dependency
238
238
  name: isoics
239
239
  requirement: !ruby/object:Gem::Requirement
@@ -282,20 +282,14 @@ dependencies:
282
282
  requirements:
283
283
  - - "~>"
284
284
  - !ruby/object:Gem::Version
285
- version: '0.2'
286
- - - ">="
287
- - !ruby/object:Gem::Version
288
- version: 0.2.5
285
+ version: 0.2.8
289
286
  type: :runtime
290
287
  prerelease: false
291
288
  version_requirements: !ruby/object:Gem::Requirement
292
289
  requirements:
293
290
  - - "~>"
294
291
  - !ruby/object:Gem::Version
295
- version: '0.2'
296
- - - ">="
297
- - !ruby/object:Gem::Version
298
- version: 0.2.5
292
+ version: 0.2.8
299
293
  - !ruby/object:Gem::Dependency
300
294
  name: lutaml-model
301
295
  requirement: !ruby/object:Gem::Requirement
@@ -370,30 +364,30 @@ dependencies:
370
364
  name: moxml
371
365
  requirement: !ruby/object:Gem::Requirement
372
366
  requirements:
373
- - - ">="
367
+ - - "~>"
374
368
  - !ruby/object:Gem::Version
375
- version: '0.5'
369
+ version: 0.6.0
376
370
  type: :runtime
377
371
  prerelease: false
378
372
  version_requirements: !ruby/object:Gem::Requirement
379
373
  requirements:
380
- - - ">="
374
+ - - "~>"
381
375
  - !ruby/object:Gem::Version
382
- version: '0.5'
376
+ version: 0.6.0
383
377
  - !ruby/object:Gem::Dependency
384
378
  name: leptris
385
379
  requirement: !ruby/object:Gem::Requirement
386
380
  requirements:
387
- - - ">="
381
+ - - "~>"
388
382
  - !ruby/object:Gem::Version
389
- version: 1.9.80
383
+ version: 1.9.391
390
384
  type: :runtime
391
385
  prerelease: false
392
386
  version_requirements: !ruby/object:Gem::Requirement
393
387
  requirements:
394
- - - ">="
388
+ - - "~>"
395
389
  - !ruby/object:Gem::Version
396
- version: 1.9.80
390
+ version: 1.9.391
397
391
  - !ruby/object:Gem::Dependency
398
392
  name: parallel
399
393
  requirement: !ruby/object:Gem::Requirement
@@ -410,92 +404,60 @@ dependencies:
410
404
  version: '1.26'
411
405
  - !ruby/object:Gem::Dependency
412
406
  name: parsanol
413
- requirement: !ruby/object:Gem::Requirement
414
- requirements:
415
- - - ">="
416
- - !ruby/object:Gem::Version
417
- version: 1.3.114
418
- - - "<"
419
- - !ruby/object:Gem::Version
420
- version: '1.4'
421
- type: :runtime
422
- prerelease: false
423
- version_requirements: !ruby/object:Gem::Requirement
424
- requirements:
425
- - - ">="
426
- - !ruby/object:Gem::Version
427
- version: 1.3.114
428
- - - "<"
429
- - !ruby/object:Gem::Version
430
- version: '1.4'
431
- - !ruby/object:Gem::Dependency
432
- name: parslet
433
407
  requirement: !ruby/object:Gem::Requirement
434
408
  requirements:
435
409
  - - "~>"
436
410
  - !ruby/object:Gem::Version
437
- version: 2.0.0
411
+ version: 1.3.114
438
412
  type: :runtime
439
413
  prerelease: false
440
414
  version_requirements: !ruby/object:Gem::Requirement
441
415
  requirements:
442
416
  - - "~>"
443
417
  - !ruby/object:Gem::Version
444
- version: 2.0.0
418
+ version: 1.3.114
445
419
  - !ruby/object:Gem::Dependency
446
420
  name: pubid
447
421
  requirement: !ruby/object:Gem::Requirement
448
422
  requirements:
449
- - - ">="
423
+ - - "~>"
450
424
  - !ruby/object:Gem::Version
451
425
  version: 2.0.1
452
- - - "<"
453
- - !ruby/object:Gem::Version
454
- version: '2.1'
455
426
  type: :runtime
456
427
  prerelease: false
457
428
  version_requirements: !ruby/object:Gem::Requirement
458
429
  requirements:
459
- - - ">="
430
+ - - "~>"
460
431
  - !ruby/object:Gem::Version
461
432
  version: 2.0.1
462
- - - "<"
463
- - !ruby/object:Gem::Version
464
- version: '2.1'
465
433
  - !ruby/object:Gem::Dependency
466
434
  name: yeptris
467
435
  requirement: !ruby/object:Gem::Requirement
468
436
  requirements:
469
- - - ">="
437
+ - - "~>"
470
438
  - !ruby/object:Gem::Version
471
439
  version: 0.6.51
472
440
  type: :runtime
473
441
  prerelease: false
474
442
  version_requirements: !ruby/object:Gem::Requirement
475
443
  requirements:
476
- - - ">="
444
+ - - "~>"
477
445
  - !ruby/object:Gem::Version
478
446
  version: 0.6.51
479
447
  - !ruby/object:Gem::Dependency
480
448
  name: relaton-render
481
449
  requirement: !ruby/object:Gem::Requirement
482
450
  requirements:
483
- - - ">="
484
- - !ruby/object:Gem::Version
485
- version: 3.0.0.pre.alpha.1
486
- - - "<"
451
+ - - "~>"
487
452
  - !ruby/object:Gem::Version
488
- version: '4'
453
+ version: '3.0'
489
454
  type: :runtime
490
455
  prerelease: false
491
456
  version_requirements: !ruby/object:Gem::Requirement
492
457
  requirements:
493
- - - ">="
494
- - !ruby/object:Gem::Version
495
- version: 3.0.0.pre.alpha.1
496
- - - "<"
458
+ - - "~>"
497
459
  - !ruby/object:Gem::Version
498
- version: '4'
460
+ version: '3.0'
499
461
  - !ruby/object:Gem::Dependency
500
462
  name: rfcxml
501
463
  requirement: !ruby/object:Gem::Requirement
@@ -1159,6 +1121,8 @@ files:
1159
1121
  - lib/relaton/nist/bibitem.rb
1160
1122
  - lib/relaton/nist/bibliography.rb
1161
1123
  - lib/relaton/nist/comment_period.rb
1124
+ - lib/relaton/nist/cswp.rb
1125
+ - lib/relaton/nist/cswp.yaml
1162
1126
  - lib/relaton/nist/data_fetcher.rb
1163
1127
  - lib/relaton/nist/date.rb
1164
1128
  - lib/relaton/nist/docidentifier.rb