pubid 2.0.12 → 2.0.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/lib/pubid/cie/identifier.rb +4 -0
- data/lib/pubid/components/edition.rb +5 -0
- data/lib/pubid/components/language.rb +12 -0
- data/lib/pubid/csa/CLAUDE.md +6 -2
- data/lib/pubid/csa/identifier.rb +6 -0
- data/lib/pubid/csa/pubid_urn_schema.rb +5 -2
- data/lib/pubid/equality_ignored.rb +129 -0
- data/lib/pubid/evs/CLAUDE.md +5 -2
- data/lib/pubid/evs/identifier.rb +5 -0
- data/lib/pubid/evs/pubid_urn_schema.rb +3 -2
- data/lib/pubid/identifier.rb +2 -0
- data/lib/pubid/ieee/identifiers/base.rb +5 -0
- data/lib/pubid/itu/components/code.rb +6 -0
- data/lib/pubid/itu/identifiers/base.rb +4 -0
- data/lib/pubid/legacy_tree_value.rb +50 -0
- data/lib/pubid/nist/CLAUDE.md +19 -0
- data/lib/pubid/nist/components/edition.rb +6 -0
- data/lib/pubid/nist/identifiers/base.rb +62 -32
- data/lib/pubid/ogc/CLAUDE.md +10 -0
- data/lib/pubid/ogc/identifier.rb +11 -0
- data/lib/pubid/oiml/identifier.rb +4 -0
- data/lib/pubid/omg/CLAUDE.md +13 -0
- data/lib/pubid/omg/identifier.rb +22 -0
- data/lib/pubid/subset_match.rb +11 -3
- data/lib/pubid/tgpp/CLAUDE.md +23 -0
- data/lib/pubid/tgpp/identifier.rb +105 -0
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c7661b8db16717e43163b2e4cfde8d3f3a922589ac9d075dddecdc529526f2e
|
|
4
|
+
data.tar.gz: f7564b3039961555f1d54287638c4c3bec2e8efd9e3786e481be99c369c078fa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e54ac7d08d541dedb44056a36f256ef1fe113a8ae3d97a91d6bb23de145931aeb7dbfcdac5ec34445afcff1f811591321f2c4c37b9303859f68dbb40139e58b5
|
|
7
|
+
data.tar.gz: 0b87dd3af21db4708d6d9784a98f051fdfc338e3de106bd4982b508a95433f8b968caf005067febd01aa598420a89b070838ba5bcba39c017f21372ab35fb3b1
|
data/lib/pubid/cie/identifier.rb
CHANGED
|
@@ -12,6 +12,10 @@ module Pubid
|
|
|
12
12
|
# It defaults to "current" (the dominant value), so canonicalize_hash drops
|
|
13
13
|
# it from the majority of rows and from_hash restores it.
|
|
14
14
|
class Identifier < ::Pubid::Identifier
|
|
15
|
+
# Spelling-only values, the facets of the pubid-scheme URN: `==`,
|
|
16
|
+
# `hash` and `===` skip them (Pubid::EqualityIgnored, issue #656).
|
|
17
|
+
equality_ignored :s_prefix, :part_separator, :d_prefix
|
|
18
|
+
|
|
15
19
|
attribute :style, :string, default: -> { "current" }
|
|
16
20
|
|
|
17
21
|
def self.parse(input)
|
|
@@ -8,11 +8,16 @@ module Pubid
|
|
|
8
8
|
# URN render: "ed-#{number}" per RFC 5141-bis.
|
|
9
9
|
class Edition < Lutaml::Model::Serializable
|
|
10
10
|
include ::Pubid::SubsetMatch
|
|
11
|
+
include ::Pubid::EqualityIgnored
|
|
11
12
|
|
|
12
13
|
attribute :year, :string
|
|
13
14
|
attribute :number, Lutaml::Model::Type::Value
|
|
14
15
|
attribute :original_text, :string
|
|
15
16
|
|
|
17
|
+
# The input spelling ("Ed 13"): the `edsrc` facet of the pubid-scheme
|
|
18
|
+
# URN, so `==`, `hash` and `===` skip it (issue #656).
|
|
19
|
+
equality_ignored :original_text
|
|
20
|
+
|
|
16
21
|
def to_s
|
|
17
22
|
number_value = number.is_a?(Components::Code) ? number.value : number
|
|
18
23
|
number_value ? "ED#{number_value}" : nil
|
|
@@ -8,6 +8,7 @@ module Pubid
|
|
|
8
8
|
# URN render: lowercase ISO 639-1 code (RFC 5141-bis).
|
|
9
9
|
class Language < Lutaml::Model::Serializable
|
|
10
10
|
include ::Pubid::SubsetMatch
|
|
11
|
+
include ::Pubid::EqualityIgnored
|
|
11
12
|
|
|
12
13
|
CHAR_MAP = {
|
|
13
14
|
"R" => "ru",
|
|
@@ -21,6 +22,17 @@ module Pubid
|
|
|
21
22
|
attribute :code, :string, values: CHAR_MAP.values
|
|
22
23
|
attribute :original_code, :string
|
|
23
24
|
|
|
25
|
+
# The input spelling ("E" for "en"): the `langsrc` facet of the
|
|
26
|
+
# pubid-scheme URN, so `==`, `hash` and `===` skip it (issue #656).
|
|
27
|
+
equality_ignored :original_code
|
|
28
|
+
|
|
29
|
+
# An unmapped one-letter code ("X") is built with no +code+: then the
|
|
30
|
+
# spelling is the only value that holds the language, so it is
|
|
31
|
+
# identity and nothing is ignored.
|
|
32
|
+
def equality_ignored_attributes
|
|
33
|
+
code.nil? ? [] : super
|
|
34
|
+
end
|
|
35
|
+
|
|
24
36
|
def to_s(lang_single: false)
|
|
25
37
|
if !lang_single && original_code&.length == 1
|
|
26
38
|
code
|
data/lib/pubid/csa/CLAUDE.md
CHANGED
|
@@ -46,8 +46,12 @@ Moved from the root `CLAUDE.md`'s `#exclude` bullet (`metanorma__pubid__exclude-
|
|
|
46
46
|
(`[[urn-csa]]`). The spelling/format cluster of the year — `year_format`,
|
|
47
47
|
`year_prefix`, `publisher_prefix`, `has_publisher`,
|
|
48
48
|
`original_reaffirmation_4digit`, `original_year_4digit`, `separator`,
|
|
49
|
-
`series_prefix
|
|
50
|
-
|
|
49
|
+
`series_prefix` — is facets, so the same document printed two ways merges
|
|
50
|
+
only spellings; everything else is an atom. `french` was a facet when the
|
|
51
|
+
schema landed; it became the `french-` atom, because the French edition is
|
|
52
|
+
another publication and a facet is skipped by `==` and `===`
|
|
53
|
+
(`equality_ignored` on `Pubid::Csa::Identifier`). `#exclude(:year)` still
|
|
54
|
+
clears it with the rest of the year cluster. An adoption's
|
|
51
55
|
document goes along `base` and keeps its own flavor's schema (`CSA
|
|
52
56
|
ISO/IEC 8824-1:22` is
|
|
53
57
|
`urn:pubid:csa:iso.is:8824:co-IEC:part-1:year-2022:ca2`).
|
data/lib/pubid/csa/identifier.rb
CHANGED
|
@@ -10,6 +10,12 @@ module Pubid
|
|
|
10
10
|
# containers used to be separate Lutaml::Model types, which is what made
|
|
11
11
|
# `root.number` raise rather than return the relaton index key.
|
|
12
12
|
class Identifier < ::Pubid::Identifier
|
|
13
|
+
# Spelling-only values, the facets of the pubid-scheme URN: `==`,
|
|
14
|
+
# `hash` and `===` skip them (Pubid::EqualityIgnored, issue #656).
|
|
15
|
+
equality_ignored :year_format, :year_prefix, :publisher_prefix,
|
|
16
|
+
:has_publisher, :original_reaffirmation_4digit,
|
|
17
|
+
:original_year_4digit, :separator, :series_prefix
|
|
18
|
+
|
|
13
19
|
def self.parse(input)
|
|
14
20
|
unless input.is_a?(String)
|
|
15
21
|
raise Pubid::Errors::InvalidInputError,
|
|
@@ -10,7 +10,7 @@ module Pubid
|
|
|
10
10
|
# The current urn:csa:… form of #to_urn is pubid-invented and stays
|
|
11
11
|
# there. The spelling/format cluster of the CSA year (`year_format`,
|
|
12
12
|
# `year_prefix`, the publisher prefix it is printed with, the
|
|
13
|
-
# original-digit spellings of a reaffirmation
|
|
13
|
+
# original-digit spellings of a reaffirmation) is
|
|
14
14
|
# facets — the same document prints two ways; everything else is an
|
|
15
15
|
# atom. A combined identifier carries its members in `identifiers`, a
|
|
16
16
|
# bundled one in `bundled_with` (both bracketed lists); the adopted
|
|
@@ -31,6 +31,10 @@ module Pubid
|
|
|
31
31
|
atom "package_materials", "packagematerials"
|
|
32
32
|
atom "identifiers", "ids", kind: :ids
|
|
33
33
|
atom "bundled_with", "bundledwith", kind: :ids
|
|
34
|
+
# The French edition ("CSA B149.1:F20") is another publication than
|
|
35
|
+
# the English one, so it is identity, not a facet (issue #656). The
|
|
36
|
+
# "F" it is printed with stays the year_prefix facet.
|
|
37
|
+
atom "french", "french", kind: :bool
|
|
34
38
|
|
|
35
39
|
facet "year_format", "yearformat"
|
|
36
40
|
facet "year_prefix", "yearprefix"
|
|
@@ -40,7 +44,6 @@ module Pubid
|
|
|
40
44
|
facet "original_year_4digit", "origyear4", kind: :bool
|
|
41
45
|
facet "separator", "separator"
|
|
42
46
|
facet "series_prefix", "seriesprefix"
|
|
43
|
-
facet "french", "french", kind: :bool
|
|
44
47
|
|
|
45
48
|
type "pubid:csa:amendment", "amd"
|
|
46
49
|
type "pubid:csa:base", "base"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
# Attributes that are not part of an identifier's identity: `==`, `eql?`,
|
|
5
|
+
# `hash` and the subset match `===` skip them.
|
|
6
|
+
#
|
|
7
|
+
# The main case is a spelling-only value: the input wrote the same
|
|
8
|
+
# document in another way (`Rev. 5` or `r5`, `(E)` or `(en)`, a
|
|
9
|
+
# different separator). It renders the original spelling again, so
|
|
10
|
+
# `to_s` and `to_hash` keep it, but a search must find the document
|
|
11
|
+
# whatever the spelling. Every facet of the pubid-scheme URN (the
|
|
12
|
+
# trailing `f` group) is such a value; `spec/pubid/pubid_urn_contract_spec.rb`
|
|
13
|
+
# checks that an id whose facets are cut is still `==` and `===` to the
|
|
14
|
+
# original (issue #656).
|
|
15
|
+
#
|
|
16
|
+
# class Pubid::Components::Language < Lutaml::Model::Serializable
|
|
17
|
+
# include ::Pubid::EqualityIgnored
|
|
18
|
+
# equality_ignored :original_code
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# The declaration goes on the class that owns the attribute, and a
|
|
22
|
+
# subclass inherits it. A class that writes its own `==` must skip
|
|
23
|
+
# `equality_ignored_attributes` there too; the subset match reads the
|
|
24
|
+
# list itself (`SubsetMatch#subset_skipped_attributes`).
|
|
25
|
+
#
|
|
26
|
+
# Include it explicitly. There is no `inherited` or `prepend` hook, per
|
|
27
|
+
# the annotated-rendering lesson in CLAUDE.md.
|
|
28
|
+
module EqualityIgnored
|
|
29
|
+
def self.included(base)
|
|
30
|
+
base.extend(ClassMethods)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Class-level declaration.
|
|
34
|
+
module ClassMethods
|
|
35
|
+
# @param names [Array<Symbol>] attributes of this class
|
|
36
|
+
# @return [Array<Symbol>] the attributes this class itself declares
|
|
37
|
+
def equality_ignored(*names)
|
|
38
|
+
@equality_ignored_own = equality_ignored_own | names.map(&:to_sym)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Array<Symbol>] this class's declarations and every one it
|
|
42
|
+
# inherits
|
|
43
|
+
def equality_ignored_attributes
|
|
44
|
+
inherited = if superclass.respond_to?(:equality_ignored_attributes)
|
|
45
|
+
superclass.equality_ignored_attributes
|
|
46
|
+
else
|
|
47
|
+
[]
|
|
48
|
+
end
|
|
49
|
+
inherited | equality_ignored_own
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def equality_ignored_own
|
|
55
|
+
@equality_ignored_own ||= []
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A value with no identity left: a component whose every attribute is
|
|
60
|
+
# ignored or empty. "ISO 22610:2006 Ed" holds an Edition with only the
|
|
61
|
+
# spelling "Ed"; with the facets cut, the edition is nil. The two must
|
|
62
|
+
# compare equal, so `==` and `===` read such a value as nil.
|
|
63
|
+
#
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def self.identity_blank?(value)
|
|
66
|
+
value.respond_to?(:identity_blank?) && value.identity_blank?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The attributes this instance does not compare: the class's list. A
|
|
70
|
+
# class overrides it when a declared attribute carries identity in some
|
|
71
|
+
# instances (Components::Language: an unmapped one-letter code has no
|
|
72
|
+
# +code+, so its spelling is the language).
|
|
73
|
+
#
|
|
74
|
+
# @return [Array<Symbol>]
|
|
75
|
+
def equality_ignored_attributes
|
|
76
|
+
self.class.equality_ignored_attributes
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [Boolean] true when every attribute that is not ignored is
|
|
80
|
+
# nil or empty (never true for an instance that ignores nothing)
|
|
81
|
+
def identity_blank?
|
|
82
|
+
ignored = equality_ignored_attributes
|
|
83
|
+
return false if ignored.empty?
|
|
84
|
+
|
|
85
|
+
(self.class.attributes.keys - ignored).all? do |name|
|
|
86
|
+
SubsetMatch.blank?(public_send(name))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# lutaml's attribute walk, without the ignored attributes, and with a
|
|
91
|
+
# value that has no identity left read as nil.
|
|
92
|
+
def ==(other)
|
|
93
|
+
return true if equal?(other)
|
|
94
|
+
return false unless other.instance_of?(self.class)
|
|
95
|
+
|
|
96
|
+
# Only what both sides ignore, so the comparison stays symmetric.
|
|
97
|
+
ignored = equality_ignored_attributes & other.equality_ignored_attributes
|
|
98
|
+
self.class.attributes.each_key.all? do |name|
|
|
99
|
+
ignored.include?(name) || identity_equal?(name, other)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @return [Boolean] true when attribute +name+ of self and +other+ is
|
|
104
|
+
# the same, a value with no identity left read as nil
|
|
105
|
+
def identity_equal?(name, other)
|
|
106
|
+
EqualityIgnored.identity_value(public_send(name)) ==
|
|
107
|
+
EqualityIgnored.identity_value(other.public_send(name))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Object, nil] nil for a value with no identity left, else
|
|
111
|
+
# the value itself
|
|
112
|
+
def self.identity_value(value)
|
|
113
|
+
identity_blank?(value) ? nil : value
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The second parameter is lutaml's (see Pubid::Identifier#eql?).
|
|
117
|
+
def eql?(other, _compared_objects = nil)
|
|
118
|
+
self == other
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def hash
|
|
122
|
+
ignored = equality_ignored_attributes
|
|
123
|
+
return super if ignored.empty?
|
|
124
|
+
|
|
125
|
+
names = self.class.attributes.keys - ignored
|
|
126
|
+
[self.class, *names.map { |name| public_send(name) }].hash
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/pubid/evs/CLAUDE.md
CHANGED
|
@@ -60,8 +60,11 @@
|
|
|
60
60
|
## pubid-scheme URN (issue #670)
|
|
61
61
|
|
|
62
62
|
`lib/pubid/evs/pubid_urn_schema.rb`; rulings in `docs/URN-SCHEMES.adoc`
|
|
63
|
-
(`[[urn-evs]]`). The separators
|
|
64
|
-
amendment/corrigendum columns are atoms.
|
|
63
|
+
(`[[urn-evs]]`). The separators are facets; the draft marker and the
|
|
64
|
+
amendment/corrigendum columns are atoms. `draft_prefix` was a facet when the
|
|
65
|
+
schema landed; it became the `draftprefix-` atom because `prEVS-EN 100:2026`
|
|
66
|
+
is a draft, another document than the published standard, and a facet is
|
|
67
|
+
skipped by `==` and `===` (`equality_ignored` on `Pubid::Evs::Identifier`). An adoption of a European
|
|
65
68
|
document carries it along `base` and it keeps the CEN/CENELEC schema
|
|
66
69
|
(`EVS-EN 18216:2026` is
|
|
67
70
|
`urn:pubid:evs:cencenelec.en:18216:year-2026:na`); `intlorg-` covers an
|
data/lib/pubid/evs/identifier.rb
CHANGED
|
@@ -18,6 +18,11 @@ module Pubid
|
|
|
18
18
|
# ("+X" a full standard plus the supplement, "/X" the supplement only)
|
|
19
19
|
# and are preserved as captured.
|
|
20
20
|
class Identifier < ::Pubid::Identifier
|
|
21
|
+
# Spelling-only values, the facets of the pubid-scheme URN: `==`,
|
|
22
|
+
# `hash` and `===` skip them (Pubid::EqualityIgnored, issue #656).
|
|
23
|
+
equality_ignored :separator, :amd_separator, :cor_separator,
|
|
24
|
+
:na_separator
|
|
25
|
+
|
|
21
26
|
attribute :base, ::Pubid::Identifier, polymorphic: true
|
|
22
27
|
# "-" or " " — preserves the printed separator ("EVS-EN" vs "EVS EN")
|
|
23
28
|
attribute :separator, :string, default: -> { "-" }
|
|
@@ -18,13 +18,14 @@ module Pubid
|
|
|
18
18
|
atom "evs_type", "evstype"
|
|
19
19
|
atom "intl_org", "intlorg"
|
|
20
20
|
atom "na_year", "nayear"
|
|
21
|
-
|
|
21
|
+
# The "pr" of "prEVS-EN 100:2026": a draft is another document than
|
|
22
|
+
# the published standard, so it is identity, not a facet (issue #656).
|
|
23
|
+
atom "draft_prefix", "draftprefix", kind: :bool
|
|
22
24
|
|
|
23
25
|
facet "separator", "separator"
|
|
24
26
|
facet "amd_separator", "amdseparator"
|
|
25
27
|
facet "cor_separator", "corseparator"
|
|
26
28
|
facet "na_separator", "naseparator"
|
|
27
|
-
facet "draft_prefix", "draftprefix", kind: :bool
|
|
28
29
|
|
|
29
30
|
type "pubid:evs:estonian-standard", "es"
|
|
30
31
|
type "pubid:evs:identifier", "id"
|
data/lib/pubid/identifier.rb
CHANGED
|
@@ -4,6 +4,8 @@ module Pubid
|
|
|
4
4
|
class Identifier < Lutaml::Model::Serializable
|
|
5
5
|
# `reference === candidate`: a subset match (see Pubid::SubsetMatch).
|
|
6
6
|
include SubsetMatch
|
|
7
|
+
# Attributes that `==`, `hash` and `===` skip (see Pubid::EqualityIgnored).
|
|
8
|
+
include EqualityIgnored
|
|
7
9
|
|
|
8
10
|
# Components that serialize as a bare scalar when they carry only their
|
|
9
11
|
# single significant field: attribute name => the key the scalar is emitted
|
|
@@ -10,6 +10,11 @@ module Pubid
|
|
|
10
10
|
# Pubid::Ieee::Identifier. IEEE
|
|
11
11
|
# builds its identifiers as instances of this class directly.
|
|
12
12
|
class Identifier < ::Pubid::Identifier
|
|
13
|
+
# Spelling-only values, the facets of the pubid-scheme URN: `==`,
|
|
14
|
+
# `hash` and `===` skip them (Pubid::EqualityIgnored, issue #656).
|
|
15
|
+
equality_ignored :separator, :separators, :rev_sep, :year_sep,
|
|
16
|
+
:date_separator, :original_format
|
|
17
|
+
|
|
13
18
|
# Generate URN for this identifier
|
|
14
19
|
#
|
|
15
20
|
# @return [String] URN representation
|
|
@@ -21,6 +21,12 @@ module Pubid
|
|
|
21
21
|
# (dash-separated).
|
|
22
22
|
class Code < Lutaml::Model::Serializable
|
|
23
23
|
include ::Pubid::SubsetMatch
|
|
24
|
+
include ::Pubid::EqualityIgnored
|
|
25
|
+
|
|
26
|
+
# The spacing flags are spelling only (facets of the pubid-scheme
|
|
27
|
+
# URN): `==` below does not compare them, and `hash` and `===`
|
|
28
|
+
# skip them through this declaration (issue #656).
|
|
29
|
+
equality_ignored :series_suffix_spaced, :qualifier_glued
|
|
24
30
|
|
|
25
31
|
attribute :imp_marker, :string
|
|
26
32
|
attribute :number, :string
|
|
@@ -4,6 +4,10 @@ module Pubid
|
|
|
4
4
|
module Itu
|
|
5
5
|
# Base class for all ITU identifiers. Canonical name Pubid::Itu::Identifier.
|
|
6
6
|
class Identifier < ::Pubid::Identifier
|
|
7
|
+
# Spelling-only values, the facets of the pubid-scheme URN: `==`,
|
|
8
|
+
# `hash` and `===` skip them (Pubid::EqualityIgnored, issue #656).
|
|
9
|
+
equality_ignored :number_glued, :slash_joined
|
|
10
|
+
|
|
7
11
|
# Parse an ITU identifier string into an identifier object.
|
|
8
12
|
def self.parse(identifier)
|
|
9
13
|
unless identifier.is_a?(String)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
# The digit-node emission of the parsanol 1.3.111 edition-2 named-tree
|
|
5
|
+
# dialect: a grammar capture whose tree is a repetition of a `digit`
|
|
6
|
+
# sub-rule serialized as the node list alone, dropping every unnamed
|
|
7
|
+
# literal between the nodes (the dots of a version run, the " beta" of an
|
|
8
|
+
# OMG version tail). The relaton-data crawlers of that era wrote the
|
|
9
|
+
# emission into their published index rows, so those rows deserialize
|
|
10
|
+
# carrying the raw node list — as the Ruby inspect string
|
|
11
|
+
# `[{"digit" => "3"}, …]` renders (`[{digit: "3"}, {digit: "0"}, …]`) or,
|
|
12
|
+
# after a JSON/YAML hop, as a real array of one-key hashes.
|
|
13
|
+
#
|
|
14
|
+
# Rendered verbatim such a row prints the node hashes instead of the
|
|
15
|
+
# assembled number, and the print never re-parses (the relaton corpus
|
|
16
|
+
# gate's Law 1a). Each affected flavor assembles the digits back into its
|
|
17
|
+
# own scalar on deserialization; this module is the one definition of the
|
|
18
|
+
# emission's shape.
|
|
19
|
+
module LegacyTreeValue
|
|
20
|
+
# One `{digit: "N"}` node, in the Ruby-inspect shorthand (`digit: "3"`)
|
|
21
|
+
# or the hash-rocket spelling (`"digit" => "3"`), keys optionally quoted.
|
|
22
|
+
NODE_RE = /\{\s*["']?digit["']?\s*(?::|=>)\s*["']?(\d+)["']?\s*\}/
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# The digit values an emission carries: an Array of single-element
|
|
27
|
+
# strings ("3", "0", …), or nil when +value+ is not the emission. Accepts
|
|
28
|
+
# the inspect string and the real array of node hashes; any other shape
|
|
29
|
+
# (a plain "3.0.0", a nil, a hash) answers nil so callers leave it alone.
|
|
30
|
+
def digit_values(value)
|
|
31
|
+
case value
|
|
32
|
+
when ::String
|
|
33
|
+
return nil unless value.match?(/\A\[#{NODE_RE.source}(?:,\s*#{NODE_RE.source})*\]\z/)
|
|
34
|
+
|
|
35
|
+
value.scan(NODE_RE).flatten
|
|
36
|
+
when ::Array
|
|
37
|
+
return nil if value.empty?
|
|
38
|
+
|
|
39
|
+
nodes = value.filter_map do |node|
|
|
40
|
+
next unless node.is_a?(::Hash)
|
|
41
|
+
|
|
42
|
+
node["digit"] || node[:digit]
|
|
43
|
+
end
|
|
44
|
+
return nil unless nodes.size == value.size
|
|
45
|
+
|
|
46
|
+
nodes.map(&:to_s)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/pubid/nist/CLAUDE.md
CHANGED
|
@@ -93,3 +93,22 @@ Read this before you change `lib/pubid/nist/` or `spec/pubid/nist/`. The root
|
|
|
93
93
|
sweep of `full/` and the spec literals, so sweep those too when a schema
|
|
94
94
|
changes. Found in passing, not fixed:
|
|
95
95
|
`NIST.IR.8259Apt` parses as `NIST IR 8259(por)` and loses the `A`.
|
|
96
|
+
- **The from_hash shape residual (part letter suffix and SP subseries)**:
|
|
97
|
+
the parse path builds two shapes the stored rows lost on the way through
|
|
98
|
+
the wire, and the relaton corpus gate's Law 1b
|
|
99
|
+
(parse(render(x)).to_hash == x.to_hash) failed for every affected row
|
|
100
|
+
(run 42204581379, 16–22 NEW nist-structure per slice; the class the
|
|
101
|
+
relaton manifest describes as "the n+UPPER token's structural placement
|
|
102
|
+
differs between the parsed and deserialized forms"). (1) A `part` hash
|
|
103
|
+
without a `type` is the letter suffix — the parse of the printed face
|
|
104
|
+
("NIST NCSTAR 1A") carries `type: ""`; left nil the component fell back
|
|
105
|
+
to the annex face and rendered "1nA", a different document (the true
|
|
106
|
+
faces are in the relaton-data-nist document records: NIST NCSTAR 1A, NBS
|
|
107
|
+
HR 1-1A). Every type-less part value in the published index is a single
|
|
108
|
+
letter, so the inference is safe. This also repaired the three
|
|
109
|
+
nist-fidelity rows ("NIST SP 1075-NCNnR", "NIST SP 1113-BFRnL",
|
|
110
|
+
"NIST SP 1113-PERMInS"). (2) An SP row's `subseries` is derived metadata
|
|
111
|
+
("800" from "800-53", Appendix A.2) that parse always recomputes;
|
|
112
|
+
`from_hash` now derives it by the Builder's own condition and rules
|
|
113
|
+
(`derive_subseries_code`, sharing SP_SUBSERIES). Locked by
|
|
114
|
+
`spec/pubid/nist/from_hash_shapes_spec.rb`.
|
|
@@ -30,6 +30,12 @@ module Pubid
|
|
|
30
30
|
# Edition.new(type: "r", id: "5", original_prefix: " Rev. ").to_s # => "Rev. 5"
|
|
31
31
|
class Edition < Lutaml::Model::Serializable
|
|
32
32
|
include ::Pubid::SubsetMatch
|
|
33
|
+
include ::Pubid::EqualityIgnored
|
|
34
|
+
|
|
35
|
+
# The `edprefix` facet of the pubid-scheme URN. `==` and `hash`
|
|
36
|
+
# below skip it by hand; this declaration makes `===` skip it too
|
|
37
|
+
# (issue #656).
|
|
38
|
+
equality_ignored :original_prefix
|
|
33
39
|
|
|
34
40
|
attribute :type, :string # "-", "e", or "r"
|
|
35
41
|
attribute :id, :string # Edition ID (number or year)
|
|
@@ -36,7 +36,53 @@ module Pubid
|
|
|
36
36
|
# do the real work. Nested `base` documents are expanded recursively by
|
|
37
37
|
# their own from_hash via apply_mappings, so only this level is touched.
|
|
38
38
|
def self.from_hash(data, options = {})
|
|
39
|
-
super(expand_compact_hash(data),
|
|
39
|
+
id = super(normalize_deserialized_shapes(expand_compact_hash(data)),
|
|
40
|
+
options)
|
|
41
|
+
derive_subseries(id)
|
|
42
|
+
id
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The parse path builds a shape the stored rows lost on the way
|
|
46
|
+
# through the wire, and Law 1b (parse(render(x)).to_hash == x.to_hash)
|
|
47
|
+
# needs it restored on deserialization: a `part` hash without a `type`
|
|
48
|
+
# is the letter suffix — the parse of the printed face ("NIST NCSTAR
|
|
49
|
+
# 1A") carries `type: ""`, and Part#to_s renders just the letter for
|
|
50
|
+
# it; left nil the component fell back to the annex face ("1nA")
|
|
51
|
+
# instead, a different document. Every type-less value in the published
|
|
52
|
+
# index is a single letter, so the inference is safe.
|
|
53
|
+
def self.normalize_deserialized_shapes(data)
|
|
54
|
+
return data unless data.is_a?(::Hash)
|
|
55
|
+
|
|
56
|
+
part = data["part"]
|
|
57
|
+
return data unless part.is_a?(::Hash) && !part.key?("type") && !part.key?(:type)
|
|
58
|
+
|
|
59
|
+
out = data.dup
|
|
60
|
+
out[out.key?("part") ? "part" : :part] = { **part, "type" => "" }
|
|
61
|
+
out
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# An SP row's `subseries` is derived metadata ("800" from "800-53",
|
|
65
|
+
# Appendix A.2), computed by the Builder on parse but absent from rows
|
|
66
|
+
# written before that derivation existed — parse always recomputes it,
|
|
67
|
+
# so from_hash must too, by the Builder's own condition.
|
|
68
|
+
def self.derive_subseries(id)
|
|
69
|
+
return unless id.is_a?(Identifiers::SpecialPublication) && id.subseries.nil?
|
|
70
|
+
|
|
71
|
+
code = derive_subseries_code(id.number.to_s)
|
|
72
|
+
id.subseries = Components::Code.new(value: code) if code
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The SP subseries code of a report-num-like first number ("800-53" →
|
|
76
|
+
# "800", "1190GB-12" → "1190GB"), or nil. The same two rules the
|
|
77
|
+
# Builder applies on parse (Identifiers::SpecialPublication).
|
|
78
|
+
def self.derive_subseries_code(number)
|
|
79
|
+
first = number.to_s.split("-").first.to_s
|
|
80
|
+
if Identifiers::SpecialPublication::SP_SUBSERIES.include?(first)
|
|
81
|
+
first
|
|
82
|
+
elsif (m = first.match(/\A(\d+GB)\b/)) &&
|
|
83
|
+
Identifiers::SpecialPublication::SP_SUBSERIES.include?(m[1])
|
|
84
|
+
m[1]
|
|
85
|
+
end
|
|
40
86
|
end
|
|
41
87
|
|
|
42
88
|
# Expand a compact hash in place-safe fashion: a scalar series/number
|
|
@@ -203,38 +249,29 @@ module Pubid
|
|
|
203
249
|
# See lib/pubid/nist/builder.rb lines 368-472 for compound number logic
|
|
204
250
|
end
|
|
205
251
|
|
|
206
|
-
# Attributes that are
|
|
207
|
-
#
|
|
208
|
-
# valid spellings of the same id (e.g. long
|
|
252
|
+
# Attributes that are not part of an identifier's logical identity, so
|
|
253
|
+
# `==`, `hash` and `===` skip them (Pubid::EqualityIgnored). They
|
|
254
|
+
# diverge between equally-valid spellings of the same id (e.g. long
|
|
255
|
+
# "Rev. 1" vs short "r1"):
|
|
209
256
|
# - first_number/second_number: decomposed parts of the canonical
|
|
210
257
|
# :number, retained from the parse for building
|
|
211
|
-
# - parsed_format: records the input format for round-trip rendering
|
|
212
258
|
# - subseries: derived from series+number (which SP subseries the
|
|
213
259
|
# document belongs to); populated by the Builder for known SP
|
|
214
|
-
# subseries codes. Ignored
|
|
215
|
-
#
|
|
260
|
+
# subseries codes. Ignored so a manually-built id without
|
|
261
|
+
# subseries set still equals a parsed one. Derived, not spelled,
|
|
262
|
+
# so it stays an atom of the pubid-scheme URN.
|
|
263
|
+
# - parsed_format: records the input format for round-trip rendering
|
|
264
|
+
# `#weight` and the older `#matches?` read this list too.
|
|
216
265
|
EQUALITY_IGNORED_ATTRS = %i[
|
|
217
266
|
first_number second_number parsed_format subseries
|
|
218
267
|
].freeze
|
|
219
268
|
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
# its rendering-only original_prefix
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
self.class.attributes.each_key.all? do |name|
|
|
227
|
-
EQUALITY_IGNORED_ATTRS.include?(name) || public_send(name) == other.public_send(name)
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
alias eql? ==
|
|
232
|
-
|
|
233
|
-
# A subset match skips the same attributes: `to_hash` drops the build
|
|
234
|
-
# artifacts, so an index row rebuilt by `from_hash` never has them.
|
|
235
|
-
def self.subset_ignored_attributes
|
|
236
|
-
EQUALITY_IGNORED_ATTRS
|
|
237
|
-
end
|
|
269
|
+
# The list above, plus the other input spellings that are facets of
|
|
270
|
+
# the pubid-scheme URN (issue #656): publisher_was_parsed and
|
|
271
|
+
# translation. (Edition ignores its rendering-only original_prefix
|
|
272
|
+
# the same way.)
|
|
273
|
+
equality_ignored(*EQUALITY_IGNORED_ATTRS, :publisher_was_parsed,
|
|
274
|
+
:translation)
|
|
238
275
|
|
|
239
276
|
# `edition` is the primary edition/revision carrier and is already in
|
|
240
277
|
# the default %i[date year edition version] list. `update`/
|
|
@@ -254,13 +291,6 @@ module Pubid
|
|
|
254
291
|
super + %i[update update_component]
|
|
255
292
|
end
|
|
256
293
|
|
|
257
|
-
def hash
|
|
258
|
-
vals = self.class.attributes.each_key.reject do |name|
|
|
259
|
-
EQUALITY_IGNORED_ATTRS.include?(name)
|
|
260
|
-
end.map { |name| public_send(name) }
|
|
261
|
-
[self.class, *vals].hash
|
|
262
|
-
end
|
|
263
|
-
|
|
264
294
|
# Wildcard / partial-identifier match. Treats +self+ as a QUERY pattern
|
|
265
295
|
# and +candidate+ as a concrete document: every ID part SET on the query
|
|
266
296
|
# must equal the candidate's, while parts left unset (nil/empty) are
|
data/lib/pubid/ogc/CLAUDE.md
CHANGED
|
@@ -32,3 +32,13 @@ These notes were part of the root `CLAUDE.md`. Read them before you change `lib/
|
|
|
32
32
|
silently skips a name a class doesn't declare. Locked by
|
|
33
33
|
`spec/pubid/ogc/all_parts_spec.rb` and
|
|
34
34
|
`spec/pubid/all_parts_edition_keys_audit_spec.rb`.
|
|
35
|
+
- **`exclude(:year)` is a no-op**: the "<yy>" token is the document's core
|
|
36
|
+
identity — the grammar's printed form is `<yy>-<nnn>` and its parse demands
|
|
37
|
+
the year run — so excluding it cannot leave a valid identifier. The base
|
|
38
|
+
`#exclude` nilled the year and the renderer printed the dangling separator
|
|
39
|
+
(`"-000"`, `"-002r1"`), which never re-parsed: the relaton corpus gate's
|
|
40
|
+
114 ogc-fuzz failures (run 42204581379). An exclusion that would leave the
|
|
41
|
+
identifier unprintable is not a mutation, so `#exclude` drops `:year` from
|
|
42
|
+
its args — the same reasoning that keeps `year` out of
|
|
43
|
+
`all_parts_edition_keys`. Every other key behaves as the base does.
|
|
44
|
+
Locked by `spec/pubid/ogc/exclude_year_spec.rb`.
|
data/lib/pubid/ogc/identifier.rb
CHANGED
|
@@ -69,6 +69,17 @@ module Pubid
|
|
|
69
69
|
# from_hash is the shared polymorphic dispatch on Pubid::Identifier.
|
|
70
70
|
# OGC_TYPE_MAP remains as the key_value polymorphic_map.
|
|
71
71
|
|
|
72
|
+
# The "<yy>" token is the document's core identity: the grammar's
|
|
73
|
+
# printed form is `<yy>-<nnn>` and its parse demands the year run, so a
|
|
74
|
+
# year-less form ("-000") is not a reference any OGC grammar can read.
|
|
75
|
+
# An exclusion that would leave the identifier unprintable is not a
|
|
76
|
+
# mutation, so :year is a no-op here — the same reasoning that keeps it
|
|
77
|
+
# out of all_parts_edition_keys. Every other key behaves as the base
|
|
78
|
+
# #exclude does.
|
|
79
|
+
def exclude(*args)
|
|
80
|
+
super(*(args - [:year]))
|
|
81
|
+
end
|
|
82
|
+
|
|
72
83
|
# Parse an OGC identifier string into an identifier object.
|
|
73
84
|
# @param identifier [String] The OGC identifier string to parse
|
|
74
85
|
# @return [Identifier] The document identifier object
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
module Pubid
|
|
4
4
|
module Oiml
|
|
5
5
|
class Identifier < Pubid::Identifier
|
|
6
|
+
# Spelling-only values, the facets of the pubid-scheme URN: `==`,
|
|
7
|
+
# `hash` and `===` skip them (Pubid::EqualityIgnored, issue #656).
|
|
8
|
+
equality_ignored :parsed_format, :space_separator
|
|
9
|
+
|
|
6
10
|
# Maps each concrete identifier's polymorphic_name => class name, so
|
|
7
11
|
# key_value (de)serialization can re-instantiate the correct subclass
|
|
8
12
|
# from the `_type` discriminator. Kept in sync with the Identifiers
|
data/lib/pubid/omg/CLAUDE.md
CHANGED
|
@@ -13,3 +13,16 @@ These notes were part of the root `CLAUDE.md`. Read them before you change `lib/
|
|
|
13
13
|
- **`root.number` is nil for every OMG identifier, and so is the MR slug. Neither moved here, and nothing depends on either yet.** OMG models `acronym`/`version`/`part` and never sets the `number` it inherits, so the index key the root file requires of every leaf is empty and `to_mr_string` is `""` for every identifier — both were already true before this branch and both are unchanged by it (the base `mr_number_with_part` reads `number`, not `part`, so adding the part does not populate the slug). OMG has **no index and no `relaton-data-omg`** — it is scraped from `www.omg.org/spec` with Mechanize — and the relaton migration hand-off explicitly forbids adding one, so the binary-search degradation the root file describes cannot bite today. Do not "fix" it by mirroring `acronym` into `number` without checking the renderer first: that is the precondition the BIPM entry names, and the NIST attempt recorded in the root file is what happens when it does not hold.
|
|
14
14
|
- **The fixture corpus is hand-written.** OMG has no `spec/fixtures/omg/identifiers/full/` tree and no entry in `spec/fixtures/classify_fixtures.rb`, so `validation:classify` never rewrites `pass/` or `fail/` and the root file's rule against hand-editing them does not apply. `spec/pubid/omg/fixtures_spec.rb` skips `#` lines, so the files carry comments.
|
|
15
15
|
- **relaton note**: `relaton/relaton` moves its OMG flavor off the hand-written regex at `lib/relaton/omg/scraper.rb:19` onto this flavor. Both blockers it named are fixed. Its probe expected `OMG DDS 1.4/PDF` to round-trip byte for byte; it now sees a normalizing parse, which is harmless there because the flavor reads `acronym` and `version` off the parsed object and keeps the document part out of the request URL. (hand-offs: `metanorma__pubid__omg-document-part-and-bare-beta`, `relaton__relaton__omg-pubid-migration`.)
|
|
16
|
+
- **The legacy named-tree version emission (from_hash)**: the parsanol 1.3.111
|
|
17
|
+
era wrote five relaton-data-omg rows (the beta documents, e.g. DPROD
|
|
18
|
+
`version: '[{digit: "2"}]'` for "1.0 beta 2") with the parse tree's node
|
|
19
|
+
list in place of the version, because that emission drops every unnamed
|
|
20
|
+
literal and only the beta tail's `digit` nodes are named in the grammar's
|
|
21
|
+
version rule. The version body is lost information; the one faithful
|
|
22
|
+
recovery is the digits themselves (the IEEE builder's per-token join), so
|
|
23
|
+
`Identifier.from_hash` assembles the emission into the joined scalar and
|
|
24
|
+
the row renders "OMG DPROD 2" — a print that re-parses to the same
|
|
25
|
+
structure (the relaton corpus gate's omg-fidelity failures). Plain
|
|
26
|
+
"1.0 beta 2"-style scalars pass through untouched. The emission shape is
|
|
27
|
+
defined once in `Pubid::LegacyTreeValue`. Locked by
|
|
28
|
+
`spec/pubid/omg/legacy_tree_version_spec.rb`.
|
data/lib/pubid/omg/identifier.rb
CHANGED
|
@@ -61,6 +61,28 @@ module Pubid
|
|
|
61
61
|
parsed = Pubid::Parg::Model.bound(:omg, identifier)
|
|
62
62
|
Builder.build(parsed)
|
|
63
63
|
end
|
|
64
|
+
|
|
65
|
+
# A legacy named-tree version emission (see {Pubid::LegacyTreeValue}) —
|
|
66
|
+
# the parsanol 1.3.111 era wrote five relaton-data-omg rows (the beta
|
|
67
|
+
# documents, `version: '[{digit: "2"}]'` for "1.0 beta 2") with the
|
|
68
|
+
# node list in place of the version, because that emission drops every
|
|
69
|
+
# unnamed literal and only the beta tail's `digit` nodes are named in
|
|
70
|
+
# the grammar's version rule. The version body is lost information; the
|
|
71
|
+
# one faithful recovery is the digits themselves (the IEEE builder's
|
|
72
|
+
# per-token join), which re-parse as a version and keep the print
|
|
73
|
+
# stable. Current parses are unaffected — only those rows deserialize
|
|
74
|
+
# through here.
|
|
75
|
+
def self.from_hash(data, options = {})
|
|
76
|
+
return super unless data.is_a?(::Hash)
|
|
77
|
+
|
|
78
|
+
value = data["version"] || data[:version]
|
|
79
|
+
digits = LegacyTreeValue.digit_values(value)
|
|
80
|
+
return super unless digits
|
|
81
|
+
|
|
82
|
+
out = data.dup
|
|
83
|
+
out[data.key?("version") ? "version" : :version] = digits.join
|
|
84
|
+
super(out, options)
|
|
85
|
+
end
|
|
64
86
|
end
|
|
65
87
|
end
|
|
66
88
|
end
|
data/lib/pubid/subset_match.rb
CHANGED
|
@@ -37,7 +37,10 @@ module Pubid
|
|
|
37
37
|
# `part`, `parts`, `subpart` and `all_parts` itself are skipped.
|
|
38
38
|
# - A nested identifier or component that includes this module is compared
|
|
39
39
|
# with its own `===`. Any other value is compared with `==`.
|
|
40
|
-
# - `#==`
|
|
40
|
+
# - Subset match leaves `#==` as it is. An error from an attribute reader
|
|
41
|
+
# propagates.
|
|
42
|
+
# - An attribute declared with `equality_ignored` (Pubid::EqualityIgnored)
|
|
43
|
+
# is skipped, as `==` and `hash` skip it.
|
|
41
44
|
#
|
|
42
45
|
# The match walks the attributes of the objects, not their `to_hash`, so a
|
|
43
46
|
# class can change the rule for its own attributes:
|
|
@@ -120,6 +123,9 @@ module Pubid
|
|
|
120
123
|
# @return [Array<Symbol>] the attributes this match does not compare
|
|
121
124
|
def subset_skipped_attributes
|
|
122
125
|
ignored = self.class.subset_ignored_attributes
|
|
126
|
+
if respond_to?(:equality_ignored_attributes)
|
|
127
|
+
ignored |= equality_ignored_attributes
|
|
128
|
+
end
|
|
123
129
|
return ignored unless subset_all_parts_wildcard?
|
|
124
130
|
|
|
125
131
|
ignored | ALL_PARTS_ATTRIBUTES
|
|
@@ -189,9 +195,11 @@ module Pubid
|
|
|
189
195
|
mine.each_with_index.all? { |value, i| value_match?(value, theirs[i]) }
|
|
190
196
|
end
|
|
191
197
|
|
|
192
|
-
# @return [Boolean] true for nil
|
|
198
|
+
# @return [Boolean] true for nil, for an empty string or collection, and
|
|
199
|
+
# for a component with no identity left (EqualityIgnored.identity_blank?)
|
|
193
200
|
def self.blank?(value)
|
|
194
|
-
value.nil? || Lutaml::Model::Utils.empty?(value)
|
|
201
|
+
value.nil? || Lutaml::Model::Utils.empty?(value) ||
|
|
202
|
+
EqualityIgnored.identity_blank?(value)
|
|
195
203
|
end
|
|
196
204
|
end
|
|
197
205
|
end
|
data/lib/pubid/tgpp/CLAUDE.md
CHANGED
|
@@ -43,3 +43,26 @@ keeps `#matches?(other, ignore:)`.
|
|
|
43
43
|
matching side. relaton had two committed specs breaking on this.
|
|
44
44
|
|
|
45
45
|
- **The flavor key is `:"3gpp"`, not the derived `:tgpp` (pubid#644)**: `PrefixesSupport#prefix_flavor_key` derives the key from the Ruby constant's name (`Pubid::Tgpp` → `:tgpp`), but the external key is "3gpp" — the registry registration, the `pubid:3gpp:*` `_type` namespace and the `urn:3gpp:` URN namespace all spell it with the digit. The derived mismatch made `Pubid.prefix_flavors` attribute the "3GPP" prefix to a key **nothing is registered under**, so the router's routing table resolved `Registry.get(:tgpp)` to `nil`, `candidates_by_prefix`'s `compact` silently dropped the owner, and `Pubid.parse("3GPP TS 38.101")` — which `Pubid::Tgpp.parse` handles fine (the prefixed spelling is a normalizing parse; canonical render drops the publisher, `with_publisher: true` puts it back) — lost its prefix-owner fallback and raised. The fix is a one-line `prefix_flavor_key` override in `lib/pubid/tgpp.rb` returning `:"3gpp"`; `Pubid.prefix_flavors["3GPP"]` now reads `[:\"3gpp\"]` and `Registry.canonical_name(Pubid::Tgpp)` answers the real key. The registry-driven tripwire `spec/pubid/parse_routing_spec.rb` ("resolves every prefix table entry to a registered flavor module") is the guard that turns any future key/registry mismatch of this shape red at the table level.
|
|
46
|
+
|
|
47
|
+
## The legacy named-tree version emission (from_hash)
|
|
48
|
+
|
|
49
|
+
The parsanol 1.3.111 edition-2 emission serialized the grammar's per-digit
|
|
50
|
+
`version` tree as the node list alone — the unnamed "." literals never
|
|
51
|
+
reached the stored row — and the 3gpp crawler of that era wrote ~89,000
|
|
52
|
+
published `relaton-data-3gpp` rows with
|
|
53
|
+
`version: '[{digit: "3"}, {digit: "0"}, {digit: "0"}]'` where the document's
|
|
54
|
+
version is `3.0.0`. Rendered verbatim the row printed the node hashes and the
|
|
55
|
+
print never re-parsed (the relaton corpus gate's Law 1a; run 42204581379,
|
|
56
|
+
~22,000 NEW 3gpp fidelity failures). `Identifier.from_hash` now assembles the
|
|
57
|
+
digits back into the three-segment version (see `Pubid::LegacyTreeValue` for
|
|
58
|
+
the emission shape and `assemble_legacy_tree_version` for the rule): the
|
|
59
|
+
release names the version major for the legacy year-releases (REL-99 → 3.x.y)
|
|
60
|
+
and for `REL-<n>`; otherwise a two-digit leading pair wins when it is a
|
|
61
|
+
plausible major (≤ REL-21) and a leading zero is always the minor's. The
|
|
62
|
+
segmentation was validated against the index's own clean rows — the crawler
|
|
63
|
+
wrote both forms per document, 89,223 pairs, 165 irrecoverable (identical
|
|
64
|
+
digit tuples whose documents genuinely differ, e.g. `1.10.0` vs `11.0.0`
|
|
65
|
+
from `[1,1,0,0]`); the deterministic choice there still parses and
|
|
66
|
+
round-trips. Current parses are unaffected: the runtime binds `version` as a
|
|
67
|
+
scalar, so only deserialization of those legacy rows reaches the assembly.
|
|
68
|
+
Locked by `spec/pubid/tgpp/legacy_tree_version_spec.rb`.
|
|
@@ -116,6 +116,111 @@ module Pubid
|
|
|
116
116
|
parsed = Pubid::Parg::Model.bound(:tgpp, identifier)
|
|
117
117
|
Builder.build(parsed)
|
|
118
118
|
end
|
|
119
|
+
|
|
120
|
+
# The version run the grammar binds is `1*digit "." 1*digit "." 1*digit`,
|
|
121
|
+
# whose named-tree materialization is per-digit `digit` nodes joined by
|
|
122
|
+
# unnamed "." literals. The parsanol 1.3.111 edition-2 emission serialized
|
|
123
|
+
# that tree as the node list alone — the dots were unnamed, so they never
|
|
124
|
+
# reached the stored row, and ~89,000 published relaton-data-3gpp rows
|
|
125
|
+
# carry `version: '[{digit: "3"}, {digit: "0"}, {digit: "0"}]'` (the
|
|
126
|
+
# node list's inspect) where the document's version is `3.0.0`. Rendered
|
|
127
|
+
# verbatim the row prints the node hashes and the print never re-parses
|
|
128
|
+
# (the relaton corpus gate's Law 1a). Current parses are unaffected —
|
|
129
|
+
# only deserialization of those legacy rows needs the assembly.
|
|
130
|
+
#
|
|
131
|
+
# The dots are lost information, so the segmentation is a deterministic
|
|
132
|
+
# reconstruction, validated against the index's own clean rows (the
|
|
133
|
+
# crawler wrote both forms per document): the release names the version
|
|
134
|
+
# major for the legacy year-releases (REL-99 documents carry version
|
|
135
|
+
# 3.x.y) and for `REL-<n>`; otherwise a two-digit leading pair wins when
|
|
136
|
+
# it is a plausible major (the largest 3GPP release is REL-21) and a
|
|
137
|
+
# leading zero is always the minor's. The residue — identical digit
|
|
138
|
+
# tuples whose documents genuinely differ, e.g. `1.10.0` vs `11.0.0`
|
|
139
|
+
# from `[1,1,0,0]` — is unrecoverable from the row; any 3-segment choice
|
|
140
|
+
# still parses and round-trips.
|
|
141
|
+
LEGACY_RELEASE_MAJORS = {
|
|
142
|
+
# The 3GPP release numbering starts Release 1999 = version 3.x.y; the
|
|
143
|
+
# earlier year-releases continue the ETSI sequence backwards.
|
|
144
|
+
"REL-96" => "5", "REL-97" => "6", "REL-98" => "7", "REL-99" => "3",
|
|
145
|
+
"UMTS" => "3", "Ph1" => "3", "Ph2" => "4"
|
|
146
|
+
}.freeze
|
|
147
|
+
|
|
148
|
+
def self.from_hash(data, options = {})
|
|
149
|
+
super(normalize_legacy_tree_version(data), options)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Assemble a legacy named-tree version emission (see
|
|
153
|
+
# {Pubid::LegacyTreeValue}) back into the dotted scalar. Leaves every
|
|
154
|
+
# other value untouched; answers nil when +value+ is not the emission.
|
|
155
|
+
def self.assemble_legacy_tree_version(value, release)
|
|
156
|
+
digits = LegacyTreeValue.digit_values(value)
|
|
157
|
+
return nil unless digits
|
|
158
|
+
|
|
159
|
+
major = release_major(release)
|
|
160
|
+
if major
|
|
161
|
+
head = digits[0, major.length].join
|
|
162
|
+
rest = digits.drop(major.length)
|
|
163
|
+
if head == major && rest.length.between?(2, 4)
|
|
164
|
+
return assemble_version_segments(major, rest)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
assemble_version_segments(nil, digits)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# The version major a release row names, when the release spelling maps
|
|
172
|
+
# to one ("REL-18" → "18", the legacy year-releases per the table).
|
|
173
|
+
def self.release_major(release)
|
|
174
|
+
return nil unless release.is_a?(::String)
|
|
175
|
+
|
|
176
|
+
LEGACY_RELEASE_MAJORS.fetch(release) do
|
|
177
|
+
release.match?(/\AREL-\d+\z/) ? release.delete_prefix("REL-") : nil
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Split a digit run into the three version segments. +major+ pins the
|
|
182
|
+
# first segment when the release names it; the run then carries the
|
|
183
|
+
# minor and patch digits, which the observed corpus always splits as
|
|
184
|
+
# two-digit-minor (`18.1.0`), one-digit-minor/two-digit-patch
|
|
185
|
+
# (`8.1.10`-era rows do not occur) or singles (`3.0.0`).
|
|
186
|
+
def self.assemble_version_segments(major, rest)
|
|
187
|
+
if major
|
|
188
|
+
case rest.length
|
|
189
|
+
when 2 then "#{major}.#{rest[0]}.#{rest[1]}"
|
|
190
|
+
when 3 then "#{major}.#{rest[0]}#{rest[1]}.#{rest[2]}"
|
|
191
|
+
when 4 then "#{major}.#{rest[0]}#{rest[1]}.#{rest[2]}#{rest[3]}"
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
case rest.length
|
|
195
|
+
when 3 then rest.join(".")
|
|
196
|
+
when 4
|
|
197
|
+
pair = rest[0] + rest[1]
|
|
198
|
+
if rest[0] == "0"
|
|
199
|
+
"0.#{rest[1]}#{rest[2]}.#{rest[3]}"
|
|
200
|
+
elsif pair.to_i <= 21
|
|
201
|
+
"#{pair}.#{rest[2]}.#{rest[3]}"
|
|
202
|
+
else
|
|
203
|
+
"#{rest[0]}.#{rest[1]}#{rest[2]}.#{rest[3]}"
|
|
204
|
+
end
|
|
205
|
+
when 5 then "#{rest[0]}#{rest[1]}.#{rest[2]}.#{rest[3]}#{rest[4]}"
|
|
206
|
+
else rest.join(".")
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def self.normalize_legacy_tree_version(data)
|
|
212
|
+
return data unless data.is_a?(::Hash)
|
|
213
|
+
|
|
214
|
+
release = data["release"] || data[:release]
|
|
215
|
+
assembled = assemble_legacy_tree_version(
|
|
216
|
+
data["version"] || data[:version], release,
|
|
217
|
+
)
|
|
218
|
+
return data unless assembled
|
|
219
|
+
|
|
220
|
+
out = data.dup
|
|
221
|
+
out[data.key?("version") ? "version" : :version] = assembled
|
|
222
|
+
out
|
|
223
|
+
end
|
|
119
224
|
end
|
|
120
225
|
end
|
|
121
226
|
end
|
data/lib/pubid/version.rb
CHANGED
data/lib/pubid.rb
CHANGED
|
@@ -142,11 +142,13 @@ module Pubid
|
|
|
142
142
|
autoload :Errors, "pubid/errors"
|
|
143
143
|
autoload :Parser, "pubid/parser"
|
|
144
144
|
autoload :Components, "pubid/components"
|
|
145
|
+
autoload :LegacyTreeValue, "pubid/legacy_tree_value"
|
|
145
146
|
autoload :BundledIdentifier, "pubid/bundled_identifier"
|
|
146
147
|
autoload :AllParts, "pubid/all_parts"
|
|
147
148
|
autoload :AllPartsIdentifier, "pubid/all_parts_identifier"
|
|
148
149
|
autoload :Identifier, "pubid/identifier"
|
|
149
150
|
autoload :SubsetMatch, "pubid/subset_match"
|
|
151
|
+
autoload :EqualityIgnored, "pubid/equality_ignored"
|
|
150
152
|
autoload :IdentifierMetadata, "pubid/identifier_metadata"
|
|
151
153
|
autoload :Rendering, "pubid/rendering"
|
|
152
154
|
autoload :Renderers, "pubid/renderers"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pubid
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -476,6 +476,7 @@ files:
|
|
|
476
476
|
- lib/pubid/ecma/renderer.rb
|
|
477
477
|
- lib/pubid/ecma/urn_generator.rb
|
|
478
478
|
- lib/pubid/ecma/urn_parser.rb
|
|
479
|
+
- lib/pubid/equality_ignored.rb
|
|
479
480
|
- lib/pubid/errors.rb
|
|
480
481
|
- lib/pubid/etsi.rb
|
|
481
482
|
- lib/pubid/etsi/CLAUDE.md
|
|
@@ -842,6 +843,7 @@ files:
|
|
|
842
843
|
- lib/pubid/jis/supplement_identifier.rb
|
|
843
844
|
- lib/pubid/jis/urn_generator.rb
|
|
844
845
|
- lib/pubid/jis/urn_parser.rb
|
|
846
|
+
- lib/pubid/legacy_tree_value.rb
|
|
845
847
|
- lib/pubid/lutaml/no_store_registration.rb
|
|
846
848
|
- lib/pubid/nist.rb
|
|
847
849
|
- lib/pubid/nist/CLAUDE.md
|