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
data/lib/pubid/identifier.rb
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Pubid
|
|
4
4
|
class Identifier < Lutaml::Model::Serializable
|
|
5
|
+
# `reference === candidate`: a subset match (see Pubid::SubsetMatch).
|
|
6
|
+
include SubsetMatch
|
|
7
|
+
|
|
5
8
|
# Components that serialize as a bare scalar when they carry only their
|
|
6
9
|
# single significant field: attribute name => the key the scalar is emitted
|
|
7
10
|
# under. `date` is RENAMED to `year`, matching the flat shape ISO and IEC
|
|
@@ -94,7 +97,13 @@ module Pubid
|
|
|
94
97
|
klass = concrete_class_for(data)
|
|
95
98
|
return klass.from_hash(data, options) if klass && klass != self
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
# A row written before "all parts" became a class carries the old
|
|
101
|
+
# `all_parts: true` key on the document itself. Read it, and wrap.
|
|
102
|
+
legacy_all_parts = data.is_a?(::Hash) &&
|
|
103
|
+
(data["all_parts"] || data[:all_parts])
|
|
104
|
+
|
|
105
|
+
id = super(inflate_scalar_components(data), options)
|
|
106
|
+
legacy_all_parts ? id.to_all_parts : id
|
|
98
107
|
end
|
|
99
108
|
|
|
100
109
|
# Accept the flat scalar form that {#to_hash} now emits, and the nested
|
|
@@ -498,9 +507,17 @@ module Pubid
|
|
|
498
507
|
end
|
|
499
508
|
|
|
500
509
|
attribute :_type, :string, polymorphic_class: true
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
510
|
+
# Plain strings, for every flavor. A Components::Code here held nothing
|
|
511
|
+
# but its `value` on the identifier path, and the disagreement between
|
|
512
|
+
# this declaration and the leaves that redeclared a scalar was the
|
|
513
|
+
# determinism landmine: a redeclaration on an inherited-from class
|
|
514
|
+
# resolves nondeterministically under multi-flavor load. With the base
|
|
515
|
+
# declaring the scalar, no flavor redeclares and the bug class is gone.
|
|
516
|
+
# A flavor that needs structure keeps its own attributes (ISO's committee
|
|
517
|
+
# codes, NIST's series/subseries, ITU's code), never a boxed `number`.
|
|
518
|
+
attribute :number, :string
|
|
519
|
+
attribute :part, :string
|
|
520
|
+
attribute :subpart, :string
|
|
504
521
|
attribute :stage_iteration, Components::Iteration
|
|
505
522
|
attribute :date, Components::Date
|
|
506
523
|
attribute :edition, Components::Edition
|
|
@@ -511,7 +528,17 @@ module Pubid
|
|
|
511
528
|
attribute :stage, Components::Stage
|
|
512
529
|
attribute :locality, Components::Locality
|
|
513
530
|
attribute :typed_stage, Components::TypedStage
|
|
514
|
-
|
|
531
|
+
# "All parts" is a class, not a flag: see {Pubid::AllParts}. These two
|
|
532
|
+
# readers keep a caller that read the old attribute working.
|
|
533
|
+
# rubocop:disable Naming/PredicateMethod
|
|
534
|
+
def all_parts
|
|
535
|
+
false
|
|
536
|
+
end
|
|
537
|
+
# rubocop:enable Naming/PredicateMethod
|
|
538
|
+
|
|
539
|
+
def all_parts?
|
|
540
|
+
false
|
|
541
|
+
end
|
|
515
542
|
# base is declared by supplement subclasses with proper type
|
|
516
543
|
def base
|
|
517
544
|
nil
|
|
@@ -818,11 +845,15 @@ module Pubid
|
|
|
818
845
|
#
|
|
819
846
|
# This is deliberately a per-class opt-in rather than a module prepended to
|
|
820
847
|
# every identifier class. That was tried: prepending to ~500 ancestor
|
|
821
|
-
# chains
|
|
822
|
-
# Report as `ISO TR …` instead of `ISO/TR …`, but only under
|
|
823
|
-
# load — the multi-flavor nondeterminism this file warns about
|
|
824
|
-
#
|
|
825
|
-
#
|
|
848
|
+
# chains correlated with a GOST identical adoption rendering its adopted
|
|
849
|
+
# ISO Technical Report as `ISO TR …` instead of `ISO/TR …`, but only under
|
|
850
|
+
# full-suite load — the multi-flavor nondeterminism this file warns about
|
|
851
|
+
# elsewhere. The correlation turned out not to be causal: the actual
|
|
852
|
+
# defect was an unrelated foreign-adoption prefix-routing race in
|
|
853
|
+
# `Gost::Builder` (`lib/pubid/gost/CLAUDE.md`), since fixed. The lesson
|
|
854
|
+
# about not prepending stands anyway: an explicit call in the handful of
|
|
855
|
+
# classes that need it changes no ancestor chain and cannot have that
|
|
856
|
+
# effect.
|
|
826
857
|
#
|
|
827
858
|
# @param rendered [String] the plain rendering this method produced
|
|
828
859
|
# @param opts [Hash] the render options it was called with
|
|
@@ -1004,6 +1035,28 @@ module Pubid
|
|
|
1004
1035
|
Pubid::UrnGenerator::Base
|
|
1005
1036
|
end
|
|
1006
1037
|
|
|
1038
|
+
# Attribute names on THIS class that hold a supplement's OWN identity
|
|
1039
|
+
# date — e.g. an Amendment's own publication year — as distinct from the
|
|
1040
|
+
# date of the document it supplements (reached one layer down, through
|
|
1041
|
+
# `base`/`identifiers`). Empty by default: nothing is protected, so
|
|
1042
|
+
# `exclude(:date)`/`exclude(:year)` fall straight through to every
|
|
1043
|
+
# declared attribute and every nested identifier, the shape almost every
|
|
1044
|
+
# flavor wants.
|
|
1045
|
+
#
|
|
1046
|
+
# A supplement leaf that owns its own date overrides this (CEN
|
|
1047
|
+
# `Amendment` `%i[year]`, CEN `Corrigendum` `%i[year month]`, BSI
|
|
1048
|
+
# `Amendment`/`Corrigendum` `%i[date]`). #exclude then protects those
|
|
1049
|
+
# attributes from a bare `:date`/`:year` — recursion into a *nested*
|
|
1050
|
+
# base/consolidated member is untouched, since recursion always uses the
|
|
1051
|
+
# caller's original args — and requires the explicit `:supplement_year`
|
|
1052
|
+
# key to clear them instead. This is what lets `exclude(:date)` on a
|
|
1053
|
+
# consolidated identifier drop the base standard's date while leaving an
|
|
1054
|
+
# attached amendment's own date alone, without a per-flavor #exclude
|
|
1055
|
+
# override.
|
|
1056
|
+
def self.supplement_date_attributes
|
|
1057
|
+
[]
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1007
1060
|
# Excluded attributes are nilled; every other value is passed through
|
|
1008
1061
|
# #exclude_from_nested so the exclusion also propagates into nested
|
|
1009
1062
|
# identifiers — wrapper types (adopted standards, consolidated amendments,
|
|
@@ -1023,6 +1076,16 @@ module Pubid
|
|
|
1023
1076
|
# has it excluded too. The loop below nils whichever name the flavor has.
|
|
1024
1077
|
excluded_args << :date if excluded_args.include?(:year)
|
|
1025
1078
|
|
|
1079
|
+
own_date_attrs = self.class.supplement_date_attributes
|
|
1080
|
+
# A no-op when own_date_attrs is empty (Array#|/#- with [] returns the
|
|
1081
|
+
# receiver unchanged), which is every class that doesn't override the
|
|
1082
|
+
# hook — no guard needed.
|
|
1083
|
+
if excluded_args.include?(:supplement_year)
|
|
1084
|
+
excluded_args |= own_date_attrs
|
|
1085
|
+
else
|
|
1086
|
+
excluded_args -= own_date_attrs
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1026
1089
|
attrs = self.class.attributes.each_with_object({}) do |(name, _), h|
|
|
1027
1090
|
value = excluded_args.include?(name) ? nil : public_send(name)
|
|
1028
1091
|
h[name] = exclude_from_nested(value, args)
|
|
@@ -1111,16 +1174,96 @@ module Pubid
|
|
|
1111
1174
|
matches?(other, ignore: [:date, :year]) && self != other
|
|
1112
1175
|
end
|
|
1113
1176
|
|
|
1177
|
+
# The document a wrapper stands in front of: a supplement's base, or
|
|
1178
|
+
# the leading member of a consolidated collection ("BS 7273-4:2015"
|
|
1179
|
+
# inside "BS 7273-4:2015+A1:2021"). Nil for a plain identifier.
|
|
1180
|
+
def wrapped_document
|
|
1181
|
+
return base if self.class.attributes.key?(:base) && base
|
|
1182
|
+
|
|
1183
|
+
if self.class.collection_attribute?(:identifiers) &&
|
|
1184
|
+
identifiers.is_a?(Array) && identifiers.first
|
|
1185
|
+
return identifiers.first
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1188
|
+
nil
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
# The [publisher, number, part] tuple naming the document this
|
|
1192
|
+
# identifier belongs to, resolved through wrappers.
|
|
1193
|
+
def document_identity
|
|
1194
|
+
wrapped_document&.document_identity || [publisher, number, part]
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1114
1197
|
# Self and +other+ have the same publisher + number but differ in
|
|
1115
1198
|
# part, subpart, or edition. This is the "sibling documents" pattern:
|
|
1116
|
-
# same base standard, different sub-component.
|
|
1199
|
+
# same base standard, different sub-component. Two wrappers compare
|
|
1200
|
+
# through the document they wrap, so e.g. the amendments of one base
|
|
1201
|
+
# standard are siblings while amendments of different documents are
|
|
1202
|
+
# not.
|
|
1117
1203
|
def sibling_of?(other)
|
|
1118
1204
|
return false unless other.is_a?(::Pubid::Identifier)
|
|
1205
|
+
return false if self == other
|
|
1119
1206
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1207
|
+
if wrapped_document && other.wrapped_document
|
|
1208
|
+
document_identity.take(2) == other.document_identity.take(2)
|
|
1209
|
+
else
|
|
1210
|
+
publisher == other.publisher &&
|
|
1211
|
+
number == other.number
|
|
1212
|
+
end
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
# The attributes that name a part of a document.
|
|
1216
|
+
PART_ATTRIBUTES = %i[part subpart parts].freeze
|
|
1217
|
+
|
|
1218
|
+
# The class that #to_all_parts builds. A flavor with its own all-parts
|
|
1219
|
+
# behaviour returns its subclass of {AllPartsIdentifier} here.
|
|
1220
|
+
def self.all_parts_class
|
|
1221
|
+
::Pubid::AllPartsIdentifier
|
|
1222
|
+
end
|
|
1223
|
+
|
|
1224
|
+
# The attributes that name an edition of a document. An all-parts
|
|
1225
|
+
# identifier ignores them, so it matches every edition. #exclude skips a
|
|
1226
|
+
# name that a class does not declare, so one list serves every flavor; a
|
|
1227
|
+
# flavor with another edition shape overrides this.
|
|
1228
|
+
def self.all_parts_edition_keys
|
|
1229
|
+
%i[date year edition version]
|
|
1230
|
+
end
|
|
1231
|
+
|
|
1232
|
+
# A new identifier for every part of this document. It holds this
|
|
1233
|
+
# identifier, unchanged, as its only member. `to_s` prints the document
|
|
1234
|
+
# without its part and edition ("ISO 9000 (all parts)"), and `===`
|
|
1235
|
+
# matches every part of it in every edition. The receiver does not change.
|
|
1236
|
+
# A parsed "(all parts)" still sets the +all_parts+ flag instead; the two
|
|
1237
|
+
# forms are not equal.
|
|
1238
|
+
# @return [Pubid::AllPartsIdentifier]
|
|
1239
|
+
def to_all_parts
|
|
1240
|
+
self.class.all_parts_class.new(identifiers: [self])
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
# @return [Boolean] true when this identifier names a part of a document
|
|
1244
|
+
def part?
|
|
1245
|
+
without_parts != normalized_copy
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1248
|
+
# A copy without `part`, `parts`, `subpart` and the +extra+ attributes.
|
|
1249
|
+
# Nested identifiers lose them too (#exclude).
|
|
1250
|
+
def without_parts(*extra)
|
|
1251
|
+
normalized_copy(*PART_ATTRIBUTES, *extra)
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
# A copy without the +attrs+ (#exclude), where a part collection holds
|
|
1255
|
+
# [] and never nil. A parse and #from_hash disagree on nil against [] in
|
|
1256
|
+
# some flavors (JIS), so two copies compare equal only after this.
|
|
1257
|
+
def normalized_copy(*attrs)
|
|
1258
|
+
exclude(*attrs).tap do |id|
|
|
1259
|
+
PART_ATTRIBUTES.each do |name|
|
|
1260
|
+
next unless self.class.collection_attribute?(name)
|
|
1261
|
+
|
|
1262
|
+
id.public_send(:"#{name}=", []) if id.public_send(name).nil?
|
|
1263
|
+
end
|
|
1264
|
+
end
|
|
1123
1265
|
end
|
|
1266
|
+
protected :without_parts, :normalized_copy
|
|
1124
1267
|
|
|
1125
1268
|
# Self is an all-parts collection that covers +other+.
|
|
1126
1269
|
def includes?(other)
|
|
@@ -1140,15 +1283,24 @@ module Pubid
|
|
|
1140
1283
|
matches?(other, ignore: [:date, :year, :stage, :typed_stage])
|
|
1141
1284
|
end
|
|
1142
1285
|
|
|
1143
|
-
# Self and +other+
|
|
1144
|
-
#
|
|
1286
|
+
# Self and +other+ are editions of the same document: the edition-year
|
|
1287
|
+
# pair ("ISO 9001:2008" → "ISO 9001:2015"), or two consolidated states
|
|
1288
|
+
# of one base document — "BS 7273-4:2015+A1:2021" is the prior edition
|
|
1289
|
+
# of "BS 7273-4:2015+A2:2023" (pubid/pubid#449). A composite and its
|
|
1290
|
+
# unwrapped base are not editions of each other: bare → +A1 → +A2 is a
|
|
1291
|
+
# supersession series, not an edition-year relationship — the amendment
|
|
1292
|
+
# supplements the base without superseding it.
|
|
1145
1293
|
def edition_of?(other)
|
|
1146
1294
|
return false unless other.is_a?(::Pubid::Identifier)
|
|
1295
|
+
return false if self == other
|
|
1147
1296
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1297
|
+
if wrapped_document && other.wrapped_document
|
|
1298
|
+
document_identity == other.document_identity
|
|
1299
|
+
else
|
|
1300
|
+
publisher == other.publisher &&
|
|
1301
|
+
number == other.number &&
|
|
1302
|
+
part == other.part
|
|
1303
|
+
end
|
|
1152
1304
|
end
|
|
1153
1305
|
|
|
1154
1306
|
# Any relational predicate holds.
|
data/lib/pubid/idf/builder.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Pubid
|
|
|
13
13
|
}.freeze
|
|
14
14
|
|
|
15
15
|
def build(parsed_hash)
|
|
16
|
+
all_parts = parsed_hash.delete(:all_parts)
|
|
16
17
|
typed_stage = Pubid::Idf.locate_stage(parsed_hash[:type_with_stage])
|
|
17
18
|
identifier = Pubid::Idf.locate_type(typed_stage.type_code).new
|
|
18
19
|
|
|
@@ -21,7 +22,9 @@ module Pubid
|
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
assign_attributes(identifier, parsed_hash)
|
|
24
|
-
|
|
25
|
+
# "(all parts)" names every part of the document, so it wraps the
|
|
26
|
+
# document, which holds no mark itself.
|
|
27
|
+
all_parts ? identifier.to_all_parts : identifier
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
private
|
|
@@ -80,3 +83,5 @@ module Pubid
|
|
|
80
83
|
end
|
|
81
84
|
end
|
|
82
85
|
end
|
|
86
|
+
|
|
87
|
+
Pubid::Idf::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
data/lib/pubid/idf/identifier.rb
CHANGED
|
@@ -5,6 +5,18 @@ module Pubid
|
|
|
5
5
|
module Idf
|
|
6
6
|
# An IDF identifier has no copublisher
|
|
7
7
|
class Identifier < ::Pubid::Identifier
|
|
8
|
+
# The all-parts identifier of this flavor.
|
|
9
|
+
def self.all_parts_class
|
|
10
|
+
Identifiers::AllParts
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# A Type INSTANCE, never the raw :key symbol - lutaml materializes
|
|
14
|
+
# defaults during from_hash; casting a Symbol into the
|
|
15
|
+
# component raises InvalidFormatError (pubid#383).
|
|
16
|
+
def self.default_type
|
|
17
|
+
Components::Type.new(abbr: type[:short])
|
|
18
|
+
end
|
|
19
|
+
|
|
8
20
|
attribute :typed_stage, Components::TypedStage
|
|
9
21
|
|
|
10
22
|
# `number`/`part`/`subpart` are declared here rather than inherited as a
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Idf
|
|
5
|
+
module Identifiers
|
|
6
|
+
# Every part of one IDF document: "IDF 1 (all parts)".
|
|
7
|
+
class AllParts < ::Pubid::Idf::Identifier
|
|
8
|
+
include ::Pubid::AllParts
|
|
9
|
+
|
|
10
|
+
# The document URN; GB and IDF mark no series in the URN.
|
|
11
|
+
def to_urn
|
|
12
|
+
identity.to_urn
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -4,7 +4,7 @@ module Pubid
|
|
|
4
4
|
module Idf
|
|
5
5
|
module Identifiers
|
|
6
6
|
class Amendment < SupplementIdentifier
|
|
7
|
-
attribute :type, Components::Type, default: -> { self.class.
|
|
7
|
+
attribute :type, Components::Type, default: -> { self.class.default_type }
|
|
8
8
|
|
|
9
9
|
TYPED_STAGES = [
|
|
10
10
|
Components::TypedStage.new(
|
|
@@ -4,7 +4,7 @@ module Pubid
|
|
|
4
4
|
module Idf
|
|
5
5
|
module Identifiers
|
|
6
6
|
class Corrigendum < SupplementIdentifier
|
|
7
|
-
attribute :type, Components::Type, default: -> { self.class.
|
|
7
|
+
attribute :type, Components::Type, default: -> { self.class.default_type }
|
|
8
8
|
|
|
9
9
|
TYPED_STAGES = [
|
|
10
10
|
Components::TypedStage.new(
|
|
@@ -5,7 +5,7 @@ module Pubid
|
|
|
5
5
|
module Identifiers
|
|
6
6
|
# International Standard Identifier
|
|
7
7
|
class InternationalStandard < SingleIdentifier
|
|
8
|
-
attribute :type, Components::Type, default: -> { self.class.
|
|
8
|
+
attribute :type, Components::Type, default: -> { self.class.default_type }
|
|
9
9
|
|
|
10
10
|
TYPED_STAGES = [
|
|
11
11
|
Components::TypedStage.new(
|
|
@@ -4,7 +4,7 @@ module Pubid
|
|
|
4
4
|
module Idf
|
|
5
5
|
module Identifiers
|
|
6
6
|
class ReviewedMethod < SingleIdentifier
|
|
7
|
-
attribute :type, Components::Type, default: -> { self.class.
|
|
7
|
+
attribute :type, Components::Type, default: -> { self.class.default_type }
|
|
8
8
|
TYPED_STAGES = [
|
|
9
9
|
Components::TypedStage.new(
|
|
10
10
|
code: :pwirm,
|
|
@@ -12,7 +12,7 @@ module Pubid
|
|
|
12
12
|
#
|
|
13
13
|
# Note: The type attribute is inherited from the base Identifier class.
|
|
14
14
|
# Concrete classes should add their own default value like:
|
|
15
|
-
# attribute :type, Components::Type, default: -> { self.class.
|
|
15
|
+
# attribute :type, Components::Type, default: -> { self.class.default_type }
|
|
16
16
|
class SingleIdentifier < Identifier
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# IEC flavor notes
|
|
2
|
+
|
|
3
|
+
IEC typed stages, `from_hash` equality, and the `number` retype that deleted `Iec::Components::Code`.
|
|
4
|
+
|
|
5
|
+
These notes were part of the root `CLAUDE.md`. Read them before you change `lib/pubid/iec/` or `spec/pubid/iec/`. The root file keeps the cross-flavor contract that every flavor obeys.
|
|
6
|
+
|
|
7
|
+
- **IEC `from_hash(to_hash) == parse` — two `from_hash` defaults that diverged from the parse path**: `==` was false for **every** IEC identifier, while `to_s`, `to_urn` and `to_hash` were all correct — so the relaton index gate (`from_hash(to_hash) == to_hash`) passed and a lookup (parsed reference vs `from_hash`-ed row) silently returned nothing. Two independent causes, one family. **(1) `original_abbr`** — the JCGM defect verbatim, with a **second** injector: `Identifier.published_typed_stage` (the attribute default) **and** `#stage_from_kv` both `dup`ped the registry entry and set `original_abbr = canonical_abbreviation`, where the parse path (`Builder#locate_typed_stage`, the `:type_with_stage` cast) leaves it nil. Both writes are deleted; the `dup` stays (`TYPED_STAGES` is frozen, its elements are not). **The hand-off warned this fix was rendering-sensitive because IEC reads the field at `iec/builder.rb:161-163` (long-vs-short `Amd 1` vs `AMD1`). It is not: that read is UNREACHABLE.** The block is gated on `identifier.class.attributes.key?(:rendering_style)`, and **no IEC class declares a `rendering_style` attribute** — verified by force-loading all 26 IEC identifier classes and inspecting `.attributes`. The whole block (builder.rb 155–200) is dead, so the read is doubly dead: never executed, and reading a field already nil on the parse path. The long spelling is already collapsed on `main` (`…/Amd 1:2017` renders back `…/AMD1:2017`). The block is **kept with an explicit UNREACHABLE comment** rather than deleted, so the next reader does not mistake it for a live consumer again. **The general lesson: before copying the JCGM delete to another flavor, check not just whether the field is read but whether the READ IS REACHABLE — a gated block whose gate is never true is not a consumer.** **(2) `Amendment`/`Corrigendum` redeclared `attribute :type`** with `default: -> { self.class.type[:key] }`. The redeclaration shadowed the derived reader `Iec::Identifier#type` (`typed_stage&.to_type`) on those two classes only; `type` is not serialized, so on `from_hash` the default assigned the **Symbol** `:amd`/`:cor` where the parse path assigns a `Components::Type`, and lutaml does not cast it. Both declarations are deleted so the two classes inherit the derived reader like every other IEC type; the class method `self.type` is untouched and still used. This is the AMCA lesson in another guise — **a default (or converter) that yields a different runtime shape from the parse path is a sign the attribute should not be there at all**. It also repairs a **parse-path** bug, not just the round trip: `Builder#wrap_with_consolidated` builds its supplement directly and never calls `assign_attributes`, so a consolidated amendment member (`IEC 60050-300:2001+AMD1:2005 CSV`) returned the bare Symbol `:amd` from `.type` on `main` even when parsed — pinned by a spec. **Latent, deliberately not changed:** `Builder#locate_typed_stage` (the parse-path lookup) hands out the shared frozen-array element **without** a `dup`, unlike the two from_hash paths; harmless today because nothing mutates a `TypedStage` in place, but it is the same "frozen array, unfrozen elements" hazard. Verified: `to_s`, `to_urn` and `to_hash` **byte-identical** across all 12,303 inputs in `spec/fixtures/iec/identifiers/pass/`, so nothing serialized moves and **no index migration follows**; `==` rises from 0 to **9,914 of 12,302**. **Verified against the published `relaton-data-iec` dataset too, which is the measurement that matters**: over all **32,817 `index-v2.yaml` rows**, the consumer shape `parse(row.to_s) == from_hash(row)` goes from **0 to 32,817** (and `matches?(ignore: [:year])` with it), while `from_hash(h).to_hash == h` holds for all 32,817 **both before and after** — so the published index is already valid and **needs no regeneration**. Over the 32,855 primary docidentifiers in `data/*.yaml`, 32,813 parse (the 42 failures are pre-existing title-bearing forms like `IEC Technology Report Cyber security:2019`, unchanged by this) and for all 32,813 `to_s`, `to_urn` and `root.number` are identical before and after, with **0 empty index keys**. The fixture corpus is the stricter test only because it contains WorkingDocument/PNW/TRF forms that the crawler does not publish. Locked by `spec/pubid/iec/typed_stage_equality_spec.rb` and by deleting the `"Pubid::Iec"` entry from `PENDING_EQUALITY`. **The 2,388 that remain have a THIRD, unrelated cause and are deliberately out of scope**: `WorkingDocument` (`technical_committee`/`wd_number`/`wd_stage`/`wd_language`, 2,318 ids — `to_hash` is `{"_type"=>"pubid:iec:working-document"}` and **`root.number` is nil**, so every WD row keys the empty string), the PNW `wp_stage`/`wp_type` pair (32) and `TestReportForm#cispr_identifier` (38) are **never serialized at all**. Mapping them ADDS keys to `to_hash` and forces a `relaton-data-iec` re-crawl, which is a separate decision — and a low-value one today, since the published index contains **zero** `working-document` and **zero** `test-report-form` rows. (hand-off: iec-typed-stage-original-abbr; follow-up hand-off: iec-unserialized-identity-attributes.)
|
|
8
|
+
|
|
9
|
+
- **PNW is a stage of its own (10.20), the stage separator is a space, and the URN is built from components — issue #360 items 1, 2 and 4.** The three are one change, because on `main` the URN was produced by a regex scraped over `identifier.to_s`, so the stage fix and the separator fix both moved it. Reference behaviour was taken from the installed `pubid-iec 1.15.21`, **except for the URN, where 1.x is the wrong oracle** — see below.
|
|
10
|
+
**(1) PNW.** `identifiers/international_standard.rb` listed `PNW` inside the `code: :pwi` stage (harmonized `00.*`), so `IEC PNW 1000-1:2023 ED2` rendered back as `IEC/PWI 1000-1:2023 ED2`. IEC's portal and `pubid-iec-1.15.21/stages.yaml` both give `PNW: ["10.20"]`. PNW now has its own `code: :pnw` TypedStage with `harmonized_stages: ["10.20"]`, and **`10.20` was removed from `:np`** so a lookup by harmonized code is unambiguous — the URN now performs exactly that lookup. `Identifiers::WorkingDocument#stage` built a bare `Components::Stage` from a hand-written `{"PWI"=>"pwi","PNW"=>"pnw"}` map, giving a stage with no harmonized code and a `"pnw"` stage code that existed nowhere else; it resolves through `Pubid::Iec.locate_stage` instead. **The grammar needed no change for the reported form**: `working_programme_with_publisher` required a literal `/` after the publisher, so `IEC PNW 1000-1:2023` already fell through to `type_with_stage`. That rule was **deleted** (it had zero corpus inputs), and `render_working_document` no longer prefixes the publisher: a work-programme registration renders bare (`PNW 65-915 ED1`), because its number is TC-scoped (`86B-15`, `SyCCOMM-1`) rather than an IEC document number, and prefixing `IEC ` would break idempotence — `IEC PNW 65-915 ED1` re-parses as an International Standard. That alone repaired 32 fixture entries.
|
|
11
|
+
**(2) Separator.** `SingleIdentifier#publisher_portion` printed `/` without copublishers and a space with them, while **twelve** per-type classes overrode it to force a space — so `IEC TS 60038` and `IEC DTR 62048` were already right and `IEC/CD 60038`, `IEC/FDAM 60038-1`, `IEC/DCOR 60038-1`, `IEC/DISH 60050-191` were not. The base now always uses a space (the slash stays the copublisher separator only) and **all twelve overrides are deleted**: nine of them built from `publisher.to_s` with no copublisher handling and no `super`, so deleting them **repairs** `ISO/IEC PAS 62975` → `ISO PAS 62975`, `ISO/IEC SRD 12345:2020` → `ISO SRD 12345:2020` and `IEC/IEEE SRD 62582:2022` → `IEC SRD 62582:2022` (three specs had pinned that loss). `renderer.rb#render_supplement_standalone` is a **second, independent** publisher/stage join that `publisher_portion` never reaches — it joined every part with `/` — and needed the same fix. **Input still accepts both spellings**; only rendering normalises. Measured: `to_s` changed for exactly 32 of 12,302 corpus entries, all of them the work-programme repairs above; `to_hash`, the identifier class and `root.number` are byte-identical for all 12,302.
|
|
12
|
+
**(3) URN. Do not use pubid-iec 1.x as the oracle here.** 1.x emits `urn:iec:std:iec:ts:60932:1988:::` — the type *before* the number — while all 32,831 published `relaton-data-iec` URNs put it *after* the date (`urn:iec:std:iec:60932:1988:ts::`). pubid 2's slot **order** was already right and is kept; what was missing were two slots the scrape could never reach. The layout is now `urn:iec:std:{authority}:{number}[-{part}[-{subpart}]]:{date}:{S7}:{S8}:{language}{adjuncts}`, where **S7** is `stage-{harmonized}` for a non-published stage and the legacy type token (`tr`/`ts`/`pas`/`srd`/`guide`) otherwise, and **S8** is the deliverable (`csv`/`rlv`/`exv`/`cmv`/`ser`) or, failing that, `ed-{n}`. So `IEC PNW 1000-1:2023 ED2` → `urn:iec:std:iec:1000-1:2023:stage-10.20:ed-2:`, the string the issue asked for. **A draft of a typed document keeps both, type first** (`IEC DTS 62271-1` → `ts-stage-50.00`): the harmonized codes are shared across types, so a stage-only slot would collide a draft TS, a draft TR and a draft International Standard, and `parse_urn` would resolve all three to an International Standard. Three details are load-bearing and are pinned by specs: the **authority takes the last whitespace-delimited token of a publisher body**, because `IEC CA 01:2017` is published as `urn:iec:std:ca:01:2017:::`; **`Components::Date#render` under a URN context drops the month** (the ISO convention), so the date slot reads the printed form instead and `IEC CA 01:2025-10` keeps its month; and a **deliverable is not always on the outermost wrapper** (`IEC 60079-10-1:2020 CMV/COR1:2021` is a corrigendum around the CMV), so `vap` is searched down the chain. A CISPR test report form carries no number of its own — the document sits in `cispr_identifier` — and the generator falls back to it, which is what keeps those 38 ids from losing their URN entirely.
|
|
13
|
+
**What the regex was actually costing.** Across the corpus the publisher slot held: `iec` 5,401, **nil 2,333**, **`trf` 2,142**, `iso-iec` 2,094, … `iec-pnw` 12, `iec-pwi` 9, `od` 6, `cs` 3, `ca` 3, `dir` 3. `IECEE TRF 10079-1A:2020` keyed `urn:iec:std:trf:10079-1::::` — publisher, letter part and year all gone. After the change **0 URNs are lost, 15 are gained, none gets fewer slots**, and 2,838 change, every one in the direction of carrying more identity.
|
|
14
|
+
**The published corpus changes, and that was a deliberate call.** 668 of the 32,831 published URNs no longer match, in three groups: **598** where the old `[\d-]+` stopped at a letter and took the date with it (`CISPR 11A:1976` → `urn:iec:std:cispr:11::::`, the same URN as `CISPR 11:1976`), **68** multi-deliverable ids where only the first code survived (`EXV-CMV` → `exv`), and **2** fragment identifiers that gain `ed-N`. All 668 are places where the published value is lossier than the identifier, and the measurement that justifies the change is the collision count: the published scheme puts **532 rows in 158 collision groups**, the new one **11 rows in 4**. `relaton-data-iec` therefore needs regenerating — hand-off `relaton-data-iec-regenerate-urns`.
|
|
15
|
+
**The fixture net had to be built first.** `spec/pubid/iec/fixtures_spec.rb` read `archived-gems/pubid-iec/spec/fixtures/iec-pubid.txt` at an 80% gate, with an empty failure-reporting block, so `spec/fixtures/iec/` was guarded by nothing and 35 `pass/` entries were already stale on `main`. It is rewritten on the `spec/pubid/csa/fixtures_spec.rb` shape (glob tripwire, zero-failure per file over `pass/` and `fail/`, conservation against `full/`, disjointness); note IEC's `full/` is many files, not one `identifiers.txt`, and carries the `!input!rendered` marker itself, so both sides split it. Regenerating exposed the CLAUDE.md landmine in the flesh: **five `ISO/IEC DIR …` fixtures had been hand-added to `pass/` without being added to `full/`**, and the classifier deleted them; they are restored into `full/iso-iec-pubid.txt`. The conservation example is what makes that loud from now on. (hand-off: iec-pnw-separator-urn; consumer follow-up: `relaton-data-iec` re-crawl.)
|
|
16
|
+
|
|
17
|
+
- **`edition` serializes flat, and the date does not need to** (pubid#360 item 3): IEC's edition emitted a one-key hash, `edition: {number: "2"}`, for a value that is only ever a scalar. `lib/pubid/iec/builder.rb` has a **single** edition construction site and it writes `Edition.new(number: value&.to_s)` and nothing else, so the component is degenerate by construction — confirmed over all **12,331** parseable IEC fixtures, where 607 carry an edition and `number` is the only field ever populated (`year` and `original_text`: zero). It now emits `edition: "2"`, and `from_hash` reads both that and the stored nested form, so no published `relaton-data-iec` row stops deserializing. The mapping at `lib/pubid/iec/identifier.rb` stays a plain `map "edition", to: :edition`; the flattening is the shared rule on `::Pubid::Identifier`, not an IEC converter. **The date half needed no work at all** — IEC already serializes it flat as top-level `year`/`month`/`day`/`undated` through its own converters, so no `date` key ever reaches the shared flattener. **That is not a coincidence worth ignoring: IEC is the flavor that proves the flat rule has to be per identifier rather than per flavor.** Of its 9,730 dated identifiers, 9,728 carry a year alone, but **10 carry a month** (`IEC CA 01:2025-10`, `IECEE 02:2025-06(en)`, `IECEE AD-001:2024-06(en)`) and **2 are undated** (`IEC 60050:--`, `IEC 60050:—`). A flavor-wide "IEC dates are year-only" switch would have thrown those twelve away silently. `spec/pubid/flat_scalar_serialization_spec.rb` asserts both directions on IEC for exactly this reason.
|
|
18
|
+
|
|
19
|
+
- **`WorkingDocument` is the one IEC type that still renders plain under `annotated: true`.** The cross-flavor annotator (`Renderers::Annotator`, see the root `CLAUDE.md`) wraps the identifier's own component values where they appear in the rendered string; a WorkingDocument's identity lives in `technical_committee` / `wd_number` / `wd_stage` / `wd_language`, none of which is one of those components. This is the same unserialized-identity gap already recorded above (`to_hash` is `{"_type"=>"pubid:iec:working-document"}` and `root.number` is nil), seen from the rendering side. Nothing raises — the flag is accepted and simply finds no token — so it is a missing span, not a failure. Fixing it means mapping those attributes, which forces a `relaton-data-iec` re-crawl, and remains the separate decision it already was. (hand-off: iec-unserialized-identity-attributes.)
|
|
20
|
+
|
|
21
|
+
- **The attribute path takes a plain scalar for language and copublisher — the residue of issue #360 item 3.** Items 1, 2 and 4 gave `IEC PNW 1000-1:2023 ED2` the right stage, separator and URN, and the constructor contract gave it `edition: 2` and `year: 2023`. The URN the issue asked for also carries a language, and that half still failed: `languages: ["en"]` stored raw Strings, so `to_s` raised `wrong number of arguments` at `lib/pubid/iec/single_identifier.rb:59` (`lang.to_s(lang_single:)`) and `to_urn` raised `undefined method 'code'` at `lib/pubid/iec/urn_generator.rb:190`. `copublishers: ["ISO"]` raised the same way on `body`. Both are now coerced by the shared `DEGENERATE_COMPONENT_FIELDS` rule on `::Pubid::Identifier` (see the root `CLAUDE.md`), not by an IEC converter — and IEC is the flavor that proves the wrap has to key on the component **class**, because its copublisher is the subclass `Pubid::Iec::Components::Publisher`, which an attribute-name or exact-class table would miss. So `Identifiers::InternationalStandard.new(number: "1000", part: "1", year: 2023, edition: 2, languages: ["en"])` now renders `IEC 1000-1:2023 ED2(en)` and yields `urn:iec:std:iec:1000-1:2023::ed-2:en`. The Hash and component forms are unchanged and still agree with the scalar form. **`number` was deliberately NOT coerced** — and the bullet below resolves that tension the other way round, by making the attribute a `:string` so a String is the type rather than a tolerated intruder. Note the copublisher renders `IEC/ISO 1000`, IEC's own ordering — the coercion does not touch order. (hand-off: iec-degenerate-component-coercion.)
|
|
22
|
+
|
|
23
|
+
- **`number`/`part`/`subpart` are plain `:string`, `Iec::Components::Code` is deleted, and three of eleven `key_value` converters go with it.** The component was an empty box: over all **12,331** parseable pass fixtures the three were an `Iec::Components::Code` **9,975 / 5,422 / 2,416** times and **not one** populated `prefix`, `parts`, `part` or `subpart` inside it. The subclass existed solely for a `to_s` that prints `"#{prefix} #{value}"`; `Code.parse` was the only site that ever passed `prefix:` and it had **zero callers**, as did the `full_code` alias, and the class had zero references outside `identifier.rb` and its own file. So the subclass is deleted outright along with its `autoload`.
|
|
24
|
+
**This was a live defect, not tidiness, and it is the reason to prefer retyping over coercing.** The parse path produced a Code while the constructor path left a String — a `:string` assigned to a Code-typed attribute stays a String — so `Identifiers::InternationalStandard.new(number: "60068", part: "2", subpart: "28", year: "2008")` was **not `==`** `Pubid::Iec.parse("IEC 60068-2-28:2008")`, and `#matches?` is `exclude(*ignore) == other.exclude(*ignore)`, so every match between a hand-built reference and a parsed one silently returned false. `to_s`, `to_urn` **and** `to_hash` all agreed on both sides, which is exactly why nothing caught it: a relaton index lookup just returned nothing, with no error. `spec/pubid/attribute_construction_spec.rb` had even pinned the String half (`"leaves a scalar number uncoerced (IEC)"`) and asserted `to_s` and `to_hash` on the two paths — but never `==`, so the spec that was closest to the bug documented it as intended behaviour. **The lesson generalises: a spec comparing two construction paths must assert `==`, not only that they render and serialize alike.**
|
|
25
|
+
**`with:` was doing two jobs, and only one of them was the Code.** `emit_code`/`build_code` unwrapped and rewrapped the component — identity functions once the attribute is a String — but the converters were *also* the mechanism by which `DelegatedFieldSuppression` no-opped the delegated fields of the four wrapper types. **Placement replaces the denylist**: `number`/`part`/`subpart` now map on **`Pubid::Iec::SingleIdentifier`**, the parent of `Identifiers::Base` and `SupplementIdentifier` — the only two classes that own a number — while `FragmentIdentifier`, `ConsolidatedIdentifier`, `VapIdentifier` and `SheetIdentifier` inherit from `Iec::Identifier` **directly** and are therefore excluded structurally, not by remembering to add a no-op. That distinction is not academic: the surviving denylist suppresses `year` but **not `month`/`day`/`undated`**, so `IEC CA 01:2025-10 CSV` still emits a top-level `"month" => "10"` duplicating the one inside `base` — a pre-existing leak, left alone here because removing a key from published rows is a wire change (hand-off `iec-wrapper-month-day-leak`).
|
|
26
|
+
**Two more converters were pure boilerplate.** `all_parts` is a Boolean attribute with `default: false` that `::Pubid::Identifier#to_hash` already drops, so it takes the plain GB spelling `map "all_parts", to: :all_parts, render_default: false`. `stage_iteration` holds a `Components::Iteration` — one `:string` field, `number`, whose `to_s`/`render` return it — so it joins the shared `FLAT_SCALAR_COMPONENTS`/`FLAT_SCALAR_FIELDS` tables on `::Pubid::Identifier` and takes a plain map too. **The blast radius of that shared change is exactly ISO and IEC**, the only two flavors that map the key, and both already emitted the bare scalar through their own converters — ISO keeps its converter, which the inflation guard defers to by design. Net: **12 converter methods and 2 helpers deleted**. The five groups that keep `with:` all earn it — `year`/`month`/`day` and `undated` split one `date` attribute across four keys, `publisher`/`copublishers` map a `Components::Publisher` to its `body` and omit the IEC default, and `stage` maps `typed_stage` to its code with class-scoped resolution on load. **The rule this leaves behind: a converter is for a custom class or for splitting one attribute across several keys; a primitive needs a plain map.**
|
|
27
|
+
**The shared splitter gained a hook rather than a special case.** `Builder::Base#parse_number_with_part(value, code_class:)` is used by ISO and IEC only; `code_class:` is now optional and the three constructions route through `wrap_number_component`, which returns the raw scalar when `code_class` is nil. ISO still passes `Pubid::Iso::Components::Code` and is untouched. This is the extension point the remaining tranches reuse.
|
|
28
|
+
**Verified by replaying a captured baseline, not by the suite alone.** Over all **12,331** IEC pass-fixture ids, `to_s`, `to_urn`, `to_mr_string`, `root.number`, the identifier class, the deep-key-sorted `to_hash`, the `from_hash(to_hash) == to_hash` flag and the `from_hash(to_hash) == id` flag are **byte-identical, 0 diffs in every field**. The verbatim `to_hash` differs for 9,950 rows and for **one reason only** — `number`/`part`/`subpart` moved to the end of the key order, because lutaml *appends* a subclass's `key_value` maps to the parent's; removing those three keys leaves the two key sequences identical for every row, and `Hash#==` ignores order, so **0 rows differ in any other way**. Over all **7,573** ISO pass-fixture ids every field including the verbatim hash is byte-identical, so neither shared-file change moved ISO. Against the published `relaton-data-iec` `index-v2.yaml` (**32,847** rows) `from_hash(row).to_hash == row` holds for 32,845 and `parse(from_hash(row).to_s) == from_hash(row)` for all 32,847, **both before and after** — the 2 misses are the pre-existing rows storing the old nested `edition: {number: …}` form. **No index migration follows**, and that is measured rather than assumed. Locked by `spec/pubid/iec/number_string_spec.rb` (structural tripwire over the whole class tree, the constructed-vs-parsed `==`, the wrapper hashes, and the absence of the deleted constant) and by IEC's entry in `spec/pubid/number_string_retype_spec.rb`.
|
|
29
|
+
**Known residue, deliberately unchanged:** after the map moved, `Pubid::Iec::Identifier.from_hash({"number" => "60038"})` — a hand-written hash with **no `_type`**, on the bare base class — silently drops the key instead of building an `Identifier` that holds it. That object is already degenerate (`to_s` on it raises `NoMethodError` for `publisher_portion`) and nothing in this repo or in `relaton-iec` constructs one. (hand-off: iec-number-string.)
|
|
30
|
+
|
|
31
|
+
**Two parent-slot gaps recorded by the cross-flavor `base` survey** (`lib/pubid/bsi/CLAUDE.md` has the measured version). `Identifiers::TestReportForm#cispr_identifier` is a polymorphic `Identifier` parent slot with no `#root` override, so `root` returns the form itself. And `Identifiers::ConsolidatedIdentifier#base_document` returns `identifiers&.first` **without** the recursive `&.base_document` that its BSI, CSA and CEN counterparts use — a one-level peel where the others peel fully, so a consolidated edition of a supplement does not reduce to the bare standard. Neither is chased here; both are pinned by nothing, which is why they are written down.
|
data/lib/pubid/iec/builder.rb
CHANGED
|
@@ -28,6 +28,8 @@ module Pubid
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def build(parsed_hash)
|
|
31
|
+
all_parts = parsed_hash.delete(:all_parts)
|
|
32
|
+
|
|
31
33
|
# Handle sheet_supplement_identifier pattern:
|
|
32
34
|
# {base: {...}, sheet_number: ..., sheet_year: ..., type_with_stage: "COR", number_with_part: "1", date: "1995"}
|
|
33
35
|
# This is a Corrigendum/Amendment wrapping a SheetIdentifier
|
|
@@ -218,7 +220,9 @@ module Pubid
|
|
|
218
220
|
vap_suffix_data)
|
|
219
221
|
end
|
|
220
222
|
|
|
221
|
-
|
|
223
|
+
# "(all parts)" and the "ser" URN slot name every part of the
|
|
224
|
+
# document, so they wrap the document, which holds no mark itself.
|
|
225
|
+
all_parts ? identifier.to_all_parts : identifier
|
|
222
226
|
end
|
|
223
227
|
|
|
224
228
|
def handle_key(identifier, key, value)
|
|
@@ -455,3 +459,5 @@ module Pubid
|
|
|
455
459
|
end
|
|
456
460
|
end
|
|
457
461
|
end
|
|
462
|
+
|
|
463
|
+
Pubid::Iec::Builder.prepend(Pubid::Builder::AllPartsWrap)
|
|
@@ -7,6 +7,8 @@ module Pubid
|
|
|
7
7
|
# Individual Amendment within a consolidated chain
|
|
8
8
|
# Single Responsibility: Represents a single amendment with number and year
|
|
9
9
|
class Amendment < Lutaml::Model::Serializable
|
|
10
|
+
include ::Pubid::SubsetMatch
|
|
11
|
+
|
|
10
12
|
attribute :number, :string
|
|
11
13
|
attribute :year, :string, default: -> {}
|
|
12
14
|
|
|
@@ -21,6 +23,8 @@ module Pubid
|
|
|
21
23
|
# Single Responsibility: Represents multiple amendments combined with + notation
|
|
22
24
|
# Example: +AMD1:2020+AMD2:2022
|
|
23
25
|
class ConsolidatedAmendment < Lutaml::Model::Serializable
|
|
26
|
+
include ::Pubid::SubsetMatch
|
|
27
|
+
|
|
24
28
|
attribute :amendments, Amendment, collection: true
|
|
25
29
|
|
|
26
30
|
def to_s
|
|
@@ -8,6 +8,8 @@ module Pubid
|
|
|
8
8
|
# Single Responsibility: Represents sheet number in IEC identifiers
|
|
9
9
|
# Example: IEC 60695-2-1/1:1994 (where /1 is the sheet)
|
|
10
10
|
class Sheet < Lutaml::Model::Serializable
|
|
11
|
+
include ::Pubid::SubsetMatch
|
|
12
|
+
|
|
11
13
|
attribute :number, :string
|
|
12
14
|
|
|
13
15
|
def to_s
|
|
@@ -8,6 +8,8 @@ module Pubid
|
|
|
8
8
|
# Single Responsibility: Represents TRF-specific publishing information
|
|
9
9
|
# Example: IECEE TRF or IECEx TRF publications
|
|
10
10
|
class TrfInfo < Lutaml::Model::Serializable
|
|
11
|
+
include ::Pubid::SubsetMatch
|
|
12
|
+
|
|
11
13
|
attribute :publisher, :string, default: -> {}
|
|
12
14
|
attribute :series, :string, default: -> {}
|
|
13
15
|
attribute :version, :string, default: -> {}
|
|
@@ -7,6 +7,8 @@ module Pubid
|
|
|
7
7
|
# VAP (Validation Assessment Programme) suffix codes
|
|
8
8
|
# Single Responsibility: Represents VAP validation status suffixes
|
|
9
9
|
class VapSuffix < Lutaml::Model::Serializable
|
|
10
|
+
include ::Pubid::SubsetMatch
|
|
11
|
+
|
|
10
12
|
# VAP suffix codes as defined by IEC
|
|
11
13
|
CODES = {
|
|
12
14
|
"CMV" => "Common Modifications and Variations",
|
data/lib/pubid/iec/identifier.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Pubid
|
|
|
16
16
|
# converter can be no-opped this way, so a key that needs `with:` for
|
|
17
17
|
# another reason — year, publisher, copublishers, stage — still lists here.
|
|
18
18
|
# (`month`/`day`/`undated` are missing from that list and leak a duplicated
|
|
19
|
-
# top-level key on a wrapper; pre-existing, see
|
|
19
|
+
# top-level key on a wrapper; pre-existing, see lib/pubid/iec/CLAUDE.md.)
|
|
20
20
|
module DelegatedFieldSuppression
|
|
21
21
|
def year_to_kv(_model, _doc); end
|
|
22
22
|
def publisher_to_kv(_model, _doc); end
|
|
@@ -25,6 +25,12 @@ module Pubid
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
class Identifier < ::Pubid::Identifier
|
|
28
|
+
# IEC prints "(all parts)" and has a series URN, so it has its own
|
|
29
|
+
# all-parts class.
|
|
30
|
+
def self.all_parts_class
|
|
31
|
+
Identifiers::AllParts
|
|
32
|
+
end
|
|
33
|
+
|
|
28
34
|
# Override base types with IEC-specific ones. publisher defaults to the
|
|
29
35
|
# type's implied publisher (IEC), so an omitted publisher key reconstructs
|
|
30
36
|
# correctly on from_hash.
|
|
@@ -187,7 +193,6 @@ module Pubid
|
|
|
187
193
|
map "stage", with: { to: :stage_to_kv, from: :stage_from_kv }
|
|
188
194
|
# A Boolean attribute whose `false` default ::Pubid::Identifier#to_hash
|
|
189
195
|
# already drops, so a plain map suffices (the GB spelling).
|
|
190
|
-
map "all_parts", to: :all_parts, render_default: false
|
|
191
196
|
end
|
|
192
197
|
|
|
193
198
|
# --- date serialized flat as year/month/day ---
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iec
|
|
5
|
+
module Identifiers
|
|
6
|
+
# Every part of one IEC document: "IEC 80000 (all parts)".
|
|
7
|
+
#
|
|
8
|
+
# The URN is the series URN: the document URN with "ser" in the
|
|
9
|
+
# deliverable slot.
|
|
10
|
+
class AllParts < ::Pubid::Iec::Identifier
|
|
11
|
+
include ::Pubid::AllParts
|
|
12
|
+
|
|
13
|
+
def to_urn
|
|
14
|
+
::Pubid::Iec::UrnGenerator.new(identity).generate_series
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Pubid
|
|
4
4
|
module Iec
|
|
5
5
|
module Identifiers
|
|
6
|
+
autoload :AllParts, "#{__dir__}/identifiers/all_parts"
|
|
6
7
|
autoload :Amendment, "#{__dir__}/identifiers/amendment"
|
|
7
8
|
autoload :Base, "#{__dir__}/identifiers/base"
|
|
8
9
|
autoload :ComponentSpecification, "#{__dir__}/identifiers/component_specification"
|
data/lib/pubid/iec/renderer.rb
CHANGED
|
@@ -43,6 +43,14 @@ module Pubid
|
|
|
43
43
|
"InterpretationSheet" => "ish",
|
|
44
44
|
}.freeze
|
|
45
45
|
|
|
46
|
+
# The series URN of the document: "ser" takes the deliverable slot.
|
|
47
|
+
# {Identifiers::AllParts} calls it with the document, because the
|
|
48
|
+
# document itself carries no all-parts mark.
|
|
49
|
+
def generate_series
|
|
50
|
+
@series = true
|
|
51
|
+
generate
|
|
52
|
+
end
|
|
53
|
+
|
|
46
54
|
def generate
|
|
47
55
|
doc = document
|
|
48
56
|
return nil if doc.nil?
|
|
@@ -146,7 +154,7 @@ module Pubid
|
|
|
146
154
|
# A deliverable code owns the slot; the edition takes it only when there
|
|
147
155
|
# is none.
|
|
148
156
|
def deliverable_slot
|
|
149
|
-
return "ser" if
|
|
157
|
+
return "ser" if @series
|
|
150
158
|
|
|
151
159
|
vap = vap_codes
|
|
152
160
|
return vap unless vap.empty?
|
data/lib/pubid/iec/urn_parser.rb
CHANGED
|
@@ -41,7 +41,6 @@ module Pubid
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
id = Pubid::Iec::Identifier.parse(code)
|
|
44
|
-
id.all_parts = true if all_parts && id.class.attributes.key?(:all_parts)
|
|
45
44
|
# The language slot joins codes with a hyphen ("en-fr"). Building one
|
|
46
45
|
# Language from the whole field gave a single bogus language that
|
|
47
46
|
# rendered as "(en-fr)".
|
|
@@ -50,7 +49,9 @@ module Pubid
|
|
|
50
49
|
::Pubid::Components::Language.new(code: code)
|
|
51
50
|
end
|
|
52
51
|
end
|
|
53
|
-
|
|
52
|
+
# The "ser" slot names every part of the document, so it wraps the
|
|
53
|
+
# document it just built.
|
|
54
|
+
all_parts ? id.to_all_parts : id
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
private
|