pubid 2.0.23 → 2.0.25

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/data/parg/calconnect_pfs.json +1 -0
  3. data/data/parg/cen_pfs.json +1 -0
  4. data/data/parg/etsi_pfs.json +1 -0
  5. data/lib/pubid/calconnect_pfs/builder.rb +35 -0
  6. data/lib/pubid/calconnect_pfs/identifier.rb +84 -0
  7. data/lib/pubid/calconnect_pfs/identifiers/standard.rb +31 -0
  8. data/lib/pubid/calconnect_pfs/identifiers.rb +7 -0
  9. data/lib/pubid/calconnect_pfs/parser.rb +10 -0
  10. data/lib/pubid/calconnect_pfs/pubid_urn_schema.rb +17 -0
  11. data/lib/pubid/calconnect_pfs/renderer.rb +10 -0
  12. data/lib/pubid/calconnect_pfs/urn_generator.rb +30 -0
  13. data/lib/pubid/calconnect_pfs.rb +60 -0
  14. data/lib/pubid/cen_pfs/builder.rb +33 -0
  15. data/lib/pubid/cen_pfs/identifier.rb +64 -0
  16. data/lib/pubid/cen_pfs/identifiers/european_norm.rb +31 -0
  17. data/lib/pubid/cen_pfs/identifiers.rb +7 -0
  18. data/lib/pubid/cen_pfs/parser.rb +10 -0
  19. data/lib/pubid/cen_pfs/pubid_urn_schema.rb +15 -0
  20. data/lib/pubid/cen_pfs/renderer.rb +10 -0
  21. data/lib/pubid/cen_pfs/urn_generator.rb +12 -0
  22. data/lib/pubid/cen_pfs.rb +71 -0
  23. data/lib/pubid/doi_pfs/identifier.rb +8 -1
  24. data/lib/pubid/doi_pfs.rb +3 -11
  25. data/lib/pubid/etsi_pfs/CLAUDE.md +95 -0
  26. data/lib/pubid/etsi_pfs/builder.rb +25 -0
  27. data/lib/pubid/etsi_pfs/identifier.rb +50 -0
  28. data/lib/pubid/etsi_pfs/identifiers/standard.rb +22 -0
  29. data/lib/pubid/etsi_pfs/identifiers.rb +7 -0
  30. data/lib/pubid/etsi_pfs/parser.rb +10 -0
  31. data/lib/pubid/etsi_pfs/pubid_urn_schema.rb +19 -0
  32. data/lib/pubid/etsi_pfs/renderer.rb +10 -0
  33. data/lib/pubid/etsi_pfs/urn_generator.rb +12 -0
  34. data/lib/pubid/etsi_pfs.rb +59 -0
  35. data/lib/pubid/itu/CLAUDE.md +25 -14
  36. data/lib/pubid/itu/builder.rb +7 -5
  37. data/lib/pubid/itu/identifiers/special_publication.rb +20 -17
  38. data/lib/pubid/itu/urn_generator.rb +3 -4
  39. data/lib/pubid/jis/CLAUDE.md +35 -0
  40. data/lib/pubid/jis/builder.rb +33 -2
  41. data/lib/pubid/jis/identifier.rb +24 -4
  42. data/lib/pubid/jis/identifiers/welding_engineering_standard.rb +21 -2
  43. data/lib/pubid/jis/identifiers/welding_engineering_technical_report.rb +13 -0
  44. data/lib/pubid/jis/identifiers/welding_engineering_technical_specification.rb +14 -0
  45. data/lib/pubid/jis/identifiers.rb +5 -0
  46. data/lib/pubid/jis/parser.rb +30 -0
  47. data/lib/pubid/jis/pubid_urn_schema.rb +14 -3
  48. data/lib/pubid/jis/renderer.rb +1 -2
  49. data/lib/pubid/version.rb +1 -1
  50. data/lib/pubid.rb +13 -0
  51. metadata +35 -2
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # PFS-generated flavor — DO NOT EDIT BY HAND.
4
+ # Source: definitions/cen.pfs (pfs:1) sha256:bc1edd5874f8938b5f946416a2b052d9d9b9df3d99f6c20a3d88fcab9ae1d70c
5
+ # Compiler: pubid/grammar/pfs 0.1.0 (baked with parsanol 1.3.126)
6
+ # Regenerate: ruby scripts/pfs compile definitions/cen.pfs --as cen_pfs --ruby-out <dir>
7
+
8
+ require "pubid"
9
+
10
+ module Pubid
11
+ # CEN/CENELEC European Norms (module composition) flavor, compiled from its PFS definition
12
+ # (pubid/pubid#706). A compiled flavor is a peer of the coded
13
+ # flavors: same registry, same input guards, same error
14
+ # namespace, same URN contracts.
15
+ module CenPfs
16
+ extend Pubid::PrefixesSupport
17
+
18
+ PREFIXES = ["EN", "CEN/", "CLC/"].freeze
19
+ # The module composition the definition binds (issue #706 §4):
20
+ # each entry names the library module, the version it was bound
21
+ # at, and — where the module declares one — its IDENTITY
22
+ # composition (wrapper-with-adopted: the adopted document is
23
+ # the base). Carried so consumers and the wizard can
24
+ # introspect what the flavor is composed of.
25
+ MODULES = [
26
+ { name: "iso.inner", version: 1, identity: { composition: "wrapper-with-adopted", base_module: "iso.inner", adopted_field: :adopted } },
27
+ { name: "en.overlay", version: 1, identity: { composition: "wrapper-with-adopted", base_module: "iso.inner", adopted_field: :adopted } },
28
+ { name: "national.prefix", version: 1, identity: { composition: "wrapper-with-adopted", base_module: "iso.inner", adopted_field: :adopted } },
29
+ { name: "supplements.core", version: 1, identity: { composition: "wrapper-with-adopted", adopted_field: :base } },
30
+ ].freeze
31
+
32
+ autoload :Builder, "#{__dir__}/cen_pfs/builder"
33
+ autoload :Identifier, "#{__dir__}/cen_pfs/identifier"
34
+ autoload :Identifiers, "#{__dir__}/cen_pfs/identifiers"
35
+ autoload :Parser, "#{__dir__}/cen_pfs/parser"
36
+ autoload :Renderer, "#{__dir__}/cen_pfs/renderer"
37
+ autoload :PubidUrnSchema, "#{__dir__}/cen_pfs/pubid_urn_schema"
38
+ autoload :UrnGenerator, "#{__dir__}/cen_pfs/urn_generator"
39
+
40
+ def self.parse(identifier)
41
+ Identifier.parse(identifier)
42
+ end
43
+
44
+ Identifier.format_registry = FormatRegistry.new(parent: Identifier.format_registry)
45
+ Identifier.format_registry.register(:human, renderer: CenPfs::Renderer)
46
+
47
+ def self.identifier_types
48
+ @identifier_types ||= Identifiers.constants
49
+ .filter_map { |c| begin; Identifiers.const_get(c); rescue NameError; nil; end }
50
+ .select { |c| c.is_a?(Class) && c.singleton_methods(false).include?(:type) }
51
+ .select { |c| c.type.is_a?(Hash) }
52
+ end
53
+
54
+ def self.all_typed_stages
55
+ @all_typed_stages ||= identifier_types.flat_map do |klass|
56
+ klass.const_defined?(:TYPED_STAGES) ? klass.const_get(:TYPED_STAGES) : []
57
+ end
58
+ end
59
+
60
+ def self.locate_type(code)
61
+ identifier_types.find { |t| t.type[:key].to_s == code.to_s }
62
+ end
63
+
64
+ def self.locate_stage(abbr)
65
+ abbr_str = abbr.to_s.upcase
66
+ all_typed_stages.find { |s| s.abbr.any? { |a| a.to_s.upcase == abbr_str } }
67
+ end
68
+ end
69
+ end
70
+
71
+ Pubid::Registry.register(:cen_pfs, Pubid::CenPfs)
@@ -14,7 +14,14 @@
14
14
  map "suffix", to: :suffix
15
15
  end
16
16
 
17
- PUBLISHER = "DOI".freeze
17
+ PUBLISHER = "DOI"
18
+
19
+ # The scheme's declared publisher token: the internal-form
20
+ # cut (to_s(with_publisher: false)) strips it from the
21
+ # rendered face.
22
+ def publisher_prefixes
23
+ [PUBLISHER]
24
+ end
18
25
 
19
26
  def self.parse(identifier)
20
27
  unless identifier.is_a?(String)
data/lib/pubid/doi_pfs.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  #
4
3
  # PFS-generated flavor — DO NOT EDIT BY HAND.
5
4
  # Source: definitions/doi.pfs (pfs:1) sha256:60abc3c403c61019d727ee44d9a72591c1549672451e6609a72d36189cae6340
@@ -17,6 +16,7 @@ module Pubid
17
16
  extend Pubid::PrefixesSupport
18
17
 
19
18
  PREFIXES = ["doi:", "DOI:", "10."].freeze
19
+
20
20
 
21
21
  autoload :Builder, "#{__dir__}/doi_pfs/builder"
22
22
  autoload :Identifier, "#{__dir__}/doi_pfs/identifier"
@@ -35,11 +35,7 @@ module Pubid
35
35
 
36
36
  def self.identifier_types
37
37
  @identifier_types ||= Identifiers.constants
38
- .filter_map do |c|
39
-
40
- Identifiers.const_get(c); rescue NameError; nil
41
-
42
- end
38
+ .filter_map { |c| begin; Identifiers.const_get(c); rescue NameError; nil; end }
43
39
  .select { |c| c.is_a?(Class) && c.singleton_methods(false).include?(:type) }
44
40
  .select { |c| c.type.is_a?(Hash) }
45
41
  end
@@ -56,11 +52,7 @@ module Pubid
56
52
 
57
53
  def self.locate_stage(abbr)
58
54
  abbr_str = abbr.to_s.upcase
59
- all_typed_stages.find do |s|
60
- s.abbr.any? do |a|
61
- a.to_s.upcase == abbr_str
62
- end
63
- end
55
+ all_typed_stages.find { |s| s.abbr.any? { |a| a.to_s.upcase == abbr_str } }
64
56
  end
65
57
  end
66
58
  end
@@ -0,0 +1,95 @@
1
+ # EtsiPfs — the PFS-compiled etsi dev alias
2
+
3
+ This flavor is **generated**, not hand-written: every file here is compiler
4
+ output from `pubid-grammar`'s `definitions/etsi.pfs` (pubid/pubid#706
5
+ phase 2), reproduce with
6
+
7
+ ```bash
8
+ cd ../pubid-grammar
9
+ ruby scripts/pfs compile definitions/etsi.pfs --as etsi_pfs \
10
+ --parsanol-version <CI pin> --ruby-out ../pubid/lib
11
+ mv ../pubid/lib/etsi_pfs.rb ../pubid/lib/pubid/etsi_pfs.rb # __dir__ paths assume lib/pubid/
12
+ mv ../pubid/lib/etsi_pfs ../pubid/lib/pubid/etsi_pfs
13
+ cp artifacts/etsi_pfs.json ../pubid/data/parg/etsi_pfs.json
14
+ ```
15
+
16
+ DO NOT EDIT these files by hand — a hand edit is silently overwritten by
17
+ the next compile, and the grammar SSOT is the definition, not this module
18
+ (the `data/parg/etsi_pfs.json` artifact's freshness envelope and the
19
+ definition's source sha pin the lineage).
20
+
21
+ ## The coexistence semantics (read before touching anything)
22
+
23
+ `etsi_pfs` is the **dev-alias key** of the etsi scheme: it registers beside
24
+ the coded `Pubid::Etsi` so the compiled pipeline can be proven against the
25
+ coded flavor without cutover. Same rules as `lib/pubid/doi_pfs/CLAUDE.md`:
26
+ the definition declares the scheme-canonical wire tag
27
+ (`pubid:etsi:etsi-standard`) and the scheme's `to_urn` namespace (`etsi`);
28
+ `Pubid.parse` keeps routing `ETSI …` to the coded flavor.
29
+
30
+ ## The parity gate
31
+
32
+ `spec/pubid/etsi_pfs/parity_spec.rb` is the deliverable: over the FULL
33
+ fixture inventory (24,718 pass fixtures — the largest corpus a compiled
34
+ flavor is bracketed against) plus constructed edge cases and 23 negatives.
35
+ Measured surface (see the spec header for the numbers and their meaning):
36
+
37
+ - **accept 24,718/24,718** and the same `ParseError` on negatives — the
38
+ language bracket: the compiled grammar accepts exactly the coded surface
39
+ (all 15 type tokens; the four number shapes GSM/dotted/complex/simple;
40
+ minor; the parts run; the /A//C supplement runs; `V`-versions and
41
+ `ed.N`; the `(YYYY-MM)` date).
42
+ - **to_hash and to_pubid_urn byte parity: 18,945/24,718** — exactly the
43
+ rows carrying none of the three constructs pfs:1 cannot declare. Each
44
+ gap is pinned per construct in the spec, so compiler progress turns the
45
+ pin red:
46
+ 1. **collection bindings** — the coded model stores `["1","0"]` (the
47
+ `part -> parts[].part` binding); the compiled capture is the printed
48
+ dash run `"-1-0"`.
49
+ 2. **the edition face** — the coded model folds `ed.N` into
50
+ `Version(version: "N", is_edition: true)`; the compiled hash carries
51
+ `edition: "N"`.
52
+ 3. **supplement wrappers** — the coded model wraps `/A1` `/C1` as
53
+ Amendment/Corrigendum classes with an Integer ordinal delegating to
54
+ their base; the compiled row stays a flat etsi-standard carrying the
55
+ printed run `"/A1"`.
56
+ - **to_s** renders the canonical CORE face (`ETSI <type> <number>`) —
57
+ pfs:1 render faces cannot render optionals, so every corpus row (all of
58
+ which carry version/ed + date) renders shorter; bare ids agree
59
+ byte-for-byte.
60
+ - **to_urn** is a compiler gap (the coded generator is hand-written Ruby
61
+ composing `urn:etsi:{type}:{code}:{version}:{date}`; the generated
62
+ generator pins the namespace only). Note the CODED generator raises
63
+ `ArgumentError` on months 08/09 (`format('%02d', "08")` — the zero-padded
64
+ string reads as an invalid octal); pre-existing on main, pinned by the
65
+ parity spec.
66
+ - **to_mr_string** is a compiler gap (the coded slug reads the flavor's
67
+ `mr_*` hooks — type/parts/edition/date segments; the base hooks find
68
+ none of them on the compiled columns).
69
+
70
+ ## Non-obvious facts, learned the hard way
71
+
72
+ - The generated `--ruby-out` lands at `lib/etsi_pfs.rb`; the autoload and
73
+ the `#{__dir__}` paths require it at `lib/pubid/etsi_pfs.rb` — move both
74
+ the file and the directory (commands above).
75
+ - `core: ["number"]` in a `.pfs` urn type row would generate
76
+ `core: [:number]`, which crashes `PubidUrn.define` at load time (the
77
+ generator emits `core.map(&:to_sym)`); only `core: []` round-trips.
78
+ The instance's URNs resolve through the CODED schema anyway (the flavor
79
+ segment of the canonical tag is `etsi`), so the alias rows are the
80
+ future-cutover shape.
81
+ - `V1..1` parses in BOTH flavors (`.` is a version_char) and `GS` is a
82
+ real type token — neither is a parity negative.
83
+ - `Pubid::EtsiPfs.parse("ETSI …")` never hijacks `Pubid.parse` (registry
84
+ keys sort `"etsi"` first), and `Pubid.from_hash` of an alias hash
85
+ resolves the coded `EtsiStandard` (one scheme, one `_type`).
86
+ - The corpus inventory (`spec/fixtures/etsi/identifiers/full/`) is the
87
+ parity source; its bucket split is pinned: plain 18,945 / parts 3,772 /
88
+ edition 1,833 / supplement 168.
89
+
90
+ ## v1 boundaries
91
+
92
+ Single-type schemes only, `kind: string` fields only, no free-standing
93
+ `stages:`/`supplements:` sections, no semantic decode rules — see
94
+ `pubid-grammar`'s `definitions/README.md` for the language reference and
95
+ the fail-closed validation rules.
@@ -0,0 +1,25 @@
1
+ module Pubid
2
+ module EtsiPfs
3
+ class Builder
4
+ def self.build(parsed_data)
5
+ new.build(parsed_data)
6
+ end
7
+
8
+ def build(data)
9
+ Identifiers::Standard.new(
10
+ type: data[:type],
11
+ number: data[:number],
12
+ minor: data[:minor],
13
+ parts: data[:parts],
14
+ supplements: data[:supplements],
15
+ version: data[:version],
16
+ edition: data[:edition],
17
+ year: data[:year],
18
+ month: data[:month],
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ Pubid::EtsiPfs::Builder.prepend(Pubid::Builder::AllPartsWrap)
@@ -0,0 +1,50 @@
1
+ module Pubid
2
+ module EtsiPfs
3
+ class Identifier < ::Pubid::Identifier
4
+ attribute :type, :string
5
+ attribute :number, :string
6
+ attribute :minor, :string
7
+ attribute :parts, :string
8
+ attribute :supplements, :string
9
+ attribute :version, :string
10
+ attribute :edition, :string
11
+ attribute :year, :string
12
+ attribute :month, :string
13
+
14
+ ETSI_TYPE_MAP = {
15
+ "pubid:etsi:etsi-standard" => "Pubid::EtsiPfs::Identifiers::Standard",
16
+ }.freeze
17
+
18
+ key_value do
19
+ map "_type", to: :_type, polymorphic_map: ETSI_TYPE_MAP
20
+ map "type", to: :type
21
+ map "number", to: :number
22
+ map "minor", to: :minor
23
+ map "parts", to: :parts
24
+ map "supplements", to: :supplements
25
+ map "version", to: :version
26
+ map "edition", to: :edition
27
+ map "year", to: :year
28
+ map "month", to: :month
29
+ end
30
+
31
+ PUBLISHER = "ETSI"
32
+
33
+ def self.parse(identifier)
34
+ unless identifier.is_a?(String)
35
+ raise Pubid::Errors::InvalidInputError,
36
+ Pubid::INPUT_NOT_A_STRING_MESSAGE
37
+ end
38
+
39
+ if identifier.length > Pubid::MAX_INPUT_LENGTH
40
+ raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
41
+ end
42
+
43
+ identifier = ::Pubid.utf8_input(identifier)
44
+
45
+ parsed = Pubid::Parg::Model.bound(:etsi_pfs, identifier)
46
+ Builder.build(parsed)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,22 @@
1
+ module Pubid
2
+ module EtsiPfs
3
+ module Identifiers
4
+ # Concrete ETSI Standard identifier type (PFS type `standard`).
5
+ class Standard < Identifier
6
+ TYPED_STAGES = [].freeze
7
+
8
+ def self.type
9
+ { key: :etsi, web: :etsi, title: "ETSI Standard", short: "ETSI" }
10
+ end
11
+
12
+ # The scheme-canonical wire tag declared in the definition:
13
+ # to_hash stays byte-identical with the scheme's coded
14
+ # flavor, and this flavor's own from_hash routes the tag
15
+ # to this class.
16
+ def self.polymorphic_name
17
+ "pubid:etsi:etsi-standard"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ module Pubid
2
+ module EtsiPfs
3
+ module Identifiers
4
+ autoload :Standard, "#{__dir__}/identifiers/standard"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ module Pubid
2
+ module EtsiPfs
3
+ # PFS-generated parser shell. Parsing runs through the baked
4
+ # PARG artifact (data/parg/etsi_pfs.json) via Pubid::Parg::Model.bound;
5
+ # the grammar source lives in pubid-grammar
6
+ # (grammars/etsi_pfs.parg, compiled from definitions/etsi.pfs).
7
+ class Parser
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ module Pubid
2
+ module EtsiPfs
3
+ # The pubid-scheme URN schema, registered under the compiled
4
+ # flavor key. The definition's declared namespace
5
+ # ("etsi") is the scheme's canonical
6
+ # namespace; identifiers carry the scheme's wire tag, so their
7
+ # pubid URNs encode through the scheme's own schema.
8
+ PubidUrnSchema = Pubid::PubidUrn.define("etsi_pfs") do
9
+ atom "type", "type"
10
+ atom "parts", "part"
11
+ atom "minor", "minor"
12
+ atom "year", "year"
13
+ atom "month", "month"
14
+ atom "version", "v"
15
+ type "pubid:etsi_pfs:identifier", "id", core: []
16
+ type "pubid:etsi_pfs:standard", "es", core: []
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ module Pubid
2
+ module EtsiPfs
3
+ # Human renderer, generated from the canonical template.
4
+ class Renderer < ::Pubid::Renderers::Base
5
+ def render(**_opts)
6
+ "ETSI #{@id.type} #{@id.number}"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ module Pubid
2
+ module EtsiPfs
3
+ # The official URN generator, pinned to the scheme's declared
4
+ # namespace ("etsi") so to_urn stays
5
+ # scheme-canonical under the compiled flavor key.
6
+ class UrnGenerator < ::Pubid::UrnGenerator::Base
7
+ def urn_namespace
8
+ "etsi"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # PFS-generated flavor — DO NOT EDIT BY HAND.
4
+ # Source: definitions/etsi.pfs (pfs:1) sha256:9343088bf087b55ea790057e6be0bbebe2272407ee81f035b52e5f9a4d5c7b76
5
+ # Compiler: pubid/grammar/pfs 0.1.0 (baked with parsanol 1.3.126)
6
+ # Regenerate: ruby scripts/pfs compile definitions/etsi.pfs --as etsi_pfs --ruby-out <dir>
7
+
8
+ require "pubid"
9
+
10
+ module Pubid
11
+ # ETSI flavor, compiled from its PFS definition
12
+ # (pubid/pubid#706). A compiled flavor is a peer of the coded
13
+ # flavors: same registry, same input guards, same error
14
+ # namespace, same URN contracts.
15
+ module EtsiPfs
16
+ extend Pubid::PrefixesSupport
17
+
18
+ PREFIXES = ["ETSI"].freeze
19
+
20
+ autoload :Builder, "#{__dir__}/etsi_pfs/builder"
21
+ autoload :Identifier, "#{__dir__}/etsi_pfs/identifier"
22
+ autoload :Identifiers, "#{__dir__}/etsi_pfs/identifiers"
23
+ autoload :Parser, "#{__dir__}/etsi_pfs/parser"
24
+ autoload :Renderer, "#{__dir__}/etsi_pfs/renderer"
25
+ autoload :PubidUrnSchema, "#{__dir__}/etsi_pfs/pubid_urn_schema"
26
+ autoload :UrnGenerator, "#{__dir__}/etsi_pfs/urn_generator"
27
+
28
+ def self.parse(identifier)
29
+ Identifier.parse(identifier)
30
+ end
31
+
32
+ Identifier.format_registry = FormatRegistry.new(parent: Identifier.format_registry)
33
+ Identifier.format_registry.register(:human, renderer: EtsiPfs::Renderer)
34
+
35
+ def self.identifier_types
36
+ @identifier_types ||= Identifiers.constants
37
+ .filter_map { |c| begin; Identifiers.const_get(c); rescue NameError; nil; end }
38
+ .select { |c| c.is_a?(Class) && c.singleton_methods(false).include?(:type) }
39
+ .select { |c| c.type.is_a?(Hash) }
40
+ end
41
+
42
+ def self.all_typed_stages
43
+ @all_typed_stages ||= identifier_types.flat_map do |klass|
44
+ klass.const_defined?(:TYPED_STAGES) ? klass.const_get(:TYPED_STAGES) : []
45
+ end
46
+ end
47
+
48
+ def self.locate_type(code)
49
+ identifier_types.find { |t| t.type[:key].to_s == code.to_s }
50
+ end
51
+
52
+ def self.locate_stage(abbr)
53
+ abbr_str = abbr.to_s.upcase
54
+ all_typed_stages.find { |s| s.abbr.any? { |a| a.to_s.upcase == abbr_str } }
55
+ end
56
+ end
57
+ end
58
+
59
+ Pubid::Registry.register(:etsi_pfs, Pubid::EtsiPfs)
@@ -82,20 +82,31 @@ a replay of all 24,382 rows and every ITU pass fixture showed 0 changes.
82
82
  code, so `#number` returns the series and `root.number` is `"RR"`. The rule
83
83
  ends in `any.absent?`: it sits before `with_series`, and PEG never re-enters
84
84
  the alternation, so a partial match on `ITU-R RR.1` must fail inside it.
85
- - **Operational Bulletins keep their sector.** This reverses the old
86
- "cross-bureau, sector must not be set" rule: `validate_ob_no_sector!` is
87
- gone, `ITU-T OB.1096 (2016)` renders back as it is, and the sector-less
88
- `ITU OB No. 1096` is unchanged. The long forms with a sector (`ITU-T OB No.
89
- 1096`) now normalise to `ITU-T OB.1096`. `No.` stays in the default
90
- render — it is how ITU's bulletin site and pubid v1 write it — and
91
- metanorma-itu's `ITU OB 1000` / `Annex to ITU OB 1000` (its i18n template
92
- omits `No.`) is accepted on parse (`ob_bare_body`) and rendered with it.
93
- The sector is a spelling, not
94
- identity: `SpecialPublication#==` skips it, the URN keeps `urn:itu:itu:…`
95
- and `mr_type` stays nil, so both spellings are one bulletin on every
96
- surface except `to_s`/`to_hash`. The printed date `- 15.III.2016` sets
97
- `date.day`, which only this form does, so `render_ob_date` uses the day as
98
- the spelling marker; `day_to_kv` emits only then.
85
+ - **An Operational Bulletin never carries a sector (#696).** OB is an
86
+ org-level publication: "ITU OB", never "ITU-T OB". This reverses the
87
+ "Operational Bulletins keep their sector" rule of `27454393`, which kept the
88
+ sector as a spelling but left it in `to_hash`, so `ITU OB No. 1096` and
89
+ `ITU-T OB.1096` were `==` and had two pubid-scheme URNs (two relaton slugs).
90
+ The TSB spellings (`ITU-T OB.1096`, `ITU-T OB No. 1096`, `ITU-T Operational
91
+ Bulletin No. 1096`, also as an `Annex to …` base) still **parse** — the
92
+ grammar is unchanged — but `build_special_publication` passes no sector and
93
+ `SpecialPublication` drops one given to `new` or to `sector=` (the path a
94
+ legacy `from_hash` row with `"sector" => "T"` takes). So every surface,
95
+ `to_s` included, is sector-free: `ITU-T OB.1096 (2016)` renders `ITU OB
96
+ No. 1096 (2016)`. The `ITU-T OB…` lines in `pass/special_publication.txt`
97
+ are therefore `!input!rendered`. `No.` stays in the default render — it is
98
+ how ITU's bulletin site and pubid v1 write it — and metanorma-itu's `ITU OB
99
+ 1000` / `Annex to ITU OB 1000` (its i18n template omits `No.`) is accepted
100
+ on parse (`ob_bare_body`) and rendered with it. The printed date
101
+ `- 15.III.2016` sets `date.day`, which only this form does, so
102
+ `render_ob_date` uses the day as the spelling marker; `day_to_kv` emits only
103
+ then. `build_supplement` does not take the sector of a TSB-spelled OB base
104
+ either, so `ITU-T OB.1096 Amd. 1` stores no sector and slugs
105
+ `itu.ob-1096…`. **Known gap:** the grammar has no sector-less OB
106
+ supplement, so that id renders `ITU OB No. 1096 Amd. 1`, which does not
107
+ parse back. No corpus holds an OB supplement; a grammar rule would be a
108
+ pubid-grammar change. Sources that cite `ITU-T OB` (relaton's
109
+ `publication_code`, the pubid-testsuite rows) are fixed in their own repos.
99
110
  - **`-YYYYMM` is a date, never a part.** `part` refuses a six-digit run with a
100
111
  19xx/20xx year and a 01–12 month (`yyyymm_shape`), and `id_date` reads it as
101
112
  year+month. `-200313`, `-180001` stay parts. `ITU-T REC T.4` drops the
@@ -170,12 +170,10 @@ module Pubid
170
170
  nil
171
171
  end
172
172
 
173
- # Build Special Publication (OB). The sector of the TSB spelling
174
- # ("ITU-T OB.1096") is kept so the bulletin renders back as it was cited;
175
- # SpecialPublication#== ignores it, since OB is cross-bureau.
173
+ # Build Special Publication (OB). An OB never carries a sector, so the
174
+ # sector of the TSB spelling ("ITU-T OB.1096") is discarded (#696).
176
175
  def build_special_publication(data)
177
176
  Identifiers::SpecialPublication.new(
178
- sector: (Components::Sector.new(sector: data[:sector].to_s) if data[:sector]),
179
177
  series: Components::Series.new(series: "OB"),
180
178
  code: data[:number] ? build_code(data) : nil,
181
179
  date: data[:year] ? build_date(data) : nil,
@@ -289,8 +287,12 @@ module Pubid
289
287
  # it via #root; otherwise a supplement of an annex would carry none of
290
288
  # them and its MR string would collapse to a bare "itu.<date>".
291
289
  base_identity = base&.sector ? base : base&.root
290
+ # An Operational Bulletin never carries a sector (#696), so a
291
+ # supplement of one does not take the sector of its TSB spelling.
292
+ own_sector = data[:sector] &&
293
+ !base_identity.is_a?(Identifiers::SpecialPublication)
292
294
  sector = base_identity&.sector ||
293
- (Components::Sector.new(sector: data[:sector].to_s) if data[:sector])
295
+ (Components::Sector.new(sector: data[:sector].to_s) if own_sector)
294
296
  series = base_identity&.series ||
295
297
  (Components::Series.new(series: data[:series].to_s) if data[:series])
296
298
 
@@ -4,29 +4,33 @@ module Pubid
4
4
  module Itu
5
5
  module Identifiers
6
6
  # ITU Special Publication — currently models the Operational Bulletin (OB).
7
- # OB is a cross-bureau publication. ITU prints it without a sector
8
- # ("ITU OB No. 1283 (01/2024)"); the TSB spelling carries one
9
- # ("ITU-T OB.1096 (2016)"), which is kept and rendered back. The sector
10
- # is not part of the identity: `==`, the URN and the MR slug ignore it,
11
- # so both spellings name one bulletin.
7
+ # OB is an org-level publication: it never carries a sector
8
+ # ("ITU OB No. 1283 (01/2024)", never "ITU-T OB"). The TSB spelling
9
+ # ("ITU-T OB.1096 (2016)") parses, but the sector is discarded here, so
10
+ # both spellings are one value on every surface — `to_s`, `to_hash`,
11
+ # `==`, the URN and the pubid-scheme URN (issue #696).
12
12
  class SpecialPublication < Identifier
13
13
  include StandardSerialization
14
14
 
15
15
  ROMAN_MONTHS = %w[I II III IV V VI VII VIII IX X XI XII].freeze
16
16
 
17
- def render_base(**_opts)
18
- number = code&.number
19
- result = if sector
20
- "#{publisher}-#{sector} #{series}.#{number}"
21
- else
22
- "#{publisher} #{series} No. #{number}"
23
- end
17
+ # Drop a sector given on construction (the parse path never passes
18
+ # one; a caller or a legacy hash row may).
19
+ def initialize(**kwargs)
20
+ super(**kwargs.except(:sector))
21
+ end
24
22
 
25
- result + render_ob_date
23
+ # Drop a sector set later — `from_hash` of a legacy row with
24
+ # `"sector" => "T"` comes through this setter.
25
+ def sector=(_value)
26
+ super(nil)
27
+ end
28
+
29
+ def render_base(**_opts)
30
+ "#{publisher} #{series} No. #{code&.number}#{render_ob_date}"
26
31
  end
27
32
 
28
- # Cross-bureau: the sector is how one bureau cites the bulletin, not
29
- # which bulletin it is.
33
+ # Only the fields a bulletin carries. The sector is always nil now.
30
34
  def ==(other)
31
35
  return false unless other.instance_of?(self.class)
32
36
 
@@ -37,8 +41,7 @@ module Pubid
37
41
  common_text_twin == other.common_text_twin
38
42
  end
39
43
 
40
- # Keep the MR slug sector-free, like `==` (it was always so, because
41
- # the sector used to be dropped).
44
+ # A bulletin has no type token in its MR slug.
42
45
  def mr_type
43
46
  nil
44
47
  end
@@ -16,10 +16,9 @@ module Pubid
16
16
  def generate_base_urn
17
17
  parts = ["urn", "itu"]
18
18
 
19
- # An Operational Bulletin is cross-bureau: its sector is a spelling,
20
- # not identity (see SpecialPublication#==), so it stays out of the URN.
21
- if identifier.sector &&
22
- !identifier.is_a?(Identifiers::SpecialPublication)
19
+ # An Operational Bulletin never carries a sector (#696), so it gets
20
+ # the `itu` segment here.
21
+ if identifier.sector
23
22
  sector = identifier.sector.to_s
24
23
  parts << sector.to_s.downcase
25
24
  else
@@ -29,6 +29,41 @@
29
29
  `UrnParser` does not prepend `JIS ` to a code that starts with `WES `.
30
30
  - `PREFIXES` includes `"WES"`, so `Pubid.parse("WES …")` routes to JIS.
31
31
 
32
+ ## WES `(E)` before the year, `WES TR`, `WES TS` (relaton-jis#18)
33
+
34
+ - JSA Webdesk lists 14 WES ids that the grammar does not read:
35
+ `WES 1112(E):2022`, `WES TR 2032:2024`, `WES TS 2816(E):2015`.
36
+ - **A Ruby shim reads them, not the grammar.** `Parser.ingest` cuts
37
+ the `TR`/`TS` token and a `(E)`/`(J)` that comes before `:year`, and
38
+ binds them as `wes_type` and `wes_language`. Other input passes unchanged.
39
+ The hand-off `pubid__pubid-grammar__jis-wes-language-tr-ts` asks
40
+ pubid-grammar to bind the same two captures. After that lands, re-vendor
41
+ `jis.json`, re-pin `parg_reaffirmed_spec.rb`, and delete the shim. The
42
+ builder does not change.
43
+ - **`WES TR`/`WES TS` are classes**: `WeldingEngineeringTechnicalReport`
44
+ and `…TechnicalSpecification` < `WeldingEngineeringStandard`, with
45
+ `WES_TYPE` in `code` (`WES TR 2032`) and in `mr_type`. Class-strict `==`
46
+ keeps them apart from `WES 2032` and `JIS TR Z 2032`.
47
+ - **`language_before_year`** records where the source put the language. It
48
+ is spelling only (`equality_ignored`, a `langfirst` facet in the pubid
49
+ URN): `WES 1112(E):2022 == WES 1112:2022(E)`, and each renders as written.
50
+ The language itself is identity, so the `(E)` edition is not the Japanese
51
+ one. JIS ids never set the flag; `JIS X 0208(E):1997` still fails.
52
+ - The pubid URN schema now places `language` and the supplement ordinal
53
+ (`core: [["number", :int]]`; an `EXPL` number is the atom `n`). Before,
54
+ every `(E)` id and every supplement raised in `to_pubid_urn`, and no
55
+ supplement was in the fixture corpus to show it.
56
+ - **`Builder` reads both parse shapes.** The typed bindings
57
+ (`Parg::Model.bound`) carry a supplement flat (`amd_type`, …); the raw
58
+ tree (`Parg::Backend.parse`) nests it under `:amendment`/`:explanation`/
59
+ `:corrigendum`. `flatten_supplement` folds the nested form. Before, the
60
+ tree path dropped every supplement silently (`…/AMD 1:2000` built as the
61
+ bare base). `typed_binding_parity_spec.rb` found it when the first
62
+ supplement entered the pass corpus.
63
+ - **The shim lives in `Parser.ingest`** and returns `[face, bindings]`.
64
+ `typed_binding_parity_spec.rb` calls it on the tree path too and merges
65
+ the bindings, as `Identifier.parse` does.
66
+
32
67
  ## Equality includes the class
33
68
 
34
69
  - `Identifier#==` requires `other.instance_of?(self.class)`: `JIS TR X