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.
- 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/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/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/national_annex.rb +18 -20
- 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/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 +8 -0
- 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 +2 -0
- data/lib/pubid/csa/identifier.rb +13 -1
- data/lib/pubid/csa/renderer.rb +12 -12
- data/lib/pubid/csa/single_identifier.rb +17 -0
- 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 +2 -0
- data/lib/pubid/gb/CLAUDE.md +140 -0
- data/lib/pubid/gb/builder.rb +7 -2
- data/lib/pubid/gb/identifier.rb +5 -3
- data/lib/pubid/gb/identifiers/all_parts.rb +17 -0
- data/lib/pubid/gb/identifiers.rb +1 -0
- data/lib/pubid/gb/renderer.rb +0 -1
- 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 +161 -17
- data/lib/pubid/idf/builder.rb +6 -1
- data/lib/pubid/idf/identifier.rb +5 -0
- data/lib/pubid/idf/identifiers/all_parts.rb +17 -0
- data/lib/pubid/idf/identifiers.rb +1 -0
- 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 +134 -5
- 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/parser.rb +134 -21
- data/lib/pubid/ieee/renderer.rb +40 -7
- data/lib/pubid/ieee/urn_generator.rb +31 -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 +19 -5
- data/lib/pubid/iso/components/publisher.rb +2 -0
- data/lib/pubid/iso/identifier.rb +6 -14
- data/lib/pubid/iso/identifiers/all_parts.rb +19 -0
- data/lib/pubid/iso/identifiers/directives_supplement.rb +4 -2
- data/lib/pubid/iso/identifiers.rb +1 -0
- data/lib/pubid/iso/rendering_style.rb +0 -1
- data/lib/pubid/itu/CLAUDE.md +69 -0
- data/lib/pubid/itu/builder.rb +2 -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/supplement.rb +15 -0
- 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 +2 -0
- 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 +24 -3
- data/lib/pubid/nist/parser.rb +15 -2
- data/lib/pubid/nist/preprocessor.rb +53 -2
- 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 +20 -0
- 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/certification_system.rb +34 -0
- data/lib/pubid/oiml/identifiers/code_number.rb +8 -0
- data/lib/pubid/oiml/identifiers/dual_published.rb +174 -0
- data/lib/pubid/oiml/identifiers.rb +2 -0
- data/lib/pubid/oiml/parser.rb +35 -4
- data/lib/pubid/oiml/renderer.rb +23 -1
- data/lib/pubid/oiml/single_identifier.rb +4 -0
- data/lib/pubid/oiml/supplement_identifier.rb +7 -0
- data/lib/pubid/oiml/urn_generator.rb +28 -0
- 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/human_readable.rb +0 -1
- data/lib/pubid/sae/builder.rb +2 -0
- data/lib/pubid/sae/components/date.rb +2 -0
- data/lib/pubid/sae/components/type.rb +2 -0
- 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
- metadata +44 -2
data/lib/pubid/iso/builder.rb
CHANGED
|
@@ -58,6 +58,12 @@ module Pubid
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def build(parsed_hash)
|
|
61
|
+
# "(all parts)" and the ":ser" URN name every part of the document,
|
|
62
|
+
# so they build an AllPartsIdentifier around the document (the URN
|
|
63
|
+
# parser puts the key on the outermost identifier for the same
|
|
64
|
+
# reason). The document itself holds no all-parts mark.
|
|
65
|
+
all_parts = parsed_hash.delete(:all_parts)
|
|
66
|
+
|
|
61
67
|
# For ISO/R legacy format, split into publisher and type
|
|
62
68
|
if parsed_hash[:iso_r_prefix]
|
|
63
69
|
parsed_hash[:publisher] = "ISO"
|
|
@@ -77,14 +83,20 @@ module Pubid
|
|
|
77
83
|
end
|
|
78
84
|
end
|
|
79
85
|
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
#
|
|
86
|
+
# For French GUIDE entries: "Guide ISO/CEI 37:1995". The rename
|
|
87
|
+
# must happen BEFORE class selection — locate_identifier_klass
|
|
88
|
+
# reads :type_with_stage, and the tree of a guide-first spelling
|
|
89
|
+
# carries an empty :type_with_stage plus the type under
|
|
90
|
+
# :type_with_stage_fr, so deferring the rename selected the
|
|
91
|
+
# default International Standard class for every guide-first
|
|
92
|
+
# (and Cyrillic "Руководства ИСО …") reference.
|
|
84
93
|
if type_with_stage_fr = parsed_hash.delete(:type_with_stage_fr)
|
|
85
94
|
parsed_hash[:type_with_stage] = type_with_stage_fr
|
|
86
95
|
end
|
|
87
96
|
|
|
97
|
+
# Instantiate the identifier based on the typed stage
|
|
98
|
+
identifier = locate_identifier_klass(parsed_hash).new
|
|
99
|
+
|
|
88
100
|
# For DirectivesSupplement, rename :publisher to :supplement_publisher
|
|
89
101
|
if identifier.is_a?(Identifiers::DirectivesSupplement) && parsed_hash[:publisher]
|
|
90
102
|
parsed_hash[:supplement_publisher] = parsed_hash.delete(:publisher)
|
|
@@ -112,7 +124,7 @@ module Pubid
|
|
|
112
124
|
identifier.type = default_typed_stage.to_type
|
|
113
125
|
end
|
|
114
126
|
|
|
115
|
-
identifier
|
|
127
|
+
all_parts ? identifier.to_all_parts : identifier
|
|
116
128
|
end
|
|
117
129
|
|
|
118
130
|
def handle_key(identifier, key, value)
|
|
@@ -313,3 +325,5 @@ module Pubid
|
|
|
313
325
|
end
|
|
314
326
|
end
|
|
315
327
|
end
|
|
328
|
+
|
|
329
|
+
Pubid::Iso::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
|
@@ -8,6 +8,8 @@ module Pubid
|
|
|
8
8
|
# ISO Publisher with copublisher support
|
|
9
9
|
# Examples: ISO, ISO/IEC, ISO/IEC/IEEE
|
|
10
10
|
class Publisher < Lutaml::Model::Serializable
|
|
11
|
+
include ::Pubid::SubsetMatch
|
|
12
|
+
|
|
11
13
|
attribute :publisher, :string, default: -> { "ISO" }
|
|
12
14
|
attribute :copublisher, :string, collection: true
|
|
13
15
|
|
data/lib/pubid/iso/identifier.rb
CHANGED
|
@@ -11,6 +11,12 @@ module Pubid
|
|
|
11
11
|
attribute :copublishers, ::Pubid::Iso::Components::Publisher,
|
|
12
12
|
collection: true
|
|
13
13
|
|
|
14
|
+
# ISO prints "(all parts)" and has a series URN, so it has its own
|
|
15
|
+
# all-parts class.
|
|
16
|
+
def self.all_parts_class
|
|
17
|
+
Identifiers::AllParts
|
|
18
|
+
end
|
|
19
|
+
|
|
14
20
|
# The publisher implied when none is serialized. ISO for most types;
|
|
15
21
|
# publisher-less types (IWA) override this to nil.
|
|
16
22
|
def self.default_publisher
|
|
@@ -137,20 +143,6 @@ module Pubid
|
|
|
137
143
|
# unique typed-stage `code` under "stage" and recompute the rest on
|
|
138
144
|
# load. _type already pins the document type.
|
|
139
145
|
map "stage", with: { to: :stage_to_kv, from: :stage_from_kv }
|
|
140
|
-
# Omit the `false` default; only the meaningful `true` is serialized.
|
|
141
|
-
map "all_parts", with: { to: :all_parts_to_kv, from: :all_parts_from_kv }
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def all_parts_to_kv(model, doc)
|
|
145
|
-
return unless model.all_parts
|
|
146
|
-
|
|
147
|
-
doc.add_child(
|
|
148
|
-
Lutaml::KeyValue::DataModel::Element.new("all_parts", true),
|
|
149
|
-
)
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def all_parts_from_kv(model, value)
|
|
153
|
-
model.all_parts = value
|
|
154
146
|
end
|
|
155
147
|
|
|
156
148
|
# Serialize typed_stage as just its unique code (e.g. "is", "dis",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iso
|
|
5
|
+
module Identifiers
|
|
6
|
+
# Every part of one ISO document: "ISO 9000 (all parts)".
|
|
7
|
+
#
|
|
8
|
+
# The URN is the series URN: the document URN without its stage, plus
|
|
9
|
+
# the "ser" slot.
|
|
10
|
+
class AllParts < ::Pubid::Iso::Identifier
|
|
11
|
+
include ::Pubid::AllParts
|
|
12
|
+
|
|
13
|
+
def to_urn
|
|
14
|
+
"#{identity.exclude(:stage, :typed_stage).to_urn}:ser"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -89,8 +89,10 @@ format: nil, stage_format_long: nil, with_date: nil, **opts)
|
|
|
89
89
|
def to_supplement_s(lang: :en, lang_single: false, with_edition: false,
|
|
90
90
|
format: nil, stage_format_long: nil, with_date: nil, **_opts)
|
|
91
91
|
date_str = if date
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
# Components::Date#render already carries the month
|
|
93
|
+
# (and day) when present — appending a month_part
|
|
94
|
+
# here doubled it (":2016-05-05-05").
|
|
95
|
+
":#{date.render}"
|
|
94
96
|
else
|
|
95
97
|
""
|
|
96
98
|
end
|
|
@@ -4,6 +4,7 @@ module Pubid
|
|
|
4
4
|
module Iso
|
|
5
5
|
module Identifiers
|
|
6
6
|
autoload :Addendum, "#{__dir__}/identifiers/addendum"
|
|
7
|
+
autoload :AllParts, "#{__dir__}/identifiers/all_parts"
|
|
7
8
|
autoload :Amendment, "#{__dir__}/identifiers/amendment"
|
|
8
9
|
autoload :Corrigendum, "#{__dir__}/identifiers/corrigendum"
|
|
9
10
|
autoload :Data, "#{__dir__}/identifiers/data"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# ITU flavor notes
|
|
2
|
+
|
|
3
|
+
ITU grammar, versions, annexes, reports and identity surfaces.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/itu/` or `spec/pubid/itu/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **ITU Questions / Handbooks / N-way combined**: `Pubid::Itu` models study-group **Questions** (`Identifiers::Question`, `_type: pubid:itu:question` — numeric `ITU-R 234-1/7:` and letter-series `ITU-R P.3/BL/7`, `ITU-R S.[4/BL/2]:`; the `/BL` segment, brackets and trailing `:` are carried as `has_bl`/`bracketed`/`has_colon` booleans + a `study_group` string, all reconstructed by `render_base`) and **Handbooks** (`Identifiers::Handbook`, `ITU-R 23.HDB`; the `.HDB` marker is implied by `_type`). Parser disambiguation is by what follows a `/`: **digits** → a Question study-group; a **series** (letters) → a combined designation — so the new rules sit before `with_series`/`without_series`. **Combined (joint) recommendations are N-way**: the primary designation stays on the base `series`/`code` (keeps `root.number` non-empty for relaton-index) and each *additional* designation is a `Components::Designation` in `CombinedIdentifier#combined` (one for a dual `G.780/Y.1351`, two+ for a triple `G.780/Y.1351/Z.1362`); the parser's `combined_designation` rule is `repeat(1)`. **`#number` at the root**: ITU stores the document number on `code`, but `Pubid::Itu::Identifier#number` delegates to `code&.number` so `id.root.number.to_s` is the key relaton-index sorts/bsearches on (non-empty for every type). The delegation is serialization-neutral (the flat block maps `number` via `number_to_kv`/`number_from_kv` off `code`, never the inherited `number` attribute); Supplement/Amendment/Corrigendum/Errata keep their own `:string` `number` ordinal (it overrides the reader), and their base document number is reached via `root.number`.
|
|
8
|
+
|
|
9
|
+
- **ITU `(V##)` versions, labelled annexes, and class-strict supplement `==`**: three ITU-T forms relaton's `relaton-data-itu` crawler needs (~930 data files had no index row because they failed to parse). **(1) `version`** — `ITU-T H.264 (V14) (08/2021)`: a plain **`attribute :version, :string`** on the shared `Pubid::Itu::Identifier` (safe — `::Pubid::Identifier` declares no `version`, so this is a *new* attribute, not the `number`/`stage` retype landmine), fed by the `version_part` rule (`space >> "(V" >> digits >> ")"`) spliced in **immediately before `date_part.maybe`** in all four document rules (`with_series`/`without_series`/`base_with_series`/`base_without_series`) — version always precedes the date in the corpus, and `date_part` requires digits after `(` so the two never compete. Rendered `" (V#{version})"` between code and date by `render_base` **and** by `CombinedIdentifier#to_s` (which overrides `to_s` and does not call `render_base` — miss it and the version is silently dropped on a joint id); mapped in `StandardSerialization`; compared in both `==` overrides. **URN and MR strings stay version-blind by design** (the ITU URN convention defines no version segment), so `(V13)`/`(V14)` share a URN. **(2) Labelled annex** — `ITU-T A.23 Annex A (06/2014)` is its **own** leaf class `Identifiers::AnnexOfRecommendation` (`_type: pubid:itu:annex-of-recommendation`), *not* an extension of `Identifiers::Annex`: that models the structurally different label-less "Annex to ITU OB No. 1000" (prefix rendering + i18n templates) whose `_type` is already persisted in index rows. Shape mirrors `Supplement`: polymorphic `base` + `:string number` (the label — `A`, `F3`, the one-off `C+`) + its own date/language, a minimal `key_value` block (never re-emitting the base's sector/series/code), `render_base` → `"<base> Annex <label>"`, `to_urn` → `"<base urn>:annex:<label>"`, `#root` walking `base` so `root.number` is the annexed document's number. Grammar: `annex_body` wraps the annexed document in **`base.as(:base)`** — load-bearing, because the base may carry its own date (`ITU-T X.692 (2002) Annex E (03/2002)`) and two `date_part`s flattened into one hash would make Parslet silently keep only the last `:year`. `annex_identifier` sits in `rule(:identifier)` **after `supplement_identifier`** (an annex can carry a trailing `Cor.`/`Err.`/`Amd.`, and PEG ordered choice never re-enters once an alternative succeeds) and **before `with_series`** (which would match the `ITU-T A.23` prefix and then fail on the unconsumed ` Annex A`); `supplement_with_base` takes `(annex_body.as(:base) | base.as(:base))` so `ITU-T G.729 Annex B (1996) Cor. 3 (03/2001)` nests annex-inside-supplement. `Builder#build_supplement`'s fallback `Components::Sector.new` is now guarded on `data[:sector]` — an annex base keeps sector/series on *its* base, and the unguarded fallback raised `Invalid sector: `. **(3) `Supplement#==`** now uses **`instance_of?(self.class)`** (so `Suppl. 2` ≠ `Amd 2` ≠ `Cor. 2`, symmetrically — `is_a?` made a Supplement equal an Amendment one-way) and compares `sector`/`series` **only when `base` is nil**. Both halves matter: the series-only form (`ITU-T A Suppl. 2`) holds its whole identity in sector+series, so ignoring them made every series' `Suppl. 2` equal (an index search returned 42 rows, and `eql?`/`hash` collapsed them in a `Set`); but a *based* supplement copies sector/series from its base while the key_value block deliberately does **not** serialize them, so comparing them unconditionally would break `parse(s) == from_hash(parse(s).to_hash)` — the very lookup this fixes. The now-identical `==` overrides on `Amendment`/`Corrigendum`/`Errata` were deleted. `language` is deliberately **not** compared (an index row without a language suffix must still match a reference that has one). Locked by `spec/pubid/itu/identifiers/{version,annex_of_recommendation,supplement}_spec.rb` + rows in `root_number_spec.rb`/`serialization_spec.rb` and the new `spec/fixtures/itu/identifiers/pass/{version,annex_of_recommendation}.txt`. **Rendering/URN/MR consequences of the annex wrapper:** `CombinedIdentifier` had its rendering in `to_s`, which the annex (composing `base.render_base`) bypassed — silently dropping the `/Y.1351` half, so `G.780/Y.1351 Annex A` and `G.780/Z.1362 Annex A` printed identically (and `to_s` is relaton's document number *and* output filename). It now renders in **`render_base`**, with the inherited `to_s` adding the language suffix and common-text twin. The annex's `to_urn` appends its **own date** after the label (`urn:itu:t:A.23:annex:a:06/2014`) — the base's date rides inside `base_urn`, so omitting the annex's would collide editions — and it defines **`mr_supplement_suffix`** (`annex.<label>[.<year>]`, `C+`→`cplus`) so the shared MrString renderer recurses into the annexed document instead of collapsing every annex onto a bare `itu.<date>`. Because the annex keeps sector/series/code on *its* base, `build_supplement` reaches identity through **`base.root`** when the base has none, so a supplement *of* an annex keys like any other supplement. **Version matching semantics:** `version` is in `==`, so a bare `ITU-T H.264` does **not** match `ITU-T H.264 (V14) …` under `ignore: %i[year month]` — version is a *separable trailing component* like ETSI's (`partial_ref_spec` now lists ITU as `omits: %i[date version]`), so callers matching a partial ref must add `:version` to `ignore`; it is deliberately **not** folded into the `:year`→`:date` alias. **Also fixed here:** `spec/pubid/itu/fixtures_spec.rb`'s glob had one `..` too many (repo-root `fixtures/`), so the whole ITU fixture round-trip spec silently iterated an empty file list. **Out of scope at the time, both since fixed:** `ITU-T V.25 ter Annex A (08/1996)` (the space-separated `ter` gap — closed by the code-suffix work below), and `Amendment#to_s`'s missing dot. (hand-off: itu-version-annex-and-supplement-matching.)
|
|
10
|
+
|
|
11
|
+
- **ITU-T unparseable print forms (the `relaton-data-itu` residue)**: 668 of the 755 unindexed `relaton-data-itu` records were ITU-T `rec_name` spellings the grammar had no rule for; all 668 now parse, render and satisfy relaton's gate (`from_hash(to_hash) == to_hash`), with **zero** change to any of the 20,728 already-indexed ids. **The single most important invariant when touching this area: every new rendering flag is a `:boolean` named for the RARE form with `default: -> { false }`**, because the canonical `to_hash` strips default-valued attributes — a flag named for the common case would add a key to every already-published index row. Ten constructs, grouped by where they live:
|
|
12
|
+
**(1) Supplement family.** `supplement_type` gained `Add.`/`Add` → the new leaf `Identifiers::Addendum < Supplement` (`_type: pubid:itu:addendum`), and `Builder#build_supplement`'s `case` gained an **`else raise ArgumentError`** — it previously fell through to a nil class and died with `undefined method 'new' for nil` one frame later. The near-identical `to_s` on `Supplement`/`Amendment`/`Corrigendum`/`Errata` collapsed into one **`Supplement#render_supplement(label)`**; each subclass now supplies only its label. `supplement_number` made the space optional (`ITU-T E Suppl.1`, `ITU-T D.211 Suppl.1` → `number_glued`) and the ordinal dotted (`ITU-T M Suppl. 1.1` → `number` is the whole `"1.1"`). `number_glued` is serialized but deliberately **not** in `==` — `Suppl.1` and `Suppl. 1` are one document. `chained_supplement`'s inner alternation gained `supplement_series_only` so a supplement *of* a series-only supplement parses (`ITU-T G Suppl. 39 (2006) Err. 1 (08/2006)`).
|
|
13
|
+
**(2) `Technical Cor.`** (158 records, the largest bucket — and the one the hand-off mislabelled as a series-code form). A `technical_marker` rule at the **head of `supplement_type`**, not folded into its alternation, so the type token stays the plain `Cor.` the builder's `case` maps; a `technical` boolean on `Corrigendum` renders and compares it. The slash-joined pair `ITU-T X.680 (1994) Amd. 1/Technical Cor. 1 (12/1997)` is a new **`slash_chained_supplement`** rule (a Corrigendum whose `base` is the Amendment) placed **before `supplement_with_base`** in the alternation — that alternative alone matches the `… Amd. 1` prefix and leaves `/Technical Cor. 1 …` unconsumed, which fails the whole parse with no re-entry.
|
|
14
|
+
**(3) Code suffixes — the highest-blast-radius change, since it fires on *every* recommendation parse.** A shared `code_suffixes` rule (`series_suffix_spaced.maybe >> qualifier_spaced.maybe`) spliced after `code` and **before `combined_suffixes`** (the corpus attaches the primary's suffix before the `/`: `ITU-T D.301 R/F.66`) in all four document rules. It cannot live *inside* `code`, or its leading space would also be offered to ` Suppl.`/` Annex` and have to backtrack out of a rule the wrappers depend on. `Components::Code` gained `series_suffix_spaced` (the spaced `ITU-T E.250 bis` vs the spec-locked glued `X.50bis` of issue #231 — the word itself is stored whitespace-free either way, so the serialized value stays comparable), plus `qualifier` + `qualifier_glued` for the trailing letter (`ITU-T D.200 R`, `ITU-T Q.2931 B`, `ITU-T R.38 A`, glued `ITU-T D.502R`, lowercase `ITU-T I.256.2a`). **Both guards on `qualifier_letter` are load-bearing**: the `A-R`/`a-r` cap keeps it off the `S` of ` Suppl.` and the `V` of a bare ` V2`, and the trailing `match["A-Za-z0-9"].absent?` is what stops ` Amd.`/` Add.`/` Annex A`/` App.`/` Cor.`/` Err.` being read as a qualifier. It cannot collide with `language` (dash-attached) or `date_part`/`version_part` (both need `(` after the space). Because `Code#to_s` can now contain a space, **`Code#compact_s`** (`to_s.delete(" ")`) was added and is what `urn_generator` interpolates — a space is not admissible in a URN segment, and dropping the suffix instead would collapse `D.200` and `D.200 R` onto one URN. The same slots were added inside `combined_designation` (positionally, so the suffix lands on the designation it printed against: `ITU-T D.300 R/E.282 R` sets both, `ITU-T E.211/Q.11 quater` only the last) — which required the parallel edits to `Builder#build_designations` **and** `CombinedIdentifier#combined_to_kv`/`combined_from_kv`, whose row hash is hand-built. Neither spacing flag is in `Code#==`: the two spellings of one number are one document. **`mr_number_with_part` also had to learn both suffixes** (glued to the number, `x-50bis`/`d-200r`) — it read only number/subseries/parts, so every qualified variant collapsed onto its base's MR slug and, worse, `Q.2931 B` and `Q.2931 C` onto each other. This changes the MR string of the handful of pre-existing glued-`bis` ids (`X.50bis`: `itu.t.x-50` → `itu.t.x-50bis`), which is the point — they were wrongly sharing a slug with `X.50`.
|
|
15
|
+
**(4) Version spellings.** `version_part` gained a bare branch, so `v.1`/`v10`/`V2` join the canonical `(V14)` in the same `version` attribute. This is the **one deliberate non-byte-exact normalisation** — all render as `(V##)` — so those strings live in `version_spec.rb` with explicit expectations and **not** in the byte-exact pass fixtures. Safe against the qualifier rule precisely because `V` is outside its `A-R` cap. relaton can now drop its own `v10` → `(V10)` normalisation.
|
|
16
|
+
**(5) Appendix.** `Identifiers::AppendixOfRecommendation` (`_type: pubid:itu:appendix-of-recommendation`) mirrors `AnnexOfRecommendation` one-for-one — including wrapping the appendixed document in **`base.as(:base)`** so the base's own date (`ITU-T G.722 (1988) App. IV (11/2006)`) lands a level down instead of colliding, and defining `mr_supplement_suffix` so the shared MR renderer recurses. Its alternation slot is the annex's reasoning verbatim: **after `supplement_identifier`** (an appendix can carry a trailing `Amd.`/`Err.`), **before `with_series`**. Three registrations are easy to miss and all three are needed: the `identifiers.rb` autoload, the `Builder#build` branch, and widening `urn_generator`'s `AnnexOfRecommendation` check — that check exists because a wrapper whose identity sits one level deeper otherwise emits `urn:itu:itu`. A `material` attribute carries the companion-artefact records (`App. II test vectors`, `App. I Software`), and is in the URN so they stay distinct from the appendix itself.
|
|
17
|
+
**(6) Series groups vs series-code documents — a corpus heuristic, not an ITU rule.** `series_group` (`E-100`, `E100-300`, `G-100`, `Q-500`) is capped at **one** leading letter; `series_code_body` (`EMC-5`, `MES-2`, `QOS-2`, `IMPL-8`, `SEC-QKD`) requires **two or more**. That split is what keeps them from shadowing each other, and it holds across the whole corpus but would misfile a future `AB-100` group or `E-QKD` document — `series_group_spec.rb` locks both sides so a change is visible. `series_group` must be tried **before** `series`, whose greedy non-backtracking `letter.repeat(1,3)` would take the `E` of `E-100` and then fail on the required space with no retry at a shorter length. `series_code_body` is a flat two-token shape (series + dash + alphanumeric number) rather than an optional middle segment, because a greedy `letter.repeat(2)` + optional `-QKD` + required dash fails at end-of-input with no backtracking into the satisfied `.maybe`; it sits **last** in `base` and after `with_series` in `identifier`, where nothing can reach it by accident. The number stays in `code.number` (not `"EMC-5"` in `series`) precisely so **`root.number`** — the key relaton-index bsearches on — is `"5"`/`"QKD"` rather than nil. `series_dash` drives the `-` vs `.` join in `render_base`; the builder's OB check gained a `series_dash.nil?` guard so a hypothetical `ITU-T OB-1` cannot be misrouted into the Operational Bulletin branch. The literal word `series` is its own `series_word` boolean and **cannot** live in the series token, because it also follows a *dotted* code (`ITU-T E.1100 series Suppl. 1`) where folding it in would corrupt the value every index row is keyed on; `Supplement` emits it only when base-less (guarded like `sector`/`series`).
|
|
18
|
+
**(7) Tail.** `attachment` boolean (`ITU-T H.350 attachment`), `range_end` string (`ITU-T Q.120-Q.139` — no conflict with `parts`, which needs digits after the dash), and `Identifier.parse` now collapses runs of whitespace (`ITU-T D.271 (10/2016)`) **after** the shared `Pubid::MAX_INPUT_LENGTH` guard, never before.
|
|
19
|
+
**The cross-cutting lesson (three review findings were all this same mistake): an identity-bearing marker must reach ALL THREE identity surfaces — `to_s`, `to_urn` and `to_mr_string` — not just `==`.** A marker that only reaches `==` still lets two distinct documents share a URN and an output slug. So `generate_base_urn` emits `series`/`attachment`/`to-<range_end>` segments and joins a series-code document with its dash (`urn:itu:t:EMC-5:2003`, not the ambiguous `EMC.5`); `generate_supplement_urn`'s base-less branch emits `series` too; and `mr_number_with_part` carries all five. `Builder#build_supplement` copies `series_word` up from the base alongside sector/series/code for the same reason — render/serialize/`==` all guard on `base.nil?`, so the copy is neutral there and only the MR slug sees it. `spec/pubid/itu/distinctness_spec.rb` is the forcing function: it asserts all four surfaces differ for every marker pair. **Two more review findings worth keeping:** `technical_marker` is bound to the **`Cor.` branch alone**, not the head of `supplement_type` — offered before any token it was accepted on `Technical Err. 1`/`Technical Amd. 1` and then silently dropped by the builder's Corrigendum-only guard, collapsing those onto a *different* document (they are now cleanly rejected); and `series_code_body` carries a `(str("OB") >> dash).absent?` guard, because `ITU-T OB-1` otherwise reached the Recommendation fallback whose `validate_ob_no_sector!` raises an `ArgumentError` that escapes `Identifier.parse`'s `Parslet::ParseFailed` rescue — turning a rejected input into a crash for callers. `Code#to_s` renders glued suffixes before spaced ones, mirroring the grammar (a fixed edition-word-first order printed `Q.11a bis` back as `Q.11 bisa`). **Two known pre-existing gaps, deliberately not fixed and both pinned by specs in `distinctness_spec.rb`:** (a) the ITU URN convention encodes no supplement *type*, so `Amd. 1`/`Cor. 1`/`Err. 1`/`Add. 1` of one base share a URN and MR slug; (b) `build_supplement` copies sector/series/code down from the base but they are deliberately not serialized, so a supplement rebuilt through `from_hash` has none of them and its **MR slug collapses to `itu.<date>`** while every other surface stays symmetric. Both are true on `main` for `Amd.`/`Cor.`/`Err.` before this change — the new types simply inherit them, and `to_s`/`to_hash`/`==` distinguish everything correctly, which is what relaton's gate uses. Both would be fixed by giving `Supplement` an `mr_supplement_suffix` so the shared MR renderer recurses into `base` (as `AnnexOfRecommendation` already does), which changes every existing ITU supplement MR string — a deliberate call, not a tag-along. Also fixed here: `render_base` gained an `elsif series` branch, so a code-less identifier no longer renders a dangling trailing dot. Locked by `spec/pubid/itu/{code_suffixes,combined_suffixes,series_group,tail_forms}_spec.rb`, `spec/pubid/itu/identifiers/{addendum,appendix_of_recommendation,supplement_spelling,technical_corrigendum}_spec.rb`, and 7 new `spec/fixtures/itu/identifiers/pass/*.txt` files drawn from the real corpus. **Still out of scope (both declared not-ours by the hand-off):** ~47 malformed ITU-R docids left by a decommissioned crawler (`ITU-R BO`, `ITU-R M.5-BL-13` — series with no number; dataset bugs, not identifiers), and the 4 space-for-dot `ITU-T G 231` spellings relaton normalises on its side. (hand-off: itu-t-unparseable-forms.)
|
|
20
|
+
|
|
21
|
+
- **ITU-R Reports (`Identifiers::Report`) and the class-strict `Identifier#==`**: ITU-R **Reports** are a publication series that numbers **independently** of Recommendations, so `ITU-R BT.2020-1` names *two* real, both-current documents (Report BT.2020-1, objective quality assessment, 2000; Recommendation BT.2020-1, UHDTV parameter values, 06/2014). 984 of 1,001 report records in `relaton-data-itu` were indexed as `pubid:itu:recommendation`, and 52 editions across 30 numbers (BT.2020, M.2083, M.2134 …) collided outright — the dataset keys files by identifier, so whichever was crawled last silently overwrote the other. ITU disambiguates with the leading word, so the identifier must too. **The leaf** `Identifiers::Report` (`_type: pubid:itu:report`) is shaped exactly like `Recommendation` — same attributes, same `StandardSerialization` flat block, so only `_type` distinguishes the two hashes — and adds just `render_base` (`"Report #{super}"`) and `mr_type` (`[super, "report"].join(".")` → `itu.r.report.bt-2020-1`). **Grammar**: a captured `report_word` marker (unlike `itu_prefix`'s decorative, *uncaptured* `"Recommendation "` literal, which stays as it is) plus a single `report_body` whose `(series >> dot).maybe` covers the with- and without-series shapes at once; `base_report` accepts **both** spellings (`Report ITU-R BT.2020-1` and the downstream `ITU-R Report BT.2020-1`), and `to_s` renders ITU's own leading form for either — so the infix spelling lives in `report_spec.rb` with an explicit expectation, **not** in the byte-exact pass fixtures. Two deliberate exclusions from `report_body`: **no `combined_suffixes`** (a joint `Report X/Y` would reach `Builder#build`'s `combined` branch, build a `CombinedIdentifier` and *silently drop the marker* — a clean parse failure is better), and a **`(str("OB") >> dot).absent?` guard** (an Operational Bulletin is cross-bureau; without it `Report ITU-T OB.1` routes to `SpecialPublication`, marker dropped, or trips `validate_ob_no_sector!`, whose `ArgumentError` escapes `Identifier.parse`'s `Parslet::ParseFailed` rescue). `base_report` is **first** in `rule(:base)` — which is what lets a supplement/annex/appendix *wrap* a Report — and order-free there because `itu_prefix` admits only `Recommendation `/`ITU` and `letter` is `[A-Z]`, so neither `series` nor `series_code_body` can consume `Report`; `report_identifier` sits in `rule(:identifier)` **after** supplement/annex/appendix (which match the longer wrapped form) and **before** `with_series`/`without_series` (which would match the infix spelling's `ITU-R` prefix and then fail on the unconsumed tail). The builder picks the class from `data[:report_marker]` in the existing Recommendation fallthrough; `build_supplement`/`build_annex_of_recommendation` need **no** change, since they recurse via `build(data[:base])`. **`Pubid::Itu::Identifier#==` is now class-strict** (`other.instance_of?(self.class)`, the rule `Supplement#==` already used) — the ITU *type* is part of the identity, not just sector/series/number. `self.class` rather than a hard-coded class keeps it **symmetric in both directions** (an `is_a?` guard would have made a Recommendation equal a Report one-way, and `#matches?` — which is `exclude(*ignore) == other.exclude(*ignore)` — resolve one to the other). Strictly stricter, so it can only turn `true` into `false`; it also closes the pre-existing one-way holes against `CombinedIdentifier` and `SpecialPublication`. **Consequences of the shared guard, both pinned by `spec/pubid/itu/type_strict_equality_spec.rb`:** comparisons that used to be *asymmetric* are now consistently false — most notably a bare primary designation vs its joint recommendation (`ITU-T G.780` vs `ITU-T G.780/Y.1351`, which was `true` one way and `false` the other on `main`, so no caller could rely on it either way). A caller matching a bare primary against a joint document must narrow on `root.number` (which still agrees) rather than on `==`. Per the cross-cutting ITU lesson, the marker reaches **all** identity surfaces: `to_s`, `generate_base_urn` (a `report` segment right after the sector — `urn:itu:r:report:BT.2020-1:2000`, which `UrnParser` also reads back), and the MR slug — including **`Supplement#mr_type`**, which is load-bearing and easy to miss: a supplement has no `mr_supplement_suffix`, so the shared MR renderer slugs it **flat** from the sector/series/code `build_supplement` copied up from its base and *never consults the base's class*, which made `Report ITU-R BT.2020-1 Suppl. 1` and `ITU-R BT.2020-1 Suppl. 1` share one slug (i.e. one output filename — the very overwrite this type prevents). It appends `report` when **`root`** is a Report, so a supplement of an *annex* of a Report is covered too. **`PREFIXES` stays `["ITU"]`** — the leading `Report` token is deliberately not registered for prefix routing (a generic English word; the mixin's ambiguous-token exclusion), and the infix spelling routes on `ITU` as before. **Backwards compatibility is exact**: a bare `ITU-R BT.2020-1` still builds a `Recommendation` with an unchanged hash/`to_s`/URN — nothing in the grammar or the builder fires without the marker, so none of the 20,728 published index rows move. Locked by `spec/pubid/itu/identifiers/report_spec.rb`, a `distinctness_spec.rb` pair, rows in `root_number_spec.rb`/`serialization_spec.rb`/`urn_parser_spec.rb`, and `spec/fixtures/itu/identifiers/pass/report.txt`. **Out of scope (relaton's side, sequenced after this):** `DataParserR`/`DataCrawlerR` emitting the new docid form, `Relaton::Itu::Pubid`'s reference parser learning a `report` rule, and the `relaton-data-itu` filename-namespace migration + re-crawl that actually recovers the 34 missing Recommendations and 18 missing Reports. (hand-off: itu-r-report-identifier-type.)
|
|
22
|
+
|
|
23
|
+
## From the root note "Parse-failure error contract — uniform across every flavor"
|
|
24
|
+
|
|
25
|
+
**ITU's length guard was an early `return`, not a raise, and its comment blamed the wrong caller.** `Identifier.normalize_whitespace` (`lib/pubid/itu/identifiers/base.rb`) carried
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
return identifier if identifier.length > ::Pubid::MAX_INPUT_LENGTH
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
under a note claiming "`Pubid.parse` rejects it before this point". That was wrong twice: `Pubid.parse` never routes a human-readable string to a flavor at all (`lib/pubid.rb` raises `ArgumentError: No flavor specified` for anything that is not an MR string or a URN), and ITU had no raising guard of its own — so an over-long string reached the ITU parser through both `Pubid::Itu.parse` and `Pubid::Itu::Identifier.parse`. The guard now lives in `self.parse` as the standard inline pair, and the comment says what actually protects the method.
|
|
32
|
+
|
|
33
|
+
**ITU was also the delegate in the most-visible class leak.** `Pubid::Iso.parse("ITU-T G.711")` detects an MR-shaped string, routes through `Pubid::Parsers::MrString`, and lands in `Pubid::Itu::Identifier.parse` — which raised `RuntimeError`. So `Pubid::Iso.parse` raised **two different classes** depending on its input, and a relaton-cli caller rescuing `Parslet::ParseFailed` got a raw backtrace for the ITU-shaped half. Converting ITU fixed the ISO symptom; `spec/pubid/parse_error_spec.rb` pins that exact route by name, because a generic junk string never reaches it.
|
|
34
|
+
|
|
35
|
+
- **The five supplement types rendered plain under `to_s(annotated: true)`.**
|
|
36
|
+
`Pubid::Itu::Identifier#to_s` annotates, but `Addendum`, `Amendment`,
|
|
37
|
+
`Corrigendum`, `Errata` and `Supplement` each override it and none calls
|
|
38
|
+
`super` — they all funnel through `Supplement#render_supplement(label)`.
|
|
39
|
+
The wrap cannot live in that helper, which takes a label and no options,
|
|
40
|
+
so each of the five `to_s` is a one-line
|
|
41
|
+
`annotate_plain_render(render_supplement("…"), **opts)`.
|
|
42
|
+
|
|
43
|
+
`render_supplement` starts from `base.to_s` with no options, so the base
|
|
44
|
+
arrives plain and the single outer annotation covers the whole string —
|
|
45
|
+
the base's own tokens are reached by `Annotator#emit_tokens` walking
|
|
46
|
+
`base`.
|
|
47
|
+
|
|
48
|
+
## Contribution (Temporary Document) — pubid#340
|
|
49
|
+
|
|
50
|
+
`Pubid::Itu::Identifiers::Contribution` models the working documents a study
|
|
51
|
+
group circulates, mirroring pubid-itu 1.15's `Identifier::Contribution`
|
|
52
|
+
(`%{series}-C%{number}`): "ITU-R SG17-C1000", sector- and language-suffixed
|
|
53
|
+
per the existing rules ("ITU-T SG17-C1000-E"). The grammar entry sits between
|
|
54
|
+
`with_series` and `series_code_identifier` — the **-C marker (dash + "C" +
|
|
55
|
+
digits) is the discriminator**: a series-code document's post-dash number
|
|
56
|
+
starts with digits or is all letters, never "C"+digits, so the two dash
|
|
57
|
+
shapes cannot shadow each other in either direction ("ITU-T EMC-5" still
|
|
58
|
+
builds a Recommendation, locked by a spec). The builder branch carries the
|
|
59
|
+
`:contribution_marker`; `root.number` reaches the C-number through the shared
|
|
60
|
+
`code.number` reader, so relaton-index keys it normally.
|
|
61
|
+
|
|
62
|
+
**`locate_type` was broken for every leaf, not just this one**: no ITU class
|
|
63
|
+
ever defined a `type` hash, so `Pubid::Itu.locate_type` raised `NoMethodError`
|
|
64
|
+
on any call. The base now derives the key from the class name
|
|
65
|
+
(`Contribution` → `:contribution`, `AnnexOfRecommendation` →
|
|
66
|
+
`:annex_of_recommendation`) — plain `gsub` camel→snake, because ActiveSupport's
|
|
67
|
+
`underscore` is not a dependency. metanorma-itu constructs through this
|
|
68
|
+
lookup; its flavor-local `pubid_contribution.rb` render override can be
|
|
69
|
+
deleted once it migrates.
|
data/lib/pubid/itu/builder.rb
CHANGED
|
@@ -20,6 +20,8 @@ module Pubid
|
|
|
20
20
|
# +subseries+ (dot-separated, flavor-specific) and +parts+
|
|
21
21
|
# (dash-separated).
|
|
22
22
|
class Code < Lutaml::Model::Serializable
|
|
23
|
+
include ::Pubid::SubsetMatch
|
|
24
|
+
|
|
23
25
|
attribute :imp_marker, :string
|
|
24
26
|
attribute :number, :string
|
|
25
27
|
attribute :series_suffix, :string
|
|
@@ -13,6 +13,8 @@ module Pubid
|
|
|
13
13
|
#
|
|
14
14
|
# Format: SERIES.CODE (e.g. "Y.1351", "Y.1362-2")
|
|
15
15
|
class Designation < Lutaml::Model::Serializable
|
|
16
|
+
include ::Pubid::SubsetMatch
|
|
17
|
+
|
|
16
18
|
attribute :series, Pubid::Itu::Components::Series
|
|
17
19
|
attribute :code, Pubid::Itu::Components::Code
|
|
18
20
|
|
|
@@ -131,6 +131,21 @@ module Pubid
|
|
|
131
131
|
# Builder#build_supplement) and are deliberately not serialized (see
|
|
132
132
|
# supplement_sector_to_kv), so comparing them would make a parsed
|
|
133
133
|
# identifier unequal to the same identifier rebuilt via from_hash.
|
|
134
|
+
# A subset match follows the same rules as `==` below: the two
|
|
135
|
+
# rendering flags are not identity, and when a base is present the
|
|
136
|
+
# sector/series/code copies of the base are not serialized.
|
|
137
|
+
SUBSET_BASE_COPIES = %i[sector series code series_word].freeze
|
|
138
|
+
|
|
139
|
+
def self.subset_ignored_attributes
|
|
140
|
+
%i[number_glued slash_joined]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def subset_attribute_match?(name, mine, theirs)
|
|
144
|
+
return true if base && SUBSET_BASE_COPIES.include?(name)
|
|
145
|
+
|
|
146
|
+
super
|
|
147
|
+
end
|
|
148
|
+
|
|
134
149
|
def ==(other)
|
|
135
150
|
return false unless other.instance_of?(self.class)
|
|
136
151
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# JCGM flavor notes
|
|
2
|
+
|
|
3
|
+
JCGM meetings and URN parsing.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/jcgm/` or `spec/pubid/jcgm/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **JCGM meeting year is optional (`JCGM 11st Meeting`)**: a meeting's trailing ` (YYYY)` group is a *separable* component, not part of its identity — JCGM numbers its meetings in one sequence, so the ordinal alone names the event. `rule(:meeting_identifier)` wraps the **whole** group in `.maybe` (not the year inside it, so a dangling `(`, an empty `()` or a truncated `(200)` still fails), mirroring `date_portion.maybe` in `rule(:base)`; `render_meeting` appends the group only when `date` is present. No builder or serialization change was needed: `Builder::Base#assign_attributes` iterates the parsed hash, so an absent `:date` key is simply never assigned, and `SingleIdentifier#emit_kv` already drops a nil value — a dateless meeting is `{_type, number}` and a dated one is byte-identical to before, so **no index migration follows**. **The load-bearing third surface is `UrnParser#parse_meeting_urn`**: `UrnGenerator#generate_meeting_urn` had *always* guarded the date and emitted the short `urn:jcgm:meeting:11`, but the parser rebuilt the ungrammatical `"JCGM 11st Meeting ()"` from it — so the dateless form must be made optional on the URN read-back too, or the round-trip the relaton index depends on breaks. **The whole `UrnParser` was converted to build identifiers directly from the URN segments** (the BIPM pattern; 5 of 38 flavors construct directly, the rest use the shared `flavor_parse` helper, which stays for them). Rendering a string and re-parsing it flattens the segments and silently loses whatever the string form cannot carry, which is what three **pre-existing** defects were: `urn:jcgm:gum.6:2020` **raised** (`"JCGM gum.6:2020"` is not a grammar form); the language segment was dropped (`urn:jcgm:100:2008:en` → `JCGM 100:2008`); and `number, year = parts` discarded every segment past the second, so a supplement decayed into **the standard it amends** — `urn:jcgm:200:2008:corrigendum` → `JCGM 200:2008`, a different document, with no error. The supplement marker (`corrigendum`/`amendment`, written by the generator as the `type_code`) splits the segments: everything before it is the base document, everything after is the supplement's own number and date. Four things the string round-trip used to supply for free are now explicit, and each is load-bearing: (1) **`typed_stage` comes from `Jcgm.locate_stage`, the same registry lookup `Builder#locate_typed_stage` uses — NOT from the attribute default** — because `SingleIdentifier.published_typed_stage` additionally sets `original_abbr`, which would make a URN-reconstructed id **unequal** to a parsed one; the marker is mapped to the abbreviation the registry indexes (`amendment` → `Amd`, since `locate_stage` matches `abbr`, not `type_code`), and the class then comes from `Jcgm.locate_type(typed_stage.type_code)` so one lookup fixes both. **Guide and GumGuide deliberately take the default instead**, because the grammar emits no type token for them and `Builder#build` fills theirs from `published_typed_stage` too — matching the builder means matching *which* of the two paths it took. (2) The meeting number is normalized with **`to_i`**, matching `Identifiers::Meeting.ordinal`, so `011` still reads back as `11` and a missing or non-numeric segment as `0`. (3) The year is **validated against the grammar's `19xx|20xx`** and rejected with `Pubid::UrnParser::Errors::ParseError` (the easc/gost convention) — with no re-parse, nothing else would catch a malformed segment. (4) A language code is mapped back through the inverse of `Builder::Base::LANG_CHAR_MAP` to restore `original_code`, which is what the renderer prints (`en,fr` → `(E/F)`). **KNOWN GAP, generator-side and deliberately unchanged**: a full date renders as its year alone (`JCGM GUM-1:2022-11-28` → `urn:jcgm:gum.1:2022`), so a URN cannot restore month and day; widening it would change already-published URNs. 30 of the 32 corpus ids now round-trip through the URN byte-exactly, and those 2 are the truncated-date pair. (That `original_abbr` asymmetry is **since fixed** — see `lib/pubid/iec/CLAUDE.md` — so the default now agrees too and the lookup mirrors the builder for clarity rather than necessity.) **Deliberately not a typed error**: the earlier reading of this defect was to raise on a nil date; an undated meeting reference is unambiguous and legitimate, and raising would leave relaton's `Bib::ItemData#to_most_recent_reference` with nothing to return. **Do not add the English teens exception to `Identifiers::Meeting.ordinal`** — the real records print `11st`/`12nd`/`13rd`, and three published documents round-trip through that naive rule. Locked by `spec/pubid/jcgm/meeting_partial_spec.rb` plus a bare-form row in `urn_parser_spec.rb`. **Still open, separate call**: `Meeting.new(number: "11").to_s` raises `NoMethodError` (`number` is a `Components::Code`; a bare String is accepted at construction and only explodes at render), and `Meeting#ordinal` renders `"0th"` for a nil `number`. (hand-off: jcgm-meeting-render-nil-date.)
|
data/lib/pubid/jcgm/builder.rb
CHANGED
data/lib/pubid/jis/builder.rb
CHANGED
|
@@ -20,6 +20,9 @@ module Pubid
|
|
|
20
20
|
build_single_identifier(data)
|
|
21
21
|
end
|
|
22
22
|
attach_symbol(identifier, data)
|
|
23
|
+
# "(all parts)" names every part of the document, so it wraps the
|
|
24
|
+
# document, which holds no mark itself.
|
|
25
|
+
data[:all_parts] ? identifier.to_all_parts : identifier
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
private
|
|
@@ -37,7 +40,6 @@ module Pubid
|
|
|
37
40
|
parts: extract_part_strings(data[:parts]),
|
|
38
41
|
year: data[:year]&.to_i,
|
|
39
42
|
language: data[:language]&.to_s,
|
|
40
|
-
all_parts: (true if data[:all_parts]),
|
|
41
43
|
reaffirmed: (true if data[:reaffirmed]),
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -114,3 +116,5 @@ module Pubid
|
|
|
114
116
|
end
|
|
115
117
|
end
|
|
116
118
|
end
|
|
119
|
+
|
|
120
|
+
Pubid::Jis::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
data/lib/pubid/jis/identifier.rb
CHANGED
|
@@ -7,6 +7,11 @@ module Pubid
|
|
|
7
7
|
# Pubid::Jis::Identifiers descend from this class, so a parsed JIS id is an
|
|
8
8
|
# instance of Pubid::Jis::Identifier.
|
|
9
9
|
class Identifier < ::Pubid::Identifier
|
|
10
|
+
# JIS prints its own all-parts suffix, "(規格群)".
|
|
11
|
+
def self.all_parts_class
|
|
12
|
+
Identifiers::AllParts
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
# JIS keeps its number flat at the top level (string, to preserve leading
|
|
11
16
|
# zeros like "0205"), with the division letter in `series` and any
|
|
12
17
|
# multi-level part numbers in `parts`. Supplements override `number` with
|
|
@@ -16,9 +21,6 @@ module Pubid
|
|
|
16
21
|
attribute :parts, :string, collection: true # Optional multi-level parts
|
|
17
22
|
attribute :year, :integer
|
|
18
23
|
attribute :language, :string # "E" or "J"
|
|
19
|
-
# Boolean flags carry no default, so they stay nil (and are omitted from
|
|
20
|
-
# the serialized hash) unless actually set true.
|
|
21
|
-
attribute :all_parts, :boolean
|
|
22
24
|
# Reaffirmation (再確認): a trailing "R" on the year marks an edition
|
|
23
25
|
# that was reaffirmed without revision (e.g. ":2019R").
|
|
24
26
|
attribute :reaffirmed, :boolean
|
|
@@ -50,7 +52,6 @@ module Pubid
|
|
|
50
52
|
map "parts", to: :parts
|
|
51
53
|
map "year", to: :year
|
|
52
54
|
map "language", to: :language
|
|
53
|
-
map "all_parts", to: :all_parts
|
|
54
55
|
map "reaffirmed", to: :reaffirmed
|
|
55
56
|
# render_empty keeps a bare "SYMBOL" (empty-string value) in the hash so
|
|
56
57
|
# it round-trips distinctly from "no symbol" (nil).
|
|
@@ -62,10 +63,6 @@ module Pubid
|
|
|
62
63
|
# would otherwise fail serialization type validation.
|
|
63
64
|
PUBLISHER = "JIS"
|
|
64
65
|
|
|
65
|
-
def all_parts?
|
|
66
|
-
all_parts == true
|
|
67
|
-
end
|
|
68
|
-
|
|
69
66
|
def reaffirmed?
|
|
70
67
|
reaffirmed == true
|
|
71
68
|
end
|
|
@@ -91,23 +88,14 @@ module Pubid
|
|
|
91
88
|
result
|
|
92
89
|
end
|
|
93
90
|
|
|
94
|
-
# Comparison with all_parts logic
|
|
95
|
-
# When either identifier has all_parts=true, compare only series and number
|
|
96
91
|
def ==(other)
|
|
97
92
|
return false unless other.is_a?(Identifier)
|
|
98
93
|
|
|
99
|
-
if all_parts? || other.all_parts?
|
|
100
|
-
# Compare only series and number, ignore year, parts, all_parts
|
|
101
|
-
return series == other.series && number == other.number
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Normal full comparison
|
|
105
94
|
series == other.series &&
|
|
106
95
|
number == other.number &&
|
|
107
96
|
(parts || []) == (other.parts || []) &&
|
|
108
97
|
year == other.year &&
|
|
109
98
|
language == other.language &&
|
|
110
|
-
all_parts? == other.all_parts? &&
|
|
111
99
|
reaffirmed? == other.reaffirmed? &&
|
|
112
100
|
symbol == other.symbol
|
|
113
101
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Jis
|
|
5
|
+
module Identifiers
|
|
6
|
+
# Every part of one JIS document: "JIS C 0617(規格群)".
|
|
7
|
+
class AllParts < ::Pubid::Jis::Identifier
|
|
8
|
+
include ::Pubid::AllParts
|
|
9
|
+
|
|
10
|
+
SUFFIX = "(規格群)"
|
|
11
|
+
|
|
12
|
+
# The URN of the document plus the "all" slot.
|
|
13
|
+
def to_urn
|
|
14
|
+
"#{identity.to_urn}:all"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Pubid
|
|
4
4
|
module Jis
|
|
5
5
|
module Identifiers
|
|
6
|
+
autoload :AllParts, "#{__dir__}/identifiers/all_parts"
|
|
6
7
|
autoload :Amendment, "#{__dir__}/identifiers/amendment"
|
|
7
8
|
autoload :Corrigendum, "#{__dir__}/identifiers/corrigendum"
|
|
8
9
|
autoload :Explanation, "#{__dir__}/identifiers/explanation"
|
data/lib/pubid/jis/renderer.rb
CHANGED
|
@@ -38,7 +38,6 @@ module Pubid
|
|
|
38
38
|
result = "#{PUBLISHER} #{id.code}"
|
|
39
39
|
result += ":#{id.year_with_reaffirmation}" if id.year
|
|
40
40
|
result += "(#{id.language})" if id.language
|
|
41
|
-
result += "(規格群)" if id.all_parts?
|
|
42
41
|
result + id.symbol_suffix
|
|
43
42
|
end
|
|
44
43
|
|
|
@@ -52,7 +51,6 @@ module Pubid
|
|
|
52
51
|
result += id.code.to_s
|
|
53
52
|
result += ":#{id.year_with_reaffirmation}" if id.year
|
|
54
53
|
result += "(#{id.language})" if id.language
|
|
55
|
-
result += "(規格群)" if id.all_parts?
|
|
56
54
|
result += id.symbol_suffix
|
|
57
55
|
result
|
|
58
56
|
end
|
|
@@ -12,7 +12,6 @@ module Pubid
|
|
|
12
12
|
|
|
13
13
|
parts << identifier.language.to_s.downcase if identifier.language
|
|
14
14
|
|
|
15
|
-
parts << "all" if identifier.all_parts?
|
|
16
15
|
|
|
17
16
|
if identifier.is_a?(SupplementIdentifier) && identifier.supplement_notation
|
|
18
17
|
parts << identifier.supplement_notation.to_s.downcase
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# NIST flavor notes
|
|
2
|
+
|
|
3
|
+
NIST rendering formats and annotated output.
|
|
4
|
+
|
|
5
|
+
Read this before you change `lib/pubid/nist/` or `spec/pubid/nist/`. The root
|
|
6
|
+
`CLAUDE.md` keeps the cross-flavor contract that every flavor obeys.
|
|
7
|
+
|
|
8
|
+
- **Six NIST types rendered plain under `to_s(annotated: true)`, and two
|
|
9
|
+
more carried a latent corruption.** NIST's `to_s` takes a **positional**
|
|
10
|
+
`format`, which Ruby fills with a Hash when a caller passes `annotated:`;
|
|
11
|
+
`Identifiers::Base#to_s` already handles that and annotates. The six that
|
|
12
|
+
did not — `CommercialStandardsMonthly`, `CrplReport`, `InteragencyReport`,
|
|
13
|
+
`MiscellaneousPublication`, `Monograph`, `Report` — either compose their
|
|
14
|
+
own string or hand `super` a **Symbol**, which drops the flag. Each now
|
|
15
|
+
pulls `annotated` out of the Hash itself and wraps its result.
|
|
16
|
+
|
|
17
|
+
**`Circular` and `Handbook` are the interesting half.** Both did
|
|
18
|
+
`result = super` — which annotates — and then rewrote the edition with a
|
|
19
|
+
**`$`-anchored** regex. Once the string ends in `</span>` the anchor
|
|
20
|
+
cannot match, so the rewrite silently stops applying. Both now strip the
|
|
21
|
+
flag before `super`, rewrite, then annotate.
|
|
22
|
+
|
|
23
|
+
**An output assertion cannot catch that**, and this is the lesson: for
|
|
24
|
+
both fixture references (`NBS CIRC 11e2-1915`, `NBS HB 44e2-1955`) the
|
|
25
|
+
annotator matches only the leading publisher, so the tail is bare and the
|
|
26
|
+
wrong order still produces the right string. The spec asserts the
|
|
27
|
+
**ordering** instead — it wraps `annotate_plain_render` and checks the
|
|
28
|
+
string handed to it is already rewritten and carries no span — and that
|
|
29
|
+
assertion was verified to fail when the order is reverted.
|
|
30
|
+
|
|
31
|
+
`CircularSupplement` and `SupplementIdentifier` were also normalised:
|
|
32
|
+
both had a date-range branch that returned a hand-composed string without
|
|
33
|
+
ever reaching `super`. `SupplementIdentifier` has four exits, so its
|
|
34
|
+
composition moved into a private `render_plain`; **`super` is not
|
|
35
|
+
reachable from a private method**, so `to_s` hands it in as a block —
|
|
36
|
+
`render_plain(format) { super(format) }`.
|
|
37
|
+
|
|
38
|
+
- **`all_parts_edition_keys` needed `update`/`update_component`, not
|
|
39
|
+
`edition_year`.** `Identifier.all_parts_edition_keys` defaults to
|
|
40
|
+
`%i[date year edition version]`; NIST's primary edition carrier
|
|
41
|
+
(`edition`, a `Components::Edition`) is already covered, but the Letter
|
|
42
|
+
Circular / Circular "rJun1992"-style revision (`Builder` around the
|
|
43
|
+
"Convert revision with month+year to update component" comment) parses
|
|
44
|
+
into a **separate** attribute, `update`/`update_component`
|
|
45
|
+
(`Components::Update`: number+year+month), which the default list
|
|
46
|
+
missed entirely — `"NBS LC 800 rJun1992"` and `"NBS LC 800 rJul1995"`
|
|
47
|
+
failed to collapse under `#to_all_parts`/`#===`. Fixed with
|
|
48
|
+
`Pubid::Nist::Identifier.all_parts_edition_keys` (`super + %i[update
|
|
49
|
+
update_component]`). **`edition_year` and `revision_year`/
|
|
50
|
+
`revision_month` were investigated and are NOT added**: `Builder` only
|
|
51
|
+
ever sets `edition_year` alongside the real `edition` component (never
|
|
52
|
+
as its sole carrier, e.g. the TechnicalNote "date IS edition" branch),
|
|
53
|
+
and `revision_year`/`revision_month` are transient — converted into
|
|
54
|
+
`update`/`update_component` and cleared to `nil` before the object is
|
|
55
|
+
returned. Neither carries live information `edition`/`update` doesn't
|
|
56
|
+
already cover. Locked by `spec/pubid/all_parts_edition_keys_audit_spec.rb`.
|
data/lib/pubid/nist/builder.rb
CHANGED
|
@@ -28,6 +28,8 @@ module Pubid
|
|
|
28
28
|
# Edition.new(type: "r", id: "5").to_s # => "r5"
|
|
29
29
|
# Edition.new(type: "r", id: "5", original_prefix: " Rev. ").to_s # => "Rev. 5"
|
|
30
30
|
class Edition < Lutaml::Model::Serializable
|
|
31
|
+
include ::Pubid::SubsetMatch
|
|
32
|
+
|
|
31
33
|
attribute :type, :string # "-", "e", or "r"
|
|
32
34
|
attribute :id, :string # Edition ID (number or year)
|
|
33
35
|
attribute :additional_text, :string # Text after "rev" (WITHOUT "rev" prefix)
|
|
@@ -8,6 +8,8 @@ module Pubid
|
|
|
8
8
|
# IssueNumber component for NIST identifiers
|
|
9
9
|
# Represents the issue/number designation (e.g., "No. 12" in "Vol. 6, No. 12")
|
|
10
10
|
class IssueNumber < Lutaml::Model::Serializable
|
|
11
|
+
include ::Pubid::SubsetMatch
|
|
12
|
+
|
|
11
13
|
attribute :number, :string
|
|
12
14
|
|
|
13
15
|
# Short form rendering: "n12"
|
|
@@ -23,6 +23,8 @@ module Pubid
|
|
|
23
23
|
# - SP: Part number (pt1)
|
|
24
24
|
# - Letter suffixes (A, B, C, etc.)
|
|
25
25
|
class Part < Lutaml::Model::Serializable
|
|
26
|
+
include ::Pubid::SubsetMatch
|
|
27
|
+
|
|
26
28
|
attribute :type, :string # "pt" for part notation, "n" for issue, "" for letter suffix
|
|
27
29
|
attribute :value, :string # Part number or letter (1, 2, A, B, etc.)
|
|
28
30
|
|
|
@@ -13,6 +13,8 @@ module Pubid
|
|
|
13
13
|
# Stage.new(id: "i", type: "pd").to_s(:short) # => "ipd"
|
|
14
14
|
# Stage.new(id: "f", type: "pd").to_s(:long) # => "(Final Public Draft)"
|
|
15
15
|
class Stage < Lutaml::Model::Serializable
|
|
16
|
+
include ::Pubid::SubsetMatch
|
|
17
|
+
|
|
16
18
|
attribute :id, :string # i, f, 1-9
|
|
17
19
|
attribute :type, :string # pd, wd, prd
|
|
18
20
|
|
|
@@ -15,6 +15,8 @@ module Pubid
|
|
|
15
15
|
# Supplement.new(month: "Jan", year: "1924").to_s(:short) # => "supJan1924"
|
|
16
16
|
# Supplement.new(has_revision: true).to_s(:short) # => "suprev"
|
|
17
17
|
class Supplement < Lutaml::Model::Serializable
|
|
18
|
+
include ::Pubid::SubsetMatch
|
|
19
|
+
|
|
18
20
|
attribute :number, :string # Supplement number (e.g., "2" in "supp2")
|
|
19
21
|
attribute :year, :string # Year (4 digits); range START year
|
|
20
22
|
attribute :month, :string # Month abbreviation; range START month
|