relaton 3.0.0.pre.alpha.17 → 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,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-etsi is a Ruby gem that retrieves ETSI (European Telecommunications Standards Institute) standards metadata using the Relaton bibliographic item model. It fetches data from the ETSI website and the relaton-data-etsi GitHub repository.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install dependencies
|
|
13
|
+
bin/setup
|
|
14
|
+
|
|
15
|
+
# Run all tests
|
|
16
|
+
rake spec
|
|
17
|
+
|
|
18
|
+
# Run a specific test file
|
|
19
|
+
rspec spec/relaton/etsi/bibliography_spec.rb
|
|
20
|
+
|
|
21
|
+
# Run a specific test by line number
|
|
22
|
+
rspec spec/relaton/etsi/bibliography_spec.rb:15
|
|
23
|
+
|
|
24
|
+
# Run linter
|
|
25
|
+
rake rubocop
|
|
26
|
+
|
|
27
|
+
# Interactive console
|
|
28
|
+
bin/console
|
|
29
|
+
|
|
30
|
+
# Install gem locally
|
|
31
|
+
bundle exec rake install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Architecture
|
|
35
|
+
|
|
36
|
+
The codebase is transitioning from `RelatonEtsi` namespace to `Relaton::Etsi` namespace.
|
|
37
|
+
|
|
38
|
+
### New Architecture (lib/relaton/etsi/)
|
|
39
|
+
|
|
40
|
+
Uses Lutaml for serialization:
|
|
41
|
+
- `Item` - base bibliographic item inheriting from `Bib::Item`
|
|
42
|
+
- `Bibitem` - for `<bibitem>` XML output (includes `Bib::BibitemShared`)
|
|
43
|
+
- `Bibdata` - for `<bibdata>` XML output (includes `Bib::BibdataShared`)
|
|
44
|
+
- `Ext` - ETSI-specific extension data (marker, frequency, mandate, custom_collection)
|
|
45
|
+
- `Doctype` - ETSI document type with abbreviations (EN, ES, GS, TS, TR, etc.)
|
|
46
|
+
|
|
47
|
+
### Legacy Architecture (lib/relaton_etsi/)
|
|
48
|
+
|
|
49
|
+
- `BibliographicItem` - extends `RelatonBib::BibliographicItem`
|
|
50
|
+
- `XMLParser` - parses XML into bibliographic items
|
|
51
|
+
- `HashConverter` - converts hashes to bibliographic items
|
|
52
|
+
- `DocumentType` - document type with type/abbreviation mapping
|
|
53
|
+
|
|
54
|
+
### Core Components
|
|
55
|
+
|
|
56
|
+
- `Bibliography` - searches and retrieves standards from relaton-data-etsi index
|
|
57
|
+
- `DataFetcher` - fetches all documents from ETSI website CSV export
|
|
58
|
+
- `DataParser` - parses CSV rows into bibliographic items
|
|
59
|
+
- `Processor` - Relaton processor for integration with the relaton ecosystem
|
|
60
|
+
|
|
61
|
+
### Data Flow
|
|
62
|
+
|
|
63
|
+
1. `Bibliography.get(ref)` searches the relaton-data-etsi index
|
|
64
|
+
2. Fetches YAML from GitHub, converts to `Item` using `from_yaml`
|
|
65
|
+
3. `DataFetcher.fetch` pulls CSV from etsi.org, parses with `DataParser`, saves to output folder
|
|
66
|
+
|
|
67
|
+
### Pubid-backed index-v2
|
|
68
|
+
|
|
69
|
+
The ETSI index is **pubid-structured** (`index-v2.yaml`/`.zip`): each row's `:id`
|
|
70
|
+
is a `Pubid::Etsi::Identifier` serialized to its `_type: pubid:etsi:{etsi-standard,
|
|
71
|
+
amendment,corrigendum}` hash (the whole published ETSI corpus round-trips on the
|
|
72
|
+
pinned pubid — see the root `CLAUDE.md` pubid-pin note). The rows use the **flat,
|
|
73
|
+
compact** shape (`type`/`number`/`version`/`year`/`month` as scalars under
|
|
74
|
+
`_type`) that the published `relaton-data-etsi` index carries — the ETSI
|
|
75
|
+
`to_hash` flattening (merged from `refactor/flatten-etsi-to-hash`) ships in the
|
|
76
|
+
released pubid `2.0.0.pre.alpha.13` that the gemspec pins. The wiring mirrors NIST/JCGM:
|
|
77
|
+
|
|
78
|
+
- **Producer** (`DataFetcher`): `index` calls `find_or_create(:etsi, file:
|
|
79
|
+
"#{INDEXFILE}.yaml", pubid_class: ::Pubid::Etsi::Identifier)`; `#save` parses the
|
|
80
|
+
docid via `#pubid` and stores the **pubid object** (`index.add_or_update pid,
|
|
81
|
+
file`) so `Relaton::Index` sorts by id number and serializes each id to its
|
|
82
|
+
`_type:` hash on save. `#pubid` returns nil (skipping the whole record) for any
|
|
83
|
+
id it can't parse **or** `to_hash`-serialize, so one malformed record can never
|
|
84
|
+
abort the crawl or corrupt the index. (No current ETSI record is skipped.)
|
|
85
|
+
- **Consumer** (`Bibliography#search`): parses the reference with
|
|
86
|
+
`::Pubid::Etsi.parse` and lets a `Pubid::Errors::ParseError` on an unrecognized ref
|
|
87
|
+
**propagate** (ISO parity — the CLI renders a friendly message, API callers
|
|
88
|
+
rescue it), then `#best_match` selects with pubid's subset match
|
|
89
|
+
`pubid === row`. The `pubid` (not a String) lets `Relaton::Index` narrow
|
|
90
|
+
candidates by number via binary search. A `version` or `date` that the ref
|
|
91
|
+
omits matches any value, so a bare `ETSI GS ZSM 012` matches every edition
|
|
92
|
+
and a fully-qualified ref matches only its edition.
|
|
93
|
+
`max_by { edition_key(row[:id]) }` returns the most recent.
|
|
94
|
+
Requires pubid `main` (partial-ref parsing, `Pubid::Errors::ParseError` on
|
|
95
|
+
failure, `subset_strict`).
|
|
96
|
+
- **A part-less reference strips the ROW's parts, it does not ask pubid for
|
|
97
|
+
"all parts".** pubid declares `parts` strict for ETSI (pubid#408), so a
|
|
98
|
+
part-less ref matches no part row on its own. `#comparable` therefore
|
|
99
|
+
matches each row without its parts (`exclude(:part, :subpart, :parts)`, a
|
|
100
|
+
copy — the cached index id is untouched), and `ETSI EN 300 175` reaches its
|
|
101
|
+
16 part rows. **Do not use `#to_all_parts` here.** Since pubid#433 "all
|
|
102
|
+
parts" is a class, and `Pubid::AllPartsIdentifier#===` compares the document
|
|
103
|
+
alone: it drops the version and the date as well, so
|
|
104
|
+
`ETSI GR ZSM 011 V1.1.1 (2023-02)` answered with V2.1.1 (2024-09) — 586,487
|
|
105
|
+
pairs over the fixture. `resolves a fully-qualified part-less reference to
|
|
106
|
+
that exact edition` in `bibliography_spec.rb` pins it.
|
|
107
|
+
- **A base reference does not reach its supplements.** `===` requires the
|
|
108
|
+
same class, so `ETSI ETR 310` does not match the corrigendum
|
|
109
|
+
`ETSI ETR 310/C1`, and a corrigendum of the base (`ETS 300 092-1/C1`) does
|
|
110
|
+
not match a corrigendum of its amendment (`092-1/A1/C1`). The
|
|
111
|
+
`matches?(…, ignore:)` it replaced did match them: over the 28,651-row
|
|
112
|
+
fixture, 298 pairs changed, all of them a supplement row that the old match
|
|
113
|
+
let in, and 218 of 94,079 references resolved to a newer amendment or
|
|
114
|
+
corrigendum instead of the document itself (`ETSI ETR 310` →
|
|
115
|
+
`ETR 310/C1 ed.1 (1996-10)`). `bibliography_spec.rb` pins that case.
|
|
116
|
+
- **`#edition_key` orders on the parsed version, never on the rendered id.** ETSI
|
|
117
|
+
versions are not zero-padded, so a String comparison of `row[:id].to_s` orders
|
|
118
|
+
`V9.0.0` above `V19.0.0` and `ed.9` above `ed.11` — it picked the wrong edition
|
|
119
|
+
for 983 of the 3007 multi-edition documents in the spec fixture, and a bare
|
|
120
|
+
`ETSI TR 155 919` resolved to the 2010 edition. `Pubid::Etsi::Identifier` is
|
|
121
|
+
**not** `Comparable` (its `<=>` returns nil), so the key is built from the
|
|
122
|
+
components: `id.version.version` holds the bare numbers (`"19.0.0"`, or `"9"`
|
|
123
|
+
for the `ed.N` form) and `id.date` renders as `yyyy-mm` for the tie-break.
|
|
124
|
+
Both delegate to `base` on a `pubid:etsi:corrigendum`/`amendment` row (162 of
|
|
125
|
+
them in the fixture), so every row shape keys the same way. The key ignores
|
|
126
|
+
`is_edition`, which puts `ed.11` above `V1.0.0` — safe because **no** document
|
|
127
|
+
mixes the two forms (measured: 0 of 28651 fixture rows). Do not put `.to_s`
|
|
128
|
+
back.
|
|
129
|
+
- **Processor** `#remove_index_file` passes `url: true` and `file:` only. It
|
|
130
|
+
needs no `pubid_class:`: the delete never reads the index (see
|
|
131
|
+
`lib/relaton/index/CLAUDE.md`).
|
|
132
|
+
|
|
133
|
+
### The crawl query keeps superseded editions (`version=1`)
|
|
134
|
+
|
|
135
|
+
`DataFetcher::SOURCEURL` is the ETSI standards-search query, and
|
|
136
|
+
`relaton-data-etsi/data/` is **exactly** what it returns — `crawler.rb` rebuilds
|
|
137
|
+
`data/` on every run, so a record the query drops cannot be reinstated in the
|
|
138
|
+
data repo. The `version` flag selects editions, not statuses: `version=0`
|
|
139
|
+
returns only the current edition of each branch, `version=1` returns the
|
|
140
|
+
superseded ones as well. The status flags (`withdrawn`, `historical`,
|
|
141
|
+
`superseded`, `onApproval`, `isCurrent`) are a **separate axis** and are already
|
|
142
|
+
all `1` — that is why the dataset carried `Historical` and `Withdrawn` documents
|
|
143
|
+
while still missing older editions (metanorma/metanorma-pdfa#95, which wanted
|
|
144
|
+
`ETSI EN 319 142-1 V1.2.1 (2024-01)` back). `version=0` also still returns
|
|
145
|
+
parallel current branches, so the response's own `superseded`/`new_versions`
|
|
146
|
+
fields, not the major version, are what `version` keys on.
|
|
147
|
+
|
|
148
|
+
The flavor uses `version=1`. Measured against the live API on 2026-08-27:
|
|
149
|
+
|
|
150
|
+
| | `version=0` | `version=1` |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| records | 28625 | 67446 (2.36x) |
|
|
153
|
+
| pages at 50/page | 573 | 1349 |
|
|
154
|
+
| `data/` on disk | 112 MB | ~264 MB |
|
|
155
|
+
| `index-v2.yaml` | 5.0 MB | ~11.8 MB |
|
|
156
|
+
|
|
157
|
+
Nothing else in the producer needed changing: the version is part of the ETSI
|
|
158
|
+
docid, so `Core::DataFetcher#unique_output_file` gives each edition its own
|
|
159
|
+
filename and `Index::Type#add_or_update` (keyed on `id.to_s`) gives each its own
|
|
160
|
+
row. `spec/etsi/relaton/etsi/data_fetcher_spec.rb` pins the query flag, and pins
|
|
161
|
+
that machinery too — it was already correct, but `version=1` is what makes
|
|
162
|
+
several editions of one document a common case rather than a rare one.
|
|
163
|
+
|
|
164
|
+
**Ordering rule for the data repo.** This is producer-side only; nothing changes
|
|
165
|
+
for users until `relaton-data-etsi` re-crawls. Do **not** deploy a `version=1`
|
|
166
|
+
crawl until the released `relaton-etsi` gem picks the newest edition. Its
|
|
167
|
+
`Bibliography#search` uses `min_by` on the rendered id, which returns the
|
|
168
|
+
**oldest** match, so a bare `ETSI EN 319 401` would resolve to the 2013 edition
|
|
169
|
+
across the whole corpus. This flavor picks the newest edition (`#best_match`
|
|
170
|
+
orders on `#edition_key`). Also check the crawl against the 6-hour GitHub
|
|
171
|
+
Actions job cap — it roughly doubles — and confirm all three EN 319 142-1
|
|
172
|
+
editions land in `data/` before merging the re-crawl.
|
|
173
|
+
|
|
174
|
+
### A bad page fails the crawl; it is never skipped
|
|
175
|
+
|
|
176
|
+
The relaton-data-etsi crawl of 2026-09-30 (run 36761804954) died at about page
|
|
177
|
+
700 with `JSON::ParserError: unexpected character: 'Array'`: data.php answered
|
|
178
|
+
once with PHP's `Array` text in place of JSON. It is transient and server side
|
|
179
|
+
— the code had not changed since the good crawl of 2026-09-28, and a full
|
|
180
|
+
re-crawl on 2026-10-02 gave valid JSON on all 1,353 pages (67,602 records,
|
|
181
|
+
`RowNum` 1..67,602 with no gap). Note also that `curl` gets an HTTP 403 page from
|
|
182
|
+
ETSI's web application firewall; Mechanize's user agent gets JSON.
|
|
183
|
+
|
|
184
|
+
`DataFetcher#fetch_page` therefore parses the body **inside**
|
|
185
|
+
`fetch_with_retry`: a body that is not a JSON array raises `BadPage`, which is
|
|
186
|
+
retried like a network error. A page still bad after its retries is deferred
|
|
187
|
+
and fetched again after the last page (`DEFERRED_DELAY`); if it is still bad,
|
|
188
|
+
the crawl raises `BadPage` with the page number and the first 200 characters of
|
|
189
|
+
the body, and `index.save` is never reached. Page 1 is not deferred (it gives
|
|
190
|
+
`total_count`). **Do not change this to skip the page:** `crawler.rb` deletes
|
|
191
|
+
`data/` before the crawl and commits what the crawl writes, so a skipped page
|
|
192
|
+
unpublishes 50 documents, while a failed crawl commits nothing and the previous
|
|
193
|
+
data stays live.
|
|
194
|
+
|
|
195
|
+
The result set is sorted by deliverable number (`sort=1`), not by date, so a
|
|
196
|
+
document that ETSI publishes during the one-hour crawl shifts the later pages
|
|
197
|
+
by one. The page count from page 1's `total_count` would then miss the last
|
|
198
|
+
record, so `fetch_remaining_pages` reads on past it while the pages stay full.
|
|
199
|
+
A record read twice overwrites its own file. Two bounds keep the loop finite
|
|
200
|
+
(data.php answers `[]` past the end today, but the loop must not depend on it):
|
|
201
|
+
a deferred page extends the range by one page only when it lies inside page
|
|
202
|
+
1's `total_count`, and a crawl still reading full pages `MAX_EXTRA_PAGES` past
|
|
203
|
+
that count raises. A deferred page that comes back `[]` inside the range also
|
|
204
|
+
fails the crawl. **Known limit:** a document *removed* during the crawl shifts
|
|
205
|
+
the later pages the other way, so one record can fall onto a page already read.
|
|
206
|
+
Nothing detects that; it is rare, because every status flag is on.
|
|
207
|
+
|
|
208
|
+
## Testing
|
|
209
|
+
|
|
210
|
+
- **Index fixture:** `spec/fixtures/index-v2.zip` (pubid `_type:` rows) is loaded
|
|
211
|
+
into the `Relaton::Index` pool in `before(:suite)` (`spec/support/webmock.rb`):
|
|
212
|
+
the YAML is written to a temp file and read through
|
|
213
|
+
`Relaton::Index::Type.new(:etsi, file:, pubid_class: ::Pubid::Etsi::Identifier)`,
|
|
214
|
+
and `type.index` forces the offline `pubid_class` deserialize before the net is
|
|
215
|
+
blocked; `actual?` is overridden to match only the remote (`url:`) lookup so the
|
|
216
|
+
producer-side `find_or_create(:etsi, file:, pubid_class:)` still gets a fresh
|
|
217
|
+
instance. Regenerate by parsing `index-v1`'s ids through `Pubid::Etsi` into a
|
|
218
|
+
`pubid_class` `Type` and re-zipping (`_type: pubid:etsi:…` rows).
|
|
219
|
+
Uses RSpec with VCR for HTTP interaction recording. VCR cassettes are in `spec/vcr_cassettes/`. When tests make new HTTP requests, VCR will record them.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
= Relaton::Etsi
|
|
2
|
+
|
|
3
|
+
Relaton::Etsi is a Ruby gem that implements the https://github.com/relaton/relaton-models#bibliographic-item[BibliographicItem model].
|
|
4
|
+
|
|
5
|
+
You can use it to retrieve metadata of ETSI Standards and access such metadata through the `Relaton::Etsi::Item` object.
|
|
6
|
+
|
|
7
|
+
== Usage
|
|
8
|
+
|
|
9
|
+
=== Search for a standard using keywords
|
|
10
|
+
|
|
11
|
+
[source,ruby]
|
|
12
|
+
----
|
|
13
|
+
require "relaton/etsi"
|
|
14
|
+
=> true
|
|
15
|
+
|
|
16
|
+
item = Relaton::Etsi::Bibliography.get("ETSI GS ZSM 012 V1.1.1")
|
|
17
|
+
[relaton-etsi] INFO: (ETSI GS ZSM 012 V1.1.1) Fetching from Relaton repository ...
|
|
18
|
+
[relaton-etsi] INFO: (ETSI GS ZSM 012 V1.1.1) Found: `ETSI GS ZSM 012 V1.1.1 (2022-12)`
|
|
19
|
+
=> #<Relaton::Etsi::ItemData:0x0000000125869dd8
|
|
20
|
+
...
|
|
21
|
+
----
|
|
22
|
+
|
|
23
|
+
=== XML serialization
|
|
24
|
+
|
|
25
|
+
[source,ruby]
|
|
26
|
+
----
|
|
27
|
+
item.to_xml
|
|
28
|
+
=> "<bibitem id="ETSIGSZSM012V111202212" schema-version="v1.5.6">
|
|
29
|
+
<fetched>2026-01-24</fetched>
|
|
30
|
+
<title language="en" script="Latn">Zero-touch network and Service Management (ZSM); Enablers for Artificial Intelligence-based Network and Service Automation</title>
|
|
31
|
+
<uri type="src">http://webapp.etsi.org/workprogram/Report_WorkItem.asp?WKI_ID=62010</uri>
|
|
32
|
+
<uri type="pdf">http://www.etsi.org/deliver/etsi_gs/ZSM/001_099/012/01.01.01_60/gs_ZSM012v010101p.pdf</uri>
|
|
33
|
+
<docidentifier type="ETSI" primary="true">ETSI GS ZSM 012 V1.1.1 (2022-12)</docidentifier>
|
|
34
|
+
...
|
|
35
|
+
</bibitem>"
|
|
36
|
+
----
|
|
37
|
+
With argument `bibdata: true` it outputs XML wrapped by `bibdata` element and adds flavour `ext` element.
|
|
38
|
+
[source,ruby]
|
|
39
|
+
----
|
|
40
|
+
item.to_xml bibdata: true
|
|
41
|
+
=> "<bibdata schema-version="v1.5.6">
|
|
42
|
+
<fetched>2026-01-24</fetched>
|
|
43
|
+
<title language="en" script="Latn">Zero-touch network and Service Management (ZSM); Enablers for Artificial Intelligence-based Network and Service Automation</title>
|
|
44
|
+
<uri type="src">http://webapp.etsi.org/workprogram/Report_WorkItem.asp?WKI_ID=62010</uri>
|
|
45
|
+
<uri type="pdf">http://www.etsi.org/deliver/etsi_gs/ZSM/001_099/012/01.01.01_60/gs_ZSM012v010101p.pdf</uri>
|
|
46
|
+
<docidentifier type="ETSI" primary="true">ETSI GS ZSM 012 V1.1.1 (2022-12)</docidentifier>
|
|
47
|
+
...
|
|
48
|
+
<ext schema-version="v0.1.2">
|
|
49
|
+
<doctype abbreviation="GS">Group Specification</doctype>
|
|
50
|
+
</ext>
|
|
51
|
+
</bibdata>"
|
|
52
|
+
----
|
|
53
|
+
|
|
54
|
+
=== Typed source links
|
|
55
|
+
|
|
56
|
+
All the ETSI documents have SRC and PDF links. The gem provides a way to access these links through the `RelatonBib::TypedUri` object.
|
|
57
|
+
|
|
58
|
+
[source,ruby]
|
|
59
|
+
----
|
|
60
|
+
item.source
|
|
61
|
+
=>
|
|
62
|
+
[#<Relaton::Bib::Uri:0x0000000129ea7b38 @content="http://webapp.etsi.org/workprogram/Report_WorkItem.asp?WKI_ID=62010", @language=nil, @locale=nil, @script=nil, @type="src">,
|
|
63
|
+
#<Relaton::Bib::Uri:0x0000000129ea76d8 @content="http://www.etsi.org/deliver/etsi_gs/ZSM/001_099/012/01.01.01_60/gs_ZSM012v010101p.pdf", @language=nil, @locale=nil, @script=nil, @type="pdf">]
|
|
64
|
+
|
|
65
|
+
item.source.size
|
|
66
|
+
=> 2
|
|
67
|
+
|
|
68
|
+
item.source[0].type
|
|
69
|
+
=> "src"
|
|
70
|
+
|
|
71
|
+
item.source[0].content
|
|
72
|
+
=> "http://webapp.etsi.org/workprogram/Report_WorkItem.asp?WKI_ID=62010"
|
|
73
|
+
|
|
74
|
+
item.source[1].type
|
|
75
|
+
=> "pdf"
|
|
76
|
+
|
|
77
|
+
item.source[1].content
|
|
78
|
+
=> "http://www.etsi.org/deliver/etsi_gs/ZSM/001_099/012/01.01.01_60/gs_ZSM012v010101p.pdf"
|
|
79
|
+
----
|
|
80
|
+
|
|
81
|
+
=== Create bibliographic item from XML
|
|
82
|
+
[source,ruby]
|
|
83
|
+
----
|
|
84
|
+
Relaton::Etsi::Item.from_xml File.read('spec/fixtures/bibdata.xml')
|
|
85
|
+
=> #<Relaton::Etsi::ItemData:0x000000012b68f660
|
|
86
|
+
...
|
|
87
|
+
----
|
|
88
|
+
|
|
89
|
+
=== Create bibliographic item from YAML
|
|
90
|
+
[source,ruby]
|
|
91
|
+
----
|
|
92
|
+
Relaton::Etsi::Item.from_yaml File.read('spec/fixtures/item.yaml')
|
|
93
|
+
=> #<Relaton::Etsi::ItemData:0x0000000126e34c58
|
|
94
|
+
...
|
|
95
|
+
----
|
|
96
|
+
|
|
97
|
+
=== Fetch data
|
|
98
|
+
|
|
99
|
+
This gem uses the https://www.etsi.org as a data source.
|
|
100
|
+
|
|
101
|
+
The method `Relaton::Etsi::DataFetcher.fetch(output: "data", format: "yaml")` fetches all the documents from the data source and saves them to the `./data` folder in YAML format.
|
|
102
|
+
Arguments:
|
|
103
|
+
|
|
104
|
+
- `output` - folder to save documents (default './data').
|
|
105
|
+
- `format` - the format in which the documents are saved. Possible formats are: `yaml`, `xml`, `bibxml` (default `yaml`).
|
|
106
|
+
|
|
107
|
+
[source,ruby]
|
|
108
|
+
----
|
|
109
|
+
require 'relaton/etsi/data_fetcher'
|
|
110
|
+
=> true
|
|
111
|
+
|
|
112
|
+
Relaton::Etsi::DataFetcher.fetch
|
|
113
|
+
Started at: 2023-12-05 22:44:32 -0500
|
|
114
|
+
Stopped at: 2023-12-05 22:47:55 -0500
|
|
115
|
+
Done in: 204 sec.
|
|
116
|
+
=> nil
|
|
117
|
+
----
|
|
118
|
+
|
|
119
|
+
=== Logging
|
|
120
|
+
|
|
121
|
+
Relaton::Etsi 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.
|
|
122
|
+
|
|
123
|
+
== Development
|
|
124
|
+
|
|
125
|
+
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.
|
|
126
|
+
|
|
127
|
+
To update the index test fixture (used by tests), run `rake spec:update_index`. This downloads the latest `index-v2.zip` (pubid-structured `_type: pubid:etsi:…` rows) from the https://github.com/relaton/relaton-data-etsi[relaton-data-etsi] repository.
|
|
128
|
+
|
|
129
|
+
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).
|
|
130
|
+
|
|
131
|
+
== Contributing
|
|
132
|
+
|
|
133
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton-etsi.
|
|
134
|
+
|
|
135
|
+
== License
|
|
136
|
+
|
|
137
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,120 @@
|
|
|
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-evs retrieves Estonian standardisation (EVS, Eesti Standardimis- ja
|
|
8
|
+
Akrediteerimiskeskus) bibliographic data using the Relaton model. Like CEN/BSI,
|
|
9
|
+
it publishes **no index and has no data repo**: the flavor **scrapes** the EVS
|
|
10
|
+
e-shop (`www.evs.ee`) with Mechanize. Reference handling is entirely
|
|
11
|
+
`Pubid::Evs` (the `:evs` flavor, registered in pubid's registry). EVS records
|
|
12
|
+
are dominated by **adoptions**: `EVS-EN …` adopts a European document,
|
|
13
|
+
`EVS-EN ISO …` a European adoption of an international one, `EVS-ISO …` /
|
|
14
|
+
`EVS-IEC …` adopt international documents directly. The adoption is expressed
|
|
15
|
+
with `adoptedFrom` relations parsed from the page's "Base Documents" group.
|
|
16
|
+
|
|
17
|
+
## Development
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle exec rake spec:evs # the EVS suite
|
|
21
|
+
cd spec/evs && bundle exec rspec -I . relaton/evs/docidentifier_spec.rb
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
(No gem-local rubocop config; the monorepo root config applies.)
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
Namespace: `Relaton::Evs`. This is the **BSI/CEN shape**: parse the query with
|
|
29
|
+
pubid, select the hit with pubid, back the `Docidentifier` with pubid. No
|
|
30
|
+
`INDEXFILE`, no `pubid_class:`, no data repo. **Don't add an index** (unless a
|
|
31
|
+
`relaton-data-evs` repo appears — relaton#164 tracks that possibility).
|
|
32
|
+
|
|
33
|
+
Retrieval flow:
|
|
34
|
+
|
|
35
|
+
1. **Bibliography** (`lib/relaton/evs/bibliography.rb`) — `get(code, year,
|
|
36
|
+
opts)` / `search` / `parse`. `parse` wraps `Pubid::Evs.parse` and **raises**
|
|
37
|
+
on an unrecognized query (root CLAUDE.md law); only the data side rescues,
|
|
38
|
+
in `Hit#pubid`. `get` keeps an early `nil` for an **empty** reference and
|
|
39
|
+
accepts the flavor's own pubid without mutating it (`Relaton::Db` hands it
|
|
40
|
+
the parse it routed). Transport errors are rescued **by name**
|
|
41
|
+
(`Net::ReadTimeout`, `Net::OpenTimeout` → `Relaton::RequestError`, which
|
|
42
|
+
`Db#net_retry` retries).
|
|
43
|
+
2. **HitCollection** (`hit_collection.rb`) — the search is a **direct slug
|
|
44
|
+
probe**, not a form submit: the catalogue URL is the printed reference
|
|
45
|
+
slugified (`EVS-EN 18216:2026` → `/en/evs-en-18216-2026`), so the product
|
|
46
|
+
page exists iff the reference denotes a published document. A miss
|
|
47
|
+
**redirects** to `/page-not-found` (HTTP 302, followed by Mechanize) — that
|
|
48
|
+
redirect target is the "no hit" signal; a hard 4xx/5xx raises
|
|
49
|
+
`Mechanize::ResponseCodeError` and is a miss (`hits` rescues to `[]`).
|
|
50
|
+
The `et`/`et-en` language codes probe the `/et/` locale, which serves the
|
|
51
|
+
Estonian-language product.
|
|
52
|
+
- **The hit's `code` keeps the language code** (only the slug derivation
|
|
53
|
+
strips it): the filter `query === hit.pubid` needs the language ON the
|
|
54
|
+
hit for a language-carrying query to match its own record.
|
|
55
|
+
- A reference **without a year** probes `…-<number>` and misses — the
|
|
56
|
+
catalogue URL always carries the year. "Not found." is honest there; do
|
|
57
|
+
not add a search-endpoint fallback without evidence the shop renders one
|
|
58
|
+
server-side (its search is client-rendered).
|
|
59
|
+
- Slug shapes verified against the live catalogue: plain `EVS 945:2023`,
|
|
60
|
+
`EVS-EN 16798-1:2019`, `EVS-EN ISO 14001:2026`, the ETSI face
|
|
61
|
+
`EVS-EN 301 406-1 V3.2.1:2026`, `EVS-EN IEC 61000-4-30:2026`.
|
|
62
|
+
3. **Hit** (`hit.rb`) — `item` lazy-loads the detail page via the scraper;
|
|
63
|
+
`pubid` memoizes the hit code parsed with pubid and is **nil** when the
|
|
64
|
+
grammar cannot read it.
|
|
65
|
+
4. **Scraper** (`scraper.rb`) — Mechanize/XPath over the product page:
|
|
66
|
+
`h1` → docidentifier, `h2.product-title` → title (`Bib::Title.from_string`
|
|
67
|
+
splits " - " into intro/main — it returns an Array, don't re-wrap), the
|
|
68
|
+
"Valid from dd.mm.yyyy" badge → `published` date, "Base Documents" →
|
|
69
|
+
`adoptedFrom` relations (a double adoption lists both bases in ONE text
|
|
70
|
+
node, `ISO 14001:2026; EN ISO 14001:2026` — split on the semicolon), the
|
|
71
|
+
scope tab (`div#tab-1`) → abstract, ICS group links → `Ext.ics` via
|
|
72
|
+
`Isoics`. Contributor/copyright: the EVS organization (publisher); place
|
|
73
|
+
Tallinn.
|
|
74
|
+
5. **ItemData / models** (`item_data.rb`, `model/`) — `Relaton::Evs::ItemData`
|
|
75
|
+
extends `Relaton::Bib::ItemData`; `Ext` adds a `StructuredIdentifier`
|
|
76
|
+
(docnumber/partnumber from the pubid **root**, agency "EVS"). There is no
|
|
77
|
+
`relaton-model-evs` gem, so `Ext` does not override `get_schema_version`
|
|
78
|
+
(inherits the nil default, like the other ten model-less flavors). The
|
|
79
|
+
**shared ext grammar requires a `doctype`** — `fetch_doctype` always emits
|
|
80
|
+
one (`standard`; `technical-specification`/`technical-report` for the
|
|
81
|
+
`EVS-TS`/`EVS-TR` faces), or Jing fails on `relaton-evs-compile.rng`.
|
|
82
|
+
|
|
83
|
+
### `#root` is the accessor that answers for every form
|
|
84
|
+
|
|
85
|
+
A `NationalAdoption` (`EVS-EN ISO 9001:2015/A1:2024`) holds the origin
|
|
86
|
+
standard's number and year behind its `base` chain (base = the CEN/CENELEC
|
|
87
|
+
document; `AdoptedEuropeanNorm#adopted` = the ISO document; `#root` recurses),
|
|
88
|
+
so the year, number and part are read from `#root`. An `InternationalAdoption`
|
|
89
|
+
(`EVS-ISO 9001:2015`) or `EstonianStandard` (`EVS 945:2023`) is its own root.
|
|
90
|
+
|
|
91
|
+
### `Docidentifier` — the year rule
|
|
92
|
+
|
|
93
|
+
`Relaton::Evs::Docidentifier` (`model/docidentifier.rb`, `< Bib::Docidentifier`)
|
|
94
|
+
parses its `content` into `@pubid` (DATA side: lazy `require "pubid"`, rescues
|
|
95
|
+
`LoadError`/`StandardError`; a foreign value stays verbatim with `@pubid` nil).
|
|
96
|
+
`Pubid#exclude` returns a **copy**, so each mutator replaces `@pubid` and
|
|
97
|
+
re-renders through the aliased `store_content`. **`remove_date!` drops the
|
|
98
|
+
LAST year**: on a supplement (`EVS-EN ISO 9001:2015/A1:2024`) the supplement's
|
|
99
|
+
year goes first (`exclude(:supplement_year)` → `…/A1`), the base year only
|
|
100
|
+
when there is none. `to_all_parts!` wraps in pubid's `AllParts` — `content`
|
|
101
|
+
stays the part-and-date-stripped string on purpose; `#pubid` renders the
|
|
102
|
+
"(all parts)" marker.
|
|
103
|
+
|
|
104
|
+
### Printed separator is identity
|
|
105
|
+
|
|
106
|
+
`EVS EN 18216:2026` (space) and `EVS-EN 18216:2026` (hyphen) parse to the same
|
|
107
|
+
class and root, but each renders back **as written** — pubid preserves the
|
|
108
|
+
input's separator. Never compare EVS identifiers by `to_s`; compare by parse
|
|
109
|
+
(`===`, `root.number`).
|
|
110
|
+
|
|
111
|
+
### Registration
|
|
112
|
+
|
|
113
|
+
`Processor` (`processor.rb`) sets `@short = :relaton_evs`, `@prefix = "EVS"`,
|
|
114
|
+
`@pubid_flavor = :Evs` (prefixes sourced from `Pubid::Evs.prefixes`) and
|
|
115
|
+
`@defaultprefix = %r{^EVS[\s-]}`. It lazy-`require_relative`s `../evs` inside
|
|
116
|
+
each method, like every other flavor (`spec/relaton/lazy_loading_spec.rb`
|
|
117
|
+
guards that). `lib/relaton.rb` autoloads `:Evs`; `Db::Registry::SUPPORTED_GEMS`
|
|
118
|
+
lists `relaton/evs`; `grammar/relaton-evs(-compile).rng` mirrors the CEN pair
|
|
119
|
+
(the shared `biblio-standoc` ext already models doctype/flavor/ics/
|
|
120
|
+
structuredidentifier, and its `DocRelationType` carries `adoptedFrom`).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
= relaton-evs
|
|
2
|
+
|
|
3
|
+
EVS is the Estonian national corpus of the https://github.com/relaton/relaton[Relaton] bibliographic model.
|
|
4
|
+
|
|
5
|
+
Retrieves Estonian standardisation records (EVS, Eesti Standardimis- ja Akrediteerimiskeskus) from the https://www.evs.ee[EVS e-shop].
|
|
6
|
+
|
|
7
|
+
- Plain Estonian standards: `EVS 945:2023`, the `EVS-TS`/`EVS-TR` faces
|
|
8
|
+
- National adoptions: `EVS-EN 18216:2026`, `EVS-EN ISO 9001:2015/A1:2024`, `EVS-EN ISO/IEC 27017:2026`
|
|
9
|
+
- Direct international adoptions: `EVS-ISO 9001:2015`, `EVS-IEC 62368-1:2020`
|
|
10
|
+
- Language codes: `EVS 811:2018 et`, `et-en`
|
|
11
|
+
|
|
12
|
+
Reference parsing is entirely https://github.com/pubid/pubid[`Pubid::Evs`]; adoptions are expressed as `adoptedFrom` relations to the base documents.
|
|
13
|
+
|
|
14
|
+
== Usage
|
|
15
|
+
|
|
16
|
+
[source,ruby]
|
|
17
|
+
----
|
|
18
|
+
require "relaton/evs"
|
|
19
|
+
|
|
20
|
+
item = Relaton::Evs::Bibliography.get "EVS-EN ISO 14001:2026"
|
|
21
|
+
#=> #<Relaton::Evs::ItemData …> with adoptedFrom relations to
|
|
22
|
+
# "ISO 14001:2026" and "EN ISO 14001:2026"
|
|
23
|
+
----
|
|
24
|
+
|
|
25
|
+
== Development
|
|
26
|
+
|
|
27
|
+
Run the suite from the repository root: `bundle exec rake spec:evs`.
|
|
@@ -0,0 +1,47 @@
|
|
|
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-gb retrieves Chinese National Standards (Guobiao — `GB`, `GB/T`, `GB/Z`, plus social-org and sector standards) using the Relaton model. It scrapes several Chinese government portals (`openstd.samr.gov.cn`, `ttbz.org.cn`, `std.gov.cn`) with Mechanize. Depends on relaton-iso for ISO-based item/extension models, and on Chinese-specific classifiers (`cnccs`, `gb-agencies`).
|
|
8
|
+
|
|
9
|
+
## Development
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle exec rake # default task → rspec
|
|
13
|
+
bundle exec rspec spec/relaton/gb/bibliography_spec.rb # single file
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
(RuboCop is configured via the shared `.hound.yml`; the monorepo root config applies otherwise.)
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
Namespace: `Relaton::Gb`. Note: GB places its model classes at the top of `lib/relaton/gb/` (`item.rb`, `bibitem.rb`, `bibdata.rb`, `ext.rb`, …) rather than under a `model/` subdir.
|
|
21
|
+
|
|
22
|
+
Retrieval flow:
|
|
23
|
+
|
|
24
|
+
1. **Bibliography** (`lib/relaton/gb/bibliography.rb`) — `get`/`search` dispatch by prefix: `GB*` → `GbScraper` (openstd.samr.gov.cn), `T/` → `TScraper` (ttbz.org.cn), others → `SecScraper` (std.gov.cn).
|
|
25
|
+
2. **Scrapers** (`scraper.rb` + `gb_scraper.rb` / `t_scraper.rb` / `sec_scraper.rb`) — scrape the relevant portal for hits, then scrape the detail page into an `ItemData`.
|
|
26
|
+
3. **HitCollection / Hit** (`hit_collection.rb`, `hit.rb`, extend the `Relaton::Core` bases) — `Hit#item` lazy-loads the document; a hit holds `pid`, `docref`, `release_date`, `status`.
|
|
27
|
+
4. **Item / ItemData / Ext** (`item.rb`, `item_data.rb`, `ext.rb`) — `Item` extends `Bib::Item`; `Ext` carries GB-specific fields (`gbtype`, `ccs`, `stagename`, `plannumber`). Supporting models: `gb_type.rb`, `ccs.rb` (Chinese classification), `stage_name.rb`, `committee.rb`, `structured_identifier.rb`.
|
|
28
|
+
5. **Processor** (`lib/relaton/gb/processor.rb`) — registry integration; `@prefix = "CN"`, `@defaultprefix = %r{^(GB|GB/T|GB/Z) }`, `@pubid_flavor = :Gb`. Lazy-`require_relative`s `../gb` in its methods.
|
|
29
|
+
|
|
30
|
+
## Identifiers (pubid)
|
|
31
|
+
|
|
32
|
+
GB parses identifiers with `Pubid::Gb::Identifier`. It publishes no index, so it has no `INDEXFILE` and no `pubid_class:` (the BSI/CEN shape, see the root `CLAUDE.md`). The attributes it reads are `publisher`, `mandate`, `number`, `part`, `year` and `all_parts`.
|
|
33
|
+
|
|
34
|
+
- **Query side raises.** `Bibliography.get` parses the caller's code without a rescue, so a reference that is not a GB identifier raises `Pubid::Errors::ParseError`. `get` also takes the `Pubid::Gb::Identifier` that `Relaton::Db` parsed (relaton#205) and never mutates it: an `AllParts` pubid (or a String `(all parts)`) sets `all_parts` and unwraps its identifier, and every change after that is on the `exclude(:year)` copy. The year comes from the argument or from `pubid.year`; `all_parts` sets `part = "1"` on the copy to find part 1. `search` still routes on the raw string (`GB|GJ|GS` → `GbScraper`, `T/…` → `TScraper`, else `SecScraper`), so the portal for each reference and the cassettes are unchanged.
|
|
35
|
+
- **Hit filter.** `search_filter` parses each `hit.docref` (rescued: a docref is data; an unparseable one drops the hit) and keeps the hits that `query.matches?`. It ignores `:year` only when the query has no year. It **never** ignores `:part`: `GB/T 20223` and `GB/T 20223.1` are two documents, and `gb_spec.rb` asserts that `GB/T 20223-2006` does not return `GB/T 20223.1-2006`. This is a deliberate departure from the migration hand-off, which proposed to ignore a part the query omits. It also stops `GB/T 1.1` from matching `GB/T 1.10`, which the old substring compare allowed.
|
|
36
|
+
- **`Docidentifier`** (`docidentifier.rb`) follows the CEN/OMG shape: `content=` parses into `@pubid` (rescued, data side), and the mutators replace the pubid with an `exclude` copy and write back through `store_content`. `to_all_parts!` excludes part and year, sets `all_parts`, and pubid renders the ` (all parts)` suffix itself (and parses it back). A nil pubid makes every mutator a no-op.
|
|
37
|
+
- **Scrapers** (`scraper.rb`) read the docref through a rescued `docref_pubid`. `get_prefix` keys `yaml/prefixes.yaml` with the first segment of `publisher` (a `Pubid::Components::Publisher`, so read it as `publisher.to_s`; `T/GZAEPI` → `T`). An unknown key gives nil, and `get_gbtype` keeps a nil prefix rather than raising — a portal docref is data. `get_mandate` maps `mandate` `T` → recommended, `Z` → guidelines, nil → mandatory. `get_contributors` passes `publisher.to_s` to `gb_agencies`, which names the same organization for `GB` and `GB/T`. `parse_docref` returns `[id without part and year, part, year]`; the first value is `plannumber`.
|
|
38
|
+
- **Pubid traps.** `Pubid::Gb` accepts any upper-case publisher code (`ISO 123` parses), so use `foo` or `GB/T` for a parse-error example. `year` is a reader over `date` (no `year=`); set it with `date = Pubid::Components::Date.new(year:)`. Local (`DB11/T`) forms do not parse. pubid PR #388 (`dcea7380`) renamed `publisher_code` to the `publisher` component, added the year to `to_hash`, and made `GBn` parse; a pubid older than that breaks this flavor. pubid renders an em dash (`T/GZAEPI 001—2018`) as a hyphen; the fixture `tgzaepi_001_2018.xml` still records the em dash, and its spec is pending until pubid decides.
|
|
39
|
+
- **Prefix register.** `@pubid_flavor = :Gb` sources `Relaton.prefix_flavor` from `Pubid::Gb.prefixes`, so `GB`, `GB/T`, `JB/T`, `T/` … resolve to `Relaton::Gb`. `CN` is not a pubid prefix, so `prefix_flavor("CN")` is `[]`; no caller in this repo used it, and `Db` routing still reads `@prefix`. `HB` resolves to `[Gb, Nist, Bsi]`, pinned in `spec/relaton/prefix_flavor_spec.rb`.
|
|
40
|
+
|
|
41
|
+
## External dependencies
|
|
42
|
+
|
|
43
|
+
`cnccs ~> 0.1.1`, `gb-agencies ~> 0.0.1`, `mechanize ~> 2.10`, `csv ~> 3.0`, `relaton-core`, `relaton-iso`.
|
|
44
|
+
|
|
45
|
+
## Testing
|
|
46
|
+
|
|
47
|
+
RSpec with WebMock + VCR (cassettes in `spec/vcr_cassettes/`). Network is blocked via WebMock. SimpleCov and equivalent-xml are loaded via `spec/support/`. No index fixture — GB is scraped, not indexed; `spec/fixtures/` holds XML/YAML round-trip data.
|