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,120 @@
1
+ = Relaton::Logger
2
+
3
+ Relaton::Logger is a Ruby gem that implements a logger for Relaton gems. It is also possible to use this gem as a standalone logger.
4
+
5
+ == Usage
6
+
7
+ The Relaton::Logger gem provides a logger pool that can be used to log messages. The logger pool is a singleton and can be accessed by calling `Relaton.logger_pool`. The logger pool is an Array that contains loggers. By default, the logger pool contains a single logger, which is an instance of `Relaton::Logger::Log` class, which inherits the `Logger` class from the Ruby standard library.
8
+
9
+ The logger pool can be used to log messages by calling the methods of the loggers in the pool. These methods are the same as the methods of the `Logger` class from the Ruby standard library (`debug`, `info`, `warn`, `error`, `fatal`, `unknown`).
10
+
11
+ [source, ruby]
12
+ ----
13
+ require "relaton/logger"
14
+
15
+ Relaton.logger_pool.info "message"
16
+ INFO: message
17
+ ----
18
+
19
+ The program name and the key can be passed as arguments to the log methods. The program name is a string that is added to the log message. The key is a string that is added to the log message in parentheses.
20
+
21
+ [source, ruby]
22
+ ----
23
+ Relaton.logger_pool.warn "message", "progname", key: "KEY"
24
+ [progname] WARN: (KEY) message
25
+ ----
26
+
27
+ The block form of the log methods can be used to log messages that are expensive to generate. The block is called only if the message is logged.
28
+
29
+ [source, ruby]
30
+ ----
31
+ Relaton.logger_pool.error("progname", key: "KEY") { "message" }
32
+ [progname] ERROR: (KEY) message
33
+ ----
34
+
35
+ The logger pool can be configured to contain multiple loggers. Initially, there is one `:default` logger. Any instance of loggers in the logger pool are accessible with `[]` method. To add or replace a logger use `[]=` method of the logger pool and pass the logger as an argument. The loggers in the pool are called in the order they are added to the pool.
36
+
37
+ [source, ruby]
38
+ ----
39
+ # Access logger
40
+ Relaton.logger_pool[:default]
41
+ =>
42
+ #<Relaton::Logger::Log:0x00000001212068c8
43
+
44
+ # Add a new logger
45
+ Relaton.logger_pool[:file] = Relaton::Logger::Log.new("relaton.log", levels: [:info, :warn])
46
+ ----
47
+
48
+ To change the log level of a logger, call the `level=` method of the logger.
49
+
50
+ [source, ruby]
51
+ ----
52
+ Relaton.logger_pool[:default].levels = [:info, :warn, :error]
53
+ Relaton.logger_pool[:default].add_level :fatal
54
+ Relaton.logger_pool[:default].remove_level :warn
55
+ ----
56
+
57
+ To create a new logger, call the `new` method of the `Relaton::Logger::Log` class and pass arguments:
58
+
59
+ - `logdev` - file name or an IO object. If nil or File::NULL, messages are not logged. If it is a file name, the file is opened in append mode. If it is an IO object, messages are written to it.
60
+ - `shift_age` - the number of old log files to keep. If 0, no old log files are kept. If it is a number, old log files are kept and the log file is rotated when it reaches the maximum size. If it is a string, old log files are kept and the log file is rotated when it reaches the maximum size. The string is a time unit and a number, for example, "daily 7" or "weekly 4". Default is 0.
61
+ - `shift_size` - the maximum size of the log file. If it is a number, the log file is rotated when it reaches the maximum size. If it is a string, the log file is rotated when it reaches the maximum size. The string is a number and a time unit, for example, "1048576" or "1M". Default is 1048576.
62
+ - `levels` - an array of symbols that represent the log levels The log levels are `:debug`, `:info`, `:warn`, `:error`, `:fatal`, `:unknown`. Default is `[:info, :warn, :error, :fatal, :unknown]`.
63
+ - `formatter` - a formatter object that is used to format the log messages. Two formatters are available at this moment `Relaton::Logger::FormatterString`, `Relaton::Logger::FormatterJson`, and `Proc`. The default formater is `Relaton::Logger::FormatterString`.
64
+ - `progname` - a string that is added to the log message. Default is nil.
65
+
66
+ [source, ruby]
67
+ ----
68
+ Relaton::Logger::Log.new("relaton.log", levels: [:info, :warn], formatter: Relaton::Logger::FormatterJSON, progname: "progname")
69
+ ----
70
+
71
+ To create a custom formatter implement a class with the `call` method which takes agruments:
72
+
73
+ - `severity` - the log level
74
+ - `time` - the time the message was logged
75
+ - `progname` - the program name
76
+ - `msg` - the message
77
+ - any other keyword arguments that are passed to the log method, for example, `key`
78
+
79
+ [source, ruby]
80
+ ----
81
+ class CustomFormatter
82
+ def call(severity, time, progname, msg, key: "Key 1")
83
+ "#{time} [#{severity}] #{progname} (#{key}): #{msg}\n"
84
+ end
85
+ end
86
+
87
+ Relaton::Logger::Log.new("relaton.log", formatter: CustomFormatter)
88
+ ----
89
+
90
+ It's possible to use `Proc` as a formatter. The `Proc` object is called with the same arguments as the `call` method of the custom formatter.
91
+
92
+ [source, ruby]
93
+ ----
94
+ log = Relaton::Logger::Log.new("relaton.log")
95
+
96
+ log.formatter = Proc.new do |severity, time, progname, msg, key: "Key 1"|
97
+ "#{time} [#{severity}] #{progname} (#{key}): #{msg}\n"
98
+ end
99
+ ----
100
+
101
+ A log file can be cleared by calling the `truncate` method of the logger pool.
102
+
103
+ [source, ruby]
104
+ ----
105
+ Relaton.logger_pool.truncate
106
+ ----
107
+
108
+ == Development
109
+
110
+ 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.
111
+
112
+ 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
113
+
114
+ == Contributing
115
+
116
+ Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton-logger.
117
+
118
+ == License
119
+
120
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,180 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ relaton-nist is a Ruby gem that retrieves and manages NIST (National Institute of Standards and Technology) bibliographic metadata as part of the Relaton ecosystem. Data sources include the NIST Cybersecurity Resource Center (CSRC) and the NIST Library (MARC21/MODS XML).
8
+
9
+ ## Development Commands
10
+
11
+ ```bash
12
+ # Install dependencies
13
+ bundle install
14
+
15
+ # Run all tests (any of these)
16
+ rake
17
+ rake spec
18
+ bundle exec rspec
19
+
20
+ # Run a single test file
21
+ bundle exec rspec spec/relaton/nist/item_spec.rb
22
+
23
+ # Run a specific test by line number
24
+ bundle exec rspec spec/relaton/nist/item_spec.rb:10
25
+
26
+ # Interactive console with gem loaded
27
+ bin/console
28
+
29
+ # Lint (uses Ribose OSS rubocop config)
30
+ bundle exec rubocop
31
+ ```
32
+
33
+ ## Architecture
34
+
35
+ ### Module Structure
36
+
37
+ All classes live under `Relaton::Nist` (entry point: `require 'relaton/nist'`).
38
+
39
+ ### Lutaml-Model Serialization
40
+
41
+ All data models inherit from `Lutaml::Model::Serializable` and use declarative attribute/mapping DSL for XML and YAML serialization. The base bibliographic types come from `relaton-bib` (aliased as `Bib`).
42
+
43
+ **Core model hierarchy**:
44
+ - `Bib::Item` → `Relaton::Nist::Item` (`model ItemData`)
45
+ - `Relaton::Nist::Bibitem` (includes `Bib::BibitemShared`)
46
+ - `Relaton::Nist::Bibdata` (includes `Bib::BibdataShared`)
47
+ - `Bib::ItemData` → `Relaton::Nist::ItemData`
48
+
49
+ **NIST-specific extensions** (under `lib/relaton/nist/`):
50
+ - `Ext` — extension block holding doctype, comment period (extends `Bib::Ext`)
51
+ - `Date` — adds `abandoned` and `superseded` date types (extends `Bib::Date`)
52
+ - `Relation` — adds `obsoletedBy`, `supersedes`, `supersededBy` relation types (extends `Bib::Relation`)
53
+ - `CommentPeriod` — from/to/extended date ranges (extends `Lutaml::Model::Serializable`)
54
+ - `Doctype` — currently only `standard` (extends `Bib::Doctype`)
55
+
56
+ **Search/retrieval layer** (extends `relaton-core`):
57
+ - `Bibliography` — class methods `search(text, year, opts)` and `get(code, year, opts)`
58
+ - `HitCollection` < `Core::HitCollection` — searches GitHub index + CSRC JSON
59
+ - `Hit` < `Core::Hit` — lazily resolves items via `Scraper`
60
+ - `Processor` < `Core::Processor` — Relaton plugin interface (`get`, `fetch_data`, `from_xml`, `from_yaml`)
61
+
62
+ **Data fetching/parsing**:
63
+ - `DataFetcher` < `Core::DataFetcher` — fetches NIST Tech Pubs MODS XML from GitHub releases. `#source_url(source)` builds the download URL: with no tag (nil/blank/`"latest"`) it uses GitHub's `releases/latest/download/allrecords-MODS.xml` redirect, so the `relaton-data-nist` crawler auto-picks up new releases without a gem release; passing a concrete tag (e.g. `"June2026"`) pins that release. The `source` flows from `Core::DataFetcher.fetch(source)` → `#fetch(source)` → `#fetch_tech_pubs(source)` (the crawler workflow's `args` input can supply it).
64
+ - **Per-record resilience (mirrors the ISO flavor).** A single bad record must never abort the crawl. `#write_file` skips (never `id.content`-crashes on) a record whose MODS entry yields **no docidentifier** (e.g. a DOI stored only in `<identifier type="doi">`, which `ModsParser#parse_doi` doesn't read), and records ids that `#pubid` can't parse (new series like `NIST RB`/`NIST CHIPS`) as "not indexed" — the YAML is still written, only the index-v2 entry is skipped. Both classes accumulate in `#failures`; the `#report_errors` override emits each at `:error` (via `#log_error` → `Util.error`), which the shared `Core::DataFetcher` gh_issue machinery turns into one "Error fetching documents" GitHub issue in CI (`GITHUB_REPOSITORY` + `GITHUB_TOKEN`). `#pubid` no longer warns per-failure; reporting is centralized in `#report_errors`.
65
+ - **Draft-stage rows come from CSRC (relaton#313).** The MODS collection has no draft records, so `#fetch_csrc` adds the CSRC pubs-export feed (`PubsExport.instance.data`) after the MODS pass. It builds each row **exactly as the runtime builds a CSRC hit** — `HitCollection#hit_data` (which calls `#pubs_export_id`) → `Scraper.parse_page` — so the crawled id and the live id are one string, with the stage, revision and update from the row's uri and DOI (`NIST FIPS 197/Upd1 ipd`, `NIST SP 800-108r1 ipd`, `NIST SP 1800-12 2pd`). Only a row whose id carries a **stage** is written; finals come from MODS, so a CSRC row never shadows a base document. On the 914-row fixture feed this writes 302 staged records. Don't go back to a hand-built `"NIST #{series} #{docnumber} #{stage}"` id: it dropped the numbered drafts (`2pd`…), the `/UpdN`, and spelled revisions `Rev. 1`. A stage that CSRC does not list (e.g. `NIST FIPS 197 fpd`) still answers through the consumer's #303 family fallback. Three more rules: `#csrc_bib` clears the `fetched` date the runtime stamps, or every draft changes each day and `Core#force_write` rewrites ~300 files per incremental crawl; CSRC lists a republished draft twice (`…/ipd` and `…/ipd-(1)`, one DOI, so one id), so each id is written once and the plain uri wins; and only the build step is rescued (to a `#failures` entry) — a write error stops the crawl, as in the MODS pass. `#pubs_export_id` must not mutate its row (`sub`, not `sub!`): the crawler and the runtime share the `PubsExport` data.
66
+ - **A CSRC relation names the related document (relaton#314).** `Scraper#related_id` finds the related feed row by its uri and derives its id with `HitCollection.pubs_export_id` (the class method the instance method delegates to). The entry's own `docidentifier` drops the update and the part, and the hit's code is the record's own id, so neither is the related id. `HitCollection#search_filter` ends in an `ensure` that returns `arr`: every branch must set `arr`, because an earlier `return` is overridden there.
67
+ - **`#write_file` serializes before it indexes**, so a record that fails to serialize leaves no index row naming a file that was never written. That is how a `Bib::Date` in `Scraper.fetch_dates` hid: `Nist::ItemData` takes `Nist::Date`, and YAML serialization rejects the parent class (the same strictness as the docidentifier note below).
68
+ - `ModsParser` — maps MODS XML (via `loc_mods`) to `ItemData`
69
+ - `Scraper` — fetches items from GitHub YAML or CSRC JSON
70
+ - `PubsExport` — singleton; caches CSRC pubs-export zip with thread-safe daily updates
71
+
72
+ ### Pubid-backed docidentifier
73
+
74
+ `Relaton::Nist::Docidentifier` (`docidentifier.rb`, `< Bib::Docidentifier`,
75
+ declared flat like the rest of the NIST models) parses its `content` into a
76
+ `Pubid::Nist::Identifier` kept in `@pubid`, while the lutaml `content` attribute
77
+ stays a plain string for serialization. It implements the base class's abstract
78
+ `remove_part!` / `remove_date!` / `to_all_parts!` (plus a NIST `remove_stage!`)
79
+ by mutating the pubid graph and re-rendering via `refresh_content!`, so NIST
80
+ items no longer raise `NotImplementedError` on the `Bib::ItemData#to_all_parts!`
81
+ / `#remove_date!` paths. It is wired into `item.rb`
82
+ (`attribute :docidentifier, Docidentifier, collection: true`) and into
83
+ `item_base.rb` (`Nist::ItemBase`, the nested bibitem used inside `Relation`), so
84
+ relation cross-reference ids are pubid-backed too — mirroring
85
+ `iso/model/item_base.rb` / `iec/model/item_base.rb`. `ModsParser` and `Scraper`
86
+ build **every** docid (primary NIST id, DOI, and relation cross-refs) as this
87
+ class. Mirrors `lib/relaton/iec/model/docidentifier.rb` and
88
+ `lib/relaton/ccsds/model/docidentifier.rb`.
89
+
90
+ NIST-specific gotchas (why this isn't a straight copy of IEC/CCSDS):
91
+
92
+ - **Adopt-on-round-trip, not a `type` gate.** A NIST DOI such as
93
+ `NIST.SP.800-162` is itself a valid NIST pubid in dotted MR form whose `:human`
94
+ render is `NIST SP 800-162` — so blindly parsing it would rewrite the DOI.
95
+ `content=` therefore keeps the parsed pubid **only when
96
+ `pubid.to_s(:human) == content`**; otherwise the raw string is preserved.
97
+ A `type == "NIST"` gate looks tempting but breaks: lutaml applies setters in
98
+ attribute-declaration order (`content` before `type`), so on `from_yaml` the
99
+ gate sees `type == nil`, drops the pubid, and the mutators silently no-op on
100
+ YAML-loaded items. The round-trip test is type/order-independent.
101
+ - **Uniform collection type.** The `docidentifier` collection is typed
102
+ `Nist::Docidentifier`, and lutaml's **YAML/JSON** serializer strictly rejects a
103
+ parent `Bib::Docidentifier` instance in that slot (`IncorrectModelError`) —
104
+ which would crash the YAML-default `DataFetcher` crawler on nearly every
105
+ DOI-bearing record (XML tolerates it, which is why it hides in XML specs). So
106
+ the DOI is a `Nist::Docidentifier` too (kept raw via the round-trip rule), not
107
+ a `Bib::Docidentifier`.
108
+ - **The date lives in the `edition` component.** NIST has no `:date` attribute;
109
+ a year is carried as the edition id (`FIPS 46e1977`) or its trailing
110
+ `additional_text` (`NBS CIRC 11e2.1915`), and the scalar `year`/`edition_year`
111
+ fields are nil on the common parse paths. `remove_date!` clears the scalar
112
+ fields *and* strips a 4-digit-year edition/`additional_text`, while preserving
113
+ numbered editions/revisions (`e2`, `r5`). Dates that ride the `update`
114
+ component (some NBS supplements) are left intact — `update` also encodes
115
+ non-date update codes (`/Upd2`), so stripping it would corrupt identity.
116
+ - **No `(all parts)` marker in `content`.** `content` is live-derived from
117
+ `@pubid`, so `to_all_parts!` freezes the already-stripped rendering into
118
+ `@raw_content` before wrapping `@pubid` in pubid's `AllParts` — matching
119
+ IEC/CCSDS. `#pubid` itself, read directly, now answers `all_parts? ==
120
+ true` and renders WITH pubid's generic "(all parts)" marker.
121
+
122
+ ### Serialization Round-Trip Pattern
123
+
124
+ Models support `from_xml`/`to_xml` and `from_yaml`/`to_yaml`. Tests verify round-trip fidelity by parsing a fixture, re-serializing, and comparing output to input.
125
+
126
+ ### XML Schema Validation
127
+
128
+ RNG (Relax NG) schemas in `grammars/` validate XML output. Tests use the `jing` gem:
129
+ ```ruby
130
+ schema = Jing.new("grammars/relaton-nist-compile.rng")
131
+ errors = schema.validate(file)
132
+ ```
133
+ `relaton-nist-compile.rng` is the top-level schema that includes `relaton-nist.rng` and `basicdoc.rng`.
134
+
135
+ ### HTTP Recording
136
+
137
+ Tests use VCR with WebMock. Cassettes are stored in `spec/vcr_cassettes/` and
138
+ `spec/support/vcr.rb` sets `record: :once` **with `re_record_interval: 7 days`**.
139
+
140
+ **Gotcha (this bites periodically):** once a cassette is older than 7 days, the
141
+ next run **re-records it from the live network** — so the suite is not truly
142
+ deterministic. Two known failure modes when that fires: the NIST GitHub source
143
+ data can drift (an author renamed, a `loc.gov` `identifier` dropped) so the
144
+ round-tripped XML no longer matches the on-disk `spec/fixtures/*.xml` fixtures;
145
+ and any flavor whose upstream throttles the recorder records a bad response.
146
+ When cassettes show up mass-modified in `git status` for no code reason, that's
147
+ the 7-day re-record firing, not a manual change. To recover: re-record cleanly
148
+ (delete the stale cassette, run the spec) and, for the integration fixtures that
149
+ compare full XML (`get.xml`, `hit.xml`, `hit_bibitem.xml`), regenerate them
150
+ (delete → the spec rewrites them via `File.write … unless File.exist?`).
151
+
152
+ ### Test Data Stubbing
153
+
154
+ Tests pre-load both the NIST index and CSRC pubs-export data from local fixtures in `before(:suite)` (see `spec/support/webmock.rb`), avoiding all HTTP requests for these data sources. VCR is configured to ignore both `index-v2.zip` and `pubs-export` requests (`spec/support/vcr.rb`).
155
+
156
+ The gem consumes the **index-v2** index (`INDEXFILE = "index-v2"`): each row's `:id` is a `Pubid::Nist::Identifier` hash (lean `to_hash` form), so `HitCollection#from_ga` narrows candidates by number via binary search before the substring block filter, and stringifies `row[:id].to_s` at the Hit boundary. `spec/fixtures/index-v2.zip` is required for the suite to run (`rake spec:update_index` downloads it from `relaton-data-nist/v2`).
157
+
158
+ - **Index**: The YAML inside `spec/fixtures/index-v2.zip` is written to a temp file and loaded through `Relaton::Index::Type.new(:nist, file:, pubid_class: ::Pubid::Nist::Identifier)`; calling `type.index` forces the offline read + `pubid_class` deserialization (and sort) before net is blocked. The instance is injected into `Relaton::Index.pool`, with `actual?` overridden to match only the remote (`url:`) lookup so the producer-side `find_or_create(:nist, file:, pubid_class:)` still gets a fresh instance. Run `rake spec:update_index` to refresh.
159
+ - **PubsExport**: The `PubsExport` singleton's `@data` is set directly from `spec/fixtures/pubs-export.zip`. Run `rake spec:update_pubs_export` to refresh.
160
+
161
+ To apply the index stubbing pattern to other relaton gems:
162
+
163
+ 1. Add a `spec:update_index` rake task (downloads `index-v2.zip` from the gem's GitHub data repo)
164
+ 2. Run `rake spec:update_index` to create `spec/fixtures/index-v2.zip`
165
+ 3. In `spec/support/webmock.rb`: extract the zip's YAML, write it to a temp file, create a `Type` with the flavor's `pubid_class`, call `type.index` to deserialize + sort, override `actual?` to match the remote `url:` lookup, and inject into `Relaton::Index.pool`
166
+ 4. In `spec/support/vcr.rb`: add `ignore_request` for `index-v2.zip`
167
+ 5. Remove any `allow_any_instance_of(Relaton::Index::Type)` workarounds from specs
168
+
169
+ ## Key Dependencies
170
+
171
+ - `relaton-bib` — base bibliographic models and shared mixins
172
+ - `relaton-core` — base classes for Processor, HitCollection, Hit, DataFetcher
173
+ - `loc_mods` — MODS (Metadata Object Description Schema) XML parsing
174
+ - `pubid` — NIST publication ID parsing
175
+ - `relaton-index` — index/search utilities
176
+ - `mechanize` — HTTP fetching for data sources
177
+
178
+ ## Code Style
179
+
180
+ RuboCop config inherits from the Ribose OSS style guide. Target Ruby version is 3.1.