pubid 2.0.0.pre.alpha.12 → 2.0.0.pre.alpha.14

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 (285) 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/data/parg/tables/bipm_groups.yaml +14 -0
  6. data/data/parg/tables/bipm_type_codes.yaml +5 -0
  7. data/data/parg/tables/bipm_type_names_en.yaml +6 -0
  8. data/data/parg/tables/bipm_type_names_fr.yaml +6 -0
  9. data/data/parg/tables/directives_supplements_typed_stages.yaml +3 -0
  10. data/data/parg/tables/directives_typed_stages.yaml +5 -0
  11. data/data/parg/tables/idf_typed_stages.yaml +27 -0
  12. data/data/parg/tables/idf_typed_stages_supplements.yaml +2 -0
  13. data/data/parg/tables/iec_typed_stages.yaml +130 -0
  14. data/data/parg/tables/iso_publishers.yaml +4 -0
  15. data/data/parg/tables/organizations.yaml +12 -0
  16. data/data/parg/tables/tc_types.yaml +42 -0
  17. data/data/parg/tables/typed_stages.yaml +114 -0
  18. data/data/parg/tables/typed_stages_supplements.yaml +64 -0
  19. data/data/parg/tables/wg_types.yaml +21 -0
  20. data/lib/pubid/adobe/builder.rb +2 -0
  21. data/lib/pubid/adobe/identifier.rb +11 -1
  22. data/lib/pubid/all_parts.rb +201 -0
  23. data/lib/pubid/all_parts_identifier.rb +19 -0
  24. data/lib/pubid/amca/CLAUDE.md +47 -0
  25. data/lib/pubid/amca/builder.rb +3 -5
  26. data/lib/pubid/amca/identifiers/base.rb +11 -1
  27. data/lib/pubid/amca/identifiers/publication.rb +13 -0
  28. data/lib/pubid/amca/parser.rb +2 -1
  29. data/lib/pubid/amca/renderer.rb +22 -33
  30. data/lib/pubid/amca/urn_generator.rb +21 -2
  31. data/lib/pubid/amca/urn_parser.rb +36 -10
  32. data/lib/pubid/ansi/builder.rb +6 -0
  33. data/lib/pubid/ansi/identifier.rb +1 -1
  34. data/lib/pubid/api/CLAUDE.md +23 -0
  35. data/lib/pubid/api/builder.rb +2 -0
  36. data/lib/pubid/api/identifier.rb +1 -1
  37. data/lib/pubid/api/parser.rb +8 -4
  38. data/lib/pubid/ashrae/CLAUDE.md +13 -0
  39. data/lib/pubid/ashrae/builder.rb +58 -14
  40. data/lib/pubid/ashrae/identifiers/base.rb +10 -1
  41. data/lib/pubid/ashrae/identifiers/errata.rb +14 -2
  42. data/lib/pubid/ashrae/identifiers/interpretation.rb +2 -10
  43. data/lib/pubid/ashrae/parser.rb +80 -39
  44. data/lib/pubid/ashrae/renderer.rb +32 -1
  45. data/lib/pubid/ashrae/urn_generator.rb +32 -9
  46. data/lib/pubid/asme/CLAUDE.md +25 -0
  47. data/lib/pubid/asme/builder.rb +16 -9
  48. data/lib/pubid/asme/components/code.rb +2 -0
  49. data/lib/pubid/asme/identifier.rb +1 -1
  50. data/lib/pubid/asme/identifiers/standard.rb +6 -1
  51. data/lib/pubid/asme/parser.rb +41 -14
  52. data/lib/pubid/astm/CLAUDE.md +9 -0
  53. data/lib/pubid/astm/builder.rb +2 -0
  54. data/lib/pubid/astm/components/code.rb +2 -0
  55. data/lib/pubid/astm/identifier.rb +1 -1
  56. data/lib/pubid/astm/parser.rb +4 -1
  57. data/lib/pubid/bipm/CLAUDE.md +11 -0
  58. data/lib/pubid/bipm/builder.rb +2 -0
  59. data/lib/pubid/bipm/identifier.rb +1 -1
  60. data/lib/pubid/bsi/CLAUDE.md +93 -0
  61. data/lib/pubid/bsi/builder.rb +13 -11
  62. data/lib/pubid/bsi/identifiers/addendum_document.rb +2 -0
  63. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +6 -54
  64. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +5 -22
  65. data/lib/pubid/bsi/identifiers/amendment.rb +36 -12
  66. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -0
  67. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +23 -26
  68. data/lib/pubid/bsi/identifiers/corrigendum.rb +29 -12
  69. data/lib/pubid/bsi/identifiers/expert_commentary.rb +6 -7
  70. data/lib/pubid/bsi/identifiers/national_annex.rb +18 -20
  71. data/lib/pubid/bsi/identifiers/root_identity.rb +31 -0
  72. data/lib/pubid/bsi/identifiers/set.rb +2 -0
  73. data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -0
  74. data/lib/pubid/bsi/identifiers.rb +1 -0
  75. data/lib/pubid/bsi/parser.rb +8 -8
  76. data/lib/pubid/bsi/renderer.rb +20 -20
  77. data/lib/pubid/bsi/single_identifier.rb +1 -3
  78. data/lib/pubid/bsi/urn_generator.rb +28 -18
  79. data/lib/pubid/builder/base.rb +27 -0
  80. data/lib/pubid/calconnect/builder.rb +2 -0
  81. data/lib/pubid/calconnect/identifier.rb +5 -1
  82. data/lib/pubid/ccsds/builder.rb +2 -0
  83. data/lib/pubid/ccsds/identifier.rb +9 -1
  84. data/lib/pubid/cen_cenelec/CLAUDE.md +59 -0
  85. data/lib/pubid/cen_cenelec/builder.rb +6 -1
  86. data/lib/pubid/cen_cenelec/identifier.rb +12 -28
  87. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +3 -10
  88. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +3 -10
  89. data/lib/pubid/cen_cenelec/parser.rb +20 -5
  90. data/lib/pubid/cie/CLAUDE.md +58 -0
  91. data/lib/pubid/cie/builder.rb +2 -0
  92. data/lib/pubid/cie/components/language.rb +2 -0
  93. data/lib/pubid/cie/identifier.rb +1 -1
  94. data/lib/pubid/cie/parser.rb +9 -2
  95. data/lib/pubid/components/adoption.rb +2 -0
  96. data/lib/pubid/components/code.rb +2 -0
  97. data/lib/pubid/components/date.rb +8 -6
  98. data/lib/pubid/components/edition.rb +2 -0
  99. data/lib/pubid/components/iteration.rb +2 -0
  100. data/lib/pubid/components/language.rb +2 -0
  101. data/lib/pubid/components/locality.rb +2 -0
  102. data/lib/pubid/components/publisher.rb +2 -0
  103. data/lib/pubid/components/relationship.rb +2 -0
  104. data/lib/pubid/components/stage.rb +2 -0
  105. data/lib/pubid/components/supplement.rb +2 -0
  106. data/lib/pubid/components/type.rb +2 -0
  107. data/lib/pubid/components/typed_stage.rb +8 -0
  108. data/lib/pubid/conformance/checks.rb +1 -1
  109. data/lib/pubid/csa/CLAUDE.md +41 -0
  110. data/lib/pubid/csa/builder.rb +2 -0
  111. data/lib/pubid/csa/identifier.rb +19 -3
  112. data/lib/pubid/csa/parser.rb +25 -8
  113. data/lib/pubid/csa/renderer.rb +12 -12
  114. data/lib/pubid/csa/single_identifier.rb +17 -0
  115. data/lib/pubid/doi/builder.rb +2 -0
  116. data/lib/pubid/doi/identifier.rb +1 -1
  117. data/lib/pubid/easc/builder.rb +2 -0
  118. data/lib/pubid/easc/identifier.rb +10 -1
  119. data/lib/pubid/ecma/CLAUDE.md +28 -0
  120. data/lib/pubid/ecma/builder.rb +2 -0
  121. data/lib/pubid/ecma/identifier.rb +8 -1
  122. data/lib/pubid/etsi/CLAUDE.md +34 -0
  123. data/lib/pubid/etsi/builder.rb +2 -0
  124. data/lib/pubid/etsi/components/code.rb +6 -0
  125. data/lib/pubid/etsi/components/version.rb +2 -0
  126. data/lib/pubid/etsi/identifiers/base.rb +1 -1
  127. data/lib/pubid/etsi/identifiers/etsi_standard.rb +7 -0
  128. data/lib/pubid/evs/CLAUDE.md +58 -0
  129. data/lib/pubid/evs/builder.rb +2 -0
  130. data/lib/pubid/evs.rb +1 -1
  131. data/lib/pubid/gb/CLAUDE.md +140 -0
  132. data/lib/pubid/gb/builder.rb +7 -2
  133. data/lib/pubid/gb/identifier.rb +6 -4
  134. data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
  135. data/lib/pubid/gb/identifiers.rb +1 -0
  136. data/lib/pubid/gb/renderer.rb +0 -1
  137. data/lib/pubid/gost/CLAUDE.md +64 -0
  138. data/lib/pubid/gost/builder.rb +3 -1
  139. data/lib/pubid/gost/identifier.rb +16 -1
  140. data/lib/pubid/gost/parser.rb +8 -1
  141. data/lib/pubid/iala/CLAUDE.md +82 -0
  142. data/lib/pubid/iala/builder.rb +2 -0
  143. data/lib/pubid/iala/identifier.rb +10 -1
  144. data/lib/pubid/iana/CLAUDE.md +7 -0
  145. data/lib/pubid/iana/builder.rb +2 -0
  146. data/lib/pubid/iana/identifier.rb +1 -1
  147. data/lib/pubid/identifier.rb +161 -17
  148. data/lib/pubid/idf/builder.rb +11 -1
  149. data/lib/pubid/idf/identifier.rb +5 -0
  150. data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
  151. data/lib/pubid/idf/identifiers.rb +1 -0
  152. data/lib/pubid/iec/CLAUDE.md +31 -0
  153. data/lib/pubid/iec/builder.rb +7 -1
  154. data/lib/pubid/iec/components/consolidated_amendment.rb +4 -0
  155. data/lib/pubid/iec/components/sheet.rb +2 -0
  156. data/lib/pubid/iec/components/trf_info.rb +2 -0
  157. data/lib/pubid/iec/components/vap_suffix.rb +2 -0
  158. data/lib/pubid/iec/identifier.rb +8 -3
  159. data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
  160. data/lib/pubid/iec/identifiers.rb +1 -0
  161. data/lib/pubid/iec/parser.rb +9 -4
  162. data/lib/pubid/iec/renderer.rb +0 -1
  163. data/lib/pubid/iec/urn_generator.rb +9 -1
  164. data/lib/pubid/iec/urn_parser.rb +3 -2
  165. data/lib/pubid/ieee/CLAUDE.md +97 -0
  166. data/lib/pubid/ieee/builder.rb +194 -27
  167. data/lib/pubid/ieee/components/code.rb +2 -0
  168. data/lib/pubid/ieee/components/draft.rb +35 -2
  169. data/lib/pubid/ieee/components/typed_stage.rb +2 -0
  170. data/lib/pubid/ieee/identifiers/base.rb +21 -1
  171. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
  172. data/lib/pubid/ieee/identifiers/joint_development.rb +66 -23
  173. data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
  174. data/lib/pubid/ieee/parser.rb +156 -29
  175. data/lib/pubid/ieee/renderer.rb +42 -7
  176. data/lib/pubid/ieee/urn_generator.rb +31 -0
  177. data/lib/pubid/ietf/CLAUDE.md +7 -0
  178. data/lib/pubid/ietf/builder.rb +2 -0
  179. data/lib/pubid/ietf/identifiers/base.rb +1 -1
  180. data/lib/pubid/iho/builder.rb +2 -0
  181. data/lib/pubid/isbn/builder.rb +2 -0
  182. data/lib/pubid/isbn/identifier.rb +1 -1
  183. data/lib/pubid/iso/CLAUDE.md +47 -0
  184. data/lib/pubid/iso/builder.rb +19 -5
  185. data/lib/pubid/iso/components/publisher.rb +2 -0
  186. data/lib/pubid/iso/identifier.rb +10 -15
  187. data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
  188. data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
  189. data/lib/pubid/iso/identifiers.rb +1 -0
  190. data/lib/pubid/iso/normalizer.rb +4 -1
  191. data/lib/pubid/iso/rendering_style.rb +0 -1
  192. data/lib/pubid/itu/CLAUDE.md +115 -0
  193. data/lib/pubid/itu/builder.rb +26 -4
  194. data/lib/pubid/itu/components/code.rb +2 -0
  195. data/lib/pubid/itu/components/designation.rb +2 -0
  196. data/lib/pubid/itu/components/sector.rb +2 -0
  197. data/lib/pubid/itu/components/series.rb +2 -0
  198. data/lib/pubid/itu/identifiers/base.rb +11 -18
  199. data/lib/pubid/itu/identifiers/radio_regulations.rb +27 -0
  200. data/lib/pubid/itu/identifiers/special_publication.rb +48 -14
  201. data/lib/pubid/itu/identifiers/standard_serialization.rb +2 -0
  202. data/lib/pubid/itu/identifiers/supplement.rb +15 -0
  203. data/lib/pubid/itu/identifiers.rb +1 -0
  204. data/lib/pubid/itu/parser.rb +108 -22
  205. data/lib/pubid/itu/urn_generator.rb +9 -2
  206. data/lib/pubid/jcgm/CLAUDE.md +7 -0
  207. data/lib/pubid/jcgm/builder.rb +2 -0
  208. data/lib/pubid/jcgm/components/publisher.rb +2 -0
  209. data/lib/pubid/jcgm.rb +1 -1
  210. data/lib/pubid/jis/builder.rb +5 -1
  211. data/lib/pubid/jis/identifier.rb +6 -18
  212. data/lib/pubid/jis/identifiers/all_parts.rb +19 -0
  213. data/lib/pubid/jis/identifiers.rb +1 -0
  214. data/lib/pubid/jis/renderer.rb +0 -2
  215. data/lib/pubid/jis/urn_generator.rb +0 -1
  216. data/lib/pubid/nist/CLAUDE.md +56 -0
  217. data/lib/pubid/nist/builder.rb +3 -0
  218. data/lib/pubid/nist/components/edition.rb +2 -0
  219. data/lib/pubid/nist/components/issue_number.rb +2 -0
  220. data/lib/pubid/nist/components/part.rb +2 -0
  221. data/lib/pubid/nist/components/stage.rb +2 -0
  222. data/lib/pubid/nist/components/supplement.rb +2 -0
  223. data/lib/pubid/nist/components/translation.rb +2 -0
  224. data/lib/pubid/nist/components/update.rb +2 -0
  225. data/lib/pubid/nist/components/version.rb +2 -0
  226. data/lib/pubid/nist/components/volume.rb +2 -0
  227. data/lib/pubid/nist/identifiers/base.rb +39 -7
  228. data/lib/pubid/nist/parser.rb +24 -2
  229. data/lib/pubid/nist/preprocessor.rb +53 -2
  230. data/lib/pubid/nist/urn_parser.rb +10 -1
  231. data/lib/pubid/oasis/CLAUDE.md +19 -0
  232. data/lib/pubid/oasis/builder.rb +2 -0
  233. data/lib/pubid/oasis/identifier.rb +20 -1
  234. data/lib/pubid/ogc/CLAUDE.md +34 -0
  235. data/lib/pubid/ogc/builder.rb +2 -0
  236. data/lib/pubid/ogc/identifier.rb +12 -1
  237. data/lib/pubid/oiml/CLAUDE.md +189 -0
  238. data/lib/pubid/oiml/builder.rb +20 -0
  239. data/lib/pubid/oiml/components/code.rb +6 -0
  240. data/lib/pubid/oiml/identifier.rb +13 -0
  241. data/lib/pubid/oiml/identifiers/annex.rb +4 -0
  242. data/lib/pubid/oiml/identifiers/certification_system.rb +34 -0
  243. data/lib/pubid/oiml/identifiers/code_number.rb +8 -0
  244. data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
  245. data/lib/pubid/oiml/identifiers.rb +2 -0
  246. data/lib/pubid/oiml/parser.rb +35 -4
  247. data/lib/pubid/oiml/renderer.rb +23 -1
  248. data/lib/pubid/oiml/single_identifier.rb +4 -0
  249. data/lib/pubid/oiml/supplement_identifier.rb +7 -0
  250. data/lib/pubid/oiml/urn_generator.rb +28 -0
  251. data/lib/pubid/oiml.rb +6 -1
  252. data/lib/pubid/omg/CLAUDE.md +15 -0
  253. data/lib/pubid/omg/builder.rb +2 -0
  254. data/lib/pubid/omg/identifier.rb +1 -1
  255. data/lib/pubid/parg/artifact.rb +46 -0
  256. data/lib/pubid/parg/backend.rb +92 -0
  257. data/lib/pubid/parg.rb +8 -0
  258. data/lib/pubid/parser/grammar.rb +23 -0
  259. data/lib/pubid/pg.rb +8 -0
  260. data/lib/pubid/plateau/builder.rb +2 -0
  261. data/lib/pubid/plateau/identifiers/base.rb +4 -0
  262. data/lib/pubid/plateau/supplement_identifier.rb +14 -2
  263. data/lib/pubid/plateau/urn_generator.rb +7 -1
  264. data/lib/pubid/plateau.rb +1 -2
  265. data/lib/pubid/renderers/human_readable.rb +0 -1
  266. data/lib/pubid/sae/builder.rb +2 -0
  267. data/lib/pubid/sae/components/date.rb +2 -0
  268. data/lib/pubid/sae/components/type.rb +2 -0
  269. data/lib/pubid/sae/identifiers/base.rb +1 -1
  270. data/lib/pubid/subset_match.rb +197 -0
  271. data/lib/pubid/tgpp/CLAUDE.md +43 -0
  272. data/lib/pubid/tgpp/builder.rb +2 -0
  273. data/lib/pubid/tgpp/identifier.rb +15 -1
  274. data/lib/pubid/type_resolver.rb +14 -2
  275. data/lib/pubid/un/builder.rb +2 -0
  276. data/lib/pubid/un/identifier.rb +1 -1
  277. data/lib/pubid/version.rb +1 -1
  278. data/lib/pubid/w3c/CLAUDE.md +7 -0
  279. data/lib/pubid/w3c/builder.rb +2 -0
  280. data/lib/pubid/w3c/identifier.rb +1 -1
  281. data/lib/pubid/xsf/CLAUDE.md +11 -0
  282. data/lib/pubid/xsf/builder.rb +2 -0
  283. data/lib/pubid/xsf/identifier.rb +1 -1
  284. data/lib/pubid.rb +17 -3
  285. metadata +78 -2
@@ -20,14 +20,17 @@ module Pubid
20
20
  rule(:identifier) do
21
21
  amendment_identifier | amendment_short | annex_letter_identifier |
22
22
  annex_identifier | plus_supplement_identifier |
23
- trailing_supplement_identifier | bulletin_identifier | base
23
+ trailing_supplement_identifier | cs_identifier |
24
+ bulletin_identifier | base
24
25
  end
25
26
 
26
27
  # Publisher - always "OIML"
27
28
  rule(:publisher) { str("OIML").as(:publisher) >> space }
28
29
 
29
- # Document type - single letter
30
- rule(:doc_type) { match("[BDEGRSVX]").as(:type) >> space }
30
+ # Document type - single letter. Strict family set: OIML publishes
31
+ # R D B G E V S documents (the estate grammar's family letters);
32
+ # any other letter is a rejection, not a flavor.
33
+ rule(:doc_type) { match("[BDEGRSV]").as(:type) >> space }
31
34
 
32
35
  # Bulletin locator — structured form. Year optionally followed by
33
36
  # 2-digit issue and 2-digit sequence:
@@ -128,7 +131,7 @@ module Pubid
128
131
  # (with optional space before year)
129
132
  rule(:date) do
130
133
  edition_portion |
131
- (colon >> space.maybe >> year_digits.as(:year)) |
134
+ (space.maybe >> colon >> space.maybe >> year_digits.as(:year)) |
132
135
  (space.maybe >> lparen >> year_digits.as(:year) >> rparen)
133
136
  end
134
137
 
@@ -167,10 +170,22 @@ module Pubid
167
170
  match("[a-z]").repeat(2, 2) # Two letters: en, fr, etc.
168
171
  end
169
172
 
173
+ # Full-word language markers as OIML prints them ("(Fra)", "(Eng)"),
174
+ # three or more letters, any case. Kept verbatim on the identifier;
175
+ # the URN lowercases.
176
+ rule(:lang_word) do
177
+ match("[A-Za-z]").repeat(3)
178
+ end
179
+
170
180
  rule(:language_code) do
171
181
  (
172
182
  (lang_single >> slash >> lang_single) | # E/F
173
183
  lang_multi_oiml | # PO, PT, PE, SR
184
+ lang_word | # Fra, eng, rus — before
185
+ # the letter rules: a
186
+ # committed "F" of "(Fra)"
187
+ # or "fr" of "(fra)" would
188
+ # never fall through
174
189
  lang_single | # E, F, D, R, S, C, A, U, X
175
190
  lang_multi # en, fr
176
191
  ).as(:language)
@@ -216,9 +231,25 @@ module Pubid
216
231
  rule(:trailing_supplement_identifier) do
217
232
  base_without_language.as(:base) >>
218
233
  space >> (str("Amendment") | str("Errata")).as(:trailing_marker) >>
234
+ (space >> digits.as(:number)).maybe >>
219
235
  language_portion.maybe.as(:language)
220
236
  end
221
237
 
238
+ # OIML-CS certification-system documents. Two head spellings
239
+ # ("OIML-CS" / "OIML CS") and two family-number separators
240
+ # ("PD-05" / "PD 05"), an "Edition N" instead of a year, and an
241
+ # optional parenthesized trailing amendment - "(Amendment 1)".
242
+ rule(:cs_identifier) do
243
+ str("OIML").as(:publisher) >>
244
+ (dash >> str("CS") | space >> str("CS")).as(:cs_series) >> space >>
245
+ (str("PD") | str("OD") | str("CID")).as(:cs_family) >>
246
+ (dash | space).as(:cs_separator) >>
247
+ digits.as(:number) >>
248
+ space >> str("Edition") >> space >> digits.as(:edition) >>
249
+ (space >> lparen >> str("Amendment") >> space >>
250
+ digits.as(:cs_amendment) >> rparen).maybe
251
+ end
252
+
222
253
  # Plus-joined supplement - "BASE:YEAR+Supplement:YEAR" form where both
223
254
  # the base and the supplement carry their own year. Used for amendments
224
255
  # and errata to dated bases (e.g. "OIML B 10:2011+Amendment:2012").
@@ -18,10 +18,14 @@ module Pubid
18
18
  @context = context
19
19
 
20
20
  case id
21
+ when Identifiers::DualPublished
22
+ render_dual_published(id)
21
23
  when Identifiers::Annex
22
24
  render_annex(id)
23
25
  when Identifiers::Bulletin
24
26
  render_bulletin(id)
27
+ when Identifiers::CertificationSystem
28
+ render_cs(id)
25
29
  when SupplementIdentifier
26
30
  render_supplement(id)
27
31
  when SingleIdentifier
@@ -33,6 +37,16 @@ module Pubid
33
37
 
34
38
  private
35
39
 
40
+ # Certification-system document: "OIML-CS PD-05 Edition 6 (Amendment 1)".
41
+ # The family-number separator keeps the parsed spelling; OIML CS
42
+ # documents state an edition instead of a year.
43
+ def render_cs(id)
44
+ result = "#{id.publisher}-CS #{id.family}" \
45
+ "#{id.space_separator ? ' ' : '-'}#{id.number} Edition #{id.edition}"
46
+ result += " (Amendment #{id.amendment})" if id.amendment
47
+ result
48
+ end
49
+
36
50
  # Render the Bulletin in the requested or parsed form. Default is the
37
51
  # structured "YYYY-II-SS" form (the dataset's primary docid). The
38
52
  # citation form ("LXVII(2) 20260211") is emitted when the user asks
@@ -93,6 +107,12 @@ module Pubid
93
107
  str.sub(/\s*\([^)]+\)\s*$/, "").strip
94
108
  end
95
109
 
110
+ # Bare "|", original left-to-right print order, e.g.
111
+ # "ISO 4064-1:2024|OIML R 49-1:2024".
112
+ def render_dual_published(id)
113
+ "#{id.first}|#{id.second}"
114
+ end
115
+
96
116
  def render_single(id)
97
117
  format = effective_format(id)
98
118
 
@@ -147,10 +167,12 @@ module Pubid
147
167
 
148
168
  # Trailing-word shorthand: "BASE Amendment" / "BASE Errata" with the
149
169
  # publication year kept on the base identifier. The word comes from the
150
- # concrete supplement class.
170
+ # concrete supplement class; an ordinal, when printed ("Amendment 1"),
171
+ # follows it.
151
172
  if id.trailing
152
173
  base_str = strip_language(id.base.to_s)
153
174
  result = "#{base_str} #{id.supplement_type}"
175
+ result += " #{id.number}" if id.number
154
176
  result += " (#{id.language})" if id.language
155
177
  return result
156
178
  end
@@ -14,6 +14,10 @@ module Pubid
14
14
  "short"
15
15
  } # Track parsed format
16
16
 
17
+ # A nil `language` means the document states none: `OIML R 126:2015
18
+ # Errata` is not `OIML R 126:2015 Errata (E)`, its English edition.
19
+ subset_strict :language
20
+
17
21
  # Serialization delta on top of Oiml::Identifier's shared block. The
18
22
  # `date` (year) component is flattened to a top-level key rather than a
19
23
  # nested hash, mirroring ISO (lib/pubid/iso/identifier.rb). `type` is
@@ -7,8 +7,14 @@ module Pubid
7
7
  # These wrap a base identifier like ISO amendments
8
8
  attribute :base, Oiml::Identifier, polymorphic: true
9
9
  attribute :year, :string
10
+ # Ordinal of the trailing-word form ("OIML R 138:2009 Amendment 1").
11
+ attribute :number, :string
10
12
  attribute :language, :string
11
13
 
14
+ # A nil `language` means the document states none, mirroring
15
+ # SingleIdentifier's rule.
16
+ subset_strict :language
17
+
12
18
  # Delegate the document code to the wrapped standard, mirroring
13
19
  # Pubid::Etsi::Identifiers::SupplementIdentifier#code.
14
20
  #
@@ -54,6 +60,7 @@ module Pubid
54
60
  map "base",
55
61
  with: { to: :base_to_kv, from: :base_from_kv }
56
62
  map "year", to: :year
63
+ map "number", to: :number
57
64
  map "trailing", to: :trailing
58
65
  map "joined", to: :joined
59
66
  end
@@ -27,6 +27,25 @@ module Pubid
27
27
  identifier.language&.to_s&.downcase
28
28
  end
29
29
 
30
+ # DualPublished declares no `date` attribute of its own (it lives on
31
+ # whichever side is OIML), so the shared Base#urn_year — which gates
32
+ # on `identifier.class.attributes.key?(:date)` — would silently drop
33
+ # the year. Read it through the OIML side instead.
34
+ def urn_year
35
+ if identifier.is_a?(Identifiers::DualPublished)
36
+ oiml_date = identifier.oiml_identifier&.date
37
+ return oiml_date&.year&.to_s
38
+ end
39
+
40
+ year = super
41
+ return year if year
42
+
43
+ # The trailing-word form keeps the publication year on the base
44
+ # ("R 138:2009 Amendment 1"); the supplement itself carries only the
45
+ # ordinal.
46
+ identifier.base&.date&.year&.to_s if identifier.is_a?(SupplementIdentifier)
47
+ end
48
+
30
49
  def generate
31
50
  # Bulletin issues carry no code; the (year, issue, sequence) tuple
32
51
  # is the locator. URNs are canonical regardless of how the input was
@@ -39,6 +58,15 @@ module Pubid
39
58
  return parts.join(":")
40
59
  end
41
60
 
61
+ # Certification-system documents: the family-number pair is the
62
+ # document identity ("cs:pd-05"); the printed edition and trailing
63
+ # amendment are print states, not URN segments.
64
+ if identifier.is_a?(Identifiers::CertificationSystem)
65
+ parts = ["urn", "oiml", "cs",
66
+ "#{identifier.family.downcase}-#{identifier.number}"]
67
+ return parts.join(":")
68
+ end
69
+
42
70
  parts = ["urn", "oiml"]
43
71
  parts << urn_type
44
72
  parts << urn_number if urn_number
data/lib/pubid/oiml.rb CHANGED
@@ -29,10 +29,15 @@ module Pubid
29
29
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
30
30
  end
31
31
 
32
+ if identifier.include?("|")
33
+ dual = Identifiers::DualPublished.build(identifier)
34
+ return dual if dual
35
+ end
36
+
32
37
  parser = Parser.new
33
38
  builder = Builder.new
34
39
 
35
- parsed = parser.parse(identifier)
40
+ parsed = Pubid::Parg::Backend.parse(:oiml, identifier)
36
41
  builder.build(parsed)
37
42
  end
38
43
 
@@ -0,0 +1,15 @@
1
+ # OMG flavor notes
2
+
3
+ OMG document parts, the separator that normalizes, the acronym charset and its slash, and the bare `beta`.
4
+
5
+ These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/omg/` or `spec/pubid/omg/`. The root file keeps the cross-flavor contract that every flavor obeys.
6
+
7
+ - **The document part reuses the inherited `part`, retyped to `:string`**: an OMG identifier is `OMG <ACRONYM>[ <VERSION>][ <PART>]`, and the third component is a real OMG form, not an invention. OMG published UML 2.1.1 as **two** documents and its URLs carry the segment (`/spec/UML/2.1.1/Superstructure`, `/spec/UML/2.1.1/Infrastructure`); the same position also holds a format name (`/spec/DDS/1.4/PDF`). Both go into one optional component. The attribute is the **`part` that `::Pubid::Identifier` already declares** as a `Components::Code`, retyped to `:string` on `Pubid::Omg::Identifier` — the tranche-1 shape (ansi/api/idf/jcgm/bsi/cen_cenelec). Three things follow from reusing the inherited name rather than inventing a `document_part`: relaton gets `remove_part!` as a plain `exclude(:part)` with no OMG-specific knowledge, `Renderers::Annotator::TOKENS` already carries `[:part, "part"]` so annotation costs nothing, and there is no second, permanently-nil `part` on every identifier. The **placement** is what makes the retype safe: it sits once on `Pubid::Omg::Identifier`, which every OMG identifier inherits from and whose class body lives in one file (`lib/pubid/omg/identifier.rb`) and is never reopened — the condition the root `CLAUDE.md` states for a base-level declaration. Never move it onto `Identifiers::Specification`, and never add a second declaration there: a redeclaration moves the generated accessor's `owner`, which the retype tripwire specs of other flavors treat as a delegation.
8
+ - **The separator normalizes, and that is a matching decision, not a cosmetic one**: OMG writes the part behind either a space or a slash, so `Parser#part_separator` is `space | str("/")` and the parser takes both. The renderer prints **only a space**, so `OMG DDS 1.4/PDF` is a *normalizing* parse rendering `OMG DDS 1.4 PDF`. The alternative — a `part_separator` sibling attribute in the CSA `year_format` shape — round-trips both spellings byte for byte but makes them **not `==`**, and `#matches?` is `exclude(*ignore) == other.exclude(*ignore)`, so a relaton index lookup between the two spellings would return nothing with no error. That is the silent failure mode the root file records as the costliest here, and it is not worth a separator. `spec/pubid/omg/identifier_spec.rb` asserts the normalization **and** the equality, so a future attempt to preserve the separator turns both red. The slash forms therefore cannot live in `spec/fixtures/omg/pass/`, whose spec demands a byte-exact round-trip; they are pinned in the identifier spec instead, with a comment in the fixture file saying why.
9
+ - **The acronym is the URL segment, so it takes every character OMG puts there — and a slash before the version belongs to it.** A consumer (relaton) builds `https://www.omg.org/spec/<acronym>/` from the parsed acronym, so the acronym must be verbatim. The old rule `[A-Z][A-Za-z0-9]*` rejected **30 of the 270** acronyms the catalog names: 25 with a hyphen (`DDS-XTypes`, `IDL4-CPP`), 4 with a slash (`EDMC-FIBO/BE`), `VSIPL++` and the lower-case `smartant`. `Parser#acronym` now starts with any letter, takes letters, digits and `+`, and joins further non-empty segments with `-` or `/`, so a trailing hyphen or slash is never consumed. **The slash is the design decision.** The document-part bullet above made a slash separate the part, and `OMG EDMC-FIBO/BE` could then read as acronym `EDMC-FIBO` with part `BE` — which builds the URL of the wrong page, silently. The rule now is: **before the version a slash is part of the acronym, after the version it separates the part** (`OMG DDS 1.4/PDF` is unchanged; `OMG EDMC-FIBO/BE 1.1/PDF` reads both). `Parser#identifier` spells that out — a part that follows the acronym directly takes only a space. The cost is that `OMG UML/Superstructure` now reads as acronym `UML/Superstructure`; no spec, fixture or relaton corpus row used that spelling, and OMG writes the space. A fixed list of the four FIBO domains was the alternative and was rejected: it drifts when OMG adds a domain. **Known limit — a two-word title still parses.** The grammar cannot tell an acronym from a word, so `OMG Real-Time Extension` reads as acronym `Real-Time`, part `Extension`. This is not new: on `main` before this change, `OMG Model Driven` already read as acronym `Model`, part `Driven`; the wider charset only adds hyphenated and lower-case first words. A longer title (`OMG Model Driven Architecture Guide rev. 2.0`) still raises, because the grammar has no place for a third word. `spec/pubid/omg/identifier_spec.rb` carries all 30 catalog acronyms as a frozen, network-free list. To re-check against the live catalog (270 acronyms, 0 rejections on 2026-09-14): `curl -s https://www.omg.org/spec/ | ruby -e 'puts STDIN.read.scan(%r{spec/(.+?)/About-[^"]*"}).flatten.uniq'`, then `Pubid::Omg.parse("OMG #{acronym}")` for each. (hand-off: `metanorma__pubid__omg-acronym-charset`.)
10
+ - **The bare `beta` is not optional polish — the document part makes it load-bearing.** `parser.rb` used to demand `" beta "` followed by at least one digit. Once an optional third token is legal, a PEG grammar parses `OMG UML 2.5 beta` as version `2.5` plus a **document part named `beta`** — a silent wrong answer, not a parse failure, and exactly the shape that costs nothing until a consumer compares two identifiers. So the beta number is `.maybe`, and the version rule consumes the label whole. **This is what OMG actually publishes, checked against the source**: `https://www.omg.org/spec/UML/2.5/Beta1/` gives its own version as **`2.5 beta`**, the unnumbered form, while DDS 1.4 supersedes `https://www.omg.org/spec/DDS/1.4/Beta2`. Both spellings are real. The regression guard is the example asserting that `OMG UML 2.5 beta` leaves `part` **nil**; the round-trip example alone would pass either way.
11
+ - **Both halves of the beta label need a word boundary, and the failure mode is a rejection rather than a misparse.** Parslet never backtracks into a `.maybe` that already succeeded. With an unanchored `str(" beta")`, `OMG DDS 1.4 beta2` and `OMG DDS 1.4 betawave` made the version rule commit to `" beta"`, fail to find the beta number, and leave `2`/`wave` with no separator in front of it — so the **whole identifier raised**, not merely parsed oddly. The same trap sits behind the beta number: without a guard, `OMG UML 2.5 beta 1x` consumed `" 1"` and then choked on `x`. `Parser#beta` therefore ends each half with `word_boundary` (`match("[A-Za-z0-9]").absent?`), which costs nothing — a genuine bare `beta` is followed by the end of input or a space, and a genuine beta number by the same. All three inputs now read the tail as a document part. A code review found the first case; the second and third came out of probing the fix. The lesson generalizes to any flavor adding an optional trailing token after an optional literal-suffixed one: **anchor the literal, or the earlier rule eats the later one's first word and the identifier is rejected**.
12
+ - **Known limit — `Beta2` reads as a document part.** OMG's URL spelling glues the label and the number and capitalizes (`.../1.4/Beta2`), so `OMG DDS 1.4 Beta2` parses as version `1.4` with the part `"Beta2"`. It round-trips and it is not wrong enough to chase: no reference in the relaton corpus uses that spelling, the version rule follows the lowercase, space-separated form the relaton regex accepted, and widening it would need a rule that tells `Beta2` from a genuine volume name. Recorded rather than fixed.
13
+ - **`root.number` is nil for every OMG identifier, and so is the MR slug. Neither moved here, and nothing depends on either yet.** OMG models `acronym`/`version`/`part` and never sets the `number` it inherits, so the index key the root file requires of every leaf is empty and `to_mr_string` is `""` for every identifier — both were already true before this branch and both are unchanged by it (the base `mr_number_with_part` reads `number`, not `part`, so adding the part does not populate the slug). OMG has **no index and no `relaton-data-omg`** — it is scraped from `www.omg.org/spec` with Mechanize — and the relaton migration hand-off explicitly forbids adding one, so the binary-search degradation the root file describes cannot bite today. Do not "fix" it by mirroring `acronym` into `number` without checking the renderer first: that is the precondition the BIPM entry names, and the NIST attempt recorded in the root file is what happens when it does not hold.
14
+ - **The fixture corpus is hand-written.** OMG has no `spec/fixtures/omg/identifiers/full/` tree and no entry in `spec/fixtures/classify_fixtures.rb`, so `validation:classify` never rewrites `pass/` or `fail/` and the root file's rule against hand-editing them does not apply. `spec/pubid/omg/fixtures_spec.rb` skips `#` lines, so the files carry comments.
15
+ - **relaton note**: `relaton/relaton` moves its OMG flavor off the hand-written regex at `lib/relaton/omg/scraper.rb:19` onto this flavor. Both blockers it named are fixed. Its probe expected `OMG DDS 1.4/PDF` to round-trip byte for byte; it now sees a normalizing parse, which is harmless there because the flavor reads `acronym` and `version` off the parsed object and keeps the document part out of the request URL. (hand-offs: `metanorma__pubid__omg-document-part-and-bare-beta`, `relaton__relaton__omg-pubid-migration`.)
@@ -18,3 +18,5 @@ module Pubid
18
18
  end
19
19
  end
20
20
  end
21
+
22
+ Pubid::Omg::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -53,7 +53,7 @@ module Pubid
53
53
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
54
54
  end
55
55
 
56
- parsed = Parser.parse(identifier)
56
+ parsed = Pubid::Parg::Backend.parse(:omg, identifier)
57
57
  Builder.build(parsed)
58
58
  end
59
59
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parsanol"
4
+
5
+ module Pubid
6
+ module Parg
7
+ # A baked, checksum-verified PG artifact for one flavor. The artifact
8
+ # is the parser of record for the flavor: its grammar, tests, entries,
9
+ # and embedded tables travel together and are verified on load.
10
+ class Artifact
11
+ DATA_DIR = File.expand_path("../../../data/parg", __dir__)
12
+ # The deferred entity atoms resolve from_table references at parse
13
+ # time from this dir, so the table YAMLs vendor alongside the
14
+ # baked artifacts.
15
+ TABLES_DIR = File.join(DATA_DIR, "tables")
16
+
17
+ @artifacts = {}
18
+
19
+ class << self
20
+ # Load and memoize the artifact for a flavor (e.g. :iso).
21
+ def for(flavor)
22
+ @artifacts[flavor] ||=
23
+ begin
24
+ path = File.join(DATA_DIR, "#{flavor}.json")
25
+ new(Parsanol::PARG::Artifact.load(path, tables_dir: TABLES_DIR))
26
+ rescue Parsanol::PARG::Error => e
27
+ raise Pubid::Errors::ParseError,
28
+ "PG artifact #{path} rejected: #{e.message}"
29
+ end
30
+ end
31
+ end
32
+
33
+ def initialize(artifact)
34
+ @artifact = artifact
35
+ end
36
+
37
+ def parse(entry, input)
38
+ @artifact.parse(entry, input)
39
+ end
40
+
41
+ def checksum
42
+ @artifact.envelope["checksum"]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Parg
5
+ # The PG-artifact parse backend: runs the flavor's baked artifact on
6
+ # an input and returns the builder-ready attribute hash the flavor's
7
+ # Builder expects — the same hash its parslet parser produced.
8
+ module Backend
9
+ LEAF_KEYS = %i[value line column offset length].freeze
10
+
11
+ module_function
12
+
13
+ ALL_PARTS_SUFFIX = "(all parts)"
14
+
15
+ def parse(flavor, input, entry: "identifier", merge_top_sequence: true)
16
+ # The parslet grammar base strips the "(all parts)" suffix before
17
+ # parsing and marks the tree (Grammar#parse / #mark_all_parts);
18
+ # the artifact backend carries the same contract so every flavor
19
+ # whose grammar does not itself consume the suffix keeps working.
20
+ if input.end_with?(ALL_PARTS_SUFFIX)
21
+ base = input.sub(/\s*\(all parts\)\s*\z/, "")
22
+ tree = to_builder_hash(Artifact.for(flavor).parse(entry, base), merge: merge_top_sequence)
23
+ return mark_all_parts(tree)
24
+ end
25
+
26
+ to_builder_hash(Artifact.for(flavor).parse(entry, input), merge: merge_top_sequence)
27
+ rescue Parsanol::ParseFailed => e
28
+ raise Pubid::Errors::ParseError.new(e.message, nil,
29
+ input: input,
30
+ flavor: registered_name(flavor))
31
+ end
32
+
33
+ # The registered flavor name (Registry-resolvable; "3gpp" for
34
+ # Pubid::Tgpp), falling back to the internal symbol when the
35
+ # flavor is not registered.
36
+ def registered_name(flavor)
37
+ mod = Pubid.const_get(flavor.to_s.split("_").map(&:capitalize).join)
38
+ names = Pubid::Registry.flavor_names.select { |name| Pubid::Registry.get(name) == mod }
39
+ # A module may register under several names; the longest is the
40
+ # canonical one ("cen_cenelec", not the "cen" alias).
41
+ names.max_by(&:length)
42
+ rescue NameError
43
+ nil
44
+ end || flavor.to_s
45
+
46
+ def mark_all_parts(tree)
47
+ case tree
48
+ when Hash then tree.merge(all_parts: true)
49
+ when Array then tree.map { |t| t.merge(all_parts: true) }
50
+ else tree
51
+ end
52
+ end
53
+
54
+ # The artifact emits the parsanol-tree wire shape: capture leaves
55
+ # ({value, line, column, offset, length}) carry their text, and the
56
+ # top-level sequence is a list of capture hashes. The builder-ready
57
+ # form scalarizes leaves and folds that top-level list into one
58
+ # attribute hash (last key wins) — parslet's sequence fold.
59
+ def to_builder_hash(shape, merge: true)
60
+ normalized = normalize(shape)
61
+ return normalized.reduce(:merge) if merge && mergeable_sequence?(normalized)
62
+
63
+ normalized
64
+ end
65
+
66
+ def normalize(node)
67
+ case node
68
+ when Parsanol::Slice then node.content
69
+ when Hash then normalize_hash(node)
70
+ when Array then node.map { |item| normalize(item) }
71
+ else node
72
+ end
73
+ end
74
+
75
+ def normalize_hash(node)
76
+ return node[:value] if wire_leaf?(node)
77
+
78
+ node.transform_values { |value| normalize(value) }
79
+ end
80
+
81
+ def mergeable_sequence?(value)
82
+ value.is_a?(Array) && value.all?(Hash)
83
+ end
84
+
85
+ def wire_leaf?(node)
86
+ return false unless node.size == LEAF_KEYS.size
87
+
88
+ LEAF_KEYS.all? { |key| node.key?(key) }
89
+ end
90
+ end
91
+ end
92
+ end
data/lib/pubid/parg.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Parg
5
+ autoload :Artifact, "pubid/parg/artifact"
6
+ autoload :Backend, "pubid/parg/backend"
7
+ end
8
+ end
@@ -21,10 +21,22 @@ module Pubid
21
21
  # bypasses this and raises a bare `Parslet::ParseFailed`. Nothing in the
22
22
  # gem does that.
23
23
  class Grammar < ::Parslet::Parser
24
+ # A trailing "(all parts)" marks the reference as the whole document.
25
+ # The flavor grammars that carry their own rule consume it inside
26
+ # parslet; this shared strip gives every other flavor the same read:
27
+ # the suffix never reaches the flavor grammar, and the parsed tree
28
+ # carries :all_parts for the builder to wrap (see Builder::Base).
29
+ ALL_PARTS_SUFFIX = "(all parts)".freeze
30
+
24
31
  # @param io [String, IO]
25
32
  # @param options [Hash] passed through to parslet
26
33
  # @raise [Pubid::Errors::ParseError]
27
34
  def parse(io, options = {})
35
+ if io.is_a?(String) && io.end_with?(ALL_PARTS_SUFFIX)
36
+ base = io.sub(/\s*\(all parts\)\s*\z/, "")
37
+ return mark_all_parts(super(base, options))
38
+ end
39
+
28
40
  super
29
41
  rescue ::Pubid::Errors::ParseError
30
42
  # A nested grammar already wrapped it. Keep the inner flavor and input.
@@ -35,6 +47,17 @@ module Pubid
35
47
 
36
48
  private
37
49
 
50
+ # Carry the stripped suffix into the tree. Parslet tops are a Hash or
51
+ # an Array of Hashes; the marker joins either shape, and every builder
52
+ # (Builder::Base and the standalone ones) routes it to #to_all_parts.
53
+ def mark_all_parts(tree)
54
+ case tree
55
+ when Hash then tree.merge(all_parts: true)
56
+ when Array then tree.map { |t| t.merge(all_parts: true) }
57
+ else tree
58
+ end
59
+ end
60
+
38
61
  # @param error [Parslet::ParseFailed]
39
62
  # @param io [String, IO] what was handed to {#parse}
40
63
  # @return [Pubid::Errors::ParseError]
data/lib/pubid/pg.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Pg
5
+ autoload :Artifact, "pubid/pg/artifact"
6
+ autoload :Backend, "pubid/pg/backend"
7
+ end
8
+ end
@@ -52,3 +52,5 @@ module Pubid
52
52
  end
53
53
  end
54
54
  end
55
+
56
+ Pubid::Plateau::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -14,6 +14,10 @@ module Pubid
14
14
  attribute :number, :integer
15
15
  attribute :annex, :integer, default: -> {}
16
16
 
17
+ # A nil `annex` means the document has none: PLATEAU Handbook #10 is
18
+ # not its annex, PLATEAU Handbook #10-1.
19
+ subset_strict :annex
20
+
17
21
  # Stored as a plain string (always "PLATEAU") so it round-trips through
18
22
  # to_hash/from_hash. Was a `def publisher` method, which made lutaml
19
23
  # serialize a String against the Components::Publisher attribute.
@@ -9,9 +9,16 @@ module Pubid
9
9
  class SupplementIdentifier < Pubid::Identifier
10
10
  attribute :base, Identifier
11
11
  attribute :letter, :string, default: -> {}
12
+ # Stored as a plain string (always "PLATEAU") so it round-trips through
13
+ # to_hash/from_hash. Was a `def publisher` method, which made lutaml
14
+ # serialize a String against the Components::Publisher attribute
15
+ # (pubid/pubid#407) — the same fix as Identifiers::Base.
16
+ attribute :publisher, :string, default: -> { "PLATEAU" }
12
17
 
13
- def publisher
14
- "PLATEAU"
18
+ # The UrnGenerator reads type_string on every identifier; the annex
19
+ # supplement's own type makes its "an" URN branch reachable.
20
+ def type_string
21
+ "Annex"
15
22
  end
16
23
 
17
24
  # Subclasses must implement supplement_string
@@ -22,6 +29,11 @@ module Pubid
22
29
  # Override base_hash to extract edition, type, and annex from base
23
30
  def base_hash
24
31
  hash = super
32
+ # The base document's number: without it from_hash cannot
33
+ # reconstruct the wrapped identifier (pubid/pubid#407).
34
+ if base.class.attributes.key?(:number) && base.number
35
+ hash[:number] = base.number
36
+ end
25
37
  # For Plateau supplements, edition comes from the base identifier
26
38
  if base.class.attributes.key?(:edition) && base.edition
27
39
  hash[:edition] = base.edition
@@ -20,7 +20,13 @@ module Pubid
20
20
 
21
21
  parts << format("%02d", identifier.number) if identifier.number
22
22
 
23
- parts << format("%02d", identifier.annex) if identifier.annex
23
+ if identifier.class.attributes.key?(:annex)
24
+ parts << format("%02d", identifier.annex) if identifier.annex
25
+ # Annex supplements carry a letter, not an annex number
26
+ # (pubid/pubid#407).
27
+ elsif identifier.class.attributes.key?(:letter) && identifier.letter
28
+ parts << identifier.letter.to_s.downcase
29
+ end
24
30
 
25
31
  parts.join(":")
26
32
  end
data/lib/pubid/plateau.rb CHANGED
@@ -31,8 +31,7 @@ module Pubid
31
31
 
32
32
  # Apply legacy update_codes normalization first
33
33
  normalized = Core::UpdateCodes.apply(input, :plateau)
34
- parser = Parser.new
35
- parsed = parser.parse(normalized)
34
+ parsed = Pubid::Parg::Backend.parse(:plateau, normalized)
36
35
  Builder.build(parsed)
37
36
  end
38
37
 
@@ -12,7 +12,6 @@ module Pubid
12
12
  parts << render_edition_portion(context) if with_edition
13
13
  result = parts.compact.join(" ")
14
14
  result << render_language_portion(context, with_edition: with_edition)
15
- result << " (all parts)" if @id.all_parts
16
15
  result
17
16
  end
18
17
 
@@ -30,3 +30,5 @@ module Pubid
30
30
  end
31
31
  end
32
32
  end
33
+
34
+ Pubid::Sae::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -8,6 +8,8 @@ module Pubid
8
8
  # Date component for SAE standards
9
9
  # SAE uses year only (e.g., 2024, 2022)
10
10
  class Date < Lutaml::Model::Serializable
11
+ include ::Pubid::SubsetMatch
12
+
11
13
  attribute :year, :integer
12
14
 
13
15
  def present?
@@ -8,6 +8,8 @@ module Pubid
8
8
  # Type component for SAE document types
9
9
  # AMS, AIR, ARP, AS, MA
10
10
  class Type < Lutaml::Model::Serializable
11
+ include ::Pubid::SubsetMatch
12
+
11
13
  attribute :abbr, :string
12
14
 
13
15
  def to_s
@@ -16,7 +16,7 @@ module Pubid
16
16
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
17
17
  end
18
18
 
19
- parsed = Parser.parse(input)
19
+ parsed = Pubid::Parg::Backend.parse(:sae, input)
20
20
  Builder.build(parsed)
21
21
  end
22
22