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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/data/equivalence/adoption-chains.yaml +59 -0
  3. data/lib/relaton/3gpp/CLAUDE.md +187 -0
  4. data/lib/relaton/3gpp/README.adoc +149 -0
  5. data/lib/relaton/adobe/CLAUDE.md +104 -0
  6. data/lib/relaton/bib/CLAUDE.md +130 -0
  7. data/lib/relaton/bib/README.adoc +330 -0
  8. data/lib/relaton/bib/model/item_shared.rb +5 -1
  9. data/lib/relaton/bib/model/series.rb +1 -1
  10. data/lib/relaton/bipm/CLAUDE.md +173 -0
  11. data/lib/relaton/bipm/README.adoc +352 -0
  12. data/lib/relaton/bipm/data_fetcher.rb +3 -2
  13. data/lib/relaton/bsi/CLAUDE.md +35 -0
  14. data/lib/relaton/bsi/README.adoc +112 -0
  15. data/lib/relaton/calconnect/CLAUDE.md +177 -0
  16. data/lib/relaton/calconnect/README.adoc +140 -0
  17. data/lib/relaton/ccsds/CLAUDE.md +74 -0
  18. data/lib/relaton/ccsds/README.adoc +154 -0
  19. data/lib/relaton/cen/CLAUDE.md +219 -0
  20. data/lib/relaton/cen/README.adoc +155 -0
  21. data/lib/relaton/cie/CLAUDE.md +90 -0
  22. data/lib/relaton/cie/README.adoc +121 -0
  23. data/lib/relaton/core/CLAUDE.md +140 -0
  24. data/lib/relaton/core/README.adoc +64 -0
  25. data/lib/relaton/core/data_fetcher.rb +142 -3
  26. data/lib/relaton/db/CLAUDE.md +373 -0
  27. data/lib/relaton/db.rb +21 -1
  28. data/lib/relaton/doi/CLAUDE.md +105 -0
  29. data/lib/relaton/doi/README.adoc +89 -0
  30. data/lib/relaton/easc/CLAUDE.md +85 -0
  31. data/lib/relaton/ecma/CLAUDE.md +217 -0
  32. data/lib/relaton/ecma/README.adoc +178 -0
  33. data/lib/relaton/equivalence.rb +117 -0
  34. data/lib/relaton/etsi/CLAUDE.md +219 -0
  35. data/lib/relaton/etsi/README.adoc +137 -0
  36. data/lib/relaton/evs/CLAUDE.md +120 -0
  37. data/lib/relaton/evs/README.adoc +27 -0
  38. data/lib/relaton/gb/CLAUDE.md +47 -0
  39. data/lib/relaton/gb/README.adoc +169 -0
  40. data/lib/relaton/gost/CLAUDE.md +100 -0
  41. data/lib/relaton/iala/CLAUDE.md +97 -0
  42. data/lib/relaton/iana/CLAUDE.md +105 -0
  43. data/lib/relaton/iana/README.adoc +107 -0
  44. data/lib/relaton/iec/CLAUDE.md +13 -0
  45. data/lib/relaton/iec/README.adoc +280 -0
  46. data/lib/relaton/iec/data_fetcher.rb +24 -8
  47. data/lib/relaton/iec/project_fetcher.rb +48 -0
  48. data/lib/relaton/iec/project_parser.rb +111 -0
  49. data/lib/relaton/iec.rb +5 -0
  50. data/lib/relaton/ieee/CLAUDE.md +176 -0
  51. data/lib/relaton/ieee/README.adoc +131 -0
  52. data/lib/relaton/ietf/CLAUDE.md +357 -0
  53. data/lib/relaton/ietf/README.adoc +167 -0
  54. data/lib/relaton/iho/CLAUDE.md +70 -0
  55. data/lib/relaton/iho/README.adoc +119 -0
  56. data/lib/relaton/index/CLAUDE.md +207 -0
  57. data/lib/relaton/index/README.adoc +221 -0
  58. data/lib/relaton/index/dataset_lock.rb +122 -0
  59. data/lib/relaton/index/family.rb +63 -0
  60. data/lib/relaton/index/type.rb +32 -0
  61. data/lib/relaton/index.rb +3 -0
  62. data/lib/relaton/isbn/CLAUDE.md +47 -0
  63. data/lib/relaton/isbn/README.adoc +91 -0
  64. data/lib/relaton/iso/CLAUDE.md +38 -0
  65. data/lib/relaton/iso/README.adoc +370 -0
  66. data/lib/relaton/iso/data_fetcher.rb +16 -1
  67. data/lib/relaton/itu/CLAUDE.md +685 -0
  68. data/lib/relaton/itu/README.adoc +188 -0
  69. data/lib/relaton/jcgm/CLAUDE.md +107 -0
  70. data/lib/relaton/jis/CLAUDE.md +64 -0
  71. data/lib/relaton/jis/README.adoc +167 -0
  72. data/lib/relaton/logger/CLAUDE.md +37 -0
  73. data/lib/relaton/logger/README.adoc +120 -0
  74. data/lib/relaton/nist/CLAUDE.md +180 -0
  75. data/lib/relaton/nist/README.adoc +389 -0
  76. data/lib/relaton/nist/bibliography.rb +17 -1
  77. data/lib/relaton/nist/data_fetcher.rb +60 -4
  78. data/lib/relaton/nist/hit_collection.rb +121 -59
  79. data/lib/relaton/nist/scraper.rb +21 -7
  80. data/lib/relaton/oasis/CLAUDE.md +251 -0
  81. data/lib/relaton/oasis/README.adoc +132 -0
  82. data/lib/relaton/ogc/CLAUDE.md +242 -0
  83. data/lib/relaton/ogc/README.adoc +134 -0
  84. data/lib/relaton/ogc/data_fetcher.rb +9 -3
  85. data/lib/relaton/oiml/CLAUDE.md +47 -0
  86. data/lib/relaton/oiml/README.adoc +70 -0
  87. data/lib/relaton/omg/CLAUDE.md +137 -0
  88. data/lib/relaton/omg/README.adoc +101 -0
  89. data/lib/relaton/plateau/CLAUDE.md +126 -0
  90. data/lib/relaton/plateau/README.adoc +139 -0
  91. data/lib/relaton/sdo/CLAUDE.md +81 -0
  92. data/lib/relaton/un/CLAUDE.md +91 -0
  93. data/lib/relaton/un/README.adoc +125 -0
  94. data/lib/relaton/version.rb +1 -1
  95. data/lib/relaton/w3c/CLAUDE.md +369 -0
  96. data/lib/relaton/w3c/README.adoc +140 -0
  97. data/lib/relaton/w3c/data_fetcher.rb +1 -1
  98. data/lib/relaton/xsf/CLAUDE.md +189 -0
  99. data/lib/relaton/xsf/README.adoc +127 -0
  100. data/lib/relaton.rb +1 -0
  101. 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).