relaton 3.0.0.pre.alpha.16 → 3.0.0.pre.alpha.18
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 +38 -0
- data/lib/relaton/3gpp/CLAUDE.md +187 -0
- data/lib/relaton/3gpp/README.adoc +149 -0
- data/lib/relaton/adobe/CLAUDE.md +104 -0
- data/lib/relaton/bib/CLAUDE.md +130 -0
- data/lib/relaton/bib/README.adoc +330 -0
- data/lib/relaton/bib/model/series.rb +1 -1
- data/lib/relaton/bipm/CLAUDE.md +173 -0
- data/lib/relaton/bipm/README.adoc +352 -0
- data/lib/relaton/bipm/data_fetcher.rb +3 -2
- data/lib/relaton/bsi/CLAUDE.md +35 -0
- data/lib/relaton/bsi/README.adoc +112 -0
- data/lib/relaton/calconnect/CLAUDE.md +177 -0
- data/lib/relaton/calconnect/README.adoc +140 -0
- data/lib/relaton/ccsds/CLAUDE.md +74 -0
- data/lib/relaton/ccsds/README.adoc +154 -0
- data/lib/relaton/cen/CLAUDE.md +219 -0
- data/lib/relaton/cen/README.adoc +155 -0
- data/lib/relaton/cie/CLAUDE.md +90 -0
- data/lib/relaton/cie/README.adoc +121 -0
- data/lib/relaton/core/CLAUDE.md +140 -0
- data/lib/relaton/core/README.adoc +64 -0
- data/lib/relaton/core/data_fetcher.rb +50 -3
- data/lib/relaton/db/CLAUDE.md +373 -0
- data/lib/relaton/db.rb +19 -1
- data/lib/relaton/doi/CLAUDE.md +105 -0
- data/lib/relaton/doi/README.adoc +89 -0
- data/lib/relaton/easc/CLAUDE.md +85 -0
- data/lib/relaton/ecma/CLAUDE.md +217 -0
- data/lib/relaton/ecma/README.adoc +178 -0
- data/lib/relaton/equivalence.rb +89 -0
- data/lib/relaton/etsi/CLAUDE.md +219 -0
- data/lib/relaton/etsi/README.adoc +137 -0
- data/lib/relaton/evs/CLAUDE.md +120 -0
- data/lib/relaton/evs/README.adoc +27 -0
- data/lib/relaton/gb/CLAUDE.md +47 -0
- data/lib/relaton/gb/README.adoc +169 -0
- data/lib/relaton/gost/CLAUDE.md +100 -0
- data/lib/relaton/iala/CLAUDE.md +97 -0
- data/lib/relaton/iana/CLAUDE.md +105 -0
- data/lib/relaton/iana/README.adoc +107 -0
- data/lib/relaton/iec/CLAUDE.md +13 -0
- data/lib/relaton/iec/README.adoc +280 -0
- data/lib/relaton/iec/data_fetcher.rb +24 -8
- data/lib/relaton/iec/project_fetcher.rb +48 -0
- data/lib/relaton/iec/project_parser.rb +111 -0
- data/lib/relaton/iec.rb +5 -0
- data/lib/relaton/ieee/CLAUDE.md +176 -0
- data/lib/relaton/ieee/README.adoc +131 -0
- data/lib/relaton/ietf/CLAUDE.md +357 -0
- data/lib/relaton/ietf/README.adoc +167 -0
- data/lib/relaton/iho/CLAUDE.md +70 -0
- data/lib/relaton/iho/README.adoc +119 -0
- data/lib/relaton/index/CLAUDE.md +207 -0
- data/lib/relaton/index/README.adoc +221 -0
- data/lib/relaton/index/dataset_lock.rb +122 -0
- data/lib/relaton/index/family.rb +63 -0
- data/lib/relaton/index/type.rb +32 -0
- data/lib/relaton/index.rb +3 -0
- data/lib/relaton/isbn/CLAUDE.md +47 -0
- data/lib/relaton/isbn/README.adoc +91 -0
- data/lib/relaton/iso/CLAUDE.md +38 -0
- data/lib/relaton/iso/README.adoc +370 -0
- data/lib/relaton/iso/data_fetcher.rb +16 -1
- data/lib/relaton/itu/CLAUDE.md +685 -0
- data/lib/relaton/itu/README.adoc +188 -0
- data/lib/relaton/jcgm/CLAUDE.md +107 -0
- data/lib/relaton/jis/CLAUDE.md +64 -0
- data/lib/relaton/jis/README.adoc +167 -0
- data/lib/relaton/logger/CLAUDE.md +37 -0
- data/lib/relaton/logger/README.adoc +120 -0
- data/lib/relaton/nist/CLAUDE.md +180 -0
- data/lib/relaton/nist/README.adoc +389 -0
- data/lib/relaton/nist/bibliography.rb +17 -1
- data/lib/relaton/nist/data_fetcher.rb +60 -4
- data/lib/relaton/nist/hit_collection.rb +121 -59
- data/lib/relaton/nist/scraper.rb +21 -7
- data/lib/relaton/oasis/CLAUDE.md +251 -0
- data/lib/relaton/oasis/README.adoc +132 -0
- data/lib/relaton/ogc/CLAUDE.md +242 -0
- data/lib/relaton/ogc/README.adoc +134 -0
- data/lib/relaton/oiml/CLAUDE.md +47 -0
- data/lib/relaton/oiml/README.adoc +70 -0
- data/lib/relaton/omg/CLAUDE.md +137 -0
- data/lib/relaton/omg/README.adoc +101 -0
- data/lib/relaton/plateau/CLAUDE.md +126 -0
- data/lib/relaton/plateau/README.adoc +139 -0
- data/lib/relaton/sdo/CLAUDE.md +81 -0
- data/lib/relaton/un/CLAUDE.md +91 -0
- data/lib/relaton/un/README.adoc +125 -0
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton/w3c/CLAUDE.md +369 -0
- data/lib/relaton/w3c/README.adoc +140 -0
- data/lib/relaton/w3c/data_fetcher.rb +1 -1
- data/lib/relaton/xsf/CLAUDE.md +189 -0
- data/lib/relaton/xsf/README.adoc +127 -0
- data/lib/relaton.rb +1 -0
- metadata +83 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
relaton-core is the foundation gem every flavor plugin builds on. It provides the abstract **`Relaton::Core::Processor`** base class (the registry plug-in interface), the search primitives (`HitCollection`, `Hit`), a parallel-fetch thread pool (`WorkersPool`), the crawl-politeness pair `Pacer` (shared request pacing) and `Governor` (pool-wide rate-limit back-pressure), the `DataFetcher` base for bulk ingest, and small parsing/utility mixins. It has no knowledge of any specific standards body.
|
|
8
|
+
|
|
9
|
+
## Development
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle exec rake # default task → rspec
|
|
13
|
+
bundle exec rspec spec/core/processor_spec.rb # single file
|
|
14
|
+
bundle exec rubocop # lint (rubocop-performance/-rake/-rspec)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
Namespace: `Relaton::Core`. Key classes in `lib/relaton/core/`:
|
|
20
|
+
|
|
21
|
+
- **`Processor`** (`processor.rb`) — abstract base with the template methods every flavor implements: `get`, `fetch_data`, `from_xml`, `from_yaml`, `grammar_hash`, `threads`. Flavor processors subclass it and set `@short`/`@prefix`/`@defaultprefix`/`@datasets` in `initialize`.
|
|
22
|
+
- **`HitCollection`** (`hit_collection.rb`) — holds search results, delegates to an internal array, and uses `WorkersPool` to fetch items in parallel (4 workers by default). Uses `WeakRef` to avoid a circular reference back to its hits.
|
|
23
|
+
- **`Hit`** (`hit.rb`) — wraps one search result; its `item` is lazy-loaded on first access.
|
|
24
|
+
- **`DataFetcher`** (`data_fetcher.rb`) — abstract base for flavor bulk fetchers; provides `gh_issue` reporting (via `Relaton::Logger::Channels::GhIssue`), `output_file`, `unique_output_file`, `write_unique`, and `serialize` helpers.
|
|
25
|
+
|
|
26
|
+
**Filename collisions.** `output_file` sanitizes `.`, `,`, `/`, `:`, `(`, `)`,
|
|
27
|
+
`-` and whitespace all to `-`, so two **distinct** docids can map to one path
|
|
28
|
+
— the live instance is relaton-data-iana's `rpki/signed-objects` and
|
|
29
|
+
`rpki-signed-objects`, which both give `data/rpki-signed-objects.yaml`. A
|
|
30
|
+
writer that merely warns and writes anyway leaves one file holding the wrong
|
|
31
|
+
document for one of two index ids: a wrong answer, not a missing one.
|
|
32
|
+
|
|
33
|
+
**`unique_output_file(docid)` is the write-path entry point.** It reserves and
|
|
34
|
+
returns `output_file(docid)` when that path is free *or already belongs to the
|
|
35
|
+
same docid*, and only a real clash with a **different** docid gets a variant
|
|
36
|
+
suffixed with a digest of that docid. Two consequences worth keeping:
|
|
37
|
+
|
|
38
|
+
- It is a **no-op for genuine duplicates**, so each flavor's own duplicate
|
|
39
|
+
handling (skip / merge / last-wins) still fires exactly as before. That is
|
|
40
|
+
what makes it safe to drop into an existing writer.
|
|
41
|
+
- The suffix is keyed on the docid, not on encounter order, so adding a record
|
|
42
|
+
never renames an existing file. (Which member of a clashing pair keeps the
|
|
43
|
+
plain path does follow write order, which is stable for a given corpus.)
|
|
44
|
+
|
|
45
|
+
**The genuine-duplicate check must come FIRST in an adopting writer.** Once a
|
|
46
|
+
docid has been disambiguated, `unique_output_file` returns that suffixed path
|
|
47
|
+
for it *forever*, so `file != output_file(docid)` stays true on every repeat.
|
|
48
|
+
Gating the flavor's duplicate handling on that comparison makes it unreachable
|
|
49
|
+
for a repeat, which silently drops a merge (3gpp) or a skip (ecma, jis). Use
|
|
50
|
+
`@files.include?(file)` as the duplicate test — a reserved path only ever
|
|
51
|
+
belongs to one docid — and use the `!= output_file` comparison only to decide
|
|
52
|
+
what to log:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
file = unique_output_file docid
|
|
56
|
+
if @files.include? file
|
|
57
|
+
<genuine duplicate: merge / skip / warn>
|
|
58
|
+
else
|
|
59
|
+
Util.warn "... writing #{file} instead" if file != output_file(docid)
|
|
60
|
+
@files << file
|
|
61
|
+
<write + index>
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**`write_unique(docid, content)` closes the cross-PROCESS gap.**
|
|
66
|
+
`unique_output_file` reserves in one process's `@file_docids`, which a forked
|
|
67
|
+
worker cannot see. `write_unique` reserves, then creates the file with
|
|
68
|
+
`O_EXCL` (`EXCLUSIVE = File::WRONLY | File::CREAT | File::EXCL`), and returns
|
|
69
|
+
the path it actually wrote. Four outcomes:
|
|
70
|
+
|
|
71
|
+
| situation | what happens |
|
|
72
|
+
|---|---|
|
|
73
|
+
| we already wrote this path for this docid | plain overwrite — a genuine duplicate stays one file |
|
|
74
|
+
| `O_EXCL` succeeds | the normal case |
|
|
75
|
+
| `EEXIST`, single process | `unique_output_file` already proved no peer of ours holds it, so it is a leftover from an earlier crawl: overwrite |
|
|
76
|
+
| `EEXIST`, `@cross_process` | a peer's file and a leftover are indistinguishable: take the digest path instead |
|
|
77
|
+
|
|
78
|
+
`@cross_process` defaults to false, so every single-process flavor takes the
|
|
79
|
+
third row and behaves exactly as it did before adopting this. A fetcher that
|
|
80
|
+
forks sets it **before** the first fork, so children inherit it — see
|
|
81
|
+
`Relaton::Ietf::DataFetcher#fetch_ieft_internet_drafts`.
|
|
82
|
+
|
|
83
|
+
A forked worker only learns that a path was taken, never *which* clashing
|
|
84
|
+
docid deserves it, so the winner would follow the race and the two filenames
|
|
85
|
+
would swap between crawls. The parent settles that afterwards, once it can see
|
|
86
|
+
every docid: `Relaton::Ietf::DataFetcher#reconcile_output_files`.
|
|
87
|
+
|
|
88
|
+
**Deliberately not a wall-clock staleness test.** `File.mtime(file) <
|
|
89
|
+
run_started_at` looks like the obvious way to tell a leftover from a peer, and
|
|
90
|
+
it is a trap: crawls run into a populated `data/`, so `EEXIST` is the *common*
|
|
91
|
+
case on a re-run and the test would decide it for every record. Its
|
|
92
|
+
false-"stale" direction is the silent overwrite this method exists to prevent,
|
|
93
|
+
and coarse filesystem mtime granularity makes that reachable. Gating on
|
|
94
|
+
`@cross_process` removes the failure class instead of tuning it.
|
|
95
|
+
|
|
96
|
+
**Do not drop the unconditional `delete_suffix("-")` in `output_file`.** It is
|
|
97
|
+
the root cause of the trailing-punctuation half of these collisions, and
|
|
98
|
+
removing it would give the clashing ids distinct names — but it renames any id
|
|
99
|
+
ending in a character the gsub maps to `-`. Measured over this repo's index
|
|
100
|
+
fixtures (2026-08-26):
|
|
101
|
+
|
|
102
|
+
| corpus | rows | filenames that would change |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `spec/itu/fixtures/index-v2.zip` | 5,361 | **808** — ids ending `:`, e.g. `ITU-R 1-3/8:` |
|
|
105
|
+
| `spec/nist/fixtures/index-v2.zip` | 19,515 | 2 — `NBS TN 467pt1 Add.` |
|
|
106
|
+
| `spec/ietf/fixtures/ietf-index-v2.zip` | 177,362 | 3 |
|
|
107
|
+
| `spec/iso` + `spec/iec` v2 | 112,189 | 0 |
|
|
108
|
+
| every v1 string-id fixture | 97,966 | 7 — IEEE `IEEE 802.3-`, OGC `20-001r2 ` |
|
|
109
|
+
|
|
110
|
+
A 15% rename of the published ITU corpus does not pay for collisions that
|
|
111
|
+
`unique_output_file` and `write_unique` already close.
|
|
112
|
+
|
|
113
|
+
**Audit of every `output_file` caller** (keep this exhaustive — it is the only
|
|
114
|
+
place the decision is recorded):
|
|
115
|
+
|
|
116
|
+
| Adopted | Abstains, and why |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `iana`, `oasis`, `calconnect`, `xsf`, `nist`, `ecma`, `jis`, `3gpp`, `iec`, `etsi`; `ietf` via `write_unique` | `ieee` — `reconcile_staged_outputs` recomputes `output_file(docnumber)` in a later pass with no reservation state, so a disambiguated path breaks the staged rename |
|
|
119
|
+
| | `itu`, `cie` — documented positional `@seen`/`pos` dedup under a parallel crawl |
|
|
120
|
+
| | `iso` — `File.exist?` → `rewrite_with_same_or_newer` version compare |
|
|
121
|
+
| | `ccsds` — `merge_links` merges into the existing file by design |
|
|
122
|
+
|
|
123
|
+
Note `etsi` had **no** collision handling at all before this (silent overwrite,
|
|
124
|
+
not even a warning), and `iec` had the warn-then-overwrite-anyway shape.
|
|
125
|
+
- **`WorkersPool`** (`workers_pool.rb`) — `SizedQueue`-backed thread pool for parallel work. **Runtime search only** — `HitCollection` uses it. No fetcher does: it carries no per-worker resources (a crawler needs one Mechanize agent per thread) and collects into an unsynchronised array. A crawler that needs a pool builds its own; see `Relaton::Itu::DataCrawlerR#in_parallel` and `Relaton::Cie::DataFetcher#process_hits`.
|
|
126
|
+
- **`Pacer`** (`pacer.rb`) — process-wide, thread-safe request pacer: the whole pool between them starts at most one request per `gap` seconds. **Shared, not per-worker** — that is the point, and the contrast with `Relaton::Cie::DataFetcher::Pacing`, whose gap is per worker (N workers there issue N requests per gap, which is right for CIE and wrong when the budget belongs to one host). A reservation never starts before "now", so an idle stretch cannot be repaid as a burst, and the mutex is never held across the sleep. `mode: :fixed` reproduces a plain `sleep delay` loop as a rollback knob. `clock:`/`sleeper:` are injectable, so the arithmetic is specced without wall-clock waits.
|
|
127
|
+
- **`Governor`** (`governor.rb`) — process-wide, thread-safe rate-limit back-pressure: one shared cooldown the whole pool observes, escalating per *round* (60 s → 900 s) rather than per worker, decaying on success, and latching a give-up after five barren rounds so a banned crawl fails fast instead of grinding out a CI job's remaining hours. Two round counters, deliberately: **`#throttle_rounds`** is the LIVE count since the last success (ITU logs it per retry as the current round) and `#succeeded!` resets it, while **`#give_up_rounds`** is the count snapshotted at the instant `@abandoned` latched, and is what an abort message must report — reading the live one after the latch names whatever re-accumulated since, which is how a W3C give-up at round 5 got logged as *"after 2 consecutive backoffs"*. Note also that `#wait` returning 0.0 once latched stops the pool *waiting*, not *asking*: a flavor must guard its own request sites (see `Relaton::W3c::SafeRealize#realize`) or a banned crawl keeps hammering the host until it winds down. Promoted here from `Relaton::W3c`. A flavor subclasses it and supplies two things: `THROTTLE_ERRORS` (or `.throttle?`, when the signal is a status code rather than an exception class, as for ITU) and `ENV_PREFIX`, which namespaces `<PREFIX>_THROTTLE_BASE/_MAX/_GIVEUP` so two flavors crawling in one process cannot share a ladder. Bindings: `Relaton::W3c::Governor`, `Relaton::Itu::Governor`.
|
|
128
|
+
- **Mixins** — `DateParser` (`parse_date` for "February 2012"/"2012-02-03"/etc.), `ArrayWrapper` (`array(x)` → always an Array), `HashKeysSymbolizer` (recursive string→symbol keys).
|
|
129
|
+
|
|
130
|
+
### How flavors consume it (important)
|
|
131
|
+
|
|
132
|
+
Every flavor's `Relaton::<Flavor>::Processor < Relaton::Core::Processor`. Because the umbrella registry now loads flavors lazily (only their `…/processor` file), any processor method that touches a flavor constant must `require_relative "../<flavor>"` first — see the root `CLAUDE.md` "Registry is lazy" note.
|
|
133
|
+
|
|
134
|
+
## External dependencies
|
|
135
|
+
|
|
136
|
+
`nokogiri ~> 1.16`, `psych ~> 5.2.0`, `relaton-logger ~> 2.2.0.pre.alpha.1`.
|
|
137
|
+
|
|
138
|
+
## Testing
|
|
139
|
+
|
|
140
|
+
RSpec with SimpleCov. Specs live under `spec/core/` and mirror the lib classes; `spec/support/` defines dummy processor/types for exercising the base classes.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
= Library for importing and caching bibliographic references to technical standards library
|
|
2
|
+
|
|
3
|
+
== Usage
|
|
4
|
+
|
|
5
|
+
=== Match a partial reference against a document identifier
|
|
6
|
+
|
|
7
|
+
`#===` is a subset match: the reference is the receiver, the candidate is the
|
|
8
|
+
argument. A component the reference leaves out matches any value. The match is
|
|
9
|
+
NOT symmetric.
|
|
10
|
+
|
|
11
|
+
[source,ruby]
|
|
12
|
+
----
|
|
13
|
+
require "relaton-core"
|
|
14
|
+
|
|
15
|
+
pubid_first = Identifier.parse("ISO 1:1999")
|
|
16
|
+
pubid_second = Identifier.parse("ISO 1")
|
|
17
|
+
|
|
18
|
+
pubid_first == pubid_second
|
|
19
|
+
=> false
|
|
20
|
+
|
|
21
|
+
pubid_second === pubid_first
|
|
22
|
+
=> true
|
|
23
|
+
|
|
24
|
+
pubid_first === pubid_second
|
|
25
|
+
=> false
|
|
26
|
+
----
|
|
27
|
+
|
|
28
|
+
=== Exclude attributes when compare
|
|
29
|
+
|
|
30
|
+
Use `#exclude` to build a copy without a component, for example to render a
|
|
31
|
+
reference without its year. For matching, prefer `#===`.
|
|
32
|
+
|
|
33
|
+
[source,ruby]
|
|
34
|
+
----
|
|
35
|
+
pubid_first.exclude(:year) == pubid_second
|
|
36
|
+
=> true
|
|
37
|
+
----
|
|
38
|
+
|
|
39
|
+
=== Using #to_h to convert identifier to hash
|
|
40
|
+
|
|
41
|
+
[source,ruby]
|
|
42
|
+
----
|
|
43
|
+
require "pubid-core"
|
|
44
|
+
|
|
45
|
+
pubid = Identifier.parse("ISO 1:1999")
|
|
46
|
+
pubid.to_h
|
|
47
|
+
=> { publisher: "ISO", number: 1, year: 1999 }
|
|
48
|
+
----
|
|
49
|
+
|
|
50
|
+
=== Using #new_edition_of? to compare identifiers
|
|
51
|
+
|
|
52
|
+
[source,ruby]
|
|
53
|
+
----
|
|
54
|
+
require "pubid-core"
|
|
55
|
+
|
|
56
|
+
pubid_first = Identifier.parse("ISO 1:1999")
|
|
57
|
+
pubid_second = Identifier.parse("ISO 1:2000")
|
|
58
|
+
|
|
59
|
+
pubid_first.new_edition_of?(pubid_second)
|
|
60
|
+
=> false
|
|
61
|
+
pubid_second.new_edition_of?(pubid_first)
|
|
62
|
+
=> true
|
|
63
|
+
|
|
64
|
+
----
|
|
@@ -25,19 +25,34 @@ module Relaton
|
|
|
25
25
|
# Set true by a fetcher that writes from forked worker processes,
|
|
26
26
|
# where @file_docids cannot see a peer's claim. See #write_unique.
|
|
27
27
|
@cross_process = false
|
|
28
|
+
# True when the caller asked this run to keep the existing output tree
|
|
29
|
+
# and prune it afterwards (see ::fetch's prune:). A flavor that wipes
|
|
30
|
+
# its output at the start of #fetch must skip the wipe in this mode.
|
|
31
|
+
@prune = false
|
|
28
32
|
# @docs = []
|
|
29
33
|
@errors = Hash.new(true)
|
|
30
34
|
end
|
|
31
35
|
|
|
36
|
+
# @return [Boolean] whether this run prunes the existing output tree
|
|
37
|
+
attr_accessor :prune
|
|
38
|
+
|
|
32
39
|
# API method for external service
|
|
33
40
|
#
|
|
41
|
+
# @param prune [Boolean] delete output files this run did not write. The
|
|
42
|
+
# flavor must write its whole corpus on every run; #prune_unwritten's
|
|
43
|
+
# ratio guard stops a partial crawl from wiping the dataset.
|
|
34
44
|
# @return the value returned by the instance `#fetch`, so callers can act
|
|
35
45
|
# on the outcome (e.g. relaton-iso returns whether it rebuilt).
|
|
36
|
-
def self.fetch(source = nil, output: "data", format: "yaml")
|
|
46
|
+
def self.fetch(source = nil, output: "data", format: "yaml", prune: false)
|
|
37
47
|
t1 = Time.now
|
|
38
48
|
puts "Started at: #{t1}"
|
|
39
49
|
FileUtils.mkdir_p output
|
|
40
|
-
|
|
50
|
+
fetcher = new(output, format)
|
|
51
|
+
# Only touched in prune mode: the default path stays exactly what it
|
|
52
|
+
# was, and existing ::fetch callers (and their specs) see no new call.
|
|
53
|
+
fetcher.prune = true if prune
|
|
54
|
+
result = fetcher.fetch(source)
|
|
55
|
+
fetcher.prune_unwritten if prune
|
|
41
56
|
t2 = Time.now
|
|
42
57
|
puts "Stopped at: #{t2}"
|
|
43
58
|
puts "Done in: #{(t2 - t1).round} sec."
|
|
@@ -187,6 +202,30 @@ module Relaton
|
|
|
187
202
|
force_write reserve(digest_output_file(docid), docid), content
|
|
188
203
|
end
|
|
189
204
|
|
|
205
|
+
# Delete output files this run did not write — the replacement for a
|
|
206
|
+
# crawler's `rm -rf data` when the data tree is kept between runs. A
|
|
207
|
+
# partial crawl must never wipe the dataset, so when the run wrote less
|
|
208
|
+
# than +min_ratio+ of the existing corpus nothing is deleted.
|
|
209
|
+
#
|
|
210
|
+
# @param min_ratio [Float]
|
|
211
|
+
# @return [Array<String>] deleted paths
|
|
212
|
+
def prune_unwritten(min_ratio: 0.9)
|
|
213
|
+
existing = Dir.glob(File.join(@output, "**", "*.#{@ext}"))
|
|
214
|
+
return [] if existing.empty?
|
|
215
|
+
|
|
216
|
+
written = existing.count { |file| @written.include?(file) }
|
|
217
|
+
if written < existing.size * min_ratio
|
|
218
|
+
warn "prune skipped: #{written}/#{existing.size} written " \
|
|
219
|
+
"(threshold #{(existing.size * min_ratio).ceil})"
|
|
220
|
+
return []
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
stale = existing - @written.to_a
|
|
224
|
+
stale.each { |file| File.delete(file) }
|
|
225
|
+
warn "pruned #{stale.size} stale file(s)" unless stale.empty?
|
|
226
|
+
stale
|
|
227
|
+
end
|
|
228
|
+
|
|
190
229
|
#
|
|
191
230
|
# Serialize bibliographic item
|
|
192
231
|
#
|
|
@@ -213,8 +252,16 @@ module Relaton
|
|
|
213
252
|
private
|
|
214
253
|
|
|
215
254
|
# Write, clobbering whatever is there. The caller has already established
|
|
216
|
-
# that the path is ours to take.
|
|
255
|
+
# that the path is ours to take. Identical content is left untouched:
|
|
256
|
+
# an incremental crawl (one that keeps the data tree) must not churn
|
|
257
|
+
# mtimes or rewrite bytes for a record that did not change. The file is
|
|
258
|
+
# still recorded as written, so #prune_unwritten keeps it.
|
|
217
259
|
def force_write(file, content)
|
|
260
|
+
if File.exist?(file) && File.read(file, encoding: "UTF-8") == content
|
|
261
|
+
@written << file
|
|
262
|
+
return file
|
|
263
|
+
end
|
|
264
|
+
|
|
218
265
|
File.write file, content, encoding: "UTF-8"
|
|
219
266
|
@written << file
|
|
220
267
|
file
|