relaton 3.0.0.pre.alpha.17 → 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 +59 -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/item_shared.rb +5 -1
- 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 +142 -3
- data/lib/relaton/db/CLAUDE.md +373 -0
- data/lib/relaton/db.rb +21 -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 +117 -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/ogc/data_fetcher.rb +9 -3
- 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 +85 -4
|
@@ -0,0 +1,219 @@
|
|
|
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-cen retrieves European Committee for Standardization (CEN/CENELEC) bibliographic data using the Relaton model. It has no API or curated index — it **scrapes** the CEN/CENELEC public portal (`standards.cencenelec.eu`) with Mechanize, simulating the search form and parsing HTML tables for title, status, ICS codes, publication dates, and committee info. Uses `Relaton::Bib::ItemData` directly (no relaton-iso dependency).
|
|
8
|
+
|
|
9
|
+
## Development
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle exec rake spec:cen # the CEN suite
|
|
13
|
+
cd spec/cen && bundle exec rspec -I . relaton/cen/docidentifier_spec.rb
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
(No gem-local rubocop config; the monorepo root config applies.)
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
Namespace: `Relaton::Cen`. Reference handling is **entirely `Pubid::CenCenelec`**
|
|
21
|
+
— there is no hand-rolled identifier regex left. Because CEN publishes **no
|
|
22
|
+
index** and has no data repo, this is the **BSI shape**, not the index-v2 shape:
|
|
23
|
+
parse the query with pubid, select the portal hits with pubid, back the
|
|
24
|
+
`Docidentifier` with pubid. There is no `INDEXFILE` and no `pubid_class:`.
|
|
25
|
+
**Don't add an index.**
|
|
26
|
+
|
|
27
|
+
Retrieval flow:
|
|
28
|
+
|
|
29
|
+
1. **Bibliography** (`lib/relaton/cen/bibliography.rb`) — `get(code, year, opts)`
|
|
30
|
+
and `search` entry points. `parse` wraps
|
|
31
|
+
`Pubid::CenCenelec::Identifier.parse` and **raises** on an unrecognized query
|
|
32
|
+
(the root `CLAUDE.md` rule "An unrecognized query reference raises"); only the
|
|
33
|
+
data side rescues, in `Hit#pubid`. `get` keeps an early `nil` for an **empty**
|
|
34
|
+
reference — an empty string is no reference, not a malformed one — and
|
|
35
|
+
otherwise lets `Pubid::Errors::ParseError` propagate. `search` keeps its
|
|
36
|
+
`rescue Mechanize::ResponseCodeError, Net::ReadTimeout` by name; don't widen
|
|
37
|
+
it to `StandardError`, which would swallow the parse error and turn a
|
|
38
|
+
transport failure into a class `Relaton::Db#net_retry` does not retry.
|
|
39
|
+
2. **HitCollection** (`lib/relaton/cen/hit_collection.rb`, extends
|
|
40
|
+
`Relaton::Core::HitCollection`) — the web flow: GET the portal, follow the
|
|
41
|
+
redirect, submit the search form (`STAND_REF`) via Mechanize, parse the result
|
|
42
|
+
table, then `sort` by a pubid-derived key.
|
|
43
|
+
3. **Hit** (`lib/relaton/cen/hit.rb`, extends `Relaton::Core::Hit`) — `item`
|
|
44
|
+
lazy-loads the detail page via the scraper; `pubid` memoizes the hit code
|
|
45
|
+
parsed with pubid and is **nil** when the grammar cannot read it, because the
|
|
46
|
+
portal lists draft revisions such as `prEN 13306 rev` and one such row must
|
|
47
|
+
not abort the search. The filter, the sort and the scraper all read it, which
|
|
48
|
+
is why it is memoized (the `Relaton::Bsi::Hit#pubid` idiom).
|
|
49
|
+
4. **Scraper** (`lib/relaton/cen/scraper.rb`) — Mechanize/XPath HTML parser.
|
|
50
|
+
Maps CEN date abbreviations (DOR→adapted, DAV→issued, DOA→announced,
|
|
51
|
+
DOP→published, DOW→obsoleted) and relation headers (supersedes→obsoletes,
|
|
52
|
+
normative reference→cites); a small `COMMITTEES` map resolves codes like
|
|
53
|
+
TC 459→ECISS. `fetch_structuredid` takes the docnumber and partnumber from
|
|
54
|
+
the pubid. Two regexes remain here and are **not** identifiers, so they stay:
|
|
55
|
+
the committee regex in `fetch_contributors` and the year-of-a-date-string
|
|
56
|
+
match in `fetch_copyright`.
|
|
57
|
+
5. **ItemData / Model::Item** (`item_data.rb`, `model/`) —
|
|
58
|
+
`Relaton::Cen::ItemData` extends `Relaton::Bib::ItemData`; `Ext` adds a
|
|
59
|
+
`StructuredIdentifier` (docnumber/partnumber, agency "CEN").
|
|
60
|
+
6. **Processor** (`lib/relaton/cen/processor.rb`) — registry integration. Sets
|
|
61
|
+
`@pubid_flavor = :CenCenelec`, so `Core::Processor#prefixes` reads
|
|
62
|
+
`Pubid::CenCenelec.prefixes` (`EN CEN CLC CWA HD ES CR ENV`) into the global
|
|
63
|
+
prefix register; every one of the eight resolves to `[Relaton::Cen]` alone.
|
|
64
|
+
`@defaultprefix` is deliberately **narrower** than that list — widening it to
|
|
65
|
+
`ES` would claim a very generic token for this flavor.
|
|
66
|
+
It lazy-`require_relative`s `../cen` inside each method, like every other
|
|
67
|
+
flavor: the require used to sit at file load, so merely building a `Db`
|
|
68
|
+
pulled in mechanize, isoics and pubid. `spec/relaton/lazy_loading_spec.rb`
|
|
69
|
+
guards that.
|
|
70
|
+
|
|
71
|
+
### `#root` is the accessor that answers for every form
|
|
72
|
+
|
|
73
|
+
An `AdoptedEuropeanNorm` (`CEN ISO/TS 21003-7:2019`) carries **no** number, part
|
|
74
|
+
or year of its own — they live on the adopted ISO identifier — so
|
|
75
|
+
`base_document.year` is nil for it while `root.year` is `"2019"`. Measured over
|
|
76
|
+
the 67-reference probe corpus, `root.year` equals the old first-year regex on
|
|
77
|
+
**every** parseable reference and `base_document.year` misses all three
|
|
78
|
+
adopted-norm forms. So the year, number and part are read from `#root`; only the
|
|
79
|
+
**family** grouping is read from `#base_document`, which keeps
|
|
80
|
+
`EN ISO 1234` and `CEN ISO/TS 1234` apart.
|
|
81
|
+
|
|
82
|
+
### `Docidentifier` — the year rule
|
|
83
|
+
|
|
84
|
+
`Relaton::Cen::Docidentifier` (`model/docidentifier.rb`, `< Bib::Docidentifier`)
|
|
85
|
+
parses its `content` into `@pubid` while the lutaml `content` attribute stays a
|
|
86
|
+
plain string for serialization. The parse is on the **data side**: it lazily
|
|
87
|
+
`require "pubid"` and rescues `LoadError`/`StandardError`, so a non-CEN value (an
|
|
88
|
+
ISBN) or a code the grammar rejects stays verbatim with `@pubid` nil, and all
|
|
89
|
+
three mutators no-op. `Pubid#exclude` returns a **copy** (the BSI shape, not
|
|
90
|
+
IALA's in-place setters), so each mutator replaces `@pubid` and re-renders
|
|
91
|
+
through the aliased `store_content` — writing through `content=` would re-parse
|
|
92
|
+
and discard the mutation.
|
|
93
|
+
|
|
94
|
+
**`remove_date!` drops the LAST year, and only that one.** On a supplement the
|
|
95
|
+
identifier names the supplement, so its own year is the last one:
|
|
96
|
+
`EN 13250:2000/A1:2005` is amendment A1 of 2005, and the `2000` belongs to the
|
|
97
|
+
base document it amends — excluding that would strip the base document's
|
|
98
|
+
identity rather than a date. So the rule is `exclude(:supplement_year)` when
|
|
99
|
+
there is a supplement year, else `exclude(:year)`:
|
|
100
|
+
|
|
101
|
+
| input | `remove_date!` |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `EN 13306:2017` | `EN 13306` |
|
|
104
|
+
| `EN 13250:2000/A1:2005` | `EN 13250:2000/A1` |
|
|
105
|
+
| `EN 285:2015+A1:2021` | `EN 285:2015+A1` |
|
|
106
|
+
| `EN 61375-2-3:2015/AC:2016-11` | `EN 61375-2-3:2015/AC` |
|
|
107
|
+
| `EN 285:2015+A1` | `EN 285+A1` |
|
|
108
|
+
|
|
109
|
+
The last two rows are where this beats the old `content.sub!(/:\d{4}$/, "")`:
|
|
110
|
+
neither string ends in a bare year, so the regex stripped **nothing** and
|
|
111
|
+
`to_most_recent_reference` returned a dated reference. `:supplement_year` is a
|
|
112
|
+
CEN-only key, and it resets the supplement's year and month together. Note that
|
|
113
|
+
ISO and IEC are different — there `exclude(:year)` removes *every* year.
|
|
114
|
+
|
|
115
|
+
`remove_part!` is `exclude(:part, :subpart)`, and `to_all_parts!` does both,
|
|
116
|
+
then wraps `@pubid` in pubid's `AllParts` (a generic `Pubid::AllPartsIdentifier`
|
|
117
|
+
— CEN has no dedicated `AllParts` subclass). `content` stays the plain
|
|
118
|
+
part-and-date-stripped string: **`Pubid::CenCenelec::Renderer` never reads
|
|
119
|
+
`all_parts`**, so CEN's own renderer never had a marker, and `content` is
|
|
120
|
+
cached from the pre-wrap pubid on purpose. `#pubid` itself, read directly, now
|
|
121
|
+
answers `all_parts? == true` and renders WITH pubid's generic "(all parts)"
|
|
122
|
+
marker (`#to_s`), since it's the wrapper. BSI and IALA make the same
|
|
123
|
+
content/`#pubid` trade-off, for the same reason. pubid holds a sub-part
|
|
124
|
+
**inside** `part` (`61375-2-3` gives `"2-3"`), which is also why
|
|
125
|
+
`fetch_structuredid` swaps the separator to
|
|
126
|
+
reproduce the old `"2:3"` partnumber.
|
|
127
|
+
|
|
128
|
+
### Hit selection — what the query left unsaid
|
|
129
|
+
|
|
130
|
+
`search_filter` sends the caller's **raw reference text** to the portal form —
|
|
131
|
+
that is search-engine input, not identifier parsing (the BSI precedent) — and
|
|
132
|
+
selects with pubid's subset match `query === hit.pubid`. A component that the
|
|
133
|
+
query omits matches any value, and `===` reads the omission from the query
|
|
134
|
+
itself, nested identifiers included (`CEN ISO/TS 21003-7` keeps its part on the
|
|
135
|
+
adopted ISO document). That gives the rules the old comparison had:
|
|
136
|
+
|
|
137
|
+
- no part → any part
|
|
138
|
+
- no base year → any year
|
|
139
|
+
- a supplement that carries **no year** → any supplement year:
|
|
140
|
+
`EN 285:2015+A1` → `EN 285:2015+A1:2021`, but never → `EN 285:2015`
|
|
141
|
+
|
|
142
|
+
pubid declares the CEN `type`, `stage` and `typed_stage` strict (pubid#408), so
|
|
143
|
+
`EN 1325` matches neither the draft `prEN 1325` nor `CEN/TS 1325`. `===`
|
|
144
|
+
requires the same class, and pubid documents that a reference never falls back
|
|
145
|
+
to the base document of a wrapper, so **a base reference never answers with its
|
|
146
|
+
own amendment's record**, and a supplement reference never answers with its
|
|
147
|
+
base document. That is this flavor's rule, and it used to need a
|
|
148
|
+
`supplement?` guard, because `matches?` enforced it only through the
|
|
149
|
+
class-strict `==`, which was pubid's internal invariant rather than a contract.
|
|
150
|
+
`spec/cen/relaton/cen/bibliography_spec.rb` still covers it (`never selects a
|
|
151
|
+
supplement for a base reference`), so a pubid that relaxed `===` would fail
|
|
152
|
+
loudly here.
|
|
153
|
+
|
|
154
|
+
Measured over the 61 hit codes the cassettes hold (141 queries: each code and
|
|
155
|
+
its forms without year, part and supplement year): `===` and the old
|
|
156
|
+
`matches?(…, ignore: ignored_components(query))` agree on all 8,601 pairs.
|
|
157
|
+
|
|
158
|
+
`absent?(id, *keys)` (`id.exclude(*keys) == id`) stays for `get`, which appends
|
|
159
|
+
the `year` argument to a reference that names no year.
|
|
160
|
+
|
|
161
|
+
Two selections got **stricter**, and both were latent bugs in the regex:
|
|
162
|
+
`code_to_parts` required a literal `+` for its `amd` group and never compared its
|
|
163
|
+
`ac` group at all, so a `/A1:2005` amendment was invisible to it — a query for
|
|
164
|
+
`EN 13250:2000` matched the amendment record, and `HD 1215-2:1988` kept its
|
|
165
|
+
corrigendum hit and was saved only by the sort order. pubid rejects both.
|
|
166
|
+
|
|
167
|
+
### Sort order
|
|
168
|
+
|
|
169
|
+
`HitCollection#sort` is a memoized `sort_by!` over
|
|
170
|
+
`[family, part_segments, -year, supplement_key]`, which reproduces the order the
|
|
171
|
+
six `code_to_parts` fields gave: the document family ascending
|
|
172
|
+
(`base_document.exclude(:year, :part, :subpart)`), then the part ascending with
|
|
173
|
+
a part-less hit **first**, then the year **descending** with a year-less hit
|
|
174
|
+
last, then the supplement, so a base document precedes its own amendments and
|
|
175
|
+
corrigenda. Part segments are compared as **integers**, so `-10` sorts after
|
|
176
|
+
`-2`. An unparseable hit sorts last. The order is load-bearing:
|
|
177
|
+
`isobib_results_filter` takes the first hit that matches the year and has an
|
|
178
|
+
item, so `EN 13306` answers with the 2017 record and `EN 1325` with the
|
|
179
|
+
part-less one.
|
|
180
|
+
|
|
181
|
+
A consolidated identifier (`EN 285:2015+A1:2021`) holds its base document and
|
|
182
|
+
its supplements in `#identifiers` and does not answer `supplement_type`
|
|
183
|
+
itself, so `supplements` reads `identifiers.drop(1)` for it and `[id]` for a
|
|
184
|
+
plain amendment or corrigendum. The sort key reads the supplement's `number`
|
|
185
|
+
and `year`, which every pubid version answers: pubid removes the
|
|
186
|
+
`supplement_number`/`supplement_year` aliases.
|
|
187
|
+
The `:supplement_year` exclude key is a different thing and stays.
|
|
188
|
+
|
|
189
|
+
## External dependencies
|
|
190
|
+
|
|
191
|
+
`mechanize ~> 2.10`, `isoics ~> 0.1` (ICS descriptions), `pubid`,
|
|
192
|
+
`relaton-core`, `relaton-bib`.
|
|
193
|
+
|
|
194
|
+
## Testing
|
|
195
|
+
|
|
196
|
+
RSpec with WebMock + VCR (cassettes in `spec/vcr_cassettes/`, record `:once`, 7-day re-record). Network is blocked via WebMock. No index fixture — CEN is scraped, not indexed; `spec/fixtures/` holds XML/YAML round-trip data. The scraper is inherently brittle to portal HTML changes.
|
|
197
|
+
|
|
198
|
+
- `spec/cen/relaton/cen/docidentifier_spec.rb` unit-tests the three mutators over
|
|
199
|
+
every identifier form the portal serves, including the two rows above that the
|
|
200
|
+
old regex left unchanged.
|
|
201
|
+
- `spec/cen/relaton/cen/bibliography_spec.rb` unit-tests hit **selection** with
|
|
202
|
+
**no HTTP at all**: `query === hit` is the whole of `search_filter`'s
|
|
203
|
+
decision, and driving it directly covers pairs no cassette
|
|
204
|
+
can. That matters here — the cassettes hold a detail page only for the
|
|
205
|
+
document their own example fetched, so an example that selects a *different*
|
|
206
|
+
hit from the same search (say the 2008 record in `cen_iso_ts_21003_7`) dies
|
|
207
|
+
with `VCR::Errors::UnhandledHTTPRequestError`, not an assertion failure.
|
|
208
|
+
- Every `docidentifier[0].content` expectation in `spec/cen/relaton/cen_spec.rb`
|
|
209
|
+
is **unedited** by the pubid migration: what the flavor returns did not change.
|
|
210
|
+
- `returns nil when document doesn't exist` is split in two, because
|
|
211
|
+
`CEN NOT FOUND` is not an identifier and now raises. One example asserts
|
|
212
|
+
`Pubid::Errors::ParseError` and makes no request; the other keeps the
|
|
213
|
+
not-found coverage with a parseable reference.
|
|
214
|
+
- **Trap:** `gets code` writes `spec/cen/fixtures/bibdata.xml` through
|
|
215
|
+
`write_file`, which writes only when the file is **missing**
|
|
216
|
+
(`spec/spec_helper.rb`). A deleted fixture is silently recreated from whatever
|
|
217
|
+
the current cassette says, and the example then always passes. Never delete it
|
|
218
|
+
to make a spec green; delete it only to regenerate deliberately, then read the
|
|
219
|
+
diff.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
= Relaton::Cen retrieve CEN Standards for bibliographic use using the BibliographicItem model
|
|
2
|
+
|
|
3
|
+
RelatonCen is a Ruby gem that implements the https://github.com/metanorma/metanorma-model-iso#iso-bibliographic-item[IsoBibliographicItem model].
|
|
4
|
+
|
|
5
|
+
== Usage
|
|
6
|
+
|
|
7
|
+
For CENELEC standards, use references with the prefix `CEN/CLC`. For example, `CEN/CLC Guide 6`.
|
|
8
|
+
|
|
9
|
+
=== Search for a standard using keywords
|
|
10
|
+
|
|
11
|
+
[source,ruby]
|
|
12
|
+
----
|
|
13
|
+
require 'relaton/cen'
|
|
14
|
+
=> true
|
|
15
|
+
|
|
16
|
+
hit_collection = Relaton::Cen::Bibliography.search("CEN ISO/TS 21003-7")
|
|
17
|
+
=> <Relaton::Cen::HitCollection:0x00000000002560 @ref=CEN ISO/TS 21003-7 @fetched=false>
|
|
18
|
+
|
|
19
|
+
item = hit_collection[0].item
|
|
20
|
+
=> #<Relaton::Cen::ItemData:0x000000012a35c458
|
|
21
|
+
...
|
|
22
|
+
----
|
|
23
|
+
|
|
24
|
+
=== Get a standard by its code
|
|
25
|
+
|
|
26
|
+
Use `Relaton::Cen::Bibliography.get(ref, year, options)` to get a standard by its code.
|
|
27
|
+
|
|
28
|
+
- `ref` is the standard code, e.g. `CEN ISO/TS 21003-7`
|
|
29
|
+
- `year` is the year of the standard, e.g. `2019` (optional)
|
|
30
|
+
- `options` is a hash of options:
|
|
31
|
+
- `keep_year` - keep the year in ID if true (optional)
|
|
32
|
+
|
|
33
|
+
[source,ruby]
|
|
34
|
+
----
|
|
35
|
+
# With year in reference
|
|
36
|
+
Relaton::Cen::Bibliography.get "EN 10160:1999"
|
|
37
|
+
[relaton-cen] INFO: (EN 10160:1999) Fetching from standards.cencenelec.eu ...
|
|
38
|
+
[relaton-cen] INFO: (EN 10160:1999) Found: `EN 10160:1999`
|
|
39
|
+
=> #<Relaton::Cen::ItemData:0x00000001273d5b58
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
# With a year as a separate argument
|
|
43
|
+
Relaton::Cen::Bibliography.get "EN 10160", "1999"
|
|
44
|
+
[relaton-cen] INFO: (EN 10160:1999) Fetching from standards.cencenelec.eu ...
|
|
45
|
+
[relaton-cen] INFO: (EN 10160:1999) Found: `EN 10160:1999`
|
|
46
|
+
=> #<Relaton::Cen::ItemData:0x00000001274d58c8
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
# To get the most recent version of a standard by its code use reference without year
|
|
50
|
+
Relaton::Cen::Bibliography.get "CEN/CLC Guide 6"
|
|
51
|
+
[relaton-cen] INFO: (CEN/CLC Guide 6) Fetching from standards.cencenelec.eu ...
|
|
52
|
+
[relaton-cen] INFO: (CEN/CLC Guide 6) Found: `CEN/CLC Guide 6:2014`
|
|
53
|
+
=> #<Relaton::Cen::ItemData:0x000000012759a010
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
# To keep the year in ID use `keep_year` option
|
|
57
|
+
> RelatonCen::CenBibliography.get "CEN/CLC Guide 6", nil, keep_year: true
|
|
58
|
+
[relaton-cen] INFO: (CEN/CLC Guide 6) Fetching from standards.cencenelec.eu ...
|
|
59
|
+
[relaton-cen] INFO: (CEN/CLC Guide 6) Found: `CEN/CLC Guide 6`
|
|
60
|
+
=> #<Relaton::Cen::ItemData:0x000000012c629760
|
|
61
|
+
...
|
|
62
|
+
----
|
|
63
|
+
|
|
64
|
+
=== XML serialization
|
|
65
|
+
|
|
66
|
+
[source,ruby]
|
|
67
|
+
----
|
|
68
|
+
item.to_xml
|
|
69
|
+
=> "<bibitem id="CENISOTS2100372019" type="standard" schema-version="v1.5.6">
|
|
70
|
+
<fetched>2025-12-15</fetched>
|
|
71
|
+
<title language="en" script="Latn" type="title-main">Multilayer piping systems for hot and cold water installations inside buildings</title>
|
|
72
|
+
...
|
|
73
|
+
</bibitem>"
|
|
74
|
+
----
|
|
75
|
+
|
|
76
|
+
With `bibdata: true` option XML output wrapped with `bibdata` element and `ext`
|
|
77
|
+
element added.
|
|
78
|
+
|
|
79
|
+
[source,ruby]
|
|
80
|
+
----
|
|
81
|
+
item.to_xml bibdata: true
|
|
82
|
+
=> "<bibdata type="standard" schema-version="v1.5.6">
|
|
83
|
+
<fetched>2025-12-15</fetched>
|
|
84
|
+
<title language="en" script="Latn" type="title-main">Multilayer piping systems for hot and cold water installations inside buildings</title>
|
|
85
|
+
...
|
|
86
|
+
<ext schema-version="v1.0.1">
|
|
87
|
+
<doctype>international-standard</doctype>
|
|
88
|
+
<flavor>cen</flavor>
|
|
89
|
+
<ics>
|
|
90
|
+
<code>23.040.20</code>
|
|
91
|
+
<text>Plastics pipes</text>
|
|
92
|
+
</ics>
|
|
93
|
+
<ics>
|
|
94
|
+
<code>91.140.60</code>
|
|
95
|
+
<text>Water supply systems</text>
|
|
96
|
+
</ics>
|
|
97
|
+
<structuredidentifier>
|
|
98
|
+
<agency>CEN</agency>
|
|
99
|
+
<docnumber>21003</docnumber>
|
|
100
|
+
<partnumber>7</partnumber>
|
|
101
|
+
</structuredidentifier>
|
|
102
|
+
</ext>
|
|
103
|
+
</bibdata>"
|
|
104
|
+
----
|
|
105
|
+
|
|
106
|
+
=== Typed links
|
|
107
|
+
|
|
108
|
+
Each CEN document has `src` type source.
|
|
109
|
+
|
|
110
|
+
[source,ruby]
|
|
111
|
+
----
|
|
112
|
+
item.source
|
|
113
|
+
=> [#<Relaton::Bib::Uri:0x0000000126a80878
|
|
114
|
+
@content="https://standards.cencenelec.eu/ords/f?p=CEN:110:::::FSP_PROJECT,FSP_ORG_ID:68120,6137&cs=1AD0C0DDDE4500D5B9BFB0BCE76BB63F9",
|
|
115
|
+
@language=nil,
|
|
116
|
+
@locale=nil,
|
|
117
|
+
@script=nil,
|
|
118
|
+
@type="src">]
|
|
119
|
+
----
|
|
120
|
+
|
|
121
|
+
=== Create bibliographic item form YAML
|
|
122
|
+
|
|
123
|
+
[source,ruby]
|
|
124
|
+
----
|
|
125
|
+
Relaton::Cen::Item.from_yaml File.read('spec/fixtures/item.yaml')
|
|
126
|
+
=> #<Relaton::Cen::ItemData:0x00000001292f71d0
|
|
127
|
+
...
|
|
128
|
+
----
|
|
129
|
+
|
|
130
|
+
=== Create bibliographic item from XML
|
|
131
|
+
|
|
132
|
+
[source,ruby]
|
|
133
|
+
----
|
|
134
|
+
Relaton::Cen::Item.from_xml File.read("spec/fixtures/bibdata.xml")
|
|
135
|
+
=> #<Relaton::Cen::ItemData:0x0000000129353fc0
|
|
136
|
+
...
|
|
137
|
+
----
|
|
138
|
+
|
|
139
|
+
=== Logging
|
|
140
|
+
|
|
141
|
+
RelatonCen uses the relaton-logger gem for logging. By default, it logs to STDOUT. To change the log levels and add other loggers, read the https://github.com/relaton/relaton-logger#usage[relaton-logger] documentation.
|
|
142
|
+
|
|
143
|
+
== Development
|
|
144
|
+
|
|
145
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
146
|
+
|
|
147
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
148
|
+
|
|
149
|
+
== Contributing
|
|
150
|
+
|
|
151
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton-cen.
|
|
152
|
+
|
|
153
|
+
== License
|
|
154
|
+
|
|
155
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Index format (pubid v2 — structured `_type:` index)
|
|
4
|
+
|
|
5
|
+
CIE consumes and produces the **structured `index-v2`** (`INDEXFILE = "index-v2"`):
|
|
6
|
+
each row's `:id` is a `Pubid::Cie::Identifier` object, serialized by `relaton-index`
|
|
7
|
+
as a `_type: pubid:cie:…` map (mirrors the NIST/ETSI flavors).
|
|
8
|
+
|
|
9
|
+
- **Producer** (`data_fetcher.rb`): `#index` passes `pubid_class:
|
|
10
|
+
::Pubid::Cie::Identifier`; `#write_file` parses each docid via the `#pubid` helper
|
|
11
|
+
and indexes the **object** (not the raw string). `#pubid` returns `nil` — the id is
|
|
12
|
+
written to disk as a document but **left out of the index** — for any id pubid can't
|
|
13
|
+
parse **or** that doesn't round-trip. The guard mirrors the read side's acceptance
|
|
14
|
+
test (`Index::FileIO#id_supported?`, `from_hash(to_hash) == to_hash`): the loader
|
|
15
|
+
rejects the *whole* index on the first non-round-tripping row, so a bad id must be
|
|
16
|
+
dropped at write time rather than poison every lookup.
|
|
17
|
+
- **Consumer** (`scrapper.rb`, `processor.rb`): both pass `pubid_class:
|
|
18
|
+
::Pubid::Cie::Identifier` to `Index.find_or_create`. The scrapper parses the ref to a
|
|
19
|
+
pubid with `::Pubid::Cie.parse` — an unparseable ref **raises** `Pubid::Errors::ParseError`
|
|
20
|
+
(partial refs such as `CIE 001` parse; the old raw-String substring fallback is gone)
|
|
21
|
+
and passes **that** to `index.search` so index-v2 narrows candidates by number via
|
|
22
|
+
binary search before the block runs (a String arg would force a full O(n) scan — see
|
|
23
|
+
`Index::Type#search_candidates`); the block keeps the broad substring match, and
|
|
24
|
+
`min_by { |r| r[:id].to_s }` picks the winner (rows are pubid objects, not
|
|
25
|
+
`Comparable`, so compare by string form). The scrapper's index comes from the
|
|
26
|
+
data repo's Pages machine index (`PAGES_URL`, relaton#206) — manifest + the
|
|
27
|
+
shard for the parsed query, the whole-index zip on the same site serving only
|
|
28
|
+
the retry on a miss; `ENDPOINT` (the `v2` branch) still serves the document
|
|
29
|
+
files.
|
|
30
|
+
|
|
31
|
+
### pubid dependency
|
|
32
|
+
|
|
33
|
+
The published `relaton-data-cie/index-v2` uses CIE **proceedings** ids
|
|
34
|
+
(`_type: pubid:cie:proceedings` with `paper`/`page_range`) and the **flattened**
|
|
35
|
+
`to_hash` (`number`/`year` scalar, no nested `code`). That support first shipped in
|
|
36
|
+
the released pubid `2.0.0.pre.alpha.13`, which `relaton.gemspec` pins — an older
|
|
37
|
+
pubid strips `paper`/`page_range` and emits the nested shape, so `Relaton::Index`
|
|
38
|
+
would reject the whole published index.
|
|
39
|
+
|
|
40
|
+
Note the CIE grammar extensions **do** parse the techstreet variant ids like
|
|
41
|
+
`CIE x051:2025/zcunvy` (as `Conference` `@variant` ids), so — unlike the old pubid —
|
|
42
|
+
they are indexed, not dropped.
|
|
43
|
+
|
|
44
|
+
## Crawler (`data_fetcher.rb`) — parallel detail fetch
|
|
45
|
+
|
|
46
|
+
The Techstreet crawl (~1147 docs) is a headless-Chrome (Ferrum) scrape. It runs in
|
|
47
|
+
**two phases**:
|
|
48
|
+
|
|
49
|
+
- **Phase 1 — `collect_hits` (serial):** page through the search results following
|
|
50
|
+
`//a[@class="next_page"]`, collecting every `//li[@data-product]` into one list
|
|
51
|
+
(~12 page loads at `per_page=100`).
|
|
52
|
+
- **Phase 2 — `process_hits` (parallel):** fan the hits out over a bounded pool.
|
|
53
|
+
**Each worker owns its own `BrowserAgent`** (its own Chrome, built via the
|
|
54
|
+
`build_agent` factory so the full UA/header + `navigator`/`window.chrome` stealth
|
|
55
|
+
masking runs per worker — Cloudflare evasion must hold per worker, not for a shared
|
|
56
|
+
singleton). Agents are built up front on the main thread (a Chrome-launch failure
|
|
57
|
+
aborts fast instead of hanging the bounded queue) and **every** agent is quit in an
|
|
58
|
+
`ensure`. The single memoized `#agent` is used only for Phase 1.
|
|
59
|
+
|
|
60
|
+
**Concurrency knob:** `RELATON_CIE_CONCURRENCY` (default **5**, min 1), read by
|
|
61
|
+
`.concurrency`. `relaton-data-cie`'s crawler workflow tunes it via env — no code change.
|
|
62
|
+
|
|
63
|
+
**Per-worker adaptive pacing (`Pacing`)** replaces the old single global 4 s gap: each
|
|
64
|
+
worker starts at `BASE_GAP` (~1 s) and **doubles its own gap up to `MAX_GAP`** only on
|
|
65
|
+
trouble — a Cloudflare challenge or a Ferrum/socket error (`RETRIABLE_ERRORS`).
|
|
66
|
+
`BrowserAgent#wait_for_challenge` raises a retriable `ChallengeError` when the challenge
|
|
67
|
+
doesn't clear within `MAX_CHALLENGE_WAIT`, so a stuck worker backs off and retries
|
|
68
|
+
(`#time_req` keeps the 4-try retry) instead of parsing the challenge HTML.
|
|
69
|
+
|
|
70
|
+
**Thread-safety & byte-identical output:** the slow `agent.get` runs lock-free; the
|
|
71
|
+
cheap build + `#write_file` run under one `@mutex`, so `@files`/`@errors`/`@seen` and the
|
|
72
|
+
index mutate single-threaded. Output is order-independent by construction — `@errors`
|
|
73
|
+
accumulates with `&&=` (AND, per-doc build is atomic under the lock), `@files` is a Set,
|
|
74
|
+
and the structured index is sorted deterministically on save. For a duplicate output
|
|
75
|
+
file, a `@seen[file] => catalogue-position` guard (`#superseded?`) keeps the
|
|
76
|
+
**last-by-position** content winner the serial crawl would pick; `index.add_or_update`
|
|
77
|
+
runs for **every** distinct id *before* that gate, so each id is still indexed. `index.save`
|
|
78
|
+
is called **once**, after the pool drains.
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
- `bundle install` — install dependencies
|
|
83
|
+
- `bundle exec rake spec` — run tests
|
|
84
|
+
- `bundle exec rubocop` — lint
|
|
85
|
+
|
|
86
|
+
## Testing
|
|
87
|
+
|
|
88
|
+
- **Framework:** RSpec with VCR cassettes and WebMock
|
|
89
|
+
- **Index fixture:** `spec/fixtures/index-v2.zip` (the published structured index) is loaded through a `Relaton::Index::Type` with `::Pubid::Cie::Identifier` and injected into the `Relaton::Index` pool in `before(:suite)` (see `spec/support/webmock.rb`, mirroring NIST/ETSI). Run `rake spec:update_index` to refresh from relaton-data-cie.
|
|
90
|
+
- **VCR cassettes:** `spec/vcr_cassettes/` — index download requests are ignored by VCR (handled by fixture).
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
= Relaton::Cie
|
|
2
|
+
|
|
3
|
+
RelatonCie is a Ruby gem that searches and fetches standards from the https://www.techstreet.com/cie/searches/31156444[International Commision on Illuminantion].
|
|
4
|
+
|
|
5
|
+
== Usage
|
|
6
|
+
|
|
7
|
+
=== Search document
|
|
8
|
+
|
|
9
|
+
[source,ruby]
|
|
10
|
+
----
|
|
11
|
+
require 'relaton/cie'
|
|
12
|
+
=> true
|
|
13
|
+
|
|
14
|
+
item = Relaton::Cie::Bibliography.get 'CIE 001-1980'
|
|
15
|
+
[relaton-cie] INFO: (CIE 001-1980) Fetching from Relaton repository ...
|
|
16
|
+
[relaton-cie] INFO: (CIE 001-1980) Found: `CIE 001-1980`
|
|
17
|
+
=> #<Relaton::Bib::ItemData:0x0000000123b05300
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
# Return nil if a document doesn't exist.
|
|
21
|
+
Relaton::Cie::Bibliography.get '1111'
|
|
22
|
+
[relaton-cie] INFO: (1111) Fetching from Relaton repository ...
|
|
23
|
+
[relaton-cie] INFO: (1111) Not found.
|
|
24
|
+
=> nil
|
|
25
|
+
----
|
|
26
|
+
|
|
27
|
+
=== Serialization
|
|
28
|
+
|
|
29
|
+
[source,ruby]
|
|
30
|
+
----
|
|
31
|
+
item.to_xml
|
|
32
|
+
=> "<bibitem id="CIE0011980" type="standard" schema-version="v1.5.6">
|
|
33
|
+
<fetched>2025-12-23</fetched>
|
|
34
|
+
<title type="title-main">Guidelines for minimizing urban sky glow near astronomical observatories (Joint Publication IAU/CIE)</title>
|
|
35
|
+
<title type="main">Guidelines for minimizing urban sky glow near astronomical observatories (Joint Publication IAU/CIE)</title>
|
|
36
|
+
<uri type="src">https://store.accuristech.com/standards/cie-001-1980?product_id=1210107</uri>
|
|
37
|
+
...
|
|
38
|
+
</bibitem>"
|
|
39
|
+
----
|
|
40
|
+
|
|
41
|
+
With `bibdata: true` option XML output is wrapped with `bibdata` element and `ext` element added.
|
|
42
|
+
[source,ruby]
|
|
43
|
+
----
|
|
44
|
+
item.to_xml bibdata: true
|
|
45
|
+
=> "<bibdata type="standard" schema-version="v1.5.6">
|
|
46
|
+
<fetched>2025-12-23</fetched>
|
|
47
|
+
<title type="title-main">Guidelines for minimizing urban sky glow near astronomical observatories (Joint Publication IAU/CIE)</title>
|
|
48
|
+
<title type="main">Guidelines for minimizing urban sky glow near astronomical observatories (Joint Publication IAU/CIE)</title>
|
|
49
|
+
<uri type="src">https://store.accuristech.com/standards/cie-001-1980?product_id=1210107</uri>
|
|
50
|
+
...
|
|
51
|
+
<ext schema-version="v1.0.0">
|
|
52
|
+
<doctype>document</doctype>
|
|
53
|
+
<flavor>cie</flavor>
|
|
54
|
+
</ext>
|
|
55
|
+
</bibdata>
|
|
56
|
+
----
|
|
57
|
+
=== Typed links
|
|
58
|
+
|
|
59
|
+
Each CIE document has `src` type source link.
|
|
60
|
+
|
|
61
|
+
[source,ruby]
|
|
62
|
+
----
|
|
63
|
+
item.source
|
|
64
|
+
=> [#<Relaton::Bib::Uri:0x0000000123cc2990
|
|
65
|
+
@content="https://store.accuristech.com/standards/cie-001-1980?product_id=1210107",
|
|
66
|
+
@language=nil,
|
|
67
|
+
@locale=nil,
|
|
68
|
+
@script=nil,
|
|
69
|
+
@type="src">]
|
|
70
|
+
----
|
|
71
|
+
|
|
72
|
+
=== Parse a file locally
|
|
73
|
+
|
|
74
|
+
[source,ruby]
|
|
75
|
+
----
|
|
76
|
+
item = Relaton::Cie::Item.from_xml File.read("spec/fixtures/bibdata.xml")
|
|
77
|
+
=> #<Relaton::Cie::ItemData:0x000000010d00d0d0
|
|
78
|
+
...
|
|
79
|
+
----
|
|
80
|
+
|
|
81
|
+
=== Fetch data
|
|
82
|
+
|
|
83
|
+
This gem uses the https://www.techstreet.com/cie/searches/31156444 dataset as one of the data sources.
|
|
84
|
+
|
|
85
|
+
The method `Relaton::Cie::DataFetcher.fetch(output: "data", format: "yaml")` fetches all the documents from the dataset and saves them to the `./data` folder in YAML format.
|
|
86
|
+
Arguments:
|
|
87
|
+
|
|
88
|
+
- `output` - folder to save documents (default './data').
|
|
89
|
+
- `format` - the format in which the documents are saved. Possible formats are: `yaml`, `xml`, `bibxml` (default `yaml`).
|
|
90
|
+
|
|
91
|
+
[source,ruby]
|
|
92
|
+
----
|
|
93
|
+
require "relaton/cie/data_fetcher"
|
|
94
|
+
=> true
|
|
95
|
+
|
|
96
|
+
Relaton::Cie::DataFetcher.fetch
|
|
97
|
+
Started at: 2021-09-08 16:37:53 +0200
|
|
98
|
+
Stopped at: 2021-09-08 16:49:17 +0200
|
|
99
|
+
Done in: 684 sec.
|
|
100
|
+
=> nil
|
|
101
|
+
----
|
|
102
|
+
|
|
103
|
+
=== Logging
|
|
104
|
+
|
|
105
|
+
RelatonCie uses the relaton-logger gem for logging. By default, it logs to STDOUT. To change the log levels and add other loggers, read the https://github.com/relaton/relaton-logger#usage[relaton-logger] documentation.
|
|
106
|
+
|
|
107
|
+
== Development
|
|
108
|
+
|
|
109
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
110
|
+
|
|
111
|
+
To update the index test fixture (used by tests), run `rake spec:update_index`. This downloads the latest `index-v2.zip` from the https://github.com/relaton/relaton-data-cie[relaton-data-cie] repository.
|
|
112
|
+
|
|
113
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
114
|
+
|
|
115
|
+
== Contributing
|
|
116
|
+
|
|
117
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton-cie.
|
|
118
|
+
|
|
119
|
+
== License
|
|
120
|
+
|
|
121
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|