pubid 2.0.20 → 2.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +73 -31
- data/data/iec/legacy-notation.md +298 -0
- data/data/iho/s4-reserved-specs.yaml +15 -0
- data/data/iso/historic-schemes.md +368 -0
- data/data/parg/bipm.json +1 -1
- data/data/parg/bipm.parg +46 -11
- data/data/parg/iec.json +1 -1
- data/data/parg/iho.json +1 -1
- data/data/parg/iho.parg +128 -11
- data/data/parg/iso.json +1 -1
- data/data/parg/iso.parg +142 -2
- data/data/parg/plateau.json +1 -1
- data/data/parg/tables/iec_typed_stages.yaml +3 -0
- data/data/parg/tables/typed_stages_ballots.yaml +6 -0
- data/data/parg/tables/typed_stages_supplements.yaml +1 -0
- 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/iec/CLAUDE.md +9 -1
- data/lib/pubid/iec/builder.rb +47 -0
- data/lib/pubid/iec/identifier.rb +19 -70
- data/lib/pubid/iec/identifiers/amendment.rb +34 -0
- data/lib/pubid/iec/identifiers/corrigendum.rb +6 -1
- data/lib/pubid/iec/identifiers/fragment_identifier.rb +3 -3
- data/lib/pubid/iec/single_identifier.rb +69 -4
- data/lib/pubid/iec/supplement_identifier.rb +89 -83
- data/lib/pubid/iho/CLAUDE.md +54 -1
- 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 +12 -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/iso/CLAUDE.md +49 -0
- data/lib/pubid/iso/builder.rb +77 -0
- data/lib/pubid/iso/identifiers/amendment.rb +1 -1
- data/lib/pubid/iso/single_identifier.rb +7 -1
- data/lib/pubid/nist/CLAUDE.md +49 -0
- data/lib/pubid/nist/builder.rb +5 -0
- data/lib/pubid/nist/identifiers/base.rb +42 -1
- data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +44 -5
- data/lib/pubid/nist/preprocessor.rb +18 -0
- data/lib/pubid/nist/urn_parser.rb +1 -1
- 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 +7 -2
|
@@ -6,21 +6,52 @@ module Pubid
|
|
|
6
6
|
# fields (rather than reconstructing a printed string) because the printed
|
|
7
7
|
# forms carry surface detail — meeting ordinals, French wording — that the
|
|
8
8
|
# canonical URN intentionally omits.
|
|
9
|
+
#
|
|
10
|
+
# Two URN generations parse. The canonical face leads with the guide's
|
|
11
|
+
# `v1` version segment (see UrnGenerator for the per-family shapes). The
|
|
12
|
+
# pre-adoption face this flavor emitted for years (`urn:bipm:cipm:res:
|
|
13
|
+
# 101-1:2012`, `urn:bipm:si-brochure:e:9e:v3.01:2019/2024`, …) stays
|
|
14
|
+
# parseable as a documented parse-in alias of the same identity: the
|
|
15
|
+
# landed relaton-data-bipm records and any consumer-stored URN keep
|
|
16
|
+
# resolving while the re-crawl lags. The EMITTER is the new shape; an
|
|
17
|
+
# alias never renders back as itself, it rebuilds the identifier whose
|
|
18
|
+
# `to_urn` is the v1 face.
|
|
9
19
|
class UrnParser < Pubid::UrnParser::Base
|
|
10
20
|
def parse_urn(urn)
|
|
11
21
|
body = strip_namespace(urn)
|
|
12
22
|
parts = body.split(":", -1)
|
|
13
23
|
|
|
24
|
+
if parts.first == "v1"
|
|
25
|
+
parse_canonical(parts.drop(1))
|
|
26
|
+
else
|
|
27
|
+
parse_legacy(parts)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# The v1 canonical shapes, with the version segment stripped. The
|
|
34
|
+
# brochure family differs structurally (group lead, no language
|
|
35
|
+
# segment, stripped e/v), so it dispatches before the group-leading
|
|
36
|
+
# committee/meeting shape.
|
|
37
|
+
def parse_canonical(parts)
|
|
14
38
|
case parts.first
|
|
15
39
|
when "metrologia" then build_metrologia(parts)
|
|
16
|
-
when "
|
|
17
|
-
else build_group_leading(parts)
|
|
40
|
+
when "bipm" then build_si_brochure_canonical(parts.drop(2))
|
|
41
|
+
else build_group_leading(parts, canonical_type: true)
|
|
18
42
|
end
|
|
19
43
|
end
|
|
20
44
|
|
|
21
|
-
|
|
45
|
+
# The legacy landed shapes (no v1 segment), kept as parse-in aliases.
|
|
46
|
+
def parse_legacy(parts)
|
|
47
|
+
case parts.first
|
|
48
|
+
when "metrologia" then build_metrologia(parts)
|
|
49
|
+
when "si-brochure" then build_si_brochure(parts)
|
|
50
|
+
else build_group_leading(parts, canonical_type: false)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
22
53
|
|
|
23
|
-
def build_group_leading(parts)
|
|
54
|
+
def build_group_leading(parts, canonical_type:)
|
|
24
55
|
group = parts[0].upcase
|
|
25
56
|
if parts[1] == "meeting"
|
|
26
57
|
Identifiers::Meeting.new(
|
|
@@ -30,13 +61,25 @@ module Pubid
|
|
|
30
61
|
number, appendix, annex = split_appendix(parts[2])
|
|
31
62
|
Identifiers::CommitteeDocument.new(
|
|
32
63
|
group: group,
|
|
33
|
-
type_code: (parts[1]
|
|
64
|
+
type_code: type_code_from_segment(parts[1],
|
|
65
|
+
canonical_type: canonical_type),
|
|
34
66
|
number: (number.empty? ? nil : number), year: parts[3]&.to_i,
|
|
35
67
|
appendix: appendix, annex: annex,
|
|
36
68
|
)
|
|
37
69
|
end
|
|
38
70
|
end
|
|
39
71
|
|
|
72
|
+
# The legacy face carried the lowercase type abbreviation ("res"); the
|
|
73
|
+
# v1 face carries the lowercase English type name ("resolution"). Both
|
|
74
|
+
# resolve through TYPE_WORD_TO_CODE, so an empty segment (the bare MRA
|
|
75
|
+
# form) stays a nil type_code on either face.
|
|
76
|
+
def type_code_from_segment(segment, canonical_type:)
|
|
77
|
+
return nil if segment.nil? || segment.empty?
|
|
78
|
+
|
|
79
|
+
word = canonical_type ? segment.capitalize : segment.upcase
|
|
80
|
+
Identifier::TYPE_WORD_TO_CODE[word]
|
|
81
|
+
end
|
|
82
|
+
|
|
40
83
|
# Inverse of UrnGenerator#committee_number_segment: splits the folded
|
|
41
84
|
# "-a{appendix}[.{annex}]" tail back out of the identifier segment. A
|
|
42
85
|
# bare number is digits and dashes only, so the letter-prefixed tail is
|
|
@@ -57,10 +100,31 @@ module Pubid
|
|
|
57
100
|
)
|
|
58
101
|
end
|
|
59
102
|
|
|
103
|
+
# Legacy brochure face: `si-brochure:<lang>:<edition>:<version>:<years>`.
|
|
60
104
|
def build_si_brochure(parts)
|
|
61
105
|
Identifiers::SiBrochure.new(
|
|
62
106
|
number: presence(parts[2]), language: parts[1].to_s.upcase,
|
|
63
|
-
edition: parts[2], version: parts[3],
|
|
107
|
+
edition: presence(parts[2]), version: presence(parts[3]),
|
|
108
|
+
years: presence(parts[4])
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Canonical brochure face:
|
|
113
|
+
# `bipm:si-brochure:<edition-number>:<version-number>:<year>` — the
|
|
114
|
+
# guide strips the "e" off the edition and the "v" off the version and
|
|
115
|
+
# drops the language segment, so they are restored here; the year
|
|
116
|
+
# segment stores back into `years` verbatim, which makes the emission
|
|
117
|
+
# idempotent ("2024" re-emits "2024"). The legacy face's language and
|
|
118
|
+
# multi-year segments are gone on this face: the guide's URN is
|
|
119
|
+
# language-independent and names the latest year only.
|
|
120
|
+
def build_si_brochure_canonical(parts)
|
|
121
|
+
edition = presence(parts[0])
|
|
122
|
+
edition = "#{edition}e" if edition
|
|
123
|
+
version = presence(parts[1])
|
|
124
|
+
version = "v#{version}" if version
|
|
125
|
+
Identifiers::SiBrochure.new(
|
|
126
|
+
number: edition, edition: edition, version: version,
|
|
127
|
+
years: presence(parts[2])
|
|
64
128
|
)
|
|
65
129
|
end
|
|
66
130
|
|
data/lib/pubid/ecma/CLAUDE.md
CHANGED
|
@@ -33,3 +33,15 @@ keeps `#matches?(other, ignore:)`.
|
|
|
33
33
|
stripper (root `CLAUDE.md`, the `with_publisher:` bullet) leaves the string
|
|
34
34
|
alone, and the `@with_publisher` ivar and the renderer branch that read it
|
|
35
35
|
were deleted. XSF made the same change for `XEP 0001`.
|
|
36
|
+
|
|
37
|
+
- **Human edition spellings are pre-parse aliases (pubid#698).** The
|
|
38
|
+
grammar reads exactly one edition face — the glued " ed<N>" — so
|
|
39
|
+
`Parser.normalize_input` (wired into `Identifier.parse`, the
|
|
40
|
+
ashrae/cie ingestion precedent) rewrites the human faces onto it before
|
|
41
|
+
the artifact sees them: the case-insensitive cover form
|
|
42
|
+
("ECMA-262 Edition 5.1"), the dotted and spaced ed variants, and the
|
|
43
|
+
bare trailing edition ("ECMA 262 5.1"), which fires only behind an
|
|
44
|
+
already-present document number so the space-separator form ("ECMA 6")
|
|
45
|
+
keeps its number and a " vol<N>" is never rewritten. Every rule
|
|
46
|
+
rewrites INTO the canonical face, so a canonical string passes through
|
|
47
|
+
byte-identically and the index key stays "ECMA-262 ed5.1".
|
|
@@ -134,8 +134,7 @@ module Pubid
|
|
|
134
134
|
raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
-
identifier = ::Pubid.utf8_input(identifier)
|
|
138
|
-
|
|
137
|
+
identifier = Parser.normalize_input(::Pubid.utf8_input(identifier))
|
|
139
138
|
parsed = Pubid::Parg::Model.bound(:ecma, identifier)
|
|
140
139
|
Builder.build(parsed)
|
|
141
140
|
end
|
data/lib/pubid/ecma/parser.rb
CHANGED
|
@@ -18,7 +18,56 @@ module Pubid
|
|
|
18
18
|
# Each branch captures its number under a distinct key so the builder can
|
|
19
19
|
# pick the identifier class without a separate type token. Numbers are kept
|
|
20
20
|
# as strings to preserve any leading zeros.
|
|
21
|
+
#
|
|
22
|
+
# The grammar reads exactly one edition spelling — the glued " ed<N>" —
|
|
23
|
+
# while the SDO's own cover and the natural human spellings differ, so
|
|
24
|
+
# {Parser.normalize_input} rewrites those faces onto the glued token
|
|
25
|
+
# before the artifact sees them (pre-parse alias; the baked grammar is
|
|
26
|
+
# untouched). A canonical face passes through byte-identically.
|
|
21
27
|
class Parser
|
|
28
|
+
# The human edition spellings that alias the glued " ed<N>" token
|
|
29
|
+
# (pubid#698), in rewrite order. Each rule rewrites into the exact
|
|
30
|
+
# canonical face, so the parse (and therefore the render, the URN and
|
|
31
|
+
# the index key) is the one " ed<N>" already produces.
|
|
32
|
+
#
|
|
33
|
+
# ECMA-262 Edition 5.1 the document cover's own form
|
|
34
|
+
# ECMA-262 ed. 5.1 dotted, spaced
|
|
35
|
+
# ECMA-262 ed 5.1 spaced, undotted
|
|
36
|
+
# ECMA-262 ed.5.1 dotted, unspaced
|
|
37
|
+
# ECMA 262 5.1 bare trailing edition
|
|
38
|
+
#
|
|
39
|
+
# " ed5.1" (no separator) is the grammar's own face and is left alone;
|
|
40
|
+
# a " vol<N>" number is never rewritten — the bare-trailing rule fires
|
|
41
|
+
# only behind a document number's own digit, so "ECMA-269 ed3 vol2"
|
|
42
|
+
# and even the spaced "… vol 2" keep their volume.
|
|
43
|
+
EDITION_ALIASES = [
|
|
44
|
+
# "Edition <N>" starts mid-word, so the replacement carries no space
|
|
45
|
+
# of its own — the space before it is kept as-is.
|
|
46
|
+
[/\bedition[.\s]*(\d+(?:\.\d+)?)\b/i, 'ed\1'],
|
|
47
|
+
# "ed. <N>" / "ed <N>" / "ed.<N>": same mid-word shape. The glued
|
|
48
|
+
# "ed5.1" has no separator after "ed" and cannot match [.\s]+, so a
|
|
49
|
+
# canonical face passes through byte-identically.
|
|
50
|
+
[/\bed[.\s]+(\d+(?:\.\d+)?)\b/i, 'ed\1'],
|
|
51
|
+
# Bare trailing edition: fires only when a document number is already
|
|
52
|
+
# present (a digit directly before the space — "ECMA 262 5.1"), so the
|
|
53
|
+
# space-separator form itself ("ECMA 6") keeps its number: there the
|
|
54
|
+
# char before the space is "A", not a digit. The match consumes that
|
|
55
|
+
# one space and the replacement puts it back before the token. A
|
|
56
|
+
# glued "vol2" has no space, and a spaced "vol 2" has no digit before
|
|
57
|
+
# the space, so a volume number is never rewritten — the grammar's
|
|
58
|
+
# ed-then-vol order is untouched.
|
|
59
|
+
[/(\d) (\d+(?:\.\d+)?)\z/, '\1 ed\2'],
|
|
60
|
+
].freeze
|
|
61
|
+
|
|
62
|
+
# Pre-parse ingestion normalization: map the human edition spellings
|
|
63
|
+
# onto the glued token the grammar reads. Every rule rewrites INTO a
|
|
64
|
+
# canonical face, so a string the grammar already accepts is returned
|
|
65
|
+
# byte-identically and only new accepts are added.
|
|
66
|
+
def self.normalize_input(string)
|
|
67
|
+
EDITION_ALIASES.reduce(string) do |acc, (pattern, replacement)|
|
|
68
|
+
acc.gsub(pattern, replacement)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
22
71
|
end
|
|
23
72
|
end
|
|
24
73
|
end
|
data/lib/pubid/iec/CLAUDE.md
CHANGED
|
@@ -27,16 +27,24 @@ These notes were part of the root `CLAUDE.md`. Read them before you change `lib/
|
|
|
27
27
|
|
|
28
28
|
- **`number`/`part`/`subpart` are plain `:string`, `Iec::Components::Code` is deleted, and three of eleven `key_value` converters go with it.** The component was an empty box: over all **12,331** parseable pass fixtures the three were an `Iec::Components::Code` **9,975 / 5,422 / 2,416** times and **not one** populated `prefix`, `parts`, `part` or `subpart` inside it. The subclass existed solely for a `to_s` that prints `"#{prefix} #{value}"`; `Code.parse` was the only site that ever passed `prefix:` and it had **zero callers**, as did the `full_code` alias, and the class had zero references outside `identifier.rb` and its own file. So the subclass is deleted outright along with its `autoload`.
|
|
29
29
|
**This was a live defect, not tidiness, and it is the reason to prefer retyping over coercing.** The parse path produced a Code while the constructor path left a String — a `:string` assigned to a Code-typed attribute stays a String — so `Identifiers::InternationalStandard.new(number: "60068", part: "2", subpart: "28", year: "2008")` was **not `==`** `Pubid::Iec.parse("IEC 60068-2-28:2008")`, and `#matches?` is `exclude(*ignore) == other.exclude(*ignore)`, so every match between a hand-built reference and a parsed one silently returned false. `to_s`, `to_urn` **and** `to_hash` all agreed on both sides, which is exactly why nothing caught it: a relaton index lookup just returned nothing, with no error. `spec/pubid/attribute_construction_spec.rb` had even pinned the String half (`"leaves a scalar number uncoerced (IEC)"`) and asserted `to_s` and `to_hash` on the two paths — but never `==`, so the spec that was closest to the bug documented it as intended behaviour. **The lesson generalises: a spec comparing two construction paths must assert `==`, not only that they render and serialize alike.**
|
|
30
|
-
**`with:` was doing two jobs, and only one of them was the Code.** `emit_code`/`build_code` unwrapped and rewrapped the component — identity functions once the attribute is a String — but the converters were *also* the mechanism by which `DelegatedFieldSuppression` no-opped the delegated fields of the four wrapper types. **Placement replaces the denylist**: `number`/`part`/`subpart` now map on **`Pubid::Iec::SingleIdentifier`**, the parent of `Identifiers::Base` and `SupplementIdentifier` — the only two classes that own a number — while `FragmentIdentifier`, `ConsolidatedIdentifier`, `VapIdentifier` and `SheetIdentifier` inherit from `Iec::Identifier` **directly** and are therefore excluded structurally, not by remembering to add a no-op.
|
|
30
|
+
**`with:` was doing two jobs, and only one of them was the Code.** `emit_code`/`build_code` unwrapped and rewrapped the component — identity functions once the attribute is a String — but the converters were *also* the mechanism by which `DelegatedFieldSuppression` no-opped the delegated fields of the four wrapper types. **Placement replaces the denylist**: `number`/`part`/`subpart` now map on **`Pubid::Iec::SingleIdentifier`**, the parent of `Identifiers::Base` and `SupplementIdentifier` — the only two classes that own a number — while `FragmentIdentifier`, `ConsolidatedIdentifier`, `VapIdentifier` and `SheetIdentifier` inherit from `Iec::Identifier` **directly** and are therefore excluded structurally, not by remembering to add a no-op. The date keys followed later — see the date-keys bullet below.
|
|
31
31
|
**Two more converters were pure boilerplate.** `all_parts` is a Boolean attribute with `default: false` that `::Pubid::Identifier#to_hash` already drops, so it takes the plain GB spelling `map "all_parts", to: :all_parts, render_default: false`. `stage_iteration` holds a `Components::Iteration` — one `:string` field, `number`, whose `to_s`/`render` return it — so it joins the shared `FLAT_SCALAR_COMPONENTS`/`FLAT_SCALAR_FIELDS` tables on `::Pubid::Identifier` and takes a plain map too. **The blast radius of that shared change is exactly ISO and IEC**, the only two flavors that map the key, and both already emitted the bare scalar through their own converters — ISO keeps its converter, which the inflation guard defers to by design. Net: **12 converter methods and 2 helpers deleted**. The five groups that keep `with:` all earn it — `year`/`month`/`day` and `undated` split one `date` attribute across four keys, `publisher`/`copublishers` map a `Components::Publisher` to its `body` and omit the IEC default, and `stage` maps `typed_stage` to its code with class-scoped resolution on load. **The rule this leaves behind: a converter is for a custom class or for splitting one attribute across several keys; a primitive needs a plain map.**
|
|
32
32
|
**The shared splitter gained a hook rather than a special case.** `Builder::Base#parse_number_with_part(value, code_class:)` is used by ISO and IEC only; `code_class:` is now optional and the three constructions route through `wrap_number_component`, which returns the raw scalar when `code_class` is nil. ISO still passes `Pubid::Iso::Components::Code` and is untouched. This is the extension point the remaining tranches reuse.
|
|
33
33
|
**Verified by replaying a captured baseline, not by the suite alone.** Over all **12,331** IEC pass-fixture ids, `to_s`, `to_urn`, `to_mr_string`, `root.number`, the identifier class, the deep-key-sorted `to_hash`, the `from_hash(to_hash) == to_hash` flag and the `from_hash(to_hash) == id` flag are **byte-identical, 0 diffs in every field**. The verbatim `to_hash` differs for 9,950 rows and for **one reason only** — `number`/`part`/`subpart` moved to the end of the key order, because lutaml *appends* a subclass's `key_value` maps to the parent's; removing those three keys leaves the two key sequences identical for every row, and `Hash#==` ignores order, so **0 rows differ in any other way**. Over all **7,573** ISO pass-fixture ids every field including the verbatim hash is byte-identical, so neither shared-file change moved ISO. Against the published `relaton-data-iec` `index-v2.yaml` (**32,847** rows) `from_hash(row).to_hash == row` holds for 32,845 and `parse(from_hash(row).to_s) == from_hash(row)` for all 32,847, **both before and after** — the 2 misses are the pre-existing rows storing the old nested `edition: {number: …}` form. **No index migration follows**, and that is measured rather than assumed. Locked by `spec/pubid/iec/number_string_spec.rb` (structural tripwire over the whole class tree, the constructed-vs-parsed `==`, the wrapper hashes, and the absence of the deleted constant) and by IEC's entry in `spec/pubid/number_string_retype_spec.rb`.
|
|
34
34
|
**Known residue, deliberately unchanged:** after the map moved, `Pubid::Iec::Identifier.from_hash({"number" => "60038"})` — a hand-written hash with **no `_type`**, on the bare base class — silently drops the key instead of building an `Identifier` that holds it. That object is already degenerate (`to_s` on it raises `NoMethodError` for `publisher_portion`) and nothing in this repo or in `relaton-iec` constructs one. (hand-off: iec-number-string.)
|
|
35
35
|
|
|
36
|
+
- **The date keys map on `SingleIdentifier` too — the wrapper `month`/`day`/`undated` leak is closed.** The denylist that `number` escaped had drifted exactly as its failure mode predicts: `DelegatedFieldSuppression` (and `FragmentIdentifier`'s inline copy) no-opped `year_to_kv` but not `month_to_kv`/`day_to_kv`/`undated_to_kv`, so every wrapper whose wrapped document had a month or was undated emitted the key twice — `IEC CA 01:2025-10 CSV` → top-level `"month" => "10"` beside the one in `base`, and the pubid URN carried it as `…:vi:month-10:…`. Vap, Consolidated, Sheet and Fragment all leaked; `AllParts` did not. The fix is the same placement, not three more no-ops: the four date `map` lines and their eight converters moved from `Iec::Identifier` to `SingleIdentifier`, and `year_to_kv` left both denylists. It was safe because **no wrapper wants a top-level date key from `date`** — `SheetIdentifier`'s own `year` attribute serializes as `sheet_year`, a different key. The denylist now holds only `publisher`/`copublishers`/`stage`, which the parent block still needs. **Measured, not assumed:** replaying all 12,331 IEC pass fixtures against a `main` baseline, `to_s`, `to_urn`, `to_mr_string`, `to_pubid_urn`, `root.number`, the class, `to_hash` by `==` and the `from_hash(to_hash) == id` flag are identical for every id — the corpus holds **0** wrapper ids with a month, day or undated date, so no fixture moved. The verbatim key order moves for 7,761 dated rows (the date keys now come after the parent block, as `number` did); `Hash#==` ignores order and the pubid URN codec uses schema order. A local copy of `relaton-data-iec/index-v2.yaml` (32,634 rows) holds 0 `month`/`day`/`undated` keys at any depth, so **no index regeneration follows**; a legacy row that still carries a leaked key deserializes to the same identifier (the stray key is ignored). Locked by `spec/pubid/iec/wrapper_date_keys_spec.rb` (structural tripwire per wrapper class, month/day/undated cases across the four wrappers, both round trips, the legacy row). (hand-off: iec-wrapper-month-day-leak.)
|
|
37
|
+
|
|
36
38
|
**Two parent-slot gaps recorded by the cross-flavor `base` survey** (`lib/pubid/bsi/CLAUDE.md` has the measured version). `Identifiers::TestReportForm#cispr_identifier` is a polymorphic `Identifier` parent slot with no `#root` override, so `root` returns the form itself — ratified as DELIBERATE by #687 (a TRF is a form about the CISPR document, not a wrapper of it; see the unserialized-identity bullet). And `Identifiers::ConsolidatedIdentifier#base_document` returns `identifiers&.first` **without** the recursive `&.base_document` that its BSI, CSA and CEN counterparts use — a one-level peel where the others peel fully, so a consolidated edition of a supplement does not reduce to the bare standard. Neither is chased here; both are pinned by nothing, which is why they are written down.
|
|
37
39
|
|
|
38
40
|
**Stage codes — the SSOT and what is settled.** The authoritative source for IEC stage abbreviations is committed in this repo: **`data/iec/stage-codes.md`** — the 52-row IEC/ISO stage-code correspondence table transcribed from IEC's official *IEC/ISO Harmonized APIs* documents (v0.0.8 2023-06, api-portal.iec.ch; v0.0.7 2021-05, iec-apiportal.apigee.io — the successors that replace the IEC Data Feeds), the legacy Data-Feeds field examples, and the IS-lifecycle ladder from `StageCodes_IS_v2.pdf` (2023-06). `www.iec.ch` itself is fully WAF-gated to non-browsers and the Wayback Machine holds no snapshots of its PDFs, so the extract — not the unreachable original — is the reference. **`Pubid::Iec.locate_stage` reads `TYPED_STAGES` only** (`lib/pubid/iec.rb`); `PROJECT_STAGES` (AFDIS, TCDV, PRVC…) are not queryable through it. Every one of metanorma-iec's 55 `STAGE_CODES` — the list quoted in pubid#237 — resolves, locked by `spec/pubid/iec/typed_stages_spec.rb` in the issue's own order; aliases are always **appended, never prepended**, so `abbr.first` (what renders) is unchanged. Notable rulings from that pass: **ADIS** is the legacy Data-Feeds spelling of AFDIS ("Approved for FDIS circulation", ISO_LEVEL 40.99) and is an alias on `cdv` (the typed stage claiming 40.99); **CDISH/CDPAS** are the 50.20 rows of the DISH/DPAS deliverables, not "committee draft ISH/PAS" (30.xx); **Reconciliation items recorded in the extract, deliberately not applied:** DELPUB sits at 99.60 there while the `is` ladder does not claim 99.60; CAN carries two codes (30.98 IS/TR/TS, 50.98 PAS/ISH/COR); the rows CDISH-table-siblings DECDISH/MERGED/SPLIT/PCC/PRVN/DEL/RPUB/TPUB and the per-deliverable translation/preparation families are not registered as typed-stage abbreviations.
|
|
39
41
|
|
|
42
|
+
- **A standalone supplement is `==` its `from_hash(to_hash)` round trip — the cause was the synthetic base's stage, not the ivar swap.** A supplement written without its base (`IEC/FDAM 60038-1`, `IEC/DCOR 60038-1`, `IEC/DISH 60038-1`) gets a bare `Pubid::Iec::SingleIdentifier` base from `SupplementIdentifier#ensure_base`; `to_s`, `to_urn`, `to_hash` and `#hash` agreed on both paths, but `==` was false, so `#matches?` failed silently. **The hand-off blamed the swapped `@number`/`@part` ivars; that was wrong.** `EqualityIgnored#==` compares through `public_send`, so the reader overrides hid the swap from it. The real difference was `base.typed_stage`: `ensure_base` set an IS/undated `TypedStage` with no `code`, which `stage_to_kv` cannot serialize, so `from_hash` rebuilt the base with none. **Diagnose an `==` failure by diffing the attributes through their readers, recursively, before trusting a hypothesis about ivars.** The stage write is deleted (the JCGM "delete the write" shape); the one visible change is `root.to_s` of the synthetic base on the parse path (`IEC IS60038` → `IEC60038`, the value `from_hash` already gave). The ivar swap was cleaned up too, because it was a hazard: `ensure_base` now moves the supplement number from `@part` into `@number`, so the stored layout is what `from_hash` rebuilds, and the `#number`/`#part` overrides, `@supplement_number`, `@synthetic_base` and `mark_synthetic_standalone!` are gone. `synthetic_base?` reads `base.instance_of?(SingleIdentifier)`, so it also survives `#exclude`, which copies `base` into a new object (on `main` the copy lost the flag and rendered in attached form, `IEC IS60038/FDAM1`). **The move must not depend on which reader runs first**, and a first version got that wrong: it called `ensure_base` only in the slash-form branch of `Builder#build`, but the canonical spelling `IEC FDAM 60038-1` (what `to_s` prints) reaches a different grammar branch, so a fresh parse kept the grammar layout until something read `base` — `.number` answered `60038`, `to_mr_string` gave `iec.60038_amd.60038`, and `#exclude` (which copies through the readers) copied the unmoved numbers onto a base synthesised afterwards, rendering `IEC FDAM 60038-60038`. So `#number`/`#part` keep a thin override that only TRIGGERS the move (`settle_standalone_layout`: `ensure_base` when there is a `part` and no base) and then return the stored value, and `Iec::Identifier.parse` calls `ensure_base` on a top-level supplement so a fresh parse already stores the moved layout. Supplements without a part (consolidation members, `IEC/AMD 1:2017`) are untouched by either. Two renderings outside the fixture corpus move, both repairs: `exclude(:date)` of a standalone supplement keeps the standalone form, and `IEC FDAM 60038-1 (all parts)` renders as itself instead of `IEC IS60038/FDAM1 (all parts)`. One hazard is left, unreachable from the grammar: reading `part` on a hand-built supplement BEFORE its base is assigned synthesises a base and moves the number, so a later `base =` renders the moved number. **A second hand-off claim was also wrong: an equal `#hash` for two unequal objects is legal** (the rule is `a == b ⇒ a.hash == b.hash`); `compute_hash` does not read `base`, which is a collision, not a defect. Measured: all 12,331 pass-fixture ids are byte-identical in `to_s`, `to_urn`, `to_mr_string`, `root.number`, `to_hash` and class; all 32,949 `relaton-data-iec` `index-v2.yaml` rows keep `from_hash(row).to_hash == row` and `parse(from_hash(row).to_s) == from_hash(row)`, and none has a `pubid:iec:single-identifier` base, so **no index migration follows**. Locked by `spec/pubid/iec/standalone_supplement_round_trip_spec.rb` and by the six standalone fixtures now in `full/iec-pubid.txt`, which the whole-corpus `==` sweep in `spec/pubid/iec/root_number_spec.rb` covers. **Residue, deliberately unchanged:** the self-base form renders its number twice — `IEC/AMD 1:2017` → `IEC Amd 1-1:2017` — because `render_supplement_standalone` prints `base.number` and then the supplement's own number, which are the same value there. (hand-off: iec-standalone-supplement-ivar-swap.) **CORRECTED 2026-11-12 (owner ruling, reverses this bullet's identity):** the part-to-number move this bullet introduced is the DEFECT the legacy-decode bullet below removes. `IEC/FDAM 60038-1` is not "base 60038 with FDAM 1": the whole `60038-1` is the base designation. The equality machinery this bullet built (the stored layout `from_hash` rebuilds, the reader-triggered settle, `synthetic_base?` reading the class) is kept and holds on the corrected identity.
|
|
43
|
+
|
|
44
|
+
- **The legacy prefix faces decode onto the modern identity (owner ruling 2026-11-12; evidence: `data/iec/legacy-notation.md`; reverses #693's resolution).** The old notation prints the stage token BEFORE the base designation — the JTC1 ballot faces `IEC/FDAM 60038-1`, `ISO/IEC PDAM 12207`, `ISO/IEC PDAM 2 10646:2012`, `ISO/IEC FPDAM 9075/1`, `ISO/IEC FDAM 1 14496-12:2004`, and the IEC draft faces `IEC/DCOR 60038-1`, `IEC/DISH 60038-1`, `IEC CDISH 60050-191`. #693's model had resolved those by STEALING the designation's part for the supplement (`ensure_base` moved `@part` into `@number`), so the old face and the modern on-base face (`IEC 60038-1/FDAM 1`) disagreed on the same document's identity. The decode now: **the designation moves WHOLE onto the base** (number, part, subpart, and the date — old notation prints the base's year, and a draft supplement carries no publication date of its own), **the supplement's number defaults to 1** (the no-number rule: an unnumbered DAM/PDAM/FDAM/FPDAM/DCOR/DISH means Amendment/Corrigendum/Interpretation-Sheet 1), and **the base is a concrete `InternationalStandard`**, not the synthetic `SingleIdentifier` — that concreteness is what makes the legacy face hash-match (and equal) the modern parse: `parse("IEC/DCOR 60038-1") == parse("IEC 60038-1/DCOR 1")`. The render is the modern on-base slash face (`IEC 60038-1/FDAM1`), so the legacy face is not a spelling facet — it is decoded, IHO-style.
|
|
45
|
+
**Two grammar halves.** The DAM-family prefix faces (`DAM`/`PDAM`/`FDAM`/`FPDAM`, spelled literally in the rule so the typed_stages table stays put for them) raised on main; the new `legacy_supplement` rule in `pubid-grammar` captures `legacy_stage`, the optional `legacy_supplement_number` (N before the base), and the base designation — including the old slash-part spelling (`FPDAM 9075/1` = base 9075, part 1; captured as `legacy_base_number` + `legacy_base_part`, and ordered BEFORE the dash-part alternative because `number_with_part` also matches a bare "9075", which would have left the "/1" dangling). The rule sits ahead of `standalone_supplement` in the ordered choice so both the slash and space faces reach it, and its date rides with the designation. PDAM/FPDAM joined the `iec_typed_stages` table and `Amendment::TYPED_STAGES` (stage codes mirror ISO's entries: PDAM at the CD level, FPDAM at the FDIS level; the LEAD harmonized codes are the circulation slots 30.20/50.20 — ISO's 30.00/50.00 are CDAM's and FDAM's (type, leading code) URN keys). The non-DAM draft faces (DCOR, FDCOR, CDISH, DISH, and any typed standalone with a designation-shaped token) already parsed; they are corrected by `ensure_base`, so the two paths converge on one decode. **The discriminator between a legacy face and a true standalone is the PART:** a designation token (`60038-1`, `9075/1`) moves whole onto the base; a bare ordinal (`IEC/AMD 1:2017`) is the supplement's own number and keeps the synthetic self-base (`IEC Amd 1-1:2017`, unchanged). Under the JTC1 reading a bare number after a DAM-family token is a base designation (`IEC FDAM 60038` → `IEC 60038/FDAM1`), not a self-base mangle — that residue from the #693 bullet is gone.
|
|
46
|
+
**Measured by the replay gate, not the suite alone:** over all 26,838 full-fixture inputs, `to_s`/`to_urn`/`to_mr_string`/`to_hash`/class/`root.number` are byte-identical to the `main` baseline EXCEPT exactly the 6 #693-era standalone rows (both faces of FDAM/DCOR/DISH `60038-1`), which re-resolve to the corrected identities; 0 inputs stopped parsing; the prefix faces go raise→parse (12,491 full entries → 12,359 pass, fail steady at 132). #693's equality invariant (`parse == from_hash(to_hash)`) holds on every corrected identity. Locked by `spec/pubid/iec/standalone_supplement_round_trip_spec.rb` (rewritten: legacy faces decode and equal their modern parses, the self-base standalone keeps its form), `stage_separator_spec.rb`, `amendment_spec.rb`, `corrigendum_spec.rb`, `interpretation_sheet_spec.rb`, and the pass fixtures re-pinned by `validation:classify`. **Remaining gaps, deliberately open:** the dotted on-base `/Amd.1` spelling still raises in the IEC flavor (ISO accepts it), and the stamped prefix faces (`ISO/IEC FPDAM 10373-6/Amd 9:2001`, `ISO/IEC PDAM 9541-1/Amd.4`, `ISO/IEC FDAM 19776-2:2005/Am1:2006`, `ISO/IEC PDAM TR 9573-11/Amd.1`) stay raising — their on-base amendment stamps need the dotted/ISO stamp spellings the IEC flavor does not carry (gap 4 of the draft); the ISO-side agent owns that study.
|
|
47
|
+
|
|
40
48
|
## pubid-scheme URN (issue #670; gaps closed by #687)
|
|
41
49
|
|
|
42
50
|
`lib/pubid/iec/pubid_urn_schema.rb` completes the partial schema the IEEE
|
data/lib/pubid/iec/builder.rb
CHANGED
|
@@ -57,6 +57,53 @@ module Pubid
|
|
|
57
57
|
def build(parsed_hash)
|
|
58
58
|
all_parts = parsed_hash.delete(:all_parts)
|
|
59
59
|
|
|
60
|
+
# Legacy DAM-family prefix face (IEC/FDAM 60038-1, ISO/IEC PDAM 2
|
|
61
|
+
# 10646:2012, ISO/IEC FPDAM 9075/1): the JTC1 ballot spelling puts
|
|
62
|
+
# the stage token before the base designation. Decode onto the
|
|
63
|
+
# modern identity (owner ruling 2026-11-12, with data/iec/
|
|
64
|
+
# legacy-notation.md): the designation is the BASE's, kept whole —
|
|
65
|
+
# the old slash-part spelling "9075/1" is base 9075 part 1 — and
|
|
66
|
+
# the supplement's number is the captured N, defaulting to 1 (the
|
|
67
|
+
# no-number rule: unnumbered DAM/PDAM/FDAM/FPDAM means Amendment 1).
|
|
68
|
+
# The date rides with the designation (old notation prints the
|
|
69
|
+
# base's year); a draft supplement carries none of its own. The
|
|
70
|
+
# base builds through the ordinary single-document path, so it is
|
|
71
|
+
# the same InternationalStandard the modern on-base parse holds and
|
|
72
|
+
# `parse(legacy) == parse(modern)`.
|
|
73
|
+
if parsed_hash[:legacy_stage]
|
|
74
|
+
supp = build_supplement(
|
|
75
|
+
type_with_stage: parsed_hash[:legacy_stage],
|
|
76
|
+
number_with_part: parsed_hash[:legacy_supplement_number] || "1",
|
|
77
|
+
)
|
|
78
|
+
base_hash = {}
|
|
79
|
+
{
|
|
80
|
+
publisher: :publisher,
|
|
81
|
+
legacy_base_number_with_part: :number_with_part,
|
|
82
|
+
date: :date,
|
|
83
|
+
undated_marker: :undated_marker,
|
|
84
|
+
edition: :edition,
|
|
85
|
+
languages: :languages,
|
|
86
|
+
}.each do |source, target|
|
|
87
|
+
base_hash[target] = parsed_hash[source] if parsed_hash[source]
|
|
88
|
+
end
|
|
89
|
+
# The grammar binds the legacy face's copublisher as a top-level
|
|
90
|
+
# scalar (legacy_copublisher) — the wasm binder scatters array
|
|
91
|
+
# bindings over the sibling list when a bare copublisher node
|
|
92
|
+
# rides a flat shape — so the builder attaches it here, in the
|
|
93
|
+
# same shape the parse path used to carry.
|
|
94
|
+
if parsed_hash[:legacy_copublisher]
|
|
95
|
+
base_hash[:copublishers] = [{ copublisher: parsed_hash[:legacy_copublisher] }]
|
|
96
|
+
end
|
|
97
|
+
# The slash-part designation ("9075/1") captured number and part
|
|
98
|
+
# separately; they go on straight as the plain strings the retyped
|
|
99
|
+
# attributes are, not through cast (which has no :part type).
|
|
100
|
+
base = build(base_hash)
|
|
101
|
+
base.number = parsed_hash[:legacy_base_number].to_s if parsed_hash[:legacy_base_number]
|
|
102
|
+
base.part = parsed_hash[:legacy_base_part].to_s if parsed_hash[:legacy_base_part]
|
|
103
|
+
return apply_fragment_notation(wrap_with_supplement(base, supp),
|
|
104
|
+
parsed_hash)
|
|
105
|
+
end
|
|
106
|
+
|
|
60
107
|
# Second supplement layer (/AMD1:2018/COR1:2019): build the
|
|
61
108
|
# first-supplement form, then wrap the second supplement around
|
|
62
109
|
# it.
|
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.
|
|
@@ -54,6 +54,25 @@ module Pubid
|
|
|
54
54
|
name: "Committee Draft Amendment",
|
|
55
55
|
harmonized_stages: %w[30.00 30.20 30.60 30.92 30.98 30.99],
|
|
56
56
|
),
|
|
57
|
+
# The JTC1 ladder's pre-DAM ballot face (zero IEC-only record usage
|
|
58
|
+
# — the DAM family rides on joint work only; see
|
|
59
|
+
# data/iec/legacy-notation.md). Stage codes mirror the ISO flavor's
|
|
60
|
+
# entry for the same letter: PDAM sits at the CD level (30.xx).
|
|
61
|
+
# The LEAD harmonized code is the circulation slot (30.20
|
|
62
|
+
# "circulated as CD"), not ISO's 30.00 — the URN writes
|
|
63
|
+
# stage-{harmonized_stages.first} and parses it back by
|
|
64
|
+
# (type, leading code), which CDAM (30.00) already claims; see
|
|
65
|
+
# spec/pubid/iec/pnw_stage_spec.rb.
|
|
66
|
+
Pubid::Components::TypedStage.new(
|
|
67
|
+
code: :pdam,
|
|
68
|
+
stage_code: :cd,
|
|
69
|
+
type_code: :amd,
|
|
70
|
+
abbr: ["PDAM"],
|
|
71
|
+
short_abbr: nil,
|
|
72
|
+
long_abbr: nil,
|
|
73
|
+
name: "Proposed Draft Amendment",
|
|
74
|
+
harmonized_stages: %w[30.20 30.00 30.60 30.92 30.93 30.98 30.99],
|
|
75
|
+
),
|
|
57
76
|
Pubid::Components::TypedStage.new(
|
|
58
77
|
code: :damd,
|
|
59
78
|
stage_code: :damd,
|
|
@@ -74,6 +93,21 @@ module Pubid
|
|
|
74
93
|
name: "Final Draft Amendment",
|
|
75
94
|
harmonized_stages: %w[50.00 50.20 50.60 50.92 50.98 50.99],
|
|
76
95
|
),
|
|
96
|
+
# The JTC1 ladder's pre-FDAM ballot face (same provenance as PDAM
|
|
97
|
+
# above): FPDAM mirrors the ISO flavor's entry, at the FDIS level
|
|
98
|
+
# (50.xx). Its LEAD harmonized code is the circulation slot
|
|
99
|
+
# (50.20), not ISO's 50.00, which FDAM already claims as its
|
|
100
|
+
# (type, leading code) URN key.
|
|
101
|
+
Pubid::Components::TypedStage.new(
|
|
102
|
+
code: :fpdam,
|
|
103
|
+
stage_code: :fdamd,
|
|
104
|
+
type_code: :amd,
|
|
105
|
+
abbr: ["FPDAM"],
|
|
106
|
+
short_abbr: nil,
|
|
107
|
+
long_abbr: nil,
|
|
108
|
+
name: "Final Proposed Draft Amendment",
|
|
109
|
+
harmonized_stages: %w[50.20 50.00 50.60 50.92 50.98 50.99],
|
|
110
|
+
),
|
|
77
111
|
Pubid::Components::TypedStage.new(
|
|
78
112
|
code: :pubamd,
|
|
79
113
|
stage_code: :published,
|
|
@@ -44,7 +44,12 @@ module Pubid
|
|
|
44
44
|
code: :cdcor,
|
|
45
45
|
stage_code: :cd,
|
|
46
46
|
type_code: :cor,
|
|
47
|
-
|
|
47
|
+
# "CDCOR" is the all-caps spelling the attached-supplement
|
|
48
|
+
# renderer emits (abbr.first.upcase) when a legacy prefix face
|
|
49
|
+
# decodes onto a CDCor draft ("IEC/CDCor 60038-1" renders
|
|
50
|
+
# "IEC 60038-1/CDCOR1"); appended as an alias so the canonical
|
|
51
|
+
# render re-parses. abbr.first stays "CDCor" (spelling-only).
|
|
52
|
+
abbr: ["CDCor", "CDCOR"],
|
|
48
53
|
short_abbr: "CDCor",
|
|
49
54
|
long_abbr: "CD Cor",
|
|
50
55
|
name: "Committee Draft Corrigendum",
|
|
@@ -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
|
|
|
@@ -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
|