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
@@ -21,15 +21,15 @@ module Pubid
21
21
  parts << identifier.flex_prefix.to_s.downcase
22
22
  end
23
23
 
24
- # Fall back to the wrapped document's number. A wrapper — an adopted
25
- # European norm, a bundle, a set — carries no number of its own, and
26
- # `AdoptedEuropeanNorm#number` only delegates ONE level, so a
27
- # "DD ENV ISO 11079:1999" (which adopts a CEN prestandard that is
28
- # itself a wrapper around the ISO standard) emitted the identity-free
24
+ # Read identity from the wrapped document when the wrapper has none of
25
+ # its own. A wrapper — an adopted European norm, a bundle, a set —
26
+ # carries no number, part, subpart or date, so a
27
+ # "DD ENV ISO 11079:1999" (which adopts a CEN prestandard that is itself
28
+ # a wrapper around the ISO standard) emitted the identity-free
29
29
  # `urn:bsi:dd` — the same URN as every other DD adoption. `#root`
30
30
  # recurses to the origin document, and for a non-wrapper it is `self`,
31
31
  # so this changes nothing for an ordinary identifier.
32
- urn_number = identifier.number || identifier.root.number
32
+ urn_number = identity(:number)
33
33
  if urn_number
34
34
  number = urn_number.to_s
35
35
  if identifier.iteration && !identifier.iteration.empty?
@@ -38,25 +38,22 @@ module Pubid
38
38
  parts << number
39
39
  end
40
40
 
41
- if identifier.part
42
- part = identifier.part.to_s
43
- parts << "-#{part}"
44
- end
41
+ urn_part = identity(:part)
42
+ parts << "-#{urn_part}" if urn_part
45
43
 
46
- if identifier.subpart
47
- subpart = identifier.subpart.to_s
48
- parts << "-#{subpart}"
49
- end
44
+ urn_subpart = identity(:subpart)
45
+ parts << "-#{urn_subpart}" if urn_subpart
50
46
 
51
47
  if identifier.second_number
52
48
  second = identifier.second_number.to_s
53
49
  parts << "/#{second}"
54
50
  end
55
51
 
56
- if identifier.date&.is_a?(::Pubid::Components::Date) && identifier.date.present?
57
- parts << identifier.date.render(context: URN_CONTEXT)
58
- elsif identifier.year
59
- parts << identifier.year.to_s
52
+ urn_date = identity(:date)
53
+ if urn_date.is_a?(::Pubid::Components::Date) && urn_date.present?
54
+ parts << urn_date.render(context: URN_CONTEXT)
55
+ elsif (urn_year = identity(:year))
56
+ parts << urn_year.to_s
60
57
  end
61
58
 
62
59
  if identifier.month
@@ -87,6 +84,19 @@ module Pubid
87
84
 
88
85
  parts.join(":")
89
86
  end
87
+
88
+ private
89
+
90
+ # An identity attribute of the identifier, or of the document it wraps.
91
+ # `#root` is `self` for a non-wrapper, so an ordinary identifier reads its
92
+ # own value twice and nothing changes.
93
+ def identity(attr)
94
+ own = identifier.public_send(attr)
95
+ return own if own
96
+
97
+ root = identifier.root
98
+ root.equal?(identifier) ? nil : root.public_send(attr)
99
+ end
90
100
  end
91
101
  end
92
102
  end
@@ -195,3 +195,30 @@ module Pubid
195
195
  end
196
196
  end
197
197
  end
198
+
199
+ module Pubid
200
+ module Builder
201
+ # A trailing "(all parts)" is stripped by the shared Grammar and marked
202
+ # on the parse tree (:all_parts). Prepended to Builder::Base so every
203
+ # subclass — including the ones that override #build — wraps the built
204
+ # identifier in its flavor's all-parts class (Pubid::AllParts).
205
+ module AllPartsWrap
206
+ # Parslet tops are a Hash or an Array of Hashes (UN's multi-token tree
207
+ # is an Array); the marker rides every element.
208
+ def all_parts_marked?(data)
209
+ case data
210
+ when Hash then data[:all_parts]
211
+ when Array then data.any? { |e| all_parts_marked?(e) }
212
+ else false
213
+ end
214
+ end
215
+
216
+ def build(data, *args, **kwargs)
217
+ marked = all_parts_marked?(data)
218
+ built = super
219
+ marked ? built.to_all_parts : built
220
+ end
221
+ end
222
+
223
+ end
224
+ end
@@ -36,3 +36,5 @@ module Pubid
36
36
  end
37
37
  end
38
38
  end
39
+
40
+ Pubid::Calconnect::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -18,6 +18,10 @@ module Pubid
18
18
  attribute :number, :string
19
19
  attribute :date, ::Pubid::Components::Date
20
20
 
21
+ # A nil `series` means the document has none: `CC 36010` is not
22
+ # `CC/WD 36010`, which is a working draft of it.
23
+ subset_strict :series
24
+
21
25
  # Polymorphic type map for lutaml::Model key_value (de)serialization,
22
26
  # mapping the concrete class's polymorphic_name to its class name so a
23
27
  # stored hash rebuilds the correct identifier type via from_hash.
@@ -55,3 +55,5 @@ module Pubid
55
55
  end
56
56
  end
57
57
  end
58
+
59
+ Pubid::Ccsds::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -22,11 +22,23 @@ module Pubid
22
22
  # are likewise CCSDS-specific plain strings.
23
23
  attribute :number, :string
24
24
  attribute :part, :string
25
+ # CCSDS never populates `subpart`, but the inherited declaration is a
26
+ # Components::Code, and a parent that disagrees with its leaves is the
27
+ # determinism landmine. Declare it here, like every converted flavor.
28
+ attribute :subpart, :string
25
29
  attribute :edition, :string
26
30
  attribute :type, :string
27
31
  attribute :suffix, :string
28
32
  attribute :language, :string
29
33
 
34
+ # A nil `language` means the original English document, not any
35
+ # translation: `CCSDS 650.0-M-2` is not its French translation.
36
+ subset_strict :language
37
+
38
+ # A nil `suffix` means the current document, not its historical `-S`
39
+ # version: `CCSDS 101.0-B-4` is not `CCSDS 101.0-B-4-S`.
40
+ subset_strict :suffix
41
+
30
42
  # Polymorphic type map for lutaml::Model key_value (de)serialization,
31
43
  # mapping each concrete class's polymorphic_name to its class name.
32
44
  CCSDS_TYPE_MAP = {
@@ -9,10 +9,14 @@ module Pubid
9
9
  attribute :publisher, Components::Publisher, default: -> {
10
10
  Components::Publisher.new(body: "CCSDS")
11
11
  }
12
- attribute :series, Components::Code
13
- attribute :number, Components::Code
14
- attribute :part, Components::Code
15
- attribute :book_color, Components::Code
12
+ # Plain :string, like the live Pubid::Ccsds::Identifier hierarchy, which
13
+ # already declared these as strings. This class is otherwise
14
+ # unreferenced — nothing inherits it and no builder instantiates it — but
15
+ # it is public API, so it is retyped rather than deleted.
16
+ attribute :series, :string
17
+ attribute :number, :string
18
+ attribute :part, :string
19
+ attribute :book_color, :string
16
20
  attribute :edition, Components::Edition
17
21
  attribute :retired, :boolean, default: -> { false }
18
22
  attribute :language, Components::Language
@@ -29,14 +33,14 @@ module Pubid
29
33
  result += " "
30
34
 
31
35
  # Series (optional single letter before number)
32
- result += series.value if series
36
+ result += series.to_s if series
33
37
 
34
38
  # Number with part (using dot notation)
35
- result += number.value
36
- result += ".#{part.value}" if part
39
+ result += number.to_s
40
+ result += ".#{part}" if part
37
41
 
38
42
  # Book color (required) - no space before dash
39
- result += "-#{book_color.value}" if book_color
43
+ result += "-#{book_color}" if book_color
40
44
 
41
45
  # Edition (optional)
42
46
  result += "-#{edition.number}" if edition
@@ -54,11 +58,11 @@ module Pubid
54
58
  return nil unless other.is_a?(SingleIdentifier)
55
59
 
56
60
  # Compare by number first
57
- num_cmp = number.value.to_i <=> other.number.value.to_i
61
+ num_cmp = number.to_i <=> other.number.to_i
58
62
  return num_cmp unless num_cmp.zero?
59
63
 
60
64
  # Then by part
61
- part_cmp = (part ? part.value.to_i : 0) <=> (other.part ? other.part.value.to_i : 0)
65
+ part_cmp = part.to_i <=> other.part.to_i
62
66
  return part_cmp unless part_cmp.zero?
63
67
 
64
68
  # Then by edition
@@ -0,0 +1,59 @@
1
+ # CEN/CENELEC flavor notes
2
+
3
+ CEN/CENELEC supplements, adopted norms, equality and the relaton matching contract.
4
+
5
+ Read these notes before you change `lib/pubid/cen_cenelec/` or `spec/pubid/cen_cenelec/`. The root `CLAUDE.md` keeps the cross-flavor contract that every flavor obeys.
6
+
7
+ - **Six defects blocked the relaton CEN migration, and the parse was not one of them.** The relaton CEN flavor scrapes `standards.cencenelec.eu` and has no index, so it needs only `parse`, `exclude` and `matches?`. 63 of its 67 references rendered correctly, but the other surfaces failed: (1) `ENV 1613:1995` and `ES 59008:2000` rendered as `ENV/ENV …` and `ES/ES …`, because the renderer knew only CWA, HD and CR as publisher-types (BSI inherited it: `DD ENV 206:1992`, `PD ES 59008-1:2000`); (2) every consolidated identifier (`EN 285:2015+A1:2021`) raised on `exclude`, `to_hash` and `matches?`; (3) an adopted norm lost its CEN supplement (`CEN ISO/TS 21003-7:2008/A1:2010` → `CEN ISO/TS 21003-7:2008`, and 8 fixture lines such as `EN ISO 13485:2016/AC:2016`); (4) `to_hash` raised on every supplement and on every adopted norm; (5) **any two amendments were `==`**, so `matches?` on an amendment was always true; (6) `from_hash` raised on every CWA, HD, CR, ES and ENV document. The single constant `Pubid::CenCenelec::PUBLISHER_TYPES` (`CWA HD ES CR ENV`) now drives both the builder's class choice and the renderer. `spec/pubid/cen_cenelec/relaton_blockers_spec.rb` locks all six. (hand-off: `metanorma__pubid__cen-cenelec-relaton-blockers`.)
8
+ - **The supplements left the legacy `Identifiers::Base`.** `Amendment`, `Corrigendum`, `Fragment` and `ConsolidatedIdentifier` descended from `Identifiers::Base`, which declares a `publisher` String collection, `year`, `parts`, `type` and a hand-written `==` that compared only publisher, number, parts and year. A supplement sets none of those, so `EN 13250:2000/A1:2005 == EN 99999:1990/A7:1991` was true. They now descend from the shared `Pubid::CenCenelec::Identifier`, and `base`/`identifiers` are typed to that class (polymorphic), so a base can be a `SingleIdentifier`, an adopted norm or another supplement. The custom `==` is deleted; lutaml's attribute-wise `==` compares the whole object. `Identifiers::Base` itself stays, because it is Category B (`spec/pubid/identifier_hierarchy_spec.rb`), but no concrete type descends from it now. The top-level `SupplementIdentifier` (a subclass of it that no builder created) and its `UrnGenerator#generate_supplement_urn` branch were deleted: both still read the old `amendment_number`/`amendment_year` names. **The `:human` format registry moved to `Pubid::CenCenelec::Identifier`**: it was registered on `SingleIdentifier` and `Identifiers::Base` only, so a re-parented supplement rendered as `""`.
9
+ - **The deleted readers were the landmine, not a convenience.** `ConsolidatedIdentifier` defined `publisher`, `number`, `year`, `parts` and `type` readers that delegated to the first member, and `AdoptedEuropeanNorm` defined `number`, `year`, `date`, `parts` and `part` readers that delegated to the ISO/IEC document. Each name is a lutaml attribute, and `#exclude` and the serializer read every attribute through its reader: the consolidated one reached `EuropeanNorm#parts`, which does not exist, and the adopted one wrote the ISO values a second time at the top level of the hash. Read the document through `#base_document`, `#drop_supplements` or `#root` instead. The supplements expose `supplement_type`, and their ordinal and year as plain `number` / `year` — the same names BSI's supplements use (the `supplement_number` / `supplement_year` aliases were deleted; see the matching-primitives bullet in CLAUDE.md). **Consequence:** a supplement's own `publisher` is now nil; use `base_document.publisher`.
10
+ - **BSI read through the deleted readers.** `Bsi::Identifiers::AdoptedEuropeanNorm#number` (and `year`, `date`, `parts`, `part`) delegated to its adopted document, and for `BS EN ISO 11819-1:2023` that document is a CEN adoption, whose number came from the deleted reader. A private `delegate_target` now reads through the CEN adoption to the ISO standard. `subpart` keeps reading the CEN object, as before. A replay of all 1,501 BSI pass fixtures against `main` gave byte-identical `to_s`, `to_urn`, `to_mr_string`, class and `root.number`, except 5 repaired doubled publishers and 2 fragments (`BS EN IEC 61535/AMD1/FRAG1`) whose `root.number` changed from `"1"` to `"61535"`.
11
+ - **The year rule is BSI's, plus a CEN key (decided with the user).** `exclude(:year)` removes only the base document's year: `EN 13250:2000/A1:2005` → `EN 13250/A1:2005`, `EN 285:2015+A1:2021` → `EN 285+A1:2021`. The key `:supplement_year` removes the supplement's own date instead, and the month goes with the year, so a `/AC:2016-11` does not keep a month with no year. ISO and IEC behave differently: there each layer keeps its year in `date`, so `exclude(:year)` removes every year. The CEN rule lets relaton keep the base year strict and make the amendment year optional. **The mechanism used to be a per-flavor `#exclude` override; it is now the shared base class** (`metanorma__pubid__exclude-recursion-depth` hand-off, `lib/pubid/bsi/CLAUDE.md`'s "The `exclude` recursion boundary"). `::Pubid::Identifier#exclude` itself now protects the attributes a class names in `self.supplement_date_attributes` (`%i[year]` on `Amendment`, `%i[year month]` on `Corrigendum`, empty on a document — and empty on every flavor that doesn't override it, so this is a no-op everywhere else) from a bare `:date`/`:year`, or clears them instead when `:supplement_year` is passed. `Pubid::CenCenelec::Identifier#exclude` was deleted; the base #exclude still passes every key on to nested identifiers, so the members of a consolidated identifier follow the rule. **relaton ignore mapping**: a query with no part adds `%i[part subpart]`, a query with no base year adds `:year`, a query with no amendment year adds `:supplement_year`; `exclude(:year, :supplement_year)` gives the fully undated `EN 285+A1`.
12
+ - **The builder has one supplement path.** `Builder#build` builds the base document first (`build_base_document`: adopted norm, ENV adoption, implicit IEC adoption or plain identifier), then wraps it: a `/` supplement makes a standalone `Amendment`/`Corrigendum`, a `+` supplement makes a `ConsolidatedIdentifier`. The adopted branches used to return early, which is how they lost the supplement. The implicit IEC adoption (`EN 60xxx` → `EN IEC 60xxx`) now runs only when there is no supplement, because it rebuilds the identifier from the bare number and dropped a `+A1` the same way. An empty captured number or month is stored as nil (`present_string`), because `to_hash` drops `""` and `from_hash` then gives nil. A mixed `/` and `+` form keeps only the first `/` supplement; that gap is older than this change and is not fixed.
13
+ - **The adopted norm holds a `Components::Publisher`.** The builder passed `publisher: ["EN"]`, an Array, into a `Components::Publisher` attribute, and the renderer joined it; `to_hash` raised and the URN and MR printed the Array's `inspect`. The second publisher of a `CEN/CLC` adoption goes in `copublishers`, set only when present (an empty collection becomes nil after a round trip). `lib/pubid/bsi/builder.rb` constructs this class too and now takes the `EN` default. An ENV adoption (`ENV ISO 11079:1999`) now carries the publisher `ENV`, not the `EN` default.
14
+ - **A CEN supplement's year is a string.** `year` is `"2005"`, not `2005`, since it became a `:string` attribute (see the wire-shape bullet below). BSI's supplements were later aligned to the same type, so a caller comparing years across the two flavors compares like with like.
15
+ - **A publisher-type does not write its publisher (decided with the user).** `SingleIdentifier.default_publisher` is the type token for the five `PUBLISHER_TYPES` classes (`CenWorkshopAgreement` CWA, `HarmonizationDocument`/`CenelecHarmonizationDocument` HD, `CenReport` CR, `EuropeanSpecification` ES, `EuropeanPrestandard` ENV) and EN for every other class. `to_hash` leaves out a value equal to its default, so `CWA 14050-21:2000` is `{"_type" => "…cen-workshop-agreement", "number" => "14050", "part" => "21", "year" => "2000"}`: `_type` already names the publisher. An adopted norm is a European Norm, so `HD IEC 60364-8-81` and `CR ISO 13434:1999` still write `"publisher" => "HD"`/`"CR"`, and a TR, TS or Guide still writes `CEN` or `CLC`, which is real information. After this, every key left in a CEN hash carries information.
16
+ - **A draft stage serializes as one code (decided with the user).** `prEN 1234:2020` wrote three nested components, `type`, `stage` and `typed_stage`, each a copy of one entry of the stage registry (with its name and harmonized stages). It now writes `"stage" => "pren"` (`fpren` for FprEN), as ISO (`"dis"`) and IEC (`"cd"`) do. `Pubid::CenCenelec::Identifier.compact_hash` writes the short form only when the three components are exactly what the registry entry gives, so the round trip stays exact; `inflate_scalar_components` rebuilds them with `CenCenelec.locate_stage_by_code`, and the nested form still reads. `compact_hash` is a new class hook on `Pubid::Identifier` (no-op by default) that the canonicalizer calls for every identifier, nested ones included, so the stage of a `base` is short too. **The slug now names the stage**: `prEN 1234:2020` and `FprEN 1234:2020` both slugged `en.en.1234.2020` (the shared `mr_type` gives the type code `en`); a CEN `mr_type` override gives `en.pren.1234.2020` and `en.fpren.1234.2020`. **An adoption keeps its draft stage**: `prEN ISO 1234:2020` rendered as `EN ISO 1234:2020`, because `build_adopted_identifier` dropped the `type_with_stage` capture. It now stores the three stage components, the renderer prints `prEN` in place of the publisher, and the URN appends the stage as the base URN does (`urn:cen:en:iso:1234:2020:stage.proposal`).
17
+ - **The adopted document is `adopted`, in CEN and BSI together (decided with the user).** CEN `AdoptedEuropeanNorm`/`EuropeanPrestandard` (and the unused legacy `Identifiers::Base`) and BSI `AdoptedEuropeanNorm`/`AdoptedInternationalStandard` renamed `adopted_identifier` to `adopted`, the name GOST `IdenticalAdoption` already used; one rename across both flavors keeps a BSI-over-CEN chain on one name (`BS EN ISO 11819-1:2023` → `bsi.adopted.adopted`). The hash key follows (`"adopted" => {…}`). Nothing outside pubid read the old name and no published index stored it, so no alias is kept. The collections GOST `Harmonized#adopted_identifiers` and IEEE `AdoptedStandard#adopted_identifiers` keep their name: `relaton-data-ieee` stores 64 rows with that key.
18
+ - **The `type` default of a typed leaf is a component.** Nine leaves declared `default: -> { self.class.type[:key] }`, a bare Symbol such as `:cwa`, in a `Components::Type` attribute. `to_hash` dropped it as a default, and `from_hash` raised when it cast the Symbol. `SingleIdentifier.default_type` returns `Components::Type.new(abbr: type[:short])`, the same value the parser sets for `CEN/TR`. `to_hash` still omits it, and now also omits the `type` of TR, TS and Guide, which equals the default. No CEN index exists, so no stored row changes.
19
+ - **The wire shape is flat (decided with the user).** A supplement uses the same keys as a document: the inherited `number` holds the amendment, corrigendum or fragment number, and `year`/`month` are `:string` attributes (they were `amendment_number`, `amendment_year`, `corrigendum_*` and `fragment_number`, with Integer years). `EN 13250:2000/A1:2005` → `{"_type" => "…amendment", "base" => {…, "number" => "13250", "year" => "2000"}, "number" => "1", "year" => "2005"}`; `…/AC:2016-11` adds `"month" => "11"`. The `publisher` is a bare string and `copublishers` a list of strings (`"publisher" => "CEN", "copublishers" => ["CLC"]`). **This is CEN-only on purpose.** `Pubid::Identifier.flat_scalar_components`/`flat_scalar_fields` are now class methods over the shared tables, and `Pubid::CenCenelec::Identifier` merges `publisher`/`copublishers` into them; the shared canonicalizer gained collection support for this. A shared entry would also flatten ANSI, BSI, IDF and some IEEE hashes, and `relaton-data-ieee` is a published index. `from_hash` still reads the nested `{"body" => …}` form. A base document with a month still writes `"date" => {"year", "month"}`, because only a single-field component flattens. **A consolidated identifier writes its base once**: the members after the first carry no `base` (the builder used to copy it into each), so `EN 285:2015+A1:2021` is `{"identifiers" => [{EN 285:2015}, {"_type" => "…amendment", "number" => "1", "year" => "2021"}]}`. A member's `base_document` is therefore itself; walk the consolidated identifier instead.
20
+ - **URN and MR of the wrappers are new.** Every amendment had the URN `urn:cen:en`, and the 165 distinct fixture identifiers had only 130 distinct URNs. Now: `EN 13250:2000/A1:2005` → `urn:cen:en:13250:2000:amd:1:2005` / `en.13250.2000_amd.1.2005`; `EN 285:2015+A1:2021` → `urn:cen:en:285:2015:plus:amd:1:2021` / `en.285.2015_plus-amd.1.2021` (the `plus` marker, as in IEC, keeps it apart from the standalone `/A1:2021`); `EN 60038 AMD1 FRAG2` → `…:amd:1:frag:2`; an adoption uses its publisher and the adopted document's MR string (`CEN ISO/TS 21003-7:2019` → `urn:cen:cen:iso:ts:21003-7:2019` / `cen.iso.ts.21003-7.2019`). An unnumbered corrigendum has no number segment (`EN 13254:2000/AC:2003` → `…:cor:2003`). Over the fixture corpus, 187 distinct identifiers now give 187 distinct slugs and URNs.
21
+ - **The fixture spec was dead.** `spec/pubid/cen_cenelec/fixtures_spec.rb` globbed `../../../fixtures/…` and ran 0 examples. It now has the `spec/pubid/csa/fixtures_spec.rb` shape (tripwire, zero failures per file, conservation against `full/*.txt`). `full/cen.txt` now carries the relaton corpus. The three grammar gaps `prEN 13306 rev`, `prEN 285 rev` and `EN 285:2015/FprA1` are recorded in `fail/`; relaton skips them, because they are catalogue hits and not caller references. The regeneration also rewrote the `fail/` class column to `Pubid::Errors::ParseError`.
22
+ - **`Pubid::TypeResolver` could not resolve a CEN `_type`, which was older than the blocker fixes.** The polymorphic name is `pubid:cencenelec:…` (the module name), but the registry knows the flavor only as `cen_cenelec` and `cen`, so `Pubid::Registry.get("cencenelec")` was nil. `Pubid::CenCenelec::Identifier.from_hash` worked, because the flavor's own `polymorphic_type_map` resolves the type first, but `Pubid::Identifier.from_hash` on a CEN hash and a CEN identifier nested in another flavor's `::Pubid::Identifier` attribute (BSI `AdoptedEuropeanNorm#adopted`) deserialized as the abstract root. The flat publisher made this visible: the root cannot cast `"CEN"` into a `Components::Publisher`, so the wrong result became a raise. `TypeResolver#flavor_by_module_segment` now falls back to the registered module whose constant gives the segment; CEN is the only flavor with the mismatch (3GPP forces `pubid:3gpp:`). BSI round trips over its pass fixtures went from 815 to 865, and `KNOWN_ROUND_TRIP_FAILURES` for BSI from 647 to 597.
23
+ - **Still open, pinned nowhere as correct:** `HD IEC 60364-8-81 ED1` drops `ED1`; `EN 60038 AMD1 FRAG2` renders as `EN 60038/A1 FRAG2`; `root.number` of an adopted norm is the ISO `Components::Code` until ISO is retyped (tranche 3 of the `number` retype).
24
+
25
+ ## CEN is the remaining `adopted` camp
26
+
27
+ BSI moved its adoption wrappers to the uniform `base` accessor (`lib/pubid/bsi/CLAUDE.md`); CEN did not, and this records the shape so the next branch does not have to re-survey it.
28
+
29
+ Three classes still name the parent slot `adopted`:
30
+
31
+ - `Identifiers::AdoptedEuropeanNorm` and `Identifiers::EuropeanPrestandard` — both carry a hand-written `#root` walking `adopted`, and an `mr_number_with_part` that puts `adopted.to_mr_string` where the number goes. So the index key and the slug are correct; only the accessor's name diverges, and `id.base` reads nil on both.
32
+ - `Identifiers::Base`, the legacy second root — its `adopted` is typed to CEN's own `Base`, **not** `::Pubid::Identifier`, so it cannot hold a cross-flavor adoption at all, and it has no `#root`. It is rarely instantiated (`SingleIdentifier` is the live branch), which is why nothing has tripped over the narrow type.
33
+
34
+ CEN is a harder rename than BSI was for one reason: `Pubid::Bsi::Builder` constructs a `CenCenelec::Identifiers::AdoptedEuropeanNorm.new(adopted: …)`, and `Bsi::Identifiers::AdoptedEuropeanNorm` used to read `target.adopted` through one CEN layer. The first of those two call sites is still there.
35
+
36
+ Note the asymmetry a rename must preserve: a BSI `AdoptedEuropeanNorm` now exposes `base`, and the CEN object it holds still exposes `adopted`, so a two-layer walk reads `id.base.adopted`. `#root` recurses through both regardless, which is why it — and not a delegation — is the right thing for a consumer to call.
37
+
38
+ ## Subset match: strict attributes
39
+
40
+ Read `docs/SUBSET_MATCH.md` first. `===` reads a nil part of the reference as
41
+ a wildcard, which is wrong for the attributes below: the flavor models a nil
42
+ value as "this document has none". They are declared with `subset_strict`, so
43
+ `===` compares them exactly and a stated collection is not a prefix. A caller
44
+ that does want every part of a document sets `all_parts` on the reference, or
45
+ keeps `#matches?(other, ignore:)`.
46
+
47
+ - **`type`, `stage` and `typed_stage` are strict**
48
+ (`lib/pubid/cen_cenelec/identifier.rb`). A published European Norm holds
49
+ all three at nil, so a nil one means "published", not "any stage":
50
+ `EN 1325 === prEN 1325` and `EN 1991 === ENV 1991-2-2` are both false,
51
+ while `EN 1325 === EN 1325:2001` still holds. The three move together
52
+ because they repeat one entry of the stage registry — the same fact that
53
+ lets `compact_hash` write them as a single `"stage" => "pren"` and
54
+ `inflate_scalar_components` read them back, which is why the strict
55
+ comparison survives a `from_hash` round trip.
56
+ - The hand-off `subset-match-nil-means-none` called this a CEN-grammar
57
+ decision, on the reading that a published norm should carry a published
58
+ stage by default. It is fixed at the matching layer instead: no grammar
59
+ change, no index re-crawl.
@@ -95,10 +95,13 @@ module Pubid
95
95
  base = locate_identifier_klass(base_data).new
96
96
  assign_attributes(base, base_data)
97
97
 
98
- # Build Amendment identifier wrapping the base
98
+ # Build Amendment identifier wrapping the base. The compact
99
+ # fragment spelling can carry the amendment's own year
100
+ # (EN 60038/A1:2009 FRAG2); the AMD-keyword spelling cannot.
99
101
  amendment = Identifiers::Amendment.new(
100
102
  base: base,
101
103
  number: data[:amendment_number].to_s,
104
+ year: data[:amendment_year]&.to_s,
102
105
  )
103
106
 
104
107
  # Build Fragment wrapping the amendment
@@ -385,3 +388,5 @@ module Pubid
385
388
  end
386
389
  end
387
390
  end
391
+
392
+ Pubid::CenCenelec::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -20,6 +20,12 @@ module Pubid
20
20
  attribute :part, :string
21
21
  attribute :subpart, :string
22
22
 
23
+ # A published European Norm holds no type, stage or typed stage, so a
24
+ # nil one means "published", not "any stage": `EN 1325` is not
25
+ # `prEN 1325`, and `EN 1991` is not `ENV 1991`. The three components
26
+ # repeat one entry of the stage registry, so they move together.
27
+ subset_strict :type, :stage, :typed_stage
28
+
23
29
  # The publisher serializes as a bare string ("publisher" => "CEN"), and
24
30
  # the copublishers as a list of strings. The shared table does not do
25
31
  # this for every flavor, because IEEE has a published index whose rows
@@ -81,33 +87,10 @@ module Pubid
81
87
  stage.code.to_s.downcase
82
88
  end
83
89
 
84
- # The attributes that hold a supplement's own date: `year`, and `month`
85
- # on a corrigendum. Empty for a document. See #exclude.
86
- def self.supplement_date_attributes
87
- []
88
- end
89
-
90
- # The CEN year rule, as in BSI: `:year` removes only the base
91
- # document's year ("EN 13250:2000/A1:2005" -> "EN 13250/A1:2005"), and
92
- # the CEN key `:supplement_year` removes the supplement's own date
93
- # ("EN 13250:2000/A1"). A supplement declares its date as a `year`
94
- # attribute, which the base #exclude resets for `:year`, so the value is
95
- # put back here. The month goes with the year, or a "/AC:2016-11" would
96
- # keep a month with no year. The base #exclude passes every key on to
97
- # the nested identifiers, so the members of a consolidated identifier
98
- # follow the same rule.
99
- def exclude(*args)
100
- result = super
101
- attrs = self.class.supplement_date_attributes
102
- # `exclude(:amendment)` returns the base document, not a copy of self.
103
- return result if attrs.empty? || !result.instance_of?(self.class)
104
-
105
- drop = args.include?(:supplement_year)
106
- attrs.each do |attr|
107
- result.public_send(:"#{attr}=", drop ? nil : public_send(attr))
108
- end
109
- result
110
- end
90
+ # The CEN year rule (`:year` vs `:supplement_year`) is the shared
91
+ # `::Pubid::Identifier#exclude` mechanism driven by `Amendment`'s and
92
+ # `Corrigendum`'s `supplement_date_attributes` — no override needed
93
+ # here. Detail: `lib/pubid/cen_cenelec/CLAUDE.md`'s year-rule bullet.
111
94
 
112
95
  def self.parse(identifier)
113
96
  unless identifier.is_a?(String)
@@ -36,20 +36,13 @@ module Pubid
36
36
  base || self
37
37
  end
38
38
 
39
- # Uniform supplement interface (shared with Corrigendum and with BSI)
40
- # so callers need not special-case the class.
39
+ # Names the supplement class, so callers need not special-case it. The
40
+ # ordinal and the year need no such method: Amendment and Corrigendum —
41
+ # in CEN and in BSI — all declare them as `number` and `year`.
41
42
  def supplement_type
42
43
  :amendment
43
44
  end
44
45
 
45
- def supplement_number
46
- number
47
- end
48
-
49
- def supplement_year
50
- year
51
- end
52
-
53
46
  def mr_supplement_suffix
54
47
  mr_join_segments("amd", number, year)
55
48
  end
@@ -29,20 +29,13 @@ module Pubid
29
29
  base || self
30
30
  end
31
31
 
32
- # Uniform supplement interface (shared with Amendment and with BSI)
33
- # so callers need not special-case the class.
32
+ # Names the supplement class, so callers need not special-case it. The
33
+ # ordinal and the year need no such method: Amendment and Corrigendum —
34
+ # in CEN and in BSI — all declare them as `number` and `year`.
34
35
  def supplement_type
35
36
  :corrigendum
36
37
  end
37
38
 
38
- def supplement_number
39
- number
40
- end
41
-
42
- def supplement_year
43
- year
44
- end
45
-
46
39
  # The date part of the supplement: "2016-11", "2003", or nil.
47
40
  def supplement_date
48
41
  return nil unless year
@@ -91,12 +91,20 @@ module Pubid
91
91
  # Edition (ED2, ED3, etc.)
92
92
  rule(:edition) { space >> str("ED") >> digits.as(:edition) }
93
93
 
94
- # Fragment identifier (EN 60038 AMD1 FRAG2)
94
+ # Fragment identifier (EN 60038 AMD1 FRAG2, EN 60038/A1 FRAG2)
95
95
  rule(:fragment_identifier) do
96
- (stage_prefix | publisher) >>
96
+ ((stage_prefix | publisher) >>
97
97
  space >> number >> parts >>
98
98
  space >> str("AMD") >> digits.as(:amendment_number) >>
99
- space >> str("FRAG") >> digits.as(:fragment_number)
99
+ space >> str("FRAG") >> digits.as(:fragment_number)) |
100
+ # Compact supplement notation: the base may carry a year, the
101
+ # amendment uses the /A1 or +A1 join (with an optional year of
102
+ # its own) instead of the spelled-out "AMD1" keyword.
103
+ ((stage_prefix | publisher) >>
104
+ space >> number >> parts >> year.maybe >>
105
+ (plus | slash) >> str("A") >> digits.as(:amendment_number) >>
106
+ (colon >> digit.repeat(4, 4).as(:amendment_year)).maybe >>
107
+ space >> str("FRAG") >> digits.as(:fragment_number))
100
108
  end
101
109
 
102
110
  # Adopted standard as opaque string - must start with org name
@@ -0,0 +1,58 @@
1
+ # CIE flavor notes
2
+
3
+ CIE builder attribute assembly.
4
+
5
+ Read this before you change `lib/pubid/cie/` or `spec/pubid/cie/`. The root
6
+ `CLAUDE.md` keeps the cross-flavor contract that every flavor obeys.
7
+
8
+ - **The builder offered every CIE type the same attribute hash, and the types
9
+ do not share one attribute set.** `Builder#extract_attributes` unconditionally
10
+ set `attributes[:s_prefix]`, but only `Standard`, `DualPublished` and
11
+ `Identical` declare that attribute — `Conference`, `Bundle`, `Proceedings`,
12
+ `Supplement` and `TutorialBundle` do not. lutaml reads only declared
13
+ attribute names out of a constructor hash and reports nothing about the rest,
14
+ so the surplus key was discarded in silence and nothing failed.
15
+
16
+ It stopped being silent when `::Pubid::Identifier` began **refusing** an
17
+ unknown constructor key (the cross-flavor contract in the root `CLAUDE.md`):
18
+ **94 CIE identifiers immediately stopped parsing** with
19
+ `ArgumentError: unknown attribute for Pubid::Cie::Identifiers::Conference:
20
+ s_prefix` — every `CIE x005-1992`-style conference paper and every
21
+ comma-list bundle. The fix is one line in `lib/pubid/cie/builder.rb`:
22
+ select the assembled attributes down to those the chosen
23
+ `identifier_class` actually declares, immediately before `new`.
24
+
25
+ **The lesson is about where to filter, not about `s_prefix`.** A builder that
26
+ assembles one hash for a family of types must narrow it per type; the
27
+ alternative — letting the constructor drop what it does not recognise — is
28
+ exactly the silence that hid this. `Pubid::Builder::Base#assign_attributes`
29
+ already documents "silently skips unknown attributes" for the same reason,
30
+ but CIE's builder constructs directly and so bypassed it.
31
+
32
+ Locked by `spec/pubid/attribute_construction_spec.rb` ("builders offer only
33
+ declared attributes"), which parses a conference paper, a bundle and an
34
+ `s_prefix`-bearing standard, and asserts `Conference` still does **not**
35
+ declare `s_prefix` — so re-adding the attribute to silence the spec would
36
+ turn it red rather than green.
37
+
38
+ - **All 10 CIE types rendered plain under `to_s(annotated: true)`.** Every
39
+ one composes its own string instead of going through `render`, so none
40
+ reached the shared annotation hook; each now calls
41
+ `annotate_plain_render` on the way out.
42
+
43
+ **`Standard` needed more than a wrap.** Its body has three exits — the
44
+ `slash_colon` language form and the legacy bare slash-year form both
45
+ `return` early — and `annotate_plain_render` needs one. The composition
46
+ moved verbatim into a private `render_plain`; `to_s` is two lines. The
47
+ trap is the `private` keyword: written as a **section** it privatises
48
+ every method below it, which swallowed `mr_type` — a public method the
49
+ MR renderer calls. The section goes at the **end** of the class.
50
+ `Bundle` keeps its `return "" unless ids&.any?` guard and wraps only the
51
+ composed value.
52
+
53
+ **`Supplement` was a different bug with the same symptom.** It is a
54
+ wrapper: `number` is the supplement ordinal (`"1"`, which does not stand
55
+ alone in `CIE 121-SP1:2009`) and the document's identity — number `121`,
56
+ year `2009` — lives on `base`. Wrapping its `to_s` changed nothing,
57
+ because the annotator only read the wrapper's own attributes. It is fixed
58
+ by `Annotator#emit_tokens` walking `base`, not by anything in CIE.
@@ -522,3 +522,5 @@ module Pubid
522
522
  end
523
523
  end
524
524
  end
525
+
526
+ Pubid::Cie::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -12,6 +12,8 @@ module Pubid
12
12
  # 3. Parenthetical: (DE), (ES), (en)
13
13
  # 4. Translation year: (RU-2021)
14
14
  class Language < Lutaml::Model::Serializable
15
+ include ::Pubid::SubsetMatch
16
+
15
17
  attribute :code, :string # "E", "DE", "RU", "en"
16
18
  attribute :format, :string # "slash", "slash_colon", "paren", "paren_year"
17
19
  attribute :translation_year, :string # "2021" in "(RU-2021)"
@@ -28,6 +28,8 @@ module Pubid
28
28
  # the prefix and any suffix notation (translation, reaffirmation,
29
29
  # commentary) by calling adoption.render(context:).
30
30
  class Adoption < Lutaml::Model::Serializable
31
+ include ::Pubid::SubsetMatch
32
+
31
33
  attribute :base, ::Pubid::Identifier, polymorphic: true
32
34
  attribute :adopter_publisher, ::Pubid::Components::Publisher
33
35
  attribute :edition, :string
@@ -10,6 +10,8 @@ module Pubid
10
10
  # composition rules — flavor-specific separators and orderings live in
11
11
  # the subclass #to_s / #render override.
12
12
  class Code < Lutaml::Model::Serializable
13
+ include ::Pubid::SubsetMatch
14
+
13
15
  attribute :value, :string
14
16
  attribute :prefix, :string
15
17
  attribute :part, :string
@@ -12,6 +12,8 @@ module Pubid
12
12
  # URN spec); flavors that drop the slot when undated gate that decision
13
13
  # at the URN-generator level, not here.
14
14
  class Date < Lutaml::Model::Serializable
15
+ include ::Pubid::SubsetMatch
16
+
15
17
  attribute :year, :string
16
18
  attribute :month, :string
17
19
  attribute :day, :string
@@ -51,12 +53,6 @@ module Pubid
51
53
  result
52
54
  end
53
55
 
54
- private
55
-
56
- def pad2(value)
57
- value.to_s.rjust(2, "0")
58
- end
59
-
60
56
  def hash
61
57
  @hash ||= [year, month, day, undated].compact.map(&:hash).hash
62
58
  end
@@ -67,6 +63,12 @@ module Pubid
67
63
  year == other.year && month == other.month && day == other.day &&
68
64
  undated == other.undated
69
65
  end
66
+
67
+ private
68
+
69
+ def pad2(value)
70
+ value.to_s.rjust(2, "0")
71
+ end
70
72
  end
71
73
  end
72
74
  end
@@ -7,6 +7,8 @@ module Pubid
7
7
  # Human render: "ED#{number}".
8
8
  # URN render: "ed-#{number}" per RFC 5141-bis.
9
9
  class Edition < Lutaml::Model::Serializable
10
+ include ::Pubid::SubsetMatch
11
+
10
12
  attribute :year, :string
11
13
  attribute :number, Lutaml::Model::Type::Value
12
14
  attribute :original_text, :string
@@ -8,6 +8,8 @@ module Pubid
8
8
  # because URN, MR, and human formats position iteration differently
9
9
  # relative to the stage abbreviation.
10
10
  class Iteration < Lutaml::Model::Serializable
11
+ include ::Pubid::SubsetMatch
12
+
11
13
  attribute :number, :string
12
14
 
13
15
  def to_s
@@ -7,6 +7,8 @@ module Pubid
7
7
  # Human render: language code, optionally single-char.
8
8
  # URN render: lowercase ISO 639-1 code (RFC 5141-bis).
9
9
  class Language < Lutaml::Model::Serializable
10
+ include ::Pubid::SubsetMatch
11
+
10
12
  CHAR_MAP = {
11
13
  "R" => "ru",
12
14
  "F" => "fr",
@@ -7,6 +7,8 @@ module Pubid
7
7
  # Renders the human-readable phrase "(all parts)" and a URN
8
8
  # suffix that completes the locality segment.
9
9
  class Locality < Lutaml::Model::Serializable
10
+ include ::Pubid::SubsetMatch
11
+
10
12
  attribute :value, :string
11
13
 
12
14
  def render(context: nil)
@@ -9,6 +9,8 @@ module Pubid
9
9
  # Human render: the publisher body (e.g. "ISO").
10
10
  # URN render: lowercase body (e.g. "iso") per RFC 5141-bis.
11
11
  class Publisher < Lutaml::Model::Serializable
12
+ include ::Pubid::SubsetMatch
13
+
12
14
  attribute :body, :string
13
15
 
14
16
  def to_s
@@ -21,6 +21,8 @@ module Pubid
21
21
  # - approved_amendments_flag: when true and no intermediate_amendments
22
22
  # are listed, renders "... and its approved amendments".
23
23
  class Relationship < Lutaml::Model::Serializable
24
+ include ::Pubid::SubsetMatch
25
+
24
26
  REVISION_OF = "revision_of"
25
27
  AMENDMENT_TO = "amendment_to"
26
28
  CORRIGENDUM_TO = "corrigendum_to"