pubid 2.0.11 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13576e1000016c57213a4a3414bd58e6a9992c94b745fa5cdf52d7883e38f559
4
- data.tar.gz: 3da18abb9b8c9db12828ce68b770d35c5f1468e99cf0f1fdcff1ce4d5c649cde
3
+ metadata.gz: 9c7661b8db16717e43163b2e4cfde8d3f3a922589ac9d075dddecdc529526f2e
4
+ data.tar.gz: f7564b3039961555f1d54287638c4c3bec2e8efd9e3786e481be99c369c078fa
5
5
  SHA512:
6
- metadata.gz: 4294d802dbad9f2d1d447673b2f10075f07eb8d766a63aec7d870da6e0f667a93be837aeca4790301c18c2b8d415c0b4924be2a593708b264aa2437673c374a0
7
- data.tar.gz: db9a36816efbddb560cf7bc2eeb3e61f251005ff8bdd7cb934593991bea31a5db6e2b28901eac267993adc880953b3efe80943656732b2a28a6ee705c710465d
6
+ metadata.gz: e54ac7d08d541dedb44056a36f256ef1fe113a8ae3d97a91d6bb23de145931aeb7dbfcdac5ec34445afcff1f811591321f2c4c37b9303859f68dbb40139e58b5
7
+ data.tar.gz: 0b87dd3af21db4708d6d9784a98f051fdfc338e3de106bd4982b508a95433f8b968caf005067febd01aa598420a89b070838ba5bcba39c017f21372ab35fb3b1
@@ -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
@@ -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`, `french` — is facets, so the same document printed two
50
- ways merges only spellings; everything else is an atom. An adoption's
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`).
@@ -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, the French marker) is
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
@@ -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 and the draft marker are facets; the
64
- amendment/corrigendum columns are atoms. An adoption of a European
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
@@ -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"
@@ -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
@@ -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), options)
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 build artifacts or rendering aliases, not part
207
- # of an identifier's logical identity. They diverge between equally-
208
- # valid spellings of the same id (e.g. long "Rev. 1" vs short "r1"):
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 in equality so a manually-built id
215
- # without subseries set still equals a parsed one.
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
- # Logical identity comparison: equal when every attribute except the
221
- # build artifacts/aliases above matches. (Edition#== already ignores
222
- # its rendering-only original_prefix.)
223
- def ==(other)
224
- return false unless other.instance_of?(self.class)
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
@@ -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`.
@@ -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
@@ -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`.
@@ -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
@@ -4,18 +4,28 @@ module Pubid
4
4
  module Sae
5
5
  # The pubid-scheme URN of SAE (issue #670; docs/URN-SCHEMES.adoc).
6
6
  #
7
- # SAE J3016_202104 -> urn:pubid:sae:id:3016:typeabbr-J:year-2021:month-4
7
+ # SAE J3016_202104 -> urn:pubid:sae:id:3016:typeabbr-J:year-2021:month-4
8
+ # SAE ARP4754A:2010 -> urn:pubid:sae:id:4754:typeabbr-ARP:rev-A:yearflat-2010
8
9
  #
9
10
  # The current urn:sae:… form of #to_urn is pubid-invented and stays
10
11
  # there. The type (`J`, `AS`, `ARP`, `AMS`) is an atom, because the
11
12
  # identifier type is one class carrying every series code; the date
12
13
  # fields are Integers.
14
+ #
15
+ # SAE's hash carries two date shapes (the canonical flattening keeps a
16
+ # year-alone date flat: pubid#360), so a year gets one prefix per shape
17
+ # — the ashrae ruling. `year-` stays the nested year+month form the
18
+ # first release emitted; `yearflat-` is the flat year-alone form the
19
+ # corpus round (pubid#669) added. A part is top-level in the hash
20
+ # (AMS-QQ-A-250/4B -> part-4).
13
21
  PubidUrnSchema = Pubid::PubidUrn.define("sae") do
14
22
  atom "type.abbr", "typeabbr"
15
23
  atom "revision", "rev"
16
24
  atom "amendment", "amd"
25
+ atom "part", "part"
17
26
  atom "date.year", "year", kind: :int
18
27
  atom "date.month", "month", kind: :int
28
+ atom "year", "yearflat"
19
29
 
20
30
  type "pubid:sae:identifier", "id"
21
31
  end
@@ -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
- # - `#==` does not change. An error from an attribute reader propagates.
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 and for an empty string or collection
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pubid
4
- VERSION = "2.0.11"
4
+ VERSION = "2.0.13"
5
5
  end
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.11
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-05 00:00:00.000000000 Z
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