relaton 3.0.0.pre.alpha.16 → 3.0.0.pre.alpha.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/data/equivalence/adoption-chains.yaml +38 -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/series.rb +1 -1
  9. data/lib/relaton/bipm/CLAUDE.md +173 -0
  10. data/lib/relaton/bipm/README.adoc +352 -0
  11. data/lib/relaton/bipm/data_fetcher.rb +3 -2
  12. data/lib/relaton/bsi/CLAUDE.md +35 -0
  13. data/lib/relaton/bsi/README.adoc +112 -0
  14. data/lib/relaton/calconnect/CLAUDE.md +177 -0
  15. data/lib/relaton/calconnect/README.adoc +140 -0
  16. data/lib/relaton/ccsds/CLAUDE.md +74 -0
  17. data/lib/relaton/ccsds/README.adoc +154 -0
  18. data/lib/relaton/cen/CLAUDE.md +219 -0
  19. data/lib/relaton/cen/README.adoc +155 -0
  20. data/lib/relaton/cie/CLAUDE.md +90 -0
  21. data/lib/relaton/cie/README.adoc +121 -0
  22. data/lib/relaton/core/CLAUDE.md +140 -0
  23. data/lib/relaton/core/README.adoc +64 -0
  24. data/lib/relaton/core/data_fetcher.rb +50 -3
  25. data/lib/relaton/db/CLAUDE.md +373 -0
  26. data/lib/relaton/db.rb +19 -1
  27. data/lib/relaton/doi/CLAUDE.md +105 -0
  28. data/lib/relaton/doi/README.adoc +89 -0
  29. data/lib/relaton/easc/CLAUDE.md +85 -0
  30. data/lib/relaton/ecma/CLAUDE.md +217 -0
  31. data/lib/relaton/ecma/README.adoc +178 -0
  32. data/lib/relaton/equivalence.rb +89 -0
  33. data/lib/relaton/etsi/CLAUDE.md +219 -0
  34. data/lib/relaton/etsi/README.adoc +137 -0
  35. data/lib/relaton/evs/CLAUDE.md +120 -0
  36. data/lib/relaton/evs/README.adoc +27 -0
  37. data/lib/relaton/gb/CLAUDE.md +47 -0
  38. data/lib/relaton/gb/README.adoc +169 -0
  39. data/lib/relaton/gost/CLAUDE.md +100 -0
  40. data/lib/relaton/iala/CLAUDE.md +97 -0
  41. data/lib/relaton/iana/CLAUDE.md +105 -0
  42. data/lib/relaton/iana/README.adoc +107 -0
  43. data/lib/relaton/iec/CLAUDE.md +13 -0
  44. data/lib/relaton/iec/README.adoc +280 -0
  45. data/lib/relaton/iec/data_fetcher.rb +24 -8
  46. data/lib/relaton/iec/project_fetcher.rb +48 -0
  47. data/lib/relaton/iec/project_parser.rb +111 -0
  48. data/lib/relaton/iec.rb +5 -0
  49. data/lib/relaton/ieee/CLAUDE.md +176 -0
  50. data/lib/relaton/ieee/README.adoc +131 -0
  51. data/lib/relaton/ietf/CLAUDE.md +357 -0
  52. data/lib/relaton/ietf/README.adoc +167 -0
  53. data/lib/relaton/iho/CLAUDE.md +70 -0
  54. data/lib/relaton/iho/README.adoc +119 -0
  55. data/lib/relaton/index/CLAUDE.md +207 -0
  56. data/lib/relaton/index/README.adoc +221 -0
  57. data/lib/relaton/index/dataset_lock.rb +122 -0
  58. data/lib/relaton/index/family.rb +63 -0
  59. data/lib/relaton/index/type.rb +32 -0
  60. data/lib/relaton/index.rb +3 -0
  61. data/lib/relaton/isbn/CLAUDE.md +47 -0
  62. data/lib/relaton/isbn/README.adoc +91 -0
  63. data/lib/relaton/iso/CLAUDE.md +38 -0
  64. data/lib/relaton/iso/README.adoc +370 -0
  65. data/lib/relaton/iso/data_fetcher.rb +16 -1
  66. data/lib/relaton/itu/CLAUDE.md +685 -0
  67. data/lib/relaton/itu/README.adoc +188 -0
  68. data/lib/relaton/jcgm/CLAUDE.md +107 -0
  69. data/lib/relaton/jis/CLAUDE.md +64 -0
  70. data/lib/relaton/jis/README.adoc +167 -0
  71. data/lib/relaton/logger/CLAUDE.md +37 -0
  72. data/lib/relaton/logger/README.adoc +120 -0
  73. data/lib/relaton/nist/CLAUDE.md +180 -0
  74. data/lib/relaton/nist/README.adoc +389 -0
  75. data/lib/relaton/nist/bibliography.rb +17 -1
  76. data/lib/relaton/nist/data_fetcher.rb +60 -4
  77. data/lib/relaton/nist/hit_collection.rb +121 -59
  78. data/lib/relaton/nist/scraper.rb +21 -7
  79. data/lib/relaton/oasis/CLAUDE.md +251 -0
  80. data/lib/relaton/oasis/README.adoc +132 -0
  81. data/lib/relaton/ogc/CLAUDE.md +242 -0
  82. data/lib/relaton/ogc/README.adoc +134 -0
  83. data/lib/relaton/oiml/CLAUDE.md +47 -0
  84. data/lib/relaton/oiml/README.adoc +70 -0
  85. data/lib/relaton/omg/CLAUDE.md +137 -0
  86. data/lib/relaton/omg/README.adoc +101 -0
  87. data/lib/relaton/plateau/CLAUDE.md +126 -0
  88. data/lib/relaton/plateau/README.adoc +139 -0
  89. data/lib/relaton/sdo/CLAUDE.md +81 -0
  90. data/lib/relaton/un/CLAUDE.md +91 -0
  91. data/lib/relaton/un/README.adoc +125 -0
  92. data/lib/relaton/version.rb +1 -1
  93. data/lib/relaton/w3c/CLAUDE.md +369 -0
  94. data/lib/relaton/w3c/README.adoc +140 -0
  95. data/lib/relaton/w3c/data_fetcher.rb +1 -1
  96. data/lib/relaton/xsf/CLAUDE.md +189 -0
  97. data/lib/relaton/xsf/README.adoc +127 -0
  98. data/lib/relaton.rb +1 -0
  99. metadata +83 -2
@@ -0,0 +1,242 @@
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-ogc is a Ruby gem for retrieving and managing OGC (Open Geospatial Consortium) Standards metadata. It is part of the Relaton ecosystem for bibliographic data management. The gem implements the OGC flavor on top of `relaton-iso`, which itself builds on `relaton-bib`.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ # Install dependencies
13
+ bundle install
14
+
15
+ # Run all tests
16
+ bundle exec rspec
17
+
18
+ # Run a single test file
19
+ bundle exec rspec spec/relaton/ogc/item_spec.rb
20
+
21
+ # Run a single test by line number
22
+ bundle exec rspec spec/relaton/ogc/item_spec.rb:7
23
+
24
+ # Lint
25
+ bundle exec rubocop
26
+
27
+ # Lint with auto-correct
28
+ bundle exec rubocop -A
29
+
30
+ # Interactive console
31
+ bin/console
32
+
33
+ # Build and install gem locally
34
+ bundle exec rake install
35
+ ```
36
+
37
+ ## Architecture
38
+
39
+ ### Class Hierarchy
40
+
41
+ All model classes use `Lutaml::Model::Serializable` for XML/YAML serialization with declarative attribute and mapping definitions.
42
+
43
+ ```
44
+ Iso::Item (from relaton-iso)
45
+ └── Relaton::Ogc::Item # serializer, extends Iso::Item with `ext` and `docidentifier`
46
+ ├── Relaton::Ogc::Bibitem # includes Bib::BibitemShared
47
+ └── Relaton::Ogc::Bibdata # includes Bib::BibdataShared
48
+
49
+ Bib::ItemData (from relaton-bib)
50
+ └── Relaton::Ogc::ItemData # data model returned by Item.from_xml / Item.from_yaml
51
+ ```
52
+
53
+ - **Item** (`lib/relaton/ogc/item.rb`) — serializer; extends `Iso::Item`, declares `model ItemData`, adds `ext` (Ext) and `docidentifier` (Docidentifier) attributes
54
+ - **ItemData** (`lib/relaton/ogc/item_data.rb`) — plain data model extending `Bib::ItemData`
55
+ - **Ext** (`lib/relaton/ogc/ext.rb`) — OGC extension data: doctype, subdoctype (with constrained values)
56
+ - **Doctype** (`lib/relaton/ogc/doctype.rb`) — enumerated OGC document types (standard, engineering-report, best-practice, etc.)
57
+ - **Docidentifier** (`lib/relaton/ogc/docidentifier.rb`) — parses its `content`
58
+ into a `Pubid::Ogc::Identifier` exposed as `#pubid`; see **Pubid-backed
59
+ docidentifier**
60
+ - **Bibitem** / **Bibdata** — thin wrappers mixing in shared serialization behavior from `relaton-bib`
61
+
62
+ ### Search & Fetch Pipeline
63
+
64
+ - **Bibliography** (`lib/relaton/ogc/bibliography.rb`) — entry point: `search(text)` and `get(code, year, opts)`
65
+ - **HitCollection** (`lib/relaton/ogc/hit_collection.rb`) — fetches from the `relaton-data-ogc` repo (`v2` branch) and resolves the reference against the index; see **Index (`index-v2`, pubid-keyed)**
66
+ - **Hit** (`lib/relaton/ogc/hit.rb`) — wraps a single search result; lazy-fetches `ItemData` on `#item`
67
+ - **Processor** (`lib/relaton/ogc/processor.rb`) — `Core::Processor` implementation for Relaton registry integration
68
+
69
+ ### Pubid-backed docidentifier
70
+
71
+ `Relaton::Ogc::Docidentifier` parses its `content` into a
72
+ `Pubid::Ogc::Identifier` kept in `@pubid`, while the lutaml `content` attribute
73
+ stays a plain **string** for serialization. Parsing is **soft**: `content=`
74
+ lazily requires pubid and rescues `LoadError`/`StandardError`, so a missing gem
75
+ or non-OGC content leaves `@pubid` nil rather than raising.
76
+
77
+ It was an empty subclass before, which meant it inherited
78
+ `Bib::Docidentifier`'s `remove_part!` / `remove_date!` / `to_all_parts!` — each
79
+ of which raises `NotImplementedError`. `Bib::ItemData` broadcasts all three to
80
+ every docidentifier (`item_data.rb:74`, `:87`), so `#to_all_parts` and
81
+ `#to_most_recent_reference` **raised on every OGC item**, and because
82
+ `NotImplementedError` descends from `ScriptError` a caller's `rescue => e` did
83
+ not catch it. The mapping is OGC-specific:
84
+
85
+ - **`remove_date!` → clears `revision`.** OGC carries no date component;
86
+ `revision` is its version discriminator — the same component a reference may
87
+ omit, which `HitCollection#best_match` then treats as a wildcard — so clearing
88
+ it yields the version-agnostic ("most recent") reference: `12-128r19` →
89
+ `12-128`.
90
+ - **`year` is never cleared.** It looks date-like but is half the document
91
+ number (`12-128`), not a publication qualifier; dropping it would leave
92
+ `-128`, which identifies nothing.
93
+ - **`remove_part!` → clears the (unused) `part`/`subpart`.** A no-op for the
94
+ rendered string today — OGC models no part — but implemented so it never
95
+ raises, and it starts working if pubid-ogc ever adds one. (The IALA
96
+ precedent.)
97
+ - **`to_all_parts!` → both, then wraps `@pubid` in pubid's `AllParts`.**
98
+ `content` stays the plain stripped id — OGC's own renderer has no
99
+ "(all parts)" marker, and `content` is cached from the pre-wrap pubid on
100
+ purpose. `#pubid` itself, read directly, now answers `all_parts? == true`
101
+ and renders WITH pubid's generic marker (`#to_s`), since it's the wrapper.
102
+
103
+ `refresh_content!` renders a bare `to_s`. The OGC printed form carries no
104
+ publisher token and the stored `content` does not either, so — unlike 3GPP,
105
+ whose `refresh_content!` must pass `with_publisher: true` — this one must not.
106
+
107
+ ### Index (`index-v2`, pubid-keyed)
108
+
109
+ `INDEXFILE` is the pubid-backed `index-v2`: rows are `Pubid::Ogc::Identifier`
110
+ hashes (`_type: pubid:ogc:document` with `year`/`number`/`revision`). The
111
+ consumer (`HitCollection#index`) and the producer (`DataFetcher#index`) pass
112
+ `pubid_class: ::Pubid::Ogc::Identifier`. That is what makes `Relaton::Index`
113
+ deserialize the rows into identifiers, sort them by `id.root.number`, and let
114
+ `Type#search` bsearch. Omitting it on the **producer** side writes v1-shaped
115
+ rows under a v2 name, silently (`FileIO#save` only calls `to_hash` when the
116
+ value is an instance of `pubid_class`); omitting it on the **consumer** side
117
+ leaves the rows raw hashes with `FileIO#sorted` false, so every lookup scans
118
+ all ~1,258 rows. `Processor#remove_index_file` passes `url: true` and `file:`
119
+ only: the delete never reads the index (see `lib/relaton/index/CLAUDE.md`).
120
+
121
+ `HitCollection#best_match` follows the ETSI/W3C/IALA idiom:
122
+
123
+ - **Pass the pubid, not the string.** `Type#search_candidates` narrows only
124
+ when the argument is not a `String`, so passing the reference text — which
125
+ this flavor used to do — disables the binary search however the index was
126
+ built. `pubid_class:` alone fixes nothing; both had to change together.
127
+ - **Selection is pubid's asymmetric subset match** (`Pubid::SubsetMatch`):
128
+ `pubid === r[:id]`, the reference on the left. A component the reference omits
129
+ matches any value, so a bare `OGC 12-128` finds the `r19` row, while
130
+ `OGC 12-128r19` reaches only its own. `revision` is OGC's only optional
131
+ component. Measured over the whole fixture index (1,258 rows, 19,590
132
+ comparisons): the subset match and the old `matches?(…, ignore: [:revision])`
133
+ agree on every pair.
134
+ - **`year` is always stated.** The bsearch key is the `<nnn>` field alone, so
135
+ one bucket holds every year that reused the number — `05-015`, `08-015r2`,
136
+ `26-015r1` all sit in bucket `015` (179 buckets, largest 20). `year` is what
137
+ tells them apart.
138
+ - **An unparseable reference raises**, and the previous full-scan substring
139
+ match on `id.to_s` is gone with the rescue: it answered an ambiguous
140
+ reference with whichever row sorted first.
141
+
142
+ #### Ordering: latest revision wins
143
+
144
+ `min_by { |r| r[:id] }` compared raw strings, so a bare `12-128` returned
145
+ `r10` — `"r10" < "r14"` as text. The key is now the revision, with its number
146
+ compared **as an integer** (`r19` beats `r2`) and the token itself breaking the
147
+ tie (`r3a` above `r3`, `r12a` above `r12`), because the index sort is not
148
+ stable.
149
+
150
+ Scored against each document's own `date[0].at` over the 106 multi-revision
151
+ documents in the published index:
152
+
153
+ | Key | Picks the newest published document |
154
+ |---|---|
155
+ | revision number, numeric | 105/106 — **99%** |
156
+ | the old `min_by { r[:id] }` | 1/106 — **1%** |
157
+
158
+ Only 106 of 1,110 documents carry more than one revision, so the change is
159
+ narrow — but for those it was returning the oldest revision almost every time.
160
+
161
+ #### Two ids that do not round-trip
162
+
163
+ Measured over all 1,258 published rows, 1,256 render back byte-identically. The
164
+ two that do not are **data defects upstream**, which pubid canonicalizes:
165
+ `"20-001r2 "` (trailing space) and `"11-038R2"` (uppercase revision). Migrating
166
+ fixes both — the v2 rows store the canonical form — so a lookup of either
167
+ spelling now resolves. Do not "fix" them by special-casing the parser.
168
+
169
+ #### Verified against the published index
170
+
171
+ `relaton-data-ogc` publishes `index-v2.zip`. Loaded through
172
+ `HitCollection#index`: 1,258 rows, all deserialized to identifiers, sorted, **0**
173
+ keying on `""`, and a `12-128` lookup narrows to **11 of 1,258**. Live fetches
174
+ resolve every shape the flavor has to handle — a bare `OGC 12-128` → `r19`
175
+ (2024-02-06), an explicit `12-128r14` → 2017, the letter-suffixed `12-128r12a`,
176
+ the two upstream data defects (`11-038R2`, `20-001r2`), the revision-less
177
+ `16-079`, both sides of the shared `015` bucket (`05-015` and `26-015r1`), and a
178
+ reference with no `OGC ` token.
179
+
180
+ The published rows are byte-for-byte what this gem's `DataFetcher` writes: when
181
+ the fixture was still being converted from `index-v1` locally, the converted set
182
+ and the published one held the same 1,258 rows.
183
+
184
+ ### Data Fetching
185
+
186
+ - **DataFetcher** (`lib/relaton/ogc/data_fetcher.rb`) — bulk-fetches OGC documents from the NamingAuthority JSON endpoint
187
+ - **Scraper** (`lib/relaton/ogc/scraper.rb`) — parses individual OGC document JSON into `ItemData`
188
+
189
+ ### Key Dependencies
190
+
191
+ - `relaton-iso` — provides `Iso::Item` base class and ISO bibliographic structures
192
+ - `relaton-bib` (transitive via relaton-iso) — core types: `Bib::ItemData`, `Bib::Doctype`, `Bib::Ext`, etc.
193
+ - `lutaml-model` (transitive) — serialization framework for XML/YAML mapping
194
+ - `faraday` — HTTP client for fetching remote data
195
+ - `relaton-index` — index-based document lookup from GitHub-hosted data repos
196
+
197
+ ### Serialization Pattern
198
+
199
+ Models declare attributes and XML mappings in the same class. `Item` is the serializer (handles `from_xml`, `from_yaml`, `to_xml`), `ItemData` is the plain data object returned. Round-trip fidelity (parse → serialize → parse) is the primary correctness criterion tested in specs.
200
+
201
+ ### Test Setup
202
+
203
+ - **RSpec** with `--format documentation`
204
+ - **VCR** + **WebMock** for HTTP interaction recording (`spec/vcr_cassettes/`)
205
+ - **Jing** for RelaxNG schema validation against `grammars/relaton-ogc-compile.rng`
206
+ - **equivalent-xml** for XML comparison in tests
207
+ - Fixtures live in `spec/fixtures/` (YAML, XML)
208
+ - Integration tests in `spec/relaton/ogc/integration_spec.rb` exercise the full search → fetch → serialize pipeline
209
+
210
+ ### Grammars
211
+
212
+ RelaxNG schemas in `grammars/` define the valid XML structure. `relaton-ogc-compile.rng` is the combined schema used for test validation.
213
+
214
+ ### API Notes
215
+
216
+ - `Bib::Docidentifier` uses `.content` (not `.id`) for the identifier string
217
+ - `Bib::Date` uses `.at` returning a `StringDate::Value`; parse with `::Date.parse(d.at.to_s)` to extract year
218
+
219
+ ## Linting
220
+
221
+ RuboCop inherits from the [Ribose OSS style guide](https://github.com/riboseinc/oss-guides). Target Ruby version is 3.1. Rails cops are loaded but disabled.
222
+
223
+ ## CI
224
+
225
+ GitHub Actions workflows in `.github/workflows/` are auto-generated by Cimas — avoid manual edits. `rake.yml` runs tests on push/PR; `release.yml` handles gem publishing.
226
+
227
+ ## Testing
228
+
229
+ - Run the suite with `bundle exec rake spec:ogc` from the repo root; specs live
230
+ in `spec/ogc/` and run self-contained (see the root `CLAUDE.md`).
231
+ - **Index fixture:** `spec/ogc/fixtures/index-v2.zip` is seeded into the
232
+ `Relaton::Index` pool by `spec/ogc/support/webmock.rb` — in `before(:each)`,
233
+ not only `before(:suite)`, because `Index::Pool#type` replaces the pooled
234
+ entry whenever `actual?` says no and `DataFetcher#index` asks for the same
235
+ type without a `url:`. The pool key is `type.upcase.to_sym`, so the flavor's
236
+ `:ogc` is pooled as `:OGC`. It is built **with `pubid_class:`**; without it
237
+ the suite would pass while exercising something the runtime never does.
238
+ - Refresh it with `bundle exec rake spec:update_index_ogc`. Unlike 3GPP's, this
239
+ fixture is the whole published index (~1,258 rows) rather than a curated
240
+ subset — small enough that keeping all of it costs nothing. Rows are copied
241
+ **verbatim** from the published `index-v2.zip`; `#build` refuses a v1 source
242
+ rather than writing a fixture `Relaton::Index` would reject wholesale.
@@ -0,0 +1,134 @@
1
+ = Relaton::Ogc
2
+
3
+ Relaton::Ogc is a Ruby gem that implements the https://github.com/metanorma/metanorma-model-iso#iso-bibliographic-item[IsoBibliographicItem model].
4
+
5
+ You can use it to retrieve metadata of OGC Standards from the https://github.com/relaton/relaton-data-ogc[relaton-data-ogc] dataset, and access such metadata through the `Relaton::Ogc::ItemData` object.
6
+
7
+ == Usage
8
+
9
+ === Search for a standard using keywords
10
+
11
+ [source,ruby]
12
+ ----
13
+ require 'relaton/ogc'
14
+ => true
15
+
16
+ hits = Relaton::Ogc::Bibliography.search("OGC 19-025r1")
17
+ => <Relaton::Ogc::HitCollection:0x00000000001230 @ref=19-025r1 @fetched=false>
18
+
19
+ item = hits[0].item
20
+ => #<Relaton::Ogc::ItemData:0x0000000127b1d870
21
+ ...
22
+ ----
23
+
24
+ === XML serialization
25
+ [source,ruby]
26
+ ----
27
+ item.to_xml
28
+ => "<bibitem id="19025r1" type="standard" schema-version="v1.4.1">
29
+ <fetched>2026-02-27</fetched>
30
+ <title language="en" script="Latn" type="title-main">Development of Spatial Data Infrastructures for Marine Data Management</title>
31
+ <title language="en" script="Latn" type="main">Development of Spatial Data Infrastructures for Marine Data Management</title>
32
+ <uri type="src">http://www.opengis.net/doc/per/marine-sdi</uri>
33
+ <uri type="pdf">https://portal.ogc.org/files/?artifact_id=88037</uri>
34
+ <docidentifier type="OGC" primary="true">19-025r1</docidentifier>
35
+ ...
36
+ </bibitem>"
37
+ ----
38
+ With argument `bibdata: true` it outputs XML wrapped by `bibdata` element and adds flavor `ext` element.
39
+ [source,ruby]
40
+ ----
41
+ item.to_xml bibdata: true
42
+ => "<bibdata type="standard" schema-version="v1.4.1">
43
+ <fetched>2026-02-27</fetched>
44
+ <title language="en" script="Latn" type="title-main">Development of Spatial Data Infrastructures for Marine Data Management</title>
45
+ <title language="en" script="Latn" type="main">Development of Spatial Data Infrastructures for Marine Data Management</title>
46
+ <uri type="src">http://www.opengis.net/doc/per/marine-sdi</uri>
47
+ <uri type="pdf">https://portal.ogc.org/files/?artifact_id=88037</uri>
48
+ <docidentifier type="OGC" primary="true">19-025r1</docidentifier>
49
+ ...
50
+ <ext schema-version="v1.0.1">
51
+ <doctype>engineering-report</doctype>
52
+ <flavor>ogc</flavor>
53
+ </ext>
54
+ </bibdata>"
55
+ ----
56
+
57
+ === Get code, and year
58
+ [source,ruby]
59
+ ----
60
+ Relaton::Ogc::Bibliography.get "OGC 19-025r1", "2019"
61
+ [relaton-ogc] INFO: (OGC 19-025r1) Fetching from Relaton repository ...
62
+ [relaton-ogc] INFO: (OGC 19-025r1) Found: `19-025r1`
63
+ => #<Relaton::Ogc::ItemData:0x00000001283d6f48
64
+ ...
65
+ ----
66
+
67
+ === Create bibliographic item from XML
68
+ [source,ruby]
69
+ ----
70
+ Relaton::Ogc::Item.from_xml File.read('spec/fixtures/12_128r14.xml')
71
+ => #<Relaton::Ogc::ItemData:0x000000012835e610
72
+ ...
73
+ ----
74
+
75
+ === Typed links
76
+
77
+ Each OGC document has `scr` type source link.
78
+
79
+ [source,ruby]
80
+ ----
81
+ item.source[0].type
82
+ => "obp"
83
+
84
+ item.source[0].content
85
+ => "http://www.opengis.net/doc/per/marine-sdi"
86
+ ----
87
+
88
+ === Create bibliographic item from YAML
89
+ [source,ruby]
90
+ ----
91
+ Relaton::Ogc::Item.from_yaml File.read('spec/fixtures/item.yaml')
92
+ => #<Relaton::Ogc::ItemData:0x0000000124b19d90
93
+ ...
94
+ ----
95
+
96
+ === Fetch data
97
+
98
+ This gem uses the https://raw.githubusercontent.com/opengeospatial/NamingAuthority/master/definitions/docs/docs.json[OGC NamingAuthority] dataset as a data source.
99
+
100
+ The method `Relaton::Ogc::DataFetcher.fetch(output: "data", format: "yaml")` fetches all the documents from the dataset and saves them to the `./data` folder in YAML format.
101
+ Arguments:
102
+
103
+ - `output` - folder to save documents (default './data').
104
+ - `format` - the format in which the documents are saved. Possible formats are: `yaml`, `xml`, `bibxml` (default `yaml`).
105
+
106
+ [source,ruby]
107
+ ----
108
+ require "relaton/ogc/data_fetcher"
109
+
110
+ Relaton::Ogc::DataFetcher.fetch
111
+ Started at: 2026-02-27 11:21:46 +0200
112
+ Stopped at: 2026-02-27 11:21:48 +0200
113
+ => nil
114
+ ----
115
+
116
+ === Logging
117
+
118
+ Relaton::Ogc 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.
119
+
120
+ == Development
121
+
122
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
123
+
124
+ To update the index test fixture (used by tests), run `rake spec:update_index`. This downloads the latest `index-v1.zip` from the https://github.com/relaton/relaton-data-ogc[relaton-data-ogc] repository.
125
+
126
+ 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).
127
+
128
+ == Contributing
129
+
130
+ Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton-ogc.
131
+
132
+ == License
133
+
134
+ The gem is available as open source under the terms of the https://opensource.org/licenses/MIT[MIT License].
@@ -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-oiml retrieves International Organization of Legal Metrology (OIML) publications using the Relaton model. It is **index-backed** (no scraping): it searches a pre-built index via `Relaton::Index` and fetches per-document YAML from the `relaton/relaton-data-oiml` GitHub repo. OIML reference parsing uses `Pubid::Oiml::Identifier`.
8
+
9
+ ## Development
10
+
11
+ ```bash
12
+ bundle exec rake # default task → rspec
13
+ bundle exec rspec spec/relaton/oiml/bibliography_spec.rb # single file
14
+ bundle exec rake spec:update_index # refresh the OIML index fixture from relaton-data-oiml
15
+ ```
16
+
17
+ (No gem-local rubocop config; the monorepo root config applies.)
18
+
19
+ ## Architecture
20
+
21
+ Namespace: `Relaton::Oiml`. Retrieval flow:
22
+
23
+ 1. **Bibliography** (`lib/relaton/oiml/bibliography.rb`) — `get(code, year)` / `search`; parses the reference with `Pubid::Oiml.parse`, looks it up in the index, and fetches the matching YAML. `get` suppresses the edition year for an **undated** citation (mirroring ISO): when the reference carries no year (and `opts[:keep_year]` is not set) it returns `item.to_most_recent_reference`, so `OIML B 18` renders undated, while a dated citation (`OIML B 18:2022`, or an explicit `year`) still pins that edition. This relies on `Oiml::Docidentifier#remove_date!` refreshing `content` from the dateless pubid — mutating the wrapped `@pubid` alone leaves the rendered `content` dated (issue #72).
24
+ 2. **Index** — `Relaton::Index.find_or_create(:oiml, url: ..., pubid_class: Pubid::Oiml::Identifier)`. `INDEXFILE` is defined in `lib/relaton/oiml.rb`. `Bibliography#best_row` matches a **Bulletin** exactly (`index.search(query, exact: true)`), and every other type by `pubid_match?`: the year-and-language-stripped "stem" is equal, the language is equal, and the year is equal when the reference or the `year` argument names one; the latest edition wins. A Bulletin needs the exact match because its year is part of its locator: the issue and the sequence render after the year, so the stem reduced every article of one year to `OIML Bulletin`, and a lookup returned an arbitrary one of them.
25
+
26
+ **OIML does not use pubid's subset match `===` yet**, unlike the other flavors that pubid#408 unblocked. Measured over the full `relaton-data-oiml` index (5,646 rows), `===` disagrees with `pubid_match?` outside the Bulletins on 192 pairs, and each one is a regression: `language` is not `subset_strict`, so a language-less Amendment, Annex or Errata reference reaches its translations (156 pairs); `subpart` is not strict, so `OIML R 137-1 (F)` reaches `OIML R 137-1-2:2012 (F)` (6); and the `year_on_base` render flag is compared, so `OIML R 102 Annex B-C` misses `OIML R 102:1995 Annex B-C` (30). A plain Recommendation escapes the first one only because its render hint `parsed_format` differs, which is luck. The hand-off `metanorma__pubid__oiml-subset-match-strict-language` asks pubid for the fix. `spec/oiml/relaton/oiml/bibliography_spec.rb` pins all of these with an in-memory index ("where the subset match would disagree"), so a later switch must pass them.
27
+
28
+ **Dual-published identifiers** (relaton#180, pubid#437): OIML sometimes co-publishes a document jointly with another SDO (ISO confirmed so far), printed with both identifiers joined by a bare `|`, e.g. `ISO 4064-1:2024|OIML R 49-1:2024`. pubid models this as `Pubid::Oiml::Identifiers::DualPublished`, which wraps `first`/`second` (each a real, independently-typed `::Pubid::Identifier`, in original print order) and delegates `#root`/`#code`/`#type`/`#stage`/`#iteration`/`#publisher`/the `mr_*` hooks to whichever side `#oiml_identifier` finds — but deliberately **not** `#year`, `#date`, `#part`, or `#language`: reading or mutating those directly on the wrapper is a silent no-op (the wrapper's own copies of those attributes are simply never populated). Only the generic, immutable `#exclude(:attr)` correctly recurses into both sides, because it walks the wrapper's declared attributes and, for the two that are nested identifiers (`first`/`second`), recurses into each.
29
+
30
+ Because `#root` already delegates to the OIML side, `Relaton::Index` needs no change — a dual-published row buckets under its OIML number exactly like a plain one. The gap was in `Bibliography` and `Docidentifier`, both of which read/mutate a pubid's flat attributes directly:
31
+
32
+ - `Bibliography#oiml_side(pubid)` unwraps a `DualPublished` to its OIML side (a plain pubid passes through unchanged), and `stem`/`pubid_match?`/`best_row`'s edition `max_by` all route through it before touching `.year`/`.language`. Without this, a plain query's stem (`"OIML R 49-1"`) can never equal a dual-published row's stem (`"ISO 4064-1|OIML R 49-1"` at full string), and `.year`/`.language` read directly off a `DualPublished` are always nil/no-op — silently breaking edition selection and language matching. Reducing both sides to the OIML identifier also makes stem comparison print-order-independent (`OIML R 49-1|ISO 4064-1` and `ISO 4064-1|OIML R 49-1` match identically).
33
+ - `Docidentifier#remove_date!`/`#remove_part!` compute the mutated string via `@pubid.exclude(:date)`/`@pubid.exclude(:part)` and reassign through `content=` (which reparses and resyncs `@pubid`), instead of the old `@pubid.date = nil`/`@pubid.part = nil` in-place mutation — the in-place form is indistinguishable from success (no exception) but silently does nothing on a `DualPublished`. `#exclude` behaves identically to the old in-place mutation for a plain (non-dual) pubid, so this is a safe rewrite, not just a dual-published-only branch.
34
+
35
+ No second `docidentifier` array entry is added for the external (ISO) side — pubid's `DualPublished` already exposes both sides structurally off the one docidentifier's `pubid` (`oiml_identifier`/`external_identifier`), so a consumer that needs the ISO side reads it from there.
36
+ 3. **Item / ItemData / Ext** (`item.rb`, `item_data.rb`, `ext.rb`) — `Item` extends `Bib::Item`; `Ext` carries OIML-specific fields (scope, quantity, measuring_instrument, focus_area, sustainability_framework, doi). `Item.from_yaml` deserializes the fetched document.
37
+ 4. **Processor** (`lib/relaton/oiml/processor.rb`) — registry integration; `@prefix = "OIML"`, `@defaultprefix = %r{^OIML\s}`. Lazy-`require_relative`s `../oiml` in its methods, including `remove_index_file`.
38
+
39
+ There are no scrapers — everything comes from the curated index + GitHub YAML.
40
+
41
+ ## External dependencies
42
+
43
+ `pubid ~> 2.0.0.pre.alpha.3`, `base64`, `relaton-core`, `relaton-bib`, `relaton-index`.
44
+
45
+ ## Testing
46
+
47
+ RSpec with WebMock + VCR. The offline index fixture `spec/fixtures/index-v2.zip` is pre-loaded into the `Relaton::Index` pool in `before(:suite)` (configured in `spec/support/webmock.rb`); per-document data requests are stubbed by WebMock and served from `spec/fixtures/data/*.yaml`. Run `rake spec:update_index` to refresh the fixture.
@@ -0,0 +1,70 @@
1
+ = Relaton OIML
2
+
3
+ RelatonOiml is a library to fetch bibliographic items for OIML
4
+ (International Organization of Legal Metrology) publications from the
5
+ https://github.com/relaton/relaton-data-oiml[relaton-data-oiml] repository.
6
+ The result is a Relaton bibliographic item that can be serialised to Relaton
7
+ XML or YAML.
8
+
9
+ It is a part of the https://github.com/relaton/relaton[Relaton] project.
10
+
11
+ == Supported document types
12
+
13
+ OIML publications are identified with https://github.com/metanorma/pubid[pubid]
14
+ (`Pubid::Oiml`). The following document types are covered:
15
+
16
+ * Recommendations (`R`)
17
+ * Documents (`D`)
18
+ * Guides (`G`)
19
+ * Vocabularies (`V`)
20
+ * Basic Publications (`B`)
21
+ * Expert Reports (`E`)
22
+ * Seminar Reports (`S`)
23
+
24
+ == Usage
25
+
26
+ === Search for a standard using keywords
27
+
28
+ [source,ruby]
29
+ ----
30
+ require "relaton/oiml"
31
+
32
+ item = Relaton::Oiml::Bibliography.get "OIML R 138"
33
+ # [relaton-oiml] INFO: (OIML R 138) Fetching from Relaton repository ...
34
+ # [relaton-oiml] INFO: (OIML R 138) Found: `OIML R 138:2007`
35
+ => #<Relaton::Oiml::Item:0x...>
36
+
37
+ item.docidentifier.first.content
38
+ => "OIML R 138:2007"
39
+ ----
40
+
41
+ A specific edition and language can be requested:
42
+
43
+ [source,ruby]
44
+ ----
45
+ Relaton::Oiml::Bibliography.get "OIML R 138:2007 (E)"
46
+ ----
47
+
48
+ === Serialization
49
+
50
+ [source,ruby]
51
+ ----
52
+ item.to_xml bibdata: true
53
+ item.to_yaml
54
+ ----
55
+
56
+ == OIML-specific metadata
57
+
58
+ Enhanced OIML Recommendations carry additional structured metadata exposed via
59
+ the bibliographic item extension (`item.ext`):
60
+
61
+ * `scope`
62
+ * `quantity`
63
+ * `measuring_instrument`
64
+ * `focus_area`
65
+ * `sustainability_framework`
66
+ * `doi`
67
+
68
+ == License
69
+
70
+ Copyright Ribose. BSD 2-clause licensed.
@@ -0,0 +1,137 @@
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-omg is a Ruby gem that searches and fetches standards from The Object Management Group (OMG) at https://www.omg.org. It is part of the larger Relaton family of bibliographic data gems (v2.0.0-alpha.1).
8
+
9
+ ## Commands
10
+
11
+ - **Run all tests:** `bundle exec rspec`
12
+ - **Run a single test:** `bundle exec rspec spec/relaton/omg/item_spec.rb`
13
+ - **Lint:** `bundle exec rubocop` (follows Ribose OSS style guide)
14
+ - **Lint with autofix:** `bundle exec rubocop -a`
15
+ - **Install deps:** `bundle install`
16
+
17
+ ## Architecture
18
+
19
+ ### Module structure
20
+
21
+ All source lives under `lib/relaton/omg/`. The main entry point is `lib/relaton/omg.rb` which loads everything under the `Relaton::Omg` module namespace.
22
+
23
+ ### Class hierarchy
24
+
25
+ The gem extends `relaton-bib` (~> 2.0.0-alpha.1), the core Relaton bibliographic data library:
26
+
27
+ - `Relaton::Omg::Item` < `Bib::Item` — base item class, uses `Omg::ItemData` model (subclass of `Bib::ItemData`)
28
+ - `Relaton::Omg::Ext` < `Bib::Ext` — overrides `get_schema_version` to return the OMG model version
29
+ - `Relaton::Omg::Bibitem` < `Item` — includes `Bib::BibitemShared`, for `<bibitem>` XML
30
+ - `Relaton::Omg::Bibdata` < `Item` — includes `Bib::BibdataShared`, for `<bibdata>` XML
31
+ - `Relaton::Omg::Docidentifier` < `Bib::Docidentifier` — the document id, backed by `Pubid::Omg`; `Item` declares it, so XML and YAML deserialization produce it too
32
+ - `Relaton::Omg::Processor` < `Core::Processor` — relaton-core integration, delegates to `Bibliography`, `Bibitem`, `Item`
33
+ - `Relaton::Omg::Bibliography` — fetches standards via `Scraper`
34
+ - `Relaton::Omg::Scraper` — scrapes https://www.omg.org/spec for bibliographic data
35
+
36
+ ### References are parsed with pubid
37
+
38
+ OMG uses `Pubid::Omg` and no hand regex. The flavor historically had no index
39
+ and no data repository (the BSI and CEN shape: parse the reference, and back
40
+ the document id with pubid). That changed: **relaton-data-omg** now crawls
41
+ www.omg.org/spec and serves the machine index on Pages (the relaton#206
42
+ contract), and `Relaton::Omg::INDEXFILE = "index-v2"` names it — the gem-side
43
+ consumer flip (reading the machine index in `Bibliography` the way the other
44
+ flavors do) is the follow-up; retrieval still scrapes live.
45
+
46
+ The grammar is `OMG <ACRONYM>[ <VERSION>][<sep><PART>]`, where `<sep>` is a
47
+ space, or a slash after a version. The version can carry a beta label (`2.5 beta`,
48
+ `2.0 beta 1`). The part is a volume or a format name (`Superstructure`, `PDF`).
49
+
50
+ - **`get` takes a String or a `Pubid::Omg::Identifier`** (the parse that
51
+ `Relaton::Db` routed with, relaton#205). `Scraper.scrape_page` parses only a
52
+ String, and the log keys print `ref.to_s`.
53
+ - **An unrecognized reference raises.** `Scraper.scrape_page` lets
54
+ `Pubid::Errors::ParseError` propagate, so
55
+ `OMG Model Driven Architecture Guide rev. 2.0` raises and does not show as
56
+ "Not found". Only the transport errors are rescued, by name.
57
+ - **The request URL is the acronym and the version, and nothing else.**
58
+ `get_doc` builds `https://www.omg.org/spec/<ACRONYM>/<VERSION>`, with each
59
+ space of the version turned into `/` (`1.0 beta 2` becomes `1.0/beta/2`). The
60
+ part stays out of the URL: `fetch_link` uses it to find the part's PDF link
61
+ on the version page, and `fetch_title` appends it. The cassettes replay only
62
+ while the URL does not change.
63
+ - **The docid version comes from the page, not from the query.** That is why
64
+ `get "OMG AMI4CCM"` answers `OMG AMI4CCM 1.1`. `fetch_docid` and the History
65
+ ids of `fetch_relation` are rendered by
66
+ `Pubid::Omg::Identifiers::Specification`, not by parsing a joined string.
67
+ - **pubid renders one separator.** `OMG DDS 1.4/PDF` renders back as
68
+ `OMG DDS 1.4 PDF`. Both spellings name the same document, and one rendering
69
+ keeps `==` and `matches?` true between them. If you echo a caller's
70
+ reference, echo the string, not `pubid.to_s`.
71
+ - **Known limit.** OMG's URL spelling `Beta2` (label and number joined) parses
72
+ as a part, not a version. It round-trips, so nothing is lost.
73
+ - **The acronym is the URL segment, verbatim.** 30 of the 270 acronyms in the
74
+ OMG catalog carry a hyphen, a slash, a plus, or start in lower case
75
+ (`DDS-XTypes`, `EDMC-FIBO/BE`, `VSIPL++`, `smartant`). pubid PR #376 accepts
76
+ all 270. A slash **before** the version belongs to the acronym
77
+ (`OMG EDMC-FIBO/BE 1.1` fetches `spec/EDMC-FIBO/BE/1.1`); a slash **after**
78
+ the version separates the part. So `OMG UML/Superstructure` is the acronym
79
+ `UML/Superstructure`; write the part behind a space. With pubid `ce2f75ef`
80
+ (PR #372) those 30 raised, and the migration waited for #376. Fix a grammar
81
+ gap in pubid; do not split the acronym in the flavor.
82
+
83
+ `Docidentifier` parses its `content` on the data side and rescues the parse, so
84
+ a value that is not an OMG identifier stays verbatim with a nil `pubid`. Its
85
+ mutators:
86
+
87
+ - `remove_date!` drops the **version**. OMG identifiers carry no date, and the
88
+ version is the discriminator, as IALA maps `remove_date!` onto its edition.
89
+ So `to_most_recent_reference` turns `OMG AMI4CCM 1.0` into `OMG AMI4CCM`.
90
+ - `remove_part!` drops the part: `OMG UML 2.1.1 Superstructure` becomes
91
+ `OMG UML 2.1.1`.
92
+ - `to_all_parts!` is the inherited no-op. An OMG part is a name, not a numbered
93
+ part, so there is no "all parts" form.
94
+
95
+ The processor sets `@pubid_flavor = :Omg`. That changes nothing today:
96
+ `Pubid::Omg.prefixes` is `["OMG"]`, the same as `@prefix`.
97
+
98
+ ### Publication date comes from JSON-LD, not the visible text
99
+
100
+ `Scraper#pub_date` reads the `publicationDate` value out of the page's
101
+ `application/ld+json` block, not the `<dt>Publication Date:</dt>` text. Two
102
+ reasons, both verified against cassettes:
103
+
104
+ - **Locale.** The OMG server renders the month name in its own locale, and
105
+ Cloudflare caches that variant. A response can carry `Content-Language: zh-CN`
106
+ with an otherwise English page (`<html lang="en">`) and a `<dd>七月 2007</dd>`,
107
+ which made `Date.parse` raise `Date::Error`. The request already sends
108
+ `Accept-Language: en-us,en;q=0.5`; the CDN answers `Vary: accept-encoding`
109
+ only, so it does not vary on language and no request header can prevent this.
110
+ - **Precision.** The visible text is a lossy render of the same value:
111
+ `2013-03-31` prints as "March 2013". Parsing the text rounded every OMG date
112
+ down to day 01.
113
+
114
+ The `<dd>` text stays as a fallback for a page with no JSON-LD, and a month name
115
+ it cannot parse logs a warning instead of raising.
116
+ `spec/omg/relaton/omg/scraper_spec.rb` guards all three paths with
117
+ `spec/omg/fixtures/localized_date.html`. Do not "simplify" the parser back to
118
+ the `<dd>` text.
119
+
120
+ ### Serialization formats
121
+
122
+ Items can be serialized to/from YAML and XML. Tests verify round-trip fidelity for all three classes. XML output is validated against RELAX NG schemas in `grammars/`.
123
+
124
+ ### Test patterns
125
+
126
+ - RSpec with `expect` syntax (monkey patching disabled)
127
+ - `equivalent-xml` for XML comparison
128
+ - `ruby-jing` for RELAX NG schema validation against `grammars/relaton-omg-compile.rng`
129
+ - VCR cassettes in `spec/vcr_cassettes/` for HTTP interaction recording
130
+ - Fixtures in `spec/fixtures/` (YAML and XML reference files)
131
+ - Tests follow a round-trip pattern: load fixture → parse → serialize → compare to fixture
132
+
133
+ ## Style
134
+
135
+ - Ruby >= 3.1.0
136
+ - Rubocop inherits from Ribose OSS guide with `rubocop-rails` required but Rails cops disabled
137
+ - OMG document reference format: `OMG {ACRONYM}[ {VERSION}][ {PART}]` (e.g., `OMG AMI4CCM 1.0`, `OMG UML 2.1.1 Superstructure`)