pubid 2.0.19 → 2.0.21
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/data/iho/s4-reserved-specs.yaml +15 -0
- data/data/parg/bipm.json +1 -1
- data/data/parg/bipm.parg +46 -11
- data/data/parg/iho.json +1 -1
- data/data/parg/iho.parg +128 -11
- data/data/parg/plateau.json +1 -1
- data/lib/pubid/bipm/CLAUDE.md +6 -1
- data/lib/pubid/bipm/builder.rb +20 -1
- data/lib/pubid/bipm/renderer.rb +14 -2
- data/lib/pubid/bipm/urn_generator.rb +80 -14
- data/lib/pubid/bipm/urn_parser.rb +70 -6
- data/lib/pubid/ecma/CLAUDE.md +12 -0
- data/lib/pubid/ecma/identifier.rb +1 -2
- data/lib/pubid/ecma/parser.rb +49 -0
- data/lib/pubid/identifier.rb +4 -3
- data/lib/pubid/iec/CLAUDE.md +20 -11
- data/lib/pubid/iec/builder.rb +19 -1
- data/lib/pubid/iec/identifier.rb +19 -70
- data/lib/pubid/iec/identifiers/fragment_identifier.rb +3 -3
- data/lib/pubid/iec/identifiers/test_report_form.rb +29 -0
- data/lib/pubid/iec/identifiers/working_document.rb +23 -6
- data/lib/pubid/iec/pubid_urn_schema.rb +15 -7
- data/lib/pubid/iec/renderer.rb +8 -4
- data/lib/pubid/iec/single_identifier.rb +69 -4
- data/lib/pubid/iec/supplement_identifier.rb +52 -76
- data/lib/pubid/iho/CLAUDE.md +52 -0
- data/lib/pubid/iho/builder.rb +105 -2
- data/lib/pubid/iho/identifiers/base.rb +52 -1
- data/lib/pubid/iho/pubid_urn_schema.rb +11 -4
- data/lib/pubid/iho/renderer.rb +39 -2
- data/lib/pubid/iho/reserved_specs.rb +54 -0
- data/lib/pubid/iho/urn_generator.rb +6 -0
- data/lib/pubid/iho/urn_parser.rb +50 -9
- data/lib/pubid/iho.rb +1 -0
- data/lib/pubid/nist/CLAUDE.md +11 -0
- data/lib/pubid/nist/preprocessor.rb +16 -0
- data/lib/pubid/plateau/CLAUDE.md +35 -0
- data/lib/pubid/plateau/builder.rb +4 -0
- data/lib/pubid/plateau/identifiers/handbook.rb +10 -2
- data/lib/pubid/plateau/pubid_urn_schema.rb +1 -0
- data/lib/pubid/plateau/renderer.rb +6 -1
- data/lib/pubid/sae/CLAUDE.md +11 -2
- data/lib/pubid/version.rb +1 -1
- metadata +18 -2
data/lib/pubid/iec/identifier.rb
CHANGED
|
@@ -10,15 +10,15 @@ module Pubid
|
|
|
10
10
|
# Iso::BundledIdentifier.) Fragment keeps its own stage/edition, so it
|
|
11
11
|
# suppresses a subset inline instead of including this.
|
|
12
12
|
#
|
|
13
|
-
# number/part/subpart
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
13
|
+
# number/part/subpart and the date keys (year/month/day/undated) are NOT
|
|
14
|
+
# here, and deliberately so: their maps live on SingleIdentifier, which
|
|
15
|
+
# the wrappers do not inherit from, so the exclusion is structural rather
|
|
16
|
+
# than a denylist that can drift. (The denylist did drift: it once listed
|
|
17
|
+
# `year` but not `month`/`day`/`undated`, and those three leaked a
|
|
18
|
+
# duplicated top-level key on every wrapper.) Only publisher,
|
|
19
|
+
# copublishers and stage stay here, because those maps are still wanted
|
|
20
|
+
# on the parent block.
|
|
20
21
|
module DelegatedFieldSuppression
|
|
21
|
-
def year_to_kv(_model, _doc); end
|
|
22
22
|
def publisher_to_kv(_model, _doc); end
|
|
23
23
|
def copublishers_to_kv(_model, _doc); end
|
|
24
24
|
def stage_to_kv(_model, _doc); end
|
|
@@ -165,7 +165,7 @@ module Pubid
|
|
|
165
165
|
key_value do
|
|
166
166
|
map "_type", to: :_type, polymorphic_map: IEC_TYPE_MAP
|
|
167
167
|
# number/part/subpart are NOT mapped here — they live on
|
|
168
|
-
# SingleIdentifier, the seam that excludes the
|
|
168
|
+
# SingleIdentifier, the seam that excludes the delegating wrappers.
|
|
169
169
|
# See the comment on that block.
|
|
170
170
|
#
|
|
171
171
|
# stage_iteration is a degenerate Components::Iteration (one :string
|
|
@@ -173,13 +173,9 @@ module Pubid
|
|
|
173
173
|
# ::Pubid::Identifier flattens it to a bare scalar and inflates it back.
|
|
174
174
|
# A plain map is enough; no converter.
|
|
175
175
|
map "stage_iteration", to: :stage_iteration, render_default: false
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
map "day", with: { to: :day_to_kv, from: :day_from_kv }
|
|
180
|
-
# IEC undated reference (e.g. IEC 60050:--). The `false` default is
|
|
181
|
-
# omitted; only the meaningful `true` round-trips via to_hash.
|
|
182
|
-
map "undated", with: { to: :undated_to_kv, from: :undated_from_kv }
|
|
176
|
+
# year/month/day/undated are NOT mapped here either — they live on
|
|
177
|
+
# SingleIdentifier for the same reason as number/part/subpart: a
|
|
178
|
+
# wrapper delegates `date` to what it wraps.
|
|
183
179
|
map "edition", to: :edition, render_default: false
|
|
184
180
|
map "languages", to: :languages, render_default: false
|
|
185
181
|
# publisher emitted only when the primary isn't the IEC default;
|
|
@@ -195,59 +191,6 @@ module Pubid
|
|
|
195
191
|
# already drops, so a plain map suffices (the GB spelling).
|
|
196
192
|
end
|
|
197
193
|
|
|
198
|
-
# --- date serialized flat as year/month/day ---
|
|
199
|
-
def year_to_kv(model, doc)
|
|
200
|
-
y = model.date&.year
|
|
201
|
-
return if y.nil? || y.to_s.empty?
|
|
202
|
-
|
|
203
|
-
doc.add_child(Lutaml::KeyValue::DataModel::Element.new("year", y.to_s))
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def year_from_kv(model, value)
|
|
207
|
-
return if value.nil? || value.to_s.empty?
|
|
208
|
-
|
|
209
|
-
(model.date ||= ::Pubid::Components::Date.new).year = value.to_s
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def month_to_kv(model, doc)
|
|
213
|
-
m = model.date&.month
|
|
214
|
-
return if m.nil? || m.to_s.empty?
|
|
215
|
-
|
|
216
|
-
doc.add_child(Lutaml::KeyValue::DataModel::Element.new("month", m.to_s))
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def month_from_kv(model, value)
|
|
220
|
-
return if value.nil? || value.to_s.empty?
|
|
221
|
-
|
|
222
|
-
(model.date ||= ::Pubid::Components::Date.new).month = value.to_s
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def day_to_kv(model, doc)
|
|
226
|
-
d = model.date&.day
|
|
227
|
-
return if d.nil? || d.to_s.empty?
|
|
228
|
-
|
|
229
|
-
doc.add_child(Lutaml::KeyValue::DataModel::Element.new("day", d.to_s))
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def day_from_kv(model, value)
|
|
233
|
-
return if value.nil? || value.to_s.empty?
|
|
234
|
-
|
|
235
|
-
(model.date ||= ::Pubid::Components::Date.new).day = value.to_s
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
# --- date undated flag (IEC undated reference, e.g. IEC 60050:--) ---
|
|
239
|
-
# The `false` default is omitted; only the meaningful `true` round-trips
|
|
240
|
-
# via to_hash / from_hash.
|
|
241
|
-
def undated_to_kv(model, doc)
|
|
242
|
-
return unless model.date&.undated?
|
|
243
|
-
|
|
244
|
-
doc.add_child(Lutaml::KeyValue::DataModel::Element.new("undated", true))
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def undated_from_kv(model, value)
|
|
248
|
-
(model.date ||= ::Pubid::Components::Date.new).undated = value.to_s == "true"
|
|
249
|
-
end
|
|
250
|
-
|
|
251
194
|
# --- publisher: primary only when non-default; copublishers as a list ---
|
|
252
195
|
def publisher_to_kv(model, doc)
|
|
253
196
|
pub = model.publisher&.body
|
|
@@ -326,7 +269,13 @@ module Pubid
|
|
|
326
269
|
# Apply legacy update_codes normalization first, before any other preprocessing
|
|
327
270
|
normalized = Core::UpdateCodes.apply(string, :iec)
|
|
328
271
|
parsed = Pubid::Parg::Model.bound(:iec, Pubid::Iec::Parser.normalize_input(normalized))
|
|
329
|
-
Pubid::Iec::Builder.new.build(parsed)
|
|
272
|
+
id = Pubid::Iec::Builder.new.build(parsed)
|
|
273
|
+
# A standalone supplement stores its numbers in the layout from_hash
|
|
274
|
+
# rebuilds only after #ensure_base runs; run it here, so a fresh
|
|
275
|
+
# parse holds that layout before any reader runs
|
|
276
|
+
# (SupplementIdentifier#number).
|
|
277
|
+
id.ensure_base if id.is_a?(SupplementIdentifier)
|
|
278
|
+
id
|
|
330
279
|
end
|
|
331
280
|
|
|
332
281
|
# from_hash is the shared polymorphic dispatch on Pubid::Identifier.
|
|
@@ -25,9 +25,9 @@ module Pubid
|
|
|
25
25
|
map "fragment_number", to: :fragment_number, render_default: false
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# number/part/subpart need no no-op here:
|
|
29
|
-
# SingleIdentifier, which this class does not
|
|
30
|
-
|
|
28
|
+
# number/part/subpart and year/month/day/undated need no no-op here:
|
|
29
|
+
# their maps live on SingleIdentifier, which this class does not
|
|
30
|
+
# inherit from.
|
|
31
31
|
def publisher_to_kv(_model, _doc); end
|
|
32
32
|
def copublishers_to_kv(_model, _doc); end
|
|
33
33
|
|
|
@@ -11,6 +11,35 @@ module Pubid
|
|
|
11
11
|
attribute :cispr_identifier, Identifier, polymorphic: true, default: -> {
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
# The embedded CISPR document is part of the form's identity, so it
|
|
15
|
+
# serializes under its own key (issue #687 — before it, a
|
|
16
|
+
# CISPR-bearing TRF hashed to just its publisher and year and never
|
|
17
|
+
# matched after a from_hash round trip). The converter shape is
|
|
18
|
+
# VapIdentifier#base_to_kv / #base_from_kv: the nested identifier
|
|
19
|
+
# carries its own polymorphic `_type`, so to_hash embeds the nested
|
|
20
|
+
# hash and from_hash dispatches back through the type map.
|
|
21
|
+
key_value do
|
|
22
|
+
map "cispr_identifier",
|
|
23
|
+
with: { to: :cispr_identifier_to_kv,
|
|
24
|
+
from: :cispr_identifier_from_kv }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def cispr_identifier_to_kv(model, doc)
|
|
28
|
+
cispr = model.cispr_identifier
|
|
29
|
+
return unless cispr
|
|
30
|
+
|
|
31
|
+
doc.add_child(
|
|
32
|
+
Lutaml::KeyValue::DataModel::Element.new("cispr_identifier",
|
|
33
|
+
cispr.to_hash),
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def cispr_identifier_from_kv(model, value)
|
|
38
|
+
if value
|
|
39
|
+
model.cispr_identifier = ::Pubid::Iec::Identifier.from_hash(value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
14
43
|
# TRF has type abbreviation for parsing
|
|
15
44
|
TYPED_STAGES = [
|
|
16
45
|
Pubid::Components::TypedStage.new(
|
|
@@ -11,12 +11,28 @@ module Pubid
|
|
|
11
11
|
# Working Document: "100/3705(F)/FDIS"
|
|
12
12
|
class WorkingDocument < Base
|
|
13
13
|
attribute :technical_committee, :string, default: -> {}
|
|
14
|
-
attribute :wd_number, :string, default: -> {}
|
|
15
14
|
attribute :wd_language, :string, default: -> {}
|
|
16
15
|
attribute :wd_stage, :string, default: -> {}
|
|
17
16
|
attribute :wp_stage, :string, default: -> {}
|
|
18
17
|
attribute :wp_type, :string, default: -> {}
|
|
19
18
|
|
|
19
|
+
# The working document's identity attributes are plain strings, so no
|
|
20
|
+
# converter is needed: lutaml writes the scalar, and
|
|
21
|
+
# ::Pubid::Identifier#to_hash drops a nil (the `-> {}` defaults are
|
|
22
|
+
# nil). This is the serialization that issue #687 restored — before
|
|
23
|
+
# it, `to_hash` was `{"_type" => "pubid:iec:working-document"}` alone,
|
|
24
|
+
# all 2,318 working documents shared one hash and one URN
|
|
25
|
+
# (`urn:pubid:iec:wd`), and no row of this class ever matched after a
|
|
26
|
+
# from_hash round trip.
|
|
27
|
+
key_value do
|
|
28
|
+
map "technical_committee", to: :technical_committee,
|
|
29
|
+
render_default: false
|
|
30
|
+
map "wd_language", to: :wd_language, render_default: false
|
|
31
|
+
map "wd_stage", to: :wd_stage, render_default: false
|
|
32
|
+
map "wp_stage", to: :wp_stage, render_default: false
|
|
33
|
+
map "wp_type", to: :wp_type, render_default: false
|
|
34
|
+
end
|
|
35
|
+
|
|
20
36
|
# Working documents have no TYPED_STAGES - they use PROJECT_STAGES only
|
|
21
37
|
TYPED_STAGES = [].freeze
|
|
22
38
|
|
|
@@ -29,14 +45,15 @@ module Pubid
|
|
|
29
45
|
end
|
|
30
46
|
|
|
31
47
|
# A working document prints its committee, number, language and stage —
|
|
32
|
-
# `1/2457(F)/FDIS` — and
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# the
|
|
48
|
+
# `1/2457(F)/FDIS` — and the document number is the inherited
|
|
49
|
+
# `number` (issue #687: it used to sit in a parallel `wd_number`
|
|
50
|
+
# attribute that nothing serialized). `wd_stage` is the printed stage
|
|
51
|
+
# token ("FDIS"); the derived `#stage` reader covers only the PWI/PNW
|
|
52
|
+
# work-programme form and is nil here.
|
|
36
53
|
def annotation_tokens
|
|
37
54
|
super + [
|
|
38
55
|
[:technical_committee, "docnumber"],
|
|
39
|
-
[:
|
|
56
|
+
[:number, "docnumber"],
|
|
40
57
|
[:wd_language, "language"],
|
|
41
58
|
[:wd_stage, "stage"],
|
|
42
59
|
[:wp_stage, "stage"],
|
|
@@ -16,13 +16,15 @@ module Pubid
|
|
|
16
16
|
# published URNs are unchanged. `database` was a string atom before any
|
|
17
17
|
# URN carried it — the corpus holds only `true`, so it is a bool.
|
|
18
18
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# the
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# it
|
|
19
|
+
# The identity gaps this schema used to inherit are closed (issue #687):
|
|
20
|
+
# the working document's committee/number/language/stage and work-programme
|
|
21
|
+
# stage/type serialize (`tc-`, `wdlang-`, `wdstage-`, `wpstage-`,
|
|
22
|
+
# `wptype-`, with the document number in the positional core), and a
|
|
23
|
+
# TestReportForm's CISPR document rides `cispr-` as a nested id — so a
|
|
24
|
+
# working document URNs per document instead of the shared type-only
|
|
25
|
+
# `urn:pubid:iec:wd` it used to be. The new atoms sit after `vap-` and
|
|
26
|
+
# before `lang-`, per the invariant below, so every landed URN encodes
|
|
27
|
+
# identically. The official RFC 5141-style urn:iec:std: form stays
|
|
26
28
|
# on #to_urn.
|
|
27
29
|
PubidUrnSchema = Pubid::PubidUrn.define("iec") do
|
|
28
30
|
atom "publisher", "pub"
|
|
@@ -41,6 +43,12 @@ module Pubid
|
|
|
41
43
|
atom "sheet_number", "sheet"
|
|
42
44
|
atom "sheet_year", "sheetyear"
|
|
43
45
|
atom "vap", "vap", kind: :list
|
|
46
|
+
atom "technical_committee", "tc"
|
|
47
|
+
atom "wd_language", "wdlang"
|
|
48
|
+
atom "wd_stage", "wdstage"
|
|
49
|
+
atom "wp_stage", "wpstage"
|
|
50
|
+
atom "wp_type", "wptype"
|
|
51
|
+
atom "cispr_identifier", "cispr", kind: :id
|
|
44
52
|
atom "languages.*.code", "lang"
|
|
45
53
|
atom "identifiers", "ids", kind: :ids
|
|
46
54
|
|
data/lib/pubid/iec/renderer.rb
CHANGED
|
@@ -208,8 +208,10 @@ module Pubid
|
|
|
208
208
|
# International Standard, so rendering would not be a fixed point.
|
|
209
209
|
parts = []
|
|
210
210
|
parts << id.wp_stage
|
|
211
|
-
if id.wp_type && !id.wp_type.
|
|
212
|
-
|
|
211
|
+
if id.wp_type && !id.wp_type.empty?
|
|
212
|
+
# wp_type is stripped at the construction site (Builder#cast),
|
|
213
|
+
# so the stored value is what renders.
|
|
214
|
+
parts << " #{id.wp_type}"
|
|
213
215
|
end
|
|
214
216
|
|
|
215
217
|
if id.number
|
|
@@ -226,8 +228,10 @@ module Pubid
|
|
|
226
228
|
parts = []
|
|
227
229
|
parts << id.technical_committee if id.technical_committee
|
|
228
230
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
+
# The working document's own number is the inherited `number`
|
|
232
|
+
# (issue #687).
|
|
233
|
+
if id.number
|
|
234
|
+
num_part = id.number.to_s
|
|
231
235
|
num_part += "(#{id.wd_language})" if id.wd_language
|
|
232
236
|
parts << num_part
|
|
233
237
|
end
|
|
@@ -22,15 +22,80 @@ module Pubid
|
|
|
22
22
|
# their own (Identifiers::Base and SupplementIdentifier).
|
|
23
23
|
#
|
|
24
24
|
# That is a structural exclusion, not a denylist of no-op converters that
|
|
25
|
-
# a later key can be forgotten from
|
|
26
|
-
#
|
|
25
|
+
# a later key can be forgotten from. The date keys live here for the
|
|
26
|
+
# same reason: the wrappers delegate `date` too, and when the date maps
|
|
27
|
+
# sat on Iec::Identifier the denylist no-opped `year` but not
|
|
28
|
+
# `month`/`day`/`undated`, so `IEC CA 01:2025-10 CSV` emitted a second,
|
|
29
|
+
# top-level "month" beside the one inside "base".
|
|
27
30
|
#
|
|
28
|
-
#
|
|
29
|
-
# the scalar, and ::Pubid::Identifier#to_hash drops a nil.
|
|
31
|
+
# number/part/subpart are plain strings, so no converter is needed:
|
|
32
|
+
# lutaml writes the scalar, and ::Pubid::Identifier#to_hash drops a nil.
|
|
33
|
+
# The date keys keep converters, because they split one `date`
|
|
34
|
+
# attribute across four keys.
|
|
30
35
|
key_value do
|
|
31
36
|
map "number", to: :number
|
|
32
37
|
map "part", to: :part
|
|
33
38
|
map "subpart", to: :subpart
|
|
39
|
+
# date serialized flat as year/month/day, nils omitted.
|
|
40
|
+
map "year", with: { to: :year_to_kv, from: :year_from_kv }
|
|
41
|
+
map "month", with: { to: :month_to_kv, from: :month_from_kv }
|
|
42
|
+
map "day", with: { to: :day_to_kv, from: :day_from_kv }
|
|
43
|
+
# IEC undated reference (e.g. IEC 60050:--). The `false` default is
|
|
44
|
+
# omitted; only the meaningful `true` round-trips via to_hash.
|
|
45
|
+
map "undated", with: { to: :undated_to_kv, from: :undated_from_kv }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# --- date serialized flat as year/month/day ---
|
|
49
|
+
def year_to_kv(model, doc)
|
|
50
|
+
y = model.date&.year
|
|
51
|
+
return if y.nil? || y.to_s.empty?
|
|
52
|
+
|
|
53
|
+
doc.add_child(Lutaml::KeyValue::DataModel::Element.new("year", y.to_s))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def year_from_kv(model, value)
|
|
57
|
+
return if value.nil? || value.to_s.empty?
|
|
58
|
+
|
|
59
|
+
(model.date ||= ::Pubid::Components::Date.new).year = value.to_s
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def month_to_kv(model, doc)
|
|
63
|
+
m = model.date&.month
|
|
64
|
+
return if m.nil? || m.to_s.empty?
|
|
65
|
+
|
|
66
|
+
doc.add_child(Lutaml::KeyValue::DataModel::Element.new("month", m.to_s))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def month_from_kv(model, value)
|
|
70
|
+
return if value.nil? || value.to_s.empty?
|
|
71
|
+
|
|
72
|
+
(model.date ||= ::Pubid::Components::Date.new).month = value.to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def day_to_kv(model, doc)
|
|
76
|
+
d = model.date&.day
|
|
77
|
+
return if d.nil? || d.to_s.empty?
|
|
78
|
+
|
|
79
|
+
doc.add_child(Lutaml::KeyValue::DataModel::Element.new("day", d.to_s))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def day_from_kv(model, value)
|
|
83
|
+
return if value.nil? || value.to_s.empty?
|
|
84
|
+
|
|
85
|
+
(model.date ||= ::Pubid::Components::Date.new).day = value.to_s
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# --- date undated flag (IEC undated reference, e.g. IEC 60050:--) ---
|
|
89
|
+
# The `false` default is omitted; only the meaningful `true` round-trips
|
|
90
|
+
# via to_hash / from_hash.
|
|
91
|
+
def undated_to_kv(model, doc)
|
|
92
|
+
return unless model.date&.undated?
|
|
93
|
+
|
|
94
|
+
doc.add_child(Lutaml::KeyValue::DataModel::Element.new("undated", true))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def undated_from_kv(model, value)
|
|
98
|
+
(model.date ||= ::Pubid::Components::Date.new).undated = value.to_s == "true"
|
|
34
99
|
end
|
|
35
100
|
|
|
36
101
|
# Generate URN for this identifier
|
|
@@ -33,26 +33,11 @@ module Pubid
|
|
|
33
33
|
def base_from_kv(model, value)
|
|
34
34
|
return unless value
|
|
35
35
|
|
|
36
|
-
base
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
# is always a concrete type (InternationalStandard, etc.). Re-engage the
|
|
42
|
-
# synthetic-base state so the supplement renders in standalone form
|
|
43
|
-
# (publisher/TYPE base-number-supp-number) instead of attached form.
|
|
44
|
-
if base.instance_of?(::Pubid::Iec::SingleIdentifier)
|
|
45
|
-
model.mark_synthetic_standalone!
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Re-engage the standalone synthetic-base state on a deserialized
|
|
50
|
-
# supplement. At this point `number` already holds the supplement number
|
|
51
|
-
# (set from the "number" key before "base"), so preserve it as the
|
|
52
|
-
# supplement number and flag the synthetic base.
|
|
53
|
-
def mark_synthetic_standalone!
|
|
54
|
-
@synthetic_base = true
|
|
55
|
-
@supplement_number = @number
|
|
36
|
+
# A bare SingleIdentifier here is the synthetic base of a standalone
|
|
37
|
+
# supplement; #synthetic_base? reads that class, so nothing else needs
|
|
38
|
+
# restoring. "number" already holds the supplement number, which is
|
|
39
|
+
# what ensure_base stores on the parse path too.
|
|
40
|
+
model.base = ::Pubid::Iec::Identifier.from_hash(value)
|
|
56
41
|
end
|
|
57
42
|
|
|
58
43
|
# Override base getter to ensure it's always created for standalone supplements
|
|
@@ -60,29 +45,21 @@ module Pubid
|
|
|
60
45
|
@base || ensure_base
|
|
61
46
|
end
|
|
62
47
|
|
|
63
|
-
#
|
|
48
|
+
# A standalone supplement holds the grammar's layout (base number in
|
|
49
|
+
# `number`, supplement number in `part`) until #ensure_base moves it.
|
|
50
|
+
# Not every construction path goes through one builder branch ("IEC FDAM
|
|
51
|
+
# 60038-1" does not, nor a hand-built Amendment), so these readers make
|
|
52
|
+
# the move before they answer: the result never depends on which reader
|
|
53
|
+
# ran first, and #exclude, which copies through the readers, copies the
|
|
54
|
+
# moved layout. They only TRIGGER the move; they return the stored value.
|
|
64
55
|
def number
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# For standalone supplements, trigger base creation first
|
|
69
|
-
ensure_base if @synthetic_base.nil? && publisher && part
|
|
70
|
-
|
|
71
|
-
if @synthetic_base
|
|
72
|
-
@supplement_number || @number
|
|
73
|
-
else
|
|
74
|
-
super
|
|
75
|
-
end
|
|
56
|
+
settle_standalone_layout
|
|
57
|
+
super
|
|
76
58
|
end
|
|
77
59
|
|
|
78
|
-
# Override part getter for standalone supplements (part becomes nil after swap)
|
|
79
60
|
def part
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
nil
|
|
83
|
-
else
|
|
84
|
-
super
|
|
85
|
-
end
|
|
61
|
+
settle_standalone_layout
|
|
62
|
+
super
|
|
86
63
|
end
|
|
87
64
|
|
|
88
65
|
# Delegate publisher and copublishers to base whenever there is
|
|
@@ -102,59 +79,52 @@ module Pubid
|
|
|
102
79
|
base ? base.copublishers : @copublishers
|
|
103
80
|
end
|
|
104
81
|
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
82
|
+
# Synthesise the base of a supplement that names none, and return it.
|
|
83
|
+
#
|
|
84
|
+
# A standalone supplement ("IEC/FDAM 60038-1") arrives from the grammar
|
|
85
|
+
# with the BASE number in `number` and the SUPPLEMENT number in `part`.
|
|
86
|
+
# This moves the base number onto a bare SingleIdentifier base and stores
|
|
87
|
+
# the supplement number in `number`, so the attributes hold what they
|
|
88
|
+
# mean — the layout `from_hash` rebuilds from the serialized hash. A
|
|
89
|
+
# supplement with no part (a bare "+AMD1:2008" in a consolidation,
|
|
90
|
+
# "IEC/AMD 1:2017") gets a self-base whose number equals its own; its
|
|
91
|
+
# `number` is left as it is.
|
|
92
|
+
#
|
|
93
|
+
# The base deliberately gets NO typed_stage. A stage without a code is
|
|
94
|
+
# not serialized, so `from_hash` cannot restore one, and a stage set
|
|
95
|
+
# here alone made `parse != from_hash(to_hash)` — `==` compares `base`
|
|
96
|
+
# (`lib/pubid/iec/CLAUDE.md`).
|
|
108
97
|
def ensure_base
|
|
109
98
|
return @base if @base
|
|
110
99
|
|
|
111
|
-
#
|
|
100
|
+
# publisher/copublishers read through `base`; this guard makes them
|
|
101
|
+
# read the supplement's own value while the base is being built.
|
|
112
102
|
@ensuring_base = true
|
|
103
|
+
return nil unless publisher && (@number || @part)
|
|
113
104
|
|
|
114
|
-
# For standalone supplements like "IEC/FDAM 60038-1"
|
|
115
|
-
# The supplement number is in the `part` attribute, while `number` is the base number
|
|
116
|
-
# We need to create a synthetic base and use part as the supplement number
|
|
117
|
-
return nil unless publisher && (number || part)
|
|
118
|
-
|
|
119
|
-
# Capture original values BEFORE marking as synthetic
|
|
120
|
-
# (otherwise our getters will return swapped values)
|
|
121
|
-
base_number = number
|
|
122
|
-
supplement_number = part
|
|
123
|
-
|
|
124
|
-
# NOW mark that this is a synthetic base (after capturing values)
|
|
125
|
-
@synthetic_base = true
|
|
126
|
-
|
|
127
|
-
# Use SingleIdentifier which has typed_stage attribute
|
|
128
105
|
base = SingleIdentifier.new
|
|
129
106
|
base.publisher = publisher
|
|
130
|
-
base.number =
|
|
131
|
-
base.
|
|
132
|
-
base.subpart = subpart if subpart
|
|
107
|
+
base.number = @number if @number
|
|
108
|
+
base.subpart = @subpart if @subpart
|
|
133
109
|
base.date = date if date
|
|
134
110
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
type_code: :is,
|
|
140
|
-
stage_code: :undated,
|
|
141
|
-
)
|
|
111
|
+
if @part
|
|
112
|
+
@number = @part
|
|
113
|
+
@part = nil
|
|
114
|
+
end
|
|
142
115
|
|
|
143
116
|
@base = base
|
|
144
|
-
|
|
145
|
-
# Store the supplement number for getter override
|
|
146
|
-
# The original number is the base number (moved to base)
|
|
147
|
-
# The original part is the supplement number (what we want to return)
|
|
148
|
-
@supplement_number = supplement_number
|
|
149
|
-
|
|
150
|
-
@base
|
|
151
117
|
ensure
|
|
152
118
|
@ensuring_base = false
|
|
153
119
|
end
|
|
154
120
|
|
|
155
|
-
#
|
|
121
|
+
# True when the base is the bare SingleIdentifier that #ensure_base (or
|
|
122
|
+
# from_hash) synthesised for a standalone supplement; a real attached
|
|
123
|
+
# base is always a concrete type (InternationalStandard, SheetIdentifier
|
|
124
|
+
# …). Read from the base itself, so it survives #exclude, which copies
|
|
125
|
+
# `base` into a new object.
|
|
156
126
|
def synthetic_base?
|
|
157
|
-
|
|
127
|
+
base.instance_of?(::Pubid::Iec::SingleIdentifier)
|
|
158
128
|
end
|
|
159
129
|
|
|
160
130
|
def to_s(**opts)
|
|
@@ -173,6 +143,12 @@ module Pubid
|
|
|
173
143
|
|
|
174
144
|
segments.join(".")
|
|
175
145
|
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def settle_standalone_layout
|
|
150
|
+
ensure_base if @base.nil? && @part && !@ensuring_base
|
|
151
|
+
end
|
|
176
152
|
end
|
|
177
153
|
end
|
|
178
154
|
end
|
data/lib/pubid/iho/CLAUDE.md
CHANGED
|
@@ -96,6 +96,58 @@ The compact face (`compact: true`) folds the code-adjacent components
|
|
|
96
96
|
onto the code: supplement and part hyphenated, volume as the slash
|
|
97
97
|
chain — `IHO M-2-2`, `IHO S-100-2 5.0.0`, `IHO P-5/2024`.
|
|
98
98
|
|
|
99
|
+
## S-4 loose-leaf sections and specs (owner rulings, 2026-11-11)
|
|
100
|
+
|
|
101
|
+
The S-4 loose-leaf publication (Parts A/B/C → Sections 100-600, Part C
|
|
102
|
+
100-500 → Regulations/Specifications cited `<letter>-<hundreds><seq>`)
|
|
103
|
+
is modeled ONE COMPONENT DEEP, not two tiers: the spec number IS the
|
|
104
|
+
leaf. `B-401` binds the letter into `part` ("B") and the number into
|
|
105
|
+
`spec` ("401"); the SECTION (the hundreds digit) is DERIVED —
|
|
106
|
+
`#spec_section` (`B-401` → `"400"`) — never stored, never emitted, and
|
|
107
|
+
the `00` slot (B-400) IS the section-level citation, so `S-4 Part B,
|
|
108
|
+
Section 400` ≡ `IHO S-4 B-400` (one identity, two spellings).
|
|
109
|
+
|
|
110
|
+
| component | attribute | canonical face | parse-only forms |
|
|
111
|
+
|--------------|--------------|---------------------|-------------------------------------|
|
|
112
|
+
| spec | `spec` | `B-401` (with part) | the spelled section (", Section 400") |
|
|
113
|
+
| spec sub | `spec_sub` | `.1` | — |
|
|
114
|
+
| spec variant | `spec_variant` | `(I)` | — |
|
|
115
|
+
|
|
116
|
+
- **Faces**: canonical compact `IHO S-4 B-401` (dated:
|
|
117
|
+
`IHO S-4 4.10.0 B-401` — the edition precedes the citation, as it
|
|
118
|
+
precedes the part). The citation face REPLACES the Part face in the
|
|
119
|
+
renderer (`compact: true` is a no-op on it — it is already compact).
|
|
120
|
+
Subs and variants combine (`B-351.1(I)` is in the 4.9.0 text).
|
|
121
|
+
- **Three forms parse to one identity**: the anchored citation, the
|
|
122
|
+
LONG form `S-4 Part B, Section 400[, B-401]` (normalizing parse; the
|
|
123
|
+
spelled section becomes `spec` alone, and beside a citation it only
|
|
124
|
+
cross-checks the citation's derived section — "Section 500, B-401"
|
|
125
|
+
raises) and the BARE `B-401` (implicit S-4 anchor; B only — bare
|
|
126
|
+
C-x stays the Circular Letter series, and the 3-digit `[1-6]xx`
|
|
127
|
+
shape keeps the B Bibliographic series (B-12) on the main branch).
|
|
128
|
+
`IHO B-401` (publisher + bare) parses to the spec too.
|
|
129
|
+
- **The anchor is load-bearing**: a spec citation requires the S-4
|
|
130
|
+
anchor (series S number 4 — the builder raises otherwise), a
|
|
131
|
+
lettered Part (A/B/C, not a numbered part), and no component
|
|
132
|
+
editions or S-4-foreign components (supplement/annex/appendix/
|
|
133
|
+
volume/issue); the citation letter must agree with a Part-rule
|
|
134
|
+
capture.
|
|
135
|
+
- **Reserved numbers** (`B-170`, `B-230`, `B-305`, `B-323` — "NOT
|
|
136
|
+
CURRENTLY USED" in the Ed 4.9.0 and 4.10.0 contents) parse and
|
|
137
|
+
resolve AS RESERVED, never invalid: `#reserved?` answers from
|
|
138
|
+
`data/iho/s4-reserved-specs.yaml` (edition-keyed, provenance in the
|
|
139
|
+
file; an edition-less reference is reserved when ANY seeded edition
|
|
140
|
+
reserves the number). `lib/pubid/iho/reserved_specs.rb` is the
|
|
141
|
+
loader (`Pubid::Iho::ReservedSpecs`).
|
|
142
|
+
- **URN atoms**: the citation is a child of its part — the letter rides
|
|
143
|
+
the existing `part-` atom (case preserved: `part-B`, never the
|
|
144
|
+
lowercase `part-b` sketch), then `spec-`, `specsub-`, `specvar-`
|
|
145
|
+
(the `appsub-` naming convention; the `:sub-1`/`:variant-i` in the
|
|
146
|
+
ruling's sketch are these atoms). `urn:iho:s:4:ed-4.10.0:part-B:
|
|
147
|
+
spec-401`; the section is not emitted. No `ed-`/`year-` after a
|
|
148
|
+
spec node (UrnParseError — the spec rides its publication's
|
|
149
|
+
edition); `specsub-`/`specvar-` only after `spec-`.
|
|
150
|
+
|
|
99
151
|
## Identity semantics
|
|
100
152
|
|
|
101
153
|
- The components are identity-bearing in `==`/`matches?`; a bare
|