pubid 2.0.0.pre.alpha.12 → 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 (227) 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/identifiers/addendum_document.rb +2 -0
  40. data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +6 -54
  41. data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +5 -22
  42. data/lib/pubid/bsi/identifiers/amendment.rb +36 -12
  43. data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -0
  44. data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +23 -26
  45. data/lib/pubid/bsi/identifiers/corrigendum.rb +29 -12
  46. data/lib/pubid/bsi/identifiers/expert_commentary.rb +6 -7
  47. data/lib/pubid/bsi/identifiers/national_annex.rb +18 -20
  48. data/lib/pubid/bsi/identifiers/root_identity.rb +31 -0
  49. data/lib/pubid/bsi/identifiers/set.rb +2 -0
  50. data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -0
  51. data/lib/pubid/bsi/identifiers.rb +1 -0
  52. data/lib/pubid/bsi/parser.rb +8 -8
  53. data/lib/pubid/bsi/renderer.rb +20 -20
  54. data/lib/pubid/bsi/urn_generator.rb +28 -18
  55. data/lib/pubid/builder/base.rb +27 -0
  56. data/lib/pubid/calconnect/builder.rb +2 -0
  57. data/lib/pubid/calconnect/identifier.rb +4 -0
  58. data/lib/pubid/ccsds/builder.rb +2 -0
  59. data/lib/pubid/ccsds/identifier.rb +8 -0
  60. data/lib/pubid/cen_cenelec/CLAUDE.md +59 -0
  61. data/lib/pubid/cen_cenelec/builder.rb +6 -1
  62. data/lib/pubid/cen_cenelec/identifier.rb +10 -27
  63. data/lib/pubid/cen_cenelec/identifiers/amendment.rb +3 -10
  64. data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +3 -10
  65. data/lib/pubid/cen_cenelec/parser.rb +11 -3
  66. data/lib/pubid/cie/CLAUDE.md +58 -0
  67. data/lib/pubid/cie/builder.rb +2 -0
  68. data/lib/pubid/cie/components/language.rb +2 -0
  69. data/lib/pubid/components/adoption.rb +2 -0
  70. data/lib/pubid/components/code.rb +2 -0
  71. data/lib/pubid/components/date.rb +8 -6
  72. data/lib/pubid/components/edition.rb +2 -0
  73. data/lib/pubid/components/iteration.rb +2 -0
  74. data/lib/pubid/components/language.rb +2 -0
  75. data/lib/pubid/components/locality.rb +2 -0
  76. data/lib/pubid/components/publisher.rb +2 -0
  77. data/lib/pubid/components/relationship.rb +2 -0
  78. data/lib/pubid/components/stage.rb +2 -0
  79. data/lib/pubid/components/supplement.rb +2 -0
  80. data/lib/pubid/components/type.rb +2 -0
  81. data/lib/pubid/components/typed_stage.rb +8 -0
  82. data/lib/pubid/csa/CLAUDE.md +41 -0
  83. data/lib/pubid/csa/builder.rb +2 -0
  84. data/lib/pubid/csa/identifier.rb +13 -1
  85. data/lib/pubid/csa/renderer.rb +12 -12
  86. data/lib/pubid/csa/single_identifier.rb +17 -0
  87. data/lib/pubid/doi/builder.rb +2 -0
  88. data/lib/pubid/easc/builder.rb +2 -0
  89. data/lib/pubid/ecma/CLAUDE.md +28 -0
  90. data/lib/pubid/ecma/builder.rb +2 -0
  91. data/lib/pubid/ecma/identifier.rb +7 -0
  92. data/lib/pubid/etsi/CLAUDE.md +34 -0
  93. data/lib/pubid/etsi/builder.rb +2 -0
  94. data/lib/pubid/etsi/components/code.rb +6 -0
  95. data/lib/pubid/etsi/components/version.rb +2 -0
  96. data/lib/pubid/etsi/identifiers/etsi_standard.rb +7 -0
  97. data/lib/pubid/evs/CLAUDE.md +58 -0
  98. data/lib/pubid/evs/builder.rb +2 -0
  99. data/lib/pubid/gb/CLAUDE.md +140 -0
  100. data/lib/pubid/gb/builder.rb +7 -2
  101. data/lib/pubid/gb/identifier.rb +5 -3
  102. data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
  103. data/lib/pubid/gb/identifiers.rb +1 -0
  104. data/lib/pubid/gb/renderer.rb +0 -1
  105. data/lib/pubid/gost/CLAUDE.md +64 -0
  106. data/lib/pubid/gost/builder.rb +3 -1
  107. data/lib/pubid/gost/identifier.rb +5 -0
  108. data/lib/pubid/iala/CLAUDE.md +82 -0
  109. data/lib/pubid/iala/builder.rb +2 -0
  110. data/lib/pubid/iana/CLAUDE.md +7 -0
  111. data/lib/pubid/iana/builder.rb +2 -0
  112. data/lib/pubid/identifier.rb +161 -17
  113. data/lib/pubid/idf/builder.rb +6 -1
  114. data/lib/pubid/idf/identifier.rb +5 -0
  115. data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
  116. data/lib/pubid/idf/identifiers.rb +1 -0
  117. data/lib/pubid/iec/CLAUDE.md +31 -0
  118. data/lib/pubid/iec/builder.rb +7 -1
  119. data/lib/pubid/iec/components/consolidated_amendment.rb +4 -0
  120. data/lib/pubid/iec/components/sheet.rb +2 -0
  121. data/lib/pubid/iec/components/trf_info.rb +2 -0
  122. data/lib/pubid/iec/components/vap_suffix.rb +2 -0
  123. data/lib/pubid/iec/identifier.rb +7 -2
  124. data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
  125. data/lib/pubid/iec/identifiers.rb +1 -0
  126. data/lib/pubid/iec/renderer.rb +0 -1
  127. data/lib/pubid/iec/urn_generator.rb +9 -1
  128. data/lib/pubid/iec/urn_parser.rb +3 -2
  129. data/lib/pubid/ieee/CLAUDE.md +97 -0
  130. data/lib/pubid/ieee/builder.rb +134 -5
  131. data/lib/pubid/ieee/components/code.rb +2 -0
  132. data/lib/pubid/ieee/components/draft.rb +35 -2
  133. data/lib/pubid/ieee/components/typed_stage.rb +2 -0
  134. data/lib/pubid/ieee/identifiers/base.rb +20 -0
  135. data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
  136. data/lib/pubid/ieee/identifiers/joint_development.rb +17 -10
  137. data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
  138. data/lib/pubid/ieee/parser.rb +134 -21
  139. data/lib/pubid/ieee/renderer.rb +40 -7
  140. data/lib/pubid/ieee/urn_generator.rb +31 -0
  141. data/lib/pubid/ietf/CLAUDE.md +7 -0
  142. data/lib/pubid/ietf/builder.rb +2 -0
  143. data/lib/pubid/iho/builder.rb +2 -0
  144. data/lib/pubid/isbn/builder.rb +2 -0
  145. data/lib/pubid/iso/CLAUDE.md +47 -0
  146. data/lib/pubid/iso/builder.rb +19 -5
  147. data/lib/pubid/iso/components/publisher.rb +2 -0
  148. data/lib/pubid/iso/identifier.rb +6 -14
  149. data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
  150. data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
  151. data/lib/pubid/iso/identifiers.rb +1 -0
  152. data/lib/pubid/iso/rendering_style.rb +0 -1
  153. data/lib/pubid/itu/CLAUDE.md +69 -0
  154. data/lib/pubid/itu/builder.rb +2 -0
  155. data/lib/pubid/itu/components/code.rb +2 -0
  156. data/lib/pubid/itu/components/designation.rb +2 -0
  157. data/lib/pubid/itu/components/sector.rb +2 -0
  158. data/lib/pubid/itu/components/series.rb +2 -0
  159. data/lib/pubid/itu/identifiers/supplement.rb +15 -0
  160. data/lib/pubid/jcgm/CLAUDE.md +7 -0
  161. data/lib/pubid/jcgm/builder.rb +2 -0
  162. data/lib/pubid/jcgm/components/publisher.rb +2 -0
  163. data/lib/pubid/jis/builder.rb +5 -1
  164. data/lib/pubid/jis/identifier.rb +5 -17
  165. data/lib/pubid/jis/identifiers/all_parts.rb +19 -0
  166. data/lib/pubid/jis/identifiers.rb +1 -0
  167. data/lib/pubid/jis/renderer.rb +0 -2
  168. data/lib/pubid/jis/urn_generator.rb +0 -1
  169. data/lib/pubid/nist/CLAUDE.md +56 -0
  170. data/lib/pubid/nist/builder.rb +2 -0
  171. data/lib/pubid/nist/components/edition.rb +2 -0
  172. data/lib/pubid/nist/components/issue_number.rb +2 -0
  173. data/lib/pubid/nist/components/part.rb +2 -0
  174. data/lib/pubid/nist/components/stage.rb +2 -0
  175. data/lib/pubid/nist/components/supplement.rb +2 -0
  176. data/lib/pubid/nist/components/translation.rb +2 -0
  177. data/lib/pubid/nist/components/update.rb +2 -0
  178. data/lib/pubid/nist/components/version.rb +2 -0
  179. data/lib/pubid/nist/components/volume.rb +2 -0
  180. data/lib/pubid/nist/identifiers/base.rb +24 -3
  181. data/lib/pubid/nist/parser.rb +15 -2
  182. data/lib/pubid/nist/preprocessor.rb +53 -2
  183. data/lib/pubid/oasis/CLAUDE.md +19 -0
  184. data/lib/pubid/oasis/builder.rb +2 -0
  185. data/lib/pubid/oasis/identifier.rb +19 -0
  186. data/lib/pubid/ogc/CLAUDE.md +34 -0
  187. data/lib/pubid/ogc/builder.rb +2 -0
  188. data/lib/pubid/ogc/identifier.rb +11 -0
  189. data/lib/pubid/oiml/CLAUDE.md +189 -0
  190. data/lib/pubid/oiml/builder.rb +20 -0
  191. data/lib/pubid/oiml/components/code.rb +6 -0
  192. data/lib/pubid/oiml/identifier.rb +13 -0
  193. data/lib/pubid/oiml/identifiers/annex.rb +4 -0
  194. data/lib/pubid/oiml/identifiers/certification_system.rb +34 -0
  195. data/lib/pubid/oiml/identifiers/code_number.rb +8 -0
  196. data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
  197. data/lib/pubid/oiml/identifiers.rb +2 -0
  198. data/lib/pubid/oiml/parser.rb +35 -4
  199. data/lib/pubid/oiml/renderer.rb +23 -1
  200. data/lib/pubid/oiml/single_identifier.rb +4 -0
  201. data/lib/pubid/oiml/supplement_identifier.rb +7 -0
  202. data/lib/pubid/oiml/urn_generator.rb +28 -0
  203. data/lib/pubid/oiml.rb +5 -0
  204. data/lib/pubid/omg/CLAUDE.md +15 -0
  205. data/lib/pubid/omg/builder.rb +2 -0
  206. data/lib/pubid/parser/grammar.rb +23 -0
  207. data/lib/pubid/plateau/builder.rb +2 -0
  208. data/lib/pubid/plateau/identifiers/base.rb +4 -0
  209. data/lib/pubid/plateau/supplement_identifier.rb +14 -2
  210. data/lib/pubid/plateau/urn_generator.rb +7 -1
  211. data/lib/pubid/renderers/human_readable.rb +0 -1
  212. data/lib/pubid/sae/builder.rb +2 -0
  213. data/lib/pubid/sae/components/date.rb +2 -0
  214. data/lib/pubid/sae/components/type.rb +2 -0
  215. data/lib/pubid/subset_match.rb +197 -0
  216. data/lib/pubid/tgpp/CLAUDE.md +43 -0
  217. data/lib/pubid/tgpp/builder.rb +2 -0
  218. data/lib/pubid/tgpp/identifier.rb +14 -0
  219. data/lib/pubid/type_resolver.rb +14 -2
  220. data/lib/pubid/un/builder.rb +2 -0
  221. data/lib/pubid/version.rb +1 -1
  222. data/lib/pubid/w3c/CLAUDE.md +7 -0
  223. data/lib/pubid/w3c/builder.rb +2 -0
  224. data/lib/pubid/xsf/CLAUDE.md +11 -0
  225. data/lib/pubid/xsf/builder.rb +2 -0
  226. data/lib/pubid.rb +16 -3
  227. metadata +44 -2
@@ -201,6 +201,18 @@ module Pubid
201
201
  hash
202
202
  end
203
203
 
204
+ # Three more IEEE discriminators the default %i[date year edition
205
+ # version] list misses, alongside `edition`/`year`: `revision` (e.g.
206
+ # "2" in "P802.16Rev2"); `reaffirmed` (the "(R2010)" year, read by
207
+ # renderer.rb and urn_generator.rb — two reaffirmations of the same
208
+ # standard failed to collapse under #to_all_parts/#===); and
209
+ # `edition_month`, the month half of "Edition N YYYY-MM"
210
+ # (renderer.rb's `"Edition #{edition} #{year}-#{edition_month}"`),
211
+ # which survived even though its sibling `year` is already stripped.
212
+ def self.all_parts_edition_keys
213
+ super + %i[revision reaffirmed edition_month]
214
+ end
215
+
204
216
  # Inverse of the to_hash compaction: expand a scalar `stage` back into the
205
217
  # `typed_stage` sub-hash (on a deep copy, recursively) before lutaml
206
218
  # deserializes, so nested bases rebuild their component too. `draft` needs
@@ -351,6 +363,14 @@ module Pubid
351
363
  def self.parse_single(input)
352
364
  # Apply legacy update_codes normalization first, before Parser's extensive preprocessing
353
365
  normalized = Core::UpdateCodes.apply(input, :ieee)
366
+ # UpdateCodes may introduce the "; " double-label separator
367
+ # ("ISO/IEC13210: 1994 (E) ANSI/IEEE Std ..." → the ISO/IEC and
368
+ # IEEE labels of one document), but the dispatch ran on the raw
369
+ # input - re-dispatch so the dual construction sees it.
370
+ if normalized != input && normalized.include?("; ")
371
+ result = PreParser.preprocess(normalized)
372
+ return build_dual(result.parts) if result.dispatch == :dual_semicolon
373
+ end
354
374
  parsed = Parser.parse(normalized) # Use class method for preprocessing
355
375
  builder = Builder.new(Identifier)
356
376
  # Pass the original input string to builder for context
@@ -26,6 +26,15 @@ module Pubid
26
26
  attribute :iec_year, :string # IEC year like "2013"
27
27
  attribute :date_info, :string # Date information like "(10/07)"
28
28
 
29
+ # `year_sep` only describes how a year is attached. When the reference
30
+ # omits the year, a subset match skips it: its default "-" would
31
+ # otherwise refuse a candidate printed with ":".
32
+ def subset_attribute_match?(name, mine, theirs)
33
+ return true if name == :year_sep && year.nil?
34
+
35
+ super
36
+ end
37
+
29
38
  # The printed IEC/IEEE number, rebuilt losslessly from the split
30
39
  # columns (the renderer/urn read this). Not stored — number/parts/
31
40
  # separators/year fully describe it, so the verbatim string never
@@ -42,6 +42,8 @@ module Pubid
42
42
  attribute :year, :string
43
43
  attribute :iso_stage, :string # For ISO stage if present
44
44
  attribute :ieee_draft, :string # For IEEE P/D notation if present
45
+ attribute :parenthetical_content, :string # e.g. the "(E)" edition
46
+ # marker of an ISO/IEC label
45
47
 
46
48
  # Accepts keyword args or a single positional hash (the base
47
49
  # #exclude/matching rebuild passes the latter) — see
@@ -138,14 +140,23 @@ module Pubid
138
140
  end
139
141
  end
140
142
 
141
- # Code (NO P prefix in ISO format, NO draft notation)
142
- code_str = code.to_s.gsub(/^P/, "")
143
+ # IEEE semantics: P = project (a draft); no P = a standard. The
144
+ # P-state is identity-bearing and prints as spelled — it is never
145
+ # added or stripped here.
146
+ code_str = code.to_s
143
147
  code_str += mark unless code_str.empty?
144
148
  parts << code_str if code_str && !code_str.empty?
145
149
 
146
- # Join with space and add year with colon
150
+ # Join with space and add year with colon; the joint stage-draft
151
+ # clause ("D=WD.5") rides after the year in the ISO-led print.
147
152
  result = parts.join(" ")
148
153
  result += ":#{year}" if year
154
+ result += "/#{ieee_draft}" if ieee_draft && ieee_draft.start_with?("D=")
155
+ # Only the language/edition marker ("(E)", "(E/F)") prints; a
156
+ # trailing relationship narrative is metadata, not identity.
157
+ if parenthetical_content&.match?(%r{\A[A-Z](?:\s*[/&]\s*[A-Z])*\z})
158
+ result += " (#{parenthetical_content})"
159
+ end
149
160
 
150
161
  result
151
162
  end
@@ -159,13 +170,9 @@ module Pubid
159
170
  # Publishers (slash-separated)
160
171
  parts << publishers.join("/") if publishers && !publishers.empty?
161
172
 
162
- # Build code part
163
- code_str = code.to_s.gsub(/^P/, "") # Remove any existing P first
164
-
165
- # Add P prefix for projects (IEEE format always shows P for drafts)
166
- if typed_stage&.project_status || type == "P"
167
- code_str = "P#{code_str}"
168
- end
173
+ # Build code part — the P-state prints as spelled (P = project
174
+ # draft; no P = standard). Never added or stripped.
175
+ code_str = code.to_s
169
176
 
170
177
  # Mark after the number, before the draft and the year
171
178
  code_str += mark unless code_str.empty?
@@ -33,10 +33,17 @@ module Pubid
33
33
  # object. The "P" is reflected in the typed_stage, not in the Code
34
34
  # component itself. Accepts keyword args or a single positional hash (the
35
35
  # base #exclude/matching rebuild passes the latter).
36
+ attribute :project_marker, :boolean, default: -> { false }
37
+
36
38
  def initialize(args = {}, **kwargs)
37
39
  args = kwargs.empty? ? args.dup : args.merge(kwargs)
38
- # If code is provided with "P" prefix (e.g., "P1234"), strip it
40
+ # If code is provided with "P" prefix (e.g., "P1234"), strip it.
41
+ # P = project (the document is a draft): the marker is
42
+ # identity-bearing, so its presence is remembered for the render —
43
+ # stripping the letter must not strip the fact (docs/
44
+ # IEEE-DRAFT-STAGES.md §1.1).
39
45
  if args[:code].is_a?(String) && args[:code].start_with?("P")
46
+ args[:project_marker] = true unless args.key?(:project_marker)
40
47
  args[:code] = args[:code][1..] # Strip leading "P"
41
48
  end
42
49
 
@@ -269,6 +269,10 @@ module Pubid
269
269
  rule(:draft_date) do
270
270
  # Enhanced to handle: ", Sept 2008" or " Sept 2008" or ", Month Year"
271
271
  ((comma | space) >> month_name.as(:month) >> space >> year_digits.as(:year)) |
272
+ # Space-separated bare year after the draft designator
273
+ # ("IEEE Draft Std P14764/D1 2004, Nov 2004" - the "D1 2004" draft
274
+ # carries its own year, with the print date still trailing).
275
+ (space >> year_digits.as(:year)) |
272
276
  # Numeric-month form ", 05 2007" / " 05 2007" — no text month name. The
273
277
  # year separator may be a dash: preprocessing (parser.rb ~1278) rewrites
274
278
  # a trailing " <digits> <year>" to "<digits>-<year>", so "05 2007"
@@ -302,6 +306,15 @@ module Pubid
302
306
  rule(:draft) do
303
307
  (draft_prefix >> draft_version.repeat(1, 2) >>
304
308
  (dot >> digits.as(:revision)).maybe >>
309
+ # The compound both-systems form (docs/IEEE-DRAFT-STAGES.md
310
+ # §1.3): "=DDIS.3" - IEEE draft ordinal = draft of the ISO/IEC
311
+ # stage, with its iteration. The glued "=DDIS3" / "=DDIS-3"
312
+ # spellings are accepted aliases of "=DDIS.3".
313
+ (str("=") >> (str("D") >> (str("FDIS") | str("CDV") | str("PWI") |
314
+ str("DIS") | str("WD") | str("NP") | str("CD")) |
315
+ (str("FDIS") | str("CDV") | str("PWI") | str("DIS") | str("WD") |
316
+ str("NP") | str("CD"))).as(:draft_iso_stage) >>
317
+ (dot | dash).maybe >> digits.as(:draft_iso_iteration).maybe).maybe >>
305
318
  draft_date.maybe).as(:draft)
306
319
  end
307
320
 
@@ -532,7 +545,9 @@ module Pubid
532
545
 
533
546
  # Additional parameters (inside parentheses)
534
547
  rule(:additional_parameters) do
535
- (space.maybe >> str("(") >> # Make space before '(' optional
548
+ (space.maybe >> str("(") >> space.maybe >> # Space before/after '(' optional
549
+ # ("( Revision of …)" - a crawl space inside the paren must not push
550
+ # the narrative to the catch-all and into the render).
536
551
  (reaffirmed |
537
552
  # Handle "Revision of IEEE Std ..." with optional space after Std
538
553
  (str("Revision of IEEE Std ") >> space.maybe >> match("[^)]").repeat(1).as(:revision_of)) |
@@ -582,15 +597,32 @@ module Pubid
582
597
  # ALSO handle: IEEE/CSA P844.1/293.1/D2 (CSA dual numbering)
583
598
  (str("ISO/IEC/IEEE") | str("ISO/IEEE") | str("IEC/IEEE") | str("IEEE/CSA")).as(:joint_publishers) >>
584
599
  space >>
585
- str("P") >> # P indicates IEEE-led
600
+ # P = project (the document is a draft): identity-bearing, so it
601
+ # is captured and preserved, never silently consumed.
602
+ str("P").as(:project_marker) >>
586
603
  digits.as(:number) >>
587
604
  ((dot | dash) >> digits.as(:part)).maybe >> # Optional part like .1 or -1
588
605
  # CSA dual numbering: /293.1 (second number)
589
606
  (slash >> digits >> (dot >> digits).maybe >> (dash >> digits.as(:draft_version)).maybe).maybe >>
590
607
  (
591
- # Variant 1: /D8 notation (original)
592
- (slash >> str("D") >> digits.as(:draft_version)) |
593
- # Variant 2: , CDV1 notation (comma before stage code)
608
+ # Variant 1b: the ordinal-less stage draft "D=CDV[:2020]" -
609
+ # D (draft) = CDV (the IEC stage it drafts). The year rides in
610
+ # the draft clause (a distinct key, so the builder keeps the
611
+ # date inside the designator).
612
+ (slash >> str("D") >> str("=") >>
613
+ (str("CDV") | str("FDIS") | str("PWI") | str("WD") |
614
+ str("NP") | str("DIS") | str("CD")).as(:draft_iso_stage) >>
615
+ (str(":") >> year_digits.as(:draft_stage_year)).maybe) |
616
+ # Variant 1: /D8 notation (original), with the compound
617
+ # both-systems suffix "=DDIS.3" (docs/IEEE-DRAFT-STAGES.md §1.3)
618
+ (slash >> str("D") >> digits.as(:draft_version) >>
619
+ (str("=") >> (str("D") >> (str("FDIS") | str("CDV") | str("PWI") |
620
+ str("DIS") | str("WD") | str("NP") | str("CD")) |
621
+ (str("FDIS") | str("CDV") | str("PWI") | str("DIS") | str("WD") |
622
+ str("NP") | str("CD"))).as(:draft_iso_stage) >>
623
+ (dot | dash).maybe >> digits.as(:draft_iso_iteration).maybe).maybe) |
624
+ # Variant 2: , CDV1 notation (comma before stage code) —
625
+ # the stage draft of the named ISO/IEC stage, its iteration
594
626
  (comma >> (str("CDV") | str("FDIS") | str("CD") | str("DIS")).as(:iec_stage) >> digits.maybe.as(:stage_iteration))
595
627
  ).maybe >>
596
628
  # Optional edition, from relaton's "/E-<n>" suffix normalized to
@@ -636,18 +668,30 @@ module Pubid
636
668
  str("").as(:iso_published) >> space.maybe)) |
637
669
  (staged_only.as(:joint_publishers) >> space >>
638
670
  iso_stage >> std_noise >> space)) >>
639
- str("P").maybe >> # optional project marker on the number
671
+ str("P").as(:project_marker).maybe >> # project marker (P =
672
+ # project/draft; its
673
+ # presence is identity)
640
674
  digits.as(:number) >>
641
- # part must not swallow the trailing year (year_digits.absent?)
642
- ((dot | dash) >> year_digits.absent? >> digits.as(:part)).maybe >>
675
+ # part must not swallow the trailing year (year_digits.absent?). A
676
+ # DASH-joined part is tagged (:part_dash) - the catalogue-printed
677
+ # joint form spells the part with a dash ("21451-7") where the ISO
678
+ # form uses the dot.
679
+ ((dot >> year_digits.absent? >> digits.as(:part)) |
680
+ (dash >> str("").as(:part_dash) >> year_digits.absent? >>
681
+ digits.as(:part))).maybe >>
643
682
  (
644
683
  (str(":") >> year_digits.as(:year)) |
684
+ # The dash-year (and ", Month YYYY" text date below) spellings are
685
+ # the catalogue-PRINTED joint form; tag them (:printed_form) so the
686
+ # builder routes to a Standard instead of the ISO joint reference.
645
687
  (dash >> year_digits.as(:year) >>
646
- (dash >> month_numeric.as(:month)).maybe) |
688
+ (dash >> month_numeric.as(:month)).maybe >>
689
+ str("").as(:printed_dash_year)) |
647
690
  # Trailing text date ", April 2015" / " April 2015" (build_joint_development
648
691
  # already reads :month/:year). No :year collision — the iso rule has no
649
692
  # other :year capture (edition uses :edition_year).
650
- ((comma | space) >> month_name.as(:month) >> space >> year_digits.as(:year))
693
+ ((comma | space) >> month_name.as(:month) >> space >>
694
+ year_digits.as(:year) >> str("").as(:printed_month_year))
651
695
  ).maybe >>
652
696
  # Optional /D<draft> tail. normalize_relaton_suffixes repositions the
653
697
  # historical "…/D-3-2017" onto the number as "…-2017/D3", so by the
@@ -658,11 +702,26 @@ module Pubid
658
702
  # keys (:draft_month/:draft_year), because a plain :month/:year
659
703
  # here collides with the date clause above and parslet drops the
660
704
  # subtree with a "Duplicate subtrees" warning.
705
+ # The ISO/IEC edition marker may sit between the year and an
706
+ # amendment/draft tail: "8802.11:2012 (E)/Amd 1-2014". A distinct
707
+ # key - a second :parameters capture here collides with the
708
+ # trailing parenthetical slot (parslet drops duplicate subtrees).
709
+ (space.maybe >> str("(") >>
710
+ match("[^)]").repeat(1).as(:edition_marker) >> str(")")).maybe >>
711
+ # The stage-draft clause of the ISO-led print: "/D=WD.5" -
712
+ # D (draft) = the ISO/IEC stage it drafts, with its iteration
713
+ # (docs/IEEE-DRAFT-STAGES.md §1.2).
714
+ ((slash >> str("D") >> str("=") >>
715
+ (str("FDIS") | str("FCD") | str("CDV") |
716
+ (str("DIS") >> digit.maybe) |
717
+ (str("CD") >> digit.maybe) |
718
+ str("WD") | str("PWI") | str("NP")).as(:draft_iso_stage) >>
719
+ (dot >> (digits >> match("[A-Za-z]").repeat(0, 1)).as(:draft_iso_iteration)).maybe) |
661
720
  (slash >> str("D") >> dash.maybe >>
662
721
  match('[0-9.]').repeat(1).as(:draft_version) >>
663
722
  (((comma | space) >> month_name.as(:draft_month) >> space >>
664
723
  year_digits.as(:draft_year)) |
665
- (comma >> year_digits.as(:draft_year))).maybe).maybe >>
724
+ (comma >> year_digits.as(:draft_year))).maybe).maybe).maybe >>
666
725
  # Optional amendment tail (pubid#317:
667
726
  # "8802-11:2012/Amd.1:2014(E)") - the flat tree keys reuse
668
727
  # build_flat_amendment.
@@ -678,7 +737,6 @@ module Pubid
678
737
  # The crawl sometimes spaces it ("…(E), January 2017" family,
679
738
  # pubid#216), and a relationship parenthetical may follow
680
739
  # ("…CD2 P15288-2013-09 (Revision of …)").
681
- space.maybe >> (str("(E)") | str("(F)")).maybe >>
682
740
  parenthetical.maybe
683
741
  end
684
742
 
@@ -692,6 +750,28 @@ module Pubid
692
750
  # the trailing `.FDIS` is the ISO stage, NOT a second part. Routes through
693
751
  # the same build_joint_development (via :joint_publishers/:iso_stage), so the
694
752
  # stage is modeled correctly and the numeric part stays separate.
753
+ # The stage-less ISO/IEC label with a colon year and the ISO/IEC
754
+ # edition marker: "ISO/IEC 13210:1994 (E)" - the ISO/IEC portion of a
755
+ # double-labeled standard. The (E) parenthetical is REQUIRED: without
756
+ # it this spelling is the ISO flavor's own form and must not be
757
+ # stolen. Routes through build_joint_development like every ISO-led
758
+ # joint reference.
759
+ rule(:joint_development_iso_iec_edition) do
760
+ str("ISO/IEC").as(:joint_publishers) >> space >>
761
+ str("").as(:iso_published) >>
762
+ str("P").as(:project_marker).maybe >>
763
+ digits.as(:number) >>
764
+ # The part may be dot- or dash-joined ("8802-9"); a dash-year
765
+ # ("1234-2012") is not a part.
766
+ # A distinct key: the iso-format route's :part_dash prints as a
767
+ # dot in the joint code (the "P24748.2:2018" convention); only
768
+ # the ISO/IEC LABEL keeps its printed dash.
769
+ ((dot | (dash >> str("").as(:iec_label_dash) >> year_digits.absent?)) >>
770
+ digits.as(:part)).maybe >>
771
+ (str(":") >> space.maybe >> year_digits.as(:year)) >>
772
+ space.maybe >> parenthetical
773
+ end
774
+
695
775
  rule(:joint_development_embedded_stage) do
696
776
  # publisher set mirrors joint_development_iso_format (incl. bare IEEE);
697
777
  # longest token first.
@@ -699,7 +779,9 @@ module Pubid
699
779
  str("ISO/IEEE") | str("IEC/IEEE") | str("IEEE/IEC") | str("ISO/IEC") |
700
780
  str("IEEE")).as(:joint_publishers) >>
701
781
  space >>
702
- str("P").maybe >> # optional project marker on the number
782
+ str("P").as(:project_marker).maybe >> # project marker (P =
783
+ # project/draft; its
784
+ # presence is identity)
703
785
  digits.as(:number) >>
704
786
  # optional numeric part (dot or dash); must not swallow a year
705
787
  ((dot | dash) >> year_digits.absent? >> digits.as(:part)).maybe >>
@@ -814,7 +896,17 @@ module Pubid
814
896
  ((space >> month_name.as(:trailing_month) >> space >> year_digits.as(:trailing_year)) |
815
897
  (space >> month_numeric.as(:trailing_month) >> (space | dash) >> year_digits.as(:trailing_year))).maybe >>
816
898
  draft.maybe >>
899
+ # A print date may trail a draft that carries its own date
900
+ # ("IEEE Unapproved Draft 24765/D1 2009, Oct 2009" - the draft's
901
+ # year is "2009", the print date is ", Oct 2009").
902
+ (comma >> space? >> month_name.as(:trailing_month) >> space >>
903
+ year_digits.as(:trailing_year)).maybe >>
817
904
  revision_suffix.maybe >>
905
+ # …and the print date may also trail the repositioned revision
906
+ # ("IEEE Unapproved Draft P802.16Rev2/D9a, March 2009" reaches the
907
+ # grammar as "…/D9a/R-2, March 2009" via normalize_revision_notation).
908
+ (comma >> space? >> month_name.as(:trailing_month) >> space >>
909
+ year_digits.as(:trailing_year)).maybe >>
818
910
  # Trailing corrigendum after the draft ("…/D2.0/Cor. 1", or
819
911
  # "…/D1.0, Dec 2007/Cor. 1" where the draft's own draft_date consumes
820
912
  # the date, leaving "/Cor. N"). The flat corrigendum+draft tree (no
@@ -899,10 +991,17 @@ module Pubid
899
991
  # Draft NESC pattern
900
992
  (str("Draft") >> space >> (str("NESC") | str("National Electrical Safety Code"))) |
901
993
  # Name-first pattern (NEW)
902
- (str("National Electrical Safety Code") >> str(",") >> space >> str("C2-"))
994
+ (str("National Electrical Safety Code") >> str(",") >> space >> str("C2-")) |
995
+ # Catalogue form: "IEEE Std YYYY NESC ..." / "IEEE Std YYYY National
996
+ # Electrical Safety Code" - the prefix is consumed here (the NESC
997
+ # renderer prepends "IEEE Std" itself) but must not fall through to
998
+ # the generic standard grammar, which would read YYYY as a number.
999
+ (str("IEEE") >> space >> str("Std") >> space >> year_digits >> space >>
1000
+ (str("NESC") | str("National Electrical Safety Code")))
903
1001
  ).present? >>
904
1002
  # Delegate to NESC parser if pattern detected
905
- Nesc::Parser.new.nesc_identifier.as(:nesc)
1003
+ ((str("IEEE") >> space >> str("Std") >> space).maybe >>
1004
+ Nesc::Parser.new.nesc_identifier).as(:nesc)
906
1005
  end
907
1006
 
908
1007
  # Draft notation for PSI (e.g., /D2, /D3, rawbib's /D-2)
@@ -1056,13 +1155,13 @@ module Pubid
1056
1155
  str("CSA") >> space >>
1057
1156
  # CSA number formats (various patterns observed)
1058
1157
  (
1059
- # Format 1: C22.2 No. 293.1-17 (with NO.)
1060
- (str("C") >> digit.repeat(2) >> dot >> digit >> space >> str("No") >> dot >> space >>
1158
+ # Format 1: C22.2 No. 293.1-17 (with NO., either case)
1159
+ (str("C") >> digit.repeat(2) >> dot >> digit >> space >> (str("No") | str("NO")) >> dot >> space >>
1061
1160
  match("[0-9.]").repeat(1) >> (dash | str(":")) >> digit.repeat(2)) |
1062
1161
  # Format 2: C293.2-17 (without NO., dash year)
1063
1162
  (str("C") >> match("[0-9.]").repeat(1) >> dash >> digit.repeat(2)) |
1064
- # Format 3: C22.2 No. 293.3:19 (with NO., colon year)
1065
- (str("C") >> digit.repeat(2) >> dot >> digit >> space >> str("No") >> dot >> space >>
1163
+ # Format 3: C22.2 No. 293.3:19 (with NO., either case, colon year)
1164
+ (str("C") >> digit.repeat(2) >> dot >> digit >> space >> (str("No") | str("NO")) >> dot >> space >>
1066
1165
  match("[0-9.]").repeat(1) >> str(":") >> digit.repeat(2)) |
1067
1166
  # Format 4: C293.4:19 (without NO., colon year)
1068
1167
  (str("C") >> match("[0-9.]").repeat(1) >> str(":") >> digit.repeat(2))
@@ -1085,6 +1184,8 @@ module Pubid
1085
1184
  conformance_identifier | # NEW: Try conformance identifier before generic patterns
1086
1185
  joint_development_ieee_format |
1087
1186
  joint_development_iso_format |
1187
+ joint_development_iso_iec_edition | # stage-less ISO/IEC label with (E) - double-label portion
1188
+
1088
1189
  joint_development_embedded_stage | # stage-LAST embedded form (before generic)
1089
1190
  iec_ieee_copublished |
1090
1191
  number_first_identifier |
@@ -1110,6 +1211,15 @@ module Pubid
1110
1211
  # never collides with the base -YYYY identity year (see
1111
1212
  # ieee_p_identifier). The builder promotes it only when no base year.
1112
1213
  trailing_month_year.maybe >>
1214
+ # The ASHRAE joint suffix may also trail a draft+date
1215
+ # ("IEEE P1635/D13, December, 2017/ASHRAE Guideline 21"), so a
1216
+ # second slot catches it after the date.
1217
+ ashrae_copub.maybe >>
1218
+ # A bracketed narrative may sit between the base and a trailing
1219
+ # corrigendum: "IEEE Std 671-1985 [Corrigendum to IEEE Std 671-1985
1220
+ # (Reaff 2008)]/Cor. 1-2010" captures the bracket as the base's
1221
+ # nickname before the wrapper is parsed.
1222
+ book_nickname.maybe >>
1113
1223
  # Trailing corrigendum after the draft+date ("IEEE Approved P1015/D1,
1114
1224
  # Jan 2007/Cor. 1"): the generic bucket is the only path a status-word
1115
1225
  # form reaches. Routes via build_flat_corrigendum (disjoint from the
@@ -1333,9 +1443,12 @@ module Pubid
1333
1443
  # Slash-stage carrying a dash-date ("…P15288/CD2-2013-09 …" — the
1334
1444
  # dash-year tail is what the joint grammar reads after the number, so
1335
1445
  # this cannot steal the plain "/FDIS, June 2021" spellings
1336
- # ieee_p_identifier finishes): → "…CD2 P15288-2013-09 …"
1446
+ # ieee_p_identifier finishes): → "…CD2 P15288-2013-09 …". The date
1447
+ # must be a plausible year — a 4-digit monthcode (YYMM, e.g.
1448
+ # "…P24748-4/DIS-1404" = April 2014) is a DRAFT designator, not a
1449
+ # dash-date, and rewriting it onto the number breaks the parse.
1337
1450
  cleaned = cleaned.sub(
1338
- %r{\A#{pubs}#{num}/ ?#{stage}(-\d{4}(?:-\d\d)?)},
1451
+ %r{\A#{pubs}#{num}/ ?#{stage}(-(?:19|20)\d\d(?:-\d\d)?)},
1339
1452
  '\1\3 \2\4',
1340
1453
  )
1341
1454
 
@@ -128,12 +128,20 @@ module Pubid
128
128
  parts << type_str unless type_str.strip.empty?
129
129
  end
130
130
 
131
- # Code - with P prefix for projects (concatenated, not separated)
131
+ # Code - with P prefix for projects (concatenated, not separated).
132
+ # IEEE semantics (normative): P = project = the document is a
133
+ # draft; no P = it has become a standard. The P-state is
134
+ # identity-bearing, so the renderer never adds or drops it —
135
+ # whatever the source spelling carries round-trips.
132
136
  if id.code_obj
133
137
  result = id.code_obj.to_s
134
138
 
135
- # Prepend P if this is a project AND code doesn't already have P
136
- if id.typed_stage&.project_status && should_render_type && !result.start_with?("P")
139
+ # Prepend P if this is a project AND code doesn't already have P.
140
+ # A recorded project marker (the source spelled the P on a
141
+ # non-IEEE-led publisher) prints regardless of the publisher.
142
+ if (id.typed_stage&.project_status && should_render_type ||
143
+ id.is_a?(Identifiers::ProjectDraftIdentifier) &&
144
+ id.project_marker) && !result.start_with?("P")
137
145
  result = "P#{result}"
138
146
  end
139
147
 
@@ -142,6 +150,7 @@ module Pubid
142
150
  result += mark(id.code_obj.number, id.code_obj.prefix,
143
151
  publishers: publishers_of(id))
144
152
 
153
+
145
154
  # Only attach year to code if there's no edition, no month, and no draft
146
155
  result += "-#{id.year}" if id.year && !id.draft_obj && !id.edition && !id.month
147
156
 
@@ -150,9 +159,29 @@ module Pubid
150
159
  # ("P802.16Rev2/D3"). Keeps a revision distinct from its base standard.
151
160
  result += "Rev#{id.revision}" if id.revision
152
161
 
153
- # Append draft to code - with or without space based on original format
162
+ # Append draft to code - with or without space based on original
163
+ # format. An exactly-IEC/IEEE co-published reference prints only the
164
+ # draft DESIGNATOR - a comma-separated project date ("IEC/IEEE
165
+ # 61886-1/D2, May 2020" prints as "…/D2") is catalogue metadata, not
166
+ # identity. Wider sets ("IEC/ISO/IEEE P82079-1/D1, June 2016") print
167
+ # the date; a space-separated date always stays ("…/DCDV July 2015").
168
+ # IEEE-published drafts normalize the comma date to the long form
169
+ # ("D08, September, 2018").
154
170
  if id.draft_obj
155
- result += id.space_before_draft ? " #{id.draft_obj}" : id.draft_obj.to_s
171
+ printed = id.draft_obj.to_s
172
+ if id.publisher == "IEC" && id.copublisher == ["IEEE"]
173
+ printed = printed.split(", ").first
174
+ elsif id.publisher == "IEEE" && id.draft_status.to_s.empty?
175
+ # The long comma form is for dated project drafts; an
176
+ # unapproved-draft render keeps its pinned single-comma form
177
+ # (pubid#318 idempotence).
178
+ printed = printed.sub(/, ([A-Z][a-z]+) (\d{4})\z/, ', \1, \2')
179
+ end
180
+ # A space-separated draft year with no print date trailing is
181
+ # IEEE's dash form ("…/D1-2006"); with a trailing print date the
182
+ # space stays ("…/D1 2004, Nov 2004").
183
+ printed = printed.sub(/\A(D\d+) (\d{4})\z/, '\1-\2')
184
+ result += id.space_before_draft ? " #{printed}" : printed
156
185
  end
157
186
 
158
187
  # Append interpretation notation (/INT)
@@ -190,8 +219,12 @@ module Pubid
190
219
  # Build the main identifier (without month yet)
191
220
  result = parts.join(" ")
192
221
 
193
- # Month/Day - append directly to avoid extra space before comma
194
- if id.month
222
+
223
+ # Month/Day - append directly to avoid extra space before comma.
224
+ # An exactly-IEC/IEEE co-published reference drops the comma date
225
+ # entirely (same catalogue-metadata rule as the draft date above):
226
+ # "IEC/IEEE P60076-16, May 2016" prints as "IEC/IEEE 60076-16".
227
+ if id.month && !(id.publisher == "IEC" && id.copublisher == ["IEEE"])
195
228
  result += ", #{id.month}"
196
229
  result += " #{id.day}" if id.day
197
230
  if id.year && !id.edition
@@ -127,6 +127,10 @@ module Pubid
127
127
  end
128
128
 
129
129
  def publisher_component
130
+ # The IEC/IEEE co-published pair is definitional to the class and
131
+ # was previously lost ("urn:ieee:ieee" — no number, no draft).
132
+ return "iec-ieee" if identifier.is_a?(Identifiers::IecIeeeCopublished)
133
+
130
134
  pub = normalized_publisher
131
135
 
132
136
  if identifier.copublisher&.any?
@@ -150,6 +154,14 @@ module Pubid
150
154
  end
151
155
 
152
156
  def type_component
157
+ # A joint stage draft carries the stage in its draft clause and the
158
+ # project P inside the code — a separate type segment would
159
+ # duplicate both.
160
+ if identifier.is_a?(Identifiers::JointDevelopment) &&
161
+ identifier.ieee_draft.to_s.start_with?("D=")
162
+ return nil
163
+ end
164
+
153
165
  return nil unless identifier.type
154
166
 
155
167
  type = identifier.type
@@ -159,6 +171,16 @@ module Pubid
159
171
  end
160
172
 
161
173
  def code_component
174
+ # The co-published code is number+parts+separators (no year — its
175
+ # own segment), e.g. "61886-1".
176
+ if identifier.is_a?(Identifiers::IecIeeeCopublished)
177
+ return nil if identifier.number.to_s.empty?
178
+
179
+ # Year-less: the publication year is its own URN segment (the
180
+ # rebuilt copublished_number would double-emit it).
181
+ return "#{identifier.number}#{identifier.parts_suffix}"
182
+ end
183
+
162
184
  return nil unless identifier.code_obj
163
185
 
164
186
  identifier.code_obj.to_s
@@ -182,6 +204,15 @@ module Pubid
182
204
  end
183
205
 
184
206
  def draft_component
207
+ if identifier.is_a?(Identifiers::JointDevelopment) &&
208
+ identifier.ieee_draft.to_s.start_with?("D=")
209
+ return "draft.#{identifier.ieee_draft}"
210
+ end
211
+
212
+ if identifier.is_a?(Identifiers::IecIeeeCopublished)
213
+ return identifier.draft_info ? "draft.#{identifier.draft_info}" : nil
214
+ end
215
+
185
216
  return nil unless identifier.draft_obj
186
217
 
187
218
  "draft.#{identifier.draft_obj}"
@@ -0,0 +1,7 @@
1
+ # IETF flavor notes
2
+
3
+ IETF readiness for the unified relaton index.
4
+
5
+ These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/ietf/` or `spec/pubid/ietf/`. The root file keeps the cross-flavor contract that every flavor obeys.
6
+
7
+ - **IETF readiness for the unified relaton index (`relaton-data-ietf`)**: relaton is collapsing RFCs, the RFC sub-series (BCP/STD/FYI) and Internet-Drafts into **one** index keyed on the structured pubid hash (`pubid_class: ::Pubid::Ietf::Identifier`). That index is **all-or-nothing** — `Relaton::Index::FileIO#deserialize_id` raises on the *first* unparseable row and `#load_index` then rejects the whole index — so one bad id out of 176,862 means no consumer resolves *any* IETF reference. Four changes, all verified against the published corpora (`relaton-data-{rfcs,rfcsubseries,ids}`, branch `v2`). **(1) `draft_rest` accepts `.` and `A-Z`** (`match['a-zA-Z0-9+_.\-']`): exactly 50 of the 166,740 published draft ids failed without them (21 dotted `draft-ietf-pilc-2.5g3g-12`, 29 uppercase `draft-chapin-clnp-ISO8473-00`, 0 both), and a character census confirms the class now covers the corpus **exactly** — the only chars outside `[a-z0-9-]` anywhere in it are `. _ +` and `A-Z`. Widening is safe because the top-level alternation is keyed on the leading token (`RFC`/`BCP|STD|FYI`/`draft-`), so a wider draft tail can never make another branch match; `Builder#split_draft_version` needed no change (it inspects only the last three characters). The set stays **closed** — a space, `/` or `&` in a slug is crawler junk and still fails (pinned by negative examples). **(2) Zero-padded ids parse, grammar-level**: `rfc-index.xml` writes sub-series membership as `<is-also><doc-id>STD0066</doc-id></is-also>` (369 padded doc-ids) and RFCs as `RFC0001`, and relaton#109 forbids relaton normalizing them itself. The space is `.maybe` on both the `rfc` and `subseries` rules and `Builder#unpad` strips the pad (`/\A0+(?=\d)/`, a look-ahead so an all-zero number keeps its last digit), so `STD0066`/`STD 0066`/`STD66` all render the canonical `STD 66`. `unpad` is applied unconditionally, so the **spaced** padded form is normalized too — `RFC 0001` previously parsed and rendered back verbatim, and now renders `RFC 1` with a correspondingly changed hash and URN. That is deliberate (one canonical spelling per document) and has **zero** effect on the published corpora, which contain no padded row. **This is a normalizing parse, so the padded spellings must NEVER go into the byte-exact `spec/fixtures/ietf/identifiers/pass/`** — they live in `spec/pubid/ietf/zero_padded_spec.rb`, the BIPM `CIPM/2005-06(REV)` precedent. **(3) The Internet-Draft slug moved from `name` into `number`, and `series` became derived**: relaton bsearches on `id.root.number.to_s`, so with the slug in `name` all 166,740 drafts keyed to `""` and the narrowing bought nothing (with the slug: 43,564 buckets, median 2, worst 101). `name` is **dropped entirely** (no alias); the slug — leading `draft-` included — is the key, deliberately shared by a draft's unversioned aggregator row and every one of its versions so a document and its variants cluster. `series` is likewise **not stored**: `_type: pubid:ietf:std` already encodes it, so `Bcp`/`Std`/`Fyi` expose a `SERIES` constant + a plain `#series` reader (safe — `::Pubid::Identifier` declares no `series` attribute and nothing generic reads `.series`) and `build_subseries` stops passing it. The whole serialized vocabulary is now **three keys**: `_type` + `number` always, `version` only on a versioned draft — a sub-series row (`{_type, number}`) has the same shape as an RFC's. **The attributes live on the five LEAVES, not the shared `Pubid::Ietf::Identifier`**, via the `Identifiers::Serialization` mixin (`number` + the `key_value` block; `InternetDraft` merges its own `version` mapping on top — lutaml combines a mixin-installed block with a second in-class one, the ITU precedent). This is the IEEE `number` determinism landmine: a `:string` redefinition of the parent's `Components::Code number` on a class the leaves *inherit* from resolves nondeterministically under multi-flavor load. It was invisible while drafts left `number` nil; it is load-bearing now. The base deliberately carries **no** `key_value` block — one there would be inherited-and-merged by every leaf. `spec/pubid/ietf/root_number_spec.rb` is the tripwire (asserts `number` is a `String`, the exact keys, the exact `to_hash` key sets) and **must run under the full `rake` suite** to mean anything. **(4) Round-trip is validated here because relaton structurally cannot**: `FileIO#id_supported?` **skips** its `to_hash`/`from_hash` check whenever the object is a concrete subclass (`return true unless obj.instance_of?(@pubid_class)`), and *every* IETF id is a subclass — so a lossy round-trip would pass validation silently and produce wrong lookups. Three layers: `fixtures_spec.rb` is now **zero-failure** (was a 90% threshold — wrong for an all-or-nothing index) and checks `to_s` byte-exactness, `from_hash(to_hash)` fidelity in both `to_s` and `to_hash`, **and** a non-empty `root.number` for every fixture line; the fixtures grew to ~2,500 lines (the complete 365-row sub-series corpus, a 139-id structural edge set, a deterministic 1-in-84 draft sample — each file's header records the regeneration command); and `spec/pubid/ietf/corpus_round_trip_spec.rb` runs the same four checks over all 176,862 published ids, **opt-in** via `PUBID_IETF_CORPUS=<dir with the three relaton-data checkouts> bundle exec rake test:corpus_ietf` (self-skips otherwise; ~100s; verified 0 failures). **relaton note — the indexes must be regenerated, there is no alias**: `name` and `series` are gone as serialized keys, so a pre-`number` row (`{_type, name, version}`) deserializes with **`number` nil** and no error at all — lutaml ignores unknown keys, and relaton's subclass skip means `id_supported?` won't catch it either, leaving every draft keyed under `""`. Rendering is therefore the loud failure: `Renderer#require_number!` raises `ArgumentError` on a nil/empty `number` rather than returning `nil` or `"-02"`, and the renderer's sub-series arm is an explicit `when Bcp, Std, Fyi` with an `else raise` instead of a catch-all (a bare `Pubid::Ietf::Identifier` has no `series` reader). Pinned by the "legacy `name`-shaped hash" block in `root_number_spec.rb`. **Pre-existing bug fixed in passing**: `UrnParser`'s bare `Errors::ParseError` does not resolve — `Errors` lives under `Pubid::UrnParser` (the module), which is neither in the lexical scope of `Pubid::Ietf::UrnParser` nor an ancestor of `Base` — so an invalid URN raised `NameError` instead, and the spec's `raise_error(StandardError)` accepted it. Now fully qualified as `Pubid::UrnParser::Errors::ParseError` (the adobe/easc/gost/iala convention) with the spec asserting that exact class. **`Pubid::Iec::UrnParser` has the identical bug and is untouched here.** Also: `Identifier.parse` gained the inline `MAX_INPUT_LENGTH` guard — relaton reaches that class-level funnel directly through `pubid_class:`, bypassing `Pubid::Ietf.parse` — and IETF was added to the three cross-flavor tables it was missing from (`identifier_roundtrip_spec`, `uniform_identifier_handle_spec`, `redos_guard_spec`), which is what exercises the leaf `number` under real multi-flavor load. (hand-off: ietf-index-readiness.)
@@ -76,3 +76,5 @@ module Pubid
76
76
  end
77
77
  end
78
78
  end
79
+
80
+ Pubid::Ietf::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -35,3 +35,5 @@ module Pubid
35
35
  end
36
36
  end
37
37
  end
38
+
39
+ Pubid::Iho::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -42,3 +42,5 @@ module Pubid
42
42
  end
43
43
  end
44
44
  end
45
+
46
+ Pubid::Isbn::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -0,0 +1,47 @@
1
+ # ISO flavor notes
2
+
3
+ - **No identifier attribute holds a `Components::Code` any more**: `number`,
4
+ `part` and `subpart` on `Pubid::Iso::Identifier`, the committee structure on
5
+ `Identifiers::TcDocument` (`tc_type`/`tc_number`/`sc_type`/`sc_number`/
6
+ `wg_type`/`wg_number`), `Identifiers::Directives#subgroup` and the
7
+ `edition.number` the builder writes are all plain strings, and
8
+ `Pubid::Iso::Components::Code` is deleted.
9
+ **The component was degenerate everywhere it was used.** Measured over the
10
+ whole 7,613-id pass corpus: no ISO Code carries `prefix`, `part`, `subpart`
11
+ or `parts`, `parts` is never written anywhere in `lib/pubid/iso`, and no Code
12
+ renders differently from its own `value`. ISO's part and subpart are sibling
13
+ attributes of the identifier, not fields of the number — the `-` join in
14
+ `Code#to_s` was unreachable.
15
+ **`edition.number` was worse than degenerate: it leaked a live object.**
16
+ `Components::Edition#number` is typed `Lutaml::Model::Type::Value`, so lutaml
17
+ never serialized it — `to_hash` returned the `Components::Code` instance
18
+ itself, and `to_yaml` emitted `!ruby/object:Pubid::Components::Code` with its
19
+ internal ivars, which `YAML.safe_load` refuses. An edition now serializes
20
+ `{"number" => "13", "original_text" => "Ed 13"}`.
21
+ **The TC converters are gone with it**: twelve `*_to_kv`/`*_from_kv` helpers
22
+ were replaced by plain `map "tc_type", to: :tc_type` declarations (the
23
+ ETSI/OIML shape), because a String needs no converter to flatten.
24
+ **Two shared surfaces needed a change, and both are the documented shims**:
25
+ `Renderers::DirectivesRenderer` called `subgroup.render(context:)` directly
26
+ and now reads through `render_component`; `Pubid::Iso.build_code` returned a
27
+ Code and now returns the string (ASTM's `to_iso_identifier` goes through it,
28
+ which is how one ASTM example caught the `NameError` after the class was
29
+ deleted).
30
+ **Verification — replay a baseline, do not trust the suite alone.** Against
31
+ all 7,621 pass-fixture ids captured on the parent commit, `to_s`, `to_urn`,
32
+ `to_mr_string`, `root.number` and the identifier class are **byte-identical**,
33
+ and `to_hash` differs for exactly **12** ids: the 9 `subgroup` rows and the 3
34
+ editions below. The first replay also caught a real regression the specs did
35
+ not — `directives.rb` still called `part.value.downcase`, so 4 Directives
36
+ URNs raised.
37
+ **relaton note — `relaton-data-iso` needs a re-crawl, and only for one key.**
38
+ `subgroup` flattens from `{"value" => "JTC 1"}` to `"JTC 1"`, and the
39
+ published `index-v2.yaml` (branch `v2`) carries **5 such rows**, all ISO/IEC
40
+ JTC 1 directives. **No compatibility shim is kept** — a stored nested
41
+ `subgroup` will not deserialize. The edition change reaches nothing stored:
42
+ `grep -c edition` over that index returns **0**.
43
+ **Spec fallout is the shape to expect for the remaining flavors**: 592
44
+ assertions across 25 files read `.number.value` / `.part.value`. The rewrite
45
+ needs a lookbehind, because `tc_number.value` and `edition.number.value` are
46
+ different attributes — the first pass stripped `edition.number.value` too,
47
+ and two corrigendum examples caught it.