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
@@ -6,18 +6,15 @@ module Pubid
6
6
  # Technical Committee Document
7
7
  # Format: TC 184/SC 4/WG 3 N 123, JTC 1 N 456, TC 184 N 100
8
8
  class TcDocument < Identifier
9
- # TC type (TC, JTC, PC, IT, etc.)
10
- attribute :tc_type, ::Pubid::Components::Code
11
- # TC number
12
- attribute :tc_number, ::Pubid::Components::Code
13
- # SC type
14
- attribute :sc_type, ::Pubid::Components::Code
15
- # SC number
16
- attribute :sc_number, ::Pubid::Components::Code
17
- # WG type
18
- attribute :wg_type, ::Pubid::Components::Code
19
- # WG number
20
- attribute :wg_number, ::Pubid::Components::Code
9
+ # The committee structure: plain strings ("TC", "184", "SC", "4").
10
+ # Each held a Components::Code carrying nothing but `value`, and the
11
+ # serialized form was already the bare scalar.
12
+ attribute :tc_type, :string
13
+ attribute :tc_number, :string
14
+ attribute :sc_type, :string
15
+ attribute :sc_number, :string
16
+ attribute :wg_type, :string
17
+ attribute :wg_number, :string
21
18
 
22
19
  # TC types from ISO system
23
20
  TC_TYPES = %w[TC JTC PC IT CAB CASCO COPOLCO COUNCIL CPSG CS DEVCO GA
@@ -33,33 +30,21 @@ module Pubid
33
30
  # TC documents don't use typed stages like other identifiers
34
31
  TYPED_STAGES = [].freeze
35
32
 
36
- # Serialize the committee structure (all plain-string Codes) on top of
37
- # the inherited ISO mapping; TC documents have no stage.
33
+ # Serialize the committee structure on top of the inherited ISO
34
+ # mapping; TC documents have no stage. The attributes are plain
35
+ # strings, so lutaml needs no converter (the ETSI/OIML shape).
38
36
  key_value do
39
- map "tc_type", with: { to: :tc_type_to_kv, from: :tc_type_from_kv }
40
- map "tc_number", with: { to: :tc_number_to_kv, from: :tc_number_from_kv }
41
- map "sc_type", with: { to: :sc_type_to_kv, from: :sc_type_from_kv }
42
- map "sc_number", with: { to: :sc_number_to_kv, from: :sc_number_from_kv }
43
- map "wg_type", with: { to: :wg_type_to_kv, from: :wg_type_from_kv }
44
- map "wg_number", with: { to: :wg_number_to_kv, from: :wg_number_from_kv }
37
+ map "tc_type", to: :tc_type
38
+ map "tc_number", to: :tc_number
39
+ map "sc_type", to: :sc_type
40
+ map "sc_number", to: :sc_number
41
+ map "wg_type", to: :wg_type
42
+ map "wg_number", to: :wg_number
45
43
  end
46
44
 
47
45
  # TC documents have no stage; suppress the inherited stage emission.
48
46
  def stage_to_kv(_model, _doc); end
49
47
 
50
- def tc_type_to_kv(m, doc) = emit_code(doc, "tc_type", m.tc_type)
51
- def tc_type_from_kv(m, v) = m.tc_type = build_code(v)
52
- def tc_number_to_kv(m, doc) = emit_code(doc, "tc_number", m.tc_number)
53
- def tc_number_from_kv(m, v) = m.tc_number = build_code(v)
54
- def sc_type_to_kv(m, doc) = emit_code(doc, "sc_type", m.sc_type)
55
- def sc_type_from_kv(m, v) = m.sc_type = build_code(v)
56
- def sc_number_to_kv(m, doc) = emit_code(doc, "sc_number", m.sc_number)
57
- def sc_number_from_kv(m, v) = m.sc_number = build_code(v)
58
- def wg_type_to_kv(m, doc) = emit_code(doc, "wg_type", m.wg_type)
59
- def wg_type_from_kv(m, v) = m.wg_type = build_code(v)
60
- def wg_number_to_kv(m, doc) = emit_code(doc, "wg_number", m.wg_number)
61
- def wg_number_from_kv(m, v) = m.wg_number = build_code(v)
62
-
63
48
  def self.type
64
49
  { key: :tc,
65
50
  web: :tc_document, title: "Technical Committee Document", short: "TC" }
@@ -73,23 +58,23 @@ module Pubid
73
58
  result = publisher.to_s if publisher
74
59
 
75
60
  # Add TC type and number
76
- result += "/#{tc_type.render} " if tc_type&.value
77
- result += tc_number.render.to_s if tc_number&.value
61
+ result += "/#{tc_type} " unless tc_type.to_s.empty?
62
+ result += tc_number.to_s unless tc_number.to_s.empty?
78
63
 
79
64
  # Add SC type and number
80
- if sc_type&.value && sc_number&.value
81
- result += "/#{sc_type.render} "
82
- result += sc_number.render.to_s
65
+ unless sc_type.to_s.empty? || sc_number.to_s.empty?
66
+ result += "/#{sc_type} "
67
+ result += sc_number.to_s
83
68
  end
84
69
 
85
70
  # Add WG type and number
86
- if wg_type&.value && wg_number&.value
87
- result += "/#{wg_type.render} "
88
- result += wg_number.render.to_s
71
+ unless wg_type.to_s.empty? || wg_number.to_s.empty?
72
+ result += "/#{wg_type} "
73
+ result += wg_number.to_s
89
74
  end
90
75
 
91
76
  # Add document number
92
- result += " N #{number.render}" if number&.value
77
+ result += " N #{number}" unless number.to_s.empty?
93
78
 
94
79
  # Add year if present
95
80
  result += ":#{date.render}" if date&.year
@@ -105,16 +90,16 @@ module Pubid
105
90
  parts << publisher.render(context: urn_ctx) if publisher
106
91
 
107
92
  # Add TC
108
- parts << "tc:#{tc_number.render(context: urn_ctx)}" if tc_number&.value
93
+ parts << "tc:#{tc_number}" unless tc_number.to_s.empty?
109
94
 
110
95
  # Add SC
111
- parts << "sc-#{sc_number.render(context: urn_ctx)}" if sc_number&.value
96
+ parts << "sc-#{sc_number}" unless sc_number.to_s.empty?
112
97
 
113
98
  # Add WG
114
- parts << "wg-#{wg_number.render(context: urn_ctx)}" if wg_number&.value
99
+ parts << "wg-#{wg_number}" unless wg_number.to_s.empty?
115
100
 
116
101
  # Add document number
117
- parts << number.render(context: urn_ctx) if number&.value
102
+ parts << number.to_s unless number.to_s.empty?
118
103
 
119
104
  parts.join(":")
120
105
  end
@@ -4,6 +4,7 @@ module Pubid
4
4
  module Iso
5
5
  module Identifiers
6
6
  autoload :Addendum, "#{__dir__}/identifiers/addendum"
7
+ autoload :AllParts, "#{__dir__}/identifiers/all_parts"
7
8
  autoload :Amendment, "#{__dir__}/identifiers/amendment"
8
9
  autoload :Corrigendum, "#{__dir__}/identifiers/corrigendum"
9
10
  autoload :Data, "#{__dir__}/identifiers/data"
@@ -73,7 +73,7 @@ module Pubid
73
73
 
74
74
  addendum = Identifiers::Addendum.new
75
75
  addendum.base = base
76
- addendum.number = Iso::Components::Code.new(value: supplement_number)
76
+ addendum.number = supplement_number
77
77
  addendum.date = Pubid::Components::Date.new(year: supplement_year) if supplement_year
78
78
 
79
79
  typed_stage = Pubid::Iso.locate_stage(stage_abbr)
@@ -46,7 +46,6 @@ module Pubid
46
46
  end
47
47
 
48
48
  # All parts notation (if applicable)
49
- result << " (all parts)" if identifier.class.attributes.key?(:all_parts) && identifier.all_parts
50
49
 
51
50
  result
52
51
  end
@@ -63,7 +63,7 @@ module Pubid
63
63
  parts << originator_component
64
64
  type_comp = type_component
65
65
  parts << type_comp if type_comp
66
- parts << identifier.number.render(context: URN_CONTEXT) if identifier.number
66
+ parts << identifier.number.to_s if identifier.number
67
67
  part_comp = part_component
68
68
  parts << part_comp if part_comp
69
69
  stage_comp = stage_component
@@ -95,7 +95,7 @@ module Pubid
95
95
  parts << base_gen.originator_component
96
96
  type_comp = base_gen.type_component
97
97
  parts << type_comp if type_comp
98
- parts << base_id.number.render(context: URN_CONTEXT) if base_id.number
98
+ parts << base_id.number.to_s if base_id.number
99
99
  part_comp = base_gen.part_component
100
100
  parts << part_comp if part_comp
101
101
 
@@ -138,13 +138,13 @@ module Pubid
138
138
  parts << supp.date.render(context: URN_CONTEXT)
139
139
  if supp.number
140
140
  if supp.stage_iteration && !supp.is_a?(Pubid::Iso::Identifiers::Supplement)
141
- parts << "v#{supp.number.render(context: URN_CONTEXT)}.#{supp.stage_iteration.render(context: URN_CONTEXT)}"
141
+ parts << "v#{supp.number}.#{supp.stage_iteration.render(context: URN_CONTEXT)}"
142
142
  else
143
- parts << "v#{supp.number.render(context: URN_CONTEXT)}"
143
+ parts << "v#{supp.number}"
144
144
  end
145
145
  end
146
146
  else
147
- parts << supp.number.render(context: URN_CONTEXT) if supp.number
147
+ parts << supp.number.to_s if supp.number
148
148
  if supp.stage_iteration && !supp.is_a?(Pubid::Iso::Identifiers::Supplement)
149
149
  parts << "v1.#{supp.stage_iteration.render(context: URN_CONTEXT)}"
150
150
  else
@@ -197,8 +197,8 @@ module Pubid
197
197
  def part_component
198
198
  return nil unless identifier.part
199
199
 
200
- result = "-#{identifier.part.render(context: URN_CONTEXT)}"
201
- result += "-#{identifier.subpart.render(context: URN_CONTEXT)}" if identifier.subpart
200
+ result = "-#{identifier.part}"
201
+ result += "-#{identifier.subpart}" if identifier.subpart
202
202
  result
203
203
  end
204
204
 
@@ -292,13 +292,13 @@ module Pubid
292
292
  parts << identifier.date.render(context: URN_CONTEXT)
293
293
  if identifier.number
294
294
  parts << if identifier.stage_iteration
295
- "v#{identifier.number.render(context: URN_CONTEXT)}.#{identifier.stage_iteration.render(context: URN_CONTEXT)}"
295
+ "v#{identifier.number}.#{identifier.stage_iteration.render(context: URN_CONTEXT)}"
296
296
  else
297
- "v#{identifier.number.render(context: URN_CONTEXT)}"
297
+ "v#{identifier.number}"
298
298
  end
299
299
  end
300
300
  else
301
- parts << identifier.number.render(context: URN_CONTEXT) if identifier.number
301
+ parts << identifier.number.to_s if identifier.number
302
302
  parts << if identifier.stage_iteration
303
303
  "v1.#{identifier.stage_iteration.render(context: URN_CONTEXT)}"
304
304
  else
data/lib/pubid/iso.rb CHANGED
@@ -66,7 +66,7 @@ module Pubid
66
66
  # @param type [Symbol] the type key (e.g. :is, :tr, :ts, :guide)
67
67
  # @param publisher [String, Pubid::Iso::Components::Publisher] primary publisher
68
68
  # @param copublishers [Array<String>, Array<Pubid::Components::Publisher>]
69
- # @param number [String, Integer, Pubid::Iso::Components::Code]
69
+ # @param number [String, Integer]
70
70
  # @param year [String, Integer, Pubid::Components::Date, nil]
71
71
  # @param attrs [Hash] additional identifier attributes
72
72
  # @return [Pubid::Iso::Identifier]
@@ -106,10 +106,11 @@ module Pubid
106
106
  private_class_method :build_copublishers
107
107
 
108
108
  # @!visibility private
109
+ # number/part/subpart are plain strings. A caller that still passes a
110
+ # Components::Code (another flavor's builder, a hand-built id) is read
111
+ # through its own #to_s rather than refused.
109
112
  def self.build_code(value)
110
- return value if value.is_a?(Components::Code)
111
-
112
- Components::Code.new(value: value.to_s)
113
+ value.to_s
113
114
  end
114
115
  private_class_method :build_code
115
116
 
@@ -0,0 +1,69 @@
1
+ # ITU flavor notes
2
+
3
+ ITU grammar, versions, annexes, reports and identity surfaces.
4
+
5
+ These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/itu/` or `spec/pubid/itu/`. The root file keeps the cross-flavor contract that every flavor obeys.
6
+
7
+ - **ITU Questions / Handbooks / N-way combined**: `Pubid::Itu` models study-group **Questions** (`Identifiers::Question`, `_type: pubid:itu:question` — numeric `ITU-R 234-1/7:` and letter-series `ITU-R P.3/BL/7`, `ITU-R S.[4/BL/2]:`; the `/BL` segment, brackets and trailing `:` are carried as `has_bl`/`bracketed`/`has_colon` booleans + a `study_group` string, all reconstructed by `render_base`) and **Handbooks** (`Identifiers::Handbook`, `ITU-R 23.HDB`; the `.HDB` marker is implied by `_type`). Parser disambiguation is by what follows a `/`: **digits** → a Question study-group; a **series** (letters) → a combined designation — so the new rules sit before `with_series`/`without_series`. **Combined (joint) recommendations are N-way**: the primary designation stays on the base `series`/`code` (keeps `root.number` non-empty for relaton-index) and each *additional* designation is a `Components::Designation` in `CombinedIdentifier#combined` (one for a dual `G.780/Y.1351`, two+ for a triple `G.780/Y.1351/Z.1362`); the parser's `combined_designation` rule is `repeat(1)`. **`#number` at the root**: ITU stores the document number on `code`, but `Pubid::Itu::Identifier#number` delegates to `code&.number` so `id.root.number.to_s` is the key relaton-index sorts/bsearches on (non-empty for every type). The delegation is serialization-neutral (the flat block maps `number` via `number_to_kv`/`number_from_kv` off `code`, never the inherited `number` attribute); Supplement/Amendment/Corrigendum/Errata keep their own `:string` `number` ordinal (it overrides the reader), and their base document number is reached via `root.number`.
8
+
9
+ - **ITU `(V##)` versions, labelled annexes, and class-strict supplement `==`**: three ITU-T forms relaton's `relaton-data-itu` crawler needs (~930 data files had no index row because they failed to parse). **(1) `version`** — `ITU-T H.264 (V14) (08/2021)`: a plain **`attribute :version, :string`** on the shared `Pubid::Itu::Identifier` (safe — `::Pubid::Identifier` declares no `version`, so this is a *new* attribute, not the `number`/`stage` retype landmine), fed by the `version_part` rule (`space >> "(V" >> digits >> ")"`) spliced in **immediately before `date_part.maybe`** in all four document rules (`with_series`/`without_series`/`base_with_series`/`base_without_series`) — version always precedes the date in the corpus, and `date_part` requires digits after `(` so the two never compete. Rendered `" (V#{version})"` between code and date by `render_base` **and** by `CombinedIdentifier#to_s` (which overrides `to_s` and does not call `render_base` — miss it and the version is silently dropped on a joint id); mapped in `StandardSerialization`; compared in both `==` overrides. **URN and MR strings stay version-blind by design** (the ITU URN convention defines no version segment), so `(V13)`/`(V14)` share a URN. **(2) Labelled annex** — `ITU-T A.23 Annex A (06/2014)` is its **own** leaf class `Identifiers::AnnexOfRecommendation` (`_type: pubid:itu:annex-of-recommendation`), *not* an extension of `Identifiers::Annex`: that models the structurally different label-less "Annex to ITU OB No. 1000" (prefix rendering + i18n templates) whose `_type` is already persisted in index rows. Shape mirrors `Supplement`: polymorphic `base` + `:string number` (the label — `A`, `F3`, the one-off `C+`) + its own date/language, a minimal `key_value` block (never re-emitting the base's sector/series/code), `render_base` → `"<base> Annex <label>"`, `to_urn` → `"<base urn>:annex:<label>"`, `#root` walking `base` so `root.number` is the annexed document's number. Grammar: `annex_body` wraps the annexed document in **`base.as(:base)`** — load-bearing, because the base may carry its own date (`ITU-T X.692 (2002) Annex E (03/2002)`) and two `date_part`s flattened into one hash would make Parslet silently keep only the last `:year`. `annex_identifier` sits in `rule(:identifier)` **after `supplement_identifier`** (an annex can carry a trailing `Cor.`/`Err.`/`Amd.`, and PEG ordered choice never re-enters once an alternative succeeds) and **before `with_series`** (which would match the `ITU-T A.23` prefix and then fail on the unconsumed ` Annex A`); `supplement_with_base` takes `(annex_body.as(:base) | base.as(:base))` so `ITU-T G.729 Annex B (1996) Cor. 3 (03/2001)` nests annex-inside-supplement. `Builder#build_supplement`'s fallback `Components::Sector.new` is now guarded on `data[:sector]` — an annex base keeps sector/series on *its* base, and the unguarded fallback raised `Invalid sector: `. **(3) `Supplement#==`** now uses **`instance_of?(self.class)`** (so `Suppl. 2` ≠ `Amd 2` ≠ `Cor. 2`, symmetrically — `is_a?` made a Supplement equal an Amendment one-way) and compares `sector`/`series` **only when `base` is nil**. Both halves matter: the series-only form (`ITU-T A Suppl. 2`) holds its whole identity in sector+series, so ignoring them made every series' `Suppl. 2` equal (an index search returned 42 rows, and `eql?`/`hash` collapsed them in a `Set`); but a *based* supplement copies sector/series from its base while the key_value block deliberately does **not** serialize them, so comparing them unconditionally would break `parse(s) == from_hash(parse(s).to_hash)` — the very lookup this fixes. The now-identical `==` overrides on `Amendment`/`Corrigendum`/`Errata` were deleted. `language` is deliberately **not** compared (an index row without a language suffix must still match a reference that has one). Locked by `spec/pubid/itu/identifiers/{version,annex_of_recommendation,supplement}_spec.rb` + rows in `root_number_spec.rb`/`serialization_spec.rb` and the new `spec/fixtures/itu/identifiers/pass/{version,annex_of_recommendation}.txt`. **Rendering/URN/MR consequences of the annex wrapper:** `CombinedIdentifier` had its rendering in `to_s`, which the annex (composing `base.render_base`) bypassed — silently dropping the `/Y.1351` half, so `G.780/Y.1351 Annex A` and `G.780/Z.1362 Annex A` printed identically (and `to_s` is relaton's document number *and* output filename). It now renders in **`render_base`**, with the inherited `to_s` adding the language suffix and common-text twin. The annex's `to_urn` appends its **own date** after the label (`urn:itu:t:A.23:annex:a:06/2014`) — the base's date rides inside `base_urn`, so omitting the annex's would collide editions — and it defines **`mr_supplement_suffix`** (`annex.<label>[.<year>]`, `C+`→`cplus`) so the shared MrString renderer recurses into the annexed document instead of collapsing every annex onto a bare `itu.<date>`. Because the annex keeps sector/series/code on *its* base, `build_supplement` reaches identity through **`base.root`** when the base has none, so a supplement *of* an annex keys like any other supplement. **Version matching semantics:** `version` is in `==`, so a bare `ITU-T H.264` does **not** match `ITU-T H.264 (V14) …` under `ignore: %i[year month]` — version is a *separable trailing component* like ETSI's (`partial_ref_spec` now lists ITU as `omits: %i[date version]`), so callers matching a partial ref must add `:version` to `ignore`; it is deliberately **not** folded into the `:year`→`:date` alias. **Also fixed here:** `spec/pubid/itu/fixtures_spec.rb`'s glob had one `..` too many (repo-root `fixtures/`), so the whole ITU fixture round-trip spec silently iterated an empty file list. **Out of scope at the time, both since fixed:** `ITU-T V.25 ter Annex A (08/1996)` (the space-separated `ter` gap — closed by the code-suffix work below), and `Amendment#to_s`'s missing dot. (hand-off: itu-version-annex-and-supplement-matching.)
10
+
11
+ - **ITU-T unparseable print forms (the `relaton-data-itu` residue)**: 668 of the 755 unindexed `relaton-data-itu` records were ITU-T `rec_name` spellings the grammar had no rule for; all 668 now parse, render and satisfy relaton's gate (`from_hash(to_hash) == to_hash`), with **zero** change to any of the 20,728 already-indexed ids. **The single most important invariant when touching this area: every new rendering flag is a `:boolean` named for the RARE form with `default: -> { false }`**, because the canonical `to_hash` strips default-valued attributes — a flag named for the common case would add a key to every already-published index row. Ten constructs, grouped by where they live:
12
+ **(1) Supplement family.** `supplement_type` gained `Add.`/`Add` → the new leaf `Identifiers::Addendum < Supplement` (`_type: pubid:itu:addendum`), and `Builder#build_supplement`'s `case` gained an **`else raise ArgumentError`** — it previously fell through to a nil class and died with `undefined method 'new' for nil` one frame later. The near-identical `to_s` on `Supplement`/`Amendment`/`Corrigendum`/`Errata` collapsed into one **`Supplement#render_supplement(label)`**; each subclass now supplies only its label. `supplement_number` made the space optional (`ITU-T E Suppl.1`, `ITU-T D.211 Suppl.1` → `number_glued`) and the ordinal dotted (`ITU-T M Suppl. 1.1` → `number` is the whole `"1.1"`). `number_glued` is serialized but deliberately **not** in `==` — `Suppl.1` and `Suppl. 1` are one document. `chained_supplement`'s inner alternation gained `supplement_series_only` so a supplement *of* a series-only supplement parses (`ITU-T G Suppl. 39 (2006) Err. 1 (08/2006)`).
13
+ **(2) `Technical Cor.`** (158 records, the largest bucket — and the one the hand-off mislabelled as a series-code form). A `technical_marker` rule at the **head of `supplement_type`**, not folded into its alternation, so the type token stays the plain `Cor.` the builder's `case` maps; a `technical` boolean on `Corrigendum` renders and compares it. The slash-joined pair `ITU-T X.680 (1994) Amd. 1/Technical Cor. 1 (12/1997)` is a new **`slash_chained_supplement`** rule (a Corrigendum whose `base` is the Amendment) placed **before `supplement_with_base`** in the alternation — that alternative alone matches the `… Amd. 1` prefix and leaves `/Technical Cor. 1 …` unconsumed, which fails the whole parse with no re-entry.
14
+ **(3) Code suffixes — the highest-blast-radius change, since it fires on *every* recommendation parse.** A shared `code_suffixes` rule (`series_suffix_spaced.maybe >> qualifier_spaced.maybe`) spliced after `code` and **before `combined_suffixes`** (the corpus attaches the primary's suffix before the `/`: `ITU-T D.301 R/F.66`) in all four document rules. It cannot live *inside* `code`, or its leading space would also be offered to ` Suppl.`/` Annex` and have to backtrack out of a rule the wrappers depend on. `Components::Code` gained `series_suffix_spaced` (the spaced `ITU-T E.250 bis` vs the spec-locked glued `X.50bis` of issue #231 — the word itself is stored whitespace-free either way, so the serialized value stays comparable), plus `qualifier` + `qualifier_glued` for the trailing letter (`ITU-T D.200 R`, `ITU-T Q.2931 B`, `ITU-T R.38 A`, glued `ITU-T D.502R`, lowercase `ITU-T I.256.2a`). **Both guards on `qualifier_letter` are load-bearing**: the `A-R`/`a-r` cap keeps it off the `S` of ` Suppl.` and the `V` of a bare ` V2`, and the trailing `match["A-Za-z0-9"].absent?` is what stops ` Amd.`/` Add.`/` Annex A`/` App.`/` Cor.`/` Err.` being read as a qualifier. It cannot collide with `language` (dash-attached) or `date_part`/`version_part` (both need `(` after the space). Because `Code#to_s` can now contain a space, **`Code#compact_s`** (`to_s.delete(" ")`) was added and is what `urn_generator` interpolates — a space is not admissible in a URN segment, and dropping the suffix instead would collapse `D.200` and `D.200 R` onto one URN. The same slots were added inside `combined_designation` (positionally, so the suffix lands on the designation it printed against: `ITU-T D.300 R/E.282 R` sets both, `ITU-T E.211/Q.11 quater` only the last) — which required the parallel edits to `Builder#build_designations` **and** `CombinedIdentifier#combined_to_kv`/`combined_from_kv`, whose row hash is hand-built. Neither spacing flag is in `Code#==`: the two spellings of one number are one document. **`mr_number_with_part` also had to learn both suffixes** (glued to the number, `x-50bis`/`d-200r`) — it read only number/subseries/parts, so every qualified variant collapsed onto its base's MR slug and, worse, `Q.2931 B` and `Q.2931 C` onto each other. This changes the MR string of the handful of pre-existing glued-`bis` ids (`X.50bis`: `itu.t.x-50` → `itu.t.x-50bis`), which is the point — they were wrongly sharing a slug with `X.50`.
15
+ **(4) Version spellings.** `version_part` gained a bare branch, so `v.1`/`v10`/`V2` join the canonical `(V14)` in the same `version` attribute. This is the **one deliberate non-byte-exact normalisation** — all render as `(V##)` — so those strings live in `version_spec.rb` with explicit expectations and **not** in the byte-exact pass fixtures. Safe against the qualifier rule precisely because `V` is outside its `A-R` cap. relaton can now drop its own `v10` → `(V10)` normalisation.
16
+ **(5) Appendix.** `Identifiers::AppendixOfRecommendation` (`_type: pubid:itu:appendix-of-recommendation`) mirrors `AnnexOfRecommendation` one-for-one — including wrapping the appendixed document in **`base.as(:base)`** so the base's own date (`ITU-T G.722 (1988) App. IV (11/2006)`) lands a level down instead of colliding, and defining `mr_supplement_suffix` so the shared MR renderer recurses. Its alternation slot is the annex's reasoning verbatim: **after `supplement_identifier`** (an appendix can carry a trailing `Amd.`/`Err.`), **before `with_series`**. Three registrations are easy to miss and all three are needed: the `identifiers.rb` autoload, the `Builder#build` branch, and widening `urn_generator`'s `AnnexOfRecommendation` check — that check exists because a wrapper whose identity sits one level deeper otherwise emits `urn:itu:itu`. A `material` attribute carries the companion-artefact records (`App. II test vectors`, `App. I Software`), and is in the URN so they stay distinct from the appendix itself.
17
+ **(6) Series groups vs series-code documents — a corpus heuristic, not an ITU rule.** `series_group` (`E-100`, `E100-300`, `G-100`, `Q-500`) is capped at **one** leading letter; `series_code_body` (`EMC-5`, `MES-2`, `QOS-2`, `IMPL-8`, `SEC-QKD`) requires **two or more**. That split is what keeps them from shadowing each other, and it holds across the whole corpus but would misfile a future `AB-100` group or `E-QKD` document — `series_group_spec.rb` locks both sides so a change is visible. `series_group` must be tried **before** `series`, whose greedy non-backtracking `letter.repeat(1,3)` would take the `E` of `E-100` and then fail on the required space with no retry at a shorter length. `series_code_body` is a flat two-token shape (series + dash + alphanumeric number) rather than an optional middle segment, because a greedy `letter.repeat(2)` + optional `-QKD` + required dash fails at end-of-input with no backtracking into the satisfied `.maybe`; it sits **last** in `base` and after `with_series` in `identifier`, where nothing can reach it by accident. The number stays in `code.number` (not `"EMC-5"` in `series`) precisely so **`root.number`** — the key relaton-index bsearches on — is `"5"`/`"QKD"` rather than nil. `series_dash` drives the `-` vs `.` join in `render_base`; the builder's OB check gained a `series_dash.nil?` guard so a hypothetical `ITU-T OB-1` cannot be misrouted into the Operational Bulletin branch. The literal word `series` is its own `series_word` boolean and **cannot** live in the series token, because it also follows a *dotted* code (`ITU-T E.1100 series Suppl. 1`) where folding it in would corrupt the value every index row is keyed on; `Supplement` emits it only when base-less (guarded like `sector`/`series`).
18
+ **(7) Tail.** `attachment` boolean (`ITU-T H.350 attachment`), `range_end` string (`ITU-T Q.120-Q.139` — no conflict with `parts`, which needs digits after the dash), and `Identifier.parse` now collapses runs of whitespace (`ITU-T D.271 (10/2016)`) **after** the shared `Pubid::MAX_INPUT_LENGTH` guard, never before.
19
+ **The cross-cutting lesson (three review findings were all this same mistake): an identity-bearing marker must reach ALL THREE identity surfaces — `to_s`, `to_urn` and `to_mr_string` — not just `==`.** A marker that only reaches `==` still lets two distinct documents share a URN and an output slug. So `generate_base_urn` emits `series`/`attachment`/`to-<range_end>` segments and joins a series-code document with its dash (`urn:itu:t:EMC-5:2003`, not the ambiguous `EMC.5`); `generate_supplement_urn`'s base-less branch emits `series` too; and `mr_number_with_part` carries all five. `Builder#build_supplement` copies `series_word` up from the base alongside sector/series/code for the same reason — render/serialize/`==` all guard on `base.nil?`, so the copy is neutral there and only the MR slug sees it. `spec/pubid/itu/distinctness_spec.rb` is the forcing function: it asserts all four surfaces differ for every marker pair. **Two more review findings worth keeping:** `technical_marker` is bound to the **`Cor.` branch alone**, not the head of `supplement_type` — offered before any token it was accepted on `Technical Err. 1`/`Technical Amd. 1` and then silently dropped by the builder's Corrigendum-only guard, collapsing those onto a *different* document (they are now cleanly rejected); and `series_code_body` carries a `(str("OB") >> dash).absent?` guard, because `ITU-T OB-1` otherwise reached the Recommendation fallback whose `validate_ob_no_sector!` raises an `ArgumentError` that escapes `Identifier.parse`'s `Parslet::ParseFailed` rescue — turning a rejected input into a crash for callers. `Code#to_s` renders glued suffixes before spaced ones, mirroring the grammar (a fixed edition-word-first order printed `Q.11a bis` back as `Q.11 bisa`). **Two known pre-existing gaps, deliberately not fixed and both pinned by specs in `distinctness_spec.rb`:** (a) the ITU URN convention encodes no supplement *type*, so `Amd. 1`/`Cor. 1`/`Err. 1`/`Add. 1` of one base share a URN and MR slug; (b) `build_supplement` copies sector/series/code down from the base but they are deliberately not serialized, so a supplement rebuilt through `from_hash` has none of them and its **MR slug collapses to `itu.<date>`** while every other surface stays symmetric. Both are true on `main` for `Amd.`/`Cor.`/`Err.` before this change — the new types simply inherit them, and `to_s`/`to_hash`/`==` distinguish everything correctly, which is what relaton's gate uses. Both would be fixed by giving `Supplement` an `mr_supplement_suffix` so the shared MR renderer recurses into `base` (as `AnnexOfRecommendation` already does), which changes every existing ITU supplement MR string — a deliberate call, not a tag-along. Also fixed here: `render_base` gained an `elsif series` branch, so a code-less identifier no longer renders a dangling trailing dot. Locked by `spec/pubid/itu/{code_suffixes,combined_suffixes,series_group,tail_forms}_spec.rb`, `spec/pubid/itu/identifiers/{addendum,appendix_of_recommendation,supplement_spelling,technical_corrigendum}_spec.rb`, and 7 new `spec/fixtures/itu/identifiers/pass/*.txt` files drawn from the real corpus. **Still out of scope (both declared not-ours by the hand-off):** ~47 malformed ITU-R docids left by a decommissioned crawler (`ITU-R BO`, `ITU-R M.5-BL-13` — series with no number; dataset bugs, not identifiers), and the 4 space-for-dot `ITU-T G 231` spellings relaton normalises on its side. (hand-off: itu-t-unparseable-forms.)
20
+
21
+ - **ITU-R Reports (`Identifiers::Report`) and the class-strict `Identifier#==`**: ITU-R **Reports** are a publication series that numbers **independently** of Recommendations, so `ITU-R BT.2020-1` names *two* real, both-current documents (Report BT.2020-1, objective quality assessment, 2000; Recommendation BT.2020-1, UHDTV parameter values, 06/2014). 984 of 1,001 report records in `relaton-data-itu` were indexed as `pubid:itu:recommendation`, and 52 editions across 30 numbers (BT.2020, M.2083, M.2134 …) collided outright — the dataset keys files by identifier, so whichever was crawled last silently overwrote the other. ITU disambiguates with the leading word, so the identifier must too. **The leaf** `Identifiers::Report` (`_type: pubid:itu:report`) is shaped exactly like `Recommendation` — same attributes, same `StandardSerialization` flat block, so only `_type` distinguishes the two hashes — and adds just `render_base` (`"Report #{super}"`) and `mr_type` (`[super, "report"].join(".")` → `itu.r.report.bt-2020-1`). **Grammar**: a captured `report_word` marker (unlike `itu_prefix`'s decorative, *uncaptured* `"Recommendation "` literal, which stays as it is) plus a single `report_body` whose `(series >> dot).maybe` covers the with- and without-series shapes at once; `base_report` accepts **both** spellings (`Report ITU-R BT.2020-1` and the downstream `ITU-R Report BT.2020-1`), and `to_s` renders ITU's own leading form for either — so the infix spelling lives in `report_spec.rb` with an explicit expectation, **not** in the byte-exact pass fixtures. Two deliberate exclusions from `report_body`: **no `combined_suffixes`** (a joint `Report X/Y` would reach `Builder#build`'s `combined` branch, build a `CombinedIdentifier` and *silently drop the marker* — a clean parse failure is better), and a **`(str("OB") >> dot).absent?` guard** (an Operational Bulletin is cross-bureau; without it `Report ITU-T OB.1` routes to `SpecialPublication`, marker dropped, or trips `validate_ob_no_sector!`, whose `ArgumentError` escapes `Identifier.parse`'s `Parslet::ParseFailed` rescue). `base_report` is **first** in `rule(:base)` — which is what lets a supplement/annex/appendix *wrap* a Report — and order-free there because `itu_prefix` admits only `Recommendation `/`ITU` and `letter` is `[A-Z]`, so neither `series` nor `series_code_body` can consume `Report`; `report_identifier` sits in `rule(:identifier)` **after** supplement/annex/appendix (which match the longer wrapped form) and **before** `with_series`/`without_series` (which would match the infix spelling's `ITU-R` prefix and then fail on the unconsumed tail). The builder picks the class from `data[:report_marker]` in the existing Recommendation fallthrough; `build_supplement`/`build_annex_of_recommendation` need **no** change, since they recurse via `build(data[:base])`. **`Pubid::Itu::Identifier#==` is now class-strict** (`other.instance_of?(self.class)`, the rule `Supplement#==` already used) — the ITU *type* is part of the identity, not just sector/series/number. `self.class` rather than a hard-coded class keeps it **symmetric in both directions** (an `is_a?` guard would have made a Recommendation equal a Report one-way, and `#matches?` — which is `exclude(*ignore) == other.exclude(*ignore)` — resolve one to the other). Strictly stricter, so it can only turn `true` into `false`; it also closes the pre-existing one-way holes against `CombinedIdentifier` and `SpecialPublication`. **Consequences of the shared guard, both pinned by `spec/pubid/itu/type_strict_equality_spec.rb`:** comparisons that used to be *asymmetric* are now consistently false — most notably a bare primary designation vs its joint recommendation (`ITU-T G.780` vs `ITU-T G.780/Y.1351`, which was `true` one way and `false` the other on `main`, so no caller could rely on it either way). A caller matching a bare primary against a joint document must narrow on `root.number` (which still agrees) rather than on `==`. Per the cross-cutting ITU lesson, the marker reaches **all** identity surfaces: `to_s`, `generate_base_urn` (a `report` segment right after the sector — `urn:itu:r:report:BT.2020-1:2000`, which `UrnParser` also reads back), and the MR slug — including **`Supplement#mr_type`**, which is load-bearing and easy to miss: a supplement has no `mr_supplement_suffix`, so the shared MR renderer slugs it **flat** from the sector/series/code `build_supplement` copied up from its base and *never consults the base's class*, which made `Report ITU-R BT.2020-1 Suppl. 1` and `ITU-R BT.2020-1 Suppl. 1` share one slug (i.e. one output filename — the very overwrite this type prevents). It appends `report` when **`root`** is a Report, so a supplement of an *annex* of a Report is covered too. **`PREFIXES` stays `["ITU"]`** — the leading `Report` token is deliberately not registered for prefix routing (a generic English word; the mixin's ambiguous-token exclusion), and the infix spelling routes on `ITU` as before. **Backwards compatibility is exact**: a bare `ITU-R BT.2020-1` still builds a `Recommendation` with an unchanged hash/`to_s`/URN — nothing in the grammar or the builder fires without the marker, so none of the 20,728 published index rows move. Locked by `spec/pubid/itu/identifiers/report_spec.rb`, a `distinctness_spec.rb` pair, rows in `root_number_spec.rb`/`serialization_spec.rb`/`urn_parser_spec.rb`, and `spec/fixtures/itu/identifiers/pass/report.txt`. **Out of scope (relaton's side, sequenced after this):** `DataParserR`/`DataCrawlerR` emitting the new docid form, `Relaton::Itu::Pubid`'s reference parser learning a `report` rule, and the `relaton-data-itu` filename-namespace migration + re-crawl that actually recovers the 34 missing Recommendations and 18 missing Reports. (hand-off: itu-r-report-identifier-type.)
22
+
23
+ ## From the root note "Parse-failure error contract — uniform across every flavor"
24
+
25
+ **ITU's length guard was an early `return`, not a raise, and its comment blamed the wrong caller.** `Identifier.normalize_whitespace` (`lib/pubid/itu/identifiers/base.rb`) carried
26
+
27
+ ```ruby
28
+ return identifier if identifier.length > ::Pubid::MAX_INPUT_LENGTH
29
+ ```
30
+
31
+ under a note claiming "`Pubid.parse` rejects it before this point". That was wrong twice: `Pubid.parse` never routes a human-readable string to a flavor at all (`lib/pubid.rb` raises `ArgumentError: No flavor specified` for anything that is not an MR string or a URN), and ITU had no raising guard of its own — so an over-long string reached the ITU parser through both `Pubid::Itu.parse` and `Pubid::Itu::Identifier.parse`. The guard now lives in `self.parse` as the standard inline pair, and the comment says what actually protects the method.
32
+
33
+ **ITU was also the delegate in the most-visible class leak.** `Pubid::Iso.parse("ITU-T G.711")` detects an MR-shaped string, routes through `Pubid::Parsers::MrString`, and lands in `Pubid::Itu::Identifier.parse` — which raised `RuntimeError`. So `Pubid::Iso.parse` raised **two different classes** depending on its input, and a relaton-cli caller rescuing `Parslet::ParseFailed` got a raw backtrace for the ITU-shaped half. Converting ITU fixed the ISO symptom; `spec/pubid/parse_error_spec.rb` pins that exact route by name, because a generic junk string never reaches it.
34
+
35
+ - **The five supplement types rendered plain under `to_s(annotated: true)`.**
36
+ `Pubid::Itu::Identifier#to_s` annotates, but `Addendum`, `Amendment`,
37
+ `Corrigendum`, `Errata` and `Supplement` each override it and none calls
38
+ `super` — they all funnel through `Supplement#render_supplement(label)`.
39
+ The wrap cannot live in that helper, which takes a label and no options,
40
+ so each of the five `to_s` is a one-line
41
+ `annotate_plain_render(render_supplement("…"), **opts)`.
42
+
43
+ `render_supplement` starts from `base.to_s` with no options, so the base
44
+ arrives plain and the single outer annotation covers the whole string —
45
+ the base's own tokens are reached by `Annotator#emit_tokens` walking
46
+ `base`.
47
+
48
+ ## Contribution (Temporary Document) — pubid#340
49
+
50
+ `Pubid::Itu::Identifiers::Contribution` models the working documents a study
51
+ group circulates, mirroring pubid-itu 1.15's `Identifier::Contribution`
52
+ (`%{series}-C%{number}`): "ITU-R SG17-C1000", sector- and language-suffixed
53
+ per the existing rules ("ITU-T SG17-C1000-E"). The grammar entry sits between
54
+ `with_series` and `series_code_identifier` — the **-C marker (dash + "C" +
55
+ digits) is the discriminator**: a series-code document's post-dash number
56
+ starts with digits or is all letters, never "C"+digits, so the two dash
57
+ shapes cannot shadow each other in either direction ("ITU-T EMC-5" still
58
+ builds a Recommendation, locked by a spec). The builder branch carries the
59
+ `:contribution_marker`; `root.number` reaches the C-number through the shared
60
+ `code.number` reader, so relaton-index keys it normally.
61
+
62
+ **`locate_type` was broken for every leaf, not just this one**: no ITU class
63
+ ever defined a `type` hash, so `Pubid::Itu.locate_type` raised `NoMethodError`
64
+ on any call. The base now derives the key from the class name
65
+ (`Contribution` → `:contribution`, `AnnexOfRecommendation` →
66
+ `:annex_of_recommendation`) — plain `gsub` camel→snake, because ActiveSupport's
67
+ `underscore` is not a dependency. metanorma-itu constructs through this
68
+ lookup; its flavor-local `pubid_contribution.rb` render override can be
69
+ deleted once it migrates.
@@ -68,6 +68,16 @@ module Pubid
68
68
  return build_question(data)
69
69
  end
70
70
 
71
+ # Contribution (Temporary Document) — "ITU-R SG17-C1000" (pubid#340).
72
+ if data[:contribution_marker]
73
+ return Identifiers::Contribution.new(
74
+ sector: Components::Sector.new(sector: data[:sector].to_s),
75
+ series: Components::Series.new(series: data[:series].to_s),
76
+ code: build_code(data),
77
+ language: data[:language]&.to_s,
78
+ )
79
+ end
80
+
71
81
  # Build basic recommendation or combined identifier
72
82
  sector = Components::Sector.new(sector: data[:sector].to_s)
73
83
  series = Components::Series.new(series: data[:series].to_s) if data[:series]
@@ -358,3 +368,5 @@ module Pubid
358
368
  end
359
369
  end
360
370
  end
371
+
372
+ Pubid::Itu::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -20,6 +20,8 @@ module Pubid
20
20
  # +subseries+ (dot-separated, flavor-specific) and +parts+
21
21
  # (dash-separated).
22
22
  class Code < Lutaml::Model::Serializable
23
+ include ::Pubid::SubsetMatch
24
+
23
25
  attribute :imp_marker, :string
24
26
  attribute :number, :string
25
27
  attribute :series_suffix, :string
@@ -13,6 +13,8 @@ module Pubid
13
13
  #
14
14
  # Format: SERIES.CODE (e.g. "Y.1351", "Y.1362-2")
15
15
  class Designation < Lutaml::Model::Serializable
16
+ include ::Pubid::SubsetMatch
17
+
16
18
  attribute :series, Pubid::Itu::Components::Series
17
19
  attribute :code, Pubid::Itu::Components::Code
18
20
 
@@ -8,6 +8,8 @@ module Pubid
8
8
  # ITU Sector component
9
9
  # Values: R (Radio), T (Telecommunications), D (Development)
10
10
  class Sector < Lutaml::Model::Serializable
11
+ include ::Pubid::SubsetMatch
12
+
11
13
  attribute :sector, :string # R, T, or D
12
14
 
13
15
  VALID_SECTORS = %w[R T D].freeze
@@ -8,6 +8,8 @@ module Pubid
8
8
  # ITU Series component
9
9
  # Examples: BO, V, X, A, etc.
10
10
  class Series < Lutaml::Model::Serializable
11
+ include ::Pubid::SubsetMatch
12
+
11
13
  attribute :series, :string # e.g., BO, V, X, R, SG1, OB
12
14
 
13
15
  def to_s
@@ -37,6 +37,16 @@ module Pubid
37
37
  end
38
38
  private_class_method :normalize_whitespace
39
39
 
40
+ # The type key used by Pubid::Itu.locate_type — the class name
41
+ # ("Contribution" => :contribution). No ITU leaf ever defined a
42
+ # `type` hash, so locate_type raised NoMethodError on every call;
43
+ # the derived default gives every concrete leaf a key for free
44
+ # (pubid#340's ask). metanorma-itu constructs via this lookup.
45
+ def self.type
46
+ leaf = name.split("::").last
47
+ { key: leaf.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym }
48
+ end
49
+
40
50
  # Long-form ↔ ITU single-letter language code map. The parser produces
41
51
  # single-letter codes (E/F/S/R/A/C); API callers (e.g. metanorma-itu)
42
52
  # pass long-form (en/fr/es/ru/ar/zh). Storage is normalized to the
@@ -0,0 +1,31 @@
1
+ # frozen_string: true
2
+
3
+ module Pubid
4
+ module Itu
5
+ module Identifiers
6
+ # ITU Contribution (Temporary Document) — the working documents a study
7
+ # group circulates, numbered per study group.
8
+ #
9
+ # Format: ITU-{SECTOR} {SERIES}-C{NUMBER} e.g. ITU-R SG17-C1000
10
+ #
11
+ # Mirrors pubid-itu 1.15's Pubid::Itu::Identifier::Contribution
12
+ # ("%{series}-C%{number}"), which metanorma-itu builds from
13
+ # `:doctype: contribution` documents (pubid#340).
14
+ class Contribution < Identifier
15
+ include StandardSerialization
16
+
17
+ def render_base(**_opts)
18
+ "#{publisher}-#{sector} #{series}-C#{code&.number}"
19
+ end
20
+
21
+ def ==(other)
22
+ return false unless other.is_a?(Contribution)
23
+
24
+ sector == other.sector &&
25
+ series == other.series &&
26
+ code == other.code
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -131,6 +131,21 @@ module Pubid
131
131
  # Builder#build_supplement) and are deliberately not serialized (see
132
132
  # supplement_sector_to_kv), so comparing them would make a parsed
133
133
  # identifier unequal to the same identifier rebuilt via from_hash.
134
+ # A subset match follows the same rules as `==` below: the two
135
+ # rendering flags are not identity, and when a base is present the
136
+ # sector/series/code copies of the base are not serialized.
137
+ SUBSET_BASE_COPIES = %i[sector series code series_word].freeze
138
+
139
+ def self.subset_ignored_attributes
140
+ %i[number_glued slash_joined]
141
+ end
142
+
143
+ def subset_attribute_match?(name, mine, theirs)
144
+ return true if base && SUBSET_BASE_COPIES.include?(name)
145
+
146
+ super
147
+ end
148
+
134
149
  def ==(other)
135
150
  return false unless other.instance_of?(self.class)
136
151
 
@@ -11,6 +11,7 @@ module Pubid
11
11
  autoload :AppendixOfRecommendation,
12
12
  "#{__dir__}/identifiers/appendix_of_recommendation"
13
13
  autoload :CombinedIdentifier, "#{__dir__}/identifiers/combined_identifier"
14
+ autoload :Contribution, "#{__dir__}/identifiers/contribution"
14
15
  autoload :Corrigendum, "#{__dir__}/identifiers/corrigendum"
15
16
  autoload :Errata, "#{__dir__}/identifiers/errata"
16
17
  autoload :Handbook, "#{__dir__}/identifiers/handbook"
@@ -323,6 +323,22 @@ module Pubid
323
323
  (digits | letter.repeat(1)).as(:number) >> parts
324
324
  end
325
325
 
326
+ # ITU Contribution (Temporary Document) — "ITU-R SG17-C1000" (pubid#340).
327
+ # The "-C" marker before the number distinguishes it from every
328
+ # neighbouring shape: a series-code document's post-dash number starts
329
+ # with digits or is all letters (never "C"+digits), and with_series
330
+ # requires a dot after the series.
331
+ rule(:contribution) do
332
+ itu_prefix >>
333
+ sector >>
334
+ space >>
335
+ series >>
336
+ str("-C").as(:contribution_marker) >>
337
+ number >>
338
+ parts >>
339
+ language.maybe
340
+ end
341
+
326
342
  rule(:base_series_code) do
327
343
  itu_prefix >>
328
344
  sector >>
@@ -656,9 +672,10 @@ module Pubid
656
672
  numeric_question |
657
673
  letter_question |
658
674
  with_series |
675
+ contribution |
659
676
  # Unreachable earlier: special_publication needs the literal "OB",
660
- # handbook/numeric_question need leading digits, and letter_question
661
- # needs series >> dot. Before without_series, whose code needs digits.
677
+ # handbook/numeric_question need leading digits, letter_question
678
+ # needs series >> dot, and contribution needs "-C" after the series.
662
679
  series_code_identifier |
663
680
  without_series
664
681
  end
@@ -0,0 +1,7 @@
1
+ # JCGM flavor notes
2
+
3
+ JCGM meetings and URN parsing.
4
+
5
+ These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/jcgm/` or `spec/pubid/jcgm/`. The root file keeps the cross-flavor contract that every flavor obeys.
6
+
7
+ - **JCGM meeting year is optional (`JCGM 11st Meeting`)**: a meeting's trailing ` (YYYY)` group is a *separable* component, not part of its identity — JCGM numbers its meetings in one sequence, so the ordinal alone names the event. `rule(:meeting_identifier)` wraps the **whole** group in `.maybe` (not the year inside it, so a dangling `(`, an empty `()` or a truncated `(200)` still fails), mirroring `date_portion.maybe` in `rule(:base)`; `render_meeting` appends the group only when `date` is present. No builder or serialization change was needed: `Builder::Base#assign_attributes` iterates the parsed hash, so an absent `:date` key is simply never assigned, and `SingleIdentifier#emit_kv` already drops a nil value — a dateless meeting is `{_type, number}` and a dated one is byte-identical to before, so **no index migration follows**. **The load-bearing third surface is `UrnParser#parse_meeting_urn`**: `UrnGenerator#generate_meeting_urn` had *always* guarded the date and emitted the short `urn:jcgm:meeting:11`, but the parser rebuilt the ungrammatical `"JCGM 11st Meeting ()"` from it — so the dateless form must be made optional on the URN read-back too, or the round-trip the relaton index depends on breaks. **The whole `UrnParser` was converted to build identifiers directly from the URN segments** (the BIPM pattern; 5 of 38 flavors construct directly, the rest use the shared `flavor_parse` helper, which stays for them). Rendering a string and re-parsing it flattens the segments and silently loses whatever the string form cannot carry, which is what three **pre-existing** defects were: `urn:jcgm:gum.6:2020` **raised** (`"JCGM gum.6:2020"` is not a grammar form); the language segment was dropped (`urn:jcgm:100:2008:en` → `JCGM 100:2008`); and `number, year = parts` discarded every segment past the second, so a supplement decayed into **the standard it amends** — `urn:jcgm:200:2008:corrigendum` → `JCGM 200:2008`, a different document, with no error. The supplement marker (`corrigendum`/`amendment`, written by the generator as the `type_code`) splits the segments: everything before it is the base document, everything after is the supplement's own number and date. Four things the string round-trip used to supply for free are now explicit, and each is load-bearing: (1) **`typed_stage` comes from `Jcgm.locate_stage`, the same registry lookup `Builder#locate_typed_stage` uses — NOT from the attribute default** — because `SingleIdentifier.published_typed_stage` additionally sets `original_abbr`, which would make a URN-reconstructed id **unequal** to a parsed one; the marker is mapped to the abbreviation the registry indexes (`amendment` → `Amd`, since `locate_stage` matches `abbr`, not `type_code`), and the class then comes from `Jcgm.locate_type(typed_stage.type_code)` so one lookup fixes both. **Guide and GumGuide deliberately take the default instead**, because the grammar emits no type token for them and `Builder#build` fills theirs from `published_typed_stage` too — matching the builder means matching *which* of the two paths it took. (2) The meeting number is normalized with **`to_i`**, matching `Identifiers::Meeting.ordinal`, so `011` still reads back as `11` and a missing or non-numeric segment as `0`. (3) The year is **validated against the grammar's `19xx|20xx`** and rejected with `Pubid::UrnParser::Errors::ParseError` (the easc/gost convention) — with no re-parse, nothing else would catch a malformed segment. (4) A language code is mapped back through the inverse of `Builder::Base::LANG_CHAR_MAP` to restore `original_code`, which is what the renderer prints (`en,fr` → `(E/F)`). **KNOWN GAP, generator-side and deliberately unchanged**: a full date renders as its year alone (`JCGM GUM-1:2022-11-28` → `urn:jcgm:gum.1:2022`), so a URN cannot restore month and day; widening it would change already-published URNs. 30 of the 32 corpus ids now round-trip through the URN byte-exactly, and those 2 are the truncated-date pair. (That `original_abbr` asymmetry is **since fixed** — see `lib/pubid/iec/CLAUDE.md` — so the default now agrees too and the lookup mirrors the builder for clarity rather than necessity.) **Deliberately not a typed error**: the earlier reading of this defect was to raise on a nil date; an undated meeting reference is unambiguous and legitimate, and raising would leave relaton's `Bib::ItemData#to_most_recent_reference` with nothing to return. **Do not add the English teens exception to `Identifiers::Meeting.ordinal`** — the real records print `11st`/`12nd`/`13rd`, and three published documents round-trip through that naive rule. Locked by `spec/pubid/jcgm/meeting_partial_spec.rb` plus a bare-form row in `urn_parser_spec.rb`. **Still open, separate call**: `Meeting.new(number: "11").to_s` raises `NoMethodError` (`number` is a `Components::Code`; a bare String is accepted at construction and only explodes at render), and `Meeting#ordinal` renders `"0th"` for a nil `number`. (hand-off: jcgm-meeting-render-nil-date.)
@@ -104,3 +104,5 @@ module Pubid
104
104
  end
105
105
  end
106
106
  end
107
+
108
+ Pubid::Jcgm::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -6,6 +6,8 @@ module Pubid
6
6
  module Jcgm
7
7
  module Components
8
8
  class Publisher < Lutaml::Model::Serializable
9
+ include ::Pubid::SubsetMatch
10
+
9
11
  attribute :publisher, :string
10
12
 
11
13
  def to_s
@@ -20,6 +20,9 @@ module Pubid
20
20
  build_single_identifier(data)
21
21
  end
22
22
  attach_symbol(identifier, data)
23
+ # "(all parts)" names every part of the document, so it wraps the
24
+ # document, which holds no mark itself.
25
+ data[:all_parts] ? identifier.to_all_parts : identifier
23
26
  end
24
27
 
25
28
  private
@@ -37,7 +40,6 @@ module Pubid
37
40
  parts: extract_part_strings(data[:parts]),
38
41
  year: data[:year]&.to_i,
39
42
  language: data[:language]&.to_s,
40
- all_parts: (true if data[:all_parts]),
41
43
  reaffirmed: (true if data[:reaffirmed]),
42
44
  }
43
45
 
@@ -114,3 +116,5 @@ module Pubid
114
116
  end
115
117
  end
116
118
  end
119
+
120
+ Pubid::Jis::Builder.prepend(Pubid::Builder::AllPartsWrap)