relaton 3.0.0.pre.alpha.18 → 3.0.0.pre.alpha.19
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/data/equivalence/adoption-chains.yaml +21 -0
- data/lib/relaton/bib/model/item_shared.rb +5 -1
- data/lib/relaton/core/data_fetcher.rb +93 -1
- data/lib/relaton/db.rb +6 -4
- data/lib/relaton/equivalence.rb +28 -0
- data/lib/relaton/ogc/data_fetcher.rb +9 -3
- data/lib/relaton/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eee3114e443e307b96ca18a96dff2a48dcb4d5b5dae424fd2ecb3ca6e57202be
|
|
4
|
+
data.tar.gz: 7208a6c3f058fca1874370a24bfe89d03516d5c4d66477d8c4160c4517717c44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f8c13c3bbdbe96caa0874d0b0c4671c05fa4c6652070372b0fc55992ac3c87834a6e03bda4a7fe17945d31db33b5f1a5a99d927d8d1d203d8238a7e837c29d77
|
|
7
|
+
data.tar.gz: 3e5e49f9fdd82e5abe32bc4afb35b2baf075bc1b10aee9aad0e663f4361a0486069ff94cf3d4c066d17ccf4f32dacfac5eccad6eb1534008978d39734577dc84
|
|
@@ -27,6 +27,18 @@ chains:
|
|
|
27
27
|
- 'EN \1'
|
|
28
28
|
- '\1'
|
|
29
29
|
|
|
30
|
+
# Adoption ladders: the reverse edges. A code matching `pattern` is a base
|
|
31
|
+
# document; `forms` are its national/regional adoptions, ordered
|
|
32
|
+
# outermost-last, so the resolver can walk a chain from either end
|
|
33
|
+
# (TODO.sota: adoption chains resolvable end-to-end, bidirectional).
|
|
34
|
+
adoptions:
|
|
35
|
+
- pattern: '^(ISO[ /].+)$'
|
|
36
|
+
forms:
|
|
37
|
+
- 'EN \1'
|
|
38
|
+
- 'BS EN \1'
|
|
39
|
+
- 'DIN EN \1'
|
|
40
|
+
- 'NF EN \1'
|
|
41
|
+
|
|
30
42
|
# Explicit equivalences: every form in `equivalents` resolves to
|
|
31
43
|
# `canonical` (and the canonical resolves to itself). Used where no pattern
|
|
32
44
|
# expresses the relationship (joint publications, historical adoptions).
|
|
@@ -36,3 +48,12 @@ equivalences:
|
|
|
36
48
|
- EN ISO/IEC 17025
|
|
37
49
|
- BS EN ISO/IEC 17025
|
|
38
50
|
- DIN EN ISO/IEC 17025
|
|
51
|
+
# Joint publication: the same document under a different publisher
|
|
52
|
+
# ordering (Law 3.1) — the direction each corpus prints is the data.
|
|
53
|
+
- canonical: ISO/IEC 14515-1
|
|
54
|
+
equivalents:
|
|
55
|
+
- IEC/ISO 14515-1
|
|
56
|
+
# Variant spelling: the prefixed form of a bare reference (Law 3.3).
|
|
57
|
+
- canonical: RFC 7200
|
|
58
|
+
equivalents:
|
|
59
|
+
- IETF RFC 7200
|
|
@@ -23,7 +23,11 @@ module Relaton
|
|
|
23
23
|
attribute :docnumber, :string
|
|
24
24
|
attribute :date, Date, collection: true, initialize_empty: true
|
|
25
25
|
attribute :contributor, Contributor, collection: true, initialize_empty: true
|
|
26
|
-
|
|
26
|
+
# Lenient ingest: some upstream corpus documents carry more than one
|
|
27
|
+
# <edition> in a bibitem (multi-edition bibdata, relaton-db#149); keep
|
|
28
|
+
# the first instead of failing the whole round-trip with
|
|
29
|
+
# CollectionTrueMissingError (lutaml-model#971, singular_tolerance).
|
|
30
|
+
attribute :edition, Edition, singular_tolerance: :first
|
|
27
31
|
attribute :version, Version, collection: true, initialize_empty: true
|
|
28
32
|
attribute :note, Note, collection: true, initialize_empty: true
|
|
29
33
|
attribute :language, :string, collection: true, initialize_empty: true
|
|
@@ -36,6 +36,73 @@ module Relaton
|
|
|
36
36
|
# @return [Boolean] whether this run prunes the existing output tree
|
|
37
37
|
attr_accessor :prune
|
|
38
38
|
|
|
39
|
+
# @return [Boolean] whether this run consults and maintains the
|
|
40
|
+
# content-digest ledger (see #unchanged?). Off by default: a flavor
|
|
41
|
+
# turns it on only where its enumeration entry determines the record
|
|
42
|
+
# (the TODO.sota crawl-audit rule).
|
|
43
|
+
attr_accessor :digest_ledger
|
|
44
|
+
|
|
45
|
+
# -- Content-digest ledger -----------------------------------------
|
|
46
|
+
#
|
|
47
|
+
# A per-output sidecar (`.digests.yaml`: key -> digest + file) lets a
|
|
48
|
+
# run skip the fetch+parse+write of a record whose enumeration entry is
|
|
49
|
+
# unchanged, where the entry determines the record. The sidecar is
|
|
50
|
+
# tracked in the data repo so a fresh CI clone still skips. Skipped
|
|
51
|
+
# files are recorded as written, so #prune_unwritten keeps them; the
|
|
52
|
+
# saved ledger keeps only files this run owned, so deleted records
|
|
53
|
+
# drop out with their file.
|
|
54
|
+
#
|
|
55
|
+
# Sound only where the entry bounds the record: a per-item detail
|
|
56
|
+
# fetch whose content the entry does not determine, or relation output
|
|
57
|
+
# discovered across records, must stay outside the skip (TODO.sota,
|
|
58
|
+
# crawl audit 2026-10-23).
|
|
59
|
+
|
|
60
|
+
LEDGER_FILE = ".digests.yaml"
|
|
61
|
+
|
|
62
|
+
# True when the ledger holds `key` with the entry's digest - the
|
|
63
|
+
# record is unchanged and the caller may skip it. Records the
|
|
64
|
+
# ledger's file as written, so prune keeps a skipped record.
|
|
65
|
+
#
|
|
66
|
+
# @param key [String] the record's stable identity (its docid)
|
|
67
|
+
# @param entry [Object] the enumeration entry; digested as JSON
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def unchanged?(key, entry)
|
|
70
|
+
return false unless @digest_ledger
|
|
71
|
+
|
|
72
|
+
row = ledger[key]
|
|
73
|
+
return false unless row.is_a?(Hash) && row["digest"] == entry_digest(entry)
|
|
74
|
+
|
|
75
|
+
@written << row["file"] if row["file"]
|
|
76
|
+
warn "Skipping unchanged `#{key}`"
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Record the entry's digest for `key`, with the file the record was
|
|
81
|
+
# written to (nil when this run wrote no file for it - the entry
|
|
82
|
+
# drops out of the ledger at save time).
|
|
83
|
+
#
|
|
84
|
+
# @param key [String]
|
|
85
|
+
# @param entry [Object]
|
|
86
|
+
# @param file [String, nil]
|
|
87
|
+
# @return [void]
|
|
88
|
+
def note_digest(key, entry, file: nil)
|
|
89
|
+
return unless @digest_ledger
|
|
90
|
+
|
|
91
|
+
ledger_new[key] = { "digest" => entry_digest(entry), "file" => file }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Persist the ledger: this run's noted entries over the loaded ones,
|
|
95
|
+
# keeping only entries whose file this run recorded as written.
|
|
96
|
+
#
|
|
97
|
+
# @return [void]
|
|
98
|
+
def save_ledger
|
|
99
|
+
kept = ledger.merge(ledger_new).select do |_, row|
|
|
100
|
+
row.is_a?(Hash) && row["file"] && @written.include?(row["file"])
|
|
101
|
+
end
|
|
102
|
+
File.write ledger_path, YAML.dump(kept), encoding: "UTF-8"
|
|
103
|
+
@ledger = kept
|
|
104
|
+
end
|
|
105
|
+
|
|
39
106
|
# API method for external service
|
|
40
107
|
#
|
|
41
108
|
# @param prune [Boolean] delete output files this run did not write. The
|
|
@@ -43,7 +110,8 @@ module Relaton
|
|
|
43
110
|
# ratio guard stops a partial crawl from wiping the dataset.
|
|
44
111
|
# @return the value returned by the instance `#fetch`, so callers can act
|
|
45
112
|
# on the outcome (e.g. relaton-iso returns whether it rebuilt).
|
|
46
|
-
def self.fetch(source = nil, output: "data", format: "yaml", prune: false
|
|
113
|
+
def self.fetch(source = nil, output: "data", format: "yaml", prune: false,
|
|
114
|
+
digest_ledger: false)
|
|
47
115
|
t1 = Time.now
|
|
48
116
|
puts "Started at: #{t1}"
|
|
49
117
|
FileUtils.mkdir_p output
|
|
@@ -51,8 +119,10 @@ module Relaton
|
|
|
51
119
|
# Only touched in prune mode: the default path stays exactly what it
|
|
52
120
|
# was, and existing ::fetch callers (and their specs) see no new call.
|
|
53
121
|
fetcher.prune = true if prune
|
|
122
|
+
fetcher.digest_ledger = digest_ledger
|
|
54
123
|
result = fetcher.fetch(source)
|
|
55
124
|
fetcher.prune_unwritten if prune
|
|
125
|
+
fetcher.save_ledger if digest_ledger
|
|
56
126
|
t2 = Time.now
|
|
57
127
|
puts "Stopped at: #{t2}"
|
|
58
128
|
puts "Done in: #{(t2 - t1).round} sec."
|
|
@@ -251,6 +321,28 @@ module Relaton
|
|
|
251
321
|
|
|
252
322
|
private
|
|
253
323
|
|
|
324
|
+
def ledger_path
|
|
325
|
+
File.join @output, LEDGER_FILE
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def ledger
|
|
329
|
+
@ledger ||= if File.exist?(ledger_path)
|
|
330
|
+
YAML.safe_load_file(ledger_path) || {}
|
|
331
|
+
else
|
|
332
|
+
{}
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def ledger_new
|
|
337
|
+
@ledger_new ||= {}
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def entry_digest(entry)
|
|
341
|
+
require "digest"
|
|
342
|
+
require "json"
|
|
343
|
+
Digest::SHA256.hexdigest JSON.generate(entry)
|
|
344
|
+
end
|
|
345
|
+
|
|
254
346
|
# Write, clobbering whatever is there. The caller has already established
|
|
255
347
|
# that the path is ours to take. Identical content is left untouched:
|
|
256
348
|
# an incremental crawl (one that keeps the data tree) must not churn
|
data/lib/relaton/db.rb
CHANGED
|
@@ -158,13 +158,15 @@ module Relaton
|
|
|
158
158
|
end
|
|
159
159
|
|
|
160
160
|
# Cross-SDO equivalence as data: on a total miss, walk the equivalence
|
|
161
|
-
# tables from relaton/equivalence.rb — a national
|
|
162
|
-
# the EN, the EN to the ISO
|
|
163
|
-
#
|
|
161
|
+
# tables from relaton/equivalence.rb in both directions — a national
|
|
162
|
+
# adoption falls back to the EN, the EN to the ISO, and a base document
|
|
163
|
+
# falls forward to its declared adoptions (.related_for). Each candidate
|
|
164
|
+
# goes through the full routed fetch (cache, co-publishers); an
|
|
165
|
+
# unrouteable candidate is skipped.
|
|
164
166
|
#
|
|
165
167
|
# @return [Relaton::Bib::ItemData, nil]
|
|
166
168
|
def fetch_equivalent(code, year, opts)
|
|
167
|
-
Equivalence.
|
|
169
|
+
Equivalence.related_for(code).each do |candidate|
|
|
168
170
|
Util.info "Not found; trying equivalent `#{candidate}`", key: code
|
|
169
171
|
std, pubid = @registry.route(candidate)
|
|
170
172
|
bib = fetch_routed(candidate, year, opts, std, pubid) and return bib
|
data/lib/relaton/equivalence.rb
CHANGED
|
@@ -84,6 +84,34 @@ module Relaton
|
|
|
84
84
|
end
|
|
85
85
|
out
|
|
86
86
|
end
|
|
87
|
+
|
|
88
|
+
# Every edge of `code`'s equivalence set: the canonical walk
|
|
89
|
+
# (.equivalents_for — a national adoption falls back to the EN, the EN
|
|
90
|
+
# to the ISO) PLUS the reverse edges, generated from the tables'
|
|
91
|
+
# `adoptions` ladders (the ISO resolves to its EN, BS EN, DIN EN and
|
|
92
|
+
# NF EN adoptions). The code itself is excluded; the walk order is
|
|
93
|
+
# canonical-first, ladder outermost-last.
|
|
94
|
+
#
|
|
95
|
+
# @param [String] code
|
|
96
|
+
# @return [Array<String>]
|
|
97
|
+
def related_for(code)
|
|
98
|
+
out = equivalents_for(code)
|
|
99
|
+
load_seed
|
|
100
|
+
([code] + out.uniq).each do |candidate|
|
|
101
|
+
tables.each do |table|
|
|
102
|
+
(table["adoptions"] || []).each do |ladder|
|
|
103
|
+
m = Regexp.new(ladder["pattern"]).match(candidate)
|
|
104
|
+
next unless m
|
|
105
|
+
|
|
106
|
+
Array(ladder["forms"]).each do |template|
|
|
107
|
+
form = template.gsub(/\\(\d+)/) { m[Regexp.last_match(1).to_i] }
|
|
108
|
+
out << form unless form == code || out.include?(form)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
out
|
|
114
|
+
end
|
|
87
115
|
end
|
|
88
116
|
end
|
|
89
117
|
end
|
|
@@ -46,9 +46,13 @@ module Relaton
|
|
|
46
46
|
|
|
47
47
|
def fetch_doc(hit)
|
|
48
48
|
return if hit["type"] == "CC"
|
|
49
|
+
# The list entry carries the whole record payload, so an unchanged
|
|
50
|
+
# entry skips the parse and the write (the digest ledger).
|
|
51
|
+
return if unchanged?(hit["identifier"], hit)
|
|
49
52
|
|
|
50
53
|
bib = Scraper.parse_page hit, @errors
|
|
51
|
-
write_document bib
|
|
54
|
+
file = write_document bib
|
|
55
|
+
note_digest(hit["identifier"], hit, file: file) if file
|
|
52
56
|
true
|
|
53
57
|
rescue StandardError => e
|
|
54
58
|
Util.error "Fetching document: #{hit['identifier']}\n" \
|
|
@@ -56,17 +60,19 @@ module Relaton
|
|
|
56
60
|
false
|
|
57
61
|
end
|
|
58
62
|
|
|
63
|
+
# @return [String, nil] the file written, or nil for a duplicate
|
|
59
64
|
def write_document(bib) # rubocop:disable Metrics/AbcSize
|
|
60
65
|
docid = bib.docidentifier[0].content
|
|
61
66
|
if @docids.include?(docid)
|
|
62
67
|
@dupids << docid
|
|
63
|
-
return
|
|
68
|
+
return nil
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
@docids << docid
|
|
67
72
|
file = file_name bib
|
|
68
73
|
index.add_or_update index_id(docid), file
|
|
69
|
-
|
|
74
|
+
force_write file, serialize(bib)
|
|
75
|
+
file
|
|
70
76
|
end
|
|
71
77
|
|
|
72
78
|
#
|
data/lib/relaton/version.rb
CHANGED
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.0.pre.alpha.
|
|
4
|
+
version: 3.0.0.pre.alpha.19
|
|
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-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: addressable
|
|
@@ -442,14 +442,14 @@ dependencies:
|
|
|
442
442
|
requirements:
|
|
443
443
|
- - "~>"
|
|
444
444
|
- !ruby/object:Gem::Version
|
|
445
|
-
version: 2.0.0.pre.alpha.
|
|
445
|
+
version: 2.0.0.pre.alpha.59
|
|
446
446
|
type: :runtime
|
|
447
447
|
prerelease: false
|
|
448
448
|
version_requirements: !ruby/object:Gem::Requirement
|
|
449
449
|
requirements:
|
|
450
450
|
- - "~>"
|
|
451
451
|
- !ruby/object:Gem::Version
|
|
452
|
-
version: 2.0.0.pre.alpha.
|
|
452
|
+
version: 2.0.0.pre.alpha.59
|
|
453
453
|
- !ruby/object:Gem::Dependency
|
|
454
454
|
name: yeptris
|
|
455
455
|
requirement: !ruby/object:Gem::Requirement
|