relaton 3.0.0 → 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/3gpp/CLAUDE.md +31 -0
- data/lib/relaton/3gpp/bibliography.rb +1 -1
- data/lib/relaton/3gpp/data_fetcher.rb +32 -9
- data/lib/relaton/3gpp/date.rb +13 -0
- data/lib/relaton/3gpp/item.rb +3 -0
- data/lib/relaton/3gpp/parser.rb +9 -6
- data/lib/relaton/bib/converter/csl.rb +3 -2
- data/lib/relaton/bib/model/docidentifier.rb +61 -0
- data/lib/relaton/bib/model/item_shared.rb +12 -1
- 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 +11 -3
- data/lib/relaton/ccsds/data/fetcher.rb +2 -1
- data/lib/relaton/ccsds/model/docidentifier.rb +0 -4
- data/lib/relaton/cie/authors_parser.rb +78 -0
- data/lib/relaton/cie/data_fetcher.rb +23 -18
- data/lib/relaton/cie.rb +1 -0
- data/lib/relaton/core/data_fetcher.rb +23 -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/iana/data_fetcher.rb +53 -4
- data/lib/relaton/iec/data_fetcher.rb +3 -2
- data/lib/relaton/iec/model/docidentifier.rb +0 -4
- data/lib/relaton/ieee/CLAUDE.md +30 -4
- data/lib/relaton/ieee/bibliography.rb +87 -32
- data/lib/relaton/ieee/data_fetcher.rb +1 -1
- data/lib/relaton/ieee/docidentifier.rb +37 -0
- data/lib/relaton/ieee/idams_parser.rb +2 -2
- data/lib/relaton/ieee/item.rb +3 -0
- data/lib/relaton/ieee/item_data.rb +7 -1
- 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/jis/hit_collection.rb +1 -2
- data/lib/relaton/jis.rb +1 -0
- 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/hit_collection.rb +1 -1
- data/lib/relaton/nist/pubs_export.rb +2 -1
- data/lib/relaton/nist.rb +1 -0
- 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/plateau/hit_collection.rb +1 -1
- data/lib/relaton/plateau.rb +1 -0
- data/lib/relaton/un/hit_collection.rb +4 -3
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton.rb +1 -0
- metadata +40 -24
|
@@ -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/jis.rb
CHANGED
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
|
|
@@ -356,7 +356,7 @@ module Relaton
|
|
|
356
356
|
end
|
|
357
357
|
|
|
358
358
|
index = Relaton::Index.find_or_create(
|
|
359
|
-
:nist,
|
|
359
|
+
:nist, pages_url: PAGES_URL,
|
|
360
360
|
pubid_class: ::Pubid::Nist::Identifier
|
|
361
361
|
)
|
|
362
362
|
# search(r) narrows candidates by number via binary search; the block
|
|
@@ -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
|
data/lib/relaton/nist.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Relaton
|
|
|
22
22
|
# (for older gem versions) is rebuilt separately by the relaton-data-nist
|
|
23
23
|
# crawler, not by this gem's DataFetcher.
|
|
24
24
|
INDEXFILE = "index-v2"
|
|
25
|
+
PAGES_URL = "https://relaton.github.io/relaton-data-nist/"
|
|
25
26
|
|
|
26
27
|
class Error < StandardError; end
|
|
27
28
|
|
|
@@ -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}"
|