relaton 3.0.0.pre.alpha.17 → 3.0.0.pre.alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/data/equivalence/adoption-chains.yaml +38 -0
- data/lib/relaton/3gpp/CLAUDE.md +187 -0
- data/lib/relaton/3gpp/README.adoc +149 -0
- data/lib/relaton/adobe/CLAUDE.md +104 -0
- data/lib/relaton/bib/CLAUDE.md +130 -0
- data/lib/relaton/bib/README.adoc +330 -0
- data/lib/relaton/bib/model/series.rb +1 -1
- data/lib/relaton/bipm/CLAUDE.md +173 -0
- data/lib/relaton/bipm/README.adoc +352 -0
- data/lib/relaton/bipm/data_fetcher.rb +3 -2
- data/lib/relaton/bsi/CLAUDE.md +35 -0
- data/lib/relaton/bsi/README.adoc +112 -0
- data/lib/relaton/calconnect/CLAUDE.md +177 -0
- data/lib/relaton/calconnect/README.adoc +140 -0
- data/lib/relaton/ccsds/CLAUDE.md +74 -0
- data/lib/relaton/ccsds/README.adoc +154 -0
- data/lib/relaton/cen/CLAUDE.md +219 -0
- data/lib/relaton/cen/README.adoc +155 -0
- data/lib/relaton/cie/CLAUDE.md +90 -0
- data/lib/relaton/cie/README.adoc +121 -0
- data/lib/relaton/core/CLAUDE.md +140 -0
- data/lib/relaton/core/README.adoc +64 -0
- data/lib/relaton/core/data_fetcher.rb +50 -3
- data/lib/relaton/db/CLAUDE.md +373 -0
- data/lib/relaton/db.rb +19 -1
- data/lib/relaton/doi/CLAUDE.md +105 -0
- data/lib/relaton/doi/README.adoc +89 -0
- data/lib/relaton/easc/CLAUDE.md +85 -0
- data/lib/relaton/ecma/CLAUDE.md +217 -0
- data/lib/relaton/ecma/README.adoc +178 -0
- data/lib/relaton/equivalence.rb +89 -0
- data/lib/relaton/etsi/CLAUDE.md +219 -0
- data/lib/relaton/etsi/README.adoc +137 -0
- data/lib/relaton/evs/CLAUDE.md +120 -0
- data/lib/relaton/evs/README.adoc +27 -0
- data/lib/relaton/gb/CLAUDE.md +47 -0
- data/lib/relaton/gb/README.adoc +169 -0
- data/lib/relaton/gost/CLAUDE.md +100 -0
- data/lib/relaton/iala/CLAUDE.md +97 -0
- data/lib/relaton/iana/CLAUDE.md +105 -0
- data/lib/relaton/iana/README.adoc +107 -0
- data/lib/relaton/iec/CLAUDE.md +13 -0
- data/lib/relaton/iec/README.adoc +280 -0
- data/lib/relaton/iec/data_fetcher.rb +24 -8
- data/lib/relaton/iec/project_fetcher.rb +48 -0
- data/lib/relaton/iec/project_parser.rb +111 -0
- data/lib/relaton/iec.rb +5 -0
- data/lib/relaton/ieee/CLAUDE.md +176 -0
- data/lib/relaton/ieee/README.adoc +131 -0
- data/lib/relaton/ietf/CLAUDE.md +357 -0
- data/lib/relaton/ietf/README.adoc +167 -0
- data/lib/relaton/iho/CLAUDE.md +70 -0
- data/lib/relaton/iho/README.adoc +119 -0
- data/lib/relaton/index/CLAUDE.md +207 -0
- data/lib/relaton/index/README.adoc +221 -0
- data/lib/relaton/index/dataset_lock.rb +122 -0
- data/lib/relaton/index/family.rb +63 -0
- data/lib/relaton/index/type.rb +32 -0
- data/lib/relaton/index.rb +3 -0
- data/lib/relaton/isbn/CLAUDE.md +47 -0
- data/lib/relaton/isbn/README.adoc +91 -0
- data/lib/relaton/iso/CLAUDE.md +38 -0
- data/lib/relaton/iso/README.adoc +370 -0
- data/lib/relaton/iso/data_fetcher.rb +16 -1
- data/lib/relaton/itu/CLAUDE.md +685 -0
- data/lib/relaton/itu/README.adoc +188 -0
- data/lib/relaton/jcgm/CLAUDE.md +107 -0
- data/lib/relaton/jis/CLAUDE.md +64 -0
- data/lib/relaton/jis/README.adoc +167 -0
- data/lib/relaton/logger/CLAUDE.md +37 -0
- data/lib/relaton/logger/README.adoc +120 -0
- data/lib/relaton/nist/CLAUDE.md +180 -0
- data/lib/relaton/nist/README.adoc +389 -0
- data/lib/relaton/nist/bibliography.rb +17 -1
- data/lib/relaton/nist/data_fetcher.rb +60 -4
- data/lib/relaton/nist/hit_collection.rb +121 -59
- data/lib/relaton/nist/scraper.rb +21 -7
- data/lib/relaton/oasis/CLAUDE.md +251 -0
- data/lib/relaton/oasis/README.adoc +132 -0
- data/lib/relaton/ogc/CLAUDE.md +242 -0
- data/lib/relaton/ogc/README.adoc +134 -0
- data/lib/relaton/oiml/CLAUDE.md +47 -0
- data/lib/relaton/oiml/README.adoc +70 -0
- data/lib/relaton/omg/CLAUDE.md +137 -0
- data/lib/relaton/omg/README.adoc +101 -0
- data/lib/relaton/plateau/CLAUDE.md +126 -0
- data/lib/relaton/plateau/README.adoc +139 -0
- data/lib/relaton/sdo/CLAUDE.md +81 -0
- data/lib/relaton/un/CLAUDE.md +91 -0
- data/lib/relaton/un/README.adoc +125 -0
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton/w3c/CLAUDE.md +369 -0
- data/lib/relaton/w3c/README.adoc +140 -0
- data/lib/relaton/w3c/data_fetcher.rb +1 -1
- data/lib/relaton/xsf/CLAUDE.md +189 -0
- data/lib/relaton/xsf/README.adoc +127 -0
- data/lib/relaton.rb +1 -0
- metadata +83 -2
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Development
|
|
4
|
+
|
|
5
|
+
- `bundle install` — install dependencies
|
|
6
|
+
- `bundle exec rake spec` — run tests
|
|
7
|
+
- `bundle exec rubocop` — lint
|
|
8
|
+
|
|
9
|
+
## Index (pubid-structured `index-v2`)
|
|
10
|
+
|
|
11
|
+
IEEE consumes and produces the **structured `index-v2`** (`INDEXFILE = "index-v2"`
|
|
12
|
+
in `lib/relaton/ieee.rb`): each row's `:id` is a `Pubid::Ieee::Identifier`
|
|
13
|
+
serialized to the `_type: pubid:ieee:*` (lutaml) form, keyed on the index via
|
|
14
|
+
`pubid_class: ::Pubid::Ieee::Identifier`.
|
|
15
|
+
|
|
16
|
+
- **Write side.** Unlike the doc-fetching pipeline, the index is built by a
|
|
17
|
+
**separate step**: `Relaton::Ieee::DataFetcher.build_index(dir:, index_file:)`
|
|
18
|
+
(`data_fetcher.rb`). It globs the per-document `data/*.yaml` files, extracts each
|
|
19
|
+
primary non-trademark IEEE docidentifier, parses it via `.pubid` (a guarded
|
|
20
|
+
`::Pubid::Ieee::Identifier.parse` + `from_hash(to_hash) == to_hash` round-trip
|
|
21
|
+
check), `add_or_update`s the object, and `save`s. Ids pubid can't parse/round-trip
|
|
22
|
+
are skipped; a per-file skip warning **and** a final
|
|
23
|
+
`IEEE index-v2.yaml: N/M indexed, K skipped (P% coverage)` line are logged so the
|
|
24
|
+
loss is never silent. `DataFetcher.fetch` (doc generation) is unchanged — the
|
|
25
|
+
data repo (`relaton-data-ieee`) calls `build_index` after obtaining `data/`.
|
|
26
|
+
- **Read side.** `bibliography.rb#search` passes `pubid_class:` to `find_or_create`,
|
|
27
|
+
parses the query with `parse_pubid` (falling back to the raw String on failure),
|
|
28
|
+
passes the pubid to `index.search` for number-narrowing, and picks the row by
|
|
29
|
+
`row[:id].to_s`. `processor.rb#remove_index_file` passes `url: true` and `file:`
|
|
30
|
+
only: the delete never reads the index (see `lib/relaton/index/CLAUDE.md`).
|
|
31
|
+
|
|
32
|
+
## Identifier resolution (`RawbibIdParser`, pubid-first)
|
|
33
|
+
|
|
34
|
+
`RawbibIdParser.parse(normtitle, stdnumber)` turns a raw IEEE catalog
|
|
35
|
+
`normtitle` into a **`::Pubid::Ieee::Identifier`** (`idams_parser.rb`/prefilter
|
|
36
|
+
consume `.to_s` / `.to_s(trademark: true)` / docnumber). It is **pubid-first**:
|
|
37
|
+
pubid parses ~93.5% of normtitles directly; the bespoke regex case statement
|
|
38
|
+
(`#parse_fallback`, ~430 lines) only normalizes the messy remainder into a
|
|
39
|
+
canonical string pubid then parses (→ 98.7% pubid objects, 100% coverage).
|
|
40
|
+
|
|
41
|
+
- **Faithfulness guard.** `#parse` always renders the bespoke reference
|
|
42
|
+
(`parse_fallback(...).to_id`) and only accepts a pubid parse when it kept every
|
|
43
|
+
non-date digit (`#faithful?`, a date-stripped **digit** multiset — digit-level
|
|
44
|
+
so a benign re-split like `P1476/D4` vs `P14764` counts as faithful while a real
|
|
45
|
+
loss like `/D3.0`→`/D.0` does not). If no pubid parse is faithful it returns the
|
|
46
|
+
private fallback `Renderer` (the old bespoke id string) — so there is **zero
|
|
47
|
+
silent digit loss**. ~1.3% of docs keep the `Renderer`.
|
|
48
|
+
- **An id must have a digit in its number.** The last two fallback branches
|
|
49
|
+
("publisher, number" and the `else` that uses `stdnumber`) accept a number
|
|
50
|
+
only when it has a digit. Without that rule the first two words of any title
|
|
51
|
+
became an id (`A Call to Action …` → `A Call`), and a category `stdnumber`
|
|
52
|
+
became one too (`White Paper` → `IEEE White Paper`). Since the id is the
|
|
53
|
+
output filename, 176 records shared 42 files and 134 were overwritten. Now
|
|
54
|
+
`parse` returns nil for them, and `DataFetcher#parse_entry` skips them with
|
|
55
|
+
the `PubID parse error` warning. A real standard with a title-like
|
|
56
|
+
`normtitle` falls through to `stdnumber` and keeps its id
|
|
57
|
+
(`IEEE Standard for Ethernet …` / `802.3bm-2015` → `IEEE Std 802.3bm-2015`);
|
|
58
|
+
16 such standards were hidden by the old branch. **The digit rule alone is
|
|
59
|
+
not enough** — see `#no_standard_number?` under **Fetch robustness**.
|
|
60
|
+
- **No public `PubId` class.** The old `Relaton::Ieee::PubId` is gone; its
|
|
61
|
+
rendering survives only as the private `RawbibIdParser::Renderer` used by the
|
|
62
|
+
fallback. pubid (`::Pubid::Ieee::Identifier`) is the identifier abstraction.
|
|
63
|
+
- **Coordination with pubid.** IEEE ids now render in pubid's canonical form
|
|
64
|
+
(`/D-5`→`/D5`, `/Amd 5-2012` with a space, `802.16-2012 - Redline`, trademark
|
|
65
|
+
`™`/`®` attached to the document **number**, full corrigendum expansion). This
|
|
66
|
+
required a batch of pubid fixes (draft-verbatim, redline, revision incl.
|
|
67
|
+
numbered `RevN`, edition, trademark, historical formats, update_codes one-offs).
|
|
68
|
+
`Core::DataFetcher#output_file` also sanitizes commas so pubid's `", Mar 2011"`
|
|
69
|
+
dates don't leak into filenames. The fallback `Renderer`'s suffix strings are
|
|
70
|
+
**not** the canonical spellings and shouldn't be "fixed" to match: they are the
|
|
71
|
+
input to `to_id`, which pubid re-parses, so `/D-N`, `/R-N`, `/E-N` are
|
|
72
|
+
deliberately the *parseable* forms pubid canonicalizes to `/DN` etc. The
|
|
73
|
+
**trademark** rendering is the exception that must track pubid: `IdamsParser`
|
|
74
|
+
emits the `scope: trademark` docidentifier with `to_s(trademark: true)` on
|
|
75
|
+
whatever `parse` returned, so the `Renderer`'s ®/™ rule
|
|
76
|
+
(`Renderer::Id::REGISTERED_SERIES`) mirrors pubid#322 — series read off the
|
|
77
|
+
*number* with a leading project `P` stripped, `8802` ungated, everything else
|
|
78
|
+
gated on IEEE being a (co)publisher. Otherwise a `P802.x` draft would print ™
|
|
79
|
+
down the fallback path and ® down the pubid path. The one
|
|
80
|
+
exception is `amd_to_s`, which emits the **spaced** `/Amd N`: pubid parses both
|
|
81
|
+
spellings, so the spaced one costs nothing and keeps the rendering canonical
|
|
82
|
+
for the ~1.3% of ids that miss the faithfulness guard and keep the `Renderer`
|
|
83
|
+
string. Changing any of these also moves the `extract_index_entry` dedup key in
|
|
84
|
+
`data_fetcher.rb`.
|
|
85
|
+
|
|
86
|
+
## Fetch robustness (no silent corpus loss)
|
|
87
|
+
|
|
88
|
+
Amendment docnumbers can be pathologically long — pubid's `to_s` embeds the full
|
|
89
|
+
"(Amendment to … as amended by …)" clause (300+ chars). Two guards keep one such
|
|
90
|
+
id from silently halving the crawl (see the `data_fetcher.rb` history):
|
|
91
|
+
|
|
92
|
+
- **A record with no standard number is skipped.** `#no_standard_number?` is
|
|
93
|
+
true when `publicationsubtype` is in `NON_STANDARD_SUBTYPES` (`Whitepapers`,
|
|
94
|
+
`Research Documents`) **and** `stdnumber` has no digit. IEEE gives such a
|
|
95
|
+
document no designation and cites it by title, so every id built from it is
|
|
96
|
+
a title fragment. The digit rule in `RawbibIdParser` cannot catch all of
|
|
97
|
+
them, because pubid parses a digit out of the title itself
|
|
98
|
+
(`IEEE 802 Nendica Report: …` → `IEEE Std 802`, three documents; `IEEE 3D
|
|
99
|
+
Body Processing …` → `IEEE Std 3D`, three more). Both conditions are
|
|
100
|
+
required, so a record of such a subtype that does carry a number keeps it
|
|
101
|
+
(`Amendment 1:2011 to SMPTE ST 430-9:2008`). The check runs in **two**
|
|
102
|
+
places: `#parse_entry` (with a warning) and `#extract_index_entry` — the
|
|
103
|
+
prefilter groups files by docnumber and keeps the newest of each group, so a
|
|
104
|
+
white paper that reads as `IEEE Std 802` would otherwise join the real
|
|
105
|
+
standard's group and push its file out. Measured on the raw corpus: 157
|
|
106
|
+
records skipped, and no kept document has a digit-less id. The subtype match
|
|
107
|
+
is exact, which the data supports — all 570,744 raw files carry one of three
|
|
108
|
+
spellings (`Standard Docs` 564,611, `Whitepapers` 5,240,
|
|
109
|
+
`Research Documents` 891), so a new spelling is the one thing to watch for.
|
|
110
|
+
- **Bounded filenames.** `Core::DataFetcher#output_file` caps the basename at
|
|
111
|
+
`MAX_BASENAME_BYTES` (255): when the sanitized id exceeds the OS limit it
|
|
112
|
+
byte-truncates (`byteslice(…).scrub("")`, valid UTF-8) and appends a 12-char
|
|
113
|
+
SHA1 of the **full** docid — bounded, unique, and **deterministic** so
|
|
114
|
+
`save_doc`/`reconcile_staged_outputs`/`read_bib` all resolve the same name.
|
|
115
|
+
Without this, `File.write` raised `Errno::ENAMETOOLONG`.
|
|
116
|
+
- **Per-document commit guard.** Both the serial (`run_shard`) and parallel
|
|
117
|
+
(`spawn_batch` fork) paths route through one `commit_entry` helper that wraps
|
|
118
|
+
`commit_doc` in a `rescue` (logs via `Util.error`, continues). A single bad doc
|
|
119
|
+
no longer aborts a serial crawl, and a forked worker still reaches its
|
|
120
|
+
`File.binwrite(state_path, …)` so `merge_state_files` sees its `saved_writes`
|
|
121
|
+
and `reconcile_staged_outputs` doesn't delete the whole batch's staged files.
|
|
122
|
+
Reconcile also logs the straggler-cleanup count so an anomaly isn't silent.
|
|
123
|
+
- **A title-fragment id can still slip past `#no_standard_number?`.**
|
|
124
|
+
`#no_standard_number?` only fires for a `publicationsubtype` in
|
|
125
|
+
`NON_STANDARD_SUBTYPES`, and a generic mid-tier `RawbibIdParser` fallback
|
|
126
|
+
branch meant for a real catalog number (e.g. "publisher, number, part",
|
|
127
|
+
built for `"IEEE 802.3"`) can still read a short, numbered title as one
|
|
128
|
+
(`"WEB 3.0: The Evolution of ..."` → `WEB 3.0`) for a subtype spelling that
|
|
129
|
+
list doesn't cover. `RawbibIdParser.fabricated_title_id?(pid, title,
|
|
130
|
+
isbn_present)` is a second, subtype-agnostic guard on the *resolved id*
|
|
131
|
+
itself, called from both `IdamsParser#pubid` (full parse, `title` from
|
|
132
|
+
`@doc.btitle` — distinct from `normtitle`, which for a genuine standard
|
|
133
|
+
holds its designation, not prose) and `DataFetcher#extract_index_entry`
|
|
134
|
+
(cheap-parse prefilter, `title` the *last* `<title>` in the raw XML — the
|
|
135
|
+
first is always the top-level designation, present even for a real
|
|
136
|
+
standard). It must run in both places for the same reason
|
|
137
|
+
`#no_standard_number?` does: the prefilter groups files by docnumber before
|
|
138
|
+
the full parse ever runs, so a fabricated id left un-nilled there still
|
|
139
|
+
merges a whitepaper's group with an unrelated document's and can silently
|
|
140
|
+
drop a file from the corpus.
|
|
141
|
+
**The check is on the id's *class*, not (only) the title prefix:** it
|
|
142
|
+
requires `pid.is_a?(Renderer)` — i.e. the unvalidated bespoke fallback
|
|
143
|
+
string, never a real `::Pubid::Ieee::Identifier` that pubid's own grammar
|
|
144
|
+
actually parsed — **and** the title is a literal prefix of `pid.to_s`
|
|
145
|
+
**and** the document carries a real ISBN. Title-prefix and ISBN alone are
|
|
146
|
+
not enough: measured over 9,091 successfully-parsed raw-corpus records, 7
|
|
147
|
+
genuine, correctly-resolved standards have a title that literally begins
|
|
148
|
+
with their own real designation and also carry an ISBN (`"IEEE Std
|
|
149
|
+
1636.99-2013, IEEE Standard for ..."`) — an earlier version of this guard
|
|
150
|
+
without the `Renderer` restriction nulled out their real ids. All 7 resolve
|
|
151
|
+
to a real `Pubid::Ieee::Identifier`, never a `Renderer`, so the class check
|
|
152
|
+
is what tells them apart from the 25 known fabricated ids in a
|
|
153
|
+
`relaton-data-ieee` audit, all of which are `Renderer`s. A flagged record is
|
|
154
|
+
treated like any other unparseable id: `docnumber` is nil and
|
|
155
|
+
`DataFetcher#parse_entry` skips it with the `PubID parse error` warning.
|
|
156
|
+
|
|
157
|
+
## Testing
|
|
158
|
+
|
|
159
|
+
- **Framework:** RSpec with VCR cassettes and WebMock.
|
|
160
|
+
- **Index fixture:** `spec/ieee/fixtures/index-v2.zip` (a small structured index for
|
|
161
|
+
the read-spec targets) is deserialized through `::Pubid::Ieee::Identifier` and
|
|
162
|
+
pre-loaded into the `Relaton::Index` pool in `before(:suite)`
|
|
163
|
+
(`spec/ieee/support/webmock.rb`, NIST recipe). `index_builder_spec.rb` covers
|
|
164
|
+
`build_index` (structured rows + skip logging).
|
|
165
|
+
When a document 404s after a cassette refresh, the fixture has gone stale
|
|
166
|
+
against the republished index: copy the matching row **wholesale** (`:id:` *and*
|
|
167
|
+
`:file:`) out of the live `…/relaton-data-ieee/refs/heads/v2/index-v2.zip`,
|
|
168
|
+
rewrite the zip with the single `index-v2.yaml` entry, and **delete the cassette**
|
|
169
|
+
so it re-records — `record: :once` will not re-record a *changed* request URI, it
|
|
170
|
+
raises `UnhandledHTTPRequestError` instead.
|
|
171
|
+
- **Expected-XML fixtures self-heal only when absent.** `ieee_spec.rb` and
|
|
172
|
+
`idams_parser_spec.rb` write `fixtures/<name>.xml` with `unless File.exist?`, so
|
|
173
|
+
reconciling a pubid rendering change means deleting the fixture, re-running, and
|
|
174
|
+
reviewing the diff — not hand-editing it.
|
|
175
|
+
- **VCR cassettes:** `spec/ieee/vcr_cassettes/` — `index-v2.zip` downloads are
|
|
176
|
+
ignored by VCR (handled by the fixture).
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
= Relaton::Ieee
|
|
2
|
+
|
|
3
|
+
RelatonIeee is a Ruby gem that implements the https://github.com/relaton/relaton-model-ieee#ieee-bibliographic-item-model[IeeeBibliographicItem model].
|
|
4
|
+
|
|
5
|
+
You can use it to retrieve metadata of IEEE Standards from standards.ieee.org, and access such metadata through the `Relaton::Ieee::ItemData` object.
|
|
6
|
+
|
|
7
|
+
== Usage
|
|
8
|
+
|
|
9
|
+
=== Search for a standard using keywords
|
|
10
|
+
|
|
11
|
+
[source,ruby]
|
|
12
|
+
----
|
|
13
|
+
require 'relaton/ieee'
|
|
14
|
+
=> true
|
|
15
|
+
|
|
16
|
+
item = Relaton::Ieee::Bibliography.search("IEEE Std 528-2019")
|
|
17
|
+
=> #<Relaton::Ieee::ItemData:0x000000012891cf70
|
|
18
|
+
...
|
|
19
|
+
----
|
|
20
|
+
|
|
21
|
+
=== XML serialization
|
|
22
|
+
|
|
23
|
+
[source,ruby]
|
|
24
|
+
----
|
|
25
|
+
item.to_xml
|
|
26
|
+
=> "<bibitem id="IEEEStd5282019" type="standard" schema-version="v1.4.1">
|
|
27
|
+
<fetched>2026-01-30</fetched>
|
|
28
|
+
<title type="main">IEEE Standard for Inertial Sensor Terminology</title>
|
|
29
|
+
<uri type="src">https://ieeexplore.ieee.org/document/8863799</uri>
|
|
30
|
+
<docidentifier type="IEEE" primary="true">IEEE Std 528-2019</docidentifier>
|
|
31
|
+
...
|
|
32
|
+
</bibitem>"
|
|
33
|
+
----
|
|
34
|
+
|
|
35
|
+
With argument `bibdata: true` it ouputs XML wrapped by `bibdata` element and adds flavor `ext` element.
|
|
36
|
+
|
|
37
|
+
[source,ruby]
|
|
38
|
+
----
|
|
39
|
+
item.to_xml bibdata: true
|
|
40
|
+
=> "<bibdata type="standard" schema-version="v1.4.1">
|
|
41
|
+
<fetched>2026-01-30</fetched>
|
|
42
|
+
<title type="main">IEEE Standard for Inertial Sensor Terminology</title>
|
|
43
|
+
<uri type="src">https://ieeexplore.ieee.org/document/8863799</uri>
|
|
44
|
+
<docidentifier type="IEEE" primary="true">IEEE Std 528-2019</docidentifier>
|
|
45
|
+
...
|
|
46
|
+
<ext schema-version="v1.0.1">
|
|
47
|
+
<doctype>standard</doctype>
|
|
48
|
+
<flavor>ieee</flavor>
|
|
49
|
+
...
|
|
50
|
+
</ext>
|
|
51
|
+
</bibdata>"
|
|
52
|
+
----
|
|
53
|
+
|
|
54
|
+
=== Get document by reference
|
|
55
|
+
[source,ruby]
|
|
56
|
+
----
|
|
57
|
+
Relaton::Ieee::Bibliography.get("IEEE Std 528-2019")
|
|
58
|
+
[relaton-ieee] INFO: (IEEE Std 528-2019) Fetching from Relaton repository ...
|
|
59
|
+
[relaton-ieee] INFO: (IEEE Std 528-2019) Found: `IEEE Std 528-2019`
|
|
60
|
+
=> #<Relaton::Ieee::ItemData:0x00000001288b9380
|
|
61
|
+
...
|
|
62
|
+
----
|
|
63
|
+
|
|
64
|
+
=== Typed source links
|
|
65
|
+
|
|
66
|
+
Each IEEE document has `src` type link.
|
|
67
|
+
|
|
68
|
+
[source,ruby]
|
|
69
|
+
----
|
|
70
|
+
item.source.first.type
|
|
71
|
+
=> "src"
|
|
72
|
+
|
|
73
|
+
item.source.first.content
|
|
74
|
+
=> "https://ieeexplore.ieee.org/document/8863799"
|
|
75
|
+
----
|
|
76
|
+
|
|
77
|
+
=== Create bibliographic item from XML
|
|
78
|
+
[source,ruby]
|
|
79
|
+
----
|
|
80
|
+
Relaton::Ieee::Item.from_xml File.read "spec/fixtures/ieee_528_2019.xml"
|
|
81
|
+
=> #<Relaton::Ieee::ItemData:0x0000000128815a00
|
|
82
|
+
...
|
|
83
|
+
----
|
|
84
|
+
|
|
85
|
+
=== Create bibliographic item from YAML
|
|
86
|
+
[source,ruby]
|
|
87
|
+
----
|
|
88
|
+
Relaton::Ieee::Item.from_yaml File.read('spec/fixtures/item.yaml')
|
|
89
|
+
=> #<Relaton::Ieee::ItemData:0x0000000122f70c88
|
|
90
|
+
...
|
|
91
|
+
----
|
|
92
|
+
|
|
93
|
+
=== Fetch data
|
|
94
|
+
|
|
95
|
+
There is an IEEE dataset https://github.com/relaton/ieee-rawbib which can be converted into BibXML/BibYAML formats. The dataset needs to be placed into the local directory.
|
|
96
|
+
|
|
97
|
+
The method `Relaton::Ieee::DataFetcher.fetch(source, output: "data", format: "yaml")` converts all the documents from the local `ieee-rawbib` directory and saves them to the `./data` folder in YAML format.
|
|
98
|
+
Arguments:
|
|
99
|
+
|
|
100
|
+
- `source` - a name of data source (default 'ieee-rawbib').
|
|
101
|
+
- `output` - folder to save documents (default './data').
|
|
102
|
+
- `format` - the format in which the documents are saved. Possible formats are: `yaml`, `xml` (default `yaml`).
|
|
103
|
+
|
|
104
|
+
[source,ruby]
|
|
105
|
+
----
|
|
106
|
+
Relaton::Ieee::DataFetcher.fetch
|
|
107
|
+
Started at: 2021-09-24 17:55:07 +0200
|
|
108
|
+
Stopped at: 2021-09-24 17:57:30 +0200
|
|
109
|
+
Done in: 143 sec.
|
|
110
|
+
=> nil
|
|
111
|
+
----
|
|
112
|
+
|
|
113
|
+
=== Logging
|
|
114
|
+
|
|
115
|
+
RelatonIeee 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.
|
|
116
|
+
|
|
117
|
+
== Development
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
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-ieee[relaton-data-ieee] repository.
|
|
122
|
+
|
|
123
|
+
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).
|
|
124
|
+
|
|
125
|
+
== Contributing
|
|
126
|
+
|
|
127
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton_ieee.
|
|
128
|
+
|
|
129
|
+
== License
|
|
130
|
+
|
|
131
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What This Gem Does
|
|
6
|
+
|
|
7
|
+
The `Relaton::Ietf` flavor fetches and parses IETF bibliographic data (RFCs, Internet-Drafts, BCPs, FYIs, STDs) into the Relaton data model. Models are `lutaml-model`-based.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
The suite runs from its own directory (`spec/ietf/`), as every flavor suite does.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle exec rake spec:ietf # run this flavor's suite
|
|
15
|
+
cd spec/ietf && bundle exec rspec -I . relaton/ietf/bibxml_parser_spec.rb # single file
|
|
16
|
+
cd spec/ietf && bundle exec rspec -I . relaton/ietf/rfc/entry_spec.rb:19 # single example
|
|
17
|
+
cd spec/ietf && bundle exec rspec -I . -e "creates primary docid" # by description
|
|
18
|
+
bundle exec rubocop # lint
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Architecture
|
|
22
|
+
|
|
23
|
+
### Data Model (Lutaml::Model)
|
|
24
|
+
|
|
25
|
+
All models use `Lutaml::Model::Serializable` with `attribute` declarations and `xml do ... end` blocks for serialization. The IETF classes extend base `Relaton::Bib` classes:
|
|
26
|
+
|
|
27
|
+
- `Relaton::Ietf::ItemData < Bib::ItemData` — core bibliographic data
|
|
28
|
+
- `Relaton::Ietf::Item < Bib::Item` — adds `ext: Ext` attribute
|
|
29
|
+
- `Relaton::Ietf::Bibdata < Item` / `Bibitem < Item` — include shared serialization concerns
|
|
30
|
+
- `Relaton::Ietf::Ext` — IETF extensions: `doctype`, `flavor`, `stream`, `area`, `ipr`, `pi`
|
|
31
|
+
- `Relaton::Ietf::Doctype` — types: `"rfc"`, `"internet-draft"`
|
|
32
|
+
|
|
33
|
+
### Namespace Resolution
|
|
34
|
+
|
|
35
|
+
Converter classes use `Bib::NamespaceHelper` which resolves `namespace` by taking the first two segments of the class name. For `Relaton::Ietf::BibXMLParser::FromRfcxml`, `namespace` returns `Relaton::Ietf`, so `namespace::ItemData` → `Relaton::Ietf::ItemData`, `namespace::Ext` → `Relaton::Ietf::Ext`, etc.
|
|
36
|
+
|
|
37
|
+
### Key Flows
|
|
38
|
+
|
|
39
|
+
1. **Single document lookup**: `Processor#get` → `Bibliography.get(code)` → `Scraper.scrape_page` → parses the reference with `Pubid::Ietf` → searches the one combined `index-v2` from `relaton-data-ietf` → fetches that row's YAML
|
|
40
|
+
|
|
41
|
+
2. **Bulk data fetching**: `DataFetcher` extends `Relaton::Core::DataFetcher` with three datasets:
|
|
42
|
+
- `ietf-rfcsubseries` / `ietf-rfc-entries`: parse `rfc-index.xml` via `Rfc::Index` / `Rfc::Entry#to_item`
|
|
43
|
+
- `ietf-internet-drafts`: parse local BibXML files via `BibXMLParser.parse`
|
|
44
|
+
|
|
45
|
+
3. **BibXML parsing** (`BibXMLParser` module):
|
|
46
|
+
- `parse(xml)` — parses `<reference>` elements via `FromRfcxml` converter
|
|
47
|
+
- `parse_rfc(xml)` — parses `<rfc>` root documents via `FromRfc` converter
|
|
48
|
+
- Converters inherit from `Bib::Converter::BibXml::FromRfcxml` with IETF overrides for contributor/person/org handling
|
|
49
|
+
|
|
50
|
+
4. **RFC Index parsing** (`Rfc::Entry#to_item`): converts RFC editor index entries (BCP/FYI/STD/RFC) into `ItemData`, handling subseries vs full RFC entries differently
|
|
51
|
+
|
|
52
|
+
### Converter Inheritance Chain
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Bib::Converter::BibXml::FromRfcxml # base: handles <reference> generically
|
|
56
|
+
└── Ietf::BibXMLParser::FromRfcxml # IETF overrides: publisher, org recognition, person names
|
|
57
|
+
└── Ietf::BibXMLParser::FromRfc # <rfc> root: uses doc_name instead of anchor, no ref-level series_info
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`FromRfc` must override all methods that access `@reference.anchor`, `@reference.target`, `@reference.format`, or `@reference.series_info` since `Rfcxml::V3::Rfc` lacks these attributes (unlike `Rfcxml::V3::Reference`).
|
|
61
|
+
|
|
62
|
+
## Unified index (relaton#109)
|
|
63
|
+
|
|
64
|
+
Producer and consumer are both on the pubid `index-v2` now. `DataFetcher` writes
|
|
65
|
+
it; `Scraper` reads one combined index from `relaton-data-ietf` — replacing the
|
|
66
|
+
three per-type `index-v1` files (`:RFC`/`:RSS`/`:IDS`) it used to read from
|
|
67
|
+
`relaton-data-{rfcs,rfcsubseries,ids}`. Those three keep publishing `index-v1`
|
|
68
|
+
untouched, for released relatons.
|
|
69
|
+
|
|
70
|
+
`INDEXFILE` is a single constant again (`index-v2`); the `INDEXFILE_V2` that
|
|
71
|
+
existed while the two halves were split is gone.
|
|
72
|
+
|
|
73
|
+
Decisions taken (2026-08-18 ... 2026-08-24):
|
|
74
|
+
|
|
75
|
+
| | |
|
|
76
|
+
|---|---|
|
|
77
|
+
| Index structure | **unchanged** - `:id` + `:file` only. No `docids[]`, no summary fields; a third key invalidates the whole index for every released gem |
|
|
78
|
+
| `:id` shape | structured pubid hash (`Pubid::Ietf::Identifier#to_hash`) |
|
|
79
|
+
| Row per record | keyed by the record's **primary** docidentifier. A sub-series id resolves to its own container record, *not* to a constituent RFC - 42 of 365 sub-series include more than one RFC; consumers follow `relation: includes` |
|
|
80
|
+
| DOI ids | omitted - not `pubid:ietf`-representable, and one unparseable row kills the index. DOIs are served by the `doi` flavor |
|
|
81
|
+
| Who builds it | `DataFetcher`, during the crawl - not a separate pass over the written records, which would be a second answer to "what is this record's id" |
|
|
82
|
+
| Three type repos | untouched: they pin `relaton/relaton-ietf`, not this monorepo, so they keep emitting `index-v1` |
|
|
83
|
+
|
|
84
|
+
**Resolved 2026-08-26.** `relaton-data-ietf`'s RFC and sub-series records were
|
|
85
|
+
schema-invalid — missing `ext.doctype`, and RFC committee organizations carrying
|
|
86
|
+
`subdivision` with no `name` — because that repo converted the `ietf-tools`
|
|
87
|
+
v1.2.3 mirrors rather than crawling. It now runs `Relaton::Ietf::DataFetcher`
|
|
88
|
+
directly, which synthesises `doctype`, `status`, `stream`, the organization names
|
|
89
|
+
and the resolved WG titles. The two `pending` examples in `ietf_spec.rb` reported
|
|
90
|
+
`FIXED` on the first run against the new corpus and have been removed.
|
|
91
|
+
|
|
92
|
+
Two consequences of that migration to know about: record filenames are now
|
|
93
|
+
lowercase *and* unpadded (`data/RFC8341.yaml` -> `data/rfc8341.yaml`,
|
|
94
|
+
`data/STD0066.yaml` -> `data/std66.yaml`), since `output_file` derives them from
|
|
95
|
+
the docnumber; and the record content changed enough that the three XML fixtures
|
|
96
|
+
had to be regenerated.
|
|
97
|
+
|
|
98
|
+
### The consumer: `Scraper` queries with parsed identifiers
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
Relaton::Index.find_or_create(
|
|
102
|
+
:IETF, url: "#{IETF}#{INDEXFILE}.zip", file: "#{INDEXFILE}.yaml",
|
|
103
|
+
pubid_class: ::Pubid::Ietf::Identifier
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`parse_id` turns the reference into a `Pubid::Ietf::Identifier` before searching,
|
|
108
|
+
and **that is not cosmetic**: `Type#search_candidates` narrows only for non-String
|
|
109
|
+
queries, so a String falls into `match_item`'s `item[:id].to_s.include?(id)`, which
|
|
110
|
+
renders every pubid in the index on every lookup. Measured against the live
|
|
111
|
+
177k-row index: **~43 s per reference with a String, sub-millisecond with a parsed
|
|
112
|
+
id.** A String query also returns *nothing* if `pubid_class:` is omitted, since the
|
|
113
|
+
rows would then be raw hashes.
|
|
114
|
+
|
|
115
|
+
`parse_id` also normalises the Internet-Draft spellings - `I-D.<slug>` and
|
|
116
|
+
`I-D <slug>` - onto the `draft-...` form, adding the `draft-` stem when absent
|
|
117
|
+
(`I-D.ietf-quic-transport`, the bibxml anchor and the `docnumber` records carry).
|
|
118
|
+
The old plain-string index matched that by substring; matching is exact now.
|
|
119
|
+
A reference pubid cannot parse returns nil, so `Bibliography.get "CN 8341"` logs
|
|
120
|
+
"Not found." rather than raising.
|
|
121
|
+
|
|
122
|
+
### The producer: `DataFetcher` writes `index-v2`
|
|
123
|
+
|
|
124
|
+
`DataFetcher` builds the index during the crawl, from the identifier it already
|
|
125
|
+
holds for each record — `record_index_entry` parses the primary docidentifier and
|
|
126
|
+
`add_or_update`s the pubid:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
Relaton::Index.find_or_create(
|
|
130
|
+
:IETF, file: "#{INDEXFILE}.yaml", pubid_class: ::Pubid::Ietf::Identifier
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Both halves are load-bearing.** `FileIO#save` serialises an id to its `_type:`
|
|
135
|
+
hash only when it is an instance of the configured `pubid_class`, so passing a
|
|
136
|
+
String to a `pubid_class:` index writes a v1-shaped file under a v2 name — no
|
|
137
|
+
error, just an index that never narrows.
|
|
138
|
+
|
|
139
|
+
Three properties worth keeping:
|
|
140
|
+
|
|
141
|
+
- **Key on the primary docidentifier**, which `serialize_and_write` already
|
|
142
|
+
picks. Not the first entry: draft records carry an `I-D.foo` anchor id (not a
|
|
143
|
+
pubid grammar) and their unversioned series id, which is the *primary* of a
|
|
144
|
+
separate aggregator record — keying on either collides on one `id.to_s` and
|
|
145
|
+
`add_or_update` silently overwrites `:file`.
|
|
146
|
+
- **DOIs stay out.** `10.17487/RFC3986` is not a `pubid:ietf` identifier, and one
|
|
147
|
+
unparseable row invalidates the whole index. The primary is never the DOI, so
|
|
148
|
+
this is already right — don't "improve" it into indexing every docidentifier.
|
|
149
|
+
- **Sub-series key to their own record.** `STD 66` → the STD container, not a
|
|
150
|
+
constituent RFC: 42 of the 365 published sub-series include more than one, so
|
|
151
|
+
there is no single document to alias to. Consumers follow `relation: includes`.
|
|
152
|
+
|
|
153
|
+
A record whose id pubid rejects is written but not indexed, warned about, and
|
|
154
|
+
tallied for a summary at the end of the crawl. The load is all-or-nothing —
|
|
155
|
+
`deserialize_id` raises on the first bad id and `load_index` then rejects the
|
|
156
|
+
entire index — so one malformed upstream record must cost one document, never
|
|
157
|
+
every lookup. All 176,862 published ids parse today; this guards against drift.
|
|
158
|
+
|
|
159
|
+
**Blast radius, worth re-checking before touching this.** The six data repos
|
|
160
|
+
feeding today's released consumers do *not* use this monorepo: `ietf-tools/*` pin
|
|
161
|
+
`relaton/relaton-ietf` @ `main` (`RelatonIetf::DataFetcher`), and
|
|
162
|
+
`relaton/relaton-data-{rfcs,rfcsubseries,ids}` pin it @ `lutaml-integration` —
|
|
163
|
+
both branches carry their own `INDEXFILE = "index-v1"` and their own fetcher. Only
|
|
164
|
+
`relaton/relaton-data-ietf` runs this one. So the producer flip needs no
|
|
165
|
+
dual-write and no cutover window.
|
|
166
|
+
|
|
167
|
+
### Still outstanding
|
|
168
|
+
|
|
169
|
+
Retiring `relaton-data-{rfcs,rfcsubseries,ids}`'s `index-v1` files, and their
|
|
170
|
+
`data-index/configs.yml` rows, waits until a relaton carrying this change has
|
|
171
|
+
shipped — see `/work/HANDOFFS/relaton__support__ietf-index-config-and-cron.md`.
|
|
172
|
+
|
|
173
|
+
## Filename collisions: workers write, the parent settles the names
|
|
174
|
+
|
|
175
|
+
`Core::DataFetcher#output_file` sanitizes `.`, `,`, `/`, `:`, `(`, `)`, `-` and
|
|
176
|
+
whitespace all to `-`, so two **distinct** docids can want one path. This flavor
|
|
177
|
+
is the only one that writes from forked processes, so it is the only one where
|
|
178
|
+
that could destroy a record without a word.
|
|
179
|
+
|
|
180
|
+
It did. In the first production crawl of `relaton-data-ietf`,
|
|
181
|
+
`data/draft-ietf-6man-hbh-processing.yaml` — the aggregator for a draft with 21
|
|
182
|
+
versions — was written and then overwritten by a singleton built from the
|
|
183
|
+
malformed sibling id `draft-ietf-6man-hbh-processing-`, which sanitizes to the
|
|
184
|
+
same name. The published corpus came out 177,362 index rows over 177,361 files,
|
|
185
|
+
and nothing in the log said so. Exactly one such group exists in that corpus;
|
|
186
|
+
the mechanism, not the blast radius, is the problem.
|
|
187
|
+
|
|
188
|
+
Three pieces close it, and all three are load-bearing:
|
|
189
|
+
|
|
190
|
+
1. **`@cross_process = true`, set in `fetch_ieft_internet_drafts` before the
|
|
191
|
+
first `parallelize`** so the forked children inherit it. It tells
|
|
192
|
+
`Core::DataFetcher#write_unique` that an existing file may belong to a peer
|
|
193
|
+
and must never be overwritten.
|
|
194
|
+
2. **`serialize_and_write` writes through `write_unique`** and reports two extra
|
|
195
|
+
keys: `docid` (the id the file was written under) and `plain_file` (the name
|
|
196
|
+
it would have taken uncontested). `docid` is **not** `index_id` — `id` falls
|
|
197
|
+
back through `docnumber` and `formattedref`, so on the RFC path the two
|
|
198
|
+
differ (`RFC0001` vs `RFC 1`, i.e. stems `rfc0001` and `rfc-1`). Keying the
|
|
199
|
+
reconciliation on `index_id` renames records to the wrong names.
|
|
200
|
+
3. **`reconcile_output_files`, in the parent, before `record_index_entry`.** A
|
|
201
|
+
worker only learns that a path was taken, never which clashing docid deserves
|
|
202
|
+
it, so left alone the winner would follow the race and the two filenames would
|
|
203
|
+
swap between crawls. The parent sees every docid, so it decides: within a
|
|
204
|
+
group of records that wanted one path, the alphabetically first docid keeps
|
|
205
|
+
it and the rest take their digest variant.
|
|
206
|
+
|
|
207
|
+
Details of (3) that are easy to get wrong:
|
|
208
|
+
|
|
209
|
+
- It runs over **every** group, not just clashing ones. A lone record that fell
|
|
210
|
+
back to a digest path must get its plain name back, or the published filename
|
|
211
|
+
churns and the old file is orphaned. `next if r[:file] == target` makes that
|
|
212
|
+
free for ~100% of records.
|
|
213
|
+
- **Losers move first.** A loser may be sitting *on* the plain path; moving the
|
|
214
|
+
winner there first would destroy it.
|
|
215
|
+
- Two results for the **same** docid are collapsed, not split: the stray file is
|
|
216
|
+
deleted and one warning is emitted, which is what the crawl did before. The
|
|
217
|
+
stray may already be **gone** — the fallback name is keyed on the docid, so two
|
|
218
|
+
workers that both lost the race to the plain path land on one name and the
|
|
219
|
+
first of them renames it onto the target. The result must follow the document
|
|
220
|
+
to the target anyway, or the index gets a row pointing at nothing.
|
|
221
|
+
- Sorting *unique docids* leaves no tie for Ruby's unstable `sort_by` to resolve
|
|
222
|
+
differently between runs.
|
|
223
|
+
- A failed rename warns and carries on. A multi-hour crawl must not die at the
|
|
224
|
+
last step.
|
|
225
|
+
|
|
226
|
+
`report_collisions` restates the total once at the end of the crawl, beside
|
|
227
|
+
`report_unparsed` and `report_unindexed` — one line for a crawl that writes
|
|
228
|
+
~177k records.
|
|
229
|
+
|
|
230
|
+
The sequential paths (`fetch_ieft_rfcsubseries`, `fetch_ieft_rfcs`) get no
|
|
231
|
+
reconciliation pass and need none: one process, so `unique_output_file` is
|
|
232
|
+
already deterministic there. They do now emit a digest filename for a genuine
|
|
233
|
+
cross-docid collision instead of overwriting, which follows `rfc-index`
|
|
234
|
+
iteration order — stable for a given input.
|
|
235
|
+
|
|
236
|
+
## Thin records inherit from their constituents
|
|
237
|
+
|
|
238
|
+
Two builders produce records for documents that have **no upstream source of
|
|
239
|
+
their own**, and both would otherwise publish without date, doctype or source:
|
|
240
|
+
|
|
241
|
+
| Builder | File | Inherits from |
|
|
242
|
+
|---|---|---|
|
|
243
|
+
| `to_subseries_item` | `rfc/entry.rb` | the newest RFC in its `is-also` |
|
|
244
|
+
| `build_unversioned_doc` | `data_fetcher.rb` | the newest version in `sorted` |
|
|
245
|
+
|
|
246
|
+
This is **not** a parsing gap to go fix in the XML mapping. A sub-series entry in
|
|
247
|
+
`rfc-index.xml` is only a pointer:
|
|
248
|
+
|
|
249
|
+
```xml
|
|
250
|
+
<bcp-entry>
|
|
251
|
+
<doc-id>BCP0003</doc-id>
|
|
252
|
+
<is-also><doc-id>RFC1915</doc-id></is-also>
|
|
253
|
+
</bcp-entry>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The RFC Editor publishes no date, title, author or status for it, because a
|
|
257
|
+
sub-series *has* no metadata of its own — it is a label on one or more RFCs.
|
|
258
|
+
Unversioned draft aggregators are further still from an upstream document: they
|
|
259
|
+
are synthesised from the versions found on disk.
|
|
260
|
+
|
|
261
|
+
Neither site needs a second pass or another read of the corpus — `rfc_index` and
|
|
262
|
+
`sorted` already hold the constituents at build time. (A downstream
|
|
263
|
+
implementation in `relaton-data-ietf`'s crawler did use a second pass over
|
|
264
|
+
written files, because its groups could span source repos; that constraint does
|
|
265
|
+
not exist here.)
|
|
266
|
+
|
|
267
|
+
**Constituent lookup is normalised on both sides, deliberately.**
|
|
268
|
+
`Entry.squish` folds case and strips whitespace and dots. Relation targets and the
|
|
269
|
+
docids they reference disagree in the published corpus (`dyndNS` vs `dyndns`),
|
|
270
|
+
which left 28 documents undated downstream on a strict lookup — and, in
|
|
271
|
+
`build_relations`, silently downgraded a full constituent bibitem to a minimal
|
|
272
|
+
one.
|
|
273
|
+
|
|
274
|
+
`rfc_index` is therefore **keyed by `Entry.squish(doc_id)`, and the caller builds
|
|
275
|
+
it** — `fetch_ieft_rfcsubseries` does so once for the crawl. Normalising inside
|
|
276
|
+
`Entry` instead would rebuild a ~9,800-row table for each of the 367 sub-series
|
|
277
|
+
(measured: ~161 MB retained, ~6 s), all of it the same table.
|
|
278
|
+
|
|
279
|
+
Why it matters beyond tidiness: the Pages index sorts by date, so undated records
|
|
280
|
+
sort as one undifferentiated block, and a record with no `ext.doctype` renders
|
|
281
|
+
with no document type at all.
|
|
282
|
+
|
|
283
|
+
Inherited values are `dup`'d rather than shared with the constituent's own bib —
|
|
284
|
+
aliasing them would make a later edit to the aggregator mutate the `-NN` record.
|
|
285
|
+
|
|
286
|
+
Verified against the live `rfc-index.xml`: **367/367 sub-series dated**, 367/367
|
|
287
|
+
with full constituent bibitems, in 1.4 s; `BCP 3` → 1996-02, `STD 51` → 1994-07,
|
|
288
|
+
`STD 66` → 2005-01 (RFC 3986's date).
|
|
289
|
+
|
|
290
|
+
## bibxml encoding: transcode, don't scrub, don't skip
|
|
291
|
+
|
|
292
|
+
Some bibxml files declare `encoding='UTF-8'` and carry **Windows-1252** bytes —
|
|
293
|
+
smart quotes and accented Latin letters. `File.read(encoding: "UTF-8")` only
|
|
294
|
+
*tags* a string; it validates nothing, so those reached lutaml as invalid UTF-8
|
|
295
|
+
and `Rfcxml::V3::Reference.from_xml` raised `Lutaml::Model::InvalidFormatError`.
|
|
296
|
+
|
|
297
|
+
`read_bibxml` transcodes them (`data_fetcher.rb`). Three things make that the
|
|
298
|
+
right call rather than `scrub`:
|
|
299
|
+
|
|
300
|
+
- **All of them decode losslessly as CP1252** — 125 of the ~122k published files
|
|
301
|
+
are affected, 0 unmappable.
|
|
302
|
+
- **The bytes present prove CP1252, not Latin-1.** `0x91`–`0x94` (`‘ ’ “ ”`) are
|
|
303
|
+
control characters in Latin-1.
|
|
304
|
+
- **`scrub` is lossy.** It replaces each byte with `U+FFFD`: `client’s` becomes
|
|
305
|
+
`client�s`, `Muñoz` becomes `Mu�oz`. Author surnames are among the
|
|
306
|
+
affected fields. Verified on a real file — scrub and transcode yield the same
|
|
307
|
+
*length*, so a length check will not catch the damage.
|
|
308
|
+
|
|
309
|
+
Don't copy `lib/relaton/bipm/si_brochure_parser.rb:33-35`, whose
|
|
310
|
+
`force_encoding` guard is a no-op. The `scrub` idiom at
|
|
311
|
+
`lib/relaton/itu/data_crawler_r.rb:588-599` is right *there* because it repairs
|
|
312
|
+
scraped HTML with no recoverable original; here there is one.
|
|
313
|
+
|
|
314
|
+
### Why the rescue is per file, and why the tally is in the parent
|
|
315
|
+
|
|
316
|
+
`parse_bibxml` rescues `StandardError` — not a narrow list. lutaml raises
|
|
317
|
+
`InvalidFormatError` on bad bytes, but the converter also runs regexes over
|
|
318
|
+
parsed text (`parse_surname_initials` and friends), which raise
|
|
319
|
+
`ArgumentError: invalid byte sequence` on anything that slips through.
|
|
320
|
+
|
|
321
|
+
It has to be **inside** the worker. The drafts path runs under `Parallel.map`,
|
|
322
|
+
which discards every result from the pass when a worker raises — so one bad file
|
|
323
|
+
out of ~167k meant no index at all, plus the already-written records orphaned on
|
|
324
|
+
disk, because `record_index_entry` and `index.save` only run after both passes
|
|
325
|
+
return. A rescue around `parallelize` would salvage "the crawl didn't die", not
|
|
326
|
+
"the other 166,999 files got indexed".
|
|
327
|
+
|
|
328
|
+
The skip therefore rides back as `{ unparsed: path, error: }` and is counted in
|
|
329
|
+
the parent, the way `parse_pubid`'s result rides back as `result[:pubid]` — a
|
|
330
|
+
counter incremented in a worker process is lost. `report_unparsed` warns once
|
|
331
|
+
with a count and a sample, not once per file.
|
|
332
|
+
|
|
333
|
+
A failed version is dropped from `sorted` **before** `link_neighbor_relations`
|
|
334
|
+
and `build_unversioned_doc` run, since both dereference `entry[:bib]`. Knock-on
|
|
335
|
+
to accept: neighbours then link across the gap, and if the dropped file was the
|
|
336
|
+
newest, the aggregator inherits from the previous version. With CP1252 recovery
|
|
337
|
+
that should approach zero — which is the argument for recovering over skipping.
|
|
338
|
+
|
|
339
|
+
Verified end to end on 41,408 real draft files (16 invalid UTF-8): the crawl
|
|
340
|
+
completes in 57 s, writes 46,461 records and 46,461 index rows, **0 skipped**,
|
|
341
|
+
and the affected files come through with their real characters —
|
|
342
|
+
`\x93Access Node Contr` → `“Access Node Contr`.
|
|
343
|
+
|
|
344
|
+
## Testing Patterns
|
|
345
|
+
|
|
346
|
+
- **Index fixture:** `spec/ietf/fixtures/ietf-index-v2.zip` - the combined index, copied **wholesale** from `relaton-data-ietf` rather than curated, since a subset goes stale and surfaces as a 404 on the document fetch. `spec/ietf/support/webmock.rb` deserialises it into pubid ids, sorts by the narrowing key, sets `sorted = true` (without which every lookup is a ~40 s scan) and pre-loads it into the pool as `:IETF` in `before(:suite)`; `spec/ietf/support/vcr.rb` ignores requests ending `index-v2.zip`. Refresh by re-downloading that zip. There is **no** `rake spec:update_index` task in this repo.
|
|
347
|
+
- **VCR cassettes** in `spec/ietf/vcr_cassettes/` record HTTP interactions; tests use `vcr: "cassette_name"` metadata
|
|
348
|
+
- **WebMock** disables net connections by default
|
|
349
|
+
- **Fixtures** in `spec/ietf/fixtures/` — XML/YAML expected outputs; many tests auto-generate fixtures on first run (`File.write file, xml unless File.exist? file`)
|
|
350
|
+
- **An oracle for `read_bibxml` must read binary.** The method is byte-verbatim (`File.binread`), so comparing it against `File.read(path, encoding: "UTF-8")` compares two different strings **on Windows only**: Ruby opens a file in text mode there and collapses CRLF to LF. The repo declares no `.gitattributes` and the GitHub Windows runner image defaults to `core.autocrlf=true`, so a fixture stored with LF is checked out CRLF — which failed all three `windows-latest` jobs while every Linux and macOS job passed. Use `File.read(path, mode: "rb", encoding: "UTF-8")`. macOS cannot reproduce this by checking the fixture out as CRLF, because its text-mode read does not translate; emulate Windows explicitly with `File.read(path, encoding: "UTF-8", universal_newline: true)`. The two `keeps CRLF line endings verbatim` examples pin the contract with `binwrite` temp files, independent of the checkout — one per branch, since the CP1252 repair rebuilds the string run by run and is where a line ending is likeliest to be lost. Note that a byte-equality oracle alone cannot catch a dropped `force_encoding`: `rfc.xml` is pure ASCII, and Ruby compares two ASCII-only strings as equal across encodings, so the example asserts `encoding` separately.
|
|
351
|
+
- **Schema validation** via `ruby-jing` against the shared RNG grammars in the repo-root `grammar/` (`Jing.new "../../grammar/relaton-ietf-compile.rng"`, relative to the suite's CWD `spec/ietf/`)
|
|
352
|
+
- **Shared examples** for org/person parsing in `bibxml_parser_spec.rb` (`parse_org`, `parse_person`)
|
|
353
|
+
- **`equivalent-xml`** gem used for XML comparison (`be_equivalent_to` matcher)
|
|
354
|
+
|
|
355
|
+
## RuboCop
|
|
356
|
+
|
|
357
|
+
Inherits from `rubose/rubocop-rubose` (Ribose OSS config). Target Ruby 3.1. Rails cops disabled.
|