pubid 2.0.0.pre.alpha.11 → 2.0.0.pre.alpha.13

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 (274) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +43 -1
  3. data/data/ieee/update_codes.yaml +17 -4
  4. data/data/nist/update_codes.yaml +7 -3
  5. data/lib/pubid/adobe/builder.rb +2 -0
  6. data/lib/pubid/all_parts.rb +201 -0
  7. data/lib/pubid/all_parts_identifier.rb +19 -0
  8. data/lib/pubid/amca/CLAUDE.md +47 -0
  9. data/lib/pubid/amca/builder.rb +3 -5
  10. data/lib/pubid/amca/identifiers/base.rb +10 -0
  11. data/lib/pubid/amca/identifiers/publication.rb +13 -0
  12. data/lib/pubid/amca/parser.rb +2 -1
  13. data/lib/pubid/amca/renderer.rb +22 -33
  14. data/lib/pubid/amca/urn_generator.rb +21 -2
  15. data/lib/pubid/amca/urn_parser.rb +36 -10
  16. data/lib/pubid/ansi/builder.rb +6 -0
  17. data/lib/pubid/api/CLAUDE.md +23 -0
  18. data/lib/pubid/api/builder.rb +2 -0
  19. data/lib/pubid/ashrae/CLAUDE.md +13 -0
  20. data/lib/pubid/ashrae/builder.rb +58 -14
  21. data/lib/pubid/ashrae/identifiers/errata.rb +14 -2
  22. data/lib/pubid/ashrae/identifiers/interpretation.rb +2 -10
  23. data/lib/pubid/ashrae/parser.rb +62 -28
  24. data/lib/pubid/ashrae/renderer.rb +32 -1
  25. data/lib/pubid/ashrae/urn_generator.rb +32 -9
  26. data/lib/pubid/asme/CLAUDE.md +25 -0
  27. data/lib/pubid/asme/builder.rb +16 -9
  28. data/lib/pubid/asme/components/code.rb +2 -0
  29. data/lib/pubid/asme/identifiers/standard.rb +6 -1
  30. data/lib/pubid/asme/parser.rb +41 -14
  31. data/lib/pubid/astm/CLAUDE.md +9 -0
  32. data/lib/pubid/astm/builder.rb +2 -0
  33. data/lib/pubid/astm/components/code.rb +2 -0
  34. data/lib/pubid/astm/parser.rb +4 -1
  35. data/lib/pubid/bipm/CLAUDE.md +11 -0
  36. data/lib/pubid/bipm/builder.rb +2 -0
  37. data/lib/pubid/bsi/CLAUDE.md +93 -0
  38. data/lib/pubid/bsi/builder.rb +13 -11
  39. data/lib/pubid/bsi/components/date.rb +10 -4
  40. data/lib/pubid/bsi/identifiers/addendum_document.rb +2 -0
  41. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +6 -54
  42. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +5 -22
  43. data/lib/pubid/bsi/identifiers/amendment.rb +36 -12
  44. data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +1 -1
  45. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -0
  46. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +23 -26
  47. data/lib/pubid/bsi/identifiers/corrigendum.rb +29 -12
  48. data/lib/pubid/bsi/identifiers/expert_commentary.rb +6 -7
  49. data/lib/pubid/bsi/identifiers/handbook.rb +1 -1
  50. data/lib/pubid/bsi/identifiers/national_annex.rb +18 -20
  51. data/lib/pubid/bsi/identifiers/practice_guide.rb +1 -1
  52. data/lib/pubid/bsi/identifiers/root_identity.rb +31 -0
  53. data/lib/pubid/bsi/identifiers/set.rb +2 -0
  54. data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -0
  55. data/lib/pubid/bsi/identifiers.rb +1 -0
  56. data/lib/pubid/bsi/parser.rb +8 -8
  57. data/lib/pubid/bsi/renderer.rb +20 -20
  58. data/lib/pubid/bsi/single_identifier.rb +11 -2
  59. data/lib/pubid/bsi/urn_generator.rb +28 -18
  60. data/lib/pubid/builder/base.rb +27 -0
  61. data/lib/pubid/calconnect/builder.rb +2 -0
  62. data/lib/pubid/calconnect/identifier.rb +4 -0
  63. data/lib/pubid/ccsds/builder.rb +2 -0
  64. data/lib/pubid/ccsds/identifier.rb +12 -0
  65. data/lib/pubid/ccsds/single_identifier.rb +14 -10
  66. data/lib/pubid/cen_cenelec/CLAUDE.md +59 -0
  67. data/lib/pubid/cen_cenelec/builder.rb +6 -1
  68. data/lib/pubid/cen_cenelec/identifier.rb +10 -27
  69. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +3 -10
  70. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +3 -10
  71. data/lib/pubid/cen_cenelec/parser.rb +11 -3
  72. data/lib/pubid/cie/CLAUDE.md +58 -0
  73. data/lib/pubid/cie/builder.rb +2 -0
  74. data/lib/pubid/cie/components/language.rb +2 -0
  75. data/lib/pubid/components/adoption.rb +2 -0
  76. data/lib/pubid/components/code.rb +2 -0
  77. data/lib/pubid/components/date.rb +8 -6
  78. data/lib/pubid/components/edition.rb +2 -0
  79. data/lib/pubid/components/iteration.rb +2 -0
  80. data/lib/pubid/components/language.rb +2 -0
  81. data/lib/pubid/components/locality.rb +2 -0
  82. data/lib/pubid/components/publisher.rb +2 -0
  83. data/lib/pubid/components/relationship.rb +2 -0
  84. data/lib/pubid/components/stage.rb +2 -0
  85. data/lib/pubid/components/supplement.rb +2 -0
  86. data/lib/pubid/components/type.rb +2 -0
  87. data/lib/pubid/components/typed_stage.rb +8 -0
  88. data/lib/pubid/csa/CLAUDE.md +41 -0
  89. data/lib/pubid/csa/builder.rb +5 -3
  90. data/lib/pubid/csa/identifier.rb +13 -1
  91. data/lib/pubid/csa/identifiers/bundled.rb +2 -2
  92. data/lib/pubid/csa/identifiers/cec.rb +1 -1
  93. data/lib/pubid/csa/renderer.rb +12 -12
  94. data/lib/pubid/csa/single_identifier.rb +18 -1
  95. data/lib/pubid/csa/urn_generator.rb +1 -1
  96. data/lib/pubid/doi/builder.rb +2 -0
  97. data/lib/pubid/easc/builder.rb +2 -0
  98. data/lib/pubid/ecma/CLAUDE.md +28 -0
  99. data/lib/pubid/ecma/builder.rb +2 -0
  100. data/lib/pubid/ecma/identifier.rb +7 -0
  101. data/lib/pubid/etsi/CLAUDE.md +34 -0
  102. data/lib/pubid/etsi/builder.rb +2 -0
  103. data/lib/pubid/etsi/components/code.rb +6 -0
  104. data/lib/pubid/etsi/components/version.rb +2 -0
  105. data/lib/pubid/etsi/identifiers/etsi_standard.rb +7 -0
  106. data/lib/pubid/evs/CLAUDE.md +58 -0
  107. data/lib/pubid/evs/builder.rb +3 -1
  108. data/lib/pubid/evs/identifier.rb +6 -6
  109. data/lib/pubid/evs/identifiers/national_adoption.rb +7 -2
  110. data/lib/pubid/evs/renderer.rb +1 -1
  111. data/lib/pubid/evs/urn_generator.rb +1 -1
  112. data/lib/pubid/evs/urn_parser.rb +1 -1
  113. data/lib/pubid/gb/CLAUDE.md +140 -0
  114. data/lib/pubid/gb/builder.rb +21 -11
  115. data/lib/pubid/gb/identifier.rb +28 -7
  116. data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
  117. data/lib/pubid/gb/identifiers.rb +1 -0
  118. data/lib/pubid/gb/parser.rb +4 -0
  119. data/lib/pubid/gb/renderer.rb +4 -4
  120. data/lib/pubid/gb.rb +1 -0
  121. data/lib/pubid/gost/CLAUDE.md +64 -0
  122. data/lib/pubid/gost/builder.rb +3 -1
  123. data/lib/pubid/gost/identifier.rb +5 -0
  124. data/lib/pubid/iala/CLAUDE.md +82 -0
  125. data/lib/pubid/iala/builder.rb +2 -0
  126. data/lib/pubid/iana/CLAUDE.md +7 -0
  127. data/lib/pubid/iana/builder.rb +2 -0
  128. data/lib/pubid/identifier.rb +172 -20
  129. data/lib/pubid/idf/builder.rb +6 -1
  130. data/lib/pubid/idf/identifier.rb +12 -0
  131. data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
  132. data/lib/pubid/idf/identifiers/amendment.rb +1 -1
  133. data/lib/pubid/idf/identifiers/corrigendum.rb +1 -1
  134. data/lib/pubid/idf/identifiers/international_standard.rb +1 -1
  135. data/lib/pubid/idf/identifiers/reviewed_method.rb +1 -1
  136. data/lib/pubid/idf/identifiers.rb +1 -0
  137. data/lib/pubid/idf/single_identifier.rb +1 -1
  138. data/lib/pubid/iec/CLAUDE.md +31 -0
  139. data/lib/pubid/iec/builder.rb +7 -1
  140. data/lib/pubid/iec/components/consolidated_amendment.rb +4 -0
  141. data/lib/pubid/iec/components/sheet.rb +2 -0
  142. data/lib/pubid/iec/components/trf_info.rb +2 -0
  143. data/lib/pubid/iec/components/vap_suffix.rb +2 -0
  144. data/lib/pubid/iec/identifier.rb +7 -2
  145. data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
  146. data/lib/pubid/iec/identifiers.rb +1 -0
  147. data/lib/pubid/iec/renderer.rb +0 -1
  148. data/lib/pubid/iec/urn_generator.rb +9 -1
  149. data/lib/pubid/iec/urn_parser.rb +3 -2
  150. data/lib/pubid/ieee/CLAUDE.md +97 -0
  151. data/lib/pubid/ieee/builder.rb +176 -7
  152. data/lib/pubid/ieee/components/code.rb +2 -0
  153. data/lib/pubid/ieee/components/draft.rb +35 -2
  154. data/lib/pubid/ieee/components/typed_stage.rb +2 -0
  155. data/lib/pubid/ieee/identifiers/base.rb +20 -0
  156. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
  157. data/lib/pubid/ieee/identifiers/joint_development.rb +17 -10
  158. data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
  159. data/lib/pubid/ieee/identifiers/si_standard.rb +5 -2
  160. data/lib/pubid/ieee/ire/parser.rb +19 -1
  161. data/lib/pubid/ieee/parser.rb +337 -49
  162. data/lib/pubid/ieee/project_renderer.rb +46 -0
  163. data/lib/pubid/ieee/renderer.rb +46 -14
  164. data/lib/pubid/ieee/urn_generator.rb +31 -0
  165. data/lib/pubid/ieee.rb +2 -0
  166. data/lib/pubid/ietf/CLAUDE.md +7 -0
  167. data/lib/pubid/ietf/builder.rb +2 -0
  168. data/lib/pubid/iho/builder.rb +2 -0
  169. data/lib/pubid/isbn/builder.rb +2 -0
  170. data/lib/pubid/iso/CLAUDE.md +47 -0
  171. data/lib/pubid/iso/builder.rb +29 -16
  172. data/lib/pubid/iso/components/publisher.rb +2 -0
  173. data/lib/pubid/iso/components.rb +0 -1
  174. data/lib/pubid/iso/identifier.rb +17 -18
  175. data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
  176. data/lib/pubid/iso/identifiers/directives.rb +7 -4
  177. data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
  178. data/lib/pubid/iso/identifiers/tc_document.rb +31 -46
  179. data/lib/pubid/iso/identifiers.rb +1 -0
  180. data/lib/pubid/iso/normalizer.rb +1 -1
  181. data/lib/pubid/iso/rendering_style.rb +0 -1
  182. data/lib/pubid/iso/urn_generator.rb +10 -10
  183. data/lib/pubid/iso.rb +5 -4
  184. data/lib/pubid/itu/CLAUDE.md +69 -0
  185. data/lib/pubid/itu/builder.rb +12 -0
  186. data/lib/pubid/itu/components/code.rb +2 -0
  187. data/lib/pubid/itu/components/designation.rb +2 -0
  188. data/lib/pubid/itu/components/sector.rb +2 -0
  189. data/lib/pubid/itu/components/series.rb +2 -0
  190. data/lib/pubid/itu/identifiers/base.rb +10 -0
  191. data/lib/pubid/itu/identifiers/contribution.rb +31 -0
  192. data/lib/pubid/itu/identifiers/supplement.rb +15 -0
  193. data/lib/pubid/itu/identifiers.rb +1 -0
  194. data/lib/pubid/itu/parser.rb +19 -2
  195. data/lib/pubid/jcgm/CLAUDE.md +7 -0
  196. data/lib/pubid/jcgm/builder.rb +2 -0
  197. data/lib/pubid/jcgm/components/publisher.rb +2 -0
  198. data/lib/pubid/jis/builder.rb +5 -1
  199. data/lib/pubid/jis/identifier.rb +5 -17
  200. data/lib/pubid/jis/identifiers/all_parts.rb +19 -0
  201. data/lib/pubid/jis/identifiers.rb +1 -0
  202. data/lib/pubid/jis/renderer.rb +0 -2
  203. data/lib/pubid/jis/urn_generator.rb +0 -1
  204. data/lib/pubid/nist/CLAUDE.md +56 -0
  205. data/lib/pubid/nist/builder.rb +14 -12
  206. data/lib/pubid/nist/components/edition.rb +2 -0
  207. data/lib/pubid/nist/components/issue_number.rb +2 -0
  208. data/lib/pubid/nist/components/part.rb +2 -0
  209. data/lib/pubid/nist/components/stage.rb +2 -0
  210. data/lib/pubid/nist/components/supplement.rb +2 -0
  211. data/lib/pubid/nist/components/translation.rb +2 -0
  212. data/lib/pubid/nist/components/update.rb +2 -0
  213. data/lib/pubid/nist/components/version.rb +2 -0
  214. data/lib/pubid/nist/components/volume.rb +2 -0
  215. data/lib/pubid/nist/identifiers/base.rb +34 -6
  216. data/lib/pubid/nist/identifiers/commercial_standard_emergency.rb +1 -1
  217. data/lib/pubid/nist/identifiers/crpl_report.rb +5 -6
  218. data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +2 -2
  219. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +2 -2
  220. data/lib/pubid/nist/parser.rb +15 -2
  221. data/lib/pubid/nist/preprocessor.rb +53 -2
  222. data/lib/pubid/nist/series/ir.rb +3 -7
  223. data/lib/pubid/oasis/CLAUDE.md +19 -0
  224. data/lib/pubid/oasis/builder.rb +2 -0
  225. data/lib/pubid/oasis/identifier.rb +19 -0
  226. data/lib/pubid/ogc/CLAUDE.md +34 -0
  227. data/lib/pubid/ogc/builder.rb +2 -0
  228. data/lib/pubid/ogc/identifier.rb +11 -0
  229. data/lib/pubid/oiml/CLAUDE.md +189 -0
  230. data/lib/pubid/oiml/builder.rb +22 -2
  231. data/lib/pubid/oiml/components/code.rb +6 -0
  232. data/lib/pubid/oiml/identifier.rb +13 -0
  233. data/lib/pubid/oiml/identifiers/annex.rb +4 -0
  234. data/lib/pubid/oiml/identifiers/bulletin.rb +30 -36
  235. data/lib/pubid/oiml/identifiers/certification_system.rb +34 -0
  236. data/lib/pubid/oiml/identifiers/code_number.rb +10 -2
  237. data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
  238. data/lib/pubid/oiml/identifiers.rb +2 -0
  239. data/lib/pubid/oiml/parser.rb +41 -5
  240. data/lib/pubid/oiml/renderer.rb +26 -4
  241. data/lib/pubid/oiml/single_identifier.rb +5 -1
  242. data/lib/pubid/oiml/supplement_identifier.rb +7 -0
  243. data/lib/pubid/oiml/urn_generator.rb +32 -4
  244. data/lib/pubid/oiml.rb +5 -0
  245. data/lib/pubid/omg/CLAUDE.md +15 -0
  246. data/lib/pubid/omg/builder.rb +2 -0
  247. data/lib/pubid/parser/grammar.rb +23 -0
  248. data/lib/pubid/plateau/builder.rb +2 -0
  249. data/lib/pubid/plateau/identifiers/base.rb +4 -0
  250. data/lib/pubid/plateau/supplement_identifier.rb +14 -2
  251. data/lib/pubid/plateau/urn_generator.rb +7 -1
  252. data/lib/pubid/renderers/directives_renderer.rb +3 -1
  253. data/lib/pubid/renderers/human_readable.rb +0 -1
  254. data/lib/pubid/sae/builder.rb +3 -1
  255. data/lib/pubid/sae/components/date.rb +2 -0
  256. data/lib/pubid/sae/components/type.rb +2 -0
  257. data/lib/pubid/sae/identifiers/base.rb +4 -1
  258. data/lib/pubid/sae/urn_generator.rb +1 -1
  259. data/lib/pubid/subset_match.rb +197 -0
  260. data/lib/pubid/tgpp/CLAUDE.md +43 -0
  261. data/lib/pubid/tgpp/builder.rb +2 -0
  262. data/lib/pubid/tgpp/identifier.rb +14 -0
  263. data/lib/pubid/type_resolver.rb +14 -2
  264. data/lib/pubid/un/builder.rb +2 -0
  265. data/lib/pubid/version.rb +1 -1
  266. data/lib/pubid/w3c/CLAUDE.md +7 -0
  267. data/lib/pubid/w3c/builder.rb +2 -0
  268. data/lib/pubid/xsf/CLAUDE.md +11 -0
  269. data/lib/pubid/xsf/builder.rb +2 -0
  270. data/lib/pubid.rb +16 -3
  271. data/lib/tasks/docs.rake +13 -11
  272. metadata +46 -4
  273. data/lib/pubid/ccsds/identifiers/base_BASE_88929.rb +0 -70
  274. data/lib/pubid/iso/components/code.rb +0 -24
@@ -0,0 +1,140 @@
1
+ # GB (Chinese Standard) flavor notes
2
+
3
+ `Pubid::Gb` covers national (`GB`), confidential national (`GBn`),
4
+ sector/industry (`JB`, `HB`, `NY`, …) and social-group (`T/{ORG}`) Chinese
5
+ standards. The mandate category follows a slash: `T` (recommended), `Z`
6
+ (guideline), or nothing (mandatory).
7
+
8
+ ## A flavor `key_value` block REPLACES the base maps — list every attribute
9
+
10
+ `Pubid::Gb::Identifier` declared its own `key_value` block and mapped
11
+ `publisher_code`, `mandate`, `number`, `part` and `all_parts` — but not
12
+ `date`. lutaml does not merge such a block with the maps of
13
+ `::Pubid::Identifier`; it replaces them. So `to_hash` silently dropped the
14
+ publication year of **every** dated GB identifier:
15
+
16
+ ```ruby
17
+ id = Pubid::Gb::Identifier.parse("GB/T 20223-2006")
18
+ id.to_s # => "GB/T 20223-2006"
19
+ id.year # => "2006"
20
+ id.to_hash # no year at all
21
+ Pubid::Gb::Identifier.from_hash(id.to_hash).to_s # => "GB/T 20223"
22
+ ```
23
+
24
+ The year survived `to_s`, `exclude` and `matches?`, so nothing in the flavor's
25
+ own behaviour looked wrong. Only `from_hash(to_hash) == parse` saw it — the
26
+ silent failure mode this repository records elsewhere: `#matches?` is
27
+ `exclude(*ignore) == other.exclude(*ignore)`, so a parsed reference never
28
+ matched a `from_hash`-ed index row, and the lookup returned nothing with no
29
+ error. Two editions of one document (`GB/T 20223-2006` and `GB/T 20223-2014`)
30
+ also collapsed onto one hash.
31
+
32
+ The fix is one line — `map "date", to: :date` — because the shared flat-scalar
33
+ rules do the rest: `flatten_scalar_components` (in `Identifier#to_hash`) writes
34
+ a bare `"year" => "2006"` instead of a nested `date` component, and
35
+ `inflate_scalar_components` reads it back. GB declares no `year` attribute, so
36
+ the `date` → `year` rename applies.
37
+
38
+ ```ruby
39
+ Pubid::Gb::Identifier.parse("GB/T 20223-2006").to_hash
40
+ # => {"_type" => "pubid:gb:standard", "publisher_code" => "GB",
41
+ # "mandate" => "T", "number" => "20223", "year" => "2006"}
42
+ ```
43
+
44
+ **Lesson for any flavor with its own `key_value` block**: an attribute absent
45
+ from the block does not serialize. Assert `from_hash(id.to_hash) == id`, not
46
+ only `to_s` and `to_hash` — the two shapes that were already correct here.
47
+
48
+ GB publishes no `relaton-data-gb` index, so no re-crawl follows.
49
+
50
+ ## `GBn` — a lowercase letter inside the publisher token
51
+
52
+ `GBn` is the confidential national series, and the only one of the 67 prefixes
53
+ the relaton GB flavor carries that did not parse. The publisher rule accepted
54
+ `[A-Z]{1,3}`, which consumed `GB` and then stopped at the `n`.
55
+
56
+ A PEG takes the first branch that matches and does not backtrack into the
57
+ alternatives, so the `str("GBn")` branch comes **before** the uppercase-only
58
+ branch in `rule(:publisher_code)`. `GBn`, `GBn/T` and `GBn/Z` now parse, and
59
+ `GBn GBn/T GBn/Z` are in `PREFIXES`, so `Pubid.parse` routes them.
60
+
61
+ All 67 relaton prefixes now round-trip a dated identifier.
62
+
63
+ ## Decision: the em dash normalizes to an ASCII hyphen
64
+
65
+ Chinese portals print an em dash before the year (`T/ZS 0467—2023`). The parser
66
+ accepts both spellings and the renderer prints an ASCII hyphen:
67
+
68
+ ```ruby
69
+ Pubid::Gb::Identifier.parse("T/ZS 0467—2023").to_s # => "T/ZS 0467-2023"
70
+ ```
71
+
72
+ This is deliberate, not a defect. The two spellings give **equal** identifiers,
73
+ so a reference in either form matches the document. The alternative — a stored
74
+ separator attribute, the CIE `date_separator` shape — would make the two
75
+ spellings unequal and would need an `exclude` override. The relaton fixture
76
+ `spec/gb/fixtures/tgzaepi_001_2018.xml` records the em-dash form and is the one
77
+ place that must follow this decision.
78
+
79
+ ## The series code lives in the inherited `publisher`
80
+
81
+ `GB`, `JB`, `GBn` and `T/GZAEPI` are stored in the `publisher` attribute
82
+ inherited from `::Pubid::Identifier`, a `Components::Publisher`. The flavor had
83
+ its own `publisher_code` string beside it while the inherited attribute stayed
84
+ nil, so two attributes described one value and the shared code that reads
85
+ `publisher` saw nothing.
86
+
87
+ The component serializes as a bare scalar, because the class adds itself to the
88
+ flat-scalar table — the CEN/CENELEC precedent, and for GB's own classes only:
89
+
90
+ ```ruby
91
+ Pubid::Gb::Identifier.flat_scalar_components # => {..., publisher: "publisher"}
92
+ Pubid::Gb::Identifier.flat_scalar_fields # => {..., publisher: :body}
93
+ ```
94
+
95
+ ```ruby
96
+ {"_type" => "pubid:gb:standard", "publisher" => "GB",
97
+ "mandate" => "T", "number" => "20223", "year" => "2006"}
98
+ ```
99
+
100
+ **The URN gained the series, which repairs a collision.** `GB 20223-2006` and
101
+ `GBn 20223-2006` are two documents and shared `urn:gb:20223:2006`; they now
102
+ give `urn:gb:gb:20223:2006` and `urn:gb:gbn:20223:2006`. The shared URN
103
+ generator lowercases the body on its own. The MR slug follows (`gb.20223.2006`,
104
+ `gbn.20223.2006`) and sanitizes the slash of a social-group code by itself
105
+ (`T/GZAEPI` → `t-gzaepi.001.2018`), so GB does not join the IEEE slash ledger.
106
+ Annotated rendering gained a `publisher` span for free.
107
+
108
+ **Note the parse-tree key keeps its name.** `rule(:publisher_code)` in the
109
+ parser still captures `:publisher_code`; parse-tree keys and attribute names
110
+ are different namespaces (the ASHRAE landmine). Only the attribute moved.
111
+
112
+ ## Known gaps
113
+
114
+ `spec/pubid/gb/fixtures_spec.rb` now reads `spec/fixtures/gb/`, which nothing
115
+ read before (the `ten-dead-fixture-specs` class). It carries a tripwire example
116
+ asserting both globs are non-empty, because a wrong glob reports 0 examples
117
+ instead of a failure. GB has no `identifiers/full/identifiers.txt`, so the
118
+ fixtures are hand-written and `rake validation:classify[gb]` does not drive
119
+ them; the reader accepts both the plain and the generated line shapes.
120
+
121
+ The URN carries the series, the number and the year, but **not the mandate**:
122
+ `GB 20223-2006` and `GB/T 20223-2006` both give `urn:gb:gb:20223:2006`, and
123
+ their MR slugs are equal too. The mandatory and the recommended standard are
124
+ two documents, so this is a real collision, narrower than the one the series
125
+ repaired. There is also no `Pubid::Gb::UrnParser`, so a GB URN cannot be read
126
+ back. Nothing consumes a GB URN today, so both are recorded, not fixed.
127
+
128
+ ## Not supported (deliberately)
129
+
130
+ These forms do not parse. Each is outside what the relaton GB flavor supports
131
+ today:
132
+
133
+ | form | what it is |
134
+ |---|---|
135
+ | `DB11/T 123-2020`, `DB37 1234-2020` | local standards; a province code follows `DB` |
136
+ | `Q/SY 123-2020` | enterprise standard |
137
+ | `GB/T 1.1-2020/XG1-2021` | amendment sheet (修改单) |
138
+
139
+ `PREFIXES` lists bare `DB`, `DB/T` and `DB/Z`, so the province-numbered form
140
+ was intended but is not parsed.
@@ -6,31 +6,39 @@ module Pubid
6
6
  #
7
7
  # The parser captures the publisher code verbatim (which may already
8
8
  # include the "/T" or "/Z" suffix). The builder normalizes: if the
9
- # suffix is in the publisher_code string, it's split out into the
10
- # separate +mandate+ attribute so the renderer can recompose either
11
- # the inline or split form.
9
+ # suffix is in the code, it is split out into the separate +mandate+
10
+ # attribute so the renderer can recompose either the inline or split
11
+ # form. The code itself goes into the inherited +publisher+ component.
12
12
  class Builder
13
13
  def self.build(parsed_data)
14
14
  new.build(parsed_data)
15
15
  end
16
16
 
17
17
  def build(data)
18
- publisher_code, mandate = split_mandate(data[:publisher_code].to_s)
18
+ code, mandate = split_mandate(data[:publisher_code].to_s)
19
19
 
20
- mandate ||= data[:mandate]&.to_s
21
-
22
- Identifiers::Standard.new(
23
- publisher_code: publisher_code,
24
- mandate: mandate,
20
+ identifier = Identifiers::Standard.new(
21
+ publisher: ::Pubid::Components::Publisher.new(body: code),
22
+ mandate: mandate || data[:mandate]&.to_s,
25
23
  number: data[:number].to_s,
26
24
  part: data[:part]&.to_s,
27
- date: data[:year] ? ::Pubid::Components::Date.new(year: data[:year].to_s) : nil,
28
- all_parts: !data[:all_parts].to_s.empty?,
25
+ date: date_for(data[:year]),
29
26
  )
27
+
28
+ # "(all parts)" names every part of the document, so it wraps the
29
+ # document, which holds no mark itself.
30
+ data[:all_parts].to_s.empty? ? identifier : identifier.to_all_parts
30
31
  end
31
32
 
32
33
  private
33
34
 
35
+ # The publication year, or nil for a partial reference.
36
+ def date_for(year)
37
+ return nil unless year
38
+
39
+ ::Pubid::Components::Date.new(year: year.to_s)
40
+ end
41
+
34
42
  # If the publisher code carries an inline /T or /Z suffix, split it off
35
43
  # and return the cleaned code + extracted mandate.
36
44
  def split_mandate(code)
@@ -43,3 +51,5 @@ module Pubid
43
51
  end
44
52
  end
45
53
  end
54
+
55
+ Pubid::Gb::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -7,9 +7,16 @@ module Pubid
7
7
  # descend from this class, so a parsed GB id is always an instance of
8
8
  # Pubid::Gb::Identifier.
9
9
  class Identifier < ::Pubid::Identifier
10
- # The issuing body code as printed, e.g. "GB", "JB", "T/GZAEPI".
11
- # Always non-empty for a valid identifier.
12
- attribute :publisher_code, :string
10
+ # The all-parts identifier of this flavor.
11
+ def self.all_parts_class
12
+ Identifiers::AllParts
13
+ end
14
+
15
+ # The issuing body code as printed — "GB", "JB", "GBn", "T/GZAEPI" —
16
+ # lives in the `publisher` attribute inherited from ::Pubid::Identifier.
17
+ # The flat-scalar hooks below serialize it as a bare string, and the
18
+ # shared URN generator renders it lowercase, so the series is part of
19
+ # the URN identity (GB 20223 and GBn 20223 are two documents).
13
20
 
14
21
  # Mandate category: "T" (recommended), "Z" (guideline), or nil
15
22
  # (mandatory). Carried after the "/" in the printed form.
@@ -23,25 +30,39 @@ module Pubid
23
30
  # uses the dotted form (e.g. "5606.1" => number "5606", part "1").
24
31
  attribute :part, :string
25
32
 
26
- # All-parts flag — true for "GB/T 5606 (all parts)" forms.
27
- attribute :all_parts, :boolean, default: -> { false }
28
33
 
29
34
  # Polymorphic type map for lutaml key_value (de)serialization.
30
35
  GB_TYPE_MAP = {
31
36
  "pubid:gb:standard" => "Pubid::Gb::Identifiers::Standard",
32
37
  }.freeze
33
38
 
39
+ # This block REPLACES the maps of ::Pubid::Identifier, so every
40
+ # attribute the flavor uses must appear here — "date" included, or the
41
+ # publication year is lost in to_hash. The shared flat-scalar rules
42
+ # (Identifier#to_hash / .from_hash) write it as a bare "year" scalar.
34
43
  key_value do
35
44
  map "_type", to: :_type, polymorphic_map: GB_TYPE_MAP
36
- map "publisher_code", to: :publisher_code
45
+ map "publisher", to: :publisher
37
46
  map "mandate", to: :mandate
38
47
  map "number", to: :number
39
48
  map "part", to: :part
40
- map "all_parts", to: :all_parts, render_default: false
49
+ map "date", to: :date
41
50
  end
42
51
 
43
52
  PUBLISHER = "CN"
44
53
 
54
+ # Serialize `publisher` as a bare string instead of a nested
55
+ # {"body" => "GB"}, the CEN/CENELEC precedent. The entry is added for
56
+ # GB's own classes only: a shared entry would change the wire format of
57
+ # every flavor that publishes an index.
58
+ def self.flat_scalar_components
59
+ super.merge(publisher: "publisher")
60
+ end
61
+
62
+ def self.flat_scalar_fields
63
+ super.merge(publisher: :body)
64
+ end
65
+
45
66
  def to_s(**opts)
46
67
  render(format: :human, **opts)
47
68
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Gb
5
+ module Identifiers
6
+ # Every part of one GB document: "GB/T 5606 (all parts)".
7
+ class AllParts < ::Pubid::Gb::Identifier
8
+ include ::Pubid::AllParts
9
+
10
+ # The document URN; GB and IDF mark no series in the URN.
11
+ def to_urn
12
+ identity.to_urn
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -3,6 +3,7 @@
3
3
  module Pubid
4
4
  module Gb
5
5
  module Identifiers
6
+ autoload :AllParts, "#{__dir__}/identifiers/all_parts"
6
7
  autoload :Standard, "#{__dir__}/identifiers/standard"
7
8
  end
8
9
  end
@@ -25,6 +25,10 @@ module Pubid
25
25
  rule(:publisher_code) do
26
26
  # Social-group form: "T/" then 2+ uppercase letters/digits.
27
27
  (str("T/") >> match("[A-Z0-9]").repeat(1)).as(:publisher_code) |
28
+ # Confidential national series. The lowercase "n" is part of the
29
+ # code, so it comes before the uppercase-only rule below: a PEG
30
+ # takes the first branch that matches and does not backtrack.
31
+ (str("GBn") >> (str("/T") | str("/Z")).maybe).as(:publisher_code) |
28
32
  # Standard form: 1-3 letters, optional slash-T-or-Z.
29
33
  (match("[A-Z]").repeat(1, 3) >>
30
34
  (str("/T") | str("/Z")).maybe).as(:publisher_code)
@@ -8,19 +8,19 @@ module Pubid
8
8
  # "GB/T 20223-2006"
9
9
  # "GB/T 5606.1-2004"
10
10
  # "GB/T 5606 (all parts)"
11
- # "T/GZAEPI 001—2018" (social-group form, em-dash year)
11
+ # "T/GZAEPI 001-2018" (social-group form; an em-dash year
12
+ # separator in the input renders as a hyphen)
12
13
  class Renderer < ::Pubid::Renderers::Base
13
14
  def render(**_opts)
14
15
  parts = [publisher_portion, " #{number_portion}"]
15
- parts << " (all parts)" if @id.all_parts
16
16
  parts.join
17
17
  end
18
18
 
19
19
  private
20
20
 
21
21
  def publisher_portion
22
- code = @id.publisher_code.to_s
23
- code += "/#{@id.mandate}" if @id.mandate && !@id.publisher_code.include?("/")
22
+ code = @id.publisher.to_s
23
+ code += "/#{@id.mandate}" if @id.mandate && !code.include?("/")
24
24
  code
25
25
  end
26
26
 
data/lib/pubid/gb.rb CHANGED
@@ -17,6 +17,7 @@ module Pubid
17
17
  # "GB" comes first so PrefixesSupport's longest-match wins.
18
18
  PREFIXES = %w[
19
19
  GB GB/T GB/Z
20
+ GBn GBn/T GBn/Z
20
21
  DB DB/T DB/Z
21
22
  QB QB/T
22
23
  ZB ZB/T
@@ -0,0 +1,64 @@
1
+ # GOST flavor notes
2
+
3
+ Foreign-adoption routing: the slash-attached prefix boundary that let another
4
+ flavor win the routing race.
5
+
6
+ These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/gost/` or `spec/pubid/gost/`. The root file keeps the cross-flavor contract that every flavor obeys.
7
+
8
+ ## Foreign-adoption routing must use a token-boundary prefix check, not a literal-space check
9
+
10
+ `Gost::Builder#parse_foreign` (`lib/pubid/gost/builder.rb`) resolves the
11
+ foreign standard a GOST document adopts (`"GOST 58904-2020/ISO/TR
12
+ 25901-1:2016"` → the adopted `"ISO/TR 25901-1:2016"`) by routing through
13
+ `Pubid.prefix_flavors` first — the same "registered prefix, not load order"
14
+ mechanism the earlier `gost-adoption-prefix-routing` fix introduced for
15
+ `"GOST 1437-2024 (ASTM D129-18)"` — and falling back to an exhaustive,
16
+ alphabetically-sorted try-every-flavor loop only when no registered prefix
17
+ matches.
18
+
19
+ `prefix_owner`'s boundary check used to require a **literal space** after the
20
+ matched prefix (`raw.start_with?("#{p} ")`). That misses a form where the
21
+ type token is attached to the publisher with a **slash** instead of a space —
22
+ `"ISO/TR 25901-1:2016"`, `"ISO/TS 10303-1:2014"` — because the character
23
+ after `"ISO"` is `/`, not a space. Those strings therefore matched no
24
+ registered owner and fell through to the exhaustive fallback loop, racing
25
+ every registered flavor's grammar alphabetically.
26
+
27
+ **The race was real, not theoretical: `Pubid::Iec`'s own grammar also accepts
28
+ a bare `"ISO/TR 25901-1:2016"`** (as its own identifier, not as a delegation
29
+ to ISO), and renders it `"ISO TR 25901-1:2016"` — a space, IEC's own
30
+ convention, not ISO's. `Pubid::Bsi` also accepts the string and correctly
31
+ delegates to `Pubid::Iso::Identifiers::TechnicalReport`, and normally wins the
32
+ race because `"bsi"` sorts alphabetically before `"iec"` — which is exactly
33
+ why the wrong render (`GOST R 58904-2020/ISO TR 25901-1:2016` instead of
34
+ `.../ISO/TR 25901-1:2016`) was rare and reproduced only intermittently, and
35
+ only under full-suite load (hand-off
36
+ `metanorma__pubid__gost-idt-order-dependent-render.md`; the hand-off chased
37
+ GC/allocation timing and `RenderingContext` memoization as candidate causes —
38
+ neither was it, and neither needed to be pinned down, since routing this
39
+ deterministically by prefix removes the race regardless of what let `bsi`
40
+ occasionally lose it).
41
+
42
+ **Fix**: `prefix_owner` now uses the same token-boundary rule the top-level
43
+ router already uses (`Pubid.prefix_match?` in `lib/pubid.rb` — a non-word
44
+ character, not specifically a space, ends the prefix). That method used to be
45
+ `@api private`; it is now public for exactly this reason — a second,
46
+ hand-copied boundary check in `Gost::Builder` would be the same
47
+ duplicated-logic-drifts-out-of-sync hazard this file documents elsewhere for
48
+ other flavors (found in code review, not the first pass: the initial fix
49
+ duplicated the regex locally instead of calling the shared method). **Do
50
+ not** re-duplicate this boundary check in a flavor's own builder; call
51
+ `::Pubid.prefix_match?(string, prefix)`.
52
+
53
+ **Not affected**: a jointly-owned prefix (`"ISO/IEC"`, `"ISO/IEC TR"`) already
54
+ falls through to the exhaustive fallback by design (`owners.one?` is false),
55
+ and every owner renders it identically — verified for both the bare and the
56
+ type-attached compound forms. Only the single-owner, slash-attached-type
57
+ shape (`"ISO/TR"`, `"ISO/TS"`, and by the same rule `"IEC/TR"` etc.) was
58
+ broken.
59
+
60
+ Locked by `spec/pubid/gost/foreign_adoption_routing_spec.rb`, which includes
61
+ a deterministic reproduction of the race (not a reliance on catching the rare
62
+ full-suite failure): it temporarily `Registry.unregister(:bsi)`s the usual
63
+ race winner and asserts routing still resolves to `Pubid::Iso` correctly —
64
+ proving the fix is prefix routing, not a lucky alphabetical ordering.
@@ -166,7 +166,7 @@ module Pubid
166
166
  def prefix_owner(raw)
167
167
  index = ::Pubid.prefix_flavors
168
168
  match = index.keys
169
- .select { |p| raw.start_with?("#{p} ") || raw == p }
169
+ .select { |p| ::Pubid.prefix_match?(raw, p) }
170
170
  .max_by(&:length)
171
171
  return nil unless match
172
172
 
@@ -183,3 +183,5 @@ module Pubid
183
183
  end
184
184
  end
185
185
  end
186
+
187
+ Pubid::Gost::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -23,6 +23,11 @@ module Pubid
23
23
  attribute :number, :string
24
24
  attribute :year, :string
25
25
 
26
+ # A nil `copublisher` means GOST alone: `GOST R 27001` is not
27
+ # `GOST R ISO/IEC 27001`, which is an adoption of another body's
28
+ # document.
29
+ subset_strict :copublisher
30
+
26
31
  GOST_TYPE_MAP = {
27
32
  "pubid:gost:interstate-standard" => "Pubid::Gost::Identifiers::InterstateStandard",
28
33
  "pubid:gost:national-standard" => "Pubid::Gost::Identifiers::NationalStandard",
@@ -0,0 +1,82 @@
1
+ # IALA flavor notes
2
+
3
+ IALA (International Organization for Marine Aids to Navigation)
4
+ publication identifiers and their MRN URNs.
5
+
6
+ Read this before you change `lib/pubid/iala/` or `spec/pubid/iala/`. The
7
+ root `CLAUDE.md` keeps the cross-flavor contract that every flavor
8
+ obeys.
9
+
10
+ ## Identifier space
11
+
12
+ Every IALA publication carries a coded reference: a series prefix
13
+ followed by a number whose shape varies per series.
14
+
15
+ | Series | Type | Numbering | Example |
16
+ |--------|------|-----------|---------|
17
+ | S | Standard | 4 digits, zero-padded | `IALA S1070 Ed 2.0` |
18
+ | R | Recommendation | 4 digits, zero-padded | `IALA R0126 Ed 2.0` |
19
+ | G | Guideline | 4 digits, zero-padded | `IALA G1015 Ed 2.2` |
20
+ | C | Model Course | 4 digits + `-` part | `IALA C0103-1 Ed 3.0` |
21
+ | M | Manual (planned) | 4 digits, zero-padded | `IALA M0001 Ed 9.0` |
22
+ | A | Advice | dashed, no edition in the corpus | `IALA A12-01` |
23
+ | GA | General Assembly resolution | dotted, each segment 2 digits | `IALA GA01.01` |
24
+ | L | Letter | dotted + dashed, verbatim | `IALA L2.1.11 Ed 2` |
25
+ | X | Report (reserved) | 4 digits | `IALA X0123` |
26
+ | P | Resolution/Council publication (reserved) | 4 digits | `IALA P0123` |
27
+ | — | Annex wrapper over any base | bare or lettered | `IALA G1128 ANNEX A Ed 1.6` |
28
+
29
+ The Annex class has no prefix of its own: it wraps a base identifier of
30
+ another series and preserves the annex marker case (`Annex` vs `ANNEX`).
31
+ X and P are reserved for future numbered series — today's reports and
32
+ Council publications carry no code; unnumbered report/symposium
33
+ documents exist in relaton-data-iala only as slug-form bibliographic
34
+ records, deliberately outside this identifier space.
35
+
36
+ ## MRN URN scheme
37
+
38
+ IALA publications are named in the `mrn` URN tree as assigning
39
+ authority `iala`, sub-namespace `pub`:
40
+
41
+ ```
42
+ urn:mrn:iala:pub:<code>[:annex[-<letter>]][:ed<x.x>][:<lang>]
43
+ ```
44
+
45
+ - `<code>` — the lowercased series prefix + number (`s1070`,
46
+ `c0103-1`, `ga01.01`, `l2.1.11`, `a12-01`, and the reserved `x0123`,
47
+ `p0123`)
48
+ - `annex` / `annex-<letter>` — the Annex wrapper (lettered form for
49
+ `ANNEX A`-style, bare for `Annex` without a letter)
50
+ - `ed<x.x>` — edition, lowercase marker, no separator dot
51
+ - `<lang>` — single lowercase language letter
52
+
53
+ The authoritative pattern for published documents is the IALA Style
54
+ Guide (G1115, Ed 1.0 January 2021) document-reference clause:
55
+ `urn:mrn:iala:pub:[p][nnnn]` with p ∈ {S, R, G, C} — printed on the
56
+ cover and footers. URNs for the other series (M, A, GA, L, X, P) are a
57
+ pattern-consistent extension: no such URN has been observed in a
58
+ published document yet, but every series round-trips through the same
59
+ generator/parser pair.
60
+
61
+ ## Parsing and normalization
62
+
63
+ - The umbrella `Pubid.parse` routes URN-form input by assigning
64
+ authority (`urn:mrn:iala:…` → the IALA flavor), not by the literal
65
+ `mrn` namespace; unknown MRN authorities fail loudly.
66
+ - `Pubid::Iala.parse` dispatches URN-form input to `UrnParser` via
67
+ `FormatDetector`, so both entry points accept both forms.
68
+ - The edition marker matches case-insensitively with an optional
69
+ separator dot. Both malformed variants printed in the published R1026
70
+ PDF (`…r1026:Ed1.0`, `…r1026:ed.1.0`) parse and normalize to the
71
+ canonical `ed1.0`.
72
+ - Short inputs are zero-padded to the canonical form (`M1` → `M0001`,
73
+ `GA1.1` → `GA01.01`); L-series numbering is preserved verbatim.
74
+
75
+ ## Conformance
76
+
77
+ Ground truth lives in the shared suite
78
+ (pubid/pubid-testsuite `reference-docs/iala`): 701 coded identifiers
79
+ from relaton-data-iala primary docidentifiers plus the 15 MRN URNs
80
+ transcribed from the published PDFs in metanorma/mn-samples-iala
81
+ `reference-docs/` (including the two malformed R1026 spellings, kept
82
+ byte-identical as normalization aliases).
@@ -100,3 +100,5 @@ module Pubid
100
100
  end
101
101
  end
102
102
  end
103
+
104
+ Pubid::Iala::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -0,0 +1,7 @@
1
+ # IANA flavor notes
2
+
3
+ IANA registry slugs as the index key.
4
+
5
+ These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/iana/` or `spec/pubid/iana/`. The root file keeps the cross-flavor contract that every flavor obeys.
6
+
7
+ - **IANA registries have no number — the top-level slug IS the index key**: an IANA identifier is a hierarchical registry slug, `IANA <registry>[/<sub-registry>]`, capped at one slash. Verified against all 3405 published `relaton-data-iana` rows: two shapes only (655 top registries, 2750 with one sub-registry, **0** with ≥2 slashes), charset exactly `[A-Za-z0-9._-]` + `/` (matching `Parser#slug` verbatim), and **1425 ids contain digits but ZERO have a separable numeric component** — digits are always glued inside a slug token (`sip-parameters/sip-parameters-13`, `idna-tables-11.0.0`, `smi-numbers/smi-numbers-1.3.6.1.2.1.198.4`, `isis-tlv-codepoints/tlv-149-150`, `_6tisch`). There is no ISO-style `<base>-<part>` to extract. So, exactly as for an Internet-Draft slug, **the slug itself is the number**: `attribute :number, :string` holds the **top-level** registry slug and `sub_registry` stays its own key, so a registry and all of its sub-registries **cluster into one bucket** (655 buckets, median 3, max 73 — `pcep`) — the analogue of a draft clustering with its versions. Before this, `root.number.to_s` was `""` for **every** IANA id, which silently defeats relaton-index's bsearch narrowing (`Type#get_id_number` reads `id.number.to_s`), and `to_mr_string` was `""` for every id, so all 3405 collided on the MR slug — and `to_slug` is used as a **filename**. **The attribute is declared on the concrete leaf `Identifiers::Registry`, NEVER on `Pubid::Iana::Identifier`** (which the leaf inherits from): it redefines the parent `::Pubid::Identifier`'s `attribute :number, Components::Code`, and on an inherited-from class that resolves nondeterministically under multi-flavor load — the IEEE/IETF landmine, so `spec/pubid/iana/root_number_spec.rb` only means anything under the **full `rake` suite**. No `#number` *method* is defined (it would collide with lutaml's generated accessor). **`registry` is no longer a serialized key** — it was the same string as `number`, so it is now a plain derived reader `def registry; number; end` on the base (safe: no lutaml accessor competes with a name that is not an attribute; the `Pubid::Ietf::Identifiers::Bcp#series` precedent), and the leaf's `key_value` block (`number`) is **merged** by lutaml with the base's (`_type` + `sub_registry`). Serialized vocabulary is thus `{_type, number}` / `{_type, number, sub_registry}`. **There is no alias**, so a pre-`number` row deserializes with a nil `number` and *no error at all* (lutaml ignores unknown keys, and relaton's `FileIO#id_supported?` skips its round-trip check for concrete subclasses — every IANA id is an `Identifiers::Registry`). Rendering is therefore the loud failure: `Renderer#require_number!` raises `ArgumentError` rather than emitting a bare `"IANA "`. Nothing published needs migrating today — `relaton-iana` has no pubid dependency and still uses the string-keyed `index-v1`. **MR**: the flavor supplies `mr_publisher` (`"iana"` — IANA keeps its publisher as the `PUBLISHER` constant, not a lutaml attribute, so the base's `publisher&.to_s` yields nil) and `mr_number_with_part` (`[number, sub_registry].compact.join(".").downcase`); verified over the corpus to give 3405 **distinct** slugs inside the documented `[a-z0-9._-]` charset. Caveat: IANA slugs legitimately contain `_`, which is MR's supplement separator, so an IANA MR is **not** re-parseable by `Pubid::Parsers::MrString` — uniqueness and filename-safety are what it buys. **URN is unchanged** (`urn:iana:<registry>[:<sub_registry>]`; the generator just reads `number` instead). Locked by `spec/pubid/iana/{root_number,mr_string}_spec.rb`, the opt-in `corpus_round_trip_spec.rb` (`PUBID_IANA_CORPUS=<dir with relaton-data-iana> bundle exec rake test:corpus_iana`, ~3s, all 3405 rows: parse + byte-exact `to_s` + `from_hash(to_hash)` fidelity + non-empty `root.number` + MR uniqueness), and IANA rows added to `uniform_identifier_handle_spec`/`redos_guard_spec`, which it was missing from. **Note**: the cross-flavor "every registered flavor has a non-empty MR" block in `spec/pubid/mr_string_spec.rb` is a hand-picked 10-flavor sample, not registry-driven, which is why the empty-MR gap went unnoticed — making it registry-driven would likely surface the same gap in other slug-only flavors (XSF, OGC, adobe, PLATEAU) and is deliberately left as separate work. (hand-off: relaton-iana pubid-index-v2-readiness.)
@@ -20,3 +20,5 @@ module Pubid
20
20
  end
21
21
  end
22
22
  end
23
+
24
+ Pubid::Iana::Builder.prepend(Pubid::Builder::AllPartsWrap)