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
@@ -7,8 +7,11 @@ module Pubid
7
7
  # Example: "BS ISO 8601:2019" where ISO 8601:2019 is an ISO identifier object
8
8
  # Example: "BS IEC 62600:2020" where IEC 62600:2020 is an IEC identifier object
9
9
  class AdoptedInternationalStandard < BritishStandard
10
- # The adopted ISO/IEC identifier object
11
- attribute :adopted, ::Pubid::Identifier, polymorphic: true
10
+ include RootIdentity
11
+
12
+ # The adopted ISO/IEC document, under the uniform parent accessor.
13
+ # `#root` and `#base_document` are inherited and walk it.
14
+ attribute :base, ::Pubid::Identifier, polymorphic: true
12
15
  attribute :edition, :string
13
16
  attribute :translation_lang, :string
14
17
  attribute :translation_upper, :string
@@ -26,26 +29,6 @@ module Pubid
26
29
  nil
27
30
  end
28
31
 
29
- # Delegate common methods to adopted identifier
30
- def number
31
- adopted&.number
32
- end
33
-
34
- def year
35
- adopted&.year if adopted&.methods&.include?(:year)
36
- end
37
-
38
- def date
39
- adopted&.date if adopted&.methods&.include?(:date)
40
- end
41
-
42
- def parts
43
- adopted&.parts if adopted&.methods&.include?(:parts)
44
- end
45
-
46
- def part
47
- adopted&.part if adopted&.methods&.include?(:part)
48
- end
49
32
  end
50
33
  end
51
34
  end
@@ -7,8 +7,22 @@ module Pubid
7
7
  # Contains a base identifier plus amendment parameters
8
8
  class Amendment < SingleIdentifier
9
9
  attribute :base, ::Pubid::Identifier, polymorphic: true
10
- attribute :amendment_number, :string
11
- attribute :amendment_year, :integer
10
+ # The amendment's ordinal lives in the `number` inherited from
11
+ # SingleIdentifier — already a `:string` there, which is what the
12
+ # ordinal is ("1", "11", "AA") — so the row reads `number:` rather than
13
+ # `amendment_number:`.
14
+ #
15
+ # This does NOT make the ordinal a document number: the amended
16
+ # standard is reached through `base`, and `#root` walks it, which is
17
+ # what relaton-index keys on.
18
+ #
19
+ # The year lives in the inherited `date` component, the natural home:
20
+ # an amendment's year belongs to the supplement, not to the standard,
21
+ # and `#exclude` protects it via `supplement_date_attributes` below,
22
+ # so `exclude(:date)` on a consolidated identifier drops only the
23
+ # standard's date. The flat scalar serialization rule renders a
24
+ # degenerate `date` as a bare `year:` key, matching CEN and every
25
+ # other BSI row.
12
26
  attribute :separator, :string, default: -> { "+" }
13
27
  # true for the trailing " AMD5" / " AMD AA" suffix form, false for the
14
28
  # compact "+A5" / "/A5" join form. Distinguishes the two when no year is
@@ -19,6 +33,23 @@ module Pubid
19
33
  base&.publisher
20
34
  end
21
35
 
36
+ # `publisher` reads through to the base, so it is never this
37
+ # amendment's own state. Lutaml only serializes the borrowed component
38
+ # when the member came from from_hash (the parse path shares the base
39
+ # instance, and lutaml omits the reference), which made
40
+ # from_hash(to_hash(id)).to_hash regrow a `publisher` key the parse
41
+ # path never emits. Drop it unconditionally to keep the wire symmetric.
42
+ def self.compact_hash(_model, hash)
43
+ hash.delete("publisher")
44
+ end
45
+
46
+ # See the comment on `date` above: protects the amendment's own date
47
+ # from a bare `exclude(:date)`/`exclude(:year)`, which otherwise
48
+ # recurses into `base` and drops the standard's date too.
49
+ def self.supplement_date_attributes
50
+ %i[date]
51
+ end
52
+
22
53
  # Base document = the standard this amendment applies to, fully peeled.
23
54
  def base_document
24
55
  base&.base_document || self
@@ -29,19 +60,12 @@ module Pubid
29
60
  base || self
30
61
  end
31
62
 
32
- # Uniform supplement interface (shared with Corrigendum) so callers need
33
- # not special-case the class.
63
+ # Names the supplement class, so callers need not special-case it. The
64
+ # ordinal and the year need no such method: Amendment and Corrigendum —
65
+ # in BSI and in CEN — all declare them as `number` and `year`.
34
66
  def supplement_type
35
67
  :amendment
36
68
  end
37
-
38
- def supplement_number
39
- amendment_number
40
- end
41
-
42
- def supplement_year
43
- amendment_year
44
- end
45
69
  end
46
70
  end
47
71
  end
@@ -6,7 +6,7 @@ module Pubid
6
6
  # British Industrial Practice (BIP)
7
7
  # Examples: "BIP 2225:2022", "BIP 0142:2014", "BIP 0009:2020"
8
8
  class BritishIndustrialPractice < SingleIdentifier
9
- attribute :date, Bsi::Components::Date
9
+
10
10
 
11
11
  def self.type
12
12
  {
@@ -12,6 +12,8 @@ module Pubid
12
12
  # BS SP 13; 14; 15 and 16:1949
13
13
  # BS 4048:Parts 1 and 2:1966
14
14
  class BundledIdentifier < SingleIdentifier
15
+ include RootIdentity
16
+
15
17
  attribute :identifiers, ::Pubid::Identifier, collection: true,
16
18
  polymorphic: true
17
19
  attribute :separators, :string, collection: true # Separators between identifiers
@@ -6,6 +6,8 @@ module Pubid
6
6
  # Consolidated Identifier - contains base document plus supplements
7
7
  # Example: "BS 4592-0:2006+A1:2012" = [BS 4592-0:2006, Amendment 1:2012]
8
8
  class ConsolidatedIdentifier < SingleIdentifier
9
+ include RootIdentity
10
+
9
11
  attribute :identifiers, ::Pubid::Identifier, polymorphic: true,
10
12
  collection: true
11
13
 
@@ -19,11 +21,11 @@ module Pubid
19
21
  # Append supplement info to URN
20
22
  identifiers[1..].each do |id|
21
23
  if id.is_a?(Amendment)
22
- urn += ":amd:#{id.amendment_number}"
23
- urn += ":#{id.amendment_year}" if id.amendment_year
24
+ urn += ":amd:#{id.number}"
25
+ urn += ":#{id.year}" if id.year
24
26
  elsif id.is_a?(Corrigendum)
25
- urn += ":cor:#{id.corrigendum_number}"
26
- urn += ":#{id.corrigendum_year}" if id.corrigendum_year
27
+ urn += ":cor:#{id.number}"
28
+ urn += ":#{id.year}" if id.year
27
29
  end
28
30
  end
29
31
  urn
@@ -34,30 +36,25 @@ module Pubid
34
36
  identifiers&.first&.publisher
35
37
  end
36
38
 
37
- def number
38
- identifiers&.first&.number
39
- end
40
-
41
- def year
42
- base = identifiers&.first
43
- base.year if base&.class&.attributes&.key?(:year)
44
- end
45
-
46
- def date
47
- base = identifiers&.first
48
- base.date if base&.class&.attributes&.key?(:date)
49
- end
50
-
51
- def parts
52
- base = identifiers&.first
53
- base.parts if base&.class&.attributes&.key?(:parts)
54
- end
55
-
56
- def part
57
- base = identifiers&.first
58
- base.part if base&.class&.attributes&.key?(:part)
39
+ # See Identifiers::Amendment.compact_hash: the publisher is derived
40
+ # from the first member and must stay off the wrapper's own wire entry,
41
+ # or from_hash round-trips regrow it.
42
+ def self.compact_hash(_model, hash)
43
+ hash.delete("publisher")
59
44
  end
60
45
 
46
+ # `number`, `part`, `parts`, `date` and `year` are deliberately NOT
47
+ # delegated. They used to read `identifiers.first`, one level only, so
48
+ # they answered with the member's value when that member was a plain
49
+ # standard and with nil when it was itself a wrapper (an adoption) —
50
+ # the same accessor reporting two different things. They also shadowed
51
+ # real lutaml attributes, and the `date` one handed lutaml a foreign
52
+ # `Pubid::Components::Date` where BSI declares its own subclass, which
53
+ # is what made `to_hash` raise.
54
+ #
55
+ # A BSI wrapper owns no identity: `#root` carries it, recursively and
56
+ # for every layer. See `Identifiers::RootIdentity` and
57
+ # lib/pubid/bsi/CLAUDE.md.
61
58
  def type
62
59
  base = identifiers&.first
63
60
  base.type if base&.class&.attributes&.key?(:type)
@@ -7,14 +7,38 @@ module Pubid
7
7
  # Contains a base identifier plus corrigendum parameters
8
8
  class Corrigendum < SingleIdentifier
9
9
  attribute :base, ::Pubid::Identifier, polymorphic: true
10
- attribute :corrigendum_number, :string
11
- attribute :corrigendum_year, :integer
10
+ # The ordinal lives in `number` (inherited, already a `:string`), and
11
+ # the year in the inherited `date` component, exactly as on Amendment
12
+ # — see that class for why `date` is now safe to use: `#exclude`
13
+ # protects it via `supplement_date_attributes` below, so
14
+ # `exclude(:date)` drops only the standard's date. The flat scalar
15
+ # serialization rule renders a degenerate `date` as a bare `year:`
16
+ # key, so relaton still compares a plain String across supplement
17
+ # classes and flavors.
18
+ #
19
+ # The year is nil for a year-less corrigendum (`BS 1234:2015+C1`),
20
+ # which parses; `number` is in practice always set, because the grammar
21
+ # requires a digit after the `C` — the unnumbered `+C:2016` form that
22
+ # CEN spells `AC` does not parse in BSI at all.
12
23
  attribute :separator, :string, default: -> { "+" }
13
24
 
14
25
  def publisher
15
26
  base&.publisher
16
27
  end
17
28
 
29
+ # See Amendment.compact_hash: the publisher is borrowed from the base
30
+ # and must never appear on the supplement's own wire entry.
31
+ def self.compact_hash(_model, hash)
32
+ hash.delete("publisher")
33
+ end
34
+
35
+ # See the comment on `date` above: protects the corrigendum's own
36
+ # date from a bare `exclude(:date)`/`exclude(:year)`, which otherwise
37
+ # recurses into `base` and drops the standard's date too.
38
+ def self.supplement_date_attributes
39
+ %i[date]
40
+ end
41
+
18
42
  # Base document = the standard this corrigendum applies to, fully peeled.
19
43
  def base_document
20
44
  base&.base_document || self
@@ -25,19 +49,12 @@ module Pubid
25
49
  base || self
26
50
  end
27
51
 
28
- # Uniform supplement interface (shared with Amendment) so callers need
29
- # not special-case the class.
52
+ # Names the supplement class, so callers need not special-case it. The
53
+ # ordinal and the year need no such method: Amendment and Corrigendum —
54
+ # in BSI and in CEN — all declare them as `number` and `year`.
30
55
  def supplement_type
31
56
  :corrigendum
32
57
  end
33
-
34
- def supplement_number
35
- corrigendum_number
36
- end
37
-
38
- def supplement_year
39
- corrigendum_year
40
- end
41
58
  end
42
59
  end
43
60
  end
@@ -10,6 +10,8 @@ module Pubid
10
10
  # 2. "ExComm" (abbreviated form)
11
11
  # 3. "ExComm (Fire)" (with optional topic suffix)
12
12
  class ExpertCommentary < SingleIdentifier
13
+ include RootIdentity
14
+
13
15
  attribute :base, ::Pubid::Identifier, polymorphic: true
14
16
  attribute :format, :string # "full", "abbr", "abbr_with_topic"
15
17
  attribute :topic, :string # e.g., "Fire"
@@ -18,13 +20,10 @@ module Pubid
18
20
  base&.publisher
19
21
  end
20
22
 
21
- def number
22
- base&.number
23
- end
24
-
25
- def year
26
- base&.year
27
- end
23
+ # `number` and `year` are deliberately NOT delegated — a BSI wrapper
24
+ # owns no identity, and a one-level delegation answered with nil
25
+ # whenever `base` was itself a wrapper. `#root` carries identity for
26
+ # every layer. See `Identifiers::RootIdentity` and lib/pubid/bsi/CLAUDE.md.
28
27
 
29
28
  # Base document = the commented standard, fully peeled.
30
29
  def base_document
@@ -6,7 +6,7 @@ module Pubid
6
6
  # BSI Handbook
7
7
  # Examples: "Handbook 17:1963", "HB 10146:1998"
8
8
  class Handbook < SingleIdentifier
9
- attribute :date, Bsi::Components::Date
9
+
10
10
  attribute :original_abbr, :string # Preserve "Handbook" or "HB"
11
11
 
12
12
  def self.type
@@ -6,8 +6,24 @@ module Pubid
6
6
  # National Annex (NA) identifier
7
7
  # Can have own supplements: "NA+A1:2012 to BASE"
8
8
  class NationalAnnex < SingleIdentifier
9
- attribute :na_supplements, ::Pubid::Identifier, polymorphic: true, collection: true # Supplements on the NA itself
10
- attribute :base_doc, ::Pubid::Identifier, polymorphic: true # The identifier after "to"
9
+ include RootIdentity
10
+
11
+ # Supplements on the NA itself. `initialize_empty: true` because the
12
+ # builder always passes an array (empty when the grammar matched no
13
+ # supplement) while `from_hash` never sets the attribute at all — the
14
+ # serialized hash omits an empty collection — so a parsed NA held `[]`
15
+ # and a deserialized one `nil`, and the two were not `==`. `#matches?`
16
+ # is `exclude(*ignore) == other.exclude(*ignore)`, so every index lookup
17
+ # of a supplement-less NA silently returned nothing. The `collection:
18
+ # true` lesson in CLAUDE.md, and the same one-line remedy.
19
+ attribute :na_supplements, ::Pubid::Identifier, polymorphic: true,
20
+ collection: true,
21
+ initialize_empty: true
22
+ # The identifier after "to", under the uniform parent accessor. `#root`
23
+ # and `#base_document` are inherited and walk it, so the delegating
24
+ # readers this class used to carry are gone — they shadowed real lutaml
25
+ # accessors and reached only one level down.
26
+ attribute :base, ::Pubid::Identifier, polymorphic: true
11
27
 
12
28
  TYPED_STAGES = [
13
29
  Pubid::Components::TypedStage.new(
@@ -24,24 +40,6 @@ module Pubid
24
40
  { key: :na,
25
41
  web: :national_annex, title: "National Annex", short: "NA" }
26
42
  end
27
-
28
- # Delegation methods to access wrapped base_doc attributes
29
- def number
30
- base_doc&.number || super
31
- end
32
-
33
- def date
34
- base_doc&.date || super
35
- end
36
-
37
- def part
38
- base_doc&.part || super
39
- end
40
-
41
- def subpart
42
- base_doc&.subpart || super
43
- end
44
-
45
43
  end
46
44
  end
47
45
  end
@@ -6,7 +6,7 @@ module Pubid
6
6
  # BSI Practice Guide (PP - Published Practice)
7
7
  # Examples: "PP 888:1982", "PP 7307:1986", "PP 7722:2006"
8
8
  class PracticeGuide < SingleIdentifier
9
- attribute :date, Bsi::Components::Date
9
+
10
10
 
11
11
  def self.type
12
12
  {
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Bsi
5
+ module Identifiers
6
+ # MR-slug hooks for a BSI wrapper that carries no identity of its own.
7
+ #
8
+ # The base `mr_number_with_part` and `mr_year` read the receiver's own
9
+ # `number`, `part`, `subpart` and `date`. A wrapper — an adoption, a
10
+ # national annex, a consolidated edition, an Expert-commentary volume —
11
+ # leaves all four nil and keeps the identity on the document it wraps, so
12
+ # those hooks produced a slug with no number at all: 158 BSI identifiers
13
+ # collapsed onto the bare `bs`, and `to_slug` is an output FILENAME.
14
+ #
15
+ # `#root` and not `base`: a "DD ENV ISO 11079:1999" adopts a CEN
16
+ # EuropeanPrestandard, which is itself a wrapper around the ISO standard,
17
+ # so one level down is not far enough. `#root` recurses, and for a
18
+ # non-wrapper it is `self` — hence the `equal?` guard, which sends an
19
+ # ordinary identifier to the base hooks unchanged.
20
+ module RootIdentity
21
+ def mr_number_with_part
22
+ root.equal?(self) ? super : root.mr_number_with_part
23
+ end
24
+
25
+ def mr_year
26
+ root.equal?(self) ? super : root.mr_year
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -13,6 +13,8 @@ module Pubid
13
13
  # BS ISO 20400 + BS ISO 44001+BS ISO 44002
14
14
  # BS ISO 9001+BS ISO 14001
15
15
  class Set < SingleIdentifier
16
+ include RootIdentity
17
+
16
18
  attribute :identifiers, ::Pubid::Identifier, collection: true,
17
19
  polymorphic: true
18
20
  attribute :separators, :string, collection: true # Should all be " + "
@@ -9,6 +9,8 @@ module Pubid
9
9
  # Forward: "BS 1000:Supplement No. 1:1972", "BS 1722-1 Supplement No. 1:1974"
10
10
  # Reverse: "Supplement No. 1 (1970) to BS 1831:1969"
11
11
  class SupplementDocument < SingleIdentifier
12
+ include RootIdentity
13
+
12
14
  attribute :base, ::Pubid::Identifier, polymorphic: true
13
15
  attribute :supplement_number, :string
14
16
  attribute :supplement_year, :integer
@@ -35,6 +35,7 @@ module Pubid
35
35
  autoload :PublishedDocument, "#{__dir__}/identifiers/published_document"
36
36
  autoload :PubliclyAvailableSpecification,
37
37
  "#{__dir__}/identifiers/publicly_available_specification"
38
+ autoload :RootIdentity, "#{__dir__}/identifiers/root_identity"
38
39
  autoload :Section, "#{__dir__}/identifiers/section"
39
40
  autoload :Set, "#{__dir__}/identifiers/set"
40
41
  autoload :StandaloneAmendment,
@@ -412,49 +412,49 @@ module Pubid
412
412
  rule(:addendum_document) do
413
413
  # With multi-letter flex type prefix (CECC, E9111, etc.)
414
414
  (bs.as(:publisher) >> space >> multi_letter_prefix.as(:flex_prefix) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
415
- (colon | space).as(:add_sep) >> str("Addendum") >> space >> str("No.").as(:add_no_prefix) >> space >>
415
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >> str("No.").as(:add_no_prefix) >> space.repeat(1) >>
416
416
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
417
417
  4).as(:addendum_year)
418
418
  ).as(:addendum_document) |
419
419
  (bs.as(:publisher) >> space >> multi_letter_prefix.as(:flex_prefix) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
420
- (colon | space).as(:add_sep) >> str("Addendum") >> space >>
420
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >>
421
421
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
422
422
  4).as(:addendum_year)
423
423
  ).as(:addendum_document) |
424
424
  # With single-letter flex type prefix (M, etc.)
425
425
  (bs.as(:publisher) >> space >> single_letter_prefix.as(:flex_prefix) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
426
- (colon | space).as(:add_sep) >> str("Addendum") >> space >> str("No.").as(:add_no_prefix) >> space >>
426
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >> str("No.").as(:add_no_prefix) >> space.repeat(1) >>
427
427
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
428
428
  4).as(:addendum_year)
429
429
  ).as(:addendum_document) |
430
430
  (bs.as(:publisher) >> space >> single_letter_prefix.as(:flex_prefix) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
431
- (colon | space).as(:add_sep) >> str("Addendum") >> space >>
431
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >>
432
432
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
433
433
  4).as(:addendum_year)
434
434
  ).as(:addendum_document) |
435
435
  # With year before Addendum and SPACE separator: "BS NUMBER:YEAR Addendum No. N:YEAR"
436
436
  (bs.as(:publisher) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
437
437
  colon >> digit.repeat(4,
438
- 4).as(:base_year) >> space.as(:add_sep) >> str("Addendum") >> space >> str("No.").as(:add_no_prefix) >> space >>
438
+ 4).as(:base_year) >> space.as(:add_sep) >> str("Addendum") >> space.repeat(1) >> str("No.").as(:add_no_prefix) >> space.repeat(1) >>
439
439
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
440
440
  4).as(:addendum_year)
441
441
  ).as(:addendum_document) |
442
442
  # With year before Addendum and COLON separator: "BS NUMBER:YEAR:Addendum No. N:YEAR"
443
443
  (bs.as(:publisher) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
444
444
  colon >> digit.repeat(4,
445
- 4).as(:base_year) >> colon.as(:add_sep) >> str("Addendum") >> space >> str("No.").as(:add_no_prefix) >> space >>
445
+ 4).as(:base_year) >> colon.as(:add_sep) >> str("Addendum") >> space.repeat(1) >> str("No.").as(:add_no_prefix) >> space.repeat(1) >>
446
446
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
447
447
  4).as(:addendum_year)
448
448
  ).as(:addendum_document) |
449
449
  # Without flex type prefix - with "No." prefix
450
450
  (bs.as(:publisher) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
451
- (colon | space).as(:add_sep) >> str("Addendum") >> space >> str("No.").as(:add_no_prefix) >> space >>
451
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >> str("No.").as(:add_no_prefix) >> space.repeat(1) >>
452
452
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
453
453
  4).as(:addendum_year)
454
454
  ).as(:addendum_document) |
455
455
  # Without "No." prefix
456
456
  (bs.as(:publisher) >> space >> number.as(:number) >> iteration.as(:iteration) >> parts.as(:parts) >>
457
- (colon | space).as(:add_sep) >> str("Addendum") >> space >>
457
+ (colon | space).as(:add_sep) >> str("Addendum") >> space.repeat(1) >>
458
458
  digits.as(:addendum_number) >> colon >> digit.repeat(4,
459
459
  4).as(:addendum_year)
460
460
  ).as(:addendum_document)
@@ -212,7 +212,7 @@ module Pubid
212
212
  end
213
213
 
214
214
  result = prefix
215
- result += " #{id.adopted}" if id.adopted
215
+ result += " #{id.base}" if id.base
216
216
  result += " ED#{id.edition}" if id.edition
217
217
 
218
218
  result += " (R#{id.reaffirmation_year})" if id.reaffirmation_year
@@ -237,7 +237,7 @@ module Pubid
237
237
  end
238
238
 
239
239
  result = prefix
240
- result += " #{id.adopted}" if id.adopted
240
+ result += " #{id.base}" if id.base
241
241
  result += " ED#{id.edition}" if id.edition
242
242
 
243
243
  result += " (R#{id.reaffirmation_year})" if id.reaffirmation_year
@@ -303,16 +303,16 @@ module Pubid
303
303
 
304
304
  if id.amd_suffix_form
305
305
  # Trailing " AMD5" / " AMD AA" suffix form
306
- suffix = if id.amendment_number&.match?(/^[A-Z]+$/)
307
- " AMD #{id.amendment_number}"
306
+ suffix = if id.number&.match?(/^[A-Z]+$/)
307
+ " AMD #{id.number}"
308
308
  else
309
- " AMD#{id.amendment_number}"
309
+ " AMD#{id.number}"
310
310
  end
311
311
  base.empty? ? suffix.lstrip : "#{base}#{suffix}"
312
312
  else
313
313
  # Compact "+A5" / "/A5" join form, with an optional ":year"
314
- compact = "#{id.separator}A#{id.amendment_number}"
315
- compact += ":#{id.amendment_year}" if id.amendment_year
314
+ compact = "#{id.separator}A#{id.number}"
315
+ compact += ":#{id.year}" if id.year
316
316
  "#{base}#{compact}"
317
317
  end
318
318
  end
@@ -422,21 +422,21 @@ module Pubid
422
422
  id.identifiers[1..].each do |idd|
423
423
  if idd.is_a?(Identifiers::Amendment)
424
424
  if idd.amd_suffix_form
425
- result += if idd.amendment_number&.match?(/^[A-Z]+$/)
426
- " AMD #{idd.amendment_number}"
425
+ result += if idd.number&.match?(/^[A-Z]+$/)
426
+ " AMD #{idd.number}"
427
427
  else
428
- " AMD#{idd.amendment_number}"
428
+ " AMD#{idd.number}"
429
429
  end
430
430
  else
431
431
  sep = idd.separator || "+"
432
- result += "#{sep}A#{idd.amendment_number}"
433
- result += ":#{idd.amendment_year}" if idd.amendment_year
432
+ result += "#{sep}A#{idd.number}"
433
+ result += ":#{idd.year}" if idd.year
434
434
  end
435
435
  elsif idd.is_a?(Identifiers::Corrigendum)
436
436
  sep = idd.separator || "+"
437
437
  result += "#{sep}C"
438
- result += idd.corrigendum_number.to_s if idd.corrigendum_number
439
- result += ":#{idd.corrigendum_year}" if idd.corrigendum_year
438
+ result += idd.number.to_s if idd.number
439
+ result += ":#{idd.year}" if idd.year
440
440
  else
441
441
  result += idd.to_s
442
442
  end
@@ -485,8 +485,8 @@ module Pubid
485
485
  def render_corrigendum(id)
486
486
  result = id.base ? id.base.to_s : ""
487
487
  result += "#{id.separator}C"
488
- result += id.corrigendum_number.to_s if id.corrigendum_number
489
- result += ":#{id.corrigendum_year}" if id.corrigendum_year
488
+ result += id.number.to_s if id.number
489
+ result += ":#{id.year}" if id.year
490
490
  result
491
491
  end
492
492
 
@@ -753,17 +753,17 @@ module Pubid
753
753
  if id.na_supplements&.any?
754
754
  id.na_supplements.each do |supp|
755
755
  if supp.is_a?(Identifiers::Amendment)
756
- result += "+A#{supp.amendment_number}:#{supp.amendment_year}"
756
+ result += "+A#{supp.number}:#{supp.year}"
757
757
  elsif supp.is_a?(Identifiers::Corrigendum)
758
- result += "+C#{supp.corrigendum_number}:#{supp.corrigendum_year}"
758
+ result += "+C#{supp.number}:#{supp.year}"
759
759
  end
760
760
  end
761
761
  end
762
762
 
763
763
  result += " to "
764
764
 
765
- result += if id.base_doc
766
- id.base_doc.to_s
765
+ result += if id.base
766
+ id.base.to_s
767
767
  else
768
768
  render_single_identifier(id)
769
769
  end
@@ -50,9 +50,18 @@ module Pubid
50
50
  attribute :part, :string
51
51
  attribute :subpart, :string
52
52
  attribute :second_number, Bsi::Components::Code # For collections like PAS 2035/2030
53
- attribute :date, Bsi::Components::Date
53
+ # The shared component, not Bsi::Components::Date: the adoption wrappers
54
+ # (AdoptedEuropeanNorm, AdoptedInternationalStandard) delegate `date` to
55
+ # the foreign identifier they wrap, whose date is a plain
56
+ # ::Pubid::Components::Date — the empty BSI subclass rejected it and
57
+ # to_hash/from_hash raised on every "BS EN …"/"BS ISO …" (pubid#379,
58
+ # the same shape as the bsi-set-cross-flavor-type fix).
59
+ attribute :date, ::Pubid::Components::Date
54
60
  attribute :stage, Pubid::Components::Stage
55
- attribute :type, Bsi::Components::Type
61
+ # Shared Type too: the leaf builders reach for ::Pubid::Components::Type
62
+ # (e.g. a default materialized from self.type), which the empty BSI
63
+ # subclass rejected — "BIP 2225:2022" et al. failed to_hash on `type`.
64
+ attribute :type, ::Pubid::Components::Type
56
65
  attribute :typed_stage, Pubid::Components::TypedStage
57
66
  attribute :edition, :string
58
67
  attribute :month, :integer