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.
- 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,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
|