relaton 3.0.1 → 3.0.2
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/lib/relaton/bib/converter/csl.rb +3 -2
- data/lib/relaton/bib/model/docidentifier.rb +61 -0
- data/lib/relaton/bib.rb +2 -1
- data/lib/relaton/bipm/CLAUDE.md +14 -0
- data/lib/relaton/bsi/model/docidentifier.rb +0 -4
- data/lib/relaton/calconnect/data_fetcher.rb +2 -2
- data/lib/relaton/ccsds/data/fetcher.rb +2 -1
- data/lib/relaton/ccsds/model/docidentifier.rb +0 -4
- data/lib/relaton/core/data_fetcher.rb +2 -1
- data/lib/relaton/core.rb +7 -0
- data/lib/relaton/db/cache.rb +3 -3
- data/lib/relaton/db.rb +20 -1
- data/lib/relaton/doi/crossref.rb +2 -1
- data/lib/relaton/doi/parser.rb +3 -2
- data/lib/relaton/etsi/data_fetcher.rb +3 -3
- data/lib/relaton/gb/sec_scraper.rb +3 -3
- data/lib/relaton/iec/data_fetcher.rb +3 -2
- data/lib/relaton/iec/model/docidentifier.rb +0 -4
- data/lib/relaton/ietf/CLAUDE.md +27 -2
- data/lib/relaton/ietf/bibxml_parser.rb +38 -0
- data/lib/relaton/ietf/data_fetcher.rb +3 -30
- data/lib/relaton/ietf/scraper.rb +60 -3
- data/lib/relaton/ietf/wg_name_resolver.rb +2 -2
- data/lib/relaton/iho/CLAUDE.md +4 -0
- data/lib/relaton/iho/bibliography.rb +7 -1
- data/lib/relaton/index/dataset_lock.rb +4 -3
- data/lib/relaton/index/shard_source.rb +3 -2
- data/lib/relaton/index/sqlite_backend.rb +5 -3
- data/lib/relaton/isbn/open_library.rb +2 -1
- data/lib/relaton/iso/data_fetcher.rb +6 -6
- data/lib/relaton/iso/model/docidentifier.rb +0 -4
- data/lib/relaton/itu/data_fetcher.rb +2 -2
- data/lib/relaton/itu/hit_collection.rb +3 -3
- data/lib/relaton/itu/recommendation_fields.rb +2 -2
- data/lib/relaton/jis/data_fetcher.rb +2 -1
- data/lib/relaton/json.rb +21 -0
- data/lib/relaton/logger/channels/gh_issue.rb +3 -3
- data/lib/relaton/logger/formatter_json.rb +2 -1
- data/lib/relaton/nist/CLAUDE.md +6 -0
- data/lib/relaton/nist/docidentifier.rb +0 -4
- data/lib/relaton/nist/pubs_export.rb +2 -1
- data/lib/relaton/ogc/data_fetcher.rb +2 -2
- data/lib/relaton/omg/scraper.rb +3 -3
- data/lib/relaton/plateau/data_fetcher.rb +2 -1
- data/lib/relaton/un/hit_collection.rb +4 -3
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton.rb +1 -0
- metadata +19 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f15745a534625b27b3d013d7b3d6edd7e0185f1bcc1c545f7b44dc6059d9d113
|
|
4
|
+
data.tar.gz: ac15b5259abb3c980cd6bee9d10c5f2f0ae12fea955478ca28f7da82027ab6f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de3f2c7182f6a2b1737a54e9168bcab856dbc81fe169f293b5b932f0224e8cb95187f506829230068ebc5308884e89a918515b03838f969c9c9870020442efff
|
|
7
|
+
data.tar.gz: a4bfe1d14667d8907eecd43fe880b02e7d15ceab20d2c6302c8f5c220d6c4398b3adefa056ff8815639686ad85674713c5c3a4759bd5ad57ffdd1e42b9bc39e1
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "yeptris"
|
|
4
|
+
|
|
4
5
|
|
|
5
6
|
module Relaton
|
|
6
7
|
module Bib
|
|
@@ -43,7 +44,7 @@ module Relaton
|
|
|
43
44
|
keywords = Array(@item.keyword).map { |k| k.content.to_s }.reject(&:empty?)
|
|
44
45
|
out[:keyword] = keywords unless keywords.empty?
|
|
45
46
|
out[:URL] = source_uri if source_uri
|
|
46
|
-
[out]
|
|
47
|
+
Yeptris::JSON.dump([out]) + "\n"
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
private
|
|
@@ -44,6 +44,67 @@ module Relaton
|
|
|
44
44
|
def to_all_parts!; end
|
|
45
45
|
|
|
46
46
|
def remove_date!; end
|
|
47
|
+
|
|
48
|
+
# Docidentifier types with an SDO-specific :external and :internal form
|
|
49
|
+
# (relaton#49), mapped to the pubid flavor that renders both. The
|
|
50
|
+
# :external form carries the publisher ("IETF RFC 8341",
|
|
51
|
+
# "BSI PAS 2031:2019"); the :internal form is the one the SDO uses for its
|
|
52
|
+
# own documents, without it ("SP 800-113" for "NIST SP 800-113"). pubid
|
|
53
|
+
# decides per identifier, so "BS EN ISO 8848:2021" and "CGPM RES (1889)"
|
|
54
|
+
# are the same in both forms. Every other type answers its content for
|
|
55
|
+
# both forms: pubid would drop "ISO" from an ISO identifier.
|
|
56
|
+
#
|
|
57
|
+
# The table keys on `type`, not on a flavor subclass, so it also covers a
|
|
58
|
+
# relation's nested `Bib::ItemData` and the `Bib::Docidentifier` objects
|
|
59
|
+
# BIPM and IETF build (a subclass-typed collection would make lutaml's
|
|
60
|
+
# YAML serializer reject those).
|
|
61
|
+
FORM_PUBID = {
|
|
62
|
+
"NIST" => :Nist,
|
|
63
|
+
"BIPM" => :Bipm,
|
|
64
|
+
"IETF" => :Ietf,
|
|
65
|
+
"Internet-Draft" => :Ietf,
|
|
66
|
+
"BSI" => :Bsi,
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
FORMS = %i[external internal].freeze
|
|
70
|
+
|
|
71
|
+
# The parsed identifier. A pubid-backed flavor subclass overrides it.
|
|
72
|
+
def pubid; end
|
|
73
|
+
|
|
74
|
+
# @param form [Symbol, nil] nil for the stored content, :external for the
|
|
75
|
+
# identifier with its publisher, :internal for the form the SDO uses
|
|
76
|
+
# for its own documents
|
|
77
|
+
# @raise [ArgumentError] for an unknown form
|
|
78
|
+
def to_s(form: nil)
|
|
79
|
+
return content.to_s unless form
|
|
80
|
+
|
|
81
|
+
form = form.to_sym
|
|
82
|
+
unless FORMS.include?(form)
|
|
83
|
+
raise ArgumentError, "unknown identifier form: #{form.inspect}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
form_content(form).to_s
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# pubid renders the form (`to_s(with_publisher:)`, pubid#620). A
|
|
92
|
+
# docidentifier with no parsed pubid (a plain `Bib::Docidentifier`) is
|
|
93
|
+
# parsed here; content that pubid cannot parse is returned unchanged.
|
|
94
|
+
def form_content(form)
|
|
95
|
+
flavor = FORM_PUBID[type]
|
|
96
|
+
id = flavor && (pubid || parse_form_pubid(flavor))
|
|
97
|
+
id ? id.to_s(with_publisher: form == :external) : content
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_form_pubid(flavor)
|
|
101
|
+
return if content.to_s.empty?
|
|
102
|
+
|
|
103
|
+
require "pubid"
|
|
104
|
+
::Pubid.const_get(flavor).parse(content)
|
|
105
|
+
rescue StandardError
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
47
108
|
end
|
|
48
109
|
end
|
|
49
110
|
end
|
data/lib/relaton/bib.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "bibtex"
|
|
|
5
5
|
require "iso639"
|
|
6
6
|
require "rfcxml"
|
|
7
7
|
require "relaton/core"
|
|
8
|
+
require "yeptris"
|
|
8
9
|
require_relative "version"
|
|
9
10
|
require_relative "bib/util"
|
|
10
11
|
require_relative "bib/sanitizer"
|
|
@@ -41,7 +42,7 @@ module Relaton
|
|
|
41
42
|
# @return [Hash{String=>String}] schema versions
|
|
42
43
|
#
|
|
43
44
|
def schema_versions
|
|
44
|
-
@@schema_versions ||= JSON.
|
|
45
|
+
@@schema_versions ||= Yeptris::JSON.load File.read(File.join(__dir__, "bib/versions.json"))
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
|
data/lib/relaton/bipm/CLAUDE.md
CHANGED
|
@@ -144,6 +144,20 @@ demonstrated rather than argued.
|
|
|
144
144
|
builds `index-v1` with them, and `id_parser_spec.rb` still covers them. Only
|
|
145
145
|
`bibliography.rb` stopped requiring the file.
|
|
146
146
|
|
|
147
|
+
### Internal identifier
|
|
148
|
+
|
|
149
|
+
`docid.to_s(form: :internal)` drops the
|
|
150
|
+
`BIPM` publisher (relaton#49): `BIPM SI Brochure Part 1` →
|
|
151
|
+
`SI Brochure Part 1`. Committee ids (`CGPM RES (1889)`, `CCTF REC 2 (2009)`)
|
|
152
|
+
have no publisher token and are the same in both forms. The rule is the `"BIPM"` row of
|
|
153
|
+
`Bib::Docidentifier::FORM_PUBID`: BIPM docidentifiers carry no pubid,
|
|
154
|
+
so the content is parsed with `Pubid::Bipm` at call time and rendered with
|
|
155
|
+
`to_s(with_publisher: false)`; unparseable content is returned unchanged. It
|
|
156
|
+
also holds for a relation's nested item. Do not move it into a `Bipm::Docidentifier`
|
|
157
|
+
subclass: typing the item's `docidentifier` collection with a subclass makes
|
|
158
|
+
YAML serialization reject the `Bib::Docidentifier` objects the parsers build
|
|
159
|
+
(`IncorrectModelError`).
|
|
160
|
+
|
|
147
161
|
### Data Sources
|
|
148
162
|
|
|
149
163
|
The gem fetches from three external datasets:
|
|
@@ -44,10 +44,6 @@ module Relaton
|
|
|
44
44
|
c if c.is_a?(::Pubid::Bsi::Identifier) || c.is_a?(::Pubid::AllParts)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def to_s
|
|
48
|
-
content.to_s
|
|
49
|
-
end
|
|
50
|
-
|
|
51
47
|
# Strip the publication date to build a most-recent (undated) reference.
|
|
52
48
|
# pubid's `exclude` propagates into nested identifiers (so a consolidated
|
|
53
49
|
# base date is dropped while the amendment is kept); `:month` is excluded
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
# frozen_string_literal:true
|
|
2
3
|
|
|
3
|
-
require "json"
|
|
4
4
|
require "mechanize"
|
|
5
5
|
require "relaton/core"
|
|
6
6
|
require "relaton/index"
|
|
@@ -54,7 +54,7 @@ module Relaton::Calconnect
|
|
|
54
54
|
# 304 Not Modified — nothing changed since the last fetch
|
|
55
55
|
return if resp.code == "304"
|
|
56
56
|
|
|
57
|
-
data =
|
|
57
|
+
data = Relaton::Json.parse_external resp.body
|
|
58
58
|
all_success = true
|
|
59
59
|
Array(data["documents"]).each { |doc| all_success &&= parse_page doc }
|
|
60
60
|
self.etag = resp.response["etag"] if all_success
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "json"
|
|
2
3
|
require "mechanize"
|
|
3
4
|
require "relaton/index"
|
|
@@ -43,7 +44,7 @@ module Relaton
|
|
|
43
44
|
#
|
|
44
45
|
def fetch_docs(url)
|
|
45
46
|
resp = agent.get(url)
|
|
46
|
-
json =
|
|
47
|
+
json = Relaton::Json.parse_external resp.body.match(/const config = (.*);/)[1]
|
|
47
48
|
@array = json["data"].map { |doc| parse_and_save doc, json["data"] }
|
|
48
49
|
end
|
|
49
50
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "fileutils"
|
|
2
3
|
|
|
3
4
|
module Relaton
|
|
@@ -361,7 +362,7 @@ module Relaton
|
|
|
361
362
|
def entry_digest(entry)
|
|
362
363
|
require "digest"
|
|
363
364
|
require "json"
|
|
364
|
-
Digest::SHA256.hexdigest JSON.
|
|
365
|
+
Digest::SHA256.hexdigest Yeptris::JSON.dump(entry)
|
|
365
366
|
end
|
|
366
367
|
|
|
367
368
|
# Write, clobbering whatever is there. The caller has already established
|
data/lib/relaton/core.rb
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
require "relaton/logger"
|
|
2
|
+
|
|
3
|
+
module Relaton
|
|
4
|
+
# Registered here as well as in the root entry: flavor files and their
|
|
5
|
+
# specs load directly via require "relaton/<flavor>", never through
|
|
6
|
+
# relaton.rb, and the boundary parsers need this constant either way.
|
|
7
|
+
autoload :Json, "relaton/json"
|
|
8
|
+
end
|
|
2
9
|
require_relative "version"
|
|
3
10
|
require_relative "core/request_error"
|
|
4
11
|
require_relative "core/array_wrapper"
|
data/lib/relaton/db/cache.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "fileutils"
|
|
2
3
|
require "digest"
|
|
3
|
-
require "json"
|
|
4
4
|
require "date"
|
|
5
5
|
require "lutaml/store"
|
|
6
6
|
require_relative "cache_entry"
|
|
@@ -520,7 +520,7 @@ module Relaton
|
|
|
520
520
|
def canonical(key)
|
|
521
521
|
return key if key.is_a? String
|
|
522
522
|
|
|
523
|
-
JSON.
|
|
523
|
+
Yeptris::JSON.load Yeptris::JSON.dump(key.to_hash)
|
|
524
524
|
end
|
|
525
525
|
|
|
526
526
|
def row_key(row)
|
|
@@ -556,7 +556,7 @@ module Relaton
|
|
|
556
556
|
|
|
557
557
|
# A digest of the canonical key, independent of the hash order.
|
|
558
558
|
def digest(key)
|
|
559
|
-
Digest::SHA256.hexdigest JSON.
|
|
559
|
+
Digest::SHA256.hexdigest Yeptris::JSON.dump(canonical_sorted(key))
|
|
560
560
|
end
|
|
561
561
|
|
|
562
562
|
def canonical_sorted(key)
|
data/lib/relaton/db.rb
CHANGED
|
@@ -127,7 +127,15 @@ module Relaton
|
|
|
127
127
|
rescue Relaton::RequestError => e
|
|
128
128
|
args[3].call e
|
|
129
129
|
rescue StandardError => e
|
|
130
|
-
|
|
130
|
+
# An unparseable reference ("ISO Standards Tag Set" in a
|
|
131
|
+
# vocabulary corpus) is a not-found input, not a failure: the
|
|
132
|
+
# citation keeps its literal text and the build continues
|
|
133
|
+
# (relaton#412). Real errors keep the ERROR level.
|
|
134
|
+
if unreadable_reference?(e)
|
|
135
|
+
Util.info "`#{args[0]}` -- #{e.message}"
|
|
136
|
+
else
|
|
137
|
+
Util.error "`#{args[0]}` -- #{e.message}"
|
|
138
|
+
end
|
|
131
139
|
args[3].call nil
|
|
132
140
|
end
|
|
133
141
|
@queues[stdclass] =
|
|
@@ -213,6 +221,14 @@ module Relaton
|
|
|
213
221
|
|
|
214
222
|
private
|
|
215
223
|
|
|
224
|
+
# True when the error is a flavor's grammar refusing to read the
|
|
225
|
+
# reference. Both are raised by the flavors' pubid/parslet parses; a
|
|
226
|
+
# reference they cannot read is not-found, not a failure (relaton#412).
|
|
227
|
+
def unreadable_reference?(error)
|
|
228
|
+
(defined?(Pubid::Errors::Error) && error.is_a?(Pubid::Errors::Error)) ||
|
|
229
|
+
(defined?(Parslet::ParseFailed) && error.is_a?(Parslet::ParseFailed))
|
|
230
|
+
end
|
|
231
|
+
|
|
216
232
|
# The flavor routing picks for `fetch_async`'s per-flavor queue; nil
|
|
217
233
|
# (logged) when no flavor recognizes the reference.
|
|
218
234
|
def async_route(ref)
|
|
@@ -309,6 +325,9 @@ module Relaton
|
|
|
309
325
|
)
|
|
310
326
|
end
|
|
311
327
|
end
|
|
328
|
+
# Nothing resolved: answer not-found, not a stub that holds only the
|
|
329
|
+
# reference (relaton-un#14, metanorma-ietf#205).
|
|
330
|
+
doc.relation.empty? ? nil : doc
|
|
312
331
|
end
|
|
313
332
|
|
|
314
333
|
# The legacy string cache key, for a processor with no pubid class. The
|
data/lib/relaton/doi/crossref.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "mechanize"
|
|
2
3
|
|
|
3
4
|
module Relaton
|
|
@@ -58,7 +59,7 @@ module Relaton
|
|
|
58
59
|
url = "https://api.crossref.org/works/#{CGI.escape(id)}"
|
|
59
60
|
loop do
|
|
60
61
|
resp = agent.get url
|
|
61
|
-
work =
|
|
62
|
+
work = Relaton::Json.parse_external resp.body
|
|
62
63
|
return work["message"] if work["status"] == "ok"
|
|
63
64
|
|
|
64
65
|
if n > 1
|
data/lib/relaton/doi/parser.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
module Relaton
|
|
2
3
|
module Doi
|
|
3
4
|
class Parser
|
|
@@ -856,7 +857,7 @@ module Relaton
|
|
|
856
857
|
throttled = 0
|
|
857
858
|
begin
|
|
858
859
|
resp = Crossref.agent.get url
|
|
859
|
-
|
|
860
|
+
Relaton::Json.parse_external(resp.body).dig("message", "items")
|
|
860
861
|
rescue Mechanize::ResponseCodeError => e
|
|
861
862
|
# 429 must be handled before the 4xx nil return: a throttle is not
|
|
862
863
|
# "no such record". Returning nil here made a rate-limited lookup look
|
|
@@ -881,7 +882,7 @@ module Relaton
|
|
|
881
882
|
retries += 1
|
|
882
883
|
retry if retries <= MAX_RETRIES
|
|
883
884
|
raise Relaton::RequestError, "Crossref network error after #{MAX_RETRIES} retries: #{e.message}"
|
|
884
|
-
rescue JSON::
|
|
885
|
+
rescue ::Yeptris::JSON::ParseError => e
|
|
885
886
|
raise Relaton::RequestError, "Crossref JSON parsing error: #{e.message}"
|
|
886
887
|
end
|
|
887
888
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "date"
|
|
2
|
-
require "json"
|
|
3
3
|
require "mechanize"
|
|
4
4
|
require "relaton/core"
|
|
5
5
|
require "relaton/index"
|
|
@@ -146,11 +146,11 @@ module Relaton
|
|
|
146
146
|
# @raise [BadPage] when the body is not a JSON array
|
|
147
147
|
#
|
|
148
148
|
def parse_page(page, body)
|
|
149
|
-
records =
|
|
149
|
+
records = Relaton::Json.parse_external(body)
|
|
150
150
|
return records if records.is_a?(Array)
|
|
151
151
|
|
|
152
152
|
raise BadPage, bad_page_message(page, body)
|
|
153
|
-
rescue JSON::
|
|
153
|
+
rescue ::Yeptris::JSON::ParseError
|
|
154
154
|
raise BadPage, bad_page_message(page, body)
|
|
155
155
|
end
|
|
156
156
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
require "net/http"
|
|
5
|
-
require "json"
|
|
6
6
|
require "moxml"
|
|
7
7
|
require_relative "scraper"
|
|
8
8
|
require_relative "item"
|
|
@@ -24,8 +24,8 @@ module Relaton
|
|
|
24
24
|
uri = URI "https://hbba.sacinfo.org.cn/stdQueryList"
|
|
25
25
|
resp = Net::HTTP.post uri, URI.encode_www_form({ key: text }),
|
|
26
26
|
"Content-Type" => "application/x-www-form-urlencoded"
|
|
27
|
-
# res =
|
|
28
|
-
json =
|
|
27
|
+
# res = Relaton::Json.parse_external Net::HTTP.get(uri)
|
|
28
|
+
json = Relaton::Json.parse_external resp.body
|
|
29
29
|
hits = json["records"].map do |h|
|
|
30
30
|
Hit.new pid: h["pk"], docref: h["code"], status: h["status"], scraper: self
|
|
31
31
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "relaton/core"
|
|
2
3
|
require_relative "../iec"
|
|
3
4
|
require_relative "data_parser"
|
|
@@ -135,7 +136,7 @@ module Relaton
|
|
|
135
136
|
Util.warn "#{res.body}"
|
|
136
137
|
break
|
|
137
138
|
end
|
|
138
|
-
json =
|
|
139
|
+
json = Relaton::Json.parse_external res.body
|
|
139
140
|
json[list_key].each { |pub| fetch_pub pub }
|
|
140
141
|
page += 1
|
|
141
142
|
next_page = res["link"]&.include? "rel=\"last\""
|
|
@@ -191,7 +192,7 @@ module Relaton
|
|
|
191
192
|
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
192
193
|
http.request req
|
|
193
194
|
end
|
|
194
|
-
|
|
195
|
+
Relaton::Json.parse_external(res.body)["access_token"]
|
|
195
196
|
end
|
|
196
197
|
end
|
|
197
198
|
|
|
@@ -46,10 +46,6 @@ module Relaton
|
|
|
46
46
|
original_content
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def to_s
|
|
50
|
-
content.to_s
|
|
51
|
-
end
|
|
52
|
-
|
|
53
49
|
# `remove_part!`/`remove_date!` are unnecessary here: `to_all_parts`'s
|
|
54
50
|
# own identity computation already excludes part/date (down the whole
|
|
55
51
|
# base chain, so a supplement's own year is dropped too — a disclosed
|
data/lib/relaton/ietf/CLAUDE.md
CHANGED
|
@@ -36,7 +36,7 @@ Converter classes use `Bib::NamespaceHelper` which resolves `namespace` by takin
|
|
|
36
36
|
|
|
37
37
|
### Key Flows
|
|
38
38
|
|
|
39
|
-
1. **Single document lookup**: `Processor#get` → `Bibliography.get(code)` → `Scraper.scrape_page` → parses the reference with `Pubid::Ietf` → searches the one combined `index-v2` from `relaton-data-ietf` → fetches that row's YAML
|
|
39
|
+
1. **Single document lookup**: `Processor#get` → `Bibliography.get(code)` → `Scraper.scrape_page` → parses the reference with `Pubid::Ietf` → searches the one combined `index-v2` from `relaton-data-ietf` → fetches that row's YAML; a versioned draft the index lacks is read from bib.ietf.org (see below)
|
|
40
40
|
|
|
41
41
|
2. **Bulk data fetching**: `DataFetcher` extends `Relaton::Core::DataFetcher` with three datasets:
|
|
42
42
|
- `ietf-rfcsubseries` / `ietf-rfc-entries`: parse `rfc-index.xml` via `Rfc::Index` / `Rfc::Entry#to_item`
|
|
@@ -119,6 +119,30 @@ The old plain-string index matched that by substring; matching is exact now.
|
|
|
119
119
|
A reference pubid cannot parse returns nil, so `Bibliography.get "CN 8341"` logs
|
|
120
120
|
"Not found." rather than raising.
|
|
121
121
|
|
|
122
|
+
### An index miss on a versioned draft reads bib.ietf.org (relaton-ietf#125)
|
|
123
|
+
|
|
124
|
+
The data repo crawls the rsync `bibxml-ids` snapshot, and that snapshot keeps
|
|
125
|
+
only some versions of each draft: `draft-ietf-vcarddav-vcardrev` has only `-22`,
|
|
126
|
+
so `-06` was "Not found." When `fetch_doc` misses and the query is a
|
|
127
|
+
`Pubid::Ietf::Identifiers::InternetDraft` **with a version**, `fetch_bibxml` reads
|
|
128
|
+
`https://bib.ietf.org/public/rfc/bibxml-ids/reference.I-D.<draft>-NN.xml`
|
|
129
|
+
(`BIBXML_IDS`). That service resolves any version the datatracker knows and
|
|
130
|
+
answers 404 otherwise; the datatracker's own `doc/bibxml3/...` URL answers 404
|
|
131
|
+
for these old versions. The parse is the crawler's: `BibXMLParser.to_utf8`
|
|
132
|
+
(the CP1252 repair, shared with `DataFetcher#read_bibxml`), `BibXMLParser.parse`,
|
|
133
|
+
and the version from the identifier, as `process_singleton` sets it.
|
|
134
|
+
|
|
135
|
+
Only a 404 is "not found". Any other non-200 (429, 5xx) raises
|
|
136
|
+
`Relaton::RequestError`, so `Db#net_retry` retries it and `Db::Cache` does not
|
|
137
|
+
store an outage as a not-found row. A 200 whose body does not parse logs a
|
|
138
|
+
warning and returns nil; that rescue names the parse errors and is not
|
|
139
|
+
`StandardError`, so a transport error still becomes `RequestError`.
|
|
140
|
+
|
|
141
|
+
RFCs, sub-series and unversioned drafts do **not** fall back: the index holds
|
|
142
|
+
all of them, so a miss is a real "not found". A record read this way has no
|
|
143
|
+
`updates`/`updatedBy` neighbours — the crawl links those, and one fetch cannot.
|
|
144
|
+
Old bibxml carries no `target`, so such a record has a `TXT` URI and no `src`.
|
|
145
|
+
|
|
122
146
|
### The producer: `DataFetcher` writes `index-v2`
|
|
123
147
|
|
|
124
148
|
`DataFetcher` builds the index during the crawl, from the identifier it already
|
|
@@ -294,7 +318,8 @@ smart quotes and accented Latin letters. `File.read(encoding: "UTF-8")` only
|
|
|
294
318
|
*tags* a string; it validates nothing, so those reached lutaml as invalid UTF-8
|
|
295
319
|
and `Rfcxml::V3::Reference.from_xml` raised `Lutaml::Model::InvalidFormatError`.
|
|
296
320
|
|
|
297
|
-
`read_bibxml` transcodes them
|
|
321
|
+
`read_bibxml` transcodes them through `BibXMLParser.to_utf8`
|
|
322
|
+
(`bibxml_parser.rb`, shared with the scraper's bib.ietf.org fallback). Three things make that the
|
|
298
323
|
right call rather than `scrub`:
|
|
299
324
|
|
|
300
325
|
- **All of them decode losslessly as CP1252** — 125 of the ~122k published files
|
|
@@ -13,6 +13,44 @@ module Relaton
|
|
|
13
13
|
FromRfc.new(rfc).transform
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
#
|
|
17
|
+
# Decode bibxml bytes as UTF-8, recovering Windows-1252 bytes.
|
|
18
|
+
#
|
|
19
|
+
# These files declare `encoding='UTF-8'` but some carry CP1252 — smart
|
|
20
|
+
# quotes and accented Latin letters. `File.read(encoding: "UTF-8")` only
|
|
21
|
+
# *tags* the string, so those reach lutaml as invalid UTF-8 and it raises.
|
|
22
|
+
#
|
|
23
|
+
# `scrub` with a block transcodes each invalid *run* as CP1252 while
|
|
24
|
+
# leaving valid UTF-8 untouched. Both halves matter:
|
|
25
|
+
#
|
|
26
|
+
# * Not plain `scrub`, which substitutes U+FFFD: `client’s` would become
|
|
27
|
+
# `client�s` and `Muñoz` `Mu�oz` — author surnames included.
|
|
28
|
+
# The damage is length-preserving, so a length check will not catch it.
|
|
29
|
+
# * Not a whole-file CP1252 re-decode, which mangles a file that is
|
|
30
|
+
# genuinely UTF-8 apart from one stray byte: `Muñoz café ’` would come
|
|
31
|
+
# back as `Muñoz café ’`, silently, since the result is valid UTF-8 and
|
|
32
|
+
# nothing raises. No such file exists in today's corpus (0 of the 125
|
|
33
|
+
# affected contain valid multi-byte UTF-8) but it grows daily, and
|
|
34
|
+
# "decodes losslessly as CP1252" is weak evidence of correctness —
|
|
35
|
+
# CP1252 maps 251 of 256 byte values.
|
|
36
|
+
#
|
|
37
|
+
# `undef: :replace` covers the five bytes CP1252 leaves undefined
|
|
38
|
+
# (0x81 0x8D 0x8F 0x90 0x9D), so this returns valid UTF-8 rather than
|
|
39
|
+
# raising and costing the whole file.
|
|
40
|
+
#
|
|
41
|
+
# @param bytes [String] raw bytes, any encoding tag
|
|
42
|
+
# @return [String] UTF-8, valid encoding
|
|
43
|
+
#
|
|
44
|
+
def to_utf8(bytes)
|
|
45
|
+
utf8 = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
46
|
+
return utf8 if utf8.valid_encoding?
|
|
47
|
+
|
|
48
|
+
utf8.scrub do |bad|
|
|
49
|
+
bad.force_encoding(Encoding::WINDOWS_1252)
|
|
50
|
+
.encode(Encoding::UTF_8, undef: :replace)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
16
54
|
def pubid_type(id)
|
|
17
55
|
pref = id.match(/^(\S+)/)[1]
|
|
18
56
|
case pref
|
|
@@ -216,41 +216,14 @@ module Relaton
|
|
|
216
216
|
end
|
|
217
217
|
|
|
218
218
|
#
|
|
219
|
-
# Read a bibxml file as UTF-8, recovering Windows-1252 bytes.
|
|
220
|
-
#
|
|
221
|
-
# These files declare `encoding='UTF-8'` but some carry CP1252 — smart
|
|
222
|
-
# quotes and accented Latin letters. `File.read(encoding: "UTF-8")` only
|
|
223
|
-
# *tags* the string, so those reach lutaml as invalid UTF-8 and it raises.
|
|
224
|
-
#
|
|
225
|
-
# `scrub` with a block transcodes each invalid *run* as CP1252 while
|
|
226
|
-
# leaving valid UTF-8 untouched. Both halves matter:
|
|
227
|
-
#
|
|
228
|
-
# * Not plain `scrub`, which substitutes U+FFFD: `client’s` would become
|
|
229
|
-
# `client\uFFFDs` and `Muñoz` `Mu\uFFFDoz` — author surnames included.
|
|
230
|
-
# The damage is length-preserving, so a length check will not catch it.
|
|
231
|
-
# * Not a whole-file CP1252 re-decode, which mangles a file that is
|
|
232
|
-
# genuinely UTF-8 apart from one stray byte: `Muñoz café ’` would come
|
|
233
|
-
# back as `Muñoz café ’`, silently, since the result is valid UTF-8 and
|
|
234
|
-
# nothing raises. No such file exists in today's corpus (0 of the 125
|
|
235
|
-
# affected contain valid multi-byte UTF-8) but it grows daily, and
|
|
236
|
-
# "decodes losslessly as CP1252" is weak evidence of correctness —
|
|
237
|
-
# CP1252 maps 251 of 256 byte values.
|
|
238
|
-
#
|
|
239
|
-
# `undef: :replace` covers the five bytes CP1252 leaves undefined
|
|
240
|
-
# (0x81 0x8D 0x8F 0x90 0x9D), so this returns valid UTF-8 rather than
|
|
241
|
-
# raising and costing the whole file.
|
|
219
|
+
# Read a bibxml file as UTF-8, recovering Windows-1252 bytes. The repair
|
|
220
|
+
# is `BibXMLParser.to_utf8`, shared with `Scraper`'s bib.ietf.org fallback.
|
|
242
221
|
#
|
|
243
222
|
# @param path [String]
|
|
244
223
|
# @return [String] UTF-8, valid encoding
|
|
245
224
|
#
|
|
246
225
|
def read_bibxml(path)
|
|
247
|
-
|
|
248
|
-
return utf8 if utf8.valid_encoding?
|
|
249
|
-
|
|
250
|
-
utf8.scrub do |bad|
|
|
251
|
-
bad.force_encoding(Encoding::WINDOWS_1252)
|
|
252
|
-
.encode(Encoding::UTF_8, undef: :replace)
|
|
253
|
-
end
|
|
226
|
+
BibXMLParser.to_utf8 File.binread(path)
|
|
254
227
|
end
|
|
255
228
|
|
|
256
229
|
#
|
data/lib/relaton/ietf/scraper.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "pubid"
|
|
4
4
|
require "pubid/ietf"
|
|
5
|
+
require_relative "bibxml_parser"
|
|
5
6
|
|
|
6
7
|
module Relaton
|
|
7
8
|
module Ietf
|
|
@@ -15,6 +16,13 @@ module Relaton
|
|
|
15
16
|
# publishing their `index-v1` for released relatons, untouched.
|
|
16
17
|
IETF = "https://raw.githubusercontent.com/relaton/relaton-data-ietf/main/"
|
|
17
18
|
|
|
19
|
+
# The IETF bibxml service, read when the index has no row for a versioned
|
|
20
|
+
# Internet-Draft (relaton-ietf#125). The data repo crawls the rsync
|
|
21
|
+
# `bibxml-ids` snapshot, which keeps only some versions of each draft
|
|
22
|
+
# (`draft-ietf-vcarddav-vcardrev`: only -22); this service resolves any
|
|
23
|
+
# version the datatracker knows, and answers 404 for one it does not.
|
|
24
|
+
BIBXML_IDS = "https://bib.ietf.org/public/rfc/bibxml-ids/"
|
|
25
|
+
|
|
18
26
|
# @param ref [String, Pubid::Ietf::Identifier] a reference, or its parse
|
|
19
27
|
# from Relaton::Db (relaton#205), which is used as it is
|
|
20
28
|
# @return [Relaton::Ietf::ItemData, nil]
|
|
@@ -24,8 +32,9 @@ module Relaton
|
|
|
24
32
|
|
|
25
33
|
fetch_doc id
|
|
26
34
|
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
|
27
|
-
|
|
28
|
-
Net::
|
|
35
|
+
Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH,
|
|
36
|
+
OpenSSL::SSL::SSLError, Net::HTTPBadResponse,
|
|
37
|
+
Net::HTTPHeaderSyntaxError, Net::ProtocolError, SocketError
|
|
29
38
|
raise Relaton::RequestError, "No document found for #{ref} reference"
|
|
30
39
|
end
|
|
31
40
|
|
|
@@ -44,7 +53,55 @@ module Relaton
|
|
|
44
53
|
# such pairs in the first 40k rows of the index fixture).
|
|
45
54
|
def fetch_doc(id)
|
|
46
55
|
row = index.search(id, exact: true).first
|
|
47
|
-
get_page "#{IETF}#{row[:file]}" if row
|
|
56
|
+
return get_page "#{IETF}#{row[:file]}" if row
|
|
57
|
+
|
|
58
|
+
fetch_bibxml id if versioned_draft? id
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Only a versioned draft falls back. The index holds every RFC, sub-series
|
|
62
|
+
# and unversioned draft aggregator the data repo has, so a miss on one of
|
|
63
|
+
# those is a real "not found", and bib.ietf.org has no aggregator to offer.
|
|
64
|
+
def versioned_draft?(id)
|
|
65
|
+
id.is_a?(::Pubid::Ietf::Identifiers::InternetDraft) && !id.version.to_s.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
#
|
|
69
|
+
# Read one draft version from bib.ietf.org and parse it the way
|
|
70
|
+
# `DataFetcher#process_singleton` parses a crawled file: the same
|
|
71
|
+
# CP1252 repair, the same parser, and the version from the identifier.
|
|
72
|
+
#
|
|
73
|
+
# A 404 is the service's "no such version". Any other non-200 (a 429, a
|
|
74
|
+
# 5xx) is push-back or an outage, so it raises `Relaton::RequestError`:
|
|
75
|
+
# `Db#net_retry` then retries it, and the Db cache does not store it as a
|
|
76
|
+
# not-found.
|
|
77
|
+
#
|
|
78
|
+
# @param id [Pubid::Ietf::Identifiers::InternetDraft]
|
|
79
|
+
# @return [Relaton::Ietf::ItemData, nil] nil on a 404 or an unreadable body
|
|
80
|
+
#
|
|
81
|
+
def fetch_bibxml(id)
|
|
82
|
+
res = Net::HTTP.get_response URI("#{BIBXML_IDS}reference.I-D.#{id}.xml")
|
|
83
|
+
return if res.code == "404"
|
|
84
|
+
raise Relaton::RequestError, "#{BIBXML_IDS} answered #{res.code} for #{id}" unless res.code == "200"
|
|
85
|
+
|
|
86
|
+
bib = parse_bibxml(res.body, id)
|
|
87
|
+
return unless bib
|
|
88
|
+
|
|
89
|
+
bib.version = [Bib::Version.new(draft: id.version.to_s)]
|
|
90
|
+
bib.fetched = Date.today.to_s
|
|
91
|
+
bib
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# A 200 that is not bibxml (an HTML error page, a truncated body) costs
|
|
95
|
+
# this one document, not the caller's compile. These are the errors the
|
|
96
|
+
# parse raises on such a body: lutaml's `InvalidFormatError`, and the
|
|
97
|
+
# converter's `NoMethodError` on a document with no `<front>` and
|
|
98
|
+
# `ArgumentError` from its regexes. They are named, not `StandardError`,
|
|
99
|
+
# so a transport error still reaches `scrape_page`'s rescue.
|
|
100
|
+
def parse_bibxml(body, id)
|
|
101
|
+
BibXMLParser.parse BibXMLParser.to_utf8(body)
|
|
102
|
+
rescue Lutaml::Model::InvalidFormatError, NoMethodError, ArgumentError => e
|
|
103
|
+
Util.warn "Unreadable bibxml for `#{id}`: #{e.message.to_s.lines.first.to_s.strip}"
|
|
104
|
+
nil
|
|
48
105
|
end
|
|
49
106
|
|
|
50
107
|
#
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
require "net/http"
|
|
5
5
|
|
|
6
6
|
module Relaton
|
|
@@ -20,7 +20,7 @@ module Relaton
|
|
|
20
20
|
response = Net::HTTP.get_response(uri)
|
|
21
21
|
break unless response.is_a?(Net::HTTPSuccess)
|
|
22
22
|
|
|
23
|
-
data =
|
|
23
|
+
data = Relaton::Json.parse_external(response.body)
|
|
24
24
|
objects = data["objects"] || []
|
|
25
25
|
break if objects.empty?
|
|
26
26
|
|
data/lib/relaton/iho/CLAUDE.md
CHANGED
|
@@ -55,6 +55,10 @@ In `item.rb`, `require_relative "relation"` is placed **after** the `Item` class
|
|
|
55
55
|
|
|
56
56
|
`Bibliography.search`/`.get` fetches YAML from the relaton-data-iho GitHub repo via `Relaton::Index`, then deserializes into model objects.
|
|
57
57
|
|
|
58
|
+
**Editions.** The pubid `version` is the edition (`IHO B-12 3.0.0`; the grammar takes `a.b.c` only). A reference with no version answers with the **latest** edition: `version_key` compares the segments as integers (`2.10.0` after `2.9.0`). It used to be `min_by` on the string, which returned the oldest.
|
|
59
|
+
|
|
60
|
+
**The edition reaches the index only through the committed row.** A record prints `B-12` and keeps its edition in `edition:`, so `relaton index` cannot read a version from the docid. `MachineIndex#refine` (relaton-cli) takes the committed `index-v3.yaml` row when it adds only a `version` to the parse. Before that, the Pages index (generated 2026-10-29) had no `version` in any of its 325 rows, so every versioned reference answered not-found (relaton-data-iho#20). The suite did not see it: `spec/fixtures/index-v3.zip` is the committed index, which has versions.
|
|
61
|
+
|
|
58
62
|
### Test Infrastructure
|
|
59
63
|
|
|
60
64
|
- **VCR cassettes** (`spec/vcr_cassettes/`): recorded HTTP responses for offline testing
|
|
@@ -21,7 +21,7 @@ module Relaton
|
|
|
21
21
|
# Pubid::Iho::Identifiers::* (Relaton::Index deserialized it via the
|
|
22
22
|
# `pubid_class` passed in `#index`), so `pubid_match?` compares pubids.
|
|
23
23
|
row = index.search(pubid) { |r| pubid_match?(r[:id], pubid) }
|
|
24
|
-
.
|
|
24
|
+
.max_by { |r| version_key(r[:id]) }
|
|
25
25
|
unless row
|
|
26
26
|
Util.info "Not found.", key: pubid.to_s
|
|
27
27
|
return
|
|
@@ -96,6 +96,12 @@ module Relaton
|
|
|
96
96
|
)
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
# The sort key for the latest edition: segments compare as integers,
|
|
100
|
+
# so 2.10.0 is later than 2.9.0. A row with no version is the oldest.
|
|
101
|
+
def version_key(id)
|
|
102
|
+
id.version.to_s.split(".").map(&:to_i)
|
|
103
|
+
end
|
|
104
|
+
|
|
99
105
|
# Both `row_id` and `query` are Pubid::Iho::Identifiers::* instances.
|
|
100
106
|
# Class identity pins the series (S/P/M/B/C). Subdivision keys
|
|
101
107
|
# (part/appendix/annex/supplement) use strict equality — a nil query
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "yeptris"
|
|
4
|
+
|
|
3
5
|
require "digest"
|
|
4
|
-
require "json"
|
|
5
6
|
require "net/http"
|
|
6
7
|
require "time"
|
|
7
8
|
require "uri"
|
|
@@ -91,8 +92,8 @@ module Relaton
|
|
|
91
92
|
end
|
|
92
93
|
raise Error, "cannot fetch #{uri}: HTTP #{res.code}" unless res.code == "200"
|
|
93
94
|
|
|
94
|
-
JSON.
|
|
95
|
-
rescue JSON::
|
|
95
|
+
Yeptris::JSON.load(res.body)
|
|
96
|
+
rescue Yeptris::JSON::ParseError => e
|
|
96
97
|
raise Error, "malformed manifest at #{uri}: #{e.message}"
|
|
97
98
|
end
|
|
98
99
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "json"
|
|
2
3
|
require "net/http"
|
|
3
4
|
require "openssl"
|
|
@@ -174,8 +175,8 @@ module Relaton
|
|
|
174
175
|
end
|
|
175
176
|
|
|
176
177
|
def parse_json(body, path)
|
|
177
|
-
JSON.
|
|
178
|
-
rescue JSON::
|
|
178
|
+
Yeptris::JSON.load(body)
|
|
179
|
+
rescue Yeptris::JSON::ParseError => e
|
|
179
180
|
raise Error, "cannot parse `#{url(path)}`: #{e.message}"
|
|
180
181
|
end
|
|
181
182
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "yeptris"
|
|
4
|
+
|
|
3
5
|
require "sqlite3"
|
|
4
6
|
|
|
5
7
|
module Relaton
|
|
@@ -34,7 +36,7 @@ module Relaton
|
|
|
34
36
|
@db.transaction do
|
|
35
37
|
@db.execute("DELETE FROM index_rows")
|
|
36
38
|
@db.prepare("INSERT INTO index_rows (number, sort_key, id_json, file) VALUES (?, ?, ?, ?)") do |stmt|
|
|
37
|
-
rows_enum.each { |(number, sort_key, id_hash, file)| stmt.execute(number, sort_key, JSON.
|
|
39
|
+
rows_enum.each { |(number, sort_key, id_hash, file)| stmt.execute(number, sort_key, Yeptris::JSON.dump(id_hash), file) }
|
|
38
40
|
end
|
|
39
41
|
meta_set("schema_version", SCHEMA_VERSION)
|
|
40
42
|
end
|
|
@@ -44,14 +46,14 @@ module Relaton
|
|
|
44
46
|
def bucket(number)
|
|
45
47
|
rows = []
|
|
46
48
|
@db.execute("SELECT id_json, file FROM index_rows WHERE number = ? ORDER BY rowid", [number]) do |row|
|
|
47
|
-
rows << { id: JSON.
|
|
49
|
+
rows << { id: Yeptris::JSON.load(row[0]), file: row[1] }
|
|
48
50
|
end
|
|
49
51
|
rows
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def each_row
|
|
53
55
|
@db.execute("SELECT id_json, file FROM index_rows ORDER BY rowid") do |row|
|
|
54
|
-
yield({ id: JSON.
|
|
56
|
+
yield({ id: Yeptris::JSON.load(row[0]), file: row[1] })
|
|
55
57
|
end
|
|
56
58
|
end
|
|
57
59
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
module Relaton
|
|
2
3
|
module Isbn
|
|
3
4
|
#
|
|
@@ -37,7 +38,7 @@ module Relaton
|
|
|
37
38
|
response = Net::HTTP.get_response uri
|
|
38
39
|
return unless response.is_a? Net::HTTPSuccess
|
|
39
40
|
|
|
40
|
-
data =
|
|
41
|
+
data = Relaton::Json.parse_external response.body
|
|
41
42
|
return unless data["records"]&.any?
|
|
42
43
|
|
|
43
44
|
data["records"].first.last
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "fileutils"
|
|
2
|
-
require "json"
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require_relative "../iso"
|
|
@@ -168,7 +168,7 @@ module Relaton
|
|
|
168
168
|
amend_map = Hash.new { |h, k| h[k] = [] }
|
|
169
169
|
date_map = {}
|
|
170
170
|
File.foreach(path, encoding: "UTF-8") do |line|
|
|
171
|
-
rec =
|
|
171
|
+
rec = Relaton::Json.parse_external(line)
|
|
172
172
|
id = rec["id"]
|
|
173
173
|
ref = normalize_reference(rec["reference"])
|
|
174
174
|
next unless ref
|
|
@@ -179,7 +179,7 @@ module Relaton
|
|
|
179
179
|
if rec["supplementType"] && (base = amend_base(ref))
|
|
180
180
|
amend_map[base] << ref
|
|
181
181
|
end
|
|
182
|
-
rescue JSON::ParserError
|
|
182
|
+
rescue ::JSON::ParserError
|
|
183
183
|
next
|
|
184
184
|
end
|
|
185
185
|
Util.info "Indexed #{ref_map.size} references; " \
|
|
@@ -216,11 +216,11 @@ module Relaton
|
|
|
216
216
|
path = download_tc_dataset
|
|
217
217
|
map = {}
|
|
218
218
|
File.foreach(path, encoding: "UTF-8") do |line|
|
|
219
|
-
rec =
|
|
219
|
+
rec = Relaton::Json.parse_external(line)
|
|
220
220
|
ref = rec["reference"]
|
|
221
221
|
title = rec["title"]
|
|
222
222
|
map[ref] = title if ref && title.is_a?(Hash)
|
|
223
|
-
rescue JSON::ParserError
|
|
223
|
+
rescue ::JSON::ParserError
|
|
224
224
|
next
|
|
225
225
|
end
|
|
226
226
|
Util.info "Indexed #{map.size} committees."
|
|
@@ -231,7 +231,7 @@ module Relaton
|
|
|
231
231
|
Util.info "Parsing records..."
|
|
232
232
|
count = 0
|
|
233
233
|
File.foreach(path, encoding: "UTF-8") do |line|
|
|
234
|
-
rec =
|
|
234
|
+
rec = Relaton::Json.parse_external(line)
|
|
235
235
|
next unless ingestable?(rec["reference"])
|
|
236
236
|
|
|
237
237
|
fetch_pub(rec, ref_index, tc_index, amend_index, date_index)
|
|
@@ -64,10 +64,6 @@ module Relaton
|
|
|
64
64
|
original_content
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def to_s
|
|
68
|
-
content.to_s
|
|
69
|
-
end
|
|
70
|
-
|
|
71
67
|
# `remove_part!`/`remove_date!` are unnecessary here: `to_all_parts`'s
|
|
72
68
|
# own identity computation already excludes part/date (down the whole
|
|
73
69
|
# base chain, so a supplement's own year is dropped too — a disclosed
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "net/http"
|
|
2
|
-
require "json"
|
|
3
3
|
require "uri"
|
|
4
4
|
require "mechanize"
|
|
5
5
|
require_relative "../itu"
|
|
@@ -542,7 +542,7 @@ module Relaton
|
|
|
542
542
|
request["User-Agent"] = USER_AGENT
|
|
543
543
|
|
|
544
544
|
response = http.request(request)
|
|
545
|
-
json =
|
|
545
|
+
json = Relaton::Json.parse_external(response.body)
|
|
546
546
|
json["Data"] || []
|
|
547
547
|
end
|
|
548
548
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
require "cgi"
|
|
4
|
-
require "json"
|
|
5
5
|
require_relative "hit"
|
|
6
6
|
|
|
7
7
|
module Relaton
|
|
@@ -239,8 +239,8 @@ module Relaton
|
|
|
239
239
|
# @return [Array<Hash>] editions of the recommendation
|
|
240
240
|
def editions(idrec)
|
|
241
241
|
# a `null` body parses to nil, so `|| []` is not redundant
|
|
242
|
-
|
|
243
|
-
rescue JSON::ParserError
|
|
242
|
+
Relaton::Json.parse_external(agent.get(format(RECEDITIONS_URL, idrec: idrec)).body) || []
|
|
243
|
+
rescue ::JSON::ParserError
|
|
244
244
|
[]
|
|
245
245
|
end
|
|
246
246
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
require_relative "family_cache"
|
|
5
5
|
|
|
6
6
|
module Relaton
|
|
@@ -251,7 +251,7 @@ module Relaton
|
|
|
251
251
|
end
|
|
252
252
|
|
|
253
253
|
def get_data(url)
|
|
254
|
-
|
|
254
|
+
Relaton::Json.parse_external request_document(url).body
|
|
255
255
|
end
|
|
256
256
|
|
|
257
257
|
def request_document(url)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
require_relative "../jis"
|
|
4
5
|
require_relative "scraper"
|
|
5
6
|
|
|
@@ -92,7 +93,7 @@ module Relaton
|
|
|
92
93
|
body = { record: 0, dantai: "JIS", searchtype2: 1,
|
|
93
94
|
status_1: 1, status_2: 2 } # rubocop:disable Naming/VariableNumber
|
|
94
95
|
resp = agent.post "#{URL}W11M0270/index", body
|
|
95
|
-
disp =
|
|
96
|
+
disp = Relaton::Json.parse_external resp.body
|
|
96
97
|
@initial_time = Time.now
|
|
97
98
|
disp["status"] || Util.warn("No results found for JIS")
|
|
98
99
|
end
|
data/lib/relaton/json.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "yeptris"
|
|
2
|
+
|
|
3
|
+
module Relaton
|
|
4
|
+
# JSON at the system boundary — data we do not control. Everything we
|
|
5
|
+
# generate or own (index manifests, shards, the db cache, request bodies,
|
|
6
|
+
# the schema-version table) parses with Yeptris::JSON's default strict
|
|
7
|
+
# surface.
|
|
8
|
+
module Json
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Third-party feeds carry duplicate keys in the wild (Crossref book
|
|
12
|
+
# records keyed by ISBN, NIST's scraped pubs export keyed by " ",
|
|
13
|
+
# the UN search API's blank keys, CCSDS's embedded config); parsing them
|
|
14
|
+
# the historical way is last-wins. yeptris 0.6.51 carries the boundary
|
|
15
|
+
# opt-out (yeptris-ruby#335/#339), so the whole stack stays on yeptris —
|
|
16
|
+
# no stdlib fallback anywhere.
|
|
17
|
+
def parse_external(source)
|
|
18
|
+
::Yeptris::JSON.load(source, allow_duplicate_key: true)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "net/http"
|
|
2
|
-
require "json"
|
|
3
3
|
|
|
4
4
|
module Relaton
|
|
5
5
|
module Logger
|
|
@@ -70,7 +70,7 @@ module Relaton
|
|
|
70
70
|
responce = get(uri)
|
|
71
71
|
return nil unless responce.code.to_i == 200
|
|
72
72
|
|
|
73
|
-
items = JSON.
|
|
73
|
+
items = Yeptris::JSON.load(responce.body)["items"] || []
|
|
74
74
|
items.find { |i| i["title"] == @title }&.fetch("number", nil)
|
|
75
75
|
rescue StandardError => e
|
|
76
76
|
puts "Failed to search for an open `#{@title}` issue: #{e.message}"
|
|
@@ -88,7 +88,7 @@ module Relaton
|
|
|
88
88
|
http.use_ssl = true
|
|
89
89
|
|
|
90
90
|
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
91
|
-
request.body = body
|
|
91
|
+
request.body = Yeptris::JSON.dump(body)
|
|
92
92
|
|
|
93
93
|
http.request(request)
|
|
94
94
|
end
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
module Relaton::Logger
|
|
2
3
|
class FormatterJSON < ::Logger::Formatter
|
|
3
4
|
def call(severity, _datetime, progname, msg, **args)
|
|
4
5
|
hash = { prog: progname, message: msg, severity: severity }.merge(args)
|
|
5
|
-
"
|
|
6
|
+
Yeptris::JSON.dump(hash) + "\n"
|
|
6
7
|
end
|
|
7
8
|
end
|
|
8
9
|
end
|
data/lib/relaton/nist/CLAUDE.md
CHANGED
|
@@ -64,6 +64,7 @@ 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
68
|
- **`#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).
|
|
68
69
|
- `ModsParser` — maps MODS XML (via `loc_mods`) to `ItemData`
|
|
69
70
|
- `Scraper` — fetches items from GitHub YAML or CSRC JSON
|
|
@@ -118,6 +119,11 @@ NIST-specific gotchas (why this isn't a straight copy of IEC/CCSDS):
|
|
|
118
119
|
`@raw_content` before wrapping `@pubid` in pubid's `AllParts` — matching
|
|
119
120
|
IEC/CCSDS. `#pubid` itself, read directly, now answers `all_parts? ==
|
|
120
121
|
true` and renders WITH pubid's generic "(all parts)" marker.
|
|
122
|
+
- **The internal form drops `NIST `/`NBS `** (relaton#49), so
|
|
123
|
+
`docid.to_s(form: :internal)` is `SP 800-113` for `NIST SP 800-113`. The rule
|
|
124
|
+
is the `"NIST"` row of `Bib::Docidentifier::FORM_PUBID`: `@pubid`
|
|
125
|
+
renders with `to_s(with_publisher: false)`. A DOI docid has type `DOI` and is
|
|
126
|
+
unchanged. `to_s` itself is inherited from `Bib::Docidentifier`.
|
|
121
127
|
|
|
122
128
|
### Serialization Round-Trip Pattern
|
|
123
129
|
|
|
@@ -74,10 +74,6 @@ module Relaton
|
|
|
74
74
|
original_content
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
def to_s
|
|
78
|
-
content.to_s
|
|
79
|
-
end
|
|
80
|
-
|
|
81
77
|
# `#exclude` (no args) clones the whole graph (incl. a supplement's
|
|
82
78
|
# base), since `walk_chain` mutates it in place. `@raw_content` freezes
|
|
83
79
|
# the stripped rendering — `content` is live from `@pubid`, and the
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "singleton"
|
|
2
3
|
|
|
3
4
|
module Relaton
|
|
@@ -61,7 +62,7 @@ module Relaton
|
|
|
61
62
|
#
|
|
62
63
|
def unzip
|
|
63
64
|
Zip::File.open(DATAFILE) do |zf|
|
|
64
|
-
|
|
65
|
+
Relaton::Json.parse_external zf.first.get_input_stream.read
|
|
65
66
|
end
|
|
66
67
|
end
|
|
67
68
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
require "fileutils"
|
|
4
5
|
require "faraday"
|
|
5
|
-
require "json"
|
|
6
6
|
require_relative "../ogc"
|
|
7
7
|
require_relative "scraper"
|
|
8
8
|
|
|
@@ -126,7 +126,7 @@ module Relaton
|
|
|
126
126
|
resp = Faraday.new(ENDPOINT, headers: h).get
|
|
127
127
|
case resp.status
|
|
128
128
|
when 200
|
|
129
|
-
json =
|
|
129
|
+
json = Relaton::Json.parse_external(resp.body)
|
|
130
130
|
block_given? ? yield(resp[:etag], json) : json
|
|
131
131
|
when 304 then []
|
|
132
132
|
else raise Relaton::RequestError, "Could not access #{ENDPOINT}"
|
data/lib/relaton/omg/scraper.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
require "mechanize"
|
|
5
5
|
require "pubid"
|
|
6
6
|
|
|
@@ -118,10 +118,10 @@ module Relaton
|
|
|
118
118
|
script = @doc.at_xpath('//script[@type="application/ld+json"]')
|
|
119
119
|
return unless script
|
|
120
120
|
|
|
121
|
-
node =
|
|
121
|
+
node = Relaton::Json.parse_external(script.text).find { |e| e.is_a?(Hash) && e[LD_DATE] }
|
|
122
122
|
value = node && node[LD_DATE].first["@value"]
|
|
123
123
|
value && ::Date.parse(value)
|
|
124
|
-
rescue JSON::ParserError, ::Date::Error
|
|
124
|
+
rescue ::JSON::ParserError, ::Date::Error
|
|
125
125
|
nil
|
|
126
126
|
end
|
|
127
127
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "yeptris"
|
|
1
2
|
require "json"
|
|
2
3
|
require "relaton/core"
|
|
3
4
|
require_relative "../plateau"
|
|
@@ -78,7 +79,7 @@ module Relaton
|
|
|
78
79
|
body = hadle_response(response)
|
|
79
80
|
|
|
80
81
|
# Parse the JSON response
|
|
81
|
-
|
|
82
|
+
Relaton::Json.parse_external(body)
|
|
82
83
|
rescue StandardError => e
|
|
83
84
|
# Handle any errors during the fetching process
|
|
84
85
|
Util.error "Error fetching JSON data from #{url}: #{e.message}"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "yeptris"
|
|
4
|
+
|
|
3
5
|
require "faraday"
|
|
4
|
-
require "json"
|
|
5
6
|
require "securerandom"
|
|
6
7
|
|
|
7
8
|
module Relaton
|
|
@@ -28,11 +29,11 @@ module Relaton
|
|
|
28
29
|
def search_api(text) # rubocop:disable Metrics/MethodLength
|
|
29
30
|
resp = connection.post("search?l=en&rid=#{SecureRandom.uuid}") do |req|
|
|
30
31
|
req.headers["Content-Type"] = "application/json"
|
|
31
|
-
req.body = JSON.
|
|
32
|
+
req.body = Yeptris::JSON.dump(search_body(text))
|
|
32
33
|
end
|
|
33
34
|
return unless resp.success?
|
|
34
35
|
|
|
35
|
-
body =
|
|
36
|
+
body = Relaton::Json.parse_external(resp.body)
|
|
36
37
|
return unless body["status"] == 1
|
|
37
38
|
|
|
38
39
|
body.dig("body", "data")
|
data/lib/relaton/version.rb
CHANGED
data/lib/relaton.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Relaton
|
|
|
22
22
|
autoload :Logger, "relaton/logger"
|
|
23
23
|
autoload :Equivalence, "relaton/equivalence"
|
|
24
24
|
autoload :Core, "relaton/core"
|
|
25
|
+
autoload :Json, "relaton/json"
|
|
25
26
|
autoload :Index, "relaton/index"
|
|
26
27
|
autoload :Bib, "relaton/bib"
|
|
27
28
|
autoload :ThreeGpp, "relaton/3gpp"
|
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.
|
|
4
|
+
version: 3.0.2
|
|
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-
|
|
11
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: addressable
|
|
@@ -128,14 +128,14 @@ dependencies:
|
|
|
128
128
|
requirements:
|
|
129
129
|
- - "~>"
|
|
130
130
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: 2.7
|
|
131
|
+
version: '2.7'
|
|
132
132
|
type: :runtime
|
|
133
133
|
prerelease: false
|
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
135
|
requirements:
|
|
136
136
|
- - "~>"
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: 2.7
|
|
138
|
+
version: '2.7'
|
|
139
139
|
- !ruby/object:Gem::Dependency
|
|
140
140
|
name: faraday-net_http_persistent
|
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -248,20 +248,6 @@ dependencies:
|
|
|
248
248
|
- - "~>"
|
|
249
249
|
- !ruby/object:Gem::Version
|
|
250
250
|
version: 0.1.6
|
|
251
|
-
- !ruby/object:Gem::Dependency
|
|
252
|
-
name: json
|
|
253
|
-
requirement: !ruby/object:Gem::Requirement
|
|
254
|
-
requirements:
|
|
255
|
-
- - "<"
|
|
256
|
-
- !ruby/object:Gem::Version
|
|
257
|
-
version: '3'
|
|
258
|
-
type: :runtime
|
|
259
|
-
prerelease: false
|
|
260
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
261
|
-
requirements:
|
|
262
|
-
- - "<"
|
|
263
|
-
- !ruby/object:Gem::Version
|
|
264
|
-
version: '3'
|
|
265
251
|
- !ruby/object:Gem::Dependency
|
|
266
252
|
name: loc_mods
|
|
267
253
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -428,7 +414,7 @@ dependencies:
|
|
|
428
414
|
requirements:
|
|
429
415
|
- - ">="
|
|
430
416
|
- !ruby/object:Gem::Version
|
|
431
|
-
version: 1.3.
|
|
417
|
+
version: 1.3.114
|
|
432
418
|
- - "<"
|
|
433
419
|
- !ruby/object:Gem::Version
|
|
434
420
|
version: '1.4'
|
|
@@ -438,7 +424,7 @@ dependencies:
|
|
|
438
424
|
requirements:
|
|
439
425
|
- - ">="
|
|
440
426
|
- !ruby/object:Gem::Version
|
|
441
|
-
version: 1.3.
|
|
427
|
+
version: 1.3.114
|
|
442
428
|
- - "<"
|
|
443
429
|
- !ruby/object:Gem::Version
|
|
444
430
|
version: '1.4'
|
|
@@ -460,30 +446,36 @@ dependencies:
|
|
|
460
446
|
name: pubid
|
|
461
447
|
requirement: !ruby/object:Gem::Requirement
|
|
462
448
|
requirements:
|
|
463
|
-
- - "
|
|
449
|
+
- - ">="
|
|
450
|
+
- !ruby/object:Gem::Version
|
|
451
|
+
version: 2.0.1
|
|
452
|
+
- - "<"
|
|
464
453
|
- !ruby/object:Gem::Version
|
|
465
|
-
version: 2.
|
|
454
|
+
version: '2.1'
|
|
466
455
|
type: :runtime
|
|
467
456
|
prerelease: false
|
|
468
457
|
version_requirements: !ruby/object:Gem::Requirement
|
|
469
458
|
requirements:
|
|
470
|
-
- - "
|
|
459
|
+
- - ">="
|
|
460
|
+
- !ruby/object:Gem::Version
|
|
461
|
+
version: 2.0.1
|
|
462
|
+
- - "<"
|
|
471
463
|
- !ruby/object:Gem::Version
|
|
472
|
-
version: 2.
|
|
464
|
+
version: '2.1'
|
|
473
465
|
- !ruby/object:Gem::Dependency
|
|
474
466
|
name: yeptris
|
|
475
467
|
requirement: !ruby/object:Gem::Requirement
|
|
476
468
|
requirements:
|
|
477
469
|
- - ">="
|
|
478
470
|
- !ruby/object:Gem::Version
|
|
479
|
-
version:
|
|
471
|
+
version: 0.6.51
|
|
480
472
|
type: :runtime
|
|
481
473
|
prerelease: false
|
|
482
474
|
version_requirements: !ruby/object:Gem::Requirement
|
|
483
475
|
requirements:
|
|
484
476
|
- - ">="
|
|
485
477
|
- !ruby/object:Gem::Version
|
|
486
|
-
version:
|
|
478
|
+
version: 0.6.51
|
|
487
479
|
- !ruby/object:Gem::Dependency
|
|
488
480
|
name: relaton-render
|
|
489
481
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -1149,6 +1141,7 @@ files:
|
|
|
1149
1141
|
- lib/relaton/jis/scraper.rb
|
|
1150
1142
|
- lib/relaton/jis/structured_identifier.rb
|
|
1151
1143
|
- lib/relaton/jis/util.rb
|
|
1144
|
+
- lib/relaton/json.rb
|
|
1152
1145
|
- lib/relaton/logger.rb
|
|
1153
1146
|
- lib/relaton/logger/CLAUDE.md
|
|
1154
1147
|
- lib/relaton/logger/README.adoc
|