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.
- checksums.yaml +4 -4
- data/README.adoc +43 -1
- data/data/ieee/update_codes.yaml +17 -4
- data/data/nist/update_codes.yaml +7 -3
- data/lib/pubid/adobe/builder.rb +2 -0
- data/lib/pubid/all_parts.rb +201 -0
- data/lib/pubid/all_parts_identifier.rb +19 -0
- data/lib/pubid/amca/CLAUDE.md +47 -0
- data/lib/pubid/amca/builder.rb +3 -5
- data/lib/pubid/amca/identifiers/base.rb +10 -0
- data/lib/pubid/amca/identifiers/publication.rb +13 -0
- data/lib/pubid/amca/parser.rb +2 -1
- data/lib/pubid/amca/renderer.rb +22 -33
- data/lib/pubid/amca/urn_generator.rb +21 -2
- data/lib/pubid/amca/urn_parser.rb +36 -10
- data/lib/pubid/ansi/builder.rb +6 -0
- data/lib/pubid/api/CLAUDE.md +23 -0
- data/lib/pubid/api/builder.rb +2 -0
- data/lib/pubid/ashrae/CLAUDE.md +13 -0
- data/lib/pubid/ashrae/builder.rb +58 -14
- data/lib/pubid/ashrae/identifiers/errata.rb +14 -2
- data/lib/pubid/ashrae/identifiers/interpretation.rb +2 -10
- data/lib/pubid/ashrae/parser.rb +62 -28
- data/lib/pubid/ashrae/renderer.rb +32 -1
- data/lib/pubid/ashrae/urn_generator.rb +32 -9
- data/lib/pubid/asme/CLAUDE.md +25 -0
- data/lib/pubid/asme/builder.rb +16 -9
- data/lib/pubid/asme/components/code.rb +2 -0
- data/lib/pubid/asme/identifiers/standard.rb +6 -1
- data/lib/pubid/asme/parser.rb +41 -14
- data/lib/pubid/astm/CLAUDE.md +9 -0
- data/lib/pubid/astm/builder.rb +2 -0
- data/lib/pubid/astm/components/code.rb +2 -0
- data/lib/pubid/astm/parser.rb +4 -1
- data/lib/pubid/bipm/CLAUDE.md +11 -0
- data/lib/pubid/bipm/builder.rb +2 -0
- data/lib/pubid/bsi/CLAUDE.md +93 -0
- data/lib/pubid/bsi/builder.rb +13 -11
- data/lib/pubid/bsi/components/date.rb +10 -4
- data/lib/pubid/bsi/identifiers/addendum_document.rb +2 -0
- data/lib/pubid/bsi/identifiers/adopted_european_norm.rb +6 -54
- data/lib/pubid/bsi/identifiers/adopted_international_standard.rb +5 -22
- data/lib/pubid/bsi/identifiers/amendment.rb +36 -12
- data/lib/pubid/bsi/identifiers/british_industrial_practice.rb +1 -1
- data/lib/pubid/bsi/identifiers/bundled_identifier.rb +2 -0
- data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +23 -26
- data/lib/pubid/bsi/identifiers/corrigendum.rb +29 -12
- data/lib/pubid/bsi/identifiers/expert_commentary.rb +6 -7
- data/lib/pubid/bsi/identifiers/handbook.rb +1 -1
- data/lib/pubid/bsi/identifiers/national_annex.rb +18 -20
- data/lib/pubid/bsi/identifiers/practice_guide.rb +1 -1
- data/lib/pubid/bsi/identifiers/root_identity.rb +31 -0
- data/lib/pubid/bsi/identifiers/set.rb +2 -0
- data/lib/pubid/bsi/identifiers/supplement_document.rb +2 -0
- data/lib/pubid/bsi/identifiers.rb +1 -0
- data/lib/pubid/bsi/parser.rb +8 -8
- data/lib/pubid/bsi/renderer.rb +20 -20
- data/lib/pubid/bsi/single_identifier.rb +11 -2
- data/lib/pubid/bsi/urn_generator.rb +28 -18
- data/lib/pubid/builder/base.rb +27 -0
- data/lib/pubid/calconnect/builder.rb +2 -0
- data/lib/pubid/calconnect/identifier.rb +4 -0
- data/lib/pubid/ccsds/builder.rb +2 -0
- data/lib/pubid/ccsds/identifier.rb +12 -0
- data/lib/pubid/ccsds/single_identifier.rb +14 -10
- data/lib/pubid/cen_cenelec/CLAUDE.md +59 -0
- data/lib/pubid/cen_cenelec/builder.rb +6 -1
- data/lib/pubid/cen_cenelec/identifier.rb +10 -27
- data/lib/pubid/cen_cenelec/identifiers/amendment.rb +3 -10
- data/lib/pubid/cen_cenelec/identifiers/corrigendum.rb +3 -10
- data/lib/pubid/cen_cenelec/parser.rb +11 -3
- data/lib/pubid/cie/CLAUDE.md +58 -0
- data/lib/pubid/cie/builder.rb +2 -0
- data/lib/pubid/cie/components/language.rb +2 -0
- data/lib/pubid/components/adoption.rb +2 -0
- data/lib/pubid/components/code.rb +2 -0
- data/lib/pubid/components/date.rb +8 -6
- data/lib/pubid/components/edition.rb +2 -0
- data/lib/pubid/components/iteration.rb +2 -0
- data/lib/pubid/components/language.rb +2 -0
- data/lib/pubid/components/locality.rb +2 -0
- data/lib/pubid/components/publisher.rb +2 -0
- data/lib/pubid/components/relationship.rb +2 -0
- data/lib/pubid/components/stage.rb +2 -0
- data/lib/pubid/components/supplement.rb +2 -0
- data/lib/pubid/components/type.rb +2 -0
- data/lib/pubid/components/typed_stage.rb +8 -0
- data/lib/pubid/csa/CLAUDE.md +41 -0
- data/lib/pubid/csa/builder.rb +5 -3
- data/lib/pubid/csa/identifier.rb +13 -1
- data/lib/pubid/csa/identifiers/bundled.rb +2 -2
- data/lib/pubid/csa/identifiers/cec.rb +1 -1
- data/lib/pubid/csa/renderer.rb +12 -12
- data/lib/pubid/csa/single_identifier.rb +18 -1
- data/lib/pubid/csa/urn_generator.rb +1 -1
- data/lib/pubid/doi/builder.rb +2 -0
- data/lib/pubid/easc/builder.rb +2 -0
- data/lib/pubid/ecma/CLAUDE.md +28 -0
- data/lib/pubid/ecma/builder.rb +2 -0
- data/lib/pubid/ecma/identifier.rb +7 -0
- data/lib/pubid/etsi/CLAUDE.md +34 -0
- data/lib/pubid/etsi/builder.rb +2 -0
- data/lib/pubid/etsi/components/code.rb +6 -0
- data/lib/pubid/etsi/components/version.rb +2 -0
- data/lib/pubid/etsi/identifiers/etsi_standard.rb +7 -0
- data/lib/pubid/evs/CLAUDE.md +58 -0
- data/lib/pubid/evs/builder.rb +3 -1
- data/lib/pubid/evs/identifier.rb +6 -6
- data/lib/pubid/evs/identifiers/national_adoption.rb +7 -2
- data/lib/pubid/evs/renderer.rb +1 -1
- data/lib/pubid/evs/urn_generator.rb +1 -1
- data/lib/pubid/evs/urn_parser.rb +1 -1
- data/lib/pubid/gb/CLAUDE.md +140 -0
- data/lib/pubid/gb/builder.rb +21 -11
- data/lib/pubid/gb/identifier.rb +28 -7
- data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
- data/lib/pubid/gb/identifiers.rb +1 -0
- data/lib/pubid/gb/parser.rb +4 -0
- data/lib/pubid/gb/renderer.rb +4 -4
- data/lib/pubid/gb.rb +1 -0
- data/lib/pubid/gost/CLAUDE.md +64 -0
- data/lib/pubid/gost/builder.rb +3 -1
- data/lib/pubid/gost/identifier.rb +5 -0
- data/lib/pubid/iala/CLAUDE.md +82 -0
- data/lib/pubid/iala/builder.rb +2 -0
- data/lib/pubid/iana/CLAUDE.md +7 -0
- data/lib/pubid/iana/builder.rb +2 -0
- data/lib/pubid/identifier.rb +172 -20
- data/lib/pubid/idf/builder.rb +6 -1
- data/lib/pubid/idf/identifier.rb +12 -0
- data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
- data/lib/pubid/idf/identifiers/amendment.rb +1 -1
- data/lib/pubid/idf/identifiers/corrigendum.rb +1 -1
- data/lib/pubid/idf/identifiers/international_standard.rb +1 -1
- data/lib/pubid/idf/identifiers/reviewed_method.rb +1 -1
- data/lib/pubid/idf/identifiers.rb +1 -0
- data/lib/pubid/idf/single_identifier.rb +1 -1
- data/lib/pubid/iec/CLAUDE.md +31 -0
- data/lib/pubid/iec/builder.rb +7 -1
- data/lib/pubid/iec/components/consolidated_amendment.rb +4 -0
- data/lib/pubid/iec/components/sheet.rb +2 -0
- data/lib/pubid/iec/components/trf_info.rb +2 -0
- data/lib/pubid/iec/components/vap_suffix.rb +2 -0
- data/lib/pubid/iec/identifier.rb +7 -2
- data/lib/pubid/iec/identifiers/all_parts.rb +19 -0
- data/lib/pubid/iec/identifiers.rb +1 -0
- data/lib/pubid/iec/renderer.rb +0 -1
- data/lib/pubid/iec/urn_generator.rb +9 -1
- data/lib/pubid/iec/urn_parser.rb +3 -2
- data/lib/pubid/ieee/CLAUDE.md +97 -0
- data/lib/pubid/ieee/builder.rb +176 -7
- data/lib/pubid/ieee/components/code.rb +2 -0
- data/lib/pubid/ieee/components/draft.rb +35 -2
- data/lib/pubid/ieee/components/typed_stage.rb +2 -0
- data/lib/pubid/ieee/identifiers/base.rb +20 -0
- data/lib/pubid/ieee/identifiers/iec_ieee_copublished.rb +9 -0
- data/lib/pubid/ieee/identifiers/joint_development.rb +17 -10
- data/lib/pubid/ieee/identifiers/project_draft_identifier.rb +8 -1
- data/lib/pubid/ieee/identifiers/si_standard.rb +5 -2
- data/lib/pubid/ieee/ire/parser.rb +19 -1
- data/lib/pubid/ieee/parser.rb +337 -49
- data/lib/pubid/ieee/project_renderer.rb +46 -0
- data/lib/pubid/ieee/renderer.rb +46 -14
- data/lib/pubid/ieee/urn_generator.rb +31 -0
- data/lib/pubid/ieee.rb +2 -0
- data/lib/pubid/ietf/CLAUDE.md +7 -0
- data/lib/pubid/ietf/builder.rb +2 -0
- data/lib/pubid/iho/builder.rb +2 -0
- data/lib/pubid/isbn/builder.rb +2 -0
- data/lib/pubid/iso/CLAUDE.md +47 -0
- data/lib/pubid/iso/builder.rb +29 -16
- data/lib/pubid/iso/components/publisher.rb +2 -0
- data/lib/pubid/iso/components.rb +0 -1
- data/lib/pubid/iso/identifier.rb +17 -18
- data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
- data/lib/pubid/iso/identifiers/directives.rb +7 -4
- data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
- data/lib/pubid/iso/identifiers/tc_document.rb +31 -46
- data/lib/pubid/iso/identifiers.rb +1 -0
- data/lib/pubid/iso/normalizer.rb +1 -1
- data/lib/pubid/iso/rendering_style.rb +0 -1
- data/lib/pubid/iso/urn_generator.rb +10 -10
- data/lib/pubid/iso.rb +5 -4
- data/lib/pubid/itu/CLAUDE.md +69 -0
- data/lib/pubid/itu/builder.rb +12 -0
- data/lib/pubid/itu/components/code.rb +2 -0
- data/lib/pubid/itu/components/designation.rb +2 -0
- data/lib/pubid/itu/components/sector.rb +2 -0
- data/lib/pubid/itu/components/series.rb +2 -0
- data/lib/pubid/itu/identifiers/base.rb +10 -0
- data/lib/pubid/itu/identifiers/contribution.rb +31 -0
- data/lib/pubid/itu/identifiers/supplement.rb +15 -0
- data/lib/pubid/itu/identifiers.rb +1 -0
- data/lib/pubid/itu/parser.rb +19 -2
- data/lib/pubid/jcgm/CLAUDE.md +7 -0
- data/lib/pubid/jcgm/builder.rb +2 -0
- data/lib/pubid/jcgm/components/publisher.rb +2 -0
- data/lib/pubid/jis/builder.rb +5 -1
- data/lib/pubid/jis/identifier.rb +5 -17
- data/lib/pubid/jis/identifiers/all_parts.rb +19 -0
- data/lib/pubid/jis/identifiers.rb +1 -0
- data/lib/pubid/jis/renderer.rb +0 -2
- data/lib/pubid/jis/urn_generator.rb +0 -1
- data/lib/pubid/nist/CLAUDE.md +56 -0
- data/lib/pubid/nist/builder.rb +14 -12
- data/lib/pubid/nist/components/edition.rb +2 -0
- data/lib/pubid/nist/components/issue_number.rb +2 -0
- data/lib/pubid/nist/components/part.rb +2 -0
- data/lib/pubid/nist/components/stage.rb +2 -0
- data/lib/pubid/nist/components/supplement.rb +2 -0
- data/lib/pubid/nist/components/translation.rb +2 -0
- data/lib/pubid/nist/components/update.rb +2 -0
- data/lib/pubid/nist/components/version.rb +2 -0
- data/lib/pubid/nist/components/volume.rb +2 -0
- data/lib/pubid/nist/identifiers/base.rb +34 -6
- data/lib/pubid/nist/identifiers/commercial_standard_emergency.rb +1 -1
- data/lib/pubid/nist/identifiers/crpl_report.rb +5 -6
- data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +2 -2
- data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +2 -2
- data/lib/pubid/nist/parser.rb +15 -2
- data/lib/pubid/nist/preprocessor.rb +53 -2
- data/lib/pubid/nist/series/ir.rb +3 -7
- data/lib/pubid/oasis/CLAUDE.md +19 -0
- data/lib/pubid/oasis/builder.rb +2 -0
- data/lib/pubid/oasis/identifier.rb +19 -0
- data/lib/pubid/ogc/CLAUDE.md +34 -0
- data/lib/pubid/ogc/builder.rb +2 -0
- data/lib/pubid/ogc/identifier.rb +11 -0
- data/lib/pubid/oiml/CLAUDE.md +189 -0
- data/lib/pubid/oiml/builder.rb +22 -2
- data/lib/pubid/oiml/components/code.rb +6 -0
- data/lib/pubid/oiml/identifier.rb +13 -0
- data/lib/pubid/oiml/identifiers/annex.rb +4 -0
- data/lib/pubid/oiml/identifiers/bulletin.rb +30 -36
- data/lib/pubid/oiml/identifiers/certification_system.rb +34 -0
- data/lib/pubid/oiml/identifiers/code_number.rb +10 -2
- data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
- data/lib/pubid/oiml/identifiers.rb +2 -0
- data/lib/pubid/oiml/parser.rb +41 -5
- data/lib/pubid/oiml/renderer.rb +26 -4
- data/lib/pubid/oiml/single_identifier.rb +5 -1
- data/lib/pubid/oiml/supplement_identifier.rb +7 -0
- data/lib/pubid/oiml/urn_generator.rb +32 -4
- data/lib/pubid/oiml.rb +5 -0
- data/lib/pubid/omg/CLAUDE.md +15 -0
- data/lib/pubid/omg/builder.rb +2 -0
- data/lib/pubid/parser/grammar.rb +23 -0
- data/lib/pubid/plateau/builder.rb +2 -0
- data/lib/pubid/plateau/identifiers/base.rb +4 -0
- data/lib/pubid/plateau/supplement_identifier.rb +14 -2
- data/lib/pubid/plateau/urn_generator.rb +7 -1
- data/lib/pubid/renderers/directives_renderer.rb +3 -1
- data/lib/pubid/renderers/human_readable.rb +0 -1
- data/lib/pubid/sae/builder.rb +3 -1
- data/lib/pubid/sae/components/date.rb +2 -0
- data/lib/pubid/sae/components/type.rb +2 -0
- data/lib/pubid/sae/identifiers/base.rb +4 -1
- data/lib/pubid/sae/urn_generator.rb +1 -1
- data/lib/pubid/subset_match.rb +197 -0
- data/lib/pubid/tgpp/CLAUDE.md +43 -0
- data/lib/pubid/tgpp/builder.rb +2 -0
- data/lib/pubid/tgpp/identifier.rb +14 -0
- data/lib/pubid/type_resolver.rb +14 -2
- data/lib/pubid/un/builder.rb +2 -0
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid/w3c/CLAUDE.md +7 -0
- data/lib/pubid/w3c/builder.rb +2 -0
- data/lib/pubid/xsf/CLAUDE.md +11 -0
- data/lib/pubid/xsf/builder.rb +2 -0
- data/lib/pubid.rb +16 -3
- data/lib/tasks/docs.rake +13 -11
- metadata +46 -4
- data/lib/pubid/ccsds/identifiers/base_BASE_88929.rb +0 -70
- data/lib/pubid/iso/components/code.rb +0 -24
|
@@ -37,6 +37,11 @@ module Pubid
|
|
|
37
37
|
def initialize(input)
|
|
38
38
|
@input = input.to_s.strip
|
|
39
39
|
@cleaned = Core::UpdateCodes.apply(@input, :nist)
|
|
40
|
+
# The format describes the string the parser will see: capture
|
|
41
|
+
# it right after the update-codes remap, before the stages'
|
|
42
|
+
# cosmetic spacing (a dotted catalogue alias that remaps to the
|
|
43
|
+
# space form renders short; the dotted originals stay :mr).
|
|
44
|
+
@format = @cleaned.include?(".") && !@cleaned.match?(/\s/) ? :mr : :short
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
# Run every normalization stage and return a Result.
|
|
@@ -53,6 +58,14 @@ module Pubid
|
|
|
53
58
|
# Extracted so rubocop can scope length/ABC metrics narrowly.
|
|
54
59
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
55
60
|
def run_stages
|
|
61
|
+
# Short-form "supprev" is the catalogue spelling of the plain
|
|
62
|
+
# supplement ("NBS CIRC 154supprev" ≡ "NBS CIRC 154sup"; the
|
|
63
|
+
# revision-bearing identity is the mr spelling "154suprev").
|
|
64
|
+
# Rewrite before the supplement/revision stages; the mr form is
|
|
65
|
+
# untouched.
|
|
66
|
+
if detected_format == :short
|
|
67
|
+
@cleaned = @cleaned.gsub("supprev", "sup")
|
|
68
|
+
end
|
|
56
69
|
normalize_spurious_u_suffix!
|
|
57
70
|
normalize_publisher_and_series!
|
|
58
71
|
normalize_lcirc_supplement_contexts!
|
|
@@ -75,12 +88,14 @@ module Pubid
|
|
|
75
88
|
normalize_part_notation!
|
|
76
89
|
normalize_series_specific_spacing!
|
|
77
90
|
normalize_verbose_keywords!
|
|
91
|
+
normalize_legacy_corpus_spellings!
|
|
78
92
|
end
|
|
79
93
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
80
94
|
|
|
81
|
-
# Detect input format: :mr (dot-separated machine-readable) or
|
|
95
|
+
# Detect input format: :mr (dot-separated machine-readable) or
|
|
96
|
+
# :short. Frozen in #initialize (post update-codes, pre-stages).
|
|
82
97
|
def detected_format
|
|
83
|
-
@
|
|
98
|
+
@format
|
|
84
99
|
end
|
|
85
100
|
|
|
86
101
|
private
|
|
@@ -167,12 +182,21 @@ module Pubid
|
|
|
167
182
|
# Trailing "-a" → "-A" at end of identifier.
|
|
168
183
|
def uppercase_dash_letter!
|
|
169
184
|
@cleaned = @cleaned.gsub(/(\d)-([a-z])$/) { "#{$1}-#{$2.upcase}" }
|
|
185
|
+
# Same letter when a part tail follows ("-add", " Add."), so the
|
|
186
|
+
# canonical case survives; lowercase update/translation codes
|
|
187
|
+
# ("-upd", ".uppl") never match (next char is a letter).
|
|
188
|
+
@cleaned = @cleaned.gsub(/(\d)-([a-z])(?=[-.\s])/) { "#{$1}-#{$2.upcase}" }
|
|
170
189
|
end
|
|
171
190
|
|
|
172
191
|
# Trailing "a" → "A" when attached directly to a digit (excludes
|
|
173
192
|
# "r" to preserve revision+year patterns like "73-197r").
|
|
174
193
|
def uppercase_trailing_letter!
|
|
175
194
|
@cleaned = @cleaned.gsub(/(\d)([a-z&&[^r]])$/) { "#{$1}#{$2.upcase}" }
|
|
195
|
+
# Same letter when an addendum tail follows ("-add", " Add."),
|
|
196
|
+
# so the canonical part case survives ("800-38a-add" ->
|
|
197
|
+
# "800-38A"); part digits ("800-85a-1") and update/translation
|
|
198
|
+
# codes ("-upd", ".uppl") never match.
|
|
199
|
+
@cleaned = @cleaned.gsub(/(\d)([a-z&&[^r]])(?=\s*[-.\s]\s*[aA]dd)/) { "#{$1}#{$2.upcase}" }
|
|
176
200
|
end
|
|
177
201
|
|
|
178
202
|
# Letter suffix on revision: "22r1a" → "22r1A".
|
|
@@ -336,6 +360,33 @@ module Pubid
|
|
|
336
360
|
end
|
|
337
361
|
end
|
|
338
362
|
|
|
363
|
+
# Legacy corpus spellings: catalogue forms the mr grammar and the
|
|
364
|
+
# renderers cannot round-trip on their own. Each rule mirrors an
|
|
365
|
+
# already-parseable spelling of the same document.
|
|
366
|
+
def normalize_legacy_corpus_spellings!
|
|
367
|
+
# The mr renderer prints the translation code with its own
|
|
368
|
+
# leading dot ("955-S..uppl"); the parser wants one dot.
|
|
369
|
+
@cleaned = @cleaned.gsub("..", ".")
|
|
370
|
+
# Trailing-dot addendum spelling; the canonical render comes from
|
|
371
|
+
# the lowercase ".add" parse ("NBS.TN.467pt1.Add." -> ".add").
|
|
372
|
+
@cleaned = @cleaned.gsub(/\.Add\.\z/, ".add")
|
|
373
|
+
# Edition glued to the series in mr form ("NBS.CIRCe2" is
|
|
374
|
+
# "NBS.CIRC.e2"; digits before "e" are untouched: "24e7", and
|
|
375
|
+
# the short form never glues an edition to a letter part:
|
|
376
|
+
# "150-1Ae2009").
|
|
377
|
+
if detected_format == :mr
|
|
378
|
+
@cleaned = @cleaned.gsub(/([A-Z])e(\d)/, '\1.e\2')
|
|
379
|
+
end
|
|
380
|
+
# Update markers without a number render a phantom "1";
|
|
381
|
+
# "…-upd" is the numberless spelling of "…-upd1".
|
|
382
|
+
@cleaned = @cleaned.gsub(/-upd\z/, "-upd1")
|
|
383
|
+
# Handbook legacy renumbering in the all-dash spelling: the
|
|
384
|
+
# e-form stage already eats the prefix generically ("HB
|
|
385
|
+
# 150-1e2017" -> "HB 1-2017"); the all-dash form carries the
|
|
386
|
+
# same 105-/150- prefixes.
|
|
387
|
+
@cleaned = @cleaned.gsub(/\b(NIST HB\s+)(?:105|150)-(\d+)-(\d{4})(?=\s|\z)/, '\1\2-\3')
|
|
388
|
+
end
|
|
389
|
+
|
|
339
390
|
# Series-specific reverts: HB handbooks, OWMWP dates, and RPT year
|
|
340
391
|
# ranges use dash-year structurally (not as an edition marker), so
|
|
341
392
|
# the broad convert_dashyear_to_edition! rule would corrupt them.
|
data/lib/pubid/nist/series/ir.rb
CHANGED
|
@@ -31,9 +31,7 @@ module Pubid
|
|
|
31
31
|
letter_suffix:)
|
|
32
32
|
return false unless letter_suffix == "R"
|
|
33
33
|
|
|
34
|
-
identifier.number =
|
|
35
|
-
value: "#{first_num.value}-#{letter_base}",
|
|
36
|
-
)
|
|
34
|
+
identifier.number = "#{first_num.value}-#{letter_base}"
|
|
37
35
|
edition_obj = Components::Edition.new(type: "r", id: "1")
|
|
38
36
|
identifier.edition = edition_obj
|
|
39
37
|
true
|
|
@@ -42,13 +40,11 @@ module Pubid
|
|
|
42
40
|
def self.finalize_identifier(identifier, _parsed_hash)
|
|
43
41
|
return unless identifier.number
|
|
44
42
|
|
|
45
|
-
value = identifier.number.
|
|
43
|
+
value = identifier.number.to_s
|
|
46
44
|
match = value.match(/^(\d+)e(\d{4})$/)
|
|
47
45
|
return unless match
|
|
48
46
|
|
|
49
|
-
identifier.number =
|
|
50
|
-
value: "#{match[1]}-#{match[2]}",
|
|
51
|
-
)
|
|
47
|
+
identifier.number = "#{match[1]}-#{match[2]}"
|
|
52
48
|
identifier.edition = nil
|
|
53
49
|
end
|
|
54
50
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# OASIS flavor notes
|
|
2
|
+
|
|
3
|
+
OASIS verbatim slugs, the index key, the MR slug and partial-reference matching.
|
|
4
|
+
|
|
5
|
+
Read them before you change `lib/pubid/oasis/` or `spec/pubid/oasis/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **The identity is the verbatim slug; the decomposition is a lossy projection of it**: an OASIS identifier is a free-form slug with no fixed internal order (`OSLC-CoreShapes-3.0-PS01-Pt8`, `OSLC-AM-3.0-Part1-PS01`, `amqp-core`), three part spellings and mixed case. The grammar therefore captures the whole slug with `any.repeat(1)` and only strips the `OASIS ` prefix; `Builder#decompose` then classifies each *whole* dash-separated fragment into `number` / `version` / `stage` / `part` / `label`. **`original` holds the printed slug verbatim and alone drives `to_s` and `to_urn`** — `Renderer#render` is a pure echo of it — so the printed form round-trips byte-exactly whatever the classifier makes of it. The decomposition is **not** an identity and cannot be one: it loses fragment order (`x-1.0-os-Pt1` and `x-1.0-Pt1-os` decompose alike) and keeps only the first fragment of each recognized kind, so a repeated one is dropped. That is not hypothetical — **7 of the 605 published `relaton-data-oasis` ids already lose a fragment** (`OASIS xacml-3.0-hierarchical-v1.0-CS02` keeps `3.0` and drops `v1.0`). Every design decision below follows from that asymmetry.
|
|
8
|
+
|
|
9
|
+
- **`spec` → `number`, the index key (PR #359)**: `Relaton::Index::Type#candidates_by_number` sorts every row and binary-searches it on `id.root.number.to_s`. OASIS kept the specification name in a bespoke `attribute :spec, :string` and never set the `number` it inherits from `::Pubid::Identifier`, so **all 605 published rows shared the empty key `""`** and the search degraded to a linear scan, silently. The attribute is **renamed**, not shadowed by a derived reader: `attribute :number, :string` plus `map "number", to: :number`, and `Builder#decompose` returns `number:`. **`spec` is dropped with no alias and no reader** — one name for one value, the W3C `code` → `number` precedent. A derived `#spec` was written first and then removed: nothing in `lib/` read it, and `relaton-oasis` has no pubid dependency at all, so it had no consumer to preserve. That is what separates this case from IANA's `#registry`, IETF's `#series` and BIPM's `#volume`, which do have one. `root_number_spec.rb` asserts the identifier does **not** respond to `spec`, so the name cannot creep back. The key **clusters**: every version, stage and part of one specification shares it — 309 buckets over 605 ids, median 1, max 25 (`STIX`) — the shape of an IETF draft slug or an IANA registry slug, not an exact key.
|
|
10
|
+
|
|
11
|
+
- **Why the `number` declaration sits on the base here**: `attribute :number, :string` redefines the parent's `Components::Code number` on a class `Identifiers::Standard` inherits from, which is the recorded determinism landmine. It is safe for the same reason W3C's is: lutaml deep-dups the parent attribute table into each subclass at class-definition time, so a subclass holds a snapshot, and Ruby resolves the superclass constant to completion before opening a leaf body. **`Pubid::Oasis::Identifier`'s class body lives in one file and is never reopened**, so the snapshot is always complete. `stage` and `part` in that same body have relied on this since the flavor landed, overriding `Components::Stage` and `Components::Code`. **Split this class across two files and the landmine comes back** — IEEE is the counter-shape, its base reachable through two paths. `spec/pubid/oasis/root_number_spec.rb` carries the structural tripwire (the base *and* the leaf must resolve `number` to `Lutaml::Model::Type::String`) and is only meaningful under the full `bundle exec rake`.
|
|
12
|
+
|
|
13
|
+
- **MR slug**: OASIS supplied no `mr_*` hook, and every base hook looks for something the flavor does not use — the publisher is the `PUBLISHER` constant rather than a lutaml attribute, there is no `Components::Date` and no `typed_stage`. With `number` nil as well, **`to_mr_string` was `""` for all 605 ids**, and `to_slug` is what consumers use as an output **filename**. The base now supplies `mr_publisher` (`"oasis"`) and `mr_number_with_part`, which returns the sanitized **`original`** — not the five decomposed fields, because those assemble `x-1.0-os-Pt1` and `x-1.0-Pt1-os` into one slug, while `original` cannot collide. A private `mr_sanitize` filters **by charset**, so a field added later cannot leak an unsafe character. It uses **BIPM's `[^a-z0-9]+` form, not ETSI's `[^a-z0-9-]+`**: a run of non-alphanumerics collapses to one `-`, without which `v3.0]-PS01` prints a bare `--`. Three characters need the filter today — the `.` in every version (`Renderers::MrString` joins *segments* with `.`, so a dot inside one breaks the documented structure), plus the space and `]` of a few malformed records. Result: **605 distinct slugs for the 605 published ids, 0 raises, 0 characters outside `[a-z0-9._-]`**. One caveat, the same one BIPM records: `-` is both the intra-slug join and the substitute, so two slugs differing only in which non-slug character they use collapse. The corpus has exactly one such pair — the malformed `OpenC2-MQTT-v1.0] -CS01` spelled once with a normal space and once with a non-breaking one, two spellings of one document.
|
|
14
|
+
|
|
15
|
+
- **`#exclude` clears `original`, so a partial reference can widen**: `#matches?` is `exclude(*ignore) == other.exclude(*ignore)`, and `original` spells out verbatim the very component the caller is ignoring. So it survived the exclusion and a bare `OASIS WSDM` could **never** match `OASIS WSDM-v1.1`, however much was ignored — relaton would narrow to the right index bucket and then match nothing in it. `Pubid::Oasis::Identifier#exclude` now nils `original` when the excluded set intersects `DECOMPOSITION_KEYS` (`number version stage part label`), the "reset the whole cluster" rule CSA applies to its year-format siblings. **A plain `==` still compares `original`, deliberately**: an excluded copy is a comparison token, never a rendered document, so widening happens only when the caller asks for it. Dropping `original` from `==` outright was considered and rejected — it costs nothing on today's corpus (no two ids collide) but it exposes those 7 lossy ids to a silent collision with a future sibling, and it would let two different printed identifiers be equal. `spec/pubid/oasis/partial_ref_spec.rb` locks both halves.
|
|
16
|
+
|
|
17
|
+
- **relaton note**: nothing published needs migrating. `relaton-oasis` has no pubid dependency and its `index-v1.yaml` is string-keyed (`:id: OASIS amqp-core`), which is why the serialized shape was changed properly rather than patched. There is **no alias** for the old `spec` key, and a pre-`number` row would deserialize with a nil `number` and no error (lutaml ignores unknown keys), so a future pubid index must be crawled *after* this. Rendering is the loud failure if it is not.
|
|
18
|
+
|
|
19
|
+
- **Verification and the fixture net**: the change was verified by replaying a `main` baseline over all 605 published ids plus the fixture lines — `to_s`, `to_urn`, identifier class and parse errors **byte-identical**, `to_hash` differing **only** by the key rename, empty index keys 607 → 0, distinct MR slugs 1 → 606 of 607, and `from_hash(to_hash) == id` for all 605 with **0** pairs of distinct ids equal. `spec/pubid/oasis/fixtures_spec.rb` is live — its glob is correct, so OASIS is not on the `ten-dead-fixture-specs` list — and `root_number_spec.rb` adds a corpus sweep over the same file. **Do not run `rake "validation:classify[oasis]"`**: there is no `spec/fixtures/oasis/identifiers/full/`, so the generator would delete `pass/oasis.txt` and rebuild nothing. (hand-off: oasis-index-number.)
|
data/lib/pubid/oasis/builder.rb
CHANGED
|
@@ -36,6 +36,18 @@ module Pubid
|
|
|
36
36
|
# building the table before `Identifiers::Standard` snapshots it — DO NOT
|
|
37
37
|
# split this class across two files.
|
|
38
38
|
attribute :number, :string
|
|
39
|
+
|
|
40
|
+
# `original` alone drives #to_s, and the shared exclude-copy loses it
|
|
41
|
+
# (the redefined attribute does not survive the rebuild). An all-parts
|
|
42
|
+
# copy is therefore dup-based: nil the part and edition-ish attributes
|
|
43
|
+
# in place, keeping the printed slug verbatim.
|
|
44
|
+
def without_parts(*extra)
|
|
45
|
+
copy = dup
|
|
46
|
+
(::Pubid::Identifier::PART_ATTRIBUTES + extra).each do |name|
|
|
47
|
+
copy.public_send(:"#{name}=", nil) if copy.respond_to?(:"#{name}=")
|
|
48
|
+
end
|
|
49
|
+
copy
|
|
50
|
+
end
|
|
39
51
|
attribute :version, :string
|
|
40
52
|
attribute :stage, :string
|
|
41
53
|
attribute :part, :string
|
|
@@ -91,6 +103,13 @@ module Pubid
|
|
|
91
103
|
result
|
|
92
104
|
end
|
|
93
105
|
|
|
106
|
+
# The same reason applies to a subset match: `original` spells out the
|
|
107
|
+
# parts a partial reference omits, so `===` compares the decomposition
|
|
108
|
+
# only. Two slugs that decompose alike therefore match each other.
|
|
109
|
+
def self.subset_ignored_attributes
|
|
110
|
+
%i[original]
|
|
111
|
+
end
|
|
112
|
+
|
|
94
113
|
# MR string hooks. `to_slug` delegates to `to_mr_string` and consumers
|
|
95
114
|
# use it as an output FILENAME, so the slug must be unique per document.
|
|
96
115
|
#
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# OGC flavor notes
|
|
2
|
+
|
|
3
|
+
OGC `all_parts_edition_keys`: `year` is core identity, not an edition marker.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/ogc/` or `spec/pubid/ogc/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **`all_parts_edition_keys` needed `revision` added AND `year` kept OUT — the
|
|
8
|
+
hand-off's own suggested override was wrong about the second half.**
|
|
9
|
+
`Identifier.all_parts_edition_keys` defaults to `%i[date year edition
|
|
10
|
+
version]`. OGC's real edition/version discriminator is `revision`
|
|
11
|
+
("r19" in "12-128r19"), which that list never covered, so
|
|
12
|
+
`"12-128r19".to_all_parts` did not collapse onto another revision of
|
|
13
|
+
the same document. But `year` is **also** in the default list, and for
|
|
14
|
+
OGC `year` is NOT an edition marker — it is the document's core
|
|
15
|
+
`"<yy>"` identity token, the first half of the `"12-128"` core that
|
|
16
|
+
`Renderer#render` requires (`"#{id.year}-#{id.number}"`). Stripping it
|
|
17
|
+
(as the default list does, and as the hand-off's suggested override
|
|
18
|
+
`%i[date year edition version revision]` would have kept doing) drops
|
|
19
|
+
the prefix: `to_all_parts.to_s` rendered `"-128r19 (all parts)"`
|
|
20
|
+
instead of `"12-128 (all parts)"`. The two symptoms — the missing
|
|
21
|
+
prefix and the un-stripped revision — are the SAME root cause
|
|
22
|
+
(`year` wrongly treated as a stripped key), not two separate issues,
|
|
23
|
+
despite looking that way in the original repro. The correct override
|
|
24
|
+
replaces the list entirely rather than extending it:
|
|
25
|
+
```ruby
|
|
26
|
+
def self.all_parts_edition_keys
|
|
27
|
+
%i[revision]
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
`date`/`edition`/`version` are harmless to omit — OGC declares none of
|
|
31
|
+
them as attributes, and `#exclude` (which `without_parts` is built on)
|
|
32
|
+
silently skips a name a class doesn't declare. Locked by
|
|
33
|
+
`spec/pubid/ogc/all_parts_spec.rb` and
|
|
34
|
+
`spec/pubid/all_parts_edition_keys_audit_spec.rb`.
|
data/lib/pubid/ogc/builder.rb
CHANGED
data/lib/pubid/ogc/identifier.rb
CHANGED
|
@@ -16,6 +16,17 @@ module Pubid
|
|
|
16
16
|
# its separator letter: "r1", "c1", "a", "r3a", "r12a". nil when absent.
|
|
17
17
|
attribute :revision, :string
|
|
18
18
|
|
|
19
|
+
# `revision` is OGC's edition/version discriminator, not the default
|
|
20
|
+
# `%i[date year edition version]`. `year` IS in that default list, but
|
|
21
|
+
# here it is the document's core "<yy>" identity token — the renderer
|
|
22
|
+
# requires it to print the "12-128" core (lib/pubid/ogc/renderer.rb) —
|
|
23
|
+
# not an edition marker, so it must stay OUT of the stripped set. Do
|
|
24
|
+
# not "fix" this by adding :revision to the base default; that keeps
|
|
25
|
+
# :year in the list and reproduces the dropped-prefix bug.
|
|
26
|
+
def self.all_parts_edition_keys
|
|
27
|
+
%i[revision]
|
|
28
|
+
end
|
|
29
|
+
|
|
19
30
|
# Polymorphic type map for lutaml::Model key_value (de)serialization: maps
|
|
20
31
|
# the concrete class's polymorphic_name to its class name so a stored hash
|
|
21
32
|
# rebuilds the correct identifier type via from_hash.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# OIML flavor notes
|
|
2
|
+
|
|
3
|
+
OIML index key, bulletins and supplement URNs.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/oiml/` or `spec/pubid/oiml/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
## From the root note "ETSI and OIML index key (`root.number`): the `code` component became flat leaf columns"
|
|
8
|
+
|
|
9
|
+
**(5) OIML `Bulletin` is the one leaf with no code** (its locator is the year/issue/sequence tuple), so it deliberately does **not** include `CodeNumber`. Its key was first the year, as a derived reader; it is now the issue, stored as `number`. The section "Bulletin: the issue is the `number`" below records why. **(6) A pre-existing OIML crash closed in passing**: `Pubid::Oiml::SupplementIdentifier` descends from `Oiml::Identifier` **directly** — it is a *sibling* of `SingleIdentifier`, where `code` and `iteration` live — while `UrnGenerator` reads both unconditionally, so `to_urn` **raised `NoMethodError`** for every Amendment/Errata/Annex (confirmed on the `main` baseline: all five supplement fixtures already raised). Two delegations (`code`/`iteration` → `base&.…`) fix it. **Two gaps deliberately left open and pinned** by expectations in `root_number_spec.rb` that assert the *current* behaviour, so a future fix must flip them: the OIML URN encodes no supplement marker, so an annex, an amendment and their base share one URN (the documented ITU supplement-URN shape); and a supplement inherits none of `SingleIdentifier`'s `mr_*` hooks, so **every** OIML supplement still slugs to `""` — a filename collision, unchanged from `main`. (follow-up hand-off: oiml-supplement-identity-surfaces.)
|
|
10
|
+
|
|
11
|
+
## Bulletin: the issue is the `number`
|
|
12
|
+
|
|
13
|
+
**The problem.** The first design derived `Bulletin#number` from `date&.year` as a reader, and did not serialize it, so that `year` did not repeat in each row. The runtime key was correct: relaton keys on `root.number.to_s`, and 4133 of the 4134 Bulletin rows of the published `relaton-data-oiml` `index-v2.yaml` keyed on their year. But a scan of the raw YAML reported **4134 rows with no `number`**, and a reader of the index could not see the key.
|
|
14
|
+
|
|
15
|
+
**The change.** The year is the volume and the issue is the number, as in the citation form `LXVII(2)`. So the attribute `issue` became `attribute :number, :string`, mapped as `"number"`, and the derived year reader was deleted. Nothing new is stored: the value moved from the `issue` key to the `number` key. No `issue` method remains, and the constructor refuses an `issue:` key: in Ruby and in the hash, the name is `number`. The attribute is declared on the Bulletin **leaf**, which has no subclasses, so the multi-flavor determinism landmine does not apply (the `CodeNumber` pattern).
|
|
16
|
+
|
|
17
|
+
**No legacy read.** pubid does not read the old `issue` key, by decision. lutaml ignores a key that no attribute maps, and it raises no error. So an old row loads WITHOUT its issue and becomes a different identifier: `{year: "1960", issue: "03", sequence: "01"}` renders as `OIML Bulletin 1960-01`, with URN `urn:oiml:bulletin:1960-01` and no index key. The published `relaton-data-oiml` `index-v2.yaml` must therefore be crawled again when this change is released, before relaton uses it for a Bulletin lookup.
|
|
18
|
+
|
|
19
|
+
**The trade-off, measured on the 4134 published Bulletin rows:**
|
|
20
|
+
|
|
21
|
+
- Rows with no `number`: 4134 → 68. These are the 67 volume rows (`OIML Bulletin 1960`) and the bare `OIML Bulletin`, which name no issue. Their key is `""`, and they share one bucket.
|
|
22
|
+
- The largest bucket: 112 → 1215. The key puts the same issue number of all volumes together (all `01` issues), so relaton's filter reads more rows after the binary search. The binary search stays valid.
|
|
23
|
+
- The MR slug does not change (`oiml.bulletin.03-01.1960`): `mr_number_with_part` emits `number`-`sequence` and not the year, which `Renderers::MrString` gives its own segment. The slug keeps each article distinct.
|
|
24
|
+
|
|
25
|
+
**Verified** by replaying a baseline captured on `main` over all 5646 published rows (each Bulletin `issue` key renamed to `number`, as a new crawl writes it) and every OIML fixture: `to_s`, `to_urn`, `to_mr_string` and the class are identical; a Bulletin `to_hash` differs only by `issue` → `number`; `from_hash(to_hash) == id` did not change for any entry. Locked by the "Bulletin keys on its issue" block of `spec/pubid/oiml/root_number_spec.rb`.
|
|
26
|
+
|
|
27
|
+
**Relaton note (not caused by this change).** `Relaton::Oiml::Bibliography#pubid_match?` compares `exclude(:year, :language).to_s`. For a Bulletin that string is `"OIML Bulletin"` for every article, so a query can return a different article of the same year (hand-off `relaton__relaton__oiml-bulletin-pubid-match`).
|
|
28
|
+
|
|
29
|
+
## Subset match: strict attributes
|
|
30
|
+
|
|
31
|
+
Read `docs/SUBSET_MATCH.md` first. `===` reads a nil part of the reference as
|
|
32
|
+
a wildcard, which is wrong for the attributes below: the flavor models a nil
|
|
33
|
+
value as "this document has none". They are declared with `subset_strict`, so
|
|
34
|
+
`===` compares them exactly and a stated collection is not a prefix. A caller
|
|
35
|
+
that does want every part of a document sets `all_parts` on the reference, or
|
|
36
|
+
keeps `#matches?(other, ignore:)`.
|
|
37
|
+
|
|
38
|
+
- **`part`, `subpart` and `suffix` are strict**, so `OIML R 138` does not
|
|
39
|
+
match `OIML R 138-Amend:2009`, which is its amendment, and
|
|
40
|
+
`OIML R 137-1 (F)` does not match `OIML R 137-1-2:2012 (F)`, which names a
|
|
41
|
+
subpart it doesn't. The declaration rides the `Identifiers::CodeNumber`
|
|
42
|
+
mixin (`lib/pubid/oiml/identifiers/code_number.rb`), so it reaches all
|
|
43
|
+
seven leaves that install the columns; `Bulletin`, which does not include
|
|
44
|
+
the mixin, is unaffected. `Oiml::Components::Code` carries the same
|
|
45
|
+
declaration for the shapes not to drift, although `===` does not reach it
|
|
46
|
+
today.
|
|
47
|
+
- **`language` is strict**, declared separately on `SingleIdentifier` and
|
|
48
|
+
`SupplementIdentifier` (each declares its own `language` attribute), so
|
|
49
|
+
`OIML R 126:2015 Errata` does not match `OIML R 126:2015 Errata (E)`, its
|
|
50
|
+
English edition. It could not be declared once on the shared `Identifier`
|
|
51
|
+
ancestor: `spec/pubid/subset_match_spec.rb`'s "names only attributes the
|
|
52
|
+
class declares" check requires every `subset_strict` name to be a real
|
|
53
|
+
attribute on the declaring class, and `Identifier` itself declares neither
|
|
54
|
+
`language` nor `letter`.
|
|
55
|
+
- **`Annex#letter` is strict**, so `OIML R 102 Annexes` (no letter — the
|
|
56
|
+
plural form) does not match `OIML R 102:1995 Annex B-C`, which names one.
|
|
57
|
+
Found in review alongside the `language`/`subpart` work above: same bug
|
|
58
|
+
shape, on the one OIML attribute besides `language` that names a specific
|
|
59
|
+
sub-document and had no `subset_strict` declaration.
|
|
60
|
+
|
|
61
|
+
## Subset match: ignored render flags
|
|
62
|
+
|
|
63
|
+
`parsed_format` (`SingleIdentifier`/`SupplementIdentifier`), `year_on_base`
|
|
64
|
+
(`Annex`), `space_suffix` (`CodeNumber` mixin / `Components::Code`),
|
|
65
|
+
`trailing` and `joined` (`SupplementIdentifier`) are Boolean or string
|
|
66
|
+
attributes with a non-nil default that record which of two equivalent input
|
|
67
|
+
spellings a reference used (short vs long format, dash- vs space-separated
|
|
68
|
+
suffix, trailing-word vs prose supplement form, year glued to the base vs to
|
|
69
|
+
the marker) — never something the document itself states. Left alone, their
|
|
70
|
+
non-nil default reads as "stated" to `===` even in a bare reference (the
|
|
71
|
+
CSA/IEEE year-format trap `docs/SUBSET_MATCH.md` describes), which is why
|
|
72
|
+
`OIML R 102 Annex B-C === OIML R 102:1995 Annex B-C` used to disagree with
|
|
73
|
+
relaton's own OIML match on `year_on_base` alone. They are listed in
|
|
74
|
+
`Oiml::Identifier.subset_ignored_attributes` (`lib/pubid/oiml/identifier.rb`)
|
|
75
|
+
as one shared list on the flavor's common ancestor: a name that isn't an
|
|
76
|
+
attribute of a given leaf (e.g. `year_on_base` only exists on `Annex`) is a
|
|
77
|
+
harmless no-op there, since `===` only walks `self.class.attributes`.
|
|
78
|
+
|
|
79
|
+
**Measurement that drove this** (hand-off
|
|
80
|
+
`metanorma__pubid__oiml-subset-match-strict-language.md`): relaton compared
|
|
81
|
+
`===` against its own OIML match over the full 5,646-row `relaton-data-oiml`
|
|
82
|
+
index (4.57M pairs) and found 192 disagreeing pairs outside the Bulletins —
|
|
83
|
+
156 on `language`, 6 on `subpart`, 30 on `year_on_base` — all wrong on the
|
|
84
|
+
`===` side before this branch.
|
|
85
|
+
|
|
86
|
+
## Dual-published identifiers: OIML co-publication with another SDO (pubid #437)
|
|
87
|
+
|
|
88
|
+
**The shape.** OIML sometimes co-publishes a document jointly with another
|
|
89
|
+
SDO — ISO confirmed so far. The printed reference carries both identifiers,
|
|
90
|
+
joined by a bare `|`, e.g. `ISO 4064-1:2024|OIML R 49-1:2024`. Both sides
|
|
91
|
+
name the same document, each in its own SDO's scheme.
|
|
92
|
+
|
|
93
|
+
**`Identifiers::DualPublished` is a THIRD sibling** of `SingleIdentifier`/
|
|
94
|
+
`SupplementIdentifier` under `Oiml::Identifier` — same shape as
|
|
95
|
+
`SupplementIdentifier`, and the same lesson applies: it inherits **nothing**
|
|
96
|
+
from `SingleIdentifier`, so every identity-bearing reader `UrnGenerator` or
|
|
97
|
+
the MR-slug renderer calls (`code`, `type`, `stage`, `iteration`,
|
|
98
|
+
`publisher`, and the `mr_publisher`/`mr_type`/`mr_number_with_part`/
|
|
99
|
+
`mr_year` hooks, plus `#root`) is hand-delegated to whichever side is OIML.
|
|
100
|
+
Without the `mr_*` delegations this class would have shipped with the exact
|
|
101
|
+
same empty-slug gap already recorded above for Amendment/Errata/Annex; this
|
|
102
|
+
class closes it from day one instead of reproducing it.
|
|
103
|
+
|
|
104
|
+
**`first`/`second` hold the two sides in original left-to-right print
|
|
105
|
+
order**, each a real `attribute ..., ::Pubid::Identifier, polymorphic: true`
|
|
106
|
+
— never an `attr_accessor` (see IEEE's `CsaDualPublished`, which used one
|
|
107
|
+
and silently dropped its second identifier from `to_hash`/`from_hash`/
|
|
108
|
+
`#exclude`). `#oiml_identifier`/`#external_identifier` pick the OIML-typed
|
|
109
|
+
member out of the pair by `is_a?`, not by position, since nothing guarantees
|
|
110
|
+
which side prints first.
|
|
111
|
+
|
|
112
|
+
**`language`/`parsed_format` are declared locally even though this class
|
|
113
|
+
never sets them.** `Oiml::Identifier`'s shared `key_value` block (inherited
|
|
114
|
+
by every OIML class) maps `"language"` and `"parsed_format"` to real
|
|
115
|
+
attributes; a subclass's own `key_value` block **merges** with its parent's
|
|
116
|
+
rather than replacing it, so `to_hash`/`from_hash` would call
|
|
117
|
+
`model.language`/`model.language=` regardless of what this class declares.
|
|
118
|
+
Leaving them undeclared would crash `to_hash` outright (no such method);
|
|
119
|
+
delegating `language` as a read-only method would crash `from_hash` (no
|
|
120
|
+
setter). `SingleIdentifier` and `SupplementIdentifier` both declare these
|
|
121
|
+
locally for the same reason — this class follows the same pattern rather
|
|
122
|
+
than inventing a new one.
|
|
123
|
+
|
|
124
|
+
**Parse dispatch lives in `Pubid::Oiml.parse`/
|
|
125
|
+
`Identifiers::DualPublished.build`, not the Parslet grammar.** The string is
|
|
126
|
+
split on `|` before the grammar ever runs; the side that matches `PREFIXES`
|
|
127
|
+
(`["OIML"]`) is parsed via `Oiml.parse` (re-entering this same module
|
|
128
|
+
method — safe, since the substring has no `|`), the other via the generic
|
|
129
|
+
top-level `Pubid.parse`, which auto-routes to any flavor by prefix. `.build`
|
|
130
|
+
returns `nil` for anything that isn't exactly two non-empty sides with
|
|
131
|
+
exactly one OIML side, letting the caller fall through to the ordinary
|
|
132
|
+
grammar — which has no `|` rule, so it raises the standard
|
|
133
|
+
`Parslet::ParseFailed`, keeping the uniform parse-error contract intact. The
|
|
134
|
+
side is detected by content, not position, so `OIML R 49-1:2024|ISO
|
|
135
|
+
4064-1:2024` parses identically in the other order.
|
|
136
|
+
|
|
137
|
+
**The URN represents only the OIML side** (`urn:oiml:...`) — there is no
|
|
138
|
+
joint URN scheme anywhere in this codebase, matching every other
|
|
139
|
+
cross-flavor wrapper (IEEE's `AdoptedStandard`/`MultiNumberedIdentifier`).
|
|
140
|
+
One generator change was required: `UrnGenerator::Base#urn_year` gates on
|
|
141
|
+
`identifier.class.attributes.key?(:date)`, and `DualPublished` declares no
|
|
142
|
+
`date` of its own (it lives on whichever side is OIML) — so
|
|
143
|
+
`Oiml::UrnGenerator#urn_year` special-cases the class and reads the date
|
|
144
|
+
through `#oiml_identifier` directly. Every other URN segment
|
|
145
|
+
(`urn_type`/`urn_number`/`urn_stage`/`urn_iteration`/`urn_language`) works
|
|
146
|
+
for free through the §1 delegations, since they already read plain
|
|
147
|
+
`identifier.type`/`.code`/etc. rather than going through the attributes
|
|
148
|
+
registry.
|
|
149
|
+
|
|
150
|
+
**`(de)serialization goes through `Pubid.from_hash`/`#to_hash`, not the
|
|
151
|
+
OIML-scoped `Identifier.from_hash`**, because either side of `first`/
|
|
152
|
+
`second` may belong to any flavor — `Identifier.from_hash` only resolves
|
|
153
|
+
OIML `_type` values. Verified this round-trips both an OIML-native nested
|
|
154
|
+
hash and a foreign one via the shared global `TypeResolver`.
|
|
155
|
+
|
|
156
|
+
**Known, deliberately out-of-scope gaps**, matching the issue's one
|
|
157
|
+
confirmed example (a plain `Recommendation` on the OIML side, no edition):
|
|
158
|
+
|
|
159
|
+
- An OIML side that is itself a Amendment/Annex/Errata
|
|
160
|
+
(`SupplementIdentifier`) is untested — the delegation chain should mostly
|
|
161
|
+
work transitively, but this hasn't been exercised.
|
|
162
|
+
- Only two-way co-publication is modeled (two fixed attributes), not
|
|
163
|
+
three-or-more.
|
|
164
|
+
- **`edition` is not delegated**, unlike `code`/`type`/`stage`/etc. This is
|
|
165
|
+
deliberate, not an oversight: OIML's own `edition` is a plain `:string`
|
|
166
|
+
attribute (`SingleIdentifier`), but the shared `#mr_edition`/`#urn_edition`
|
|
167
|
+
(`lib/pubid/identifier.rb`, `lib/pubid/urn_generator/base.rb`) assume the
|
|
168
|
+
`Components::Edition` shape and call `edition.number` — a **pre-existing**
|
|
169
|
+
bug in OIML itself: `Pubid::Oiml.parse("OIML E 5 6th Edition 2015
|
|
170
|
+
(E)").to_mr_string` already raises `NoMethodError` on `main`, with no
|
|
171
|
+
`DualPublished` involved. Delegating `edition` here would only reproduce
|
|
172
|
+
that crash through a second path; leaving it undelegated means an
|
|
173
|
+
edition-bearing OIML side silently drops its edition from the MR slug/URN
|
|
174
|
+
instead of raising — worse data loss, but no new crash surface. Fixing the
|
|
175
|
+
root cause (OIML's edition/MR-edition type mismatch) is unrelated to
|
|
176
|
+
dual-published identifiers and is left for its own change.
|
|
177
|
+
- **Annotated rendering (`to_s(annotated: true)`) only spans the OIML
|
|
178
|
+
side's publisher/type**, not its number/year, and not the external side at
|
|
179
|
+
all. This matches — not regresses — the existing shipped behavior of the
|
|
180
|
+
closest precedent, IEEE's `Identifiers::AdoptedStandard`: `Renderers::
|
|
181
|
+
Annotator#nested_identifiers` (`lib/pubid/renderers/annotator.rb`) only
|
|
182
|
+
recurses into `base`/`ids`/`identifiers`/`bundled_with`, none of which
|
|
183
|
+
`DualPublished` (or `AdoptedStandard`, which uses `ieee_identifier`/
|
|
184
|
+
`adopted_identifiers`) uses, so neither wrapper's nested sides get full
|
|
185
|
+
token annotation. The uniform contract this repo enforces is narrower than
|
|
186
|
+
full annotation: every `to_s` must **accept** `annotated:` without
|
|
187
|
+
raising, which this class does (verified by a spec). Widening
|
|
188
|
+
`Annotator#nested_identifiers` to close this for every such wrapper at
|
|
189
|
+
once is a separate, cross-flavor change.
|
data/lib/pubid/oiml/builder.rb
CHANGED
|
@@ -16,6 +16,9 @@ module Pubid
|
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
18
18
|
def build(parsed_hash)
|
|
19
|
+
# Certification-system documents carry no type letter
|
|
20
|
+
return build_cs(parsed_hash) if parsed_hash[:cs_series]
|
|
21
|
+
|
|
19
22
|
# Check for short amendment format (has amd_marker)
|
|
20
23
|
if parsed_hash[:amd_marker]
|
|
21
24
|
return build_short_amendment(parsed_hash)
|
|
@@ -32,6 +35,20 @@ module Pubid
|
|
|
32
35
|
|
|
33
36
|
private
|
|
34
37
|
|
|
38
|
+
# OIML-CS documents: family (PD/OD/CID), zero-padded number, the
|
|
39
|
+
# printed "Edition N", and an optional parenthesized trailing
|
|
40
|
+
# amendment. The family-number separator is a spelling flag.
|
|
41
|
+
def build_cs(parsed_hash)
|
|
42
|
+
identifier = Identifiers::CertificationSystem.new
|
|
43
|
+
identifier.publisher = parsed_hash[:publisher].to_s if parsed_hash[:publisher]
|
|
44
|
+
identifier.family = parsed_hash[:cs_family].to_s
|
|
45
|
+
identifier.number = parsed_hash[:number].to_s
|
|
46
|
+
identifier.edition = parsed_hash[:edition].to_s if parsed_hash[:edition]
|
|
47
|
+
identifier.amendment = parsed_hash[:cs_amendment].to_s if parsed_hash[:cs_amendment]
|
|
48
|
+
identifier.space_separator = parsed_hash[:cs_separator] == " "
|
|
49
|
+
identifier
|
|
50
|
+
end
|
|
51
|
+
|
|
35
52
|
def build_short_amendment(parsed_hash)
|
|
36
53
|
# Build base identifier from the code and type
|
|
37
54
|
base_hash = {
|
|
@@ -110,6 +127,7 @@ module Pubid
|
|
|
110
127
|
|
|
111
128
|
# Set supplement-specific attributes
|
|
112
129
|
supplement.year = year_value.to_s if year_value
|
|
130
|
+
supplement.number = parsed_hash[:number].to_s if parsed_hash[:number]
|
|
113
131
|
supplement.language = extract_language(parsed_hash[:language]) if parsed_hash[:language]
|
|
114
132
|
supplement.letter = parsed_hash[:annex_letter].to_s if parsed_hash[:annex_letter]
|
|
115
133
|
|
|
@@ -228,13 +246,13 @@ module Pubid
|
|
|
228
246
|
article_id = parsed_hash[:article_id].to_s
|
|
229
247
|
identifier.date ||= Pubid::Components::Date.new
|
|
230
248
|
identifier.date.year = article_id[0, 4]
|
|
231
|
-
identifier.
|
|
249
|
+
identifier.number = article_id[4, 2]
|
|
232
250
|
identifier.sequence = article_id[6, 2]
|
|
233
251
|
warn_on_volume_mismatch(identifier, parsed_hash[:volume_roman])
|
|
234
252
|
return
|
|
235
253
|
end
|
|
236
254
|
|
|
237
|
-
identifier.
|
|
255
|
+
identifier.number = parsed_hash[:issue].to_s if parsed_hash[:issue]
|
|
238
256
|
identifier.sequence = parsed_hash[:sequence].to_s if parsed_hash[:sequence]
|
|
239
257
|
end
|
|
240
258
|
|
|
@@ -268,3 +286,5 @@ module Pubid
|
|
|
268
286
|
end
|
|
269
287
|
end
|
|
270
288
|
end
|
|
289
|
+
|
|
290
|
+
Pubid::Oiml::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
|
@@ -10,6 +10,8 @@ module Pubid
|
|
|
10
10
|
# shape is otherwise compatible; a future rename of +number+ →
|
|
11
11
|
# +value+ would let this become a subclass.
|
|
12
12
|
class Code < Lutaml::Model::Serializable
|
|
13
|
+
include ::Pubid::SubsetMatch
|
|
14
|
+
|
|
13
15
|
attribute :number, :string
|
|
14
16
|
attribute :part, :string
|
|
15
17
|
attribute :subpart, :string
|
|
@@ -20,6 +22,10 @@ module Pubid
|
|
|
20
22
|
# the default dash ("R 60-sup").
|
|
21
23
|
attribute :space_suffix, :boolean, default: false
|
|
22
24
|
|
|
25
|
+
# Mirrors the flat columns the CodeNumber mixin installs on the
|
|
26
|
+
# seven leaves, which own the identity.
|
|
27
|
+
subset_strict :part, :subpart, :suffix
|
|
28
|
+
|
|
23
29
|
def to_s
|
|
24
30
|
result = number.to_s
|
|
25
31
|
result += "-#{part}" if part
|
|
@@ -12,6 +12,7 @@ module Pubid
|
|
|
12
12
|
"pubid:oiml:basic-publication" => "Pubid::Oiml::Identifiers::BasicPublication",
|
|
13
13
|
"pubid:oiml:bulletin" => "Pubid::Oiml::Identifiers::Bulletin",
|
|
14
14
|
"pubid:oiml:document" => "Pubid::Oiml::Identifiers::Document",
|
|
15
|
+
"pubid:oiml:dual-published" => "Pubid::Oiml::Identifiers::DualPublished",
|
|
15
16
|
"pubid:oiml:guide" => "Pubid::Oiml::Identifiers::Guide",
|
|
16
17
|
"pubid:oiml:vocabulary" => "Pubid::Oiml::Identifiers::Vocabulary",
|
|
17
18
|
"pubid:oiml:expert-report" => "Pubid::Oiml::Identifiers::ExpertReport",
|
|
@@ -19,6 +20,7 @@ module Pubid
|
|
|
19
20
|
"pubid:oiml:amendment" => "Pubid::Oiml::Identifiers::Amendment",
|
|
20
21
|
"pubid:oiml:errata" => "Pubid::Oiml::Identifiers::Errata",
|
|
21
22
|
"pubid:oiml:annex" => "Pubid::Oiml::Identifiers::Annex",
|
|
23
|
+
"pubid:oiml:certification-system" => "Pubid::Oiml::Identifiers::CertificationSystem",
|
|
22
24
|
}.freeze
|
|
23
25
|
|
|
24
26
|
# The base Pubid::Identifier no longer auto-maps attributes, so OIML must
|
|
@@ -34,6 +36,17 @@ module Pubid
|
|
|
34
36
|
map "parsed_format", to: :parsed_format
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
# Rendering-only flags: each records which of two equivalent input
|
|
40
|
+
# spellings a document used (short vs long format, dash- vs
|
|
41
|
+
# space-separated suffix, trailing-word vs prose supplement form, year
|
|
42
|
+
# glued to the base vs to the marker), not something the document
|
|
43
|
+
# itself states. Their non-nil default would otherwise read as
|
|
44
|
+
# "stated" in a bare reference and wrongly refuse a candidate that
|
|
45
|
+
# sets them (the CSA/IEEE year-format trap).
|
|
46
|
+
def self.subset_ignored_attributes
|
|
47
|
+
%i[parsed_format year_on_base space_suffix trailing joined]
|
|
48
|
+
end
|
|
49
|
+
|
|
37
50
|
# lutaml's polymorphic_map only validates `_type` on deserialization; it
|
|
38
51
|
# does not re-instantiate the concrete subclass. Route by `_type` so
|
|
39
52
|
# `Pubid::Oiml::Identifier.from_hash(h)` returns the right class (and its
|
|
@@ -15,6 +15,10 @@ module Pubid
|
|
|
15
15
|
map "year_on_base", to: :year_on_base
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# A nil `letter` means the plural "Annexes" reference, not "any
|
|
19
|
+
# annex": it must not match a candidate naming a specific one.
|
|
20
|
+
subset_strict :letter
|
|
21
|
+
|
|
18
22
|
def supplement_type
|
|
19
23
|
letter ? "Annex #{letter}" : "Annexes"
|
|
20
24
|
end
|