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,85 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with
4
+ the EASC flavor. It is a dev doc — excluded from the packaged gem via the
5
+ gemspec `files` glob.
6
+
7
+ ## What this flavor is
8
+
9
+ `Relaton::Easc` retrieves **EASC** (Eurasian Economic Standards Council; the
10
+ Cyrillic МГС council) publications using the Relaton model. It
11
+ is **index-backed** (no scraping): it searches a pre-built index via
12
+ `Relaton::Index` and fetches per-document YAML from the
13
+ `relaton/relaton-data-easc` GitHub repo. Reference parsing uses
14
+ `Pubid::Easc::Identifier`.
15
+
16
+ Two document series, both issued by EASC and catalogued on **mgscatalog.by**:
17
+
18
+ - **ПМГ** (`pmg`) — Правила по межгосударственной стандартизации (interstate
19
+ standardization *rules*), e.g. `ПМГ 03-2025`, plus the defense variant
20
+ `ПМГ В 31-2001`.
21
+ - **РМГ** (`rmg`) — Рекомендации по межгосударственной стандартизации
22
+ (interstate standardization *recommendations*), e.g. `РМГ 151-2025`.
23
+
24
+ ## Architecture
25
+
26
+ Namespace: `Relaton::Easc`. Retrieval flow:
27
+
28
+ 1. **Bibliography** (`lib/relaton/easc/bibliography.rb`) — `get(code, year)` /
29
+ `search`; parses the reference with `Pubid::Easc.parse`, narrows the index by
30
+ number, fetches the matching YAML over `Net::HTTP`, and returns
31
+ `Item.from_yaml` (an `ItemData`), stamping `fetched`. EASC citations are
32
+ always dated (`ПМГ 03-2025`), so — unlike ISO/OIML — there is no
33
+ undated/most-recent handling; `get` just returns what `search` resolves.
34
+ `ENDPOINT` is the `relaton-data-easc/main` raw base; `INDEXFILE` (`index-v2`)
35
+ is defined in `lib/relaton/easc.rb`.
36
+ 2. **Index keying** — `Relaton::Index.find_or_create(:easc, url: ...,
37
+ pubid_class: ::Pubid::Easc::Identifier)`. `Pubid::Easc` (from
38
+ `metanorma/pubid`) parses both the Cyrillic surface forms and the Latin
39
+ transliterations (`PMG`/`RMG`/`V`) and renders canonical Cyrillic; its
40
+ attributes are `series` (`"PMG"|"RMG"`, Latin-canonical), `variant`
41
+ (`"V"|nil` — the «В» defense marker), `number`, `year`, and it serializes to
42
+ `_type: pubid:easc:{pmg,rmg}`. `Bibliography#pubid_match?` matches on
43
+ series + variant + number (nil-tolerant year), so ПМГ never matches РМГ and
44
+ the «В» variant never matches the plain series.
45
+ 3. **Item / ItemData / Ext** (`item.rb`, `item_data.rb`, `ext.rb`,
46
+ `item_base.rb`, `bibitem.rb`, `bibdata.rb`) — `Item` extends `Bib::Item`
47
+ with the typed `Ext` subclass; `Bibitem`/`Bibdata` alias it. `Ext` carries
48
+ the EASC-specific fields so they round-trip natively through both XML and
49
+ YAML (no per-repo merge hacks): `urn` (`urn:easc:<series>:...`), `webpage`
50
+ (mgscatalog.by detail URL), `session` (adopting МГС council session, e.g.
51
+ `67МГС`), `developer` (Разработчик), `joining_states` (collection of country
52
+ codes, e.g. `%w[АРМ БЕИ КАЗ]`), and `assigned_to` (Закреплен за).
53
+ 4. **Docidentifier** (`docidentifier.rb`) — a plain `Bib::Docidentifier`
54
+ subclass; the `content` is the canonical Cyrillic citation (`ПМГ 03-2025`).
55
+ Subclassed so future `#pubid` integration can hook in without changing the
56
+ public interface.
57
+ 5. **Doctype** (`doctype.rb`) — `TYPES = %w[pmg rmg]`.
58
+ 6. **Processor** (`lib/relaton/easc/processor.rb`) — registry integration;
59
+ `@prefix = "EASC"`, `@idtype = "EASC"`, and `@defaultprefix =
60
+ %r{^(?:ПМГ|РМГ|PMG|RMG)\b}` routes **both** Cyrillic and Latin series
61
+ prefixes here. `get` delegates to `Bibliography.get`. Per the lazy-registry
62
+ invariant, each method `require_relative "../easc"` before touching a flavor
63
+ constant, including `remove_index_file`.
64
+
65
+ There are no scrapers — everything comes from the curated index + GitHub YAML.
66
+
67
+ ## Grammar (upstream, not yet present)
68
+
69
+ The RelaxNG schemas in the repo-root `grammar/` are **copies of the upstream
70
+ source of truth** `metanorma/metanorma-model-iso/grammars/` (authored as `.rnc`,
71
+ compiled to `.rng`). There is **no EASC grammar upstream yet**, so this flavor
72
+ intentionally ships **no** `grammar/relaton-easc*.rng` and **no** Jing/XML-schema
73
+ validation spec. Adding the upstream `relaton-easc.rnc` (extending
74
+ `BibDataExtensionType` with the `Ext` elements above, à la the plateau pattern)
75
+ is tracked as a cross-project hand-off to metanorma-model-iso; once it lands,
76
+ copy the compiled pair into `grammar/` and add the round-trip Jing spec.
77
+
78
+ ## Testing
79
+
80
+ RSpec + WebMock. `spec/easc/support/webmock.rb` builds an offline `index-v2`
81
+ in-memory from `Pubid::Easc` identifiers (so each row round-trips through
82
+ `from_hash`/`to_hash` like a published index), preloads it into the
83
+ `Relaton::Index` pool in `before(:suite)`, and stubs per-document requests from
84
+ `spec/easc/fixtures/data/*.yaml`. No test hits the network. Run with
85
+ `bundle exec rake spec:easc`.
@@ -0,0 +1,217 @@
1
+ # CLAUDE.md
2
+
3
+ ## Development
4
+
5
+ - `bundle install` — install dependencies
6
+ - `bundle exec rake spec:ecma` — run this flavor's suite (from the repo root)
7
+ - The ECMA flavor needs pubid support that is not in the released
8
+ `2.0.0.pre.alpha.8`, so the root `Gemfile` git-pins pubid. Pick it up with
9
+ **`bundle update pubid`**, never `bundle install` — see the
10
+ stale-pubid-lock trap in the root `CLAUDE.md`.
11
+
12
+ ## Pubid-backed docidentifier
13
+
14
+ `Relaton::Ecma::Docidentifier` parses its `content` into a
15
+ `Pubid::Ecma::Identifier` kept in `@pubid`, while the lutaml `content` attribute
16
+ stays a plain **string** for serialization. Parsing is **soft**: `content=`
17
+ lazily requires pubid and rescues `LoadError`/`StandardError`, so a missing gem
18
+ or non-ECMA content leaves `@pubid` nil rather than raising. All three mutators
19
+ therefore no-op safely, and `Bib::ItemData`'s `#to_all_parts` /
20
+ `#to_most_recent_reference` never raise on ECMA items.
21
+
22
+ **The one ECMA-specific rule — `refresh_content!` renders**
23
+ **`to_s(with_edition: false, with_volume: false)`.**
24
+
25
+ `Pubid::Ecma::Identifier#to_s` renders the edition and the volume **by default**
26
+ (`"ECMA-269 ed3 vol2"`). That default is deliberate on the pubid side, because
27
+ the index keys on a bare `to_s` — see below. A **document's own** docidentifier
28
+ is the opposite: every `ECMA-269` volume file in `relaton-data-ecma` carries
29
+ `docidentifier: ECMA-269` and the same title, so the edition and the volume are
30
+ index metadata, not part of the printed id. Without the opt-out, any mutation
31
+ would silently promote the stored content to the index form.
32
+
33
+ This is the mirror image of `Relaton::ThreeGpp::Docidentifier`, whose
34
+ `refresh_content!` must pass `with_publisher: true` because *its* pubid defaults
35
+ to the index rendering and *its* stored content is the fuller one. The two look
36
+ contradictory only until you notice each re-renders what its own `content`
37
+ already holds.
38
+
39
+ Component mapping:
40
+
41
+ - **`remove_date!` → clears `edition`.** ECMA has no date component; `edition`
42
+ is its version discriminator. It is invisible in the rendered bare form, but
43
+ the identifier really does change and a consumer reading `#pubid` sees it.
44
+ - **`remove_part!` → clears `part`/`subpart`.** Real here: `ECMA-418-1` →
45
+ `ECMA-418`.
46
+ - **`to_all_parts!` → both, then wraps `@pubid` in pubid's `AllParts`.**
47
+ `content` stays the plain stripped id — the ECMA renderer has no
48
+ "(all parts)" marker, and `content` is cached from the pre-wrap pubid on
49
+ purpose. `#pubid` itself, read directly, now answers `all_parts? == true`
50
+ and renders WITH pubid's generic marker (`#to_s`), since it's the wrapper.
51
+
52
+ ## Index
53
+
54
+ ### The gem builds and reads `index-v2`; the data repo derives `index-v1`
55
+
56
+ `INDEXFILE` is the pubid-backed `index-v2`: rows are `Pubid::Ecma::Identifier`
57
+ hashes (`_type: pubid:ecma:{standard,technical-report,memento}` with
58
+ `number`/`part`/`edition`/`volume`). The consumer (`Bibliography#index`) and
59
+ the producer (`DataFetcher#index`) pass
60
+ `pubid_class: ::Pubid::Ecma::Identifier`. Omitting it on the **producer** writes
61
+ v1-shaped rows under a v2 name, silently (`FileIO#save` calls `to_hash` only for
62
+ instances of `pubid_class`); omitting it on the **consumer** leaves the rows raw
63
+ hashes with `FileIO#sorted` false, so every lookup scans all 804.
64
+ `Processor#remove_index_file` passes `url: true` and `file:` only: the delete
65
+ never reads the index (see `lib/relaton/index/CLAUDE.md`).
66
+
67
+ This gem no longer produces the legacy `index-v1`. `relaton-data-ecma`'s
68
+ `crawler.rb` derives it from the v2 rows, the IANA/BIPM/W3C shape.
69
+
70
+ **Why the edition must be in the key.** `Index::Type#add_or_update` keys on a
71
+ **bare** `id.to_s`, and 740 of the 804 published rows carry an edition. Before
72
+ pubid rendered it, those 804 rows collapsed onto 421 keys — `ECMA-74` 22 rows to
73
+ 1, `ECMA-262` 18 to 1 — so a crawl would drop **383 of 804 rows and report
74
+ success**. Four rows also need `volume`: `ECMA-269` edition 3, volumes 1-4 share
75
+ one docidentifier *and* one title, so the volume is the only thing that tells
76
+ them apart.
77
+
78
+ **`#index_id` builds the identifier from the MODEL, not from a rendered string.**
79
+ It takes the docidentifier's own pubid, **duplicates** it, then sets `edition`
80
+ from `bib.edition.content` and `volume` from the extent locality — the same
81
+ three fields `#filename_id` reads. The `dup` is load-bearing: those two are
82
+ index metadata, so setting them on the shared object would promote the
83
+ document's own printed id to the index form. Filenames are unaffected either
84
+ way; `output_file` is fed by `#filename_id`, giving `ecma-269-3-1.yaml`.
85
+
86
+ **An unparseable docid is recorded, not just warned.** `#add_to_index` writes
87
+ `@errors[docid] = "Unparseable primary id … was not indexed (…)"`, which the
88
+ inherited `report_errors` logs and `Logger::Channels::GhIssue` turns into a
89
+ GitHub issue at the end of the crawl. (`Core::DataFetcher#report_errors` treats
90
+ a **String** value as the message; the `@errors[:key] &&= …` boolean form the
91
+ parsers use means "this field failed for every record" and is the wrong shape
92
+ here.) The row is skipped rather than indexed unparsed, because
93
+ `Relaton::Index` rejects the **whole** index if one row fails to deserialize and
94
+ its sort calls `.root.number` on every id. The data file is still written, so
95
+ the document is unindexed, never lost. (The 3GPP/W3C precedent.)
96
+
97
+ Verified against the live published corpus: all 804 rows build an identifier,
98
+ they render **804 distinct keys** (not 421), `root.number` is empty for none,
99
+ `from_hash(to_hash)` round-trips all 804, and the four `ECMA-269` volumes stay
100
+ distinct.
101
+
102
+ ### Lookup: `best_match`
103
+
104
+ `Bibliography#search` follows the ETSI/W3C/OGC idiom:
105
+
106
+ - **Pass the pubid, not the string.** `Type#search_candidates` narrows only when
107
+ the argument is not a `String`, and a block alone never narrows — so the plain
108
+ string this flavor used to pass disabled the binary search however the index
109
+ was built. `pubid_class:` alone fixes nothing; both had to change together.
110
+ Measured: `ECMA-269` now narrows to 12 of 804 rows.
111
+ - **Select with the subset match.** `search` calls `index.search(pubid)` with
112
+ no block, so `Index::Type#search` selects with pubid's `pubid === row`. An
113
+ `edition` or `volume` that the reference omits matches any value; they are
114
+ index metadata that a document's own docidentifier never carries. pubid
115
+ declares `part` strict for ECMA (pubid#408), so a bare `ECMA-418` does not
116
+ match `ECMA-418-1`. `===` requires the same class, which keeps `ECMA-100`
117
+ and `ECMA TR/100` apart. Measured over the 804-row fixture: `===` and the
118
+ old `matches?(…, ignore: %i[edition volume])` agree on all 6,492 pairs.
119
+ - **An unparseable reference finds nothing**, with a warning. There is
120
+ deliberately no substring-scan fallback (OGC has one): an ECMA row renders as
121
+ `ECMA-262 ed17`, so a scan would answer a truncated `ECMA-26` with every
122
+ ECMA-26x document, and an ambiguous answer is worse than none.
123
+
124
+ **A reference must now parse WHOLE.** The old `parse_ref` regex was unanchored
125
+ at the end, so trailing text after a valid prefix was ignored and still
126
+ resolved; pubid rejects it. Measured:
127
+
128
+ | reference | old | new |
129
+ |---|---|---|
130
+ | `ECMA-6 (draft)` | `ECMA-6` | none |
131
+ | `ECMA-6:1991` | `ECMA-6` | none |
132
+ | `ECMA-6 2nd edition` | `ECMA-6` | none |
133
+ | `ECMA-6 ` (trailing space) | `ECMA-6` | `ECMA-6` |
134
+ | ` ECMA-6` (leading space) | none | `ECMA-6` — `parse_ref` strips |
135
+
136
+ That is the same trade-off as the missing fallback — a strict parse beats an
137
+ ambiguous match — and it is the first thing to check against a report that a
138
+ reference "used to resolve and now does not". `ecma-6` (lowercase) parsed under
139
+ neither.
140
+
141
+ #### Ordering: latest edition, then lowest volume
142
+
143
+ That is the order the bespoke `compare_edition_volume` + `min` implemented, kept
144
+ deliberately — but the edition is compared **segment-wise as integers**
145
+ (`edition_key`), and `r[:file]` breaks the tie because the index sort is not
146
+ stable. Comparing the rendered strings made `"9"` beat `"17"`, so 5 of the 421
147
+ document families returned an older document than the reference asked for.
148
+ Scored against each document's own published date:
149
+
150
+ | family | string compare | integer compare | dates |
151
+ |---|---|---|---|
152
+ | ECMA-262 | ed9 | **ed17** | 2018-06 → 2026-06 |
153
+ | ECMA-74 | ed9 | **ed22** | 2005-12 → 2025-12 |
154
+ | ECMA-402 | ed9 | **ed13** | → 2026-06 |
155
+ | ECMA-328 | ed7 | **ed10** | same direction |
156
+ | ECMA-109 | ed9 | **ed11** | same direction |
157
+
158
+ The integer key picks the newer document in all 5. An absent edition sorts below
159
+ every present one — right for the 64 edition-less rows (mementos, a few
160
+ reports), none of which shares a document with an edition-bearing row.
161
+
162
+ **`ECMA-262 ed5.1` is the only dotted edition in the whole published corpus.**
163
+ It is what forces the segment-wise compare (`5.1` must beat `5`), but since 262's
164
+ latest is ed17 the dotted ordering never decides a bare lookup — so it is pinned
165
+ by a unit assertion on `edition_key` rather than by an end-to-end example. Do not
166
+ "simplify" the key to `to_i`.
167
+
168
+ The old `match_ref` also matched on a **prefix** (`/^ECMA[-\s]#{id}/`), so
169
+ `ECMA-43` matched `ECMA-430…434`. Four families collided this way (418, 43, 35,
170
+ 13); none of the four changed its answer, so exact matching removed a latent
171
+ risk rather than moving results.
172
+
173
+ #### Verified against the published index
174
+
175
+ `relaton-data-ecma` publishes `index-v2.zip`. Loaded through
176
+ `Bibliography#index`: **804 rows**, all deserialized to identifiers, sorted,
177
+ **0** keying on `""`, 804 distinct keys, and `from_hash(to_hash)` round-trips
178
+ every row. Live lookups resolve every shape the flavor handles, each to a file
179
+ that returns HTTP 200 — a bare `ECMA-269` → ed9, the space form `ECMA 269` →
180
+ the same, `ECMA-269 ed3` → vol1, all four `ed3 vol<N>`, `ECMA-262` → ed17
181
+ (2026-06), `ECMA-262 ed5.1`, `ECMA-418` vs `ECMA-418-1`, `ECMA-100` vs
182
+ `ECMA TR/100`, `ECMA MEM/2021`, and `ECMA-43` narrowed to number `43` alone.
183
+
184
+ ## Testing
185
+
186
+ - **Framework:** RSpec with VCR cassettes and WebMock.
187
+ - **Index fixture:** `spec/ecma/fixtures/index-v2.zip` — the whole published
188
+ index (804 rows), copied verbatim, so the stored shapes are exactly what the
189
+ runtime deserializes. Refresh it with `bundle exec rake spec:update_index_ecma`
190
+ (`tasks/index_fixture_ecma.rb`); `#build` refuses a source that is not a pubid
191
+ index-v2 rather than writing a fixture `Relaton::Index` would reject wholesale.
192
+ - It is seeded into the `Relaton::Index` pool by `spec/ecma/support/webmock.rb`,
193
+ built **with `pubid_class:`** — without it the rows stay raw hashes,
194
+ `FileIO#sorted` stays false, and `Type#search` silently stops narrowing, so the
195
+ suite would pass while exercising something the runtime never does. Seeded in
196
+ `before(:each)`, not only `before(:suite)`, because `Index::Pool#type` replaces
197
+ the pooled entry whenever `actual?` says no and `DataFetcher#index` asks for
198
+ the same type with `file:` but no `url:`. With `before(:suite)` alone every
199
+ example after the first data-fetcher one searched a producer index, and the
200
+ consumer then built a third type and went to the network for real. The pool key
201
+ is `type.upcase.to_sym`, so the fetcher's `:ecma` and the consumer's `:ECMA`
202
+ share one slot. (The OGC pattern.)
203
+ - **VCR cassettes:** `spec/ecma/vcr_cassettes/` — index downloads are ignored by
204
+ VCR, matched on `INDEXFILE` so a version bump cannot let a real download
205
+ through.
206
+ - `spec/ecma/relaton/ecma/index_key_spec.rb` checks the whole fixture corpus for
207
+ the properties the index depends on: every row deserializes, one distinct
208
+ rendered key per row, no empty bsearch key, sorted, and a `to_hash` round-trip.
209
+ - A live check of the flavor cannot use `Bibliography.get` behind an HTTP proxy:
210
+ the document fetch goes through **Mechanize**, which does not read
211
+ `http_proxy`/`https_proxy` the way `Net::HTTP` does, so it dies with a
212
+ connection error on a host `curl` reaches. The index download (`Net::HTTP`)
213
+ works, so drive `best_match` and fetch the resolved file with `Net::HTTP`
214
+ instead.
215
+ - The `bibdata`/`bibitem` round-trip examples validate XML with Jing, which
216
+ needs a **JVM on PATH**. Without one they fail with `Jing::ExecutionError`;
217
+ that is environmental, not a code defect.
@@ -0,0 +1,178 @@
1
+ = Relaton::Ecma
2
+
3
+ Relaton::Ecma is a Ruby gem that searches and fetches standards from the https://www.ecma-international.org[European Computer Manufacturers Association].
4
+
5
+ == Usage
6
+
7
+ === Fetch documents
8
+
9
+ Documents can be fetched by reference. The structure of the reference depends on the type of the document. There are three types of documents:
10
+
11
+ - ECMA standards
12
+ - ECMA technical reports
13
+ - ECMA mementos
14
+
15
+ ECMA standards have the following reference structure: `ECMA-{NUMBER}[ ed{EDITION}][ vol{VOLUME}]`. Where: `NUMBER` is a number of the standard, `EDITION` is an edition of the standard, and `VOLUME` is a volume of the standard. The `EDITION` and `VOLUME` are optional. If `EDITION` is not specified, the latest edition of the standard will be fetched. If `VOLUME` is not specified, the first volume of the standard will be fetched. +
16
+
17
+ ECMA technical reports have the following reference structure: `ECMA TR/{NUMBER}[ ed{EDITION}]`. Where: `NUMBER` is a number of the technical report, and `EDITION` is an edition of the technical report. The `EDITION` is optional. If `EDITION` is not specified, the latest edition of the technical report will be fetched. +
18
+
19
+ ECMA mementos have the following reference structure: `ECMA MEM/{YEAR}`. Where: `YEAR` is an year of the memento.
20
+
21
+ [source,ruby]
22
+ ----
23
+ require 'relaton/ecma'
24
+ => true
25
+
26
+ # fetch ECMA standard
27
+ item = Relaton::Ecma::Bibliography.get 'ECMA-6'
28
+ [relaton-ecma] INFO: (ECMA-6) Fetching from Relaton repository ...
29
+ [relaton-ecma] INFO: (ECMA-6) Found: `ECMA-6`
30
+ => #<Relaton::Ecma::ItemData:0x000000012791d7a0
31
+ ...
32
+
33
+ # fetch ECMA standard with edition and volume
34
+ Relaton::Ecma::Bibliography.get "ECMA-269 ed3 vol2"
35
+ [relaton-ecma] INFO: (ECMA-269 ed3 vol2) Fetching from Relaton repository ...
36
+ [relaton-ecma] INFO: (ECMA-269 ed3 vol2) Found: `ECMA-269`
37
+ => #<Relaton::Ecma::ItemData:0x0000000127b81410
38
+ ...
39
+
40
+ # fetch the last edition of ECMA standard
41
+ bib = Relaton::Ecma::Bibliography.get "ECMA-269"
42
+ [relaton-ecma] INFO: (ECMA-269) Fetching from Relaton repository ...
43
+ [relaton-ecma] INFO: (ECMA-269) Found: `ECMA-269`
44
+ => #<Relaton::Ecma::ItemData:0x0000000127cd0a00
45
+ ...
46
+
47
+ bib.edition.content
48
+ => "9"
49
+
50
+ # fetch the first volume of ECMA standard
51
+ bib = Relaton::Ecma::Bibliography.get "ECMA-269 ed3"
52
+ [relaton-ecma] INFO: (ECMA-269 ed3) Fetching from Relaton repository ...
53
+ [relaton-ecma] INFO: (ECMA-269 ed3) Found: `ECMA-269`
54
+ => #<Relaton::Ecma::ItemData:0x0000000127cf7808
55
+ ...
56
+
57
+ bib.extent.first.locality.first.reference_from
58
+ => "1"
59
+
60
+ # fetch ECMA technical report
61
+ Relaton::Ecma::Bibliography.get 'ECMA TR/18'
62
+ [relaton-ecma] INFO: (ECMA TR/18) Fetching from Relaton repository ...
63
+ [relaton-ecma] INFO: (ECMA TR/18) Found: `ECMA TR/18`
64
+ => #<Relaton::Ecma::ItemData:0x0000000127cdc940
65
+ ...
66
+
67
+ # fetch ECMA memento
68
+ Relaton::Ecma::Bibliography.get "ECMA MEM/2021"
69
+ [relaton-ecma] INFO: (ECMA MEM/2021) Fetching from Relaton repository ...
70
+ [relaton-ecma] INFO: (ECMA MEM/2021) Found: `ECMA MEM/2021`
71
+ => #<Relaton::Ecma::ItemData:0x0000000127cf71c8
72
+ ...
73
+
74
+ # Return nil if a document doesn't exist.
75
+ Relaton::Ecma::Bibliography.get '1111'
76
+ [relaton-ecma] INFO: (1111) Fetching from Relaton repository ...
77
+ [relaton-ecma] WARN: Failed to parse pubid `1111`: Failed to parse ECMA identifier '1111': ...
78
+ [relaton-ecma] INFO: (1111) Not found.
79
+ => nil
80
+ ----
81
+
82
+ === Serialization
83
+
84
+ [source,ruby]
85
+ ----
86
+ item.to_xml
87
+ => "<bibitem id="ECMA6" type="standard" schema-version="v1.5.6">
88
+ <fetched>2025-12-29</fetched>
89
+ <title language="en" script="Latn">7-bit coded character set</title>
90
+ <uri type="src">https://ecma-international.org/publications-and-standards/standards/ecma-6/</uri>
91
+ <uri type="pdf">https://ecma-international.org/wp-content/uploads/ECMA-6_6th_edition_december_1991.pdf</uri>
92
+ <docidentifier type="ECMA" primary="true">ECMA-6</docidentifier>
93
+ ...
94
+ </bibitem>"
95
+ ----
96
+
97
+ With `bibdata: true` option XML output wrapped with `bibdata` element and `ext` element added.
98
+ [source,ruby]
99
+ ----
100
+ item.to_xml bibdata: true
101
+ => "<bibdata type="standard" schema-version="v1.5.6">
102
+ <fetched>2025-12-29</fetched>
103
+ <title language="en" script="Latn">7-bit coded character set</title>
104
+ <uri type="src">https://ecma-international.org/publications-and-standards/standards/ecma-6/</uri>
105
+ <uri type="pdf">https://ecma-international.org/wp-content/uploads/ECMA-6_6th_edition_december_1991.pdf</uri>
106
+ <docidentifier type="ECMA" primary="true">ECMA-6</docidentifier>
107
+ ...
108
+ <ext schema-version="v1.0.0">
109
+ <doctype>document</doctype>
110
+ <flavor>ecma</flavor>
111
+ </ext>
112
+ </bibdata>"
113
+ ----
114
+
115
+ === Typed links
116
+
117
+ Each ECMA document has `src` and `doi` link types.
118
+
119
+ [source,ruby]
120
+ ----
121
+ item.source
122
+ =>
123
+ [#<Relaton::Bib::Uri:0x00000001278dc5e8 @content="https://ecma-international.org/publications-and-standards/standards/ecma-6/", @language=nil, @locale=nil, @script=nil, @type="src">,
124
+ #<Relaton::Bib::Uri:0x00000001278dc188
125
+ @content="https://ecma-international.org/wp-content/uploads/ECMA-6_6th_edition_december_1991.pdf",
126
+ @language=nil,
127
+ @locale=nil,
128
+ @script=nil,
129
+ @type="pdf">]
130
+ ----
131
+
132
+ === Parse a file locally
133
+
134
+ [source,ruby]
135
+ ----
136
+ item = Relaton::Ecma::Item.from_xml File.read("spec/fixtures/bibdata.xml")
137
+ => #<Relaton::Ecma::ItemData:0x0000000127cbb1a0
138
+ ...
139
+ ----
140
+
141
+ === Fetch data
142
+
143
+ This gem uses a https://github.com/relaton/relaton-data-ecma[ecma-standards] prefetched dataset as a data source. The dataset contains documents from ECMA https://www.ecma-international.org/publications-and-standards/standards/[Standards], https://www.ecma-international.org/publications-and-standards/technical-reports/[Technical Reports], and https://www.ecma-international.org/publications-and-standards/mementos/[Mementos] pages.
144
+
145
+ The method `Relaton::Ecma::DataFetcher.new(output, format).fetch` fetches all the documents from the pages and saves them to the `./data` folder in YAML format.
146
+ Arguments:
147
+
148
+ - `output` - folder to save documents (default './data').
149
+ - `format` - the format in which the documents are saved. Possible formats are: `yaml`, `xml`, `bibxxml` (default `yaml`).
150
+
151
+ [source,ruby]
152
+ ----
153
+ Relaton::Ecma::DataFetcher.new.fetch
154
+ Started at: 2022-06-23 09:36:55 +0200
155
+ Stopped at: 2022-06-23 09:36:58 +0200
156
+ Done in: 752 sec.
157
+ => nil
158
+ ----
159
+
160
+ === Logging
161
+
162
+ Relaton::Ecma 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.
163
+
164
+ == Development
165
+
166
+ 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.
167
+
168
+ 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-ecma[relaton-data-ecma] repository.
169
+
170
+ 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).
171
+
172
+ == Contributing
173
+
174
+ Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton-ecma.
175
+
176
+ == License
177
+
178
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,89 @@
1
+ require "yaml"
2
+
3
+ module Relaton
4
+ # Cross-SDO equivalence as data: chain patterns and explicit equivalences
5
+ # the resolver consults when a reference misses. Tables are YAML files
6
+ # (schema: data/equivalence/adoption-chains.yaml); the gem ships the seed
7
+ # table and callers register more with {.register}.
8
+ module Equivalence
9
+ class << self
10
+ # @return [Array<Hash>] loaded tables, innermost last
11
+ def tables
12
+ @tables ||= []
13
+ end
14
+
15
+ # Register a table file or every *.yaml under a directory. Later
16
+ # registrations take precedence (checked first).
17
+ #
18
+ # @param [String] path file or directory
19
+ # @return [Array<Hash>] the tables now loaded
20
+ def register(path)
21
+ files = if File.directory?(path)
22
+ Dir.glob(File.join(path, "*.yaml")).sort.reverse
23
+ else
24
+ [path]
25
+ end
26
+ files.each { |f| tables << YAML.safe_load_file(f, aliases: true) }
27
+ tables
28
+ end
29
+
30
+ # Load the gem's seed table (idempotent). {Db} calls this on first use.
31
+ def load_seed
32
+ register(seed_path) unless tables.any?
33
+ tables
34
+ end
35
+
36
+ def seed_path
37
+ File.join(__dir__, "..", "..", "data", "equivalence", "adoption-chains.yaml")
38
+ end
39
+
40
+ # Reset (spec seam).
41
+ def reset!
42
+ @tables = nil
43
+ end
44
+
45
+ # The canonical reference for `code`, or nil when no table maps it.
46
+ # The code itself is not returned: a miss on a canonical form stays a
47
+ # miss.
48
+ #
49
+ # @param [String] code
50
+ # @return [String, nil]
51
+ def canonical_for(code)
52
+ load_seed
53
+ tables.each do |table|
54
+ (table["equivalences"] || []).each do |eq|
55
+ next if eq["canonical"] == code
56
+ next unless Array(eq["equivalents"]).include?(code)
57
+
58
+ return eq["canonical"]
59
+ end
60
+ (table["chains"] || []).each do |chain|
61
+ m = Regexp.new(chain["match"]).match(code)
62
+ next unless m
63
+
64
+ chain["resolve"].each do |template|
65
+ candidate = template.gsub(/\\(\d+)/) { m[Regexp.last_match(1).to_i] }
66
+ return candidate unless candidate == code
67
+ end
68
+ end
69
+ end
70
+ nil
71
+ end
72
+
73
+ # Every equivalent of `code`, canonical form first, the code itself
74
+ # excluded.
75
+ #
76
+ # @param [String] code
77
+ # @return [Array<String>]
78
+ def equivalents_for(code)
79
+ out = []
80
+ cursor = code
81
+ while (next_code = canonical_for(cursor)) && !out.include?(next_code)
82
+ out << next_code
83
+ cursor = next_code
84
+ end
85
+ out
86
+ end
87
+ end
88
+ end
89
+ end